Skip to main content

haste_operation_executor/
validate.rs

1use haste_fhir_model::r4::generated::{
2    resources::{
3        OperationDefinitionParameter, OperationOutcome, OperationOutcomeIssue, Parameters,
4        ParametersParameter,
5    },
6    terminology::{BoundCode, IssueSeverity, IssueType, OperationParameterUse},
7};
8use haste_fhir_operation_error::OperationOutcomeError;
9use haste_reflect::MetaValue as _;
10
11/// Which direction of `OperationDefinition.parameter` to validate against.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ParameterDirection {
14    In,
15    Out,
16}
17
18fn create_issue(
19    severity: BoundCode<IssueSeverity>,
20    type_: BoundCode<IssueType>,
21    diagnostics: String,
22) -> OperationOutcomeIssue {
23    OperationOutcomeIssue {
24        severity: severity,
25        code: type_,
26        diagnostics: Some(Box::new(diagnostics.into())),
27        ..Default::default()
28    }
29}
30
31/// Validate Parameters against the corresponding OperationDefinitionParameter definitions for the specified direction.
32pub fn validate_parameters(
33    parameters: &Parameters,
34    operation_params: &[OperationDefinitionParameter],
35    direction: &BoundCode<OperationParameterUse>,
36) -> Result<(), OperationOutcomeError> {
37    let parameter_definitions: Vec<&OperationDefinitionParameter> = operation_params
38        .iter()
39        .filter(|p| &p.use_ == direction)
40        .collect();
41
42    let parameters_to_validate: &[ParametersParameter] =
43        parameters.parameter.as_deref().unwrap_or_default();
44
45    let mut issues: Vec<OperationOutcomeIssue> = Vec::new();
46
47    // --- Check each definition against what was supplied ---
48    for parameter_definition in &parameter_definitions {
49        let name = match parameter_definition.name.value.as_deref() {
50            Some(n) => n,
51            None => continue,
52        };
53
54        let found_parameters: Vec<&ParametersParameter> = parameters_to_validate
55            .iter()
56            .filter(|p| p.name.value.as_deref() == Some(name))
57            .collect();
58
59        let count = found_parameters.len() as i64;
60
61        // Minimum cardinality
62        let min = parameter_definition.min.value.unwrap_or(0);
63        if count < min {
64            issues.push(create_issue(
65                IssueSeverity::error(),
66                IssueType::invariant(),
67                format!(
68                    "Parameter '{}' requires at least {} occurrence(s) but only {} were supplied.",
69                    name, min, count
70                ),
71            ));
72        }
73
74        // Maximum cardinality ("*" means unbounded)
75        if let Some(max_str) = parameter_definition.max.value.as_deref() {
76            if max_str != "*" {
77                if let Ok(max) = max_str.parse::<i64>() {
78                    if count > max {
79                        issues.push(create_issue(IssueSeverity::error(), IssueType::invariant(),
80                        format!(
81                                "Parameter '{}' allows a maximum of {} occurrence(s) but {} were supplied.",
82                                name, max, count
83                            )));
84                    }
85                }
86            }
87        }
88
89        // Validate type if specified. The type of a supplied parameter is determined by:
90        // 1. If it has a `resource` field, use the resource type.
91        // 2. Otherwise, use the type of the `value` field.
92        if let Some(parameter_def_type) = &parameter_definition.type_ {
93            let type_name = parameter_def_type.as_str();
94            for found_parameter in found_parameters.iter() {
95                let type_ = if let Some(resource) = found_parameter.resource.as_ref() {
96                    resource.fhir_type()
97                } else {
98                    found_parameter.value.fhir_type()
99                };
100
101                if type_ != type_name.as_deref().unwrap_or_default() {
102                    issues.push(create_issue(
103                        IssueSeverity::error(),
104                        IssueType::invalid(),
105                        format!(
106                            "Parameter '{}' expects type '{}' but found '{}'.",
107                            name,
108                            type_name.as_deref().unwrap_or("<unknown>"),
109                            type_
110                        ),
111                    ));
112                }
113            }
114        }
115
116        // Recursively validate parts when both the definition and the
117        // supplied parameter declare nested parts.
118        if let Some(part_defs) = &parameter_definition.part {
119            for supplied_param in &found_parameters {
120                if let Some(supplied_parts) = &supplied_param.part {
121                    let parts_as_parameters = Parameters {
122                        parameter: Some(supplied_parts.clone()),
123                        ..Default::default()
124                    };
125                    validate_parameters(&parts_as_parameters, part_defs, &direction)?;
126                }
127            }
128        }
129    }
130
131    // --- Warn about parameters that have no matching definition ---
132    for supplied_param in parameters_to_validate {
133        let name = supplied_param.name.value.as_deref().unwrap_or("<unnamed>");
134        let defined = parameter_definitions
135            .iter()
136            .any(|d| d.name.value.as_deref() == Some(name));
137        if !defined {
138            let display_direction = direction.as_str();
139            issues.push(create_issue(
140                IssueSeverity::error(),
141                IssueType::invalid(),
142                format!(
143                    "Parameter '{}' is not defined for the '{}' direction.",
144                    name,
145                    display_direction.as_deref().unwrap_or("<unknown>")
146                ),
147            ));
148        }
149    }
150
151    if issues.is_empty() {
152        Ok(())
153    } else {
154        Err(OperationOutcomeError::new(
155            None,
156            OperationOutcome {
157                issue: issues,
158                ..Default::default()
159            },
160        ))
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use haste_fhir_model::r4::generated::{
168        resources::{
169            OperationDefinitionParameter, Parameters, ParametersParameter,
170            ParametersParameterValueTypeChoice, Patient, Practitioner, Resource,
171        },
172        terminology::{AllTypes, OperationParameterUse},
173        types::{FHIRCode, FHIRInteger, FHIRString},
174    };
175
176    fn make_def(
177        name: &str,
178        direction: BoundCode<OperationParameterUse>,
179        min: i64,
180        max: &str,
181        type_: Option<BoundCode<AllTypes>>,
182    ) -> OperationDefinitionParameter {
183        OperationDefinitionParameter {
184            name: Box::new(FHIRCode {
185                value: Some(name.to_string()),
186                ..Default::default()
187            }),
188            use_: direction,
189            min: Box::new(FHIRInteger {
190                value: Some(min),
191                ..Default::default()
192            }),
193            max: Box::new(FHIRString {
194                value: Some(max.to_string()),
195                ..Default::default()
196            }),
197            type_: type_,
198            ..Default::default()
199        }
200    }
201
202    fn make_param(name: &str) -> ParametersParameter {
203        ParametersParameter {
204            name: Box::new(FHIRString {
205                value: Some(name.to_string()),
206                ..Default::default()
207            }),
208            ..Default::default()
209        }
210    }
211
212    #[test]
213    fn required_param_missing_fails() {
214        let defs = vec![make_def(
215            "subject",
216            OperationParameterUse::in_(),
217            1,
218            "1",
219            None,
220        )];
221        let params = Parameters {
222            parameter: None,
223            ..Default::default()
224        };
225        assert!(validate_parameters(&params, &defs, &OperationParameterUse::in_()).is_err());
226    }
227
228    #[test]
229    fn required_param_present_passes() {
230        let defs = vec![make_def(
231            "subject",
232            OperationParameterUse::in_(),
233            1,
234            "1",
235            None,
236        )];
237        let params = Parameters {
238            parameter: Some(vec![make_param("subject")]),
239            ..Default::default()
240        };
241        assert!(validate_parameters(&params, &defs, &OperationParameterUse::in_()).is_ok());
242    }
243
244    #[test]
245    fn extra_param_is_rejected() {
246        let defs = vec![make_def(
247            "subject",
248            OperationParameterUse::in_(),
249            0,
250            "1",
251            None,
252        )];
253        let params = Parameters {
254            parameter: Some(vec![make_param("unknown")]),
255            ..Default::default()
256        };
257        assert!(validate_parameters(&params, &defs, &OperationParameterUse::in_()).is_err());
258    }
259
260    #[test]
261    fn max_exceeded_fails() {
262        let defs = vec![make_def(
263            "subject",
264            OperationParameterUse::in_(),
265            0,
266            "1",
267            None,
268        )];
269        let params = Parameters {
270            parameter: Some(vec![make_param("subject"), make_param("subject")]),
271            ..Default::default()
272        };
273        assert!(validate_parameters(&params, &defs, &OperationParameterUse::in_()).is_err());
274    }
275
276    #[test]
277    fn out_direction_ignored_for_in_validation() {
278        // An "out" definition should be invisible when validating "in"
279        let defs = vec![make_def(
280            "result",
281            OperationParameterUse::out(),
282            1,
283            "1",
284            None,
285        )];
286        let params = Parameters {
287            parameter: None,
288            ..Default::default()
289        };
290        // No "in" definitions exist, so nothing to violate → should pass.
291        assert!(validate_parameters(&params, &defs, &OperationParameterUse::in_()).is_ok());
292    }
293
294    #[test]
295    fn unbounded_max_passes() {
296        let defs = vec![make_def("note", OperationParameterUse::in_(), 0, "*", None)];
297        let params = Parameters {
298            parameter: Some(vec![
299                make_param("note"),
300                make_param("note"),
301                make_param("note"),
302            ]),
303            ..Default::default()
304        };
305        assert!(validate_parameters(&params, &defs, &OperationParameterUse::in_()).is_ok());
306    }
307
308    #[test]
309    fn basic_type_validation() {
310        let defs = vec![make_def(
311            "note",
312            OperationParameterUse::in_(),
313            0,
314            "*",
315            Some(AllTypes::string()),
316        )];
317
318        let mut parameter_note = make_param("note");
319        parameter_note.value = Some(ParametersParameterValueTypeChoice::String(Box::new(
320            FHIRString {
321                value: Some("This is a note.".to_string()),
322                ..Default::default()
323            },
324        )));
325
326        let params = Parameters {
327            parameter: Some(vec![parameter_note.clone(), parameter_note.clone()]),
328            ..Default::default()
329        };
330
331        assert!(validate_parameters(&params, &defs, &OperationParameterUse::in_()).is_ok());
332
333        parameter_note.value = Some(ParametersParameterValueTypeChoice::Integer(Box::new(
334            FHIRInteger {
335                value: Some(42),
336                ..Default::default()
337            },
338        )));
339
340        let params = Parameters {
341            parameter: Some(vec![parameter_note]),
342            ..Default::default()
343        };
344
345        assert!(validate_parameters(&params, &defs, &OperationParameterUse::in_()).is_err());
346    }
347
348    #[test]
349    fn resource_validation() {
350        let defs = vec![make_def(
351            "note",
352            OperationParameterUse::in_(),
353            0,
354            "*",
355            Some(AllTypes::patient()),
356        )];
357
358        let mut parameter_note = make_param("note");
359        parameter_note.resource = Some(Box::new(Resource::Patient(Patient {
360            ..Default::default()
361        })));
362
363        let params = Parameters {
364            parameter: Some(vec![parameter_note.clone(), parameter_note.clone()]),
365            ..Default::default()
366        };
367
368        assert!(validate_parameters(&params, &defs, &OperationParameterUse::in_()).is_ok());
369
370        parameter_note.resource = Some(Box::new(Resource::Practitioner(Practitioner {
371            ..Default::default()
372        })));
373
374        let params = Parameters {
375            parameter: Some(vec![parameter_note]),
376            ..Default::default()
377        };
378
379        assert!(validate_parameters(&params, &defs, &OperationParameterUse::in_()).is_err());
380    }
381
382    #[test]
383    fn test_nested() {
384        let mut parent = make_def("parent", OperationParameterUse::in_(), 1, "1", None);
385
386        parent.part = Some(vec![make_def(
387            "child",
388            OperationParameterUse::in_(),
389            1,
390            "1",
391            Some(AllTypes::string()),
392        )]);
393
394        let defs = vec![parent];
395
396        let mut child_param = make_param("child");
397        child_param.value = Some(ParametersParameterValueTypeChoice::String(Box::new(
398            FHIRString {
399                value: Some("I am a child parameter.".to_string()),
400                ..Default::default()
401            },
402        )));
403
404        let mut parent_param = make_param("parent");
405        parent_param.part = Some(vec![child_param.clone()]);
406
407        let params = Parameters {
408            parameter: Some(vec![parent_param.clone()]),
409            ..Default::default()
410        };
411
412        assert!(validate_parameters(&params, &defs, &OperationParameterUse::in_()).is_ok());
413
414        child_param.value = Some(ParametersParameterValueTypeChoice::Integer(Box::new(
415            FHIRInteger {
416                value: Some(42),
417                ..Default::default()
418            },
419        )));
420
421        parent_param.part = Some(vec![child_param]);
422
423        let params = Parameters {
424            parameter: Some(vec![parent_param.clone()]),
425            ..Default::default()
426        };
427
428        assert!(validate_parameters(&params, &defs, &OperationParameterUse::in_()).is_err());
429    }
430}