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

Implement choice #36

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
101 changes: 101 additions & 0 deletions xml_schema/tests/choice.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
extern crate yaserde_derive;

use xml_schema_derive::XmlSchema;
use yaserde::de::from_str;
use yaserde::ser::to_string;

#[test]
fn choice() {
#[derive(Debug, XmlSchema)]
#[xml_schema(source = "xml_schema/tests/choice.xsd")]
struct ChoiceTypeSchema;

use choice_type_schema::xml_schema_types::*;

let xml_1 = r#"<?xml version="1.0" encoding="UTF-8"?>
<person>
<firstname>John</firstname>
</person>
"#;

let sample_1: Person = from_str(xml_1).unwrap();

let model = Person {
firstname: Some(Firstname {
base: "John".to_string(),
scope: None,
}),
lastname: None,
Copy link
Contributor

Choose a reason for hiding this comment

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

In that case I expect more to retrieve an enum here.
The test is not easy to understand as a person has basically a first name and a last name, so it's not a choice, more a combination.

I think the example here is more relevant: https://www.w3schools.com/xml/el_choice.asp
Where. Person may have role employee or member.

So for me the expected code will be:

let model = Person::Employee("John".to_string());

// Generated part by xml-schema
enum Person {
  Employee(String),
  Member(String),
}

};

assert_eq!(sample_1, model);

let data = to_string(&model).unwrap();
assert_eq!(
data,
r#"<?xml version="1.0" encoding="utf-8"?><Person><firstname>John</firstname></Person>"#
);
}

#[test]
fn choice_sequence() {
#[derive(Debug, XmlSchema)]
#[xml_schema(source = "xml_schema/tests/choice_sequence.xsd")]
struct ChoiceTypeSchema;

use choice_type_schema::xml_schema_types::*;

let xml_1 = r#"<?xml version="1.0" encoding="UTF-8"?>
<person>
<name>Doe</name>
<firstname>John</firstname>
</person>
"#;

let sample_1: Person = from_str(xml_1).unwrap();

let model = Person {
name: "Doe".to_string(),
firstname: Some(Firstname {
base: "John".to_string(),
scope: None,
}),
lastname: None,
};

assert_eq!(sample_1, model);

let data = to_string(&model).unwrap();
assert_eq!(
data,
r#"<?xml version="1.0" encoding="utf-8"?><Person><name>Doe</name><firstname>John</firstname></Person>"#
);
}

