1pub mod primitives;
3pub mod writer;
4use indexmap::IndexMap;
5
6use crate::{
7 constants::MAX_DEPTH,
8 types::{
9 EncodeOptions,
10 IntoJsonValue,
11 JsonValue as Value,
12 ToonError,
13 ToonResult,
14 },
15 utils::{
16 normalize,
17 validation::validate_depth,
18 QuotingContext,
19 },
20};
21
22pub fn encode<V: IntoJsonValue>(value: V, options: &EncodeOptions) -> ToonResult<String> {
40 let json_value = value.into_json_value();
41 encode_impl(&json_value, options)
42}
43
44fn encode_impl(value: &Value, options: &EncodeOptions) -> ToonResult<String> {
45 let normalized: Value = normalize(value.clone());
46 let mut writer = writer::Writer::new(options.clone());
47
48 match &normalized {
49 Value::Array(arr) => {
50 write_array(&mut writer, None, arr, 0)?;
51 }
52 Value::Object(obj) => {
53 write_object(&mut writer, obj, 0)?;
54 }
55 _ => {
56 write_primitive_value(&mut writer, &normalized, QuotingContext::ObjectValue)?;
57 }
58 }
59
60 Ok(writer.finish())
61}
62
63pub fn encode_default<V: IntoJsonValue>(value: V) -> ToonResult<String> {
97 encode(value, &EncodeOptions::default())
98}
99
100pub fn encode_object<V: IntoJsonValue>(value: V, options: &EncodeOptions) -> ToonResult<String> {
120 let json_value = value.into_json_value();
121 if !json_value.is_object() {
122 return Err(ToonError::TypeMismatch {
123 expected: "object".to_string(),
124 found: value_type_name(&json_value).to_string(),
125 });
126 }
127 encode_impl(&json_value, options)
128}
129
130pub fn encode_array<V: IntoJsonValue>(value: V, options: &EncodeOptions) -> ToonResult<String> {
150 let json_value = value.into_json_value();
151 if !json_value.is_array() {
152 return Err(ToonError::TypeMismatch {
153 expected: "array".to_string(),
154 found: value_type_name(&json_value).to_string(),
155 });
156 }
157 encode_impl(&json_value, options)
158}
159
160fn value_type_name(value: &Value) -> &'static str {
161 match value {
162 Value::Null => "null",
163 Value::Bool(_) => "boolean",
164 Value::Number(_) => "number",
165 Value::String(_) => "string",
166 Value::Array(_) => "array",
167 Value::Object(_) => "object",
168 }
169}
170
171fn write_object(
172 writer: &mut writer::Writer,
173 obj: &IndexMap<String, Value>,
174 depth: usize,
175) -> ToonResult<()> {
176 validate_depth(depth, MAX_DEPTH)?;
177
178 let keys: Vec<&String> = obj.keys().collect();
179
180 for (i, key) in keys.iter().enumerate() {
181 if i > 0 {
182 writer.write_newline()?;
183 }
184
185 let value = &obj[*key];
186
187 match value {
188 Value::Array(arr) => {
189 if depth > 0 {
190 writer.write_indent(depth)?;
191 }
192 write_array(writer, Some(key), arr, 0)?;
194 }
195 Value::Object(nested_obj) => {
196 if depth > 0 {
197 writer.write_indent(depth)?;
198 }
199 writer.write_key(key)?;
200 writer.write_char(':')?;
201 if !nested_obj.is_empty() {
202 writer.write_newline()?;
203 write_object(writer, nested_obj, depth + 1)?;
204 }
205 }
206 _ => {
207 if depth > 0 {
208 writer.write_indent(depth)?;
209 }
210 writer.write_key(key)?;
211 writer.write_char(':')?;
212 writer.write_char(' ')?;
213 write_primitive_value(writer, value, QuotingContext::ObjectValue)?;
214 }
215 }
216 }
217
218 Ok(())
219}
220
221fn write_array(
222 writer: &mut writer::Writer,
223 key: Option<&str>,
224 arr: &[Value],
225 depth: usize,
226) -> ToonResult<()> {
227 validate_depth(depth, MAX_DEPTH)?;
228
229 if arr.is_empty() {
230 writer.write_empty_array_with_key(key)?;
231 return Ok(());
232 }
233
234 if let Some(keys) = is_tabular_array(arr) {
237 encode_tabular_array(writer, key, arr, &keys, depth)?;
238 } else if is_primitive_array(arr) {
239 encode_primitive_array(writer, key, arr, depth)?;
240 } else {
241 encode_nested_array(writer, key, arr, depth)?;
242 }
243
244 Ok(())
245}
246
247fn is_tabular_array(arr: &[Value]) -> Option<Vec<String>> {
250 if arr.is_empty() {
251 return None;
252 }
253
254 let first = arr.first()?;
255 if !first.is_object() {
256 return None;
257 }
258
259 let first_obj = first.as_object()?;
260 let keys: Vec<String> = first_obj.keys().cloned().collect();
261
262 for value in first_obj.values() {
264 if !is_primitive(value) {
265 return None;
266 }
267 }
268
269 for val in arr.iter().skip(1) {
271 if let Some(obj) = val.as_object() {
272 if obj.len() != keys.len() {
273 return None;
274 }
275 for key in &keys {
277 if !obj.contains_key(key) {
278 return None;
279 }
280 }
281 for value in obj.values() {
283 if !is_primitive(value) {
284 return None;
285 }
286 }
287 } else {
288 return None;
289 }
290 }
291
292 Some(keys)
293}
294
295fn is_primitive(value: &Value) -> bool {
297 matches!(
298 value,
299 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_)
300 )
301}
302
303fn is_primitive_array(arr: &[Value]) -> bool {
305 arr.iter().all(is_primitive)
306}
307
308fn encode_primitive_array(
309 writer: &mut writer::Writer,
310 key: Option<&str>,
311 arr: &[Value],
312 depth: usize,
313) -> ToonResult<()> {
314 writer.write_array_header(key, arr.len(), None, depth)?;
315 writer.write_char(' ')?;
316 writer.push_active_delimiter(writer.options.delimiter);
318
319 for (i, val) in arr.iter().enumerate() {
320 if i > 0 {
321 writer.write_delimiter()?;
322 }
323 write_primitive_value(writer, val, QuotingContext::ArrayValue)?;
324 }
325 writer.pop_active_delimiter();
326
327 Ok(())
328}
329
330fn write_primitive_value(
331 writer: &mut writer::Writer,
332 value: &Value,
333 context: QuotingContext,
334) -> ToonResult<()> {
335 match value {
336 Value::Null => writer.write_str("null"),
337 Value::Bool(b) => writer.write_str(&b.to_string()),
338 Value::Number(n) => {
339 let num_str = if let Some(f) = n.as_f64() {
341 if f.is_finite() && f.fract() == 0.0 && f.abs() <= i64::MAX as f64 {
342 format!("{}", f as i64)
343 } else {
344 format!("{f}")
345 }
346 } else {
347 n.to_string()
348 };
349 writer.write_str(&num_str)
350 }
351 Value::String(s) => {
352 if writer.needs_quoting(s, context) {
353 writer.write_quoted_string(s)
354 } else {
355 writer.write_str(s)
356 }
357 }
358 _ => Err(ToonError::InvalidInput(
359 "Expected primitive value".to_string(),
360 )),
361 }
362}
363
364fn encode_tabular_array(
365 writer: &mut writer::Writer,
366 key: Option<&str>,
367 arr: &[Value],
368 keys: &[String],
369 depth: usize,
370) -> ToonResult<()> {
371 writer.write_array_header(key, arr.len(), Some(keys), depth)?;
372 writer.write_newline()?;
373
374 writer.push_active_delimiter(writer.options.delimiter);
375
376 for (row_index, obj_val) in arr.iter().enumerate() {
378 if let Some(obj) = obj_val.as_object() {
379 writer.write_indent(depth + 1)?;
380
381 for (i, key) in keys.iter().enumerate() {
382 if i > 0 {
383 writer.write_delimiter()?;
384 }
385
386 if let Some(val) = obj.get(key) {
388 write_primitive_value(writer, val, QuotingContext::ArrayValue)?;
389 } else {
390 writer.write_str("null")?;
391 }
392 }
393
394 if row_index < arr.len() - 1 {
395 writer.write_newline()?;
396 }
397 }
398 }
399
400 Ok(())
401}
402
403fn encode_nested_array(
404 writer: &mut writer::Writer,
405 key: Option<&str>,
406 arr: &[Value],
407 depth: usize,
408) -> ToonResult<()> {
409 writer.write_array_header(key, arr.len(), None, depth)?;
410 writer.write_newline()?;
411 writer.push_active_delimiter(writer.options.delimiter);
412
413 for (i, val) in arr.iter().enumerate() {
414 writer.write_indent(depth + 1)?;
415 writer.write_char('-')?;
416 writer.write_char(' ')?;
417
418 match val {
419 Value::Array(inner_arr) => {
420 write_array(writer, None, inner_arr, depth + 1)?;
421 }
422 Value::Object(obj) => {
423 let keys: Vec<&String> = obj.keys().collect();
425 if let Some(first_key) = keys.first() {
426 let first_val = &obj[*first_key];
427
428 match first_val {
429 Value::Array(arr) => {
430 writer.write_key(first_key)?;
432 write_array(writer, None, arr, depth + 1)?;
433 }
434 Value::Object(nested_obj) => {
435 writer.write_key(first_key)?;
436 writer.write_char(':')?;
437 if !nested_obj.is_empty() {
438 writer.write_newline()?;
439 write_object(writer, nested_obj, depth + 2)?;
440 }
441 }
442 _ => {
443 writer.write_key(first_key)?;
444 writer.write_char(':')?;
445 writer.write_char(' ')?;
446 write_primitive_value(writer, first_val, QuotingContext::ObjectValue)?;
447 }
448 }
449
450 for key in keys.iter().skip(1) {
452 writer.write_newline()?;
453 writer.write_indent(depth + 2)?;
454
455 let value = &obj[*key];
456 match value {
457 Value::Array(arr) => {
458 writer.write_key(key)?;
459 write_array(writer, None, arr, depth + 2)?;
460 }
461 Value::Object(nested_obj) => {
462 writer.write_key(key)?;
463 writer.write_char(':')?;
464 if !nested_obj.is_empty() {
465 writer.write_newline()?;
466 write_object(writer, nested_obj, depth + 3)?;
467 }
468 }
469 _ => {
470 writer.write_key(key)?;
471 writer.write_char(':')?;
472 writer.write_char(' ')?;
473 write_primitive_value(writer, value, QuotingContext::ObjectValue)?;
474 }
475 }
476 }
477 }
478 }
479 _ => {
480 write_primitive_value(writer, val, QuotingContext::ArrayValue)?;
481 }
482 }
483
484 if i < arr.len() - 1 {
485 writer.write_newline()?;
486 }
487 }
488 writer.pop_active_delimiter();
489
490 Ok(())
491}
492
493#[cfg(test)]
494mod tests {
495 use core::f64;
496
497 use serde_json::json;
498
499 use super::*;
500
501 #[test]
502 fn test_encode_null() {
503 let value = json!(null);
504 assert_eq!(encode_default(&value).unwrap(), "null");
505 }
506
507 #[test]
508 fn test_encode_bool() {
509 assert_eq!(encode_default(json!(true)).unwrap(), "true");
510 assert_eq!(encode_default(json!(false)).unwrap(), "false");
511 }
512
513 #[test]
514 fn test_encode_number() {
515 assert_eq!(encode_default(json!(42)).unwrap(), "42");
516 assert_eq!(
517 encode_default(json!(f64::consts::PI)).unwrap(),
518 "3.141592653589793"
519 );
520 assert_eq!(encode_default(json!(-5)).unwrap(), "-5");
521 }
522
523 #[test]
524 fn test_encode_string() {
525 assert_eq!(encode_default(json!("hello")).unwrap(), "hello");
526 assert_eq!(encode_default(json!("hello world")).unwrap(), "hello world");
527 }
528
529 #[test]
530 fn test_encode_simple_object() {
531 let obj = json!({"name": "Alice", "age": 30});
532 let result = encode_default(&obj).unwrap();
533 assert!(result.contains("name: Alice"));
534 assert!(result.contains("age: 30"));
535 }
536
537 #[test]
538 fn test_encode_primitive_array() {
539 let obj = json!({"tags": ["reading", "gaming", "coding"]});
540 let result = encode_default(&obj).unwrap();
541 assert_eq!(result, "tags[3]: reading,gaming,coding");
542 }
543
544 #[test]
545 fn test_encode_tabular_array() {
546 let obj = json!({
547 "users": [
548 {"id": 1, "name": "Alice"},
549 {"id": 2, "name": "Bob"}
550 ]
551 });
552 let result = encode_default(&obj).unwrap();
553 assert!(result.contains("users[2]{id,name}:"));
554 assert!(result.contains("1,Alice"));
555 assert!(result.contains("2,Bob"));
556 }
557
558 #[test]
559 fn test_encode_empty_array() {
560 let obj = json!({"items": []});
561 let result = encode_default(&obj).unwrap();
562 assert_eq!(result, "items[0]:");
563 }
564
565 #[test]
566 fn test_encode_nested_object() {
567 let obj = json!({
568 "user": {
569 "name": "Alice",
570 "age": 30
571 }
572 });
573 let result = encode_default(&obj).unwrap();
574 assert!(result.contains("user:"));
575 assert!(result.contains("name: Alice"));
576 assert!(result.contains("age: 30"));
577 }
578}