From afc39e276dd23e5e341ae686e3a860cbc3a08b55 Mon Sep 17 00:00:00 2001 From: choinskib <37154155+choinskib@users.noreply.github.com> Date: Mon, 4 Jul 2022 14:33:54 +0200 Subject: [PATCH] Handling namespaces attributes in fields (#4) --- .gitignore | 2 + instant-xml-macros/src/lib.rs | 186 +++++++++++++++++++++++++++------- instant-xml/tests/all.rs | 6 +- 3 files changed, 155 insertions(+), 39 deletions(-) diff --git a/.gitignore b/.gitignore index 96ef6c0..f040f76 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target +/instant-xml/target +/instant-xml-macros/target Cargo.lock diff --git a/instant-xml-macros/src/lib.rs b/instant-xml-macros/src/lib.rs index 3768d03..0bc8cc2 100644 --- a/instant-xml-macros/src/lib.rs +++ b/instant-xml-macros/src/lib.rs @@ -2,36 +2,156 @@ extern crate proc_macro; use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, DeriveInput, Lit, Meta, NestedMeta}; +use std::collections::HashMap; +use syn::{parse_macro_input, Lit, Meta, NestedMeta}; -fn retrieve_default_namespace(input: &DeriveInput) -> Option { - for attr in &input.attrs { - if !attr.path.is_ident(XML) { - continue; +const XML: &str = "xml"; + +enum FieldAttribute { + Namespace(String), + PrefixIdentifier(String), +} + +struct Serializer { + default_namespace: Option, + other_namespaces: HashMap, +} + +impl<'a> Serializer { + pub fn new(attributes: &'a Vec) -> Serializer { + let mut default_namespace = None; + let mut other_namespaces = HashMap::default(); + + if let Some(list) = Self::retrieve_namespace_list(attributes) { + match list.path.get_ident() { + Some(ident) if ident == "namespace" => { + let mut iter = list.nested.iter(); + if let Some(NestedMeta::Lit(Lit::Str(v))) = iter.next() { + default_namespace = Some(v.value()); + } + + for item in iter { + match item { + NestedMeta::Meta(Meta::NameValue(key)) => { + if let Lit::Str(value) = &key.lit { + other_namespaces.insert( + key.path.get_ident().unwrap().to_string(), + value.value(), + ); + } + } + _ => todo!(), + } + } + } + _ => (), + } } - let nested = match attr.parse_meta() { - Ok(Meta::List(meta)) => meta.nested, - Ok(_) => todo!(), - _ => todo!(), - }; - - let list = match nested.first() { - Some(NestedMeta::Meta(Meta::List(list))) => list, - _ => todo!(), - }; - - if list.path.get_ident()? == "namespace" { - if let NestedMeta::Lit(Lit::Str(v)) = list.nested.first()? { - return Some(v.value()); - } + Serializer { + default_namespace, + other_namespaces, } } - None -} + fn add_header(&mut self, root_name: &str, output: &'a mut proc_macro2::TokenStream) { + output.extend(quote!(+ "<" + #root_name)); -const XML: &str = "xml"; + if let Some(default_namespace) = self.default_namespace.as_ref() { + output.extend(quote!(+ " xmlns=\"" + #default_namespace + "\"")); + } + + let mut sorted_values: Vec<_> = self.other_namespaces.iter().collect(); + sorted_values.sort(); + + for (key, val) in sorted_values { + output.extend(quote!(+ " xmlns:" + #key + "=\"" + #val + "\"")); + } + + output.extend(quote!(+ ">")); + } + + fn add_footer(&mut self, root_name: &str, output: &'a mut proc_macro2::TokenStream) { + output.extend(quote!(+ "")); + } + + fn process_named_field( + &mut self, + field: &syn::Field, + output: &'a mut proc_macro2::TokenStream, + ) { + let field_name = field.ident.as_ref().unwrap().to_string(); + let field_value = field.ident.as_ref().unwrap(); + let mut prefix = String::default(); + + match Self::retrieve_field_attribute(field) { + Some(FieldAttribute::Namespace(namespace)) => { + output.extend(quote!(+ "<" + #field_name + " xmlns=\"" + #namespace + "\"")); + } + Some(FieldAttribute::PrefixIdentifier(prefix_key)) + if !self.other_namespaces.is_empty() => + { + match self.other_namespaces.get(&prefix_key) { + Some(_) => { + prefix = prefix_key + ":"; + output.extend(quote!(+ "<" + #prefix + #field_name)); + } + None => todo!(), // return the error + }; + } + _ => { + // Without the namespace + output.extend(quote!(+ "<" + #field_name)); + } + }; + + output.extend( + quote!(+ ">" + self.#field_value.to_string().as_str() + ""), + ); + } + + fn retrieve_namespace_list(attributes: &Vec) -> Option { + for attr in attributes { + if !attr.path.is_ident(XML) { + continue; + } + + let nested = match attr.parse_meta() { + Ok(Meta::List(meta)) => meta.nested, + Ok(_) => todo!(), + _ => todo!(), + }; + + let list = match nested.first() { + Some(NestedMeta::Meta(Meta::List(list))) => list, + _ => todo!(), + }; + + if list.path.get_ident()? == "namespace" { + return Some(list.to_owned()); + } + } + + None + } + + fn retrieve_field_attribute(input: &syn::Field) -> Option { + if let Some(list) = Self::retrieve_namespace_list(&input.attrs) { + match list.nested.first() { + Some(NestedMeta::Lit(Lit::Str(v))) => { + return Some(FieldAttribute::Namespace(v.value())); + } + Some(NestedMeta::Meta(Meta::Path(v))) => { + if let Some(ident) = v.get_ident() { + return Some(FieldAttribute::PrefixIdentifier(ident.to_string())); + } + } + _ => (), + }; + } + None + } +} #[proc_macro_derive(ToXml, attributes(xml))] pub fn to_xml(input: TokenStream) -> TokenStream { @@ -39,25 +159,17 @@ pub fn to_xml(input: TokenStream) -> TokenStream { let ident = &ast.ident; let root_name = ident.to_string(); - let header = match retrieve_default_namespace(&ast) { - Some(ns) => format!("{} xmlns=\"{}\"", root_name, ns), - None => root_name.clone(), - }; + let mut output: proc_macro2::TokenStream = TokenStream::from(quote!("".to_owned())).into(); - let mut output: proc_macro2::TokenStream = - TokenStream::from(quote!("<".to_owned() + #header + ">")).into(); + let mut serializer = Serializer::new(&ast.attrs); + serializer.add_header(&root_name, &mut output); match &ast.data { syn::Data::Struct(ref data) => { match data.fields { syn::Fields::Named(ref fields) => { - fields - .named - .iter() - .for_each(|field| { - let field_name = field.ident.as_ref().unwrap().to_string(); - let field_value = field.ident.as_ref().unwrap(); - output.extend(quote!(+ "<" + #field_name + ">" + self.#field_value.to_string().as_str() + "")); + fields.named.iter().for_each(|field| { + serializer.process_named_field(field, &mut output); }); } syn::Fields::Unnamed(_) => todo!(), @@ -67,7 +179,7 @@ pub fn to_xml(input: TokenStream) -> TokenStream { _ => todo!(), }; - output.extend(quote!(+ "")); + serializer.add_footer(&root_name, &mut output); TokenStream::from(quote!( impl ToXml for #ident { diff --git a/instant-xml/tests/all.rs b/instant-xml/tests/all.rs index 5ce382f..f93efde 100644 --- a/instant-xml/tests/all.rs +++ b/instant-xml/tests/all.rs @@ -4,10 +4,12 @@ use instant_xml::{FromXml, ToXml}; struct Unit; #[derive(Debug, Eq, PartialEq, ToXml)] -#[xml(namespace("URI"))] +#[xml(namespace("URI", bar = "BAZ", foo = "BAR"))] struct StructWithNamedFields { flag: bool, + #[xml(namespace(bar))] string: String, + #[xml(namespace("typo"))] number: i32, } @@ -27,6 +29,6 @@ fn struct_with_named_fields() { } .to_xml() .unwrap(), - "truetest1" + "truetest1" ); }