#[test]
fn choice_multiple() {
#[derive(Debug, XmlSchema)]
#[xml_schema(source = "xml_schema/tests/choice_multiple.xsd")]
struct ChoiceTypeSchema;

let xml_1 = r#"<?xml version="1.0" encoding="UTF-8"?>
<person>
<firstname>John</firstname>
</person>
"#;

let sample_1: Person = from_str(xml_1).unwrap();

let model = Person {
firstname: "John".to_string(),
lastname: "".to_string(),
};
Copy link
Contributor

Choose a reason for hiding this comment

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

in that case I expect to have:

Person {
  parents: vec![Parent::Firstname("John".to_string())],
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup exactly, I just patched this in one commit just to get it to compile. I am currently working on making this a vector.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh sorry I just implemented:

  let model = Person {
    firstname_list: vec!["John".to_string()],
    lastname_list: vec![],
  };

which i notice now is a bit different from what you imagined.

Copy link
Contributor Author

@matzipan matzipan Jan 4, 2024

Choose a reason for hiding this comment

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

I have some attempts which try to implement this as enum, you can see the last commit here: 740c37b

Unforuntately the test fails with:

thread 'choice_multiple' panicked at xml_schema/tests/choice.rs:99:3:
assertion `left == right` failed
  left: Gift { content: [] }
 right: Gift { content: [price_history(1), price_history(3)] }

So I probably got the yaserde config wrong :(

I will pause this work for now.


assert_eq!(sample_1, model);

let data = to_string(&model).unwrap();
assert_eq!(
data,
r#"<?xml version="1.0" encoding="utf-8"?><Person><firstname>John</firstname></Person>"#
);
}
25 changes: 25 additions & 0 deletions xml_schema/tests/choice.xsd
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="firstname">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="scope" type="xs:anyURI" use="optional" default="http://example.com#elements"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>

<xs:complexType name="lastname">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="scope" type="xs:anyURI" use="optional" default="http://example.com#elements"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>


<xs:complexType name="person">
<xs:choice minOccurs="0">
<xs:element name="firstname" type="firstname"/>
<xs:element name="lastname" type="lastname"/>
</xs:choice>
</xs:complexType>
</xs:schema>
10 changes: 10 additions & 0 deletions xml_schema/tests/choice_multiple.xsd
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="person">
<xs:complexType name="parents">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="firstname" type="xs:string"/>
<xs:element name="lastname" type="xs:string"/>
</xs:choice>
</xs:complexType>
</xs:element>
</xs:schema>
29 changes: 29 additions & 0 deletions xml_schema/tests/choice_sequence.xsd
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="firstname">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="scope" type="xs:anyURI" use="optional" default="http://example.com#elements"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>

<xs:complexType name="lastname">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="scope" type="xs:anyURI" use="optional" default="http://example.com#elements"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>

<xs:element name="person">
<xs:complexType name="parent">
<xs:sequence>
<xs:element name="name" type="xs:string" minOccurs="1"/>
<xs:choice>
<xs:element name="firstname" type="firstname"/>
<xs:element name="lastname" type="lastname"/>
</xs:choice>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
89 changes: 89 additions & 0 deletions xml_schema_derive/src/xsd/choice.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//! The children of a choice are mapped to Option fields.
//! Generating an enum would have been the better way but the choice element
//! may not have a name, so it's impossible to name the generated Rust enum.
//! The enum would have been nice to avoid runtime checks that only a single choice element is used.

use crate::xsd::{
annotation::Annotation, attribute::Attribute, element::Element, max_occurences::MaxOccurences,
Implementation, XsdContext,
};
use log::info;
use proc_macro2::TokenStream;

#[derive(Clone, Default, Debug, PartialEq, YaDeserialize)]
#[yaserde(
rename = "choice"
prefix = "xs",
namespace = "xs: http://www.w3.org/2001/XMLSchema"
)]
pub struct Choice {
#[yaserde(attribute)]
pub id: Option<String>,
#[yaserde(rename = "attribute")]
pub attributes: Vec<Attribute>,
#[yaserde(rename = "minOccurs", attribute)]
pub min_occurences: Option<u64>,
#[yaserde(rename = "maxOccurs", attribute)]
pub max_occurences: Option<MaxOccurences>,
#[yaserde(rename = "annotation")]
pub annotation: Option<Annotation>,
#[yaserde(rename = "element")]
pub element: Vec<Element>,
}

impl Implementation for Choice {
fn implement(
&self,
namespace_definition: &TokenStream,
prefix: &Option<String>,
context: &XsdContext,
) -> TokenStream {
let elements: TokenStream = self
.element
.iter()
.map(|element| element.implement(&namespace_definition, prefix, context))
.collect();

quote! {
#elements
}
}
}

impl Choice {
pub fn get_sub_types_implementation(
&self,
context: &XsdContext,
namespace_definition: &TokenStream,
prefix: &Option<String>,
) -> TokenStream {
info!("Generate choice sub types implementation");
self
.element
.iter()
.map(|element| element.get_subtypes_implementation(namespace_definition, prefix, context))
.collect()
}

pub fn get_field_implementation(
&self,
context: &XsdContext,
prefix: &Option<String>,
) -> TokenStream {
info!("Generate choice elements");

let multiple = matches!(self.min_occurences, Some(min_occurences) if min_occurences > 1)
|| matches!(self.max_occurences, Some(MaxOccurences::Unbounded))
|| matches!(self.max_occurences, Some(MaxOccurences::Number{value}) if value > 1);

// Element fields are by default declared as Option type due to the nature of the choice element.
// Since a vector can also be empty, use Vec<_>, rather than Option<Vec<_>>.
let optional = !multiple;

self
.element
.iter()
.map(|element| element.get_field_implementation(context, prefix, optional))
.collect()
}
}
33 changes: 29 additions & 4 deletions xml_schema_derive/src/xsd/complex_type.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::xsd::{
annotation::Annotation, attribute::Attribute, complex_content::ComplexContent,
annotation::Annotation, attribute::Attribute, choice::Choice, complex_content::ComplexContent,
sequence::Sequence, simple_content::SimpleContent, Implementation, XsdContext,
};
use heck::ToUpperCamelCase;
Expand All @@ -17,13 +17,16 @@ pub struct ComplexType {
pub name: String,
#[yaserde(rename = "attribute")]
pub attributes: Vec<Attribute>,
#[yaserde(rename = "sequence")]
pub sequence: Option<Sequence>,
#[yaserde(rename = "simpleContent")]
pub simple_content: Option<SimpleContent>,
#[yaserde(rename = "complexContent")]
pub complex_content: Option<ComplexContent>,
#[yaserde(rename = "annotation")]
pub annotation: Option<Annotation>,
#[yaserde(rename = "choice")]
pub choice: Option<Choice>,
}

