agent_first_data/document/
coerce.rs1use crate::document::{DocumentError, DocumentResult, Value};
4
5fn explicit_prefix(s: &str) -> DocumentResult<Option<Value>> {
14 if let Some(rest) = s.strip_prefix("s:") {
15 return Ok(Some(Value::String(rest.to_string())));
16 }
17 if let Some(rest) = s.strip_prefix("b:") {
18 let value = parse_bool(rest).ok_or_else(|| DocumentError::ParseError {
19 format: "boolean".to_string(),
20 detail: format!(
21 "invalid b: value `{rest}`; expected true/false, yes/no, on/off, or 1/0"
22 ),
23 })?;
24 return Ok(Some(Value::Bool(value)));
25 }
26 if let Some(rest) = s.strip_prefix("i:") {
27 return rest
28 .parse::<i64>()
29 .map(Value::Integer)
30 .map(Some)
31 .map_err(|_| DocumentError::ParseError {
32 format: "integer".to_string(),
33 detail: format!("invalid i: value `{rest}`"),
34 });
35 }
36 if let Some(rest) = s.strip_prefix("j:") {
37 let value = serde_json::from_str::<serde_json::Value>(rest).map_err(|error| {
38 DocumentError::ParseError {
39 format: "JSON".to_string(),
40 detail: error.to_string(),
41 }
42 })?;
43 return Ok(Some(Value::from(value)));
44 }
45 Ok(None)
46}
47
48fn json_or_string(s: &str) -> Value {
52 if (s.starts_with('[') || s.starts_with('{'))
53 && s.trim_end().ends_with([']', '}'])
54 && let Ok(v) = serde_json::from_str::<serde_json::Value>(s)
55 {
56 return Value::from(v);
57 }
58 Value::String(s.to_string())
59}
60
61pub fn coerce_scalar_typed(s: &str, existing: Option<&Value>) -> DocumentResult<Value> {
69 if let Some(v) = explicit_prefix(s)? {
70 return Ok(v);
71 }
72 if s.eq_ignore_ascii_case("null") {
73 return Ok(Value::Null);
74 }
75 match existing {
76 Some(Value::Bool(_)) => Ok(parse_bool(s)
77 .map(Value::Bool)
78 .unwrap_or_else(|| Value::String(s.to_string()))),
79 Some(Value::Integer(_)) => Ok(s
80 .parse::<i64>()
81 .map(Value::Integer)
82 .unwrap_or_else(|_| Value::String(s.to_string()))),
83 Some(Value::Float(_)) => Ok(s
84 .parse::<f64>()
85 .map(Value::Float)
86 .unwrap_or_else(|_| Value::String(s.to_string()))),
87 Some(Value::Unsigned(_)) => Ok(s
88 .parse::<u64>()
89 .map(Value::Unsigned)
90 .unwrap_or_else(|_| Value::String(s.to_string()))),
91 Some(Value::String(_)) => Ok(Value::String(s.to_string())),
92 Some(_) => Ok(json_or_string(s)),
93 None => Ok(coerce_scalar(s)),
94 }
95}
96
97pub fn coerce_values_typed(values: &[String], existing: Option<&Value>) -> DocumentResult<Value> {
104 if values.is_empty() {
105 return Err(DocumentError::EmptyValues);
106 }
107 if values.len() == 1 {
108 return coerce_scalar_typed(&values[0], existing);
109 }
110 let elem = existing.and_then(Value::as_array).and_then(|a| a.first());
111 Ok(Value::Array(
112 values
113 .iter()
114 .map(|v| coerce_scalar_typed(v, elem))
115 .collect::<DocumentResult<Vec<_>>>()?,
116 ))
117}
118
119pub fn coerce_scalar(s: &str) -> Value {
124 if let Ok(Some(v)) = explicit_prefix(s) {
125 return v;
126 }
127 match s.to_lowercase().as_str() {
128 "null" => return Value::Null,
129 "true" => return Value::Bool(true),
130 "false" => return Value::Bool(false),
131 _ => {}
132 }
133 if let Ok(i) = s.parse::<i64>() {
134 return Value::Integer(i);
135 }
136 if let Ok(u) = s.parse::<u64>() {
137 return Value::Unsigned(u);
138 }
139 if let Ok(f) = s.parse::<f64>() {
140 return Value::Float(f);
141 }
142 json_or_string(s)
143}
144
145fn parse_bool(value: &str) -> Option<bool> {
146 match value.to_ascii_lowercase().as_str() {
147 "true" | "yes" | "on" | "1" => Some(true),
148 "false" | "no" | "off" | "0" => Some(false),
149 _ => None,
150 }
151}
152
153pub fn coerce_values(values: &[String]) -> DocumentResult<Value> {
159 if values.is_empty() {
160 return Err(DocumentError::EmptyValues);
161 }
162
163 if values.len() == 1 {
164 return Ok(coerce_scalar(&values[0]));
165 }
166
167 Ok(Value::Array(
168 values.iter().map(|v| coerce_scalar(v)).collect(),
169 ))
170}
171
172#[cfg(test)]
173mod tests {
174 #![allow(clippy::unwrap_used, clippy::panic)]
175 use super::*;
176
177 #[test]
178 fn test_coerce_null() {
179 assert_eq!(coerce_scalar("null"), Value::Null);
180 assert_eq!(coerce_scalar("NULL"), Value::Null);
181 }
182
183 #[test]
184 fn test_coerce_bool() {
185 assert_eq!(coerce_scalar("true"), Value::Bool(true));
186 assert_eq!(coerce_scalar("false"), Value::Bool(false));
187 assert_eq!(coerce_scalar("TRUE"), Value::Bool(true));
188 }
189
190 #[test]
191 fn test_coerce_integer() {
192 assert_eq!(coerce_scalar("42"), Value::Integer(42));
193 assert_eq!(coerce_scalar("-100"), Value::Integer(-100));
194 }
195
196 #[test]
197 #[allow(clippy::approx_constant)]
198 fn test_coerce_float() {
199 match coerce_scalar("3.14") {
200 Value::Float(f) => assert!((f - 3.14).abs() < 0.001),
201 _ => panic!("expected float"),
202 }
203 }
204
205 #[test]
206 fn test_coerce_string() {
207 assert_eq!(coerce_scalar("hello"), Value::String("hello".to_string()));
208 }
209
210 #[test]
211 fn test_coerce_explicit_prefix_s() {
212 assert_eq!(coerce_scalar("s:true"), Value::String("true".to_string()));
213 assert_eq!(coerce_scalar("s:42"), Value::String("42".to_string()));
214 }
215
216 #[test]
217 fn test_coerce_explicit_prefix_b() {
218 assert_eq!(coerce_scalar("b:yes"), Value::Bool(true));
219 assert_eq!(coerce_scalar("b:no"), Value::Bool(false));
220 }
221
222 #[test]
223 fn test_coerce_explicit_prefix_i() {
224 assert_eq!(coerce_scalar("i:999"), Value::Integer(999));
225 }
226
227 #[test]
228 fn test_coerce_values_single() {
229 let result = coerce_values(&["42".to_string()]).unwrap();
230 assert_eq!(result, Value::Integer(42));
231 }
232
233 #[test]
234 fn test_coerce_values_multiple() {
235 let result =
236 coerce_values(&["dev".to_string(), "staging".to_string(), "prod".to_string()]).unwrap();
237 match result {
238 Value::Array(arr) => {
239 assert_eq!(arr.len(), 3);
240 assert_eq!(arr[0], Value::String("dev".to_string()));
241 }
242 _ => panic!("expected array"),
243 }
244 }
245
246 #[test]
247 fn test_coerce_values_empty_error() {
248 assert!(coerce_values(&[]).is_err());
249 }
250}