Skip to main content

haste_repository/
utilities.rs

1use std::sync::LazyLock;
2
3use haste_fhir_model::r4::generated::{
4    resources::Resource,
5    terminology::IssueType,
6    types::{FHIRId, Meta},
7};
8use haste_fhir_operation_error::{OperationOutcomeError, derive::OperationOutcomeError};
9use haste_reflect::MetaValue;
10
11static ID_CHARACTERS: &[char] = &[
12    '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
13    'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '-',
14];
15
16// [A-Za-z0-9\-\.]{1,64} See https://hl7.org/fhir/r4/datatypes.html#id
17// Can't use _ for compliance.
18pub fn generate_id(len: Option<usize>) -> String {
19    let len = len.unwrap_or(26);
20    nanoid::nanoid!(len, ID_CHARACTERS).to_string()
21}
22
23static ID_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
24    let characters_allowed = ID_CHARACTERS.iter().collect::<String>();
25    regex::Regex::new(&format!("^[{characters_allowed}]*$"))
26        .expect("ID_CHARACTERS should produce a valid regex")
27});
28
29/// Validates a FHIR resource ID.
30///
31/// # Errors
32///
33/// Returns an [`OperationOutcomeError`] if `id` contains characters that are
34/// not permitted in a FHIR resource ID.
35pub fn validate_id(id: &str) -> Result<(), OperationOutcomeError> {
36    if ID_REGEX.is_match(id) {
37        Ok(())
38    } else {
39        Err(OperationOutcomeError::fatal(
40            IssueType::invalid(),
41            format!("ID contains invalid characters: {id}"),
42        ))
43    }
44}
45
46#[derive(OperationOutcomeError)]
47pub enum DataTransformError {
48    #[error(code = "invalid", diagnostic = "Invalid data: '{arg0}'")]
49    InvalidData(String),
50    #[error(code = "not-found", diagnostic = "Data not found")]
51    NotFound(String),
52}
53
54/// Sets the ID of a FHIR resource.
55///
56/// If `id_` is `Some`, that value is assigned as the resource ID. Otherwise, a
57/// new ID is generated and assigned.
58///
59/// # Errors
60///
61/// Returns an [`OperationOutcomeError`] if the resource does not contain an `id`
62/// field or if the `id` field is not of type [`Option<String>`].
63pub fn set_resource_id(
64    resource: &mut Resource,
65    id_: Option<String>,
66) -> Result<(), OperationOutcomeError> {
67    let id: &mut dyn std::any::Any =
68        resource
69            .get_field_mut("id")
70            .ok_or(DataTransformError::InvalidData(
71                "Missing 'id' field".to_string(),
72            ))?;
73    let id: &mut Option<String> =
74        id.downcast_mut::<Option<String>>()
75            .ok_or(DataTransformError::InvalidData(
76                "Invalid 'id' field".to_string(),
77            ))?;
78    *id = Some(id_.unwrap_or_else(|| generate_id(None)));
79    Ok(())
80}
81
82/// Sets the version ID of a FHIR resource.
83///
84/// If the resource does not have a `meta` element, one is created. The
85/// `versionId` field is then populated with a newly generated ID.
86///
87/// # Errors
88///
89/// Returns an [`OperationOutcomeError`] if the resource does not contain a
90/// `meta` field or if the `meta` field is not of type [`Option<Box<Meta>>`].
91pub fn set_version_id(resource: &mut Resource) -> Result<(), OperationOutcomeError> {
92    let meta: &mut dyn std::any::Any =
93        resource
94            .get_field_mut("meta")
95            .ok_or(DataTransformError::InvalidData(
96                "Missing 'meta' field".to_string(),
97            ))?;
98    let meta: &mut Option<Box<Meta>> =
99        meta.downcast_mut::<Option<Box<Meta>>>()
100            .ok_or(DataTransformError::InvalidData(
101                "Invalid 'meta' field".to_string(),
102            ))?;
103
104    if meta.is_none() {
105        *meta = Some(Box::new(Meta::default()));
106    }
107    if let Some(meta) = meta.as_mut() {
108        meta.versionId = Some(Box::new(FHIRId {
109            id: None,
110            extension: None,
111            value: Some(generate_id(None)),
112        }));
113    }
114
115    Ok(())
116}