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 scalar_kind(value: &Value) -> Option<ScalarKind> {
158 match value {
159 Value::Null => Some(ScalarKind::Null),
160 Value::Bool(_) => Some(ScalarKind::Bool),
161 Value::Integer(_) | Value::Unsigned(_) | Value::Float(_) | Value::Number(_) => {
162 Some(ScalarKind::Number)
163 }
164 Value::String(_) => Some(ScalarKind::String),
165 Value::Array(_) | Value::Object(_) => None,
166 }
167}
168
169pub fn guard_bare_overwrite(existing: Option<&Value>) -> Result<(), ScalarKind> {
182 match existing.and_then(scalar_kind) {
183 Some(ScalarKind::String) | None => Ok(()),
184 Some(other) => Err(other),
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 #![allow(clippy::unwrap_used, clippy::panic)]
191 use super::*;
192
193 #[test]
194 fn bare_string_is_zero_coercion() {
195 for raw in ["007", "1.0", "true", "null", "3e10"] {
196 assert_eq!(
197 value_from_type(ValueType::String, Some(raw)).unwrap(),
198 Value::String(raw.to_string())
199 );
200 }
201 }
202
203 #[test]
204 fn value_type_number_is_literal_faithful() {
205 assert_eq!(
206 value_from_type(ValueType::Number, Some("18446744073709551615")).unwrap(),
207 Value::Unsigned(u64::MAX)
208 );
209 let huge = "123456789012345678901234567890";
210 assert_eq!(
211 value_from_type(ValueType::Number, Some(huge)).unwrap(),
212 Value::Number(huge.to_string())
213 );
214 let precise = "0.1000000000000000055511151231257827";
215 assert_eq!(
216 value_from_type(ValueType::Number, Some(precise)).unwrap(),
217 Value::Number(precise.to_string())
218 );
219 }
220
221 #[test]
222 fn value_type_number_rejects_leading_zero_and_non_numeric() {
223 assert!(value_from_type(ValueType::Number, Some("007")).is_err());
224 assert!(value_from_type(ValueType::Number, Some("abc")).is_err());
225 assert!(value_from_type(ValueType::Number, Some("+5")).is_err());
226 }
227
228 #[test]
229 fn value_type_bool_is_lenient() {
230 assert_eq!(
231 value_from_type(ValueType::Bool, Some("yes")).unwrap(),
232 Value::Bool(true)
233 );
234 assert!(value_from_type(ValueType::Bool, Some("nope")).is_err());
235 }
236
237 #[test]
238 fn value_type_null_takes_no_value() {
239 assert_eq!(value_from_type(ValueType::Null, None).unwrap(), Value::Null);
240 assert!(value_from_type(ValueType::String, None).is_err());
241 assert!(value_from_type(ValueType::Null, Some("x")).is_err());
242 }
243
244 #[test]
245 fn value_type_json_is_the_only_container_entry_point() {
246 let value = value_from_type(ValueType::Json, Some(r#"["a","b"]"#)).unwrap();
247 assert_eq!(
248 value,
249 Value::Array(vec![
250 Value::String("a".to_string()),
251 Value::String("b".to_string())
252 ])
253 );
254 assert_eq!(
257 value_from_type(ValueType::Json, Some("\"8080\"")).unwrap(),
258 Value::String("8080".to_string())
259 );
260 }
261
262 #[test]
263 fn guard_fires_only_for_bare_overwrite_of_a_differently_kinded_scalar() {
264 assert_eq!(guard_bare_overwrite(None), Ok(()));
265 assert_eq!(
266 guard_bare_overwrite(Some(&Value::String("x".to_string()))),
267 Ok(())
268 );
269 assert_eq!(guard_bare_overwrite(Some(&Value::Array(vec![]))), Ok(()));
270 assert_eq!(
271 guard_bare_overwrite(Some(&Value::Integer(8080))),
272 Err(ScalarKind::Number)
273 );
274 assert_eq!(
275 guard_bare_overwrite(Some(&Value::Bool(true))),
276 Err(ScalarKind::Bool)
277 );
278 assert_eq!(
279 guard_bare_overwrite(Some(&Value::Null)),
280 Err(ScalarKind::Null)
281 );
282 }
283}