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::{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: IssueSeverity,
20    type_: IssueType,
21    diagnostics: String,
22) -> OperationOutcomeIssue {
23    OperationOutcomeIssue {
24        severity: Box::new(severity),
25        code: Box::new(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: &OperationParameterUse,
36) -> Result<(), OperationOutcomeError> {
37    let parameter_definitions: Vec<&OperationDefinitionParameter> = operation_params
38        .iter()
39        .filter(|p| std::mem::discriminant(p.use_.as_ref()) == std::mem::discriminant(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(None),
66                IssueType::Invariant(None),
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(None), IssueType::Invariant(None),
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: Option<String> = parameter_def_type.as_ref().into();
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(None),
104                        IssueType::Invalid(None),
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: Option<String> = (direction).into();
139            issues.push(create_issue(
140                IssueSeverity::Error(None),
141                IssueType::Invalid(None),
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: OperationParameterUse,
179        min: i64,
180        max: &str,
181        type_: Option<Box<AllTypes>>,
182    ) -> OperationDefinitionParameter {
183        OperationDefinitionParameter {
184            name: Box::new(FHIRCode {
185                value: Some(name.to_string()),
186                ..Default::default()
187            }),
188            use_: Box::new(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(None),
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(None)).is_err());
226    }
227
228    #[test]
229    fn required_param_present_passes() {
230        let defs = vec![make_def(
231            "subject",
232            OperationParameterUse::In(None),
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(None)).is_ok());
242    }
243
244    #[test]
245    fn extra_param_is_rejected() {
246        let defs = vec![make_def(
247            "subject",
248            OperationParameterUse::In(None),
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(None)).is_err());
258    }
259
260    #[test]
261    fn max_exceeded_fails() {
262        let defs = vec![make_def(
263            "subject",
264            OperationParameterUse::In(None),
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(None)).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(None),
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(None)).is_ok());
292    }
293
294    #[test]
295    fn unbounded_max_passes() {
296        let defs = vec![make_def(
297            "note",
298            OperationParameterUse::In(None),
299            0,
300            "*",
301            None,
302        )];
303        let params = Parameters {
304            parameter: Some(vec![
305                make_param("note"),
306                make_param("note"),
307                make_param("note"),
308            ]),
309            ..Default::default()
310        };
311        assert!(validate_parameters(&params, &defs, &OperationParameterUse::In(None)).is_ok());
312    }
313
314    #[test]
315    fn basic_type_validation() {
316        let defs = vec![make_def(
317            "note",
318            OperationParameterUse::In(None),
319            0,
320            "*",
321            Some(Box::new(AllTypes::String(None))),
322        )];
323
324        let mut parameter_note = make_param("note");
325        parameter_note.value = Some(ParametersParameterValueTypeChoice::String(Box::new(
326            FHIRString {
327                value: Some("This is a note.".to_string()),
328                ..Default::default()
329            },
330        )));
331
332        let params = Parameters {
333            parameter: Some(vec![parameter_note.clone(), parameter_note.clone()]),
334            ..Default::default()
335        };
336
337        assert!(validate_parameters(&params, &defs, &OperationParameterUse::In(None)).is_ok());
338
339        parameter_note.value = Some(ParametersParameterValueTypeChoice::Integer(Box::new(
340            FHIRInteger {
341                value: Some(42),
342                ..Default::default()
343            },
344        )));
345
346        let params = Parameters {
347            parameter: Some(vec![parameter_note]),
348            ..Default::default()
349        };
350
351        assert!(validate_parameters(&params, &defs, &OperationParameterUse::In(None)).is_err());
352    }
353
354    #[test]
355    fn resource_validation() {
356        let defs = vec![make_def(
357            "note",
358            OperationParameterUse::In(None),
359            0,
360            "*",
361            Some(Box::new(AllTypes::Patient(None))),
362        )];
363
364        let mut parameter_note = make_param("note");
365        parameter_note.resource = Some(Box::new(Resource::Patient(Patient {
366            ..Default::default()
367        })));
368
369        let params = Parameters {
370            parameter: Some(vec![parameter_note.clone(), parameter_note.clone()]),
371            ..Default::default()
372        };
373
374        assert!(validate_parameters(&params, &defs, &OperationParameterUse::In(None)).is_ok());
375
376        parameter_note.resource = Some(Box::new(Resource::Practitioner(Practitioner {
377            ..Default::default()
378        })));
379
380        let params = Parameters {
381            parameter: Some(vec![parameter_note]),
382            ..Default::default()
383        };
384
385        assert!(validate_parameters(&params, &defs, &OperationParameterUse::In(None)).is_err());
386    }
387
388    #[test]
389    fn test_nested() {
390        let mut parent = make_def("parent", OperationParameterUse::In(None), 1, "1", None);
391
392        parent.part = Some(vec![make_def(
393            "child",
394            OperationParameterUse::In(None),
395            1,
396            "1",
397            Some(Box::new(AllTypes::String(None))),
398        )]);
399
400        let defs = vec![parent];
401
402        let mut child_param = make_param("child");
403        child_param.value = Some(ParametersParameterValueTypeChoice::String(Box::new(
404            FHIRString {
405                value: Some("I am a child parameter.".to_string()),
406                ..Default::default()
407            },
408        )));
409
410        let mut parent_param = make_param("parent");
411        parent_param.part = Some(vec![child_param.clone()]);
412
413        let params = Parameters {
414            parameter: Some(vec![parent_param.clone()]),
415            ..Default::default()
416        };
417
418        assert!(validate_parameters(&params, &defs, &OperationParameterUse::In(None)).is_ok());
419
420        child_param.value = Some(ParametersParameterValueTypeChoice::Integer(Box::new(
421            FHIRInteger {
422                value: Some(42),
423                ..Default::default()
424            },
425        )));
426
427        parent_param.part = Some(vec![child_param]);
428
429        let params = Parameters {
430            parameter: Some(vec![parent_param.clone()]),
431            ..Default::default()
432        };
433
434        assert!(validate_parameters(&params, &defs, &OperationParameterUse::In(None)).is_err());
435    }
436}