Repository: greyblake/nutype (Rust)

Issue:
Add `finite` validation rule on f32,f64 types
## Spec

* For float based types implement `finite` validation rule, which will invoke [`is_finite`](https://doc.rust-lang.org/std/primitive.f64.html#method.is_finite) under the hood.
* Generate `NotFinite` error variant

## Example

```rs
#[nutype(validate(finite))]
struct Weight(f64)

// valid
Weight::new(123.45);

// invalid
Weight::new(f64::NAN)
Weight::new(f64::INFINITY)
```


Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7bb65d4..739438f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
+### v0.3.0 - 2023-??-??
+* Add `finite` validation for float types which checks against NaN and infinity.
 
-### v0.2.0 - 2023-??-??
+### v0.2.0 - 2023-04-13
 
 * [BREAKING] Rename string validator `present` -> `not_empty`. Rename error variant `Missing` -> `Empty`.
 * [BREAKING] Rename feature `serde1` to `serde`.
diff --git a/README.md b/README.md
index 0a69a36..18d58cf 100644
--- a/README.md
+++ b/README.md
@@ -219,11 +219,12 @@ The float inner types are: `f32`, `f64`.
 
 ### Float validators
 
-| Validator | Description         | Error variant | Example                       |
-|-----------|---------------------|---------------|-------------------------------|
-| `max`     | Maximum valid value | `TooBig`      | `max = 100.0`                 |
-| `min`     | Minimum valid value | `TooSmall`    | `min = 0.0`                   |
-| `with`    | Custom validator    | `Invalid`     | `with = \|val\| val != 50.0`  |
+| Validator | Description                    | Error variant | Example                      |
+|-----------|--------------------------------|---------------|------------------------------|
+| `max`     | Maximum valid value            | `TooBig`      | `max = 100.0`                |
+| `min`     | Minimum valid value            | `TooSmall`    | `min = 0.0`                  |
+| `finite`  | Check against NaN and infinity | `NotFinite`   | `finite`                     |
+| `with`    | Custom validator               | `Invalid`     | `with = \|val\| val != 50.0` |
 
 ### Float derivable traits
 
diff --git a/nutype/src/lib.rs b/nutype/src/lib.rs
index 9f3fc46..7007f9b 100644
--- a/nutype/src/lib.rs
+++ b/nutype/src/lib.rs
@@ -216,11 +216,12 @@
 //!
 //! ### Float validators
 //!
-//! | Validator | Description         | Error variant | Example                       |
-//! |-----------|---------------------|---------------|-------------------------------|
-//! | `max`     | Maximum valid value | `TooBig`      | `max = 100.0`                 |
-//! | `min`     | Minimum valid value | `TooSmall`    | `min = 0.0`                   |
-//! | `with`    | Custom validator    | `Invalid`     | `with = \|val\| val != 50.0`  |
+//! | Validator | Description                    | Error variant | Example                      |
+//! |-----------|--------------------------------|---------------|------------------------------|
+//! | `max`     | Maximum valid value            | `TooBig`      | `max = 100.0`                |
+//! | `min`     | Minimum valid value            | `TooSmall`    | `min = 0.0`                  |
+//! | `finite`  | Check against NaN and infinity | `NotFinite`   | `finite`                     |
+//! | `with`    | Custom validator               | `Invalid`     | `with = \|val\| val != 50.0` |
 //!
 //! ### Float derivable traits
 //!
diff --git a/nutype_macros/src/float/gen/error.rs b/nutype_macros/src/float/gen/error.rs
index c79c7c5..b5e9e6e 100644
--- a/nutype_macros/src/float/gen/error.rs
+++ b/nutype_macros/src/float/gen/error.rs
@@ -42,6 +42,9 @@ fn gen_definition<T>(
             FloatValidator::With(_) => {
                 quote!(Invalid,)
             }
+            FloatValidator::Finite => {
+                quote!(NotFinite,)
+            }
         })
         .collect();
 
@@ -66,6 +69,9 @@ fn gen_impl_display_trait<T>(
         FloatValidator::With(_) => quote! {
              #error_type_name::Invalid => write!(f, "invalid")
         },
+        FloatValidator::Finite => quote! {
+             #error_type_name::NotFinite => write!(f, "not finite")
+        },
     });
 
     quote! {
diff --git a/nutype_macros/src/float/gen/mod.rs b/nutype_macros/src/float/gen/mod.rs
index 9e95477..0c9430e 100644
--- a/nutype_macros/src/float/gen/mod.rs
+++ b/nutype_macros/src/float/gen/mod.rs
@@ -220,6 +220,13 @@ where
                     }
                 )
             }
