Repository: shuttle-hq/synth (language: rust)

Issue:
Feature: Allow use of numbers in string.format
**Required Functionality**
Allowing the use of numbers when using the string.format generator, would greatly expand the use cases of the formatter.

**Proposed Solution**
Allow and convert numerical values to string when used within the contents of string.format

Currently this throws a BadRequest:
```
> .\synth.exe generate examples --collection string_format_int
Error: At namespace "examples"

Caused by:
    BadRequest: invalid type: expected 'String', found 'Number'
```

**Use case**
Such a feature would allow the mixing of numerical generators (e.g. ID) to create customized ID fields, Street addresses, and more.

Example
```json
{
  "type": "array",
  "length": 5,
  "content": {
    "type": "object",
    "str_int": {
      "type": "string",
      "format": {
        "format": "{id}_suffix",
        "arguments": {
          "id": {
            "type": "number",
            "id": {
              "start_at": 100000
            }
          }
        }
      }
    }
  }
}
```



Discussion / hints:
@cryocaustik this is a great idea. @brokad what would be the concrete steps for implementation. How tied down is the `format` generator to strings?
Thank you @cryocaustik, this should clearly be supported :smile: 

[format.rs#L58](https://github.com/getsynth/synth/blob/8b957443db972bb8b923054d663d7f5418cd6883/core/src/graph/string/format.rs#L58) is where the conversion from the inner [`Value`](https://github.com/getsynth/synth/blob/8b957443db972bb8b923054d663d7f5418cd6883/core/src/graph/mod.rs#L166) generated by the parameter nodes gets converted into string. We could just change the `TryInto::try_into` in there for a pattern match which picks up [`Value::Number`](https://github.com/getsynth/synth/blob/8b957443db972bb8b923054d663d7f5418cd6883/core/src/graph/mod.rs#L169) variants and converts them to string there.
Generally speaking, would like to see support for datetime as well. The slight complication for both is that format strings should be able to precisely describe __how__ you want the formatting to work, and I'm not sure `dynfmt` supports that at the moment, which means we might need to think about a format string format.

Fix patch:
diff --git a/core/src/graph/mod.rs b/core/src/graph/mod.rs
index 19822709..d2f9fcc4 100644
--- a/core/src/graph/mod.rs
+++ b/core/src/graph/mod.rs
@@ -94,23 +94,6 @@ macro_rules! derive_from {
 			Self::$variant(value)
 		    }
 		}
-
-		impl TryInto<$ty> for $id {
-		    type Error = Error;
-		    fn try_into(self) -> Result<$ty, Self::Error> {
-			match self {
-			    Self::$variant(value) => Ok(value),
-			    otherwise => Err(
-				failed_crate!(
-				    target: Release,
-				    "invalid type: expected '{}', found '{}'",
-				    stringify!($variant),
-				    otherwise.type_()
-				)
-			    )
-			}
-		    }
-		}
 	    )?
 	)*
     };
@@ -174,6 +157,38 @@ derive_from! {
     }
 }
 
