agent_first_data/document/
coerce.rs1use crate::document::{DocumentError, DocumentResult, Value};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ValueType {
19 String,
20 Number,
21 Bool,
22 Null,
23 Json,
24}
25
26impl ValueType {
27 pub fn parse(name: &str) -> Option<Self> {
29 match name {
30 "string" => Some(Self::String),
31 "number" => Some(Self::Number),
32 "bool" => Some(Self::Bool),
33 "null" => Some(Self::Null),
34 "json" => Some(Self::Json),
35 _ => None,
36 }
37 }
38
39 pub fn name(self) -> &'static str {
42 match self {
43 Self::String => "string",
44 Self::Number => "number",
45 Self::Bool => "bool",
46 Self::Null => "null",
47 Self::Json => "json",
48 }
49 }
50}
51
52pub fn value_from_type(value_type: ValueType, raw: Option<&str>) -> DocumentResult<Value> {
64 match value_type {
65 ValueType::Null => match raw {
66 None => Ok(Value::Null),
67 Some(_) => Err(DocumentError::ParseError {
68 format: "value".to_string(),
69 detail: "--value-type null takes no VALUE".to_string(),
70 }),
71 },
72 ValueType::String => Ok(Value::String(require_value(raw, value_type)?.to_string())),
73 ValueType::Bool => {
74 let raw = require_value(raw, value_type)?;
75 parse_bool(raw).map(Value::Bool).ok_or_else(|| {
76 DocumentError::ParseError {
77 format: "boolean".to_string(),
78 detail: format!(
79 "invalid --value-type bool value `{raw}`; expected true/false, yes/no, on/off, or 1/0"
80 ),
81 }
82 })
83 }
84 ValueType::Number => parse_number_literal(require_value(raw, value_type)?),
85 ValueType::Json => {
86 let raw = require_value(raw, value_type)?;
87 serde_json::from_str::<serde_json::Value>(raw)
88 .map(Value::from)
89 .map_err(|error| DocumentError::ParseError {
90 format: "JSON".to_string(),
91 detail: error.to_string(),
92 })
93 }
94 }
95}
96
97fn require_value(raw: Option<&str>, value_type: ValueType) -> DocumentResult<&str> {
98 raw.ok_or_else(|| DocumentError::ParseError {
99 format: "value".to_string(),
100 detail: format!("--value-type {} requires a VALUE", value_type.name()),
101 })
102}
103
104fn parse_number_literal(text: &str) -> DocumentResult<Value> {
112 serde_json::from_str::<serde_json::Value>(text)
113 .ok()
114 .filter(serde_json::Value::is_number)
115 .map(Value::from)
116 .ok_or_else(|| DocumentError::ParseError {
117 format: "number".to_string(),
118 detail: format!("invalid --value-type number literal `{text}`"),
119 })
120}
121
122fn parse_bool(value: &str) -> Option<bool> {
123 match value.to_ascii_lowercase().as_str() {
124 "true" | "yes" | "on" | "1" => Some(true),
125 "false" | "no" | "off" | "0" => Some(false),
126 _ => None,
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum ScalarKind {
137 Null,
138 Bool,
139 Number,
140 String,
141}
142
143impl ScalarKind {
144 pub fn value_type_name(self) -> &'static str {
146 match self {
147 Self::Null => "null",
148 Self::Bool => "bool",
149 Self::Number => "number",
150 Self::String => "string",
151 }
152 }
153}
154
155pub fn value_matches_type(value: &Value, expected: ValueType) -> bool {
161 match expected {
162 ValueType::Json => true,
163 ValueType::String => matches!(value, Value::String(_)),
164 ValueType::Bool => matches!(value, Value::Bool(_)),
165 ValueType::Null => matches!(value, Value::Null),
166 ValueType::Number => matches!(
167 value,
168 Value::Integer(_) | Value::Unsigned(_) | Value::Float(_) | Value::Number(_)
169 ),
170 }
171}
172
173pub 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
187pub fn guard_bare_overwrite(existing: Option<&Value>) -> Result<(), ScalarKind> {
200 match existing.and_then(scalar_kind) {
201 Some(ScalarKind::String) | None => Ok(()),
202 Some(other) => Err(other),
203 }
204}
205
206pub fn coerce_toward(raw: &str, existing: Option<&Value>) -> DocumentResult<Value> {
219 match existing.and_then(scalar_kind) {
220 Some(ScalarKind::Bool) => Ok(value_from_type(ValueType::Bool, Some(raw))
221 .unwrap_or_else(|_| Value::String(raw.to_string()))),
222 Some(ScalarKind::Number) => Ok(value_from_type(ValueType::Number, Some(raw))
223 .unwrap_or_else(|_| Value::String(raw.to_string()))),
224 Some(ScalarKind::String | ScalarKind::Null) => Ok(Value::String(raw.to_string())),
225 None => coerce_structured_or_string(raw),
230 }
231}
232
233fn coerce_structured_or_string(raw: &str) -> DocumentResult<Value> {
237 let trimmed = raw.trim_start();
238 if trimmed.starts_with('[') || trimmed.starts_with('{') {
239 value_from_type(ValueType::Json, Some(raw))
240 } else {
241 Ok(Value::String(raw.to_string()))
242 }
243}
244
245pub fn coerce_values_toward(values: &[String], existing: Option<&Value>) -> DocumentResult<Value> {
249 match values {
250 [] => Err(DocumentError::EmptyValues),
251 [one] => coerce_toward(one, existing),
252 many => {
253 let element = existing
254 .and_then(Value::as_array)
255 .and_then(|array| array.first());
256 Ok(Value::Array(
257 many.iter()
258 .map(|value| coerce_toward(value, element))
259 .collect::<DocumentResult<Vec<_>>>()?,
260 ))
261 }
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 #![allow(clippy::unwrap_used, clippy::panic)]
268 use super::*;
269
270 #[test]
271 fn bare_string_is_zero_coercion() {
272 for raw in ["007", "1.0", "true", "null", "3e10"] {
273 assert_eq!(
274 value_from_type(ValueType::String, Some(raw)).unwrap(),
275 Value::String(raw.to_string())
276 );
277 }
278 }
279
280 #[test]
281 fn value_type_number_is_literal_faithful() {
282 assert_eq!(
283 value_from_type(ValueType::Number, Some("18446744073709551615")).unwrap(),
284 Value::Unsigned(u64::MAX)
285 );
286 let huge = "123456789012345678901234567890";
287 assert_eq!(
288 value_from_type(ValueType::Number, Some(huge)).unwrap(),
289 Value::Number(huge.to_string())
290 );
291 let precise = "0.1000000000000000055511151231257827";
292 assert_eq!(
293 value_from_type(ValueType::Number, Some(precise)).unwrap(),
294 Value::Number(precise.to_string())
295 );
296 }
297
298 #[test]
299 fn value_type_number_rejects_leading_zero_and_non_numeric() {
300 assert!(value_from_type(ValueType::Number, Some("007")).is_err());
301 assert!(value_from_type(ValueType::Number, Some("abc")).is_err());
302 assert!(value_from_type(ValueType::Number, Some("+5")).is_err());
303 }
304
305 #[test]
306 fn value_type_bool_is_lenient() {
307 assert_eq!(
308 value_from_type(ValueType::Bool, Some("yes")).unwrap(),
309 Value::Bool(true)
310 );
311 assert!(value_from_type(ValueType::Bool, Some("nope")).is_err());
312 }
313
314 #[test]
315 fn value_type_null_takes_no_value() {
316 assert_eq!(value_from_type(ValueType::Null, None).unwrap(), Value::Null);
317 assert!(value_from_type(ValueType::String, None).is_err());
318 assert!(value_from_type(ValueType::Null, Some("x")).is_err());
319 }
320
321 #[test]
322 fn value_type_json_is_the_only_container_entry_point() {
323 let value = value_from_type(ValueType::Json, Some(r#"["a","b"]"#)).unwrap();
324 assert_eq!(
325 value,
326 Value::Array(vec![
327 Value::String("a".to_string()),
328 Value::String("b".to_string())
329 ])
330 );
331 assert_eq!(
334 value_from_type(ValueType::Json, Some("\"8080\"")).unwrap(),
335 Value::String("8080".to_string())
336 );
337 }
338
339 #[test]
340 fn guard_fires_only_for_bare_overwrite_of_a_differently_kinded_scalar() {
341 assert_eq!(guard_bare_overwrite(None), Ok(()));
342 assert_eq!(
343 guard_bare_overwrite(Some(&Value::String("x".to_string()))),
344 Ok(())
345 );
346 assert_eq!(guard_bare_overwrite(Some(&Value::Array(vec![]))), Ok(()));
347 assert_eq!(
348 guard_bare_overwrite(Some(&Value::Integer(8080))),
349 Err(ScalarKind::Number)
350 );
351 assert_eq!(
352 guard_bare_overwrite(Some(&Value::Bool(true))),
353 Err(ScalarKind::Bool)
354 );
355 assert_eq!(
356 guard_bare_overwrite(Some(&Value::Null)),
357 Err(ScalarKind::Null)
358 );
359 }
360
361 #[test]
362 fn coerce_toward_is_type_directed_not_shape_guessing() {
363 assert_eq!(
367 coerce_toward("false", Some(&Value::Bool(true))).unwrap(),
368 Value::Bool(false)
369 );
370 assert_eq!(
371 coerce_toward("5432", Some(&Value::Integer(1))).unwrap(),
372 Value::from(serde_json::json!(5432))
373 );
374 assert_eq!(
375 coerce_toward("not-a-number", Some(&Value::Integer(1))).unwrap(),
376 Value::String("not-a-number".to_string())
377 );
378 assert_eq!(
379 coerce_toward("007", Some(&Value::String("x".to_string()))).unwrap(),
380 Value::String("007".to_string())
381 );
382 assert_eq!(
385 coerce_toward("007", None).unwrap(),
386 Value::String("007".to_string())
387 );
388 assert!(matches!(
389 coerce_toward("[]", None).unwrap(),
390 Value::Array(_)
391 ));
392 assert!(matches!(
393 coerce_toward("{\"a\":1}", Some(&Value::Object(Default::default()))).unwrap(),
394 Value::Object(_)
395 ));
396 }
397
398 #[test]
399 fn coerce_values_toward_scalar_vs_array() {
400 assert!(matches!(
401 coerce_values_toward(&[], None),
402 Err(DocumentError::EmptyValues)
403 ));
404 assert_eq!(
405 coerce_values_toward(&["x".to_string()], None).unwrap(),
406 Value::String("x".to_string())
407 );
408 let existing = Value::Array(vec![Value::Bool(true)]);
411 assert_eq!(
412 coerce_values_toward(&["false".to_string(), "true".to_string()], Some(&existing))
413 .unwrap(),
414 Value::Array(vec![Value::Bool(false), Value::Bool(true)])
415 );
416 }
417}