Skip to main content

octofhir_sof/
view_definition.rs

1//! ViewDefinition parsing and types.
2//!
3//! This module defines the data structures for parsing FHIR ViewDefinition resources
4//! as specified in the SQL on FHIR Implementation Guide.
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::Error;
10
11/// A ViewDefinition resource that defines a tabular view over FHIR data.
12///
13/// ViewDefinitions specify how to transform FHIR resources into flat,
14/// tabular data suitable for SQL queries and analytics.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct ViewDefinition {
18    /// The FHIR resource type (always "ViewDefinition" when present).
19    ///
20    /// Optional so that partial ViewDefinition objects (as used by the
21    /// SQL-on-FHIR conformance test suite) parse without a wrapper.
22    pub resource_type: Option<String>,
23
24    /// Canonical URL identifying this ViewDefinition.
25    pub url: Option<String>,
26
27    /// Human-readable name for the view.
28    pub name: Option<String>,
29
30    /// Publication status: draft | active | retired | unknown.
31    pub status: Option<String>,
32
33    /// The FHIR resource type this view is based on (e.g., "Patient", "Observation").
34    pub resource: String,
35
36    /// Description of the view's purpose.
37    pub description: Option<String>,
38
39    /// Target FHIR profiles this view executes against.
40    #[serde(default)]
41    pub profile: Vec<String>,
42
43    /// Applicable FHIR versions for this view.
44    #[serde(default)]
45    pub fhir_version: Vec<String>,
46
47    /// The columns and nested selects to include in the view.
48    #[serde(default)]
49    pub select: Vec<SelectColumn>,
50
51    /// Filter conditions to apply to the view.
52    /// Note: Named `where_` because `where` is a Rust reserved keyword.
53    #[serde(default, rename = "where")]
54    pub where_: Vec<WhereClause>,
55
56    /// Constants that can be referenced in FHIRPath expressions.
57    #[serde(default)]
58    pub constant: Vec<Constant>,
59}
60
61/// A select clause that defines columns or nested structures.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct SelectColumn {
65    /// FHIRPath expression to evaluate for this select.
66    pub path: Option<String>,
67
68    /// Alias for this select (used as table prefix for nested selects).
69    pub alias: Option<String>,
70
71    /// Whether this select represents a collection.
72    #[serde(default)]
73    pub collection: bool,
74
75    /// Nested select clauses.
76    #[serde(default)]
77    pub select: Vec<SelectColumn>,
78
79    /// Column definitions at this level.
80    pub column: Option<Vec<Column>>,
81
82    /// FHIRPath expression for array expansion (creates one row per element).
83    pub for_each: Option<String>,
84
85    /// Like forEach, but includes a row with nulls if the array is empty.
86    pub for_each_or_null: Option<String>,
87
88    /// Recursive FHIRPath expressions for hierarchical data traversal.
89    #[serde(default)]
90    pub repeat: Vec<String>,
91
92    /// Union of multiple select clauses.
93    pub union_all: Option<Vec<SelectColumn>>,
94}
95
96/// A column definition in a ViewDefinition.
97#[derive(Debug, Clone, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct Column {
100    /// The column name in the output.
101    pub name: String,
102
103    /// FHIRPath expression to extract the column value.
104    pub path: String,
105
106    /// Expected data type of the column.
107    #[serde(rename = "type")]
108    pub col_type: Option<String>,
109
110    /// Whether this column can contain multiple values.
111    pub collection: Option<bool>,
112
113    /// Human-readable description of the column.
114    pub description: Option<String>,
115
116    /// Metadata key-value pairs for implementation directives.
117    #[serde(default)]
118    pub tag: Vec<Tag>,
119}
120
121/// A metadata tag for columns with key-value pairs.
122#[derive(Debug, Clone, Serialize, Deserialize)]
123#[serde(rename_all = "camelCase")]
124pub struct Tag {
125    /// Tag name/key.
126    pub name: String,
127
128    /// Tag value.
129    pub value: Option<String>,
130}
131
132/// A where clause for filtering rows.
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct WhereClause {
135    /// FHIRPath expression that must evaluate to true for the row to be included.
136    pub path: String,
137
138    /// Human-readable explanation of the constraint.
139    pub description: Option<String>,
140}
141
142/// A constant value that can be referenced in FHIRPath expressions.
143#[derive(Debug, Clone, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub struct Constant {
146    /// Name of the constant (referenced as %name in FHIRPath).
147    pub name: String,
148
149    /// String value of the constant.
150    pub value_string: Option<String>,
151
152    /// Integer value of the constant.
153    pub value_integer: Option<i64>,
154
155    /// Boolean value of the constant.
156    pub value_boolean: Option<bool>,
157
158    /// Decimal value of the constant.
159    pub value_decimal: Option<f64>,
160
161    /// Any other polymorphic `value[x]` field (`valueCode`, `valueUri`,
162    /// `valueDateTime`, `valuePositiveInt`, …). The SQL-on-FHIR spec allows the
163    /// full FHIR primitive `value[x]` set on a constant; the explicit fields
164    /// above cover the common cases, and this captures the remainder.
165    #[serde(flatten)]
166    pub values: serde_json::Map<String, Value>,
167}
168
169impl ViewDefinition {
170    /// Parse a ViewDefinition from a JSON Value.
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if the JSON is not a valid ViewDefinition.
175    pub fn from_json(value: &Value) -> Result<Self, Error> {
176        serde_json::from_value(value.clone())
177            .map_err(|e| Error::InvalidViewDefinition(e.to_string()))
178    }
179
180    /// Parse a ViewDefinition from a JSON string.
181    ///
182    /// # Errors
183    ///
184    /// Returns an error if the string is not valid JSON or not a valid ViewDefinition.
185    pub fn parse(s: &str) -> Result<Self, Error> {
186        serde_json::from_str(s).map_err(|e| Error::InvalidViewDefinition(e.to_string()))
187    }
188
189    /// Get the list of all column names defined in this view.
190    pub fn column_names(&self) -> Vec<String> {
191        let mut names = Vec::new();
192        collect_column_names(&self.select, &mut names);
193        names
194    }
195}
196
197/// Recursively collect column names from select clauses.
198fn collect_column_names(selects: &[SelectColumn], names: &mut Vec<String>) {
199    for select in selects {
200        if let Some(columns) = &select.column {
201            for col in columns {
202                names.push(col.name.clone());
203            }
204        }
205        collect_column_names(&select.select, names);
206
207        if let Some(union_selects) = &select.union_all {
208            collect_column_names(union_selects, names);
209        }
210    }
211}
212
213impl Constant {
214    /// Get the value of this constant as a serde_json::Value.
215    pub fn value(&self) -> Value {
216        if let Some(s) = &self.value_string {
217            Value::String(s.clone())
218        } else if let Some(i) = self.value_integer {
219            Value::Number(i.into())
220        } else if let Some(b) = self.value_boolean {
221            Value::Bool(b)
222        } else if let Some(d) = self.value_decimal {
223            serde_json::Number::from_f64(d)
224                .map(Value::Number)
225                .unwrap_or(Value::Null)
226        } else {
227            Value::Null
228        }
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235    use serde_json::json;
236
237    #[test]
238    fn test_parse_simple_view_definition() {
239        let json = json!({
240            "resourceType": "ViewDefinition",
241            "name": "patient_demographics",
242            "status": "active",
243            "resource": "Patient",
244            "select": [{
245                "column": [{
246                    "name": "id",
247                    "path": "id"
248                }, {
249                    "name": "gender",
250                    "path": "gender"
251                }]
252            }]
253        });
254
255        let view = ViewDefinition::from_json(&json).unwrap();
256        assert_eq!(view.name.as_deref(), Some("patient_demographics"));
257        assert_eq!(view.resource, "Patient");
258        assert_eq!(view.select.len(), 1);
259
260        let columns = view.select[0].column.as_ref().unwrap();
261        assert_eq!(columns.len(), 2);
262        assert_eq!(columns[0].name, "id");
263        assert_eq!(columns[1].name, "gender");
264    }
265
266    #[test]
267    fn test_parse_view_with_foreach() {
268        let json = json!({
269            "resourceType": "ViewDefinition",
270            "name": "patient_names",
271            "status": "active",
272            "resource": "Patient",
273            "select": [{
274                "forEach": "name",
275                "column": [{
276                    "name": "family",
277                    "path": "family"
278                }, {
279                    "name": "given",
280                    "path": "given.first()"
281                }]
282            }]
283        });
284
285        let view = ViewDefinition::from_json(&json).unwrap();
286        assert_eq!(view.select[0].for_each, Some("name".to_string()));
287    }
288
289    #[test]
290    fn test_parse_view_with_where() {
291        let json = json!({
292            "resourceType": "ViewDefinition",
293            "name": "active_patients",
294            "status": "active",
295            "resource": "Patient",
296            "select": [{
297                "column": [{
298                    "name": "id",
299                    "path": "id"
300                }]
301            }],
302            "where": [{
303                "path": "active = true"
304            }]
305        });
306
307        let view = ViewDefinition::from_json(&json).unwrap();
308        assert_eq!(view.where_.len(), 1);
309        assert_eq!(view.where_[0].path, "active = true");
310    }
311
312    #[test]
313    fn test_parse_view_with_constants() {
314        let json = json!({
315            "resourceType": "ViewDefinition",
316            "name": "test_view",
317            "status": "active",
318            "resource": "Patient",
319            "constant": [{
320                "name": "statusFilter",
321                "valueString": "active"
322            }, {
323                "name": "maxAge",
324                "valueInteger": 65
325            }],
326            "select": [{
327                "column": [{
328                    "name": "id",
329                    "path": "id"
330                }]
331            }]
332        });
333
334        let view = ViewDefinition::from_json(&json).unwrap();
335        assert_eq!(view.constant.len(), 2);
336        assert_eq!(view.constant[0].name, "statusFilter");
337        assert_eq!(view.constant[0].value_string, Some("active".to_string()));
338        assert_eq!(view.constant[1].value_integer, Some(65));
339    }
340
341    #[test]
342    fn test_column_names() {
343        let json = json!({
344            "resourceType": "ViewDefinition",
345            "name": "test_view",
346            "status": "active",
347            "resource": "Patient",
348            "select": [{
349                "column": [{
350                    "name": "id",
351                    "path": "id"
352                }, {
353                    "name": "gender",
354                    "path": "gender"
355                }]
356            }, {
357                "forEach": "name",
358                "column": [{
359                    "name": "family",
360                    "path": "family"
361                }]
362            }]
363        });
364
365        let view = ViewDefinition::from_json(&json).unwrap();
366        let names = view.column_names();
367        assert_eq!(names, vec!["id", "gender", "family"]);
368    }
369
370    #[test]
371    fn test_constant_values() {
372        let string_const = Constant {
373            name: "s".to_string(),
374            value_string: Some("test".to_string()),
375            value_integer: None,
376            value_boolean: None,
377            value_decimal: None,
378            values: Default::default(),
379        };
380        assert_eq!(string_const.value(), Value::String("test".to_string()));
381
382        let int_const = Constant {
383            name: "i".to_string(),
384            value_string: None,
385            value_integer: Some(42),
386            value_boolean: None,
387            value_decimal: None,
388            values: Default::default(),
389        };
390        assert_eq!(int_const.value(), json!(42));
391
392        let bool_const = Constant {
393            name: "b".to_string(),
394            value_string: None,
395            value_integer: None,
396            value_boolean: Some(true),
397            value_decimal: None,
398            values: Default::default(),
399        };
400        assert_eq!(bool_const.value(), Value::Bool(true));
401    }
402}