diff --git a/strong-xml-derive/src/read/mod.rs b/strong-xml-derive/src/read/mod.rs index 9e8103a..f7911b0 100644 --- a/strong-xml-derive/src/read/mod.rs +++ b/strong-xml-derive/src/read/mod.rs @@ -18,9 +18,12 @@ pub fn impl_read(element: Element) -> TokenStream { }); let read = variants.iter().map(|variant| match variant { - Fields::Named { tag, name, fields } => { - named::read(&tag, quote!(#ele_name::#name), &fields) - } + Fields::Named { + tag, + name, + fields, + namespaces: _, + } => named::read(&tag, quote!(#ele_name::#name), &fields), Fields::Newtype { name, ty, .. } => newtype::read(&ty, quote!(#ele_name::#name)), }); @@ -42,7 +45,12 @@ pub fn impl_read(element: Element) -> TokenStream { } Element::Struct { fields, .. } => match fields { - Fields::Named { tag, name, fields } => named::read(&tag, quote!(#name), &fields), + Fields::Named { + tag, + name, + fields, + namespaces: _, + } => named::read(&tag, quote!(#name), &fields), Fields::Newtype { name, ty, .. } => newtype::read(&ty, quote!(#name)), }, } diff --git a/strong-xml-derive/src/read/named.rs b/strong-xml-derive/src/read/named.rs index 59bf50f..9d34556 100644 --- a/strong-xml-derive/src/read/named.rs +++ b/strong-xml-derive/src/read/named.rs @@ -1,10 +1,10 @@ use proc_macro2::TokenStream; use quote::quote; -use syn::{Ident, LitStr}; +use syn::Ident; -use crate::types::{Field, Type}; +use crate::types::{Field, QName, Type}; -pub fn read(tag: &LitStr, ele_name: TokenStream, fields: &[Field]) -> TokenStream { +pub fn read(tag: &QName, ele_name: TokenStream, fields: &[Field]) -> TokenStream { let init_fields = fields.iter().map(|field| match field { Field::Attribute { bind, ty, .. } | Field::Child { bind, ty, .. } @@ -167,7 +167,7 @@ fn return_value( } fn read_attrs( - tag: &LitStr, + tag: &QName, bind: &Ident, name: &TokenStream, ty: &Type, @@ -191,7 +191,7 @@ fn read_attrs( } fn read_text( - tag: &LitStr, + tag: &QName, bind: &Ident, name: &TokenStream, ty: &Type, @@ -214,7 +214,7 @@ fn read_text( } fn read_children( - tags: &[LitStr], + tags: &[QName], bind: &Ident, name: &TokenStream, ty: &Type, @@ -242,7 +242,7 @@ fn read_children( } fn read_flatten_text( - tag: &LitStr, + tag: &QName, bind: &Ident, name: &TokenStream, ty: &Type, diff --git a/strong-xml-derive/src/types.rs b/strong-xml-derive/src/types.rs index 0252f9e..6ae35cb 100644 --- a/strong-xml-derive/src/types.rs +++ b/strong-xml-derive/src/types.rs @@ -1,5 +1,6 @@ use proc_macro2::TokenStream; -use quote::{format_ident, quote}; +use quote::{format_ident, quote, ToTokens}; +use std::collections::BTreeMap; use syn::{Lit::*, Meta::*, *}; use crate::utils::elide_type_lifetimes; @@ -28,9 +29,10 @@ pub enum Fields { /// } /// ``` Named { - tag: LitStr, + tag: QName, name: Ident, fields: Vec, + namespaces: NamespaceDefs, }, /// Newtype struct or newtype variant /// @@ -46,9 +48,10 @@ pub enum Fields { /// } /// ``` Newtype { - tags: Vec, + tags: Vec, name: Ident, ty: Type, + namespaces: NamespaceDefs, }, } @@ -65,7 +68,7 @@ pub enum Field { name: TokenStream, bind: Ident, ty: Type, - tag: LitStr, + tag: QName, default: bool, }, /// Child(ren) Field @@ -81,7 +84,8 @@ pub enum Field { bind: Ident, ty: Type, default: bool, - tags: Vec, + tags: Vec, + namespaces: NamespaceDefs, }, /// Text Field /// @@ -110,7 +114,7 @@ pub enum Field { bind: Ident, ty: Type, default: bool, - tag: LitStr, + tag: QName, is_cdata: bool, }, } @@ -136,6 +140,19 @@ pub enum Type { OptionBool, } +#[derive(Clone)] +pub enum QName { + Prefixed(LitStr), + Unprefixed(LitStr), +} + +pub struct NamespaceDef { + prefix: Option, + namespace: String, +} + +pub type NamespaceDefs = BTreeMap, NamespaceDef>; + impl Element { pub fn parse(input: DeriveInput) -> Element { match input.data { @@ -160,16 +177,54 @@ impl Fields { pub fn parse(fields: syn::Fields, attrs: Vec, name: Ident) -> Fields { // Finding `tag` attribute let mut tags = Vec::new(); + let mut namespaces: NamespaceDefs = BTreeMap::default(); for meta in attrs.into_iter().filter_map(get_xml_meta).flatten() { match meta { NestedMeta::Meta(NameValue(m)) if m.path.is_ident("tag") => { if let Str(lit) = m.lit { - tags.push(lit); + match QName::parse(lit) { + Ok(q) => tags.push(q), + Err(e) => panic!("{}", e), + } } else { panic!("Expected a string literal."); } } + NestedMeta::Meta(NameValue(MetaNameValue { lit, path, .. })) + if path.is_ident("ns") => + { + let (prefix, namespace) = if let Str(lit) = lit { + if let Some((pfx, ns)) = lit.value().split_once(": ") { + (Some(pfx.to_string()), ns.to_string()) + } else { + (None, lit.value().to_string()) + } + } else { + panic!("Expected a string literal."); + }; + + if namespaces.contains_key(&prefix) { + if let Some(ref prefix) = prefix { + panic!("namespace {} already defined", prefix); + } else { + panic!("default namespace already defined"); + }; + } + + if let Some(prefix) = &prefix { + if prefix.contains(":") { + panic!("prefix cannot contain `:`"); + } + + if prefix == "xml" && namespace != "http://www.w3.org/XML/1998/namespace" { + panic!("xml prefix can only be bound to http://www.w3.org/XML/1998/namespace"); + } else if prefix.starts_with("xml") { + panic!("prefix cannot start with `xml`"); + } + } + namespaces.insert(prefix.clone(), NamespaceDef { prefix, namespace }); + } _ => (), } } @@ -182,6 +237,7 @@ impl Fields { syn::Fields::Unit => Fields::Named { name, tag: tags.remove(0), + namespaces, fields: Vec::new(), }, syn::Fields::Unnamed(fields) => { @@ -194,6 +250,7 @@ impl Fields { name, tags, ty: Type::parse(field.ty), + namespaces, }; } } @@ -201,6 +258,7 @@ impl Fields { Fields::Named { name, tag: tags.remove(0), + namespaces, fields: fields .unnamed .into_iter() @@ -216,6 +274,7 @@ impl Fields { syn::Fields::Named(_) => Fields::Named { name, tag: tags.remove(0), + namespaces, fields: fields .into_iter() .map(|field| { @@ -260,7 +319,10 @@ impl Field { } else if flatten_text_tag.is_some() { panic!("`attr` attribute and `flatten_text` attribute is disjoint."); } else { - attr_tag = Some(lit); + match QName::parse(lit) { + Ok(q) => attr_tag = Some(q), + Err(e) => panic!("{}", e), + } } } else { panic!("Expected a string literal."); @@ -301,7 +363,10 @@ impl Field { } else if flatten_text_tag.is_some() { panic!("`child` attribute and `flatten_text` attribute is disjoint."); } else { - child_tags.push(lit); + match QName::parse(lit) { + Ok(q) => child_tags.push(q), + Err(e) => panic!("{}", e), + } } } else { panic!("Expected a string literal."); @@ -318,12 +383,23 @@ impl Field { } else if flatten_text_tag.is_some() { panic!("Duplicate `flatten_text` attribute."); } else { - flatten_text_tag = Some(lit); + match QName::parse(lit) { + Ok(q) => flatten_text_tag = Some(q), + Err(e) => panic!("{}", e), + } } } else { panic!("Expected a string literal."); } } + NestedMeta::Meta(NameValue(m)) + if m.path + .get_ident() + .filter(|ident| ident.to_string().starts_with("ns")) + .is_some() => + { + panic!("Namespace declaration not supported in this position"); + } _ => (), } } @@ -343,6 +419,7 @@ impl Field { ty: Type::parse(field.ty), default, tags: child_tags, + namespaces: NamespaceDefs::new(), } } else if is_text { Field::Text { @@ -473,6 +550,69 @@ impl Type { } } } +use std::io::{Error, ErrorKind, Result}; + +impl QName { + pub fn parse(name: LitStr) -> Result { + let space_count = name.value().matches(' ').count(); + if space_count > 0 { + return Err(Error::new(ErrorKind::Other, "QName can not contain spaces")); + } + + let colon_count = name.value().matches(':').count(); + match colon_count { + 0 => Ok(QName::Unprefixed(name)), + 1 => Ok(QName::Prefixed(name)), + _ => Err(Error::new( + ErrorKind::Other, + "QName can only have a max of 1 colon", + )), + } + } + + pub fn prefix(&self) -> Option { + match self { + Self::Prefixed(name) => name + .value() + .split_once(":") + .map(|(prefix, _)| prefix.to_owned()), + Self::Unprefixed(_) => None, + } + } + + pub fn local(&self) -> String { + match self { + Self::Prefixed(name) => name.value().split_once(":").unwrap().1.to_owned(), + Self::Unprefixed(name) => name.value(), + } + } +} + +impl ToString for QName { + fn to_string(&self) -> String { + match self { + QName::Prefixed(tag) | QName::Unprefixed(tag) => tag.value(), + } + } +} + +impl ToTokens for QName { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + QName::Prefixed(tag) | QName::Unprefixed(tag) => tag.to_tokens(tokens), + } + } +} + +impl NamespaceDef { + pub fn prefix(&self) -> Option<&str> { + self.prefix.as_deref() + } + + pub fn namespace(&self) -> &str { + &self.namespace + } +} fn get_xml_meta(attr: Attribute) -> Option> { if attr.path.segments.len() == 1 && attr.path.segments[0].ident == "xml" { diff --git a/strong-xml-derive/src/write/mod.rs b/strong-xml-derive/src/write/mod.rs index 7407b31..b4df629 100644 --- a/strong-xml-derive/src/write/mod.rs +++ b/strong-xml-derive/src/write/mod.rs @@ -26,9 +26,12 @@ pub fn impl_write(element: Element) -> TokenStream { }); let read = variants.iter().map(|variant| match variant { - Fields::Named { tag, name, fields } => { - named::write(&tag, quote!( #ele_name::#name ), &fields) - } + Fields::Named { + tag, + name, + fields, + namespaces, + } => named::write(tag, quote!( #ele_name::#name ), &fields, &namespaces), Fields::Newtype { name, .. } => newtype::write(quote!( #ele_name::#name )), }); @@ -43,7 +46,12 @@ pub fn impl_write(element: Element) -> TokenStream { name: ele_name, fields, } => match fields { - Fields::Named { tag, name, fields } => { + Fields::Named { + tag, + name, + fields, + namespaces, + } => { let bindings = fields.iter().map(|field| match field { Field::Attribute { bind, name, .. } | Field::Child { bind, name, .. } @@ -51,7 +59,7 @@ pub fn impl_write(element: Element) -> TokenStream { | Field::FlattenText { bind, name, .. } => quote!( #name: #bind ), }); - let read = named::write(&tag, quote!(#name), &fields); + let read = named::write(&tag, quote!(#name), &fields, &namespaces); quote! { let #ele_name { #( #bindings ),* } = self; diff --git a/strong-xml-derive/src/write/named.rs b/strong-xml-derive/src/write/named.rs index 9d9fa00..dee21d2 100644 --- a/strong-xml-derive/src/write/named.rs +++ b/strong-xml-derive/src/write/named.rs @@ -1,10 +1,17 @@ use proc_macro2::TokenStream; use quote::quote; -use syn::{Ident, LitStr}; +use syn::Ident; -use crate::types::{Field, Type}; +use crate::types::{Field, NamespaceDefs, QName, Type}; + +pub fn write( + tag: &QName, + ele_name: TokenStream, + fields: &[Field], + namespaces: &NamespaceDefs, +) -> TokenStream { + let write_namespaces = write_namespaces(namespaces); -pub fn write(tag: &LitStr, ele_name: TokenStream, fields: &[Field]) -> TokenStream { let write_attributes = fields.iter().filter_map(|field| match field { Field::Attribute { tag, bind, ty, .. } => Some(write_attrs(&tag, &bind, &ty, &ele_name)), _ => None, @@ -81,6 +88,7 @@ pub fn write(tag: &LitStr, ele_name: TokenStream, fields: &[Field]) -> TokenStre writer.write_element_start(#tag)?; + #write_namespaces #( #write_attributes )* #write_element_end @@ -89,7 +97,7 @@ pub fn write(tag: &LitStr, ele_name: TokenStream, fields: &[Field]) -> TokenStre } } -fn write_attrs(tag: &LitStr, name: &Ident, ty: &Type, ele_name: &TokenStream) -> TokenStream { +fn write_attrs(tag: &QName, name: &Ident, ty: &Type, ele_name: &TokenStream) -> TokenStream { let to_str = to_str(ty); if ty.is_vec() { @@ -116,6 +124,25 @@ fn write_attrs(tag: &LitStr, name: &Ident, ty: &Type, ele_name: &TokenStream) -> } } +fn write_namespaces(namespaces: &NamespaceDefs) -> TokenStream { + namespaces + .values() + .map(|namespace_def| { + let namespace = namespace_def.namespace(); + + let prefix = if let Some(prefix) = namespace_def.prefix() { + quote!(Some(#prefix)) + } else { + quote!(None) + }; + + quote! { + writer.write_namespace_declaration(#prefix, #namespace)?; + } + }) + .collect() +} + fn write_child(name: &Ident, ty: &Type, ele_name: &TokenStream) -> TokenStream { match ty { Type::OptionT(_) => quote! { @@ -139,7 +166,7 @@ fn write_child(name: &Ident, ty: &Type, ele_name: &TokenStream) -> TokenStream { Type::T(_) => quote! { strong_xml::log_start_writing_field!(#ele_name, #name); - &#name.to_writer(&mut writer)?; + #name.to_writer(&mut writer)?; strong_xml::log_finish_writing_field!(#ele_name, #name); }, @@ -148,7 +175,7 @@ fn write_child(name: &Ident, ty: &Type, ele_name: &TokenStream) -> TokenStream { } fn write_text( - tag: &LitStr, + tag: &QName, name: &Ident, ty: &Type, ele_name: &TokenStream, @@ -177,7 +204,7 @@ fn write_text( } fn write_flatten_text( - tag: &LitStr, + tag: &QName, name: &Ident, ty: &Type, ele_name: &TokenStream, diff --git a/strong-xml/src/xml_reader.rs b/strong-xml/src/xml_reader.rs index eb84505..1bfd9fb 100644 --- a/strong-xml/src/xml_reader.rs +++ b/strong-xml/src/xml_reader.rs @@ -36,7 +36,7 @@ impl<'a> XmlReader<'a> { } #[inline] - pub fn read_text(&mut self, end_tag: &str) -> XmlResult> { + pub fn read_text(&mut self, end_tag: &'a str) -> XmlResult> { let mut res = None; while let Some(token) = self.next() { @@ -56,14 +56,12 @@ impl<'a> XmlReader<'a> { end: ElementEnd::Close(_, _), span, } => { - let span = span.as_str(); // - let tag = &span[2..span.len() - 1]; // remove `` - if end_tag == tag { + if end_tag == &span[2..span.len() - 1] { break; } else { return Err(XmlError::TagMismatch { expected: end_tag.to_owned(), - found: tag.to_owned(), + found: span[2..span.len() - 1].to_owned(), }); } } @@ -108,13 +106,22 @@ impl<'a> XmlReader<'a> { pub fn find_attribute(&mut self) -> XmlResult)>> { if let Some(token) = self.tokenizer.peek() { match token { - Ok(Token::Attribute { span, value, .. }) => { - let value = value.as_str(); - let span = span.as_str(); // key="value" - let key = &span[0..span.len() - value.len() - 3]; // remove `="`, value and `"` - let value = Cow::Borrowed(value); + Ok(Token::Attribute { + prefix, + local, + value, + span, + }) => { + let length = local.len() + + if !prefix.is_empty() { + prefix.len() + 1 + } else { + 0 + }; + let name = &span.as_str()[0..length]; + let value = Cow::Borrowed(value.as_str()); self.next(); - return Ok(Some((key, value))); + return Ok(Some((name, value))); } Ok(Token::ElementEnd { end: ElementEnd::Open, @@ -151,15 +158,14 @@ impl<'a> XmlReader<'a> { span, }) if end_tag.is_some() => { let end_tag = end_tag.unwrap(); - let span = span.as_str(); // let tag = &span[2..span.len() - 1]; // remove `` if tag == end_tag { self.next(); return Ok(None); } else { return Err(XmlError::TagMismatch { - expected: end_tag.to_owned(), - found: tag.to_owned(), + expected: end_tag.to_string(), + found: tag.to_string(), }); } } @@ -239,7 +245,7 @@ impl<'a> XmlReader<'a> { Token::ElementEnd { end: ElementEnd::Close(_, _), span, - } if end_tag == &span.as_str()[2..span.as_str().len() - 1] => { + } if end_tag == &span[2..span.len() - 1] => { depth -= 1; if depth == 0 { return Ok(()); diff --git a/strong-xml/src/xml_writer.rs b/strong-xml/src/xml_writer.rs index d1ad475..01b65ad 100644 --- a/strong-xml/src/xml_writer.rs +++ b/strong-xml/src/xml_writer.rs @@ -1,3 +1,4 @@ +use std::collections::{BTreeMap, BTreeSet}; use std::io::Result; use std::io::Write; @@ -5,11 +6,17 @@ use crate::xml_escape::xml_escape; pub struct XmlWriter { pub inner: W, + pub used_namespaces: BTreeMap, Vec<&'static str>>, + pub set_prefixes: Vec>>, } impl XmlWriter { pub fn new(inner: W) -> Self { - XmlWriter { inner } + XmlWriter { + inner, + used_namespaces: BTreeMap::new(), + set_prefixes: Vec::new(), + } } pub fn into_inner(self) -> W { @@ -17,9 +24,29 @@ impl XmlWriter { } pub fn write_element_start(&mut self, tag: &str) -> Result<()> { + // Add new level to store set prefixes for this scope + self.set_prefixes.push(BTreeSet::new()); write!(self.inner, "<{}", tag) } + pub fn write_namespace_declaration( + &mut self, + prefix: Option<&'static str>, + ns: &'static str, + ) -> Result<()> { + if !self.is_prefix_defined_as(&prefix, ns) { + // let writer know that there has been a prefix that has been re/defined. + self.push_changed_namespace(prefix, ns)?; + if let Some(prefix) = prefix { + write!(self.inner, r#" xmlns:{}="{}""#, prefix, xml_escape(ns)) + } else { + write!(self.inner, r#" xmlns="{}""#, xml_escape(ns)) + } + } else { + Ok(()) + } + } + pub fn write_attribute(&mut self, key: &str, value: &str) -> Result<()> { write!(self.inner, r#" {}="{}""#, key, xml_escape(value)) } @@ -49,10 +76,99 @@ impl XmlWriter { } pub fn write_element_end_close(&mut self, tag: &str) -> Result<()> { + // Restore the previous namespace declarations + self.pop_changed_namespaces()?; write!(self.inner, "", tag) } pub fn write_element_end_empty(&mut self) -> Result<()> { + // Restore the previous namespace declarations + self.pop_changed_namespaces()?; write!(self.inner, "/>") } + + pub fn is_prefix_defined_as(&mut self, prefix: &Option<&str>, namespace: &str) -> bool { + match self.used_namespaces.get(prefix) { + Some(scope) => scope.last() == Some(&namespace), + _ => false, + } + } + + pub fn get_namespace(&mut self, prefix: &Option<&str>) -> Option<&'static str> { + match self.used_namespaces.get(prefix) { + Some(scope) => { + if let Some(&namespace) = scope.last() { + Some(namespace) + } else { + None + } + } + _ => None, + } + } + + fn pop_changed_namespaces(&mut self) -> Result<()> { + use std::io::{Error, ErrorKind}; + + // Go and get the current set_prefixes for this scope + // and pop of the last namespace for each prefix + if let Some(set_prefixes) = self.set_prefixes.pop() { + set_prefixes + .iter() + .map(|pfx| -> Result<()> { + match self.used_namespaces.get_mut(pfx) { + Some(v) => { + if let Some(_) = v.pop() { + Ok(()) + } else { + Err(Error::new( + ErrorKind::Other, + "Prefix state could not be popped", + )) + } + } + None => Err(Error::new( + ErrorKind::Other, + "Prefix does not exist in scope", + )), + } + }) + .collect::>>()?; + Ok(()) + } else { + Err(Error::new( + ErrorKind::Other, + "Failed to restore previous prefix scope", + )) + } + } + + fn push_changed_namespace( + &mut self, + prefix: Option<&'static str>, + namespace: &'static str, + ) -> Result<()> { + use std::io::{Error, ErrorKind}; + + // Get the current prefix scope off of the stack + let set_prefixes = if let Some(prefixes) = self.set_prefixes.last_mut() { + prefixes + } else { + return Err(Error::new( + ErrorKind::Other, + "Failed to get current prefix scope", + )); + }; + + // Push prefix onto stack for namespace + // or create new stack with namespace as only element + if let Some(v) = self.used_namespaces.get_mut(&prefix) { + v.push(namespace); + } else { + self.used_namespaces.insert(prefix, vec![namespace]); + } + set_prefixes.insert(prefix); + + Ok(()) + } } diff --git a/test-suite/tests/namespaces.rs b/test-suite/tests/namespaces.rs new file mode 100644 index 0000000..1002bf6 --- /dev/null +++ b/test-suite/tests/namespaces.rs @@ -0,0 +1,137 @@ +use strong_xml::{XmlRead, XmlResult, XmlWrite}; + +#[test] +fn test_nested_namespaces() -> XmlResult<()> { + #[derive(XmlWrite, XmlRead, PartialEq, Debug)] + #[xml(tag = "n:nested", ns = "n: http://www.example.com")] + struct Nested { + #[xml(child = "n:nested")] + contents: Vec, + } + + let _ = env_logger::builder() + .is_test(true) + .format_timestamp(None) + .try_init(); + + assert_eq!( + (Nested { + contents: vec![Nested { + contents: vec![Nested { + contents: vec![Nested { contents: vec![] }] + }] + }] + }) + .to_string()?, + r#""# + ); + + Ok(()) +} + +#[test] +fn test_namespace_clashes() -> XmlResult<()> { + #[derive(XmlWrite, XmlRead, PartialEq, Debug)] + #[xml( + tag = "n:nested", + ns = "n: http://www.example.com", + ns = "n2: http://www.example.com" + )] + struct Nested { + #[xml(child = "n:nested")] + nested: Vec, + #[xml(child = "n2:nested2")] + nested2: Nested2, + } + + #[derive(XmlWrite, XmlRead, PartialEq, Debug)] + #[xml(tag = "n2:nested2", ns = "n2: http://www.example.com")] + struct Nested2 { + #[xml(attr = "n2:nest")] + value: String, + } + + let _ = env_logger::builder() + .is_test(true) + .format_timestamp(None) + .try_init(); + + assert_eq!( + (Nested { + nested: vec![], + nested2: Nested2 { + value: "hello world".into() + } + }) + .to_string()?, + r#""# + ); + + assert_eq!( + (Nested { + nested: vec![], + nested2: Nested2 { + value: "hello world".into() + } + }), + Nested::from_str( + r#""# + )? + ); + + #[derive(XmlWrite, XmlRead, PartialEq, Debug)] + #[xml(tag = "a:tag", ns = "a: http://www.example.com")] + struct A { + #[xml(text)] + value: String, + } + + #[derive(XmlWrite, XmlRead, PartialEq, Debug)] + #[xml(tag = "b:tag", ns = "b: http://www.example.com/1")] + struct B { + #[xml(text)] + value: String, + } + + #[derive(XmlWrite, XmlRead, PartialEq, Debug)] + #[xml(tag = "a:root", ns = "a: ns_a", ns = "b: ns_b")] + struct C { + #[xml(child = "a:tag")] + a: A, + #[xml(child = "b:tag")] + b: B, + } + + assert_eq!( + C { + a: A { + value: "hello".into() + }, + b: B { + value: "world".into() + } + } + .to_string()?, + r#"helloworld"# + ); + + /* + TODO: namespaces are not enforced when readeing. + Should we have options that the user can set? + */ + assert_eq!( + C { + a: A { + value: "hello".into() + }, + b: B { + value: "world".into() + } + }, + C::from_str( + r#"helloworld"# + )? + ); + + Ok(()) +}