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