Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial version of crate #1

Merged
merged 13 commits into from
Mar 26, 2024
20 changes: 20 additions & 0 deletions xml_struct/src/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use quick_xml::{

use crate::{Error, XmlSerialize, XmlSerializeAttr};

/// Serializes a string as a text content node.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (non-blocking): I think it could be helpful to mention how it interacts with the Writer it borrows from the consumer, i.e what event(s) each impl send (or doesn't). Same for the otherimpl blocks.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raises an interesting question of where the line is between implementation details and visible side effects. Given how easy autogenerated Rust docs make it to access the source, I'm going to lean toward "describe in comments, let them check source if they really need details".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where the line is between implementation details and visible side effects

fwiw I think that if the writer is given to us by the consumer, then it means the consumer might want to write more stuff into it, in which case what's already in there should be easy to infer from the documentation.

impl XmlSerialize for str {
fn serialize_child_nodes<W>(&self, writer: &mut Writer<W>) -> Result<(), Error>
where
Expand All @@ -23,6 +24,7 @@ impl XmlSerialize for str {
}
}

/// Serializes a reference to a string as a text content node.
impl<T> XmlSerialize for &T
where
T: AsRef<str>,
Expand All @@ -37,6 +39,7 @@ where
}
}

/// Serializes a string as a text content node.
impl XmlSerialize for String {
fn serialize_child_nodes<W>(&self, writer: &mut Writer<W>) -> Result<(), Error>
where
Expand All @@ -48,6 +51,7 @@ impl XmlSerialize for String {
}
}

/// Serializes a string as a text content node.
impl XmlSerialize for &str {
fn serialize_child_nodes<W>(&self, writer: &mut Writer<W>) -> Result<(), Error>
where
Expand All @@ -59,6 +63,9 @@ impl XmlSerialize for &str {
}
}

/// Serializes the contents of an `Option<T>` as content nodes.
///
/// `Some(t)` is serialized identically to `t`, while `None` produces no output.
impl<T> XmlSerialize for Option<T>
babolivier marked this conversation as resolved.
Show resolved Hide resolved
where
T: XmlSerialize,
Expand All @@ -84,6 +91,10 @@ where
}
}

/// Serializes the contents of a `Vec<T>` 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<T> XmlSerialize for Vec<T>
babolivier marked this conversation as resolved.
Show resolved Hide resolved
where
T: XmlSerialize,
Expand All @@ -104,12 +115,14 @@ where
}
}

/// 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<T> XmlSerializeAttr for &T
where
T: AsRef<str>,
Expand All @@ -119,18 +132,23 @@ where
}
}

