1use std::collections::BTreeMap;
15use std::fs;
16
17use incise_core::describe::describe_change;
18use incise_core::front::{find_frontmatter, format_path, parse_path, Fmt, Seg};
19use incise_core::heading::{find_sections, inert_headings};
20use incise_core::list::find_lists;
21use incise_core::ops::dispatch::apply_op;
22use incise_core::ops::frontmatter::{
23 describe_frontmatter_change, frontmatter_get, render_frontmatter, render_frontmatter_get,
24};
25use incise_core::ops::list::{
26 list_lists, render_list_summary, resolve_item, resolve_list, ListAddress,
27};
28use incise_core::ops::section::{
29 render_section_outline, resolve_section, section_outline, SectionAddress,
30};
31use incise_core::ops::table::{
32 list_tables, render_table_get, render_table_list, resolve_row, resolve_table, table_add_row,
33 table_delete_row, table_update_cell, TableAddress, Values,
34};
35use incise_core::table::find_tables;
36use incise_core::{args as va, json};
37
38const FS: char = '\u{1f}'; const RS: char = '\u{1e}'; const GS: char = '\u{1d}'; fn esc(s: &str) -> String {
43 s.replace('\\', "\\\\")
44 .replace('\n', "\\n")
45 .replace('\r', "\\r")
46}
47
48fn opt(v: Option<String>) -> String {
51 v.unwrap_or_else(|| "-".to_string())
52}
53
54fn main() {
55 let args: Vec<String> = std::env::args().collect();
56 if args.len() != 4 {
57 eprintln!("usage: oracle_cases <cases> <corpus-root> <out>");
58 std::process::exit(2);
59 }
60 let cases = fs::read_to_string(&args[1]).expect("read cases");
61 let root = std::path::Path::new(&args[2]);
62 let mut out = String::new();
63 let mut docs: BTreeMap<String, String> = BTreeMap::new();
64
65 for line in cases.lines() {
66 if line.is_empty() {
67 continue;
68 }
69 let parts: Vec<&str> = line.split(FS).collect();
70 let (id, file, op, argblob) = (
71 parts[0],
72 parts[1],
73 parts[2],
74 parts.get(3).copied().unwrap_or(""),
75 );
76 let content = docs
77 .entry(file.to_string())
78 .or_insert_with(|| fs::read_to_string(root.join(file)).expect("read fixture"))
79 .clone();
80 let a = parse_args(argblob);
81 let (status, payload) = run(&content, op, &a);
82 out.push_str(&format!("{id}{FS}{status}{FS}{}\n", esc(&payload)));
83 }
84 fs::write(&args[3], out).expect("write results");
85}
86
87type Args = BTreeMap<String, String>;
88
89fn parse_args(blob: &str) -> Args {
90 let mut m = BTreeMap::new();
91 for pair in blob.split(RS) {
92 if pair.is_empty() {
93 continue;
94 }
95 let mut it = pair.splitn(2, GS);
96 let k = it.next().unwrap_or("").to_string();
97 let v = it.next().unwrap_or("").to_string();
98 m.insert(k, v);
99 }
100 m
101}
102
103fn where_pairs(a: &Args) -> Vec<(String, json::Value)> {
107 indexed(a, "wk", "wv")
108 .into_iter()
109 .map(|(k, v)| (k, json::Value::Str(v)))
110 .collect()
111}
112
113fn indexed(a: &Args, kp: &str, vp: &str) -> Vec<(String, String)> {
122 let mut out: Vec<(String, String)> = Vec::new();
123 for i in 0.. {
124 match (a.get(&format!("{kp}{i}")), a.get(&format!("{vp}{i}"))) {
125 (Some(k), Some(v)) => match out.iter_mut().find(|(ek, _)| ek == k) {
126 Some(slot) => slot.1 = v.clone(),
127 None => out.push((k.clone(), v.clone())),
128 },
129 _ => break,
130 }
131 }
132 out
133}
134
135fn filter_arg(a: &Args) -> Option<json::Value> {
142 let pairs = indexed(a, "fk", "fv");
143 if pairs.is_empty() && a.get("shape").map(String::as_str) != Some("filter") {
144 return None;
145 }
146 Some(json::Value::Object(
147 pairs
148 .into_iter()
149 .map(|(k, v)| (k, json::Value::Str(v)))
150 .collect(),
151 ))
152}
153
154fn address(a: &Args) -> TableAddress {
155 TableAddress {
156 heading: a.get("heading").map(|h| json::Value::Str(h.clone())),
157 ordinal: a
160 .get("ordinal")
161 .and_then(|s| s.parse::<i64>().ok())
162 .map(json::Value::Int),
163 }
164}
165
166fn typed_args(a: &Args) -> Option<json::Value> {
178 a.get("args").and_then(|t| json::parse(t))
179}
180
181fn typed_address(v: Option<&json::Value>) -> TableAddress {
187 match v {
188 Some(json::Value::Object(_)) => TableAddress {
189 heading: v.unwrap().get("heading").cloned(),
190 ordinal: v.unwrap().get("ordinal").cloned(),
191 },
192 Some(x) => TableAddress {
193 heading: Some(x.clone()),
194 ordinal: None,
195 },
196 None => TableAddress {
197 heading: None,
198 ordinal: None,
199 },
200 }
201}
202
203fn typed_filter(v: &json::Value) -> Option<json::Value> {
213 match v.get("filter") {
214 None | Some(json::Value::Null) => None,
215 Some(x) => Some(x.clone()),
216 }
217}
218
219fn list_address(a: &Args) -> ListAddress {
222 ListAddress {
223 heading: a.get("heading").map(|h| json::Value::Str(h.clone())),
224 ordinal: a
225 .get("ordinal")
226 .and_then(|s| s.parse::<i64>().ok())
227 .map(json::Value::Int),
228 }
229}
230
231fn section_address(a: &Args) -> SectionAddress {
236 SectionAddress {
237 path: a.get("spath").map(|p| json::Value::Str(p.clone())),
238 heading: a.get("sheading").map(|h| json::Value::Str(h.clone())),
239 ordinal: a
240 .get("sordinal")
241 .and_then(|s| s.parse::<i64>().ok())
242 .map(json::Value::Int),
243 }
244}
245
246fn values(a: &Args) -> Values {
247 let named = indexed(a, "vk", "vv");
248 if !named.is_empty() || a.get("shape").map(String::as_str) == Some("named") {
249 return Values::named(named);
250 }
251 let mut ordered = Vec::new();
252 for i in 0.. {
253 match a.get(&format!("vo{i}")) {
254 Some(v) => ordered.push(v.clone()),
255 None => break,
256 }
257 }
258 Values::ordered(ordered)
259}
260
261fn position(a: &Args) -> Option<json::Value> {
270 let raw = a.get("position").map(String::as_str).unwrap_or("end");
271 Some(match raw {
272 "start" | "end" => json::Value::Str(raw.to_string()),
273 n => match n.parse::<i64>() {
274 Ok(i) => json::Value::Int(i),
275 Err(_) => json::Value::Str(n.to_string()),
276 },
277 })
278}
279
280fn arg_case(fname: &str, text: &str) -> Result<String, incise_core::error::OpError> {
287 let absent = text == "ABSENT";
288 let parsed = if absent { None } else { json::parse(text) };
289 if !absent && parsed.is_none() {
292 return Ok(format!("PARSE-FAILED {text}"));
293 }
294 let v = parsed.as_ref();
295 let opt = |name: &str| -> json::Value {
296 match v {
297 None => json::Value::Object(vec![]),
298 Some(x) => json::Value::Object(vec![(name.to_string(), x.clone())]),
299 }
300 };
301 let repr_opt = |o: Option<json::Value>| match o {
302 None => "None".to_string(),
303 Some(x) => json::py_repr(&x),
304 };
305
306 Ok(match fname {
307 "unstring_obj" => repr_opt(va::unstring(v, va::Expect::Object, "table", false)?),
308 "unstring_obj_plain" => repr_opt(va::unstring(v, va::Expect::Object, "table", true)?),
309 "unstring_objarr" => repr_opt(va::unstring(v, va::Expect::ObjectOrArray, "values", false)?),
310 "clean_keys" => repr_opt(va::clean_keys(v.cloned())),
311 "check_address" => repr_opt(va::check_address(v.cloned(), "table")?),
312 "check_heading" => match va::check_heading(v, "table")? {
313 None => "None".to_string(),
314 Some(s) => json::py_repr_str(&s),
315 },
316 "check_ordinal" => match va::check_ordinal(v, "table")? {
317 None => "None".to_string(),
318 Some(n) => n.repr(),
319 },
320 "check_position" => match va::check_position(v)? {
321 va::Position::Start => "'start'".to_string(),
322 va::Position::End => "'end'".to_string(),
323 va::Position::Index(n) => n.repr(),
324 },
325 "check_cell" => json::py_repr_str(&va::check_cell(
326 v.unwrap_or(&json::Value::Null),
327 "Component",
328 "values",
329 )?),
330 "check_where" => repr_opt(va::check_where(v.cloned())?),
331 "check_filter" => repr_opt(va::check_filter(v)?.map(|p| json::Value::Object(p.clone()))),
332 "check_column" => json::py_repr_str(&va::check_column(v)?),
333 "address" => repr_opt(va::address(&opt("table"))?),
334 "values" => repr_opt(va::values(&opt("values"), "values")?),
335 "where_arg" => repr_opt(va::where_arg(&opt("where"))?),
336 other => panic!("unknown arg fn {other}"),
337 })
338}
339
340fn run(content: &str, op: &str, a: &Args) -> (&'static str, String) {
341 match op {
342 "py_repr" => {
343 let text = a.get("text").map(String::as_str).unwrap_or("");
344 match json::parse(text) {
345 None => ("ok", "UNPARSED".to_string()),
346 Some(v) => ("ok", json::py_repr(&v)),
347 }
348 }
349 "check_args" => {
350 let f = a.get("fn").map(String::as_str).unwrap_or("");
351 let text = a.get("text").map(String::as_str).unwrap_or("");
352 match arg_case(f, text) {
353 Ok(s) => ("ok", s),
354 Err(e) => ("err", e.message().to_string()),
355 }
356 }
357 "render_table_list" => (
358 "ok",
359 render_table_list(content, a.get("path").map(String::as_str).unwrap_or("")),
360 ),
361 "list_tables" => {
362 let dump = list_tables(content)
363 .iter()
364 .map(|e| {
365 format!(
366 "{}|{}|{}|{}|{}",
367 e.ordinal,
368 e.heading,
369 e.caption,
370 e.columns.join(" | "),
371 e.rows
372 )
373 })
374 .collect::<Vec<_>>()
375 .join("\n");
376 ("ok", dump)
377 }
378 "find_tables" => {
379 let dump = find_tables(content)
380 .iter()
381 .map(|t| format!("{}|{}|{}|{}", t.start, t.end, t.is_aligned(), t.has_tabs()))
382 .collect::<Vec<_>>()
383 .join("\n");
384 ("ok", dump)
385 }
386 "find_sections" => {
387 let dump = find_sections(content)
388 .iter()
389 .map(|s| {
390 format!(
391 "{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}",
392 s.start,
393 s.heading_end,
394 s.own_end,
395 s.end,
396 s.level,
397 s.style.as_str(),
398 s.gap_after,
399 s.parent.map(|p| p as isize).unwrap_or(-1),
400 esc(&s.indent),
401 s.marker,
402 esc(&s.space),
403 s.closing,
404 esc(&s.raw_text),
405 s.path.join(" > ")
406 )
407 })
408 .collect::<Vec<_>>()
409 .join("\n");
410 ("ok", dump)
411 }
412 "find_lists" => {
418 let mut dump: Vec<String> = Vec::new();
419 for l in find_lists(content) {
420 dump.push(format!(
421 "{}|{}|{}|{}|{}|{}",
422 l.start,
423 l.end,
424 l.ordered,
425 l.bullet,
426 esc(&l.indent),
427 l.loose
428 ));
429 for it in &l.items {
430 dump.push(format!(
431 " {}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}",
432 it.start,
433 it.own_end,
434 it.end,
435 it.depth,
436 esc(&it.indent),
437 it.marker,
438 it.ordered,
439 opt(it.number.map(|n| n.to_string())),
440 opt(it.delim.map(|c| c.to_string())),
441 opt(it.checkbox.map(|c| c.to_string())),
442 it.parent.map(|p| p as isize).unwrap_or(-1),
443 esc(&it.text)
444 ));
445 }
446 }
447 ("ok", dump.join("\n"))
448 }
449 "parse_path" => match parse_path(a.get("text").map(String::as_str).unwrap_or("")) {
458 None => ("ok", "NONE".to_string()),
459 Some(p) => {
460 let kinds = p
461 .iter()
462 .map(|s| match s {
463 Seg::Index(n) => format!("i:{n}"),
464 Seg::Key(k) => format!("k:{k}"),
465 })
466 .collect::<Vec<_>>()
467 .join("|");
468 ("ok", format!("{}\n{kinds}", format_path(&p)))
469 }
470 },
471 "find_frontmatter" => {
472 let fm = find_frontmatter(content);
473 if !fm.present {
474 return ("ok", "absent".to_string());
475 }
476 let mut dump = vec![format!(
477 "{}|{}|{}|{}|{}|{}",
478 match fm.fmt {
479 Some(Fmt::Yaml) => "yaml",
480 _ => "toml",
481 },
482 fm.start,
483 fm.end,
484 fm.delim,
485 fm.close,
486 esc(&fm.eol)
487 )];
488 for e in &fm.entries {
489 dump.push(format!(
490 " {}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}",
491 format_path(&e.path),
492 e.line,
493 e.end,
494 e.kind.as_str(),
495 esc(&e.prefix),
496 esc(&e.key_text),
497 esc(&e.gap),
498 esc(&e.value),
499 esc(&e.pad),
500 esc(&e.comment),
501 esc(&e.eol),
502 esc(&e.rebuilt(None))
503 ));
504 }
505 ("ok", dump.join("\n"))
506 }
507 "describe_front" => {
511 let name = a.get("opname").map(String::as_str).unwrap_or("");
512 let arg = a.get("args").map(String::as_str).unwrap_or("ABSENT");
513 let parsed = if arg == "ABSENT" {
514 None
515 } else {
516 json::parse(arg)
517 };
518 if arg != "ABSENT" && parsed.is_none() {
519 ("ok", format!("PARSE-FAILED {arg}"))
520 } else {
521 match apply_op(content, name, parsed.as_ref()) {
522 Ok(after) => ("ok", describe_frontmatter_change(content, &after)),
523 Err(e) => ("err", e.0),
524 }
525 }
526 }
527 "render_frontmatter" => (
528 "ok",
529 render_frontmatter(content, a.get("path").map(String::as_str).unwrap_or("")),
530 ),
531 "frontmatter_get" => {
537 let path = a.get("path").map(String::as_str).unwrap_or("");
538 let key = match a.get("key") {
543 None => None,
544 Some(text) => match json::parse(text) {
545 Some(v) => Some(v),
546 None => return ("ok", format!("PARSE-FAILED {text}")),
547 },
548 };
549 let got = match frontmatter_get(content, key.as_ref()) {
550 Ok(g) => g,
551 Err(e) => return ("err", e.0),
552 };
553 let text = match render_frontmatter_get(content, path, key.as_ref()) {
554 Ok(t) => t,
555 Err(e) => return ("err", e.0),
556 };
557 let mut dump = vec![format!(
558 "{}|{}|{}",
559 got.state,
560 match got.format {
561 Some(Fmt::Yaml) => "yaml",
562 Some(Fmt::Toml) => "toml",
563 None => "None",
567 },
568 got.keys.len()
569 )];
570 for k in &got.keys {
571 dump.push(format!(
572 "{}|{}|{}|{}",
573 k.path,
574 k.kind,
575 esc(&k.value),
576 k.lines
577 ));
578 }
579 dump.push("--".to_string());
580 dump.push(text);
581 ("ok", dump.join("\n"))
582 }
583 "list_lists" => {
587 let dump = list_lists(content)
588 .iter()
589 .map(|e| {
590 format!(
591 "{}|{}|{}|{}|{}|{}|{}|{}",
592 e.ordinal, e.heading, e.kind, e.marker, e.items, e.levels, e.loose, e.tasks
593 )
594 })
595 .collect::<Vec<_>>()
596 .join("\n");
597 ("ok", dump)
598 }
599 "render_list_summary" => (
600 "ok",
601 render_list_summary(content, a.get("path").map(String::as_str).unwrap_or("")),
602 ),
603 "section_outline" => {
607 let dump = section_outline(content)
608 .iter()
609 .map(|e| {
610 format!(
611 "{}|{}|{}|{}|{}|{}|{}|{}",
612 e.level,
613 e.path,
614 e.text,
615 e.style,
616 e.ordinal,
617 e.unique,
618 e.has_body,
619 e.subsections
620 )
621 })
622 .collect::<Vec<_>>()
623 .join("\n");
624 ("ok", dump)
625 }
626 "render_section_outline" => (
627 "ok",
628 render_section_outline(content, a.get("path").map(String::as_str).unwrap_or("")),
629 ),
630 "resolve_section" => match resolve_section(content, §ion_address(a)) {
634 Ok(s) => (
635 "ok",
636 format!(
637 "{}|{}|{}|{}|{}",
638 s.start, s.heading_end, s.own_end, s.end, s.level
639 ),
640 ),
641 Err(e) => ("err", e.0),
642 },
643 "resolve_list" => match resolve_list(content, &list_address(a)) {
644 Ok(l) => ("ok", format!("{}|{}", l.start, l.end)),
645 Err(e) => ("err", e.0),
646 },
647 "resolve_item" => match resolve_list(content, &list_address(a))
648 .and_then(|l| resolve_item(&l, a.get("item").map(String::as_str), "item"))
649 {
650 Ok(i) => ("ok", i.to_string()),
651 Err(e) => ("err", e.0),
652 },
653 "inert_headings" => {
654 let dump = inert_headings(content)
655 .iter()
656 .map(|h| format!("{}|{}|{}", h.line, h.reason, h.text))
657 .collect::<Vec<_>>()
658 .join("\n");
659 ("ok", dump)
660 }
661 "resolve_table" => match resolve_table(content, &address(a)) {
662 Ok(t) => ("ok", format!("{}|{}", t.start, t.end)),
663 Err(e) => ("err", e.0),
664 },
665 "resolve_row" => match resolve_table(content, &address(a))
666 .and_then(|t| resolve_row(&t, &where_pairs(a)))
667 {
668 Ok(i) => ("ok", i.to_string()),
669 Err(e) => ("err", e.0),
670 },
671 "add_row" => match table_add_row(content, &address(a), &values(a), position(a).as_ref()) {
672 Ok(s) => ("ok", s),
673 Err(e) => ("err", e.0),
674 },
675 "update_cell" => match table_update_cell(
676 content,
677 &address(a),
678 &where_pairs(a),
679 Some(&json::Value::Str(
683 a.get("column").cloned().unwrap_or_default(),
684 )),
685 Some(&json::Value::Str(
686 a.get("value").cloned().unwrap_or_default(),
687 )),
688 ) {
689 Ok(s) => ("ok", s),
690 Err(e) => ("err", e.0),
691 },
692 "table_get" => {
698 let (addr, filt) = match typed_args(a) {
703 Some(v) => (typed_address(v.get("table")), typed_filter(&v)),
704 None => (address(a), filter_arg(a)),
705 };
706 match render_table_get(content, &addr, filt.as_ref()) {
707 Ok(s) => ("ok", s),
708 Err(e) => ("err", e.0),
709 }
710 }
711 "delete_row" => match table_delete_row(content, &address(a), &where_pairs(a)) {
712 Ok(s) => ("ok", s),
713 Err(e) => ("err", e.0),
714 },
715 "apply_op" => {
721 let name = a.get("opname").map(String::as_str).unwrap_or("");
722 let text = a.get("args").map(String::as_str).unwrap_or("ABSENT");
723 let parsed = if text == "ABSENT" {
724 None
725 } else {
726 json::parse(text)
727 };
728 if text != "ABSENT" && parsed.is_none() {
732 ("ok", format!("PARSE-FAILED {text}"))
733 } else {
734 match apply_op(content, name, parsed.as_ref()) {
735 Ok(s) => ("ok", s),
736 Err(e) => ("err", e.0),
737 }
738 }
739 }
740 "describe_change" => {
752 if let Some(text) = a.get("after") {
753 return match json::parse(text) {
754 Some(json::Value::Str(s)) => ("ok", describe_change(content, &s)),
755 _ => ("ok", format!("PARSE-FAILED {text}")),
756 };
757 }
758 let name = a.get("opname").map(String::as_str).unwrap_or("");
759 let arg = a.get("args").map(String::as_str).unwrap_or("ABSENT");
760 let parsed = if arg == "ABSENT" {
761 None
762 } else {
763 json::parse(arg)
764 };
765 if arg != "ABSENT" && parsed.is_none() {
766 ("ok", format!("PARSE-FAILED {arg}"))
767 } else {
768 match apply_op(content, name, parsed.as_ref()) {
769 Ok(after) => ("ok", describe_change(content, &after)),
770 Err(e) => ("err", e.0),
771 }
772 }
773 }
774 other => ("err", format!("unknown op {other}")),
775 }
776}