├── strong-xml ├── LICENSE ├── src │ ├── xml_write.rs │ ├── xml_read.rs │ ├── noop_log.rs │ ├── xml_writer.rs │ ├── xml_escape.rs │ ├── xml_unescape.rs │ ├── xml_error.rs │ ├── log.rs │ ├── lib.rs │ └── xml_reader.rs ├── Cargo.toml └── README.md ├── README.md ├── .gitignore ├── strong-xml-derive ├── LICENSE ├── src │ ├── utils │ │ ├── mod.rs │ │ ├── input_lifetime.rs │ │ └── elide_lifetime.rs │ ├── write │ │ ├── newtype.rs │ │ ├── mod.rs │ │ └── named.rs │ ├── read │ │ ├── newtype.rs │ │ ├── mod.rs │ │ └── named.rs │ ├── lib.rs │ └── types.rs └── Cargo.toml ├── Cargo.toml ├── test-suite ├── Cargo.toml └── tests │ ├── empty.rs │ ├── xml_header.rs │ ├── nested.rs │ ├── issue_7.rs │ ├── issue_5.rs │ ├── multiple_lifetimes.rs │ ├── display.rs │ ├── text.rs │ ├── where_clause.rs │ ├── from_str.rs │ ├── tuple_struct.rs │ ├── cdata.rs │ ├── enum.rs │ ├── default.rs │ ├── bool.rs │ ├── struct.rs │ └── log.rs ├── LICENSE └── .github └── workflows └── test.yml /strong-xml/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | strong-xml/README.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /strong-xml-derive/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "strong-xml", 4 | "strong-xml-derive", 5 | "test-suite" 6 | ] 7 | -------------------------------------------------------------------------------- /strong-xml-derive/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | mod elide_lifetime; 2 | mod input_lifetime; 3 | 4 | pub use elide_lifetime::elide_type_lifetimes; 5 | pub use input_lifetime::gen_input_lifetime; 6 | -------------------------------------------------------------------------------- /test-suite/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "strong-xml-test-suite" 3 | version = "0.1.0" 4 | authors = ["PoiScript "] 5 | edition = "2018" 6 | publish = false 7 | 8 | [dependencies] 9 | chrono = "0.4" 10 | env_logger = "0.8" 11 | log = "0.4" 12 | strong-xml = { path = "../strong-xml", features = ["log"] } 13 | -------------------------------------------------------------------------------- /strong-xml-derive/src/write/newtype.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | 4 | pub fn write(name: TokenStream) -> TokenStream { 5 | quote! { 6 | strong_xml::log_start_writing!(#name); 7 | 8 | __inner.to_writer(writer)?; 9 | 10 | strong_xml::log_finish_writing!(#name); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /strong-xml/src/xml_write.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use crate::{XmlResult, XmlWriter}; 4 | 5 | pub trait XmlWrite { 6 | fn to_writer(&self, writer: &mut XmlWriter) -> XmlResult<()>; 7 | 8 | fn to_string(&self) -> XmlResult { 9 | let mut writer = XmlWriter::new(Vec::new()); 10 | 11 | self.to_writer(&mut writer)?; 12 | 13 | Ok(String::from_utf8(writer.inner)?) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /strong-xml-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "strong-xml-derive" 3 | version = "0.6.3" 4 | repository = "https://github.com/PoiScript/strong-xml" 5 | description = "Derive marco of strong-xml." 6 | license = "MIT" 7 | authors = ["PoiScript "] 8 | edition = "2018" 9 | 10 | [lib] 11 | name = "strong_xml_derive" 12 | proc-macro = true 13 | 14 | [dependencies] 15 | proc-macro2 = "1.0" 16 | quote = "1.0" 17 | syn = "1.0" 18 | -------------------------------------------------------------------------------- /strong-xml/src/xml_read.rs: -------------------------------------------------------------------------------- 1 | use crate::{XmlReader, XmlResult}; 2 | 3 | pub trait XmlRead<'a>: Sized { 4 | fn from_reader(reader: &mut XmlReader<'a>) -> XmlResult; 5 | 6 | fn from_str(text: &'a str) -> XmlResult { 7 | let mut reader = XmlReader::new(text); 8 | Self::from_reader(&mut reader) 9 | } 10 | } 11 | 12 | pub trait XmlReadOwned: for<'s> XmlRead<'s> {} 13 | 14 | impl XmlReadOwned for T where T: for<'s> XmlRead<'s> {} 15 | -------------------------------------------------------------------------------- /test-suite/tests/empty.rs: -------------------------------------------------------------------------------- 1 | use strong_xml::{XmlRead, XmlResult, XmlWrite}; 2 | 3 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 4 | #[xml(tag = "empty")] 5 | struct Empty; 6 | 7 | #[test] 8 | fn test() -> XmlResult<()> { 9 | let _ = env_logger::builder() 10 | .is_test(true) 11 | .format_timestamp(None) 12 | .try_init(); 13 | 14 | assert_eq!(Empty.to_string()?, ""); 15 | 16 | assert_eq!(Empty::from_str("")?, Empty); 17 | 18 | Ok(()) 19 | } 20 | -------------------------------------------------------------------------------- /strong-xml-derive/src/read/newtype.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | 4 | use crate::types::Type; 5 | 6 | pub fn read(ty: &Type, ele_name: TokenStream) -> TokenStream { 7 | let ty = match ty { 8 | Type::T(ty) => ty, 9 | _ => panic!("strong-xml only supports newtype_struct and newtype_enum for now."), 10 | }; 11 | 12 | quote! { 13 | strong_xml::log_start_reading!(#ele_name); 14 | 15 | let res = <#ty as XmlRead>::from_reader(reader)?; 16 | 17 | strong_xml::log_finish_reading!(#ele_name); 18 | 19 | return Ok(#ele_name(res)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test-suite/tests/xml_header.rs: -------------------------------------------------------------------------------- 1 | use strong_xml::{XmlRead, XmlResult, XmlWrite}; 2 | 3 | #[derive(XmlRead, XmlWrite, PartialEq, Debug)] 4 | #[xml(tag = "foo")] 5 | struct Foo(#[xml(text)] String); 6 | 7 | #[test] 8 | fn test() -> XmlResult<()> { 9 | let _ = env_logger::builder() 10 | .is_test(true) 11 | .format_timestamp(None) 12 | .try_init(); 13 | 14 | assert_eq!( 15 | Foo::from_str( 16 | r#"foo"# 17 | )?, 18 | Foo("foo".into()) 19 | ); 20 | 21 | Ok(()) 22 | } 23 | -------------------------------------------------------------------------------- /strong-xml/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "strong-xml" 3 | version = "0.6.3" 4 | repository = "https://github.com/PoiScript/strong-xml" 5 | description = "Strong typed xml, based on xmlparser." 6 | license = "MIT" 7 | readme = "README.md" 8 | authors = ["PoiScript "] 9 | keywords = ["xml", "xmlparser", "derive", "proc-macro"] 10 | edition = "2018" 11 | 12 | [dependencies] 13 | jetscii = "0.5" 14 | lazy_static = "1.4" 15 | log = { version = "0.4", optional = true } 16 | memchr = "2.4" 17 | strong-xml-derive = { version = "0.6.3", path = "../strong-xml-derive" } 18 | xmlparser = "0.13" 19 | 20 | [dev-dependencies] 21 | env_logger = "0.8" 22 | -------------------------------------------------------------------------------- /strong-xml-derive/src/utils/input_lifetime.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Span; 2 | use syn::token::Colon; 3 | use syn::{Generics, Lifetime, LifetimeDef}; 4 | 5 | // generate a new lifetime ('__input) which outlives every lifetimes from the given generics 6 | // 7 | // <'a, 'b, T> => '__input: 'a + 'b 8 | // => '__input 9 | pub fn gen_input_lifetime(generics: &Generics) -> LifetimeDef { 10 | let lt = Lifetime::new("'__input", Span::call_site()); 11 | 12 | if generics.lifetimes().count() == 0 { 13 | return LifetimeDef::new(lt); 14 | } 15 | 16 | LifetimeDef { 17 | attrs: Vec::new(), 18 | lifetime: lt, 19 | colon_token: Some(Colon::default()), 20 | bounds: generics.lifetimes().map(|lt| lt.lifetime.clone()).collect(), 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test-suite/tests/nested.rs: -------------------------------------------------------------------------------- 1 | use strong_xml::{XmlRead, XmlResult, XmlWrite}; 2 | 3 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 4 | #[xml(tag = "nested")] 5 | struct Nested { 6 | #[xml(child = "nested")] 7 | contents: Vec, 8 | } 9 | 10 | #[test] 11 | fn test() -> XmlResult<()> { 12 | let _ = env_logger::builder() 13 | .is_test(true) 14 | .format_timestamp(None) 15 | .try_init(); 16 | 17 | assert_eq!( 18 | (Nested { 19 | contents: vec![Nested { 20 | contents: vec![Nested { 21 | contents: vec![Nested { contents: vec![] }] 22 | }] 23 | }] 24 | }) 25 | .to_string()?, 26 | r#""# 27 | ); 28 | 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /strong-xml-derive/src/utils/elide_lifetime.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Span; 2 | use syn::GenericArgument; 3 | use syn::PathArguments; 4 | use syn::{Lifetime, Type}; 5 | 6 | // replace every lifetime with an anonymous lifetime 7 | // 8 | // Foo<'bar> => Foo<'_> 9 | // Foo => Foo 10 | pub fn elide_type_lifetimes(ty: &mut Type) { 11 | if let Type::Path(ty) = ty { 12 | ty.path.segments.iter_mut().for_each(|seg| { 13 | if let PathArguments::AngleBracketed(args) = &mut seg.arguments { 14 | args.args.iter_mut().for_each(|arg| { 15 | if let GenericArgument::Lifetime(lt) = arg { 16 | *lt = Lifetime::new("'_", Span::call_site()) 17 | } 18 | }); 19 | } 20 | }) 21 | } 22 | // TODO: should we take care of other types? 23 | } 24 | -------------------------------------------------------------------------------- /test-suite/tests/issue_7.rs: -------------------------------------------------------------------------------- 1 | use chrono::offset::TimeZone; 2 | use chrono::{DateTime, Utc}; 3 | use strong_xml::{XmlRead, XmlResult, XmlWrite}; 4 | 5 | #[derive(Debug, PartialEq, XmlRead, XmlWrite)] 6 | #[xml(tag = "document")] 7 | struct Document { 8 | #[xml(attr = "datetime")] 9 | datetime: DateTime, 10 | } 11 | 12 | #[test] 13 | fn test() -> XmlResult<()> { 14 | let _ = env_logger::builder() 15 | .is_test(true) 16 | .format_timestamp(None) 17 | .try_init(); 18 | 19 | assert_eq!( 20 | Document::from_str(r#""#)?, 21 | Document { 22 | datetime: Utc.timestamp(0, 0) 23 | } 24 | ); 25 | 26 | assert_eq!( 27 | (Document { 28 | datetime: Utc.ymd(2018, 1, 26).and_hms_micro(18, 30, 9, 453_829) 29 | }) 30 | .to_string()?, 31 | r#""# 32 | ); 33 | 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /test-suite/tests/issue_5.rs: -------------------------------------------------------------------------------- 1 | use strong_xml::{XmlRead, XmlResult, XmlWrite}; 2 | 3 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 4 | #[xml(tag = "root")] 5 | struct Root { 6 | #[xml(child = "child")] 7 | child: Child, 8 | } 9 | 10 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 11 | #[xml(tag = "child")] 12 | struct Child { 13 | #[xml(text)] 14 | content: bool, 15 | } 16 | 17 | #[test] 18 | fn test() -> XmlResult<()> { 19 | let _ = env_logger::builder() 20 | .is_test(true) 21 | .format_timestamp(None) 22 | .try_init(); 23 | 24 | assert_eq!( 25 | Root { 26 | child: Child { content: false } 27 | }, 28 | Root::from_str(r#"false"#)? 29 | ); 30 | 31 | assert_eq!( 32 | Root { 33 | child: Child { content: false } 34 | }, 35 | Root::from_str( 36 | r#" 37 | 38 | ]]> 39 | false 40 | 43 | 44 | "# 45 | )? 46 | ); 47 | 48 | Ok(()) 49 | } 50 | -------------------------------------------------------------------------------- /test-suite/tests/multiple_lifetimes.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use strong_xml::{XmlRead, XmlResult, XmlWrite}; 3 | 4 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 5 | #[xml(tag = "foo")] 6 | struct Foo<'a, 'b, 'c> { 7 | #[xml(attr = "bar")] 8 | bar: Cow<'a, str>, 9 | #[xml(attr = "baz")] 10 | baz: Cow<'b, str>, 11 | #[xml(attr = "quz")] 12 | quz: Cow<'c, str>, 13 | } 14 | 15 | #[test] 16 | fn test() -> XmlResult<()> { 17 | let _ = env_logger::builder() 18 | .is_test(true) 19 | .format_timestamp(None) 20 | .try_init(); 21 | 22 | assert_eq!( 23 | (Foo { 24 | bar: "bar".into(), 25 | baz: "baz".into(), 26 | quz: "quz".into(), 27 | }) 28 | .to_string()?, 29 | r#""# 30 | ); 31 | 32 | assert_eq!( 33 | Foo::from_str(r#""#)?, 34 | Foo { 35 | bar: "bar".into(), 36 | baz: "baz".into(), 37 | quz: "quz".into(), 38 | } 39 | ); 40 | 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /test-suite/tests/display.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use strong_xml::{XmlResult, XmlWrite}; 4 | 5 | #[derive(PartialEq, Debug)] 6 | struct Foo; 7 | 8 | impl fmt::Display for Foo { 9 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 10 | write!(f, "foo") 11 | } 12 | } 13 | 14 | #[derive(XmlWrite, PartialEq, Debug)] 15 | #[xml(tag = "root")] 16 | struct Root { 17 | #[xml(attr = "foo")] 18 | foo: Option, 19 | #[xml(attr = "bar")] 20 | bar: String, 21 | } 22 | 23 | #[test] 24 | fn test() -> XmlResult<()> { 25 | let _ = env_logger::builder() 26 | .is_test(true) 27 | .format_timestamp(None) 28 | .try_init(); 29 | 30 | assert_eq!( 31 | (Root { 32 | foo: None, 33 | bar: "bar".into() 34 | }) 35 | .to_string()?, 36 | r#""# 37 | ); 38 | 39 | assert_eq!( 40 | (Root { 41 | foo: Some(Foo), 42 | bar: "bar".into() 43 | }) 44 | .to_string()?, 45 | r#""# 46 | ); 47 | 48 | Ok(()) 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 Alex Lin (poi) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@master 16 | 17 | - name: Install latest stable 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | toolchain: stable 21 | default: true 22 | override: true 23 | 24 | - name: Install minimal stable with clippy and rustfmt 25 | uses: actions-rs/toolchain@v1 26 | with: 27 | profile: minimal 28 | toolchain: stable 29 | components: rustfmt, clippy 30 | 31 | - name: Run rustfmt 32 | uses: actions-rs/cargo@v1 33 | with: 34 | command: fmt 35 | args: --all -- --check 36 | 37 | - name: Run clippy 38 | uses: actions-rs/cargo@v1 39 | with: 40 | command: clippy 41 | args: --all 42 | 43 | - name: Run test 44 | uses: actions-rs/cargo@v1 45 | with: 46 | command: test 47 | args: --all 48 | -------------------------------------------------------------------------------- /test-suite/tests/text.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use strong_xml::{XmlRead, XmlResult, XmlWrite}; 3 | 4 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 5 | #[xml(tag = "root")] 6 | struct Root<'a> { 7 | #[xml(text)] 8 | content: Cow<'a, str>, 9 | } 10 | 11 | #[test] 12 | fn test() -> XmlResult<()> { 13 | let _ = env_logger::builder() 14 | .is_test(true) 15 | .format_timestamp(None) 16 | .try_init(); 17 | 18 | assert_eq!( 19 | Root { content: "".into() }, 20 | Root::from_str(r#""#)? 21 | ); 22 | 23 | assert_eq!( 24 | Root { 25 | content: "content".into() 26 | }, 27 | Root::from_str(r#"content"#)? 28 | ); 29 | 30 | assert_eq!( 31 | Root { content: "".into() }, 32 | Root::from_str(r#""#)? 33 | ); 34 | 35 | assert_eq!( 36 | Root { 37 | content: "<>".into() 38 | }, 39 | Root::from_str(r#""#)? 40 | ); 41 | 42 | assert_eq!( 43 | Root { 44 | content: "".into() 45 | }, 46 | Root::from_str(r#"]]>"#)? 47 | ); 48 | 49 | Ok(()) 50 | } 51 | -------------------------------------------------------------------------------- /strong-xml/src/noop_log.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | #[doc(hidden)] 3 | macro_rules! log_start_reading { 4 | ($element:path) => {}; 5 | } 6 | 7 | #[macro_export] 8 | #[doc(hidden)] 9 | macro_rules! log_finish_reading { 10 | ($element:path) => {}; 11 | } 12 | 13 | #[macro_export] 14 | #[doc(hidden)] 15 | macro_rules! log_start_reading_field { 16 | ($element:path, $name:expr) => {}; 17 | } 18 | 19 | #[macro_export] 20 | #[doc(hidden)] 21 | macro_rules! log_finish_reading_field { 22 | ($element:path, $name:expr) => {}; 23 | } 24 | 25 | #[macro_export] 26 | #[doc(hidden)] 27 | macro_rules! log_skip_attribute { 28 | ($element:path, $key:ident) => {}; 29 | } 30 | 31 | #[macro_export] 32 | #[doc(hidden)] 33 | macro_rules! log_skip_element { 34 | ($element:path, $tag:ident) => {}; 35 | } 36 | 37 | #[macro_export] 38 | #[doc(hidden)] 39 | macro_rules! log_start_writing { 40 | ($element:path) => {}; 41 | } 42 | 43 | #[macro_export] 44 | #[doc(hidden)] 45 | macro_rules! log_finish_writing { 46 | ($element:path) => {}; 47 | } 48 | 49 | #[macro_export] 50 | #[doc(hidden)] 51 | macro_rules! log_start_writing_field { 52 | ($element:path, $name:expr) => {}; 53 | } 54 | 55 | #[macro_export] 56 | #[doc(hidden)] 57 | macro_rules! log_finish_writing_field { 58 | ($element:path, $name:expr) => {}; 59 | } 60 | -------------------------------------------------------------------------------- /test-suite/tests/where_clause.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt::Display; 3 | use std::str::FromStr; 4 | 5 | use strong_xml::{XmlRead, XmlReadOwned, XmlResult, XmlWrite}; 6 | 7 | #[derive(XmlRead, XmlWrite, PartialEq, Debug)] 8 | #[xml(tag = "wrapper")] 9 | struct Wrapper 10 | where 11 | T: XmlReadOwned + XmlWrite, 12 | U: Display + FromStr, 13 | // This bounds is required because we need to wrap 14 | // the error with a `Box` 15 | ::Err: 'static + Error + Send + Sync, 16 | { 17 | #[xml(attr = "attr")] 18 | attr: U, 19 | #[xml(child = "empty")] 20 | child: T, 21 | } 22 | 23 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 24 | #[xml(tag = "empty")] 25 | struct Empty; 26 | 27 | #[test] 28 | fn test() -> XmlResult<()> { 29 | let _ = env_logger::builder() 30 | .is_test(true) 31 | .format_timestamp(None) 32 | .try_init(); 33 | 34 | assert_eq!( 35 | Wrapper { 36 | attr: String::from("attr"), 37 | child: Empty 38 | } 39 | .to_string()?, 40 | r#""# 41 | ); 42 | 43 | assert_eq!( 44 | Wrapper::from_str(r#""#)?, 45 | Wrapper { 46 | attr: false, 47 | child: Empty 48 | } 49 | ); 50 | 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /test-suite/tests/from_str.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use strong_xml::{XmlRead, XmlResult}; 4 | 5 | #[derive(PartialEq, Debug)] 6 | struct Foo; 7 | 8 | impl FromStr for Foo { 9 | type Err = String; 10 | 11 | fn from_str(s: &str) -> Result { 12 | if s == "foo" || s == "FOO" { 13 | Ok(Foo) 14 | } else { 15 | Err("invalid Foo".into()) 16 | } 17 | } 18 | } 19 | 20 | #[derive(XmlRead, PartialEq, Debug)] 21 | #[xml(tag = "root")] 22 | struct Root { 23 | #[xml(attr = "foo")] 24 | foo: Foo, 25 | #[xml(attr = "bar")] 26 | bar: Option, 27 | #[xml(attr = "baz")] 28 | baz: Option, 29 | } 30 | 31 | #[test] 32 | fn test() -> XmlResult<()> { 33 | let _ = env_logger::builder() 34 | .is_test(true) 35 | .format_timestamp(None) 36 | .try_init(); 37 | 38 | assert_eq!( 39 | Root::from_str(r#""#)?, 40 | Root { 41 | foo: Foo, 42 | bar: None, 43 | baz: Some(100) 44 | } 45 | ); 46 | 47 | assert_eq!( 48 | Root::from_str(r#""#)?, 49 | Root { 50 | foo: Foo, 51 | bar: Some("bar".into()), 52 | baz: None 53 | } 54 | ); 55 | 56 | assert!(Root::from_str(r#""#).is_err()); 57 | 58 | assert!(Root::from_str(r#""#).is_err()); 59 | 60 | Ok(()) 61 | } 62 | -------------------------------------------------------------------------------- /test-suite/tests/tuple_struct.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use strong_xml::{XmlRead, XmlResult, XmlWrite}; 3 | 4 | #[derive(XmlWrite, XmlRead)] 5 | #[xml(tag = "tag1")] 6 | struct Tag1<'a>( 7 | #[xml(attr = "att1")] Option>, 8 | #[xml(text)] Cow<'a, str>, 9 | ); 10 | 11 | #[derive(XmlWrite, XmlRead)] 12 | #[xml(tag = "tag2")] 13 | struct Tag2<'a>( 14 | #[xml(attr = "att1")] Cow<'a, str>, 15 | #[xml(attr = "att2")] Cow<'a, str>, 16 | ); 17 | 18 | #[derive(XmlWrite, XmlRead)] 19 | #[xml(tag = "tag3")] 20 | struct Tag3<'a>( 21 | #[xml(attr = "att1")] Cow<'a, str>, 22 | #[xml(child = "tag1")] Vec>, 23 | #[xml(child = "tag2")] Option>, 24 | #[xml(flatten_text = "text")] Option>, 25 | ); 26 | 27 | #[test] 28 | fn test() -> XmlResult<()> { 29 | let _ = env_logger::builder() 30 | .is_test(true) 31 | .format_timestamp(None) 32 | .try_init(); 33 | 34 | assert_eq!( 35 | (Tag3( 36 | "att1".into(), 37 | vec![Tag1(None, "content".into())], 38 | None, 39 | Some("tag3_content".into()), 40 | )) 41 | .to_string()?, 42 | r#"contenttag3_content"# 43 | ); 44 | 45 | assert_eq!( 46 | (Tag3( 47 | "att1".into(), 48 | vec![ 49 | Tag1(Some("att11".into()), "content1".into()), 50 | Tag1(Some("att12".into()), "content2".into()), 51 | ], 52 | None, 53 | None, 54 | )) 55 | .to_string()?, 56 | r#"content1content2"# 57 | ); 58 | 59 | Ok(()) 60 | } 61 | -------------------------------------------------------------------------------- /strong-xml/src/xml_writer.rs: -------------------------------------------------------------------------------- 1 | use std::io::Result; 2 | use std::io::Write; 3 | 4 | use crate::xml_escape::xml_escape; 5 | 6 | pub struct XmlWriter { 7 | pub inner: W, 8 | } 9 | 10 | impl XmlWriter { 11 | pub fn new(inner: W) -> Self { 12 | XmlWriter { inner } 13 | } 14 | 15 | pub fn into_inner(self) -> W { 16 | self.inner 17 | } 18 | 19 | pub fn write_element_start(&mut self, tag: &str) -> Result<()> { 20 | write!(self.inner, "<{}", tag) 21 | } 22 | 23 | pub fn write_attribute(&mut self, key: &str, value: &str) -> Result<()> { 24 | write!(self.inner, r#" {}="{}""#, key, xml_escape(value)) 25 | } 26 | 27 | pub fn write_text(&mut self, content: &str) -> Result<()> { 28 | write!(self.inner, "{}", xml_escape(content)) 29 | } 30 | 31 | pub fn write_cdata_text(&mut self, content: &str) -> Result<()> { 32 | write!(self.inner, "", content) 33 | } 34 | 35 | pub fn write_element_end_open(&mut self) -> Result<()> { 36 | write!(self.inner, ">") 37 | } 38 | 39 | pub fn write_flatten_text(&mut self, tag: &str, content: &str, is_cdata: bool) -> Result<()> { 40 | self.write_element_start(tag)?; 41 | self.write_element_end_open()?; 42 | if is_cdata { 43 | self.write_cdata_text(content)?; 44 | } else { 45 | self.write_text(content)?; 46 | } 47 | self.write_element_end_close(tag)?; 48 | Ok(()) 49 | } 50 | 51 | pub fn write_element_end_close(&mut self, tag: &str) -> Result<()> { 52 | write!(self.inner, "", tag) 53 | } 54 | 55 | pub fn write_element_end_empty(&mut self) -> Result<()> { 56 | write!(self.inner, "/>") 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test-suite/tests/cdata.rs: -------------------------------------------------------------------------------- 1 | use strong_xml::{XmlRead, XmlResult, XmlWrite}; 2 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 3 | #[xml(tag = "bar")] 4 | struct Bar(#[xml(text, cdata)] String); 5 | 6 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 7 | #[xml(tag = "foo")] 8 | struct Foo { 9 | #[xml(child = "bar")] 10 | bar: Bar, 11 | 12 | #[xml(flatten_text = "qux", cdata)] 13 | baz: String, 14 | } 15 | 16 | #[test] 17 | fn test() -> XmlResult<()> { 18 | let _ = env_logger::builder() 19 | .is_test(true) 20 | .format_timestamp(None) 21 | .try_init(); 22 | 23 | assert_eq!( 24 | Foo { 25 | bar: Bar("<".into()), 26 | baz: ">".into() 27 | }, 28 | Foo::from_str(r#"<>"#)? 29 | ); 30 | 31 | assert_eq!( 32 | Foo { 33 | bar: Bar("<".into()), 34 | baz: ">".into() 35 | }, 36 | Foo::from_str(r#"]]>"#)? 37 | ); 38 | 39 | assert_eq!( 40 | Foo { 41 | bar: Bar("<".into()), 42 | baz: ">".into() 43 | }, 44 | Foo::from_str(r#""#)? 45 | ); 46 | 47 | assert_eq!( 48 | (Foo { 49 | bar: Bar("<".into()), 50 | baz: ">".into() 51 | }) 52 | .to_string()?, 53 | r#""#, 54 | ); 55 | 56 | assert_eq!( 57 | (Foo { 58 | bar: Bar("<".into()), 59 | baz: ">".into() 60 | }) 61 | .to_string()?, 62 | r#"]]>"#, 63 | ); 64 | 65 | Ok(()) 66 | } 67 | -------------------------------------------------------------------------------- /strong-xml-derive/src/read/mod.rs: -------------------------------------------------------------------------------- 1 | mod named; 2 | mod newtype; 3 | 4 | use crate::types::{Element, Fields}; 5 | 6 | use proc_macro2::TokenStream; 7 | use quote::quote; 8 | 9 | pub fn impl_read(element: Element) -> TokenStream { 10 | match element { 11 | Element::Enum { 12 | name: ele_name, 13 | variants, 14 | } => { 15 | let tags = variants.iter().map(|variant| match variant { 16 | Fields::Newtype { tags, .. } => tags.clone(), 17 | Fields::Named { tag, .. } => vec![tag.clone()], 18 | }); 19 | 20 | let read = variants.iter().map(|variant| match variant { 21 | Fields::Named { tag, name, fields } => { 22 | named::read(&tag, quote!(#ele_name::#name), &fields) 23 | } 24 | Fields::Newtype { name, ty, .. } => newtype::read(&ty, quote!(#ele_name::#name)), 25 | }); 26 | 27 | quote! { 28 | while let Some(tag) = reader.find_element_start(None)? { 29 | match tag { 30 | #( #( #tags )|* => { #read } )* 31 | tag => { 32 | strong_xml::log_skip_element!(#ele_name, tag); 33 | // skip the start tag 34 | reader.next(); 35 | reader.read_to_end(tag)?; 36 | }, 37 | } 38 | } 39 | 40 | Err(XmlError::UnexpectedEof) 41 | } 42 | } 43 | 44 | Element::Struct { fields, .. } => match fields { 45 | Fields::Named { tag, name, fields } => named::read(&tag, quote!(#name), &fields), 46 | Fields::Newtype { name, ty, .. } => newtype::read(&ty, quote!(#name)), 47 | }, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /strong-xml/src/xml_escape.rs: -------------------------------------------------------------------------------- 1 | use jetscii::{bytes, BytesConst}; 2 | use lazy_static::lazy_static; 3 | use std::borrow::Cow; 4 | 5 | pub fn xml_escape(raw: &str) -> Cow<'_, str> { 6 | lazy_static! { 7 | static ref ESCAPE_BYTES: BytesConst = bytes!(b'<', b'>', b'&', b'\'', b'"'); 8 | } 9 | 10 | let bytes = raw.as_bytes(); 11 | 12 | if let Some(off) = ESCAPE_BYTES.find(bytes) { 13 | let mut result = String::with_capacity(raw.len()); 14 | 15 | result.push_str(&raw[0..off]); 16 | 17 | let mut pos = off + 1; 18 | 19 | match bytes[pos - 1] { 20 | b'<' => result.push_str("<"), 21 | b'>' => result.push_str(">"), 22 | b'&' => result.push_str("&"), 23 | b'\'' => result.push_str("'"), 24 | b'"' => result.push_str("""), 25 | _ => unreachable!(), 26 | } 27 | 28 | while let Some(off) = ESCAPE_BYTES.find(&bytes[pos..]) { 29 | result.push_str(&raw[pos..pos + off]); 30 | 31 | pos += off + 1; 32 | 33 | match bytes[pos - 1] { 34 | b'<' => result.push_str("<"), 35 | b'>' => result.push_str(">"), 36 | b'&' => result.push_str("&"), 37 | b'\'' => result.push_str("'"), 38 | b'"' => result.push_str("""), 39 | _ => unreachable!(), 40 | } 41 | } 42 | 43 | result.push_str(&raw[pos..]); 44 | 45 | Cow::Owned(result) 46 | } else { 47 | Cow::Borrowed(raw) 48 | } 49 | } 50 | 51 | #[test] 52 | fn test_escape() { 53 | assert_eq!(xml_escape("< < <"), "< < <"); 54 | assert_eq!( 55 | xml_escape(""), 56 | "<script>alert('Hello XSS')</script>" 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /test-suite/tests/enum.rs: -------------------------------------------------------------------------------- 1 | use strong_xml::{XmlRead, XmlResult, XmlWrite}; 2 | 3 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 4 | #[xml(tag = "a")] 5 | struct A; 6 | 7 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 8 | #[xml(tag = "b")] 9 | struct B; 10 | 11 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 12 | #[xml(tag = "c")] 13 | struct C; 14 | 15 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 16 | #[xml(tag = "d")] 17 | struct D; 18 | 19 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 20 | enum AB { 21 | #[xml(tag = "a")] 22 | A(A), 23 | #[xml(tag = "b")] 24 | B(B), 25 | } 26 | 27 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 28 | enum CD { 29 | #[xml(tag = "c")] 30 | C(C), 31 | #[xml(tag = "d")] 32 | D(D), 33 | } 34 | 35 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 36 | enum ABCDEFG { 37 | #[xml(tag = "a", tag = "b")] 38 | AB(AB), 39 | #[xml(tag = "c", tag = "d")] 40 | CD(CD), 41 | #[xml(tag = "e")] 42 | E, 43 | #[xml(tag = "f")] 44 | F { 45 | #[xml(text)] 46 | foo: String, 47 | }, 48 | #[xml(tag = "g")] 49 | G { 50 | #[xml(attr = "foo")] 51 | foo: usize, 52 | #[xml(flatten_text = "bar")] 53 | bar: bool, 54 | }, 55 | } 56 | 57 | #[test] 58 | fn test() -> XmlResult<()> { 59 | let _ = env_logger::builder() 60 | .is_test(true) 61 | .format_timestamp(None) 62 | .try_init(); 63 | 64 | assert_eq!((ABCDEFG::AB(AB::A(A))).to_string()?, ""); 65 | 66 | assert_eq!( 67 | (ABCDEFG::G { 68 | foo: 42, 69 | bar: false 70 | }) 71 | .to_string()?, 72 | r#"false"# 73 | ); 74 | 75 | assert_eq!(ABCDEFG::from_str("")?, ABCDEFG::CD(CD::D(D))); 76 | 77 | assert_eq!( 78 | ABCDEFG::from_str("foo")?, 79 | ABCDEFG::F { foo: "foo".into() } 80 | ); 81 | 82 | Ok(()) 83 | } 84 | -------------------------------------------------------------------------------- /test-suite/tests/default.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use strong_xml::{XmlRead, XmlResult, XmlWrite}; 3 | 4 | #[derive(Default, XmlWrite, XmlRead, PartialEq, Debug)] 5 | #[xml(tag = "root")] 6 | struct Root<'a> { 7 | #[xml(default, attr = "att1")] 8 | att1: Cow<'a, str>, 9 | #[xml(default, child = "child")] 10 | child: Child<'a>, 11 | } 12 | 13 | #[derive(Default, XmlWrite, XmlRead, PartialEq, Debug)] 14 | #[xml(tag = "child")] 15 | struct Child<'a> { 16 | #[xml(default, attr = "att1")] 17 | att1: Cow<'a, str>, 18 | #[xml(default, attr = "att2")] 19 | att2: bool, 20 | } 21 | 22 | #[test] 23 | fn test() -> XmlResult<()> { 24 | let _ = env_logger::builder() 25 | .is_test(true) 26 | .format_timestamp(None) 27 | .try_init(); 28 | 29 | assert_eq!(Root::default(), Root::from_str(r#""#)?); 30 | 31 | assert_eq!( 32 | Root { 33 | att1: "value".into(), 34 | child: Child::default() 35 | }, 36 | Root::from_str(r#""#)? 37 | ); 38 | 39 | assert_eq!( 40 | Root { 41 | att1: "value".into(), 42 | child: Child::default() 43 | }, 44 | Root::from_str(r#""#)? 45 | ); 46 | 47 | assert_eq!( 48 | Root { 49 | att1: "value".into(), 50 | child: Child { 51 | att1: "value".into(), 52 | ..Default::default() 53 | } 54 | }, 55 | Root::from_str(r#""#)? 56 | ); 57 | 58 | assert_eq!( 59 | Root { 60 | att1: "value".into(), 61 | child: Child { 62 | att2: true, 63 | ..Default::default() 64 | } 65 | }, 66 | Root::from_str(r#""#)? 67 | ); 68 | 69 | Ok(()) 70 | } 71 | -------------------------------------------------------------------------------- /test-suite/tests/bool.rs: -------------------------------------------------------------------------------- 1 | use strong_xml::{XmlRead, XmlResult, XmlWrite}; 2 | 3 | #[derive(XmlRead, XmlWrite, PartialEq, Debug)] 4 | #[xml(tag = "attr")] 5 | struct Attr { 6 | #[xml(attr = "attr")] 7 | attr: Option, 8 | } 9 | 10 | #[derive(XmlRead, XmlWrite, PartialEq, Debug)] 11 | #[xml(tag = "text")] 12 | struct FlattenText { 13 | #[xml(flatten_text = "foo")] 14 | foo: Vec, 15 | } 16 | 17 | #[derive(XmlRead, XmlWrite, PartialEq, Debug)] 18 | #[xml(tag = "text")] 19 | struct Text { 20 | #[xml(text)] 21 | text: bool, 22 | } 23 | 24 | #[test] 25 | fn test() -> XmlResult<()> { 26 | let _ = env_logger::builder() 27 | .is_test(true) 28 | .format_timestamp(None) 29 | .try_init(); 30 | 31 | assert_eq!( 32 | Attr::from_str(r#""#)?, 33 | Attr { attr: None } 34 | ); 35 | 36 | assert_eq!( 37 | Attr::from_str(r#""#)?, 38 | Attr { attr: Some(true) } 39 | ); 40 | 41 | assert_eq!( 42 | (Attr { attr: Some(true) }).to_string()?, 43 | r#""#, 44 | ); 45 | 46 | assert_eq!(Text::from_str(r#"off"#)?, Text { text: false }); 47 | 48 | assert_eq!((Text { text: false }).to_string()?, r#"false"#); 49 | 50 | assert_eq!( 51 | FlattenText::from_str(r#""#)?, 52 | FlattenText { foo: vec![] } 53 | ); 54 | 55 | assert_eq!( 56 | FlattenText::from_str(r#"1"#)?, 57 | FlattenText { foo: vec![true] } 58 | ); 59 | 60 | assert_eq!( 61 | FlattenText::from_str(r#"ft"#)?, 62 | FlattenText { 63 | foo: vec![false, true] 64 | } 65 | ); 66 | 67 | assert_eq!( 68 | (FlattenText { 69 | foo: vec![false, true] 70 | }) 71 | .to_string()?, 72 | r#"falsetrue"#, 73 | ); 74 | 75 | Ok(()) 76 | } 77 | -------------------------------------------------------------------------------- /strong-xml-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "256"] 2 | 3 | extern crate proc_macro; 4 | 5 | mod read; 6 | mod types; 7 | mod utils; 8 | mod write; 9 | 10 | use proc_macro::TokenStream; 11 | use quote::quote; 12 | use syn::{parse_macro_input, DeriveInput}; 13 | use types::Element; 14 | 15 | #[proc_macro_derive(XmlRead, attributes(xml))] 16 | pub fn derive_xml_read(input: TokenStream) -> TokenStream { 17 | let input = parse_macro_input!(input as DeriveInput); 18 | 19 | let name = &input.ident; 20 | let generics = &input.generics; 21 | 22 | let params = &generics.params; 23 | 24 | let where_clause = &generics.where_clause; 25 | 26 | let input_lifetime = utils::gen_input_lifetime(&generics); 27 | 28 | let mut params_with_input_lifetime = generics.params.clone(); 29 | 30 | params_with_input_lifetime.insert(0, input_lifetime.into()); 31 | 32 | let impl_read = read::impl_read(Element::parse(input.clone())); 33 | 34 | let gen = quote! { 35 | impl <#params_with_input_lifetime> strong_xml::XmlRead<'__input> for #name <#params> 36 | #where_clause 37 | { 38 | fn from_reader( 39 | mut reader: &mut strong_xml::XmlReader<'__input> 40 | ) -> strong_xml::XmlResult { 41 | use strong_xml::xmlparser::{ElementEnd, Token, Tokenizer}; 42 | use strong_xml::XmlError; 43 | #impl_read 44 | } 45 | } 46 | }; 47 | 48 | gen.into() 49 | } 50 | 51 | #[proc_macro_derive(XmlWrite, attributes(xml))] 52 | pub fn derive_xml_write(input: TokenStream) -> TokenStream { 53 | let input = parse_macro_input!(input as DeriveInput); 54 | 55 | let name = &input.ident; 56 | let generics = &input.generics; 57 | 58 | let params = &generics.params; 59 | 60 | let where_clause = &generics.where_clause; 61 | 62 | let impl_write = write::impl_write(Element::parse(input.clone())); 63 | 64 | let gen = quote! { 65 | impl <#params> strong_xml::XmlWrite for #name <#params> 66 | #where_clause 67 | { 68 | fn to_writer( 69 | &self, 70 | mut writer: &mut strong_xml::XmlWriter 71 | ) -> strong_xml::XmlResult<()> { 72 | #impl_write 73 | 74 | Ok(()) 75 | } 76 | } 77 | }; 78 | 79 | gen.into() 80 | } 81 | -------------------------------------------------------------------------------- /strong-xml-derive/src/write/mod.rs: -------------------------------------------------------------------------------- 1 | mod named; 2 | mod newtype; 3 | 4 | use crate::types::{Element, Field, Fields}; 5 | 6 | use proc_macro2::TokenStream; 7 | use quote::quote; 8 | 9 | pub fn impl_write(element: Element) -> TokenStream { 10 | match element { 11 | Element::Enum { 12 | name: ele_name, 13 | variants, 14 | } => { 15 | let branches = variants.iter().map(|variant| match variant { 16 | Fields::Named { name, fields, .. } => { 17 | let bindings = fields.iter().map(|field| match field { 18 | Field::Attribute { bind, name, .. } 19 | | Field::Child { bind, name, .. } 20 | | Field::Text { bind, name, .. } 21 | | Field::FlattenText { bind, name, .. } => quote!( #name: #bind ), 22 | }); 23 | quote!( #ele_name::#name { #( #bindings ),* } ) 24 | } 25 | Fields::Newtype { name, .. } => quote!( #ele_name::#name(__inner) ), 26 | }); 27 | 28 | let read = variants.iter().map(|variant| match variant { 29 | Fields::Named { tag, name, fields } => { 30 | named::write(&tag, quote!( #ele_name::#name ), &fields) 31 | } 32 | Fields::Newtype { name, .. } => newtype::write(quote!( #ele_name::#name )), 33 | }); 34 | 35 | quote! { 36 | match self { 37 | #( #branches => { #read }, )* 38 | } 39 | } 40 | } 41 | 42 | Element::Struct { 43 | name: ele_name, 44 | fields, 45 | } => match fields { 46 | Fields::Named { tag, name, fields } => { 47 | let bindings = fields.iter().map(|field| match field { 48 | Field::Attribute { bind, name, .. } 49 | | Field::Child { bind, name, .. } 50 | | Field::Text { bind, name, .. } 51 | | Field::FlattenText { bind, name, .. } => quote!( #name: #bind ), 52 | }); 53 | 54 | let read = named::write(&tag, quote!(#name), &fields); 55 | 56 | quote! { 57 | let #ele_name { #( #bindings ),* } = self; 58 | 59 | #read 60 | } 61 | } 62 | Fields::Newtype { name, .. } => { 63 | let read = newtype::write(quote!(#name)); 64 | 65 | quote! { 66 | let __inner = &self.0; 67 | 68 | #read 69 | } 70 | } 71 | }, 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test-suite/tests/struct.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use strong_xml::{XmlRead, XmlResult, XmlWrite}; 3 | 4 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 5 | #[xml(tag = "tag1")] 6 | struct Tag1<'a> { 7 | #[xml(attr = "att1")] 8 | att1: Option>, 9 | #[xml(text)] 10 | content: Cow<'a, str>, 11 | } 12 | 13 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 14 | #[xml(tag = "tag2")] 15 | struct Tag2<'a> { 16 | #[xml(attr = "att1")] 17 | att1: Cow<'a, str>, 18 | #[xml(attr = "att2")] 19 | att2: Cow<'a, str>, 20 | } 21 | 22 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 23 | #[xml(tag = "tag3")] 24 | struct Tag3<'a> { 25 | #[xml(attr = "att1")] 26 | att1: Cow<'a, str>, 27 | #[xml(child = "tag1")] 28 | tag1: Vec>, 29 | #[xml(child = "tag2")] 30 | tag2: Option>, 31 | #[xml(flatten_text = "text")] 32 | text: Option>, 33 | } 34 | 35 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 36 | #[xml(tag = "tag3")] 37 | struct Tag4<'a>(Tag3<'a>); 38 | 39 | #[test] 40 | fn test() -> XmlResult<()> { 41 | let _ = env_logger::builder() 42 | .is_test(true) 43 | .format_timestamp(None) 44 | .try_init(); 45 | 46 | assert_eq!( 47 | (Tag3 { 48 | att1: "att1".into(), 49 | tag1: vec![Tag1 { 50 | att1: None, 51 | content: "content".into(), 52 | }], 53 | tag2: None, 54 | text: Some("tag3_content".into()), 55 | }) 56 | .to_string()?, 57 | r#"contenttag3_content"# 58 | ); 59 | 60 | assert_eq!( 61 | (Tag3 { 62 | att1: "att1".into(), 63 | tag1: vec![ 64 | Tag1 { 65 | att1: Some("att11".into()), 66 | content: "content1".into(), 67 | }, 68 | Tag1 { 69 | att1: Some("att12".into()), 70 | content: "content2".into(), 71 | }, 72 | ], 73 | tag2: None, 74 | text: None, 75 | }) 76 | .to_string()?, 77 | r#"content1content2"# 78 | ); 79 | 80 | assert_eq!( 81 | Tag4::from_str(r#""#)?, 82 | Tag4(Tag3 { 83 | att1: "att1".into(), 84 | tag1: vec![], 85 | tag2: Some(Tag2 { 86 | att1: "att1".into(), 87 | att2: "att2".into(), 88 | }), 89 | text: None, 90 | }) 91 | ); 92 | 93 | Ok(()) 94 | } 95 | -------------------------------------------------------------------------------- /strong-xml/src/xml_unescape.rs: -------------------------------------------------------------------------------- 1 | use memchr::memchr; 2 | use std::borrow::Cow; 3 | use std::char; 4 | 5 | use crate::{XmlError, XmlResult}; 6 | 7 | pub fn xml_unescape<'a>(raw: &'a str) -> XmlResult> { 8 | let bytes = raw.as_bytes(); 9 | 10 | if let Some(i) = memchr(b'&', bytes) { 11 | let mut result = String::with_capacity(raw.len()); 12 | 13 | result.push_str(&raw[0..i]); 14 | 15 | let mut pos = i + 1; 16 | 17 | if let Some(i) = memchr(b';', &bytes[pos..]) { 18 | recognize(&raw[pos..pos + i], &mut result)?; 19 | 20 | pos += i + 1; 21 | } else { 22 | return Err(XmlError::UnterminatedEntity { 23 | entity: String::from(&raw[pos - 1..]), 24 | }); 25 | } 26 | 27 | while let Some(i) = memchr(b'&', &bytes[pos..]) { 28 | result.push_str(&raw[pos..pos + i]); 29 | 30 | pos += i + 1; 31 | 32 | if let Some(i) = memchr(b';', &bytes[pos..]) { 33 | recognize(&raw[pos..pos + i], &mut result)?; 34 | 35 | pos += i + 1; 36 | } else { 37 | return Err(XmlError::UnterminatedEntity { 38 | entity: String::from(&raw[pos - 1..]), 39 | }); 40 | } 41 | } 42 | 43 | result.push_str(&raw[pos..]); 44 | 45 | Ok(Cow::Owned(result)) 46 | } else { 47 | Ok(Cow::Borrowed(raw)) 48 | } 49 | } 50 | 51 | fn recognize(entity: &str, result: &mut String) -> XmlResult<()> { 52 | match entity { 53 | "quot" => result.push('"'), 54 | "apos" => result.push('\''), 55 | "gt" => result.push('>'), 56 | "lt" => result.push('<'), 57 | "amp" => result.push('&'), 58 | _ => { 59 | let val = if entity.starts_with("#x") { 60 | u32::from_str_radix(&entity[2..], 16).ok() 61 | } else if entity.starts_with('#') { 62 | u32::from_str_radix(&entity[1..], 10).ok() 63 | } else { 64 | None 65 | }; 66 | match val.and_then(char::from_u32) { 67 | Some(c) => result.push(c), 68 | None => { 69 | return Err(XmlError::UnrecognizedSymbol { 70 | symbol: String::from(entity), 71 | }) 72 | } 73 | } 74 | } 75 | } 76 | Ok(()) 77 | } 78 | 79 | #[test] 80 | fn test_unescape() { 81 | assert_eq!(xml_unescape("test").unwrap(), "test"); 82 | assert_eq!(xml_unescape("<test>").unwrap(), ""); 83 | assert_eq!(xml_unescape("0").unwrap(), "0"); 84 | assert_eq!(xml_unescape("0").unwrap(), "0"); 85 | } 86 | -------------------------------------------------------------------------------- /strong-xml/src/xml_error.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, io::Error as IOError, str::Utf8Error, string::FromUtf8Error}; 2 | use xmlparser::Error as ParserError; 3 | 4 | #[derive(Debug)] 5 | pub enum XmlError { 6 | IO(IOError), 7 | Parser(ParserError), 8 | Utf8(Utf8Error), 9 | UnexpectedEof, 10 | UnexpectedToken { token: String }, 11 | TagMismatch { expected: String, found: String }, 12 | MissingField { name: String, field: String }, 13 | UnterminatedEntity { entity: String }, 14 | UnrecognizedSymbol { symbol: String }, 15 | FromStr(Box), 16 | } 17 | 18 | impl From for XmlError { 19 | fn from(err: IOError) -> Self { 20 | XmlError::IO(err) 21 | } 22 | } 23 | 24 | impl From for XmlError { 25 | fn from(err: Utf8Error) -> Self { 26 | XmlError::Utf8(err) 27 | } 28 | } 29 | 30 | impl From for XmlError { 31 | fn from(err: FromUtf8Error) -> Self { 32 | XmlError::Utf8(err.utf8_error()) 33 | } 34 | } 35 | 36 | impl From for XmlError { 37 | fn from(err: ParserError) -> Self { 38 | XmlError::Parser(err) 39 | } 40 | } 41 | 42 | /// Specialized `Result` which the error value is `Error`. 43 | pub type XmlResult = Result; 44 | 45 | impl Error for XmlError { 46 | fn source(&self) -> Option<&(dyn Error + 'static)> { 47 | use XmlError::*; 48 | match self { 49 | IO(e) => Some(e), 50 | Parser(e) => Some(e), 51 | Utf8(e) => Some(e), 52 | FromStr(e) => Some(e.as_ref()), 53 | _ => None, 54 | } 55 | } 56 | } 57 | 58 | impl std::fmt::Display for XmlError { 59 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 60 | use XmlError::*; 61 | match self { 62 | IO(e) => write!(f, "I/O error: {}", e), 63 | Parser(e) => write!(f, "XML parser error: {}", e), 64 | Utf8(e) => write!(f, "invalid UTF-8: {}", e), 65 | UnexpectedEof => f.write_str("unexpected end of file"), 66 | UnexpectedToken { token } => write!(f, "unexpected token in XML: {:?}", token), 67 | TagMismatch { expected, found } => write!( 68 | f, 69 | "mismatched XML tag; expected {:?}, found {:?}", 70 | expected, found 71 | ), 72 | MissingField { name, field } => { 73 | write!(f, "missing field in XML of {:?}: {:?}", name, field) 74 | } 75 | UnterminatedEntity { entity } => write!(f, "unterminated XML entity: {}", entity), 76 | UnrecognizedSymbol { symbol } => write!(f, "unrecognized XML symbol: {}", symbol), 77 | FromStr(e) => write!(f, "error parsing XML value: {}", e), 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /strong-xml/src/log.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | #[doc(hidden)] 3 | macro_rules! log_start_reading { 4 | ($element:path) => { 5 | $crate::lib::log::debug!(concat!("[", stringify!($element), "] Started reading")); 6 | }; 7 | } 8 | 9 | #[macro_export] 10 | #[doc(hidden)] 11 | macro_rules! log_finish_reading { 12 | ($element:path) => { 13 | $crate::lib::log::debug!(concat!("[", stringify!($element), "] Finished reading")); 14 | }; 15 | } 16 | 17 | #[macro_export] 18 | #[doc(hidden)] 19 | macro_rules! log_start_reading_field { 20 | ($element:path, $name:expr) => { 21 | $crate::lib::log::trace!(concat!( 22 | "[", 23 | stringify!($element), 24 | "] Started reading field `", 25 | stringify!($name), 26 | "`" 27 | )); 28 | }; 29 | } 30 | 31 | #[macro_export] 32 | #[doc(hidden)] 33 | macro_rules! log_finish_reading_field { 34 | ($element:path, $name:expr) => { 35 | $crate::lib::log::trace!(concat!( 36 | "[", 37 | stringify!($element), 38 | "] Finished reading field `", 39 | stringify!($name), 40 | "`" 41 | )); 42 | }; 43 | } 44 | 45 | #[macro_export] 46 | #[doc(hidden)] 47 | macro_rules! log_skip_attribute { 48 | ($element:path, $key:ident) => { 49 | $crate::lib::log::info!( 50 | concat!("[", stringify!($element), "] Skip attribute `{}`"), 51 | $key 52 | ); 53 | }; 54 | } 55 | 56 | #[macro_export] 57 | #[doc(hidden)] 58 | macro_rules! log_skip_element { 59 | ($element:path, $tag:ident) => { 60 | $crate::lib::log::info!( 61 | concat!("[", stringify!($element), "] Skip element `{}`"), 62 | $tag 63 | ); 64 | }; 65 | } 66 | 67 | #[macro_export] 68 | #[doc(hidden)] 69 | macro_rules! log_start_writing { 70 | ($element:path) => { 71 | $crate::lib::log::debug!(concat!("[", stringify!($element), "] Started writing")); 72 | }; 73 | } 74 | 75 | #[macro_export] 76 | #[doc(hidden)] 77 | macro_rules! log_finish_writing { 78 | ($element:path) => { 79 | $crate::lib::log::debug!(concat!("[", stringify!($element), "] Finished writing")); 80 | }; 81 | } 82 | 83 | #[macro_export] 84 | #[doc(hidden)] 85 | macro_rules! log_start_writing_field { 86 | ($element:path, $name:expr) => { 87 | $crate::lib::log::trace!(concat!( 88 | "[", 89 | stringify!($element), 90 | "] Started writing field `", 91 | stringify!($name), 92 | "`" 93 | )); 94 | }; 95 | } 96 | 97 | #[macro_export] 98 | #[doc(hidden)] 99 | macro_rules! log_finish_writing_field { 100 | ($element:path, $name:expr) => { 101 | $crate::lib::log::trace!(concat!( 102 | "[", 103 | stringify!($element), 104 | "] Finished writing field `", 105 | stringify!($name), 106 | "`" 107 | )); 108 | }; 109 | } 110 | -------------------------------------------------------------------------------- /test-suite/tests/log.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::fmt::Write; 3 | use std::sync::{Arc, Mutex}; 4 | 5 | use log::{LevelFilter, Log, Metadata, Record}; 6 | use strong_xml::{XmlRead, XmlResult, XmlWrite}; 7 | 8 | struct Logger(Arc>); 9 | 10 | impl Log for Logger { 11 | fn enabled(&self, _: &Metadata) -> bool { 12 | true 13 | } 14 | 15 | fn log(&self, record: &Record) { 16 | writeln!( 17 | *self.0.lock().unwrap(), 18 | "{:<5} {}", 19 | record.level(), 20 | record.args() 21 | ) 22 | .unwrap(); 23 | } 24 | 25 | fn flush(&self) {} 26 | } 27 | 28 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 29 | #[xml(tag = "tag1")] 30 | struct Tag1<'a> { 31 | #[xml(attr = "att1")] 32 | att1: Option>, 33 | #[xml(text)] 34 | content: Cow<'a, str>, 35 | } 36 | 37 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 38 | #[xml(tag = "tag2")] 39 | struct Tag2<'a> { 40 | #[xml(attr = "att1")] 41 | att1: Cow<'a, str>, 42 | #[xml(attr = "att2")] 43 | att2: Cow<'a, str>, 44 | } 45 | 46 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 47 | #[xml(tag = "tag3")] 48 | struct Tag3<'a> { 49 | #[xml(attr = "att1")] 50 | att1: Cow<'a, str>, 51 | #[xml(child = "tag1")] 52 | tag1: Vec>, 53 | #[xml(child = "tag2")] 54 | tag2: Option>, 55 | #[xml(flatten_text = "text")] 56 | text: Option>, 57 | } 58 | 59 | #[test] 60 | fn info() -> XmlResult<()> { 61 | let buf = Arc::new(Mutex::new(String::new())); 62 | 63 | let logger = Logger(buf.clone()); 64 | 65 | log::set_boxed_logger(Box::new(logger)).unwrap(); 66 | log::set_max_level(LevelFilter::Trace); 67 | 68 | let _ = Tag3::from_str( 69 | "\ 70 | \ 71 | \ 72 | content\ 73 | tag3_content\ 74 | \ 75 | ", 76 | ) 77 | .unwrap(); 78 | 79 | assert_eq!( 80 | r#"DEBUG [Tag3] Started reading 81 | TRACE [Tag3] Started reading field `att1` 82 | TRACE [Tag3] Finished reading field `att1` 83 | INFO [Tag3] Skip attribute `att2` 84 | TRACE [Tag3] Started reading field `tag2` 85 | DEBUG [Tag2] Started reading 86 | TRACE [Tag2] Started reading field `att1` 87 | TRACE [Tag2] Finished reading field `att1` 88 | TRACE [Tag2] Started reading field `att2` 89 | TRACE [Tag2] Finished reading field `att2` 90 | DEBUG [Tag2] Finished reading 91 | TRACE [Tag3] Finished reading field `tag2` 92 | INFO [Tag3] Skip element `tag4` 93 | TRACE [Tag3] Started reading field `tag1` 94 | DEBUG [Tag1] Started reading 95 | TRACE [Tag1] Started reading field `content` 96 | TRACE [Tag1] Finished reading field `content` 97 | DEBUG [Tag1] Finished reading 98 | TRACE [Tag3] Finished reading field `tag1` 99 | TRACE [Tag3] Started reading field `text` 100 | TRACE [Tag3] Finished reading field `text` 101 | INFO [Tag3] Skip element `tag5` 102 | DEBUG [Tag3] Finished reading 103 | "#, 104 | buf.lock().unwrap().as_str() 105 | ); 106 | 107 | Ok(()) 108 | } 109 | -------------------------------------------------------------------------------- /strong-xml-derive/src/write/named.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::{Ident, LitStr}; 4 | 5 | use crate::types::{Field, Type}; 6 | 7 | pub fn write(tag: &LitStr, ele_name: TokenStream, fields: &[Field]) -> TokenStream { 8 | let write_attributes = fields.iter().filter_map(|field| match field { 9 | Field::Attribute { tag, bind, ty, .. } => Some(write_attrs(&tag, &bind, &ty, &ele_name)), 10 | _ => None, 11 | }); 12 | 13 | let write_text = fields.iter().filter_map(|field| match field { 14 | Field::Text { 15 | bind, ty, is_cdata, .. 16 | } => Some(write_text(tag, bind, ty, &ele_name, *is_cdata)), 17 | _ => None, 18 | }); 19 | 20 | let write_flatten_text = fields.iter().filter_map(|field| match field { 21 | Field::FlattenText { 22 | tag, 23 | bind, 24 | ty, 25 | is_cdata, 26 | .. 27 | } => Some(write_flatten_text(tag, bind, ty, &ele_name, *is_cdata)), 28 | _ => None, 29 | }); 30 | 31 | let write_child = fields.iter().filter_map(|field| match field { 32 | Field::Child { bind, ty, .. } => Some(write_child(bind, ty, &ele_name)), 33 | _ => None, 34 | }); 35 | 36 | let is_leaf_element = fields 37 | .iter() 38 | .all(|field| matches!(field, Field::Attribute { .. })); 39 | 40 | let is_text_element = fields 41 | .iter() 42 | .any(|field| matches!(field, Field::Text { .. })); 43 | 44 | let can_self_close = fields.iter().all(|field| match field { 45 | Field::Child { ty, .. } | Field::FlattenText { ty, .. } => ty.is_vec() || ty.is_option(), 46 | _ => true, 47 | }); 48 | 49 | let content_is_empty = fields.iter().filter_map(|field| match field { 50 | Field::Child { ty, bind, .. } | Field::FlattenText { ty, bind, .. } => { 51 | if ty.is_vec() { 52 | Some(quote! { #bind.is_empty() }) 53 | } else if ty.is_option() { 54 | Some(quote! { #bind.is_none() }) 55 | } else { 56 | None 57 | } 58 | } 59 | _ => None, 60 | }); 61 | 62 | let write_element_end = if is_leaf_element { 63 | quote! { writer.write_element_end_empty()?; } 64 | } else if is_text_element { 65 | quote! { #( #write_text )* } 66 | } else { 67 | quote! { 68 | if #can_self_close #( && #content_is_empty )* { 69 | writer.write_element_end_empty()?; 70 | } else { 71 | writer.write_element_end_open()?; 72 | #( #write_child )* 73 | #( #write_flatten_text )* 74 | writer.write_element_end_close(#tag)?; 75 | } 76 | } 77 | }; 78 | 79 | quote! { 80 | strong_xml::log_start_writing!(#ele_name); 81 | 82 | writer.write_element_start(#tag)?; 83 | 84 | #( #write_attributes )* 85 | 86 | #write_element_end 87 | 88 | strong_xml::log_finish_writing!(#ele_name); 89 | } 90 | } 91 | 92 | fn write_attrs(tag: &LitStr, name: &Ident, ty: &Type, ele_name: &TokenStream) -> TokenStream { 93 | let to_str = to_str(ty); 94 | 95 | if ty.is_vec() { 96 | panic!("`attr` attribute doesn't support Vec."); 97 | } else if ty.is_option() { 98 | quote! { 99 | strong_xml::log_start_writing_field!(#ele_name, #name); 100 | 101 | if let Some(__value) = #name { 102 | writer.write_attribute(#tag, #to_str)?; 103 | } 104 | 105 | strong_xml::log_finish_writing_field!(#ele_name, #name); 106 | } 107 | } else { 108 | quote! { 109 | strong_xml::log_start_writing_field!(#ele_name, #name); 110 | 111 | let __value = #name; 112 | writer.write_attribute(#tag, #to_str)?; 113 | 114 | strong_xml::log_finish_writing_field!(#ele_name, #name); 115 | } 116 | } 117 | } 118 | 119 | fn write_child(name: &Ident, ty: &Type, ele_name: &TokenStream) -> TokenStream { 120 | match ty { 121 | Type::OptionT(_) => quote! { 122 | strong_xml::log_start_writing_field!(#ele_name, #name); 123 | 124 | if let Some(ref ele) = #name { 125 | ele.to_writer(&mut writer)?; 126 | } 127 | 128 | strong_xml::log_finish_writing_field!(#ele_name, #name); 129 | }, 130 | Type::VecT(_) => quote! { 131 | strong_xml::log_start_writing_field!(#ele_name, #name); 132 | 133 | for ele in #name { 134 | ele.to_writer(&mut writer)?; 135 | } 136 | 137 | strong_xml::log_finish_writing_field!(#ele_name, #name); 138 | }, 139 | Type::T(_) => quote! { 140 | strong_xml::log_start_writing_field!(#ele_name, #name); 141 | 142 | &#name.to_writer(&mut writer)?; 143 | 144 | strong_xml::log_finish_writing_field!(#ele_name, #name); 145 | }, 146 | _ => panic!("`child` attribute only supports Vec, Option and T."), 147 | } 148 | } 149 | 150 | fn write_text( 151 | tag: &LitStr, 152 | name: &Ident, 153 | ty: &Type, 154 | ele_name: &TokenStream, 155 | is_cdata: bool, 156 | ) -> TokenStream { 157 | let to_str = to_str(ty); 158 | let wrtie_fn = if is_cdata { 159 | quote!(write_cdata_text) 160 | } else { 161 | quote!(write_text) 162 | }; 163 | 164 | quote! { 165 | writer.write_element_end_open()?; 166 | 167 | strong_xml::log_start_writing_field!(#ele_name, #name); 168 | 169 | let __value = &#name; 170 | 171 | writer.#wrtie_fn(#to_str)?; 172 | 173 | strong_xml::log_finish_writing_field!(#ele_name, #name); 174 | 175 | writer.write_element_end_close(#tag)?; 176 | } 177 | } 178 | 179 | fn write_flatten_text( 180 | tag: &LitStr, 181 | name: &Ident, 182 | ty: &Type, 183 | ele_name: &TokenStream, 184 | is_cdata: bool, 185 | ) -> TokenStream { 186 | let to_str = to_str(ty); 187 | 188 | if ty.is_vec() { 189 | quote! { 190 | strong_xml::log_finish_writing_field!(#ele_name, #name); 191 | 192 | for __value in #name { 193 | writer.write_flatten_text(#tag, #to_str, #is_cdata)?; 194 | } 195 | 196 | strong_xml::log_finish_writing_field!(#ele_name, #name); 197 | } 198 | } else if ty.is_option() { 199 | quote! { 200 | strong_xml::log_finish_writing_field!(#ele_name, #name); 201 | 202 | if let Some(__value) = #name { 203 | writer.write_flatten_text(#tag, #to_str, #is_cdata)?; 204 | } 205 | 206 | strong_xml::log_finish_writing_field!(#ele_name, #name); 207 | } 208 | } else { 209 | quote! { 210 | strong_xml::log_finish_writing_field!(#ele_name, #name); 211 | 212 | let __value = &#name; 213 | writer.write_flatten_text(#tag, #to_str, #is_cdata)?; 214 | 215 | strong_xml::log_finish_writing_field!(#ele_name, #name); 216 | } 217 | } 218 | } 219 | 220 | fn to_str(ty: &Type) -> TokenStream { 221 | match &ty { 222 | Type::CowStr | Type::OptionCowStr | Type::VecCowStr => { 223 | quote! { __value } 224 | } 225 | Type::Bool | Type::OptionBool | Type::VecBool => quote! { 226 | match __value { 227 | true => "true", 228 | false => "false" 229 | } 230 | }, 231 | Type::T(_) | Type::OptionT(_) | Type::VecT(_) => { 232 | quote! { &format!("{}", __value) } 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /strong-xml/README.md: -------------------------------------------------------------------------------- 1 | # strong-xml 2 | 3 | Strong typed xml, based on xmlparser. 4 | 5 | [![Build Status](https://github.com/PoiScript/strong-xml/workflows/Test/badge.svg)](https://github.com/PoiScript/strong-xml/actions?query=workflow%3ATest) 6 | [![Crates.io](https://img.shields.io/crates/v/strong-xml.svg)](https://crates.io/crates/strong-xml) 7 | [![Document](https://docs.rs/strong-xml/badge.svg)](https://docs.rs/strong-xml) 8 | 9 | ### Quick Start 10 | 11 | ```toml 12 | strong-xml = "0.6" 13 | ``` 14 | 15 | ```rust 16 | use std::borrow::Cow; 17 | use strong_xml::{XmlRead, XmlWrite}; 18 | 19 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 20 | #[xml(tag = "parent")] 21 | struct Parent<'a> { 22 | #[xml(attr = "attr1")] 23 | attr1: Cow<'a, str>, 24 | #[xml(attr = "attr2")] 25 | attr2: Option>, 26 | #[xml(child = "child")] 27 | child: Vec>, 28 | } 29 | 30 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 31 | #[xml(tag = "child")] 32 | struct Child<'a> { 33 | #[xml(text)] 34 | text: Cow<'a, str>, 35 | } 36 | 37 | assert_eq!( 38 | (Parent { attr1: "val".into(), attr2: None, child: vec![] }).to_string().unwrap(), 39 | r#""# 40 | ); 41 | 42 | assert_eq!( 43 | Parent::from_str(r#""#).unwrap(), 44 | Parent { attr1: "val".into(), attr2: Some("val".into()), child: vec![Child { text: "".into() }] } 45 | ); 46 | ``` 47 | 48 | ### Attributes 49 | 50 | #### `#[xml(tag = "")]` 51 | 52 | Specifies the xml tag of a struct or an enum variant. 53 | 54 | ```rust 55 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 56 | #[xml(tag = "parent")] 57 | struct Parent {} 58 | 59 | assert_eq!( 60 | (Parent {}).to_string().unwrap(), 61 | r#""# 62 | ); 63 | 64 | assert_eq!( 65 | Parent::from_str(r#""#).unwrap(), 66 | Parent {} 67 | ); 68 | ``` 69 | 70 | ```rust 71 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 72 | #[xml(tag = "tag1")] 73 | struct Tag1 {} 74 | 75 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 76 | #[xml(tag = "tag2")] 77 | struct Tag2 {} 78 | 79 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 80 | enum Tag { 81 | #[xml(tag = "tag1")] 82 | Tag1(Tag1), 83 | #[xml(tag = "tag2")] 84 | Tag2(Tag2), 85 | } 86 | 87 | assert_eq!( 88 | (Tag::Tag1(Tag1 {})).to_string().unwrap(), 89 | r#""# 90 | ); 91 | 92 | assert_eq!( 93 | Tag::from_str(r#""#).unwrap(), 94 | Tag::Tag2(Tag2 {}) 95 | ); 96 | ``` 97 | 98 | #### `#[xml(attr = "")]` 99 | 100 | Specifies that a struct field is attribute. Support 101 | `Cow`, `Option>`, `T` and `Option` 102 | where `T: FromStr + Display`. 103 | 104 | ```rust 105 | use strong_xml::{XmlRead, XmlWrite}; 106 | 107 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 108 | #[xml(tag = "parent")] 109 | struct Parent { 110 | #[xml(attr = "attr")] 111 | attr: usize 112 | } 113 | 114 | assert_eq!( 115 | (Parent { attr: 42 }).to_string().unwrap(), 116 | r#""# 117 | ); 118 | 119 | assert_eq!( 120 | Parent::from_str(r#""#).unwrap(), 121 | Parent { attr: 48 } 122 | ); 123 | ``` 124 | 125 | #### `#[xml(child = "")]` 126 | 127 | Specifies that a struct field is a child element. Support 128 | `T`, `Option`, `Vec` where `T: XmlRead + XmlWrite`. 129 | 130 | ```rust 131 | use strong_xml::{XmlRead, XmlWrite}; 132 | 133 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 134 | #[xml(tag = "tag1")] 135 | struct Tag1 {} 136 | 137 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 138 | #[xml(tag = "tag2")] 139 | struct Tag2 {} 140 | 141 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 142 | #[xml(tag = "tag3")] 143 | struct Tag3 {} 144 | 145 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 146 | enum Tag12 { 147 | #[xml(tag = "tag1")] 148 | Tag1(Tag1), 149 | #[xml(tag = "tag2")] 150 | Tag2(Tag2), 151 | } 152 | 153 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 154 | #[xml(tag = "parent")] 155 | struct Parent { 156 | #[xml(child = "tag3")] 157 | tag3: Vec, 158 | #[xml(child = "tag1", child = "tag2")] 159 | tag12: Option 160 | } 161 | 162 | assert_eq!( 163 | (Parent { tag3: vec![Tag3 {}], tag12: None }).to_string().unwrap(), 164 | r#""# 165 | ); 166 | 167 | assert_eq!( 168 | Parent::from_str(r#""#).unwrap(), 169 | Parent { tag3: vec![], tag12: Some(Tag12::Tag2(Tag2 {})) } 170 | ); 171 | ``` 172 | 173 | #### `#[xml(text)]` 174 | 175 | Specifies that a struct field is text content. 176 | Support `Cow`, `Vec>`, `Option>`, 177 | `T`, `Vec`, `Option` where `T: FromStr + Display`. 178 | 179 | ```rust 180 | use std::borrow::Cow; 181 | use strong_xml::{XmlRead, XmlWrite}; 182 | 183 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 184 | #[xml(tag = "parent")] 185 | struct Parent<'a> { 186 | #[xml(text)] 187 | content: Cow<'a, str>, 188 | } 189 | 190 | assert_eq!( 191 | (Parent { content: "content".into() }).to_string().unwrap(), 192 | r#"content"# 193 | ); 194 | 195 | assert_eq!( 196 | Parent::from_str(r#""#).unwrap(), 197 | Parent { content: "".into() } 198 | ); 199 | ``` 200 | 201 | #### `#[xml(flatten_text = "")]` 202 | 203 | Specifies that a struct field is child text element. 204 | Support `Cow`, `Vec>`, `Option>`, 205 | `T`, `Vec`, `Option` where `T: FromStr + Display`. 206 | 207 | ```rust 208 | use std::borrow::Cow; 209 | use strong_xml::{XmlRead, XmlWrite}; 210 | 211 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 212 | #[xml(tag = "parent")] 213 | struct Parent<'a> { 214 | #[xml(flatten_text = "child")] 215 | content: Cow<'a, str>, 216 | } 217 | 218 | assert_eq!( 219 | (Parent { content: "content".into() }).to_string().unwrap(), 220 | r#"content"# 221 | ); 222 | 223 | assert_eq!( 224 | Parent::from_str(r#""#).unwrap(), 225 | Parent { content: "".into() } 226 | ); 227 | ``` 228 | 229 | #### `#[xml(cdata)]` 230 | 231 | Specifies a CDATA text. Should be used together with `text` or `flatten_text`. 232 | 233 | > `#[xml(cdata)]` only changes the behavior of writing, 234 | > text field without `#[xml(cdata)]` can still works with cdata tag. 235 | 236 | ```rust 237 | use std::borrow::Cow; 238 | use strong_xml::{XmlRead, XmlWrite}; 239 | 240 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 241 | #[xml(tag = "parent")] 242 | struct Parent<'a> { 243 | #[xml(text, cdata)] 244 | content: Cow<'a, str>, 245 | } 246 | 247 | assert_eq!( 248 | (Parent { content: "content".into() }).to_string().unwrap(), 249 | r#""# 250 | ); 251 | ``` 252 | 253 | ```rust 254 | use std::borrow::Cow; 255 | use strong_xml::{XmlRead, XmlWrite}; 256 | 257 | #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 258 | #[xml(tag = "parent")] 259 | struct Parent<'a> { 260 | #[xml(flatten_text = "code", cdata)] 261 | content: Cow<'a, str>, 262 | } 263 | 264 | assert_eq!( 265 | (Parent { content: r#"hello("deities!");"#.into() }).to_string().unwrap(), 266 | r#""# 267 | ); 268 | ``` 269 | 270 | #### `#[xml(default)]` 271 | 272 | Use `Default::default()` if the value is not present when reading. 273 | 274 | ```rust 275 | use std::borrow::Cow; 276 | use strong_xml::XmlRead; 277 | 278 | #[derive(XmlRead, PartialEq, Debug)] 279 | #[xml(tag = "root")] 280 | struct Root { 281 | #[xml(default, attr = "attr")] 282 | attr: bool, 283 | } 284 | 285 | assert_eq!( 286 | Root::from_str(r#""#).unwrap(), 287 | Root { attr: false } 288 | ); 289 | 290 | assert_eq!( 291 | Root::from_str(r#""#).unwrap(), 292 | Root { attr: true } 293 | ); 294 | ``` 295 | 296 | ### License 297 | 298 | MIT 299 | 300 | License: MIT 301 | -------------------------------------------------------------------------------- /strong-xml-derive/src/read/named.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::{Ident, LitStr}; 4 | 5 | use crate::types::{Field, Type}; 6 | 7 | pub fn read(tag: &LitStr, ele_name: TokenStream, fields: &[Field]) -> TokenStream { 8 | let init_fields = fields.iter().map(|field| match field { 9 | Field::Attribute { bind, ty, .. } 10 | | Field::Child { bind, ty, .. } 11 | | Field::FlattenText { bind, ty, .. } => init_value(bind, ty), 12 | Field::Text { bind, .. } => quote! { let #bind; }, 13 | }); 14 | 15 | let return_fields = fields.iter().map(|field| match field { 16 | Field::Attribute { 17 | name, 18 | bind, 19 | ty, 20 | default, 21 | .. 22 | } 23 | | Field::Child { 24 | name, 25 | bind, 26 | ty, 27 | default, 28 | .. 29 | } 30 | | Field::FlattenText { 31 | name, 32 | bind, 33 | ty, 34 | default, 35 | .. 36 | } => return_value(name, bind, ty, *default, &ele_name), 37 | Field::Text { name, bind, ty, .. } => return_value(name, bind, ty, false, &ele_name), 38 | }); 39 | 40 | let read_attr_fields = fields.iter().filter_map(|field| match field { 41 | Field::Attribute { 42 | bind, 43 | ty, 44 | tag, 45 | name, 46 | .. 47 | } => Some(read_attrs(&tag, &bind, &name, &ty, &ele_name)), 48 | _ => None, 49 | }); 50 | 51 | let read_child_fields = fields.iter().filter_map(|field| match field { 52 | Field::Child { 53 | bind, 54 | ty, 55 | tags, 56 | name, 57 | .. 58 | } => Some(read_children(tags, bind, name, ty, &ele_name)), 59 | _ => None, 60 | }); 61 | 62 | let read_flatten_text_fields = fields.iter().filter_map(|field| match field { 63 | Field::FlattenText { 64 | bind, 65 | ty, 66 | tag, 67 | name, 68 | .. 69 | } => Some(read_flatten_text(tag, bind, name, ty, &ele_name)), 70 | _ => None, 71 | }); 72 | 73 | let read_text_fields = fields.iter().filter_map(|field| match field { 74 | Field::Text { bind, ty, name, .. } => Some(read_text(&tag, bind, name, ty, &ele_name)), 75 | _ => None, 76 | }); 77 | 78 | let is_text_element = fields 79 | .iter() 80 | .any(|field| matches!(field, Field::Text { .. })); 81 | 82 | let return_fields = quote! { 83 | let __res = #ele_name { 84 | #( #return_fields, )* 85 | }; 86 | 87 | strong_xml::log_finish_reading!(#ele_name); 88 | 89 | return Ok(__res); 90 | }; 91 | 92 | let read_content = if is_text_element { 93 | quote! { 94 | #( #read_text_fields )* 95 | #return_fields 96 | } 97 | } else { 98 | quote! { 99 | if let Token::ElementEnd { end: ElementEnd::Empty, .. } = reader.next().unwrap()? { 100 | #return_fields 101 | } 102 | 103 | while let Some(__tag) = reader.find_element_start(Some(#tag))? { 104 | match __tag { 105 | #( #read_child_fields, )* 106 | #( #read_flatten_text_fields, )* 107 | tag => { 108 | strong_xml::log_skip_element!(#ele_name, tag); 109 | // skip the start tag 110 | reader.next(); 111 | reader.read_to_end(tag)?; 112 | }, 113 | } 114 | } 115 | 116 | #return_fields 117 | } 118 | }; 119 | 120 | quote! { 121 | strong_xml::log_start_reading!(#ele_name); 122 | 123 | #( #init_fields )* 124 | 125 | reader.read_till_element_start(#tag)?; 126 | 127 | while let Some((__key, __value)) = reader.find_attribute()? { 128 | match __key { 129 | #( #read_attr_fields, )* 130 | key => { 131 | strong_xml::log_skip_attribute!(#ele_name, key); 132 | }, 133 | } 134 | } 135 | 136 | #read_content 137 | } 138 | } 139 | 140 | fn init_value(name: &Ident, ty: &Type) -> TokenStream { 141 | if ty.is_vec() { 142 | quote! { let mut #name = Vec::new(); } 143 | } else { 144 | quote! { let mut #name = None; } 145 | } 146 | } 147 | 148 | fn return_value( 149 | name: &TokenStream, 150 | bind: &Ident, 151 | ty: &Type, 152 | default: bool, 153 | ele_name: &TokenStream, 154 | ) -> TokenStream { 155 | if ty.is_vec() || ty.is_option() { 156 | quote! { #name: #bind } 157 | } else if default { 158 | quote! { #name: #bind.unwrap_or_default() } 159 | } else { 160 | quote! { 161 | #name: #bind.ok_or(XmlError::MissingField { 162 | name: stringify!(#ele_name).to_owned(), 163 | field: stringify!(#name).to_owned(), 164 | })? 165 | } 166 | } 167 | } 168 | 169 | fn read_attrs( 170 | tag: &LitStr, 171 | bind: &Ident, 172 | name: &TokenStream, 173 | ty: &Type, 174 | ele_name: &TokenStream, 175 | ) -> TokenStream { 176 | let from_str = from_str(ty); 177 | 178 | if ty.is_vec() { 179 | panic!("`attr` attribute doesn't support Vec."); 180 | } else { 181 | quote! { 182 | #tag => { 183 | strong_xml::log_start_reading_field!(#ele_name, #name); 184 | 185 | #bind = Some(#from_str); 186 | 187 | strong_xml::log_finish_reading_field!(#ele_name, #name); 188 | } 189 | } 190 | } 191 | } 192 | 193 | fn read_text( 194 | tag: &LitStr, 195 | bind: &Ident, 196 | name: &TokenStream, 197 | ty: &Type, 198 | ele_name: &TokenStream, 199 | ) -> TokenStream { 200 | let from_str = from_str(ty); 201 | 202 | if ty.is_vec() { 203 | panic!("`text` attribute doesn't support Vec."); 204 | } else { 205 | quote! { 206 | strong_xml::log_start_reading_field!(#ele_name, #name); 207 | 208 | let __value = reader.read_text(#tag)?; 209 | #bind = Some(#from_str); 210 | 211 | strong_xml::log_finish_reading_field!(#ele_name, #name); 212 | } 213 | } 214 | } 215 | 216 | fn read_children( 217 | tags: &[LitStr], 218 | bind: &Ident, 219 | name: &TokenStream, 220 | ty: &Type, 221 | ele_name: &TokenStream, 222 | ) -> TokenStream { 223 | let from_reader = match &ty { 224 | Type::VecT(ty) => quote! { 225 | #bind.push(<#ty as strong_xml::XmlRead>::from_reader(reader)?); 226 | }, 227 | Type::OptionT(ty) | Type::T(ty) => quote! { 228 | #bind = Some(<#ty as strong_xml::XmlRead>::from_reader(reader)?); 229 | }, 230 | _ => panic!("`child` attribute only supports Vec, Option and T."), 231 | }; 232 | 233 | quote! { 234 | #( #tags )|* => { 235 | strong_xml::log_start_reading_field!(#ele_name, #name); 236 | 237 | #from_reader 238 | 239 | strong_xml::log_finish_reading_field!(#ele_name, #name); 240 | } 241 | } 242 | } 243 | 244 | fn read_flatten_text( 245 | tag: &LitStr, 246 | bind: &Ident, 247 | name: &TokenStream, 248 | ty: &Type, 249 | ele_name: &TokenStream, 250 | ) -> TokenStream { 251 | let from_str = from_str(ty); 252 | 253 | let read_text = if ty.is_vec() { 254 | quote! { 255 | let __value = reader.read_text(#tag)?; 256 | #bind.push(#from_str); 257 | } 258 | } else { 259 | quote! { 260 | let __value = reader.read_text(#tag)?; 261 | #bind = Some(#from_str); 262 | } 263 | }; 264 | 265 | quote! { 266 | #tag => { 267 | // skip element start 268 | reader.next(); 269 | 270 | strong_xml::log_start_reading_field!(#ele_name, #name); 271 | 272 | #read_text 273 | 274 | strong_xml::log_finish_reading_field!(#ele_name, #name); 275 | } 276 | } 277 | } 278 | 279 | fn from_str(ty: &Type) -> TokenStream { 280 | match &ty { 281 | Type::CowStr | Type::OptionCowStr | Type::VecCowStr => quote! { __value }, 282 | Type::Bool | Type::OptionBool | Type::VecBool => quote! { 283 | match &*__value { 284 | "t" | "true" | "y" | "yes" | "on" | "1" => true, 285 | "f" | "false" | "n" | "no" | "off" | "0" => false, 286 | _ => ::from_str(&__value).map_err(|e| XmlError::FromStr(e.into()))? 287 | } 288 | }, 289 | Type::T(ty) | Type::OptionT(ty) | Type::VecT(ty) => quote! { 290 | <#ty as std::str::FromStr>::from_str(&__value).map_err(|e| XmlError::FromStr(e.into()))? 291 | }, 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /strong-xml/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Strong typed xml, based on xmlparser. 2 | //! 3 | //! [![Build Status](https://github.com/PoiScript/strong-xml/workflows/Test/badge.svg)](https://github.com/PoiScript/strong-xml/actions?query=workflow%3ATest) 4 | //! [![Crates.io](https://img.shields.io/crates/v/strong-xml.svg)](https://crates.io/crates/strong-xml) 5 | //! [![Document](https://docs.rs/strong-xml/badge.svg)](https://docs.rs/strong-xml) 6 | //! 7 | //! ## Quick Start 8 | //! 9 | //! ```toml 10 | //! strong-xml = "0.6" 11 | //! ``` 12 | //! 13 | //! ```rust 14 | //! use std::borrow::Cow; 15 | //! use strong_xml::{XmlRead, XmlWrite}; 16 | //! 17 | //! #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 18 | //! #[xml(tag = "parent")] 19 | //! struct Parent<'a> { 20 | //! #[xml(attr = "attr1")] 21 | //! attr1: Cow<'a, str>, 22 | //! #[xml(attr = "attr2")] 23 | //! attr2: Option>, 24 | //! #[xml(child = "child")] 25 | //! child: Vec>, 26 | //! } 27 | //! 28 | //! #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 29 | //! #[xml(tag = "child")] 30 | //! struct Child<'a> { 31 | //! #[xml(text)] 32 | //! text: Cow<'a, str>, 33 | //! } 34 | //! 35 | //! assert_eq!( 36 | //! (Parent { attr1: "val".into(), attr2: None, child: vec![] }).to_string().unwrap(), 37 | //! r#""# 38 | //! ); 39 | //! 40 | //! assert_eq!( 41 | //! Parent::from_str(r#""#).unwrap(), 42 | //! Parent { attr1: "val".into(), attr2: Some("val".into()), child: vec![Child { text: "".into() }] } 43 | //! ); 44 | //! ``` 45 | //! 46 | //! ## Attributes 47 | //! 48 | //! ### `#[xml(tag = "")]` 49 | //! 50 | //! Specifies the xml tag of a struct or an enum variant. 51 | //! 52 | //! ```rust 53 | //! # use strong_xml::{XmlRead, XmlWrite}; 54 | //! #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 55 | //! #[xml(tag = "parent")] 56 | //! struct Parent {} 57 | //! 58 | //! assert_eq!( 59 | //! (Parent {}).to_string().unwrap(), 60 | //! r#""# 61 | //! ); 62 | //! 63 | //! assert_eq!( 64 | //! Parent::from_str(r#""#).unwrap(), 65 | //! Parent {} 66 | //! ); 67 | //! ``` 68 | //! 69 | //! ```rust 70 | //! # use strong_xml::{XmlRead, XmlWrite}; 71 | //! #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 72 | //! #[xml(tag = "tag1")] 73 | //! struct Tag1 {} 74 | //! 75 | //! #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 76 | //! #[xml(tag = "tag2")] 77 | //! struct Tag2 {} 78 | //! 79 | //! #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 80 | //! enum Tag { 81 | //! #[xml(tag = "tag1")] 82 | //! Tag1(Tag1), 83 | //! #[xml(tag = "tag2")] 84 | //! Tag2(Tag2), 85 | //! } 86 | //! 87 | //! assert_eq!( 88 | //! (Tag::Tag1(Tag1 {})).to_string().unwrap(), 89 | //! r#""# 90 | //! ); 91 | //! 92 | //! assert_eq!( 93 | //! Tag::from_str(r#""#).unwrap(), 94 | //! Tag::Tag2(Tag2 {}) 95 | //! ); 96 | //! ``` 97 | //! 98 | //! ### `#[xml(attr = "")]` 99 | //! 100 | //! Specifies that a struct field is attribute. Support 101 | //! `Cow`, `Option>`, `T` and `Option` 102 | //! where `T: FromStr + Display`. 103 | //! 104 | //! ```rust 105 | //! use strong_xml::{XmlRead, XmlWrite}; 106 | //! 107 | //! #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 108 | //! #[xml(tag = "parent")] 109 | //! struct Parent { 110 | //! #[xml(attr = "attr")] 111 | //! attr: usize 112 | //! } 113 | //! 114 | //! assert_eq!( 115 | //! (Parent { attr: 42 }).to_string().unwrap(), 116 | //! r#""# 117 | //! ); 118 | //! 119 | //! assert_eq!( 120 | //! Parent::from_str(r#""#).unwrap(), 121 | //! Parent { attr: 48 } 122 | //! ); 123 | //! ``` 124 | //! 125 | //! ### `#[xml(child = "")]` 126 | //! 127 | //! Specifies that a struct field is a child element. Support 128 | //! `T`, `Option`, `Vec` where `T: XmlRead + XmlWrite`. 129 | //! 130 | //! ```rust 131 | //! use strong_xml::{XmlRead, XmlWrite}; 132 | //! 133 | //! #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 134 | //! #[xml(tag = "tag1")] 135 | //! struct Tag1 {} 136 | //! 137 | //! #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 138 | //! #[xml(tag = "tag2")] 139 | //! struct Tag2 {} 140 | //! 141 | //! #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 142 | //! #[xml(tag = "tag3")] 143 | //! struct Tag3 {} 144 | //! 145 | //! #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 146 | //! enum Tag12 { 147 | //! #[xml(tag = "tag1")] 148 | //! Tag1(Tag1), 149 | //! #[xml(tag = "tag2")] 150 | //! Tag2(Tag2), 151 | //! } 152 | //! 153 | //! #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 154 | //! #[xml(tag = "parent")] 155 | //! struct Parent { 156 | //! #[xml(child = "tag3")] 157 | //! tag3: Vec, 158 | //! #[xml(child = "tag1", child = "tag2")] 159 | //! tag12: Option 160 | //! } 161 | //! 162 | //! assert_eq!( 163 | //! (Parent { tag3: vec![Tag3 {}], tag12: None }).to_string().unwrap(), 164 | //! r#""# 165 | //! ); 166 | //! 167 | //! assert_eq!( 168 | //! Parent::from_str(r#""#).unwrap(), 169 | //! Parent { tag3: vec![], tag12: Some(Tag12::Tag2(Tag2 {})) } 170 | //! ); 171 | //! ``` 172 | //! 173 | //! ### `#[xml(text)]` 174 | //! 175 | //! Specifies that a struct field is text content. 176 | //! Support `Cow`, `Vec>`, `Option>`, 177 | //! `T`, `Vec`, `Option` where `T: FromStr + Display`. 178 | //! 179 | //! ```rust 180 | //! use std::borrow::Cow; 181 | //! use strong_xml::{XmlRead, XmlWrite}; 182 | //! 183 | //! #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 184 | //! #[xml(tag = "parent")] 185 | //! struct Parent<'a> { 186 | //! #[xml(text)] 187 | //! content: Cow<'a, str>, 188 | //! } 189 | //! 190 | //! assert_eq!( 191 | //! (Parent { content: "content".into() }).to_string().unwrap(), 192 | //! r#"content"# 193 | //! ); 194 | //! 195 | //! assert_eq!( 196 | //! Parent::from_str(r#""#).unwrap(), 197 | //! Parent { content: "".into() } 198 | //! ); 199 | //! ``` 200 | //! 201 | //! ### `#[xml(flatten_text = "")]` 202 | //! 203 | //! Specifies that a struct field is child text element. 204 | //! Support `Cow`, `Vec>`, `Option>`, 205 | //! `T`, `Vec`, `Option` where `T: FromStr + Display`. 206 | //! 207 | //! ```rust 208 | //! use std::borrow::Cow; 209 | //! use strong_xml::{XmlRead, XmlWrite}; 210 | //! 211 | //! #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 212 | //! #[xml(tag = "parent")] 213 | //! struct Parent<'a> { 214 | //! #[xml(flatten_text = "child")] 215 | //! content: Cow<'a, str>, 216 | //! } 217 | //! 218 | //! assert_eq!( 219 | //! (Parent { content: "content".into() }).to_string().unwrap(), 220 | //! r#"content"# 221 | //! ); 222 | //! 223 | //! assert_eq!( 224 | //! Parent::from_str(r#""#).unwrap(), 225 | //! Parent { content: "".into() } 226 | //! ); 227 | //! ``` 228 | //! 229 | //! ### `#[xml(cdata)]` 230 | //! 231 | //! Specifies a CDATA text. Should be used together with `text` or `flatten_text`. 232 | //! 233 | //! > `#[xml(cdata)]` only changes the behavior of writing, 234 | //! > text field without `#[xml(cdata)]` can still works with cdata tag. 235 | //! 236 | //! ```rust 237 | //! use std::borrow::Cow; 238 | //! use strong_xml::{XmlRead, XmlWrite}; 239 | //! 240 | //! #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 241 | //! #[xml(tag = "parent")] 242 | //! struct Parent<'a> { 243 | //! #[xml(text, cdata)] 244 | //! content: Cow<'a, str>, 245 | //! } 246 | //! 247 | //! assert_eq!( 248 | //! (Parent { content: "content".into() }).to_string().unwrap(), 249 | //! r#""# 250 | //! ); 251 | //! ``` 252 | //! 253 | //! ```rust 254 | //! use std::borrow::Cow; 255 | //! use strong_xml::{XmlRead, XmlWrite}; 256 | //! 257 | //! #[derive(XmlWrite, XmlRead, PartialEq, Debug)] 258 | //! #[xml(tag = "parent")] 259 | //! struct Parent<'a> { 260 | //! #[xml(flatten_text = "code", cdata)] 261 | //! content: Cow<'a, str>, 262 | //! } 263 | //! 264 | //! assert_eq!( 265 | //! (Parent { content: r#"hello("deities!");"#.into() }).to_string().unwrap(), 266 | //! r#""# 267 | //! ); 268 | //! ``` 269 | //! 270 | //! ### `#[xml(default)]` 271 | //! 272 | //! Use `Default::default()` if the value is not present when reading. 273 | //! 274 | //! ```rust 275 | //! use std::borrow::Cow; 276 | //! use strong_xml::XmlRead; 277 | //! 278 | //! #[derive(XmlRead, PartialEq, Debug)] 279 | //! #[xml(tag = "root")] 280 | //! struct Root { 281 | //! #[xml(default, attr = "attr")] 282 | //! attr: bool, 283 | //! } 284 | //! 285 | //! assert_eq!( 286 | //! Root::from_str(r#""#).unwrap(), 287 | //! Root { attr: false } 288 | //! ); 289 | //! 290 | //! assert_eq!( 291 | //! Root::from_str(r#""#).unwrap(), 292 | //! Root { attr: true } 293 | //! ); 294 | //! ``` 295 | //! 296 | //! ## License 297 | //! 298 | //! MIT 299 | 300 | #[cfg(feature = "log")] 301 | mod log; 302 | #[cfg(not(feature = "log"))] 303 | mod noop_log; 304 | 305 | #[cfg(feature = "log")] 306 | #[doc(hidden)] 307 | pub mod lib { 308 | pub use log; 309 | } 310 | 311 | mod xml_error; 312 | mod xml_escape; 313 | mod xml_read; 314 | mod xml_reader; 315 | mod xml_unescape; 316 | mod xml_write; 317 | mod xml_writer; 318 | 319 | pub use self::xml_error::{XmlError, XmlResult}; 320 | pub use self::xml_read::{XmlRead, XmlReadOwned}; 321 | pub use self::xml_reader::XmlReader; 322 | pub use self::xml_write::XmlWrite; 323 | pub use self::xml_writer::XmlWriter; 324 | 325 | pub use strong_xml_derive::{XmlRead, XmlWrite}; 326 | 327 | pub use xmlparser; 328 | 329 | pub mod utils { 330 | pub use super::xml_escape::xml_escape; 331 | pub use super::xml_unescape::xml_unescape; 332 | } 333 | -------------------------------------------------------------------------------- /strong-xml/src/xml_reader.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::iter::{Iterator, Peekable}; 3 | 4 | use xmlparser::ElementEnd; 5 | use xmlparser::Error; 6 | use xmlparser::Token; 7 | use xmlparser::Tokenizer; 8 | 9 | use crate::xml_unescape::xml_unescape; 10 | use crate::{XmlError, XmlResult}; 11 | 12 | /// Xml Reader 13 | /// 14 | /// It behaves almost exactly like `xmlparser::Tokenizer::from("...").peekable()` 15 | /// but with some helper functions. 16 | pub struct XmlReader<'a> { 17 | tokenizer: Peekable>, 18 | } 19 | 20 | impl<'a> XmlReader<'a> { 21 | #[inline] 22 | pub fn new(text: &'a str) -> XmlReader<'a> { 23 | XmlReader { 24 | tokenizer: Tokenizer::from(text).peekable(), 25 | } 26 | } 27 | 28 | #[inline] 29 | pub fn next(&mut self) -> Option, Error>> { 30 | self.tokenizer.next() 31 | } 32 | 33 | #[inline] 34 | pub fn peek(&mut self) -> Option<&Result, Error>> { 35 | self.tokenizer.peek() 36 | } 37 | 38 | #[inline] 39 | pub fn read_text(&mut self, end_tag: &str) -> XmlResult> { 40 | let mut res = None; 41 | 42 | while let Some(token) = self.next() { 43 | match token? { 44 | Token::ElementEnd { 45 | end: ElementEnd::Open, 46 | .. 47 | } 48 | | Token::Attribute { .. } => (), 49 | Token::Text { text } => { 50 | res = Some(xml_unescape(text.as_str())?); 51 | } 52 | Token::Cdata { text, .. } => { 53 | res = Some(Cow::Borrowed(text.as_str())); 54 | } 55 | Token::ElementEnd { 56 | end: ElementEnd::Close(_, _), 57 | span, 58 | } => { 59 | let span = span.as_str(); // 60 | let tag = &span[2..span.len() - 1]; // remove `` 61 | if end_tag == tag { 62 | break; 63 | } else { 64 | return Err(XmlError::TagMismatch { 65 | expected: end_tag.to_owned(), 66 | found: tag.to_owned(), 67 | }); 68 | } 69 | } 70 | token => { 71 | return Err(XmlError::UnexpectedToken { 72 | token: format!("{:?}", token), 73 | }); 74 | } 75 | } 76 | } 77 | 78 | Ok(res.unwrap_or_default()) 79 | } 80 | 81 | #[inline] 82 | pub fn read_till_element_start(&mut self, end_tag: &str) -> XmlResult<()> { 83 | while let Some(token) = self.next() { 84 | match token? { 85 | Token::ElementStart { span, .. } => { 86 | let tag = &span.as_str()[1..]; 87 | if end_tag == tag { 88 | break; 89 | } else { 90 | self.read_to_end(tag)?; 91 | } 92 | } 93 | Token::ElementEnd { .. } 94 | | Token::Attribute { .. } 95 | | Token::Text { .. } 96 | | Token::Cdata { .. } => { 97 | return Err(XmlError::UnexpectedToken { 98 | token: format!("{:?}", token), 99 | }); 100 | } 101 | _ => (), 102 | } 103 | } 104 | Ok(()) 105 | } 106 | 107 | #[inline] 108 | pub fn find_attribute(&mut self) -> XmlResult)>> { 109 | if let Some(token) = self.tokenizer.peek() { 110 | match token { 111 | Ok(Token::Attribute { span, value, .. }) => { 112 | let value = value.as_str(); 113 | let span = span.as_str(); // key="value" 114 | let key = &span[0..span.len() - value.len() - 3]; // remove `="`, value and `"` 115 | let value = Cow::Borrowed(value); 116 | self.next(); 117 | return Ok(Some((key, value))); 118 | } 119 | Ok(Token::ElementEnd { 120 | end: ElementEnd::Open, 121 | .. 122 | }) 123 | | Ok(Token::ElementEnd { 124 | end: ElementEnd::Empty, 125 | .. 126 | }) => return Ok(None), 127 | Ok(token) => { 128 | return Err(XmlError::UnexpectedToken { 129 | token: format!("{:?}", token), 130 | }) 131 | } 132 | Err(_) => { 133 | // we have call .peek() above, and it's safe to use unwrap 134 | self.next().unwrap()?; 135 | } 136 | } 137 | } 138 | 139 | Err(XmlError::UnexpectedEof) 140 | } 141 | 142 | #[inline] 143 | pub fn find_element_start(&mut self, end_tag: Option<&str>) -> XmlResult> { 144 | while let Some(token) = self.tokenizer.peek() { 145 | match token { 146 | Ok(Token::ElementStart { span, .. }) => { 147 | return Ok(Some(&span.as_str()[1..])); 148 | } 149 | Ok(Token::ElementEnd { 150 | end: ElementEnd::Close(_, _), 151 | span, 152 | }) if end_tag.is_some() => { 153 | let end_tag = end_tag.unwrap(); 154 | let span = span.as_str(); // 155 | let tag = &span[2..span.len() - 1]; // remove `` 156 | if tag == end_tag { 157 | self.next(); 158 | return Ok(None); 159 | } else { 160 | return Err(XmlError::TagMismatch { 161 | expected: end_tag.to_owned(), 162 | found: tag.to_owned(), 163 | }); 164 | } 165 | } 166 | Ok(Token::ElementEnd { .. }) | Ok(Token::Attribute { .. }) => { 167 | return Err(XmlError::UnexpectedToken { 168 | token: format!("{:?}", token), 169 | }) 170 | } 171 | _ => { 172 | // we have call .peek() above, and it's safe to use unwrap 173 | self.next().unwrap()?; 174 | } 175 | } 176 | } 177 | 178 | Err(XmlError::UnexpectedEof) 179 | } 180 | 181 | #[inline] 182 | pub fn read_to_end(&mut self, end_tag: &str) -> XmlResult<()> { 183 | while let Some(token) = self.next() { 184 | match token? { 185 | // if this element is emtpy, just return 186 | Token::ElementEnd { 187 | end: ElementEnd::Empty, 188 | .. 189 | } => return Ok(()), 190 | Token::ElementEnd { 191 | end: ElementEnd::Open, 192 | .. 193 | } => break, 194 | Token::Attribute { .. } => (), 195 | // there shouldn't have any token but Attribute between ElementStart and ElementEnd 196 | token => { 197 | return Err(XmlError::UnexpectedToken { 198 | token: format!("{:?}", token), 199 | }) 200 | } 201 | } 202 | } 203 | 204 | let mut depth = 1; 205 | 206 | while let Some(token) = self.next() { 207 | match token? { 208 | Token::ElementStart { span, .. } if end_tag == &span.as_str()[1..] => { 209 | while let Some(token) = self.next() { 210 | match token? { 211 | Token::ElementEnd { 212 | end: ElementEnd::Empty, 213 | .. 214 | } => { 215 | if depth == 0 { 216 | return Ok(()); 217 | } else { 218 | // don't advance depth in this case 219 | break; 220 | } 221 | } 222 | Token::ElementEnd { 223 | end: ElementEnd::Open, 224 | .. 225 | } => { 226 | depth += 1; 227 | break; 228 | } 229 | Token::Attribute { .. } => (), 230 | // there shouldn't have any token but Attribute between ElementStart and ElementEnd 231 | token => { 232 | return Err(XmlError::UnexpectedToken { 233 | token: format!("{:?}", token), 234 | }); 235 | } 236 | } 237 | } 238 | } 239 | Token::ElementEnd { 240 | end: ElementEnd::Close(_, _), 241 | span, 242 | } if end_tag == &span.as_str()[2..span.as_str().len() - 1] => { 243 | depth -= 1; 244 | if depth == 0 { 245 | return Ok(()); 246 | } 247 | } 248 | _ => (), 249 | } 250 | } 251 | 252 | Err(XmlError::UnexpectedEof) 253 | } 254 | } 255 | 256 | #[test] 257 | fn read_text() -> XmlResult<()> { 258 | let mut reader = XmlReader::new(""); 259 | 260 | assert!(reader.next().is_some()); // "text"); 265 | 266 | assert!(reader.next().is_some()); // "text"); 271 | 272 | assert!(reader.next().is_some()); // ""'<>&"); 277 | 278 | assert!(reader.next().is_some()); // "&"#); 280 | assert!(reader.next().is_none()); 281 | 282 | let mut reader = XmlReader::new(""); 283 | 284 | assert!(reader.next().is_some()); // ""); 289 | 290 | assert!(reader.next().is_some()); // ""); 295 | 296 | assert!(reader.next().is_some()); // "]]>"); 301 | 302 | assert!(reader.next().is_some()); // ""); 304 | assert!(reader.next().is_none()); 305 | 306 | reader = 307 | XmlReader::new(""); 308 | 309 | assert!(reader.next().is_some()); // " XmlResult<()> { 318 | let mut reader = XmlReader::new(""); 319 | 320 | reader.read_till_element_start("tag")?; 321 | assert!(reader.next().is_some()); // "/>" 322 | assert!(reader.next().is_none()); 323 | 324 | reader = XmlReader::new(""); 325 | 326 | assert!(reader.next().is_some()); // "" 328 | reader.read_till_element_start("tag")?; 329 | assert!(reader.next().is_some()); // "/>" 330 | assert!(reader.next().is_some()); // "" 331 | assert!(reader.next().is_none()); 332 | 333 | reader = XmlReader::new(""); 334 | 335 | assert!(reader.next().is_some()); // "" 337 | reader.read_till_element_start("tag")?; 338 | assert!(reader.next().is_some()); // "/>" 339 | assert!(reader.next().is_some()); // "" 340 | assert!(reader.next().is_none()); 341 | 342 | reader = XmlReader::new(""); 343 | 344 | assert!(reader.next().is_some()); // "" 346 | reader.read_till_element_start("tag")?; 347 | assert!(reader.next().is_some()); // "/>" 348 | assert!(reader.next().is_some()); // "" 349 | assert!(reader.next().is_none()); 350 | 351 | reader = XmlReader::new(""); 352 | 353 | assert!(reader.next().is_some()); // "" 355 | reader.read_till_element_start("tag")?; 356 | assert!(reader.next().is_some()); // "/>" 357 | assert!(reader.next().is_some()); // "" 358 | assert!(reader.next().is_none()); 359 | 360 | Ok(()) 361 | } 362 | 363 | #[test] 364 | fn read_to_end() -> XmlResult<()> { 365 | let mut reader = XmlReader::new(""); 366 | 367 | assert!(reader.next().is_some()); // "" 369 | assert!(reader.next().is_some()); // "" 372 | assert!(reader.next().is_none()); 373 | 374 | reader = XmlReader::new(""); 375 | 376 | assert!(reader.next().is_some()); // "" 378 | assert!(reader.next().is_some()); // "" 381 | assert!(reader.next().is_none()); 382 | 383 | reader = XmlReader::new(""); 384 | 385 | assert!(reader.next().is_some()); // "" 387 | assert!(reader.next().is_some()); // "" 390 | assert!(reader.next().is_none()); 391 | 392 | reader = XmlReader::new(""); 393 | 394 | assert!(reader.next().is_some()); // "" 396 | assert!(reader.next().is_some()); // "" 399 | assert!(reader.next().is_none()); 400 | 401 | Ok(()) 402 | } 403 | -------------------------------------------------------------------------------- /strong-xml-derive/src/types.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{format_ident, quote}; 3 | use syn::{Lit::*, Meta::*, *}; 4 | 5 | use crate::utils::elide_type_lifetimes; 6 | 7 | pub enum Element { 8 | Struct { name: Ident, fields: Fields }, 9 | Enum { name: Ident, variants: Vec }, 10 | } 11 | 12 | pub enum Fields { 13 | /// Named fields of a struct or struct variant 14 | /// 15 | /// ```ignore 16 | /// #[xml(tag = "$tag")] 17 | /// struct $name { 18 | /// $( $fields )* 19 | /// } 20 | /// ``` 21 | /// 22 | /// ```ignore 23 | /// enum Foo { 24 | /// #[xml(tag = "$tag")] 25 | /// $name { 26 | /// $( $fields )* 27 | /// } 28 | /// } 29 | /// ``` 30 | Named { 31 | tag: LitStr, 32 | name: Ident, 33 | fields: Vec, 34 | }, 35 | /// Newtype struct or newtype variant 36 | /// 37 | /// ```ignore 38 | /// #[xml($(tag = "$tags",)*)] 39 | /// struct $name($ty); 40 | /// ``` 41 | /// 42 | /// ```ignore 43 | /// enum Foo { 44 | /// #[xml($(tag = "$tags",)*)] 45 | /// $name($ty) 46 | /// } 47 | /// ``` 48 | Newtype { 49 | tags: Vec, 50 | name: Ident, 51 | ty: Type, 52 | }, 53 | } 54 | 55 | pub enum Field { 56 | /// Arrtibute Field 57 | /// 58 | /// ```ignore 59 | /// struct Foo { 60 | /// #[xml(attr = "$tag", $default)] 61 | /// $name: $ty, 62 | /// } 63 | /// ``` 64 | Attribute { 65 | name: TokenStream, 66 | bind: Ident, 67 | ty: Type, 68 | tag: LitStr, 69 | default: bool, 70 | }, 71 | /// Child(ren) Field 72 | /// 73 | /// ```ignore 74 | /// struct Foo { 75 | /// #[xml(child = "$tag", child = "$tag", $default)] 76 | /// $name: $ty, 77 | /// } 78 | /// ``` 79 | Child { 80 | name: TokenStream, 81 | bind: Ident, 82 | ty: Type, 83 | default: bool, 84 | tags: Vec, 85 | }, 86 | /// Text Field 87 | /// 88 | /// ```ignore 89 | /// struct Foo { 90 | /// #[xml(text, $default)] 91 | /// $name: $ty, 92 | /// } 93 | /// ``` 94 | Text { 95 | name: TokenStream, 96 | bind: Ident, 97 | ty: Type, 98 | is_cdata: bool, 99 | }, 100 | /// Flatten Text 101 | /// 102 | /// ```ignore 103 | /// struct Foo { 104 | /// #[xml(flatten_text = "$tag", $default)] 105 | /// $name: $ty, 106 | /// } 107 | /// ``` 108 | FlattenText { 109 | name: TokenStream, 110 | bind: Ident, 111 | ty: Type, 112 | default: bool, 113 | tag: LitStr, 114 | is_cdata: bool, 115 | }, 116 | } 117 | 118 | pub enum Type { 119 | // Cow<'a, str> 120 | CowStr, 121 | // Option> 122 | OptionCowStr, 123 | // Vec> 124 | VecCowStr, 125 | // T 126 | T(syn::Type), 127 | // Option 128 | OptionT(syn::Type), 129 | // Vec 130 | VecT(syn::Type), 131 | // bool 132 | Bool, 133 | // Vec 134 | VecBool, 135 | // Option 136 | OptionBool, 137 | } 138 | 139 | impl Element { 140 | pub fn parse(input: DeriveInput) -> Element { 141 | match input.data { 142 | Data::Struct(data) => Element::Struct { 143 | name: input.ident.clone(), 144 | fields: Fields::parse(data.fields, input.attrs, input.ident), 145 | }, 146 | Data::Enum(data) => Element::Enum { 147 | name: input.ident, 148 | variants: data 149 | .variants 150 | .into_iter() 151 | .map(|variant| Fields::parse(variant.fields, variant.attrs, variant.ident)) 152 | .collect::>(), 153 | }, 154 | Data::Union(_) => panic!("strong-xml doesn't support Union."), 155 | } 156 | } 157 | } 158 | 159 | impl Fields { 160 | pub fn parse(fields: syn::Fields, attrs: Vec, name: Ident) -> Fields { 161 | // Finding `tag` attribute 162 | let mut tags = Vec::new(); 163 | 164 | for meta in attrs.into_iter().filter_map(get_xml_meta).flatten() { 165 | match meta { 166 | NestedMeta::Meta(NameValue(m)) if m.path.is_ident("tag") => { 167 | if let Str(lit) = m.lit { 168 | tags.push(lit); 169 | } else { 170 | panic!("Expected a string literal."); 171 | } 172 | } 173 | _ => (), 174 | } 175 | } 176 | 177 | if tags.is_empty() { 178 | panic!("Missing `tag` attribute."); 179 | } 180 | 181 | match fields { 182 | syn::Fields::Unit => Fields::Named { 183 | name, 184 | tag: tags.remove(0), 185 | fields: Vec::new(), 186 | }, 187 | syn::Fields::Unnamed(fields) => { 188 | // we will assume it's a newtype stuct/enum 189 | // if it has only one field and no field attribute 190 | if fields.unnamed.len() == 1 { 191 | let field = fields.unnamed.first().unwrap().clone(); 192 | if field.attrs.into_iter().filter_map(get_xml_meta).count() == 0 { 193 | return Fields::Newtype { 194 | name, 195 | tags, 196 | ty: Type::parse(field.ty), 197 | }; 198 | } 199 | } 200 | 201 | Fields::Named { 202 | name, 203 | tag: tags.remove(0), 204 | fields: fields 205 | .unnamed 206 | .into_iter() 207 | .enumerate() 208 | .map(|(index, field)| { 209 | let index = syn::Index::from(index); 210 | let bind = format_ident!("__self_{}", index); 211 | Field::parse(quote!(#index), bind, field) 212 | }) 213 | .collect::>(), 214 | } 215 | } 216 | syn::Fields::Named(_) => Fields::Named { 217 | name, 218 | tag: tags.remove(0), 219 | fields: fields 220 | .into_iter() 221 | .map(|field| { 222 | let name = field.ident.clone().unwrap(); 223 | let bind = format_ident!("__self_{}", name); 224 | Field::parse(quote!(#name), bind, field) 225 | }) 226 | .collect::>(), 227 | }, 228 | } 229 | } 230 | } 231 | 232 | impl Field { 233 | pub fn parse(name: TokenStream, bind: Ident, field: syn::Field) -> Field { 234 | let mut default = false; 235 | let mut attr_tag = None; 236 | let mut child_tags = Vec::new(); 237 | let mut is_text = false; 238 | let mut flatten_text_tag = None; 239 | let mut is_cdata = false; 240 | 241 | for meta in field.attrs.into_iter().filter_map(get_xml_meta).flatten() { 242 | match meta { 243 | NestedMeta::Meta(Path(p)) if p.is_ident("default") => { 244 | if default { 245 | panic!("Duplicate `default` attribute."); 246 | } else { 247 | default = true; 248 | } 249 | } 250 | NestedMeta::Meta(NameValue(m)) if m.path.is_ident("attr") => { 251 | if let Str(lit) = m.lit { 252 | if attr_tag.is_some() { 253 | panic!("Duplicate `attr` attribute."); 254 | } else if is_text { 255 | panic!("`attr` attribute and `text` attribute is disjoint."); 256 | } else if is_cdata { 257 | panic!("`attr` attribute and `cdata` attribute is disjoint.") 258 | } else if !child_tags.is_empty() { 259 | panic!("`attr` attribute and `child` attribute is disjoint."); 260 | } else if flatten_text_tag.is_some() { 261 | panic!("`attr` attribute and `flatten_text` attribute is disjoint."); 262 | } else { 263 | attr_tag = Some(lit); 264 | } 265 | } else { 266 | panic!("Expected a string literal."); 267 | } 268 | } 269 | NestedMeta::Meta(Path(ref p)) if p.is_ident("text") => { 270 | if is_text { 271 | panic!("Duplicate `text` attribute."); 272 | } else if attr_tag.is_some() { 273 | panic!("`text` attribute and `attr` attribute is disjoint."); 274 | } else if !child_tags.is_empty() { 275 | panic!("`text` attribute and `child` attribute is disjoint."); 276 | } else if flatten_text_tag.is_some() { 277 | panic!("`text` attribute and `flatten_text` attribute is disjoint."); 278 | } else { 279 | is_text = true; 280 | } 281 | } 282 | NestedMeta::Meta(Path(ref p)) if p.is_ident("cdata") => { 283 | if is_cdata { 284 | panic!("Duplicate `cdata` attribute."); 285 | } else if attr_tag.is_some() { 286 | panic!("`text` attribute and `attr` attribute is disjoint."); 287 | } else if !child_tags.is_empty() { 288 | panic!("`text` attribute and `child` attribute is disjoint."); 289 | } else { 290 | is_cdata = true; 291 | } 292 | } 293 | NestedMeta::Meta(NameValue(m)) if m.path.is_ident("child") => { 294 | if let Str(lit) = m.lit { 295 | if is_text { 296 | panic!("`child` attribute and `text` attribute is disjoint."); 297 | } else if attr_tag.is_some() { 298 | panic!("`child` attribute and `attr` attribute is disjoint."); 299 | } else if is_cdata { 300 | panic!("`child` attribute and `cdata` attribute is disjoint.") 301 | } else if flatten_text_tag.is_some() { 302 | panic!("`child` attribute and `flatten_text` attribute is disjoint."); 303 | } else { 304 | child_tags.push(lit); 305 | } 306 | } else { 307 | panic!("Expected a string literal."); 308 | } 309 | } 310 | NestedMeta::Meta(NameValue(m)) if m.path.is_ident("flatten_text") => { 311 | if let Str(lit) = m.lit { 312 | if is_text { 313 | panic!("`flatten_text` attribute and `text` attribute is disjoint."); 314 | } else if !child_tags.is_empty() { 315 | panic!("`flatten_text` attribute and `child` attribute is disjoint."); 316 | } else if attr_tag.is_some() { 317 | panic!("`flatten_text` attribute and `attr` attribute is disjoint."); 318 | } else if flatten_text_tag.is_some() { 319 | panic!("Duplicate `flatten_text` attribute."); 320 | } else { 321 | flatten_text_tag = Some(lit); 322 | } 323 | } else { 324 | panic!("Expected a string literal."); 325 | } 326 | } 327 | _ => (), 328 | } 329 | } 330 | 331 | if let Some(tag) = attr_tag { 332 | Field::Attribute { 333 | name, 334 | bind, 335 | ty: Type::parse(field.ty), 336 | tag, 337 | default, 338 | } 339 | } else if !child_tags.is_empty() { 340 | Field::Child { 341 | name, 342 | bind, 343 | ty: Type::parse(field.ty), 344 | default, 345 | tags: child_tags, 346 | } 347 | } else if is_text { 348 | Field::Text { 349 | name, 350 | bind, 351 | ty: Type::parse(field.ty), 352 | is_cdata, 353 | } 354 | } else if let Some(tag) = flatten_text_tag { 355 | Field::FlattenText { 356 | name, 357 | bind, 358 | ty: Type::parse(field.ty), 359 | default, 360 | tag, 361 | is_cdata, 362 | } 363 | } else { 364 | panic!("Field should have one of `attr`, `child`, `text` or `flatten_text` attribute."); 365 | } 366 | } 367 | } 368 | 369 | impl Type { 370 | pub fn is_option(&self) -> bool { 371 | matches!( 372 | self, 373 | Type::OptionCowStr | Type::OptionT(_) | Type::OptionBool 374 | ) 375 | } 376 | 377 | pub fn is_vec(&self) -> bool { 378 | matches!(self, Type::VecCowStr | Type::VecT(_) | Type::VecBool) 379 | } 380 | 381 | fn parse(mut ty: syn::Type) -> Self { 382 | fn is_vec(ty: &syn::Type) -> Option<&syn::Type> { 383 | let path = match ty { 384 | syn::Type::Path(ty) => &ty.path, 385 | _ => return None, 386 | }; 387 | let seg = path.segments.last()?; 388 | let args = match &seg.arguments { 389 | PathArguments::AngleBracketed(bracketed) => &bracketed.args, 390 | _ => return None, 391 | }; 392 | if seg.ident == "Vec" && args.len() == 1 { 393 | match args[0] { 394 | GenericArgument::Type(ref arg) => Some(arg), 395 | _ => None, 396 | } 397 | } else { 398 | None 399 | } 400 | } 401 | 402 | fn is_option(ty: &syn::Type) -> Option<&syn::Type> { 403 | let path = match ty { 404 | syn::Type::Path(ty) => &ty.path, 405 | _ => return None, 406 | }; 407 | let seg = path.segments.last()?; 408 | let args = match &seg.arguments { 409 | PathArguments::AngleBracketed(bracketed) => &bracketed.args, 410 | _ => return None, 411 | }; 412 | if seg.ident == "Option" && args.len() == 1 { 413 | match &args[0] { 414 | GenericArgument::Type(arg) => Some(arg), 415 | _ => None, 416 | } 417 | } else { 418 | None 419 | } 420 | } 421 | 422 | fn is_cow_str(ty: &syn::Type) -> bool { 423 | let path = match ty { 424 | syn::Type::Path(ty) => &ty.path, 425 | _ => return false, 426 | }; 427 | let seg = match path.segments.last() { 428 | Some(seg) => seg, 429 | None => return false, 430 | }; 431 | let args = match &seg.arguments { 432 | PathArguments::AngleBracketed(bracketed) => &bracketed.args, 433 | _ => return false, 434 | }; 435 | if seg.ident == "Cow" && args.len() == 2 { 436 | match &args[1] { 437 | GenericArgument::Type(syn::Type::Path(ty)) => ty.path.is_ident("str"), 438 | _ => false, 439 | } 440 | } else { 441 | false 442 | } 443 | } 444 | 445 | fn is_bool(ty: &syn::Type) -> bool { 446 | matches!(ty, syn::Type::Path(ty) if ty.path.is_ident("bool")) 447 | } 448 | 449 | elide_type_lifetimes(&mut ty); 450 | 451 | if let Some(ty) = is_vec(&ty) { 452 | if is_cow_str(&ty) { 453 | Type::VecCowStr 454 | } else if is_bool(&ty) { 455 | Type::VecBool 456 | } else { 457 | Type::VecT(ty.clone()) 458 | } 459 | } else if let Some(ty) = is_option(&ty) { 460 | if is_cow_str(&ty) { 461 | Type::OptionCowStr 462 | } else if is_bool(&ty) { 463 | Type::OptionBool 464 | } else { 465 | Type::OptionT(ty.clone()) 466 | } 467 | } else if is_cow_str(&ty) { 468 | Type::CowStr 469 | } else if is_bool(&ty) { 470 | Type::Bool 471 | } else { 472 | Type::T(ty) 473 | } 474 | } 475 | } 476 | 477 | fn get_xml_meta(attr: Attribute) -> Option> { 478 | if attr.path.segments.len() == 1 && attr.path.segments[0].ident == "xml" { 479 | match attr.parse_meta() { 480 | Ok(Meta::List(meta)) => Some(meta.nested.iter().cloned().collect()), 481 | _ => None, 482 | } 483 | } else { 484 | None 485 | } 486 | } 487 | --------------------------------------------------------------------------------