Skip to main content

haste_fhir_profiling/
lib.rs

1//! Validates a FHIR resource against a profile (a `StructureDefinition` with
2//! `derivation = constraint`).
3//!
4//! A profile is compiled once into a tree the validator walks directly, and
5//! that compiled form, the profiles it refers to and the value sets its slices
6//! bind are cached for as long as the [`CanonicalResolver`] keeps handing out
7//! the same resources. Validating a resource is then a synchronous walk over
8//! the value with no path parsing and no resolver calls.
9use haste_fhir_client::canonical_resolver::CanonicalResolver;
10use haste_fhir_model::r4::generated::{
11    resources::{OperationOutcome, Resource, ResourceType, StructureDefinition},
12    terminology::{IssueType, TypeDerivationRule},
13};
14use haste_fhir_operation_error::OperationOutcomeError;
15use haste_reflect::MetaValue;
16use std::sync::Arc;
17
18mod compile;
19mod graph;
20mod slicing;
21mod validate;
22mod validators;
23
24pub struct FHIRProfileArguments<Resolver: CanonicalResolver> {
25    resolver: Arc<Resolver>,
26}
27
28impl<Resolver: CanonicalResolver> FHIRProfileArguments<Resolver> {
29    pub fn new(resolver: Arc<Resolver>) -> Self {
30        Self { resolver }
31    }
32}
33
34#[derive(Clone)]
35pub struct FHIRProfileCTX<'a, Resolver: CanonicalResolver> {
36    resolver: Arc<Resolver>,
37    profile: Arc<Resource>,
38    root: &'a dyn MetaValue,
39}
40
41impl<'a, Resolver: CanonicalResolver> FHIRProfileCTX<'a, Resolver> {
42    /// Creates a new FHIR profile context.
43    ///
44    /// # Errors
45    ///
46    /// Returns [`OperationOutcomeError`] if `profile` is not a
47    /// [`StructureDefinition`] resource.
48    pub fn new(
49        resolver: Arc<Resolver>,
50        profile: Arc<Resource>,
51        root: &'a dyn MetaValue,
52    ) -> Result<Self, OperationOutcomeError> {
53        match &*profile {
54            Resource::StructureDefinition(_profile) => Ok(Self {
55                resolver,
56                profile,
57                root,
58            }),
59            _ => Err(OperationOutcomeError::error(
60                IssueType::invalid(),
61                "Profile resource must be a StructureDefinition".to_string(),
62            )),
63        }
64    }
65
66    /// Returns the [`StructureDefinition`] associated with this profile context.
67    ///
68    /// # Panics
69    ///
70    /// Panics if the profile is not a [`Resource::StructureDefinition`].
71    #[must_use]
72    pub fn profile(&'a self) -> &'a StructureDefinition {
73        match self.profile.as_ref() {
74            Resource::StructureDefinition(sd) => sd,
75            _ => panic!(
76                "Invalid state for profile ctx, profile field must be a StructureDefinition."
77            ),
78        }
79    }
80}
81
82/// Validates the profile represented by the given context.
83///
84/// # Errors
85///
86/// Returns [`OperationOutcomeError`] if:
87/// - validating an element fails; or
88/// - the profile does not have a derivation of `constraint`.
89pub async fn validate_profile(
90    ctx: Arc<FHIRProfileCTX<'_, impl CanonicalResolver>>,
91) -> Result<OperationOutcome, OperationOutcomeError> {
92    if ctx.profile().derivation.as_ref() != Some(&TypeDerivationRule::constraint()) {
93        return Err(OperationOutcomeError::error(
94            IssueType::invalid(),
95            "Only profiles with derivation 'constraint' are supported".to_string(),
96        ));
97    }
98    let graph = graph::graph_for(ctx.resolver.as_ref(), &ctx.profile).await?;
99    Ok(OperationOutcome {
100        issue: validate::validate(&graph, ctx.root)?,
101        ..Default::default()
102    })
103}
104
105/// Validates a value against the profile identified by its canonical URL.
106///
107/// # Errors
108///
109/// Returns [`OperationOutcomeError`] if resolving the profile fails, if the
110/// resolved resource is not a supported profile, or if profile validation fails.
111pub async fn validate_profile_by_url(
112    args: FHIRProfileArguments<impl CanonicalResolver>,
113    canonical_url: &str,
114    value: &dyn MetaValue,
115) -> Result<OperationOutcome, OperationOutcomeError> {
116    let Some(profile) = args
117        .resolver
118        .resolve(ResourceType::StructureDefinition, canonical_url)
119        .await?
120    else {
121        tracing::warn!("Profile with url '{canonical_url}' not found");
122        return Ok(OperationOutcome::default());
123    };
124
125    let ctx = Arc::new(FHIRProfileCTX::new(args.resolver.clone(), profile, value)?);
126
127    validate_profile(ctx).await
128}