/// 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<T>` as an XML attribute value.
///
/// `Some(t)` is serialized identically to `t`, while `None` produces no output.
impl<T> XmlSerializeAttr for Option<T>
where
T: XmlSerializeAttr,
Expand All @@ -153,6 +171,7 @@ where
macro_rules! impl_as_text_for {
($( $ty:ty ),*) => {
$(
/// Serializes an integer as a text content node.
impl XmlSerialize for $ty {
fn serialize_child_nodes<W>(&self, writer: &mut Writer<W>) -> Result<(), Error>
where
Expand All @@ -165,6 +184,7 @@ macro_rules! impl_as_text_for {
}
}

/// Serializes an integer as an XML attribute value.
impl XmlSerializeAttr for $ty {
fn serialize_as_attribute(
&self,
Expand Down
66 changes: 64 additions & 2 deletions xml_struct/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
//! 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.
Expand All @@ -39,8 +43,66 @@ 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,
/// Foobar(String),
babolivier marked this conversation as resolved.
Show resolved Hide resolved
/// }
///
/// impl XmlSerialize for Bar {
/// fn serialize_child_nodes<W>(&self, writer: &mut Writer<W>) -> Result<(), Error>
/// where
/// W: std::io::Write,
/// {
/// match self {
/// Self::Baz => writer.write_event(Event::Text(BytesText::new("BAZ")))?,
/// Self::Foobar(foobar) => foobar.serialize_as_element(writer, "Foobar")?,
/// }
///
/// Ok(())
/// }
/// }
///
/// let mut writer: Writer<Vec<u8>> = 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#"<FlyYouFoo xmlns="http://foo.example/"><SomeField>foo</SomeField>BAZ</FlyYouFoo>"#,
/// );
/// ```
pub trait XmlSerialize {
/// Serializes this value's child content nodes within an enclosing XML element.
/// Serializes this value as XML content nodes within an enclosing XML
/// element.
fn serialize_as_element<W>(&self, writer: &mut Writer<W>, name: &str) -> Result<(), Error>
where
W: std::io::Write,
Expand All @@ -54,7 +116,7 @@ pub trait XmlSerialize {
Ok(())
}

/// Serializes the child content nodes of this value.
/// Serializes this value as XML content nodes.
fn serialize_child_nodes<W>(&self, writer: &mut Writer<W>) -> Result<(), Error>
where
W: std::io::Write;
Expand Down
53 changes: 45 additions & 8 deletions xml_struct_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,11 @@ use crate::serialize::{write_serialize_impl_for_enum, write_serialize_impl_for_s
// This value must match the `attributes` attribute for the derive macro.
const MACRO_ATTRIBUTE: &str = "xml_struct";
babolivier marked this conversation as resolved.
Show resolved Hide resolved

/// A macro providing automated derivation of the `XmlSerialize` and
/// `XmlSerializeAttr` traits.
/// 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 field's name. When applied to an enum, the implementation
/// will write an XML element with a tag name derived from the variant's name,
/// and any fields on that variant will be serialized as children of that
/// element with tag names derived from the field's name.
/// derived from the name of the field.
///
/// For example, the following declaration corresponds to the following output:
///
Expand Down Expand Up @@ -50,7 +46,12 @@ const MACRO_ATTRIBUTE: &str = "xml_struct";
/// </Another>
/// ```
///
/// Likewise, the following enum corresponds to the following output structure:
/// 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
babolivier marked this conversation as resolved.
Show resolved Hide resolved
/// the name of the field.
///
/// As above, the following enum corresponds to the following output:
///
/// ```ignore
/// #[derive(XmlSerialize)]
Expand Down Expand Up @@ -84,13 +85,46 @@ const MACRO_ATTRIBUTE: &str = "xml_struct";
/// 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
babolivier marked this conversation as resolved.
Show resolved Hide resolved
///
/// The output from derived implementations may be configured with the
/// `xml_struct` attribute. The following options are available:
/// `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
/// <Baz foo:Another="I am text!">
babolivier marked this conversation as resolved.
Show resolved Hide resolved
/// ...
/// </Baz>
/// ```
///
/// 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
Expand Down Expand Up @@ -139,6 +173,9 @@ const MACRO_ATTRIBUTE: &str = "xml_struct";
///
/// ## 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,
Expand Down
45 changes: 31 additions & 14 deletions xml_struct_derive/src/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,19 @@ impl TypeProps {
let mut errors = Vec::new();

// We start with the default set of properties, then parse the
// `xml_struct` attribute to change any properties which deviate from
// the default.
// `xml_struct` attribute to modify any property which deviates from the
// default.
let mut properties = TypeProps::default();
babolivier marked this conversation as resolved.
Show resolved Hide resolved
for meta in attr.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)? {
match meta {
Meta::Path(path) => {
if path.is_ident("text") {
let is_unit_enum = match &input.data {
// 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()
Expand All @@ -85,7 +90,7 @@ impl TypeProps {
_ => false,
};

if is_unit_enum {
if is_unit_only_enum {
properties.should_serialize_as_text = true;
} else {
// There is no clear representation of non-unit enum
Expand All @@ -102,9 +107,10 @@ impl TypeProps {
}
Meta::NameValue(name_value) => {
if name_value.path.is_ident("default_ns") {
// When serialized as an element, values of this type should
// declare a default namespace, e.g. `xmlns="foo"`. A
// declaration of this type should occur at most once per type.
// 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(
Expand All @@ -119,9 +125,11 @@ impl TypeProps {
}
}
} else if name_value.path.is_ident("ns") {
// When serialized as an element, values of this type should
// declare a namespace with prefix, e.g. `xmlns:foo="bar"`.
// There can be many of these declarations per type.
// 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.
Expand All @@ -139,6 +147,9 @@ impl TypeProps {
)),
}
} 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 {
babolivier marked this conversation as resolved.
Show resolved Hide resolved
Some(_) => {
errors.push(Error::new(
Expand Down Expand Up @@ -183,6 +194,9 @@ impl TypeProps {
}

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",
Expand Down Expand Up @@ -234,7 +248,7 @@ impl FieldProps {
/// from its struct attributes.
pub(crate) fn try_from_attrs(
value: Vec<Attribute>,
is_named_field: bool,
field_has_name: bool,
) -> Result<Self, Error> {
// Find the attribute for configuring behavior of the derivation, if
// any.
Expand All @@ -254,14 +268,17 @@ impl FieldProps {
let mut errors = Vec::new();

// We start with the default set of properties, then parse the
// `xml_struct` attribute to change any properties which deviate from
// the default.
// `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::<Meta, Token![,]>::parse_terminated)? {
match meta {
Meta::Path(path) => {
if path.is_ident("attribute") {
babolivier marked this conversation as resolved.
Show resolved Hide resolved
babolivier marked this conversation as resolved.
Show resolved Hide resolved
if is_named_field {
// 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(
Expand Down
Loading