From 656c507a26c44c0973bfe311811700bcda11e7ec Mon Sep 17 00:00:00 2001 From: rooooooooob Date: Tue, 30 Jan 2024 09:43:05 -0800 Subject: [PATCH] Wrapper JSON Overhaul (#224) Wrapper JSON Overhaul Wrapper newtypes will generate custom implementations that will defer to the inner type's JSON traits instead of deriving to get them. This gives us much nicer JSON implementations allowing directly `T` instead of `{ inner: T }`. Bytes newtypes will have a specialization that serializes to hex bytestring instead of the `[number]` array that would otherwise be used. Introduces `@custom_json` comment DSL for newtypes that tell cddl-codegen to avoid generating/deriving any JSON traits under the assumption that some custom trait impls will be provided post-generation by the user. --- .gitignore | 1 + docs/docs/comment_dsl.mdx | 8 ++ src/comment_ast.rs | 44 ++++++- src/generation.rs | 259 ++++++++++++++++++++++++++++++++------ src/intermediate.rs | 3 + src/parsing.rs | 4 +- src/test.rs | 61 ++++++++- tests/external_json_impls | 40 ++++++ tests/json/input.cddl | 13 ++ tests/json/tests.rs | 92 ++++++++++++++ 10 files changed, 485 insertions(+), 40 deletions(-) create mode 100644 tests/external_json_impls create mode 100644 tests/json/input.cddl create mode 100644 tests/json/tests.rs diff --git a/.gitignore b/.gitignore index 596ac7e..28a6a61 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ book/ # Test files tests/*/export/** tests/*/export_wasm/** +tests/*/export_preserve/** diff --git a/docs/docs/comment_dsl.mdx b/docs/docs/comment_dsl.mdx index 137b033..2d40369 100644 --- a/docs/docs/comment_dsl.mdx +++ b/docs/docs/comment_dsl.mdx @@ -96,6 +96,14 @@ Putting this comment on a type forces that type to derive those traits even if i This is useful for when you are writing utility code that would put them in a map and want the generated code to have it already, which is particularly useful for re-generating as it lets your `mod.rs` files remain untouched. +## @custom_json + +```cddl +foo = uint ; @newtype @custom_json +``` + +Avoids generating and/or deriving json-related traits under the assumption that the user will supply their own implementation to be used in the generated library. + ## _CDDL_CODEGEN_EXTERN_TYPE_ While not as a comment, this allows you to compose in hand-written structs into a cddl spec. diff --git a/src/comment_ast.rs b/src/comment_ast.rs index a37fe60..aef250a 100644 --- a/src/comment_ast.rs +++ b/src/comment_ast.rs @@ -12,6 +12,7 @@ pub struct RuleMetadata { pub is_newtype: bool, pub no_alias: bool, pub used_as_key: bool, + pub custom_json: bool, } pub fn merge_metadata(r1: &RuleMetadata, r2: &RuleMetadata) -> RuleMetadata { @@ -26,6 +27,7 @@ pub fn merge_metadata(r1: &RuleMetadata, r2: &RuleMetadata) -> RuleMetadata { is_newtype: r1.is_newtype || r2.is_newtype, no_alias: r1.no_alias || r2.no_alias, used_as_key: r1.used_as_key || r2.used_as_key, + custom_json: r1.custom_json || r2.custom_json, }; merged.verify(); merged @@ -36,6 +38,7 @@ enum ParseResult { Name(String), DontGenAlias, UsedAsKey, + CustomJson, } impl RuleMetadata { @@ -61,6 +64,9 @@ impl RuleMetadata { ParseResult::UsedAsKey => { base.used_as_key = true; } + ParseResult::CustomJson => { + base.custom_json = true; + } } } base.verify(); @@ -101,9 +107,21 @@ fn tag_used_as_key(input: &str) -> IResult<&str, ParseResult> { Ok((input, ParseResult::UsedAsKey)) } +fn tag_custom_json(input: &str) -> IResult<&str, ParseResult> { + let (input, _) = tag("@custom_json")(input)?; + + Ok((input, ParseResult::CustomJson)) +} + fn whitespace_then_tag(input: &str) -> IResult<&str, ParseResult> { let (input, _) = take_while(char::is_whitespace)(input)?; - let (input, result) = alt((tag_name, tag_newtype, tag_no_alias, tag_used_as_key))(input)?; + let (input, result) = alt(( + tag_name, + tag_newtype, + tag_no_alias, + tag_used_as_key, + tag_custom_json, + ))(input)?; Ok((input, result)) } @@ -144,6 +162,7 @@ fn parse_comment_name() { is_newtype: false, no_alias: false, used_as_key: false, + custom_json: false, } )) ); @@ -160,6 +179,7 @@ fn parse_comment_newtype() { is_newtype: true, no_alias: false, used_as_key: false, + custom_json: false, } )) ); @@ -176,6 +196,7 @@ fn parse_comment_newtype_and_name() { is_newtype: true, no_alias: false, used_as_key: false, + custom_json: false, } )) ); @@ -192,6 +213,7 @@ fn parse_comment_newtype_and_name_and_used_as_key() { is_newtype: true, no_alias: false, used_as_key: true, + custom_json: false, } )) ); @@ -208,6 +230,7 @@ fn parse_comment_used_as_key() { is_newtype: false, no_alias: false, used_as_key: true, + custom_json: false, } )) ); @@ -224,6 +247,7 @@ fn parse_comment_newtype_and_name_inverse() { is_newtype: true, no_alias: false, used_as_key: false, + custom_json: false, } )) ); @@ -240,6 +264,24 @@ fn parse_comment_name_noalias() { is_newtype: false, no_alias: true, used_as_key: false, + custom_json: false, + } + )) + ); +} + +#[test] +fn parse_comment_newtype_and_custom_json() { + assert_eq!( + rule_metadata("@custom_json @newtype"), + Ok(( + "", + RuleMetadata { + name: None, + is_newtype: true, + no_alias: false, + used_as_key: false, + custom_json: true, } )) ); diff --git a/src/generation.rs b/src/generation.rs index ca27055..a78fc13 100644 --- a/src/generation.rs +++ b/src/generation.rs @@ -653,18 +653,29 @@ impl GenerationScope { rust_struct.tag(), cli, ), - RustStructType::Wrapper { wrapped, min_max } => match rust_struct.tag() { + RustStructType::Wrapper { + wrapped, + min_max, + custom_json, + } => match rust_struct.tag() { Some(tag) => generate_wrapper_struct( self, types, rust_ident, &wrapped.clone().tag(tag), *min_max, + *custom_json, + cli, + ), + None => generate_wrapper_struct( + self, + types, + rust_ident, + wrapped, + *min_max, + *custom_json, cli, ), - None => { - generate_wrapper_struct(self, types, rust_ident, wrapped, *min_max, cli) - } }, RustStructType::Extern => { #[allow(clippy::single_match)] @@ -828,10 +839,13 @@ impl GenerationScope { // import encoding structs (struct files) if cli.preserve_encodings { for (rust_ident, rust_struct) in types.rust_structs() { - if matches!( - rust_struct.variant(), - RustStructType::Record(_) | RustStructType::Wrapper { .. } - ) { + if match rust_struct.variant() { + RustStructType::Record(_) => true, + RustStructType::Wrapper { wrapped, .. } => { + !encoding_fields(types, rust_ident.as_ref(), wrapped, true, cli).is_empty() + } + _ => false, + } { // ALL records have an encoding struct since at minimum they contian // the array or map encoding details so no need to check fields self.rust(types, rust_ident).push_import( @@ -1147,7 +1161,25 @@ impl GenerationScope { if cli.json_schema_export { rust_cargo_toml.push_str("schemars = \"0.8.8\"\n"); } - if export_raw_bytes_encoding_trait { + if export_raw_bytes_encoding_trait + || types + .rust_structs() + .iter() + .any(|(_, rust_struct)| match &rust_struct.variant { + RustStructType::Wrapper { + wrapped, + custom_json, + .. + } => { + !custom_json + && matches!( + wrapped.resolve_alias_shallow(), + ConceptualRustType::Primitive(Primitive::Bytes) + ) + } + _ => false, + }) + { rust_cargo_toml.push_str("hex = \"0.4.3\"\n"); } if cli.wasm @@ -3219,7 +3251,7 @@ fn canonical_param(cli: &Cli) -> &'static str { /// the codegen crate doesn't support proc macros for fields so we need to /// do this with newlines. codegen takes care of indentation somehow. -fn encoding_var_macros(used_in_key: bool, cli: &Cli) -> String { +fn encoding_var_macros(used_in_key: bool, custom_json: bool, cli: &Cli) -> String { let mut ret = if used_in_key { format!( "#[derivative({})]\n", @@ -3232,7 +3264,7 @@ fn encoding_var_macros(used_in_key: bool, cli: &Cli) -> String { } else { String::new() }; - if cli.json_serde_derives { + if cli.json_serde_derives && !custom_json { ret.push_str("#[serde(skip)]\n"); } ret @@ -3513,11 +3545,18 @@ impl DataType for codegen::Enum { fn create_base_rust_struct( types: &IntermediateTypes<'_>, ident: &RustIdent, + manual_json_impl: bool, cli: &Cli, ) -> (codegen::Struct, codegen::Impl) { let name = &ident.to_string(); let mut s = codegen::Struct::new(name); - add_struct_derives(&mut s, types.used_as_key(ident), false, cli); + add_struct_derives( + &mut s, + types.used_as_key(ident), + false, + manual_json_impl, + cli, + ); let group_impl = codegen::Impl::new(name); // TODO: anything here? (s, group_impl) @@ -4984,7 +5023,7 @@ fn codegen_struct( // Rust-only for the rest of this function // Struct (fields) + constructor - let (mut native_struct, mut native_impl) = create_base_rust_struct(types, name, cli); + let (mut native_struct, mut native_impl) = create_base_rust_struct(types, name, false, cli); native_struct.vis("pub"); let mut native_new = codegen::Function::new("new"); let (ctor_ret, ctor_before) = if new_can_fail { @@ -5070,7 +5109,7 @@ fn codegen_struct( native_struct.field( &format!( "{}pub encodings", - encoding_var_macros(types.used_as_key(name), cli) + encoding_var_macros(types.used_as_key(name), false, cli) ), format!("Option<{encoding_name}>"), ); @@ -6189,7 +6228,7 @@ impl EnumVariantInRust { // the codeen crate doesn't support proc macros on fields but we just inline // these with a newline in the field names for declaring as workaround. // Indentation is never an issue as we're always 2 levels deep for field declarations - format!("{}{}", encoding_var_macros(used_in_key, cli), name) + format!("{}{}", encoding_var_macros(used_in_key, false, cli), name) } }) .collect() @@ -6306,7 +6345,7 @@ fn generate_c_style_enum( ) .vis("pub"); } - add_struct_derives(&mut e, types.used_as_key(name), true, cli); + add_struct_derives(&mut e, types.used_as_key(name), true, false, cli); for variant in variants.iter() { e.new_variant(variant.name.to_string()); } @@ -6467,7 +6506,7 @@ fn generate_enum( // instead of using create_serialize_impl() and having the length encoded there, we want to make it easier // to offer definite length encoding even if we're mixing plain group members and non-plain group members (or mixed length plain ones) // by potentially wrapping the choices with the array/map tag in the variant branch when applicable - add_struct_derives(&mut e, types.used_as_key(name), true, cli); + add_struct_derives(&mut e, types.used_as_key(name), true, false, cli); let mut ser_impl = make_serialization_impl(name.as_ref(), cli); let mut ser_func = make_serialization_function("serialize", cli); if let Some(tag) = tag { @@ -7066,6 +7105,7 @@ fn generate_wrapper_struct( type_name: &RustIdent, field_type: &RustType, min_max: Option<(Option, Option)>, + custom_json: bool, cli: &Cli, ) { if min_max.is_some() { @@ -7103,7 +7143,136 @@ fn generate_wrapper_struct( // TODO: do we want to get rid of the rust struct and embed the tag / min/max size here? // The tag is easy but the min/max size would require error types in any place that sets/modifies these in other structs. - let (mut s, mut s_impl) = create_base_rust_struct(types, type_name, cli); + let (mut s, mut s_impl) = create_base_rust_struct(types, type_name, true, cli); + let (inner_var, self_var) = if cli.preserve_encodings { + ("inner", "self.inner") + } else { + ("0", "self.0") + }; + + // manual JSON impls + let mut serde_ser_impl = codegen::Impl::new(type_name); + let mut serde_deser_impl = codegen::Impl::new(type_name); + let mut json_schema_impl = codegen::Impl::new(type_name); + let json_hex_bytes = matches!( + field_type.resolve_alias_shallow(), + ConceptualRustType::Primitive(Primitive::Bytes) + ); + let json_schema_type = if json_hex_bytes { + Cow::Borrowed("String") + } else { + Cow::Owned(field_type.for_rust_member(types, false, cli)) + }; + + if !custom_json { + // serde Serialize / Deserialize + if cli.json_serde_derives { + let mut serde_ser_fn = codegen::Function::new("serialize"); + serde_ser_fn + .generic("S") + .bound("S", "serde::Serializer") + .arg_ref_self() + .arg("serializer", "S") + .ret("Result"); + let mut serde_deser_fn = codegen::Function::new("deserialize"); + serde_deser_fn + .generic("D") + .bound("D", "serde::de::Deserializer<'de>") + .arg("deserializer", "D") + .ret("Result"); + if json_hex_bytes { + serde_ser_fn.line(format!( + "serializer.serialize_str(&hex::encode({self_var}.clone()))" + )); + let err_body = "{ serde::de::Error::invalid_value(serde::de::Unexpected::Str(&s), &\"invalid hex bytes\") }"; + serde_deser_fn + .line("let s = ::deserialize(deserializer)?;") + .line("hex::decode(&s)"); + if types.can_new_fail(type_name) { + serde_deser_fn + .line(format!( + ".ok().and_then(|bytes| {type_name}::new(bytes).ok())" + )) + .line(format!(".ok_or_else(|| {err_body})")); + } else { + serde_deser_fn + .line(format!(".map({type_name}::new)")) + .line(format!(".map_err(|_e| {err_body})")); + } + } else { + serde_ser_fn.line(format!("{self_var}.serialize(serializer)")); + serde_deser_fn + .line(format!("let inner = <{json_schema_type} as serde::de::Deserialize>::deserialize(deserializer)?;")); + if types.can_new_fail(type_name) { + let unexpected = match field_type.resolve_alias_shallow() { + ConceptualRustType::Alias(_, _) => unreachable!(), + ConceptualRustType::Array(_) => "Seq", + ConceptualRustType::Fixed(fixed) => match fixed { + FixedValue::Bool(_) => "Bool(inner)", + FixedValue::Float(_) => "Float(inner)", + FixedValue::Nint(_) => "Signed(inner as i64)", + FixedValue::Null => "Option", + FixedValue::Text(_) => "Str(&inner)", + FixedValue::Uint(_) => "Unsigned(inner)", + }, + ConceptualRustType::Map(_, _) => "Map", + ConceptualRustType::Optional(_) => "Option", + ConceptualRustType::Primitive(p) => match p { + Primitive::Bool => "Bool(inner)", + Primitive::Bytes => "Bytes(&inner)", + Primitive::F32 => "Float(inner as f64)", + Primitive::F64 => "Float(inner)", + Primitive::I8 + | Primitive::I16 + | Primitive::I32 + | Primitive::I64 + | Primitive::N64 => "Signed(inner as i64)", + Primitive::Str => "Str(&inner)", + Primitive::U8 | Primitive::U16 | Primitive::U32 => { + "Unsigned(inner as u64)" + } + Primitive::U64 => "Unsigned(inner)", + }, + ConceptualRustType::Rust(_) => "StructVariant", + }; + serde_deser_fn + .line("Self::new(inner)") + .line(format!(".map_err(|_e| {{ serde::de::Error::invalid_value(serde::de::Unexpected::{unexpected}, &\"invalid {type_name}\") }})")); + } else { + serde_deser_fn.line("Ok(Self::new(inner))"); + } + } + serde_ser_impl + .impl_trait("serde::Serialize") + .push_fn(serde_ser_fn); + serde_deser_impl + .impl_trait("serde::de::Deserialize<'de>") + .generic("'de") + .push_fn(serde_deser_fn); + } + + // JsonSchema + if cli.json_schema_export { + let mut schema_name_fn = codegen::Function::new("schema_name"); + schema_name_fn + .ret("String") + .line(format!("String::from(\"{type_name}\")")); + let mut json_schema_fn = codegen::Function::new("json_schema"); + json_schema_fn + .arg("gen", "&mut schemars::gen::SchemaGenerator") + .ret("schemars::schema::Schema") + .line(format!("{json_schema_type}::json_schema(gen)")); + let mut is_referenceable = codegen::Function::new("is_referenceable"); + is_referenceable + .ret("bool") + .line(format!("{json_schema_type}::is_referenceable()")); + json_schema_impl + .impl_trait("schemars::JsonSchema") + .push_fn(schema_name_fn) + .push_fn(json_schema_fn) + .push_fn(is_referenceable); + } + } s.vis("pub"); let encoding_name = RustIdent::new(CDDLIdent::new(format!("{type_name}Encoding"))); let enc_fields = if cli.preserve_encodings { @@ -7120,7 +7289,7 @@ fn generate_wrapper_struct( s.field( &format!( "{}pub encodings", - encoding_var_macros(types.used_as_key(type_name), cli) + encoding_var_macros(types.used_as_key(type_name), true, cli) ), format!("Option<{encoding_name}>"), ); @@ -7147,11 +7316,6 @@ fn generate_wrapper_struct( if field_type.is_copy(types) && !cli.preserve_encodings { s.derive("Copy"); } - let (inner_var, self_var) = if cli.preserve_encodings { - ("inner", "self.inner") - } else { - ("0", "self.0") - }; let mut get = codegen::Function::new("get"); get.vis("pub").arg_ref_self(); if field_type.is_copy(types) { @@ -7419,6 +7583,17 @@ fn generate_wrapper_struct( .push_impl(s_impl) .push_impl(from_impl) .push_impl(from_inner_impl); + if !custom_json { + if cli.json_serde_derives { + gen_scope + .rust(types, type_name) + .push_impl(serde_ser_impl) + .push_impl(serde_deser_impl); + } + if cli.json_schema_export { + gen_scope.rust(types, type_name).push_impl(json_schema_impl); + } + } gen_scope .rust_serialize(types, type_name) .push_impl(ser_impl) @@ -7441,15 +7616,23 @@ fn key_derives(for_ignore: bool, cli: &Cli) -> &'static [&'static str] { } } -fn add_struct_derives(data_type: &mut T, used_in_key: bool, is_enum: bool, cli: &Cli) { +fn add_struct_derives( + data_type: &mut T, + used_in_key: bool, + is_enum: bool, + custom_json: bool, + cli: &Cli, +) { data_type.derive("Clone").derive("Debug"); - if cli.json_serde_derives { - data_type - .derive("serde::Deserialize") - .derive("serde::Serialize"); - } - if cli.json_schema_export { - data_type.derive("schemars::JsonSchema"); + if !custom_json { + if cli.json_serde_derives { + data_type + .derive("serde::Deserialize") + .derive("serde::Serialize"); + } + if cli.json_schema_export { + data_type.derive("schemars::JsonSchema"); + } } if used_in_key { if cli.preserve_encodings { @@ -7530,14 +7713,14 @@ fn generate_int(gen_scope: &mut GenerationScope, types: &IntermediateTypes, cli: uint.named("value", "u64").named( &format!( "{}encoding", - encoding_var_macros(types.used_as_key(&ident), cli) + encoding_var_macros(types.used_as_key(&ident), true, cli) ), "Option", ); nint.named("value", "u64").named( &format!( "{}encoding", - encoding_var_macros(types.used_as_key(&ident), cli) + encoding_var_macros(types.used_as_key(&ident), true, cli) ), "Option", ); @@ -7547,7 +7730,13 @@ fn generate_int(gen_scope: &mut GenerationScope, types: &IntermediateTypes, cli: } native_struct.push_variant(uint); native_struct.push_variant(nint); - add_struct_derives(&mut native_struct, types.used_as_key(&ident), true, cli); + add_struct_derives( + &mut native_struct, + types.used_as_key(&ident), + true, + true, + cli, + ); // impl Int let mut native_impl = codegen::Impl::new("Int"); diff --git a/src/intermediate.rs b/src/intermediate.rs index 1c30060..4b59ba8 100644 --- a/src/intermediate.rs +++ b/src/intermediate.rs @@ -2167,6 +2167,7 @@ pub enum RustStructType { Wrapper { wrapped: RustType, min_max: Option<(Option, Option)>, + custom_json: bool, }, /// This is a no-op in generation but to prevent lookups of things in the prelude /// e.g. `int` from not being resolved while still being able to detect it when @@ -2263,6 +2264,7 @@ impl RustStruct { tag: Option, wrapped_type: RustType, min_max: Option<(Option, Option)>, + custom_json: bool, ) -> Self { Self { ident, @@ -2270,6 +2272,7 @@ impl RustStruct { variant: RustStructType::Wrapper { wrapped: wrapped_type, min_max, + custom_json, }, } } diff --git a/src/parsing.rs b/src/parsing.rs index 13aaa32..1f8b00b 100644 --- a/src/parsing.rs +++ b/src/parsing.rs @@ -41,7 +41,7 @@ pub fn rule_is_scope_marker(cddl_rule: &cddl::ast::Rule) -> Option if value.type_choices.len() == 1 && ident.starts_with(SCOPE_MARKER) { match &value.type_choices[0].type1.type2 { Type2::TextValue { value, .. } => Some(ModuleScope::new( - value.to_string().split("::").map(String::from).collect(), + value.as_ref().split("::").map(String::from).collect(), )), _ => None, } @@ -525,6 +525,7 @@ fn parse_type( outer_tag, ranged_type, Some(min_max), + rule_metadata.custom_json, ), cli, ); @@ -613,6 +614,7 @@ fn parse_type( None, concrete_type, None, + rule_metadata.custom_json, ), cli, ); diff --git a/src/test.rs b/src/test.rs index 72cbe7b..a94d2dc 100644 --- a/src/test.rs +++ b/src/test.rs @@ -9,6 +9,7 @@ fn run_test( external_rust_file_path: Option, external_wasm_file_path: Option, input_is_dir: bool, + test_deps: &[&str], ) { use std::str::FromStr; let export_path = match export_suffix { @@ -45,7 +46,6 @@ fn run_test( assert!(cargo_run_result.status.success()); // copy tests into generated code let mut lib_rs = std::fs::OpenOptions::new() - .write(true) .append(true) .open(test_path.join(format!("{export_path}/rust/src/lib.rs"))) .unwrap(); @@ -71,6 +71,15 @@ fn run_test( lib_rs.write_all("\n\n".as_bytes()).unwrap(); lib_rs.write_all(test_rs.as_bytes()).unwrap(); std::mem::drop(lib_rs); + // add extra deps used within tests + let mut cargo_toml = std::fs::OpenOptions::new() + .append(true) + .open(test_path.join(format!("{export_path}/rust/Cargo.toml"))) + .unwrap(); + for dep in test_deps { + cargo_toml.write_all(dep.as_bytes()).unwrap(); + } + std::mem::drop(cargo_toml); // run tests in generated code println!(" ------ testing ------"); let cargo_test = std::process::Command::new("cargo") @@ -97,7 +106,6 @@ fn run_test( if let Some(external_wasm_file_path) = external_wasm_file_path { println!("trying to open: {external_wasm_file_path:?}"); let mut wasm_lib_rs = std::fs::OpenOptions::new() - .write(true) .append(true) .open(test_path.join(format!("{export_path}/wasm/src/lib.rs"))) .unwrap(); @@ -182,6 +190,7 @@ fn core_with_wasm() { Some(extern_rust_path), Some(extern_wasm_path), false, + &[], ); } @@ -198,6 +207,7 @@ fn core_no_wasm() { Some(extern_rust_path), None, false, + &[], ); } @@ -210,6 +220,7 @@ fn comment_dsl() { None, None, false, + &[], ); } @@ -222,6 +233,7 @@ fn preserve_encodings() { None, None, false, + &[], ); } @@ -234,12 +246,13 @@ fn canonical() { None, None, false, + &[], ); } #[test] fn rust_wasm_split() { - run_test("rust-wasm-split", &[], None, None, None, false); + run_test("rust-wasm-split", &[], None, None, None, false, &[]); } #[test] @@ -259,6 +272,7 @@ fn multifile() { Some(extern_rust_path), Some(extern_wasm_path), true, + &[], ); } @@ -286,6 +300,7 @@ fn multifile_json_preserve() { Some(extern_rust_path), Some(extern_wasm_path), true, + &[], ); } @@ -305,6 +320,7 @@ fn raw_bytes() { Some(extern_rust_path), Some(extern_wasm_path), false, + &[], ); } @@ -324,5 +340,44 @@ fn raw_bytes_preserve() { Some(extern_rust_path), Some(extern_wasm_path), false, + &[], + ); +} + +#[test] +fn json() { + use std::str::FromStr; + let extern_rust_path = std::path::PathBuf::from_str("tests") + .unwrap() + .join("external_json_impls"); + run_test( + "json", + &["--json-serde-derives=true", "--json-schema-export=true"], + None, + Some(extern_rust_path), + None, + false, + &[], + ); +} + +#[test] +fn json_preserve() { + use std::str::FromStr; + let extern_rust_path = std::path::PathBuf::from_str("tests") + .unwrap() + .join("external_json_impls"); + run_test( + "json", + &[ + "--preserve-encodings=true", + "--json-serde-derives=true", + "--json-schema-export=true", + ], + Some("preserve"), + Some(extern_rust_path), + None, + false, + &[], ); } diff --git a/tests/external_json_impls b/tests/external_json_impls new file mode 100644 index 0000000..45ee0ab --- /dev/null +++ b/tests/external_json_impls @@ -0,0 +1,40 @@ +impl serde::Serialize for CustomWrapper { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&u64::from(self.clone()).to_string()) + } +} + +impl<'de> serde::de::Deserialize<'de> for CustomWrapper { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + use std::str::FromStr; + let s = ::deserialize(deserializer)?; + u64::from_str(&s) + .map(CustomWrapper::new) + .map_err(|_e| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Str(&s), + &"invalid u64 as string", + ) + }) + } +} + +impl schemars::JsonSchema for CustomWrapper { + fn schema_name() -> String { + String::from("CustomWrapper") + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + String::json_schema(gen) + } + + fn is_referenceable() -> bool { + String::is_referenceable() + } +} \ No newline at end of file diff --git a/tests/json/input.cddl b/tests/json/input.cddl new file mode 100644 index 0000000..1a268c5 --- /dev/null +++ b/tests/json/input.cddl @@ -0,0 +1,13 @@ +bytes_wrapper = bytes ; @newtype +str_wrapper = text ; @newtype +u8_wrapper = uint .lt 256 ; @newtype +u64_wrapper = uint ; @newtype +i16_wrapper = int .size 2 ; @newtype +i64_wrapper = int .size 8 ; @newtype +nint_wrapper = nint ; @newtype + +; TODO: issue: https://github.com/dcSpark/cddl-codegen/issues/223 +; bool_wrapper = bool ; @newtype + +struct_wrapper = u64_wrapper ; @newtype +custom_wrapper = uint ; @newtype @custom_json \ No newline at end of file diff --git a/tests/json/tests.rs b/tests/json/tests.rs new file mode 100644 index 0000000..8afd25e --- /dev/null +++ b/tests/json/tests.rs @@ -0,0 +1,92 @@ +#[cfg(test)] +mod tests { + use super::*; + use cbor_event::de::Deserializer; + use serialization::Deserialize; + + #[test] + fn bytes_wrapper() { + let bytes = vec![0xBA, 0xAD, 0xF0, 0x0D]; + let hex = format!("\"{}\"", hex::encode(&bytes)); + let from_bytes = BytesWrapper::new(bytes.clone()); + let from_hex: BytesWrapper = serde_json::from_str(&hex).unwrap(); + assert_eq!(hex, serde_json::to_string_pretty(&from_bytes).unwrap()); + assert_eq!(hex, serde_json::to_string_pretty(&from_hex).unwrap()); + } + + #[test] + fn str_wrapper() { + let text = "hello, world"; + let json_str = format!("\"{text}\""); + let from_str = StrWrapper::new(text.to_owned()); + let from_json: StrWrapper = serde_json::from_str(&json_str).unwrap(); + assert_eq!(json_str, serde_json::to_string_pretty(&from_str).unwrap()); + assert_eq!(json_str, serde_json::to_string_pretty(&from_json).unwrap()); + } + + fn json_wrapper_test(value: V) + where W: TryFrom + serde::Serialize + for <'de> serde::Deserialize<'de>, + V: std::fmt::Display, + >::Error: std::fmt::Debug + { + let json_str = value.to_string(); + let from_value = W::try_from(value).unwrap(); + let from_json: W = serde_json::from_str(&json_str).unwrap(); + assert_eq!(json_str, serde_json::to_string_pretty(&from_value).unwrap()); + assert_eq!(json_str, serde_json::to_string_pretty(&from_json).unwrap()); + } + + #[test] + fn u8_wrapper() { + json_wrapper_test::(u8::MIN); + json_wrapper_test::(u8::MAX); + } + + #[test] + fn u64_wrapper() { + json_wrapper_test::(u64::MIN); + json_wrapper_test::(u64::MAX); + } + + #[test] + fn i16_wrapper() { + json_wrapper_test::(i16::MIN); + json_wrapper_test::(i16::MAX); + } + + #[test] + fn i64_wrapper() { + json_wrapper_test::(i64::MIN); + json_wrapper_test::(i64::MAX); + } + + #[test] + fn nint_wrapper() { + json_wrapper_test::(u64::MIN); + json_wrapper_test::(u64::MAX); + } + + // #[test] + // fn bool_wrapper() { + // json_wrapper_test::(false); + // json_wrapper_test::(true); + // } + + #[test] + fn struct_wrapper() { + let json_str = u64::MAX.to_string(); + let from_value = StructWrapper::from(U64Wrapper::from(u64::MAX)); + let from_json: StructWrapper = serde_json::from_str(&json_str).unwrap(); + assert_eq!(json_str, serde_json::to_string_pretty(&from_value).unwrap()); + assert_eq!(json_str, serde_json::to_string_pretty(&from_json).unwrap()); + } + + #[test] + fn custom_wrapper() { + let json_str = "\"1234\""; + let from_value = CustomWrapper::from(1234u64); + let from_json: CustomWrapper = serde_json::from_str(&json_str).unwrap(); + assert_eq!(json_str, serde_json::to_string_pretty(&from_value).unwrap()); + assert_eq!(json_str, serde_json::to_string_pretty(&from_json).unwrap()); + } +}