Repository: juhaku/utoipa
Pull request #41 (2022-03-13 16:45:17)

Add xml schema object
* Add xml schema object. This allows users to define xml styles for properties of objects.
* Add tests


Patch:
diff --git a/src/openapi/mod.rs b/src/openapi/mod.rs
index d76f4f5..57fd905 100644
--- a/src/openapi/mod.rs
+++ b/src/openapi/mod.rs
@@ -33,6 +33,7 @@ pub mod schema;
 pub mod security;
 pub mod server;
 pub mod tag;
+pub mod xml;
 
 const OPENAPI_VERSION_3: &str = "3.0.3";
 
diff --git a/src/openapi/schema.rs b/src/openapi/schema.rs
index fed6b71..9dec397 100644
--- a/src/openapi/schema.rs
+++ b/src/openapi/schema.rs
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
 #[cfg(feature = "serde_json")]
 use serde_json::Value;
 
-use super::{security::SecuritySchema, Deprecated};
+use super::{security::SecuritySchema, xml::Xml, Deprecated};
 
 #[non_exhaustive]
 #[derive(Serialize, Deserialize, Default, Clone)]
@@ -185,6 +185,9 @@ pub struct Property {
 
     #[serde(skip_serializing_if = "Option::is_none")]
     read_only: Option<bool>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    xml: Option<Xml>,
 }
 
 impl Property {
@@ -263,6 +266,12 @@ impl Property {
 
         self
     }
+
+    pub fn with_xml(mut self, xml: Xml) -> Self {
+        self.xml = Some(xml);
+
+        self
+    }
 }
 
 impl From<Property> for Component {
@@ -303,6 +312,9 @@ pub struct Object {
     #[serde(skip_serializing_if = "Option::is_none")]
     #[cfg(not(feature = "serde_json"))]
     example: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    xml: Option<Xml>,
 }
 
 impl Object {
@@ -354,6 +366,12 @@ impl Object {
 
         self
     }
+
+    pub fn with_xml(mut self, xml: Xml) -> Self {
+        self.xml = Some(xml);
+
+        self
+    }
 }
 
 impl From<Object> for Component {
@@ -413,6 +431,9 @@ pub struct Array {
 
     #[serde(skip_serializing_if = "Option::is_none")]
     min_items: Option<usize>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    xml: Option<Xml>,
 }
 
 impl Array {
@@ -435,6 +456,12 @@ impl Array {
 
         self
     }
+
+    pub fn with_xml(mut self, xml: Xml) -> Self {
+        self.xml = Some(xml);
+
+        self
+    }
 }
 
 impl From<Array> for Component {
diff --git a/src/openapi/xml.rs b/src/openapi/xml.rs
new file mode 100644
index 0000000..555e91b
--- /dev/null
+++ b/src/openapi/xml.rs
@@ -0,0 +1,60 @@
+use std::borrow::Cow;
+
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Default, Clone)]
+#[cfg_attr(feature = "debug", derive(Debug))]
+pub struct Xml {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub name: Option<Cow<'static, str>>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub namespace: Option<Cow<'static, str>>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub prefix: Option<Cow<'static, str>>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub attribute: Option<bool>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub wrapped: Option<bool>,
+}
+
+impl Xml {
+    pub fn new() -> Self {
+        Self {
+            ..Default::default()
+        }
+    }
+
+    pub fn name<S: Into<Cow<'static, str>>>(mut self, name: Option<S>) -> Self {
+        self.name = name.map(|name| name.into());
+
+        self
+    }
+
+    pub fn namespace<S: Into<Cow<'static, str>>>(mut self, namespace: Option<S>) -> Self {
+        self.namespace = namespace.map(|namespace| namespace.into());
+
+        self
+    }
+
+    pub fn prefix<S: Into<Cow<'static, str>>>(mut self, prefix: Option<S>) -> Self {
+        self.prefix = prefix.map(|prefix| prefix.into());
+
+        self
+    }
+
+    pub fn attribute(mut self, attribute: Option<bool>) -> Self {
+        self.attribute = attribute;
+
+        self
+    }
+
+    pub fn wrapped(mut self, wrapped: Option<bool>) -> Self {
+        self.wrapped = wrapped;
+
+        self
+    }
+}
diff --git a/utoipa-gen/src/component.rs b/utoipa-gen/src/component.rs
index e07ec71..b1f4f98 100644
--- a/utoipa-gen/src/component.rs
+++ b/utoipa-gen/src/component.rs
@@ -15,9 +15,13 @@ use crate::{
     Array, Deprecated,
 };
 
