1mod 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
52pub fn emit(graph: &TypeGraph) -> String {
67 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 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 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 syn::parse_file(&code).expect("generated code must be valid Rust");
292
293 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}