Skip to main content

helios_sof/sqlquery/
params.rs

1//! Parse the FHIR `Parameters` body for a `$sql-run` whose subject is a
2//! SQLQuery or SQLView Library.
3//!
4//! The subject itself is named by the shared `subjectCanonical` /
5//! `subjectReference` / `subjectResource` trio and resolved before this runs
6//! (see `helios_sof::params` and the REST `sof::subject` module), so this
7//! struct covers only the parameters specific to executing a Library.
8
9use serde_json::Value;
10
11/// Library-specific `$sql-run` parameters lifted out of a FHIR `Parameters`
12/// body.
13#[derive(Debug, Default, Clone)]
14pub struct SqlQueryRunParams {
15    /// `_format` — `valueCode` (spec) or `valueString` (lenient). Optional;
16    /// defaults to `ndjson`.
17    pub format: Option<String>,
18    /// `header` — CSV header control (default `true`).
19    pub header: Option<bool>,
20    /// `parameters` — the nested `Parameters` resource of name-to-value bindings
21    /// carried in `parameter.resource`. Left as raw JSON; bound after the
22    /// Library's parameter declarations are known.
23    ///
24    /// Permitted only when the subject is a SQLQuery or SQLView. Supplying it
25    /// with a ViewDefinition subject is a `400`, because a ViewDefinition
26    /// declares no parameters.
27    pub parameters: Option<Value>,
28    /// `source` — external data source URL (out of scope v1).
29    pub source: Option<String>,
30    /// `_limit` — soft cap on the final result-set size, applied AFTER SQL
31    /// evaluation (including any in-query `LIMIT`). The server MAY return
32    /// fewer rows than requested without erroring; returning fewer rows than
33    /// the supplied `_limit` is not an error.
34    pub limit: Option<u32>,
35}
36
37/// Walks a `Parameters` body and pulls every Library-specific `$sql-run` field.
38pub fn extract_sqlquery_params_from_json(body: &Value) -> SqlQueryRunParams {
39    let mut out = SqlQueryRunParams::default();
40    if body.get("resourceType").and_then(|v| v.as_str()) != Some("Parameters") {
41        return out;
42    }
43    let Some(entries) = body.get("parameter").and_then(|p| p.as_array()) else {
44        return out;
45    };
46    for p in entries {
47        let Some(name) = p.get("name").and_then(|n| n.as_str()) else {
48            continue;
49        };
50        match name {
51            "_format" | "format" => {
52                if out.format.is_none() {
53                    out.format = read_str(p, &["valueCode", "valueString"]);
54                }
55            }
56            "header" => {
57                if out.header.is_none() {
58                    if let Some(b) = p.get("valueBoolean").and_then(|v| v.as_bool()) {
59                        out.header = Some(b);
60                    } else if let Some(s) = p.get("valueString").and_then(|v| v.as_str()) {
61                        out.header = Some(s == "true" || s == "1");
62                    }
63                }
64            }
65            "parameters" => {
66                if out.parameters.is_none() {
67                    if let Some(r) = p.get("resource") {
68                        out.parameters = Some(r.clone());
69                    }
70                }
71            }
72            "source" => {
73                if out.source.is_none() {
74                    out.source = read_str(p, &["valueString", "valueUri"]);
75                }
76            }
77            "_limit" => {
78                if out.limit.is_none() {
79                    if let Some(n) = p.get("valueInteger").and_then(|v| v.as_u64()) {
80                        out.limit = Some(n as u32);
81                    } else if let Some(n) = p
82                        .get("valuePositiveInt")
83                        .or_else(|| p.get("valueUnsignedInt"))
84                        .and_then(|v| v.as_u64())
85                    {
86                        out.limit = Some(n as u32);
87                    }
88                }
89            }
90            _ => {}
91        }
92    }
93    out
94}
95
96fn read_str(p: &Value, keys: &[&str]) -> Option<String> {
97    for k in keys {
98        if let Some(s) = p.get(*k).and_then(|v| v.as_str()) {
99            return Some(s.to_string());
100        }
101    }
102    None
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use serde_json::json;
109
110    #[test]
111    fn extracts_format_and_header() {
112        let body = json!({
113            "resourceType": "Parameters",
114            "parameter": [
115                {"name": "_format", "valueCode": "csv"},
116                {"name": "header", "valueBoolean": false}
117            ]
118        });
119        let p = extract_sqlquery_params_from_json(&body);
120        assert_eq!(p.format.as_deref(), Some("csv"));
121        assert_eq!(p.header, Some(false));
122    }
123
124    #[test]
125    fn extracts_parameter_bindings() {
126        let body = json!({
127            "resourceType": "Parameters",
128            "parameter": [
129                {"name": "_format", "valueCode": "json"},
130                {"name": "parameters", "resource": {
131                    "resourceType": "Parameters",
132                    "parameter": [{"name": "min_age", "valueInteger": 18}]
133                }}
134            ]
135        });
136        let p = extract_sqlquery_params_from_json(&body);
137        assert_eq!(p.format.as_deref(), Some("json"));
138        assert!(p.parameters.is_some());
139    }
140
141    #[test]
142    fn non_parameters_body_returns_default() {
143        let p = extract_sqlquery_params_from_json(&json!({"resourceType": "Bundle"}));
144        assert!(p.format.is_none());
145    }
146
147    #[test]
148    fn extracts_limit() {
149        let body = json!({
150            "resourceType": "Parameters",
151            "parameter": [
152                {"name": "_limit", "valueInteger": 50}
153            ]
154        });
155        let p = extract_sqlquery_params_from_json(&body);
156        assert_eq!(p.limit, Some(50));
157    }
158
159    #[test]
160    fn pre_ballot_query_parameters_are_ignored() {
161        // `queryReference` / `queryResource` belonged to `$sqlquery-run`, which
162        // was consolidated into `$sql-run`. The subject now arrives through the
163        // shared `subject*` trio, so these names carry no meaning here.
164        let body = json!({
165            "resourceType": "Parameters",
166            "parameter": [
167                {"name": "_format", "valueCode": "json"},
168                {"name": "queryReference", "valueReference": {"reference": "Library/foo"}},
169                {"name": "queryResource", "resource": {"resourceType": "Library"}}
170            ]
171        });
172        let p = extract_sqlquery_params_from_json(&body);
173        assert_eq!(p.format.as_deref(), Some("json"));
174        assert!(p.parameters.is_none());
175    }
176}