Skip to main content

helios_sof/sqlquery/
params.rs

1//! Parse the FHIR `Parameters` body for `$sqlquery-run`.
2
3use serde_json::Value;
4
5/// Parameters lifted out of a FHIR `Parameters` body for `$sqlquery-run`.
6#[derive(Debug, Default, Clone)]
7pub struct SqlQueryRunParams {
8    /// `_format` — `valueCode` (spec) or `valueString` (lenient). Optional;
9    /// defaults to `ndjson` per SoF v2 PR #353.
10    pub format: Option<String>,
11    /// `header` — CSV header control (default `true`).
12    pub header: Option<bool>,
13    /// `queryReference` — extracted strictly from `valueReference.reference`
14    /// per the operation's `Reference` typing. May be a relative `Library/{id}`
15    /// or an absolute / canonical URL the server can resolve.
16    pub query_reference: Option<String>,
17    /// `queryResource` — inline `Library` resource carried in `parameter.resource`.
18    pub query_resource: Option<Value>,
19    /// `parameters` — the nested `Parameters` resource of name-to-value bindings
20    /// carried in `parameter.resource`. Left as raw JSON; bound after the
21    /// Library's parameter declarations are known.
22    pub parameters: Option<Value>,
23    /// `source` — external data source URL (out of scope v1).
24    pub source: Option<String>,
25    /// `_limit` — soft cap on the final result-set size, applied AFTER SQL
26    /// evaluation (including any in-query `LIMIT`). Per SoF v2 PR #353, the
27    /// server MAY return fewer rows than requested without erroring;
28    /// returning fewer rows than the supplied `_limit` is not an error.
29    pub limit: Option<u32>,
30}
31
32/// Walks a `Parameters` body and pulls every `$sqlquery-run` field.
33pub fn extract_sqlquery_params_from_json(body: &Value) -> SqlQueryRunParams {
34    let mut out = SqlQueryRunParams::default();
35    if body.get("resourceType").and_then(|v| v.as_str()) != Some("Parameters") {
36        return out;
37    }
38    let Some(entries) = body.get("parameter").and_then(|p| p.as_array()) else {
39        return out;
40    };
41    for p in entries {
42        let Some(name) = p.get("name").and_then(|n| n.as_str()) else {
43            continue;
44        };
45        match name {
46            "_format" | "format" => {
47                if out.format.is_none() {
48                    out.format = read_str(p, &["valueCode", "valueString"]);
49                }
50            }
51            "header" => {
52                if out.header.is_none() {
53                    if let Some(b) = p.get("valueBoolean").and_then(|v| v.as_bool()) {
54                        out.header = Some(b);
55                    } else if let Some(s) = p.get("valueString").and_then(|v| v.as_str()) {
56                        out.header = Some(s == "true" || s == "1");
57                    }
58                }
59            }
60            "queryReference" => {
61                if out.query_reference.is_none() {
62                    out.query_reference = read_reference(p);
63                }
64            }
65            "queryResource" => {
66                if out.query_resource.is_none() {
67                    if let Some(r) = p.get("resource") {
68                        out.query_resource = Some(r.clone());
69                    }
70                }
71            }
72            "parameters" => {
73                if out.parameters.is_none() {
74                    if let Some(r) = p.get("resource") {
75                        out.parameters = Some(r.clone());
76                    }
77                }
78            }
79            "source" => {
80                if out.source.is_none() {
81                    out.source = read_str(p, &["valueString", "valueUri"]);
82                }
83            }
84            "_limit" => {
85                if out.limit.is_none() {
86                    if let Some(n) = p.get("valueInteger").and_then(|v| v.as_u64()) {
87                        out.limit = Some(n as u32);
88                    } else if let Some(n) = p
89                        .get("valuePositiveInt")
90                        .or_else(|| p.get("valueUnsignedInt"))
91                        .and_then(|v| v.as_u64())
92                    {
93                        out.limit = Some(n as u32);
94                    }
95                }
96            }
97            _ => {}
98        }
99    }
100    out
101}
102
103fn read_str(p: &Value, keys: &[&str]) -> Option<String> {
104    for k in keys {
105        if let Some(s) = p.get(*k).and_then(|v| v.as_str()) {
106            return Some(s.to_string());
107        }
108    }
109    None
110}
111
112/// Spec: `queryReference` is typed as `Reference`, so only
113/// `valueReference.reference` is honored. Other shapes (`valueString`,
114/// `valueUri`, `valueCanonical`) are ignored.
115fn read_reference(p: &Value) -> Option<String> {
116    p.get("valueReference")
117        .and_then(|v| v.get("reference"))
118        .and_then(|v| v.as_str())
119        .map(str::to_string)
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use serde_json::json;
126
127    #[test]
128    fn extracts_format_and_header() {
129        let body = json!({
130            "resourceType": "Parameters",
131            "parameter": [
132                {"name": "_format", "valueCode": "csv"},
133                {"name": "header", "valueBoolean": false}
134            ]
135        });
136        let p = extract_sqlquery_params_from_json(&body);
137        assert_eq!(p.format.as_deref(), Some("csv"));
138        assert_eq!(p.header, Some(false));
139    }
140
141    #[test]
142    fn extracts_query_reference_and_resource() {
143        let body = json!({
144            "resourceType": "Parameters",
145            "parameter": [
146                {"name": "_format", "valueCode": "json"},
147                {"name": "queryReference", "valueReference": {"reference": "Library/foo"}},
148                {"name": "queryResource", "resource": {"resourceType": "Library"}}
149            ]
150        });
151        let p = extract_sqlquery_params_from_json(&body);
152        assert_eq!(p.query_reference.as_deref(), Some("Library/foo"));
153        assert!(p.query_resource.is_some());
154    }
155
156    #[test]
157    fn non_parameters_body_returns_default() {
158        let p = extract_sqlquery_params_from_json(&json!({"resourceType": "Bundle"}));
159        assert!(p.format.is_none());
160    }
161
162    #[test]
163    fn extracts_limit() {
164        let body = json!({
165            "resourceType": "Parameters",
166            "parameter": [
167                {"name": "_limit", "valueInteger": 50}
168            ]
169        });
170        let p = extract_sqlquery_params_from_json(&body);
171        assert_eq!(p.limit, Some(50));
172    }
173
174    #[test]
175    fn query_reference_only_reads_value_reference() {
176        // valueString / valueUri / valueCanonical are NOT accepted — the spec
177        // types queryReference strictly as Reference.
178        let body = json!({
179            "resourceType": "Parameters",
180            "parameter": [
181                {"name": "_format", "valueCode": "json"},
182                {"name": "queryReference", "valueString": "Library/foo"}
183            ]
184        });
185        let p = extract_sqlquery_params_from_json(&body);
186        assert!(p.query_reference.is_none());
187    }
188}