Skip to main content

haste_fhir_ops/
lib.rs

1use std::{pin::Pin, sync::Arc};
2
3use haste_fhir_client::request::InvocationRequest;
4use haste_fhir_model::r4::generated::{
5    resources::{OperationDefinition, Parameters, ParametersParameter, Resource},
6    terminology::{BoundCode, ResourceTypes},
7    types::FHIRBoolean,
8};
9use haste_fhir_operation_error::OperationOutcomeError;
10use haste_jwt::{ProjectId, TenantId};
11
12#[cfg(feature = "derive")]
13pub mod derive;
14
15pub enum Param<
16    T: TryFrom<Vec<ParametersParameter>, Error = OperationOutcomeError>
17        + Into<Vec<ParametersParameter>>,
18> {
19    Value(T),
20    Parameters(Parameters),
21}
22
23impl<
24    T: TryFrom<Vec<ParametersParameter>, Error = OperationOutcomeError>
25        + Into<Vec<ParametersParameter>>,
26> Param<T>
27{
28    pub fn as_parameters(self) -> Parameters {
29        match self {
30            Param::Value(v) => Parameters {
31                parameter: Some(v.into()),
32                ..Default::default()
33            },
34            Param::Parameters(p) => p,
35        }
36    }
37}
38
39fn allows(level: &FHIRBoolean) -> bool {
40    level.value.unwrap_or(false)
41}
42
43/// The resource types `definition` is defined on, or `None` for any.
44fn resource_types(definition: &OperationDefinition) -> Option<&[BoundCode<ResourceTypes>]> {
45    definition
46        .resource
47        .as_deref()
48        .filter(|types| !types.is_empty())
49}
50
51/// Whether `definition` defines the operation `request` invokes: its code, at
52/// a level it allows, on a resource type it's defined on.
53#[must_use]
54pub fn defines(definition: &OperationDefinition, request: &InvocationRequest) -> bool {
55    let (operation, level, resource_type) = match request {
56        InvocationRequest::System(request) => (&request.operation, &definition.system, None),
57        InvocationRequest::Type(request) => (
58            &request.operation,
59            &definition.type_,
60            Some(&request.resource_type),
61        ),
62        InvocationRequest::Instance(request) => (
63            &request.operation,
64            &definition.instance,
65            Some(&request.resource_type),
66        ),
67    };
68
69    definition.code.value.as_deref() == Some(operation.name())
70        && allows(level)
71        && resource_type.is_none_or(|resource_type| {
72            resource_types(definition).is_none_or(|types| {
73                types
74                    .iter()
75                    .any(|listed| listed.as_str() == Some(resource_type.as_ref()))
76            })
77        })
78}
79
80/// Whether `a` and `b` both define some invocation: the same code at a level
81/// both allow, on a resource type both are defined on below the system level.
82#[must_use]
83pub fn overlaps(a: &OperationDefinition, b: &OperationDefinition) -> bool {
84    let same_code = a.code.value.is_some() && a.code.value == b.code.value;
85    let shared_resource_type = match (resource_types(a), resource_types(b)) {
86        (Some(a), Some(b)) => a.iter().any(|resource_type| b.contains(resource_type)),
87        _ => true,
88    };
89
90    same_code
91        && ((allows(&a.system) && allows(&b.system))
92            || (shared_resource_type
93                && ((allows(&a.type_) && allows(&b.type_))
94                    || (allows(&a.instance) && allows(&b.instance)))))
95}
96
97pub trait OperationInvocation<CTX: Send>: Send + Sync {
98    fn execute<'a>(
99        &self,
100        ctx: CTX,
101        tenant: TenantId,
102        project: ProjectId,
103        request: &'a InvocationRequest,
104    ) -> Pin<Box<dyn Future<Output = Result<Resource, OperationOutcomeError>> + Send + 'a>>;
105    /// The `OperationDefinition` this operation implements.
106    fn operation_definition(&self) -> &'static OperationDefinition;
107}
108
109type Executor<CTX, I, O> = Box<
110    dyn Fn(
111            CTX,
112            TenantId,
113            ProjectId,
114            &InvocationRequest,
115            I,
116        ) -> Pin<Box<dyn Future<Output = Result<O, OperationOutcomeError>> + Send>>
117        + Send
118        + Sync,
119>;
120
121pub struct OperationExecutor<
122    CTX: Send,
123    I: TryFrom<Vec<ParametersParameter>, Error = OperationOutcomeError>
124        + Into<Vec<ParametersParameter>>
125        + Send,
126    O: TryFrom<Vec<ParametersParameter>, Error = OperationOutcomeError> + Into<Resource> + Send,
127> {
128    _ctx: std::marker::PhantomData<CTX>,
129    operation_definition: &'static OperationDefinition,
130    executor: Arc<Executor<CTX, I, O>>,
131}
132
133impl<
134    CTX: Send,
135    I: TryFrom<Vec<ParametersParameter>, Error = OperationOutcomeError>
136        + Into<Vec<ParametersParameter>>
137        + Send,
138    O: TryFrom<Vec<ParametersParameter>, Error = OperationOutcomeError> + Into<Resource> + Send,
139> OperationExecutor<CTX, I, O>
140{
141    /// Runs the operation `operation_definition` (a generated module's
142    /// `OPERATION_DEFINITION`) defines, when it's invoked where the definition
143    /// allows.
144    #[must_use]
145    pub fn new(
146        operation_definition: &'static OperationDefinition,
147        executor: Executor<CTX, I, O>,
148    ) -> Self {
149        Self {
150            _ctx: std::marker::PhantomData,
151            executor: Arc::new(executor),
152            operation_definition,
153        }
154    }
155}
156
157impl<
158    CTX: Send + Sync + 'static,
159    I: TryFrom<Vec<ParametersParameter>, Error = OperationOutcomeError>
160        + Into<Vec<ParametersParameter>>
161        + Send
162        + 'static,
163    O: TryFrom<Vec<ParametersParameter>, Error = OperationOutcomeError>
164        + Into<Resource>
165        + Send
166        + 'static,
167> OperationInvocation<CTX> for OperationExecutor<CTX, I, O>
168{
169    fn execute<'a>(
170        &self,
171        ctx: CTX,
172        tenant: TenantId,
173        project: ProjectId,
174        request: &'a InvocationRequest,
175    ) -> Pin<Box<dyn Future<Output = Result<Resource, OperationOutcomeError>> + Send + 'a>> {
176        let executor = self.executor.clone();
177        Box::pin(async move {
178            let parameters = match request {
179                InvocationRequest::Instance(instance_request) => &instance_request.parameters,
180                InvocationRequest::Type(type_request) => &type_request.parameters,
181                InvocationRequest::System(system_request) => &system_request.parameters,
182            };
183
184            let input = I::try_from(parameters.parameter.clone().unwrap_or_default())?;
185
186            let output = (executor)(ctx, tenant, project, request, input).await?;
187
188            Ok(output.into())
189        })
190    }
191
192    fn operation_definition(&self) -> &'static OperationDefinition {
193        self.operation_definition
194    }
195}