1use helios_fhirpath_support::{EvaluationResult, TypeInfoResult};
20use serde_json::Value;
21
22use crate::SofError;
23
24#[derive(Debug, Clone, PartialEq)]
30pub enum ConstantValue {
31 String(String),
33 Code(String),
35 Identifier(String),
38 Base64Binary(String),
40 Markdown(String),
43 Boolean(bool),
45 Integer(i64),
47 PositiveInt(i64),
49 UnsignedInt(i64),
51 Integer64(i64),
53 Decimal(String),
56 Date(String),
58 DateTime(String),
61 Time(String),
64 Instant(String),
67}
68
69impl ConstantValue {
70 pub fn to_evaluation_result(&self) -> Result<EvaluationResult, SofError> {
75 Ok(match self {
76 ConstantValue::String(s)
77 | ConstantValue::Code(s)
78 | ConstantValue::Identifier(s)
79 | ConstantValue::Base64Binary(s)
80 | ConstantValue::Markdown(s) => EvaluationResult::String(s.clone(), None, None),
81 ConstantValue::Boolean(b) => EvaluationResult::Boolean(*b, None, None),
82 ConstantValue::Integer(i)
83 | ConstantValue::PositiveInt(i)
84 | ConstantValue::UnsignedInt(i) => EvaluationResult::Integer(*i, None, None),
85 ConstantValue::Integer64(i) => EvaluationResult::Integer64(*i, None, None),
86 ConstantValue::Decimal(s) => {
87 let parsed = s.parse().map_err(|_| {
88 SofError::InvalidViewDefinition(format!("Invalid decimal value '{s}'"))
89 })?;
90 EvaluationResult::Decimal(parsed, None, None)
91 }
92 ConstantValue::Date(s) => EvaluationResult::Date(s.clone(), None, None),
93 ConstantValue::DateTime(s) => EvaluationResult::DateTime(
94 prefix_at(s),
95 Some(TypeInfoResult::new("FHIR", "dateTime")),
96 None,
97 ),
98 ConstantValue::Time(s) => EvaluationResult::Time(prefix_at_t(s), None, None),
99 ConstantValue::Instant(s) => EvaluationResult::DateTime(
100 prefix_at(s),
101 Some(TypeInfoResult::new("FHIR", "instant")),
102 None,
103 ),
104 })
105 }
106}
107
108fn prefix_at(s: &str) -> String {
109 if s.starts_with('@') {
110 s.to_string()
111 } else {
112 format!("@{s}")
113 }
114}
115
116fn prefix_at_t(s: &str) -> String {
117 if s.starts_with("@T") {
118 s.to_string()
119 } else {
120 format!("@T{s}")
121 }
122}
123
124pub fn parse_constant_from_json(c: &Value) -> Result<(String, ConstantValue), SofError> {
132 let name = c
133 .get("name")
134 .and_then(|v| v.as_str())
135 .ok_or_else(|| {
136 SofError::InvalidViewDefinition("ViewDefinition.constant.name is required".to_string())
137 })?
138 .to_string();
139 let value = read_constant_value(c).ok_or_else(|| {
140 SofError::InvalidViewDefinition(format!(
141 "ViewDefinition.constant '{name}' must have exactly one supported value[X] field"
142 ))
143 })?;
144 Ok((name, value))
145}
146
147fn read_constant_value(c: &Value) -> Option<ConstantValue> {
148 if let Some(s) = c.get("valueString").and_then(|v| v.as_str()) {
149 return Some(ConstantValue::String(s.to_string()));
150 }
151 if let Some(b) = c.get("valueBoolean").and_then(|v| v.as_bool()) {
152 return Some(ConstantValue::Boolean(b));
153 }
154 if let Some(n) = c.get("valueInteger").and_then(|v| v.as_i64()) {
155 return Some(ConstantValue::Integer(n));
156 }
157 if let Some(n) = c.get("valueInteger64").and_then(|v| v.as_i64()) {
158 return Some(ConstantValue::Integer64(n));
159 }
160 if let Some(n) = c.get("valuePositiveInt").and_then(|v| v.as_i64()) {
161 return Some(ConstantValue::PositiveInt(n));
162 }
163 if let Some(n) = c.get("valueUnsignedInt").and_then(|v| v.as_i64()) {
164 return Some(ConstantValue::UnsignedInt(n));
165 }
166 if let Some(n) = c.get("valueDecimal") {
167 return Some(ConstantValue::Decimal(n.to_string()));
169 }
170 if let Some(s) = c.get("valueCode").and_then(|v| v.as_str()) {
171 return Some(ConstantValue::Code(s.to_string()));
172 }
173 if let Some(s) = c.get("valueBase64Binary").and_then(|v| v.as_str()) {
174 return Some(ConstantValue::Base64Binary(s.to_string()));
175 }
176 if let Some(s) = c.get("valueMarkdown").and_then(|v| v.as_str()) {
177 return Some(ConstantValue::Markdown(s.to_string()));
178 }
179 for key in [
180 "valueId",
181 "valueUri",
182 "valueUrl",
183 "valueOid",
184 "valueUuid",
185 "valueCanonical",
186 ] {
187 if let Some(s) = c.get(key).and_then(|v| v.as_str()) {
188 return Some(ConstantValue::Identifier(s.to_string()));
189 }
190 }
191 if let Some(s) = c.get("valueDate").and_then(|v| v.as_str()) {
192 return Some(ConstantValue::Date(s.to_string()));
193 }
194 if let Some(s) = c.get("valueDateTime").and_then(|v| v.as_str()) {
195 return Some(ConstantValue::DateTime(s.to_string()));
196 }
197 if let Some(s) = c.get("valueTime").and_then(|v| v.as_str()) {
198 return Some(ConstantValue::Time(s.to_string()));
199 }
200 if let Some(s) = c.get("valueInstant").and_then(|v| v.as_str()) {
201 return Some(ConstantValue::Instant(s.to_string()));
202 }
203 None
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use serde_json::json;
210
211 fn parse(v: serde_json::Value) -> ConstantValue {
212 parse_constant_from_json(&v).expect("parse").1
213 }
214
215 #[test]
216 fn each_value_field_lowers_to_matching_variant() {
217 let cases: &[(serde_json::Value, ConstantValue)] = &[
218 (
219 json!({"name": "x", "valueString": "hello"}),
220 ConstantValue::String("hello".to_string()),
221 ),
222 (
223 json!({"name": "x", "valueBoolean": true}),
224 ConstantValue::Boolean(true),
225 ),
226 (
227 json!({"name": "x", "valueInteger": 7}),
228 ConstantValue::Integer(7),
229 ),
230 (
231 json!({"name": "x", "valueInteger64": 9_000_000_000i64}),
232 ConstantValue::Integer64(9_000_000_000),
233 ),
234 (
235 json!({"name": "x", "valuePositiveInt": 2}),
236 ConstantValue::PositiveInt(2),
237 ),
238 (
239 json!({"name": "x", "valueUnsignedInt": 0}),
240 ConstantValue::UnsignedInt(0),
241 ),
242 (
243 json!({"name": "x", "valueDecimal": 1.25}),
244 ConstantValue::Decimal("1.25".to_string()),
245 ),
246 (
247 json!({"name": "x", "valueCode": "active"}),
248 ConstantValue::Code("active".to_string()),
249 ),
250 (
251 json!({"name": "x", "valueBase64Binary": "QUJD"}),
252 ConstantValue::Base64Binary("QUJD".to_string()),
253 ),
254 (
255 json!({"name": "x", "valueMarkdown": "# h"}),
256 ConstantValue::Markdown("# h".to_string()),
257 ),
258 (
259 json!({"name": "x", "valueId": "abc-123"}),
260 ConstantValue::Identifier("abc-123".to_string()),
261 ),
262 (
263 json!({"name": "x", "valueUri": "http://example.org/"}),
264 ConstantValue::Identifier("http://example.org/".to_string()),
265 ),
266 (
267 json!({"name": "x", "valueUrl": "http://example.org/r"}),
268 ConstantValue::Identifier("http://example.org/r".to_string()),
269 ),
270 (
271 json!({"name": "x", "valueOid": "urn:oid:1.2.3"}),
272 ConstantValue::Identifier("urn:oid:1.2.3".to_string()),
273 ),
274 (
275 json!({"name": "x", "valueUuid": "urn:uuid:00000000-0000-0000-0000-000000000000"}),
276 ConstantValue::Identifier(
277 "urn:uuid:00000000-0000-0000-0000-000000000000".to_string(),
278 ),
279 ),
280 (
281 json!({"name": "x", "valueCanonical": "http://x|1"}),
282 ConstantValue::Identifier("http://x|1".to_string()),
283 ),
284 (
285 json!({"name": "x", "valueDate": "2024-01-02"}),
286 ConstantValue::Date("2024-01-02".to_string()),
287 ),
288 (
289 json!({"name": "x", "valueDateTime": "2024-01-02T03:04:05Z"}),
290 ConstantValue::DateTime("2024-01-02T03:04:05Z".to_string()),
291 ),
292 (
293 json!({"name": "x", "valueTime": "03:04:05"}),
294 ConstantValue::Time("03:04:05".to_string()),
295 ),
296 (
297 json!({"name": "x", "valueInstant": "2024-01-02T03:04:05Z"}),
298 ConstantValue::Instant("2024-01-02T03:04:05Z".to_string()),
299 ),
300 ];
301 for (input, expected) in cases {
302 assert_eq!(&parse(input.clone()), expected, "input={input}");
303 }
304 }
305
306 #[test]
307 fn missing_name_errors() {
308 let err = parse_constant_from_json(&json!({"valueString": "x"})).unwrap_err();
309 assert!(matches!(err, SofError::InvalidViewDefinition(_)));
310 }
311
312 #[test]
313 fn unknown_value_field_errors() {
314 let err = parse_constant_from_json(&json!({"name": "x", "valueWhatever": 1})).unwrap_err();
315 assert!(matches!(err, SofError::InvalidViewDefinition(_)));
316 }
317
318 #[test]
319 fn datetime_prefixing_idempotent() {
320 let cv = ConstantValue::DateTime("2024-01-02T03:04:05Z".to_string());
321 match cv.to_evaluation_result().unwrap() {
322 EvaluationResult::DateTime(s, _, _) => assert_eq!(s, "@2024-01-02T03:04:05Z"),
323 other => panic!("unexpected: {other:?}"),
324 }
325 let cv = ConstantValue::DateTime("@2024-01-02T03:04:05Z".to_string());
326 match cv.to_evaluation_result().unwrap() {
327 EvaluationResult::DateTime(s, _, _) => assert_eq!(s, "@2024-01-02T03:04:05Z"),
328 other => panic!("unexpected: {other:?}"),
329 }
330 }
331
332 #[test]
333 fn time_prefixing_idempotent() {
334 let cv = ConstantValue::Time("03:04:05".to_string());
335 match cv.to_evaluation_result().unwrap() {
336 EvaluationResult::Time(s, _, _) => assert_eq!(s, "@T03:04:05"),
337 other => panic!("unexpected: {other:?}"),
338 }
339 let cv = ConstantValue::Time("@T03:04:05".to_string());
340 match cv.to_evaluation_result().unwrap() {
341 EvaluationResult::Time(s, _, _) => assert_eq!(s, "@T03:04:05"),
342 other => panic!("unexpected: {other:?}"),
343 }
344 }
345
346 #[test]
347 fn bad_decimal_errors() {
348 let cv = ConstantValue::Decimal("not-a-number".to_string());
349 assert!(matches!(
350 cv.to_evaluation_result(),
351 Err(SofError::InvalidViewDefinition(_))
352 ));
353 }
354}