impl Implementation for ComplexType {
Expand Down Expand Up @@ -69,7 +72,7 @@ impl Implementation for ComplexType {
.map(|attribute| attribute.implement(namespace_definition, prefix, context))
.collect();

let sub_types_implementation = self
let sequence_sub_types = self
.sequence
.as_ref()
.map(|sequence| sequence.get_sub_types_implementation(context, namespace_definition, prefix))
Expand All @@ -81,6 +84,18 @@ impl Implementation for ComplexType {
.map(|annotation| annotation.implement(namespace_definition, prefix, context))
.unwrap_or_default();

let choice_sub_types = self
.choice
.as_ref()
.map(|choice| choice.get_sub_types_implementation(context, &namespace_definition, prefix))
.unwrap_or_else(TokenStream::new);

let choice_field = self
.choice
.as_ref()
.map(|choice| choice.get_field_implementation(context, prefix))
.unwrap_or_else(TokenStream::new);

quote! {
#docs

Expand All @@ -90,10 +105,12 @@ impl Implementation for ComplexType {
#sequence
#simple_content
#complex_content
#choice_field
#attributes
}

#sub_types_implementation
#sequence_sub_types
#choice_sub_types
}
}
}
Expand All @@ -110,12 +127,20 @@ impl ComplexType {
.as_ref()
.map(|sequence| sequence.get_field_implementation(context, prefix))
.unwrap_or_default()
} else {
} else if self.simple_content.is_some() {
self
.simple_content
.as_ref()
.map(|simple_content| simple_content.get_field_implementation(context, prefix))
.unwrap_or_default()
} else if self.choice.is_some() {
self
.choice
.as_ref()
.map(|choice| choice.get_field_implementation(context, prefix))
.unwrap_or_else(TokenStream::new)
} else {
TokenStream::new()
}
}

Expand Down
27 changes: 15 additions & 12 deletions xml_schema_derive/src/xsd/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ impl Element {
&self,
context: &XsdContext,
prefix: &Option<String>,
optional: bool,
) -> TokenStream {
if self.name.is_empty() {
return quote!();
Expand Down Expand Up @@ -146,17 +147,6 @@ impl Element {
rust_type
};

let rust_type = if !multiple && self.min_occurences == Some(0) {
quote!(Option<#rust_type>)
} else {
rust_type
};

let prefix_attribute = prefix
.as_ref()
.map(|prefix| quote!(, prefix=#prefix))
.unwrap_or_default();

let module = (!context.is_in_sub_module()
&& !self
.kind
Expand All @@ -169,9 +159,22 @@ impl Element {
.then_some(quote!(xml_schema_types::))
.unwrap_or_default();

let rust_type = quote!(#module#rust_type);

let rust_type = if optional || (!multiple && self.min_occurences == Some(0)) {
quote!(Option<#rust_type>)
} else {
rust_type
};

let prefix_attribute = prefix
.as_ref()
.map(|prefix| quote!(, prefix=#prefix))
.unwrap_or_default();

quote! {
#[yaserde(rename=#yaserde_rename #prefix_attribute)]
pub #attribute_name: #module#rust_type,
pub #attribute_name: #rust_type,
}
}
}
Expand Down
Loading