Skip to main content

mx20022_codegen/emit/
mod.rs

1//! Code emission: IR [`TypeGraph`] → Rust source string.
2//!
3//! The main entry point is [`emit`], which takes a fully-lowered [`TypeGraph`]
4//! and returns a formatted Rust source file as a `String`.
5//!
6//! # Pipeline
7//!
8//! 1. Each [`TypeDef`] variant is dispatched to the appropriate submodule.
9//! 2. Each submodule returns a `proc_macro2::TokenStream`.
10//! 3. All token streams are collected and formatted with `prettyplease`.
11//!
12//! # Example
13//!
14//! ```no_run
15//! use mx20022_codegen::{xsd, ir, emit};
16//!
17//! let schema = xsd::parse_str(r#"<xs:schema
18//!     xmlns:xs="http://www.w3.org/2001/XMLSchema"
19//!     targetNamespace="urn:example">
20//!   <xs:element name="Root" type="RootType"/>
21//!   <xs:complexType name="RootType">
22//!     <xs:sequence>
23//!       <xs:element name="Id" type="xs:string"/>
24//!     </xs:sequence>
25//!   </xs:complexType>
26//! </xs:schema>"#).unwrap();
27//!
28//! let graph = ir::lower(&schema).unwrap();
29//! let code = emit::emit(&graph);
30//! assert!(code.contains("pub struct RootType"));
31//! ```
32
33mod builders;
34mod code_enums;
35mod enums;
36mod newtypes;
37mod opaque;
38pub(crate) mod pattern_codegen;
39mod structs;
40mod validate;
41mod value_with_attr;
42
43pub(crate) mod util;
44
45use std::collections::HashSet;
46
47use proc_macro2::TokenStream;
48use quote::quote;
49
50use crate::ir::types::{TypeDef, TypeGraph};
51
52/// Emit a complete Rust source file from the given [`TypeGraph`].
53///
54/// The returned string is formatted with `prettyplease` and is guaranteed to
55/// be parseable by `syn::parse_file`.
56///
57/// xs:choice types (IR [`crate::ir::types::EnumDef`]) that appear as struct
58/// field values are automatically wrapped in
59/// `crate::common::ChoiceWrapper<T>` so that `quick-xml` + serde can
60/// round-trip them correctly.
61///
62/// # Panics
63///
64/// Panics if the generated token stream cannot be parsed by `syn`. This
65/// indicates a bug in the emitter (not in user input).
66pub fn emit(graph: &TypeGraph) -> String {
67    // Build the set of xs:choice type names (all EnumDef names in the graph).
68    let choice_types: HashSet<String> = graph
69        .types
70        .values()
71        .filter_map(|t| {
72            if let TypeDef::Enum(e) = t {
73                Some(e.name.clone())
74            } else {
75                None
76            }
77        })
78        .collect();
79
80    let mut tokens = TokenStream::new();
81
82    // Module-level doc comment with namespace.
83    let ns = &graph.namespace;
84    let doc = format!(" Generated from ISO 20022 XSD schema.\n Namespace: `{ns}`");
85    tokens.extend(quote! {
86        #![doc = #doc]
87    });
88
89    for type_def in graph.types.values() {
90        let type_tokens = match type_def {
91            TypeDef::Struct(d) => {
92                let mut ts = structs::emit_struct(d, &choice_types);
93                ts.extend(builders::emit_builder(d, &choice_types));
94                ts
95            }
96            TypeDef::Enum(d) => enums::emit_enum(d),
97            TypeDef::Newtype(d) => newtypes::emit_newtype(d),
98            TypeDef::CodeEnum(d) => code_enums::emit_code_enum(d),
99            TypeDef::ValueWithAttr(d) => value_with_attr::emit_value_with_attr(d),
100            TypeDef::Opaque(d) => opaque::emit_opaque(d),
101        };
102        tokens.extend(type_tokens);
103    }
104
105    // Emit `impl Validatable` for all types and `impl IsoMessage` for
106    // document types.
107    tokens.extend(validate::emit_validatable_impls(graph, &choice_types));
108
109    let file_str = tokens.to_string();
110    let parsed = syn::parse_file(&file_str)
111        .unwrap_or_else(|e| panic!("emitter produced invalid Rust (syn error: {e}):\n{file_str}"));
112    prettyplease::unparse(&parsed)
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use crate::ir::types::{
119        AttrDef, Cardinality, CodeEnumDef, CodeValue, EnumDef, FieldDef, NewtypeDef, OpaqueDef,
120        RootElement, RustType, StructDef, TypeDef, TypeGraph, TypeRef, ValueWithAttrDef,
121        VariantDef,
122    };
123    use indexmap::IndexMap;
124
125    fn make_graph(types: Vec<(String, TypeDef)>) -> TypeGraph {
126        TypeGraph {
127            namespace: "urn:test:namespace".to_owned(),
128            root_elements: vec![RootElement {
129                xml_name: "Root".to_owned(),
130                type_name: "RootType".to_owned(),
131            }],
132            types: types.into_iter().collect::<IndexMap<_, _>>(),
133        }
134    }
135
136    #[test]
137    fn emit_empty_graph_is_valid_rust() {
138        let graph = TypeGraph {
139            namespace: "urn:empty".to_owned(),
140            root_elements: vec![],
141            types: IndexMap::new(),
142        };
143        let code = emit(&graph);
144        syn::parse_file(&code).expect("must be valid Rust");
145    }
146
147    #[test]
148    fn emit_simple_struct() {
149        let graph = make_graph(vec![(
150            "MyStruct".to_owned(),
151            TypeDef::Struct(StructDef {
152                name: "MyStruct".to_owned(),
153                fields: vec![FieldDef {
154                    xml_name: "Name".to_owned(),
155                    rust_name: "name".to_owned(),
156                    type_ref: TypeRef::Builtin(RustType::String),
157                    cardinality: Cardinality::Required,
158                }],
159            }),
160        )]);
161        let code = emit(&graph);
162        syn::parse_file(&code).expect("must be valid Rust");
163        assert!(code.contains("pub struct MyStruct"));
164        assert!(code.contains("pub name: String"));
165    }
166
167    #[test]
168    fn emit_newtype() {
169        let graph = make_graph(vec![(
170            "Max35Text".to_owned(),
171            TypeDef::Newtype(NewtypeDef {
172                name: "Max35Text".to_owned(),
173                inner: RustType::String,
174                constraints: vec![],
175            }),
176        )]);
177        let code = emit(&graph);
178        syn::parse_file(&code).expect("must be valid Rust");
179        assert!(code.contains("pub struct Max35Text"));
180    }
181
182    #[test]
183    fn emit_code_enum() {
184        let graph = make_graph(vec![(
185            "AddressType2Code".to_owned(),
186            TypeDef::CodeEnum(CodeEnumDef {
187                name: "AddressType2Code".to_owned(),
188                codes: vec![
189                    CodeValue {
190                        xml_value: "ADDR".to_owned(),
191                        rust_name: "Addr".to_owned(),
192                    },
193                    CodeValue {
194                        xml_value: "PBOX".to_owned(),
195                        rust_name: "Pbox".to_owned(),
196                    },
197                ],
198            }),
199        )]);
200        let code = emit(&graph);
201        syn::parse_file(&code).expect("must be valid Rust");
202        assert!(code.contains("pub enum AddressType2Code"));
203        assert!(code.contains("Addr"));
204        assert!(code.contains("Pbox"));
205    }
206
207    #[test]
208    fn emit_choice_enum() {
209        let graph = make_graph(vec![(
210            "AddressType3Choice".to_owned(),
211            TypeDef::Enum(EnumDef {
212                name: "AddressType3Choice".to_owned(),
213                variants: vec![
214                    VariantDef {
215                        xml_name: "Cd".to_owned(),
216                        rust_name: "Cd".to_owned(),
217                        type_ref: TypeRef::Named("AddressType2Code".to_owned()),
218                    },
219                    VariantDef {
220                        xml_name: "Prtry".to_owned(),
221                        rust_name: "Prtry".to_owned(),
222                        type_ref: TypeRef::Named("GenericIdentification30".to_owned()),
223                    },
224                ],
225            }),
226        )]);
227        let code = emit(&graph);
228        syn::parse_file(&code).expect("must be valid Rust");
229        assert!(code.contains("pub enum AddressType3Choice"));
230    }
231
232    #[test]
233    fn emit_value_with_attr() {
234        let graph = make_graph(vec![(
235            "ActiveCurrencyAndAmount".to_owned(),
236            TypeDef::ValueWithAttr(ValueWithAttrDef {
237                name: "ActiveCurrencyAndAmount".to_owned(),
238                value_type: TypeRef::Builtin(RustType::Decimal),
239                attributes: vec![AttrDef {
240                    xml_name: "Ccy".to_owned(),
241                    rust_name: "ccy".to_owned(),
242                    type_ref: TypeRef::Named("ActiveCurrencyCode".to_owned()),
243                    required: true,
244                }],
245            }),
246        )]);
247        let code = emit(&graph);
248        syn::parse_file(&code).expect("must be valid Rust");
249        assert!(code.contains("pub struct ActiveCurrencyAndAmount"));
250        assert!(code.contains("$value"));
251        assert!(code.contains("@Ccy"));
252    }
253
254    #[test]
255    fn emit_opaque() {
256        let graph = make_graph(vec![(
257            "SignatureEnvelope".to_owned(),
258            TypeDef::Opaque(OpaqueDef {
259                name: "SignatureEnvelope".to_owned(),
260                namespace: Some("##other".to_owned()),
261            }),
262        )]);
263        let code = emit(&graph);
264        syn::parse_file(&code).expect("must be valid Rust");
265        assert!(code.contains("pub struct SignatureEnvelope"));
266    }
267
268    #[test]
269    fn emit_namespace_doc_comment() {
270        let graph = TypeGraph {
271            namespace: "urn:iso:std:iso:20022:tech:xsd:head.001.001.04".to_owned(),
272            root_elements: vec![],
273            types: IndexMap::new(),
274        };
275        let code = emit(&graph);
276        assert!(code.contains("head.001.001.04"));
277    }
278
279    #[test]
280    fn emit_head_001_e2e() {
281        let xsd_path = concat!(
282            env!("CARGO_MANIFEST_DIR"),
283            "/../../schemas/head/head.001.001.04.xsd"
284        );
285        let file = std::fs::File::open(xsd_path).expect("head.001.001.04.xsd not found");
286        let schema = crate::xsd::parse(std::io::BufReader::new(file)).expect("XSD parse failed");
287        let graph = crate::ir::lower(&schema).unwrap();
288        let code = emit(&graph);
289
290        // Must be parseable by syn (i.e., valid Rust).
291        syn::parse_file(&code).expect("generated code must be valid Rust");
292
293        // Must contain expected types.
294        assert!(code.contains("pub struct BusinessApplicationHeaderV04"));
295        assert!(code.contains("pub enum AddressType3Choice"));
296        assert!(code.contains("pub enum AddressType2Code"));
297        assert!(code.contains("pub struct Max35Text"));
298        assert!(code.contains("pub struct SignatureEnvelope"));
299    }
300}