+            FloatValidator::Finite => {
+                quote!(
+                    if !val.is_finite() {
+                        return Err(#error_name::NotFinite);
+                    }
+                )
+            }
         })
         .collect();
 
diff --git a/nutype_macros/src/float/models.rs b/nutype_macros/src/float/models.rs
index 49b9366..d04bd93 100644
--- a/nutype_macros/src/float/models.rs
+++ b/nutype_macros/src/float/models.rs
@@ -50,6 +50,7 @@ pub enum FloatValidator<T> {
     Min(T),
     Max(T),
     With(TokenStream),
+    Finite,
 }
 
 pub type SpannedFloatValidator<T> = SpannedItem<FloatValidator<T>>;
@@ -59,6 +60,7 @@ pub enum FloatValidatorKind {
     Min,
     Max,
     With,
+    Finite,
 }
 
 impl std::fmt::Display for FloatValidatorKind {
@@ -67,6 +69,7 @@ impl std::fmt::Display for FloatValidatorKind {
             Self::Min => write!(f, "min"),
             Self::Max => write!(f, "max"),
             Self::With => write!(f, "with"),
+            Self::Finite => write!(f, "finite"),
         }
     }
 }
@@ -79,6 +82,7 @@ impl<T> Kind for FloatValidator<T> {
             Self::Min(_) => FloatValidatorKind::Min,
             Self::Max(_) => FloatValidatorKind::Max,
             Self::With(_) => FloatValidatorKind::With,
+            Self::Finite => FloatValidatorKind::Finite,
         }
     }
 }
diff --git a/nutype_macros/src/float/parse.rs b/nutype_macros/src/float/parse.rs
index 64e511a..ff8d164 100644
--- a/nutype_macros/src/float/parse.rs
+++ b/nutype_macros/src/float/parse.rs
@@ -128,6 +128,14 @@ where
                     item: validator,
                 })
             }
+            "finite" => {
+                let validator = FloatValidator::Finite;
+                let parsed_validator = SpannedFloatValidator {
+                    item: validator,
+                    span: ident.span(),
+                };
+                Ok(parsed_validator)
+            }
             unknown_validator => {
                 let msg = format!("Unknown validation rule `{unknown_validator}`");
                 let error = syn::Error::new(ident.span(), msg);


Test patch:
diff --git a/test_suite/tests/float.rs b/test_suite/tests/float.rs
index 088aa26..bd1f84c 100644
--- a/test_suite/tests/float.rs
+++ b/test_suite/tests/float.rs
@@ -83,6 +83,41 @@ mod validators {
         assert_eq!(Age::new(25.0).unwrap().into_inner(), 25.0);
     }
 
+    #[test]
+    fn test_finite_f64() {
+        #[nutype(validate(finite))]
+        #[derive(Debug, PartialEq)]
+        struct Dist(f64);
+
+        // invalid
+        assert_eq!(Dist::new(f64::INFINITY), Err(DistError::NotFinite));
+        assert_eq!(Dist::new(f64::NEG_INFINITY), Err(DistError::NotFinite));
+        assert_eq!(Dist::new(f64::NAN), Err(DistError::NotFinite));
+        assert_eq!(Dist::new(-1.0 / 0.0), Err(DistError::NotFinite));
+        assert_eq!(Dist::new(1.0 / 0.0), Err(DistError::NotFinite));
+        assert_eq!(Dist::new(0.0 / 0.0), Err(DistError::NotFinite));
+
+        // valid
+        assert_eq!(Dist::new(12.345).unwrap().into_inner(), 12.345);
+        assert_eq!(Dist::new(-999.12).unwrap().into_inner(), -999.12);
+    }
+
+    #[test]
+    fn test_finite_f32() {
+        #[nutype(validate(finite))]
+        #[derive(*)]
+        struct Dist(f32);
+
+        // invalid
+        assert_eq!(Dist::new(-1.0 / 0.0), Err(DistError::NotFinite));
+        assert_eq!(Dist::new(1.0 / 0.0), Err(DistError::NotFinite));
+        assert_eq!(Dist::new(0.0 / 0.0), Err(DistError::NotFinite));
+
+        // valid
+        assert_eq!(Dist::new(12.345).unwrap().into_inner(), 12.345);
+        assert_eq!(Dist::new(-999.12).unwrap().into_inner(), -999.12);
+    }
+
     #[cfg(test)]
     mod with {
         use super::*;