Skip to content

Commit 95ecfbe

Browse files
committed
Fix and simplify the vsme example
1 parent 137683d commit 95ecfbe

7 files changed

Lines changed: 625 additions & 9 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ Examples contained in this directory:
44

55
- [`read_write`](./read_write/README.md) - Generates code from a simple schema using a build script and then uses the generated code to deserialize and serialize a XML file during runtime.
66
- [`bpmn`](./bpmn/README.md) - Example that generates code for BPMN 2.0 and loads a diagram file.
7+
- [`vsme`](./vsme/README.md) - Example that generates code for the XBRL based VSME taxonomy. It shows how a custom render step can be used to collapse the huge `xbrli:item` substitution group into a few `ItemWrapper` based types instead of generating one type per element.

examples/vsme/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ xsd-parser-types = { workspace = true, features = [ "quick-xml" ] }
1616

1717
[build-dependencies]
1818
anyhow = { workspace = true }
19+
proc-macro2 = { workspace = true }
20+
quote = { workspace = true }
1921
xsd-parser = { workspace = true }
2022

2123
[lints]

examples/vsme/README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# VSME example
2+
3+
This example generates and uses the code for the XBRL based [VSME](https://xbrl.efrag.org/) taxonomy (the *Voluntary standard for non-listed Small- and Medium-sized Enterprises*).
4+
5+
XBRL instance documents store their facts as members of the `xbrli:item` substitution group. The VSME taxonomy defines **thousands** of such facts, but most of them share the same underlying type and only differ by their XML tag name (e.g. `vsme:Assets`, `vsme:AverageNumberOfAnnualTrainingHoursPerMaleEmployee`, ...). Generating a dedicated Rust type (and `enum` variant) for every single element would produce a huge, unwieldy amount of code.
6+
7+
## What this example shows
8+
9+
Instead of generating one type per element, the [build script](build.rs) installs a custom [`RenderStep`] (`FixItemType`) that:
10+
11+
1. Looks up the content of the `xbrli:item` element (a big `xs:choice`).
12+
2. Groups all element variants of the choice by their type and removes them from the choice.
13+
3. Adds a single group element per type that references a synthetic `XxxWrapped` custom type.
14+
4. Emits a type definition and an [`ItemTags`](src/item.rs) implementation for each of those wrapper types:
15+
16+
```rust,ignore
17+
pub type AmountOfEmissionToAirWrapped =
18+
crate::item::ItemWrapper<vsme::AmountOfEmissionToAirDyn, AmountOfEmissionToAirWrappedTags>;
19+
20+
pub struct AmountOfEmissionToAirWrappedTags;
21+
22+
impl crate::item::ItemTags for AmountOfEmissionToAirWrappedTags {
23+
fn tags() -> &'static [crate::item::ItemTag] {
24+
static TAGS: [crate::item::ItemTag; 78] = [
25+
crate::item::ItemTag {
26+
tag: "vsme:Assets",
27+
name: "Assets",
28+
namespace: NS_VSME,
29+
},
30+
/* ... */
31+
];
32+
33+
&TAGS
34+
}
35+
}
36+
```
37+
38+
The [`ItemWrapper`](src/item.rs) type defined in this example provides the runtime support for this. It wraps the shared inner type and implements a `Serializer` and `Deserializer` that:
39+
40+
- on **deserialization**, only accept an element whose namespace-resolved tag is part of the associated `ItemTags` (so a document may use any prefix for the namespace), and remember which tag was used; and
41+
- on **serialization**, write the value back using that remembered tag.
42+
43+
This way a few hundred elements collapse into just a handful of generated types while the round-trip still preserves the exact XML tag of every fact.
44+
45+
## Running
46+
47+
```sh
48+
cargo run -p vsme
49+
```
50+
51+
This deserializes [`xml/example.xml`](xml/example.xml), prints the parsed object and serializes it back to XML.

examples/vsme/build.rs

Lines changed: 281 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,35 @@
11
//! This is a build script to generate the code for the `vsme` schema.
22
3+
use std::cell::RefCell;
4+
use std::collections::BTreeMap;
35
use std::env::var;
46
use std::fs::{create_dir_all, remove_dir_all};
57
use std::path::PathBuf;
8+
use std::rc::Rc;
69

710
use anyhow::{Context, Error};
11+
use proc_macro2::TokenStream;
12+
use quote::{format_ident, quote};
813

14+
use xsd_parser::models::data::TagName;
915
use xsd_parser::{
10-
config::{GeneratorFlags, InterpreterFlags, OptimizerFlags, Schema},
11-
generate_modules,
12-
models::{meta::MetaType, Naming},
16+
config::{GeneratorFlags, IdentQuadruple, InterpreterFlags, OptimizerFlags, Schema},
17+
exec_generator_with_ident_cache, exec_interpreter_with_ident_cache, exec_optimizer,
18+
exec_parser, exec_render,
19+
models::{
20+
code::{Module, ModulePath},
21+
data::PathData,
22+
meta::{
23+
CustomMeta, ElementMeta, ElementMetaVariant, ElementMode, MetaType, MetaTypeVariant,
24+
MetaTypes,
25+
},
26+
schema::{xs::FormChoiceType, Schemas},
27+
ElementIdent, IdentType, Naming,
28+
},
29+
pipeline::{
30+
generator::{Context as GeneratorContext, Error as GeneratorError},
31+
renderer::{MetaData, RenderStep, RenderStepType},
32+
},
1333
traits::{NameBuilder as NameBuilderTrait, Naming as NamingTrait},
1434
Config, Name, TypeIdent,
1535
};
@@ -26,16 +46,33 @@ fn main() -> Result<(), Error> {
2646
.context("Missing or invalid schema file!")?;
2747

2848
// This is almost the starting point defined in the main `[README.md]`.
49+
let fix_item_type = FixItemType::default();
2950
let config = Config::default()
3051
.with_schema(Schema::File(schema_file))
52+
.with_generate([(IdentType::Element, "xbrli:xbrl")])
3153
.with_interpreter_flags(InterpreterFlags::all() - InterpreterFlags::WITH_NUM_BIG_INT)
3254
.with_optimizer_flags(OptimizerFlags::all())
3355
.with_generator_flags(GeneratorFlags::all() - GeneratorFlags::ADVANCED_ENUMS)
3456
.with_naming(CustomNaming::default())
35-
.with_quick_xml();
57+
.with_quick_xml()
58+
.with_render_step(fix_item_type.clone());
3659

37-
// Generate the code based on the configuration above.
38-
let modules = generate_modules(config)?;
60+
// Generate the code based on the configuration above. We run the pipeline
61+
// manually instead of using `generate_modules`, because we need to inject
62+
// the `FixItemType` transformation in between the optimizer and the
63+
// generator.
64+
let schemas = exec_parser(config.parser)?;
65+
let (meta_types, ident_cache) =
66+
exec_interpreter_with_ident_cache(config.interpreter, &schemas)?;
67+
let meta_types = exec_optimizer(config.optimizer, meta_types)?;
68+
let meta_types = fix_item_type.prepare_types(meta_types, &schemas)?;
69+
let data_types = exec_generator_with_ident_cache(
70+
config.generator,
71+
&schemas,
72+
Some(&ident_cache),
73+
&meta_types,
74+
)?;
75+
let modules = exec_render(config.renderer, &data_types)?;
3976

4077
// Write the generated code to the module directory specified by Cargo.
4178
let target_dir = cargo_dir.join("src/schema");
@@ -99,3 +136,241 @@ impl NamingTrait for CustomNaming {
99136
format!("{s}_attr")
100137
}
101138
}
139+
140+
/// Render step (and meta type transformation) that collapses the many concrete
141+
/// element variants of the `xbrli:item` choice into a few `ItemWrapper` based
142+
/// types.
143+
///
144+
/// XBRL defines hundreds of facts as members of the `xbrli:item` substitution
145+
/// group. Most of them share the same (Rust) type and only differ by their XML
146+
/// tag name. Generating a dedicated enum variant for each of them would be
147+
/// wasteful, so we instead group the elements by their type and represent each
148+
/// group with a single [`ItemWrapper`](crate::item::ItemWrapper) that keeps the
149+
/// list of supported tags around at runtime.
150+
#[derive(Default, Debug, Clone)]
151+
struct FixItemType(Rc<RefCell<Vec<SharedWrapped>>>);
152+
153+
/// A [`WrappedType`] that is shared between the custom generator step (which
154+
/// resolves and stores the path to the concrete type) and the render step
155+
/// (which uses that information to render the actual code).
156+
type SharedWrapped = Rc<RefCell<WrappedType>>;
157+
158+
/// Information about a single synthetic `XxxWrapped` type.
159+
#[derive(Debug)]
160+
struct WrappedType {
161+
/// Identifier of the synthetic `XxxWrapped` custom type.
162+
ident: TypeIdent,
163+
164+
/// Identifier of the concrete type that is shared by all elements
165+
/// represented by this wrapper.
166+
type_: TypeIdent,
167+
168+
/// XML tags of all elements that are represented by this wrapper.
169+
tags: Vec<TagInfo>,
170+
171+
/// Path to the concrete type relative to the root module (e.g.
172+
/// `vsme :: AmountOfEmissionToAirDyn`). This is resolved and stored by the
173+
/// custom generator step (see [`WrappedType::resolve`]).
174+
target_type: Option<PathData>,
175+
}
176+
177+
/// Information about a single XML tag represented by an [`ItemWrapper`].
178+
#[derive(Debug)]
179+
struct TagInfo {
180+
/// Identifier (namespace and local name) of the element.
181+
ident: ElementIdent,
182+
183+
/// Form of the element, used to decide whether the tag needs a namespace
184+
/// prefix.
185+
form: FormChoiceType,
186+
}
187+
188+
impl FixItemType {
189+
fn prepare_types(&self, mut types: MetaTypes, schemas: &Schemas) -> Result<MetaTypes, Error> {
190+
let item_ident = IdentQuadruple::from((IdentType::Element, "xbrli:item"));
191+
let item_ident = item_ident
192+
.resolve(schemas)
193+
.context("Unable to resolve `xbrli:item` element")?;
194+
195+
let item_ty = types
196+
.items
197+
.get(&item_ident)
198+
.context("Unknown element: `xbrli:item`")?;
199+
let MetaTypeVariant::ComplexType(meta) = &item_ty.variant else {
200+
anyhow::bail!("`xbrli:item` is not a complex type")
201+
};
202+
let content_ident = meta
203+
.content
204+
.clone()
205+
.context("`xbrli:item` is missing a content type")?;
206+
207+
let content_ty = types
208+
.items
209+
.get_mut(&content_ident)
210+
.context("Unknown content type for `xbrli:item`")?;
211+
let MetaTypeVariant::Choice(meta) = &mut content_ty.variant else {
212+
anyhow::bail!("Content type of `xbrli:item` is not a choice")
213+
};
214+
215+
// Group all concrete element variants of the choice by their type and
216+
// remove them from the choice. We use a `BTreeMap` to get a stable
217+
// order of the generated types. For each removed element we remember the
218+
// information needed to reconstruct its XML tag name later on.
219+
let mut map = BTreeMap::<TypeIdent, Vec<(ElementIdent, FormChoiceType)>>::new();
220+
meta.elements.0.retain(|el| {
221+
let ElementMetaVariant::Type {
222+
type_,
223+
mode: ElementMode::Element,
224+
} = &el.variant
225+
else {
226+
return true;
227+
};
228+
229+
map.entry(type_.clone())
230+
.or_default()
231+
.push((el.ident.clone(), el.form));
232+
233+
false
234+
});
235+
236+
// Add one group element per type that references the synthetic wrapper
237+
// type instead of the removed elements.
238+
let mut pending = Vec::new();
239+
for (concrete, tags) in map {
240+
let mut wrapped = concrete.clone();
241+
wrapped.name = Name::new_named(format!("{}Wrapped", wrapped.name));
242+
243+
meta.elements.0.push(ElementMeta::new(
244+
concrete.to_property_ident(),
245+
wrapped.clone(),
246+
ElementMode::Group,
247+
FormChoiceType::Unqualified,
248+
));
249+
250+
pending.push((wrapped, concrete, tags));
251+
}
252+
253+
// `meta` (and therefore the mutable borrow of `types`) is not used
254+
// beyond this point, so we can now resolve the tag names and register
255+
// the synthetic wrapper types as custom types.
256+
for (ident, type_, tags) in pending {
257+
let tags = tags
258+
.into_iter()
259+
.map(|(ident, form)| TagInfo::new(ident, form))
260+
.collect::<Vec<_>>();
261+
262+
let wrapped = Rc::new(RefCell::new(WrappedType {
263+
ident: ident.clone(),
264+
type_,
265+
tags,
266+
target_type: None,
267+
}));
268+
self.0.borrow_mut().push(wrapped.clone());
269+
270+
// The custom generator step resolves the path to the concrete type
271+
// during code generation and stores it in the (shared) wrapper.
272+
let custom = CustomMeta::new(ident.name.clone()).with_generator(
273+
move |ctx: &mut GeneratorContext<'_, '_>, _: &CustomMeta| {
274+
wrapped.borrow_mut().resolve(ctx)
275+
},
276+
);
277+
types
278+
.items
279+
.insert(ident, MetaType::new(MetaTypeVariant::Custom(custom)));
280+
}
281+
282+
Ok(types)
283+
}
284+
}
285+
286+
impl RenderStep for FixItemType {
287+
fn render_step_type(&self) -> RenderStepType {
288+
RenderStepType::ExtraTypes
289+
}
290+
291+
fn finish(&mut self, meta: &MetaData<'_>, module: &mut Module) {
292+
for wrapped in self.0.borrow().iter() {
293+
wrapped.borrow().render(meta, module);
294+
}
295+
}
296+
}
297+
298+
impl WrappedType {
299+
/// Resolve the path to the concrete type (relative to the root module) and
300+
/// store it. This is called by the custom generator step during code
301+
/// generation, where the path information is available. Requesting the type
302+
/// reference also makes sure the concrete type is actually generated.
303+
fn resolve(&mut self, ctx: &mut GeneratorContext<'_, '_>) -> Result<(), GeneratorError> {
304+
let target_type = ctx.get_or_create_type_ref(&self.type_)?.path.clone();
305+
306+
self.target_type = Some(target_type);
307+
308+
Ok(())
309+
}
310+
311+
/// Render the type definition and the `ItemTags` implementation for this
312+
/// wrapper into the given `module`.
313+
fn render(&self, meta: &MetaData<'_>, module: &mut Module) {
314+
let wrapped_ident = format_ident!("{}", self.ident.name.as_str());
315+
let tags_ident = format_ident!("{}Tags", self.ident.name.as_str());
316+
317+
let target_type = self
318+
.target_type
319+
.as_ref()
320+
.expect("the concrete type path is resolved by the custom generator step")
321+
.resolve_relative_to(&ModulePath::root());
322+
323+
let tags = self.tags.iter().map(|tag| tag.render(meta));
324+
let count = self.tags.len();
325+
326+
module.append(quote! {
327+
pub type #wrapped_ident = crate::item::ItemWrapper<#target_type, #tags_ident>;
328+
329+
#[derive(Debug)]
330+
pub struct #tags_ident;
331+
332+
impl crate::item::ItemTags for #tags_ident {
333+
fn tags() -> &'static [crate::item::ItemTag] {
334+
static TAGS: [crate::item::ItemTag; #count] = [ #( #tags ),* ];
335+
336+
&TAGS
337+
}
338+
}
339+
});
340+
}
341+
}
342+
343+
impl TagInfo {
344+
/// Resolve the information for a single tag from the passed element data.
345+
fn new(ident: ElementIdent, form: FormChoiceType) -> Self {
346+
Self { ident, form }
347+
}
348+
349+
/// Render this tag as a `crate::item::ItemTag` value.
350+
fn render(&self, meta: &MetaData<'_>) -> TokenStream {
351+
let Self { ident, form } = self;
352+
353+
let types = meta.types.meta.types;
354+
let module = types
355+
.modules
356+
.get(&ident.ns)
357+
.expect("the module for the tag's namespace exists");
358+
359+
// Reuse the namespace constant generated next to the schema types
360+
// (e.g. `NS_VSME`) instead of repeating the namespace URI.
361+
let namespace = module
362+
.make_ns_const()
363+
.expect("the tag has a namespace")
364+
.resolve_relative_to(&ModulePath::root());
365+
let tag = TagName::new(types, ident.ns, &ident.name, *form).get(true);
366+
let name = ident.name.as_str();
367+
368+
quote! {
369+
crate::item::ItemTag {
370+
tag: #tag,
371+
name: #name,
372+
namespace: #namespace,
373+
}
374+
}
375+
}
376+
}

0 commit comments

Comments
 (0)