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