1use crate::{LemmaDoc, LemmaError, LemmaType};
2use serde_json::Value;
3use std::collections::HashMap;
4
5fn serialize_value(value: &Value, fact_type: &LemmaType) -> Result<String, LemmaError> {
7 match fact_type {
8 LemmaType::Text => match value {
9 Value::String(s) => Ok(format!("\"{}\"", s)),
10 _ => Err(LemmaError::Engine(format!(
11 "Expected string for Text, got {:?}",
12 value
13 ))),
14 },
15 LemmaType::Number => match value {
16 Value::Number(n) => Ok(n.to_string()),
17 Value::String(s) => s
18 .trim()
19 .parse::<f64>()
20 .map(|_| s.trim().to_string())
21 .map_err(|_| LemmaError::Engine(format!("Invalid number string: '{}'", s))),
22 _ => Err(LemmaError::Engine(format!(
23 "Expected number or string for Number, got {:?}",
24 value
25 ))),
26 },
27 LemmaType::Percentage => match value {
28 Value::Number(n) => {
29 let decimal = n.as_f64().ok_or_else(|| {
30 LemmaError::Engine(format!("Invalid number for percentage: {:?}", n))
31 })?;
32 Ok(format!("{}%", decimal * 100.0))
33 }
34 Value::String(s) => Ok(s.clone()),
35 _ => Err(LemmaError::Engine(format!(
36 "Expected number or string for Percentage, got {:?}",
37 value
38 ))),
39 },
40 LemmaType::Boolean => match value {
41 Value::Bool(b) => Ok(if *b { "true" } else { "false" }.to_string()),
42 Value::String(s) => Ok(s.clone()),
43 _ => Err(LemmaError::Engine(format!(
44 "Expected boolean or string for Boolean, got {:?}",
45 value
46 ))),
47 },
48 LemmaType::Date => match value {
49 Value::String(s) => Ok(s.clone()),
50 _ => Err(LemmaError::Engine(format!(
51 "Expected string for Date, got {:?}",
52 value
53 ))),
54 },
55 LemmaType::Regex => match value {
56 Value::String(s) => {
57 if s.starts_with('/') && s.ends_with('/') {
58 Ok(s.clone())
59 } else {
60 Ok(format!("/{}/", s))
61 }
62 }
63 _ => Err(LemmaError::Engine(format!(
64 "Expected string for Regex, got {:?}",
65 value
66 ))),
67 },
68 LemmaType::Mass
69 | LemmaType::Length
70 | LemmaType::Volume
71 | LemmaType::Duration
72 | LemmaType::Temperature
73 | LemmaType::Power
74 | LemmaType::Energy
75 | LemmaType::Force
76 | LemmaType::Pressure
77 | LemmaType::Frequency
78 | LemmaType::Data
79 | LemmaType::Money => match value {
80 Value::String(s) => Ok(s.clone()),
81 _ => Err(LemmaError::Engine(format!(
82 "Expected string with value and unit for {:?} (e.g., \"100 kilogram\"), got {:?}",
83 fact_type, value
84 ))),
85 },
86 }
87}
88
89pub fn to_lemma_syntax(
111 json: &[u8],
112 doc: &LemmaDoc,
113 all_docs: &HashMap<String, LemmaDoc>,
114) -> Result<Vec<String>, crate::LemmaError> {
115 let map: HashMap<String, Value> = serde_json::from_slice(json)
116 .map_err(|e| crate::LemmaError::Engine(format!("JSON parse error: {}", e)))?;
117
118 let mut lemma_strings = Vec::new();
119
120 for (name, value) in map {
121 let fact_type = super::find_fact_type(&name, doc, all_docs)?;
122 let lemma_value = serialize_value(&value, &fact_type)?;
123 lemma_strings.push(format!("{}={}", name, lemma_value));
124 }
125
126 Ok(lemma_strings)
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::{Engine, LemmaResult};
133
134 #[test]
135 fn test_percentage_as_number() -> LemmaResult<()> {
136 let mut engine = Engine::new();
137 engine.add_lemma_code(
138 r#"
139 doc test
140 fact discount = 10%
141 "#,
142 "test.lemma",
143 )?;
144
145 let doc = engine.get_document("test").unwrap();
146 let all_docs = engine.get_all_documents();
147
148 let json = r#"{"discount": 0.9}"#;
150 let result = to_lemma_syntax(json.as_bytes(), doc, all_docs)?;
151
152 assert_eq!(result.len(), 1);
153 assert_eq!(result[0], "discount=90%");
154 Ok(())
155 }
156
157 #[test]
158 fn test_percentage_as_string_with_percent() -> LemmaResult<()> {
159 let mut engine = Engine::new();
160 engine.add_lemma_code(
161 r#"
162 doc test
163 fact discount = 10%
164 "#,
165 "test.lemma",
166 )?;
167
168 let doc = engine.get_document("test").unwrap();
169 let all_docs = engine.get_all_documents();
170
171 let json = r#"{"discount": "90%"}"#;
173 let result = to_lemma_syntax(json.as_bytes(), doc, all_docs)?;
174
175 assert_eq!(result.len(), 1);
176 assert_eq!(result[0], "discount=90%");
177 Ok(())
178 }
179
180 #[test]
181 fn test_percentage_as_string_without_percent() -> LemmaResult<()> {
182 let mut engine = Engine::new();
183 engine.add_lemma_code(
184 r#"
185 doc test
186 fact discount = 10%
187 "#,
188 "test.lemma",
189 )?;
190
191 let doc = engine.get_document("test").unwrap();
192 let all_docs = engine.get_all_documents();
193
194 let json = r#"{"discount": "90"}"#;
196 let result = to_lemma_syntax(json.as_bytes(), doc, all_docs)?;
197
198 assert_eq!(result.len(), 1);
199 assert_eq!(result[0], "discount=90");
200 Ok(())
201 }
202
203 #[test]
204 fn test_text_with_quotes() -> LemmaResult<()> {
205 let mut engine = Engine::new();
206 engine.add_lemma_code(
207 r#"
208 doc test
209 fact name = "Alice"
210 "#,
211 "test.lemma",
212 )?;
213
214 let doc = engine.get_document("test").unwrap();
215 let all_docs = engine.get_all_documents();
216
217 let json = r#"{"name": "Bob"}"#;
218 let result = to_lemma_syntax(json.as_bytes(), doc, all_docs)?;
219
220 assert_eq!(result.len(), 1);
221 assert_eq!(result[0], r#"name="Bob""#);
222 Ok(())
223 }
224
225 #[test]
226 fn test_number_as_string() -> LemmaResult<()> {
227 let mut engine = Engine::new();
228 engine.add_lemma_code(
229 r#"
230 doc test
231 fact age = 30
232 "#,
233 "test.lemma",
234 )?;
235
236 let doc = engine.get_document("test").unwrap();
237 let all_docs = engine.get_all_documents();
238
239 let json = r#"{"age": "42"}"#;
240 let result = to_lemma_syntax(json.as_bytes(), doc, all_docs)?;
241
242 assert_eq!(result.len(), 1);
243 assert_eq!(result[0], "age=42");
244 Ok(())
245 }
246
247 #[test]
248 fn test_unit_as_string() -> LemmaResult<()> {
249 let mut engine = Engine::new();
250 engine.add_lemma_code(
251 r#"
252 doc test
253 fact price = 100 USD
254 fact weight = 50 kilogram
255 "#,
256 "test.lemma",
257 )?;
258
259 let doc = engine.get_document("test").unwrap();
260 let all_docs = engine.get_all_documents();
261
262 let json = r#"{"price": "200 USD", "weight": "75 kilogram"}"#;
263 let result = to_lemma_syntax(json.as_bytes(), doc, all_docs)?;
264
265 assert_eq!(result.len(), 2);
266 assert!(result.contains(&"price=200 USD".to_string()));
267 assert!(result.contains(&"weight=75 kilogram".to_string()));
268 Ok(())
269 }
270
271 #[test]
272 fn test_boolean_values() -> LemmaResult<()> {
273 let mut engine = Engine::new();
274 engine.add_lemma_code(
275 r#"
276 doc test
277 fact active = false
278 "#,
279 "test.lemma",
280 )?;
281
282 let doc = engine.get_document("test").unwrap();
283 let all_docs = engine.get_all_documents();
284
285 let json = r#"{"active": true}"#;
286 let result = to_lemma_syntax(json.as_bytes(), doc, all_docs)?;
287
288 assert_eq!(result.len(), 1);
289 assert_eq!(result[0], "active=true");
290 Ok(())
291 }
292
293 #[test]
294 fn test_boolean_as_string() -> LemmaResult<()> {
295 let mut engine = Engine::new();
296 engine.add_lemma_code(
297 r#"
298 doc test
299 fact status = yes
300 "#,
301 "test.lemma",
302 )?;
303
304 let doc = engine.get_document("test").unwrap();
305 let all_docs = engine.get_all_documents();
306
307 let json = r#"{"status": "no"}"#;
308 let result = to_lemma_syntax(json.as_bytes(), doc, all_docs)?;
309
310 assert_eq!(result.len(), 1);
311 assert_eq!(result[0], "status=no");
312 Ok(())
313 }
314
315 #[test]
316 fn test_date_as_string() -> LemmaResult<()> {
317 let mut engine = Engine::new();
318 engine.add_lemma_code(
319 r#"
320 doc test
321 fact start_date = 2024-01-01
322 "#,
323 "test.lemma",
324 )?;
325
326 let doc = engine.get_document("test").unwrap();
327 let all_docs = engine.get_all_documents();
328
329 let json = r#"{"start_date": "2024-12-25"}"#;
330 let result = to_lemma_syntax(json.as_bytes(), doc, all_docs)?;
331
332 assert_eq!(result.len(), 1);
333 assert_eq!(result[0], "start_date=2024-12-25");
334 Ok(())
335 }
336
337 #[test]
338 fn test_mixed_types() -> LemmaResult<()> {
339 let mut engine = Engine::new();
340 engine.add_lemma_code(
341 r#"
342 doc test
343 fact name = "Alice"
344 fact age = 30
345 fact discount = 10%
346 fact active = true
347 fact price = 100 USD
348 "#,
349 "test.lemma",
350 )?;
351
352 let doc = engine.get_document("test").unwrap();
353 let all_docs = engine.get_all_documents();
354
355 let json = r#"{
356 "name": "Bob",
357 "age": 25,
358 "discount": 0.15,
359 "active": false,
360 "price": "200 USD"
361 }"#;
362 let result = to_lemma_syntax(json.as_bytes(), doc, all_docs)?;
363
364 assert_eq!(result.len(), 5);
365 assert!(result.contains(&r#"name="Bob""#.to_string()));
366 assert!(result.contains(&"age=25".to_string()));
367 assert!(result.contains(&"discount=15%".to_string()));
368 assert!(result.contains(&"active=false".to_string()));
369 assert!(result.contains(&"price=200 USD".to_string()));
370 Ok(())
371 }
372
373 #[test]
374 fn test_type_mismatch_error() {
375 let mut engine = Engine::new();
376 engine
377 .add_lemma_code(
378 r#"
379 doc test
380 fact age = 30
381 "#,
382 "test.lemma",
383 )
384 .unwrap();
385
386 let doc = engine.get_document("test").unwrap();
387 let all_docs = engine.get_all_documents();
388
389 let json = r#"{"age": "not a number"}"#;
391 let result = to_lemma_syntax(json.as_bytes(), doc, all_docs);
392
393 assert!(result.is_err());
394 }
395
396 #[test]
397 fn test_unknown_fact_error() {
398 let mut engine = Engine::new();
399 engine
400 .add_lemma_code(
401 r#"
402 doc test
403 fact age = 30
404 "#,
405 "test.lemma",
406 )
407 .unwrap();
408
409 let doc = engine.get_document("test").unwrap();
410 let all_docs = engine.get_all_documents();
411
412 let json = r#"{"unknown_fact": 42}"#;
414 let result = to_lemma_syntax(json.as_bytes(), doc, all_docs);
415
416 assert!(result.is_err());
417 assert!(result.unwrap_err().to_string().contains("not found"));
418 }
419}