diff --git a/Cargo.lock b/Cargo.lock index 82ee7679..3431213a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -321,6 +321,7 @@ dependencies = [ "jtd-codegen", "jtd_codegen_test", "lazy_static", + "regex", "serde_json", ] diff --git a/crates/core/src/target/inflect.rs b/crates/core/src/target/inflect.rs index 80958df9..394f0632 100644 --- a/crates/core/src/target/inflect.rs +++ b/crates/core/src/target/inflect.rs @@ -66,11 +66,21 @@ impl Inflector for TailInflector { fn decompose(s: &str) -> Vec { let mut out: Vec> = vec![vec![]]; for c in s.chars() { - if c.is_whitespace() || c == '-' || c == '_' || c == ':' { + // Non-ASCII alphanumeric characters, such as whitespace, dashes, + // underscores, or non-ASCII characters, are presumed to always be + // delimiters. + if !c.is_ascii_alphanumeric() { out.push(vec![]); continue; } + // Do not allow a part to start with a digit. Most languages prohibit + // digits at the beginning of identifiers. Just ignore the digit to make + // this happen. + if c.is_ascii_digit() && out.last().unwrap().is_empty() { + continue; + } + if let Some(last_char) = out.last().unwrap().last() { if last_char.is_lowercase() && c.is_uppercase() { out.push(vec![]); @@ -154,13 +164,17 @@ impl Case { } pub fn inflect(&self, words: &[String]) -> String { - if words.is_empty() { - return "".to_owned(); + let mut word_parts: Vec<_> = words.into_iter().flat_map(|word| decompose(word)).collect(); + + // If after decomposing the word into its parts (and after the + // associated stripping of non-ASCII alphanumerics) we don't have any + // words to work with, then inflect a "default name" instead. + if word_parts.is_empty() { + word_parts = vec!["default".into(), "name".into()]; } - let parts: Vec<_> = words + let parts: Vec<_> = word_parts .into_iter() - .flat_map(|word| decompose(word)) .enumerate() .map(|(i, word)| { if self.initialisms.contains(&word) { @@ -245,7 +259,7 @@ mod tests { #[test] fn test_camel_case() { - assert_eq!("", Case::camel_case().inflect(&[])); + assert_eq!("defaultName", Case::camel_case().inflect(&[])); assert_eq!("foo", Case::camel_case().inflect(&["foo".to_owned()])); assert_eq!( @@ -260,7 +274,7 @@ mod tests { #[test] fn test_pascal_case() { - assert_eq!("", Case::pascal_case().inflect(&[])); + assert_eq!("DefaultName", Case::pascal_case().inflect(&[])); assert_eq!("Foo", Case::pascal_case().inflect(&["foo".to_owned()])); assert_eq!( @@ -275,7 +289,7 @@ mod tests { #[test] fn test_snake_case() { - assert_eq!("", Case::snake_case().inflect(&[])); + assert_eq!("default_name", Case::snake_case().inflect(&[])); assert_eq!("foo", Case::snake_case().inflect(&["foo".to_owned()])); assert_eq!( @@ -290,7 +304,7 @@ mod tests { #[test] fn test_screaming_snake_case() { - assert_eq!("", Case::screaming_snake_case().inflect(&[])); + assert_eq!("DEFAULT_NAME", Case::screaming_snake_case().inflect(&[])); assert_eq!( "FOO", diff --git a/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/DefaultName.cs b/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/DefaultName.cs new file mode 100644 index 00000000..06eed28b --- /dev/null +++ b/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/DefaultName.cs @@ -0,0 +1,30 @@ +// Code generated by jtd-codegen for C# + System.Text.Json v0.2.0 + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JtdCodegenE2E +{ + [JsonConverter(typeof(DefaultNameJsonConverter))] + public class DefaultName + { + /// + /// The underlying data being wrapped. + /// + public string Value { get; set; } + } + + public class DefaultNameJsonConverter : JsonConverter + { + public override DefaultName Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new DefaultName { Value = JsonSerializer.Deserialize(ref reader, options) }; + } + + public override void Write(Utf8JsonWriter writer, DefaultName value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value.Value, options); + } + } +} diff --git a/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/Foo.cs b/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/Foo.cs new file mode 100644 index 00000000..79f95a54 --- /dev/null +++ b/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/Foo.cs @@ -0,0 +1,30 @@ +// Code generated by jtd-codegen for C# + System.Text.Json v0.2.0 + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JtdCodegenE2E +{ + [JsonConverter(typeof(FooJsonConverter))] + public class Foo + { + /// + /// The underlying data being wrapped. + /// + public string Value { get; set; } + } + + public class FooJsonConverter : JsonConverter + { + public override Foo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new Foo { Value = JsonSerializer.Deserialize(ref reader, options) }; + } + + public override void Write(Utf8JsonWriter writer, Foo value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value.Value, options); + } + } +} diff --git a/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/Foo0.cs b/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/Foo0.cs new file mode 100644 index 00000000..e96e52b8 --- /dev/null +++ b/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/Foo0.cs @@ -0,0 +1,30 @@ +// Code generated by jtd-codegen for C# + System.Text.Json v0.2.0 + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JtdCodegenE2E +{ + [JsonConverter(typeof(Foo0JsonConverter))] + public class Foo0 + { + /// + /// The underlying data being wrapped. + /// + public string Value { get; set; } + } + + public class Foo0JsonConverter : JsonConverter + { + public override Foo0 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new Foo0 { Value = JsonSerializer.Deserialize(ref reader, options) }; + } + + public override void Write(Utf8JsonWriter writer, Foo0 value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value.Value, options); + } + } +} diff --git a/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/Foo0bar.cs b/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/Foo0bar.cs new file mode 100644 index 00000000..a5ac2a1c --- /dev/null +++ b/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/Foo0bar.cs @@ -0,0 +1,30 @@ +// Code generated by jtd-codegen for C# + System.Text.Json v0.2.0 + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JtdCodegenE2E +{ + [JsonConverter(typeof(Foo0barJsonConverter))] + public class Foo0bar + { + /// + /// The underlying data being wrapped. + /// + public string Value { get; set; } + } + + public class Foo0barJsonConverter : JsonConverter + { + public override Foo0bar Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new Foo0bar { Value = JsonSerializer.Deserialize(ref reader, options) }; + } + + public override void Write(Utf8JsonWriter writer, Foo0bar value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value.Value, options); + } + } +} diff --git a/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/Foo1.cs b/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/Foo1.cs new file mode 100644 index 00000000..0d5f73a7 --- /dev/null +++ b/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/Foo1.cs @@ -0,0 +1,30 @@ +// Code generated by jtd-codegen for C# + System.Text.Json v0.2.0 + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JtdCodegenE2E +{ + [JsonConverter(typeof(Foo1JsonConverter))] + public class Foo1 + { + /// + /// The underlying data being wrapped. + /// + public string Value { get; set; } + } + + public class Foo1JsonConverter : JsonConverter + { + public override Foo1 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new Foo1 { Value = JsonSerializer.Deserialize(ref reader, options) }; + } + + public override void Write(Utf8JsonWriter writer, Foo1 value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value.Value, options); + } + } +} diff --git a/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/FooBar.cs b/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/FooBar.cs new file mode 100644 index 00000000..d03d5b41 --- /dev/null +++ b/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/FooBar.cs @@ -0,0 +1,30 @@ +// Code generated by jtd-codegen for C# + System.Text.Json v0.2.0 + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JtdCodegenE2E +{ + [JsonConverter(typeof(FooBarJsonConverter))] + public class FooBar + { + /// + /// The underlying data being wrapped. + /// + public string Value { get; set; } + } + + public class FooBarJsonConverter : JsonConverter + { + public override FooBar Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new FooBar { Value = JsonSerializer.Deserialize(ref reader, options) }; + } + + public override void Write(Utf8JsonWriter writer, FooBar value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value.Value, options); + } + } +} diff --git a/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/FooBar0.cs b/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/FooBar0.cs new file mode 100644 index 00000000..c3efb8e4 --- /dev/null +++ b/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/FooBar0.cs @@ -0,0 +1,30 @@ +// Code generated by jtd-codegen for C# + System.Text.Json v0.2.0 + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JtdCodegenE2E +{ + [JsonConverter(typeof(FooBar0JsonConverter))] + public class FooBar0 + { + /// + /// The underlying data being wrapped. + /// + public string Value { get; set; } + } + + public class FooBar0JsonConverter : JsonConverter + { + public override FooBar0 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new FooBar0 { Value = JsonSerializer.Deserialize(ref reader, options) }; + } + + public override void Write(Utf8JsonWriter writer, FooBar0 value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value.Value, options); + } + } +} diff --git a/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/FooBar1.cs b/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/FooBar1.cs new file mode 100644 index 00000000..41c8c4a8 --- /dev/null +++ b/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/FooBar1.cs @@ -0,0 +1,30 @@ +// Code generated by jtd-codegen for C# + System.Text.Json v0.2.0 + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JtdCodegenE2E +{ + [JsonConverter(typeof(FooBar1JsonConverter))] + public class FooBar1 + { + /// + /// The underlying data being wrapped. + /// + public string Value { get; set; } + } + + public class FooBar1JsonConverter : JsonConverter + { + public override FooBar1 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new FooBar1 { Value = JsonSerializer.Deserialize(ref reader, options) }; + } + + public override void Write(Utf8JsonWriter writer, FooBar1 value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value.Value, options); + } + } +} diff --git a/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/Root.cs b/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/Root.cs new file mode 100644 index 00000000..83585ad4 --- /dev/null +++ b/crates/target_csharp_system_text/output/empty_and_nonascii_definitions/Root.cs @@ -0,0 +1,30 @@ +// Code generated by jtd-codegen for C# + System.Text.Json v0.2.0 + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JtdCodegenE2E +{ + [JsonConverter(typeof(RootJsonConverter))] + public class Root + { + /// + /// The underlying data being wrapped. + /// + public string Value { get; set; } + } + + public class RootJsonConverter : JsonConverter + { + public override Root Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new Root { Value = JsonSerializer.Deserialize(ref reader, options) }; + } + + public override void Write(Utf8JsonWriter writer, Root value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value.Value, options); + } + } +} diff --git a/crates/target_csharp_system_text/output/empty_and_nonascii_enum_values/Root.cs b/crates/target_csharp_system_text/output/empty_and_nonascii_enum_values/Root.cs new file mode 100644 index 00000000..96fb8a4f --- /dev/null +++ b/crates/target_csharp_system_text/output/empty_and_nonascii_enum_values/Root.cs @@ -0,0 +1,87 @@ +// Code generated by jtd-codegen for C# + System.Text.Json v0.2.0 + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JtdCodegenE2E +{ + [JsonConverter(typeof(RootJsonConverter))] + public enum Root + { + DefaultName, + + Foo, + + Foo0, + + Foo1, + + FooBar, + + FooBar0, + + Foo0bar, + + FooBar1, + } + public class RootJsonConverter : JsonConverter + { + public override Root Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string value = JsonSerializer.Deserialize(ref reader, options); + switch (value) + { + case "": + return Root.DefaultName; + case "$foo": + return Root.Foo; + case "0foo": + return Root.Foo0; + case "_foo": + return Root.Foo1; + case "foo\nbar": + return Root.FooBar; + case "foo bar": + return Root.FooBar0; + case "foo0bar": + return Root.Foo0bar; + case "foo﷽bar": + return Root.FooBar1; + default: + throw new ArgumentException(String.Format("Bad Root value: {0}", value)); + } + } + + public override void Write(Utf8JsonWriter writer, Root value, JsonSerializerOptions options) + { + switch (value) + { + case Root.DefaultName: + JsonSerializer.Serialize(writer, "", options); + return; + case Root.Foo: + JsonSerializer.Serialize(writer, "$foo", options); + return; + case Root.Foo0: + JsonSerializer.Serialize(writer, "0foo", options); + return; + case Root.Foo1: + JsonSerializer.Serialize(writer, "_foo", options); + return; + case Root.FooBar: + JsonSerializer.Serialize(writer, "foo\nbar", options); + return; + case Root.FooBar0: + JsonSerializer.Serialize(writer, "foo bar", options); + return; + case Root.Foo0bar: + JsonSerializer.Serialize(writer, "foo0bar", options); + return; + case Root.FooBar1: + JsonSerializer.Serialize(writer, "foo﷽bar", options); + return; + } + } + } +} diff --git a/crates/target_csharp_system_text/output/empty_and_nonascii_properties/Root.cs b/crates/target_csharp_system_text/output/empty_and_nonascii_properties/Root.cs new file mode 100644 index 00000000..ddba832d --- /dev/null +++ b/crates/target_csharp_system_text/output/empty_and_nonascii_properties/Root.cs @@ -0,0 +1,33 @@ +// Code generated by jtd-codegen for C# + System.Text.Json v0.2.0 + +using System.Text.Json.Serialization; + +namespace JtdCodegenE2E +{ + public class Root + { + [JsonPropertyName("")] + public string DefaultName { get; set; } + + [JsonPropertyName("$foo")] + public string Foo { get; set; } + + [JsonPropertyName("0foo")] + public string Foo0 { get; set; } + + [JsonPropertyName("_foo")] + public string Foo1 { get; set; } + + [JsonPropertyName("foo\nbar")] + public string FooBar { get; set; } + + [JsonPropertyName("foo bar")] + public string FooBar0 { get; set; } + + [JsonPropertyName("foo0bar")] + public string Foo0bar { get; set; } + + [JsonPropertyName("foo﷽bar")] + public string FooBar1 { get; set; } + } +} diff --git a/crates/target_csharp_system_text/src/lib.rs b/crates/target_csharp_system_text/src/lib.rs index ba365be7..f1f6e371 100644 --- a/crates/target_csharp_system_text/src/lib.rs +++ b/crates/target_csharp_system_text/src/lib.rs @@ -478,4 +478,16 @@ mod tests { mod std_tests { jtd_codegen_test::std_test_cases!(&crate::Target::new("JtdCodegenE2E".into())); } + + mod optional_std_tests { + jtd_codegen_test::strict_std_test_case!( + &crate::Target::new("JtdCodegenE2E".into()), + empty_and_nonascii_properties + ); + + jtd_codegen_test::strict_std_test_case!( + &crate::Target::new("JtdCodegenE2E".into()), + empty_and_nonascii_enum_values + ); + } } diff --git a/crates/target_go/output/empty_and_nonascii_definitions/jtd_codegen_e2e.go b/crates/target_go/output/empty_and_nonascii_definitions/jtd_codegen_e2e.go new file mode 100644 index 00000000..692e2c75 --- /dev/null +++ b/crates/target_go/output/empty_and_nonascii_definitions/jtd_codegen_e2e.go @@ -0,0 +1,21 @@ +// Code generated by jtd-codegen for Go v0.2.0. DO NOT EDIT. + +package jtd_codegen_e2e + +type Root = string + +type DefaultName = string + +type Foo = string + +type Foo0 = string + +type Foo1 = string + +type FooBar = string + +type FooBar0 = string + +type Foo0bar = string + +type FooBar1 = string diff --git a/crates/target_go/output/empty_and_nonascii_enum_values/jtd_codegen_e2e.go b/crates/target_go/output/empty_and_nonascii_enum_values/jtd_codegen_e2e.go new file mode 100644 index 00000000..0c02cda0 --- /dev/null +++ b/crates/target_go/output/empty_and_nonascii_enum_values/jtd_codegen_e2e.go @@ -0,0 +1,23 @@ +// Code generated by jtd-codegen for Go v0.2.0. DO NOT EDIT. + +package jtd_codegen_e2e + +type Root string + +const ( + Root0 Root = "" + + RootFoo Root = "$foo" + + RootFoo0 Root = "0foo" + + RootFoo1 Root = "_foo" + + RootFooBar Root = "foo\nbar" + + RootFooBar0 Root = "foo bar" + + RootFoo0bar Root = "foo0bar" + + RootFooBar1 Root = "foo﷽bar" +) diff --git a/crates/target_go/src/lib.rs b/crates/target_go/src/lib.rs index f56baeca..3c934b86 100644 --- a/crates/target_go/src/lib.rs +++ b/crates/target_go/src/lib.rs @@ -394,4 +394,11 @@ mod tests { mod std_tests { jtd_codegen_test::std_test_cases!(&crate::Target::new("jtd_codegen_e2e".into())); } + + mod optional_std_tests { + jtd_codegen_test::strict_std_test_case!( + &crate::Target::new("jtd_codegen_e2e".into()), + empty_and_nonascii_enum_values + ); + } } diff --git a/crates/target_java_jackson/output/empty_and_nonascii_definitions/DefaultName.java b/crates/target_java_jackson/output/empty_and_nonascii_definitions/DefaultName.java new file mode 100644 index 00000000..59d74115 --- /dev/null +++ b/crates/target_java_jackson/output/empty_and_nonascii_definitions/DefaultName.java @@ -0,0 +1,27 @@ +// Code generated by jtd-codegen for Java + Jackson v0.2.0 + +package com.example; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public class DefaultName { + @JsonValue + private String value; + + public DefaultName() { + } + + @JsonCreator + public DefaultName(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/crates/target_java_jackson/output/empty_and_nonascii_definitions/Foo.java b/crates/target_java_jackson/output/empty_and_nonascii_definitions/Foo.java new file mode 100644 index 00000000..049c6b19 --- /dev/null +++ b/crates/target_java_jackson/output/empty_and_nonascii_definitions/Foo.java @@ -0,0 +1,27 @@ +// Code generated by jtd-codegen for Java + Jackson v0.2.0 + +package com.example; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public class Foo { + @JsonValue + private String value; + + public Foo() { + } + + @JsonCreator + public Foo(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/crates/target_java_jackson/output/empty_and_nonascii_definitions/Foo0.java b/crates/target_java_jackson/output/empty_and_nonascii_definitions/Foo0.java new file mode 100644 index 00000000..d6a3884e --- /dev/null +++ b/crates/target_java_jackson/output/empty_and_nonascii_definitions/Foo0.java @@ -0,0 +1,27 @@ +// Code generated by jtd-codegen for Java + Jackson v0.2.0 + +package com.example; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public class Foo0 { + @JsonValue + private String value; + + public Foo0() { + } + + @JsonCreator + public Foo0(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/crates/target_java_jackson/output/empty_and_nonascii_definitions/Foo0bar.java b/crates/target_java_jackson/output/empty_and_nonascii_definitions/Foo0bar.java new file mode 100644 index 00000000..bfd1ad91 --- /dev/null +++ b/crates/target_java_jackson/output/empty_and_nonascii_definitions/Foo0bar.java @@ -0,0 +1,27 @@ +// Code generated by jtd-codegen for Java + Jackson v0.2.0 + +package com.example; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public class Foo0bar { + @JsonValue + private String value; + + public Foo0bar() { + } + + @JsonCreator + public Foo0bar(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/crates/target_java_jackson/output/empty_and_nonascii_definitions/Foo1.java b/crates/target_java_jackson/output/empty_and_nonascii_definitions/Foo1.java new file mode 100644 index 00000000..70514cf5 --- /dev/null +++ b/crates/target_java_jackson/output/empty_and_nonascii_definitions/Foo1.java @@ -0,0 +1,27 @@ +// Code generated by jtd-codegen for Java + Jackson v0.2.0 + +package com.example; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public class Foo1 { + @JsonValue + private String value; + + public Foo1() { + } + + @JsonCreator + public Foo1(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/crates/target_java_jackson/output/empty_and_nonascii_definitions/FooBar.java b/crates/target_java_jackson/output/empty_and_nonascii_definitions/FooBar.java new file mode 100644 index 00000000..ec63f4a5 --- /dev/null +++ b/crates/target_java_jackson/output/empty_and_nonascii_definitions/FooBar.java @@ -0,0 +1,27 @@ +// Code generated by jtd-codegen for Java + Jackson v0.2.0 + +package com.example; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public class FooBar { + @JsonValue + private String value; + + public FooBar() { + } + + @JsonCreator + public FooBar(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/crates/target_java_jackson/output/empty_and_nonascii_definitions/FooBar0.java b/crates/target_java_jackson/output/empty_and_nonascii_definitions/FooBar0.java new file mode 100644 index 00000000..d95e0598 --- /dev/null +++ b/crates/target_java_jackson/output/empty_and_nonascii_definitions/FooBar0.java @@ -0,0 +1,27 @@ +// Code generated by jtd-codegen for Java + Jackson v0.2.0 + +package com.example; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public class FooBar0 { + @JsonValue + private String value; + + public FooBar0() { + } + + @JsonCreator + public FooBar0(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/crates/target_java_jackson/output/empty_and_nonascii_definitions/FooBar1.java b/crates/target_java_jackson/output/empty_and_nonascii_definitions/FooBar1.java new file mode 100644 index 00000000..ece59112 --- /dev/null +++ b/crates/target_java_jackson/output/empty_and_nonascii_definitions/FooBar1.java @@ -0,0 +1,27 @@ +// Code generated by jtd-codegen for Java + Jackson v0.2.0 + +package com.example; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public class FooBar1 { + @JsonValue + private String value; + + public FooBar1() { + } + + @JsonCreator + public FooBar1(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/crates/target_java_jackson/output/empty_and_nonascii_definitions/Root.java b/crates/target_java_jackson/output/empty_and_nonascii_definitions/Root.java new file mode 100644 index 00000000..016aae18 --- /dev/null +++ b/crates/target_java_jackson/output/empty_and_nonascii_definitions/Root.java @@ -0,0 +1,27 @@ +// Code generated by jtd-codegen for Java + Jackson v0.2.0 + +package com.example; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public class Root { + @JsonValue + private String value; + + public Root() { + } + + @JsonCreator + public Root(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/crates/target_java_jackson/output/empty_and_nonascii_definitions/UnsignedByte.java b/crates/target_java_jackson/output/empty_and_nonascii_definitions/UnsignedByte.java new file mode 100644 index 00000000..b87ca9ed --- /dev/null +++ b/crates/target_java_jackson/output/empty_and_nonascii_definitions/UnsignedByte.java @@ -0,0 +1,47 @@ +// Code generated by jtd-codegen for Java + Jackson v0.2.0 + +package com.example; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import java.io.IOException; + +@JsonSerialize(using = UnsignedByte.Serializer.class) +@JsonDeserialize(using = UnsignedByte.Deserializer.class) +public class UnsignedByte { + private byte value; + + public UnsignedByte(byte value) { + this.value = value; + } + + public byte getValue() { + return value; + } + + public void setValue(byte value) { + this.value = value; + } + + public static class Serializer extends JsonSerializer { + @Override + public void serialize(UnsignedByte value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeNumber(Byte.toUnsignedLong(value.getValue())); + } + } + + public static class Deserializer extends JsonDeserializer { + @Override + public UnsignedByte deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { + return new UnsignedByte((byte) p.getLongValue()); + } + } +} diff --git a/crates/target_java_jackson/output/empty_and_nonascii_definitions/UnsignedInteger.java b/crates/target_java_jackson/output/empty_and_nonascii_definitions/UnsignedInteger.java new file mode 100644 index 00000000..c30b07e5 --- /dev/null +++ b/crates/target_java_jackson/output/empty_and_nonascii_definitions/UnsignedInteger.java @@ -0,0 +1,47 @@ +// Code generated by jtd-codegen for Java + Jackson v0.2.0 + +package com.example; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import java.io.IOException; + +@JsonSerialize(using = UnsignedInteger.Serializer.class) +@JsonDeserialize(using = UnsignedInteger.Deserializer.class) +public class UnsignedInteger { + private int value; + + public UnsignedInteger(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + + public static class Serializer extends JsonSerializer { + @Override + public void serialize(UnsignedInteger value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeNumber(Integer.toUnsignedLong(value.getValue())); + } + } + + public static class Deserializer extends JsonDeserializer { + @Override + public UnsignedInteger deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { + return new UnsignedInteger((int) p.getLongValue()); + } + } +} diff --git a/crates/target_java_jackson/output/empty_and_nonascii_definitions/UnsignedShort.java b/crates/target_java_jackson/output/empty_and_nonascii_definitions/UnsignedShort.java new file mode 100644 index 00000000..3a939415 --- /dev/null +++ b/crates/target_java_jackson/output/empty_and_nonascii_definitions/UnsignedShort.java @@ -0,0 +1,47 @@ +// Code generated by jtd-codegen for Java + Jackson v0.2.0 + +package com.example; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import java.io.IOException; + +@JsonSerialize(using = UnsignedShort.Serializer.class) +@JsonDeserialize(using = UnsignedShort.Deserializer.class) +public class UnsignedShort { + private short value; + + public UnsignedShort(short value) { + this.value = value; + } + + public short getValue() { + return value; + } + + public void setValue(short value) { + this.value = value; + } + + public static class Serializer extends JsonSerializer { + @Override + public void serialize(UnsignedShort value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeNumber(Short.toUnsignedLong(value.getValue())); + } + } + + public static class Deserializer extends JsonDeserializer { + @Override + public UnsignedShort deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { + return new UnsignedShort((short) p.getLongValue()); + } + } +} diff --git a/crates/target_python/output/empty_and_nonascii_definitions/__init__.py b/crates/target_python/output/empty_and_nonascii_definitions/__init__.py new file mode 100644 index 00000000..a6e1eb71 --- /dev/null +++ b/crates/target_python/output/empty_and_nonascii_definitions/__init__.py @@ -0,0 +1,165 @@ +# Code generated by jtd-codegen for Python v0.3.0 + +import re +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from typing import Any, Union, get_args, get_origin + + +@dataclass +class Root: + value: 'str' + + @classmethod + def from_json_data(cls, data: Any) -> 'Root': + return cls(_from_json_data(str, data)) + + def to_json_data(self) -> Any: + return _to_json_data(self.value) + +@dataclass +class DefaultName: + value: 'str' + + @classmethod + def from_json_data(cls, data: Any) -> 'DefaultName': + return cls(_from_json_data(str, data)) + + def to_json_data(self) -> Any: + return _to_json_data(self.value) + +@dataclass +class Foo: + value: 'str' + + @classmethod + def from_json_data(cls, data: Any) -> 'Foo': + return cls(_from_json_data(str, data)) + + def to_json_data(self) -> Any: + return _to_json_data(self.value) + +@dataclass +class Foo0: + value: 'str' + + @classmethod + def from_json_data(cls, data: Any) -> 'Foo0': + return cls(_from_json_data(str, data)) + + def to_json_data(self) -> Any: + return _to_json_data(self.value) + +@dataclass +class Foo1: + value: 'str' + + @classmethod + def from_json_data(cls, data: Any) -> 'Foo1': + return cls(_from_json_data(str, data)) + + def to_json_data(self) -> Any: + return _to_json_data(self.value) + +@dataclass +class FooBar: + value: 'str' + + @classmethod + def from_json_data(cls, data: Any) -> 'FooBar': + return cls(_from_json_data(str, data)) + + def to_json_data(self) -> Any: + return _to_json_data(self.value) + +@dataclass +class FooBar0: + value: 'str' + + @classmethod + def from_json_data(cls, data: Any) -> 'FooBar0': + return cls(_from_json_data(str, data)) + + def to_json_data(self) -> Any: + return _to_json_data(self.value) + +@dataclass +class Foo0bar: + value: 'str' + + @classmethod + def from_json_data(cls, data: Any) -> 'Foo0bar': + return cls(_from_json_data(str, data)) + + def to_json_data(self) -> Any: + return _to_json_data(self.value) + +@dataclass +class FooBar1: + value: 'str' + + @classmethod + def from_json_data(cls, data: Any) -> 'FooBar1': + return cls(_from_json_data(str, data)) + + def to_json_data(self) -> Any: + return _to_json_data(self.value) + +def _from_json_data(cls: Any, data: Any) -> Any: + if data is None or cls in [bool, int, float, str, object] or cls is Any: + return data + if cls is datetime: + return _parse_rfc3339(data) + if get_origin(cls) is Union: + return _from_json_data(get_args(cls)[0], data) + if get_origin(cls) is list: + return [_from_json_data(get_args(cls)[0], d) for d in data] + if get_origin(cls) is dict: + return { k: _from_json_data(get_args(cls)[1], v) for k, v in data.items() } + return cls.from_json_data(data) + +def _to_json_data(data: Any) -> Any: + if data is None or type(data) in [bool, int, float, str, object]: + return data + if type(data) is datetime: + return data.isoformat() + if type(data) is list: + return [_to_json_data(d) for d in data] + if type(data) is dict: + return { k: _to_json_data(v) for k, v in data.items() } + return data.to_json_data() + +def _parse_rfc3339(s: str) -> datetime: + datetime_re = '^(\d{4})-(\d{2})-(\d{2})[tT](\d{2}):(\d{2}):(\d{2})(\.\d+)?([zZ]|((\+|-)(\d{2}):(\d{2})))$' + match = re.match(datetime_re, s) + if not match: + raise ValueError('Invalid RFC3339 date/time', s) + + (year, month, day, hour, minute, second, frac_seconds, offset, + *tz) = match.groups() + + frac_seconds_parsed = None + if frac_seconds: + frac_seconds_parsed = int(float(frac_seconds) * 1_000_000) + else: + frac_seconds_parsed = 0 + + tzinfo = None + if offset == 'Z': + tzinfo = timezone.utc + else: + hours = int(tz[2]) + minutes = int(tz[3]) + sign = 1 if tz[1] == '+' else -1 + + if minutes not in range(60): + raise ValueError('minute offset must be in 0..59') + + tzinfo = timezone(timedelta(minutes=sign * (60 * hours + minutes))) + + second_parsed = int(second) + if second_parsed == 60: + second_parsed = 59 + + return datetime(int(year), int(month), int(day), int(hour), int(minute), + second_parsed, frac_seconds_parsed, tzinfo) diff --git a/crates/target_python/output/empty_and_nonascii_enum_values/__init__.py b/crates/target_python/output/empty_and_nonascii_enum_values/__init__.py new file mode 100644 index 00000000..a3165649 --- /dev/null +++ b/crates/target_python/output/empty_and_nonascii_enum_values/__init__.py @@ -0,0 +1,82 @@ +# Code generated by jtd-codegen for Python v0.3.0 + +import re +from datetime import datetime, timedelta, timezone +from enum import Enum +from typing import Any, Union, get_args, get_origin + + +class Root(Enum): + DEFAULT_NAME = "" + FOO = "$foo" + FOO0 = "0foo" + FOO1 = "_foo" + FOO_BAR = "foo\nbar" + FOO_BAR0 = "foo bar" + FOO0BAR = "foo0bar" + FOO_BAR1 = "foo﷽bar" + @classmethod + def from_json_data(cls, data: Any) -> 'Root': + return cls(data) + + def to_json_data(self) -> Any: + return self.value + +def _from_json_data(cls: Any, data: Any) -> Any: + if data is None or cls in [bool, int, float, str, object] or cls is Any: + return data + if cls is datetime: + return _parse_rfc3339(data) + if get_origin(cls) is Union: + return _from_json_data(get_args(cls)[0], data) + if get_origin(cls) is list: + return [_from_json_data(get_args(cls)[0], d) for d in data] + if get_origin(cls) is dict: + return { k: _from_json_data(get_args(cls)[1], v) for k, v in data.items() } + return cls.from_json_data(data) + +def _to_json_data(data: Any) -> Any: + if data is None or type(data) in [bool, int, float, str, object]: + return data + if type(data) is datetime: + return data.isoformat() + if type(data) is list: + return [_to_json_data(d) for d in data] + if type(data) is dict: + return { k: _to_json_data(v) for k, v in data.items() } + return data.to_json_data() + +def _parse_rfc3339(s: str) -> datetime: + datetime_re = '^(\d{4})-(\d{2})-(\d{2})[tT](\d{2}):(\d{2}):(\d{2})(\.\d+)?([zZ]|((\+|-)(\d{2}):(\d{2})))$' + match = re.match(datetime_re, s) + if not match: + raise ValueError('Invalid RFC3339 date/time', s) + + (year, month, day, hour, minute, second, frac_seconds, offset, + *tz) = match.groups() + + frac_seconds_parsed = None + if frac_seconds: + frac_seconds_parsed = int(float(frac_seconds) * 1_000_000) + else: + frac_seconds_parsed = 0 + + tzinfo = None + if offset == 'Z': + tzinfo = timezone.utc + else: + hours = int(tz[2]) + minutes = int(tz[3]) + sign = 1 if tz[1] == '+' else -1 + + if minutes not in range(60): + raise ValueError('minute offset must be in 0..59') + + tzinfo = timezone(timedelta(minutes=sign * (60 * hours + minutes))) + + second_parsed = int(second) + if second_parsed == 60: + second_parsed = 59 + + return datetime(int(year), int(month), int(day), int(hour), int(minute), + second_parsed, frac_seconds_parsed, tzinfo) diff --git a/crates/target_python/output/empty_and_nonascii_properties/__init__.py b/crates/target_python/output/empty_and_nonascii_properties/__init__.py new file mode 100644 index 00000000..f7d61813 --- /dev/null +++ b/crates/target_python/output/empty_and_nonascii_properties/__init__.py @@ -0,0 +1,102 @@ +# Code generated by jtd-codegen for Python v0.3.0 + +import re +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from typing import Any, Dict, Optional, Union, get_args, get_origin + + +@dataclass +class Root: + default_name: 'str' + foo: 'str' + foo0: 'str' + foo1: 'str' + foo_bar: 'str' + foo_bar0: 'str' + foo0bar: 'str' + foo_bar1: 'str' + + @classmethod + def from_json_data(cls, data: Any) -> 'Root': + return cls( + _from_json_data(str, data.get("")), + _from_json_data(str, data.get("$foo")), + _from_json_data(str, data.get("0foo")), + _from_json_data(str, data.get("_foo")), + _from_json_data(str, data.get("foo\nbar")), + _from_json_data(str, data.get("foo bar")), + _from_json_data(str, data.get("foo0bar")), + _from_json_data(str, data.get("foo﷽bar")), + ) + + def to_json_data(self) -> Any: + data: Dict[str, Any] = {} + data[""] = _to_json_data(self.default_name) + data["$foo"] = _to_json_data(self.foo) + data["0foo"] = _to_json_data(self.foo0) + data["_foo"] = _to_json_data(self.foo1) + data["foo\nbar"] = _to_json_data(self.foo_bar) + data["foo bar"] = _to_json_data(self.foo_bar0) + data["foo0bar"] = _to_json_data(self.foo0bar) + data["foo﷽bar"] = _to_json_data(self.foo_bar1) + return data + +def _from_json_data(cls: Any, data: Any) -> Any: + if data is None or cls in [bool, int, float, str, object] or cls is Any: + return data + if cls is datetime: + return _parse_rfc3339(data) + if get_origin(cls) is Union: + return _from_json_data(get_args(cls)[0], data) + if get_origin(cls) is list: + return [_from_json_data(get_args(cls)[0], d) for d in data] + if get_origin(cls) is dict: + return { k: _from_json_data(get_args(cls)[1], v) for k, v in data.items() } + return cls.from_json_data(data) + +def _to_json_data(data: Any) -> Any: + if data is None or type(data) in [bool, int, float, str, object]: + return data + if type(data) is datetime: + return data.isoformat() + if type(data) is list: + return [_to_json_data(d) for d in data] + if type(data) is dict: + return { k: _to_json_data(v) for k, v in data.items() } + return data.to_json_data() + +def _parse_rfc3339(s: str) -> datetime: + datetime_re = '^(\d{4})-(\d{2})-(\d{2})[tT](\d{2}):(\d{2}):(\d{2})(\.\d+)?([zZ]|((\+|-)(\d{2}):(\d{2})))$' + match = re.match(datetime_re, s) + if not match: + raise ValueError('Invalid RFC3339 date/time', s) + + (year, month, day, hour, minute, second, frac_seconds, offset, + *tz) = match.groups() + + frac_seconds_parsed = None + if frac_seconds: + frac_seconds_parsed = int(float(frac_seconds) * 1_000_000) + else: + frac_seconds_parsed = 0 + + tzinfo = None + if offset == 'Z': + tzinfo = timezone.utc + else: + hours = int(tz[2]) + minutes = int(tz[3]) + sign = 1 if tz[1] == '+' else -1 + + if minutes not in range(60): + raise ValueError('minute offset must be in 0..59') + + tzinfo = timezone(timedelta(minutes=sign * (60 * hours + minutes))) + + second_parsed = int(second) + if second_parsed == 60: + second_parsed = 59 + + return datetime(int(year), int(month), int(day), int(hour), int(minute), + second_parsed, frac_seconds_parsed, tzinfo) diff --git a/crates/target_python/src/lib.rs b/crates/target_python/src/lib.rs index fa0f0473..1e91cb59 100644 --- a/crates/target_python/src/lib.rs +++ b/crates/target_python/src/lib.rs @@ -591,4 +591,16 @@ mod tests { mod std_tests { jtd_codegen_test::std_test_cases!(&crate::Target::new()); } + + mod optional_std_tests { + jtd_codegen_test::strict_std_test_case!( + &crate::Target::new(), + empty_and_nonascii_properties + ); + + jtd_codegen_test::strict_std_test_case!( + &crate::Target::new(), + empty_and_nonascii_enum_values + ); + } } diff --git a/crates/target_ruby/output/empty_and_nonascii_definitions/jtdcodegen_e2e.rb b/crates/target_ruby/output/empty_and_nonascii_definitions/jtdcodegen_e2e.rb new file mode 100644 index 00000000..46bc2cce --- /dev/null +++ b/crates/target_ruby/output/empty_and_nonascii_definitions/jtdcodegen_e2e.rb @@ -0,0 +1,163 @@ +# Code generated by jtd-codegen for Ruby v0.1.0 + +require 'json' +require 'time' + +module JTDCodegenE2E + + class Root + attr_accessor :value + + def self.from_json_data(data) + out = Root.new + out.value = JTDCodegenE2E.from_json_data(String, data) + out + end + + def to_json_data + JTDCodegenE2E.to_json_data(value) + end + end + + class DefaultName + attr_accessor :value + + def self.from_json_data(data) + out = DefaultName.new + out.value = JTDCodegenE2E.from_json_data(String, data) + out + end + + def to_json_data + JTDCodegenE2E.to_json_data(value) + end + end + + class Foo + attr_accessor :value + + def self.from_json_data(data) + out = Foo.new + out.value = JTDCodegenE2E.from_json_data(String, data) + out + end + + def to_json_data + JTDCodegenE2E.to_json_data(value) + end + end + + class Foo0 + attr_accessor :value + + def self.from_json_data(data) + out = Foo0.new + out.value = JTDCodegenE2E.from_json_data(String, data) + out + end + + def to_json_data + JTDCodegenE2E.to_json_data(value) + end + end + + class Foo1 + attr_accessor :value + + def self.from_json_data(data) + out = Foo1.new + out.value = JTDCodegenE2E.from_json_data(String, data) + out + end + + def to_json_data + JTDCodegenE2E.to_json_data(value) + end + end + + class FooBar + attr_accessor :value + + def self.from_json_data(data) + out = FooBar.new + out.value = JTDCodegenE2E.from_json_data(String, data) + out + end + + def to_json_data + JTDCodegenE2E.to_json_data(value) + end + end + + class FooBar0 + attr_accessor :value + + def self.from_json_data(data) + out = FooBar0.new + out.value = JTDCodegenE2E.from_json_data(String, data) + out + end + + def to_json_data + JTDCodegenE2E.to_json_data(value) + end + end + + class Foo0bar + attr_accessor :value + + def self.from_json_data(data) + out = Foo0bar.new + out.value = JTDCodegenE2E.from_json_data(String, data) + out + end + + def to_json_data + JTDCodegenE2E.to_json_data(value) + end + end + + class FooBar1 + attr_accessor :value + + def self.from_json_data(data) + out = FooBar1.new + out.value = JTDCodegenE2E.from_json_data(String, data) + out + end + + def to_json_data + JTDCodegenE2E.to_json_data(value) + end + end + + private + + def self.from_json_data(type, data) + if data.nil? || [Object, TrueClass, Integer, Float, String].include?(type) + data + elsif type == DateTime + DateTime.rfc3339(data) + elsif type.is_a?(Array) + data.map { |elem| from_json_data(type.first, elem) } + elsif type.is_a?(Hash) + data.transform_values { |elem| from_json_data(type.values.first, elem) } + else + type.from_json_data(data) + end + end + + def self.to_json_data(data) + if data.nil? || [TrueClass, FalseClass, Integer, Float, String].include?(data.class) + data + elsif data.is_a?(DateTime) + data.rfc3339 + elsif data.is_a?(Array) + data.map { |elem| to_json_data(elem) } + elsif data.is_a?(Hash) + data.transform_values { |elem| to_json_data(elem) } + else + data.to_json_data + end + end +end diff --git a/crates/target_ruby/output/empty_and_nonascii_enum_values/jtdcodegen_e2e.rb b/crates/target_ruby/output/empty_and_nonascii_enum_values/jtdcodegen_e2e.rb new file mode 100644 index 00000000..0f48d832 --- /dev/null +++ b/crates/target_ruby/output/empty_and_nonascii_enum_values/jtdcodegen_e2e.rb @@ -0,0 +1,73 @@ +# Code generated by jtd-codegen for Ruby v0.1.0 + +require 'json' +require 'time' + +module JTDCodegenE2E + + class Root + attr_accessor :value + + def initialize(value) + self.value = value + end + + private_class_method :new + + DEFAULT_NAME = new("") + FOO = new("$foo") + FOO0 = new("0foo") + FOO1 = new("_foo") + FOO_BAR = new("foo\nbar") + FOO_BAR0 = new("foo bar") + FOO0BAR = new("foo0bar") + FOO_BAR1 = new("foo﷽bar") + + def self.from_json_data(data) + { + "" => DEFAULT_NAME, + "$foo" => FOO, + "0foo" => FOO0, + "_foo" => FOO1, + "foo\nbar" => FOO_BAR, + "foo bar" => FOO_BAR0, + "foo0bar" => FOO0BAR, + "foo﷽bar" => FOO_BAR1, + }[data] + end + + def to_json_data + value + end + end + + private + + def self.from_json_data(type, data) + if data.nil? || [Object, TrueClass, Integer, Float, String].include?(type) + data + elsif type == DateTime + DateTime.rfc3339(data) + elsif type.is_a?(Array) + data.map { |elem| from_json_data(type.first, elem) } + elsif type.is_a?(Hash) + data.transform_values { |elem| from_json_data(type.values.first, elem) } + else + type.from_json_data(data) + end + end + + def self.to_json_data(data) + if data.nil? || [TrueClass, FalseClass, Integer, Float, String].include?(data.class) + data + elsif data.is_a?(DateTime) + data.rfc3339 + elsif data.is_a?(Array) + data.map { |elem| to_json_data(elem) } + elsif data.is_a?(Hash) + data.transform_values { |elem| to_json_data(elem) } + else + data.to_json_data + end + end +end diff --git a/crates/target_ruby/output/empty_and_nonascii_properties/jtdcodegen_e2e.rb b/crates/target_ruby/output/empty_and_nonascii_properties/jtdcodegen_e2e.rb new file mode 100644 index 00000000..e7b7d893 --- /dev/null +++ b/crates/target_ruby/output/empty_and_nonascii_properties/jtdcodegen_e2e.rb @@ -0,0 +1,74 @@ +# Code generated by jtd-codegen for Ruby v0.1.0 + +require 'json' +require 'time' + +module JTDCodegenE2E + + class Root + attr_accessor :default_name + attr_accessor :foo + attr_accessor :foo0 + attr_accessor :foo1 + attr_accessor :foo_bar + attr_accessor :foo_bar0 + attr_accessor :foo0bar + attr_accessor :foo_bar1 + + def self.from_json_data(data) + out = Root.new + out.default_name = JTDCodegenE2E::from_json_data(String, data[""]) + out.foo = JTDCodegenE2E::from_json_data(String, data["$foo"]) + out.foo0 = JTDCodegenE2E::from_json_data(String, data["0foo"]) + out.foo1 = JTDCodegenE2E::from_json_data(String, data["_foo"]) + out.foo_bar = JTDCodegenE2E::from_json_data(String, data["foo\nbar"]) + out.foo_bar0 = JTDCodegenE2E::from_json_data(String, data["foo bar"]) + out.foo0bar = JTDCodegenE2E::from_json_data(String, data["foo0bar"]) + out.foo_bar1 = JTDCodegenE2E::from_json_data(String, data["foo﷽bar"]) + out + end + + def to_json_data + data = {} + data[""] = JTDCodegenE2E::to_json_data(default_name) + data["$foo"] = JTDCodegenE2E::to_json_data(foo) + data["0foo"] = JTDCodegenE2E::to_json_data(foo0) + data["_foo"] = JTDCodegenE2E::to_json_data(foo1) + data["foo\nbar"] = JTDCodegenE2E::to_json_data(foo_bar) + data["foo bar"] = JTDCodegenE2E::to_json_data(foo_bar0) + data["foo0bar"] = JTDCodegenE2E::to_json_data(foo0bar) + data["foo﷽bar"] = JTDCodegenE2E::to_json_data(foo_bar1) + data + end + end + + private + + def self.from_json_data(type, data) + if data.nil? || [Object, TrueClass, Integer, Float, String].include?(type) + data + elsif type == DateTime + DateTime.rfc3339(data) + elsif type.is_a?(Array) + data.map { |elem| from_json_data(type.first, elem) } + elsif type.is_a?(Hash) + data.transform_values { |elem| from_json_data(type.values.first, elem) } + else + type.from_json_data(data) + end + end + + def self.to_json_data(data) + if data.nil? || [TrueClass, FalseClass, Integer, Float, String].include?(data.class) + data + elsif data.is_a?(DateTime) + data.rfc3339 + elsif data.is_a?(Array) + data.map { |elem| to_json_data(elem) } + elsif data.is_a?(Hash) + data.transform_values { |elem| to_json_data(elem) } + else + data.to_json_data + end + end +end diff --git a/crates/target_ruby/src/lib.rs b/crates/target_ruby/src/lib.rs index d5514074..db7e3ee7 100644 --- a/crates/target_ruby/src/lib.rs +++ b/crates/target_ruby/src/lib.rs @@ -454,4 +454,16 @@ mod tests { mod std_tests { jtd_codegen_test::std_test_cases!(&crate::Target::new("JTDCodegenE2E".into())); } + + mod optional_std_tests { + jtd_codegen_test::strict_std_test_case!( + &crate::Target::new("JTDCodegenE2E".into()), + empty_and_nonascii_properties + ); + + jtd_codegen_test::strict_std_test_case!( + &crate::Target::new("JTDCodegenE2E".into()), + empty_and_nonascii_enum_values + ); + } } diff --git a/crates/target_ruby_sig/output/empty_and_nonascii_definitions/jtd_codegen_e2e.rbs b/crates/target_ruby_sig/output/empty_and_nonascii_definitions/jtd_codegen_e2e.rbs new file mode 100644 index 00000000..5e9fca24 --- /dev/null +++ b/crates/target_ruby_sig/output/empty_and_nonascii_definitions/jtd_codegen_e2e.rbs @@ -0,0 +1,70 @@ +# Code generated by jtd-codegen for Ruby Type Signatures v0.1.0 + +module jtd_codegen_e2e + + class Root + attr_accessor value: String + + def self.from_json_data: (untyped) -> Root + def to_json_data: () -> untyped + end + + class DefaultName + attr_accessor value: String + + def self.from_json_data: (untyped) -> DefaultName + def to_json_data: () -> untyped + end + + class Foo + attr_accessor value: String + + def self.from_json_data: (untyped) -> Foo + def to_json_data: () -> untyped + end + + class Foo0 + attr_accessor value: String + + def self.from_json_data: (untyped) -> Foo0 + def to_json_data: () -> untyped + end + + class Foo1 + attr_accessor value: String + + def self.from_json_data: (untyped) -> Foo1 + def to_json_data: () -> untyped + end + + class FooBar + attr_accessor value: String + + def self.from_json_data: (untyped) -> FooBar + def to_json_data: () -> untyped + end + + class FooBar0 + attr_accessor value: String + + def self.from_json_data: (untyped) -> FooBar0 + def to_json_data: () -> untyped + end + + class Foo0bar + attr_accessor value: String + + def self.from_json_data: (untyped) -> Foo0bar + def to_json_data: () -> untyped + end + + class FooBar1 + attr_accessor value: String + + def self.from_json_data: (untyped) -> FooBar1 + def to_json_data: () -> untyped + end + + def self.from_json_data: (untyped, untyped) -> untyped + def self.to_json_data: (untyped) -> untyped +end diff --git a/crates/target_ruby_sig/output/empty_and_nonascii_enum_values/jtd_codegen_e2e.rbs b/crates/target_ruby_sig/output/empty_and_nonascii_enum_values/jtd_codegen_e2e.rbs new file mode 100644 index 00000000..a0189a4f --- /dev/null +++ b/crates/target_ruby_sig/output/empty_and_nonascii_enum_values/jtd_codegen_e2e.rbs @@ -0,0 +1,23 @@ +# Code generated by jtd-codegen for Ruby Type Signatures v0.1.0 + +module jtd_codegen_e2e + + class Root + attr_accessor value: String + + DEFAULT_NAME: Root + FOO: Root + FOO0: Root + FOO1: Root + FOO_BAR: Root + FOO_BAR0: Root + FOO0BAR: Root + FOO_BAR1: Root + + def self.from_json_data: (untyped) -> Root + def to_json_data: () -> untyped + end + + def self.from_json_data: (untyped, untyped) -> untyped + def self.to_json_data: (untyped) -> untyped +end diff --git a/crates/target_ruby_sig/output/empty_and_nonascii_properties/jtd_codegen_e2e.rbs b/crates/target_ruby_sig/output/empty_and_nonascii_properties/jtd_codegen_e2e.rbs new file mode 100644 index 00000000..c86909a5 --- /dev/null +++ b/crates/target_ruby_sig/output/empty_and_nonascii_properties/jtd_codegen_e2e.rbs @@ -0,0 +1,21 @@ +# Code generated by jtd-codegen for Ruby Type Signatures v0.1.0 + +module jtd_codegen_e2e + + class Root + attr_accessor default_name: String + attr_accessor foo: String + attr_accessor foo0: String + attr_accessor foo1: String + attr_accessor foo_bar: String + attr_accessor foo_bar0: String + attr_accessor foo0bar: String + attr_accessor foo_bar1: String + + def self.from_json_data: (untyped) -> Root + def to_json_data: () -> untyped + end + + def self.from_json_data: (untyped, untyped) -> untyped + def self.to_json_data: (untyped) -> untyped +end diff --git a/crates/target_ruby_sig/src/lib.rs b/crates/target_ruby_sig/src/lib.rs index a18d0093..271bab75 100644 --- a/crates/target_ruby_sig/src/lib.rs +++ b/crates/target_ruby_sig/src/lib.rs @@ -276,4 +276,16 @@ mod tests { mod std_tests { jtd_codegen_test::std_test_cases!(&crate::Target::new("jtd_codegen_e2e".into())); } + + mod optional_std_tests { + jtd_codegen_test::strict_std_test_case!( + &crate::Target::new("jtd_codegen_e2e".into()), + empty_and_nonascii_properties + ); + + jtd_codegen_test::strict_std_test_case!( + &crate::Target::new("jtd_codegen_e2e".into()), + empty_and_nonascii_enum_values + ); + } } diff --git a/crates/target_rust/output/empty_and_nonascii_definitions/mod.rs b/crates/target_rust/output/empty_and_nonascii_definitions/mod.rs new file mode 100644 index 00000000..238a311f --- /dev/null +++ b/crates/target_rust/output/empty_and_nonascii_definitions/mod.rs @@ -0,0 +1,19 @@ +// Code generated by jtd-codegen for Rust v0.2.0 + +pub type Root = String; + +pub type DefaultName = String; + +pub type Foo = String; + +pub type Foo0 = String; + +pub type Foo1 = String; + +pub type FooBar = String; + +pub type FooBar0 = String; + +pub type Foo0bar = String; + +pub type FooBar1 = String; diff --git a/crates/target_rust/output/empty_and_nonascii_enum_values/mod.rs b/crates/target_rust/output/empty_and_nonascii_enum_values/mod.rs new file mode 100644 index 00000000..f59525d2 --- /dev/null +++ b/crates/target_rust/output/empty_and_nonascii_enum_values/mod.rs @@ -0,0 +1,30 @@ +// Code generated by jtd-codegen for Rust v0.2.0 + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub enum Root { + #[serde(rename = "")] + DefaultName, + + #[serde(rename = "$foo")] + Foo, + + #[serde(rename = "0foo")] + Foo0, + + #[serde(rename = "_foo")] + Foo1, + + #[serde(rename = "foo\nbar")] + FooBar, + + #[serde(rename = "foo bar")] + FooBar0, + + #[serde(rename = "foo0bar")] + Foo0bar, + + #[serde(rename = "foo﷽bar")] + FooBar1, +} diff --git a/crates/target_rust/output/empty_and_nonascii_properties/mod.rs b/crates/target_rust/output/empty_and_nonascii_properties/mod.rs new file mode 100644 index 00000000..fb44cf39 --- /dev/null +++ b/crates/target_rust/output/empty_and_nonascii_properties/mod.rs @@ -0,0 +1,30 @@ +// Code generated by jtd-codegen for Rust v0.2.0 + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct Root { + #[serde(rename = "")] + pub defaultName: String, + + #[serde(rename = "$foo")] + pub foo: String, + + #[serde(rename = "0foo")] + pub foo0: String, + + #[serde(rename = "_foo")] + pub foo1: String, + + #[serde(rename = "foo\nbar")] + pub fooBar: String, + + #[serde(rename = "foo bar")] + pub fooBar0: String, + + #[serde(rename = "foo0bar")] + pub foo0bar: String, + + #[serde(rename = "foo﷽bar")] + pub fooBar1: String, +} diff --git a/crates/target_rust/src/lib.rs b/crates/target_rust/src/lib.rs index a6851574..5c7ae462 100644 --- a/crates/target_rust/src/lib.rs +++ b/crates/target_rust/src/lib.rs @@ -391,4 +391,16 @@ mod tests { mod std_tests { jtd_codegen_test::std_test_cases!(&crate::Target::new()); } + + mod optional_std_tests { + jtd_codegen_test::strict_std_test_case!( + &crate::Target::new(), + empty_and_nonascii_properties + ); + + jtd_codegen_test::strict_std_test_case!( + &crate::Target::new(), + empty_and_nonascii_enum_values + ); + } } diff --git a/crates/target_typescript/Cargo.toml b/crates/target_typescript/Cargo.toml index 264e1213..724d8ffa 100644 --- a/crates/target_typescript/Cargo.toml +++ b/crates/target_typescript/Cargo.toml @@ -8,6 +8,7 @@ edition = "2018" jtd-codegen = { path = "../core" } lazy_static = "1.4.0" serde_json = "1.0" +regex = "1" [dev-dependencies] jtd_codegen_test = { path = "../test" } diff --git a/crates/target_typescript/output/empty_and_nonascii_definitions/index.ts b/crates/target_typescript/output/empty_and_nonascii_definitions/index.ts new file mode 100644 index 00000000..ea9f5f9d --- /dev/null +++ b/crates/target_typescript/output/empty_and_nonascii_definitions/index.ts @@ -0,0 +1,19 @@ +// Code generated by jtd-codegen for TypeScript v0.2.0 + +export type Root = string; + +export type DefaultName = string; + +export type Foo = string; + +export type Foo0 = string; + +export type Foo1 = string; + +export type FooBar = string; + +export type FooBar0 = string; + +export type Foo0bar = string; + +export type FooBar1 = string; diff --git a/crates/target_typescript/output/empty_and_nonascii_enum_values/index.ts b/crates/target_typescript/output/empty_and_nonascii_enum_values/index.ts new file mode 100644 index 00000000..16b6f26a --- /dev/null +++ b/crates/target_typescript/output/empty_and_nonascii_enum_values/index.ts @@ -0,0 +1,12 @@ +// Code generated by jtd-codegen for TypeScript v0.2.0 + +export enum Root { + DefaultName = "", + Foo = "$foo", + Foo0 = "0foo", + Foo1 = "_foo", + FooBar = "foo\nbar", + FooBar0 = "foo bar", + Foo0bar = "foo0bar", + FooBar1 = "foo﷽bar", +} diff --git a/crates/target_typescript/output/empty_and_nonascii_properties/index.ts b/crates/target_typescript/output/empty_and_nonascii_properties/index.ts new file mode 100644 index 00000000..c768ff67 --- /dev/null +++ b/crates/target_typescript/output/empty_and_nonascii_properties/index.ts @@ -0,0 +1,12 @@ +// Code generated by jtd-codegen for TypeScript v0.2.0 + +export interface Root { + "": string; + $foo: string; + "0foo": string; + _foo: string; + "foo\nbar": string; + "foo bar": string; + foo0bar: string; + "foo﷽bar": string; +} diff --git a/crates/target_typescript/src/lib.rs b/crates/target_typescript/src/lib.rs index b17f9579..a6465579 100644 --- a/crates/target_typescript/src/lib.rs +++ b/crates/target_typescript/src/lib.rs @@ -1,6 +1,7 @@ use jtd_codegen::target::{self, inflect, metadata}; use jtd_codegen::Result; use lazy_static::lazy_static; +use regex::Regex; use serde_json::Value; use std::collections::{BTreeMap, BTreeSet}; use std::io::Write; @@ -179,9 +180,19 @@ impl jtd_codegen::target::Target for Target { write!(out, "{}", &description)?; if field.optional { - writeln!(out, " {}?: {};", field.json_name, field.type_)?; + writeln!( + out, + " {}?: {};", + format_property(field.json_name.clone()), + field.type_ + )?; } else { - writeln!(out, " {}: {};", field.json_name, field.type_)?; + writeln!( + out, + " {}: {};", + format_property(field.json_name.clone()), + field.type_ + )?; } } writeln!(out, "}}")?; @@ -240,9 +251,19 @@ impl jtd_codegen::target::Target for Target { write!(out, "{}", &description)?; if field.optional { - writeln!(out, " {}?: {};", field.json_name, field.type_)?; + writeln!( + out, + " {}?: {};", + format_property(field.json_name.clone()), + field.type_ + )?; } else { - writeln!(out, " {}: {};", field.json_name, field.type_)?; + writeln!( + out, + " {}: {};", + format_property(field.json_name.clone()), + field.type_ + )?; } } writeln!(out, "}}")?; @@ -253,6 +274,25 @@ impl jtd_codegen::target::Target for Target { } } +fn format_property(s: String) -> String { + // This implements a conservative subset of the set of allowable identifiers + // in JavaScript: + // + // https://tc39.es/ecma262/#sec-names-and-keywords + // + // If a property isn't an allowable identifier by these rules, then we + // escape the property. + lazy_static! { + static ref IDENTIFIER: Regex = Regex::new("^[a-zA-Z_$][a-zA-Z0-9_$]*$").unwrap(); + } + + if IDENTIFIER.is_match(&s) { + s + } else { + format!("{:?}", s) + } +} + pub fn description(metadata: &BTreeMap, indent: usize) -> String { doc(indent, jtd_codegen::target::metadata::description(metadata)) } @@ -283,4 +323,16 @@ mod tests { mod std_tests { jtd_codegen_test::std_test_cases!(&crate::Target::new()); } + + mod optional_std_tests { + jtd_codegen_test::strict_std_test_case!( + &crate::Target::new(), + empty_and_nonascii_properties + ); + + jtd_codegen_test::strict_std_test_case!( + &crate::Target::new(), + empty_and_nonascii_enum_values + ); + } } diff --git a/crates/test/schemas/roundtrip_strict/empty_and_nonascii_definitions.jtd.json b/crates/test/schemas/roundtrip_strict/empty_and_nonascii_definitions.jtd.json new file mode 100644 index 00000000..e530cf4c --- /dev/null +++ b/crates/test/schemas/roundtrip_strict/empty_and_nonascii_definitions.jtd.json @@ -0,0 +1,13 @@ +{ + "definitions": { + "": { "type": "string" }, + "$foo": { "type": "string" }, + "_foo": { "type": "string" }, + "0foo": { "type": "string" }, + "foo0bar": { "type": "string" }, + "foo bar": { "type": "string" }, + "foo\nbar": { "type": "string" }, + "foo\uFDFDbar": { "type": "string" } + }, + "type": "string" +} diff --git a/crates/test/schemas/roundtrip_strict/empty_and_nonascii_enum_values.jtd.json b/crates/test/schemas/roundtrip_strict/empty_and_nonascii_enum_values.jtd.json new file mode 100644 index 00000000..f00cd534 --- /dev/null +++ b/crates/test/schemas/roundtrip_strict/empty_and_nonascii_enum_values.jtd.json @@ -0,0 +1,12 @@ +{ + "enum": [ + "", + "$foo", + "_foo", + "0foo", + "foo0bar", + "foo bar", + "foo\nbar", + "foo\uFDFDbar" + ] +} diff --git a/crates/test/schemas/roundtrip_strict/empty_and_nonascii_properties.jtd.json b/crates/test/schemas/roundtrip_strict/empty_and_nonascii_properties.jtd.json new file mode 100644 index 00000000..d5313ed7 --- /dev/null +++ b/crates/test/schemas/roundtrip_strict/empty_and_nonascii_properties.jtd.json @@ -0,0 +1,12 @@ +{ + "properties": { + "": { "type": "string" }, + "$foo": { "type": "string" }, + "_foo": { "type": "string" }, + "0foo": { "type": "string" }, + "foo0bar": { "type": "string" }, + "foo bar": { "type": "string" }, + "foo\nbar": { "type": "string" }, + "foo\uFDFDbar": { "type": "string" } + } +} diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs index 9ee10889..73488056 100644 --- a/crates/test/src/lib.rs +++ b/crates/test/src/lib.rs @@ -22,6 +22,7 @@ macro_rules! std_test_cases { $crate::strict_std_test_case!($target, definition_name_collisions); $crate::strict_std_test_case!($target, description); $crate::strict_std_test_case!($target, elements); + $crate::strict_std_test_case!($target, empty_and_nonascii_definitions); $crate::strict_std_test_case!($target, enum_collisions); $crate::strict_std_test_case!($target, enum_variant_collisions); $crate::strict_std_test_case!($target, initialisms);