Repository: oxidecomputer/typify (rust)

Issue:
compilation failure when mixing `default` with `NonZeroU8`
A schema like this...

```json
    {
      "type": "object",
      "properties": {
        "x": {
          "type": "integer",
          "format": "uint8",
          "minimum": 1,
          "default": 1
        }
      }
    }
```

... turns into code like this...

```rust
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct X {
    #[serde(default = "defaults::default_u64::<std::num::NonZeroU8, 1>")]
    pub x: std::num::NonZeroU8,
}
impl From<&X> for X {
    fn from(value: &X) -> Self {
        value.clone()
    }
}
pub mod defaults {
    pub(super) fn default_u64<T, const V: u64>() -> T
    where
        T: std::convert::TryFrom<u64>,
        <T as std::convert::TryFrom<u64>>::Error: std::fmt::Debug,
    {
        T::try_from(V).unwrap()
    }
}
```

Which is as-intended: `defaults::default_u64` is intended to produce the default value that we want.

However!
```
error[E0277]: the trait bound `NonZeroU8: From<u64>` is not satisfied
```

The `NonZero*` types implement `TryFrom` for their corresponding primitive type, but not other primitive types. This appears to be a point of principle: https://github.com/rust-lang/rust/pull/72712#issuecomment-635679120

So: cute as this would be, we need a different approach. Some options:

1. Use some new trait that takes the place of `TryFrom` but has more implementations. This might be kind of annoying because we'd probably dump all this code into the `mod defaults` by default (no pun intended).
1. We could generate `default_non_zero_u64`... it would only create a little bit of annoying code to determine which one to use.
1. We could generate a function for each default. This would be fine, but it's been nice to share the integer implementations
1.  Parameterize `default_u64` with two types (rather than one) i.e. the output type and an "intermediate type": `defaults::default_u64::<NonZeroU8, u8>`


Pull request description:
handle default values for fields represented as NonZeroU{8,16,32}
Fixes #390 

Of the solutions proposed in that issue, this is option 2. Of the several bad choices, this seemed... fine.

Patch:
diff --git a/typify-impl/src/defaults.rs b/typify-impl/src/defaults.rs
index a88c205..94dc1b6 100644
--- a/typify-impl/src/defaults.rs
+++ b/typify-impl/src/defaults.rs
@@ -43,6 +43,17 @@ impl From<&DefaultImpl> for TokenStream {
                     T::try_from(V).unwrap()
                 }
             },
+            DefaultImpl::NZU64 => quote! {
+                pub(super) fn default_nzu64<T, const V: u64>() -> T
+                where
+                    T: std::convert::TryFrom<std::num::NonZeroU64>,
+                    <T as std::convert::TryFrom<std::num::NonZeroU64>>::Error:
+                        std::fmt::Debug,
+                {
+                    T::try_from(std::num::NonZeroU64::try_from(V).unwrap())
+                        .unwrap()
+                }
+            },
         }
     }
 }
@@ -272,11 +283,17 @@ impl TypeEntry {
             },
             // Note that min and max values are handled already by the
             // conversion routines since we have those close at hand.
