Allow paths as namespace values

This commit is contained in:
Dirkjan Ochtman 2022-09-03 11:56:42 +02:00
parent e9e339afc9
commit 685189f00d
6 changed files with 435 additions and 133 deletions

View File

@ -35,10 +35,9 @@ impl Deserializer {
pub fn new(input: &syn::DeriveInput) -> Deserializer { pub fn new(input: &syn::DeriveInput) -> Deserializer {
let ident = &input.ident; let ident = &input.ident;
let container_meta = ContainerMeta::from_derive(input); let container_meta = ContainerMeta::from_derive(input);
let default_namespace = match &container_meta.ns.default { let default_namespace = match &container_meta.ns.uri {
Namespace::Default => "", Some(ns) => quote!(#ns),
Namespace::Prefix(_) => panic!("container namespace cannot be prefix"), None => quote!(""),
Namespace::Literal(ns) => ns,
}; };
let mut xml_generics = input.generics.clone(); let mut xml_generics = input.generics.clone();
@ -71,12 +70,6 @@ impl Deserializer {
syn::Fields::Named(ref fields) => { syn::Fields::Named(ref fields) => {
fields.named.iter().enumerate().for_each(|(index, field)| { fields.named.iter().enumerate().for_each(|(index, field)| {
let field_meta = FieldMeta::from_field(field); let field_meta = FieldMeta::from_field(field);
if let Namespace::Prefix(prefix) = &field_meta.ns.default {
if !container_meta.ns.prefixes.contains_key(prefix) {
panic!("unknown prefix for this type");
}
}
let tokens = match field_meta.attribute { let tokens = match field_meta.attribute {
true => &mut attributes_tokens, true => &mut attributes_tokens,
false => &mut elements_tokens, false => &mut elements_tokens,
@ -230,18 +223,15 @@ impl Deserializer {
let enum_name = Ident::new(&format!("__Value{index}"), Span::call_site()); let enum_name = Ident::new(&format!("__Value{index}"), Span::call_site());
tokens.enum_.extend(quote!(#enum_name,)); tokens.enum_.extend(quote!(#enum_name,));
let default_ns = match &field_meta.ns.default { let default_ns = match &field_meta.ns.uri {
Namespace::Default => &container_meta.ns.default, None => &container_meta.ns.uri,
_ => &field_meta.ns.default, _ => &field_meta.ns.uri,
}; };
let ns = match default_ns { let ns = match default_ns {
Namespace::Default => "", Some(Namespace::Path(path)) => quote!(#path),
Namespace::Prefix(prefix) => match container_meta.ns.prefixes.get(prefix) { Some(Namespace::Literal(ns)) => quote!(#ns),
Some(ns) => ns, None => quote!(""),
None => panic!("undefined prefix {prefix} in xml attribute"),
},
Namespace::Literal(ns) => ns,
}; };
tokens.consts.extend(quote!( tokens.consts.extend(quote!(

View File

@ -3,11 +3,13 @@ extern crate proc_macro;
mod de; mod de;
mod ser; mod ser;
use std::collections::HashMap; use std::collections::BTreeMap;
use quote::quote; use proc_macro2::{Delimiter, Group, Ident, Punct, TokenStream, TokenTree, Literal};
use quote::{quote, ToTokens};
use syn::parse_macro_input;
use syn::punctuated::Punctuated; use syn::punctuated::Punctuated;
use syn::{parse_macro_input, Meta, NestedMeta}; use syn::token::Colon2;
#[proc_macro_derive(ToXml, attributes(xml))] #[proc_macro_derive(ToXml, attributes(xml))]
pub fn to_xml(input: proc_macro::TokenStream) -> proc_macro::TokenStream { pub fn to_xml(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
@ -26,7 +28,7 @@ pub fn from_xml(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
)) ))
} }
#[derive(Debug, Default)] #[derive(Default)]
struct ContainerMeta { struct ContainerMeta {
ns: NamespaceMeta, ns: NamespaceMeta,
} }
@ -36,17 +38,15 @@ impl ContainerMeta {
let mut meta = ContainerMeta::default(); let mut meta = ContainerMeta::default();
for item in meta_items(&input.attrs) { for item in meta_items(&input.attrs) {
match item { match item {
Meta::List(list) if list.path.is_ident("ns") => { MetaItem::Attribute => panic!("attribute key invalid in container xml attribute"),
meta.ns = NamespaceMeta::from_list(&list.nested) MetaItem::Ns(ns) => meta.ns = ns,
}
_ => panic!("invalid xml attribute syntax"),
} }
} }
meta meta
} }
} }
#[derive(Debug, Default)] #[derive(Default)]
struct FieldMeta { struct FieldMeta {
attribute: bool, attribute: bool,
ns: NamespaceMeta, ns: NamespaceMeta,
@ -57,80 +57,396 @@ impl FieldMeta {
let mut meta = FieldMeta::default(); let mut meta = FieldMeta::default();
for item in meta_items(&input.attrs) { for item in meta_items(&input.attrs) {
match item { match item {
Meta::Path(path) if path.is_ident("attribute") => meta.attribute = true, MetaItem::Attribute => meta.attribute = true,
Meta::List(list) if list.path.is_ident("ns") => { MetaItem::Ns(ns) => meta.ns = ns,
meta.ns = NamespaceMeta::from_list(&list.nested)
}
_ => panic!("invalid xml attribute syntax"),
} }
} }
meta meta
} }
} }
#[derive(Debug, Default)] #[derive(Default)]
struct NamespaceMeta { struct NamespaceMeta {
default: Namespace, uri: Option<Namespace>,
prefixes: HashMap<String, String>, prefixes: BTreeMap<String, Namespace>,
} }
impl NamespaceMeta { impl NamespaceMeta {
fn from_list(list: &Punctuated<NestedMeta, syn::token::Comma>) -> NamespaceMeta { fn from_tokens(group: Group) -> Self {
let mut meta = NamespaceMeta::default(); let mut new = NamespaceMeta::default();
for (i, item) in list.iter().enumerate() { let mut state = NsState::Start;
match item { for tree in group.stream() {
NestedMeta::Meta(inner) => match inner { state = match (state, tree) {
Meta::Path(path) => match path.get_ident() { (NsState::Start, TokenTree::Literal(lit)) => {
Some(id) => meta.default = Namespace::Prefix(id.to_string()), new.uri = Some(Namespace::Literal(lit));
None => panic!("invalid xml attribute syntax"), NsState::Comma
},
Meta::NameValue(nv) => match (nv.path.get_ident(), &nv.lit) {
(Some(id), syn::Lit::Str(lit)) => {
meta.prefixes.insert(id.to_string(), lit.value());
}
_ => panic!("invalid xml attribute syntax"),
},
_ => panic!("invalid xml attribute syntax"),
},
NestedMeta::Lit(syn::Lit::Str(lit)) if i == 0 => {
meta.default = Namespace::Literal(lit.value())
} }
_ => panic!("invalid xml attribute syntax"), (NsState::Start, TokenTree::Punct(punct)) if punct.as_char() == ':' => {
} NsState::Path {
colon1: Some(punct),
colon2: None,
path: None,
}
}
(NsState::Start, TokenTree::Ident(id)) => NsState::Path {
colon1: None,
colon2: None,
path: Some(syn::Path::from(id)),
},
(NsState::Comma, TokenTree::Punct(punct)) if punct.as_char() == ',' => {
NsState::Prefix
}
(
NsState::Path {
colon1: None,
colon2: None,
path,
},
TokenTree::Punct(punct),
) if punct.as_char() == ':' => NsState::Path {
colon1: Some(punct),
colon2: None,
path,
},
(
NsState::Path {
colon1: colon1 @ Some(_),
colon2: None,
path,
},
TokenTree::Punct(punct),
) if punct.as_char() == ':' => NsState::Path {
colon1,
colon2: Some(punct),
path,
},
(
NsState::Path {
colon1: Some(colon1),
colon2: Some(colon2),
path,
},
TokenTree::Ident(id),
) => {
let path = match path {
Some(mut path) => {
path.segments.push(syn::PathSegment::from(id));
path
}
None => {
let mut segments = Punctuated::new();
segments.push_value(id.into());
syn::Path {
leading_colon: Some(Colon2 {
spans: [colon1.span(), colon2.span()],
}),
segments,
}
}
};
NsState::Path {
colon1: None,
colon2: None,
path: Some(path),
}
}
(
NsState::Path {
colon1: None,
colon2: None,
path: Some(path),
},
TokenTree::Punct(punct),
) if punct.as_char() == ',' => {
new.uri = Some(Namespace::Path(path));
NsState::Prefix
}
(
NsState::Path {
colon1: None,
colon2: None,
path: Some(path),
},
TokenTree::Punct(punct),
) if punct.as_char() == '=' => {
if path.leading_colon.is_some() {
panic!("prefix cannot be defined on a path in xml attribute");
}
if path.segments.len() != 1 {
panic!("prefix key must be a single identifier");
}
let segment = path.segments.into_iter().next().unwrap();
if !segment.arguments.is_empty() {
panic!("prefix key must be a single identifier without arguments");
}
NsState::PrefixValue {
prefix: segment.ident,
}
}
(NsState::Prefix, TokenTree::Ident(id)) => NsState::Eq { prefix: id },
(NsState::Eq { prefix }, TokenTree::Punct(punct)) if punct.as_char() == '=' => {
NsState::PrefixValue { prefix }
}
(NsState::PrefixValue { prefix }, TokenTree::Literal(lit)) => {
new.prefixes
.insert(prefix.to_string(), Namespace::Literal(lit));
NsState::Comma
}
(NsState::PrefixValue { prefix }, TokenTree::Punct(punct))
if punct.as_char() == ':' =>
{
NsState::PrefixPath {
prefix,
colon1: Some(punct),
colon2: None,
path: None,
}
}
(NsState::PrefixValue { prefix }, TokenTree::Ident(id)) => NsState::PrefixPath {
prefix,
colon1: None,
colon2: None,
path: Some(syn::Path::from(id)),
},
(
NsState::PrefixPath {
prefix,
colon1: None,
colon2: None,
path,
},
TokenTree::Punct(punct),
) if punct.as_char() == ':' => NsState::PrefixPath {
prefix,
colon1: Some(punct),
colon2: None,
path,
},
(
NsState::PrefixPath {
prefix,
colon1: colon1 @ Some(_),
colon2: None,
path,
},
TokenTree::Punct(punct),
) if punct.as_char() == ':' => NsState::PrefixPath {
prefix,
colon1,
colon2: Some(punct),
path,
},
(
NsState::PrefixPath {
prefix,
colon1: Some(colon1),
colon2: Some(colon2),
path,
},
TokenTree::Ident(id),
) => {
let path = match path {
Some(mut path) => {
path.segments.push(syn::PathSegment::from(id));
path
}
None => {
let mut segments = Punctuated::new();
segments.push_value(id.into());
syn::Path {
leading_colon: Some(Colon2 {
spans: [colon1.span(), colon2.span()],
}),
segments,
}
}
};
NsState::PrefixPath {
prefix,
colon1: None,
colon2: None,
path: Some(path),
}
}
(
NsState::PrefixPath {
prefix,
colon1: None,
colon2: None,
path: Some(path),
},
TokenTree::Punct(punct),
) if punct.as_char() == ',' => {
new.prefixes
.insert(prefix.to_string(), Namespace::Path(path));
NsState::Prefix
}
(state, tree) => {
panic!(
"invalid state transition while parsing ns in xml attribute ({}, {tree})",
state.name()
)
}
};
} }
meta
match state {
NsState::Start | NsState::Comma => {}
NsState::Path { colon1: None, colon2: None, path: Some(path) } => {
new.uri = Some(Namespace::Path(path));
}
NsState::PrefixPath { prefix, colon1: None, colon2: None, path: Some(path) } => {
new.prefixes.insert(prefix.to_string(), Namespace::Path(path));
}
state => panic!("invalid ns end state in xml attribute ({})", state.name()),
}
new
} }
} }
fn meta_items(attrs: &[syn::Attribute]) -> impl Iterator<Item = Meta> + '_ { fn meta_items(attrs: &[syn::Attribute]) -> Vec<MetaItem> {
attrs let mut items = Vec::new();
.iter() let attr = match attrs.iter().find(|attr| attr.path.is_ident("xml")) {
.filter_map(|attr| { Some(attr) => attr,
if !attr.path.is_ident("xml") { None => return items,
return None; };
}
match attr.parse_meta() { let mut iter = attr.tokens.clone().into_iter();
Ok(Meta::List(meta)) => Some(meta.nested.into_iter()), let first = match iter.next() {
_ => panic!("unexpected xml attribute syntax"), Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis => {
group.stream()
}
_ => panic!("expected parenthesized group in xml attribute"),
};
if iter.next().is_some() {
panic!("expected single token tree in xml attribute");
}
let mut state = MetaState::Start;
for tree in first {
state = match (state, tree) {
(MetaState::Start, TokenTree::Ident(id)) => {
if id == "attribute" {
items.push(MetaItem::Attribute);
MetaState::Comma
} else if id == "ns" {
MetaState::Ns
} else {
panic!("unexpected key in xml attribute");
}
} }
}) (MetaState::Comma, TokenTree::Punct(punct)) if punct.as_char() == ',' => {
.flatten() MetaState::Start
.map(|item| match item { }
NestedMeta::Meta(item) => item, (MetaState::Ns, TokenTree::Group(group))
NestedMeta::Lit(_) => panic!("unexpected xml attribute syntax"), if group.delimiter() == Delimiter::Parenthesis =>
}) {
items.push(MetaItem::Ns(NamespaceMeta::from_tokens(group)));
MetaState::Comma
}
(state, tree) => {
panic!(
"invalid state transition while parsing xml attribute ({}, {tree})",
state.name()
)
}
};
}
items
} }
#[derive(Debug)] #[derive(Debug)]
enum Namespace { enum MetaState {
Default, Start,
Prefix(String), Comma,
Literal(String), Ns,
} }
impl Default for Namespace { impl MetaState {
fn default() -> Self { fn name(&self) -> &'static str {
Namespace::Default match self {
MetaState::Start => "Start",
MetaState::Comma => "Comma",
MetaState::Ns => "Ns",
}
}
}
enum NsState {
Start,
Comma,
Path {
colon1: Option<Punct>,
colon2: Option<Punct>,
path: Option<syn::Path>,
},
Prefix,
Eq {
prefix: Ident,
},
PrefixValue {
prefix: Ident,
},
PrefixPath {
prefix: Ident,
colon1: Option<Punct>,
colon2: Option<Punct>,
path: Option<syn::Path>,
},
}
impl NsState {
fn name(&self) -> &'static str {
match self {
NsState::Start => "Start",
NsState::Comma => "Comma",
NsState::Path {
colon1,
colon2,
path,
} => match (colon1, colon2, path) {
(None, None, None) => "Path [000]",
(Some(_), None, None) => "Path [100]",
(None, Some(_), None) => "Path [010]",
(None, None, Some(_)) => "Path [001]",
(Some(_), Some(_), None) => "Path [110]",
(None, Some(_), Some(_)) => "Path [011]",
(Some(_), None, Some(_)) => "Path [101]",
(Some(_), Some(_), Some(_)) => "Path [111]",
},
NsState::Prefix => "Prefix",
NsState::Eq { .. } => "Eq",
NsState::PrefixValue { .. } => "PrefixValue",
NsState::PrefixPath { .. } => "PrefixPath",
}
}
}
enum MetaItem {
Ns(NamespaceMeta),
Attribute,
}
enum Namespace {
Path(syn::Path),
Literal(Literal),
}
impl ToTokens for Namespace {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Namespace::Path(path) => path.to_tokens(tokens),
Namespace::Literal(lit) => lit.to_tokens(tokens),
}
} }
} }

View File

@ -1,7 +1,7 @@
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::quote; use quote::quote;
use crate::{ContainerMeta, FieldMeta, Namespace}; use crate::{ContainerMeta, FieldMeta};
pub fn to_xml(input: &syn::DeriveInput) -> proc_macro2::TokenStream { pub fn to_xml(input: &syn::DeriveInput) -> proc_macro2::TokenStream {
let ident = &input.ident; let ident = &input.ident;
@ -79,28 +79,34 @@ impl<'a> Serializer {
fn add_header(&mut self, output: &'a mut TokenStream) { fn add_header(&mut self, output: &'a mut TokenStream) {
output.extend(quote!( output.extend(quote!(
serializer.output.write_char('<')?; serializer.output.write_char('<')?;
serializer.output.write_str(field_context.name)?;
)); ));
let default_namespace = match &self.meta.ns.default { let default_namespace = match &self.meta.ns.uri {
Namespace::Default => "", Some(ns) => quote!(#ns),
Namespace::Prefix(_) => panic!("type cannot have prefix as namespace"), None => quote!(""),
Namespace::Literal(ns) => ns,
}; };
output.extend(quote!( output.extend(quote!(
// Check if parent default namespace equals // Check if parent default namespace equals
if serializer.parent_default_namespace() != #default_namespace { if serializer.parent_default_namespace() != #default_namespace {
serializer.output.write_str(" xmlns=\"")?; if let Some(prefix) = serializer.parent_namespaces.get(#default_namespace) {
serializer.output.write_str(#default_namespace)?; serializer.output.write_str(prefix)?;
serializer.output.write_char('\"')?; serializer.output.write_char(':')?;
serializer.output.write_str(field_context.name)?;
} else {
serializer.output.write_str(field_context.name)?;
serializer.output.write_str(" xmlns=\"")?;
serializer.output.write_str(#default_namespace)?;
serializer.output.write_char('\"')?;
}
} else {
serializer.output.write_str(field_context.name)?;
} }
serializer.update_parent_default_namespace(#default_namespace); serializer.update_parent_default_namespace(#default_namespace);
)); ));
let mut sorted_values: Vec<_> = self.meta.ns.prefixes.iter().collect(); for (key, val) in &self.meta.ns.prefixes {
sorted_values.sort();
for (key, val) in sorted_values {
output.extend(quote!( output.extend(quote!(
if serializer.parent_namespaces.get(#val).is_none() { if serializer.parent_namespaces.get(#val).is_none() {
serializer.output.write_str(" xmlns:")?; serializer.output.write_str(" xmlns:")?;
@ -160,37 +166,20 @@ impl<'a> Serializer {
return; return;
} }
if let Namespace::Literal(ns) = &field_meta.ns.default { let ns = match field_meta.ns.uri {
body.extend(quote!( Some(ns) => quote!(#ns),
#declaration None => match &self.meta.ns.uri {
field.attribute = Some(instant_xml::FieldAttribute::Namespace(#ns)); Some(ns) => quote!(#ns),
)); None => quote!(""),
} else if let Namespace::Prefix(prefix) = &field_meta.ns.default { },
match self.meta.ns.prefixes.get(prefix) {
Some(val) => {
body.extend(quote!(
#declaration
// Check if such namespace already exist, if so change its prefix to parent prefix
let prefix_key = match serializer.parent_namespaces.get(#val) {
Some(key) => key,
None => #prefix,
};
));
}
None => panic!("Prefix not defined: {}", prefix),
};
body.extend(quote!(
field.attribute = Some(instant_xml::FieldAttribute::Prefix(prefix_key));
));
} else {
body.extend(quote!(
#declaration
));
}; };
body.extend(quote!( body.extend(quote!(
#declaration
match serializer.parent_namespaces.get(#ns) {
Some(prefix) => field.attribute = Some(::instant_xml::FieldAttribute::Prefix(prefix)),
None => field.attribute = Some(::instant_xml::FieldAttribute::Namespace(#ns)),
}
serializer.set_field_context(field)?; serializer.set_field_context(field)?;
self.#field_value.serialize(serializer)?; self.#field_value.serialize(serializer)?;
)); ));

View File

@ -3,22 +3,24 @@ use similar_asserts::assert_eq;
use instant_xml::{from_str, FromXml}; use instant_xml::{from_str, FromXml};
#[derive(Debug, Eq, PartialEq, FromXml)] #[derive(Debug, Eq, PartialEq, FromXml)]
#[xml(ns("URI", bar = "BAZ"))] #[xml(ns("URI", bar = BAR))]
struct NestedDe { struct NestedDe {
#[xml(ns(bar))] #[xml(ns(BAR))]
flag: bool, flag: bool,
} }
#[derive(Debug, Eq, PartialEq, FromXml)] #[derive(Debug, Eq, PartialEq, FromXml)]
#[xml(ns("URI", bar = "BAZ", foo = "BAR"))] #[xml(ns("URI", bar = "BAZ", foo = "BAR"))]
struct StructWithCustomFieldFromXml { struct StructWithCustomFieldFromXml {
#[xml(ns(bar))] #[xml(ns(BAR))]
flag: bool, flag: bool,
#[xml(attribute)] #[xml(attribute)]
flag_attribute: bool, flag_attribute: bool,
test: NestedDe, test: NestedDe,
} }
const BAR: &str = "BAZ";
#[test] #[test]
fn struct_with_custom_field_from_xml() { fn struct_with_custom_field_from_xml() {
assert_eq!( assert_eq!(

View File

@ -3,9 +3,9 @@ use similar_asserts::assert_eq;
use instant_xml::{to_string, ToXml}; use instant_xml::{to_string, ToXml};
#[derive(Debug, Eq, PartialEq, ToXml)] #[derive(Debug, Eq, PartialEq, ToXml)]
#[xml(ns(dar = "BAZ", internal = "INTERNAL"))] #[xml(ns(dar = "BAZ", internal = INTERNAL))]
struct NestedDifferentNamespace { struct NestedDifferentNamespace {
#[xml(ns(internal))] #[xml(ns(INTERNAL))]
flag_internal_prefix: bool, flag_internal_prefix: bool,
} }
@ -17,14 +17,17 @@ struct StructChildNamespaces {
} }
#[derive(Debug, Eq, PartialEq, ToXml)] #[derive(Debug, Eq, PartialEq, ToXml)]
#[xml(ns("URI", dar = "BAZ", internal = "INTERNAL"))] #[xml(ns("URI", dar = DAR, internal = INTERNAL))]
struct Nested { struct Nested {
#[xml(ns(dar))] #[xml(ns(DAR))]
flag_parent_prefix: bool, flag_parent_prefix: bool,
#[xml(ns(internal))] #[xml(ns(INTERNAL))]
flag_internal_prefix: bool, flag_internal_prefix: bool,
} }
const DAR: &str = "BAZ";
const INTERNAL: &str = "INTERNAL";
// Tests: // Tests:
// - Different child namespace // - Different child namespace
// - The same child namespace // - The same child namespace

View File

@ -3,12 +3,14 @@ use similar_asserts::assert_eq;
use instant_xml::{to_string, ToXml}; use instant_xml::{to_string, ToXml};
#[derive(Debug, Eq, PartialEq, ToXml)] #[derive(Debug, Eq, PartialEq, ToXml)]
#[xml(ns("URI", dar = "BAZ", internal = "INTERNAL"))] #[xml(ns("URI", dar = "BAZ", internal = INTERNAL))]
struct Nested { struct Nested {
#[xml(ns(internal))] #[xml(ns(INTERNAL))]
flag_internal_prefix: bool, flag_internal_prefix: bool,
} }
const INTERNAL: &str = "INTERNAL";
#[derive(Debug, Eq, PartialEq, ToXml)] #[derive(Debug, Eq, PartialEq, ToXml)]
#[xml(ns("URI", bar = "BAZ", foo = "BAR"))] #[xml(ns("URI", bar = "BAZ", foo = "BAR"))]
struct StructWithCustomField { struct StructWithCustomField {
@ -42,6 +44,6 @@ fn struct_with_custom_field() {
}, },
}) })
.unwrap(), .unwrap(),
"<StructWithCustomField xmlns=\"URI\" xmlns:bar=\"BAZ\" xmlns:foo=\"BAR\" int_attribute=\"42\"><flag_direct_namespace_same_the_same_as_prefix xmlns=\"BAZ\">true</flag_direct_namespace_same_the_same_as_prefix><flag_direct_namespace xmlns=\"DIFFERENT\">true</flag_direct_namespace><Nested xmlns:internal=\"INTERNAL\"><internal:flag_internal_prefix>false</internal:flag_internal_prefix></Nested></StructWithCustomField>" "<StructWithCustomField xmlns=\"URI\" xmlns:bar=\"BAZ\" xmlns:foo=\"BAR\" int_attribute=\"42\"><bar:flag_direct_namespace_same_the_same_as_prefix>true</bar:flag_direct_namespace_same_the_same_as_prefix><flag_direct_namespace xmlns=\"DIFFERENT\">true</flag_direct_namespace><Nested xmlns:internal=\"INTERNAL\"><internal:flag_internal_prefix>false</internal:flag_internal_prefix></Nested></StructWithCustomField>"
); );
} }