├── .gitignore ├── Cargo.toml ├── xml_struct_derive ├── Cargo.toml └── src │ ├── lib.rs │ ├── serialize.rs │ ├── properties.rs │ └── serialize │ └── codegen.rs ├── xml_struct_tests ├── integration │ ├── lib.rs │ ├── unit_struct.rs │ ├── text_enum.rs │ ├── tuple_struct.rs │ ├── struct.rs │ └── enum.rs ├── ui │ ├── test_cases │ │ └── type_properties │ │ │ ├── multiple_defaults.stderr │ │ │ ├── text_enum_with_non_unit_variants.stderr │ │ │ ├── invalid_attributes.stderr │ │ │ ├── multiple_defaults.rs │ │ │ ├── text_struct.stderr │ │ │ ├── invalid_attributes.rs │ │ │ ├── text_struct.rs │ │ │ ├── text_enum_with_non_unit_variants.rs │ │ │ ├── text_enum.rs │ │ │ ├── text_enum_with_namespaces.stderr │ │ │ ├── no_properties.rs │ │ │ ├── text_enum_with_namespaces.rs │ │ │ └── valid_namespaces.rs │ └── lib.rs ├── Cargo.toml └── src │ └── lib.rs ├── xml_struct ├── Cargo.toml └── src │ ├── lib.rs │ ├── impls.rs │ └── tests.rs ├── README.md ├── .github └── workflows │ └── ci.yaml ├── Cargo.lock └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "xml_struct", 5 | "xml_struct_derive", 6 | "xml_struct_tests" 7 | ] 8 | -------------------------------------------------------------------------------- /xml_struct_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xml_struct_derive" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | proc-macro = true 8 | 9 | [dependencies] 10 | proc-macro2 = "1.0.74" 11 | quote = "1.0.35" 12 | syn = { version = "2.0.46", features = ["full"], default-features = false } 13 | -------------------------------------------------------------------------------- /xml_struct_tests/integration/lib.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | mod r#enum; 6 | mod r#struct; 7 | mod text_enum; 8 | mod tuple_struct; 9 | mod unit_struct; 10 | -------------------------------------------------------------------------------- /xml_struct_tests/ui/test_cases/type_properties/multiple_defaults.stderr: -------------------------------------------------------------------------------- 1 | error: cannot declare more than one default namespace 2 | --> ui/test_cases/type_properties/multiple_defaults.rs:8:87 3 | | 4 | 8 | #[xml_struct(default_ns = "http://foo.example/", ns = ("bar", "http://bar.example/"), default_ns = "http://baz.example/")] 5 | | ^^^^^^^^^^ 6 | -------------------------------------------------------------------------------- /xml_struct/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xml_struct" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.69" 10 | quick-xml = "0.31.0" 11 | thiserror = "1.0.56" 12 | xml_struct_derive = { version = "0.1.0", path = "../xml_struct_derive" } 13 | 14 | [dev-dependencies] 15 | xml_struct_tests = { path = "../xml_struct_tests" } 16 | -------------------------------------------------------------------------------- /xml_struct_tests/ui/test_cases/type_properties/text_enum_with_non_unit_variants.stderr: -------------------------------------------------------------------------------- 1 | error: only unit enums may be derived as text 2 | --> ui/test_cases/type_properties/text_enum_with_non_unit_variants.rs:8:14 3 | | 4 | 8 | #[xml_struct(text)] 5 | | ^^^^ 6 | 7 | error: only unit enums may be derived as text 8 | --> ui/test_cases/type_properties/text_enum_with_non_unit_variants.rs:16:14 9 | | 10 | 16 | #[xml_struct(text)] 11 | | ^^^^ 12 | -------------------------------------------------------------------------------- /xml_struct_tests/ui/test_cases/type_properties/invalid_attributes.stderr: -------------------------------------------------------------------------------- 1 | error: unrecognized `xml_struct` attribute 2 | --> ui/test_cases/type_properties/invalid_attributes.rs:8:14 3 | | 4 | 8 | #[xml_struct(defaut_ns = "http://foo.example/", ns = ("bar", "http://bar.example/"))] 5 | | ^^^^^^^^^ 6 | 7 | error: unrecognized `xml_struct` attribute 8 | --> ui/test_cases/type_properties/invalid_attributes.rs:12:14 9 | | 10 | 12 | #[xml_struct(everybody_loves_xml)] 11 | | ^^^^^^^^^^^^^^^^^^^ 12 | -------------------------------------------------------------------------------- /xml_struct_tests/ui/test_cases/type_properties/multiple_defaults.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use xml_struct::XmlSerialize; 6 | 7 | #[derive(XmlSerialize)] 8 | #[xml_struct(default_ns = "http://foo.example/", ns = ("bar", "http://bar.example/"), default_ns = "http://baz.example/")] 9 | struct MultipleDefaultNamespaces; 10 | 11 | fn main() {} 12 | -------------------------------------------------------------------------------- /xml_struct_tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xml_struct_tests" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | quick-xml = "0.31.0" 11 | thiserror = "1.0.56" 12 | trybuild = "1.0.89" 13 | xml_struct = { version = "0.1.0", path = "../xml_struct" } 14 | 15 | [[test]] 16 | name = "integration_tests" 17 | path = "integration/lib.rs" 18 | harness = true 19 | 20 | [[test]] 21 | name = "build_tests" 22 | path = "ui/lib.rs" 23 | harness = true 24 | -------------------------------------------------------------------------------- /xml_struct_tests/ui/test_cases/type_properties/text_struct.stderr: -------------------------------------------------------------------------------- 1 | error: only unit enums may be derived as text 2 | --> ui/test_cases/type_properties/text_struct.rs:8:14 3 | | 4 | 8 | #[xml_struct(text)] 5 | | ^^^^ 6 | 7 | error: only unit enums may be derived as text 8 | --> ui/test_cases/type_properties/text_struct.rs:12:14 9 | | 10 | 12 | #[xml_struct(text)] 11 | | ^^^^ 12 | 13 | error: only unit enums may be derived as text 14 | --> ui/test_cases/type_properties/text_struct.rs:16:14 15 | | 16 | 16 | #[xml_struct(text)] 17 | | ^^^^ 18 | -------------------------------------------------------------------------------- /xml_struct_tests/ui/test_cases/type_properties/invalid_attributes.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use xml_struct::XmlSerialize; 6 | 7 | #[derive(XmlSerialize)] 8 | #[xml_struct(defaut_ns = "http://foo.example/", ns = ("bar", "http://bar.example/"))] 9 | struct MisspelledNameValueAttribute; 10 | 11 | #[derive(XmlSerialize)] 12 | #[xml_struct(everybody_loves_xml)] 13 | struct UnrecognizedPathAttribute; 14 | 15 | fn main() {} 16 | -------------------------------------------------------------------------------- /xml_struct_tests/ui/test_cases/type_properties/text_struct.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use xml_struct::XmlSerialize; 6 | 7 | #[derive(XmlSerialize)] 8 | #[xml_struct(text)] 9 | struct TextUnitStruct; 10 | 11 | #[derive(XmlSerialize)] 12 | #[xml_struct(text)] 13 | struct TextTupleStruct(String); 14 | 15 | #[derive(XmlSerialize)] 16 | #[xml_struct(text)] 17 | struct TextStruct { 18 | value: String, 19 | } 20 | 21 | fn main() {} 22 | -------------------------------------------------------------------------------- /xml_struct_tests/ui/test_cases/type_properties/text_enum_with_non_unit_variants.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use xml_struct::XmlSerialize; 6 | 7 | #[derive(XmlSerialize)] 8 | #[xml_struct(text)] 9 | enum TextEnumWithTupleVariant { 10 | UnitVariant, 11 | TupleVariant(String), 12 | OtherUnitVariant, 13 | } 14 | 15 | #[derive(XmlSerialize)] 16 | #[xml_struct(text)] 17 | enum TextEnumWithStructVariant { 18 | UnitVariant, 19 | StructVariant { value: String }, 20 | OtherUnitVariant, 21 | } 22 | 23 | fn main() {} 24 | -------------------------------------------------------------------------------- /xml_struct_tests/ui/test_cases/type_properties/text_enum.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use xml_struct::XmlSerialize; 6 | 7 | #[derive(XmlSerialize)] 8 | #[xml_struct(text)] 9 | enum UnitVariants { 10 | Foo, 11 | Bar, 12 | Baz, 13 | FooBar, 14 | } 15 | 16 | fn main() -> Result<(), xml_struct::Error> { 17 | let bytes: Vec = Vec::new(); 18 | let mut writer = quick_xml::writer::Writer::new(bytes); 19 | 20 | let content = UnitVariants::Bar; 21 | content.serialize_as_element(&mut writer, "foo")?; 22 | 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /xml_struct_tests/ui/test_cases/type_properties/text_enum_with_namespaces.stderr: -------------------------------------------------------------------------------- 1 | error: cannot declare namespaces for text content 2 | --> ui/test_cases/type_properties/text_enum_with_namespaces.rs:8:1 3 | | 4 | 8 | #[xml_struct(text, default_ns = "http://foo.example/")] 5 | | ^ 6 | 7 | error: cannot declare namespaces for text content 8 | --> ui/test_cases/type_properties/text_enum_with_namespaces.rs:17:1 9 | | 10 | 17 | #[xml_struct(default_ns = "http://foo.example/", text)] 11 | | ^ 12 | 13 | error: cannot declare namespaces for text content 14 | --> ui/test_cases/type_properties/text_enum_with_namespaces.rs:26:1 15 | | 16 | 26 | #[xml_struct(text, ns = ("foo", "http://foo.example/"))] 17 | | ^ 18 | 19 | error: cannot declare namespaces for text content 20 | --> ui/test_cases/type_properties/text_enum_with_namespaces.rs:35:1 21 | | 22 | 35 | #[xml_struct(ns = ("foo", "http://foo.example/"), text)] 23 | | ^ 24 | -------------------------------------------------------------------------------- /xml_struct_tests/ui/test_cases/type_properties/no_properties.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use xml_struct::XmlSerialize; 6 | 7 | #[derive(XmlSerialize)] 8 | struct NoAttributes; 9 | 10 | // There's no clear use case for this pattern at this time, but it shouldn't 11 | // error anyhow. 12 | #[derive(XmlSerialize)] 13 | #[xml_struct()] 14 | struct EmptyAttribute; 15 | 16 | fn main() -> Result<(), xml_struct::Error> { 17 | let bytes: Vec = Vec::new(); 18 | let mut writer = quick_xml::writer::Writer::new(bytes); 19 | 20 | let content = NoAttributes; 21 | content.serialize_as_element(&mut writer, "foo")?; 22 | 23 | let content = EmptyAttribute; 24 | content.serialize_as_element(&mut writer, "foo")?; 25 | 26 | Ok(()) 27 | } 28 | -------------------------------------------------------------------------------- /xml_struct_tests/ui/lib.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use std::path::PathBuf; 6 | 7 | #[test] 8 | fn type_properties() { 9 | let base_path = test_case_base_path().join("type_properties"); 10 | 11 | let t = trybuild::TestCases::new(); 12 | t.pass(base_path.join("no_properties.rs")); 13 | t.pass(base_path.join("valid_namespaces.rs")); 14 | t.compile_fail(base_path.join("multiple_defaults.rs")); 15 | t.pass(base_path.join("text_enum.rs")); 16 | t.compile_fail(base_path.join("text_struct.rs")); 17 | t.compile_fail(base_path.join("text_enum_with_non_unit_variants.rs")); 18 | t.compile_fail(base_path.join("text_enum_with_namespaces.rs")); 19 | t.compile_fail(base_path.join("invalid_attributes.rs")); 20 | } 21 | 22 | fn test_case_base_path() -> PathBuf { 23 | PathBuf::from("ui/test_cases") 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `xml_struct` 2 | 3 | The `xml_struct` crate is intended to provide simple, flexible, low-boilerplate 4 | serialization of Rust data structures to XML. 5 | 6 | ## Limitations 7 | 8 | In its current iteration, this project makes several behavioral assumptions 9 | which make it unsuitable for general use. Primary among these are that 10 | transformation of field/structure names to XML tag names is not configurable 11 | (all names are transformed to PascalCase) and whether fields are serialized as 12 | XML elements or attributes by default is not configurable. 13 | 14 | Deserialization is likewise not supported at this time. 15 | 16 | Due to the listed limitations, `xml_struct` is not currently published to 17 | crates.io and no support is offered at this time. These limitations may be 18 | addressed at a later time if there is general interest in this crate or if 19 | workload allows. 20 | 21 | For general-purpose XML serialization or deserialization, one of these crates 22 | may better suit your needs at this time: 23 | 24 | - [`xmlserde`](https://github.com/imjeremyhe/xmlserde) 25 | - [`yaserde`](https://github.com/media-io/yaserde) 26 | -------------------------------------------------------------------------------- /xml_struct_tests/ui/test_cases/type_properties/text_enum_with_namespaces.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use xml_struct::XmlSerialize; 6 | 7 | #[derive(XmlSerialize)] 8 | #[xml_struct(text, default_ns = "http://foo.example/")] 9 | enum UnitVariantsWithDefaultNamespace { 10 | Foo, 11 | Bar, 12 | Baz, 13 | FooBar, 14 | } 15 | 16 | #[derive(XmlSerialize)] 17 | #[xml_struct(default_ns = "http://foo.example/", text)] 18 | enum UnitVariantsWithDefaultNamespaceInDifferentOrder { 19 | Foo, 20 | Bar, 21 | Baz, 22 | FooBar, 23 | } 24 | 25 | #[derive(XmlSerialize)] 26 | #[xml_struct(text, ns = ("foo", "http://foo.example/"))] 27 | enum UnitVariantsWithNamespace { 28 | Foo, 29 | Bar, 30 | Baz, 31 | FooBar, 32 | } 33 | 34 | #[derive(XmlSerialize)] 35 | #[xml_struct(ns = ("foo", "http://foo.example/"), text)] 36 | enum UnitVariantsWithNamespaceInDifferentOrder { 37 | Foo, 38 | Bar, 39 | Baz, 40 | FooBar, 41 | } 42 | 43 | fn main() {} 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: '30 2 * * *' 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | name: Build & Test Project 18 | 19 | steps: 20 | - name: Checkout source 21 | uses: actions/checkout@v4 22 | 23 | - name: Install Rust 24 | uses: dtolnay/rust-toolchain@stable 25 | with: 26 | components: rustfmt, clippy 27 | 28 | - name: Check Formatting 29 | run: cargo fmt --all -- --check 30 | 31 | - name: Cargo Cache 32 | uses: actions/cache@v3 33 | with: 34 | path: | 35 | ~/.cargo/registry/index/ 36 | ~/.cargo/registry/cache/ 37 | target/ 38 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 39 | 40 | - name: Build project 41 | run: cargo build 42 | 43 | - name: Test project 44 | run: cargo test --all 45 | 46 | - name: Run clippy 47 | uses: giraffate/clippy-action@v1 48 | with: 49 | reporter: 'github-pr-check' 50 | clippy_flags: --no-deps 51 | filter_mode: nofilter 52 | github_token: ${{ secrets.GITHUB_TOKEN }} 53 | -------------------------------------------------------------------------------- /xml_struct_tests/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use quick_xml::Writer; 6 | use thiserror::Error; 7 | use xml_struct::XmlSerialize; 8 | 9 | pub fn serialize_value_as_element(value: T, root_name: &str) -> Result 10 | where 11 | T: XmlSerialize, 12 | { 13 | let buf = Vec::default(); 14 | let mut writer = Writer::new(buf); 15 | 16 | value.serialize_as_element(&mut writer, root_name)?; 17 | 18 | let out = String::from_utf8(writer.into_inner())?; 19 | 20 | Ok(out) 21 | } 22 | 23 | pub fn serialize_value_children(value: T) -> Result 24 | where 25 | T: XmlSerialize, 26 | { 27 | let buf = Vec::default(); 28 | let mut writer = Writer::new(buf); 29 | 30 | value.serialize_child_nodes(&mut writer)?; 31 | 32 | let out = String::from_utf8(writer.into_inner())?; 33 | 34 | Ok(out) 35 | } 36 | 37 | #[derive(Debug, Error)] 38 | #[non_exhaustive] 39 | pub enum TestError { 40 | #[error("error in processing XML document")] 41 | XmlStruct(#[from] xml_struct::Error), 42 | 43 | #[error("serialization produced invalid UTF-8")] 44 | Utf8(#[from] std::string::FromUtf8Error), 45 | } 46 | -------------------------------------------------------------------------------- /xml_struct_tests/ui/test_cases/type_properties/valid_namespaces.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use xml_struct::XmlSerialize; 6 | 7 | #[derive(XmlSerialize)] 8 | #[xml_struct(ns = ("foo", "http://foo.example/"))] 9 | struct SingleNamespace; 10 | 11 | #[derive(XmlSerialize)] 12 | #[xml_struct(ns = ("foo", "http://foo.example/"), ns = ("bar", "http://bar.example/"))] 13 | struct MultipleNamespaces; 14 | 15 | const FOO_PREFIX: &str = "foo"; 16 | const BAR_NAME: &str = "http://bar.example/"; 17 | 18 | #[derive(XmlSerialize)] 19 | #[xml_struct(ns = (FOO_PREFIX, "http://foo.example/"), ns = ("bar", BAR_NAME))] 20 | struct ConstsInNamespaceDecls; 21 | 22 | #[derive(XmlSerialize)] 23 | #[xml_struct(default_ns = "http://default.example/")] 24 | struct DefaultNamespace; 25 | 26 | #[derive(XmlSerialize)] 27 | #[xml_struct(default_ns = "http://default.example/", ns = ("foo", "http://foo.example/"), ns = ("bar", BAR_NAME))] 28 | struct DefaultNamespaceWithOthers; 29 | 30 | fn main() -> Result<(), xml_struct::Error> { 31 | let bytes: Vec = Vec::new(); 32 | let mut writer = quick_xml::writer::Writer::new(bytes); 33 | 34 | let content = SingleNamespace; 35 | content.serialize_as_element(&mut writer, "foo")?; 36 | 37 | let content = MultipleNamespaces; 38 | content.serialize_as_element(&mut writer, "foo")?; 39 | 40 | let content = ConstsInNamespaceDecls; 41 | content.serialize_as_element(&mut writer, "foo")?; 42 | 43 | let content = DefaultNamespace; 44 | content.serialize_as_element(&mut writer, "foo")?; 45 | 46 | let content = DefaultNamespaceWithOthers; 47 | content.serialize_as_element(&mut writer, "foo")?; 48 | 49 | Ok(()) 50 | } 51 | -------------------------------------------------------------------------------- /xml_struct_tests/integration/unit_struct.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use xml_struct::XmlSerialize; 6 | use xml_struct_tests::{serialize_value_as_element, serialize_value_children}; 7 | 8 | #[derive(XmlSerialize)] 9 | struct UnitStruct; 10 | 11 | const BAR_PREFIX: &str = "bar"; 12 | const BAZ_NAME: &str = "http://baz.example/"; 13 | 14 | #[derive(XmlSerialize)] 15 | #[xml_struct(default_ns = "http://foo.example/", ns = (BAR_PREFIX, "http://bar.example/"), ns = ("baz", BAZ_NAME))] 16 | struct UnitStructWithNamespaces; 17 | 18 | #[test] 19 | fn unit_struct() { 20 | let content = UnitStruct; 21 | 22 | let expected = ""; 23 | 24 | let actual = serialize_value_children(content).expect("Failed to write unit struct"); 25 | 26 | assert_eq!(actual, expected, "Unit struct should have no content"); 27 | } 28 | 29 | #[test] 30 | fn unit_struct_as_element() { 31 | let content = UnitStruct; 32 | 33 | let expected = ""; 34 | 35 | let actual = serialize_value_as_element(content, "foo").expect("Failed to write unit struct"); 36 | 37 | assert_eq!( 38 | actual, expected, 39 | "Unit struct should serialize as empty element" 40 | ); 41 | } 42 | 43 | #[test] 44 | fn unit_struct_with_namespaces() { 45 | let content = UnitStructWithNamespaces; 46 | 47 | let expected = ""; 48 | 49 | let actual = serialize_value_children(content).expect("Failed to write unit struct"); 50 | 51 | assert_eq!(actual, expected, "Unit struct should have no content"); 52 | } 53 | 54 | #[test] 55 | fn unit_struct_with_namespaces_as_element() { 56 | let content = UnitStructWithNamespaces; 57 | 58 | let expected = r#""#; 59 | 60 | let actual = serialize_value_as_element(content, "foo").expect("Failed to write unit struct"); 61 | 62 | assert_eq!( 63 | actual, expected, 64 | "Unit struct should serialize as empty element with namespace attributes" 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /xml_struct_tests/integration/text_enum.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use xml_struct::XmlSerialize; 6 | use xml_struct_tests::{serialize_value_as_element, serialize_value_children}; 7 | 8 | #[derive(XmlSerialize)] 9 | #[xml_struct(text)] 10 | enum TextEnum { 11 | A, 12 | Two, 13 | Gamma, 14 | } 15 | 16 | #[derive(XmlSerialize)] 17 | struct StructWithTextEnumFields { 18 | child_field: String, 19 | 20 | #[xml_struct(attribute)] 21 | string_attr: String, 22 | 23 | #[xml_struct(attribute)] 24 | enum_attr: TextEnum, 25 | 26 | enum_child: TextEnum, 27 | } 28 | 29 | #[test] 30 | fn text_enum() { 31 | let content = TextEnum::Two; 32 | 33 | let expected = "Two"; 34 | 35 | let actual = serialize_value_children(content).expect("Failed to write enum"); 36 | 37 | assert_eq!( 38 | actual, expected, 39 | "Variants of text enums should be serialized as a text node" 40 | ); 41 | } 42 | 43 | #[test] 44 | fn text_enum_as_element() { 45 | let content = TextEnum::Two; 46 | 47 | let expected = r#"Two"#; 48 | 49 | let actual = serialize_value_as_element(content, "foo").expect("Failed to write enum"); 50 | 51 | assert_eq!( 52 | actual, expected, 53 | "Variants of text enums should be serialized as a parented text node" 54 | ); 55 | } 56 | 57 | #[test] 58 | fn struct_with_text_enum_fields() { 59 | let content = StructWithTextEnumFields { 60 | child_field: String::from("this is a regular string field"), 61 | string_attr: String::from("this is a regular attr field"), 62 | enum_attr: TextEnum::Gamma, 63 | enum_child: TextEnum::A, 64 | }; 65 | 66 | let expected = 67 | "this is a regular string fieldA"; 68 | 69 | let actual = serialize_value_children(content).expect("Failed to write struct"); 70 | 71 | assert_eq!( 72 | actual, expected, 73 | "Text enum fields should be serialized as text nodes" 74 | ) 75 | } 76 | 77 | #[test] 78 | fn struct_with_text_enum_fields_as_element() { 79 | let content = StructWithTextEnumFields { 80 | child_field: String::from("this is a regular string field"), 81 | string_attr: String::from("this is a regular attr field"), 82 | enum_attr: TextEnum::Gamma, 83 | enum_child: TextEnum::A, 84 | }; 85 | 86 | let expected = r#"this is a regular string fieldA"#; 87 | 88 | let actual = serialize_value_as_element(content, "namehere").expect("Failed to write struct"); 89 | 90 | assert_eq!( 91 | actual, expected, 92 | "Text enum attributes should be serialized as text values" 93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /xml_struct_tests/integration/tuple_struct.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use quick_xml::events::{BytesText, Event}; 6 | use xml_struct::XmlSerialize; 7 | use xml_struct_tests::{serialize_value_as_element, serialize_value_children}; 8 | 9 | #[derive(XmlSerialize)] 10 | struct TupleStruct(ChildType, String); 11 | 12 | #[derive(XmlSerialize)] 13 | #[xml_struct(default_ns = "http://foo.example/this_ns", ns = ("other_ns", "http://bar.example/other_ns"))] 14 | struct TupleStructWithNamespaces(ChildType, String); 15 | 16 | struct ChildType { 17 | _grandchild: &'static str, 18 | } 19 | 20 | impl ChildType { 21 | #[allow(dead_code)] 22 | fn serialize_child_nodes( 23 | &self, 24 | _writer: &mut quick_xml::Writer, 25 | ) -> Result<(), xml_struct::Error> 26 | where 27 | W: std::io::Write, 28 | { 29 | panic!("`XmlSerialize` calls should not dispatch non-trait functions"); 30 | } 31 | } 32 | 33 | // We explicitly implement `XmlSerialize` for this type in a way which doesn't 34 | // match the default in order to verify that `ChildType`'s implementation is 35 | // used rather than some other magic. 36 | impl XmlSerialize for ChildType { 37 | fn serialize_child_nodes( 38 | &self, 39 | writer: &mut quick_xml::Writer, 40 | ) -> Result<(), xml_struct::Error> 41 | where 42 | W: std::io::Write, 43 | { 44 | writer.write_event(Event::Text(BytesText::new("bare text child node")))?; 45 | 46 | Ok(()) 47 | } 48 | } 49 | 50 | #[test] 51 | fn tuple_struct() { 52 | let content = TupleStruct( 53 | ChildType { 54 | _grandchild: "this text shouldn't show up", 55 | }, 56 | String::from("bare text node"), 57 | ); 58 | 59 | let expected = "bare text child nodebare text node"; 60 | 61 | let actual = serialize_value_children(content).expect("Failed to write struct"); 62 | 63 | assert_eq!( 64 | actual, expected, 65 | "Struct fields should each be serialized as a child node" 66 | ); 67 | } 68 | 69 | #[test] 70 | fn tuple_struct_as_element() { 71 | let content = TupleStruct( 72 | ChildType { 73 | _grandchild: "this text shouldn't show up", 74 | }, 75 | String::from("bare text node"), 76 | ); 77 | 78 | let expected = r#"bare text child nodebare text node"#; 79 | 80 | let actual = serialize_value_as_element(content, "parent").expect("Failed to write struct"); 81 | 82 | assert_eq!( 83 | actual, expected, 84 | "Struct should be serialized as element with fields as attribute and children as appropriate" 85 | ); 86 | } 87 | 88 | #[test] 89 | fn tuple_struct_with_namespaces() { 90 | let content = TupleStructWithNamespaces( 91 | ChildType { 92 | _grandchild: "this text shouldn't show up", 93 | }, 94 | String::from("bare text node"), 95 | ); 96 | 97 | let expected = "bare text child nodebare text node"; 98 | 99 | let actual = serialize_value_children(content).expect("Failed to write struct"); 100 | 101 | assert_eq!( 102 | actual, expected, 103 | "Struct fields should each be serialized as a child node" 104 | ); 105 | } 106 | 107 | #[test] 108 | fn tuple_struct_with_namespaces_as_element() { 109 | let content = TupleStructWithNamespaces( 110 | ChildType { 111 | _grandchild: "this text shouldn't show up", 112 | }, 113 | String::from("bare text node"), 114 | ); 115 | 116 | let expected = r#"bare text child nodebare text node"#; 117 | 118 | let actual = serialize_value_as_element(content, "parent").expect("Failed to write struct"); 119 | 120 | assert_eq!( 121 | actual, expected, 122 | "Struct should be serialized with namespaces as attributes" 123 | ); 124 | } 125 | -------------------------------------------------------------------------------- /xml_struct/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | //! This crate provides a mechanism for serializing Rust data structures as 6 | //! well-formed XML with a minimum of boilerplate. 7 | //! 8 | //! Consumers can provide manual implementations of the [`XmlSerialize`] and 9 | //! [`XmlSerializeAttr`] traits if desired, but the primary intent of this crate 10 | //! is to provide automated derivation of these traits in order to facilitate 11 | //! serialization of complex XML structures. 12 | //! 13 | //! # Limitations 14 | //! 15 | //! At present, derived implementations of these traits are designed to handle 16 | //! the specific case of Microsoft Exchange Web Services. As such, all XML 17 | //! elements and attributes are named in PascalCase and certain behaviors are 18 | //! not supported (such as serializing enum variants without enclosing XML 19 | //! elements derived from the variant name). 20 | //! 21 | //! Furthermore, the PascalCase implementation is naïve and depends on 22 | //! [`char::to_ascii_uppercase`], making it unsuitable for use with non-ASCII 23 | //! identifiers. 24 | //! 25 | //! There is also currently no provision for deserialization from XML, as the 26 | //! support offered by `quick_xml`'s serde implementation has been found to be 27 | //! sufficient for the time being. 28 | //! 29 | //! In recognition of these limitations, this crate should not be published to 30 | //! crates.io at this time. If a generalized implementation generates interest 31 | //! or is thought to have merit, these limitations may be addressed at a later 32 | //! time. 33 | 34 | mod impls; 35 | mod tests; 36 | 37 | use quick_xml::{ 38 | events::{BytesEnd, BytesStart, Event}, 39 | Writer, 40 | }; 41 | use thiserror::Error; 42 | 43 | pub use xml_struct_derive::*; 44 | 45 | /// A data structure which can be serialized as XML content nodes. 46 | /// 47 | /// # Usage 48 | /// 49 | /// The following demonstrates end-to-end usage of `XmlSerialize` with both 50 | /// derived and manual implementations. 51 | /// 52 | /// ``` 53 | /// use quick_xml::{ 54 | /// events::{BytesText, Event}, 55 | /// writer::Writer 56 | /// }; 57 | /// use xml_struct::{Error, XmlSerialize}; 58 | /// 59 | /// #[derive(XmlSerialize)] 60 | /// #[xml_struct(default_ns = "http://foo.example/")] 61 | /// struct Foo { 62 | /// some_field: String, 63 | /// 64 | /// #[xml_struct(flatten)] 65 | /// something_else: Bar, 66 | /// } 67 | /// 68 | /// enum Bar { 69 | /// Baz, 70 | /// Qux(String), 71 | /// } 72 | /// 73 | /// impl XmlSerialize for Bar { 74 | /// fn serialize_child_nodes(&self, writer: &mut Writer) -> Result<(), Error> 75 | /// where 76 | /// W: std::io::Write, 77 | /// { 78 | /// match self { 79 | /// Self::Baz => writer.write_event(Event::Text(BytesText::new("BAZ")))?, 80 | /// Self::Qux(qux) => qux.serialize_as_element(writer, "Qux")?, 81 | /// } 82 | /// 83 | /// Ok(()) 84 | /// } 85 | /// } 86 | /// 87 | /// let mut writer: Writer> = Writer::new(Vec::new()); 88 | /// let foo = Foo { 89 | /// some_field: "foo".into(), 90 | /// something_else: Bar::Baz, 91 | /// }; 92 | /// 93 | /// assert!(foo.serialize_as_element(&mut writer, "FlyYouFoo").is_ok()); 94 | /// 95 | /// let out = writer.into_inner(); 96 | /// let out = std::str::from_utf8(&out).unwrap(); 97 | /// 98 | /// assert_eq!( 99 | /// out, 100 | /// r#"fooBAZ"#, 101 | /// ); 102 | /// ``` 103 | pub trait XmlSerialize { 104 | /// Serializes this value as XML content nodes within an enclosing XML 105 | /// element. 106 | fn serialize_as_element(&self, writer: &mut Writer, name: &str) -> Result<(), Error> 107 | where 108 | W: std::io::Write, 109 | { 110 | writer.write_event(Event::Start(BytesStart::new(name)))?; 111 | 112 | self.serialize_child_nodes(writer)?; 113 | 114 | writer.write_event(Event::End(BytesEnd::new(name)))?; 115 | 116 | Ok(()) 117 | } 118 | 119 | /// Serializes this value as XML content nodes. 120 | fn serialize_child_nodes(&self, writer: &mut Writer) -> Result<(), Error> 121 | where 122 | W: std::io::Write; 123 | } 124 | 125 | /// A data structure which can be serialized as the value of an XML attribute. 126 | pub trait XmlSerializeAttr { 127 | /// Serializes this value as the value of an XML attribute. 128 | fn serialize_as_attribute(&self, start_tag: &mut BytesStart, name: &str); 129 | } 130 | 131 | /// An error generated during the XML serialization process. 132 | #[derive(Debug, Error)] 133 | #[non_exhaustive] 134 | pub enum Error { 135 | #[error("failed to process XML document")] 136 | Xml(#[from] quick_xml::Error), 137 | 138 | /// An error representing a failure in formatting a data structure prior to 139 | /// serializing it into XML. Its inner type is generic on purpose, as the 140 | /// specific error type might be defined by a third-party crate. 141 | #[error("failed to serialize value as text")] 142 | Value(#[from] anyhow::Error), 143 | } 144 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.69" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" 10 | 11 | [[package]] 12 | name = "basic-toml" 13 | version = "0.1.8" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "2db21524cad41c5591204d22d75e1970a2d1f71060214ca931dc7d5afe2c14e5" 16 | dependencies = [ 17 | "serde", 18 | ] 19 | 20 | [[package]] 21 | name = "glob" 22 | version = "0.3.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 25 | 26 | [[package]] 27 | name = "itoa" 28 | version = "1.0.10" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 31 | 32 | [[package]] 33 | name = "memchr" 34 | version = "2.7.1" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 37 | 38 | [[package]] 39 | name = "once_cell" 40 | version = "1.19.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 43 | 44 | [[package]] 45 | name = "proc-macro2" 46 | version = "1.0.76" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" 49 | dependencies = [ 50 | "unicode-ident", 51 | ] 52 | 53 | [[package]] 54 | name = "quick-xml" 55 | version = "0.31.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" 58 | dependencies = [ 59 | "memchr", 60 | ] 61 | 62 | [[package]] 63 | name = "quote" 64 | version = "1.0.35" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 67 | dependencies = [ 68 | "proc-macro2", 69 | ] 70 | 71 | [[package]] 72 | name = "ryu" 73 | version = "1.0.16" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" 76 | 77 | [[package]] 78 | name = "serde" 79 | version = "1.0.195" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" 82 | dependencies = [ 83 | "serde_derive", 84 | ] 85 | 86 | [[package]] 87 | name = "serde_derive" 88 | version = "1.0.195" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" 91 | dependencies = [ 92 | "proc-macro2", 93 | "quote", 94 | "syn", 95 | ] 96 | 97 | [[package]] 98 | name = "serde_json" 99 | version = "1.0.111" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" 102 | dependencies = [ 103 | "itoa", 104 | "ryu", 105 | "serde", 106 | ] 107 | 108 | [[package]] 109 | name = "syn" 110 | version = "2.0.48" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" 113 | dependencies = [ 114 | "proc-macro2", 115 | "quote", 116 | "unicode-ident", 117 | ] 118 | 119 | [[package]] 120 | name = "termcolor" 121 | version = "1.4.1" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 124 | dependencies = [ 125 | "winapi-util", 126 | ] 127 | 128 | [[package]] 129 | name = "thiserror" 130 | version = "1.0.56" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" 133 | dependencies = [ 134 | "thiserror-impl", 135 | ] 136 | 137 | [[package]] 138 | name = "thiserror-impl" 139 | version = "1.0.56" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" 142 | dependencies = [ 143 | "proc-macro2", 144 | "quote", 145 | "syn", 146 | ] 147 | 148 | [[package]] 149 | name = "trybuild" 150 | version = "1.0.89" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "9a9d3ba662913483d6722303f619e75ea10b7855b0f8e0d72799cf8621bb488f" 153 | dependencies = [ 154 | "basic-toml", 155 | "glob", 156 | "once_cell", 157 | "serde", 158 | "serde_derive", 159 | "serde_json", 160 | "termcolor", 161 | ] 162 | 163 | [[package]] 164 | name = "unicode-ident" 165 | version = "1.0.12" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 168 | 169 | [[package]] 170 | name = "winapi" 171 | version = "0.3.9" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 174 | dependencies = [ 175 | "winapi-i686-pc-windows-gnu", 176 | "winapi-x86_64-pc-windows-gnu", 177 | ] 178 | 179 | [[package]] 180 | name = "winapi-i686-pc-windows-gnu" 181 | version = "0.4.0" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 184 | 185 | [[package]] 186 | name = "winapi-util" 187 | version = "0.1.6" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 190 | dependencies = [ 191 | "winapi", 192 | ] 193 | 194 | [[package]] 195 | name = "winapi-x86_64-pc-windows-gnu" 196 | version = "0.4.0" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 199 | 200 | [[package]] 201 | name = "xml_struct" 202 | version = "0.1.0" 203 | dependencies = [ 204 | "anyhow", 205 | "quick-xml", 206 | "thiserror", 207 | "xml_struct_derive", 208 | "xml_struct_tests", 209 | ] 210 | 211 | [[package]] 212 | name = "xml_struct_derive" 213 | version = "0.1.0" 214 | dependencies = [ 215 | "proc-macro2", 216 | "quote", 217 | "syn", 218 | ] 219 | 220 | [[package]] 221 | name = "xml_struct_tests" 222 | version = "0.1.0" 223 | dependencies = [ 224 | "quick-xml", 225 | "thiserror", 226 | "trybuild", 227 | "xml_struct", 228 | ] 229 | -------------------------------------------------------------------------------- /xml_struct_tests/integration/struct.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use quick_xml::events::{BytesText, Event}; 6 | use xml_struct::XmlSerialize; 7 | use xml_struct_tests::{serialize_value_as_element, serialize_value_children}; 8 | 9 | #[derive(XmlSerialize)] 10 | struct Struct { 11 | #[xml_struct(attribute)] 12 | str_attr: &'static str, 13 | 14 | #[xml_struct(attribute, ns_prefix = "other_ns")] 15 | string_attr: String, 16 | 17 | child: ChildType, 18 | more_complex_field_name: String, 19 | } 20 | 21 | #[derive(XmlSerialize)] 22 | #[xml_struct(default_ns = "http://foo.example/this_ns", ns = ("other_ns", "http://bar.example/other_ns"))] 23 | struct StructWithNamespaces { 24 | #[xml_struct(attribute)] 25 | str_attr: &'static str, 26 | 27 | #[xml_struct(attribute, ns_prefix = "other_ns")] 28 | string_attr: String, 29 | 30 | child: ChildType, 31 | more_complex_field_name: String, 32 | } 33 | 34 | #[derive(XmlSerialize)] 35 | struct StructWithFlattenedField { 36 | #[xml_struct(attribute)] 37 | str_attr: &'static str, 38 | 39 | #[xml_struct(attribute, ns_prefix = "other_ns")] 40 | string_attr: String, 41 | 42 | #[xml_struct(flatten)] 43 | child: ChildType, 44 | more_complex_field_name: String, 45 | } 46 | 47 | struct ChildType { 48 | _grandchild: &'static str, 49 | } 50 | 51 | impl ChildType { 52 | #[allow(dead_code)] 53 | fn serialize_child_nodes( 54 | &self, 55 | _writer: &mut quick_xml::Writer, 56 | ) -> Result<(), xml_struct::Error> 57 | where 58 | W: std::io::Write, 59 | { 60 | panic!("`XmlSerialize` calls should not dispatch non-trait functions"); 61 | } 62 | } 63 | 64 | // We explicitly implement `XmlSerialize` for this type in a way which doesn't 65 | // match the default in order to verify that `ChildType`'s implementation is 66 | // used rather than some other magic. 67 | impl XmlSerialize for ChildType { 68 | fn serialize_child_nodes( 69 | &self, 70 | writer: &mut quick_xml::Writer, 71 | ) -> Result<(), xml_struct::Error> 72 | where 73 | W: std::io::Write, 74 | { 75 | writer.write_event(Event::Text(BytesText::new("bare text child node")))?; 76 | 77 | Ok(()) 78 | } 79 | } 80 | 81 | #[test] 82 | fn r#struct() { 83 | let content = Struct { 84 | str_attr: "arbitrary text", 85 | string_attr: String::from("other text"), 86 | child: ChildType { 87 | _grandchild: "this text shouldn't show up", 88 | }, 89 | more_complex_field_name: String::from("bare text node"), 90 | }; 91 | 92 | let expected = "bare text child nodebare text node"; 93 | 94 | let actual = serialize_value_children(content).expect("Failed to write struct"); 95 | 96 | assert_eq!( 97 | actual, expected, 98 | "Struct fields should each be serialized as a child node" 99 | ); 100 | } 101 | 102 | #[test] 103 | fn struct_as_element() { 104 | let content = Struct { 105 | str_attr: "arbitrary text", 106 | string_attr: String::from("other text"), 107 | child: ChildType { 108 | _grandchild: "this text shouldn't show up", 109 | }, 110 | more_complex_field_name: String::from("bare text node"), 111 | }; 112 | 113 | let expected = r#"bare text child nodebare text node"#; 114 | 115 | let actual = serialize_value_as_element(content, "parent").expect("Failed to write struct"); 116 | 117 | assert_eq!( 118 | actual, expected, 119 | "Struct should be serialized as element with fields as attribute and children as appropriate" 120 | ); 121 | } 122 | 123 | #[test] 124 | fn struct_with_namespaces() { 125 | let content = StructWithNamespaces { 126 | str_attr: "arbitrary text", 127 | string_attr: String::from("other text"), 128 | child: ChildType { 129 | _grandchild: "this text shouldn't show up", 130 | }, 131 | more_complex_field_name: String::from("bare text node"), 132 | }; 133 | 134 | let expected = "bare text child nodebare text node"; 135 | 136 | let actual = serialize_value_children(content).expect("Failed to write struct"); 137 | 138 | assert_eq!( 139 | actual, expected, 140 | "Struct fields should each be serialized as a child node" 141 | ); 142 | } 143 | 144 | #[test] 145 | fn struct_with_namespaces_as_element() { 146 | let content = StructWithNamespaces { 147 | str_attr: "arbitrary text", 148 | string_attr: String::from("other text"), 149 | child: ChildType { 150 | _grandchild: "this text shouldn't show up", 151 | }, 152 | more_complex_field_name: String::from("bare text node"), 153 | }; 154 | 155 | let expected = r#"bare text child nodebare text node"#; 156 | 157 | let actual = serialize_value_as_element(content, "parent").expect("Failed to write struct"); 158 | 159 | assert_eq!( 160 | actual, expected, 161 | "Struct should be serialized with namespaces as attributes" 162 | ); 163 | } 164 | 165 | #[test] 166 | fn struct_with_flattened_field() { 167 | let content = StructWithFlattenedField { 168 | str_attr: "arbitrary text", 169 | string_attr: String::from("other text"), 170 | child: ChildType { 171 | _grandchild: "this text shouldn't show up", 172 | }, 173 | more_complex_field_name: String::from("bare text node"), 174 | }; 175 | 176 | let expected = 177 | "bare text child nodebare text node"; 178 | 179 | let actual = serialize_value_children(content).expect("Failed to write struct"); 180 | 181 | assert_eq!( 182 | actual, expected, 183 | "Flattened field should be serialized as content only" 184 | ); 185 | } 186 | 187 | #[test] 188 | fn struct_with_flattened_field_as_element() { 189 | let content = StructWithFlattenedField { 190 | str_attr: "arbitrary text", 191 | string_attr: String::from("other text"), 192 | child: ChildType { 193 | _grandchild: "this text shouldn't show up", 194 | }, 195 | more_complex_field_name: String::from("bare text node"), 196 | }; 197 | 198 | let expected = r#"bare text child nodebare text node"#; 199 | 200 | let actual = serialize_value_as_element(content, "parent").expect("Failed to write struct"); 201 | 202 | assert_eq!( 203 | actual, expected, 204 | "Flattened field should be serialized as content only" 205 | ); 206 | } 207 | -------------------------------------------------------------------------------- /xml_struct_derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | mod properties; 6 | mod serialize; 7 | 8 | use syn::{parse_macro_input, DeriveInput}; 9 | 10 | pub(crate) use properties::*; 11 | 12 | use crate::serialize::{write_serialize_impl_for_enum, write_serialize_impl_for_struct}; 13 | 14 | // This value must match the `attributes` attribute for the derive macro. 15 | const MACRO_ATTRIBUTE: &str = "xml_struct"; 16 | 17 | /// A macro providing automated derivation of the `XmlSerialize` trait. 18 | /// 19 | /// By default, when applied to a struct, the resulting implementation will 20 | /// serialize each of the struct's fields as an XML element with a tag name 21 | /// derived from the name of the field. 22 | /// 23 | /// For example, the following declaration corresponds to the following output: 24 | /// 25 | /// ```ignore 26 | /// #[derive(XmlSerialize)] 27 | /// struct Foo { 28 | /// some_field: SerializeableType, 29 | /// another: String, 30 | /// } 31 | /// 32 | /// let foo = Foo { 33 | /// some_field: SerializeableType { 34 | /// ... 35 | /// }, 36 | /// another: String::from("I am text!"), 37 | /// }; 38 | /// ``` 39 | /// 40 | /// ```text 41 | /// 42 | /// ... 43 | /// 44 | /// 45 | /// I am text! 46 | /// 47 | /// ``` 48 | /// 49 | /// When applied to an enum, the implementation will write an XML element with a 50 | /// tag name derived from the name of the variant. Any fields of the variant 51 | /// will be serialized as children of that element, with tag names derived from 52 | /// the name of the field. 53 | /// 54 | /// As above, the following enum corresponds to the following output: 55 | /// 56 | /// ```ignore 57 | /// #[derive(XmlSerialize)] 58 | /// enum Bar { 59 | /// Foobar { 60 | /// some_field: SerializeableType, 61 | /// another: String, 62 | /// }, 63 | /// ... 64 | /// } 65 | /// 66 | /// let bar = Bar::Foobar { 67 | /// some_field: SerializeableType { 68 | /// ... 69 | /// }, 70 | /// another: String::from("I am text!"), 71 | /// }; 72 | /// ``` 73 | /// 74 | /// ```text 75 | /// 76 | /// 77 | /// ... 78 | /// 79 | /// 80 | /// I am text! 81 | /// 82 | /// 83 | /// ``` 84 | /// 85 | /// Unnamed fields, i.e. fields of tuple structs or enum tuple variants, are 86 | /// serialized without an enclosing element. 87 | /// 88 | /// Enums which consist solely of unit variants will also receive an 89 | /// implementation of the `XmlSerializeAttr` trait. 90 | /// 91 | /// # Configuration 92 | /// 93 | /// The output from derived implementations may be configured with the 94 | /// `xml_struct` attribute. For example, serializing the following as an element 95 | /// named "Baz" corresponds to the following output: 96 | /// 97 | /// ```ignore 98 | /// #[derive(XmlSerialize)] 99 | /// #[xml_struct(default_ns = "http://foo.example/")] 100 | /// struct Baz { 101 | /// #[xml_struct(flatten)] 102 | /// some_field: SerializeableType, 103 | /// 104 | /// #[xml_struct(attribute, ns_prefix = "foo")] 105 | /// another: String, 106 | /// } 107 | /// 108 | /// let foo = Baz { 109 | /// some_field: SerializeableType { 110 | /// ... 111 | /// }, 112 | /// another: String::from("I am text!"), 113 | /// }; 114 | /// ``` 115 | /// 116 | /// ```text 117 | /// 118 | /// ... 119 | /// 120 | /// ``` 121 | /// 122 | /// The following options are available: 123 | /// 124 | /// ## Data Structures 125 | /// 126 | /// These options affect the serialization of a struct or enum as a whole. 127 | /// 128 | /// - `default_ns = "http://foo.example/"` 129 | /// 130 | /// Provides the name to be used as the default namespace of elements 131 | /// representing the marked structure, i.e.: 132 | /// 133 | /// ```text 134 | /// 135 | /// ``` 136 | /// 137 | /// **NOTE**: The namespace will not be specified if values are serialized as 138 | /// content nodes only. 139 | /// 140 | /// - `ns = ("foo", "http://foo.example/")` 141 | /// 142 | /// Declares a namespace to be used for elements representing the marked 143 | /// structure, i.e.: 144 | /// 145 | /// ```text 146 | /// 147 | /// ``` 148 | /// 149 | /// Multiple namespaces may be declared for each structure. 150 | /// 151 | /// **NOTE**: The namespace will not be specified if values are serialized as 152 | /// content nodes only. 153 | /// 154 | /// - `text` 155 | /// 156 | /// Specifies that a marked enum's variants should be serialized as text nodes 157 | /// or as XML attribute values (depending on use in containing structures). 158 | /// 159 | /// **NOTE**: This option is only valid for enums which contain solely unit 160 | /// variants. 161 | /// 162 | /// - `variant_ns_prefix = "foo"` 163 | /// 164 | /// Specifies that a marked enum's variants, when serialized as XML elements, 165 | /// should include a namespace prefix, i.e. 166 | /// 167 | /// ```text 168 | /// 169 | /// ``` 170 | /// 171 | /// **NOTE**: This option is only valid for enums which are not serialized as 172 | /// text nodes. 173 | /// 174 | /// ## Structure Fields 175 | /// 176 | /// These options affect the serialization of a single field in a struct or enum 177 | /// variant. 178 | /// 179 | /// - `attribute` 180 | /// 181 | /// Specifies that the marked field should be serialized as an XML attribute, 182 | /// i.e. `Field="value"`. 183 | /// 184 | /// - `element` 185 | /// 186 | /// Specifies that the marked field should be serialized as an XML element. 187 | /// This is the default behavior, and use of this attribute is optional. 188 | /// 189 | /// - `flatten` 190 | /// 191 | /// Specifies that the marked field should be serialized as content nodes 192 | /// without an enclosing XML element. 193 | /// 194 | /// - `ns_prefix = "foo"` 195 | /// 196 | /// Specifies that the marked field, when serialized as an XML element or 197 | /// attribute, should use include a namespace prefix, i.e. `foo:Field="value"` 198 | /// or 199 | /// 200 | /// ```text 201 | /// 202 | /// ``` 203 | #[proc_macro_derive(XmlSerialize, attributes(xml_struct))] 204 | pub fn derive_xml_serialize(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 205 | let input = parse_macro_input!(input as DeriveInput); 206 | 207 | let props = match TypeProps::try_from_input(&input) { 208 | Ok(props) => props, 209 | Err(err) => return err.into_compile_error().into(), 210 | }; 211 | 212 | let DeriveInput { 213 | generics, ident, .. 214 | } = input; 215 | 216 | match input.data { 217 | syn::Data::Struct(input) => write_serialize_impl_for_struct(ident, generics, input, props), 218 | syn::Data::Enum(input) => write_serialize_impl_for_enum(ident, generics, input, props), 219 | syn::Data::Union(_) => panic!("Serializing unions as XML is unsupported"), 220 | } 221 | // `syn` and `quote` use the `proc_macro2` crate, so internally we deal in 222 | // its `TokenStream`, but derive macros must use `proc_macro`'s, so convert 223 | // at the last minute. 224 | .into() 225 | } 226 | -------------------------------------------------------------------------------- /xml_struct/src/impls.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | //! This module provides implementations of serialization for common types from 6 | //! the standard library. 7 | 8 | use quick_xml::{ 9 | events::{BytesText, Event}, 10 | Writer, 11 | }; 12 | 13 | use crate::{Error, XmlSerialize, XmlSerializeAttr}; 14 | 15 | /// Serializes a string as a text content node. 16 | impl XmlSerialize for str { 17 | fn serialize_child_nodes(&self, writer: &mut Writer) -> Result<(), Error> 18 | where 19 | W: std::io::Write, 20 | { 21 | writer.write_event(Event::Text(BytesText::new(self)))?; 22 | 23 | Ok(()) 24 | } 25 | } 26 | 27 | /// Serializes a reference to a string as a text content node. 28 | impl XmlSerialize for &T 29 | where 30 | T: AsRef, 31 | { 32 | fn serialize_child_nodes(&self, writer: &mut Writer) -> Result<(), Error> 33 | where 34 | W: std::io::Write, 35 | { 36 | writer.write_event(Event::Text(BytesText::new(self.as_ref())))?; 37 | 38 | Ok(()) 39 | } 40 | } 41 | 42 | /// Serializes a string as a text content node. 43 | impl XmlSerialize for String { 44 | fn serialize_child_nodes(&self, writer: &mut Writer) -> Result<(), Error> 45 | where 46 | W: std::io::Write, 47 | { 48 | writer.write_event(Event::Text(BytesText::new(self.as_str())))?; 49 | 50 | Ok(()) 51 | } 52 | } 53 | 54 | /// Serializes a string as a text content node. 55 | impl XmlSerialize for &str { 56 | fn serialize_child_nodes(&self, writer: &mut Writer) -> Result<(), Error> 57 | where 58 | W: std::io::Write, 59 | { 60 | writer.write_event(Event::Text(BytesText::new(self)))?; 61 | 62 | Ok(()) 63 | } 64 | } 65 | 66 | /// Serializes a boolean as a text content node. 67 | /// 68 | /// `true` is serialized as the string value "true", while `false` is serialized 69 | /// as the string value "false". 70 | impl XmlSerialize for bool { 71 | fn serialize_child_nodes(&self, writer: &mut Writer) -> Result<(), Error> 72 | where 73 | W: std::io::Write, 74 | { 75 | let content = if *self { "true" } else { "false" }; 76 | 77 | writer.write_event(Event::Text(BytesText::new(content)))?; 78 | 79 | Ok(()) 80 | } 81 | } 82 | 83 | /// Serializes the contents of an `Option` as content nodes. 84 | /// 85 | /// `Some(t)` is serialized identically to `t`, while `None` produces no output. 86 | impl XmlSerialize for Option 87 | where 88 | T: XmlSerialize, 89 | { 90 | fn serialize_as_element(&self, writer: &mut Writer, name: &str) -> Result<(), Error> 91 | where 92 | W: std::io::Write, 93 | { 94 | match self { 95 | Some(value) => ::serialize_as_element(value, writer, name), 96 | None => Ok(()), 97 | } 98 | } 99 | 100 | fn serialize_child_nodes(&self, writer: &mut Writer) -> Result<(), Error> 101 | where 102 | W: std::io::Write, 103 | { 104 | match self { 105 | Some(value) => ::serialize_child_nodes(value, writer), 106 | None => Ok(()), 107 | } 108 | } 109 | } 110 | 111 | /// Serializes the contents of a `Vec` as content nodes. 112 | /// 113 | /// Each element of the `Vec` is serialized via its `serialize_child_nodes()` 114 | /// implementation. If the `Vec` is empty, no output is produced. 115 | impl XmlSerialize for Vec 116 | where 117 | T: XmlSerialize, 118 | { 119 | fn serialize_child_nodes(&self, writer: &mut Writer) -> Result<(), Error> 120 | where 121 | W: std::io::Write, 122 | { 123 | if self.is_empty() { 124 | return Ok(()); 125 | } 126 | 127 | for value in self { 128 | ::serialize_child_nodes(value, writer)?; 129 | } 130 | 131 | Ok(()) 132 | } 133 | } 134 | 135 | /// Serializes a string as an XML attribute value. 136 | impl XmlSerializeAttr for str { 137 | fn serialize_as_attribute(&self, start_tag: &mut quick_xml::events::BytesStart, name: &str) { 138 | start_tag.push_attribute((name, self)); 139 | } 140 | } 141 | 142 | /// Serializes a reference to a string as an XML attribute value. 143 | impl XmlSerializeAttr for &T 144 | where 145 | T: AsRef, 146 | { 147 | fn serialize_as_attribute(&self, start_tag: &mut quick_xml::events::BytesStart, name: &str) { 148 | start_tag.push_attribute((name, self.as_ref())); 149 | } 150 | } 151 | 152 | /// Serializes a string as an XML attribute value. 153 | impl XmlSerializeAttr for String { 154 | fn serialize_as_attribute(&self, start_tag: &mut quick_xml::events::BytesStart, name: &str) { 155 | start_tag.push_attribute((name, self.as_str())); 156 | } 157 | } 158 | 159 | /// Serializes a string as an XML attribute value. 160 | impl XmlSerializeAttr for &str { 161 | fn serialize_as_attribute(&self, start_tag: &mut quick_xml::events::BytesStart, name: &str) { 162 | start_tag.push_attribute((name, *self)); 163 | } 164 | } 165 | 166 | /// Serializes a boolean as an XML attribute value. 167 | /// 168 | /// `true` is serialized as the string value "true", while `false` is serialized 169 | /// as the string value "false". 170 | impl XmlSerializeAttr for bool { 171 | fn serialize_as_attribute(&self, start_tag: &mut quick_xml::events::BytesStart, name: &str) { 172 | let content = if *self { "true" } else { "false" }; 173 | 174 | start_tag.push_attribute((name, content)); 175 | } 176 | } 177 | 178 | /// Serializes the contents of an `Option` as an XML attribute value. 179 | /// 180 | /// `Some(t)` is serialized identically to `t`, while `None` produces no output. 181 | impl XmlSerializeAttr for Option 182 | where 183 | T: XmlSerializeAttr, 184 | { 185 | fn serialize_as_attribute(&self, start_tag: &mut quick_xml::events::BytesStart, name: &str) { 186 | match self { 187 | Some(value) => value.serialize_as_attribute(start_tag, name), 188 | None => (), 189 | } 190 | } 191 | } 192 | 193 | /// Implements serialization of a type as either an XML text node or attribute 194 | /// value. 195 | /// 196 | /// This is a convenience macro intended for implementing basic serialization of 197 | /// primitive/standard library types. This is done per-type rather than 198 | /// wholesale for `ToString` in order to avoid requiring that `Display` and 199 | /// `XmlSerialize`/`XmlSerializeAttr` share a form. 200 | macro_rules! impl_as_text_for { 201 | ($( $ty:ty ),*) => { 202 | $( 203 | /// Serializes an integer as a text content node. 204 | impl XmlSerialize for $ty { 205 | fn serialize_child_nodes(&self, writer: &mut Writer) -> Result<(), Error> 206 | where 207 | W: std::io::Write, 208 | { 209 | let string = self.to_string(); 210 | writer.write_event(Event::Text(BytesText::new(&string)))?; 211 | 212 | Ok(()) 213 | } 214 | } 215 | 216 | /// Serializes an integer as an XML attribute value. 217 | impl XmlSerializeAttr for $ty { 218 | fn serialize_as_attribute( 219 | &self, 220 | start_tag: &mut quick_xml::events::BytesStart, 221 | name: &str, 222 | ) { 223 | start_tag.push_attribute((name, self.to_string().as_str())); 224 | } 225 | })* 226 | }; 227 | } 228 | 229 | impl_as_text_for!(i8, u8, i16, u16, i32, u32, i64, u64, usize); 230 | -------------------------------------------------------------------------------- /xml_struct_tests/integration/enum.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use xml_struct::XmlSerialize; 6 | use xml_struct_tests::{serialize_value_as_element, serialize_value_children}; 7 | 8 | #[derive(XmlSerialize)] 9 | enum MixedEnum { 10 | UnitVariant, 11 | StructVariant { 12 | some_field: String, 13 | }, 14 | TupleVariant(&'static str), 15 | StructVariantWithAttributes { 16 | child_field: String, 17 | 18 | #[xml_struct(attribute)] 19 | attr_field: String, 20 | }, 21 | } 22 | 23 | #[derive(XmlSerialize)] 24 | enum AllUnitEnum { 25 | A, 26 | Two, 27 | Gamma, 28 | } 29 | 30 | #[derive(XmlSerialize)] 31 | struct StructWithAllUnitEnumFields { 32 | child_field: String, 33 | enum_child: AllUnitEnum, 34 | 35 | #[xml_struct(ns_prefix = "foo")] 36 | enum_child_with_prefix: AllUnitEnum, 37 | } 38 | 39 | #[derive(XmlSerialize)] 40 | #[xml_struct(variant_ns_prefix = "foo")] 41 | enum EnumWithNamespacePrefix { 42 | SomeValue, 43 | } 44 | 45 | #[test] 46 | fn mixed_enum_unit_variant() { 47 | let content = MixedEnum::UnitVariant; 48 | 49 | let expected = ""; 50 | let actual = serialize_value_children(content).expect("Failed to serialize enum value"); 51 | 52 | assert_eq!( 53 | actual, expected, 54 | "Unit variants should serialize as an element with no content" 55 | ); 56 | } 57 | 58 | #[test] 59 | fn mixed_enum_unit_variant_as_element() { 60 | let content = MixedEnum::UnitVariant; 61 | 62 | let expected = ""; 63 | let actual = 64 | serialize_value_as_element(content, "parent_name").expect("Failed to serialize enum value"); 65 | 66 | assert_eq!( 67 | actual, expected, 68 | "Unit variants should serialize as a parented element with no content" 69 | ); 70 | } 71 | 72 | #[test] 73 | fn mixed_enum_struct_variant() { 74 | let content = MixedEnum::StructVariant { 75 | some_field: String::from("some content"), 76 | }; 77 | 78 | let expected = "some content"; 79 | let actual = serialize_value_children(content).expect("Failed to serialize enum value"); 80 | 81 | assert_eq!( 82 | actual, expected, 83 | "Struct variants should serialize as an element with serialized fields as content" 84 | ); 85 | } 86 | 87 | #[test] 88 | fn mixed_enum_struct_variant_as_element() { 89 | let content = MixedEnum::StructVariant { 90 | some_field: String::from("some content"), 91 | }; 92 | 93 | let expected = 94 | "some content"; 95 | let actual = 96 | serialize_value_as_element(content, "FooBar").expect("Failed to serialize enum value"); 97 | 98 | assert_eq!( 99 | actual, expected, 100 | "Struct variants should serialize as a parented element with serialized fields as content" 101 | ); 102 | } 103 | 104 | #[test] 105 | fn mixed_enum_tuple_variant() { 106 | let content = MixedEnum::TupleVariant("something in a tuple"); 107 | 108 | let expected = "something in a tuple"; 109 | let actual = serialize_value_children(content).expect("Failed to serialize enum value"); 110 | 111 | assert_eq!( 112 | actual, expected, 113 | "Tuple variants should serialize as an element with serialized fields as content" 114 | ); 115 | } 116 | 117 | #[test] 118 | fn mixed_enum_tuple_variant_as_element() { 119 | let content = MixedEnum::TupleVariant("something in a tuple"); 120 | 121 | let expected = "something in a tuple"; 122 | let actual = 123 | serialize_value_as_element(content, "banana").expect("Failed to serialize enum value"); 124 | 125 | assert_eq!( 126 | actual, expected, 127 | "Tuple variants should serialize as a parented element with serialized fields as content" 128 | ); 129 | } 130 | 131 | #[test] 132 | fn mixed_enum_struct_variant_with_attributes() { 133 | let content = MixedEnum::StructVariantWithAttributes { 134 | child_field: String::from("some child content"), 135 | attr_field: String::from("an attribute"), 136 | }; 137 | 138 | let expected = r#"some child content"#; 139 | let actual = serialize_value_children(content).expect("Failed to serialize enum value"); 140 | 141 | assert_eq!( 142 | actual, expected, 143 | "Attributes should be applied to the variant element" 144 | ); 145 | } 146 | 147 | #[test] 148 | fn mixed_enum_struct_variant_with_attributes_as_element() { 149 | let content = MixedEnum::StructVariantWithAttributes { 150 | child_field: String::from("some child content"), 151 | attr_field: String::from("an attribute"), 152 | }; 153 | 154 | let expected = r#"some child content"#; 155 | let actual = 156 | serialize_value_as_element(content, "Arbitrary").expect("Failed to serialize enum value"); 157 | 158 | assert_eq!( 159 | actual, expected, 160 | "Attributes should be applied to the variant element rather than the parent" 161 | ); 162 | } 163 | 164 | #[test] 165 | fn all_unit_enum() { 166 | let content = AllUnitEnum::Two; 167 | 168 | let expected = ""; 169 | let actual = serialize_value_children(content).expect("Failed to serialize enum value"); 170 | 171 | assert_eq!( 172 | actual, expected, 173 | "Unit variants should be serialized as an empty element" 174 | ); 175 | } 176 | 177 | #[test] 178 | fn all_unit_enum_as_element() { 179 | let content = AllUnitEnum::Two; 180 | 181 | let expected = r#""#; 182 | let actual = 183 | serialize_value_as_element(content, "foo").expect("Failed to serialize enum value"); 184 | 185 | assert_eq!( 186 | actual, expected, 187 | "Unit variants should be serialized as a parented empty element" 188 | ); 189 | } 190 | 191 | #[test] 192 | fn struct_with_all_unit_enum_fields() { 193 | let content = StructWithAllUnitEnumFields { 194 | child_field: String::from("this is a regular string field"), 195 | enum_child: AllUnitEnum::A, 196 | enum_child_with_prefix: AllUnitEnum::Gamma, 197 | }; 198 | 199 | let expected = 200 | "this is a regular string field"; 201 | let actual = serialize_value_children(content).expect("Failed to serialize struct value"); 202 | 203 | assert_eq!( 204 | actual, expected, 205 | "Unit enum fields should be serialized as empty elements" 206 | ) 207 | } 208 | 209 | #[test] 210 | fn struct_with_all_unit_enum_fields_as_element() { 211 | let content = StructWithAllUnitEnumFields { 212 | child_field: String::from("this is a regular string field"), 213 | enum_child: AllUnitEnum::A, 214 | enum_child_with_prefix: AllUnitEnum::Gamma, 215 | }; 216 | 217 | let expected = r#"this is a regular string field"#; 218 | let actual = 219 | serialize_value_as_element(content, "TAGNAME").expect("Failed to serialize struct value"); 220 | 221 | assert_eq!( 222 | actual, expected, 223 | "Unit enum fields should be serialized as parented empty elements" 224 | ) 225 | } 226 | 227 | #[test] 228 | fn enum_with_namespace_prefix() { 229 | let content = EnumWithNamespacePrefix::SomeValue; 230 | 231 | let expected = ""; 232 | let actual = serialize_value_children(content).expect("Failed to serialize enum value"); 233 | 234 | assert_eq!( 235 | actual, expected, 236 | "Enum variants should be serialized with specified prefix" 237 | ); 238 | } 239 | 240 | #[test] 241 | fn enum_with_namespace_prefix_as_element() { 242 | let content = EnumWithNamespacePrefix::SomeValue; 243 | 244 | let expected = ""; 245 | let actual = 246 | serialize_value_as_element(content, "outer_foo").expect("Failed to serialize enum value"); 247 | 248 | assert_eq!( 249 | actual, expected, 250 | "Enum variants should be serialized with specified prefix" 251 | ); 252 | } 253 | -------------------------------------------------------------------------------- /xml_struct_derive/src/serialize.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | mod codegen; 6 | 7 | use proc_macro2::{Ident, Literal, TokenStream}; 8 | use quote::{format_ident, quote, ToTokens}; 9 | use syn::{DataEnum, DataStruct, Generics}; 10 | 11 | use crate::{FieldProps, TypeProps}; 12 | 13 | use self::codegen::{ 14 | generate_serialize_impl_for, with_enum_variants, with_struct_fields, with_text_variants, Field, 15 | FieldKind, Variant, VariantKind, 16 | }; 17 | 18 | /// Generates an implementation of the `XmlSerialize` trait for a Rust struct 19 | /// and its fields. 20 | pub(crate) fn write_serialize_impl_for_struct( 21 | ident: Ident, 22 | generics: Generics, 23 | input: DataStruct, 24 | props: TypeProps, 25 | ) -> TokenStream { 26 | // We build a list of errors so that we can combine them later and emit 27 | // them all instead of quitting at the first we encounter. 28 | let mut errors = Vec::new(); 29 | 30 | // Process the struct's fields in order to determine how to represent them, 31 | // based on struct type and any consumer-applied attributes. 32 | let fields = match input.fields { 33 | // Fields in a regular struct, i.e. declared with a name and type. 34 | syn::Fields::Named(fields) => fields 35 | .named 36 | .into_iter() 37 | .map(process_named_field( 38 | &mut errors, 39 | |ident| quote!(self.#ident), 40 | )) 41 | .collect(), 42 | 43 | // Fields in a tuple struct, i.e. declared by type and position only. 44 | syn::Fields::Unnamed(fields) => fields 45 | .unnamed 46 | .into_iter() 47 | .enumerate() 48 | .map(process_unnamed_field(&mut errors, |idx| { 49 | let idx_literal = Literal::usize_unsuffixed(idx); 50 | quote!(self.#idx_literal) 51 | })) 52 | .collect(), 53 | 54 | // A unit struct, i.e. one which has no fields. 55 | syn::Fields::Unit => vec![], 56 | }; 57 | 58 | // Combine and return errors if there are any. If none, we've successfully 59 | // handled all fields and can generate the final implementation. 60 | let err = errors.into_iter().reduce(|mut acc, err| { 61 | acc.combine(err); 62 | 63 | acc 64 | }); 65 | 66 | if let Some(err) = err { 67 | return err.into_compile_error(); 68 | } 69 | 70 | generate_serialize_impl_for(ident, generics, props, with_struct_fields(fields)) 71 | } 72 | 73 | /// Generates an implementation of the `XmlSerialize` trait (and the 74 | /// `XmlSerializeAttr` trait if appropriate) for a Rust enum, its variants, and 75 | /// their fields. 76 | pub(crate) fn write_serialize_impl_for_enum( 77 | ident: Ident, 78 | generics: Generics, 79 | input: DataEnum, 80 | mut props: TypeProps, 81 | ) -> TokenStream { 82 | if props.should_serialize_as_text { 83 | // We depend on the code which generates `TypeProps` to handle verifying 84 | // that this enum consists solely of unit variants when setting this 85 | // property, so we just collect variant identifiers. 86 | let variants = input 87 | .variants 88 | .into_iter() 89 | .map(|variant| variant.ident) 90 | .collect(); 91 | 92 | return generate_serialize_impl_for(ident, generics, props, with_text_variants(variants)); 93 | } 94 | 95 | // We build a list of errors so that we can combine them later and emit 96 | // them all instead of quitting at the first we encounter. 97 | let mut errors = Vec::new(); 98 | 99 | // Process the enum's variants in order to determine how to represent them, 100 | // based on variant type and any consumer-applied attributes. 101 | let variants = input 102 | .variants 103 | .into_iter() 104 | .map(process_enum_variant(&mut errors)) 105 | .collect(); 106 | 107 | // Combine and return errors if there are any. If none, we've successfully 108 | // handled all fields and can generate the final implementation. 109 | let err = errors.into_iter().reduce(|mut acc, err| { 110 | acc.combine(err); 111 | 112 | acc 113 | }); 114 | 115 | if let Some(err) = err { 116 | return err.into_compile_error(); 117 | } 118 | 119 | // Since this is enum-specific, there should be no reason for it to be used 120 | // in codegen and we can just steal the memory. 121 | let ns_prefix = props.ns_prefix_for_variants.take(); 122 | 123 | generate_serialize_impl_for( 124 | ident, 125 | generics, 126 | props, 127 | with_enum_variants(variants, ns_prefix), 128 | ) 129 | } 130 | 131 | /// Creates a callback for processing a `syn` enum variant into codegen details. 132 | fn process_enum_variant(errors: &mut Vec) -> impl FnMut(syn::Variant) -> Variant + '_ { 133 | |variant| { 134 | // Process the variants's fields in order to determine how to represent 135 | // them, based on variant type and any consumer-applied attributes. 136 | let kind = match variant.fields { 137 | syn::Fields::Named(fields) => { 138 | let fields = fields 139 | .named 140 | .into_iter() 141 | .map(process_named_field(errors, Ident::to_token_stream)) 142 | .collect(); 143 | 144 | VariantKind::Struct(fields) 145 | } 146 | syn::Fields::Unnamed(fields) => { 147 | let fields = fields 148 | .unnamed 149 | .into_iter() 150 | .enumerate() 151 | .map(process_unnamed_field(errors, |idx| { 152 | format_ident!("field{idx}").into_token_stream() 153 | })) 154 | .collect(); 155 | 156 | VariantKind::Tuple(fields) 157 | } 158 | syn::Fields::Unit => VariantKind::Unit, 159 | }; 160 | 161 | Variant { 162 | ident: variant.ident, 163 | kind, 164 | } 165 | } 166 | } 167 | 168 | /// Creates a callback for extracting representation details from a named field 169 | /// (i.e., a field of a regular struct or a struct enum variant) and its 170 | /// attributes. 171 | /// 172 | /// The `accessor_generator` callback should, based on the name of a field, 173 | /// return an expression for accessing the value of that field (either on `self` 174 | /// or within a match arm). 175 | fn process_named_field<'cb, 'g: 'cb, G>( 176 | errors: &'cb mut Vec, 177 | mut accessor_generator: G, 178 | ) -> impl FnMut(syn::Field) -> Field + 'cb 179 | where 180 | G: FnMut(&Ident) -> TokenStream + 'g, 181 | { 182 | move |field| { 183 | // We should be able to unwrap without panicking, since we know this is 184 | // a named field. 185 | let ident = field.ident.unwrap(); 186 | let accessor = accessor_generator(&ident); 187 | 188 | let props = FieldProps::try_from_attrs(field.attrs, true) 189 | .unwrap_or_else(collect_field_processing_error(errors)); 190 | 191 | Field { 192 | kind: FieldKind::Named(ident), 193 | ty: field.ty.into_token_stream(), 194 | accessor, 195 | props, 196 | } 197 | } 198 | } 199 | 200 | /// Creates a callback for extracting representation details from an unnamed 201 | /// field (i.e., a field of a tuple struct or a tuple enum variant) and its 202 | /// attributes. 203 | /// 204 | /// The `accessor_generator` callback should, based on the position of a field, 205 | /// return an expression for accessing the value of that field (either on `self` 206 | /// or within a match arm). 207 | fn process_unnamed_field<'cb, 'g: 'cb, G>( 208 | errors: &'cb mut Vec, 209 | mut accessor_generator: G, 210 | ) -> impl FnMut((usize, syn::Field)) -> Field + 'cb 211 | where 212 | G: FnMut(usize) -> TokenStream + 'g, 213 | { 214 | move |(idx, field)| { 215 | let accessor = accessor_generator(idx); 216 | 217 | let props = FieldProps::try_from_attrs(field.attrs, false) 218 | .unwrap_or_else(collect_field_processing_error(errors)); 219 | 220 | Field { 221 | kind: FieldKind::Unnamed, 222 | ty: field.ty.into_token_stream(), 223 | accessor, 224 | props, 225 | } 226 | } 227 | } 228 | 229 | /// Creates a callback for handling errors in processing field properties. 230 | fn collect_field_processing_error( 231 | errors: &mut Vec, 232 | ) -> impl FnMut(syn::Error) -> FieldProps + '_ { 233 | |err| { 234 | errors.push(err); 235 | 236 | FieldProps::default() 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /xml_struct/src/tests.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | #![cfg(test)] 6 | 7 | use quick_xml::events::{attributes::Attribute, BytesStart}; 8 | use xml_struct_tests::{serialize_value_as_element, serialize_value_children}; 9 | 10 | use crate::XmlSerializeAttr; 11 | 12 | #[test] 13 | fn string_as_content_node() { 14 | let content = String::from("some arbitrary content"); 15 | let expected = content.clone(); 16 | 17 | let actual = 18 | serialize_value_children(content).expect("Failed to serialize string as text content"); 19 | assert_eq!( 20 | actual, expected, 21 | "Serializing `String` should result in bare text content" 22 | ); 23 | } 24 | 25 | #[test] 26 | fn string_as_element() { 27 | let name = "SomeTag"; 28 | 29 | let content = String::from("some arbitrary content"); 30 | let expected = format!("<{name}>{content}"); 31 | 32 | let actual = serialize_value_as_element(content.clone(), name) 33 | .expect("Failed to serialize string as text content"); 34 | assert_eq!( 35 | actual, expected, 36 | "Serializing `String` should result in element with text content" 37 | ); 38 | 39 | let actual = serialize_value_as_element(&content, name) 40 | .expect("Failed to serialize string as text content"); 41 | assert_eq!( 42 | actual, expected, 43 | "Serializing `&String` should result in element with text content" 44 | ); 45 | 46 | let actual = serialize_value_as_element(content.as_str(), name) 47 | .expect("Failed to serialize string as text content"); 48 | assert_eq!( 49 | actual, expected, 50 | "Serializing `&str` should result in element with text content" 51 | ); 52 | } 53 | 54 | #[test] 55 | fn int_as_content_node() { 56 | let content: i8 = 17; 57 | let expected = format!("{content}"); 58 | 59 | let actual = 60 | serialize_value_children(content).expect("Failed to serialize int as text content"); 61 | assert_eq!( 62 | actual, expected, 63 | "Serializing `i8` should result in bare text content" 64 | ); 65 | 66 | let actual = 67 | serialize_value_children(content as u8).expect("Failed to serialize int as text content"); 68 | assert_eq!( 69 | actual, expected, 70 | "Serializing `u8` should result in bare text content" 71 | ); 72 | 73 | let actual = 74 | serialize_value_children(content as i16).expect("Failed to serialize int as text content"); 75 | assert_eq!( 76 | actual, expected, 77 | "Serializing `i16` should result in bare text content" 78 | ); 79 | 80 | let actual = 81 | serialize_value_children(content as u16).expect("Failed to serialize int as text content"); 82 | assert_eq!( 83 | actual, expected, 84 | "Serializing `u16` should result in bare text content" 85 | ); 86 | 87 | let actual = 88 | serialize_value_children(content as i32).expect("Failed to serialize int as text content"); 89 | assert_eq!( 90 | actual, expected, 91 | "Serializing `i32` should result in bare text content" 92 | ); 93 | 94 | let actual = 95 | serialize_value_children(content as u32).expect("Failed to serialize int as text content"); 96 | assert_eq!( 97 | actual, expected, 98 | "Serializing `u32` should result in bare text content" 99 | ); 100 | 101 | let actual = 102 | serialize_value_children(content as i64).expect("Failed to serialize int as text content"); 103 | assert_eq!( 104 | actual, expected, 105 | "Serializing `i64` should result in bare text content" 106 | ); 107 | 108 | let actual = 109 | serialize_value_children(content as u64).expect("Failed to serialize int as text content"); 110 | assert_eq!( 111 | actual, expected, 112 | "Serializing `u64` should result in bare text content" 113 | ); 114 | 115 | let actual = serialize_value_children(content as usize) 116 | .expect("Failed to serialize int as text content"); 117 | assert_eq!( 118 | actual, expected, 119 | "Serializing `usize` should result in bare text content" 120 | ); 121 | } 122 | 123 | #[test] 124 | fn int_as_element() { 125 | let name = "last_march_of_the_ints"; 126 | 127 | let content: i8 = 17; 128 | let expected = format!("<{name}>{content}"); 129 | 130 | let actual = 131 | serialize_value_as_element(content, name).expect("Failed to serialize int as text content"); 132 | assert_eq!( 133 | actual, expected, 134 | "Serializing `i8` should result in bare text content" 135 | ); 136 | 137 | let actual = serialize_value_as_element(content as u8, name) 138 | .expect("Failed to serialize int as text content"); 139 | assert_eq!( 140 | actual, expected, 141 | "Serializing `u8` should result in bare text content" 142 | ); 143 | 144 | let actual = serialize_value_as_element(content as i16, name) 145 | .expect("Failed to serialize int as text content"); 146 | assert_eq!( 147 | actual, expected, 148 | "Serializing `i16` should result in bare text content" 149 | ); 150 | 151 | let actual = serialize_value_as_element(content as u16, name) 152 | .expect("Failed to serialize int as text content"); 153 | assert_eq!( 154 | actual, expected, 155 | "Serializing `u16` should result in bare text content" 156 | ); 157 | 158 | let actual = serialize_value_as_element(content as i32, name) 159 | .expect("Failed to serialize int as text content"); 160 | assert_eq!( 161 | actual, expected, 162 | "Serializing `i32` should result in bare text content" 163 | ); 164 | 165 | let actual = serialize_value_as_element(content as u32, name) 166 | .expect("Failed to serialize int as text content"); 167 | assert_eq!( 168 | actual, expected, 169 | "Serializing `u32` should result in bare text content" 170 | ); 171 | 172 | let actual = serialize_value_as_element(content as i64, name) 173 | .expect("Failed to serialize int as text content"); 174 | assert_eq!( 175 | actual, expected, 176 | "Serializing `i64` should result in bare text content" 177 | ); 178 | 179 | let actual = serialize_value_as_element(content as u64, name) 180 | .expect("Failed to serialize int as text content"); 181 | assert_eq!( 182 | actual, expected, 183 | "Serializing `u64` should result in bare text content" 184 | ); 185 | 186 | let actual = serialize_value_as_element(content as usize, name) 187 | .expect("Failed to serialize int as text content"); 188 | assert_eq!( 189 | actual, expected, 190 | "Serializing `usize` should result in bare text content" 191 | ); 192 | } 193 | 194 | #[test] 195 | fn bool_as_content_node() { 196 | let content = true; 197 | let expected = "true"; 198 | 199 | let actual = serialize_value_children(content).expect("Failed to serialize value"); 200 | 201 | assert_eq!( 202 | actual, expected, 203 | "`true` should be serialized as string value 'true'" 204 | ); 205 | 206 | let content = false; 207 | let expected = "false"; 208 | 209 | let actual = serialize_value_children(content).expect("Failed to serialize value"); 210 | 211 | assert_eq!( 212 | actual, expected, 213 | "`false` should be serialized as string value 'false'" 214 | ); 215 | } 216 | 217 | #[test] 218 | fn bool_as_attribute_value() { 219 | let element_name = "foo"; 220 | let attr_name = "bar"; 221 | 222 | let content = true; 223 | let expected = vec![Attribute::from((attr_name, "true"))]; 224 | 225 | let mut start = BytesStart::new(element_name); 226 | content.serialize_as_attribute(&mut start, &attr_name); 227 | 228 | let actual: Vec<_> = start 229 | .attributes() 230 | .map(|result| result.expect("Failed to get attribute value")) 231 | .collect(); 232 | 233 | assert_eq!( 234 | actual, expected, 235 | "`true` should be serialized as string value 'true'" 236 | ); 237 | 238 | let content = false; 239 | let expected = vec![Attribute::from((attr_name, "false"))]; 240 | 241 | let mut start = BytesStart::new(element_name); 242 | content.serialize_as_attribute(&mut start, &attr_name); 243 | 244 | let actual: Vec<_> = start 245 | .attributes() 246 | .map(|result| result.expect("Failed to get attribute value")) 247 | .collect(); 248 | 249 | assert_eq!( 250 | actual, expected, 251 | "`false` should be serialized as string value 'false'" 252 | ); 253 | } 254 | 255 | #[test] 256 | fn bool_as_element() { 257 | let name = "george"; 258 | 259 | let content = true; 260 | let expected = format!("<{name}>true"); 261 | 262 | let actual = serialize_value_as_element(content, name).expect("Failed to serialize value"); 263 | 264 | assert_eq!( 265 | actual, expected, 266 | "Serializing `bool` should result in bare text content" 267 | ); 268 | 269 | let content = false; 270 | let expected = format!("<{name}>false"); 271 | 272 | let actual = serialize_value_as_element(content, name).expect("Failed to serialize value"); 273 | 274 | assert_eq!( 275 | actual, expected, 276 | "Serializing `bool` should result in bare text content" 277 | ); 278 | } 279 | -------------------------------------------------------------------------------- /xml_struct_derive/src/properties.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use proc_macro2::TokenStream; 6 | use quote::ToTokens as _; 7 | use syn::{ 8 | punctuated::Punctuated, spanned::Spanned as _, Attribute, DeriveInput, Error, Expr, Meta, Token, 9 | }; 10 | 11 | use crate::MACRO_ATTRIBUTE; 12 | 13 | const UNRECOGNIZED_ATTRIBUTE_MSG: &str = "unrecognized `xml_struct` attribute"; 14 | 15 | #[derive(Debug, Default)] 16 | /// Properties governing the serialization of a struct or enum with a derived 17 | /// `XmlSerialize` implementation. 18 | pub(crate) struct TypeProps { 19 | /// A declaration of a name for the default XML namespace. 20 | /// 21 | /// The value of this name, if any, will be represented as an `xmlns` 22 | /// attribute on the start tag if a field of this type is serialized as an 23 | /// XML element. 24 | /// 25 | /// Note that, in XML terminology, the "name" is the value of the `xmlns` 26 | /// attribute, usually a URI. 27 | pub default_ns_name: Option, 28 | 29 | /// Declarations of XML namespaces. 30 | /// 31 | /// The values of these declarations, if any, will be represented as 32 | /// `xmlns:{prefix}` attributes on the start tag if a field of this type is 33 | /// serialized as an XML element. 34 | pub ns_decls: Vec, 35 | 36 | /// Whether values of this type should be serialized as text nodes instead 37 | /// of element nodes. 38 | /// 39 | /// A value of `true` is only valid when the type to which it is applied is 40 | /// an `enum` consisting only of unit variants. 41 | pub should_serialize_as_text: bool, 42 | 43 | /// A namespace prefix to apply to tags representing enum variants. 44 | /// 45 | /// This property is invalid for structs or text enums. 46 | pub ns_prefix_for_variants: Option, 47 | } 48 | 49 | impl TypeProps { 50 | /// Constructs a set of serialization properties for an enum or struct from 51 | /// its input to the derive macro. 52 | pub(crate) fn try_from_input(input: &DeriveInput) -> Result { 53 | let attr = match find_configuration_attribute(&input.attrs) { 54 | Some(attr) => attr, 55 | 56 | // If we don't find a matching attribute, we assume the default set 57 | // of properties. 58 | None => return Ok(Self::default()), 59 | }; 60 | 61 | // We build a list of errors so that we can combine them later and emit 62 | // them all instead of quitting at the first we encounter. 63 | let mut errors = Vec::new(); 64 | 65 | // We start with the default set of properties, then parse the 66 | // `xml_struct` attribute to modify any property which deviates from the 67 | // default. 68 | let mut properties = TypeProps::default(); 69 | for meta in attr.parse_args_with(Punctuated::::parse_terminated)? { 70 | match meta { 71 | Meta::Path(path) => { 72 | if path.is_ident("text") { 73 | // The consumer has specified that they want to 74 | // represent values of the type to which this is applied 75 | // as text. This is only possible when the type is an 76 | // enum, for which all variants are unit. When that's 77 | // the case, we use the variant name as the text value. 78 | let is_unit_only_enum = match &input.data { 79 | syn::Data::Enum(input) => input 80 | .variants 81 | .iter() 82 | .all(|variant| matches!(variant.fields, syn::Fields::Unit)), 83 | 84 | _ => false, 85 | }; 86 | 87 | if is_unit_only_enum { 88 | properties.should_serialize_as_text = true; 89 | } else { 90 | // There is no clear representation of non-unit enum 91 | // variants or of structs as text nodes or text 92 | // attributes, so we just forbid it. 93 | errors.push(Error::new( 94 | path.span(), 95 | "only unit enums may be derived as text", 96 | )) 97 | } 98 | } else { 99 | errors.push(Error::new(path.span(), UNRECOGNIZED_ATTRIBUTE_MSG)); 100 | } 101 | } 102 | Meta::NameValue(name_value) => { 103 | if name_value.path.is_ident("default_ns") { 104 | // When serialized as an element, values of the type to 105 | // which this is applied should include a declaration of 106 | // a default namespace, e.g. `xmlns="foo"`. This 107 | // attribute should occur at most once per type. 108 | match properties.default_ns_name { 109 | Some(_) => { 110 | errors.push(Error::new( 111 | name_value.path.span(), 112 | "cannot declare more than one default namespace", 113 | )); 114 | } 115 | 116 | None => { 117 | properties.default_ns_name = 118 | Some(name_value.value.to_token_stream()) 119 | } 120 | } 121 | } else if name_value.path.is_ident("ns") { 122 | // When serialized as an element, values of the type to 123 | // which this is applied should include a declaration of 124 | // a namespace with prefix, e.g. `xmlns:foo="bar"`. 125 | // There can be many of these attributes per type. 126 | // 127 | // Ideally, we could prevent duplicate namespace prefixes here, 128 | // but allowing consumers to pass either by variable or by 129 | // literal makes that exceedingly difficult. 130 | match &name_value.value { 131 | Expr::Tuple(tuple) if tuple.elems.len() == 2 => { 132 | properties.ns_decls.push(NamespaceDecl { 133 | prefix: tuple.elems[0].to_token_stream(), 134 | name: tuple.elems[1].to_token_stream(), 135 | }) 136 | } 137 | 138 | unexpected => errors.push(Error::new( 139 | unexpected.span(), 140 | "namespace value must be a tuple of exactly two elements", 141 | )), 142 | } 143 | } else if name_value.path.is_ident("variant_ns_prefix") { 144 | // When serialized as an element, values of the enum 145 | // type to which this is applied should have a namespace 146 | // prefix added to the element's tag name. 147 | match properties.ns_prefix_for_variants { 148 | Some(_) => { 149 | errors.push(Error::new( 150 | name_value.path.span(), 151 | "cannot declare more than one namespace prefix", 152 | )); 153 | } 154 | None => match &input.data { 155 | syn::Data::Enum(_) => { 156 | properties.ns_prefix_for_variants = 157 | Some(name_value.value.to_token_stream()); 158 | } 159 | 160 | _ => { 161 | errors.push(Error::new( 162 | name_value.path.span(), 163 | "cannot declare variant namespace prefix for non-enum", 164 | )); 165 | } 166 | }, 167 | } 168 | } else { 169 | errors.push(Error::new(name_value.span(), UNRECOGNIZED_ATTRIBUTE_MSG)); 170 | } 171 | } 172 | 173 | _ => { 174 | errors.push(Error::new(meta.span(), UNRECOGNIZED_ATTRIBUTE_MSG)); 175 | } 176 | } 177 | } 178 | 179 | let has_namespace_decl = 180 | properties.default_ns_name.is_some() || !properties.ns_decls.is_empty(); 181 | if has_namespace_decl && properties.should_serialize_as_text { 182 | // There's no meaningful way to namespace text content, so the 183 | // combination of these properties is almost certainly a mistake. 184 | errors.push(Error::new( 185 | attr.span(), 186 | "cannot declare namespaces for text content", 187 | )); 188 | } 189 | 190 | if properties.ns_prefix_for_variants.is_some() && properties.should_serialize_as_text { 191 | // Namespace prefixes are added as part of an element name and so 192 | // cannot be applied to values which will be serialized as a text 193 | // node. 194 | errors.push(Error::new( 195 | attr.span(), 196 | "cannot declare variant namespace prefix for text enum", 197 | )); 198 | } 199 | 200 | // Combine and return errors if there are any. If none, we've 201 | // successfully parsed the attributes and can return the appropriate 202 | // props. 203 | match errors.into_iter().reduce(|mut combined, err| { 204 | combined.combine(err); 205 | 206 | combined 207 | }) { 208 | Some(err) => Err(err), 209 | None => Ok(properties), 210 | } 211 | } 212 | } 213 | 214 | #[derive(Debug)] 215 | /// A declaration of an XML namespace for a type with a derived `XmlSerialize` 216 | /// implementation. 217 | pub(crate) struct NamespaceDecl { 218 | pub prefix: TokenStream, 219 | pub name: TokenStream, 220 | } 221 | 222 | #[derive(Debug, Default)] 223 | /// Properties governing the serialization of a field in a struct or enum with a 224 | /// derived `XmlSerialize` implementation. 225 | pub(crate) struct FieldProps { 226 | /// The type of XML structure which the field represents. 227 | pub repr: FieldRepr, 228 | 229 | /// Whether the field should be serialized with a "flat" representation. 230 | /// 231 | /// A flattened field will be serialized only as its content nodes, rather 232 | /// than as an XML element containing those content nodes. 233 | pub should_flatten: bool, 234 | 235 | /// A prefix to add to this field's name when serialized as an element or 236 | /// attribute. 237 | pub namespace_prefix: Option, 238 | } 239 | 240 | impl FieldProps { 241 | /// Constructs a set of serialization properties for an enum or struct field 242 | /// from its struct attributes. 243 | pub(crate) fn try_from_attrs( 244 | value: Vec, 245 | field_has_name: bool, 246 | ) -> Result { 247 | // Find the attribute for configuring behavior of the derivation, if 248 | // any. 249 | let attr = match find_configuration_attribute(&value) { 250 | Some(attr) => attr, 251 | 252 | // If we don't find a matching attribute, we assume the default set 253 | // of properties. 254 | None => return Ok(Self::default()), 255 | }; 256 | 257 | // We build a list of errors so that we can combine them later and emit 258 | // them all instead of only emitting the first. 259 | let mut errors = Vec::new(); 260 | 261 | // We start with the default set of properties, then parse the 262 | // `xml_struct` attribute to modify any property which deviates from the 263 | // default. 264 | let mut properties = FieldProps::default(); 265 | for meta in attr.parse_args_with(Punctuated::::parse_terminated)? { 266 | match meta { 267 | Meta::Path(path) => { 268 | if path.is_ident("attribute") { 269 | // The name of the field is used as the XML attribute 270 | // name, so unnamed fields (e.g., members of tuple 271 | // structs) cannot be represented as attributes. 272 | if field_has_name { 273 | properties.repr = FieldRepr::Attribute; 274 | } else { 275 | errors.push(Error::new( 276 | path.span(), 277 | "cannot serialize unnamed field as XML attribute", 278 | )) 279 | } 280 | } else if path.is_ident("element") { 281 | properties.repr = FieldRepr::Element; 282 | } else if path.is_ident("flatten") { 283 | properties.should_flatten = true; 284 | } else { 285 | errors.push(Error::new(path.span(), UNRECOGNIZED_ATTRIBUTE_MSG)); 286 | } 287 | } 288 | Meta::NameValue(name_value) => { 289 | if name_value.path.is_ident("ns_prefix") { 290 | match properties.namespace_prefix { 291 | Some(_) => errors.push(Error::new( 292 | name_value.span(), 293 | "cannot declare more than one namespace prefix", 294 | )), 295 | None => { 296 | properties.namespace_prefix = 297 | Some(name_value.value.to_token_stream()); 298 | } 299 | } 300 | } else { 301 | errors.push(Error::new(name_value.span(), UNRECOGNIZED_ATTRIBUTE_MSG)); 302 | } 303 | } 304 | 305 | _ => { 306 | errors.push(Error::new(meta.span(), UNRECOGNIZED_ATTRIBUTE_MSG)); 307 | } 308 | } 309 | } 310 | 311 | if matches!(properties.repr, FieldRepr::Attribute) && properties.should_flatten { 312 | errors.push(Error::new(attr.span(), "cannot flatten attribute fields")); 313 | } 314 | 315 | // Combine and return errors if there are any. If none, we've 316 | // successfully parsed the attributes and can return the appropriate 317 | // props. 318 | match errors.into_iter().reduce(|mut combined, err| { 319 | combined.combine(err); 320 | 321 | combined 322 | }) { 323 | Some(err) => Err(err), 324 | None => Ok(properties), 325 | } 326 | } 327 | } 328 | 329 | #[derive(Clone, Copy, Debug, Default)] 330 | /// The types of XML structure which fields may represent. 331 | pub(crate) enum FieldRepr { 332 | Attribute, 333 | 334 | #[default] 335 | Element, 336 | } 337 | 338 | /// Gets the attribute containing configuration parameters for this derive 339 | /// macro, if any. 340 | fn find_configuration_attribute(attrs: &[Attribute]) -> Option<&Attribute> { 341 | attrs 342 | .iter() 343 | .find(|attr| attr.path().is_ident(MACRO_ATTRIBUTE)) 344 | } 345 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /xml_struct_derive/src/serialize/codegen.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use proc_macro2::{Ident, Literal, TokenStream}; 6 | use quote::{quote, ToTokens}; 7 | use syn::Generics; 8 | 9 | use crate::{FieldProps, FieldRepr, TypeProps}; 10 | 11 | /// Generates an implementation of the `XmlSerialize` trait and, if appropriate, 12 | /// the `XmlSerializeAttr` trait. 13 | pub(super) fn generate_serialize_impl_for( 14 | type_ident: Ident, 15 | generics: Generics, 16 | props: TypeProps, 17 | body_generator: G, 18 | ) -> TokenStream 19 | where 20 | G: FnOnce(&[XmlAttribute]) -> ImplTokenSets, 21 | { 22 | let default_ns_attr = props.default_ns_name.map(|ns_name| XmlAttribute { 23 | // The terminology is a little confusing here. In terms of the XML 24 | // spec, the "name" of a namespace is the (usually) URI used as the 25 | // _value_ of the namespace declaration attribute. 26 | name: Literal::string("xmlns").into_token_stream(), 27 | value: ns_name, 28 | }); 29 | 30 | let ns_decl_attrs = props.ns_decls.into_iter().map(|ns_decl| XmlAttribute { 31 | name: generate_static_string_concat("xmlns:", ns_decl.prefix), 32 | value: ns_decl.name, 33 | }); 34 | 35 | let namespace_attrs: Vec<_> = default_ns_attr.into_iter().chain(ns_decl_attrs).collect(); 36 | 37 | let ImplTokenSets { 38 | as_element_impl, 39 | child_nodes_body, 40 | as_attr_body, 41 | } = body_generator(&namespace_attrs); 42 | 43 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 44 | 45 | let attr_impl = if let Some(body) = as_attr_body { 46 | quote! { 47 | #[automatically_derived] 48 | impl #impl_generics ::xml_struct::XmlSerializeAttr for #type_ident #ty_generics #where_clause { 49 | fn serialize_as_attribute(&self, start_tag: &mut ::quick_xml::events::BytesStart, name: &str) { 50 | #body 51 | } 52 | } 53 | } 54 | } else { 55 | // In cases where there is no clear text representation of a value, we 56 | // provide no derivation of `XmlSerializeAttr`. 57 | TokenStream::default() 58 | }; 59 | 60 | // Construct the final implementation from the type-specific sets of tokens. 61 | quote! { 62 | #[automatically_derived] 63 | impl #impl_generics ::xml_struct::XmlSerialize for #type_ident #ty_generics #where_clause { 64 | #as_element_impl 65 | 66 | fn serialize_child_nodes( 67 | &self, 68 | writer: &mut ::quick_xml::writer::Writer 69 | ) -> Result<(), ::xml_struct::Error> { 70 | #child_nodes_body 71 | 72 | Ok(()) 73 | } 74 | } 75 | 76 | #attr_impl 77 | } 78 | } 79 | 80 | /// The sets of tokens which make up the implementations or bodies of 81 | /// `XmlSerialize` and `XmlSerializeAttr` trait methods. 82 | pub(super) struct ImplTokenSets { 83 | /// The implementation of `XmlSerialize::serialize_as_element()` if it is 84 | /// necessary to override the provided default implementation. 85 | as_element_impl: TokenStream, 86 | 87 | /// The body of `XmlSerialize::serialize_child_nodes()`. 88 | child_nodes_body: TokenStream, 89 | 90 | /// The body of `XmlSerializeAttr::serialize_as_attribute()` if the type is 91 | /// capable of being serialized as such. 92 | as_attr_body: Option, 93 | } 94 | 95 | /// Creates a generator for the sets of tokens necessary to serialize a struct 96 | /// with the provided fields. 97 | pub(super) fn with_struct_fields( 98 | fields: Vec, 99 | ) -> impl FnOnce(&[XmlAttribute]) -> ImplTokenSets { 100 | move |namespace_attrs| { 101 | let Fields { 102 | attr_fields, 103 | child_fields, 104 | } = partition_fields(fields); 105 | 106 | let content_call = if !child_fields.is_empty() { 107 | Some(quote! { 108 | ::serialize_child_nodes(self, writer)?; 109 | }) 110 | } else { 111 | None 112 | }; 113 | 114 | let impl_body = 115 | generate_xml_tag_calls(quote!(name), namespace_attrs, &attr_fields, content_call); 116 | 117 | ImplTokenSets { 118 | as_element_impl: quote! { 119 | fn serialize_as_element( 120 | &self, 121 | writer: &mut ::quick_xml::writer::Writer, 122 | name: &str, 123 | ) -> Result<(), ::xml_struct::Error> { 124 | #impl_body 125 | 126 | Ok(()) 127 | } 128 | }, 129 | child_nodes_body: generate_field_content_node_calls(child_fields), 130 | 131 | // There is no clear text representation of an arbitrary struct, so 132 | // we cannot provide an `XmlSerializeAttr` derivation. 133 | as_attr_body: None, 134 | } 135 | } 136 | } 137 | 138 | /// Creates a generator for the sets of tokens necessary to serialize a 139 | /// unit-only enum as text nodes or attribute values. 140 | pub(super) fn with_text_variants( 141 | variants: Vec, 142 | ) -> impl FnOnce(&[XmlAttribute]) -> ImplTokenSets { 143 | // While the generator function takes namespace attributes as its argument, 144 | // we expect that the consuming code has already verified that there are 145 | // none for this enum, since attributes cannot be specified for text content 146 | // nodes. 147 | move |_| { 148 | let match_arms: Vec<_> = variants 149 | .iter() 150 | .map(|variant| quote!(Self::#variant => stringify!(#variant))) 151 | .collect(); 152 | 153 | let text_from_value = quote! { 154 | let text = match self { 155 | #(#match_arms,)* 156 | }; 157 | }; 158 | 159 | ImplTokenSets { 160 | // No namespaces can be declared on enums which are serialized as 161 | // text, nor can they contain any attribute fields, so the default 162 | // implementation of `serialize_as_element()` is sufficient. 163 | as_element_impl: TokenStream::default(), 164 | child_nodes_body: quote! { 165 | #text_from_value 166 | 167 | writer.write_event( 168 | ::quick_xml::events::Event::Text( 169 | ::quick_xml::events::BytesText::new(text) 170 | ) 171 | )?; 172 | }, 173 | as_attr_body: Some(quote! { 174 | #text_from_value 175 | 176 | // `start_tag` is one of the parameters to the 177 | // `serialize_as_attribute()` method. 178 | start_tag.push_attribute((name, text)); 179 | }), 180 | } 181 | } 182 | } 183 | 184 | /// Creates a generator for the sets of tokens necessary to serialize an enum 185 | /// with the provided variants. 186 | pub(super) fn with_enum_variants( 187 | variants: Vec, 188 | ns_prefix: Option, 189 | ) -> impl FnOnce(&[XmlAttribute]) -> ImplTokenSets { 190 | move |namespace_attrs| { 191 | let match_arms: TokenStream = variants 192 | .into_iter() 193 | .map(|variant| { 194 | let ident = variant.ident; 195 | 196 | let name_tokens = { 197 | // If the consumer has specified that variants should be 198 | // serialized with a namespace prefix, we need to statically 199 | // concatenate the prefix with the variant name. Otherwise, 200 | // we just need to stringify the variant name. 201 | if let Some(prefix) = &ns_prefix { 202 | let ident_as_str = ident.to_string(); 203 | let ident_as_str_tokens = format!(":{ident_as_str}"); 204 | generate_static_string_concat(prefix, ident_as_str_tokens) 205 | } else { 206 | quote!(stringify!(#ident)) 207 | } 208 | }; 209 | 210 | match variant.kind { 211 | VariantKind::Struct(fields) => { 212 | let VariantTokenSets { 213 | accessors, 214 | body: content_calls, 215 | } = generate_variant_token_sets(name_tokens, namespace_attrs, fields); 216 | 217 | quote! { 218 | Self::#ident { #(#accessors),* } => { 219 | #content_calls 220 | } 221 | } 222 | } 223 | VariantKind::Tuple(fields) => { 224 | let VariantTokenSets { 225 | accessors, 226 | body: content_calls, 227 | } = generate_variant_token_sets(name_tokens, namespace_attrs, fields); 228 | 229 | quote! { 230 | Self::#ident(#(#accessors),*) => { 231 | #content_calls 232 | } 233 | } 234 | } 235 | VariantKind::Unit => { 236 | let content_calls = 237 | generate_xml_tag_calls(name_tokens, namespace_attrs, &[], None); 238 | 239 | quote! { 240 | Self::#ident => { 241 | #content_calls 242 | } 243 | } 244 | } 245 | } 246 | }) 247 | .collect(); 248 | 249 | ImplTokenSets { 250 | // No namespaces can be declared directly on the element enclosing 251 | // an enum value, nor can it be provided with attribute fields, so 252 | // the default `serialize_as_element()` implementation is 253 | // sufficient. 254 | as_element_impl: TokenStream::default(), 255 | 256 | child_nodes_body: quote! { 257 | match self { 258 | #match_arms 259 | } 260 | }, 261 | 262 | // There is no clear text representation of an arbitrary enum 263 | // variant, so we cannot provide an `XmlSerializeAttr` derivation. 264 | as_attr_body: None, 265 | } 266 | } 267 | } 268 | 269 | /// The common sets of tokens which make up a `match` arm for an enum variant. 270 | struct VariantTokenSets { 271 | /// The identifiers used for accessing the fields of an enum variant. 272 | accessors: Vec, 273 | 274 | /// The calls for serializing the child nodes of the XML element 275 | /// representing an enum variant. 276 | body: TokenStream, 277 | } 278 | 279 | /// Generates a list of accessors and set of calls to serialize content for an 280 | /// enum variant. 281 | fn generate_variant_token_sets( 282 | name_tokens: TokenStream, 283 | namespace_attrs: &[XmlAttribute], 284 | fields: Vec, 285 | ) -> VariantTokenSets { 286 | let accessors: Vec<_> = fields 287 | .iter() 288 | .map(|field| &field.accessor) 289 | .cloned() 290 | .collect(); 291 | 292 | let Fields { 293 | attr_fields, 294 | child_fields, 295 | } = partition_fields(fields); 296 | 297 | let content_calls = if !child_fields.is_empty() { 298 | Some(generate_field_content_node_calls(child_fields)) 299 | } else { 300 | None 301 | }; 302 | 303 | let variant_body = 304 | generate_xml_tag_calls(name_tokens, namespace_attrs, &attr_fields, content_calls); 305 | 306 | VariantTokenSets { 307 | accessors, 308 | body: variant_body, 309 | } 310 | } 311 | 312 | /// Divides the fields of a struct or enum variant into those which will be 313 | /// represented as attributes and those which will be represented as child nodes. 314 | fn partition_fields(fields: Vec) -> Fields { 315 | let (attr_fields, child_fields) = fields 316 | .into_iter() 317 | .partition(|field| matches!(field.props.repr, FieldRepr::Attribute)); 318 | 319 | Fields { 320 | attr_fields, 321 | child_fields, 322 | } 323 | } 324 | 325 | /// Generates tokens representing a call to add namespace attributes to an 326 | /// element. 327 | fn generate_namespace_attrs_call(namespace_attrs: &[XmlAttribute]) -> TokenStream { 328 | if !namespace_attrs.is_empty() { 329 | let namespace_attrs: Vec<_> = namespace_attrs 330 | .iter() 331 | .map(|XmlAttribute { name, value }| quote!((#name, #value))) 332 | .collect(); 333 | 334 | quote! { 335 | .with_attributes([ 336 | #(#namespace_attrs,)* 337 | ]) 338 | } 339 | } else { 340 | TokenStream::default() 341 | } 342 | } 343 | 344 | /// Generates calls to serialize struct or enum fields as XML attributes. 345 | fn generate_attribute_field_calls(attr_fields: &[Field]) -> TokenStream { 346 | if !attr_fields.is_empty() { 347 | attr_fields 348 | .iter() 349 | .map(|field| { 350 | let name = field_name_to_string_tokens(field); 351 | let accessor = &field.accessor; 352 | let ty = &field.ty; 353 | 354 | quote! { 355 | <#ty as ::xml_struct::XmlSerializeAttr>::serialize_as_attribute(&#accessor, &mut start_tag, #name); 356 | } 357 | }) 358 | .collect() 359 | } else { 360 | TokenStream::default() 361 | } 362 | } 363 | 364 | /// Generates calls to add a new XML element to a document, including any 365 | /// necessary attributes and content nodes. 366 | /// 367 | /// If `content_calls` is `None`, the XML element will be an empty tag (e.g., 368 | /// ""). Otherwise, the XML element will enclose any content added to 369 | /// the writer by those calls. 370 | fn generate_xml_tag_calls( 371 | name_tokens: TokenStream, 372 | namespace_attrs: &[XmlAttribute], 373 | attr_fields: &[Field], 374 | content_calls: Option, 375 | ) -> TokenStream { 376 | let namespaces_call = generate_namespace_attrs_call(namespace_attrs); 377 | let attr_calls = generate_attribute_field_calls(attr_fields); 378 | 379 | let calls = if let Some(content_calls) = content_calls { 380 | // If the type has fields to serialize as child elements, wrap them 381 | // first in an appropriate parent element. 382 | quote! { 383 | writer.write_event( 384 | ::quick_xml::events::Event::Start(start_tag) 385 | )?; 386 | 387 | #content_calls 388 | 389 | writer.write_event( 390 | ::quick_xml::events::Event::End( 391 | ::quick_xml::events::BytesEnd::new(#name_tokens) 392 | ) 393 | )?; 394 | } 395 | } else { 396 | // If the type has no fields which are to be serialized as child 397 | // elements, write an empty XML tag. 398 | quote! { 399 | writer.write_event( 400 | ::quick_xml::events::Event::Empty(start_tag) 401 | )?; 402 | } 403 | }; 404 | 405 | quote! { 406 | let mut start_tag = ::quick_xml::events::BytesStart::new(#name_tokens) 407 | #namespaces_call; 408 | 409 | #attr_calls 410 | 411 | #calls 412 | } 413 | } 414 | 415 | /// Generates calls to serialize the given fields as XML content nodes. 416 | fn generate_field_content_node_calls(child_fields: Vec) -> TokenStream { 417 | child_fields 418 | .into_iter() 419 | .map(|field| { 420 | if matches!(field.props.repr, FieldRepr::Attribute) { 421 | panic!("attribute field passed to child node call generator"); 422 | } 423 | 424 | let ty = &field.ty; 425 | let accessor = &field.accessor; 426 | 427 | match field.kind { 428 | FieldKind::Named(_) if !field.props.should_flatten => { 429 | let child_name = field_name_to_string_tokens(&field); 430 | 431 | quote! { 432 | <#ty as ::xml_struct::XmlSerialize>::serialize_as_element(&#accessor, writer, #child_name)?; 433 | } 434 | } 435 | 436 | // If this is a tuple struct or the consumer has specifically 437 | // requested a flat representation, serialize without a 438 | // containing element. 439 | _ => { 440 | quote! { 441 | <#ty as ::xml_struct::XmlSerialize>::serialize_child_nodes(&#accessor, writer)?; 442 | } 443 | } 444 | } 445 | }) 446 | .collect() 447 | } 448 | 449 | /// Converts the name of a field to a string suitable for use as a tag name. 450 | /// 451 | /// The identifier is stringified and converted to the desired case system. It 452 | /// will also generate code for concatenating the field name with any namespace 453 | /// prefix to be added. 454 | fn field_name_to_string_tokens(field: &Field) -> TokenStream { 455 | match &field.kind { 456 | FieldKind::Named(ident) => { 457 | let name = ident.to_string(); 458 | 459 | let case_mapped = kebab_to_pascal(&name); 460 | 461 | if let Some(prefix) = &field.props.namespace_prefix { 462 | let string_with_colon = format!(":{case_mapped}"); 463 | generate_static_string_concat(prefix, Literal::string(&string_with_colon)) 464 | } else { 465 | Literal::string(&case_mapped).into_token_stream() 466 | } 467 | } 468 | 469 | FieldKind::Unnamed => panic!("cannot stringify unnamed field"), 470 | } 471 | } 472 | 473 | /// Converts a kebab_case identifier string to PascalCase. 474 | fn kebab_to_pascal(kebab: &str) -> String { 475 | let mut capitalize_next = true; 476 | 477 | kebab 478 | .chars() 479 | .filter_map(|character| { 480 | if character == '_' { 481 | // Consume the underscore and capitalize the next character. 482 | capitalize_next = true; 483 | 484 | None 485 | } else if capitalize_next { 486 | capitalize_next = false; 487 | 488 | // Rust supports non-ASCII identifiers, so this could 489 | // technically fail, but this macro does not currently handle 490 | // the general XML case, and so full Unicode case mapping is out 491 | // of scope at present. 492 | Some(character.to_ascii_uppercase()) 493 | } else { 494 | Some(character) 495 | } 496 | }) 497 | .collect() 498 | } 499 | 500 | #[derive(Debug)] 501 | /// A representation of an enum variant. 502 | pub(crate) struct Variant { 503 | // The identifier for the variant. 504 | pub ident: Ident, 505 | 506 | // The form of the variant, along with any fields. 507 | pub kind: VariantKind, 508 | } 509 | 510 | #[derive(Debug)] 511 | /// The form of an enum variant and its contained fields. 512 | pub(crate) enum VariantKind { 513 | Struct(Vec), 514 | Tuple(Vec), 515 | Unit, 516 | } 517 | 518 | #[derive(Debug)] 519 | /// A representation of a struct or enum field. 520 | pub(crate) struct Field { 521 | // The form of the field, along with any identifier. 522 | pub kind: FieldKind, 523 | 524 | // The type of the field. 525 | pub ty: TokenStream, 526 | 527 | // An expression which will access the value of the field. 528 | pub accessor: TokenStream, 529 | 530 | // Properties affecting the serialization of the field. 531 | pub props: FieldProps, 532 | } 533 | 534 | #[derive(Debug)] 535 | /// A container for partitioned attribute and child element fields. 536 | struct Fields { 537 | attr_fields: Vec, 538 | child_fields: Vec, 539 | } 540 | 541 | #[derive(Debug)] 542 | /// The form of a field, whether named or unnamed. 543 | pub(crate) enum FieldKind { 544 | Named(Ident), 545 | Unnamed, 546 | } 547 | 548 | /// Tokens representing an XML attribute's name/value pair. 549 | pub(crate) struct XmlAttribute { 550 | name: TokenStream, 551 | value: TokenStream, 552 | } 553 | 554 | /// Generates code for concatenating strings at compile-time. 555 | /// 556 | /// This code allows for concatenating `const` string references and/or string 557 | /// literals with zero runtime cost. 558 | fn generate_static_string_concat(a: T, b: U) -> TokenStream 559 | where 560 | T: ToTokens, 561 | U: ToTokens, 562 | { 563 | quote!({ 564 | const LEN: usize = #a.len() + #b.len(); 565 | 566 | const fn copy_bytes_into(input: &[u8], mut output: [u8; LEN], offset: usize) -> [u8; LEN] { 567 | // Copy the input byte-by-byte into the output buffer at the 568 | // specified offset. 569 | // NOTE: If/when `const_for` is stabilized, this can become a `for` 570 | // loop. https://github.com/rust-lang/rust/issues/87575 571 | let mut index = 0; 572 | loop { 573 | output[offset + index] = input[index]; 574 | index += 1; 575 | if index == input.len() { 576 | break; 577 | } 578 | } 579 | 580 | // We must return the buffer, as `const` functions cannot take a 581 | // mutable reference, so it's moved into and out of scope. 582 | output 583 | } 584 | 585 | const fn constcat(prefix: &'static str, value: &'static str) -> [u8; LEN] { 586 | let mut output = [0u8; LEN]; 587 | output = copy_bytes_into(prefix.as_bytes(), output, 0); 588 | output = copy_bytes_into(value.as_bytes(), output, prefix.len()); 589 | 590 | output 591 | } 592 | 593 | // As of writing this comment, Rust does not provide a standard macro 594 | // for compile-time string concatenation, so we exploit the fact that 595 | // `str::as_bytes()` and `std::str::from_utf8()` are `const`. 596 | const BYTES: [u8; LEN] = constcat(#a, #b); 597 | match std::str::from_utf8(&BYTES) { 598 | Ok(value) => value, 599 | 600 | // Given that both inputs to `constcat()` are Rust strings, they're 601 | // guaranteed to be valid UTF-8. As such, directly concatenating 602 | // them should create valid UTF-8 as well. If we hit this panic, 603 | // it's probably a bug in one of the above functions. 604 | Err(_) => panic!("Unable to statically concatenate strings"), 605 | } 606 | }) 607 | } 608 | --------------------------------------------------------------------------------