+impl TryFrom<Value> for String {
+    type Error = Error;
+
+    fn try_from(value: Value) -> Result<String, Self::Error> {
+        match value {
+            Value::String(str) => Ok(str),
+            Value::Number(num) => Ok(num.to_string()),
+            Value::DateTime(date) => Ok(date.format_to_string()),
+            otherwise => Err(failed_crate!(
+                target: Release,
+                "invalid type: expected 'String', found '{}'",
+                otherwise.type_()
+            )),
+        }
+    }
+}
+
+impl TryFrom<Value> for Number {
+    type Error = Error;
+
+    fn try_from(value: Value) -> Result<Number, Self::Error> {
+        match value {
+            Value::Number(num) => Ok(num),
+            otherwise => Err(failed_crate!(
+                target: Release,
+                "invalid type: expected 'Number', found '{}'",
+                otherwise.type_()
+            )),
+        }
+    }
+}
+
 impl Display for Value {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         write!(f, "{}", serde_json::to_string(&self).unwrap())
diff --git a/core/src/graph/string/format.rs b/core/src/graph/string/format.rs
index 48782a96..4312ae3d 100644
--- a/core/src/graph/string/format.rs
+++ b/core/src/graph/string/format.rs
@@ -86,7 +86,11 @@ impl dynfmt::FormatArgs for FormatArgs<String> {
 #[cfg(test)]
 pub mod tests {
     use super::*;
-    use crate::graph::{Graph, RandFaker, RandomString, StringNode};
+    use crate::graph::{
+        ChronoValue, Graph, NumberNode, RandFaker, RandomDateTime, RandomI64, RandomString,
+        StringNode,
+    };
+    use chrono::naive::NaiveDate;
 
     fn faker_graph(name: &str) -> Graph {
         Graph::String(StringNode::from(RandomString::from(
@@ -135,4 +139,56 @@ pub mod tests {
             .collect::<Result<Vec<_>, _>>()
             .unwrap();
     }
+
+    #[test]
+    fn format_with_number_args() {
+        let mut rng = rand::thread_rng();
+
+        let args = FormatArgs {
+            named: vec![(
+                "id".to_string(),
+                Graph::Number(NumberNode::from(RandomI64::constant(42))),
+            )]
+            .into_iter()
+            .collect(),
+            ..Default::default()
+        };
+        let formatted = Format::new("{id}_suffix".to_string(), args);
+        let gen = formatted
+            .repeat(1024)
+            .complete(&mut rng)
+            .into_iter()
+            .collect::<Result<Vec<_>, _>>()
+            .unwrap();
+
+        assert_eq!(gen[0], "42_suffix");
+    }
+
+    #[test]
+    fn format_with_date_args() {
+        let mut rng = rand::thread_rng();
+
+        let args = FormatArgs {
+            named: vec![(
+                "date".to_string(),
+                Graph::String(StringNode::from(RandomDateTime::new(
+                    ChronoValue::NaiveDate(NaiveDate::from_ymd(2021, 10, 4))
+                        ..ChronoValue::NaiveDate(NaiveDate::from_ymd(2021, 10, 4)),
+                    "%Y-%m-%d",
+                ))),
+            )]
+            .into_iter()
+            .collect(),
+            ..Default::default()
+        };
+        let formatted = Format::new("{date}.png".to_string(), args);
+        let gen = formatted
+            .repeat(1024)
+            .complete(&mut rng)
+            .into_iter()
+            .collect::<Result<Vec<_>, _>>()
+            .unwrap();
+
+        assert_eq!(gen[0], "2021-10-04.png");
+    }
 }
diff --git a/gen/src/error.rs b/gen/src/error.rs
index 7cc07259..f4394ed0 100644
--- a/gen/src/error.rs
+++ b/gen/src/error.rs
@@ -1,4 +1,4 @@
-use std::fmt::Display;
+use std::fmt::{Display, Debug};
 
 use crate::value::{IntoToken, Special, Token};
 
@@ -11,10 +11,10 @@ pub enum Error {
 }
 
 impl Error {
-    pub fn type_<T1: ToString, T2: ToString>(expected: T1, got: T2) -> Self {
+    pub fn type_<T1: ToString, T2: Debug>(expected: T1, got: T2) -> Self {
         Self::Type {
             expected: expected.to_string(),
-            got: got.to_string(),
+            got: format!("{:?}", got),
         }
     }
 
diff --git a/gen/src/value.rs b/gen/src/value.rs
index f75e6c1d..36602ab5 100644
--- a/gen/src/value.rs
+++ b/gen/src/value.rs
@@ -36,6 +36,16 @@ macro_rules! generate_enum {
 	}
 
 	impl std::fmt::Display for $id {
+	    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+		match self {
+		    $(
+			Self::$variant(inner) => write!(f, "{:?}", inner),
+		    )*
+		}
+	    }
+	}
+
+	impl std::fmt::Debug for $id {
 	    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 		match self {
 		    $(
@@ -144,7 +154,7 @@ macro_rules! generate_special_enum {
 			    Token::$id($id::$variant$(($($var,)*))?) => Ok(($($($var,)*)*)),
 			    otherwise => {
 				let err = format!(
-				    "unexpected: wanted {}, got {}",
+				    "unexpected: wanted {}, got {:?}",
 				    stringify!($variant),
 				    otherwise
 				);
@@ -216,7 +226,7 @@ macro_rules! data_model_variant_impl_ext {
 generate_enum!(
     /// Token variant for serde primitive numerical types.
     #[allow(missing_docs)]
-    #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+    #[derive(Clone, Copy, Hash, PartialEq, Eq)]
     pub enum Number {
         I8(i8),
         I16(i16),
@@ -290,7 +300,7 @@ impl serde::Serialize for Number {
 generate_enum!(
     /// Token variant for all serde primitive (non-composite) types.
     #[allow(missing_docs)]
-    #[derive(Clone, Debug, PartialEq, Eq, Hash)]
+    #[derive(Clone, PartialEq, Eq, Hash)]
     pub enum Primitive {
         Bool(bool),
         String(String),
@@ -342,7 +352,7 @@ generate_special_enum!(
 
 generate_enum!(
     /// A custom tokenization for the serde data model.
-    #[derive(Clone, Debug, PartialEq, Eq, Hash)]
+    #[derive(Clone, PartialEq, Eq, Hash)]
     pub enum Token {
         /// A token encoding serde primitive types.
         Primitive(Primitive),


Test patch: