diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..68fe2d0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,221 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "basic-toml" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2db21524cad41c5591204d22d75e1970a2d1f71060214ca931dc7d5afe2c14e5" +dependencies = [ + "serde", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "proc-macro2" +version = "1.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "serde" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "trybuild" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a9d3ba662913483d6722303f619e75ea10b7855b0f8e0d72799cf8621bb488f" +dependencies = [ + "basic-toml", + "glob", + "once_cell", + "serde", + "serde_derive", + "serde_json", + "termcolor", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "xml_struct" +version = "0.1.0" +dependencies = [ + "quick-xml", + "thiserror", + "xml_struct_derive", + "xml_struct_tests", +] + +[[package]] +name = "xml_struct_derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "xml_struct_tests" +version = "0.1.0" +dependencies = [ + "quick-xml", + "thiserror", + "trybuild", + "xml_struct", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2a456a7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[workspace] +resolver = "2" +members = [ + "xml_struct", + "xml_struct_derive", + "xml_struct_tests" +] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d0a1fa1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2914fa3 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# `xml_struct` + +The `xml_struct` crate is intended to provide simple, flexible, low-boilerplate +serialization of Rust data structures to XML. + +## Limitations + +In its current iteration, this project makes several behavioral assumptions +which make it unsuitable for general use. Primary among these are that +transformation of field/structure names to XML tag names is not configurable +(all names are transformed to PascalCase) and whether fields are serialized as +XML elements or attributes by default is not configurable. + +Deserialization is likewise not supported at this time. + +Due to the listed limitations, `xml_struct` is not currently published to +crates.io and no support is offered at this time. These limitations may be +addressed at a later time if there is general interest in this crate or if +workload allows. + +For general-purpose XML serialization or deserialization, one of these crates +may better suit your needs at this time: + +- [`xmlserde`](https://github.com/imjeremyhe/xmlserde) +- [`yaserde`](https://github.com/media-io/yaserde) diff --git a/xml_struct/Cargo.toml b/xml_struct/Cargo.toml new file mode 100644 index 0000000..62f573f --- /dev/null +++ b/xml_struct/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "xml_struct" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +quick-xml = "0.31.0" +thiserror = "1.0.56" +xml_struct_derive = { version = "0.1.0", path = "../xml_struct_derive" } + +[dev-dependencies] +xml_struct_tests = { path = "../xml_struct_tests" } diff --git a/xml_struct/src/impls.rs b/xml_struct/src/impls.rs new file mode 100644 index 0000000..77d3ff5 --- /dev/null +++ b/xml_struct/src/impls.rs @@ -0,0 +1,200 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! This module provides implementations of serialization for common types from +//! the standard library. + +use quick_xml::{ + events::{BytesText, Event}, + Writer, +}; + +use crate::{Error, XmlSerialize, XmlSerializeAttr}; + +/// Serializes a string as a text content node. +impl XmlSerialize for str { + fn serialize_child_nodes(&self, writer: &mut Writer) -> Result<(), Error> + where + W: std::io::Write, + { + writer.write_event(Event::Text(BytesText::new(self)))?; + + Ok(()) + } +} + +/// Serializes a reference to a string as a text content node. +impl XmlSerialize for &T +where + T: AsRef, +{ + fn serialize_child_nodes(&self, writer: &mut Writer) -> Result<(), Error> + where + W: std::io::Write, + { + writer.write_event(Event::Text(BytesText::new(self.as_ref())))?; + + Ok(()) + } +} + +/// Serializes a string as a text content node. +impl XmlSerialize for String { + fn serialize_child_nodes(&self, writer: &mut Writer) -> Result<(), Error> + where + W: std::io::Write, + { + writer.write_event(Event::Text(BytesText::new(self.as_str())))?; + + Ok(()) + } +} + +/// Serializes a string as a text content node. +impl XmlSerialize for &str { + fn serialize_child_nodes(&self, writer: &mut Writer) -> Result<(), Error> + where + W: std::io::Write, + { + writer.write_event(Event::Text(BytesText::new(self)))?; + + Ok(()) + } +} + +/// Serializes the contents of an `Option` as content nodes. +/// +/// `Some(t)` is serialized identically to `t`, while `None` produces no output. +impl XmlSerialize for Option +where + T: XmlSerialize, +{ + fn serialize_as_element(&self, writer: &mut Writer, name: &str) -> Result<(), Error> + where + W: std::io::Write, + { + match self { + Some(value) => ::serialize_as_element(value, writer, name), + None => Ok(()), + } + } + + fn serialize_child_nodes(&self, writer: &mut Writer) -> Result<(), Error> + where + W: std::io::Write, + { + match self { + Some(value) => ::serialize_child_nodes(value, writer), + None => Ok(()), + } + } +} + +/// Serializes the contents of a `Vec` as content nodes. +/// +/// Each element of the `Vec` is serialized via its `serialize_child_nodes()` +/// implementation. If the `Vec` is empty, no output is produced. +impl XmlSerialize for Vec +where + T: XmlSerialize, +{ + fn serialize_child_nodes(&self, writer: &mut Writer) -> Result<(), Error> + where + W: std::io::Write, + { + if self.is_empty() { + return Ok(()); + } + + for value in self { + ::serialize_child_nodes(value, writer)?; + } + + Ok(()) + } +} + +/// Serializes a string as an XML attribute value. +impl XmlSerializeAttr for str { + fn serialize_as_attribute(&self, start_tag: &mut quick_xml::events::BytesStart, name: &str) { + start_tag.push_attribute((name, self)); + } +} + +/// Serializes a reference to a string as an XML attribute value. +impl XmlSerializeAttr for &T +where + T: AsRef, +{ + fn serialize_as_attribute(&self, start_tag: &mut quick_xml::events::BytesStart, name: &str) { + start_tag.push_attribute((name, self.as_ref())); + } +} + +/// Serializes a string as an XML attribute value. +impl XmlSerializeAttr for String { + fn serialize_as_attribute(&self, start_tag: &mut quick_xml::events::BytesStart, name: &str) { + start_tag.push_attribute((name, self.as_str())); + } +} + +/// Serializes a string as an XML attribute value. +impl XmlSerializeAttr for &str { + fn serialize_as_attribute(&self, start_tag: &mut quick_xml::events::BytesStart, name: &str) { + start_tag.push_attribute((name, *self)); + } +} + +/// Serializes the contents of an `Option` as an XML attribute value. +/// +/// `Some(t)` is serialized identically to `t`, while `None` produces no output. +impl XmlSerializeAttr for Option +where + T: XmlSerializeAttr, +{ + fn serialize_as_attribute(&self, start_tag: &mut quick_xml::events::BytesStart, name: &str) { + match self { + Some(value) => value.serialize_as_attribute(start_tag, name), + None => (), + } + } +} + +/// Implements serialization of a type as either an XML text node or attribute +/// value. +/// +/// This is a convenience macro intended for implementing basic serialization of +/// primitive/standard library types. This is done per-type rather than +/// wholesale for `ToString` in order to avoid requiring that `Display` and +/// `XmlSerialize`/`XmlSerializeAttr` share a form. +macro_rules! impl_as_text_for { + ($( $ty:ty ),*) => { + $( + /// Serializes an integer as a text content node. + impl XmlSerialize for $ty { + fn serialize_child_nodes(&self, writer: &mut Writer) -> Result<(), Error> + where + W: std::io::Write, + { + let string = self.to_string(); + writer.write_event(Event::Text(BytesText::new(&string)))?; + + Ok(()) + } + } + + /// Serializes an integer as an XML attribute value. + impl XmlSerializeAttr for $ty { + fn serialize_as_attribute( + &self, + start_tag: &mut quick_xml::events::BytesStart, + name: &str, + ) { + start_tag.push_attribute((name, self.to_string().as_str())); + } + })* + }; +} + +impl_as_text_for!(i8, u8, i16, u16, i32, u32, i64, u64); diff --git a/xml_struct/src/lib.rs b/xml_struct/src/lib.rs new file mode 100644 index 0000000..20b6e1b --- /dev/null +++ b/xml_struct/src/lib.rs @@ -0,0 +1,137 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! This crate provides a mechanism for serializing Rust data structures as +//! well-formed XML with a minimum of boilerplate. +//! +//! Consumers can provide manual implementations of the [`XmlSerialize`] and +//! [`XmlSerializeAttr`] traits if desired, but the primary intent of this crate +//! is to provide automated derivation of these traits in order to facilitate +//! serialization of complex XML structures. +//! +//! # Limitations +//! +//! At present, derived implementations of these traits are designed to handle +//! the specific case of Microsoft Exchange Web Services. As such, all XML +//! elements and attributes are named in PascalCase and certain behaviors are +//! not supported (such as serializing enum variants without enclosing XML +//! elements derived from the variant name). +//! +//! Furthermore, the PascalCase implementation is naïve and depends on +//! [`char::to_ascii_uppercase`], making it unsuitable for use with non-ASCII +//! identifiers. +//! +//! There is also currently no provision for deserialization from XML, as the +//! support offered by `quick_xml`'s serde implementation has been found to be +//! sufficient for the time being. +//! +//! In recognition of these limitations, this crate should not be published to +//! crates.io at this time. If a generalized implementation generates interest +//! or is thought to have merit, these limitations may be addressed at a later +//! time. + +mod impls; +mod tests; + +use quick_xml::{ + events::{BytesEnd, BytesStart, Event}, + Writer, +}; +use thiserror::Error; + +pub use xml_struct_derive::*; + +/// A data structure which can be serialized as XML content nodes. +/// +/// # Usage +/// +/// The following demonstrates end-to-end usage of `XmlSerialize` with both +/// derived and manual implementations. +/// +/// ``` +/// use quick_xml::{ +/// events::{BytesText, Event}, +/// writer::Writer +/// }; +/// use xml_struct::{Error, XmlSerialize}; +/// +/// #[derive(XmlSerialize)] +/// #[xml_struct(default_ns = "http://foo.example/")] +/// struct Foo { +/// some_field: String, +/// +/// #[xml_struct(flatten)] +/// something_else: Bar, +/// } +/// +/// enum Bar { +/// Baz, +/// Qux(String), +/// } +/// +/// impl XmlSerialize for Bar { +/// fn serialize_child_nodes(&self, writer: &mut Writer) -> Result<(), Error> +/// where +/// W: std::io::Write, +/// { +/// match self { +/// Self::Baz => writer.write_event(Event::Text(BytesText::new("BAZ")))?, +/// Self::Qux(qux) => qux.serialize_as_element(writer, "Qux")?, +/// } +/// +/// Ok(()) +/// } +/// } +/// +/// let mut writer: Writer> = Writer::new(Vec::new()); +/// let foo = Foo { +/// some_field: "foo".into(), +/// something_else: Bar::Baz, +/// }; +/// +/// assert!(foo.serialize_as_element(&mut writer, "FlyYouFoo").is_ok()); +/// +/// let out = writer.into_inner(); +/// let out = std::str::from_utf8(&out).unwrap(); +/// +/// assert_eq!( +/// out, +/// r#"fooBAZ"#, +/// ); +/// ``` +pub trait XmlSerialize { + /// Serializes this value as XML content nodes within an enclosing XML + /// element. + fn serialize_as_element(&self, writer: &mut Writer, name: &str) -> Result<(), Error> + where + W: std::io::Write, + { + writer.write_event(Event::Start(BytesStart::new(name)))?; + + self.serialize_child_nodes(writer)?; + + writer.write_event(Event::End(BytesEnd::new(name)))?; + + Ok(()) + } + + /// Serializes this value as XML content nodes. + fn serialize_child_nodes(&self, writer: &mut Writer) -> Result<(), Error> + where + W: std::io::Write; +} + +/// A data structure which can be serialized as the value of an XML attribute. +pub trait XmlSerializeAttr { + /// Serializes this value as the value of an XML attribute. + fn serialize_as_attribute(&self, start_tag: &mut BytesStart, name: &str); +} + +/// An error generated during the XML serialization process. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum Error { + #[error("failed to process XML document")] + Xml(#[from] quick_xml::Error), +} diff --git a/xml_struct/src/tests.rs b/xml_struct/src/tests.rs new file mode 100644 index 0000000..69cf8eb --- /dev/null +++ b/xml_struct/src/tests.rs @@ -0,0 +1,175 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#![cfg(test)] + +use xml_struct_tests::{serialize_value_as_element, serialize_value_children}; + +#[test] +fn string() { + let content = String::from("some arbitrary content"); + let expected = content.clone(); + + let actual = + serialize_value_children(content).expect("Failed to serialize string as text content"); + assert_eq!( + actual, expected, + "Serializing `String` should result in bare text content" + ); +} + +#[test] +fn string_as_element() { + let name = "SomeTag"; + + let content = String::from("some arbitrary content"); + let expected = format!("<{name}>{content}"); + + let actual = serialize_value_as_element(content.clone(), name) + .expect("Failed to serialize string as text content"); + assert_eq!( + actual, expected, + "Serializing `String` should result in element with text content" + ); + + let actual = serialize_value_as_element(&content, name) + .expect("Failed to serialize string as text content"); + assert_eq!( + actual, expected, + "Serializing `&String` should result in element with text content" + ); + + let actual = serialize_value_as_element(content.as_str(), name) + .expect("Failed to serialize string as text content"); + assert_eq!( + actual, expected, + "Serializing `&str` should result in element with text content" + ); +} + +#[test] +fn int() { + let content: i8 = 17; + let expected = format!("{content}"); + + let actual = + serialize_value_children(content).expect("Failed to serialize int as text content"); + assert_eq!( + actual, expected, + "Serializing `i8` should result in bare text content" + ); + + let actual = + serialize_value_children(content as u8).expect("Failed to serialize int as text content"); + assert_eq!( + actual, expected, + "Serializing `u8` should result in bare text content" + ); + + let actual = + serialize_value_children(content as i16).expect("Failed to serialize int as text content"); + assert_eq!( + actual, expected, + "Serializing `i16` should result in bare text content" + ); + + let actual = + serialize_value_children(content as u16).expect("Failed to serialize int as text content"); + assert_eq!( + actual, expected, + "Serializing `u16` should result in bare text content" + ); + + let actual = + serialize_value_children(content as i32).expect("Failed to serialize int as text content"); + assert_eq!( + actual, expected, + "Serializing `i32` should result in bare text content" + ); + + let actual = + serialize_value_children(content as u32).expect("Failed to serialize int as text content"); + assert_eq!( + actual, expected, + "Serializing `u32` should result in bare text content" + ); + + let actual = + serialize_value_children(content as i64).expect("Failed to serialize int as text content"); + assert_eq!( + actual, expected, + "Serializing `i64` should result in bare text content" + ); + + let actual = + serialize_value_children(content as u64).expect("Failed to serialize int as text content"); + assert_eq!( + actual, expected, + "Serializing `u64` should result in bare text content" + ); +} + +#[test] +fn int_as_element() { + let name = "last_march_of_the_ints"; + + let content: i8 = 17; + let expected = format!("<{name}>{content}"); + + let actual = + serialize_value_as_element(content, name).expect("Failed to serialize int as text content"); + assert_eq!( + actual, expected, + "Serializing `i8` should result in bare text content" + ); + + let actual = serialize_value_as_element(content as u8, name) + .expect("Failed to serialize int as text content"); + assert_eq!( + actual, expected, + "Serializing `u8` should result in bare text content" + ); + + let actual = serialize_value_as_element(content as i16, name) + .expect("Failed to serialize int as text content"); + assert_eq!( + actual, expected, + "Serializing `i16` should result in bare text content" + ); + + let actual = serialize_value_as_element(content as u16, name) + .expect("Failed to serialize int as text content"); + assert_eq!( + actual, expected, + "Serializing `u16` should result in bare text content" + ); + + let actual = serialize_value_as_element(content as i32, name) + .expect("Failed to serialize int as text content"); + assert_eq!( + actual, expected, + "Serializing `i32` should result in bare text content" + ); + + let actual = serialize_value_as_element(content as u32, name) + .expect("Failed to serialize int as text content"); + assert_eq!( + actual, expected, + "Serializing `u32` should result in bare text content" + ); + + let actual = serialize_value_as_element(content as i64, name) + .expect("Failed to serialize int as text content"); + assert_eq!( + actual, expected, + "Serializing `i64` should result in bare text content" + ); + + let actual = serialize_value_as_element(content as u64, name) + .expect("Failed to serialize int as text content"); + assert_eq!( + actual, expected, + "Serializing `u64` should result in bare text content" + ); +} diff --git a/xml_struct_derive/Cargo.toml b/xml_struct_derive/Cargo.toml new file mode 100644 index 0000000..b3e1fd2 --- /dev/null +++ b/xml_struct_derive/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "xml_struct_derive" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.74" +quote = "1.0.35" +syn = { version = "2.0.46", features = ["full"], default-features = false } diff --git a/xml_struct_derive/src/lib.rs b/xml_struct_derive/src/lib.rs new file mode 100644 index 0000000..3f23ed1 --- /dev/null +++ b/xml_struct_derive/src/lib.rs @@ -0,0 +1,225 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +mod properties; +mod serialize; + +use syn::{parse_macro_input, DeriveInput}; + +pub(crate) use properties::*; + +use crate::serialize::{write_serialize_impl_for_enum, write_serialize_impl_for_struct}; + +// This value must match the `attributes` attribute for the derive macro. +const MACRO_ATTRIBUTE: &str = "xml_struct"; + +/// A macro providing automated derivation of the `XmlSerialize` trait. +/// +/// By default, when applied to a struct, the resulting implementation will +/// serialize each of the struct's fields as an XML element with a tag name +/// derived from the name of the field. +/// +/// For example, the following declaration corresponds to the following output: +/// +/// ```ignore +/// #[derive(XmlSerialize)] +/// struct Foo { +/// some_field: SerializeableType, +/// another: String, +/// } +/// +/// let foo = Foo { +/// some_field: SerializeableType { +/// ... +/// }, +/// another: String::from("I am text!"), +/// }; +/// ``` +/// +/// ```text +/// +/// ... +/// +/// +/// I am text! +/// +/// ``` +/// +/// When applied to an enum, the implementation will write an XML element with a +/// tag name derived from the name of the variant. Any fields of the variant +/// will be serialized as children of that element, with tag names derived from +/// the name of the field. +/// +/// As above, the following enum corresponds to the following output: +/// +/// ```ignore +/// #[derive(XmlSerialize)] +/// enum Bar { +/// Foobar { +/// some_field: SerializeableType, +/// another: String, +/// }, +/// ... +/// } +/// +/// let bar = Bar::Foobar { +/// some_field: SerializeableType { +/// ... +/// }, +/// another: String::from("I am text!"), +/// }; +/// ``` +/// +/// ```text +/// +/// +/// ... +/// +/// +/// I am text! +/// +/// +/// ``` +/// +/// Unnamed fields, i.e. fields of tuple structs or enum tuple variants, are +/// serialized without an enclosing element. +/// +/// Enums which consist solely of unit variants will also receive an +/// implementation of the `XmlSerializeAttr` trait. +/// +/// # Configuration +/// +/// The output from derived implementations may be configured with the +/// `xml_struct` attribute. For example, serializing the following as an element +/// named "Baz" corresponds to the following output: +/// +/// ```ignore +/// #[derive(XmlSerialize)] +/// #[xml_struct(default_ns = "http://foo.example/")] +/// struct Baz { +/// #[xml_struct(flatten)] +/// some_field: SerializeableType, +/// +/// #[xml_struct(attribute, ns_prefix = "foo")] +/// another: String, +/// } +/// +/// let foo = Baz { +/// some_field: SerializeableType { +/// ... +/// }, +/// another: String::from("I am text!"), +/// }; +/// ``` +/// +/// ```text +/// +/// ... +/// +/// ``` +/// +/// The following options are available: +/// +/// ## Data Structures +/// +/// These options affect the serialization of a struct or enum as a whole. +/// +/// - `default_ns = "http://foo.example/"` +/// +/// Provides the name to be used as the default namespace of elements +/// representing the marked structure, i.e.: +/// +/// ```text +/// +/// ``` +/// +/// **NOTE**: The namespace will not be specified if values are serialized as +/// content nodes only. +/// +/// - `ns = ("foo", "http://foo.example/")` +/// +/// Declares a namespace to be used for elements representing the marked +/// structure, i.e.: +/// +/// ```text +/// +/// ``` +/// +/// Multiple namespaces may be declared for each structure. +/// +/// **NOTE**: The namespace will not be specified if values are serialized as +/// content nodes only. +/// +/// - `text` +/// +/// Specifies that a marked enum's variants should be serialized as text nodes +/// or as XML attribute values (depending on use in containing structures). +/// +/// **NOTE**: This option is only valid for enums which contain solely unit +/// variants. +/// +/// - `variant_ns_prefix = "foo"` +/// +/// Specifies that a marked enum's variants, when serialized as XML elements, +/// should include a namespace prefix, i.e. +/// +/// ```text +/// +/// ``` +/// +/// **NOTE**: This option is only valid for enums which are not serialized as +/// text nodes. +/// +/// ## Structure Fields +/// +/// These options affect the serialization of a single field in a struct or enum +/// variant. +/// +/// - `attribute` +/// +/// Specifies that the marked field should be serialized as an XML attribute, +/// i.e. `Field="value"`. +/// +/// - `element` +/// +/// Specifies that the marked field should be serialized as an XML element. +/// This is the default behavior, and use of this attribute is optional. +/// +/// - `flatten` +/// +/// Specifies that the marked field should be serialized as content nodes +/// without an enclosing XML element. +/// +/// - `ns_prefix = "foo"` +/// +/// Specifies that the marked field, when serialized as an XML element or +/// attribute, should use include a namespace prefix, i.e. `foo:Field="value"` +/// or +/// +/// ```text +/// +/// ``` +#[proc_macro_derive(XmlSerialize, attributes(xml_struct))] +pub fn derive_xml_serialize(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let props = match TypeProps::try_from_input(&input) { + Ok(props) => props, + Err(err) => return err.into_compile_error().into(), + }; + + let DeriveInput { + generics, ident, .. + } = input; + + match input.data { + syn::Data::Struct(input) => write_serialize_impl_for_struct(ident, generics, input, props), + syn::Data::Enum(input) => write_serialize_impl_for_enum(ident, generics, input, props), + syn::Data::Union(_) => panic!("Serializing unions as XML is unsupported"), + } + // `syn` and `quote` use the `proc_macro2` crate, so internally we deal in + // its `TokenStream`, but derive macros must use `proc_macro`'s, so convert + // at the last minute. + .into() +} diff --git a/xml_struct_derive/src/properties.rs b/xml_struct_derive/src/properties.rs new file mode 100644 index 0000000..ad06314 --- /dev/null +++ b/xml_struct_derive/src/properties.rs @@ -0,0 +1,344 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use proc_macro2::TokenStream; +use quote::ToTokens as _; +use syn::{ + punctuated::Punctuated, spanned::Spanned as _, Attribute, DeriveInput, Error, Expr, Meta, Token, +}; + +use crate::MACRO_ATTRIBUTE; + +const UNRECOGNIZED_ATTRIBUTE_MSG: &str = "unrecognized `xml_struct` attribute"; + +#[derive(Debug, Default)] +/// Properties governing the serialization of a struct or enum with a derived +/// `XmlSerialize` implementation. +pub(crate) struct TypeProps { + /// A declaration of a name for the default XML namespace. + /// + /// The value of this name, if any, will be represented as an `xmlns` + /// attribute on the start tag if a field of this type is serialized as an + /// XML element. + /// + /// Note that, in XML terminology, the "name" is the value of the `xmlns` + /// attribute, usually a URI. + pub default_ns_name: Option, + + /// Declarations of XML namespaces. + /// + /// The values of these declarations, if any, will be represented as + /// `xmlns:{prefix}` attributes on the start tag if a field of this type is + /// serialized as an XML element. + pub ns_decls: Vec, + + /// Whether values of this type should be serialized as text nodes instead + /// of element nodes. + /// + /// A value of `true` is only valid when the type to which it is applied is + /// an `enum` consisting only of unit variants. + pub should_serialize_as_text: bool, + + /// A namespace prefix to apply to tags representing enum variants. + /// + /// This property is invalid for structs or text enums. + pub ns_prefix_for_variants: Option, +} + +impl TypeProps { + /// Constructs a set of serialization properties for an enum or struct from + /// its input to the derive macro. + pub(crate) fn try_from_input(input: &DeriveInput) -> Result { + let attr = match find_configuration_attribute(&input.attrs) { + Some(attr) => attr, + + // If we don't find a matching attribute, we assume the default set + // of properties. + None => return Ok(Self::default()), + }; + + // We build a list of errors so that we can combine them later and emit + // them all instead of quitting at the first we encounter. + let mut errors = Vec::new(); + + // We start with the default set of properties, then parse the + // `xml_struct` attribute to modify any property which deviates from the + // default. + let mut properties = TypeProps::default(); + for meta in attr.parse_args_with(Punctuated::::parse_terminated)? { + match meta { + Meta::Path(path) => { + if path.is_ident("text") { + // The consumer has specified that they want to + // represent values of the type to which this is applied + // as text. This is only possible when the type is an + // enum, for which all variants are unit. When that's + // the case, we use the variant name as the text value. + let is_unit_only_enum = match &input.data { + syn::Data::Enum(input) => input + .variants + .iter() + .all(|variant| matches!(variant.fields, syn::Fields::Unit)), + + _ => false, + }; + + if is_unit_only_enum { + properties.should_serialize_as_text = true; + } else { + // There is no clear representation of non-unit enum + // variants or of structs as text nodes or text + // attributes, so we just forbid it. + errors.push(Error::new( + path.span(), + "only unit enums may be derived as text", + )) + } + } else { + errors.push(Error::new(path.span(), UNRECOGNIZED_ATTRIBUTE_MSG)); + } + } + Meta::NameValue(name_value) => { + if name_value.path.is_ident("default_ns") { + // When serialized as an element, values of the type to + // which this is applied should include a declaration of + // a default namespace, e.g. `xmlns="foo"`. This + // attribute should occur at most once per type. + match properties.default_ns_name { + Some(_) => { + errors.push(Error::new( + name_value.path.span(), + "cannot declare more than one default namespace", + )); + } + + None => { + properties.default_ns_name = + Some(name_value.value.to_token_stream()) + } + } + } else if name_value.path.is_ident("ns") { + // When serialized as an element, values of the type to + // which this is applied should include a declaration of + // a namespace with prefix, e.g. `xmlns:foo="bar"`. + // There can be many of these attributes per type. + // + // Ideally, we could prevent duplicate namespace prefixes here, + // but allowing consumers to pass either by variable or by + // literal makes that exceedingly difficult. + match &name_value.value { + Expr::Tuple(tuple) if tuple.elems.len() == 2 => { + properties.ns_decls.push(NamespaceDecl { + prefix: tuple.elems[0].to_token_stream(), + name: tuple.elems[1].to_token_stream(), + }) + } + + unexpected => errors.push(Error::new( + unexpected.span(), + "namespace value must be a tuple of exactly two elements", + )), + } + } else if name_value.path.is_ident("variant_ns_prefix") { + // When serialized as an element, values of the enum + // type to which this is applied should have a namespace + // prefix added to the element's tag name. + match properties.ns_prefix_for_variants { + Some(_) => { + errors.push(Error::new( + name_value.path.span(), + "cannot declare more than one namespace prefix", + )); + } + None => match &input.data { + syn::Data::Enum(_) => { + properties.ns_prefix_for_variants = + Some(name_value.value.to_token_stream()); + } + + _ => { + errors.push(Error::new( + name_value.path.span(), + "cannot declare variant namespace prefix for non-enum", + )); + } + }, + } + } else { + errors.push(Error::new(name_value.span(), UNRECOGNIZED_ATTRIBUTE_MSG)); + } + } + + _ => { + errors.push(Error::new(meta.span(), UNRECOGNIZED_ATTRIBUTE_MSG)); + } + } + } + + let has_namespace_decl = + properties.default_ns_name.is_some() || !properties.ns_decls.is_empty(); + if has_namespace_decl && properties.should_serialize_as_text { + // There's no meaningful way to namespace text content, so the + // combination of these properties is almost certainly a mistake. + errors.push(Error::new( + attr.span(), + "cannot declare namespaces for text content", + )); + } + + if properties.ns_prefix_for_variants.is_some() && properties.should_serialize_as_text { + // Namespace prefixes are added as part of an element name and so + // cannot be applied to values which will be serialized as a text + // node. + errors.push(Error::new( + attr.span(), + "cannot declare variant namespace prefix for text enum", + )); + } + + // Combine and return errors if there are any. If none, we've + // successfully parsed the attributes and can return the appropriate + // props. + match errors.into_iter().reduce(|mut combined, err| { + combined.combine(err); + + combined + }) { + Some(err) => Err(err), + None => Ok(properties), + } + } +} + +#[derive(Debug)] +/// A declaration of an XML namespace for a type with a derived `XmlSerialize` +/// implementation. +pub(crate) struct NamespaceDecl { + pub prefix: TokenStream, + pub name: TokenStream, +} + +#[derive(Debug, Default)] +/// Properties governing the serialization of a field in a struct or enum with a +/// derived `XmlSerialize` implementation. +pub(crate) struct FieldProps { + /// The type of XML structure which the field represents. + pub repr: FieldRepr, + + /// Whether the field should be serialized with a "flat" representation. + /// + /// A flattened field will be serialized only as its content nodes, rather + /// than as an XML element containing those content nodes. + pub should_flatten: bool, + + /// A prefix to add to this field's name when serialized as an element or + /// attribute. + pub namespace_prefix: Option, +} + +impl FieldProps { + /// Constructs a set of serialization properties for an enum or struct field + /// from its struct attributes. + pub(crate) fn try_from_attrs( + value: Vec, + field_has_name: bool, + ) -> Result { + // Find the attribute for configuring behavior of the derivation, if + // any. + let attr = match find_configuration_attribute(&value) { + Some(attr) => attr, + + // If we don't find a matching attribute, we assume the default set + // of properties. + None => return Ok(Self::default()), + }; + + // We build a list of errors so that we can combine them later and emit + // them all instead of only emitting the first. + let mut errors = Vec::new(); + + // We start with the default set of properties, then parse the + // `xml_struct` attribute to modify any property which deviates from the + // default. + let mut properties = FieldProps::default(); + for meta in attr.parse_args_with(Punctuated::::parse_terminated)? { + match meta { + Meta::Path(path) => { + if path.is_ident("attribute") { + // The name of the field is used as the XML attribute + // name, so unnamed fields (e.g., members of tuple + // structs) cannot be represented as attributes. + if field_has_name { + properties.repr = FieldRepr::Attribute; + } else { + errors.push(Error::new( + path.span(), + "cannot serialize unnamed field as XML attribute", + )) + } + } else if path.is_ident("element") { + properties.repr = FieldRepr::Element; + } else if path.is_ident("flatten") { + properties.should_flatten = true; + } else { + errors.push(Error::new(path.span(), UNRECOGNIZED_ATTRIBUTE_MSG)); + } + } + Meta::NameValue(name_value) => { + if name_value.path.is_ident("ns_prefix") { + match properties.namespace_prefix { + Some(_) => errors.push(Error::new( + name_value.span(), + "cannot declare more than one namespace prefix", + )), + None => { + properties.namespace_prefix = + Some(name_value.value.to_token_stream()); + } + } + } else { + errors.push(Error::new(name_value.span(), UNRECOGNIZED_ATTRIBUTE_MSG)); + } + } + + _ => { + errors.push(Error::new(meta.span(), UNRECOGNIZED_ATTRIBUTE_MSG)); + } + } + } + + if matches!(properties.repr, FieldRepr::Attribute) && properties.should_flatten { + errors.push(Error::new(attr.span(), "cannot flatten attribute fields")); + } + + // Combine and return errors if there are any. If none, we've + // successfully parsed the attributes and can return the appropriate + // props. + match errors.into_iter().reduce(|mut combined, err| { + combined.combine(err); + + combined + }) { + Some(err) => Err(err), + None => Ok(properties), + } + } +} + +#[derive(Clone, Copy, Debug, Default)] +/// The types of XML structure which fields may represent. +pub(crate) enum FieldRepr { + Attribute, + + #[default] + Element, +} + +/// Gets the attribute containing configuration parameters for this derive +/// macro, if any. +fn find_configuration_attribute(attrs: &[Attribute]) -> Option<&Attribute> { + attrs + .iter() + .find(|attr| attr.path().is_ident(MACRO_ATTRIBUTE)) +} diff --git a/xml_struct_derive/src/serialize.rs b/xml_struct_derive/src/serialize.rs new file mode 100644 index 0000000..0ea4580 --- /dev/null +++ b/xml_struct_derive/src/serialize.rs @@ -0,0 +1,238 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +mod codegen; + +use proc_macro2::{Ident, Literal, TokenStream}; +use quote::{format_ident, quote, ToTokens}; +use syn::{DataEnum, DataStruct, Generics}; + +use crate::{FieldProps, TypeProps}; + +use self::codegen::{ + generate_serialize_impl_for, with_enum_variants, with_struct_fields, with_text_variants, Field, + FieldKind, Variant, VariantKind, +}; + +/// Generates an implementation of the `XmlSerialize` trait for a Rust struct +/// and its fields. +pub(crate) fn write_serialize_impl_for_struct( + ident: Ident, + generics: Generics, + input: DataStruct, + props: TypeProps, +) -> TokenStream { + // We build a list of errors so that we can combine them later and emit + // them all instead of quitting at the first we encounter. + let mut errors = Vec::new(); + + // Process the struct's fields in order to determine how to represent them, + // based on struct type and any consumer-applied attributes. + let fields = match input.fields { + // Fields in a regular struct, i.e. declared with a name and type. + syn::Fields::Named(fields) => fields + .named + .into_iter() + .map(process_named_field( + &mut errors, + |ident| quote!(self.#ident), + )) + .collect(), + + // Fields in a tuple struct, i.e. declared by type and position only. + syn::Fields::Unnamed(fields) => fields + .unnamed + .into_iter() + .enumerate() + .map(process_unnamed_field(&mut errors, |idx| { + let idx_literal = Literal::usize_unsuffixed(idx); + quote!(self.#idx_literal) + })) + .collect(), + + // A unit struct, i.e. one which has no fields. + syn::Fields::Unit => vec![], + }; + + // Combine and return errors if there are any. If none, we've successfully + // handled all fields and can generate the final implementation. + let err = errors.into_iter().reduce(|mut acc, err| { + acc.combine(err); + + acc + }); + + if let Some(err) = err { + return err.into_compile_error(); + } + + generate_serialize_impl_for(ident, generics, props, with_struct_fields(fields)) +} + +/// Generates an implementation of the `XmlSerialize` trait (and the +/// `XmlSerializeAttr` trait if appropriate) for a Rust enum, its variants, and +/// their fields. +pub(crate) fn write_serialize_impl_for_enum( + ident: Ident, + generics: Generics, + input: DataEnum, + mut props: TypeProps, +) -> TokenStream { + if props.should_serialize_as_text { + // We depend on the code which generates `TypeProps` to handle verifying + // that this enum consists solely of unit variants when setting this + // property, so we just collect variant identifiers. + let variants = input + .variants + .into_iter() + .map(|variant| variant.ident) + .collect(); + + return generate_serialize_impl_for(ident, generics, props, with_text_variants(variants)); + } + + // We build a list of errors so that we can combine them later and emit + // them all instead of quitting at the first we encounter. + let mut errors = Vec::new(); + + // Process the enum's variants in order to determine how to represent them, + // based on variant type and any consumer-applied attributes. + let variants = input + .variants + .into_iter() + .map(process_enum_variant(&mut errors)) + .collect(); + + // Combine and return errors if there are any. If none, we've successfully + // handled all fields and can generate the final implementation. + let err = errors.into_iter().reduce(|mut acc, err| { + acc.combine(err); + + acc + }); + + if let Some(err) = err { + return err.into_compile_error(); + } + + // Since this is enum-specific, there should be no reason for it to be used + // in codegen and we can just steal the memory. + let ns_prefix = props.ns_prefix_for_variants.take(); + + generate_serialize_impl_for( + ident, + generics, + props, + with_enum_variants(variants, ns_prefix), + ) +} + +/// Creates a callback for processing a `syn` enum variant into codegen details. +fn process_enum_variant(errors: &mut Vec) -> impl FnMut(syn::Variant) -> Variant + '_ { + |variant| { + // Process the variants's fields in order to determine how to represent + // them, based on variant type and any consumer-applied attributes. + let kind = match variant.fields { + syn::Fields::Named(fields) => { + let fields = fields + .named + .into_iter() + .map(process_named_field(errors, Ident::to_token_stream)) + .collect(); + + VariantKind::Struct(fields) + } + syn::Fields::Unnamed(fields) => { + let fields = fields + .unnamed + .into_iter() + .enumerate() + .map(process_unnamed_field(errors, |idx| { + format_ident!("field{idx}").into_token_stream() + })) + .collect(); + + VariantKind::Tuple(fields) + } + syn::Fields::Unit => VariantKind::Unit, + }; + + Variant { + ident: variant.ident, + kind, + } + } +} + +/// Creates a callback for extracting representation details from a named field +/// (i.e., a field of a regular struct or a struct enum variant) and its +/// attributes. +/// +/// The `accessor_generator` callback should, based on the name of a field, +/// return an expression for accessing the value of that field (either on `self` +/// or within a match arm). +fn process_named_field<'cb, 'g: 'cb, G>( + errors: &'cb mut Vec, + mut accessor_generator: G, +) -> impl FnMut(syn::Field) -> Field + 'cb +where + G: FnMut(&Ident) -> TokenStream + 'g, +{ + move |field| { + // We should be able to unwrap without panicking, since we know this is + // a named field. + let ident = field.ident.unwrap(); + let accessor = accessor_generator(&ident); + + let props = FieldProps::try_from_attrs(field.attrs, true) + .unwrap_or_else(collect_field_processing_error(errors)); + + Field { + kind: FieldKind::Named(ident), + ty: field.ty.into_token_stream(), + accessor, + props, + } + } +} + +/// Creates a callback for extracting representation details from an unnamed +/// field (i.e., a field of a tuple struct or a tuple enum variant) and its +/// attributes. +/// +/// The `accessor_generator` callback should, based on the position of a field, +/// return an expression for accessing the value of that field (either on `self` +/// or within a match arm). +fn process_unnamed_field<'cb, 'g: 'cb, G>( + errors: &'cb mut Vec, + mut accessor_generator: G, +) -> impl FnMut((usize, syn::Field)) -> Field + 'cb +where + G: FnMut(usize) -> TokenStream + 'g, +{ + move |(idx, field)| { + let accessor = accessor_generator(idx); + + let props = FieldProps::try_from_attrs(field.attrs, false) + .unwrap_or_else(collect_field_processing_error(errors)); + + Field { + kind: FieldKind::Unnamed, + ty: field.ty.into_token_stream(), + accessor, + props, + } + } +} + +/// Creates a callback for handling errors in processing field properties. +fn collect_field_processing_error( + errors: &mut Vec, +) -> impl FnMut(syn::Error) -> FieldProps + '_ { + |err| { + errors.push(err); + + FieldProps::default() + } +} diff --git a/xml_struct_derive/src/serialize/codegen.rs b/xml_struct_derive/src/serialize/codegen.rs new file mode 100644 index 0000000..fb6f99e --- /dev/null +++ b/xml_struct_derive/src/serialize/codegen.rs @@ -0,0 +1,607 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use proc_macro2::{Ident, Literal, TokenStream}; +use quote::{quote, ToTokens}; +use syn::Generics; + +use crate::{FieldProps, FieldRepr, TypeProps}; + +/// Generates an implementation of the `XmlSerialize` trait and, if appropriate, +/// the `XmlSerializeAttr` trait. +pub(super) fn generate_serialize_impl_for( + type_ident: Ident, + generics: Generics, + props: TypeProps, + body_generator: G, +) -> TokenStream +where + G: FnOnce(&[XmlAttribute]) -> ImplTokenSets, +{ + let default_ns_attr = props.default_ns_name.map(|ns_name| XmlAttribute { + // The terminology is a little confusing here. In terms of the XML + // spec, the "name" of a namespace is the (usually) URI used as the + // _value_ of the namespace declaration attribute. + name: Literal::string("xmlns").into_token_stream(), + value: ns_name, + }); + + let ns_decl_attrs = props.ns_decls.into_iter().map(|ns_decl| XmlAttribute { + name: generate_static_string_concat("xmlns:", ns_decl.prefix), + value: ns_decl.name, + }); + + let namespace_attrs: Vec<_> = default_ns_attr.into_iter().chain(ns_decl_attrs).collect(); + + let ImplTokenSets { + as_element_impl, + child_nodes_body, + as_attr_body, + } = body_generator(&namespace_attrs); + + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let attr_impl = if let Some(body) = as_attr_body { + quote! { + #[automatically_derived] + impl #impl_generics ::xml_struct::XmlSerializeAttr for #type_ident #ty_generics #where_clause { + fn serialize_as_attribute(&self, start_tag: &mut ::quick_xml::events::BytesStart, name: &str) { + #body + } + } + } + } else { + // In cases where there is no clear text representation of a value, we + // provide no derivation of `XmlSerializeAttr`. + TokenStream::default() + }; + + // Construct the final implementation from the type-specific sets of tokens. + quote! { + #[automatically_derived] + impl #impl_generics ::xml_struct::XmlSerialize for #type_ident #ty_generics #where_clause { + #as_element_impl + + fn serialize_child_nodes( + &self, + writer: &mut ::quick_xml::writer::Writer + ) -> Result<(), ::xml_struct::Error> { + #child_nodes_body + + Ok(()) + } + } + + #attr_impl + } +} + +/// The sets of tokens which make up the implementations or bodies of +/// `XmlSerialize` and `XmlSerializeAttr` trait methods. +pub(super) struct ImplTokenSets { + /// The implementation of `XmlSerialize::serialize_as_element()` if it is + /// necessary to override the provided default implementation. + as_element_impl: TokenStream, + + /// The body of `XmlSerialize::serialize_child_nodes()`. + child_nodes_body: TokenStream, + + /// The body of `XmlSerializeAttr::serialize_as_attribute()` if the type is + /// capable of being serialized as such. + as_attr_body: Option, +} + +/// Creates a generator for the sets of tokens necessary to serialize a struct +/// with the provided fields. +pub(super) fn with_struct_fields( + fields: Vec, +) -> impl FnOnce(&[XmlAttribute]) -> ImplTokenSets { + move |namespace_attrs| { + let Fields { + attr_fields, + child_fields, + } = partition_fields(fields); + + let content_call = if !child_fields.is_empty() { + Some(quote! { + ::serialize_child_nodes(self, writer)?; + }) + } else { + None + }; + + let impl_body = + generate_xml_tag_calls(quote!(name), namespace_attrs, &attr_fields, content_call); + + ImplTokenSets { + as_element_impl: quote! { + fn serialize_as_element( + &self, + writer: &mut ::quick_xml::writer::Writer, + name: &str, + ) -> Result<(), ::xml_struct::Error> { + #impl_body + + Ok(()) + } + }, + child_nodes_body: generate_field_content_node_calls(child_fields), + + // There is no clear text representation of an arbitrary struct, so + // we cannot provide an `XmlSerializeAttr` derivation. + as_attr_body: None, + } + } +} + +/// Creates a generator for the sets of tokens necessary to serialize a +/// unit-only enum as text nodes or attribute values. +pub(super) fn with_text_variants( + variants: Vec, +) -> impl FnOnce(&[XmlAttribute]) -> ImplTokenSets { + // While the generator function takes namespace attributes as its argument, + // we expect that the consuming code has already verified that there are + // none for this enum, since attributes cannot be specified for text content + // nodes. + move |_| { + let match_arms: Vec<_> = variants + .iter() + .map(|variant| quote!(Self::#variant => stringify!(#variant))) + .collect(); + + let text_from_value = quote! { + let text = match self { + #(#match_arms,)* + }; + }; + + ImplTokenSets { + // No namespaces can be declared on enums which are serialized as + // text, nor can they contain any attribute fields, so the default + // implementation of `serialize_as_element()` is sufficient. + as_element_impl: TokenStream::default(), + child_nodes_body: quote! { + #text_from_value + + writer.write_event( + ::quick_xml::events::Event::Text( + ::quick_xml::events::BytesText::new(text) + ) + )?; + }, + as_attr_body: Some(quote! { + #text_from_value + + // `start_tag` is one of the parameters to the + // `serialize_as_attribute()` method. + start_tag.push_attribute((name, text)); + }), + } + } +} + +/// Creates a generator for the sets of tokens necessary to serialize an enum +/// with the provided variants. +pub(super) fn with_enum_variants( + variants: Vec, + ns_prefix: Option, +) -> impl FnOnce(&[XmlAttribute]) -> ImplTokenSets { + move |namespace_attrs| { + let match_arms: TokenStream = variants + .into_iter() + .map(|variant| { + let ident = variant.ident; + + let name_tokens = { + // If the consumer has specified that variants should be + // serialized with a namespace prefix, we need to statically + // concatenate the prefix with the variant name. Otherwise, + // we just need to stringify the variant name. + if let Some(prefix) = &ns_prefix { + let ident_as_str = ident.to_string(); + let ident_as_str_tokens = format!(":{ident_as_str}"); + generate_static_string_concat(prefix, ident_as_str_tokens) + } else { + quote!(stringify!(#ident)) + } + }; + + match variant.kind { + VariantKind::Struct(fields) => { + let VariantTokenSets { + accessors, + content_calls, + } = generate_variant_token_sets(name_tokens, namespace_attrs, fields); + + quote! { + Self::#ident { #(#accessors),* } => { + #content_calls + } + } + } + VariantKind::Tuple(fields) => { + let VariantTokenSets { + accessors, + content_calls, + } = generate_variant_token_sets(name_tokens, namespace_attrs, fields); + + quote! { + Self::#ident(#(#accessors),*) => { + #content_calls + } + } + } + VariantKind::Unit => { + let content_calls = + generate_xml_tag_calls(name_tokens, namespace_attrs, &[], None); + + quote! { + Self::#ident => { + #content_calls + } + } + } + } + }) + .collect(); + + ImplTokenSets { + // No namespaces can be declared directly on the element enclosing + // an enum value, nor can it be provided with attribute fields, so + // the default `serialize_as_element()` implementation is + // sufficient. + as_element_impl: TokenStream::default(), + + child_nodes_body: quote! { + match self { + #match_arms + } + }, + + // There is no clear text representation of an arbitrary enum + // variant, so we cannot provide an `XmlSerializeAttr` derivation. + as_attr_body: None, + } + } +} + +/// The common sets of tokens which make up a `match` arm for an enum variant. +struct VariantTokenSets { + /// The identifiers used for accessing the fields of an enum variant. + accessors: Vec, + + /// The calls for serializing the child nodes of the XML element + /// representing an enum variant. + content_calls: TokenStream, +} + +/// Generates a list of accessors and set of calls to serialize content for an +/// enum variant. +fn generate_variant_token_sets( + name_tokens: TokenStream, + namespace_attrs: &[XmlAttribute], + fields: Vec, +) -> VariantTokenSets { + let accessors: Vec<_> = fields + .iter() + .map(|field| &field.accessor) + .cloned() + .collect(); + + let Fields { + attr_fields, + child_fields, + } = partition_fields(fields); + + let child_node_calls = generate_field_content_node_calls(child_fields); + + let content_calls = generate_xml_tag_calls( + name_tokens, + namespace_attrs, + &attr_fields, + Some(child_node_calls), + ); + + VariantTokenSets { + accessors, + content_calls, + } +} + +/// Divides the fields of a struct or enum variant into those which will be +/// represented as attributes and those which will be represented as child nodes. +fn partition_fields(fields: Vec) -> Fields { + let (attr_fields, child_fields) = fields + .into_iter() + .partition(|field| matches!(field.props.repr, FieldRepr::Attribute)); + + Fields { + attr_fields, + child_fields, + } +} + +/// Generates tokens representing a call to add namespace attributes to an +/// element. +fn generate_namespace_attrs_call(namespace_attrs: &[XmlAttribute]) -> TokenStream { + if !namespace_attrs.is_empty() { + let namespace_attrs: Vec<_> = namespace_attrs + .iter() + .map(|XmlAttribute { name, value }| quote!((#name, #value))) + .collect(); + + quote! { + .with_attributes([ + #(#namespace_attrs,)* + ]) + } + } else { + TokenStream::default() + } +} + +/// Generates calls to serialize struct or enum fields as XML attributes. +fn generate_attribute_field_calls(attr_fields: &[Field]) -> TokenStream { + if !attr_fields.is_empty() { + attr_fields + .iter() + .map(|field| { + let name = field_name_to_string_tokens(field); + let accessor = &field.accessor; + let ty = &field.ty; + + quote! { + <#ty as ::xml_struct::XmlSerializeAttr>::serialize_as_attribute(&#accessor, &mut start_tag, #name); + } + }) + .collect() + } else { + TokenStream::default() + } +} + +/// Generates calls to add a new XML element to a document, including any +/// necessary attributes and content nodes. +/// +/// If `content_calls` is `None`, the XML element will be an empty tag (e.g., +/// ""). Otherwise, the XML element will enclose any content added to +/// the writer by those calls. +fn generate_xml_tag_calls( + name_tokens: TokenStream, + namespace_attrs: &[XmlAttribute], + attr_fields: &[Field], + content_calls: Option, +) -> TokenStream { + let namespaces_call = generate_namespace_attrs_call(namespace_attrs); + let attr_calls = generate_attribute_field_calls(attr_fields); + + let calls = if let Some(content_calls) = content_calls { + // If the type has fields to serialize as child elements, wrap them + // first in an appropriate parent element. + quote! { + writer.write_event( + ::quick_xml::events::Event::Start(start_tag) + )?; + + #content_calls + + writer.write_event( + ::quick_xml::events::Event::End( + ::quick_xml::events::BytesEnd::new(#name_tokens) + ) + )?; + } + } else { + // If the type has no fields which are to be serialized as child + // elements, write an empty XML tag. + quote! { + writer.write_event( + ::quick_xml::events::Event::Empty(start_tag) + )?; + } + }; + + quote! { + let mut start_tag = ::quick_xml::events::BytesStart::new(#name_tokens) + #namespaces_call; + + #attr_calls + + #calls + } +} + +/// Generates calls to serialize the given fields as XML content nodes. +fn generate_field_content_node_calls(child_fields: Vec) -> TokenStream { + child_fields + .into_iter() + .map(|field| { + if matches!(field.props.repr, FieldRepr::Attribute) { + panic!("attribute field passed to child node call generator"); + } + + let ty = &field.ty; + let accessor = &field.accessor; + + match field.kind { + FieldKind::Named(_) if !field.props.should_flatten => { + let child_name = field_name_to_string_tokens(&field); + + quote! { + <#ty as ::xml_struct::XmlSerialize>::serialize_as_element(&#accessor, writer, #child_name)?; + } + } + + // If this is a tuple struct or the consumer has specifically + // requested a flat representation, serialize without a + // containing element. + _ => { + quote! { + <#ty as ::xml_struct::XmlSerialize>::serialize_child_nodes(&#accessor, writer)?; + } + } + } + }) + .collect() +} + +/// Converts the name of a field to a string suitable for use as a tag name. +/// +/// The identifier is stringified and converted to the desired case system. It +/// will also generate code for concatenating the field name with any namespace +/// prefix to be added. +fn field_name_to_string_tokens(field: &Field) -> TokenStream { + match &field.kind { + FieldKind::Named(ident) => { + let name = ident.to_string(); + + let case_mapped = kebab_to_pascal(&name); + + if let Some(prefix) = &field.props.namespace_prefix { + let string_with_colon = format!(":{case_mapped}"); + generate_static_string_concat(prefix, Literal::string(&string_with_colon)) + } else { + Literal::string(&case_mapped).into_token_stream() + } + } + + FieldKind::Unnamed => panic!("cannot stringify unnamed field"), + } +} + +/// Converts a kebab_case identifier string to PascalCase. +fn kebab_to_pascal(kebab: &str) -> String { + let mut capitalize_next = true; + + kebab + .chars() + .filter_map(|character| { + if character == '_' { + // Consume the underscore and capitalize the next character. + capitalize_next = true; + + None + } else if capitalize_next { + capitalize_next = false; + + // Rust supports non-ASCII identifiers, so this could + // technically fail, but this macro does not currently handle + // the general XML case, and so full Unicode case mapping is out + // of scope at present. + Some(character.to_ascii_uppercase()) + } else { + Some(character) + } + }) + .collect() +} + +#[derive(Debug)] +/// A representation of an enum variant. +pub(crate) struct Variant { + // The identifier for the variant. + pub ident: Ident, + + // The form of the variant, along with any fields. + pub kind: VariantKind, +} + +#[derive(Debug)] +/// The form of an enum variant and its contained fields. +pub(crate) enum VariantKind { + Struct(Vec), + Tuple(Vec), + Unit, +} + +#[derive(Debug)] +/// A representation of a struct or enum field. +pub(crate) struct Field { + // The form of the field, along with any identifier. + pub kind: FieldKind, + + // The type of the field. + pub ty: TokenStream, + + // An expression which will access the value of the field. + pub accessor: TokenStream, + + // Properties affecting the serialization of the field. + pub props: FieldProps, +} + +#[derive(Debug)] +/// A container for partitioned attribute and child element fields. +struct Fields { + attr_fields: Vec, + child_fields: Vec, +} + +#[derive(Debug)] +/// The form of a field, whether named or unnamed. +pub(crate) enum FieldKind { + Named(Ident), + Unnamed, +} + +/// Tokens representing an XML attribute's name/value pair. +pub(crate) struct XmlAttribute { + name: TokenStream, + value: TokenStream, +} + +/// Generates code for concatenating strings at compile-time. +/// +/// This code allows for concatenating `const` string references and/or string +/// literals with zero runtime cost. +fn generate_static_string_concat(a: T, b: U) -> TokenStream +where + T: ToTokens, + U: ToTokens, +{ + quote!({ + const LEN: usize = #a.len() + #b.len(); + + const fn copy_bytes_into(input: &[u8], mut output: [u8; LEN], offset: usize) -> [u8; LEN] { + // Copy the input byte-by-byte into the output buffer at the + // specified offset. + // NOTE: If/when `const_for` is stabilized, this can become a `for` + // loop. https://github.com/rust-lang/rust/issues/87575 + let mut index = 0; + loop { + output[offset + index] = input[index]; + index += 1; + if index == input.len() { + break; + } + } + + // We must return the buffer, as `const` functions cannot take a + // mutable reference, so it's moved into and out of scope. + output + } + + const fn constcat(prefix: &'static str, value: &'static str) -> [u8; LEN] { + let mut output = [0u8; LEN]; + output = copy_bytes_into(prefix.as_bytes(), output, 0); + output = copy_bytes_into(value.as_bytes(), output, prefix.len()); + + output + } + + // As of writing this comment, Rust does not provide a standard macro + // for compile-time string concatenation, so we exploit the fact that + // `str::as_bytes()` and `std::str::from_utf8()` are `const`. + const BYTES: [u8; LEN] = constcat(#a, #b); + match std::str::from_utf8(&BYTES) { + Ok(value) => value, + + // Given that both inputs to `constcat()` are Rust strings, they're + // guaranteed to be valid UTF-8. As such, directly concatenating + // them should create valid UTF-8 as well. If we hit this panic, + // it's probably a bug in one of the above functions. + Err(_) => panic!("Unable to statically concatenate strings"), + } + }) +} diff --git a/xml_struct_tests/Cargo.toml b/xml_struct_tests/Cargo.toml new file mode 100644 index 0000000..302bda7 --- /dev/null +++ b/xml_struct_tests/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "xml_struct_tests" +version = "0.1.0" +edition = "2021" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +quick-xml = "0.31.0" +thiserror = "1.0.56" +trybuild = "1.0.89" +xml_struct = { version = "0.1.0", path = "../xml_struct" } + +[[test]] +name = "integration_tests" +path = "integration/lib.rs" +harness = true + +[[test]] +name = "build_tests" +path = "ui/lib.rs" +harness = true diff --git a/xml_struct_tests/integration/enum.rs b/xml_struct_tests/integration/enum.rs new file mode 100644 index 0000000..4162684 --- /dev/null +++ b/xml_struct_tests/integration/enum.rs @@ -0,0 +1,252 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use xml_struct::XmlSerialize; +use xml_struct_tests::{serialize_value_as_element, serialize_value_children}; + +#[derive(XmlSerialize)] +enum MixedEnum { + UnitVariant, + StructVariant { + some_field: String, + }, + TupleVariant(&'static str), + StructVariantWithAttributes { + child_field: String, + + #[xml_struct(attribute)] + attr_field: String, + }, +} + +#[derive(XmlSerialize)] +enum AllUnitEnum { + A, + Two, + Gamma, +} + +#[derive(XmlSerialize)] +struct StructWithAllUnitEnumFields { + child_field: String, + enum_child: AllUnitEnum, + + #[xml_struct(ns_prefix = "foo")] + enum_child_with_prefix: AllUnitEnum, +} + +#[derive(XmlSerialize)] +#[xml_struct(variant_ns_prefix = "foo")] +enum EnumWithNamespacePrefix { + SomeValue, +} + +#[test] +fn mixed_enum_unit_variant() { + let content = MixedEnum::UnitVariant; + + let expected = ""; + let actual = serialize_value_children(content).expect("Failed to serialize enum value"); + + assert_eq!( + actual, expected, + "Unit variants should serialize as an element with no content" + ); +} + +#[test] +fn mixed_enum_unit_variant_as_element() { + let content = MixedEnum::UnitVariant; + + let expected = ""; + let actual = + serialize_value_as_element(content, "parent_name").expect("Failed to serialize enum value"); + + assert_eq!( + actual, expected, + "Unit variants should serialize as a parented element with no content" + ); +} + +#[test] +fn mixed_enum_struct_variant() { + let content = MixedEnum::StructVariant { + some_field: String::from("some content"), + }; + + let expected = "some content"; + let actual = serialize_value_children(content).expect("Failed to serialize enum value"); + + assert_eq!( + actual, expected, + "Struct variants should serialize as an element with serialized fields as content" + ); +} + +#[test] +fn mixed_enum_struct_variant_as_element() { + let content = MixedEnum::StructVariant { + some_field: String::from("some content"), + }; + + let expected = + "some content"; + let actual = + serialize_value_as_element(content, "FooBar").expect("Failed to serialize enum value"); + + assert_eq!( + actual, expected, + "Struct variants should serialize as a parented element with serialized fields as content" + ); +} + +#[test] +fn mixed_enum_tuple_variant() { + let content = MixedEnum::TupleVariant("something in a tuple"); + + let expected = "something in a tuple"; + let actual = serialize_value_children(content).expect("Failed to serialize enum value"); + + assert_eq!( + actual, expected, + "Tuple variants should serialize as an element with serialized fields as content" + ); +} + +#[test] +fn mixed_enum_tuple_variant_as_element() { + let content = MixedEnum::TupleVariant("something in a tuple"); + + let expected = "something in a tuple"; + let actual = + serialize_value_as_element(content, "banana").expect("Failed to serialize enum value"); + + assert_eq!( + actual, expected, + "Tuple variants should serialize as a parented element with serialized fields as content" + ); +} + +#[test] +fn mixed_enum_struct_variant_with_attributes() { + let content = MixedEnum::StructVariantWithAttributes { + child_field: String::from("some child content"), + attr_field: String::from("an attribute"), + }; + + let expected = r#"some child content"#; + let actual = serialize_value_children(content).expect("Failed to serialize enum value"); + + assert_eq!( + actual, expected, + "Attributes should be applied to the variant element" + ); +} + +#[test] +fn mixed_enum_struct_variant_with_attributes_as_element() { + let content = MixedEnum::StructVariantWithAttributes { + child_field: String::from("some child content"), + attr_field: String::from("an attribute"), + }; + + let expected = r#"some child content"#; + let actual = + serialize_value_as_element(content, "Arbitrary").expect("Failed to serialize enum value"); + + assert_eq!( + actual, expected, + "Attributes should be applied to the variant element rather than the parent" + ); +} + +#[test] +fn all_unit_enum() { + let content = AllUnitEnum::Two; + + let expected = ""; + let actual = serialize_value_children(content).expect("Failed to serialize enum value"); + + assert_eq!( + actual, expected, + "Unit variants should be serialized as an empty element" + ); +} + +#[test] +fn all_unit_enum_as_element() { + let content = AllUnitEnum::Two; + + let expected = r#""#; + let actual = + serialize_value_as_element(content, "foo").expect("Failed to serialize enum value"); + + assert_eq!( + actual, expected, + "Unit variants should be serialized as a parented empty element" + ); +} + +#[test] +fn struct_with_all_unit_enum_fields() { + let content = StructWithAllUnitEnumFields { + child_field: String::from("this is a regular string field"), + enum_child: AllUnitEnum::A, + enum_child_with_prefix: AllUnitEnum::Gamma, + }; + + let expected = + "this is a regular string field"; + let actual = serialize_value_children(content).expect("Failed to serialize struct value"); + + assert_eq!( + actual, expected, + "Unit enum fields should be serialized as empty elements" + ) +} + +#[test] +fn struct_with_all_unit_enum_fields_as_element() { + let content = StructWithAllUnitEnumFields { + child_field: String::from("this is a regular string field"), + enum_child: AllUnitEnum::A, + enum_child_with_prefix: AllUnitEnum::Gamma, + }; + + let expected = r#"this is a regular string field"#; + let actual = + serialize_value_as_element(content, "TAGNAME").expect("Failed to serialize struct value"); + + assert_eq!( + actual, expected, + "Unit enum fields should be serialized as parented empty elements" + ) +} + +#[test] +fn enum_with_namespace_prefix() { + let content = EnumWithNamespacePrefix::SomeValue; + + let expected = ""; + let actual = serialize_value_children(content).expect("Failed to serialize enum value"); + + assert_eq!( + actual, expected, + "Enum variants should be serialized with specified prefix" + ); +} + +#[test] +fn enum_with_namespace_prefix_as_element() { + let content = EnumWithNamespacePrefix::SomeValue; + + let expected = ""; + let actual = + serialize_value_as_element(content, "outer_foo").expect("Failed to serialize enum value"); + + assert_eq!( + actual, expected, + "Enum variants should be serialized with specified prefix" + ); +} diff --git a/xml_struct_tests/integration/lib.rs b/xml_struct_tests/integration/lib.rs new file mode 100644 index 0000000..2d134da --- /dev/null +++ b/xml_struct_tests/integration/lib.rs @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +mod r#enum; +mod r#struct; +mod text_enum; +mod tuple_struct; +mod unit_struct; diff --git a/xml_struct_tests/integration/struct.rs b/xml_struct_tests/integration/struct.rs new file mode 100644 index 0000000..7196b6c --- /dev/null +++ b/xml_struct_tests/integration/struct.rs @@ -0,0 +1,206 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use quick_xml::events::{BytesText, Event}; +use xml_struct::XmlSerialize; +use xml_struct_tests::{serialize_value_as_element, serialize_value_children}; + +#[derive(XmlSerialize)] +struct Struct { + #[xml_struct(attribute)] + str_attr: &'static str, + + #[xml_struct(attribute, ns_prefix = "other_ns")] + string_attr: String, + + child: ChildType, + more_complex_field_name: String, +} + +#[derive(XmlSerialize)] +#[xml_struct(default_ns = "http://foo.example/this_ns", ns = ("other_ns", "http://bar.example/other_ns"))] +struct StructWithNamespaces { + #[xml_struct(attribute)] + str_attr: &'static str, + + #[xml_struct(attribute, ns_prefix = "other_ns")] + string_attr: String, + + child: ChildType, + more_complex_field_name: String, +} + +#[derive(XmlSerialize)] +struct StructWithFlattenedField { + #[xml_struct(attribute)] + str_attr: &'static str, + + #[xml_struct(attribute, ns_prefix = "other_ns")] + string_attr: String, + + #[xml_struct(flatten)] + child: ChildType, + more_complex_field_name: String, +} + +struct ChildType { + _grandchild: &'static str, +} + +impl ChildType { + #[allow(dead_code)] + fn serialize_child_nodes( + &self, + _writer: &mut quick_xml::Writer, + ) -> Result<(), xml_struct::Error> + where + W: std::io::Write, + { + panic!("`XmlSerialize` calls should not dispatch non-trait functions"); + } +} + +// We explicitly implement `XmlSerialize` for this type in a way which doesn't +// match the default in order to verify that `ChildType`'s implementation is +// used rather than some other magic. +impl XmlSerialize for ChildType { + fn serialize_child_nodes( + &self, + writer: &mut quick_xml::Writer, + ) -> Result<(), xml_struct::Error> + where + W: std::io::Write, + { + writer.write_event(Event::Text(BytesText::new("bare text child node")))?; + + Ok(()) + } +} + +#[test] +fn r#struct() { + let content = Struct { + str_attr: "arbitrary text", + string_attr: String::from("other text"), + child: ChildType { + _grandchild: "this text shouldn't show up", + }, + more_complex_field_name: String::from("bare text node"), + }; + + let expected = "bare text child nodebare text node"; + + let actual = serialize_value_children(content).expect("Failed to write struct"); + + assert_eq!( + actual, expected, + "Struct fields should each be serialized as a child node" + ); +} + +#[test] +fn struct_as_element() { + let content = Struct { + str_attr: "arbitrary text", + string_attr: String::from("other text"), + child: ChildType { + _grandchild: "this text shouldn't show up", + }, + more_complex_field_name: String::from("bare text node"), + }; + + let expected = r#"bare text child nodebare text node"#; + + let actual = serialize_value_as_element(content, "parent").expect("Failed to write struct"); + + assert_eq!( + actual, expected, + "Struct should be serialized as element with fields as attribute and children as appropriate" + ); +} + +#[test] +fn struct_with_namespaces() { + let content = StructWithNamespaces { + str_attr: "arbitrary text", + string_attr: String::from("other text"), + child: ChildType { + _grandchild: "this text shouldn't show up", + }, + more_complex_field_name: String::from("bare text node"), + }; + + let expected = "bare text child nodebare text node"; + + let actual = serialize_value_children(content).expect("Failed to write struct"); + + assert_eq!( + actual, expected, + "Struct fields should each be serialized as a child node" + ); +} + +#[test] +fn struct_with_namespaces_as_element() { + let content = StructWithNamespaces { + str_attr: "arbitrary text", + string_attr: String::from("other text"), + child: ChildType { + _grandchild: "this text shouldn't show up", + }, + more_complex_field_name: String::from("bare text node"), + }; + + let expected = r#"bare text child nodebare text node"#; + + let actual = serialize_value_as_element(content, "parent").expect("Failed to write struct"); + + assert_eq!( + actual, expected, + "Struct should be serialized with namespaces as attributes" + ); +} + +#[test] +fn struct_with_flattened_field() { + let content = StructWithFlattenedField { + str_attr: "arbitrary text", + string_attr: String::from("other text"), + child: ChildType { + _grandchild: "this text shouldn't show up", + }, + more_complex_field_name: String::from("bare text node"), + }; + + let expected = + "bare text child nodebare text node"; + + let actual = serialize_value_children(content).expect("Failed to write struct"); + + assert_eq!( + actual, expected, + "Flattened field should be serialized as content only" + ); +} + +#[test] +fn struct_with_flattened_field_as_element() { + let content = StructWithFlattenedField { + str_attr: "arbitrary text", + string_attr: String::from("other text"), + child: ChildType { + _grandchild: "this text shouldn't show up", + }, + more_complex_field_name: String::from("bare text node"), + }; + + let expected = r#"bare text child nodebare text node"#; + + let actual = serialize_value_as_element(content, "parent").expect("Failed to write struct"); + + assert_eq!( + actual, expected, + "Flattened field should be serialized as content only" + ); +} diff --git a/xml_struct_tests/integration/text_enum.rs b/xml_struct_tests/integration/text_enum.rs new file mode 100644 index 0000000..4a59ad3 --- /dev/null +++ b/xml_struct_tests/integration/text_enum.rs @@ -0,0 +1,94 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use xml_struct::XmlSerialize; +use xml_struct_tests::{serialize_value_as_element, serialize_value_children}; + +#[derive(XmlSerialize)] +#[xml_struct(text)] +enum TextEnum { + A, + Two, + Gamma, +} + +#[derive(XmlSerialize)] +struct StructWithTextEnumFields { + child_field: String, + + #[xml_struct(attribute)] + string_attr: String, + + #[xml_struct(attribute)] + enum_attr: TextEnum, + + enum_child: TextEnum, +} + +#[test] +fn text_enum() { + let content = TextEnum::Two; + + let expected = "Two"; + + let actual = serialize_value_children(content).expect("Failed to write enum"); + + assert_eq!( + actual, expected, + "Variants of text enums should be serialized as a text node" + ); +} + +#[test] +fn text_enum_as_element() { + let content = TextEnum::Two; + + let expected = r#"Two"#; + + let actual = serialize_value_as_element(content, "foo").expect("Failed to write enum"); + + assert_eq!( + actual, expected, + "Variants of text enums should be serialized as a parented text node" + ); +} + +#[test] +fn struct_with_text_enum_fields() { + let content = StructWithTextEnumFields { + child_field: String::from("this is a regular string field"), + string_attr: String::from("this is a regular attr field"), + enum_attr: TextEnum::Gamma, + enum_child: TextEnum::A, + }; + + let expected = + "this is a regular string fieldA"; + + let actual = serialize_value_children(content).expect("Failed to write struct"); + + assert_eq!( + actual, expected, + "Text enum fields should be serialized as text nodes" + ) +} + +#[test] +fn struct_with_text_enum_fields_as_element() { + let content = StructWithTextEnumFields { + child_field: String::from("this is a regular string field"), + string_attr: String::from("this is a regular attr field"), + enum_attr: TextEnum::Gamma, + enum_child: TextEnum::A, + }; + + let expected = r#"this is a regular string fieldA"#; + + let actual = serialize_value_as_element(content, "namehere").expect("Failed to write struct"); + + assert_eq!( + actual, expected, + "Text enum attributes should be serialized as text values" + ) +} diff --git a/xml_struct_tests/integration/tuple_struct.rs b/xml_struct_tests/integration/tuple_struct.rs new file mode 100644 index 0000000..dce4717 --- /dev/null +++ b/xml_struct_tests/integration/tuple_struct.rs @@ -0,0 +1,124 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use quick_xml::events::{BytesText, Event}; +use xml_struct::XmlSerialize; +use xml_struct_tests::{serialize_value_as_element, serialize_value_children}; + +#[derive(XmlSerialize)] +struct TupleStruct(ChildType, String); + +#[derive(XmlSerialize)] +#[xml_struct(default_ns = "http://foo.example/this_ns", ns = ("other_ns", "http://bar.example/other_ns"))] +struct TupleStructWithNamespaces(ChildType, String); + +struct ChildType { + _grandchild: &'static str, +} + +impl ChildType { + #[allow(dead_code)] + fn serialize_child_nodes( + &self, + _writer: &mut quick_xml::Writer, + ) -> Result<(), xml_struct::Error> + where + W: std::io::Write, + { + panic!("`XmlSerialize` calls should not dispatch non-trait functions"); + } +} + +// We explicitly implement `XmlSerialize` for this type in a way which doesn't +// match the default in order to verify that `ChildType`'s implementation is +// used rather than some other magic. +impl XmlSerialize for ChildType { + fn serialize_child_nodes( + &self, + writer: &mut quick_xml::Writer, + ) -> Result<(), xml_struct::Error> + where + W: std::io::Write, + { + writer.write_event(Event::Text(BytesText::new("bare text child node")))?; + + Ok(()) + } +} + +#[test] +fn tuple_struct() { + let content = TupleStruct( + ChildType { + _grandchild: "this text shouldn't show up", + }, + String::from("bare text node"), + ); + + let expected = "bare text child nodebare text node"; + + let actual = serialize_value_children(content).expect("Failed to write struct"); + + assert_eq!( + actual, expected, + "Struct fields should each be serialized as a child node" + ); +} + +#[test] +fn tuple_struct_as_element() { + let content = TupleStruct( + ChildType { + _grandchild: "this text shouldn't show up", + }, + String::from("bare text node"), + ); + + let expected = r#"bare text child nodebare text node"#; + + let actual = serialize_value_as_element(content, "parent").expect("Failed to write struct"); + + assert_eq!( + actual, expected, + "Struct should be serialized as element with fields as attribute and children as appropriate" + ); +} + +#[test] +fn tuple_struct_with_namespaces() { + let content = TupleStructWithNamespaces( + ChildType { + _grandchild: "this text shouldn't show up", + }, + String::from("bare text node"), + ); + + let expected = "bare text child nodebare text node"; + + let actual = serialize_value_children(content).expect("Failed to write struct"); + + assert_eq!( + actual, expected, + "Struct fields should each be serialized as a child node" + ); +} + +#[test] +fn tuple_struct_with_namespaces_as_element() { + let content = TupleStructWithNamespaces( + ChildType { + _grandchild: "this text shouldn't show up", + }, + String::from("bare text node"), + ); + + let expected = r#"bare text child nodebare text node"#; + + let actual = serialize_value_as_element(content, "parent").expect("Failed to write struct"); + + assert_eq!( + actual, expected, + "Struct should be serialized with namespaces as attributes" + ); +} diff --git a/xml_struct_tests/integration/unit_struct.rs b/xml_struct_tests/integration/unit_struct.rs new file mode 100644 index 0000000..643acb0 --- /dev/null +++ b/xml_struct_tests/integration/unit_struct.rs @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use xml_struct::XmlSerialize; +use xml_struct_tests::{serialize_value_as_element, serialize_value_children}; + +#[derive(XmlSerialize)] +struct UnitStruct; + +const BAR_PREFIX: &str = "bar"; +const BAZ_NAME: &str = "http://baz.example/"; + +#[derive(XmlSerialize)] +#[xml_struct(default_ns = "http://foo.example/", ns = (BAR_PREFIX, "http://bar.example/"), ns = ("baz", BAZ_NAME))] +struct UnitStructWithNamespaces; + +#[test] +fn unit_struct() { + let content = UnitStruct; + + let expected = ""; + + let actual = serialize_value_children(content).expect("Failed to write unit struct"); + + assert_eq!(actual, expected, "Unit struct should have no content"); +} + +#[test] +fn unit_struct_as_element() { + let content = UnitStruct; + + let expected = ""; + + let actual = serialize_value_as_element(content, "foo").expect("Failed to write unit struct"); + + assert_eq!( + actual, expected, + "Unit struct should serialize as empty element" + ); +} + +#[test] +fn unit_struct_with_namespaces() { + let content = UnitStructWithNamespaces; + + let expected = ""; + + let actual = serialize_value_children(content).expect("Failed to write unit struct"); + + assert_eq!(actual, expected, "Unit struct should have no content"); +} + +#[test] +fn unit_struct_with_namespaces_as_element() { + let content = UnitStructWithNamespaces; + + let expected = r#""#; + + let actual = serialize_value_as_element(content, "foo").expect("Failed to write unit struct"); + + assert_eq!( + actual, expected, + "Unit struct should serialize as empty element with namespace attributes" + ); +} diff --git a/xml_struct_tests/src/lib.rs b/xml_struct_tests/src/lib.rs new file mode 100644 index 0000000..e17df6b --- /dev/null +++ b/xml_struct_tests/src/lib.rs @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use quick_xml::Writer; +use thiserror::Error; +use xml_struct::XmlSerialize; + +pub fn serialize_value_as_element(value: T, root_name: &str) -> Result +where + T: XmlSerialize, +{ + let buf = Vec::default(); + let mut writer = Writer::new(buf); + + value.serialize_as_element(&mut writer, root_name)?; + + let out = String::from_utf8(writer.into_inner())?; + + Ok(out) +} + +pub fn serialize_value_children(value: T) -> Result +where + T: XmlSerialize, +{ + let buf = Vec::default(); + let mut writer = Writer::new(buf); + + value.serialize_child_nodes(&mut writer)?; + + let out = String::from_utf8(writer.into_inner())?; + + Ok(out) +} + +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum TestError { + #[error("error in processing XML document")] + XmlStruct(#[from] xml_struct::Error), + + #[error("serialization produced invalid UTF-8")] + Utf8(#[from] std::string::FromUtf8Error), +} diff --git a/xml_struct_tests/ui/lib.rs b/xml_struct_tests/ui/lib.rs new file mode 100644 index 0000000..333d0df --- /dev/null +++ b/xml_struct_tests/ui/lib.rs @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::path::PathBuf; + +#[test] +fn type_properties() { + let base_path = test_case_base_path().join("type_properties"); + + let t = trybuild::TestCases::new(); + t.pass(base_path.join("no_properties.rs")); + t.pass(base_path.join("valid_namespaces.rs")); + t.compile_fail(base_path.join("multiple_defaults.rs")); + t.pass(base_path.join("text_enum.rs")); + t.compile_fail(base_path.join("text_struct.rs")); + t.compile_fail(base_path.join("text_enum_with_non_unit_variants.rs")); + t.compile_fail(base_path.join("text_enum_with_namespaces.rs")); + t.compile_fail(base_path.join("invalid_attributes.rs")); +} + +fn test_case_base_path() -> PathBuf { + PathBuf::from("ui/test_cases") +} diff --git a/xml_struct_tests/ui/test_cases/type_properties/invalid_attributes.rs b/xml_struct_tests/ui/test_cases/type_properties/invalid_attributes.rs new file mode 100644 index 0000000..c989701 --- /dev/null +++ b/xml_struct_tests/ui/test_cases/type_properties/invalid_attributes.rs @@ -0,0 +1,15 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use xml_struct::XmlSerialize; + +#[derive(XmlSerialize)] +#[xml_struct(defaut_ns = "http://foo.example/", ns = ("bar", "http://bar.example/"))] +struct MisspelledNameValueAttribute; + +#[derive(XmlSerialize)] +#[xml_struct(everybody_loves_xml)] +struct UnrecognizedPathAttribute; + +fn main() {} diff --git a/xml_struct_tests/ui/test_cases/type_properties/invalid_attributes.stderr b/xml_struct_tests/ui/test_cases/type_properties/invalid_attributes.stderr new file mode 100644 index 0000000..ed931d2 --- /dev/null +++ b/xml_struct_tests/ui/test_cases/type_properties/invalid_attributes.stderr @@ -0,0 +1,11 @@ +error: unrecognized `xml_struct` attribute + --> ui/test_cases/type_properties/invalid_attributes.rs:8:14 + | +8 | #[xml_struct(defaut_ns = "http://foo.example/", ns = ("bar", "http://bar.example/"))] + | ^^^^^^^^^ + +error: unrecognized `xml_struct` attribute + --> ui/test_cases/type_properties/invalid_attributes.rs:12:14 + | +12 | #[xml_struct(everybody_loves_xml)] + | ^^^^^^^^^^^^^^^^^^^ diff --git a/xml_struct_tests/ui/test_cases/type_properties/multiple_defaults.rs b/xml_struct_tests/ui/test_cases/type_properties/multiple_defaults.rs new file mode 100644 index 0000000..604a561 --- /dev/null +++ b/xml_struct_tests/ui/test_cases/type_properties/multiple_defaults.rs @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use xml_struct::XmlSerialize; + +#[derive(XmlSerialize)] +#[xml_struct(default_ns = "http://foo.example/", ns = ("bar", "http://bar.example/"), default_ns = "http://baz.example/")] +struct MultipleDefaultNamespaces; + +fn main() {} diff --git a/xml_struct_tests/ui/test_cases/type_properties/multiple_defaults.stderr b/xml_struct_tests/ui/test_cases/type_properties/multiple_defaults.stderr new file mode 100644 index 0000000..90fd976 --- /dev/null +++ b/xml_struct_tests/ui/test_cases/type_properties/multiple_defaults.stderr @@ -0,0 +1,5 @@ +error: cannot declare more than one default namespace + --> ui/test_cases/type_properties/multiple_defaults.rs:8:87 + | +8 | #[xml_struct(default_ns = "http://foo.example/", ns = ("bar", "http://bar.example/"), default_ns = "http://baz.example/")] + | ^^^^^^^^^^ diff --git a/xml_struct_tests/ui/test_cases/type_properties/no_properties.rs b/xml_struct_tests/ui/test_cases/type_properties/no_properties.rs new file mode 100644 index 0000000..e893e25 --- /dev/null +++ b/xml_struct_tests/ui/test_cases/type_properties/no_properties.rs @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use xml_struct::XmlSerialize; + +#[derive(XmlSerialize)] +struct NoAttributes; + +// There's no clear use case for this pattern at this time, but it shouldn't +// error anyhow. +#[derive(XmlSerialize)] +#[xml_struct()] +struct EmptyAttribute; + +fn main() -> Result<(), xml_struct::Error> { + let bytes: Vec = Vec::new(); + let mut writer = quick_xml::writer::Writer::new(bytes); + + let content = NoAttributes; + content.serialize_as_element(&mut writer, "foo")?; + + let content = EmptyAttribute; + content.serialize_as_element(&mut writer, "foo")?; + + Ok(()) +} diff --git a/xml_struct_tests/ui/test_cases/type_properties/text_enum.rs b/xml_struct_tests/ui/test_cases/type_properties/text_enum.rs new file mode 100644 index 0000000..b08cdd2 --- /dev/null +++ b/xml_struct_tests/ui/test_cases/type_properties/text_enum.rs @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use xml_struct::XmlSerialize; + +#[derive(XmlSerialize)] +#[xml_struct(text)] +enum UnitVariants { + Foo, + Bar, + Baz, + FooBar, +} + +fn main() -> Result<(), xml_struct::Error> { + let bytes: Vec = Vec::new(); + let mut writer = quick_xml::writer::Writer::new(bytes); + + let content = UnitVariants::Bar; + content.serialize_as_element(&mut writer, "foo")?; + + Ok(()) +} diff --git a/xml_struct_tests/ui/test_cases/type_properties/text_enum_with_namespaces.rs b/xml_struct_tests/ui/test_cases/type_properties/text_enum_with_namespaces.rs new file mode 100644 index 0000000..e0ac709 --- /dev/null +++ b/xml_struct_tests/ui/test_cases/type_properties/text_enum_with_namespaces.rs @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use xml_struct::XmlSerialize; + +#[derive(XmlSerialize)] +#[xml_struct(text, default_ns = "http://foo.example/")] +enum UnitVariantsWithDefaultNamespace { + Foo, + Bar, + Baz, + FooBar, +} + +#[derive(XmlSerialize)] +#[xml_struct(default_ns = "http://foo.example/", text)] +enum UnitVariantsWithDefaultNamespaceInDifferentOrder { + Foo, + Bar, + Baz, + FooBar, +} + +#[derive(XmlSerialize)] +#[xml_struct(text, ns = ("foo", "http://foo.example/"))] +enum UnitVariantsWithNamespace { + Foo, + Bar, + Baz, + FooBar, +} + +#[derive(XmlSerialize)] +#[xml_struct(ns = ("foo", "http://foo.example/"), text)] +enum UnitVariantsWithNamespaceInDifferentOrder { + Foo, + Bar, + Baz, + FooBar, +} + +fn main() {} diff --git a/xml_struct_tests/ui/test_cases/type_properties/text_enum_with_namespaces.stderr b/xml_struct_tests/ui/test_cases/type_properties/text_enum_with_namespaces.stderr new file mode 100644 index 0000000..f6c84e8 --- /dev/null +++ b/xml_struct_tests/ui/test_cases/type_properties/text_enum_with_namespaces.stderr @@ -0,0 +1,23 @@ +error: cannot declare namespaces for text content + --> ui/test_cases/type_properties/text_enum_with_namespaces.rs:8:1 + | +8 | #[xml_struct(text, default_ns = "http://foo.example/")] + | ^ + +error: cannot declare namespaces for text content + --> ui/test_cases/type_properties/text_enum_with_namespaces.rs:17:1 + | +17 | #[xml_struct(default_ns = "http://foo.example/", text)] + | ^ + +error: cannot declare namespaces for text content + --> ui/test_cases/type_properties/text_enum_with_namespaces.rs:26:1 + | +26 | #[xml_struct(text, ns = ("foo", "http://foo.example/"))] + | ^ + +error: cannot declare namespaces for text content + --> ui/test_cases/type_properties/text_enum_with_namespaces.rs:35:1 + | +35 | #[xml_struct(ns = ("foo", "http://foo.example/"), text)] + | ^ diff --git a/xml_struct_tests/ui/test_cases/type_properties/text_enum_with_non_unit_variants.rs b/xml_struct_tests/ui/test_cases/type_properties/text_enum_with_non_unit_variants.rs new file mode 100644 index 0000000..4793a16 --- /dev/null +++ b/xml_struct_tests/ui/test_cases/type_properties/text_enum_with_non_unit_variants.rs @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use xml_struct::XmlSerialize; + +#[derive(XmlSerialize)] +#[xml_struct(text)] +enum TextEnumWithTupleVariant { + UnitVariant, + TupleVariant(String), + OtherUnitVariant, +} + +#[derive(XmlSerialize)] +#[xml_struct(text)] +enum TextEnumWithStructVariant { + UnitVariant, + StructVariant { value: String }, + OtherUnitVariant, +} + +fn main() {} diff --git a/xml_struct_tests/ui/test_cases/type_properties/text_enum_with_non_unit_variants.stderr b/xml_struct_tests/ui/test_cases/type_properties/text_enum_with_non_unit_variants.stderr new file mode 100644 index 0000000..a35c50b --- /dev/null +++ b/xml_struct_tests/ui/test_cases/type_properties/text_enum_with_non_unit_variants.stderr @@ -0,0 +1,11 @@ +error: only unit enums may be derived as text + --> ui/test_cases/type_properties/text_enum_with_non_unit_variants.rs:8:14 + | +8 | #[xml_struct(text)] + | ^^^^ + +error: only unit enums may be derived as text + --> ui/test_cases/type_properties/text_enum_with_non_unit_variants.rs:16:14 + | +16 | #[xml_struct(text)] + | ^^^^ diff --git a/xml_struct_tests/ui/test_cases/type_properties/text_struct.rs b/xml_struct_tests/ui/test_cases/type_properties/text_struct.rs new file mode 100644 index 0000000..05ca05c --- /dev/null +++ b/xml_struct_tests/ui/test_cases/type_properties/text_struct.rs @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use xml_struct::XmlSerialize; + +#[derive(XmlSerialize)] +#[xml_struct(text)] +struct TextUnitStruct; + +#[derive(XmlSerialize)] +#[xml_struct(text)] +struct TextTupleStruct(String); + +#[derive(XmlSerialize)] +#[xml_struct(text)] +struct TextStruct { + value: String, +} + +fn main() {} diff --git a/xml_struct_tests/ui/test_cases/type_properties/text_struct.stderr b/xml_struct_tests/ui/test_cases/type_properties/text_struct.stderr new file mode 100644 index 0000000..a6d3d26 --- /dev/null +++ b/xml_struct_tests/ui/test_cases/type_properties/text_struct.stderr @@ -0,0 +1,17 @@ +error: only unit enums may be derived as text + --> ui/test_cases/type_properties/text_struct.rs:8:14 + | +8 | #[xml_struct(text)] + | ^^^^ + +error: only unit enums may be derived as text + --> ui/test_cases/type_properties/text_struct.rs:12:14 + | +12 | #[xml_struct(text)] + | ^^^^ + +error: only unit enums may be derived as text + --> ui/test_cases/type_properties/text_struct.rs:16:14 + | +16 | #[xml_struct(text)] + | ^^^^ diff --git a/xml_struct_tests/ui/test_cases/type_properties/valid_namespaces.rs b/xml_struct_tests/ui/test_cases/type_properties/valid_namespaces.rs new file mode 100644 index 0000000..175ad9c --- /dev/null +++ b/xml_struct_tests/ui/test_cases/type_properties/valid_namespaces.rs @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use xml_struct::XmlSerialize; + +#[derive(XmlSerialize)] +#[xml_struct(ns = ("foo", "http://foo.example/"))] +struct SingleNamespace; + +#[derive(XmlSerialize)] +#[xml_struct(ns = ("foo", "http://foo.example/"), ns = ("bar", "http://bar.example/"))] +struct MultipleNamespaces; + +const FOO_PREFIX: &str = "foo"; +const BAR_NAME: &str = "http://bar.example/"; + +#[derive(XmlSerialize)] +#[xml_struct(ns = (FOO_PREFIX, "http://foo.example/"), ns = ("bar", BAR_NAME))] +struct ConstsInNamespaceDecls; + +#[derive(XmlSerialize)] +#[xml_struct(default_ns = "http://default.example/")] +struct DefaultNamespace; + +#[derive(XmlSerialize)] +#[xml_struct(default_ns = "http://default.example/", ns = ("foo", "http://foo.example/"), ns = ("bar", BAR_NAME))] +struct DefaultNamespaceWithOthers; + +fn main() -> Result<(), xml_struct::Error> { + let bytes: Vec = Vec::new(); + let mut writer = quick_xml::writer::Writer::new(bytes); + + let content = SingleNamespace; + content.serialize_as_element(&mut writer, "foo")?; + + let content = MultipleNamespaces; + content.serialize_as_element(&mut writer, "foo")?; + + let content = ConstsInNamespaceDecls; + content.serialize_as_element(&mut writer, "foo")?; + + let content = DefaultNamespace; + content.serialize_as_element(&mut writer, "foo")?; + + let content = DefaultNamespaceWithOthers; + content.serialize_as_element(&mut writer, "foo")?; + + Ok(()) +}