-            TypeEntryDetails::Integer(_) => match (default.as_u64(), default.as_i64()) {
+            TypeEntryDetails::Integer(itype) => match (default.as_u64(), default.as_i64()) {
                 (None, None) => Err(Error::invalid_value()),
                 (Some(0), _) => Ok(DefaultKind::Intrinsic),
                 (_, Some(0)) => unreachable!(),
-                (Some(_), _) => Ok(DefaultKind::Generic(DefaultImpl::U64)),
+                (Some(_), _) => {
+                    if itype.starts_with("std::num::NonZero") {
+                        Ok(DefaultKind::Generic(DefaultImpl::NZU64))
+                    } else {
+                        Ok(DefaultKind::Generic(DefaultImpl::U64))
+                    }
+                }
                 (_, Some(_)) => Ok(DefaultKind::Generic(DefaultImpl::I64)),
             },
             TypeEntryDetails::Float(_) => {
@@ -319,7 +336,11 @@ impl TypeEntry {
             TypeEntryDetails::Boolean => Some("defaults::default_bool::<true>".to_string()),
             TypeEntryDetails::Integer(name) => {
                 if let Some(value) = default.as_u64() {
-                    Some(format!("defaults::default_u64::<{}, {}>", name, value))
+                    if name.starts_with("std::num::NonZero") {
+                        Some(format!("defaults::default_nzu64::<{}, {}>", name, value))
+                    } else {
+                        Some(format!("defaults::default_u64::<{}, {}>", name, value))
+                    }
                 } else if let Some(value) = default.as_i64() {
                     Some(format!("defaults::default_i64::<{}, {}>", name, value))
                 } else {
diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs
index 906f458..b66f0b7 100644
--- a/typify-impl/src/lib.rs
+++ b/typify-impl/src/lib.rs
@@ -234,6 +234,7 @@ pub(crate) enum DefaultImpl {
     Boolean,
     I64,
     U64,
+    NZU64,
 }
 
 /// Settings that alter type generation.


Test patch:
diff --git a/typify/tests/schemas/types-with-defaults.json b/typify/tests/schemas/types-with-defaults.json
index 7b7d18f..c677c2c 100644
--- a/typify/tests/schemas/types-with-defaults.json
+++ b/typify/tests/schemas/types-with-defaults.json
@@ -59,6 +59,23 @@
           "default": "1970-01-01T00:00:00Z"
         }
       }
+    },
+    "MrDefaultNumbers": {
+      "type": "object",
+      "properties": {
+        "little_u8": {
+          "type": "integer",
+          "format": "uint8",
+          "minimum": 1,
+          "default": 2
+        },
+        "little_u16": {
+          "type": "integer",
+          "format": "uint16",
+          "minimum": 1,
+          "default": 3
+        }
+      }
     }
   }
 }
\ No newline at end of file
diff --git a/typify/tests/schemas/types-with-defaults.rs b/typify/tests/schemas/types-with-defaults.rs
index 17d1749..603ecdf 100644
--- a/typify/tests/schemas/types-with-defaults.rs
+++ b/typify/tests/schemas/types-with-defaults.rs
@@ -53,6 +53,42 @@ impl From<&Doodad> for Doodad {
         value.clone()
     }
 }
+#[doc = "MrDefaultNumbers"]
+#[doc = r""]
+#[doc = r" <details><summary>JSON schema</summary>"]
+#[doc = r""]
+#[doc = r" ```json"]
+#[doc = "{"]
+#[doc = "  \"type\": \"object\","]
+#[doc = "  \"properties\": {"]
+#[doc = "    \"little_u16\": {"]
+#[doc = "      \"default\": 3,"]
+#[doc = "      \"type\": \"integer\","]
+#[doc = "      \"format\": \"uint16\","]
+#[doc = "      \"minimum\": 1.0"]
+#[doc = "    },"]
+#[doc = "    \"little_u8\": {"]
+#[doc = "      \"default\": 2,"]
+#[doc = "      \"type\": \"integer\","]
+#[doc = "      \"format\": \"uint8\","]
+#[doc = "      \"minimum\": 1.0"]
+#[doc = "    }"]
+#[doc = "  }"]
+#[doc = "}"]
+#[doc = r" ```"]
+#[doc = r" </details>"]
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct MrDefaultNumbers {
+    #[serde(default = "defaults::default_nzu64::<std::num::NonZeroU16, 3>")]
+    pub little_u16: std::num::NonZeroU16,
+    #[serde(default = "defaults::default_nzu64::<std::num::NonZeroU8, 2>")]
+    pub little_u8: std::num::NonZeroU8,
+}
+impl From<&MrDefaultNumbers> for MrDefaultNumbers {
+    fn from(value: &MrDefaultNumbers) -> Self {
+        value.clone()
+    }
+}
 #[doc = "OuterThing"]
 #[doc = r""]
 #[doc = r" <details><summary>JSON schema</summary>"]
@@ -183,6 +219,13 @@ impl Default for ThingWithDefaults {
 }
 #[doc = r" Generation of default values for serde."]
 pub mod defaults {
+    pub(super) fn default_nzu64<T, const V: u64>() -> T
+    where
+        T: std::convert::TryFrom<std::num::NonZeroU64>,
+        <T as std::convert::TryFrom<std::num::NonZeroU64>>::Error: std::fmt::Debug,
+    {
+        T::try_from(std::num::NonZeroU64::try_from(V).unwrap()).unwrap()
+    }
     pub(super) fn doodad_when() -> chrono::DateTime<chrono::offset::Utc> {
         serde_json::from_str::<chrono::DateTime<chrono::offset::Utc>>("\"1970-01-01T00:00:00Z\"")
             .unwrap()