Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PoC for moving enums from common -> Nexus db::model #776

Merged
merged 8 commits into from
Mar 16, 2022

Conversation

david-crespo
Copy link
Contributor

@david-crespo david-crespo commented Mar 16, 2022

After @smklein revived the dream of #388 (moving as much as possible out of common and into the appropriate service) in #774, I wanted to see what the remaining obstacles were. Unfortunately we've moved almost all the easy stuff — everything left is kind of a pain. I want to get consensus on an approach before putting up a 1000 line PR.

A bunch of important enums like VpcRouterKind (system or custom) are defined in common and then wrapped in a struct in Nexus's model.rs. The models depend on the views/params, which seems to me backwards. Instead, the enum should be defined in the model file, and then the view re-implements the enum with a dumb boilerplate Into to convert between them. Theoretically you could use the model enum directly in the view, but I don't think it's a good idea to blur the line between models and views — that's precisely what we're trying to stop doing with these changes.

The annoying part for the purposes of this PR is that the impl_enum_type! macro needs to be changed (here copied with modification to avoid changing everything at once) to make pub enum VpcRouterKind instead of pub struct VpcRouterKind(pub external::VpcRouterKind). The rest of the changes very straightforward fall out of that.

Possible fanciness

There's a diesel-derive-enum proc macro that would let us do this

#[derive(DbEnum)]
pub enum VpcRouterKind {
    System,
    Custom,
}

and basically be done with it. I feel this is a lot nicer than what we have, but I couldn't get it to work (though I didn't try very hard). I think if we wanted to go this route we'd probably be better off making our own version of that macro that does exactly what we want and nothing more.

@david-crespo david-crespo requested a review from smklein March 16, 2022 15:35
$(
$enum_item,
)*
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the analogous line in the original. Instead of defining the enum, it wrapped the imported external one.

pub struct $model_type(pub $ext_type);

match DB::as_bytes(bytes) {
$(
$sql_value => {
Ok($model_type::$enum_item)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original did this, i.e., Ok(VpcRouterKind(external::VpcRouterKind::Custom)) as opposed to the simple Ok(VpcRouterKind::Custom).

Ok($model_type(<$ext_type>::$enum_item))

}
}
}

Copy link
Contributor Author

@david-crespo david-crespo Mar 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Complete diff with the existing macro.

@@ -50,7 +50,7 @@ macro_rules! impl_enum_type {
         pub struct $diesel_type:ident;
 
         $(#[$model_meta:meta])*
-        pub struct $model_type:ident(pub $ext_type:ty);
+        pub enum $model_type:ident;
         $($enum_item:ident => $sql_value:literal)+
     ) => {
 
@@ -58,7 +58,11 @@ macro_rules! impl_enum_type {
         pub struct $diesel_type;
 
         $(#[$model_meta])*
-        pub struct $model_type(pub $ext_type);
+        pub enum $model_type {
+            $(
+                $enum_item,
+            )*
+        }
 
         impl<DB> ToSql<$diesel_type, DB> for $model_type
         where
@@ -68,9 +72,9 @@ macro_rules! impl_enum_type {
                 &self,
                 out: &mut serialize::Output<W, DB>,
             ) -> serialize::Result {
-                match self.0 {
+                match self {
                     $(
-                    <$ext_type>::$enum_item => {
+                    $model_type::$enum_item => {
                         out.write_all($sql_value)?
                     }
                     )*
@@ -87,7 +91,7 @@ macro_rules! impl_enum_type {
                 match DB::as_bytes(bytes) {
                     $(
                     $sql_value => {
-                        Ok($model_type(<$ext_type>::$enum_item))
+                        Ok($model_type::$enum_item)
                     }
                     )*
                     _ => {

Copy link
Collaborator

@smklein smklein left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This direction looks right to me. I share the intuition that "conversion functions should, when possible, live alongside the view/params".

I think the distinction of "where is the enum defined" to be kinda arbitrary (is the "real" one in models and the derivative one in views, or vice versa) but your approach of making them both "just enums/structures, that can be converted to one another" seems balanced.

I think the original intent of having "the structure in one layer be a newtype wrapper around the other" was just to do what we could to reduce boilerplate, but there are other ways of accomplishing that goal. The confusion caused by the nesting probably wasn't worth the LoC reduction.

#[sql_type = "VpcRouterKindEnum"]
pub struct VpcRouterKind(pub external::VpcRouterKind);
pub enum VpcRouterKind;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do like the idea of this thing actually being an enum now; that's way more intuitive

Custom,
}

impl Into<VpcRouterKind> for model::VpcRouterKind {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to impl From<model::VpcRouterKind> for VpcRouterKind?

I think it's functionally identical to this, but it's preferable to impl From over Into when given the option:

One should always prefer implementing From over Into because implementing From automatically provides one with an implementation of Into thanks to the blanket implementation in the standard library.
Only implement Into when targeting a version prior to Rust 1.41 and converting to a type outside the current crate. From was not able to do these types of conversions in earlier versions because of Rust’s orphaning rules.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, definitely. We should change them all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 636c72d. I'll change the others in a separate PR to reduce noise.

pub vpc_id: Uuid,
}

impl Into<VpcRouter> for model::VpcRouter {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above

@david-crespo
Copy link
Contributor Author

I share the intuition that "conversion functions should, when possible, live alongside the view/params".

Generally I agree, though so far we've been following a slightly more nuanced pattern where conversion from model to view is defined next to the view, but the conversion from params to model is defined with the model because

  1. It's usually the model constructor
  2. I think of views as "knowing about" models because they're lenses onto them, but params don't "know about" models because they're really just HTTP request bodies, they don't care what they are turned into

@david-crespo david-crespo changed the title PoC on shared enums switcheroo PoC for moving enums from common -> Nexus db::model Mar 16, 2022
@david-crespo david-crespo marked this pull request as ready for review March 16, 2022 18:45
@david-crespo
Copy link
Contributor Author

Decided to tweak names so this could be merged as-is without having to convert all the enums affected by this macro, which it turns out would be a very complex undertaking.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants