├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── README.md ├── cover.svg ├── deny.toml ├── instant-xml-macros ├── Cargo.toml └── src │ ├── case.rs │ ├── de.rs │ ├── lib.rs │ ├── meta.rs │ └── ser.rs └── instant-xml ├── Cargo.toml ├── benches └── decode.rs ├── src ├── de.rs ├── impls.rs ├── lib.rs └── ser.rs └── tests ├── attributes.rs ├── chrono.rs ├── de-nested.rs ├── de-ns.rs ├── direct.rs ├── escaping.rs ├── forward-enum.rs ├── generics.rs ├── lifetime.rs ├── option.rs ├── rename.rs ├── scalar-enum.rs ├── scalar.rs ├── ser-child-ns.rs ├── ser-named.rs ├── ser-nested.rs ├── ser-unit.rs ├── transparent.rs ├── unnamed.rs ├── vec.rs └── with.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [djc] 2 | patreon: dochtman 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: "13:00" 8 | open-pull-requests-limit: 99 9 | - package-ecosystem: github-actions 10 | directory: "/" 11 | schedule: 12 | interval: weekly 13 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | pull_request: 7 | schedule: 8 | - cron: "34 6 * * 5" 9 | 10 | jobs: 11 | test: 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, macos-latest, windows-latest] 15 | rust: [stable] 16 | include: 17 | - os: ubuntu-latest 18 | rust: beta 19 | 20 | runs-on: ${{ matrix.os }} 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: dtolnay/rust-toolchain@master 25 | with: 26 | toolchain: ${{ matrix.rust }} 27 | - run: cargo check --all-features --all-targets 28 | env: 29 | CARGO_INCREMENTAL: 0 # https://github.com/rust-lang/rust/issues/101518 30 | - run: cargo test --all-features 31 | env: 32 | CARGO_INCREMENTAL: 0 # https://github.com/rust-lang/rust/issues/101518 33 | 34 | msrv: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: dtolnay/rust-toolchain@master 39 | with: 40 | toolchain: 1.62.0 41 | - run: cargo check --all-features --lib 42 | 43 | lint: 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: dtolnay/rust-toolchain@stable 48 | with: 49 | toolchain: stable 50 | components: rustfmt, clippy 51 | - run: cargo fmt --all -- --check 52 | - run: cargo clippy --all-targets --all-features -- -D warnings 53 | env: 54 | CARGO_INCREMENTAL: 0 # https://github.com/rust-lang/rust/issues/101518 55 | 56 | audit: 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/checkout@v4 60 | - uses: EmbarkStudios/cargo-deny-action@v2 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /instant-xml/target 3 | /instant-xml-macros/target 4 | Cargo.lock 5 | **/.DS_Store 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["instant-xml", "instant-xml-macros"] 3 | resolver = "2" 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Cover logo](./cover.svg) 2 | 3 | # instant-xml: more rigorously mapping XML to Rust types 4 | 5 | [![Documentation](https://docs.rs/instant-xml/badge.svg)](https://docs.rs/instant-xml) 6 | [![Crates.io](https://img.shields.io/crates/v/instant-xml.svg)](https://crates.io/crates/instant-xml) 7 | [![Build status](https://github.com/djc/instant-xml/workflows/CI/badge.svg)](https://github.com/djc/instant-xml/actions?query=workflow%3ACI) 8 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE-MIT) 9 | [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE-APACHE) 10 | 11 | instant-xml is a serde-like library providing traits and procedural macros to help map XML to Rust 12 | types. While serde is great for formats like JSON, the underlying structure it provides is not a 13 | great fit for XML, limiting serde-based tools like quick-xml. instant-xml more rigorously maps the 14 | XML data model (including namespaces) to Rust types while providing a serde-like interface. 15 | 16 | This library is used in production at [Instant Domain Search](https://instantdomainsearch.com/). 17 | 18 | ## Features 19 | 20 | * Familiar serde-like interface 21 | * Full support for XML namespaces 22 | * Avoids copying deserialized data where possible 23 | * Minimum supported Rust version is 1.58 24 | 25 | ## Limitations 26 | 27 | instant-xml is still early in its lifecycle. While it works well for our use cases, it might not 28 | work well for you, and several more semver-incompatible releases should be expected to flesh out 29 | the core trait APIs as we throw more test cases at it. There's also currently not that much 30 | documentation. 31 | 32 | We'd love to hear your feedback! 33 | 34 | ## Thanks 35 | 36 | Thanks to [@rsdy](https://github.com/rsdy) and [@choinskib](https://github.com/choinskib) for 37 | their work on this library, and thanks (of course) to [@dtolnay](https://github.com/dtolnay/) for 38 | creating serde. 39 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [licenses] 2 | allow = ["Apache-2.0", "MIT", "Unicode-3.0"] 3 | -------------------------------------------------------------------------------- /instant-xml-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "instant-xml-macros" 3 | version = "0.6.0" 4 | edition = "2021" 5 | rust-version = "1.62" 6 | workspace = ".." 7 | license = "Apache-2.0 OR MIT" 8 | description = "Procedural macros for instant-xml" 9 | documentation = "https://docs.rs/instant-xml-macros" 10 | repository = "https://github.com/djc/instant-xml" 11 | readme = "../README.md" 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | heck = "0.5" 18 | proc-macro2 = "1.0.39" 19 | quote = "1.0.18" 20 | syn = { version = "2", features = ["full"] } 21 | -------------------------------------------------------------------------------- /instant-xml-macros/src/case.rs: -------------------------------------------------------------------------------- 1 | //! Originally from 2 | //! Code to convert the Rust-styled field/variant (e.g. `my_field`, `MyType`) to the 3 | //! case of the source (e.g. `my-field`, `MY_FIELD`). 4 | 5 | // See https://users.rust-lang.org/t/psa-dealing-with-warning-unused-import-std-ascii-asciiext-in-today-s-nightly/13726 6 | #[allow(deprecated, unused_imports)] 7 | use std::ascii::AsciiExt; 8 | 9 | use std::fmt::{self, Debug, Display}; 10 | 11 | use proc_macro2::Ident; 12 | #[cfg(test)] 13 | use proc_macro2::Span; 14 | 15 | use self::RenameRule::*; 16 | 17 | /// The different possible ways to change case of fields in a struct, or variants in an enum. 18 | #[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] 19 | pub enum RenameRule { 20 | /// Don't apply a default rename rule. 21 | #[default] 22 | None, 23 | /// Rename direct children to "lowercase" style. 24 | LowerCase, 25 | /// Rename direct children to "UPPERCASE" style. 26 | UpperCase, 27 | /// Rename direct children to "PascalCase" style, as typically used for 28 | /// enum variants. 29 | PascalCase, 30 | /// Rename direct children to "camelCase" style. 31 | CamelCase, 32 | /// Rename direct children to "snake_case" style, as commonly used for 33 | /// fields. 34 | SnakeCase, 35 | /// Rename direct children to "SCREAMING_SNAKE_CASE" style, as commonly 36 | /// used for constants. 37 | ScreamingSnakeCase, 38 | /// Rename direct children to "kebab-case" style. 39 | KebabCase, 40 | /// Rename direct children to "SCREAMING-KEBAB-CASE" style. 41 | ScreamingKebabCase, 42 | } 43 | 44 | const RENAME_RULES: &[(&str, RenameRule)] = &[ 45 | ("\"lowercase\"", LowerCase), 46 | ("\"UPPERCASE\"", UpperCase), 47 | ("\"PascalCase\"", PascalCase), 48 | ("\"camelCase\"", CamelCase), 49 | ("\"snake_case\"", SnakeCase), 50 | ("\"SCREAMING_SNAKE_CASE\"", ScreamingSnakeCase), 51 | ("\"kebab-case\"", KebabCase), 52 | ("\"SCREAMING-KEBAB-CASE\"", ScreamingKebabCase), 53 | ]; 54 | 55 | impl RenameRule { 56 | pub fn from_str(rename_all_str: &str) -> Result { 57 | for (name, rule) in RENAME_RULES { 58 | if rename_all_str == *name { 59 | return Ok(*rule); 60 | } 61 | } 62 | Err(ParseError { 63 | unknown: rename_all_str, 64 | }) 65 | } 66 | 67 | /// Apply a renaming rule to an enum variant, returning the version expected in the source. 68 | pub fn apply_to_variant(&self, ident: &Ident) -> String { 69 | let variant = ident.to_string(); 70 | match *self { 71 | None | PascalCase => variant, 72 | LowerCase => variant.to_ascii_lowercase(), 73 | UpperCase => variant.to_ascii_uppercase(), 74 | CamelCase => variant[..1].to_ascii_lowercase() + &variant[1..], 75 | SnakeCase => { 76 | let mut snake = String::new(); 77 | for (i, ch) in variant.char_indices() { 78 | if i > 0 && ch.is_uppercase() { 79 | snake.push('_'); 80 | } 81 | snake.push(ch.to_ascii_lowercase()); 82 | } 83 | snake 84 | } 85 | ScreamingSnakeCase => SnakeCase.apply_to_variant(ident).to_ascii_uppercase(), 86 | KebabCase => SnakeCase.apply_to_variant(ident).replace('_', "-"), 87 | ScreamingKebabCase => ScreamingSnakeCase.apply_to_variant(ident).replace('_', "-"), 88 | } 89 | } 90 | 91 | /// Apply a renaming rule to a struct field, returning the version expected in the source. 92 | pub fn apply_to_field(&self, ident: &Ident) -> String { 93 | let mut field = ident.to_string(); 94 | if field.starts_with("r#") { 95 | field = field[2..].to_string(); 96 | } 97 | 98 | match *self { 99 | None | LowerCase | SnakeCase => field, 100 | UpperCase => field.to_ascii_uppercase(), 101 | PascalCase => { 102 | let mut pascal = String::new(); 103 | let mut capitalize = true; 104 | for ch in field.chars() { 105 | if ch == '_' { 106 | capitalize = true; 107 | } else if capitalize { 108 | pascal.push(ch.to_ascii_uppercase()); 109 | capitalize = false; 110 | } else { 111 | pascal.push(ch); 112 | } 113 | } 114 | pascal 115 | } 116 | CamelCase => { 117 | let pascal = PascalCase.apply_to_field(ident); 118 | pascal[..1].to_ascii_lowercase() + &pascal[1..] 119 | } 120 | ScreamingSnakeCase => field.to_ascii_uppercase(), 121 | KebabCase => field.replace('_', "-"), 122 | ScreamingKebabCase => ScreamingSnakeCase.apply_to_field(ident).replace('_', "-"), 123 | } 124 | } 125 | } 126 | 127 | pub struct ParseError<'a> { 128 | unknown: &'a str, 129 | } 130 | 131 | impl Display for ParseError<'_> { 132 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 133 | f.write_str("unknown rename rule `rename_all = ")?; 134 | Debug::fmt(self.unknown, f)?; 135 | f.write_str("`, expected one of ")?; 136 | for (i, (name, _rule)) in RENAME_RULES.iter().enumerate() { 137 | if i > 0 { 138 | f.write_str(", ")?; 139 | } 140 | Debug::fmt(name, f)?; 141 | } 142 | Ok(()) 143 | } 144 | } 145 | 146 | #[test] 147 | fn rename_variants() { 148 | for &(name, lower, upper, camel, snake, screaming, kebab, screaming_kebab) in &[ 149 | ( 150 | "Outcome", "outcome", "OUTCOME", "outcome", "outcome", "OUTCOME", "outcome", "OUTCOME", 151 | ), 152 | ( 153 | "VeryTasty", 154 | "verytasty", 155 | "VERYTASTY", 156 | "veryTasty", 157 | "very_tasty", 158 | "VERY_TASTY", 159 | "very-tasty", 160 | "VERY-TASTY", 161 | ), 162 | ("A", "a", "A", "a", "a", "A", "a", "A"), 163 | ("Z42", "z42", "Z42", "z42", "z42", "Z42", "z42", "Z42"), 164 | ] { 165 | let original = &Ident::new(name, Span::call_site()); 166 | 167 | assert_eq!(None.apply_to_variant(original), name); 168 | assert_eq!(LowerCase.apply_to_variant(original), lower); 169 | assert_eq!(UpperCase.apply_to_variant(original), upper); 170 | assert_eq!(PascalCase.apply_to_variant(original), name); 171 | assert_eq!(CamelCase.apply_to_variant(original), camel); 172 | assert_eq!(SnakeCase.apply_to_variant(original), snake); 173 | assert_eq!(ScreamingSnakeCase.apply_to_variant(original), screaming); 174 | assert_eq!(KebabCase.apply_to_variant(original), kebab); 175 | assert_eq!( 176 | ScreamingKebabCase.apply_to_variant(original), 177 | screaming_kebab 178 | ); 179 | } 180 | } 181 | 182 | #[test] 183 | fn rename_fields() { 184 | for &(name, upper, pascal, camel, screaming, kebab, screaming_kebab) in &[ 185 | ( 186 | "outcome", "OUTCOME", "Outcome", "outcome", "OUTCOME", "outcome", "OUTCOME", 187 | ), 188 | ( 189 | "very_tasty", 190 | "VERY_TASTY", 191 | "VeryTasty", 192 | "veryTasty", 193 | "VERY_TASTY", 194 | "very-tasty", 195 | "VERY-TASTY", 196 | ), 197 | ("a", "A", "A", "a", "A", "a", "A"), 198 | ("z42", "Z42", "Z42", "z42", "Z42", "z42", "Z42"), 199 | ] { 200 | let original = &Ident::new(name, Span::call_site()); 201 | 202 | assert_eq!(None.apply_to_field(original), name); 203 | assert_eq!(UpperCase.apply_to_field(original), upper); 204 | assert_eq!(PascalCase.apply_to_field(original), pascal); 205 | assert_eq!(CamelCase.apply_to_field(original), camel); 206 | assert_eq!(SnakeCase.apply_to_field(original), name); 207 | assert_eq!(ScreamingSnakeCase.apply_to_field(original), screaming); 208 | assert_eq!(KebabCase.apply_to_field(original), kebab); 209 | assert_eq!(ScreamingKebabCase.apply_to_field(original), screaming_kebab); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /instant-xml-macros/src/de.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | 3 | use proc_macro2::{Ident, Literal, Span, TokenStream}; 4 | use quote::quote; 5 | use syn::spanned::Spanned; 6 | 7 | use super::{ 8 | discard_lifetimes, meta_items, ContainerMeta, FieldMeta, Mode, Namespace, VariantMeta, 9 | }; 10 | 11 | pub(crate) fn from_xml(input: &syn::DeriveInput) -> TokenStream { 12 | let meta = match ContainerMeta::from_derive(input) { 13 | Ok(meta) => meta, 14 | Err(e) => return e.to_compile_error(), 15 | }; 16 | 17 | match (&input.data, meta.mode) { 18 | (syn::Data::Struct(data), None) => match &data.fields { 19 | syn::Fields::Named(fields) => deserialize_struct(input, fields, meta), 20 | syn::Fields::Unnamed(fields) => deserialize_tuple_struct(input, fields, meta), 21 | syn::Fields::Unit => deserialize_unit_struct(input, &meta), 22 | }, 23 | (syn::Data::Struct(data), Some(Mode::Transparent)) => match &data.fields { 24 | syn::Fields::Named(fields) => deserialize_inline_struct(input, fields, meta), 25 | _ => syn::Error::new( 26 | input.span(), 27 | "inline mode is only supported on types with named fields", 28 | ) 29 | .to_compile_error(), 30 | }, 31 | (syn::Data::Enum(data), Some(Mode::Scalar)) => deserialize_scalar_enum(input, data, meta), 32 | (syn::Data::Enum(data), Some(Mode::Forward)) => deserialize_forward_enum(input, data, meta), 33 | (syn::Data::Struct(_), Some(mode)) => syn::Error::new( 34 | input.span(), 35 | format_args!("{mode:?} mode not allowed on struct type"), 36 | ) 37 | .to_compile_error(), 38 | (syn::Data::Enum(_), Some(mode)) => syn::Error::new( 39 | input.span(), 40 | format_args!("{mode:?} mode not allowed on enum type"), 41 | ) 42 | .to_compile_error(), 43 | (syn::Data::Enum(_), None) => { 44 | syn::Error::new(input.span(), "missing mode").to_compile_error() 45 | } 46 | _ => todo!(), 47 | } 48 | } 49 | 50 | fn deserialize_scalar_enum( 51 | input: &syn::DeriveInput, 52 | data: &syn::DataEnum, 53 | meta: ContainerMeta, 54 | ) -> TokenStream { 55 | let ident = &input.ident; 56 | let mut variants = TokenStream::new(); 57 | 58 | for variant in data.variants.iter() { 59 | let v_ident = &variant.ident; 60 | let meta = match VariantMeta::from_variant(variant, &meta) { 61 | Ok(meta) => meta, 62 | Err(err) => return err.to_compile_error(), 63 | }; 64 | 65 | let serialize_as = meta.serialize_as; 66 | variants.extend(quote!(#serialize_as => #ident::#v_ident,)); 67 | } 68 | 69 | let default_namespace = meta.default_namespace(); 70 | 71 | let generics = meta.xml_generics(BTreeSet::new()); 72 | let (impl_generics, _, _) = generics.split_for_impl(); 73 | let (_, ty_generics, where_clause) = input.generics.split_for_impl(); 74 | let type_str = ident.to_string(); 75 | 76 | quote!( 77 | impl #impl_generics FromXml<'xml> for #ident #ty_generics #where_clause { 78 | #[inline] 79 | fn matches(id: ::instant_xml::Id<'_>, field: Option<::instant_xml::Id<'_>>) -> bool { 80 | id == ::instant_xml::Id { 81 | ns: #default_namespace, 82 | name: match field { 83 | Some(fid) => fid.name, 84 | None => id.name, 85 | }, 86 | } 87 | } 88 | 89 | fn deserialize<'cx>( 90 | into: &mut Self::Accumulator, 91 | field: &'static str, 92 | deserializer: &mut ::instant_xml::Deserializer<'cx, 'xml>, 93 | ) -> ::std::result::Result<(), ::instant_xml::Error> { 94 | use ::instant_xml::Error; 95 | 96 | if into.is_some() { 97 | return Err(Error::DuplicateValue(field)); 98 | } 99 | 100 | let cow_str = match deserializer.take_str()? { 101 | Some(val) => val, 102 | None => return Err(Error::MissingValue(#type_str)), 103 | }; 104 | 105 | let value = match cow_str.as_ref() { 106 | #variants 107 | _ => return Err(Error::UnexpectedValue( 108 | format!("enum variant not found for '{}' in field {}", cow_str, field), 109 | )), 110 | }; 111 | 112 | *into = Some(value); 113 | Ok(()) 114 | } 115 | 116 | type Accumulator = Option; 117 | const KIND: ::instant_xml::Kind = ::instant_xml::Kind::Scalar; 118 | } 119 | ) 120 | } 121 | 122 | fn deserialize_forward_enum( 123 | input: &syn::DeriveInput, 124 | data: &syn::DataEnum, 125 | meta: ContainerMeta, 126 | ) -> TokenStream { 127 | if data.variants.is_empty() { 128 | return syn::Error::new(input.span(), "empty enum is not supported").to_compile_error(); 129 | } 130 | 131 | let ident = &input.ident; 132 | let field_str = format!("{ident}::0"); 133 | let mut matches = TokenStream::new(); 134 | let mut variants = TokenStream::new(); 135 | let mut borrowed = BTreeSet::new(); 136 | for variant in data.variants.iter() { 137 | let field = match &variant.fields { 138 | syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { 139 | fields.unnamed.first().unwrap() 140 | } 141 | _ => { 142 | return syn::Error::new( 143 | input.span(), 144 | "wrapped enum variants must have 1 unnamed field", 145 | ) 146 | .to_compile_error() 147 | } 148 | }; 149 | 150 | if !meta_items(&variant.attrs).is_empty() { 151 | return syn::Error::new( 152 | input.span(), 153 | "attributes not allowed on wrapped enum variants", 154 | ) 155 | .to_compile_error(); 156 | } 157 | 158 | let mut no_lifetime_type = field.ty.clone(); 159 | discard_lifetimes(&mut no_lifetime_type, &mut borrowed, false, true); 160 | 161 | if !matches.is_empty() { 162 | matches.extend(quote!(||)); 163 | } 164 | matches.extend(quote!(<#no_lifetime_type as FromXml>::matches(id, field))); 165 | 166 | if !variants.is_empty() { 167 | variants.extend(quote!(else)); 168 | } 169 | 170 | let v_ident = &variant.ident; 171 | variants.extend( 172 | quote!(if <#no_lifetime_type as FromXml>::matches(id, None) { 173 | let mut value = <#no_lifetime_type as FromXml>::Accumulator::default(); 174 | <#no_lifetime_type as FromXml>::deserialize(&mut value, #field_str, deserializer)?; 175 | *into = ::instant_xml::Accumulate::try_done(value, #field_str).map(#ident::#v_ident).ok(); 176 | }), 177 | ); 178 | } 179 | 180 | let generics = meta.xml_generics(borrowed); 181 | let (xml_impl_generics, _, _) = generics.split_for_impl(); 182 | let (_, ty_generics, where_clause) = input.generics.split_for_impl(); 183 | quote!( 184 | impl #xml_impl_generics FromXml<'xml> for #ident #ty_generics #where_clause { 185 | #[inline] 186 | fn matches(id: ::instant_xml::Id<'_>, field: Option<::instant_xml::Id<'_>>) -> bool { 187 | use ::instant_xml::FromXml; 188 | #matches 189 | } 190 | 191 | fn deserialize<'cx>( 192 | into: &mut Self::Accumulator, 193 | field: &'static str, 194 | deserializer: &mut ::instant_xml::Deserializer<'cx, 'xml>, 195 | ) -> ::std::result::Result<(), ::instant_xml::Error> { 196 | use ::instant_xml::de::Node; 197 | use ::instant_xml::{Accumulate, Error, FromXml}; 198 | 199 | let id = deserializer.parent(); 200 | #variants else { 201 | return Err(Error::UnexpectedTag(format!("{:?}", id))); 202 | }; 203 | 204 | if let Some(_) = deserializer.next() { 205 | return Err(Error::UnexpectedState("unexpected node after wrapped enum variant")); 206 | } 207 | 208 | Ok(()) 209 | } 210 | 211 | type Accumulator = Option; 212 | const KIND: ::instant_xml::Kind = ::instant_xml::Kind::Element; 213 | } 214 | ) 215 | } 216 | 217 | fn deserialize_struct( 218 | input: &syn::DeriveInput, 219 | fields: &syn::FieldsNamed, 220 | container_meta: ContainerMeta, 221 | ) -> TokenStream { 222 | let mut namespaces_map = quote!(let mut namespaces_map = std::collections::HashMap::new();); 223 | for (k, v) in container_meta.ns.prefixes.iter() { 224 | namespaces_map.extend(quote!( 225 | namespaces_map.insert(#k, #v); 226 | )) 227 | } 228 | 229 | // Varying values 230 | let mut elements_tokens = Tokens::default(); 231 | let mut attributes_tokens = Tokens::default(); 232 | 233 | // Common values 234 | let mut declare_values = TokenStream::new(); 235 | let mut return_val = TokenStream::new(); 236 | let mut direct = TokenStream::new(); 237 | 238 | let mut borrowed = BTreeSet::new(); 239 | for (index, field) in fields.named.iter().enumerate() { 240 | if !direct.is_empty() { 241 | return syn::Error::new(field.span(), "direct field must be the last") 242 | .into_compile_error(); 243 | } 244 | 245 | let field_meta = match FieldMeta::from_field(field, &container_meta) { 246 | Ok(meta) => meta, 247 | Err(err) => return err.into_compile_error(), 248 | }; 249 | 250 | let tokens = match field_meta.attribute { 251 | true => &mut attributes_tokens, 252 | false => &mut elements_tokens, 253 | }; 254 | 255 | let result = named_field( 256 | field, 257 | index, 258 | &mut declare_values, 259 | &mut return_val, 260 | tokens, 261 | &mut borrowed, 262 | &mut direct, 263 | field_meta, 264 | &input.ident, 265 | &container_meta, 266 | ); 267 | 268 | if let Err(err) = result { 269 | return err.into_compile_error(); 270 | } 271 | } 272 | 273 | if direct.is_empty() { 274 | direct.extend(quote!(Node::Text(_) => { 275 | // no direct field, ignore 276 | })); 277 | } 278 | 279 | // Elements 280 | let elements_enum = elements_tokens.r#enum; 281 | let mut elements_branches = elements_tokens.branches; 282 | let elem_type_match = elements_tokens.r#match; 283 | elements_branches.extend(match elements_branches.is_empty() { 284 | true => quote!(__Elements::__Ignore), 285 | false => quote!(else { __Elements::__Ignore }), 286 | }); 287 | 288 | // Attributes 289 | let attributes_enum = attributes_tokens.r#enum; 290 | let mut attributes_branches = attributes_tokens.branches; 291 | let attr_type_match = attributes_tokens.r#match; 292 | attributes_branches.extend(match attributes_branches.is_empty() { 293 | true => quote!(__Attributes::__Ignore), 294 | false => quote!(else { __Attributes::__Ignore }), 295 | }); 296 | 297 | let ident = &input.ident; 298 | let ident_str = format!("{ident}"); 299 | let name = container_meta.tag(); 300 | let default_namespace = container_meta.default_namespace(); 301 | let generics = container_meta.xml_generics(borrowed); 302 | 303 | let (xml_impl_generics, _, _) = generics.split_for_impl(); 304 | let (_, ty_generics, where_clause) = input.generics.split_for_impl(); 305 | 306 | quote!( 307 | impl #xml_impl_generics FromXml<'xml> for #ident #ty_generics #where_clause { 308 | #[inline] 309 | fn matches(id: ::instant_xml::Id<'_>, field: Option<::instant_xml::Id<'_>>) -> bool { 310 | id == ::instant_xml::Id { ns: #default_namespace, name: #name } 311 | } 312 | 313 | fn deserialize<'cx>( 314 | into: &mut Self::Accumulator, 315 | field: &'static str, 316 | deserializer: &mut ::instant_xml::Deserializer<'cx, 'xml>, 317 | ) -> ::std::result::Result<(), ::instant_xml::Error> { 318 | use ::instant_xml::de::Node; 319 | use ::instant_xml::{Accumulate, Error, FromXml, Id, Kind}; 320 | 321 | enum __Elements { 322 | #elements_enum 323 | __Ignore, 324 | } 325 | 326 | enum __Attributes { 327 | #attributes_enum 328 | __Ignore, 329 | } 330 | 331 | #declare_values 332 | loop { 333 | let node = match deserializer.next() { 334 | Some(result) => result?, 335 | None => break, 336 | }; 337 | 338 | match node { 339 | Node::Attribute(attr) => { 340 | let id = deserializer.attribute_id(&attr)?; 341 | let field = #attributes_branches; 342 | 343 | match field { 344 | #attr_type_match 345 | __Attributes::__Ignore => {} 346 | } 347 | } 348 | Node::Open(data) => { 349 | let id = deserializer.element_id(&data)?; 350 | let element = #elements_branches; 351 | 352 | match element { 353 | #elem_type_match 354 | __Elements::__Ignore => { 355 | let mut nested = deserializer.nested(data); 356 | nested.ignore()?; 357 | } 358 | } 359 | } 360 | #direct 361 | node => return Err(Error::UnexpectedNode(format!("{:?} in {}", node, #ident_str))), 362 | } 363 | } 364 | 365 | *into = Some(Self { #return_val }); 366 | Ok(()) 367 | } 368 | 369 | type Accumulator = Option; 370 | const KIND: ::instant_xml::Kind = ::instant_xml::Kind::Element; 371 | } 372 | ) 373 | } 374 | 375 | fn deserialize_inline_struct( 376 | input: &syn::DeriveInput, 377 | fields: &syn::FieldsNamed, 378 | meta: ContainerMeta, 379 | ) -> TokenStream { 380 | if !meta.ns.prefixes.is_empty() { 381 | return syn::Error::new( 382 | input.span(), 383 | "inline structs cannot have namespace declarations", 384 | ) 385 | .to_compile_error(); 386 | } else if let Some(ns) = meta.ns.uri { 387 | return syn::Error::new( 388 | ns.span(), 389 | "inline structs cannot have namespace declarations", 390 | ) 391 | .to_compile_error(); 392 | } else if let Some(rename) = meta.rename { 393 | return syn::Error::new(rename.span(), "inline structs cannot be renamed") 394 | .to_compile_error(); 395 | } 396 | 397 | // Varying values 398 | let mut elements_tokens = Tokens::default(); 399 | 400 | // Common values 401 | let mut declare_values = TokenStream::new(); 402 | let mut return_val = TokenStream::new(); 403 | let mut direct = TokenStream::new(); 404 | 405 | let mut borrowed = BTreeSet::new(); 406 | let mut matches = TokenStream::new(); 407 | let mut acc_field_defs = TokenStream::new(); 408 | let mut acc_field_inits = TokenStream::new(); 409 | let mut deserialize = TokenStream::new(); 410 | let mut acc_field_defaults = TokenStream::new(); 411 | for (index, field) in fields.named.iter().enumerate() { 412 | let field_meta = match FieldMeta::from_field(field, &meta) { 413 | Ok(meta) => meta, 414 | Err(err) => return err.into_compile_error(), 415 | }; 416 | 417 | if field_meta.direct { 418 | return syn::Error::new(field.span(), "inline structs cannot have a direct field") 419 | .to_compile_error(); 420 | } else if field_meta.attribute { 421 | return syn::Error::new(field.span(), "inline structs cannot have attribute fields") 422 | .to_compile_error(); 423 | } 424 | 425 | let result = named_field( 426 | field, 427 | index, 428 | &mut declare_values, 429 | &mut return_val, 430 | &mut elements_tokens, 431 | &mut borrowed, 432 | &mut direct, 433 | field_meta, 434 | &input.ident, 435 | &meta, 436 | ); 437 | 438 | let data = match result { 439 | Ok(data) => data, 440 | Err(err) => return err.into_compile_error(), 441 | }; 442 | 443 | if !matches.is_empty() { 444 | matches.extend(quote!(||)); 445 | } 446 | 447 | let field_ty = data.no_lifetime_type; 448 | matches.extend(quote!( 449 | <#field_ty as FromXml<'xml>>::matches(id, None) 450 | )); 451 | 452 | let field_name = &field.ident; 453 | let field_ty_with_lifetime = &field.ty; 454 | acc_field_defs 455 | .extend(quote!(#field_name: <#field_ty_with_lifetime as FromXml<'xml>>::Accumulator,)); 456 | let field_str = format!("{}::{}", input.ident, data.field_name); 457 | acc_field_inits.extend(quote!(#field_name: self.#field_name.try_done(#field_str)?,)); 458 | acc_field_defaults.extend(quote!(#field_name: Default::default(),)); 459 | 460 | if !deserialize.is_empty() { 461 | deserialize.extend(quote!(else)); 462 | } 463 | if let Some(with) = data.deserialize_with { 464 | deserialize.extend( 465 | quote!(if <#field_ty as FromXml<'xml>>::matches(current, None) { 466 | #with(&mut into.#field_name, #field_str, deserializer)?; 467 | }), 468 | ); 469 | } else { 470 | deserialize.extend(quote!(if <#field_ty as FromXml<'xml>>::matches(current, None) { 471 | match <#field_ty as FromXml>::KIND { 472 | Kind::Element => { 473 | <#field_ty as FromXml>::deserialize(&mut into.#field_name, #field_str, deserializer)?; 474 | } 475 | Kind::Scalar => { 476 | <#field_ty as FromXml>::deserialize(&mut into.#field_name, #field_str, deserializer)?; 477 | deserializer.ignore()?; 478 | } 479 | } 480 | })); 481 | } 482 | } 483 | 484 | // Attributes 485 | let ident = &input.ident; 486 | let accumulator = Ident::new(&format!("__{}Accumulator", ident), Span::call_site()); 487 | let generics = meta.xml_generics(borrowed); 488 | 489 | let (xml_impl_generics, xml_ty_generics, _) = generics.split_for_impl(); 490 | let (_, ty_generics, where_clause) = input.generics.split_for_impl(); 491 | let visibility = &input.vis; 492 | 493 | quote!( 494 | impl #xml_impl_generics FromXml<'xml> for #ident #ty_generics #where_clause { 495 | #[inline] 496 | fn matches(id: ::instant_xml::Id<'_>, _: Option<::instant_xml::Id<'_>>) -> bool { 497 | #matches 498 | } 499 | 500 | fn deserialize<'cx>( 501 | into: &mut Self::Accumulator, 502 | _: &'static str, 503 | deserializer: &mut ::instant_xml::Deserializer<'cx, 'xml>, 504 | ) -> ::std::result::Result<(), ::instant_xml::Error> { 505 | use ::instant_xml::Kind; 506 | 507 | let current = deserializer.parent(); 508 | #deserialize 509 | 510 | Ok(()) 511 | } 512 | 513 | type Accumulator = #accumulator #xml_ty_generics; 514 | const KIND: ::instant_xml::Kind = ::instant_xml::Kind::Element; 515 | } 516 | 517 | #visibility struct #accumulator #xml_ty_generics #where_clause { 518 | #acc_field_defs 519 | } 520 | 521 | impl #xml_impl_generics ::instant_xml::Accumulate<#ident #ty_generics> for #accumulator #xml_ty_generics #where_clause { 522 | fn try_done(self, _: &'static str) -> ::std::result::Result<#ident #ty_generics, ::instant_xml::Error> { 523 | Ok(#ident { 524 | #acc_field_inits 525 | }) 526 | } 527 | } 528 | 529 | impl #xml_impl_generics Default for #accumulator #xml_ty_generics #where_clause { 530 | fn default() -> Self { 531 | Self { 532 | #acc_field_defaults 533 | } 534 | } 535 | } 536 | ) 537 | } 538 | 539 | #[allow(clippy::too_many_arguments)] 540 | fn named_field<'a>( 541 | field: &'a syn::Field, 542 | index: usize, 543 | declare_values: &mut TokenStream, 544 | return_val: &mut TokenStream, 545 | tokens: &mut Tokens, 546 | borrowed: &mut BTreeSet, 547 | direct: &mut TokenStream, 548 | mut field_meta: FieldMeta, 549 | type_name: &Ident, 550 | container_meta: &ContainerMeta, 551 | ) -> Result, syn::Error> { 552 | let field_name = field.ident.as_ref().unwrap(); 553 | let field_tag = field_meta.tag; 554 | let default_ns = match &field_meta.ns.uri { 555 | None if field_meta.attribute => &None, 556 | None => &container_meta.ns.uri, 557 | _ => &field_meta.ns.uri, 558 | }; 559 | 560 | let ns = match default_ns { 561 | Some(Namespace::Path(path)) => quote!(#path), 562 | Some(Namespace::Literal(ns)) => quote!(#ns), 563 | None => quote!(""), 564 | }; 565 | 566 | if field_meta.borrow && field_meta.deserialize_with.is_none() { 567 | if is_cow(&field.ty, is_str) { 568 | field_meta.deserialize_with = 569 | Some(Literal::string("::instant_xml::de::borrow_cow_str")); 570 | } else if is_cow(&field.ty, is_slice_u8) { 571 | field_meta.deserialize_with = 572 | Some(Literal::string("::instant_xml::de::borrow_cow_slice_u8")); 573 | } 574 | } 575 | 576 | let mut no_lifetime_type = field.ty.clone(); 577 | discard_lifetimes(&mut no_lifetime_type, borrowed, field_meta.borrow, true); 578 | 579 | let enum_name = Ident::new(&format!("__Value{index}"), Span::call_site()); 580 | if !field_meta.direct { 581 | tokens.r#enum.extend(quote!(#enum_name,)); 582 | 583 | if !tokens.branches.is_empty() { 584 | tokens.branches.extend(quote!(else)); 585 | } 586 | tokens.branches.extend(quote!( 587 | if <#no_lifetime_type as FromXml>::matches(id, Some(Id { ns: #ns, name: #field_tag })) 588 | )); 589 | 590 | tokens.branches.extend(match field_meta.attribute { 591 | true => quote!({ __Attributes::#enum_name }), 592 | false => quote!({ __Elements::#enum_name }), 593 | }); 594 | } 595 | 596 | let val_name = Ident::new(&format!("__value{index}"), Span::call_site()); 597 | declare_values.extend(quote!( 598 | let mut #val_name = <#no_lifetime_type as FromXml>::Accumulator::default(); 599 | )); 600 | 601 | if field_meta.direct { 602 | declare_values.extend(quote!( 603 | // We will synthesize an empty text node when processing a direct 604 | // element. Without this, we can miscategorize an empty tag as 605 | // a missing tag 606 | let mut seen_direct = false; 607 | )); 608 | } 609 | 610 | let deserialize_with = field_meta 611 | .deserialize_with 612 | .map(|with| { 613 | let path = with.to_string(); 614 | syn::parse_str::(path.trim_matches('"')).map_err(|err| { 615 | syn::Error::new( 616 | with.span(), 617 | format!("failed to parse deserialize_with as path: {err}"), 618 | ) 619 | }) 620 | }) 621 | .transpose()?; 622 | 623 | let field_str = format!("{type_name}::{field_name}"); 624 | if !field_meta.attribute { 625 | if let Some(with) = &deserialize_with { 626 | if field_meta.direct { 627 | return Err(syn::Error::new( 628 | field.span(), 629 | "direct attribute is not supported deserialization functions", 630 | )); 631 | } 632 | 633 | tokens.r#match.extend(quote!( 634 | __Elements::#enum_name => { 635 | let mut nested = deserializer.nested(data); 636 | #with(&mut #val_name, #field_str, &mut nested)?; 637 | }, 638 | )); 639 | } else if field_meta.direct { 640 | direct.extend(quote!( 641 | Node::Text(text) => { 642 | seen_direct = true; 643 | let mut nested = deserializer.for_node(Node::Text(text)); 644 | <#no_lifetime_type as FromXml>::deserialize(&mut #val_name, #field_str, &mut nested)?; 645 | } 646 | )); 647 | } else { 648 | tokens.r#match.extend(quote!( 649 | __Elements::#enum_name => match <#no_lifetime_type as FromXml>::KIND { 650 | Kind::Element => { 651 | let mut nested = deserializer.nested(data); 652 | <#no_lifetime_type as FromXml>::deserialize(&mut #val_name, #field_str, &mut nested)?; 653 | } 654 | Kind::Scalar => { 655 | let mut nested = deserializer.nested(data); 656 | <#no_lifetime_type as FromXml>::deserialize(&mut #val_name, #field_str, &mut nested)?; 657 | nested.ignore()?; 658 | } 659 | }, 660 | )); 661 | } 662 | } else { 663 | if field_meta.direct { 664 | return Err(syn::Error::new( 665 | field.span(), 666 | "direct attribute is not supported for attribute fields", 667 | )); 668 | } 669 | 670 | if let Some(with) = &deserialize_with { 671 | tokens.r#match.extend(quote!( 672 | __Attributes::#enum_name => { 673 | let mut nested = deserializer.nested(data); 674 | #with(&mut #val_name, #field_str, &mut nested)?; 675 | }, 676 | )); 677 | } else { 678 | tokens.r#match.extend(quote!( 679 | __Attributes::#enum_name => { 680 | let mut nested = deserializer.for_node(Node::AttributeValue(attr.value)); 681 | let new = <#no_lifetime_type as FromXml>::deserialize(&mut #val_name, #field_str, &mut nested)?; 682 | }, 683 | )); 684 | } 685 | }; 686 | 687 | if !field_meta.direct { 688 | return_val.extend(quote!( 689 | #field_name: #val_name.try_done(#field_str)?, 690 | )); 691 | } else { 692 | return_val.extend(quote!( 693 | #field_name: match #val_name.try_done(#field_str) { 694 | Ok(value) => value, 695 | Err(Error::MissingValue(_)) => { 696 | let mut acc = <#no_lifetime_type as FromXml>::Accumulator::default(); 697 | let mut nested = deserializer.for_node(Node::Text("".into())); 698 | <#no_lifetime_type as FromXml>::deserialize(&mut acc, #field_str, &mut nested)?; 699 | acc.try_done(#field_str)? 700 | } 701 | Err(e) => return Err(e), 702 | } 703 | )); 704 | } 705 | 706 | Ok(FieldData { 707 | field_name, 708 | no_lifetime_type, 709 | deserialize_with, 710 | }) 711 | } 712 | 713 | struct FieldData<'a> { 714 | field_name: &'a Ident, 715 | no_lifetime_type: syn::Type, 716 | deserialize_with: Option, 717 | } 718 | 719 | fn deserialize_tuple_struct( 720 | input: &syn::DeriveInput, 721 | fields: &syn::FieldsUnnamed, 722 | container_meta: ContainerMeta, 723 | ) -> TokenStream { 724 | let mut namespaces_map = quote!(let mut namespaces_map = std::collections::HashMap::new();); 725 | for (k, v) in container_meta.ns.prefixes.iter() { 726 | namespaces_map.extend(quote!( 727 | namespaces_map.insert(#k, #v); 728 | )) 729 | } 730 | 731 | // Varying values 732 | let mut declare_values = TokenStream::new(); 733 | let mut return_val = TokenStream::new(); 734 | let mut borrowed = BTreeSet::new(); 735 | for (index, field) in fields.unnamed.iter().enumerate() { 736 | if !field.attrs.is_empty() { 737 | return syn::Error::new( 738 | field.span(), 739 | "attributes not allowed on tuple struct fields", 740 | ) 741 | .to_compile_error(); 742 | } 743 | 744 | unnamed_field( 745 | field, 746 | index, 747 | &mut declare_values, 748 | &mut return_val, 749 | &mut borrowed, 750 | &input.ident, 751 | ); 752 | } 753 | 754 | let ident = &input.ident; 755 | let name = container_meta.tag(); 756 | let default_namespace = container_meta.default_namespace(); 757 | let generics = container_meta.xml_generics(borrowed); 758 | 759 | let (xml_impl_generics, _, _) = generics.split_for_impl(); 760 | let (_, ty_generics, where_clause) = input.generics.split_for_impl(); 761 | 762 | quote!( 763 | impl #xml_impl_generics FromXml<'xml> for #ident #ty_generics #where_clause { 764 | #[inline] 765 | fn matches(id: ::instant_xml::Id<'_>, field: Option<::instant_xml::Id<'_>>) -> bool { 766 | id == ::instant_xml::Id { ns: #default_namespace, name: #name } 767 | } 768 | 769 | fn deserialize<'cx>( 770 | into: &mut Self::Accumulator, 771 | field: &'static str, 772 | deserializer: &mut ::instant_xml::Deserializer<'cx, 'xml>, 773 | ) -> ::std::result::Result<(), ::instant_xml::Error> { 774 | use ::instant_xml::de::Node; 775 | use ::instant_xml::{Accumulate, Error, FromXml, Id, Kind}; 776 | 777 | #declare_values 778 | deserializer.ignore()?; 779 | 780 | *into = Some(Self(#return_val)); 781 | Ok(()) 782 | } 783 | 784 | type Accumulator = Option; 785 | const KIND: ::instant_xml::Kind = ::instant_xml::Kind::Element; 786 | } 787 | ) 788 | } 789 | 790 | #[allow(clippy::too_many_arguments)] 791 | fn unnamed_field( 792 | field: &syn::Field, 793 | index: usize, 794 | declare_values: &mut TokenStream, 795 | return_val: &mut TokenStream, 796 | borrowed: &mut BTreeSet, 797 | type_name: &Ident, 798 | ) { 799 | let mut no_lifetime_type = field.ty.clone(); 800 | discard_lifetimes(&mut no_lifetime_type, borrowed, false, true); 801 | 802 | let name = Ident::new(&format!("v{index}"), Span::call_site()); 803 | let field_str = format!("{type_name}::{index}"); 804 | declare_values.extend(quote!( 805 | let #name = match <#no_lifetime_type as FromXml>::KIND { 806 | Kind::Element => match deserializer.next() { 807 | Some(Ok(Node::Open(data))) => { 808 | let mut nested = deserializer.nested(data); 809 | let mut value = <#no_lifetime_type as FromXml>::Accumulator::default(); 810 | <#no_lifetime_type as FromXml>::deserialize(&mut value, #field_str, &mut nested)?; 811 | nested.ignore()?; 812 | value 813 | } 814 | Some(Ok(node)) => return Err(Error::UnexpectedNode(format!("{:?}", node))), 815 | Some(Err(e)) => return Err(e), 816 | None => return Err(Error::MissingValue(#field_str)), 817 | } 818 | Kind::Scalar => { 819 | let mut value = <#no_lifetime_type as FromXml>::Accumulator::default(); 820 | <#no_lifetime_type as FromXml>::deserialize(&mut value, #field_str, deserializer)?; 821 | value 822 | } 823 | }; 824 | )); 825 | 826 | return_val.extend(quote!( 827 | #name.try_done(#field_str)?, 828 | )); 829 | } 830 | 831 | fn deserialize_unit_struct(input: &syn::DeriveInput, meta: &ContainerMeta) -> TokenStream { 832 | let ident = &input.ident; 833 | let name = meta.tag(); 834 | let default_namespace = meta.default_namespace(); 835 | let generics = meta.xml_generics(BTreeSet::new()); 836 | 837 | let (xml_impl_generics, _, _) = generics.split_for_impl(); 838 | let (_, ty_generics, where_clause) = input.generics.split_for_impl(); 839 | 840 | quote!( 841 | impl #xml_impl_generics FromXml<'xml> for #ident #ty_generics #where_clause { 842 | #[inline] 843 | fn matches(id: ::instant_xml::Id<'_>, field: Option<::instant_xml::Id<'_>>) -> bool { 844 | id == ::instant_xml::Id { ns: #default_namespace, name: #name } 845 | } 846 | 847 | fn deserialize<'cx>( 848 | into: &mut Self::Accumulator, 849 | field: &'static str, 850 | deserializer: &mut ::instant_xml::Deserializer<'cx, 'xml>, 851 | ) -> ::std::result::Result<(), ::instant_xml::Error> { 852 | deserializer.ignore()?; 853 | *into = Some(Self); 854 | Ok(()) 855 | } 856 | 857 | type Accumulator = Option; 858 | const KIND: ::instant_xml::Kind = ::instant_xml::Kind::Element; 859 | } 860 | ) 861 | } 862 | 863 | fn is_cow(ty: &syn::Type, elem: fn(&syn::Type) -> bool) -> bool { 864 | let path = match ungroup(ty) { 865 | syn::Type::Path(ty) => &ty.path, 866 | _ => { 867 | return false; 868 | } 869 | }; 870 | 871 | let seg = match path.segments.last() { 872 | Some(seg) => seg, 873 | None => { 874 | return false; 875 | } 876 | }; 877 | 878 | let args = match &seg.arguments { 879 | syn::PathArguments::AngleBracketed(bracketed) => &bracketed.args, 880 | _ => { 881 | return false; 882 | } 883 | }; 884 | 885 | seg.ident == "Cow" 886 | && args.len() == 2 887 | && match (&args[0], &args[1]) { 888 | (syn::GenericArgument::Lifetime(_), syn::GenericArgument::Type(arg)) => elem(arg), 889 | _ => false, 890 | } 891 | } 892 | 893 | fn is_str(ty: &syn::Type) -> bool { 894 | is_primitive_type(ty, "str") 895 | } 896 | 897 | fn is_slice_u8(ty: &syn::Type) -> bool { 898 | match ungroup(ty) { 899 | syn::Type::Slice(ty) => is_primitive_type(&ty.elem, "u8"), 900 | _ => false, 901 | } 902 | } 903 | 904 | fn is_primitive_type(ty: &syn::Type, primitive: &str) -> bool { 905 | match ungroup(ty) { 906 | syn::Type::Path(ty) => ty.qself.is_none() && is_primitive_path(&ty.path, primitive), 907 | _ => false, 908 | } 909 | } 910 | 911 | fn is_primitive_path(path: &syn::Path, primitive: &str) -> bool { 912 | path.leading_colon.is_none() 913 | && path.segments.len() == 1 914 | && path.segments[0].ident == primitive 915 | && path.segments[0].arguments.is_empty() 916 | } 917 | 918 | pub fn ungroup(mut ty: &syn::Type) -> &syn::Type { 919 | while let syn::Type::Group(group) = ty { 920 | ty = &group.elem; 921 | } 922 | ty 923 | } 924 | 925 | #[derive(Default)] 926 | struct Tokens { 927 | r#enum: TokenStream, 928 | branches: TokenStream, 929 | r#match: TokenStream, 930 | } 931 | -------------------------------------------------------------------------------- /instant-xml-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use std::collections::BTreeSet; 4 | use std::mem; 5 | 6 | use proc_macro2::{Literal, Span, TokenStream}; 7 | use quote::{quote, ToTokens}; 8 | use syn::spanned::Spanned; 9 | use syn::{parse_macro_input, DeriveInput, Generics}; 10 | 11 | mod case; 12 | use case::RenameRule; 13 | mod de; 14 | mod meta; 15 | use meta::{meta_items, MetaItem, Namespace, NamespaceMeta}; 16 | mod ser; 17 | 18 | #[proc_macro_derive(ToXml, attributes(xml))] 19 | pub fn to_xml(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 20 | let ast = parse_macro_input!(input as syn::DeriveInput); 21 | ser::to_xml(&ast).into() 22 | } 23 | 24 | #[proc_macro_derive(FromXml, attributes(xml))] 25 | pub fn from_xml(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 26 | let ast = parse_macro_input!(input as syn::DeriveInput); 27 | proc_macro::TokenStream::from(de::from_xml(&ast)) 28 | } 29 | 30 | struct ContainerMeta<'input> { 31 | input: &'input DeriveInput, 32 | ns: NamespaceMeta, 33 | rename: Option, 34 | rename_all: RenameRule, 35 | mode: Option, 36 | } 37 | 38 | impl<'input> ContainerMeta<'input> { 39 | fn from_derive(input: &'input syn::DeriveInput) -> Result { 40 | let mut ns = NamespaceMeta::default(); 41 | let mut rename = Default::default(); 42 | let mut rename_all = Default::default(); 43 | let mut mode = None; 44 | 45 | for (item, span) in meta_items(&input.attrs) { 46 | match item { 47 | MetaItem::Ns(namespace) => ns = namespace, 48 | MetaItem::Rename(lit) => rename = Some(lit), 49 | MetaItem::RenameAll(lit) => { 50 | rename_all = match RenameRule::from_str(&lit.to_string()) { 51 | Ok(rule) => rule, 52 | Err(err) => return Err(syn::Error::new(span, err)), 53 | }; 54 | } 55 | MetaItem::Mode(new) => match mode { 56 | None => mode = Some(new), 57 | Some(_) => return Err(syn::Error::new(span, "cannot have two modes")), 58 | }, 59 | _ => { 60 | return Err(syn::Error::new( 61 | span, 62 | "invalid field in container xml attribute", 63 | )) 64 | } 65 | } 66 | } 67 | 68 | Ok(Self { 69 | input, 70 | ns, 71 | rename, 72 | rename_all, 73 | mode, 74 | }) 75 | } 76 | 77 | fn xml_generics(&self, borrowed: BTreeSet) -> Generics { 78 | let mut xml_generics = self.input.generics.clone(); 79 | let mut xml = syn::LifetimeParam::new(syn::Lifetime::new("'xml", Span::call_site())); 80 | xml.bounds.extend(borrowed); 81 | xml_generics.params.push(xml.into()); 82 | 83 | for param in xml_generics.type_params_mut() { 84 | param 85 | .bounds 86 | .push(syn::parse_str("::instant_xml::FromXml<'xml>").unwrap()); 87 | } 88 | 89 | xml_generics 90 | } 91 | 92 | fn tag(&self) -> TokenStream { 93 | match &self.rename { 94 | Some(name) => quote!(#name), 95 | None => self.input.ident.to_string().into_token_stream(), 96 | } 97 | } 98 | 99 | fn default_namespace(&self) -> TokenStream { 100 | match &self.ns.uri { 101 | Some(ns) => quote!(#ns), 102 | None => quote!(""), 103 | } 104 | } 105 | } 106 | 107 | #[derive(Debug, Default)] 108 | struct FieldMeta { 109 | attribute: bool, 110 | borrow: bool, 111 | direct: bool, 112 | ns: NamespaceMeta, 113 | tag: TokenStream, 114 | serialize_with: Option, 115 | deserialize_with: Option, 116 | } 117 | 118 | impl FieldMeta { 119 | fn from_field(input: &syn::Field, container: &ContainerMeta) -> Result { 120 | let field_name = input.ident.as_ref().unwrap(); 121 | let mut meta = FieldMeta { 122 | tag: container 123 | .rename_all 124 | .apply_to_field(field_name) 125 | .into_token_stream(), 126 | ..Default::default() 127 | }; 128 | 129 | for (item, span) in meta_items(&input.attrs) { 130 | match item { 131 | MetaItem::Attribute => meta.attribute = true, 132 | MetaItem::Borrow => meta.borrow = true, 133 | MetaItem::Direct => meta.direct = true, 134 | MetaItem::Ns(ns) => meta.ns = ns, 135 | MetaItem::Rename(lit) => meta.tag = quote!(#lit), 136 | MetaItem::SerializeWith(lit) => meta.serialize_with = Some(lit), 137 | MetaItem::DeserializeWith(lit) => meta.deserialize_with = Some(lit), 138 | MetaItem::RenameAll(_) => { 139 | return Err(syn::Error::new( 140 | span, 141 | "attribute 'rename_all' invalid in field xml attribute", 142 | )) 143 | } 144 | MetaItem::Mode(_) => { 145 | return Err(syn::Error::new(span, "invalid attribute for struct field")); 146 | } 147 | } 148 | } 149 | 150 | Ok(meta) 151 | } 152 | } 153 | 154 | #[derive(Debug, Default)] 155 | struct VariantMeta { 156 | serialize_as: TokenStream, 157 | } 158 | 159 | impl VariantMeta { 160 | fn from_variant( 161 | input: &syn::Variant, 162 | container: &ContainerMeta, 163 | ) -> Result { 164 | if !input.fields.is_empty() { 165 | return Err(syn::Error::new( 166 | input.fields.span(), 167 | "only unit enum variants are permitted!", 168 | )); 169 | } 170 | 171 | let mut rename = None; 172 | for (item, span) in meta_items(&input.attrs) { 173 | match item { 174 | MetaItem::Rename(lit) => rename = Some(lit.to_token_stream()), 175 | _ => { 176 | return Err(syn::Error::new( 177 | span, 178 | "only 'rename' attribute is permitted on enum variants", 179 | )) 180 | } 181 | } 182 | } 183 | 184 | let discriminant = match input.discriminant { 185 | Some(( 186 | _, 187 | syn::Expr::Lit(syn::ExprLit { 188 | lit: syn::Lit::Str(ref lit), 189 | .. 190 | }), 191 | )) => Some(lit.to_token_stream()), 192 | Some(( 193 | _, 194 | syn::Expr::Lit(syn::ExprLit { 195 | lit: syn::Lit::Int(ref lit), 196 | .. 197 | }), 198 | )) => Some(lit.base10_digits().to_token_stream()), 199 | Some((_, ref value)) => { 200 | return Err(syn::Error::new( 201 | value.span(), 202 | "invalid field discriminant value!", 203 | )) 204 | } 205 | None => None, 206 | }; 207 | 208 | if discriminant.is_some() && rename.is_some() { 209 | return Err(syn::Error::new( 210 | input.span(), 211 | "conflicting `rename` attribute and variant discriminant!", 212 | )); 213 | } 214 | 215 | let serialize_as = match rename.or(discriminant) { 216 | Some(lit) => lit.into_token_stream(), 217 | None => container 218 | .rename_all 219 | .apply_to_variant(&input.ident) 220 | .to_token_stream(), 221 | }; 222 | 223 | Ok(VariantMeta { serialize_as }) 224 | } 225 | } 226 | 227 | fn discard_lifetimes( 228 | ty: &mut syn::Type, 229 | borrowed: &mut BTreeSet, 230 | borrow: bool, 231 | top: bool, 232 | ) { 233 | match ty { 234 | syn::Type::Path(ty) => discard_path_lifetimes(ty, borrowed, borrow), 235 | syn::Type::Reference(ty) => { 236 | if top { 237 | // If at the top level, we'll want to borrow from `&'a str` and `&'a [u8]`. 238 | match &*ty.elem { 239 | syn::Type::Path(inner) if top && inner.path.is_ident("str") => { 240 | if let Some(lt) = ty.lifetime.take() { 241 | borrowed.insert(lt); 242 | } 243 | } 244 | syn::Type::Slice(inner) if top => match &*inner.elem { 245 | syn::Type::Path(inner) if inner.path.is_ident("u8") => { 246 | borrowed.extend(ty.lifetime.take()); 247 | } 248 | _ => {} 249 | }, 250 | _ => {} 251 | } 252 | } else if borrow { 253 | // Otherwise, only borrow if the user has requested it. 254 | borrowed.extend(ty.lifetime.take()); 255 | } else { 256 | ty.lifetime = None; 257 | } 258 | 259 | discard_lifetimes(&mut ty.elem, borrowed, borrow, false); 260 | } 261 | _ => {} 262 | } 263 | } 264 | 265 | fn discard_path_lifetimes( 266 | path: &mut syn::TypePath, 267 | borrowed: &mut BTreeSet, 268 | borrow: bool, 269 | ) { 270 | if let Some(q) = &mut path.qself { 271 | discard_lifetimes(&mut q.ty, borrowed, borrow, false); 272 | } 273 | 274 | for segment in &mut path.path.segments { 275 | match &mut segment.arguments { 276 | syn::PathArguments::None => {} 277 | syn::PathArguments::AngleBracketed(args) => { 278 | args.args.iter_mut().for_each(|arg| match arg { 279 | syn::GenericArgument::Lifetime(lt) => { 280 | let lt = mem::replace(lt, syn::Lifetime::new("'_", Span::call_site())); 281 | if borrow { 282 | borrowed.insert(lt); 283 | } 284 | } 285 | syn::GenericArgument::Type(ty) => { 286 | discard_lifetimes(ty, borrowed, borrow, false) 287 | } 288 | _ => {} 289 | }) 290 | } 291 | syn::PathArguments::Parenthesized(args) => args 292 | .inputs 293 | .iter_mut() 294 | .for_each(|ty| discard_lifetimes(ty, borrowed, borrow, false)), 295 | } 296 | } 297 | } 298 | 299 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 300 | enum Mode { 301 | Forward, 302 | Scalar, 303 | Transparent, 304 | } 305 | 306 | #[cfg(test)] 307 | mod tests { 308 | use syn::parse_quote; 309 | 310 | #[test] 311 | fn non_unit_enum_variant_unsupported() { 312 | dbg!(super::ser::to_xml(&parse_quote! { 313 | #[xml(scalar)] 314 | pub enum TestEnum { 315 | Foo(String), 316 | Bar, 317 | Baz 318 | } 319 | }) 320 | .to_string()) 321 | .find("compile_error ! { \"only unit enum variants are permitted!\" }") 322 | .unwrap(); 323 | } 324 | 325 | #[test] 326 | fn non_scalar_enums_unsupported() { 327 | dbg!(super::ser::to_xml(&parse_quote! { 328 | #[xml()] 329 | pub enum TestEnum { 330 | Foo, 331 | Bar, 332 | Baz 333 | } 334 | }) 335 | .to_string()) 336 | .find("compile_error ! { \"missing mode\" }") 337 | .unwrap(); 338 | } 339 | 340 | #[test] 341 | fn scalar_variant_attribute_not_permitted() { 342 | dbg!(super::ser::to_xml(&parse_quote! { 343 | #[xml(scalar)] 344 | pub enum TestEnum { 345 | Foo, 346 | Bar, 347 | #[xml(scalar)] 348 | Baz 349 | } 350 | }) 351 | .to_string()) 352 | .find("compile_error ! { \"only 'rename' attribute is permitted on enum variants\" }") 353 | .unwrap(); 354 | } 355 | 356 | #[test] 357 | fn scalar_discrimintant_must_be_literal() { 358 | assert_eq!( 359 | None, 360 | dbg!(super::ser::to_xml(&parse_quote! { 361 | #[xml(scalar)] 362 | pub enum TestEnum { 363 | Foo = 1, 364 | Bar, 365 | Baz 366 | } 367 | }) 368 | .to_string()) 369 | .find("compile_error ! { \"invalid field discriminant value!\" }") 370 | ); 371 | 372 | dbg!(super::ser::to_xml(&parse_quote! { 373 | #[xml(scalar)] 374 | pub enum TestEnum { 375 | Foo = 1+1, 376 | Bar, 377 | Baz 378 | } 379 | }) 380 | .to_string()) 381 | .find("compile_error ! { \"invalid field discriminant value!\" }") 382 | .unwrap(); 383 | } 384 | 385 | #[test] 386 | fn rename_all_attribute_not_permitted() { 387 | dbg!(super::ser::to_xml(&parse_quote! { 388 | pub struct TestStruct { 389 | #[xml(rename_all = "UPPERCASE")] 390 | field_1: String, 391 | field_2: u8, 392 | } 393 | }) 394 | .to_string()) 395 | .find("compile_error ! { \"attribute 'rename_all' invalid in field xml attribute\" }") 396 | .unwrap(); 397 | 398 | dbg!(super::ser::to_xml(&parse_quote! { 399 | #[xml(scalar)] 400 | pub enum TestEnum { 401 | Foo = 1, 402 | Bar, 403 | #[xml(rename_all = "UPPERCASE")] 404 | Baz 405 | } 406 | }) 407 | .to_string()) 408 | .find("compile_error ! { \"only 'rename' attribute is permitted on enum variants\" }") 409 | .unwrap(); 410 | } 411 | 412 | #[test] 413 | fn bogus_rename_all_not_permitted() { 414 | dbg!(super::ser::to_xml(&parse_quote! { 415 | #[xml(rename_all = "forgetaboutit")] 416 | pub struct TestStruct { 417 | field_1: String, 418 | field_2: u8, 419 | } 420 | }) 421 | .to_string()) 422 | .find("compile_error ! {") 423 | .unwrap(); 424 | } 425 | } 426 | -------------------------------------------------------------------------------- /instant-xml-macros/src/meta.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::fmt; 3 | 4 | use proc_macro2::{Delimiter, Group, Literal, Punct, Span, TokenStream, TokenTree}; 5 | use quote::ToTokens; 6 | use syn::punctuated::Punctuated; 7 | 8 | use super::Mode; 9 | 10 | #[derive(Debug, Default)] 11 | pub(crate) struct NamespaceMeta { 12 | pub(crate) uri: Option, 13 | pub(crate) prefixes: BTreeMap, 14 | } 15 | 16 | impl NamespaceMeta { 17 | fn from_tokens(group: Group) -> Self { 18 | let mut new = NamespaceMeta::default(); 19 | let mut state = NsState::Start; 20 | for tree in group.stream() { 21 | state = match (state, tree) { 22 | (NsState::Start, TokenTree::Literal(lit)) => { 23 | new.uri = Some(Namespace::Literal(lit)); 24 | NsState::Comma 25 | } 26 | (NsState::Start, TokenTree::Punct(punct)) if punct.as_char() == ':' => { 27 | NsState::Path { 28 | colon1: Some(punct), 29 | colon2: None, 30 | path: None, 31 | } 32 | } 33 | (NsState::Start, TokenTree::Ident(id)) => NsState::Path { 34 | colon1: None, 35 | colon2: None, 36 | path: Some(syn::Path::from(id)), 37 | }, 38 | (NsState::Comma, TokenTree::Punct(punct)) if punct.as_char() == ',' => { 39 | NsState::Prefix 40 | } 41 | ( 42 | NsState::Path { 43 | colon1: None, 44 | colon2: None, 45 | path, 46 | }, 47 | TokenTree::Punct(punct), 48 | ) if punct.as_char() == ':' => NsState::Path { 49 | colon1: Some(punct), 50 | colon2: None, 51 | path, 52 | }, 53 | ( 54 | NsState::Path { 55 | colon1: colon1 @ Some(_), 56 | colon2: None, 57 | path, 58 | }, 59 | TokenTree::Punct(punct), 60 | ) if punct.as_char() == ':' => NsState::Path { 61 | colon1, 62 | colon2: Some(punct), 63 | path, 64 | }, 65 | ( 66 | NsState::Path { 67 | colon1: Some(colon1), 68 | colon2: Some(colon2), 69 | path, 70 | }, 71 | TokenTree::Ident(id), 72 | ) => { 73 | let path = match path { 74 | Some(mut path) => { 75 | path.segments.push(syn::PathSegment::from(id)); 76 | path 77 | } 78 | None => { 79 | let mut segments = Punctuated::new(); 80 | segments.push_value(id.into()); 81 | 82 | syn::Path { 83 | leading_colon: Some(syn::Token![::]([ 84 | colon1.span(), 85 | colon2.span(), 86 | ])), 87 | segments, 88 | } 89 | } 90 | }; 91 | 92 | NsState::Path { 93 | colon1: None, 94 | colon2: None, 95 | path: Some(path), 96 | } 97 | } 98 | ( 99 | NsState::Path { 100 | colon1: None, 101 | colon2: None, 102 | path: Some(path), 103 | }, 104 | TokenTree::Punct(punct), 105 | ) if punct.as_char() == ',' => { 106 | new.uri = Some(Namespace::Path(path)); 107 | NsState::Prefix 108 | } 109 | ( 110 | NsState::Path { 111 | colon1: None, 112 | colon2: None, 113 | path: Some(path), 114 | }, 115 | TokenTree::Punct(punct), 116 | ) if punct.as_char() == '=' => { 117 | if path.leading_colon.is_some() { 118 | panic!("prefix cannot be defined on a path in xml attribute"); 119 | } 120 | 121 | if path.segments.len() != 1 { 122 | panic!("prefix key must be a single identifier"); 123 | } 124 | 125 | let segment = path.segments.into_iter().next().unwrap(); 126 | if !segment.arguments.is_empty() { 127 | panic!("prefix key must be a single identifier without arguments"); 128 | } 129 | 130 | NsState::PrefixValue { 131 | prefix: segment.ident.to_string(), 132 | } 133 | } 134 | (NsState::Prefix, TokenTree::Ident(id)) => NsState::Eq { 135 | prefix: id.to_string(), 136 | }, 137 | (NsState::Eq { mut prefix }, TokenTree::Punct(punct)) 138 | if punct.as_char() == '-' || punct.as_char() == '.' => 139 | { 140 | prefix.push(punct.as_char()); 141 | NsState::Eq { prefix } 142 | } 143 | (NsState::Eq { mut prefix }, TokenTree::Ident(id)) => { 144 | prefix.push_str(&id.to_string()); 145 | NsState::Eq { prefix } 146 | } 147 | (NsState::Eq { prefix }, TokenTree::Punct(punct)) if punct.as_char() == '=' => { 148 | NsState::PrefixValue { prefix } 149 | } 150 | (NsState::PrefixValue { prefix }, TokenTree::Literal(lit)) => { 151 | new.prefixes 152 | .insert(prefix.to_string(), Namespace::Literal(lit)); 153 | NsState::Comma 154 | } 155 | (NsState::PrefixValue { prefix }, TokenTree::Punct(punct)) 156 | if punct.as_char() == ':' => 157 | { 158 | NsState::PrefixPath { 159 | prefix, 160 | colon1: Some(punct), 161 | colon2: None, 162 | path: None, 163 | } 164 | } 165 | (NsState::PrefixValue { prefix }, TokenTree::Ident(id)) => NsState::PrefixPath { 166 | prefix, 167 | colon1: None, 168 | colon2: None, 169 | path: Some(syn::Path::from(id)), 170 | }, 171 | ( 172 | NsState::PrefixPath { 173 | prefix, 174 | colon1: None, 175 | colon2: None, 176 | path, 177 | }, 178 | TokenTree::Punct(punct), 179 | ) if punct.as_char() == ':' => NsState::PrefixPath { 180 | prefix, 181 | colon1: Some(punct), 182 | colon2: None, 183 | path, 184 | }, 185 | ( 186 | NsState::PrefixPath { 187 | prefix, 188 | colon1: colon1 @ Some(_), 189 | colon2: None, 190 | path, 191 | }, 192 | TokenTree::Punct(punct), 193 | ) if punct.as_char() == ':' => NsState::PrefixPath { 194 | prefix, 195 | colon1, 196 | colon2: Some(punct), 197 | path, 198 | }, 199 | ( 200 | NsState::PrefixPath { 201 | prefix, 202 | colon1: Some(colon1), 203 | colon2: Some(colon2), 204 | path, 205 | }, 206 | TokenTree::Ident(id), 207 | ) => { 208 | let path = match path { 209 | Some(mut path) => { 210 | path.segments.push(syn::PathSegment::from(id)); 211 | path 212 | } 213 | None => { 214 | let mut segments = Punctuated::new(); 215 | segments.push_value(id.into()); 216 | 217 | syn::Path { 218 | leading_colon: Some(syn::Token![::]([ 219 | colon1.span(), 220 | colon2.span(), 221 | ])), 222 | segments, 223 | } 224 | } 225 | }; 226 | 227 | NsState::PrefixPath { 228 | prefix, 229 | colon1: None, 230 | colon2: None, 231 | path: Some(path), 232 | } 233 | } 234 | ( 235 | NsState::PrefixPath { 236 | prefix, 237 | colon1: None, 238 | colon2: None, 239 | path: Some(path), 240 | }, 241 | TokenTree::Punct(punct), 242 | ) if punct.as_char() == ',' => { 243 | new.prefixes 244 | .insert(prefix.to_string(), Namespace::Path(path)); 245 | NsState::Prefix 246 | } 247 | (state, tree) => { 248 | panic!( 249 | "invalid state transition while parsing ns in xml attribute ({}, {tree})", 250 | state.name() 251 | ) 252 | } 253 | }; 254 | } 255 | 256 | match state { 257 | NsState::Start | NsState::Comma => {} 258 | NsState::Path { 259 | colon1: None, 260 | colon2: None, 261 | path: Some(path), 262 | } => { 263 | new.uri = Some(Namespace::Path(path)); 264 | } 265 | NsState::PrefixPath { 266 | prefix, 267 | colon1: None, 268 | colon2: None, 269 | path: Some(path), 270 | } => { 271 | new.prefixes 272 | .insert(prefix.to_string(), Namespace::Path(path)); 273 | } 274 | state => panic!("invalid ns end state in xml attribute ({})", state.name()), 275 | } 276 | 277 | new 278 | } 279 | } 280 | 281 | pub(crate) fn meta_items(attrs: &[syn::Attribute]) -> Vec<(MetaItem, Span)> { 282 | let list = match attrs.iter().find(|attr| attr.path().is_ident("xml")) { 283 | Some(attr) => match &attr.meta { 284 | syn::Meta::List(list) => list, 285 | _ => panic!("expected list in xml attribute"), 286 | }, 287 | None => return Vec::new(), 288 | }; 289 | 290 | let mut items = Vec::new(); 291 | let mut state = MetaState::Start; 292 | for tree in list.tokens.clone() { 293 | let span = tree.span(); 294 | state = match (state, tree) { 295 | (MetaState::Start, TokenTree::Ident(id)) => { 296 | if id == "attribute" { 297 | items.push((MetaItem::Attribute, span)); 298 | MetaState::Comma 299 | } else if id == "borrow" { 300 | items.push((MetaItem::Borrow, span)); 301 | MetaState::Comma 302 | } else if id == "direct" { 303 | items.push((MetaItem::Direct, span)); 304 | MetaState::Comma 305 | } else if id == "transparent" { 306 | items.push((MetaItem::Mode(Mode::Transparent), span)); 307 | MetaState::Comma 308 | } else if id == "ns" { 309 | MetaState::Ns 310 | } else if id == "rename" { 311 | MetaState::Rename 312 | } else if id == "rename_all" { 313 | MetaState::RenameAll 314 | } else if id == "forward" { 315 | items.push((MetaItem::Mode(Mode::Forward), span)); 316 | MetaState::Comma 317 | } else if id == "scalar" { 318 | items.push((MetaItem::Mode(Mode::Scalar), span)); 319 | MetaState::Comma 320 | } else if id == "serialize_with" { 321 | MetaState::SerializeWith 322 | } else if id == "deserialize_with" { 323 | MetaState::DeserializeWith 324 | } else { 325 | panic!("unexpected key in xml attribute"); 326 | } 327 | } 328 | (MetaState::Comma, TokenTree::Punct(punct)) if punct.as_char() == ',' => { 329 | MetaState::Start 330 | } 331 | (MetaState::Ns, TokenTree::Group(group)) 332 | if group.delimiter() == Delimiter::Parenthesis => 333 | { 334 | items.push((MetaItem::Ns(NamespaceMeta::from_tokens(group)), span)); 335 | MetaState::Comma 336 | } 337 | (MetaState::Rename, TokenTree::Punct(punct)) if punct.as_char() == '=' => { 338 | MetaState::RenameValue 339 | } 340 | (MetaState::RenameValue, TokenTree::Literal(lit)) => { 341 | items.push((MetaItem::Rename(lit), span)); 342 | MetaState::Comma 343 | } 344 | (MetaState::RenameAll, TokenTree::Punct(punct)) if punct.as_char() == '=' => { 345 | MetaState::RenameAllValue 346 | } 347 | (MetaState::RenameAllValue, TokenTree::Literal(lit)) => { 348 | items.push((MetaItem::RenameAll(lit), span)); 349 | MetaState::Comma 350 | } 351 | (MetaState::SerializeWith, TokenTree::Punct(punct)) if punct.as_char() == '=' => { 352 | MetaState::SerializeWithValue 353 | } 354 | (MetaState::SerializeWithValue, TokenTree::Literal(lit)) => { 355 | items.push((MetaItem::SerializeWith(lit), span)); 356 | MetaState::Comma 357 | } 358 | (MetaState::DeserializeWith, TokenTree::Punct(punct)) if punct.as_char() == '=' => { 359 | MetaState::DeserializeWithValue 360 | } 361 | (MetaState::DeserializeWithValue, TokenTree::Literal(lit)) => { 362 | items.push((MetaItem::DeserializeWith(lit), span)); 363 | MetaState::Comma 364 | } 365 | (state, tree) => { 366 | panic!( 367 | "invalid state transition while parsing xml attribute ({}, {tree})", 368 | state.name() 369 | ) 370 | } 371 | }; 372 | } 373 | 374 | items 375 | } 376 | 377 | #[derive(Debug)] 378 | enum MetaState { 379 | Start, 380 | Comma, 381 | Ns, 382 | Rename, 383 | RenameValue, 384 | RenameAll, 385 | RenameAllValue, 386 | SerializeWith, 387 | SerializeWithValue, 388 | DeserializeWith, 389 | DeserializeWithValue, 390 | } 391 | 392 | impl MetaState { 393 | fn name(&self) -> &'static str { 394 | match self { 395 | MetaState::Start => "Start", 396 | MetaState::Comma => "Comma", 397 | MetaState::Ns => "Ns", 398 | MetaState::Rename => "Rename", 399 | MetaState::RenameValue => "RenameValue", 400 | MetaState::RenameAll => "RenameAll", 401 | MetaState::RenameAllValue => "RenameAllValue", 402 | MetaState::SerializeWith => "SerializeWith", 403 | MetaState::SerializeWithValue => "SerializeWithValue", 404 | MetaState::DeserializeWith => "DeserializeWith", 405 | MetaState::DeserializeWithValue => "DeserializeWithValue", 406 | } 407 | } 408 | } 409 | 410 | enum NsState { 411 | Start, 412 | Comma, 413 | Path { 414 | colon1: Option, 415 | colon2: Option, 416 | path: Option, 417 | }, 418 | Prefix, 419 | Eq { 420 | prefix: String, 421 | }, 422 | PrefixValue { 423 | prefix: String, 424 | }, 425 | PrefixPath { 426 | prefix: String, 427 | colon1: Option, 428 | colon2: Option, 429 | path: Option, 430 | }, 431 | } 432 | 433 | impl NsState { 434 | fn name(&self) -> &'static str { 435 | match self { 436 | NsState::Start => "Start", 437 | NsState::Comma => "Comma", 438 | NsState::Path { 439 | colon1, 440 | colon2, 441 | path, 442 | } => match (colon1, colon2, path) { 443 | (None, None, None) => "Path [000]", 444 | (Some(_), None, None) => "Path [100]", 445 | (None, Some(_), None) => "Path [010]", 446 | (None, None, Some(_)) => "Path [001]", 447 | (Some(_), Some(_), None) => "Path [110]", 448 | (None, Some(_), Some(_)) => "Path [011]", 449 | (Some(_), None, Some(_)) => "Path [101]", 450 | (Some(_), Some(_), Some(_)) => "Path [111]", 451 | }, 452 | NsState::Prefix => "Prefix", 453 | NsState::Eq { .. } => "Eq", 454 | NsState::PrefixValue { .. } => "PrefixValue", 455 | NsState::PrefixPath { .. } => "PrefixPath", 456 | } 457 | } 458 | } 459 | 460 | pub(crate) enum Namespace { 461 | Path(syn::Path), 462 | Literal(Literal), 463 | } 464 | 465 | impl ToTokens for Namespace { 466 | fn to_tokens(&self, tokens: &mut TokenStream) { 467 | match self { 468 | Namespace::Path(path) => path.to_tokens(tokens), 469 | Namespace::Literal(lit) => lit.to_tokens(tokens), 470 | } 471 | } 472 | } 473 | 474 | impl fmt::Debug for Namespace { 475 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 476 | match self { 477 | Self::Path(arg0) => f 478 | .debug_tuple("Path") 479 | .field(&arg0.into_token_stream().to_string()) 480 | .finish(), 481 | Self::Literal(arg0) => f.debug_tuple("Literal").field(arg0).finish(), 482 | } 483 | } 484 | } 485 | 486 | #[derive(Debug)] 487 | pub(crate) enum MetaItem { 488 | Attribute, 489 | Borrow, 490 | Direct, 491 | Ns(NamespaceMeta), 492 | Rename(Literal), 493 | Mode(Mode), 494 | RenameAll(Literal), 495 | SerializeWith(Literal), 496 | DeserializeWith(Literal), 497 | } 498 | -------------------------------------------------------------------------------- /instant-xml-macros/src/ser.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | 3 | use proc_macro2::TokenStream; 4 | use quote::{quote, ToTokens}; 5 | use syn::spanned::Spanned; 6 | 7 | use super::{discard_lifetimes, meta_items, ContainerMeta, FieldMeta, Mode, VariantMeta}; 8 | use crate::{case::RenameRule, Namespace}; 9 | 10 | pub fn to_xml(input: &syn::DeriveInput) -> proc_macro2::TokenStream { 11 | let meta = match ContainerMeta::from_derive(input) { 12 | Ok(meta) => meta, 13 | Err(e) => return e.to_compile_error(), 14 | }; 15 | 16 | match (&input.data, meta.mode) { 17 | (syn::Data::Struct(data), None) => serialize_struct(input, data, meta), 18 | (syn::Data::Struct(data), Some(Mode::Transparent)) => { 19 | serialize_inline_struct(input, data, meta) 20 | } 21 | (syn::Data::Enum(data), Some(Mode::Scalar)) => serialize_scalar_enum(input, data, meta), 22 | (syn::Data::Enum(data), Some(Mode::Forward)) => serialize_forward_enum(input, data, meta), 23 | (syn::Data::Struct(_), Some(mode)) => syn::Error::new( 24 | input.span(), 25 | format_args!("{mode:?} mode not allowed on struct type"), 26 | ) 27 | .to_compile_error(), 28 | (syn::Data::Enum(_), Some(mode)) => syn::Error::new( 29 | input.span(), 30 | format_args!("{mode:?} mode not allowed on enum type"), 31 | ) 32 | .to_compile_error(), 33 | (syn::Data::Enum(_), None) => { 34 | syn::Error::new(input.span(), "missing mode").to_compile_error() 35 | } 36 | _ => todo!(), 37 | } 38 | } 39 | 40 | fn serialize_scalar_enum( 41 | input: &syn::DeriveInput, 42 | data: &syn::DataEnum, 43 | meta: ContainerMeta, 44 | ) -> TokenStream { 45 | let ident = &input.ident; 46 | let mut variants = TokenStream::new(); 47 | 48 | for variant in data.variants.iter() { 49 | let meta = match VariantMeta::from_variant(variant, &meta) { 50 | Ok(meta) => meta, 51 | Err(err) => return err.to_compile_error(), 52 | }; 53 | 54 | let v_ident = &variant.ident; 55 | let serialize_as = meta.serialize_as; 56 | variants.extend(quote!(#ident::#v_ident => #serialize_as,)); 57 | } 58 | 59 | let default_namespace = meta.default_namespace(); 60 | 61 | let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); 62 | quote!( 63 | impl #impl_generics ToXml for #ident #ty_generics #where_clause { 64 | fn serialize( 65 | &self, 66 | field: Option<::instant_xml::Id<'_>>, 67 | serializer: &mut instant_xml::Serializer, 68 | ) -> ::std::result::Result<(), instant_xml::Error> { 69 | let prefix = match field { 70 | Some(id) => { 71 | let prefix = serializer.write_start(id.name, #default_namespace)?; 72 | serializer.end_start()?; 73 | Some((prefix, id.name)) 74 | } 75 | None => None, 76 | }; 77 | 78 | serializer.write_str(match self { #variants })?; 79 | if let Some((prefix, name)) = prefix { 80 | serializer.write_close(prefix, name)?; 81 | } 82 | 83 | Ok(()) 84 | } 85 | } 86 | ) 87 | } 88 | 89 | fn serialize_forward_enum( 90 | input: &syn::DeriveInput, 91 | data: &syn::DataEnum, 92 | meta: ContainerMeta, 93 | ) -> TokenStream { 94 | if meta.rename_all != RenameRule::None { 95 | return syn::Error::new( 96 | input.span(), 97 | "rename_all is not allowed on wrapped enum type", 98 | ) 99 | .to_compile_error(); 100 | } 101 | 102 | let ident = &input.ident; 103 | let mut variants = TokenStream::new(); 104 | for variant in data.variants.iter() { 105 | match &variant.fields { 106 | syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {} 107 | _ => { 108 | return syn::Error::new( 109 | input.span(), 110 | "wrapped enum variants must have 1 unnamed field", 111 | ) 112 | .to_compile_error() 113 | } 114 | } 115 | 116 | if !meta_items(&variant.attrs).is_empty() { 117 | return syn::Error::new( 118 | input.span(), 119 | "attributes not allowed on wrapped enum variants", 120 | ) 121 | .to_compile_error(); 122 | } 123 | 124 | let v_ident = &variant.ident; 125 | variants.extend(quote!(#ident::#v_ident(inner) => inner.serialize(None, serializer)?,)); 126 | } 127 | 128 | let default_namespace = meta.default_namespace(); 129 | let cx_len = meta.ns.prefixes.len(); 130 | let mut context = quote!( 131 | let mut new = ::instant_xml::ser::Context::<#cx_len>::default(); 132 | new.default_ns = #default_namespace; 133 | ); 134 | 135 | for (i, (prefix, ns)) in meta.ns.prefixes.iter().enumerate() { 136 | context.extend(quote!( 137 | new.prefixes[#i] = ::instant_xml::ser::Prefix { ns: #ns, prefix: #prefix }; 138 | )); 139 | } 140 | 141 | let mut generics = input.generics.clone(); 142 | for param in generics.type_params_mut() { 143 | param 144 | .bounds 145 | .push(syn::parse_str("::instant_xml::ToXml").unwrap()); 146 | } 147 | 148 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 149 | quote!( 150 | impl #impl_generics ToXml for #ident #ty_generics #where_clause { 151 | fn serialize( 152 | &self, 153 | field: Option<::instant_xml::Id<'_>>, 154 | serializer: &mut instant_xml::Serializer, 155 | ) -> ::std::result::Result<(), instant_xml::Error> { 156 | match self { 157 | #variants 158 | } 159 | 160 | Ok(()) 161 | } 162 | }; 163 | ) 164 | } 165 | 166 | fn serialize_struct( 167 | input: &syn::DeriveInput, 168 | data: &syn::DataStruct, 169 | meta: ContainerMeta, 170 | ) -> proc_macro2::TokenStream { 171 | let mut out = StructOutput::default(); 172 | match &data.fields { 173 | syn::Fields::Named(fields) => { 174 | if let Err(err) = out.named_fields(fields, false, &meta) { 175 | return err; 176 | } 177 | } 178 | syn::Fields::Unnamed(fields) => { 179 | if let Err(err) = out.unnamed_fields(fields, false, &meta) { 180 | return err; 181 | } 182 | } 183 | syn::Fields::Unit => out.body.extend(quote!(serializer.end_empty()?;)), 184 | } 185 | 186 | let default_namespace = meta.default_namespace(); 187 | let cx_len = meta.ns.prefixes.len(); 188 | let mut context = quote!( 189 | let mut new = ::instant_xml::ser::Context::<#cx_len>::default(); 190 | new.default_ns = #default_namespace; 191 | ); 192 | 193 | for (i, (prefix, ns)) in meta.ns.prefixes.iter().enumerate() { 194 | context.extend(quote!( 195 | new.prefixes[#i] = ::instant_xml::ser::Prefix { ns: #ns, prefix: #prefix }; 196 | )); 197 | } 198 | 199 | let mut generics = input.generics.clone(); 200 | for param in generics.type_params_mut() { 201 | param 202 | .bounds 203 | .push(syn::parse_str("::instant_xml::ToXml").unwrap()); 204 | } 205 | 206 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 207 | let tag = meta.tag(); 208 | let ident = &input.ident; 209 | quote!( 210 | impl #impl_generics ToXml for #ident #ty_generics #where_clause { 211 | fn serialize( 212 | &self, 213 | field: Option<::instant_xml::Id<'_>>, 214 | serializer: &mut instant_xml::Serializer, 215 | ) -> ::std::result::Result<(), instant_xml::Error> { 216 | // Start tag 217 | let prefix = serializer.write_start(#tag, #default_namespace)?; 218 | 219 | // Set up element context, this will also emit namespace declarations 220 | #context 221 | let old = serializer.push(new)?; 222 | 223 | // Finalize start element 224 | #out 225 | 226 | serializer.pop(old); 227 | Ok(()) 228 | } 229 | }; 230 | ) 231 | } 232 | 233 | fn serialize_inline_struct( 234 | input: &syn::DeriveInput, 235 | data: &syn::DataStruct, 236 | meta: ContainerMeta, 237 | ) -> proc_macro2::TokenStream { 238 | if !meta.ns.prefixes.is_empty() { 239 | return syn::Error::new( 240 | input.span(), 241 | "inline structs cannot have namespace declarations", 242 | ) 243 | .to_compile_error(); 244 | } else if let Some(ns) = meta.ns.uri { 245 | return syn::Error::new( 246 | ns.span(), 247 | "inline structs cannot have namespace declarations", 248 | ) 249 | .to_compile_error(); 250 | } else if let Some(rename) = meta.rename { 251 | return syn::Error::new(rename.span(), "inline structs cannot be renamed") 252 | .to_compile_error(); 253 | } 254 | 255 | let mut out = StructOutput::default(); 256 | match &data.fields { 257 | syn::Fields::Named(fields) => { 258 | if let Err(err) = out.named_fields(fields, true, &meta) { 259 | return err; 260 | } 261 | } 262 | syn::Fields::Unnamed(fields) => { 263 | if let Err(err) = out.unnamed_fields(fields, true, &meta) { 264 | return err; 265 | } 266 | } 267 | syn::Fields::Unit => out.body.extend(quote!(serializer.end_empty()?;)), 268 | } 269 | 270 | let mut generics = input.generics.clone(); 271 | for param in generics.type_params_mut() { 272 | param 273 | .bounds 274 | .push(syn::parse_str("::instant_xml::ToXml").unwrap()); 275 | } 276 | 277 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 278 | let ident = &input.ident; 279 | quote!( 280 | impl #impl_generics ToXml for #ident #ty_generics #where_clause { 281 | fn serialize( 282 | &self, 283 | field: Option<::instant_xml::Id<'_>>, 284 | serializer: &mut instant_xml::Serializer, 285 | ) -> ::std::result::Result<(), instant_xml::Error> { 286 | #out 287 | Ok(()) 288 | } 289 | }; 290 | ) 291 | } 292 | 293 | #[derive(Default)] 294 | struct StructOutput { 295 | body: TokenStream, 296 | attributes: TokenStream, 297 | borrowed: BTreeSet, 298 | } 299 | 300 | impl StructOutput { 301 | fn named_fields( 302 | &mut self, 303 | fields: &syn::FieldsNamed, 304 | inline: bool, 305 | meta: &ContainerMeta, 306 | ) -> Result<(), proc_macro2::TokenStream> { 307 | let fields = fields 308 | .named 309 | .iter() 310 | .map(|field| FieldMeta::from_field(field, meta).map(|meta| (field, meta))) 311 | .collect::, _>>() 312 | .map_err(|err| err.to_compile_error())?; 313 | 314 | let mut attrs_only = true; 315 | let mut direct = None; 316 | for (field, field_meta) in &fields { 317 | if !field_meta.attribute { 318 | attrs_only = false; 319 | } 320 | 321 | if direct.is_some() { 322 | return Err( 323 | syn::Error::new(field.span(), "direct field must be the last") 324 | .into_compile_error(), 325 | ); 326 | } 327 | 328 | if field_meta.direct { 329 | direct = Some(field.ident.as_ref().unwrap()); 330 | } 331 | } 332 | 333 | if !inline { 334 | self.body.extend(match (attrs_only, direct) { 335 | (true, _) => quote!(serializer.end_empty()?;), 336 | (false, Some(field)) => quote!( 337 | match self.#field.present() { 338 | true => serializer.end_start()?, 339 | false => serializer.end_empty()?, 340 | } 341 | ), 342 | (false, None) => quote!(serializer.end_start()?;), 343 | }) 344 | } 345 | 346 | for (field, field_meta) in fields { 347 | if let Err(err) = self.named_field(field, field_meta, meta) { 348 | return Err(err.to_compile_error()); 349 | } 350 | 351 | if inline && !self.attributes.is_empty() { 352 | return Err(syn::Error::new( 353 | field.span(), 354 | "no attributes allowed on inline structs", 355 | ) 356 | .to_compile_error()); 357 | } 358 | } 359 | 360 | if !inline && !attrs_only { 361 | let tag = meta.tag(); 362 | self.body.extend(match direct { 363 | Some(field) => quote!( 364 | match self.#field.present() { 365 | true => serializer.write_close(prefix, #tag)?, 366 | false => (), 367 | } 368 | ), 369 | None => quote!(serializer.write_close(prefix, #tag)?;), 370 | }); 371 | } 372 | 373 | Ok(()) 374 | } 375 | 376 | fn named_field( 377 | &mut self, 378 | field: &syn::Field, 379 | field_meta: FieldMeta, 380 | meta: &ContainerMeta, 381 | ) -> Result<(), syn::Error> { 382 | let field_name = field.ident.as_ref().unwrap(); 383 | 384 | let tag = field_meta.tag; 385 | let default_ns = match &meta.ns.uri { 386 | Some(ns) => quote!(#ns), 387 | None => quote!(""), 388 | }; 389 | 390 | if field_meta.attribute { 391 | if field_meta.direct { 392 | return Err(syn::Error::new( 393 | field.span(), 394 | "direct attribute is not supported on attributes", 395 | )); 396 | } 397 | 398 | let (ns, error) = match &field_meta.ns.uri { 399 | Some(Namespace::Path(path)) => match path.get_ident() { 400 | Some(path) => (quote!(#path), quote!()), 401 | None => ( 402 | quote!(""), 403 | syn::Error::new( 404 | field_meta.ns.uri.span(), 405 | "attribute namespace must be a prefix identifier", 406 | ) 407 | .into_compile_error(), 408 | ), 409 | }, 410 | Some(Namespace::Literal(_)) => ( 411 | quote!(""), 412 | syn::Error::new( 413 | field_meta.ns.uri.span(), 414 | "attribute namespace must be a prefix identifier", 415 | ) 416 | .into_compile_error(), 417 | ), 418 | None => (default_ns, quote!()), 419 | }; 420 | 421 | self.attributes.extend(quote!( 422 | #error 423 | if self.#field_name.present() { 424 | serializer.write_attr(#tag, #ns, &self.#field_name)?; 425 | } 426 | )); 427 | return Ok(()); 428 | } 429 | 430 | let ns = match field_meta.ns.uri { 431 | Some(ref ns) => quote!(#ns), 432 | None => default_ns, 433 | }; 434 | 435 | let mut no_lifetime_type = field.ty.clone(); 436 | discard_lifetimes(&mut no_lifetime_type, &mut self.borrowed, false, true); 437 | if let Some(with) = field_meta.serialize_with { 438 | if field_meta.direct { 439 | return Err(syn::Error::new( 440 | field.span(), 441 | "direct serialization is not supported with `serialize_with`", 442 | )); 443 | } 444 | 445 | let path = with.to_string(); 446 | let path = syn::parse_str::(path.trim_matches('"')).map_err(|err| { 447 | syn::Error::new( 448 | with.span(), 449 | format!("failed to parse serialize_with as path: {err}"), 450 | ) 451 | })?; 452 | 453 | self.body 454 | .extend(quote!(#path(&self.#field_name, serializer)?;)); 455 | return Ok(()); 456 | } else if field_meta.direct { 457 | self.body.extend(quote!( 458 | <#no_lifetime_type as ToXml>::serialize( 459 | &self.#field_name, None, serializer 460 | )?; 461 | )); 462 | } else { 463 | self.body.extend(quote!( 464 | <#no_lifetime_type as ToXml>::serialize( 465 | &self.#field_name, 466 | Some(::instant_xml::Id { ns: #ns, name: #tag }), 467 | serializer, 468 | )?; 469 | )); 470 | } 471 | 472 | Ok(()) 473 | } 474 | 475 | fn unnamed_fields( 476 | &mut self, 477 | fields: &syn::FieldsUnnamed, 478 | inline: bool, 479 | meta: &ContainerMeta, 480 | ) -> Result<(), proc_macro2::TokenStream> { 481 | if !inline { 482 | self.body.extend(quote!(serializer.end_start()?;)); 483 | } 484 | 485 | for (index, field) in fields.unnamed.iter().enumerate() { 486 | if let Err(err) = self.unnamed_field(field, index) { 487 | return Err(err.to_compile_error()); 488 | } 489 | } 490 | 491 | if !inline { 492 | let tag = meta.tag(); 493 | self.body 494 | .extend(quote!(serializer.write_close(prefix, #tag)?;)); 495 | } 496 | 497 | Ok(()) 498 | } 499 | 500 | fn unnamed_field(&mut self, field: &syn::Field, index: usize) -> Result<(), syn::Error> { 501 | if !field.attrs.is_empty() { 502 | return Err(syn::Error::new( 503 | field.span(), 504 | "unnamed fields cannot have attributes", 505 | )); 506 | } 507 | 508 | let mut no_lifetime_type = field.ty.clone(); 509 | discard_lifetimes(&mut no_lifetime_type, &mut self.borrowed, false, true); 510 | let index = syn::Index::from(index); 511 | self.body.extend(quote!( 512 | self.#index.serialize(None, serializer)?; 513 | )); 514 | 515 | Ok(()) 516 | } 517 | } 518 | 519 | impl ToTokens for StructOutput { 520 | fn to_tokens(&self, tokens: &mut TokenStream) { 521 | self.attributes.to_tokens(tokens); 522 | self.body.to_tokens(tokens); 523 | } 524 | } 525 | -------------------------------------------------------------------------------- /instant-xml/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "instant-xml" 3 | version = "0.6.0" 4 | edition = "2021" 5 | rust-version = "1.62" 6 | workspace = ".." 7 | license = "Apache-2.0 OR MIT" 8 | description = "A more rigorous way to map XML to Rust types" 9 | documentation = "https://docs.rs/instant-xml" 10 | repository = "https://github.com/djc/instant-xml" 11 | readme = "../README.md" 12 | 13 | [dependencies] 14 | chrono = { version = "0.4.23", optional = true } 15 | macros = { package = "instant-xml-macros", version = "0.6", path = "../instant-xml-macros" } 16 | thiserror = "2.0.3" 17 | xmlparser = "0.13.3" 18 | 19 | [dev-dependencies] 20 | bencher = "0.1.5" 21 | serde = { version = "1", features = ["derive"] } 22 | similar-asserts = "1.4.2" 23 | 24 | [[bench]] 25 | name = "decode" 26 | harness = false 27 | -------------------------------------------------------------------------------- /instant-xml/benches/decode.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use bencher::Bencher; 4 | use bencher::{benchmark_group, benchmark_main}; 5 | use instant_xml::{from_str, FromXml}; 6 | 7 | fn decode_short_ascii(bench: &mut Bencher) { 8 | let xml = "foobar"; 9 | bench.iter(|| { 10 | from_str::(xml).unwrap(); 11 | }) 12 | } 13 | 14 | fn decode_longer_ascii(bench: &mut Bencher) { 15 | let mut xml = String::with_capacity(4096); 16 | xml.push_str(""); 17 | for _ in 0..64 { 18 | xml.push_str("abcdefghijklmnopqrstuvwxyz"); 19 | xml.push_str("ABCDEFGHIJKLMNOPQRSTUVWXYZ"); 20 | xml.push_str("0123456789"); 21 | } 22 | xml.push_str(""); 23 | 24 | bench.iter(|| { 25 | from_str::(&xml).unwrap(); 26 | }) 27 | } 28 | 29 | fn decode_short_escaped(bench: &mut Bencher) { 30 | let xml = "foo & bar"; 31 | bench.iter(|| { 32 | from_str::(xml).unwrap(); 33 | }) 34 | } 35 | 36 | fn decode_longer_escaped(bench: &mut Bencher) { 37 | let mut xml = String::with_capacity(4096); 38 | xml.push_str(""); 39 | for _ in 0..64 { 40 | xml.push_str("abcdefghijklmnopqrstuvwxyz"); 41 | xml.push_str("ABCDEFGHIJKLMNOPQRSTUVWXYZ"); 42 | xml.push_str("0123456789"); 43 | xml.push_str("""); 44 | } 45 | xml.push_str(""); 46 | 47 | bench.iter(|| { 48 | from_str::(&xml).unwrap(); 49 | }) 50 | } 51 | 52 | #[derive(Debug, FromXml)] 53 | struct Element<'a> { 54 | #[allow(dead_code)] 55 | inner: Cow<'a, str>, 56 | } 57 | 58 | benchmark_group!( 59 | benches, 60 | decode_short_ascii, 61 | decode_longer_ascii, 62 | decode_short_escaped, 63 | decode_longer_escaped, 64 | ); 65 | benchmark_main!(benches); 66 | -------------------------------------------------------------------------------- /instant-xml/src/de.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::collections::{BTreeMap, VecDeque}; 3 | use std::str::{self, FromStr}; 4 | 5 | use xmlparser::{ElementEnd, Token, Tokenizer}; 6 | 7 | use crate::impls::CowStrAccumulator; 8 | use crate::{Error, Id}; 9 | 10 | pub struct Deserializer<'cx, 'xml> { 11 | pub(crate) local: &'xml str, 12 | prefix: Option<&'xml str>, 13 | level: usize, 14 | done: bool, 15 | context: &'cx mut Context<'xml>, 16 | } 17 | 18 | impl<'cx, 'xml> Deserializer<'cx, 'xml> { 19 | pub(crate) fn new(element: Element<'xml>, context: &'cx mut Context<'xml>) -> Self { 20 | let level = context.stack.len(); 21 | Self { 22 | local: element.local, 23 | prefix: element.prefix, 24 | level, 25 | done: false, 26 | context, 27 | } 28 | } 29 | 30 | pub fn take_str(&mut self) -> Result>, Error> { 31 | loop { 32 | match self.next() { 33 | Some(Ok(Node::AttributeValue(s))) => return Ok(Some(s)), 34 | Some(Ok(Node::Text(s))) => return Ok(Some(s)), 35 | Some(Ok(Node::Attribute(_))) => continue, 36 | Some(Ok(node)) => return Err(Error::ExpectedScalar(format!("{node:?}"))), 37 | Some(Err(e)) => return Err(e), 38 | None => return Ok(None), 39 | } 40 | } 41 | } 42 | 43 | pub fn nested<'a>(&'a mut self, element: Element<'xml>) -> Deserializer<'a, 'xml> 44 | where 45 | 'cx: 'a, 46 | { 47 | Deserializer::new(element, self.context) 48 | } 49 | 50 | pub fn ignore(&mut self) -> Result<(), Error> { 51 | loop { 52 | match self.next() { 53 | Some(Err(e)) => return Err(e), 54 | Some(Ok(Node::Open(element))) => { 55 | let mut nested = self.nested(element); 56 | nested.ignore()?; 57 | } 58 | Some(_) => continue, 59 | None => return Ok(()), 60 | } 61 | } 62 | } 63 | 64 | pub fn for_node<'a>(&'a mut self, node: Node<'xml>) -> Deserializer<'a, 'xml> 65 | where 66 | 'cx: 'a, 67 | { 68 | self.context.records.push_front(node); 69 | Deserializer { 70 | local: self.local, 71 | prefix: self.prefix, 72 | level: self.level, 73 | done: self.done, 74 | context: self.context, 75 | } 76 | } 77 | 78 | pub fn parent(&self) -> Id<'xml> { 79 | Id { 80 | ns: match self.prefix { 81 | Some(ns) => self.context.lookup(ns).unwrap(), 82 | None => self.context.default_ns(), 83 | }, 84 | name: self.local, 85 | } 86 | } 87 | 88 | #[inline] 89 | pub fn element_id(&self, element: &Element<'xml>) -> Result, Error> { 90 | self.context.element_id(element) 91 | } 92 | 93 | #[inline] 94 | pub fn attribute_id(&self, attr: &Attribute<'xml>) -> Result, Error> { 95 | self.context.attribute_id(attr) 96 | } 97 | } 98 | 99 | impl<'xml> Iterator for Deserializer<'_, 'xml> { 100 | type Item = Result, Error>; 101 | 102 | fn next(&mut self) -> Option { 103 | if self.done { 104 | return None; 105 | } 106 | 107 | let (prefix, local) = match self.context.next() { 108 | Some(Ok(Node::Close { prefix, local })) => (prefix, local), 109 | item => return item, 110 | }; 111 | 112 | if self.context.stack.len() == self.level - 1 113 | && local == self.local 114 | && prefix == self.prefix 115 | { 116 | self.done = true; 117 | return None; 118 | } 119 | 120 | Some(Err(Error::UnexpectedState("close element mismatch"))) 121 | } 122 | } 123 | 124 | pub(crate) struct Context<'xml> { 125 | parser: Tokenizer<'xml>, 126 | stack: Vec>, 127 | records: VecDeque>, 128 | } 129 | 130 | impl<'xml> Context<'xml> { 131 | pub(crate) fn new(input: &'xml str) -> Result<(Self, Element<'xml>), Error> { 132 | let mut new = Self { 133 | parser: Tokenizer::from(input), 134 | stack: Vec::new(), 135 | records: VecDeque::new(), 136 | }; 137 | 138 | let root = match new.next() { 139 | Some(result) => match result? { 140 | Node::Open(element) => element, 141 | _ => return Err(Error::UnexpectedState("first node does not open element")), 142 | }, 143 | None => return Err(Error::UnexpectedEndOfStream), 144 | }; 145 | 146 | Ok((new, root)) 147 | } 148 | 149 | pub(crate) fn element_id(&self, element: &Element<'xml>) -> Result, Error> { 150 | Ok(Id { 151 | ns: match (element.default_ns, element.prefix) { 152 | (_, Some(prefix)) => match self.lookup(prefix) { 153 | Some(ns) => ns, 154 | None => return Err(Error::UnknownPrefix(prefix.to_owned())), 155 | }, 156 | (Some(ns), None) => ns, 157 | (None, None) => self.default_ns(), 158 | }, 159 | name: element.local, 160 | }) 161 | } 162 | 163 | fn attribute_id(&self, attr: &Attribute<'xml>) -> Result, Error> { 164 | Ok(Id { 165 | ns: match attr.prefix { 166 | Some(ns) => self 167 | .lookup(ns) 168 | .ok_or_else(|| Error::UnknownPrefix(ns.to_owned()))?, 169 | None => "", 170 | }, 171 | name: attr.local, 172 | }) 173 | } 174 | 175 | fn default_ns(&self) -> &'xml str { 176 | self.stack 177 | .iter() 178 | .rev() 179 | .find_map(|level| level.default_ns) 180 | .unwrap_or("") 181 | } 182 | 183 | fn lookup(&self, prefix: &str) -> Option<&'xml str> { 184 | // The prefix xml is by definition bound to the namespace 185 | // name http://www.w3.org/XML/1998/namespace 186 | // See https://www.w3.org/TR/xml-names/#ns-decl 187 | if prefix == "xml" { 188 | return Some("http://www.w3.org/XML/1998/namespace"); 189 | } 190 | 191 | self.stack 192 | .iter() 193 | .rev() 194 | .find_map(|level| level.prefixes.get(prefix).copied()) 195 | } 196 | } 197 | 198 | impl<'xml> Iterator for Context<'xml> { 199 | type Item = Result, Error>; 200 | 201 | fn next(&mut self) -> Option { 202 | if let Some(record) = self.records.pop_front() { 203 | if let Node::Close { .. } = &record { 204 | self.stack.pop(); 205 | } 206 | return Some(Ok(record)); 207 | } 208 | 209 | loop { 210 | match self.parser.next()? { 211 | Ok(Token::ElementStart { prefix, local, .. }) => { 212 | let prefix = prefix.as_str(); 213 | self.stack.push(Level { 214 | local: local.as_str(), 215 | prefix: match prefix.is_empty() { 216 | true => None, 217 | false => Some(prefix), 218 | }, 219 | default_ns: None, 220 | prefixes: BTreeMap::new(), 221 | }); 222 | } 223 | Ok(Token::ElementEnd { end, .. }) => match end { 224 | ElementEnd::Open => { 225 | let level = match self.stack.last() { 226 | Some(level) => level, 227 | None => { 228 | return Some(Err(Error::UnexpectedState( 229 | "opening element with no parent", 230 | ))) 231 | } 232 | }; 233 | 234 | let element = Element { 235 | local: level.local, 236 | prefix: level.prefix, 237 | default_ns: level.default_ns, 238 | }; 239 | 240 | return Some(Ok(Node::Open(element))); 241 | } 242 | ElementEnd::Close(prefix, v) => { 243 | let level = match self.stack.pop() { 244 | Some(level) => level, 245 | None => { 246 | return Some(Err(Error::UnexpectedState( 247 | "closing element without parent", 248 | ))) 249 | } 250 | }; 251 | 252 | let prefix = match prefix.is_empty() { 253 | true => None, 254 | false => Some(prefix.as_str()), 255 | }; 256 | 257 | match v.as_str() == level.local && prefix == level.prefix { 258 | true => { 259 | return Some(Ok(Node::Close { 260 | prefix, 261 | local: level.local, 262 | })) 263 | } 264 | false => { 265 | return Some(Err(Error::UnexpectedState("close element mismatch"))) 266 | } 267 | } 268 | } 269 | ElementEnd::Empty => { 270 | let level = match self.stack.last() { 271 | Some(level) => level, 272 | None => { 273 | return Some(Err(Error::UnexpectedState( 274 | "opening element with no parent", 275 | ))) 276 | } 277 | }; 278 | 279 | self.records.push_back(Node::Close { 280 | prefix: level.prefix, 281 | local: level.local, 282 | }); 283 | 284 | let element = Element { 285 | local: level.local, 286 | prefix: level.prefix, 287 | default_ns: level.default_ns, 288 | }; 289 | 290 | return Some(Ok(Node::Open(element))); 291 | } 292 | }, 293 | Ok(Token::Attribute { 294 | prefix, 295 | local, 296 | value, 297 | .. 298 | }) => { 299 | if prefix.is_empty() && local.as_str() == "xmlns" { 300 | match self.stack.last_mut() { 301 | Some(level) => level.default_ns = Some(value.as_str()), 302 | None => { 303 | return Some(Err(Error::UnexpectedState( 304 | "attribute without element context", 305 | ))) 306 | } 307 | } 308 | } else if prefix.as_str() == "xmlns" { 309 | match self.stack.last_mut() { 310 | Some(level) => { 311 | level.prefixes.insert(local.as_str(), value.as_str()); 312 | } 313 | None => { 314 | return Some(Err(Error::UnexpectedState( 315 | "attribute without element context", 316 | ))) 317 | } 318 | } 319 | } else { 320 | let value = match decode(value.as_str()) { 321 | Ok(value) => value, 322 | Err(e) => return Some(Err(e)), 323 | }; 324 | 325 | self.records.push_back(Node::Attribute(Attribute { 326 | prefix: match prefix.is_empty() { 327 | true => None, 328 | false => Some(prefix.as_str()), 329 | }, 330 | local: local.as_str(), 331 | value, 332 | })); 333 | } 334 | } 335 | Ok(Token::Text { text }) => { 336 | return Some(decode(text.as_str()).map(Node::Text)); 337 | } 338 | Ok(Token::Cdata { text, .. }) => { 339 | return Some(Ok(Node::Text(Cow::Borrowed(text.as_str())))); 340 | } 341 | Ok(token @ Token::Declaration { .. }) => { 342 | if !self.stack.is_empty() { 343 | return Some(Err(Error::UnexpectedToken(format!("{token:?}")))); 344 | } 345 | } 346 | Ok(Token::Comment { .. }) => continue, 347 | Ok(token) => return Some(Err(Error::UnexpectedToken(format!("{token:?}")))), 348 | Err(e) => return Some(Err(Error::Parse(e))), 349 | } 350 | } 351 | } 352 | } 353 | 354 | pub fn borrow_cow_str<'a, 'xml: 'a>( 355 | into: &mut CowStrAccumulator<'xml, 'a>, 356 | field: &'static str, 357 | deserializer: &mut Deserializer<'_, 'xml>, 358 | ) -> Result<(), Error> { 359 | if into.inner.is_some() { 360 | return Err(Error::DuplicateValue(field)); 361 | } 362 | 363 | match deserializer.take_str()? { 364 | Some(value) => into.inner = Some(value), 365 | None => return Ok(()), 366 | }; 367 | 368 | deserializer.ignore()?; 369 | Ok(()) 370 | } 371 | 372 | pub fn borrow_cow_slice_u8<'xml>( 373 | into: &mut Option>, 374 | field: &'static str, 375 | deserializer: &mut Deserializer<'_, 'xml>, 376 | ) -> Result<(), Error> { 377 | if into.is_some() { 378 | return Err(Error::DuplicateValue(field)); 379 | } 380 | 381 | if let Some(value) = deserializer.take_str()? { 382 | *into = Some(match value { 383 | Cow::Borrowed(v) => Cow::Borrowed(v.as_bytes()), 384 | Cow::Owned(v) => Cow::Owned(v.into_bytes()), 385 | }); 386 | } 387 | 388 | deserializer.ignore()?; 389 | Ok(()) 390 | } 391 | 392 | fn decode(input: &str) -> Result, Error> { 393 | let mut result = String::with_capacity(input.len()); 394 | let (mut state, mut last_end) = (DecodeState::Normal, 0); 395 | for (i, &b) in input.as_bytes().iter().enumerate() { 396 | // use a state machine to find entities 397 | state = match (state, b) { 398 | (DecodeState::Normal, b'&') => DecodeState::Entity([0; 6], 0), 399 | (DecodeState::Normal, _) => DecodeState::Normal, 400 | (DecodeState::Entity(chars, len), b';') => { 401 | let decoded = match &chars[..len] { 402 | [b'a', b'm', b'p'] => '&', 403 | [b'a', b'p', b'o', b's'] => '\'', 404 | [b'g', b't'] => '>', 405 | [b'l', b't'] => '<', 406 | [b'q', b'u', b'o', b't'] => '"', 407 | [b'#', b'x' | b'X', hex @ ..] => { 408 | // Hexadecimal character reference e.g. "|" -> '|' 409 | str::from_utf8(hex) 410 | .ok() 411 | .and_then(|hex_str| u32::from_str_radix(hex_str, 16).ok()) 412 | .and_then(char::from_u32) 413 | .filter(valid_xml_character) 414 | .ok_or_else(|| { 415 | Error::InvalidEntity( 416 | String::from_utf8_lossy(&chars[..len]).into_owned(), 417 | ) 418 | })? 419 | } 420 | [b'#', decimal @ ..] => { 421 | // Decimal character reference e.g. "Ӓ" -> 'Ӓ' 422 | str::from_utf8(decimal) 423 | .ok() 424 | .and_then(|decimal_str| u32::from_str(decimal_str).ok()) 425 | .and_then(char::from_u32) 426 | .filter(valid_xml_character) 427 | .ok_or_else(|| { 428 | Error::InvalidEntity( 429 | String::from_utf8_lossy(&chars[..len]).into_owned(), 430 | ) 431 | })? 432 | } 433 | _ => { 434 | return Err(Error::InvalidEntity( 435 | String::from_utf8_lossy(&chars[..len]).into_owned(), 436 | )) 437 | } 438 | }; 439 | 440 | let start = i - (len + 1); // current position - (length of entity characters + 1 for '&') 441 | if last_end < start { 442 | // Unwrap should be safe: `last_end` and `start` must be at character boundaries. 443 | result.push_str(input.get(last_end..start).unwrap()); 444 | } 445 | 446 | last_end = i + 1; 447 | result.push(decoded); 448 | DecodeState::Normal 449 | } 450 | (DecodeState::Entity(mut chars, len), b) => { 451 | if len >= 6 { 452 | let mut bytes = Vec::with_capacity(7); 453 | bytes.extend(&chars[..len]); 454 | bytes.push(b); 455 | return Err(Error::InvalidEntity( 456 | String::from_utf8_lossy(&bytes).into_owned(), 457 | )); 458 | } 459 | 460 | chars[len] = b; 461 | DecodeState::Entity(chars, len + 1) 462 | } 463 | }; 464 | } 465 | 466 | // Unterminated entity (& without ;) at end of input 467 | if let DecodeState::Entity(chars, len) = state { 468 | return Err(Error::InvalidEntity( 469 | String::from_utf8_lossy(&chars[..len]).into_owned(), 470 | )); 471 | } 472 | 473 | Ok(match result.is_empty() { 474 | true => Cow::Borrowed(input), 475 | false => { 476 | // Unwrap should be safe: `last_end` and `input.len()` must be at character boundaries. 477 | result.push_str(input.get(last_end..input.len()).unwrap()); 478 | Cow::Owned(result) 479 | } 480 | }) 481 | } 482 | 483 | #[derive(Debug)] 484 | enum DecodeState { 485 | Normal, 486 | Entity([u8; 6], usize), 487 | } 488 | 489 | /// Valid character ranges per 490 | fn valid_xml_character(c: &char) -> bool { 491 | matches!(c, '\u{9}' | '\u{A}' | '\u{D}' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}') 492 | } 493 | 494 | #[derive(Debug)] 495 | pub enum Node<'xml> { 496 | Attribute(Attribute<'xml>), 497 | AttributeValue(Cow<'xml, str>), 498 | Close { 499 | prefix: Option<&'xml str>, 500 | local: &'xml str, 501 | }, 502 | Text(Cow<'xml, str>), 503 | Open(Element<'xml>), 504 | } 505 | 506 | #[derive(Debug)] 507 | pub struct Element<'xml> { 508 | local: &'xml str, 509 | default_ns: Option<&'xml str>, 510 | prefix: Option<&'xml str>, 511 | } 512 | 513 | #[derive(Debug)] 514 | struct Level<'xml> { 515 | local: &'xml str, 516 | prefix: Option<&'xml str>, 517 | default_ns: Option<&'xml str>, 518 | prefixes: BTreeMap<&'xml str, &'xml str>, 519 | } 520 | 521 | #[derive(Debug)] 522 | pub struct Attribute<'xml> { 523 | pub prefix: Option<&'xml str>, 524 | pub local: &'xml str, 525 | pub value: Cow<'xml, str>, 526 | } 527 | 528 | #[cfg(test)] 529 | mod tests { 530 | use super::*; 531 | 532 | #[test] 533 | fn test_decode() { 534 | decode_ok("foo", "foo"); 535 | decode_ok("foo & bar", "foo & bar"); 536 | decode_ok("foo < bar", "foo < bar"); 537 | decode_ok("foo > bar", "foo > bar"); 538 | decode_ok("foo " bar", "foo \" bar"); 539 | decode_ok("foo ' bar", "foo ' bar"); 540 | decode_ok("foo &lt; bar", "foo < bar"); 541 | decode_ok("& foo", "& foo"); 542 | decode_ok("foo &", "foo &"); 543 | decode_ok("cbdtéda&sü", "cbdtéda&sü"); 544 | // Decimal character references 545 | decode_ok("Ӓ", "Ӓ"); 546 | decode_ok("foo bar", "foo \t bar"); 547 | decode_ok("foo | bar", "foo | bar"); 548 | decode_ok("foo Ӓ bar", "foo Ӓ bar"); 549 | // Hexadecimal character references 550 | decode_ok("Ä", "Ä"); 551 | decode_ok("Ä", "Ä"); 552 | decode_ok("foo bar", "foo \t bar"); 553 | decode_ok("foo | bar", "foo | bar"); 554 | decode_ok("foo Ä bar", "foo Ä bar"); 555 | decode_ok("foo Ä bar", "foo Ä bar"); 556 | decode_ok("foo პ bar", "foo პ bar"); 557 | 558 | decode_err("&"); 559 | decode_err("&#"); 560 | decode_err("&#;"); 561 | decode_err("foo&"); 562 | decode_err("&bar"); 563 | decode_err("&foo;"); 564 | decode_err("&foobar;"); 565 | decode_err("cbdtéd&ü"); 566 | } 567 | 568 | fn decode_ok(input: &str, expected: &'static str) { 569 | assert_eq!(super::decode(input).unwrap(), expected, "{input:?}"); 570 | } 571 | 572 | fn decode_err(input: &str) { 573 | assert!(super::decode(input).is_err(), "{input:?}"); 574 | } 575 | } 576 | -------------------------------------------------------------------------------- /instant-xml/src/impls.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::fmt; 3 | use std::net::IpAddr; 4 | use std::str; 5 | use std::str::FromStr; 6 | use std::{any::type_name, marker::PhantomData}; 7 | 8 | #[cfg(feature = "chrono")] 9 | use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc}; 10 | 11 | use crate::{Accumulate, Deserializer, Error, FromXml, Id, Kind, Serializer, ToXml}; 12 | 13 | // Deserializer 14 | 15 | pub fn from_xml_str( 16 | into: &mut Option, 17 | field: &'static str, 18 | deserializer: &mut Deserializer<'_, '_>, 19 | ) -> Result<(), Error> { 20 | if into.is_some() { 21 | return Err(Error::DuplicateValue(field)); 22 | } 23 | 24 | let value = match deserializer.take_str()? { 25 | Some(value) => value, 26 | None => return Ok(()), 27 | }; 28 | 29 | match T::from_str(value.as_ref()) { 30 | Ok(value) => { 31 | *into = Some(value); 32 | Ok(()) 33 | } 34 | Err(_) => Err(Error::UnexpectedValue(format!( 35 | "unable to parse {} from `{value}`", 36 | type_name::() 37 | ))), 38 | } 39 | } 40 | 41 | struct FromXmlStr(T); 42 | 43 | impl<'xml, T: FromStr> FromXml<'xml> for FromXmlStr { 44 | #[inline] 45 | fn matches(id: Id<'_>, field: Option>) -> bool { 46 | match field { 47 | Some(field) => id == field, 48 | None => false, 49 | } 50 | } 51 | 52 | fn deserialize( 53 | into: &mut Self::Accumulator, 54 | field: &'static str, 55 | deserializer: &mut Deserializer<'_, 'xml>, 56 | ) -> Result<(), Error> { 57 | if into.is_some() { 58 | return Err(Error::DuplicateValue(field)); 59 | } 60 | 61 | let value = match deserializer.take_str()? { 62 | Some(value) => value, 63 | None => return Ok(()), 64 | }; 65 | 66 | match T::from_str(value.as_ref()) { 67 | Ok(value) => { 68 | *into = Some(FromXmlStr(value)); 69 | Ok(()) 70 | } 71 | Err(_) => Err(Error::UnexpectedValue(format!( 72 | "unable to parse {} from `{value}` for {field}", 73 | type_name::() 74 | ))), 75 | } 76 | } 77 | 78 | type Accumulator = Option>; 79 | const KIND: Kind = Kind::Scalar; 80 | } 81 | 82 | impl<'xml> FromXml<'xml> for bool { 83 | #[inline] 84 | fn matches(id: Id<'_>, field: Option>) -> bool { 85 | match field { 86 | Some(field) => id == field, 87 | None => false, 88 | } 89 | } 90 | 91 | fn deserialize<'cx>( 92 | into: &mut Self::Accumulator, 93 | field: &'static str, 94 | deserializer: &mut Deserializer<'cx, 'xml>, 95 | ) -> Result<(), Error> { 96 | if into.is_some() { 97 | return Err(Error::DuplicateValue(field)); 98 | } 99 | 100 | let value = match deserializer.take_str()? { 101 | Some(value) => value, 102 | None => return Ok(()), 103 | }; 104 | 105 | let value = match value.as_ref() { 106 | "true" | "1" => true, 107 | "false" | "0" => false, 108 | val => { 109 | return Err(Error::UnexpectedValue(format!( 110 | "unable to parse bool from '{val}' for {field}" 111 | ))) 112 | } 113 | }; 114 | 115 | *into = Some(value); 116 | Ok(()) 117 | } 118 | 119 | type Accumulator = Option; 120 | const KIND: Kind = Kind::Scalar; 121 | } 122 | 123 | // Serializer 124 | 125 | pub fn display_to_xml( 126 | value: &impl fmt::Display, 127 | field: Option>, 128 | serializer: &mut Serializer, 129 | ) -> Result<(), Error> { 130 | DisplayToXml(value).serialize(field, serializer) 131 | } 132 | 133 | struct DisplayToXml<'a, T: fmt::Display>(pub &'a T); 134 | 135 | impl ToXml for DisplayToXml<'_, T> 136 | where 137 | T: fmt::Display, 138 | { 139 | fn serialize( 140 | &self, 141 | field: Option>, 142 | serializer: &mut Serializer, 143 | ) -> Result<(), Error> { 144 | let prefix = match field { 145 | Some(id) => { 146 | let prefix = serializer.write_start(id.name, id.ns)?; 147 | serializer.end_start()?; 148 | Some((prefix, id.name)) 149 | } 150 | None => None, 151 | }; 152 | 153 | serializer.write_str(self.0)?; 154 | if let Some((prefix, name)) = prefix { 155 | serializer.write_close(prefix, name)?; 156 | } 157 | 158 | Ok(()) 159 | } 160 | } 161 | 162 | macro_rules! to_xml_for_number { 163 | ($typ:ty) => { 164 | impl ToXml for $typ { 165 | fn serialize( 166 | &self, 167 | field: Option>, 168 | serializer: &mut Serializer, 169 | ) -> Result<(), Error> { 170 | DisplayToXml(self).serialize(field, serializer) 171 | } 172 | } 173 | }; 174 | } 175 | 176 | macro_rules! from_xml_for_number { 177 | ($typ:ty) => { 178 | impl<'xml> FromXml<'xml> for $typ { 179 | #[inline] 180 | fn matches(id: Id<'_>, field: Option>) -> bool { 181 | match field { 182 | Some(field) => id == field, 183 | None => false, 184 | } 185 | } 186 | 187 | fn deserialize<'cx>( 188 | into: &mut Self::Accumulator, 189 | field: &'static str, 190 | deserializer: &mut Deserializer<'cx, 'xml>, 191 | ) -> Result<(), Error> { 192 | if into.is_some() { 193 | return Err(Error::DuplicateValue(field)); 194 | } 195 | 196 | let mut value = None; 197 | FromXmlStr::::deserialize(&mut value, field, deserializer)?; 198 | if let Some(value) = value { 199 | *into = Some(value.0); 200 | } 201 | 202 | Ok(()) 203 | } 204 | 205 | type Accumulator = Option; 206 | const KIND: Kind = Kind::Scalar; 207 | } 208 | }; 209 | } 210 | 211 | from_xml_for_number!(i8); 212 | from_xml_for_number!(i16); 213 | from_xml_for_number!(i32); 214 | from_xml_for_number!(i64); 215 | from_xml_for_number!(isize); 216 | from_xml_for_number!(u8); 217 | from_xml_for_number!(u16); 218 | from_xml_for_number!(u32); 219 | from_xml_for_number!(u64); 220 | from_xml_for_number!(usize); 221 | from_xml_for_number!(f32); 222 | from_xml_for_number!(f64); 223 | 224 | impl<'xml> FromXml<'xml> for char { 225 | #[inline] 226 | fn matches(id: Id<'_>, field: Option>) -> bool { 227 | match field { 228 | Some(field) => id == field, 229 | None => false, 230 | } 231 | } 232 | 233 | fn deserialize<'cx>( 234 | into: &mut Self::Accumulator, 235 | field: &'static str, 236 | deserializer: &mut Deserializer<'cx, 'xml>, 237 | ) -> Result<(), Error> { 238 | if into.is_some() { 239 | return Err(Error::DuplicateValue(field)); 240 | } 241 | 242 | let mut value = None; 243 | FromXmlStr::::deserialize(&mut value, field, deserializer)?; 244 | if let Some(value) = value { 245 | *into = Some(value.0); 246 | } 247 | 248 | Ok(()) 249 | } 250 | 251 | type Accumulator = Option; 252 | const KIND: Kind = Kind::Scalar; 253 | } 254 | 255 | impl<'xml> FromXml<'xml> for String { 256 | #[inline] 257 | fn matches(id: Id<'_>, field: Option>) -> bool { 258 | match field { 259 | Some(field) => id == field, 260 | None => false, 261 | } 262 | } 263 | 264 | fn deserialize<'cx>( 265 | into: &mut Self::Accumulator, 266 | field: &'static str, 267 | deserializer: &mut Deserializer<'cx, 'xml>, 268 | ) -> Result<(), Error> { 269 | if into.is_some() { 270 | return Err(Error::DuplicateValue(field)); 271 | } 272 | 273 | *into = Some(match deserializer.take_str()? { 274 | Some(value) => value.into_owned(), 275 | None => String::new(), 276 | }); 277 | 278 | Ok(()) 279 | } 280 | 281 | type Accumulator = Option; 282 | const KIND: Kind = Kind::Scalar; 283 | } 284 | 285 | impl<'xml, 'a> FromXml<'xml> for Cow<'a, str> { 286 | #[inline] 287 | fn matches(id: Id<'_>, field: Option>) -> bool { 288 | match field { 289 | Some(field) => id == field, 290 | None => false, 291 | } 292 | } 293 | 294 | fn deserialize( 295 | into: &mut Self::Accumulator, 296 | field: &'static str, 297 | deserializer: &mut Deserializer<'_, 'xml>, 298 | ) -> Result<(), Error> { 299 | if into.inner.is_some() { 300 | return Err(Error::DuplicateValue(field)); 301 | } 302 | 303 | into.inner = Some(match deserializer.take_str()? { 304 | Some(value) => value.into_owned().into(), 305 | None => "".into(), 306 | }); 307 | 308 | Ok(()) 309 | } 310 | 311 | type Accumulator = CowStrAccumulator<'xml, 'a>; 312 | const KIND: Kind = Kind::Scalar; 313 | } 314 | 315 | #[derive(Default)] 316 | pub struct CowStrAccumulator<'xml, 'a> { 317 | pub(crate) inner: Option>, 318 | marker: PhantomData<&'xml str>, 319 | } 320 | 321 | impl<'a> Accumulate> for CowStrAccumulator<'_, 'a> { 322 | fn try_done(self, field: &'static str) -> Result, Error> { 323 | match self.inner { 324 | Some(inner) => Ok(inner), 325 | None => Err(Error::MissingValue(field)), 326 | } 327 | } 328 | } 329 | 330 | // The `FromXml` implementation for `Cow<'a, [T]>` always builds a `Cow::Owned`: 331 | // it is not possible to deserialize into a `Cow::Borrowed` because there's no 332 | // place to store the originating slice (length only known at run-time). 333 | impl<'xml, T: FromXml<'xml>> FromXml<'xml> for Cow<'_, [T]> 334 | where 335 | [T]: ToOwned>, 336 | { 337 | #[inline] 338 | fn matches(id: Id<'_>, field: Option>) -> bool { 339 | T::matches(id, field) 340 | } 341 | 342 | fn deserialize( 343 | into: &mut Self::Accumulator, 344 | field: &'static str, 345 | deserializer: &mut Deserializer<'_, 'xml>, 346 | ) -> Result<(), Error> { 347 | let mut value = T::Accumulator::default(); 348 | T::deserialize(&mut value, field, deserializer)?; 349 | into.push(value.try_done(field)?); 350 | Ok(()) 351 | } 352 | 353 | type Accumulator = Vec; 354 | const KIND: Kind = Kind::Scalar; 355 | } 356 | 357 | impl ToXml for Cow<'_, [T]> 358 | where 359 | [T]: ToOwned, 360 | { 361 | fn serialize( 362 | &self, 363 | field: Option>, 364 | serializer: &mut Serializer, 365 | ) -> Result<(), Error> { 366 | self.as_ref().serialize(field, serializer) 367 | } 368 | } 369 | 370 | impl<'xml, T: FromXml<'xml>> FromXml<'xml> for Option { 371 | #[inline] 372 | fn matches(id: Id<'_>, field: Option>) -> bool { 373 | T::matches(id, field) 374 | } 375 | 376 | fn deserialize<'cx>( 377 | into: &mut Self::Accumulator, 378 | field: &'static str, 379 | deserializer: &mut Deserializer<'cx, 'xml>, 380 | ) -> Result<(), Error> { 381 | ::deserialize(&mut into.value, field, deserializer)?; 382 | Ok(()) 383 | } 384 | 385 | type Accumulator = OptionAccumulator; 386 | const KIND: Kind = ::KIND; 387 | } 388 | 389 | pub struct OptionAccumulator> { 390 | value: A, 391 | marker: PhantomData, 392 | } 393 | 394 | impl> OptionAccumulator { 395 | pub fn get_mut(&mut self) -> &mut A { 396 | &mut self.value 397 | } 398 | } 399 | 400 | impl> Default for OptionAccumulator { 401 | fn default() -> Self { 402 | Self { 403 | value: A::default(), 404 | marker: PhantomData, 405 | } 406 | } 407 | } 408 | 409 | impl> Accumulate> for OptionAccumulator { 410 | fn try_done(self, field: &'static str) -> Result, Error> { 411 | match self.value.try_done(field) { 412 | Ok(value) => Ok(Some(value)), 413 | Err(_) => Ok(None), 414 | } 415 | } 416 | } 417 | 418 | to_xml_for_number!(i8); 419 | to_xml_for_number!(i16); 420 | to_xml_for_number!(i32); 421 | to_xml_for_number!(i64); 422 | to_xml_for_number!(isize); 423 | to_xml_for_number!(u8); 424 | to_xml_for_number!(u16); 425 | to_xml_for_number!(u32); 426 | to_xml_for_number!(u64); 427 | to_xml_for_number!(usize); 428 | to_xml_for_number!(f32); 429 | to_xml_for_number!(f64); 430 | 431 | impl ToXml for bool { 432 | fn serialize( 433 | &self, 434 | field: Option>, 435 | serializer: &mut Serializer, 436 | ) -> Result<(), Error> { 437 | let value = match self { 438 | true => "true", 439 | false => "false", 440 | }; 441 | 442 | DisplayToXml(&value).serialize(field, serializer) 443 | } 444 | } 445 | 446 | impl ToXml for String { 447 | fn serialize( 448 | &self, 449 | field: Option>, 450 | serializer: &mut Serializer, 451 | ) -> Result<(), Error> { 452 | DisplayToXml(&encode(self)?).serialize(field, serializer) 453 | } 454 | } 455 | 456 | impl ToXml for char { 457 | fn serialize( 458 | &self, 459 | field: Option>, 460 | serializer: &mut Serializer, 461 | ) -> Result<(), Error> { 462 | let mut tmp = [0u8; 4]; 463 | DisplayToXml(&encode(&*self.encode_utf8(&mut tmp))?).serialize(field, serializer) 464 | } 465 | } 466 | 467 | impl ToXml for str { 468 | fn serialize( 469 | &self, 470 | field: Option>, 471 | serializer: &mut Serializer, 472 | ) -> Result<(), Error> { 473 | DisplayToXml(&encode(self)?).serialize(field, serializer) 474 | } 475 | } 476 | 477 | impl ToXml for Cow<'_, str> { 478 | fn serialize( 479 | &self, 480 | field: Option>, 481 | serializer: &mut Serializer, 482 | ) -> Result<(), Error> { 483 | DisplayToXml(&encode(self)?).serialize(field, serializer) 484 | } 485 | } 486 | 487 | impl ToXml for Option { 488 | fn serialize( 489 | &self, 490 | field: Option>, 491 | serializer: &mut Serializer, 492 | ) -> Result<(), Error> { 493 | match self { 494 | Some(v) => v.serialize(field, serializer), 495 | None => Ok(()), 496 | } 497 | } 498 | 499 | fn present(&self) -> bool { 500 | self.is_some() 501 | } 502 | } 503 | 504 | impl ToXml for Box { 505 | fn serialize( 506 | &self, 507 | field: Option>, 508 | serializer: &mut Serializer, 509 | ) -> Result<(), Error> { 510 | self.as_ref().serialize(field, serializer) 511 | } 512 | } 513 | 514 | impl<'xml, T: FromXml<'xml>> FromXml<'xml> for Box { 515 | #[inline] 516 | fn matches(id: Id<'_>, field: Option>) -> bool { 517 | T::matches(id, field) 518 | } 519 | 520 | fn deserialize<'cx>( 521 | into: &mut Self::Accumulator, 522 | field: &'static str, 523 | deserializer: &mut Deserializer<'cx, 'xml>, 524 | ) -> Result<(), Error> { 525 | if into.is_some() { 526 | return Err(Error::DuplicateValue(field)); 527 | } 528 | 529 | let mut value = T::Accumulator::default(); 530 | T::deserialize(&mut value, field, deserializer)?; 531 | *into = Some(Box::new(value.try_done(field)?)); 532 | 533 | Ok(()) 534 | } 535 | 536 | type Accumulator = Option; 537 | const KIND: Kind = T::KIND; 538 | } 539 | 540 | fn encode(input: &str) -> Result, Error> { 541 | let mut result = String::with_capacity(input.len()); 542 | let mut last_end = 0; 543 | for (start, c) in input.char_indices() { 544 | let to = match c { 545 | '&' => "&", 546 | '"' => """, 547 | '<' => "<", 548 | '>' => ">", 549 | '\'' => "'", 550 | _ => continue, 551 | }; 552 | result.push_str(input.get(last_end..start).unwrap()); 553 | result.push_str(to); 554 | last_end = start + 1; 555 | } 556 | 557 | if result.is_empty() { 558 | return Ok(Cow::Borrowed(input)); 559 | } 560 | 561 | result.push_str(input.get(last_end..input.len()).unwrap()); 562 | Ok(Cow::Owned(result)) 563 | } 564 | 565 | impl<'xml, T: FromXml<'xml>> FromXml<'xml> for Vec { 566 | #[inline] 567 | fn matches(id: Id<'_>, field: Option>) -> bool { 568 | T::matches(id, field) 569 | } 570 | 571 | fn deserialize<'cx>( 572 | into: &mut Self::Accumulator, 573 | field: &'static str, 574 | deserializer: &mut Deserializer<'cx, 'xml>, 575 | ) -> Result<(), Error> { 576 | let mut value = T::Accumulator::default(); 577 | T::deserialize(&mut value, field, deserializer)?; 578 | into.push(value.try_done(field)?); 579 | Ok(()) 580 | } 581 | 582 | type Accumulator = Vec; 583 | const KIND: Kind = T::KIND; 584 | } 585 | 586 | impl ToXml for Vec { 587 | fn serialize( 588 | &self, 589 | field: Option>, 590 | serializer: &mut Serializer, 591 | ) -> Result<(), Error> { 592 | self.as_slice().serialize(field, serializer) 593 | } 594 | } 595 | 596 | impl ToXml for [T] { 597 | fn serialize( 598 | &self, 599 | field: Option>, 600 | serializer: &mut Serializer, 601 | ) -> Result<(), Error> { 602 | for i in self { 603 | i.serialize(field, serializer)?; 604 | } 605 | 606 | Ok(()) 607 | } 608 | } 609 | 610 | #[cfg(feature = "chrono")] 611 | impl ToXml for DateTime { 612 | fn serialize( 613 | &self, 614 | field: Option>, 615 | serializer: &mut Serializer, 616 | ) -> Result<(), Error> { 617 | let prefix = match field { 618 | Some(id) => { 619 | let prefix = serializer.write_start(id.name, id.ns)?; 620 | serializer.end_start()?; 621 | Some((prefix, id.name)) 622 | } 623 | None => None, 624 | }; 625 | 626 | serializer.write_str(&self.to_rfc3339())?; 627 | if let Some((prefix, name)) = prefix { 628 | serializer.write_close(prefix, name)?; 629 | } 630 | 631 | Ok(()) 632 | } 633 | } 634 | 635 | #[cfg(feature = "chrono")] 636 | impl<'xml> FromXml<'xml> for DateTime { 637 | #[inline] 638 | fn matches(id: Id<'_>, field: Option>) -> bool { 639 | match field { 640 | Some(field) => id == field, 641 | None => false, 642 | } 643 | } 644 | 645 | fn deserialize<'cx>( 646 | into: &mut Self::Accumulator, 647 | field: &'static str, 648 | deserializer: &mut Deserializer<'cx, 'xml>, 649 | ) -> Result<(), Error> { 650 | if into.is_some() { 651 | return Err(Error::DuplicateValue(field)); 652 | } 653 | 654 | let value = match deserializer.take_str()? { 655 | Some(value) => value, 656 | None => return Ok(()), 657 | }; 658 | 659 | match DateTime::parse_from_rfc3339(value.as_ref()) { 660 | Ok(dt) if dt.timezone().utc_minus_local() == 0 => { 661 | *into = Some(dt.with_timezone(&Utc)); 662 | Ok(()) 663 | } 664 | _ => Err(Error::Other("invalid date/time".into())), 665 | } 666 | } 667 | 668 | type Accumulator = Option; 669 | const KIND: Kind = Kind::Scalar; 670 | } 671 | 672 | #[cfg(feature = "chrono")] 673 | impl ToXml for NaiveDateTime { 674 | fn serialize( 675 | &self, 676 | field: Option>, 677 | serializer: &mut Serializer, 678 | ) -> Result<(), Error> { 679 | let prefix = match field { 680 | Some(id) => { 681 | let prefix = serializer.write_start(id.name, id.ns)?; 682 | serializer.end_start()?; 683 | Some((prefix, id.name)) 684 | } 685 | None => None, 686 | }; 687 | 688 | serializer.write_str(&self.format("%Y-%m-%dT%H:%M:%S%.f"))?; 689 | if let Some((prefix, name)) = prefix { 690 | serializer.write_close(prefix, name)?; 691 | } 692 | 693 | Ok(()) 694 | } 695 | } 696 | 697 | #[cfg(feature = "chrono")] 698 | impl<'xml> FromXml<'xml> for NaiveDateTime { 699 | fn matches(id: Id<'_>, field: Option>) -> bool { 700 | match field { 701 | Some(field) => id == field, 702 | None => false, 703 | } 704 | } 705 | 706 | fn deserialize<'cx>( 707 | into: &mut Self::Accumulator, 708 | field: &'static str, 709 | deserializer: &mut Deserializer<'cx, 'xml>, 710 | ) -> Result<(), Error> { 711 | if into.is_some() { 712 | return Err(Error::DuplicateValue(field)); 713 | } 714 | 715 | let value = match deserializer.take_str()? { 716 | Some(value) => value, 717 | None => return Ok(()), 718 | }; 719 | 720 | match NaiveDateTime::parse_from_str(value.as_ref(), "%Y-%m-%dT%H:%M:%S%.f") { 721 | Ok(dt) => { 722 | *into = Some(dt); 723 | Ok(()) 724 | } 725 | _ => Err(Error::Other("invalid date/time".into())), 726 | } 727 | } 728 | 729 | type Accumulator = Option; 730 | 731 | const KIND: Kind = Kind::Scalar; 732 | } 733 | 734 | #[cfg(feature = "chrono")] 735 | impl ToXml for NaiveDate { 736 | fn serialize( 737 | &self, 738 | field: Option>, 739 | serializer: &mut Serializer, 740 | ) -> Result<(), Error> { 741 | let prefix = match field { 742 | Some(id) => { 743 | let prefix = serializer.write_start(id.name, id.ns)?; 744 | serializer.end_start()?; 745 | Some((prefix, id.name)) 746 | } 747 | None => None, 748 | }; 749 | 750 | serializer.write_str(&self)?; 751 | if let Some((prefix, name)) = prefix { 752 | serializer.write_close(prefix, name)?; 753 | } 754 | 755 | Ok(()) 756 | } 757 | } 758 | 759 | #[cfg(feature = "chrono")] 760 | impl<'xml> FromXml<'xml> for NaiveDate { 761 | #[inline] 762 | fn matches(id: Id<'_>, field: Option>) -> bool { 763 | match field { 764 | Some(field) => id == field, 765 | None => false, 766 | } 767 | } 768 | 769 | fn deserialize<'cx>( 770 | into: &mut Self::Accumulator, 771 | field: &'static str, 772 | deserializer: &mut Deserializer<'cx, 'xml>, 773 | ) -> Result<(), Error> { 774 | if into.is_some() { 775 | return Err(Error::DuplicateValue(field)); 776 | } 777 | 778 | let value = match deserializer.take_str()? { 779 | Some(value) => value, 780 | None => return Ok(()), 781 | }; 782 | 783 | match NaiveDate::parse_from_str(value.as_ref(), "%Y-%m-%d") { 784 | Ok(d) => { 785 | *into = Some(d); 786 | Ok(()) 787 | } 788 | _ => Err(Error::Other("invalid date/time".into())), 789 | } 790 | } 791 | 792 | type Accumulator = Option; 793 | const KIND: Kind = Kind::Scalar; 794 | } 795 | 796 | impl<'xml> FromXml<'xml> for () { 797 | #[inline] 798 | fn matches(id: Id<'_>, field: Option>) -> bool { 799 | match field { 800 | Some(field) => id == field, 801 | None => false, 802 | } 803 | } 804 | 805 | fn deserialize<'cx>( 806 | into: &mut Self::Accumulator, 807 | _: &'static str, 808 | _: &mut Deserializer<'cx, 'xml>, 809 | ) -> Result<(), Error> { 810 | *into = Some(()); 811 | Ok(()) 812 | } 813 | 814 | type Accumulator = Option; 815 | const KIND: Kind = Kind::Scalar; 816 | } 817 | 818 | impl ToXml for IpAddr { 819 | fn serialize( 820 | &self, 821 | field: Option>, 822 | serializer: &mut Serializer, 823 | ) -> Result<(), Error> { 824 | DisplayToXml(self).serialize(field, serializer) 825 | } 826 | } 827 | 828 | impl<'xml> FromXml<'xml> for IpAddr { 829 | #[inline] 830 | fn matches(id: Id<'_>, field: Option>) -> bool { 831 | match field { 832 | Some(field) => id == field, 833 | None => false, 834 | } 835 | } 836 | 837 | fn deserialize<'cx>( 838 | into: &mut Self::Accumulator, 839 | field: &'static str, 840 | deserializer: &mut Deserializer<'cx, 'xml>, 841 | ) -> Result<(), Error> { 842 | if into.is_some() { 843 | return Err(Error::DuplicateValue(field)); 844 | } 845 | 846 | let mut value = None; 847 | FromXmlStr::::deserialize(&mut value, field, deserializer)?; 848 | if let Some(value) = value { 849 | *into = Some(value.0); 850 | } 851 | 852 | Ok(()) 853 | } 854 | 855 | type Accumulator = Option; 856 | const KIND: Kind = Kind::Scalar; 857 | } 858 | 859 | #[cfg(test)] 860 | mod tests { 861 | use super::*; 862 | 863 | #[test] 864 | fn encode_unicode() { 865 | let input = "Iñtërnâ&tiônàlizætiøn"; 866 | assert_eq!(encode(input).unwrap(), "Iñtërnâ&tiônàlizætiøn"); 867 | } 868 | } 869 | -------------------------------------------------------------------------------- /instant-xml/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, fmt}; 2 | 3 | use thiserror::Error; 4 | 5 | pub use macros::{FromXml, ToXml}; 6 | 7 | #[doc(hidden)] 8 | pub mod de; 9 | mod impls; 10 | use de::Context; 11 | pub use de::Deserializer; 12 | pub use impls::{display_to_xml, from_xml_str, OptionAccumulator}; 13 | #[doc(hidden)] 14 | pub mod ser; 15 | pub use ser::Serializer; 16 | 17 | pub trait ToXml { 18 | fn serialize( 19 | &self, 20 | field: Option>, 21 | serializer: &mut Serializer, 22 | ) -> Result<(), Error>; 23 | 24 | fn present(&self) -> bool { 25 | true 26 | } 27 | } 28 | 29 | impl ToXml for &T { 30 | fn serialize( 31 | &self, 32 | field: Option>, 33 | serializer: &mut Serializer, 34 | ) -> Result<(), Error> { 35 | (*self).serialize(field, serializer) 36 | } 37 | } 38 | 39 | pub trait FromXml<'xml>: Sized { 40 | fn matches(id: Id<'_>, field: Option>) -> bool; 41 | 42 | fn deserialize<'cx>( 43 | into: &mut Self::Accumulator, 44 | field: &'static str, 45 | deserializer: &mut Deserializer<'cx, 'xml>, 46 | ) -> Result<(), Error>; 47 | 48 | type Accumulator: Accumulate; 49 | const KIND: Kind; 50 | } 51 | 52 | /// A type implementing `Accumulate` is used to accumulate a value of type `T`. 53 | pub trait Accumulate: Default { 54 | fn try_done(self, field: &'static str) -> Result; 55 | } 56 | 57 | impl Accumulate for Option { 58 | fn try_done(self, field: &'static str) -> Result { 59 | self.ok_or(Error::MissingValue(field)) 60 | } 61 | } 62 | 63 | impl Accumulate> for Vec { 64 | fn try_done(self, _: &'static str) -> Result, Error> { 65 | Ok(self) 66 | } 67 | } 68 | 69 | impl<'a, T> Accumulate> for Vec 70 | where 71 | [T]: ToOwned>, 72 | { 73 | fn try_done(self, _: &'static str) -> Result, Error> { 74 | Ok(Cow::Owned(self)) 75 | } 76 | } 77 | 78 | impl Accumulate> for Option { 79 | fn try_done(self, _: &'static str) -> Result, Error> { 80 | Ok(self) 81 | } 82 | } 83 | 84 | pub fn from_str<'xml, T: FromXml<'xml>>(input: &'xml str) -> Result { 85 | let (mut context, root) = Context::new(input)?; 86 | let id = context.element_id(&root)?; 87 | 88 | if !T::matches(id, None) { 89 | return Err(Error::UnexpectedValue(match id.ns.is_empty() { 90 | true => format!("unexpected root element {:?}", id.name), 91 | false => format!( 92 | "unexpected root element {:?} in namespace {:?}", 93 | id.name, id.ns 94 | ), 95 | })); 96 | } 97 | 98 | let mut value = T::Accumulator::default(); 99 | T::deserialize( 100 | &mut value, 101 | "", 102 | &mut Deserializer::new(root, &mut context), 103 | )?; 104 | value.try_done("") 105 | } 106 | 107 | pub fn to_string(value: &(impl ToXml + ?Sized)) -> Result { 108 | let mut output = String::new(); 109 | to_writer(value, &mut output)?; 110 | Ok(output) 111 | } 112 | 113 | pub fn to_writer( 114 | value: &(impl ToXml + ?Sized), 115 | output: &mut (impl fmt::Write + ?Sized), 116 | ) -> Result<(), Error> { 117 | value.serialize(None, &mut Serializer::new(output)) 118 | } 119 | 120 | pub trait FromXmlOwned: for<'xml> FromXml<'xml> {} 121 | 122 | impl FromXmlOwned for T where T: for<'xml> FromXml<'xml> {} 123 | 124 | #[derive(Clone, Debug, Eq, Error, PartialEq)] 125 | pub enum Error { 126 | #[error("format: {0}")] 127 | Format(#[from] fmt::Error), 128 | #[error("invalid entity: {0}")] 129 | InvalidEntity(String), 130 | #[error("parse: {0}")] 131 | Parse(#[from] xmlparser::Error), 132 | #[error("other: {0}")] 133 | Other(std::string::String), 134 | #[error("unexpected end of stream")] 135 | UnexpectedEndOfStream, 136 | #[error("unexpected value: '{0}'")] 137 | UnexpectedValue(String), 138 | #[error("unexpected tag: {0}")] 139 | UnexpectedTag(String), 140 | #[error("missing tag")] 141 | MissingTag, 142 | #[error("missing value: {0}")] 143 | MissingValue(&'static str), 144 | #[error("unexpected token: {0}")] 145 | UnexpectedToken(String), 146 | #[error("unknown prefix: {0}")] 147 | UnknownPrefix(String), 148 | #[error("unexpected node: {0}")] 149 | UnexpectedNode(String), 150 | #[error("unexpected state: {0}")] 151 | UnexpectedState(&'static str), 152 | #[error("expected scalar, found {0}")] 153 | ExpectedScalar(String), 154 | #[error("duplicate value for {0}")] 155 | DuplicateValue(&'static str), 156 | } 157 | 158 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 159 | pub enum Kind { 160 | Scalar, 161 | Element, 162 | } 163 | 164 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 165 | pub struct Id<'a> { 166 | pub ns: &'a str, 167 | pub name: &'a str, 168 | } 169 | -------------------------------------------------------------------------------- /instant-xml/src/ser.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map::Entry; 2 | use std::collections::HashMap; 3 | use std::fmt::{self}; 4 | use std::mem; 5 | 6 | use super::Error; 7 | use crate::ToXml; 8 | 9 | pub struct Serializer<'xml, W: fmt::Write + ?Sized> { 10 | output: &'xml mut W, 11 | /// Map namespace keys to prefixes. 12 | /// 13 | /// The prefix map is updated using `Context` types that are held on the 14 | /// stack in the relevant `ToXml` implementation. If a prefix is already 15 | /// defined for a given namespace, we don't update the set the new prefix. 16 | prefixes: HashMap<&'static str, &'static str>, 17 | default_ns: &'static str, 18 | state: State, 19 | } 20 | 21 | impl<'xml, W: fmt::Write + ?Sized> Serializer<'xml, W> { 22 | pub fn new(output: &'xml mut W) -> Self { 23 | Self { 24 | output, 25 | prefixes: HashMap::new(), 26 | default_ns: "", 27 | state: State::Element, 28 | } 29 | } 30 | 31 | pub fn write_start(&mut self, name: &str, ns: &str) -> Result, Error> { 32 | if self.state != State::Element { 33 | return Err(Error::UnexpectedState("invalid state for element start")); 34 | } 35 | 36 | let prefix = match (ns == self.default_ns, self.prefixes.get(ns)) { 37 | (true, _) => { 38 | self.output.write_fmt(format_args!("<{name}"))?; 39 | None 40 | } 41 | (false, Some(prefix)) => { 42 | self.output.write_fmt(format_args!("<{prefix}:{name}"))?; 43 | Some(*prefix) 44 | } 45 | _ => { 46 | self.output 47 | .write_fmt(format_args!("<{name} xmlns=\"{ns}\""))?; 48 | None 49 | } 50 | }; 51 | 52 | self.state = State::Attribute; 53 | Ok(prefix) 54 | } 55 | 56 | pub fn write_attr( 57 | &mut self, 58 | name: &str, 59 | ns: &str, 60 | value: &V, 61 | ) -> Result<(), Error> { 62 | if self.state != State::Attribute { 63 | return Err(Error::UnexpectedState("invalid state for attribute")); 64 | } 65 | 66 | match ns == self.default_ns { 67 | true => self.output.write_fmt(format_args!(" {name}=\""))?, 68 | false => { 69 | let prefix = self 70 | .prefixes 71 | .get(ns) 72 | .ok_or(Error::UnexpectedState("unknown prefix"))?; 73 | self.output.write_fmt(format_args!(" {prefix}:{name}=\""))?; 74 | } 75 | } 76 | 77 | self.state = State::Scalar; 78 | value.serialize(None, self)?; 79 | self.state = State::Attribute; 80 | self.output.write_char('"')?; 81 | Ok(()) 82 | } 83 | 84 | pub fn write_str(&mut self, value: &V) -> Result<(), Error> { 85 | if !matches!(self.state, State::Element | State::Scalar) { 86 | return Err(Error::UnexpectedState("invalid state for scalar")); 87 | } 88 | 89 | self.output.write_fmt(format_args!("{value}"))?; 90 | self.state = State::Element; 91 | Ok(()) 92 | } 93 | 94 | pub fn end_start(&mut self) -> Result<(), Error> { 95 | if self.state != State::Attribute { 96 | return Err(Error::UnexpectedState("invalid state for element end")); 97 | } 98 | 99 | self.output.write_char('>')?; 100 | self.state = State::Element; 101 | Ok(()) 102 | } 103 | 104 | pub fn end_empty(&mut self) -> Result<(), Error> { 105 | if self.state != State::Attribute { 106 | return Err(Error::UnexpectedState("invalid state for element end")); 107 | } 108 | 109 | self.output.write_str(" />")?; 110 | self.state = State::Element; 111 | Ok(()) 112 | } 113 | 114 | pub fn write_close(&mut self, prefix: Option<&str>, name: &str) -> Result<(), Error> { 115 | if self.state != State::Element { 116 | return Err(Error::UnexpectedState("invalid state for close element")); 117 | } 118 | 119 | match prefix { 120 | Some(prefix) => self.output.write_fmt(format_args!(""))?, 121 | None => self.output.write_fmt(format_args!(""))?, 122 | } 123 | 124 | Ok(()) 125 | } 126 | 127 | pub fn push(&mut self, new: Context) -> Result, Error> { 128 | if self.state != State::Attribute { 129 | return Err(Error::UnexpectedState("invalid state for attribute")); 130 | } 131 | 132 | let mut old = Context::default(); 133 | let prev = mem::replace(&mut self.default_ns, new.default_ns); 134 | let _ = mem::replace(&mut old.default_ns, prev); 135 | 136 | let mut used = 0; 137 | for prefix in new.prefixes.into_iter() { 138 | if prefix.prefix.is_empty() { 139 | continue; 140 | } 141 | 142 | if self.prefixes.contains_key(prefix.ns) { 143 | continue; 144 | } 145 | 146 | self.output 147 | .write_fmt(format_args!(" xmlns:{}=\"{}\"", prefix.prefix, prefix.ns))?; 148 | 149 | let prev = match self.prefixes.entry(prefix.ns) { 150 | Entry::Occupied(mut entry) => mem::replace(entry.get_mut(), prefix.prefix), 151 | Entry::Vacant(entry) => { 152 | entry.insert(prefix.prefix); 153 | "" 154 | } 155 | }; 156 | 157 | old.prefixes[used] = Prefix { 158 | ns: prefix.ns, 159 | prefix: prev, 160 | }; 161 | used += 1; 162 | } 163 | 164 | Ok(old) 165 | } 166 | 167 | pub fn pop(&mut self, old: Context) { 168 | let _ = mem::replace(&mut self.default_ns, old.default_ns); 169 | for prefix in old.prefixes.into_iter() { 170 | if prefix.ns.is_empty() && prefix.prefix.is_empty() { 171 | continue; 172 | } 173 | 174 | let mut entry = match self.prefixes.entry(prefix.ns) { 175 | Entry::Occupied(entry) => entry, 176 | Entry::Vacant(_) => unreachable!(), 177 | }; 178 | 179 | match prefix.prefix { 180 | "" => { 181 | entry.remove(); 182 | } 183 | prev => { 184 | let _ = mem::replace(entry.get_mut(), prev); 185 | } 186 | } 187 | } 188 | } 189 | 190 | pub fn prefix(&self, ns: &str) -> Option<&'static str> { 191 | self.prefixes.get(ns).copied() 192 | } 193 | 194 | pub fn default_ns(&self) -> &'static str { 195 | self.default_ns 196 | } 197 | } 198 | 199 | #[derive(Debug)] 200 | pub struct Context { 201 | pub default_ns: &'static str, 202 | pub prefixes: [Prefix; N], 203 | } 204 | 205 | impl Default for Context { 206 | fn default() -> Self { 207 | Self { 208 | default_ns: Default::default(), 209 | prefixes: [Prefix { prefix: "", ns: "" }; N], 210 | } 211 | } 212 | } 213 | 214 | #[derive(Clone, Copy, Debug, Default)] 215 | pub struct Prefix { 216 | pub prefix: &'static str, 217 | pub ns: &'static str, 218 | } 219 | 220 | #[derive(Debug, Eq, PartialEq)] 221 | enum State { 222 | Attribute, 223 | Element, 224 | Scalar, 225 | } 226 | -------------------------------------------------------------------------------- /instant-xml/tests/attributes.rs: -------------------------------------------------------------------------------- 1 | use similar_asserts::assert_eq; 2 | 3 | use instant_xml::{from_str, to_string, FromXml, ToXml}; 4 | 5 | #[derive(Debug, Eq, FromXml, PartialEq, ToXml)] 6 | struct Basic { 7 | #[xml(attribute)] 8 | flag: bool, 9 | } 10 | 11 | #[test] 12 | fn basic() { 13 | assert_eq!( 14 | from_str::(""), 15 | Ok(Basic { flag: true }) 16 | ); 17 | 18 | assert_eq!( 19 | to_string(&Basic { flag: true }).unwrap(), 20 | "" 21 | ); 22 | } 23 | 24 | #[derive(Debug, Eq, FromXml, PartialEq)] 25 | struct Empty; 26 | 27 | #[test] 28 | fn empty() { 29 | assert_eq!( 30 | from_str::(""), 31 | Ok(Empty) 32 | ); 33 | } 34 | 35 | #[derive(FromXml, ToXml, PartialEq)] 36 | #[xml(ns(bar = BAR))] 37 | struct NoPrefixAttrNs { 38 | #[xml(attribute, ns(BAR))] 39 | flag: bool, 40 | } 41 | 42 | const BAR: &str = "BAR"; 43 | 44 | #[test] 45 | fn no_prefix_attr_ns() { 46 | let v = NoPrefixAttrNs { flag: true }; 47 | let xml = ""; 48 | assert_eq!(to_string(&v).unwrap(), xml); 49 | assert_eq!(from_str::(xml).unwrap(), v); 50 | } 51 | -------------------------------------------------------------------------------- /instant-xml/tests/chrono.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "chrono")] 2 | 3 | use chrono::{DateTime, NaiveDateTime, TimeZone, Utc}; 4 | use similar_asserts::assert_eq; 5 | 6 | use instant_xml::{from_str, to_string, FromXml, ToXml}; 7 | 8 | #[derive(Debug, Eq, PartialEq, FromXml, ToXml)] 9 | struct Test { 10 | dt: T, 11 | } 12 | 13 | #[test] 14 | fn datetime() { 15 | let dt = Utc.with_ymd_and_hms(2022, 11, 21, 21, 17, 23).unwrap(); 16 | let test = Test { dt }; 17 | let xml = "
2022-11-21T21:17:23+00:00
"; 18 | assert_eq!(to_string(&test).unwrap(), xml); 19 | assert_eq!(from_str::>>(xml).unwrap(), test); 20 | 21 | let zulu = xml.replace("+00:00", "Z"); 22 | assert_eq!(from_str::>>(&zulu).unwrap(), test); 23 | } 24 | 25 | #[test] 26 | fn naive_datetime() { 27 | let dt = NaiveDateTime::parse_from_str("2022-11-21T21:17:23", "%Y-%m-%dT%H:%M:%S").unwrap(); 28 | let test = Test { dt }; 29 | let xml = "
2022-11-21T21:17:23
"; 30 | assert_eq!(to_string(&test).unwrap(), xml); 31 | assert_eq!(from_str::>(xml).unwrap(), test); 32 | } 33 | -------------------------------------------------------------------------------- /instant-xml/tests/de-nested.rs: -------------------------------------------------------------------------------- 1 | use similar_asserts::assert_eq; 2 | 3 | use instant_xml::{from_str, FromXml}; 4 | 5 | #[derive(Debug, Eq, PartialEq, FromXml)] 6 | #[xml(ns("URI", bar = BAR))] 7 | struct NestedDe { 8 | #[xml(ns(BAR))] 9 | flag: bool, 10 | } 11 | 12 | #[derive(Debug, Eq, PartialEq, FromXml)] 13 | #[xml(ns("URI", bar = "BAZ", foo = "BAR"))] 14 | struct StructWithCustomFieldFromXml { 15 | #[xml(ns(BAR))] 16 | r#flag: bool, 17 | #[xml(attribute)] 18 | flag_attribute: bool, 19 | test: NestedDe, 20 | } 21 | 22 | const BAR: &str = "BAZ"; 23 | 24 | #[test] 25 | fn struct_with_custom_field_from_xml() { 26 | assert_eq!( 27 | from_str::("falsetrue").unwrap(), 28 | StructWithCustomFieldFromXml { 29 | flag: false, 30 | flag_attribute: true, 31 | test: NestedDe { flag: true } 32 | } 33 | ); 34 | // Different order 35 | assert_eq!( 36 | from_str::("truefalse").unwrap(), 37 | StructWithCustomFieldFromXml { 38 | flag: false, 39 | flag_attribute: true, 40 | test: NestedDe { flag: true } 41 | } 42 | ); 43 | 44 | // Different prefixes then in definition 45 | assert_eq!( 46 | from_str::("falsetrue").unwrap(), 47 | StructWithCustomFieldFromXml { 48 | flag: false, 49 | flag_attribute: true, 50 | test: NestedDe { flag: true } 51 | } 52 | ); 53 | 54 | assert_eq!( 55 | from_str::( 56 | "true" 57 | ) 58 | .unwrap(), 59 | NestedDe { flag: true } 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /instant-xml/tests/de-ns.rs: -------------------------------------------------------------------------------- 1 | use similar_asserts::assert_eq; 2 | 3 | use instant_xml::{from_str, Error, FromXml}; 4 | 5 | #[derive(Debug, Eq, PartialEq, FromXml)] 6 | struct NestedWrongNamespace { 7 | flag: bool, 8 | } 9 | 10 | #[derive(Debug, Eq, PartialEq, FromXml)] 11 | #[xml(ns("URI", bar = "BAZ"))] 12 | struct NestedDe { 13 | #[xml(ns("BAZ"))] 14 | flag: bool, 15 | } 16 | 17 | #[derive(Debug, Eq, PartialEq, FromXml)] 18 | #[xml(ns("URI", bar = "BAZ"))] 19 | struct StructWithCorrectNestedNamespace { 20 | test: NestedDe, 21 | } 22 | 23 | #[derive(Debug, Eq, PartialEq, FromXml)] 24 | #[xml(ns("URI", bar = "BAZ"))] 25 | struct StructWithWrongNestedNamespace { 26 | test: NestedWrongNamespace, 27 | } 28 | 29 | #[test] 30 | fn default_namespaces() { 31 | // Default namespace not-nested 32 | assert_eq!( 33 | from_str("true"), 34 | Ok(NestedDe { flag: true }) 35 | ); 36 | 37 | // Default namespace not-nested - with xml:lang 38 | assert_eq!( 39 | from_str("true"), 40 | Ok(NestedDe { flag: true }) 41 | ); 42 | 43 | // Default namespace not-nested - wrong namespace 44 | assert_eq!( 45 | from_str( 46 | "true" 47 | ), 48 | Err::(Error::UnexpectedValue( 49 | "unexpected root element \"NestedDe\" in namespace \"WRONG\"".to_owned() 50 | )) 51 | ); 52 | 53 | // Correct child namespace 54 | assert_eq!( 55 | from_str("true"), 56 | Ok(StructWithCorrectNestedNamespace { 57 | test: NestedDe { flag: true } 58 | }) 59 | ); 60 | 61 | // Correct child namespace - without child redefinition 62 | assert_eq!( 63 | from_str("true"), 64 | Ok(StructWithCorrectNestedNamespace { 65 | test: NestedDe { flag: true } 66 | }) 67 | ); 68 | 69 | // Different child namespace 70 | assert_eq!( 71 | from_str("true"), 72 | Ok(StructWithWrongNestedNamespace { 73 | test: NestedWrongNamespace { 74 | flag: true 75 | } 76 | }) 77 | ); 78 | 79 | // Wrong child namespace 80 | assert_eq!( 81 | from_str("true"), 82 | Err::( 83 | Error::MissingValue("StructWithWrongNestedNamespace::test") 84 | ) 85 | ); 86 | } 87 | 88 | #[derive(Debug, Eq, PartialEq, FromXml)] 89 | #[xml(ns("URI", bar = "BAZ"))] 90 | struct NestedOtherNamespace { 91 | #[xml(ns("BAZ"))] 92 | flag: bool, 93 | } 94 | 95 | #[derive(Debug, Eq, PartialEq, FromXml)] 96 | #[xml(ns("URI", bar = "BAZ"))] 97 | struct StructOtherNamespace { 98 | test: NestedOtherNamespace, 99 | } 100 | 101 | #[test] 102 | fn other_namespaces() { 103 | // Other namespace not-nested 104 | assert_eq!( 105 | from_str( 106 | "true" 107 | ), 108 | Ok(NestedOtherNamespace { flag: true }) 109 | ); 110 | 111 | // Other namespace not-nested - wrong defined namespace 112 | assert_eq!( 113 | from_str( 114 | "true" 115 | ), 116 | Err::(Error::UnknownPrefix("wrong".to_owned())) 117 | ); 118 | 119 | // Other namespace not-nested - wrong parser namespace 120 | assert_eq!( 121 | from_str( 122 | "true" 123 | ), 124 | Err::(Error::MissingValue("NestedOtherNamespace::flag")) 125 | ); 126 | 127 | // Other namespace not-nested - missing parser prefix 128 | assert_eq!( 129 | from_str( 130 | "true" 131 | ), 132 | Err::(Error::MissingValue("NestedOtherNamespace::flag")) 133 | ); 134 | 135 | // Correct child other namespace 136 | assert_eq!( 137 | from_str( 138 | "true" 139 | ), 140 | Ok(StructOtherNamespace { 141 | test: NestedOtherNamespace { 142 | flag: true, 143 | } 144 | }) 145 | ); 146 | 147 | // Correct child other namespace - without child redefinition 148 | assert_eq!( 149 | from_str( 150 | "true" 151 | ), 152 | Ok(StructOtherNamespace { 153 | test: NestedOtherNamespace { 154 | flag: true, 155 | } 156 | }) 157 | ); 158 | 159 | // Wrong child other namespace - without child redefinition 160 | assert_eq!( 161 | from_str( 162 | "true" 163 | ), 164 | Err::(Error::UnknownPrefix("wrong".to_owned())) 165 | ); 166 | } 167 | 168 | #[derive(Debug, Eq, PartialEq, FromXml)] 169 | #[xml(ns("URI", da_sh.ed-ns = "dashed"))] 170 | struct DashedNs { 171 | #[xml(ns("dashed"))] 172 | element: String, 173 | } 174 | 175 | #[test] 176 | fn dashed_ns() { 177 | assert_eq!( 178 | from_str("hello"), 179 | Ok(DashedNs { element: "hello".to_owned() }) 180 | ); 181 | } 182 | -------------------------------------------------------------------------------- /instant-xml/tests/direct.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use similar_asserts::assert_eq; 4 | 5 | use instant_xml::{from_str, to_string, Error, FromXml, ToXml}; 6 | 7 | #[derive(Clone, Debug, Eq, FromXml, PartialEq, ToXml)] 8 | struct Foo { 9 | #[xml(attribute)] 10 | flag: bool, 11 | #[xml(direct)] 12 | inner: String, 13 | } 14 | 15 | #[test] 16 | fn direct() { 17 | let v = Foo { 18 | flag: true, 19 | inner: "cbdté".to_string(), 20 | }; 21 | let xml = "cbdté"; 22 | 23 | assert_eq!(to_string(&v).unwrap(), xml); 24 | assert_eq!(from_str::(xml), Ok(v.clone())); 25 | 26 | let xml = "cbdté"; 27 | assert_eq!(from_str::(xml), Ok(v.clone())); 28 | 29 | let xml = "cbdté"; 30 | assert_eq!(from_str::(xml), Ok(v.clone())); 31 | 32 | let xml = "cbdté"; 33 | assert_eq!(from_str::(xml), Ok(v)); 34 | } 35 | 36 | #[derive(Debug, Eq, PartialEq, FromXml)] 37 | #[xml(ns("URI"))] 38 | struct StructDirectNamespace { 39 | #[xml(ns("BAZ"))] 40 | flag: bool, 41 | } 42 | 43 | #[test] 44 | fn direct_namespaces() { 45 | // Correct direct namespace 46 | assert_eq!( 47 | from_str( 48 | "true" 49 | ), 50 | Ok(StructDirectNamespace { flag: true }) 51 | ); 52 | 53 | // Wrong direct namespace 54 | assert_eq!( 55 | from_str( 56 | "true" 57 | ), 58 | Err::(Error::MissingValue("StructDirectNamespace::flag")) 59 | ); 60 | 61 | // Wrong direct namespace - missing namespace 62 | assert_eq!( 63 | from_str("true"), 64 | Err::(Error::MissingValue("StructDirectNamespace::flag")) 65 | ); 66 | } 67 | 68 | #[derive(Debug, Eq, PartialEq, FromXml)] 69 | struct DirectString { 70 | s: String, 71 | } 72 | 73 | #[test] 74 | fn direct_string() { 75 | assert_eq!( 76 | from_str("hello"), 77 | Ok(DirectString { 78 | s: "hello".to_string() 79 | }) 80 | ); 81 | } 82 | 83 | #[derive(Debug, Eq, PartialEq, FromXml)] 84 | struct DirectStr<'a> { 85 | s: Cow<'a, str>, 86 | } 87 | 88 | #[test] 89 | fn direct_empty_str() { 90 | assert_eq!( 91 | from_str(""), 92 | Ok(DirectStr { s: "".into() }) 93 | ); 94 | } 95 | 96 | #[test] 97 | fn direct_missing_string() { 98 | assert_eq!( 99 | from_str(""), 100 | Err::(Error::MissingValue("DirectString::s")) 101 | ); 102 | } 103 | 104 | #[derive(Debug, PartialEq, FromXml)] 105 | struct ArtUri { 106 | #[xml(direct)] 107 | uri: String, 108 | } 109 | 110 | #[derive(Debug, PartialEq, FromXml)] 111 | struct Container { 112 | art: Option, 113 | } 114 | 115 | #[test] 116 | fn container_empty_string() { 117 | assert_eq!( 118 | from_str(""), 119 | Ok(Container { 120 | art: Some(ArtUri { 121 | uri: "".to_string() 122 | }) 123 | }) 124 | ); 125 | assert_eq!( 126 | from_str(""), 127 | Ok(Container { 128 | art: Some(ArtUri { 129 | uri: "".to_string() 130 | }) 131 | }) 132 | ); 133 | } 134 | 135 | #[derive(ToXml, FromXml, Debug, PartialEq, Eq)] 136 | struct Options { 137 | #[xml(attribute)] 138 | attribute: Option, 139 | #[xml(direct)] 140 | direct: Option, 141 | } 142 | 143 | #[test] 144 | fn direct_options() { 145 | let v = Options { 146 | attribute: Some("Attribute text".to_string()), 147 | direct: None, 148 | }; 149 | let xml = r#""#; 150 | 151 | assert_eq!(xml, to_string(&v).unwrap()); 152 | assert_eq!(from_str::(xml).unwrap(), v); 153 | } 154 | -------------------------------------------------------------------------------- /instant-xml/tests/escaping.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use similar_asserts::assert_eq; 4 | 5 | use instant_xml::{from_str, to_string, FromXml, ToXml}; 6 | 7 | #[derive(Debug, PartialEq, Eq, FromXml, ToXml)] 8 | #[xml(ns("URI"))] 9 | struct StructSpecialEntities<'a> { 10 | string: String, 11 | #[xml(borrow)] 12 | cow: Cow<'a, str>, 13 | } 14 | 15 | #[test] 16 | fn escape_back() { 17 | assert_eq!( 18 | from_str( 19 | "<>&"'adsad"str&" 20 | ), 21 | Ok(StructSpecialEntities { 22 | string: String::from("<>&\"'adsad\""), 23 | cow: Cow::Owned("str&".to_string()), 24 | }) 25 | ); 26 | 27 | // Borrowed 28 | let escape_back = from_str::( 29 | "<>&"'adsad"str" 30 | ) 31 | .unwrap(); 32 | 33 | if let Cow::Owned(_) = escape_back.cow { 34 | panic!("Should be Borrowed") 35 | } 36 | 37 | // Owned 38 | let escape_back = from_str::( 39 | "<>&"'adsad"str&" 40 | ) 41 | .unwrap(); 42 | 43 | if let Cow::Borrowed(_) = escape_back.cow { 44 | panic!("Should be Owned") 45 | } 46 | } 47 | 48 | #[test] 49 | fn special_entities() { 50 | assert_eq!( 51 | to_string(&StructSpecialEntities{ 52 | string: "&\"<>\'aa".to_string(), 53 | cow: Cow::from("&\"<>\'cc"), 54 | }).unwrap(), 55 | "&"<>'aa&"<>'cc", 56 | ); 57 | } 58 | 59 | #[derive(Debug, PartialEq, Eq, FromXml, ToXml)] 60 | struct SimpleCData<'a> { 61 | #[xml(borrow)] 62 | foo: Cow<'a, str>, 63 | } 64 | 65 | #[test] 66 | fn simple_cdata() { 67 | assert_eq!( 68 | from_str::("]]>") 69 | .unwrap(), 70 | SimpleCData { 71 | foo: Cow::Borrowed("") 72 | } 73 | ); 74 | 75 | assert_eq!( 76 | to_string(&SimpleCData { 77 | foo: Cow::Borrowed("") 78 | }) 79 | .unwrap(), 80 | "<foo>", 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /instant-xml/tests/forward-enum.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use similar_asserts::assert_eq; 4 | 5 | use instant_xml::{from_str, to_string, FromXml, ToXml}; 6 | 7 | #[derive(Debug, FromXml, PartialEq, ToXml)] 8 | #[xml(forward)] 9 | enum Foo { 10 | Bar(Bar), 11 | Baz(Baz), 12 | } 13 | 14 | #[derive(Debug, FromXml, PartialEq, ToXml)] 15 | struct Bar { 16 | bar: u8, 17 | } 18 | 19 | #[derive(Debug, FromXml, PartialEq, ToXml)] 20 | struct Baz { 21 | baz: String, 22 | } 23 | 24 | #[test] 25 | fn wrapped_enum() { 26 | let v = Foo::Bar(Bar { bar: 42 }); 27 | let xml = r#"42"#; 28 | assert_eq!(xml, to_string(&v).unwrap()); 29 | assert_eq!(v, from_str(xml).unwrap()); 30 | } 31 | 32 | #[derive(Debug, FromXml, PartialEq, ToXml)] 33 | #[xml(forward)] 34 | enum FooCow<'a> { 35 | Bar(Cow<'a, [BarBorrowed<'a>]>), 36 | Baz(Cow<'a, [BazBorrowed<'a>]>), 37 | } 38 | 39 | #[derive(Clone, Debug, FromXml, PartialEq, ToXml)] 40 | #[xml(rename = "Bar")] 41 | struct BarBorrowed<'a> { 42 | bar: Cow<'a, str>, 43 | } 44 | 45 | #[derive(Clone, Debug, FromXml, PartialEq, ToXml)] 46 | #[xml(rename = "Baz")] 47 | struct BazBorrowed<'a> { 48 | baz: Cow<'a, str>, 49 | } 50 | 51 | #[test] 52 | fn with_cow_accumulator() { 53 | let v = FooCow::Bar(Cow::Borrowed(&[BarBorrowed { 54 | bar: Cow::Borrowed("test"), 55 | }])); 56 | let xml = r#"test"#; 57 | 58 | assert_eq!(xml, to_string(&v).unwrap()); 59 | assert_eq!(v, from_str(xml).unwrap()); 60 | } 61 | -------------------------------------------------------------------------------- /instant-xml/tests/generics.rs: -------------------------------------------------------------------------------- 1 | use similar_asserts::assert_eq; 2 | 3 | use instant_xml::{from_str, to_string, FromXml, ToXml}; 4 | 5 | #[derive(Debug, Eq, FromXml, ToXml, PartialEq)] 6 | struct Foo { 7 | inner: T, 8 | } 9 | 10 | #[derive(Debug, Eq, FromXml, ToXml, PartialEq)] 11 | struct Bar { 12 | bar: String, 13 | } 14 | 15 | #[allow(clippy::disallowed_names)] // `foo` is not allowed 16 | #[test] 17 | fn serialize_generics() { 18 | let foo = Foo { 19 | inner: Bar { 20 | bar: "Bar".to_owned(), 21 | }, 22 | }; 23 | 24 | let xml = "Bar"; 25 | 26 | assert_eq!(to_string(&foo).unwrap(), xml); 27 | assert_eq!(from_str::>(xml).unwrap(), foo); 28 | } 29 | -------------------------------------------------------------------------------- /instant-xml/tests/lifetime.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use similar_asserts::assert_eq; 4 | 5 | use instant_xml::{from_str, to_string, FromXml, ToXml}; 6 | 7 | #[derive(Debug, Eq, FromXml, PartialEq, ToXml)] 8 | struct Foo { 9 | bar: Bar<'static>, 10 | } 11 | 12 | #[derive(Debug, Eq, FromXml, PartialEq, ToXml)] 13 | struct Bar<'a> { 14 | baz: Cow<'a, str>, 15 | } 16 | 17 | #[test] 18 | fn lifetime() { 19 | let v = Foo { 20 | bar: Bar { 21 | baz: Cow::Borrowed("hello"), 22 | }, 23 | }; 24 | let xml = r#"hello"#; 25 | assert_eq!(xml, to_string(&v).unwrap()); 26 | assert_eq!(v, from_str(xml).unwrap()); 27 | } 28 | -------------------------------------------------------------------------------- /instant-xml/tests/option.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use similar_asserts::assert_eq; 4 | 5 | use instant_xml::{from_str, to_string, FromXml, ToXml}; 6 | 7 | #[derive(Debug, Eq, FromXml, PartialEq, ToXml)] 8 | struct Foo { 9 | inner: Option>, 10 | } 11 | 12 | #[test] 13 | fn option_vec() { 14 | let v = Foo { 15 | inner: Some(vec!["a".to_string(), "b".to_string()]), 16 | }; 17 | let xml = r#"ab"#; 18 | 19 | assert_eq!(xml, to_string(&v).unwrap()); 20 | assert_eq!(v, from_str(xml).unwrap()); 21 | } 22 | 23 | #[derive(Debug, Eq, FromXml, PartialEq, ToXml)] 24 | struct Bar<'a> { 25 | #[xml(attribute, borrow)] 26 | maybe: Option>, 27 | } 28 | 29 | #[test] 30 | fn option_borrow() { 31 | let v = Bar { 32 | maybe: Some("a".into()), 33 | }; 34 | let xml = r#""#; 35 | 36 | assert_eq!(xml, to_string(&v).unwrap()); 37 | assert_eq!(v, from_str(xml).unwrap()); 38 | 39 | let v = Bar { maybe: None }; 40 | let xml = r#""#; 41 | 42 | assert_eq!(xml, to_string(&v).unwrap()); 43 | assert_eq!(v, from_str(xml).unwrap()); 44 | } 45 | -------------------------------------------------------------------------------- /instant-xml/tests/rename.rs: -------------------------------------------------------------------------------- 1 | use similar_asserts::assert_eq; 2 | 3 | use instant_xml::{from_str, to_string, FromXml, ToXml}; 4 | 5 | #[derive(Debug, Eq, PartialEq, FromXml, ToXml)] 6 | #[xml(rename = "renamed")] 7 | struct Renamed { 8 | #[xml(attribute, rename = "renamed")] 9 | flag: bool, 10 | } 11 | 12 | #[test] 13 | fn renamed() { 14 | assert_eq!( 15 | from_str::(""), 16 | Ok(Renamed { flag: true }) 17 | ); 18 | 19 | assert_eq!( 20 | to_string(&Renamed { flag: true }).unwrap(), 21 | "" 22 | ); 23 | } 24 | 25 | #[test] 26 | fn rename_all_struct() { 27 | #[derive(Debug, PartialEq, Eq, ToXml, FromXml)] 28 | #[xml(rename_all = "UPPERCASE")] 29 | pub struct TestStruct { 30 | field_1: String, 31 | #[xml(attribute)] 32 | field_2: bool, 33 | } 34 | 35 | let serialized = r#"value"#; 36 | let instance = TestStruct { 37 | field_1: "value".into(), 38 | field_2: true, 39 | }; 40 | 41 | assert_eq!(to_string(&instance).unwrap(), serialized); 42 | assert_eq!(from_str::(serialized), Ok(instance)); 43 | } 44 | 45 | #[test] 46 | fn rename_all_enum_variant() { 47 | #[derive(Debug, PartialEq, Eq, ToXml, FromXml)] 48 | #[xml(scalar, rename_all = "snake_case")] 49 | pub enum TestEnum { 50 | SnakeCased, 51 | ThisToo, 52 | } 53 | 54 | #[derive(Debug, PartialEq, Eq, ToXml, FromXml)] 55 | #[xml(rename_all = "UPPERCASE")] 56 | pub struct TestStruct { 57 | field_1: TestEnum, 58 | #[xml(attribute)] 59 | field_2: TestEnum, 60 | } 61 | 62 | let serialized = 63 | r#"snake_cased"#; 64 | let instance = TestStruct { 65 | field_1: TestEnum::SnakeCased, 66 | field_2: TestEnum::ThisToo, 67 | }; 68 | 69 | assert_eq!(to_string(&instance).unwrap(), serialized); 70 | assert_eq!(from_str::(serialized), Ok(instance)); 71 | } 72 | -------------------------------------------------------------------------------- /instant-xml/tests/scalar-enum.rs: -------------------------------------------------------------------------------- 1 | use similar_asserts::assert_eq; 2 | 3 | use instant_xml::{from_str, to_string, FromXml, ToXml}; 4 | 5 | #[derive(Debug, Eq, FromXml, PartialEq, ToXml)] 6 | #[xml(scalar)] 7 | enum Foo { 8 | A, 9 | B, 10 | } 11 | 12 | #[derive(Debug, Eq, FromXml, PartialEq, ToXml)] 13 | struct Container { 14 | foo: Foo, 15 | } 16 | 17 | #[test] 18 | fn scalar_enum() { 19 | let v = Container { foo: Foo::A }; 20 | let xml = r#"A"#; 21 | assert_eq!(xml, to_string(&v).unwrap()); 22 | assert_eq!(v, from_str(xml).unwrap()); 23 | } 24 | 25 | #[derive(Debug, Eq, FromXml, PartialEq, ToXml)] 26 | #[xml(scalar, ns("URI", x = "URI"))] 27 | enum Bar { 28 | A, 29 | B, 30 | } 31 | 32 | #[derive(Debug, Eq, FromXml, PartialEq, ToXml)] 33 | #[xml(ns("OTHER", x = "URI"))] 34 | struct NsContainer { 35 | bar: Bar, 36 | } 37 | 38 | #[test] 39 | fn scalar_enum_ns() { 40 | let v = NsContainer { bar: Bar::A }; 41 | let xml = r#"A"#; 42 | assert_eq!(xml, to_string(&v).unwrap()); 43 | assert_eq!(v, from_str(xml).unwrap()); 44 | } 45 | 46 | const DIDL: &str = "DIDL"; 47 | const UPNP: &str = "UPNP"; 48 | const DC: &str = "DC"; 49 | 50 | #[derive(Debug, FromXml, PartialEq, ToXml)] 51 | #[xml(rename = "DIDL-Lite", ns(DIDL, dc = DC, upnp = UPNP))] 52 | struct DidlLite { 53 | item: Vec, 54 | } 55 | 56 | #[derive(Debug, FromXml, PartialEq, ToXml)] 57 | #[xml(rename = "item", ns(DIDL))] 58 | struct UpnpItem { 59 | class: Option, 60 | } 61 | 62 | #[derive(Debug, Clone, PartialEq, FromXml, ToXml)] 63 | #[xml(rename = "class", scalar, ns(UPNP, upnp = UPNP))] 64 | enum ObjectClass { 65 | #[xml(rename = "object.item.audioItem.musicTrack")] 66 | MusicTrack, 67 | #[xml(rename = "object.item.audioItem.audioBroadcast")] 68 | AudioBroadcast, 69 | #[xml(rename = "object.container.playlistContainer")] 70 | PlayList, 71 | } 72 | 73 | #[test] 74 | fn scalar_enum_ns_match() { 75 | let v = DidlLite { 76 | item: vec![UpnpItem { 77 | class: Some(ObjectClass::AudioBroadcast), 78 | }], 79 | }; 80 | 81 | // Keep the `upnp::mimeType` element after `upnp::class` to ensure that 82 | // we tickle a `DuplicateValue` error if we don't match correctly. 83 | let xml = r#" 84 | 85 | object.item.audioItem.audioBroadcast 86 | audio/flac 87 | 88 | "#; 89 | assert_eq!(v, from_str(xml).unwrap()); 90 | } 91 | -------------------------------------------------------------------------------- /instant-xml/tests/scalar.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use similar_asserts::assert_eq; 5 | 6 | use instant_xml::{from_str, FromXml, ToXml}; 7 | 8 | #[derive(Debug, Deserialize, PartialEq, Eq, Serialize, FromXml, ToXml)] 9 | #[xml(ns("URI"))] 10 | struct NestedLifetimes<'a> { 11 | flag: bool, 12 | str_type_a: Cow<'a, str>, 13 | } 14 | 15 | #[derive(Debug, Deserialize, PartialEq, Serialize, FromXml, ToXml)] 16 | #[xml(ns("URI"))] 17 | struct StructDeserializerScalars<'a, 'b> { 18 | bool_type: bool, 19 | i8_type: i8, 20 | u32_type: u32, 21 | string_type: String, 22 | str_type_a: Cow<'a, str>, 23 | str_type_b: Cow<'b, str>, 24 | char_type: char, 25 | f32_type: f32, 26 | nested: NestedLifetimes<'a>, 27 | cow: Cow<'a, str>, 28 | option: Option>, 29 | slice: Cow<'a, [u8]>, 30 | } 31 | 32 | #[test] 33 | fn scalars() { 34 | assert_eq!( 35 | from_str( 36 | "true142stringlifetime alifetime bc1.20trueasd123123" 37 | ), 38 | Ok(StructDeserializerScalars{ 39 | bool_type: true, 40 | i8_type: 1, 41 | u32_type: 42, 42 | string_type: "string".to_string(), 43 | str_type_a: "lifetime a".into(), 44 | str_type_b: "lifetime b".into(), 45 | char_type: 'c', 46 | f32_type: 1.20, 47 | nested: NestedLifetimes { 48 | flag: true, 49 | str_type_a: "asd".into() 50 | }, 51 | cow: Cow::from("123"), 52 | option: None, 53 | slice: Cow::Borrowed(&[1, 2, 3]), 54 | }) 55 | ); 56 | 57 | // Option none 58 | assert_eq!( 59 | from_str( 60 | "true142stringlifetime alifetime bc1.2trueasd123123" 61 | ), 62 | Ok(StructDeserializerScalars{ 63 | bool_type: true, 64 | i8_type: 1, 65 | u32_type: 42, 66 | string_type: "string".to_string(), 67 | str_type_a: "lifetime a".into(), 68 | str_type_b: "lifetime b".into(), 69 | char_type: 'c', 70 | f32_type: 1.20, 71 | nested: NestedLifetimes { 72 | flag: true, 73 | str_type_a: "asd".into(), 74 | }, 75 | cow: Cow::from("123"), 76 | option: Some("asd".into()), 77 | slice: Cow::Borrowed(&[1, 2, 3]), 78 | }) 79 | ); 80 | } 81 | 82 | #[derive(Debug, FromXml, PartialEq)] 83 | struct ScalarElementAttr { 84 | s: String, 85 | } 86 | 87 | #[test] 88 | fn scalar_element_attr() { 89 | assert_eq!( 90 | from_str::( 91 | "hello" 92 | ) 93 | .unwrap(), 94 | ScalarElementAttr { 95 | s: "hello".to_string(), 96 | } 97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /instant-xml/tests/ser-child-ns.rs: -------------------------------------------------------------------------------- 1 | use similar_asserts::assert_eq; 2 | 3 | use instant_xml::{to_string, ToXml}; 4 | 5 | #[derive(Debug, Eq, PartialEq, ToXml)] 6 | #[xml(ns(dar = "BAZ", internal = INTERNAL))] 7 | struct NestedDifferentNamespace { 8 | #[xml(ns(INTERNAL))] 9 | flag_internal_prefix: bool, 10 | } 11 | 12 | #[derive(Debug, Eq, PartialEq, ToXml)] 13 | #[xml(ns("URI", bar = "BAZ", foo = "BAR"))] 14 | struct StructChildNamespaces { 15 | different_child_namespace: NestedDifferentNamespace, 16 | same_child_namespace: Nested, 17 | } 18 | 19 | #[derive(Debug, Eq, PartialEq, ToXml)] 20 | #[xml(ns("URI", dar = DAR, internal = INTERNAL))] 21 | struct Nested { 22 | #[xml(ns(DAR))] 23 | flag_parent_prefix: bool, 24 | #[xml(ns(INTERNAL))] 25 | flag_internal_prefix: bool, 26 | } 27 | 28 | const DAR: &str = "BAZ"; 29 | const INTERNAL: &str = "INTERNAL"; 30 | 31 | // Tests: 32 | // - Different child namespace 33 | // - The same child namespace 34 | #[test] 35 | fn struct_child_namespaces() { 36 | assert_eq!( 37 | to_string(&StructChildNamespaces { 38 | different_child_namespace: NestedDifferentNamespace { 39 | flag_internal_prefix: false, 40 | }, 41 | same_child_namespace: Nested { 42 | flag_parent_prefix: true, 43 | flag_internal_prefix: false, 44 | }, 45 | }) 46 | .unwrap(), 47 | "falsetruefalse" 48 | ); 49 | } 50 | 51 | #[derive(Debug, ToXml)] 52 | #[xml(rename = "DIDL-Lite", ns("DIDL", upnp = "UPNP"))] 53 | pub struct DidlLite { 54 | pub item: UpnpItem, 55 | } 56 | 57 | #[derive(Debug, ToXml)] 58 | #[xml(rename = "item", ns("DIDL"))] 59 | pub struct UpnpItem { 60 | pub album_art: Option, 61 | } 62 | 63 | #[derive(Debug, ToXml)] 64 | #[xml(rename = "albumArtURI", ns("UPNP", upnp = "UPNP"))] 65 | pub struct AlbumArtUri { 66 | #[xml(direct)] 67 | pub uri: String, 68 | } 69 | 70 | #[test] 71 | fn test_didl() { 72 | let didl = DidlLite { 73 | item: UpnpItem { 74 | album_art: Some(AlbumArtUri { 75 | uri: "http://art".to_string(), 76 | }), 77 | }, 78 | }; 79 | assert_eq!(to_string(&didl).unwrap(), "http://art"); 80 | } 81 | -------------------------------------------------------------------------------- /instant-xml/tests/ser-named.rs: -------------------------------------------------------------------------------- 1 | use similar_asserts::assert_eq; 2 | 3 | use instant_xml::{to_string, ToXml}; 4 | 5 | #[derive(Debug, Eq, PartialEq, ToXml)] 6 | #[xml(ns(bar = "BAZ", foo = "BAR"))] 7 | struct StructWithNamedFields { 8 | flag: bool, 9 | #[xml(ns("BAZ"))] 10 | string: String, 11 | #[xml(ns("typo"))] 12 | number: i32, 13 | } 14 | 15 | // Tests: 16 | // - Empty default namespace 17 | // - Prefix namespace 18 | // - Direct namespace 19 | 20 | #[test] 21 | fn struct_with_named_fields() { 22 | assert_eq!( 23 | to_string(&StructWithNamedFields { 24 | flag: true, 25 | string: "test".to_string(), 26 | number: 1, 27 | }) 28 | .unwrap(), 29 | "truetest1" 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /instant-xml/tests/ser-nested.rs: -------------------------------------------------------------------------------- 1 | use similar_asserts::assert_eq; 2 | 3 | use instant_xml::{to_string, ToXml}; 4 | 5 | #[derive(Debug, Eq, PartialEq, ToXml)] 6 | #[xml(ns("URI", dar = "BAZ", internal = INTERNAL))] 7 | struct Nested { 8 | #[xml(ns(INTERNAL))] 9 | flag_internal_prefix: bool, 10 | } 11 | 12 | const INTERNAL: &str = "INTERNAL"; 13 | 14 | #[derive(Debug, Eq, PartialEq, ToXml)] 15 | #[xml(ns("URI", bar = "BAZ", foo = "BAR"))] 16 | struct StructWithCustomField { 17 | #[xml(attribute)] 18 | int_attribute: i32, 19 | #[xml(ns("BAZ"))] 20 | flag_direct_namespace_same_the_same_as_prefix: bool, 21 | #[xml(ns("DIFFERENT"))] 22 | flag_direct_namespace: bool, 23 | test: Nested, 24 | } 25 | 26 | // Tests: 27 | // - The same direct namespace as the one from prefix 28 | // - Attribute handling 29 | // - Omitting redeclared child default namespace 30 | // - Omitting redeclared child namespace with different prefix 31 | // - Unique direct namespace 32 | // - Child unique prefix 33 | // - Child repeated prefix 34 | // - Child default namespace the same as parent 35 | #[test] 36 | fn struct_with_custom_field() { 37 | assert_eq!( 38 | to_string(&StructWithCustomField { 39 | int_attribute: 42, 40 | flag_direct_namespace_same_the_same_as_prefix: true, 41 | flag_direct_namespace: true, 42 | test: Nested { 43 | flag_internal_prefix: false, 44 | }, 45 | }) 46 | .unwrap(), 47 | "truetruefalse" 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /instant-xml/tests/ser-unit.rs: -------------------------------------------------------------------------------- 1 | use similar_asserts::assert_eq; 2 | 3 | use instant_xml::{to_string, ToXml}; 4 | 5 | #[derive(Debug, Eq, PartialEq, ToXml)] 6 | struct Unit; 7 | 8 | #[test] 9 | fn unit() { 10 | assert_eq!(to_string(&Unit).unwrap(), ""); 11 | } 12 | -------------------------------------------------------------------------------- /instant-xml/tests/transparent.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use similar_asserts::assert_eq; 4 | 5 | use instant_xml::{from_str, to_string, Error, FromXml, ToXml}; 6 | 7 | #[derive(Debug, Eq, FromXml, PartialEq, ToXml)] 8 | struct Wrapper { 9 | inline: Inline<'static>, 10 | } 11 | 12 | #[derive(Debug, Eq, FromXml, PartialEq, ToXml)] 13 | #[xml(transparent)] 14 | struct Inline<'a> { 15 | foo: Foo, 16 | bar: Bar<'a>, 17 | } 18 | 19 | #[derive(Debug, Eq, FromXml, PartialEq, ToXml)] 20 | struct Foo { 21 | i: u8, 22 | } 23 | 24 | #[derive(Debug, Eq, FromXml, PartialEq, ToXml)] 25 | struct Bar<'a> { 26 | s: Cow<'a, str>, 27 | } 28 | 29 | #[test] 30 | fn inline() { 31 | let v = Wrapper { 32 | inline: Inline { 33 | foo: Foo { i: 42 }, 34 | bar: Bar { s: "hello".into() }, 35 | }, 36 | }; 37 | 38 | let xml = r#"42hello"#; 39 | assert_eq!(xml, to_string(&v).unwrap()); 40 | assert_eq!(v, from_str(xml).unwrap()); 41 | 42 | assert_eq!( 43 | from_str::("42hello") 44 | .unwrap_err(), 45 | Error::MissingValue("Inline::bar") 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /instant-xml/tests/unnamed.rs: -------------------------------------------------------------------------------- 1 | use similar_asserts::assert_eq; 2 | 3 | use instant_xml::{from_str, to_string, FromXml, ToXml}; 4 | 5 | #[derive(Debug, Eq, FromXml, PartialEq, ToXml)] 6 | struct OneNumber(i32); 7 | 8 | #[test] 9 | fn one_number() { 10 | let v = OneNumber(42); 11 | let xml = r#"42"#; 12 | assert_eq!(xml, to_string(&v).unwrap()); 13 | assert_eq!(v, from_str(xml).unwrap()); 14 | } 15 | 16 | #[derive(Debug, Eq, FromXml, PartialEq, ToXml)] 17 | struct OneString(String); 18 | 19 | #[test] 20 | fn one_string() { 21 | let v = OneString("f42".to_owned()); 22 | let xml = r#"f42"#; 23 | assert_eq!(xml, to_string(&v).unwrap()); 24 | assert_eq!(v, from_str(xml).unwrap()); 25 | } 26 | 27 | #[derive(Debug, Eq, FromXml, PartialEq, ToXml)] 28 | struct StringElement(String, Foo); 29 | 30 | #[derive(Debug, Eq, FromXml, PartialEq, ToXml)] 31 | struct Foo; 32 | 33 | #[test] 34 | fn string_element() { 35 | let v = StringElement("f42".to_owned(), Foo); 36 | let xml = r#"f42"#; 37 | assert_eq!(xml, to_string(&v).unwrap()); 38 | assert_eq!(v, from_str(xml).unwrap()); 39 | } 40 | -------------------------------------------------------------------------------- /instant-xml/tests/vec.rs: -------------------------------------------------------------------------------- 1 | use similar_asserts::assert_eq; 2 | 3 | use instant_xml::{from_str, to_string, FromXml, ToXml}; 4 | 5 | #[derive(Debug, Eq, FromXml, PartialEq, ToXml)] 6 | struct Foo { 7 | bar: usize, 8 | } 9 | 10 | #[derive(Debug, Eq, FromXml, PartialEq, ToXml)] 11 | struct Bar { 12 | foo: Vec, 13 | baz: Vec, 14 | } 15 | 16 | #[test] 17 | fn vec() { 18 | let val = Bar { 19 | foo: vec![], 20 | baz: vec![], 21 | }; 22 | let xml = ""; 23 | assert_eq!(xml, to_string(&val).unwrap()); 24 | assert_eq!(val, from_str(xml).unwrap()); 25 | 26 | let val = Bar { 27 | foo: vec![Foo { bar: 42 }], 28 | baz: vec!["hello".to_owned()], 29 | }; 30 | let xml = "42hello"; 31 | assert_eq!(xml, to_string(&val).unwrap()); 32 | assert_eq!(val, from_str(xml).unwrap()); 33 | 34 | let val = Bar { 35 | foo: vec![Foo { bar: 42 }, Foo { bar: 73 }], 36 | baz: vec!["hello".to_owned(), "world".to_owned()], 37 | }; 38 | let xml = "4273helloworld"; 39 | assert_eq!(xml, to_string(&val).unwrap()); 40 | assert_eq!(val, from_str(xml).unwrap()); 41 | } 42 | -------------------------------------------------------------------------------- /instant-xml/tests/with.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use similar_asserts::assert_eq; 4 | 5 | use instant_xml::{to_string, Error, Serializer, ToXml}; 6 | 7 | #[derive(ToXml)] 8 | struct Foo { 9 | #[xml(serialize_with = "serialize_foo")] 10 | foo: u8, 11 | } 12 | 13 | fn serialize_foo( 14 | value: &u8, 15 | serializer: &mut Serializer<'_, W>, 16 | ) -> Result<(), Error> { 17 | serializer.write_str(&format_args!("foo: {value}")) 18 | } 19 | 20 | #[test] 21 | fn serialize_with() { 22 | let v = Foo { foo: 42 }; 23 | let xml = r#"foo: 42"#; 24 | assert_eq!(xml, to_string(&v).unwrap()); 25 | } 26 | --------------------------------------------------------------------------------