haste_repository/
utilities.rs1use std::sync::LazyLock;
2
3use haste_fhir_model::r4::{
4 datetime::Instant,
5 generated::{
6 resources::Resource,
7 terminology::IssueType,
8 types::{Extension, ExtensionValueTypeChoice, FHIRId, Meta, Reference},
9 },
10};
11use haste_fhir_operation_error::{OperationOutcomeError, derive::OperationOutcomeError};
12use haste_jwt::{AuthorId, AuthorKind};
13use haste_reflect::MetaValue;
14
15static ID_CHARACTERS: &[char] = &[
16 '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
17 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '-',
18];
19
20pub fn generate_id(len: Option<usize>) -> String {
23 let len = len.unwrap_or(26);
24 nanoid::nanoid!(len, ID_CHARACTERS).to_string()
25}
26
27static ID_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
28 let characters_allowed = ID_CHARACTERS.iter().collect::<String>();
29 regex::Regex::new(&format!("^[{characters_allowed}]*$"))
30 .expect("ID_CHARACTERS should produce a valid regex")
31});
32
33pub fn validate_id(id: &str) -> Result<(), OperationOutcomeError> {
40 if ID_REGEX.is_match(id) {
41 Ok(())
42 } else {
43 Err(OperationOutcomeError::fatal(
44 IssueType::invalid(),
45 format!("ID contains invalid characters: {id}"),
46 ))
47 }
48}
49
50#[derive(OperationOutcomeError)]
51pub enum DataTransformError {
52 #[error(code = "invalid", diagnostic = "Invalid data: '{arg0}'")]
53 InvalidData(String),
54 #[error(code = "not-found", diagnostic = "Data not found")]
55 NotFound(String),
56}
57
58pub fn set_resource_id(
68 resource: &mut Resource,
69 id_: Option<String>,
70) -> Result<(), OperationOutcomeError> {
71 let id: &mut dyn std::any::Any =
72 resource
73 .get_field_mut("id")
74 .ok_or(DataTransformError::InvalidData(
75 "Missing 'id' field".to_string(),
76 ))?;
77 let id: &mut Option<String> =
78 id.downcast_mut::<Option<String>>()
79 .ok_or(DataTransformError::InvalidData(
80 "Invalid 'id' field".to_string(),
81 ))?;
82 *id = Some(id_.unwrap_or_else(|| generate_id(None)));
83 Ok(())
84}
85
86fn get_or_create_meta(resource: &mut Resource) -> Result<&mut Box<Meta>, OperationOutcomeError> {
87 let meta: &mut dyn std::any::Any =
88 resource
89 .get_field_mut("meta")
90 .ok_or(DataTransformError::InvalidData(
91 "Missing 'meta' field".to_string(),
92 ))?;
93 let meta: &mut Option<Box<Meta>> =
94 meta.downcast_mut::<Option<Box<Meta>>>()
95 .ok_or(DataTransformError::InvalidData(
96 "Invalid 'meta' field".to_string(),
97 ))?;
98
99 if meta.is_none() {
100 *meta = Some(Box::new(Meta::default()));
101 }
102
103 let meta = meta.as_mut().ok_or(DataTransformError::NotFound(
104 "Failed to create 'meta' field".to_string(),
105 ))?;
106
107 Ok(meta)
108}
109
110static HASTE_AUTHOR_ING_EXTENSION_URL: &str = "https://haste.health/author";
111
112static HASTE_EXTENSIONS: &[&str] = &[HASTE_AUTHOR_ING_EXTENSION_URL];
113
114fn create_haste_extensions(author_type: &AuthorKind, author_id: &AuthorId) -> Vec<Extension> {
115 vec![Extension {
116 url: HASTE_AUTHOR_ING_EXTENSION_URL.to_string(),
117 value: Some(ExtensionValueTypeChoice::Reference(Box::new(Reference {
118 reference: Some(Box::new(format!("{author_type}/{author_id}").into())),
119 ..Default::default()
120 }))),
121 ..Default::default()
122 }]
123}
124
125fn filter_and_set_haste_extensions(
126 meta: &mut Meta,
127 author_type: &AuthorKind,
128 author_id: &AuthorId,
129) {
130 let haste_extensions = create_haste_extensions(author_type, author_id);
131
132 meta.extension = Some(
133 meta.extension
134 .take()
135 .unwrap_or_default()
136 .into_iter()
137 .filter(|ext| !HASTE_EXTENSIONS.contains(&ext.url.as_str()))
138 .chain(haste_extensions)
139 .collect::<Vec<Extension>>(),
140 );
141}
142
143pub fn set_resource_meta(
153 resource: &mut Resource,
154 author_type: &AuthorKind,
155 author_id: &AuthorId,
156) -> Result<(), OperationOutcomeError> {
157 let meta = get_or_create_meta(resource)?;
158 meta.versionId = Some(Box::new(FHIRId {
159 id: None,
160 extension: None,
161 value: Some(generate_id(None)),
162 }));
163
164 meta.lastUpdated = Some(Box::new(Instant::Iso8601(chrono::Utc::now()).into()));
165
166 filter_and_set_haste_extensions(meta, author_type, author_id);
167
168 Ok(())
169}