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 choice type support #20

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions xml_schema/tests/choice.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#[macro_use]
extern crate yaserde_derive;

use log::debug;
use std::io::prelude::*;
use xml_schema_derive::XmlSchema;
use yaserde::de::from_str;
use yaserde::ser::to_string;
use yaserde::{YaDeserialize, YaSerialize};

#[test]
fn choice() {
#[derive(Debug, XmlSchema)]
#[xml_schema(source = "xml_schema/tests/choice.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: Some(Firstname {
content: "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><firstname>John</firstname></Person>"#
);
}

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

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 {
content: "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 {
firstnames: vec!["John".to_string()],
lastnames: vec![],
};

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>
91 changes: 91 additions & 0 deletions xml_schema_derive/src/xsd/choice.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//! 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::{debug, info};
use proc_macro2::TokenStream;
use std::io::prelude::*;
use yaserde::YaDeserialize;

#[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, multiple, 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::CamelCase;
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_else(TokenStream::new);

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_else(TokenStream::new)
} 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_else(TokenStream::new)
} 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
4 changes: 3 additions & 1 deletion xml_schema_derive/src/xsd/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ impl Element {
&self,
context: &XsdContext,
prefix: &Option<String>,
multiple: bool,
optional: bool,
) -> TokenStream {
if self.name.is_empty() {
return quote!();
Expand Down Expand Up @@ -142,7 +144,7 @@ impl Element {
rust_type
};

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