-use self::attr::{ComponentAttr, Enum, NamedField, UnnamedFieldStruct};
+use self::{
+    attr::{ComponentAttr, Enum, NamedField, UnnamedFieldStruct},
+    xml::Xml,
+};
 
 mod attr;
+mod xml;
 
 pub struct Component<'a> {
     ident: &'a Ident,
@@ -126,15 +130,22 @@ impl ToTokens for NamedStructComponent<'_> {
             let field_name = &*field.ident.as_ref().unwrap().to_string();
 
             let component_part = &ComponentPart::from_type(&field.ty);
-
             let deprecated = get_deprecated(&field.attrs);
-            let attrs = attr::parse_component_attr::<ComponentAttr<NamedField>>(&field.attrs);
+            let attrs = ComponentAttr::<NamedField>::from_attributes_validated(
+                &field.attrs,
+                component_part,
+            );
+            let xml_value = attrs
+                .as_ref()
+                .and_then(|named_field| named_field.as_ref().xml.as_ref());
             let comments = CommentAttributes::from_attributes(&field.attrs);
+
             let component = ComponentProperty::new(
                 component_part,
                 Some(&comments),
                 attrs.as_ref(),
                 deprecated.as_ref(),
+                xml_value,
             );
 
             tokens.extend(quote! {
@@ -152,7 +163,7 @@ impl ToTokens for NamedStructComponent<'_> {
             tokens.extend(quote! { .with_deprecated(#deprecated) });
         }
 
-        let attrs = attr::parse_component_attr::<ComponentAttr<attr::Struct>>(self.attributes);
+        let attrs = ComponentAttr::<attr::Struct>::from_attributes_validated(self.attributes);
         if let Some(attrs) = attrs {
             tokens.extend(attrs.to_token_stream());
         }
@@ -192,7 +203,7 @@ impl ToTokens for UnnamedStructComponent<'_> {
         let deprecated = get_deprecated(self.attributes);
         if all_fields_are_same {
             tokens.extend(
-                ComponentProperty::new(first_part, None, attrs.as_ref(), deprecated.as_ref())
+                ComponentProperty::new(first_part, None, attrs.as_ref(), deprecated.as_ref(), None)
                     .to_token_stream(),
             );
         } else {
@@ -522,6 +533,7 @@ struct ComponentProperty<'a, T> {
     comments: Option<&'a CommentAttributes>,
     attrs: Option<&'a ComponentAttr<T>>,
     deprecated: Option<&'a Deprecated>,
+    xml: Option<&'a Xml>,
 }
 
 impl<'a, T: Sized + ToTokens> ComponentProperty<'a, T> {
@@ -530,12 +542,14 @@ impl<'a, T: Sized + ToTokens> ComponentProperty<'a, T> {
         comments: Option<&'a CommentAttributes>,
         attrs: Option<&'a ComponentAttr<T>>,
         deprecated: Option<&'a Deprecated>,
+        xml: Option<&'a Xml>,
     ) -> Self {
         Self {
             component_part,
             comments,
             attrs,
             deprecated,
+            xml,
         }
     }
 
@@ -570,11 +584,21 @@ where
                     self.comments,
                     self.attrs,
                     self.deprecated,
+                    self.xml,
                 );
 
                 tokens.extend(quote! {
                     #component_property.to_array()
                 });
+
+                if let Some(xml_value) = self.xml {
+                    match xml_value {
+                        Xml::Slice { vec, value: _ } => tokens.extend(quote! {
+                            .with_xml(#vec)
+                        }),
+                        Xml::NonSlice(_) => (),
+                    }
+                }
             }
             Some(GenericType::Option)
             | Some(GenericType::Cow)
@@ -585,6 +609,7 @@ where
                     self.comments,
                     self.attrs,
                     self.deprecated,
+                    self.xml,
                 );
 
                 tokens.extend(component_property.into_token_stream())
@@ -619,6 +644,17 @@ where
                     if let Some(attributes) = self.attrs {
                         tokens.extend(attributes.to_token_stream())
                     }
+
+                    if let Some(xml_value) = self.xml {
+                        match xml_value {
+                            Xml::Slice { vec: _, value } => tokens.extend(quote! {
+                                .with_xml(#value)
+                            }),
+                            Xml::NonSlice(xml) => tokens.extend(quote! {
+                                .with_xml(#xml)
+                            }),
+                        }
+                    }
                 }
                 ValueType::Object => {
                     let name = &*self.component_part.ident.to_string();
diff --git a/utoipa-gen/src/component/attr.rs b/utoipa-gen/src/component/attr.rs
index a474105..59f3775 100644
--- a/utoipa-gen/src/component/attr.rs
+++ b/utoipa-gen/src/component/attr.rs
@@ -1,13 +1,21 @@
+use std::mem;
+
 use proc_macro2::{Ident, TokenStream};
 use proc_macro_error::{abort, ResultExt};
 use quote::{quote, ToTokens};
 use syn::{
+    parenthesized,
     parse::{Parse, ParseBuffer},
     Attribute, Error, ExprPath, Lit, Token,
 };
 
 use crate::{parse_utils, Example};
 
+use super::{
+    xml::{Xml, XmlAttr},
+    ComponentPart,
+};
+
 #[cfg_attr(feature = "debug", derive(Debug))]
 pub struct ComponentAttr<T>
 where
@@ -32,9 +40,11 @@ pub struct Enum {
     example: Option<TokenStream>,
 }
 
+#[derive(Default)]
 #[cfg_attr(feature = "debug", derive(Debug))]
 pub struct Struct {
     example: Option<Example>,
+    xml_attr: Option<XmlAttr>,
 }
 
 #[derive(Default)]
@@ -52,6 +62,8 @@ pub struct NamedField {
     default: Option<TokenStream>,
     write_only: Option<bool>,
     read_only: Option<bool>,
+    xml_attr: Option<XmlAttr>,
+    pub(super) xml: Option<Xml>,
 }
 
 impl Parse for ComponentAttr<Enum> {
@@ -98,28 +110,56 @@ impl Parse for ComponentAttr<Enum> {
     }
 }
 
+impl ComponentAttr<Struct> {
+    pub(super) fn from_attributes_validated(attributes: &[Attribute]) -> Option<Self> {
+        parse_component_attr::<ComponentAttr<Struct>>(attributes).map(|attrs| {
+            if let Some(ref wrapped_ident) = attrs
+                .as_ref()
+                .xml_attr
+                .as_ref()
+                .and_then(|xml| xml.is_wrapped.as_ref())
+            {
+                abort! {wrapped_ident, "cannot use `wrapped` attribute in non slice type";
+                    help = "Try removing `wrapped` attribute"
+                }
+            }
+
+            attrs
+        })
+    }
+}
+
 impl Parse for ComponentAttr<Struct> {
     fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
-        let ident = input
-            .parse::<Ident>()
-            .expect_or_abort("Unparseable ComponentAttr<Struct>, expected Ident");
-        let name = &*ident.to_string();
-
-        match name {
-            "example" => {
-                let example = parse_utils::parse_next_lit_str_or_json_example(input, &ident);
-
-                Ok(Self {
-                    inner: Struct {
-                        example: Some(example),
-                    },
-                })
+        let mut struct_ = Struct::default();
+
+        while !input.is_empty() {
+            let ident = input
+                .parse::<Ident>()
+                .expect_or_abort("Unparseable ComponentAttr<Struct>, expected Ident");
+            let name = &*ident.to_string();
+
+            match name {
+                "example" => {
+                    struct_.example = Some(parse_utils::parse_next_lit_str_or_json_example(
+                        input, &ident,
+                    ));
+                }
+                "xml" => {
+                    let xml;
+                    parenthesized!(xml in input);
+                    struct_.xml_attr = Some(xml.parse()?)
+                }
+                _ => {
+                    return Err(Error::new(
+                        ident.span(),
+                        format!("unexpected identifer: {name}, expected one of: example, xml"),
+                    ))
+                }
             }
-            _ => Err(Error::new(
-                ident.span(),
-                format!("unexpected identifer: {}, expected: example", name),
-            )),
         }
+
+        Ok(Self { inner: struct_ })
     }
 }
 
@@ -170,6 +210,49 @@ impl Parse for ComponentAttr<UnnamedFieldStruct> {
     }
 }
 
+impl ComponentAttr<NamedField> {
+    pub(super) fn from_attributes_validated(
+        attributes: &[Attribute],
+        component_part: &ComponentPart,
+    ) -> Option<Self> {
+        parse_component_attr::<ComponentAttr<NamedField>>(attributes).map(|attrs| {
+            if !matches!(
+                component_part.generic_type,
+                Some(crate::component::GenericType::Vec)
+            ) {
+                if let Some(ref wrapped_ident) = attrs
+                    .as_ref()
+                    .xml_attr
+                    .as_ref()
+                    .and_then(|xml| xml.is_wrapped.as_ref())
+                {
+                    abort! {wrapped_ident, "cannot use `wrapped` attribute in non slice field type";
+                        help = "Try removing `wrapped` attribute or make your field `Vec`"
+                    }
+                }
+            }
+
+            attrs
+        })
+        .map(|mut attrs| {
+            if matches!(component_part.generic_type, Some(super::GenericType::Vec)) {
+                if let Some(ref mut xml) = attrs.inner.xml_attr {
+                    let mut value_xml = mem::take(xml);
+                    let vec_xml = XmlAttr::with_wrapped(
+                        mem::take(&mut value_xml.is_wrapped),
+                        mem::take(&mut value_xml.wrap_name));
+
+                    attrs.inner.xml = Some(Xml::Slice{vec: vec_xml, value: value_xml});
+                }
+            } else if let Some(ref mut xml) = attrs.inner.xml_attr {
+                attrs.inner.xml = Some(Xml::NonSlice(mem::take(xml)));
+            }
+
+            attrs
+        })
+    }
+}
+
 impl Parse for ComponentAttr<NamedField> {
     fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
         let mut field = NamedField::default();
@@ -205,13 +288,18 @@ impl Parse for ComponentAttr<NamedField> {
                         parse_lit_or_fn_ref_as_token_stream(input, name)
                     }))
                 }
-                "write_only" => field.write_only = Some(parse_utils::parse_bool_or_true(input)),
-                "read_only" => field.read_only = Some(parse_utils::parse_bool_or_true(input)),
+                "write_only" => field.write_only = Some(parse_utils::parse_bool_or_true(input)?),
+                "read_only" => field.read_only = Some(parse_utils::parse_bool_or_true(input)?),
+                "xml" => {
+                    let xml;
+                    parenthesized!(xml in input);
+                    field.xml_attr = Some(xml.parse()?)
+                }
                 _ => {
                     return Err(Error::new(
                         ident.span(),
                         format!(
-                            "unexpected identifier: {}, expected any of: example, format, default, write_only, read_only",
+                            "unexpected identifier: {}, expected any of: example, format, default, write_only, read_only, xml",
                             name
                         ),
                     ))
@@ -312,6 +400,11 @@ impl ToTokens for Struct {
                 .with_example(#example)
             })
         }
+        if let Some(ref xml) = self.xml_attr {
+            tokens.extend(quote!(
+                 .with_xml(#xml)
+            ))
+        }
     }
 }
 
diff --git a/utoipa-gen/src/component/xml.rs b/utoipa-gen/src/component/xml.rs
new file mode 100644
index 0000000..9d4f729
--- /dev/null
+++ b/utoipa-gen/src/component/xml.rs
@@ -0,0 +1,119 @@
+use proc_macro2::Ident;
+use quote::{quote, ToTokens};
+use syn::{parenthesized, parse::Parse, token::Paren, LitStr, Token};
+
+use crate::parse_utils;
+
+#[derive(Default)]
+#[cfg_attr(feature = "debug", derive(Debug))]
+pub(super) struct XmlAttr {
+    pub(super) name: Option<String>,
+    pub(super) namespace: Option<String>,
+    pub(super) prefix: Option<String>,
+    pub(super) is_attribute: bool,
+    pub(super) is_wrapped: Option<Ident>,
+    pub(super) wrap_name: Option<String>,
+}
+
+impl XmlAttr {
+    pub(super) fn with_wrapped(is_wrapped: Option<Ident>, wrap_name: Option<String>) -> Self {
+        Self {
+            is_wrapped,
+            wrap_name,
+            ..Default::default()
+        }
+    }
+}
+
+impl Parse for XmlAttr {
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let mut xml = XmlAttr::default();
+
+        while !input.is_empty() {
+            let attribute = input.parse::<Ident>()?;
+            let attribute_name = &*attribute.to_string();
+
+            match attribute_name {
+                "name" => xml.name = Some(parse_utils::parse_next(input, || input.parse::<LitStr>())?.value()),
+                "namespace" => xml.namespace = Some(parse_utils::parse_next(input, || input.parse::<LitStr>())?.value()),
+                "prefix" => xml.prefix = Some(parse_utils::parse_next(input, || input.parse::<LitStr>())?.value()),
+                "attribute" => xml.is_attribute = parse_utils::parse_bool_or_true(input)?,
+                // wrapped or wrapped(name = "wrap_name")
+                "wrapped" => {
+                    if input.peek(Paren) {
+                        let group;
+                        parenthesized!(group in input);
+
+                        let wrapped_attribute =group.parse::<Ident>()?;
+                        if wrapped_attribute != "name" {
+                            return Err(syn::Error::new(wrapped_attribute.span(), "unexpected wrapped attribute, expected: name"));
+                        }
+                        group.parse::<Token![=]>()?;
+                        xml.wrap_name = Some(group.parse::<LitStr>()?.value());
+                    }
+                    xml.is_wrapped = Some(attribute);
+                },
+                _ => {
+                    return Err(syn::Error::new(attribute.span(), &format!("unexpected attribute: {attribute_name}, expected one of: name, namespace, prefix, attribute, wrapped")))
+                },
+            }
+
+            if input.peek(Token![,]) {
+                input.parse::<Token![,]>().unwrap();
+            }
+        }
+
+        Ok(xml)
+    }
+}
+
+impl ToTokens for XmlAttr {
+    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
+        tokens.extend(quote! {
+            utoipa::openapi::xml::Xml::new()
+        });
+
+        if let Some(ref name) = self.name {
+            tokens.extend(quote! {
+                .name(Some(#name))
+            })
+        }
+
+        if let Some(ref namespace) = self.namespace {
+            tokens.extend(quote! {
+                .namespace(Some(#namespace))
+            })
+        }
+
+        if let Some(ref prefix) = self.prefix {
+            tokens.extend(quote! {
+                .prefix(Some(#prefix))
+            })
+        }
+
+        if self.is_attribute {
+            tokens.extend(quote! {
+                .attribute(Some(true))
+            })
+        }
+
+        if self.is_wrapped.is_some() {
+            tokens.extend(quote! {
+                .wrapped(Some(true))
+            });
+
+            // if is wrapped and wrap name is defined use wrap name instead
+            if let Some(ref wrap_name) = self.wrap_name {
+                tokens.extend(quote! {
+                    .name(Some(#wrap_name))
+                })
+            }
+        }
+    }
+}
+
+#[cfg_attr(feature = "debug", derive(Debug))]
+pub(super) enum Xml {
+    NonSlice(XmlAttr),
+    Slice { vec: XmlAttr, value: XmlAttr },
+}
diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs
index 66ac22f..a1d8cee 100644
--- a/utoipa-gen/src/lib.rs
+++ b/utoipa-gen/src/lib.rs
@@ -687,13 +687,13 @@ mod parse_utils {
         })
     }
 
-    pub fn parse_bool_or_true(input: ParseStream) -> bool {
+    pub fn parse_bool_or_true(input: ParseStream) -> Result<bool, syn::Error> {
         if input.peek(Token![=]) && input.peek2(LitBool) {
-            input.parse::<Token![=]>().unwrap();
+            input.parse::<Token![=]>()?;
 
-            input.parse::<LitBool>().unwrap().value()
+            Ok(input.parse::<LitBool>()?.value())
         } else {
-            true
+            Ok(true)
         }
     }
 
diff --git a/utoipa-gen/src/path/parameter.rs b/utoipa-gen/src/path/parameter.rs
index c2f6b25..826869e 100644
--- a/utoipa-gen/src/path/parameter.rs
+++ b/utoipa-gen/src/path/parameter.rs
@@ -82,7 +82,7 @@ impl Parse for Parameter {
                 "path" | "query" | "header" | "cookie" => {
                     parameter.parameter_in = name.parse::<ParameterIn>().unwrap_or_abort();
                 }
-                "deprecated" => parameter.deprecated = parse_utils::parse_bool_or_true(input),
+                "deprecated" => parameter.deprecated = parse_utils::parse_bool_or_true(input)?,
                 "description" => {
                     parameter.description = parse_utils::parse_next(input, || {
                         Some(
diff --git a/utoipa-gen/src/security_requirement.rs b/utoipa-gen/src/security_requirement.rs
index 1c28ac8..d879282 100644
--- a/utoipa-gen/src/security_requirement.rs
+++ b/utoipa-gen/src/security_requirement.rs
@@ -67,7 +67,7 @@ pub fn security_requirements_to_tokens(
         if let (Some(name), Some(scopes)) = (&security.name, &security.scopes) {
             let scopes_array = scopes.iter().collect::<Array<&String>>();
             let scopes_len = scopes.len();
-            
+
             quote! {
                 utoipa::openapi::security::SecurityRequirement::new::<&str, [&str; #scopes_len], &str>(#name, #scopes_array)
             }


Test patch:
diff --git a/tests/common.rs b/tests/common.rs
index 7abccba..0f564a6 100644
--- a/tests/common.rs
+++ b/tests/common.rs
@@ -21,6 +21,7 @@ pub fn value_as_string(value: Option<&'_ Value>) -> String {
     value.unwrap_or(&Value::Null).to_string()
 }
 
+#[allow(unused)]
 pub fn assert_json_array_len(value: &Value, len: usize) {
     match value {
         Value::Array(array) => assert_eq!(
diff --git a/tests/component_derive_test.rs b/tests/component_derive_test.rs
index 0b72792..84bc82d 100644
--- a/tests/component_derive_test.rs
+++ b/tests/component_derive_test.rs
@@ -549,3 +549,55 @@ fn derive_struct_with_read_only_and_write_only() {
         "properties.username.writeOnly" = r###"null"###, "User username write only"
     }
 }
+
+#[test]
+fn derive_struct_xml() {
+    let user = api_doc! {
+        #[component(xml(name = "user", prefix = "u", namespace = "https://mynamespace.test"))]
+        struct User {
+            #[component(xml(attribute, prefix = "u"))]
+            id: i64,
+            #[component(xml(name = "user_name", prefix = "u"))]
+            username: String,
+            #[component(xml(wrapped(name = "linkList"), name = "link"))]
+            links: Vec<String>,
+            #[component(xml(wrapped, name = "photo_url"))]
+            photos_urls: Vec<String>
+        }
+    };
+
+    assert_value! {user=>
+        "xml.attribute" = r###"null"###, "User xml attribute"
+        "xml.name" = r###""user""###, "User xml name"
+        "xml.prefix" = r###""u""###, "User xml prefix"
+        "xml.namespace" = r###""https://mynamespace.test""###, "User xml namespace"
+        "properties.id.xml.attribute" = r###"true"###, "User id xml attribute"
+        "properties.id.xml.name" = r###"null"###, "User id xml name"
+        "properties.id.xml.prefix" = r###""u""###, "User id xml prefix"
+        "properties.id.xml.namespace" = r###"null"###, "User id xml namespace"
+        "properties.username.xml.attribute" = r###"null"###, "User username xml attribute"
+        "properties.username.xml.name" = r###""user_name""###, "User username xml name"
+        "properties.username.xml.prefix" = r###""u""###, "User username xml prefix"
+        "properties.username.xml.namespace" = r###"null"###, "User username xml namespace"
+        "properties.links.xml.attribute" = r###"null"###, "User links xml attribute"
+        "properties.links.xml.name" = r###""linkList""###, "User links xml name"
+        "properties.links.xml.prefix" = r###"null"###, "User links xml prefix"
+        "properties.links.xml.namespace" = r###"null"###, "User links xml namespace"
+        "properties.links.xml.wrapped" = r###"true"###, "User links xml wrapped"
+        "properties.links.items.xml.attribute" = r###"null"###, "User links xml items attribute"
+        "properties.links.items.xml.name" = r###""link""###, "User links xml items name"
+        "properties.links.items.xml.prefix" = r###"null"###, "User links xml items prefix"
+        "properties.links.items.xml.namespace" = r###"null"###, "User links xml items namespace"
+        "properties.links.items.xml.wrapped" = r###"null"###, "User links xml items wrapped"
+        "properties.photos_urls.xml.attribute" = r###"null"###, "User photos_urls xml attribute"
+        "properties.photos_urls.xml.name" = r###"null"###, "User photos_urls xml name"
+        "properties.photos_urls.xml.prefix" = r###"null"###, "User photos_urls xml prefix"
+        "properties.photos_urls.xml.namespace" = r###"null"###, "User photos_urls xml namespace"
+        "properties.photos_urls.xml.wrapped" = r###"true"###, "User photos_urls xml wrapped"
+        "properties.photos_urls.items.xml.attribute" = r###"null"###, "User photos_urls xml items attribute"
+        "properties.photos_urls.items.xml.name" = r###""photo_url""###, "User photos_urls xml items name"
+        "properties.photos_urls.items.xml.prefix" = r###"null"###, "User photos_urls xml items prefix"
+        "properties.photos_urls.items.xml.namespace" = r###"null"###, "User photos_urls xml items namespace"
+        "properties.photos_urls.items.xml.wrapped" = r###"null"###, "User photos_urls links xml items wrapped"
+    }
+}