1use rusqlite::types::Value as SqlValue;
5use serde_json::Value;
6
7use super::{LibraryParameter, SqlQueryError};
8
9#[derive(Debug, Clone)]
12pub struct BoundParam {
13 pub name: String,
14 pub value: SqlValue,
15}
16
17pub fn bind_supplied_params(
21 declared: &[LibraryParameter],
22 supplied: Option<&Value>,
23) -> Result<Vec<BoundParam>, SqlQueryError> {
24 let supplied_entries: Vec<&Value> = supplied
25 .and_then(|v| v.get("parameter"))
26 .and_then(|p| p.as_array())
27 .map(|arr| arr.iter().collect())
28 .unwrap_or_default();
29
30 let declared_names: std::collections::HashSet<&str> =
32 declared.iter().map(|d| d.name.as_str()).collect();
33 for entry in &supplied_entries {
34 if let Some(name) = entry.get("name").and_then(|n| n.as_str()) {
35 if !declared_names.contains(name) {
36 return Err(SqlQueryError::BindParameter(format!(
37 "supplied parameter '{name}' is not declared in Library.parameter"
38 )));
39 }
40 }
41 }
42
43 let mut out = Vec::with_capacity(declared.len());
44 for p in declared {
45 let supplied_entry = supplied_entries
46 .iter()
47 .find(|e| e.get("name").and_then(|n| n.as_str()) == Some(p.name.as_str()));
48 let value = if let Some(entry) = supplied_entry {
49 value_for_param(p, entry)?
50 } else if let Some(default) = &p.default_value {
51 let fake = serde_json::json!({ "name": p.name, "value": default });
54 let key = format!(
57 "value{}",
58 first_letter_upper(value_x_suffix_for(&p.type_code))
59 );
60 let mut obj = serde_json::Map::new();
61 obj.insert("name".to_string(), Value::String(p.name.clone()));
62 obj.insert(key, default.clone());
63 let _ = fake; value_for_param(p, &Value::Object(obj))?
65 } else {
66 return Err(SqlQueryError::BindParameter(format!(
67 "parameter '{}' has no supplied value and no default",
68 p.name
69 )));
70 };
71 out.push(BoundParam {
72 name: p.name.clone(),
73 value,
74 });
75 }
76 Ok(out)
77}
78
79fn value_for_param(p: &LibraryParameter, entry: &Value) -> Result<SqlValue, SqlQueryError> {
80 let obj = entry
81 .as_object()
82 .ok_or_else(|| SqlQueryError::BindParameter("parameter entry must be an object".into()))?;
83
84 let suffix = value_x_suffix_for(&p.type_code);
85 let expected_keys = expected_value_keys_for(&p.type_code);
86 let value = obj
87 .iter()
88 .find(|(k, _)| {
89 k.starts_with("value")
90 && (expected_keys.contains(&k.as_str()) || k == &&format!("value{suffix}"))
91 })
92 .map(|(_, v)| v);
93
94 let value = match value {
95 Some(v) => v,
96 None => {
97 return Err(SqlQueryError::BindParameter(format!(
98 "parameter '{}' (type {}) is missing a value{suffix} entry",
99 p.name, p.type_code
100 )));
101 }
102 };
103
104 bind_value(&p.name, &p.type_code, value)
105}
106
107fn first_letter_upper(s: &str) -> String {
108 let mut chars = s.chars();
109 match chars.next() {
110 Some(c) => c.to_uppercase().chain(chars).collect(),
111 None => String::new(),
112 }
113}
114
115fn value_x_suffix_for(type_code: &str) -> &'static str {
116 match type_code {
117 "boolean" => "Boolean",
118 "integer" | "positiveInt" | "unsignedInt" => "Integer",
119 "integer64" => "Integer64",
120 "decimal" => "Decimal",
121 "date" => "Date",
122 "dateTime" => "DateTime",
123 "instant" => "Instant",
124 "time" => "Time",
125 "string" => "String",
126 "code" => "Code",
127 "id" => "Id",
128 "uri" => "Uri",
129 "url" => "Url",
130 "canonical" => "Canonical",
131 "markdown" => "Markdown",
132 "oid" => "Oid",
133 "uuid" => "Uuid",
134 "base64Binary" => "Base64Binary",
135 _ => "String",
136 }
137}
138
139fn expected_value_keys_for(type_code: &str) -> &'static [&'static str] {
140 match type_code {
141 "boolean" => &["valueBoolean"],
142 "integer" | "positiveInt" | "unsignedInt" => {
143 &["valueInteger", "valuePositiveInt", "valueUnsignedInt"]
144 }
145 "integer64" => &["valueInteger64"],
146 "decimal" => &["valueDecimal"],
147 "date" => &["valueDate"],
148 "dateTime" => &["valueDateTime"],
149 "instant" => &["valueInstant"],
150 "time" => &["valueTime"],
151 "string" | "code" | "id" | "uri" | "url" | "canonical" | "markdown" | "oid" | "uuid" => &[
152 "valueString",
153 "valueCode",
154 "valueId",
155 "valueUri",
156 "valueUrl",
157 "valueCanonical",
158 "valueMarkdown",
159 "valueOid",
160 "valueUuid",
161 ],
162 "base64Binary" => &["valueBase64Binary"],
163 _ => &["valueString"],
164 }
165}
166
167fn bind_value(name: &str, type_code: &str, v: &Value) -> Result<SqlValue, SqlQueryError> {
168 let invalid = |reason: String| SqlQueryError::BindParameter(format!("'{name}': {reason}"));
169 match type_code {
170 "boolean" => v
171 .as_bool()
172 .map(|b| SqlValue::Integer(if b { 1 } else { 0 }))
173 .ok_or_else(|| invalid("expected JSON boolean".into())),
174 "integer" | "positiveInt" | "unsignedInt" => v
175 .as_i64()
176 .map(SqlValue::Integer)
177 .ok_or_else(|| invalid("expected JSON integer".into())),
178 "integer64" => {
179 if let Some(i) = v.as_i64() {
182 return Ok(SqlValue::Integer(i));
183 }
184 if let Some(s) = v.as_str() {
185 return s
186 .parse::<i64>()
187 .map(SqlValue::Integer)
188 .map_err(|e| invalid(format!("integer64 parse: {e}")));
189 }
190 Err(invalid("expected JSON integer or numeric string".into()))
191 }
192 "decimal" => v
193 .as_f64()
194 .map(SqlValue::Real)
195 .or_else(|| v.as_i64().map(|i| SqlValue::Real(i as f64)))
196 .ok_or_else(|| invalid("expected JSON number".into())),
197 "date" => {
198 let s = v
199 .as_str()
200 .ok_or_else(|| invalid("expected JSON string".into()))?;
201 chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")
202 .map_err(|e| invalid(format!("invalid date '{s}': {e}")))?;
203 Ok(SqlValue::Text(s.to_string()))
204 }
205 "dateTime" | "instant" => {
206 let s = v
207 .as_str()
208 .ok_or_else(|| invalid("expected JSON string".into()))?;
209 chrono::DateTime::parse_from_rfc3339(s)
210 .map_err(|e| invalid(format!("invalid {type_code} '{s}': {e}")))?;
211 Ok(SqlValue::Text(s.to_string()))
212 }
213 "time" => {
214 let s = v
215 .as_str()
216 .ok_or_else(|| invalid("expected JSON string".into()))?;
217 chrono::NaiveTime::parse_from_str(s, "%H:%M:%S")
218 .or_else(|_| chrono::NaiveTime::parse_from_str(s, "%H:%M:%S%.f"))
219 .map_err(|e| invalid(format!("invalid time '{s}': {e}")))?;
220 Ok(SqlValue::Text(s.to_string()))
221 }
222 _ => v
224 .as_str()
225 .map(|s| SqlValue::Text(s.to_string()))
226 .ok_or_else(|| invalid("expected JSON string".into())),
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use serde_json::json;
234
235 fn decl(name: &str, type_code: &str) -> LibraryParameter {
236 LibraryParameter {
237 name: name.into(),
238 type_code: type_code.into(),
239 has_default: false,
240 default_value: None,
241 }
242 }
243
244 #[test]
245 fn binds_integer_and_string() {
246 let declared = vec![decl("min", "integer"), decl("city", "string")];
247 let supplied = json!({
248 "resourceType": "Parameters",
249 "parameter": [
250 {"name": "min", "valueInteger": 18},
251 {"name": "city", "valueString": "NYC"}
252 ]
253 });
254 let out = bind_supplied_params(&declared, Some(&supplied)).unwrap();
255 assert_eq!(out.len(), 2);
256 assert!(matches!(out[0].value, SqlValue::Integer(18)));
257 assert!(matches!(&out[1].value, SqlValue::Text(s) if s == "NYC"));
258 }
259
260 #[test]
261 fn missing_required_param_errors() {
262 let declared = vec![decl("min", "integer")];
263 let supplied = json!({"resourceType": "Parameters", "parameter": []});
264 let err = bind_supplied_params(&declared, Some(&supplied)).unwrap_err();
265 assert!(matches!(err, SqlQueryError::BindParameter(_)));
266 }
267
268 #[test]
269 fn unknown_supplied_param_errors() {
270 let declared = vec![decl("min", "integer")];
271 let supplied = json!({
272 "resourceType": "Parameters",
273 "parameter": [
274 {"name": "min", "valueInteger": 1},
275 {"name": "unknown", "valueString": "x"}
276 ]
277 });
278 let err = bind_supplied_params(&declared, Some(&supplied)).unwrap_err();
279 assert!(matches!(err, SqlQueryError::BindParameter(_)));
280 }
281
282 #[test]
283 fn type_mismatch_errors() {
284 let declared = vec![decl("min", "integer")];
285 let supplied = json!({
286 "resourceType": "Parameters",
287 "parameter": [{"name": "min", "valueString": "oops"}]
288 });
289 let err = bind_supplied_params(&declared, Some(&supplied)).unwrap_err();
290 assert!(matches!(err, SqlQueryError::BindParameter(_)));
291 }
292
293 #[test]
294 fn datetime_validates() {
295 let declared = vec![decl("ts", "dateTime")];
296 let supplied = json!({
297 "resourceType": "Parameters",
298 "parameter": [{"name": "ts", "valueDateTime": "not-a-date"}]
299 });
300 assert!(bind_supplied_params(&declared, Some(&supplied)).is_err());
301
302 let ok = json!({
303 "resourceType": "Parameters",
304 "parameter": [{"name": "ts", "valueDateTime": "2025-01-02T03:04:05Z"}]
305 });
306 assert!(bind_supplied_params(&declared, Some(&ok)).is_ok());
307 }
308
309 #[test]
310 fn injection_payload_bound_as_text() {
311 let declared = vec![decl("name", "string")];
312 let supplied = json!({
313 "resourceType": "Parameters",
314 "parameter": [{"name": "name", "valueString": "Robert');--"}]
315 });
316 let out = bind_supplied_params(&declared, Some(&supplied)).unwrap();
317 match &out[0].value {
318 SqlValue::Text(s) => assert_eq!(s, "Robert');--"),
319 _ => panic!("expected Text"),
320 }
321 }
322}