Skip to main content

rh_codegen/generators/
binding_generator.rs

1//! Binding constant generation
2//!
3//! This module generates constant declarations for FHIR element bindings that can be
4//! embedded in generated resource and datatype structs.
5
6use crate::bindings;
7use crate::fhir_types::StructureDefinition;
8use quote::quote;
9
10/// Generator for binding constants
11pub struct BindingGenerator;
12
13impl BindingGenerator {
14    /// Generate a BINDINGS constant for a StructureDefinition
15    ///
16    /// Returns Rust code as a string containing the static declaration,
17    /// or an empty string if there are no required bindings.
18    ///
19    /// Uses `once_cell::sync::Lazy` for runtime initialization.
20    ///
21    /// Example output:
22    /// ```rust,ignore
23    /// pub static BINDINGS: once_cell::sync::Lazy<Vec<rh_foundation::ElementBinding>> =
24    ///     once_cell::sync::Lazy::new(|| vec![
25    ///         rh_foundation::ElementBinding::new(
26    ///             "Patient.gender",
27    ///             rh_foundation::BindingStrength::Required,
28    ///             "http://hl7.org/fhir/ValueSet/administrative-gender"
29    ///         ).with_description("The gender of the patient"),
30    ///     ]);
31    /// ```
32    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            // Escape strings for Rust string literals
52            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            // Use the builder pattern: ElementBinding::new().with_description() if description exists
71            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    /// Generate bindings constant using quote! macro (alternative implementation)
88    #[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            /// FHIR required bindings for this resource/datatype
134            ///
135            /// These bindings define which ValueSets must be used for coded elements.
136            /// Only 'required' strength bindings are included (extensible/preferred are not enforced).
137            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
145/// Escape a string for use in a Rust string literal
146fn 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        // Should be empty because binding is not required
261        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}