1use serde_json::Value;
7
8pub fn coerce_to_number(val: &Value) -> Option<f64> {
9 match val {
10 Value::Number(n) => n.as_f64(),
11 Value::String(s) => s.trim().parse::<f64>().ok(),
12 _ => None,
13 }
14}
15
16pub fn coerce_to_integer(val: &Value) -> Option<i64> {
17 match val {
18 Value::Number(n) => n.as_i64().or_else(|| {
19 let f = n.as_f64()?;
20 if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 { Some(f as i64) } else { None }
21 }),
22 Value::String(s) => coerce_str_to_integer(s),
23 _ => None,
24 }
25}
26
27pub fn coerce_str_to_integer(s: &str) -> Option<i64> {
30 let s = s.trim();
31 s.parse::<i64>().ok().or_else(|| {
32 let f = s.parse::<f64>().ok()?;
33 if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 { Some(f as i64) } else { None }
34 })
35}
36
37pub fn coerce_to_string(val: &Value) -> Option<String> {
38 match val {
39 Value::String(s) => Some(s.clone()),
40 Value::Bool(b) => Some(if *b { "true" } else { "false" }.into()),
41 Value::Number(n) => {
42 if let Some(i) = n.as_i64() {
43 Some(i.to_string())
44 } else {
45 Some(n.to_string())
46 }
47 }
48 _ => None,
49 }
50}
51
52pub fn coerce_string_or_integer_to_string(value: &Value) -> Option<String> {
55 match value {
56 Value::String(text) => Some(text.clone()),
57 Value::Number(number) => number
58 .as_i64()
59 .map(|integer| integer.to_string())
60 .or_else(|| number.as_u64().map(|integer| integer.to_string())),
61 _ => None,
62 }
63}
64
65pub fn coerce_to_bool(val: &Value) -> Option<bool> {
74 match val {
75 Value::Bool(b) => Some(*b),
76 Value::String(s) => match s.as_str() {
77 "true" | "True" | "TRUE" | "yes" | "Yes" | "YES" | "on" | "On" | "ON" | "y" | "Y" | "1" => Some(true),
78 "false" | "False" | "FALSE" | "no" | "No" | "NO" | "off" | "Off" | "OFF" | "n" | "N" | "0" => Some(false),
79 _ => None,
80 },
81 _ => None,
82 }
83}
84
85pub fn coerce_port_to_string(val: &Value) -> Option<String> {
96 match val {
97 Value::String(s) => Some(s.clone()),
98 Value::Number(n) => n.as_i64().map(|i| i.to_string()).or_else(|| n.as_u64().map(|u| u.to_string())),
99 _ => None,
100 }
101}
102
103pub fn scalar_eq(a: &Value, b: &Value) -> bool {
114 if a == b {
115 return true;
116 }
117 if (a.is_number() || b.is_number())
118 && let (Some(na), Some(nb)) = (coerce_to_number(a), coerce_to_number(b))
119 {
120 return na == nb;
121 }
122 match (coerce_to_string(a), coerce_to_string(b)) {
123 (Some(sa), Some(sb)) => sa == sb,
124 _ => false,
125 }
126}
127
128pub fn type_compatible(val: &Value, expected: &str) -> bool {
137 match expected {
138 "string" => val.is_string(),
141 "integer" => {
142 if val.is_boolean() {
143 return false;
144 }
145 coerce_to_integer(val).is_some()
146 }
147 "number" => {
148 if val.is_boolean() {
149 return false;
150 }
151 coerce_to_number(val).is_some()
152 }
153 "boolean" => coerce_to_bool(val).is_some(),
154 "object" => val.is_object(),
155 "array" => val.is_array(),
156 "null" => val.is_null(),
157 _ => false,
158 }
159}
160
161#[derive(Debug, Clone, PartialEq)]
163pub enum CoerceResult {
164 AlreadyCorrect,
166 Coerced(Value, String),
169 Failed,
171}
172
173pub fn coerce_value(val: &Value, expected: &str) -> CoerceResult {
179 let native_match = match expected {
180 "string" => val.is_string(),
181 "integer" => {
182 val.is_i64() || val.is_u64() || (val.is_f64() && val.as_f64().map(|f| f.fract() == 0.0).unwrap_or(false))
183 }
184 "number" | "double" | "float" => val.is_number(),
185 "boolean" => val.is_boolean(),
186 "object" => val.is_object(),
187 "array" => val.is_array(),
188 "null" => val.is_null(),
189 _ => return CoerceResult::Failed,
190 };
191 if native_match {
192 return CoerceResult::AlreadyCorrect;
193 }
194
195 match expected {
196 "string" => coerce_to_string(val)
197 .map(|s| {
198 let from = if val.is_boolean() { "boolean" } else { "number" };
199 CoerceResult::Coerced(Value::String(s), format!("{from} to string"))
200 })
201 .unwrap_or(CoerceResult::Failed),
202 "integer" => {
203 if val.is_boolean() {
204 return CoerceResult::Failed;
205 }
206 coerce_to_integer(val)
207 .map(|i| CoerceResult::Coerced(Value::Number(i.into()), "string to integer".into()))
208 .unwrap_or(CoerceResult::Failed)
209 }
210 "number" | "double" | "float" => {
211 if val.is_boolean() {
212 return CoerceResult::Failed;
213 }
214 coerce_to_number(val)
215 .and_then(|f| {
216 serde_json::Number::from_f64(f)
217 .map(|n| CoerceResult::Coerced(Value::Number(n), "string to number".into()))
218 })
219 .unwrap_or(CoerceResult::Failed)
220 }
221 "boolean" => coerce_to_bool(val)
222 .map(|b| CoerceResult::Coerced(Value::Bool(b), "string to boolean".into()))
223 .unwrap_or(CoerceResult::Failed),
224 _ => CoerceResult::Failed,
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use serde_json::json;
232
233 #[test]
234 fn coerce_to_number_from_string() {
235 assert_eq!(coerce_to_number(&json!("512")), Some(512.0));
236 assert_eq!(coerce_to_number(&json!("3.14")), Some(3.14));
237 assert_eq!(coerce_to_number(&json!(" 42 ")), Some(42.0));
238 assert_eq!(coerce_to_number(&json!("abc")), None);
239 assert_eq!(coerce_to_number(&json!("")), None);
240 }
241
242 #[test]
243 fn coerce_to_number_from_number() {
244 assert_eq!(coerce_to_number(&json!(512)), Some(512.0));
245 assert_eq!(coerce_to_number(&json!(3.14)), Some(3.14));
246 }
247
248 #[test]
249 fn coerce_to_number_rejects_non_scalars() {
250 assert_eq!(coerce_to_number(&json!(true)), None);
251 assert_eq!(coerce_to_number(&json!(null)), None);
252 assert_eq!(coerce_to_number(&json!([1])), None);
253 assert_eq!(coerce_to_number(&json!({"a": 1})), None);
254 }
255
256 #[test]
257 fn coerce_to_integer_from_string() {
258 assert_eq!(coerce_to_integer(&json!("512")), Some(512));
259 assert_eq!(coerce_to_integer(&json!("3.0")), Some(3));
260 assert_eq!(coerce_to_integer(&json!("3.5")), None);
261 assert_eq!(coerce_to_integer(&json!("abc")), None);
262 }
263
264 #[test]
265 fn coerce_to_string_from_bool() {
266 assert_eq!(coerce_to_string(&json!(true)), Some("true".into()));
267 assert_eq!(coerce_to_string(&json!(false)), Some("false".into()));
268 }
269
270 #[test]
271 fn coerce_to_string_from_number() {
272 assert_eq!(coerce_to_string(&json!(512)), Some("512".into()));
273 }
274
275 #[test]
276 fn coerce_to_string_rejects_complex() {
277 assert_eq!(coerce_to_string(&json!(null)), None);
278 assert_eq!(coerce_to_string(&json!([1])), None);
279 assert_eq!(coerce_to_string(&json!({"a": 1})), None);
280 }
281
282 #[test]
283 fn coerce_to_bool_from_string() {
284 assert_eq!(coerce_to_bool(&json!("true")), Some(true));
285 assert_eq!(coerce_to_bool(&json!("True")), Some(true));
286 assert_eq!(coerce_to_bool(&json!("TRUE")), Some(true));
287 assert_eq!(coerce_to_bool(&json!("false")), Some(false));
288 assert_eq!(coerce_to_bool(&json!("False")), Some(false));
289 assert_eq!(coerce_to_bool(&json!("FALSE")), Some(false));
290 }
291
292 #[test]
293 fn coerce_to_bool_yaml11_true_variants() {
294 assert_eq!(coerce_to_bool(&json!("yes")), Some(true));
295 assert_eq!(coerce_to_bool(&json!("Yes")), Some(true));
296 assert_eq!(coerce_to_bool(&json!("YES")), Some(true));
297 assert_eq!(coerce_to_bool(&json!("on")), Some(true));
298 assert_eq!(coerce_to_bool(&json!("On")), Some(true));
299 assert_eq!(coerce_to_bool(&json!("ON")), Some(true));
300 assert_eq!(coerce_to_bool(&json!("y")), Some(true));
301 assert_eq!(coerce_to_bool(&json!("Y")), Some(true));
302 assert_eq!(coerce_to_bool(&json!("1")), Some(true));
303 }
304
305 #[test]
306 fn coerce_to_bool_yaml11_false_variants() {
307 assert_eq!(coerce_to_bool(&json!("no")), Some(false));
308 assert_eq!(coerce_to_bool(&json!("No")), Some(false));
309 assert_eq!(coerce_to_bool(&json!("NO")), Some(false));
310 assert_eq!(coerce_to_bool(&json!("off")), Some(false));
311 assert_eq!(coerce_to_bool(&json!("Off")), Some(false));
312 assert_eq!(coerce_to_bool(&json!("OFF")), Some(false));
313 assert_eq!(coerce_to_bool(&json!("n")), Some(false));
314 assert_eq!(coerce_to_bool(&json!("N")), Some(false));
315 assert_eq!(coerce_to_bool(&json!("0")), Some(false));
316 }
317
318 #[test]
319 fn coerce_to_bool_rejects_nonspec_casings() {
320 assert_eq!(coerce_to_bool(&json!("tRuE")), None);
321 assert_eq!(coerce_to_bool(&json!("yEs")), None);
322 assert_eq!(coerce_to_bool(&json!("oN")), None);
323 assert_eq!(coerce_to_bool(&json!("fAlSe")), None);
324 assert_eq!(coerce_to_bool(&json!("nO")), None);
325 assert_eq!(coerce_to_bool(&json!("oFf")), None);
326 assert_eq!(coerce_to_bool(&json!("maybe")), None);
327 assert_eq!(coerce_to_bool(&json!("")), None);
328 assert_eq!(coerce_to_bool(&json!("2")), None);
329 }
330
331 #[test]
332 fn coerce_port_to_string_accepts_string_and_integer_only() {
333 assert_eq!(coerce_port_to_string(&json!("80")), Some("80".into()));
334 assert_eq!(coerce_port_to_string(&json!(80)), Some("80".into()));
335 assert_eq!(coerce_port_to_string(&json!(0)), Some("0".into()));
336 assert_eq!(coerce_port_to_string(&json!(80.0)), None);
338 assert_eq!(coerce_port_to_string(&json!(80.5)), None);
339 assert_eq!(coerce_port_to_string(&json!(true)), None);
340 assert_eq!(coerce_port_to_string(&json!(null)), None);
341 assert_eq!(coerce_port_to_string(&json!([80])), None);
342 }
343
344 #[test]
345 fn scalar_eq_native_and_coerced_scalars() {
346 assert!(scalar_eq(&json!(512), &json!("512")));
347 assert!(scalar_eq(&json!("512"), &json!(512)));
348 assert!(scalar_eq(&json!(true), &json!("true")));
349 assert!(scalar_eq(&json!("hello"), &json!("hello")));
350 assert!(!scalar_eq(&json!(512), &json!("513")));
351 assert!(!scalar_eq(&json!(true), &json!("false")));
352 }
353
354 #[test]
355 fn scalar_eq_integral_float_equals_integer() {
356 assert!(scalar_eq(&json!(30.0), &json!(30)), "30.0 must equal 30");
360 assert!(scalar_eq(&json!(30), &json!(30.0)), "30 must equal 30.0");
361 assert!(scalar_eq(&json!(30.0), &json!("30")), "30.0 must equal '30'");
362 assert!(scalar_eq(&json!("30.0"), &json!(30)), "'30.0' must equal 30");
363 assert!(!scalar_eq(&json!(30.5), &json!(30)), "30.5 must not equal 30");
364 assert!(!scalar_eq(&json!(30.0), &json!(31)), "30.0 must not equal 31");
365 }
366
367 #[test]
368 fn scalar_eq_non_numeric_string_keeps_exact_comparison() {
369 assert!(!scalar_eq(&json!("30x"), &json!(30)), "'30x' is not a number and must not equal 30");
370 assert!(!scalar_eq(&json!("tcp"), &json!("TCP")), "string comparison stays case-sensitive");
371 }
372
373 #[test]
374 fn scalar_eq_distinct_non_scalars_never_collapse() {
375 assert!(!scalar_eq(&json!([1]), &json!([2])));
378 assert!(!scalar_eq(&json!({"a": 1}), &json!({"b": 2})));
379 assert!(!scalar_eq(&json!([1]), &json!("[1]")));
380 assert!(!scalar_eq(&json!(null), &json!("null")));
381 assert!(scalar_eq(&json!([1, 2]), &json!([1, 2])));
383 assert!(scalar_eq(&json!(null), &json!(null)));
384 }
385
386 #[test]
387 fn type_compatible_string() {
388 assert!(type_compatible(&json!("hello"), "string"));
389 assert!(!type_compatible(&json!(512), "string"));
390 assert!(!type_compatible(&json!(true), "string"));
391 assert!(!type_compatible(&json!(null), "string"));
392 assert!(!type_compatible(&json!([1]), "string"));
393 assert!(!type_compatible(&json!({"a": 1}), "string"));
394 }
395
396 #[test]
397 fn type_compatible_integer() {
398 assert!(type_compatible(&json!(512), "integer"));
399 assert!(type_compatible(&json!("512"), "integer"));
400 assert!(!type_compatible(&json!("3.5"), "integer"));
401 assert!(!type_compatible(&json!("abc"), "integer"));
402 assert!(!type_compatible(&json!(true), "integer"));
403 }
404
405 #[test]
406 fn type_compatible_number() {
407 assert!(type_compatible(&json!(3.14), "number"));
408 assert!(type_compatible(&json!("3.14"), "number"));
409 assert!(type_compatible(&json!(512), "number"));
410 assert!(!type_compatible(&json!("abc"), "number"));
411 assert!(!type_compatible(&json!(true), "number"));
412 }
413
414 #[test]
415 fn type_compatible_boolean() {
416 assert!(type_compatible(&json!(true), "boolean"));
417 assert!(type_compatible(&json!("true"), "boolean"));
418 assert!(type_compatible(&json!("FALSE"), "boolean"));
419 assert!(type_compatible(&json!("yes"), "boolean"));
420 assert!(type_compatible(&json!("Yes"), "boolean"));
421 assert!(type_compatible(&json!("on"), "boolean"));
422 assert!(type_compatible(&json!("OFF"), "boolean"));
423 assert!(type_compatible(&json!("y"), "boolean"));
424 assert!(type_compatible(&json!("N"), "boolean"));
425 assert!(type_compatible(&json!("1"), "boolean"));
426 assert!(type_compatible(&json!("0"), "boolean"));
427 assert!(!type_compatible(&json!(1), "boolean"));
428 assert!(!type_compatible(&json!("maybe"), "boolean"));
429 }
430
431 #[test]
432 fn type_compatible_object_array_null() {
433 assert!(type_compatible(&json!({}), "object"));
434 assert!(!type_compatible(&json!("s"), "object"));
435 assert!(type_compatible(&json!([]), "array"));
436 assert!(!type_compatible(&json!(1), "array"));
437 assert!(type_compatible(&json!(null), "null"));
438 assert!(!type_compatible(&json!(""), "null"));
439 }
440
441 #[test]
442 fn type_compatible_unknown_type_returns_false() {
443 assert!(!type_compatible(&json!("x"), "foobar"));
444 }
445
446 #[test]
447 fn coerce_value_already_correct() {
448 assert_eq!(coerce_value(&json!("hello"), "string"), CoerceResult::AlreadyCorrect);
449 assert_eq!(coerce_value(&json!(42), "integer"), CoerceResult::AlreadyCorrect);
450 assert_eq!(coerce_value(&json!(3.14), "number"), CoerceResult::AlreadyCorrect);
451 assert_eq!(coerce_value(&json!(true), "boolean"), CoerceResult::AlreadyCorrect);
452 assert_eq!(coerce_value(&json!({}), "object"), CoerceResult::AlreadyCorrect);
453 assert_eq!(coerce_value(&json!([]), "array"), CoerceResult::AlreadyCorrect);
454 assert_eq!(coerce_value(&json!(null), "null"), CoerceResult::AlreadyCorrect);
455 }
456
457 #[test]
458 fn coerce_value_string_to_integer() {
459 match coerce_value(&json!("42"), "integer") {
460 CoerceResult::Coerced(v, desc) => {
461 assert_eq!(v, json!(42));
462 assert!(desc.contains("integer"));
463 }
464 other => panic!("expected Coerced, got {:?}", other),
465 }
466 }
467
468 #[test]
469 fn coerce_value_string_to_number() {
470 match coerce_value(&json!("3.14"), "number") {
471 CoerceResult::Coerced(v, _) => assert_eq!(v.as_f64().unwrap(), 3.14),
472 other => panic!("expected Coerced, got {:?}", other),
473 }
474 }
475
476 #[test]
477 fn coerce_value_string_to_boolean() {
478 match coerce_value(&json!("yes"), "boolean") {
479 CoerceResult::Coerced(v, _) => assert_eq!(v, json!(true)),
480 other => panic!("expected Coerced, got {:?}", other),
481 }
482 }
483
484 #[test]
485 fn coerce_value_bool_to_string() {
486 match coerce_value(&json!(true), "string") {
487 CoerceResult::Coerced(v, desc) => {
488 assert_eq!(v, json!("true"));
489 assert!(desc.contains("boolean"));
490 }
491 other => panic!("expected Coerced, got {:?}", other),
492 }
493 }
494
495 #[test]
496 fn coerce_value_number_to_string() {
497 match coerce_value(&json!(42), "string") {
498 CoerceResult::Coerced(v, desc) => {
499 assert_eq!(v, json!("42"));
500 assert!(desc.contains("number"));
501 }
502 other => panic!("expected Coerced, got {:?}", other),
503 }
504 }
505
506 #[test]
507 fn coerce_value_bool_to_integer_fails() {
508 assert_eq!(coerce_value(&json!(true), "integer"), CoerceResult::Failed);
509 }
510
511 #[test]
512 fn coerce_value_bool_to_number_fails() {
513 assert_eq!(coerce_value(&json!(true), "number"), CoerceResult::Failed);
514 }
515
516 #[test]
517 fn coerce_value_incompatible_fails() {
518 assert_eq!(coerce_value(&json!(null), "string"), CoerceResult::Failed);
519 assert_eq!(coerce_value(&json!([1]), "string"), CoerceResult::Failed);
520 assert_eq!(coerce_value(&json!("abc"), "integer"), CoerceResult::Failed);
521 assert_eq!(coerce_value(&json!("abc"), "number"), CoerceResult::Failed);
522 assert_eq!(coerce_value(&json!("maybe"), "boolean"), CoerceResult::Failed);
523 }
524
525 #[test]
526 fn coerce_value_double_and_float_aliases() {
527 assert_eq!(coerce_value(&json!(1.5), "double"), CoerceResult::AlreadyCorrect);
528 assert_eq!(coerce_value(&json!(1.5), "float"), CoerceResult::AlreadyCorrect);
529 match coerce_value(&json!("1.5"), "double") {
530 CoerceResult::Coerced(v, _) => assert_eq!(v.as_f64().unwrap(), 1.5),
531 other => panic!("expected Coerced, got {:?}", other),
532 }
533 }
534
535 #[test]
536 fn coerce_value_unknown_type_fails() {
537 assert_eq!(coerce_value(&json!("x"), "foobar"), CoerceResult::Failed);
538 }
539
540 #[test]
541 fn coerce_to_integer_from_float_number() {
542 assert_eq!(coerce_to_integer(&json!(3.0)), Some(3));
543 assert_eq!(coerce_to_integer(&json!(3.5)), None);
544 }
545}