rh_codegen/generators/
binding_generator.rs1use crate::bindings;
7use crate::fhir_types::StructureDefinition;
8use quote::quote;
9
10pub struct BindingGenerator;
12
13impl BindingGenerator {
14 pub fn generate_bindings_constant(structure_def: &StructureDefinition) -> String {
33 let bindings = bindings::extract_required_bindings(structure_def);
34
35 if bindings.is_empty() {
36 return String::new();
37 }
38
39 let mut code = String::new();
40 code.push_str("/// FHIR required bindings for this resource/datatype\n");
41 code.push_str("///\n");
42 code.push_str(
43 "/// These bindings define which ValueSets must be used for coded elements.\n",
44 );
45 code.push_str(
46 "/// Only 'required' strength bindings are included (extensible/preferred are not enforced).\n",
47 );
48 code.push_str("pub static BINDINGS: once_cell::sync::Lazy<Vec<rh_foundation::ElementBinding>> = once_cell::sync::Lazy::new(|| vec![\n");
49
50 for binding in &bindings {
51 let path = escape_rust_string(&binding.path);
53 let value_set_url = escape_rust_string(&binding.value_set_url);
54
55 let strength = match binding.strength {
56 rh_foundation::validation::BindingStrength::Required => {
57 "rh_foundation::BindingStrength::Required"
58 }
59 rh_foundation::validation::BindingStrength::Extensible => {
60 "rh_foundation::BindingStrength::Extensible"
61 }
62 rh_foundation::validation::BindingStrength::Preferred => {
63 "rh_foundation::BindingStrength::Preferred"
64 }
65 rh_foundation::validation::BindingStrength::Example => {
66 "rh_foundation::BindingStrength::Example"
67 }
68 };
69
70 code.push_str(&format!(
72 " rh_foundation::ElementBinding::new(\"{path}\", {strength}, \"{value_set_url}\")"
73 ));
74
75 if let Some(description) = &binding.description {
76 let escaped_desc = escape_rust_string(description);
77 code.push_str(&format!(".with_description(\"{escaped_desc}\")"));
78 }
79
80 code.push_str(",\n");
81 }
82
83 code.push_str("]);\n");
84 code
85 }
86
87 #[allow(dead_code)]
89 pub fn generate_bindings_tokens(
90 structure_def: &StructureDefinition,
91 ) -> proc_macro2::TokenStream {
92 let bindings = bindings::extract_required_bindings(structure_def);
93
94 if bindings.is_empty() {
95 return quote! {};
96 }
97
98 let binding_items: Vec<_> = bindings
99 .iter()
100 .map(|binding| {
101 let path = &binding.path;
102 let value_set_url = &binding.value_set_url;
103
104 let strength_tokens = match binding.strength {
105 rh_foundation::validation::BindingStrength::Required => {
106 quote! { rh_foundation::BindingStrength::Required }
107 }
108 rh_foundation::validation::BindingStrength::Extensible => {
109 quote! { rh_foundation::BindingStrength::Extensible }
110 }
111 rh_foundation::validation::BindingStrength::Preferred => {
112 quote! { rh_foundation::BindingStrength::Preferred }
113 }
114 rh_foundation::validation::BindingStrength::Example => {
115 quote! { rh_foundation::BindingStrength::Example }
116 }
117 };
118
119 if let Some(description) = &binding.description {
120 quote! {
121 rh_foundation::ElementBinding::new(#path, #strength_tokens, #value_set_url)
122 .with_description(#description)
123 }
124 } else {
125 quote! {
126 rh_foundation::ElementBinding::new(#path, #strength_tokens, #value_set_url)
127 }
128 }
129 })
130 .collect();
131
132 quote! {
133 pub static BINDINGS: once_cell::sync::Lazy<Vec<rh_foundation::ElementBinding>> =
138 once_cell::sync::Lazy::new(|| vec![
139 #(#binding_items),*
140 ]);
141 }
142 }
143}
144
145fn escape_rust_string(s: &str) -> String {
147 s.replace('\\', "\\\\")
148 .replace('"', "\\\"")
149 .replace('\n', "\\n")
150 .replace('\r', "\\r")
151 .replace('\t', "\\t")
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use crate::fhir_types::{
158 ElementBinding as FhirElementBinding, ElementDefinition, StructureDefinitionSnapshot,
159 };
160
161 fn make_test_element(
162 path: &str,
163 strength: &str,
164 value_set: &str,
165 description: Option<&str>,
166 ) -> ElementDefinition {
167 ElementDefinition {
168 id: Some(path.to_string()),
169 path: path.to_string(),
170 short: None,
171 definition: None,
172 min: None,
173 max: None,
174 element_type: None,
175 fixed: None,
176 pattern: None,
177 binding: Some(FhirElementBinding {
178 strength: strength.to_string(),
179 description: description.map(|s| s.to_string()),
180 value_set: Some(value_set.to_string()),
181 }),
182 constraint: None,
183 }
184 }
185
186 fn make_test_sd(elements: Vec<ElementDefinition>) -> StructureDefinition {
187 StructureDefinition {
188 resource_type: "StructureDefinition".to_string(),
189 id: "test".to_string(),
190 url: "http://test.com/test".to_string(),
191 version: None,
192 name: "Test".to_string(),
193 title: None,
194 status: "active".to_string(),
195 description: None,
196 purpose: None,
197 kind: "resource".to_string(),
198 is_abstract: false,
199 base_type: "Patient".to_string(),
200 base_definition: None,
201 differential: None,
202 snapshot: Some(StructureDefinitionSnapshot { element: elements }),
203 }
204 }
205
206 #[test]
207 fn test_generate_single_binding() {
208 let element = make_test_element(
209 "Patient.gender",
210 "required",
211 "http://hl7.org/fhir/ValueSet/administrative-gender",
212 Some("The gender of the patient"),
213 );
214 let sd = make_test_sd(vec![element]);
215
216 let code = BindingGenerator::generate_bindings_constant(&sd);
217
218 assert!(!code.is_empty());
219 assert!(code.contains("BINDINGS"));
220 assert!(code.contains("Patient.gender"));
221 assert!(code.contains("BindingStrength::Required"));
222 assert!(code.contains("administrative-gender"));
223 assert!(code.contains("The gender of the patient"));
224 }
225
226 #[test]
227 fn test_generate_no_bindings() {
228 let element = ElementDefinition {
229 id: Some("Patient.id".to_string()),
230 path: "Patient.id".to_string(),
231 short: None,
232 definition: None,
233 min: None,
234 max: None,
235 element_type: None,
236 fixed: None,
237 pattern: None,
238 binding: None,
239 constraint: None,
240 };
241 let sd = make_test_sd(vec![element]);
242
243 let code = BindingGenerator::generate_bindings_constant(&sd);
244
245 assert!(code.is_empty());
246 }
247
248 #[test]
249 fn test_generate_skip_non_required() {
250 let element = make_test_element(
251 "Patient.maritalStatus",
252 "extensible",
253 "http://hl7.org/fhir/ValueSet/marital-status",
254 None,
255 );
256 let sd = make_test_sd(vec![element]);
257
258 let code = BindingGenerator::generate_bindings_constant(&sd);
259
260 assert!(code.is_empty());
262 }
263
264 #[test]
265 fn test_string_escaping() {
266 assert_eq!(escape_rust_string("simple"), "simple");
267 assert_eq!(escape_rust_string("with \"quotes\""), "with \\\"quotes\\\"");
268 assert_eq!(escape_rust_string("with \n newline"), "with \\n newline");
269 assert_eq!(
270 escape_rust_string("with \\ backslash"),
271 "with \\\\ backslash"
272 );
273 }
274
275 #[test]
276 fn test_generate_tokens() {
277 let element = make_test_element(
278 "Patient.gender",
279 "required",
280 "http://hl7.org/fhir/ValueSet/administrative-gender",
281 Some("The gender of the patient"),
282 );
283 let sd = make_test_sd(vec![element]);
284
285 let tokens = BindingGenerator::generate_bindings_tokens(&sd);
286 let code = tokens.to_string();
287
288 assert!(!code.is_empty());
289 assert!(code.contains("BINDINGS"));
290 assert!(code.contains("Patient.gender"));
291 }
292}