1use crate::document::{DocumentError, DocumentResult, Value};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ValueType {
18 String,
19 Number,
20 Bool,
21 Null,
22 Json,
23}
24
25impl ValueType {
26 pub fn parse(name: &str) -> Option<Self> {
28 match name {
29 "string" => Some(Self::String),
30 "number" => Some(Self::Number),
31 "bool" => Some(Self::Bool),
32 "null" => Some(Self::Null),
33 "json" => Some(Self::Json),
34 _ => None,
35 }
36 }
37
38 pub fn name(self) -> &'static str {
41 match self {
42 Self::String => "string",
43 Self::Number => "number",
44 Self::Bool => "bool",
45 Self::Null => "null",
46 Self::Json => "json",
47 }
48 }
49}
50
51pub fn value_from_type(value_type: ValueType, raw: Option<&str>) -> DocumentResult<Value> {
62 match value_type {
63 ValueType::Null => match raw {
64 None => Ok(Value::Null),
65 Some(_) => Err(DocumentError::ParseError {
66 format: "value".to_string(),
67 detail: "--value-type null takes no VALUE".to_string(),
68 }),
69 },
70 ValueType::String => Ok(Value::String(require_value(raw, value_type)?.to_string())),
71 ValueType::Bool => {
72 let raw = require_value(raw, value_type)?;
73 parse_bool(raw).map(Value::Bool).ok_or_else(|| {
74 DocumentError::ParseError {
75 format: "boolean".to_string(),
76 detail: format!(
77 "invalid --value-type bool value `{raw}`; expected true/false, yes/no, on/off, or 1/0"
78 ),
79 }
80 })
81 }
82 ValueType::Number => parse_number_literal(require_value(raw, value_type)?),
83 ValueType::Json => {
84 let raw = require_value(raw, value_type)?;
85 serde_json::from_str::<serde_json::Value>(raw)
86 .map(Value::from)
87 .map_err(|error| DocumentError::ParseError {
88 format: "JSON".to_string(),
89 detail: error.to_string(),
90 })
91 }
92 }
93}
94
95fn require_value(raw: Option<&str>, value_type: ValueType) -> DocumentResult<&str> {
96 raw.ok_or_else(|| DocumentError::ParseError {
97 format: "value".to_string(),
98 detail: format!("--value-type {} requires a VALUE", value_type.name()),
99 })
100}
101
102fn parse_number_literal(text: &str) -> DocumentResult<Value> {
110 serde_json::from_str::<serde_json::Value>(text)
111 .ok()
112 .filter(serde_json::Value::is_number)
113 .map(Value::from)
114 .ok_or_else(|| DocumentError::ParseError {
115 format: "number".to_string(),
116 detail: format!("invalid --value-type number literal `{text}`"),
117 })
118}
119
120fn parse_bool(value: &str) -> Option<bool> {
121 match value.to_ascii_lowercase().as_str() {
122 "true" | "yes" | "on" | "1" => Some(true),
123 "false" | "no" | "off" | "0" => Some(false),
124 _ => None,
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum ScalarKind {
135 Null,
136 Bool,
137 Number,
138 String,
139}
140
141impl ScalarKind {
142 pub fn value_type_name(self) -> &'static str {
144 match self {
145 Self::Null => "null",
146 Self::Bool => "bool",
147 Self::Number => "number",
148 Self::String => "string",
149 }
150 }
151}
152
153pub fn value_matches_type(value: &Value, expected: ValueType) -> bool {
159 match expected {
160 ValueType::Json => true,
161 ValueType::String => matches!(value, Value::String(_)),
162 ValueType::Bool => matches!(value, Value::Bool(_)),
163 ValueType::Null => matches!(value, Value::Null),
164 ValueType::Number => matches!(
165 value,
166 Value::Integer(_) | Value::Unsigned(_) | Value::Float(_) | Value::Number(_)
167 ),
168 }
169}
170
171pub fn scalar_kind(value: &Value) -> Option<ScalarKind> {
176 match value {
177 Value::Null => Some(ScalarKind::Null),
178 Value::Bool(_) => Some(ScalarKind::Bool),
179 Value::Integer(_) | Value::Unsigned(_) | Value::Float(_) | Value::Number(_) => {
180 Some(ScalarKind::Number)
181 }
182 Value::String(_) => Some(ScalarKind::String),
183 Value::Array(_) | Value::Object(_) => None,
184 }
185}
186
187#[derive(Clone, Copy, Debug, PartialEq, Eq)]
190pub enum BareOverwrite {
191 Scalar(ScalarKind),
193 Container(&'static str),
195}
196
197impl BareOverwrite {
198 pub fn found(self) -> &'static str {
200 match self {
201 Self::Scalar(kind) => kind.value_type_name(),
202 Self::Container(name) => name,
203 }
204 }
205
206 pub fn keeps(self) -> &'static str {
211 match self {
212 Self::Scalar(kind) => kind.value_type_name(),
213 Self::Container(_) => "json",
214 }
215 }
216}
217
218pub fn guard_bare_overwrite(existing: Option<&Value>) -> Result<(), BareOverwrite> {
237 match existing {
238 None | Some(Value::String(_)) => Ok(()),
239 Some(Value::Array(_)) => Err(BareOverwrite::Container("array")),
240 Some(Value::Object(_)) => Err(BareOverwrite::Container("object")),
241 Some(other) => match scalar_kind(other) {
242 Some(kind) => Err(BareOverwrite::Scalar(kind)),
243 None => Ok(()),
244 },
245 }
246}
247
248pub fn coerce_toward(raw: &str, existing: Option<&Value>) -> DocumentResult<Value> {
261 match existing.and_then(scalar_kind) {
262 Some(ScalarKind::Bool) => Ok(value_from_type(ValueType::Bool, Some(raw))
263 .unwrap_or_else(|_| Value::String(raw.to_string()))),
264 Some(ScalarKind::Number) => Ok(value_from_type(ValueType::Number, Some(raw))
265 .unwrap_or_else(|_| Value::String(raw.to_string()))),
266 Some(ScalarKind::String | ScalarKind::Null) => Ok(Value::String(raw.to_string())),
267 None => coerce_structured_or_string(raw),
272 }
273}
274
275fn coerce_structured_or_string(raw: &str) -> DocumentResult<Value> {
279 let trimmed = raw.trim_start();
280 if trimmed.starts_with('[') || trimmed.starts_with('{') {
281 value_from_type(ValueType::Json, Some(raw))
282 } else {
283 Ok(Value::String(raw.to_string()))
284 }
285}
286
287pub fn coerce_values_toward(values: &[String], existing: Option<&Value>) -> DocumentResult<Value> {
291 match values {
292 [] => Err(DocumentError::EmptyValues),
293 [one] => coerce_toward(one, existing),
294 many => {
295 let element = existing
296 .and_then(Value::as_array)
297 .and_then(|array| array.first());
298 Ok(Value::Array(
299 many.iter()
300 .map(|value| coerce_toward(value, element))
301 .collect::<DocumentResult<Vec<_>>>()?,
302 ))
303 }
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 #![allow(clippy::unwrap_used, clippy::panic)]
310 use super::*;
311
312 #[test]
313 fn bare_string_is_zero_coercion() {
314 for raw in ["007", "1.0", "true", "null", "3e10"] {
315 assert_eq!(
316 value_from_type(ValueType::String, Some(raw)).unwrap(),
317 Value::String(raw.to_string())
318 );
319 }
320 }
321
322 #[test]
323 fn value_type_number_is_literal_faithful() {
324 assert_eq!(
325 value_from_type(ValueType::Number, Some("18446744073709551615")).unwrap(),
326 Value::Unsigned(u64::MAX)
327 );
328 let huge = "123456789012345678901234567890";
329 assert_eq!(
330 value_from_type(ValueType::Number, Some(huge)).unwrap(),
331 Value::Number(huge.to_string())
332 );
333 let precise = "0.1000000000000000055511151231257827";
334 assert_eq!(
335 value_from_type(ValueType::Number, Some(precise)).unwrap(),
336 Value::Number(precise.to_string())
337 );
338 }
339
340 #[test]
341 fn value_type_number_rejects_leading_zero_and_non_numeric() {
342 assert!(value_from_type(ValueType::Number, Some("007")).is_err());
343 assert!(value_from_type(ValueType::Number, Some("abc")).is_err());
344 assert!(value_from_type(ValueType::Number, Some("+5")).is_err());
345 }
346
347 #[test]
348 fn value_type_bool_is_lenient() {
349 assert_eq!(
350 value_from_type(ValueType::Bool, Some("yes")).unwrap(),
351 Value::Bool(true)
352 );
353 assert!(value_from_type(ValueType::Bool, Some("nope")).is_err());
354 }
355
356 #[test]
357 fn value_type_null_takes_no_value() {
358 assert_eq!(value_from_type(ValueType::Null, None).unwrap(), Value::Null);
359 assert!(value_from_type(ValueType::String, None).is_err());
360 assert!(value_from_type(ValueType::Null, Some("x")).is_err());
361 }
362
363 #[test]
364 fn value_type_json_is_the_only_container_entry_point() {
365 let value = value_from_type(ValueType::Json, Some(r#"["a","b"]"#)).unwrap();
366 assert_eq!(
367 value,
368 Value::Array(vec![
369 Value::String("a".to_string()),
370 Value::String("b".to_string())
371 ])
372 );
373 assert_eq!(
376 value_from_type(ValueType::Json, Some("\"8080\"")).unwrap(),
377 Value::String("8080".to_string())
378 );
379 }
380
381 #[test]
382 fn guard_fires_for_any_bare_overwrite_that_is_not_string_to_string() {
383 assert_eq!(guard_bare_overwrite(None), Ok(()));
384 assert_eq!(
385 guard_bare_overwrite(Some(&Value::String("x".to_string()))),
386 Ok(())
387 );
388 assert_eq!(
391 guard_bare_overwrite(Some(&Value::Array(vec![]))),
392 Err(BareOverwrite::Container("array"))
393 );
394 assert_eq!(
395 guard_bare_overwrite(Some(&Value::Integer(8080))),
396 Err(BareOverwrite::Scalar(ScalarKind::Number))
397 );
398 assert_eq!(
399 guard_bare_overwrite(Some(&Value::Bool(true))),
400 Err(BareOverwrite::Scalar(ScalarKind::Bool))
401 );
402 assert_eq!(
403 guard_bare_overwrite(Some(&Value::Null)),
404 Err(BareOverwrite::Scalar(ScalarKind::Null))
405 );
406 }
407
408 #[test]
409 fn coerce_toward_is_type_directed_not_shape_guessing() {
410 assert_eq!(
414 coerce_toward("false", Some(&Value::Bool(true))).unwrap(),
415 Value::Bool(false)
416 );
417 assert_eq!(
418 coerce_toward("5432", Some(&Value::Integer(1))).unwrap(),
419 Value::from(serde_json::json!(5432))
420 );
421 assert_eq!(
422 coerce_toward("not-a-number", Some(&Value::Integer(1))).unwrap(),
423 Value::String("not-a-number".to_string())
424 );
425 assert_eq!(
426 coerce_toward("007", Some(&Value::String("x".to_string()))).unwrap(),
427 Value::String("007".to_string())
428 );
429 assert_eq!(
432 coerce_toward("007", None).unwrap(),
433 Value::String("007".to_string())
434 );
435 assert!(matches!(
436 coerce_toward("[]", None).unwrap(),
437 Value::Array(_)
438 ));
439 assert!(matches!(
440 coerce_toward("{\"a\":1}", Some(&Value::Object(Default::default()))).unwrap(),
441 Value::Object(_)
442 ));
443 }
444
445 #[test]
446 fn coerce_values_toward_scalar_vs_array() {
447 assert!(matches!(
448 coerce_values_toward(&[], None),
449 Err(DocumentError::EmptyValues)
450 ));
451 assert_eq!(
452 coerce_values_toward(&["x".to_string()], None).unwrap(),
453 Value::String("x".to_string())
454 );
455 let existing = Value::Array(vec![Value::Bool(true)]);
458 assert_eq!(
459 coerce_values_toward(&["false".to_string(), "true".to_string()], Some(&existing))
460 .unwrap(),
461 Value::Array(vec![Value::Bool(false), Value::Bool(true)])
462 );
463 }
464}
465
466#[cfg(test)]
467mod bare_overwrite_tests {
468 use super::*;
469 use std::collections::BTreeMap;
470
471 #[test]
472 fn containers_are_guarded_and_ask_for_json() {
473 let array = Value::Array(vec![Value::String("a".to_string())]);
474 let object = Value::Object(BTreeMap::new());
475 for (value, name) in [(&array, "array"), (&object, "object")] {
476 let overwrite = guard_bare_overwrite(Some(value)).unwrap_err();
477 assert_eq!(overwrite.found(), name);
478 assert_eq!(overwrite.keeps(), "json");
480 }
481 }
482
483 #[test]
484 fn scalars_keep_naming_their_own_type() {
485 let overwrite = guard_bare_overwrite(Some(&Value::Integer(8080))).unwrap_err();
486 assert_eq!(overwrite, BareOverwrite::Scalar(ScalarKind::Number));
487 assert_eq!(overwrite.keeps(), "number");
488 }
489
490 #[test]
491 fn a_new_key_or_an_existing_string_is_not_a_rewrite() {
492 assert!(guard_bare_overwrite(None).is_ok());
493 assert!(guard_bare_overwrite(Some(&Value::String("old".to_string()))).is_ok());
494 }
495}