///|
fn table_json(table : Table) -> Json {
Json::object({ "columns": Json(table.columns), "rows": Json(table.rows) })
}
///|
fn table_from_json(value : Json) -> Table raise LoomError {
let obj = j_object(value)
let table = {
columns: j_array(required(obj, "columns")).map(j_text),
rows: j_array(required(obj, "rows")).map(r => j_array(r).map(j_text)),
}
validate_table(table)
table
}
///|
fn prediction_json(prediction : Result[String, String]) -> Json {
match prediction {
Ok(value) => Json::object({ "ok": Json(true), "value": Json(value) })
Err(message) => Json::object({ "ok": Json(false), "error": Json(message) })
}
}
///|
fn option_int(
obj : Map[String, Json],
key : String,
fallback : Int32,
) -> Int32 raise LoomError {
match obj.get(key) {
Some(v) => j_int(v)
None => fallback
}
}
///|
fn options_from_json(value : Json) -> SearchOptions raise LoomError {
let obj = j_object(value)
fields_only(obj, [
"max_candidates", "max_parts", "max_atom_evaluations", "max_steps", "max_frontier",
])
let defaults = default_options()
{
max_candidates: option_int(obj, "max_candidates", defaults.max_candidates),
max_parts: option_int(obj, "max_parts", defaults.max_parts),
max_atom_evaluations: option_int(
obj,
"max_atom_evaluations",
defaults.max_atom_evaluations,
),
max_steps: option_int(obj, "max_steps", defaults.max_steps),
max_frontier: option_int(obj, "max_frontier", defaults.max_frontier),
}
}
///|
fn dispatch(request : Json) -> Json raise LoomError {
let obj = j_object(request)
match j_text(required(obj, "op")) {
"parse" => {
let text = j_text(required(obj, "text"))
let table = match j_text(required(obj, "format")) {
"csv" => parse_csv(text)
"jsonl" => parse_jsonl(text)
_ => raise LoomError("format must be csv or jsonl")
}
Json::object({ "ok": Json(true), "table": table_json(table) })
}
"synthesize" => {
let examples = j_array(required(obj, "examples")).map(v => {
let e = j_object(v)
Example::{
input: j_array(required(e, "input")).map(j_text),
output: j_text(required(e, "output")),
}
})
let options = match obj.get("options") {
Some(v) => options_from_json(v)
None => default_options()
}
let rows = match obj.get("rows") {
Some(v) => j_array(v).map(r => j_array(r).map(j_text))
None => []
}
let separators = match obj.get("separators") {
Some(v) => j_array(v).map(j_text)
None => []
}
let report = synthesize(examples, options~, separators~)
let suggestions = disagreements(report.candidates, rows)
Json::object({
"ok": Json(true),
"status": Json(report.status),
"truncated": Json(report.truncated),
"stats": Json::object({
"atom_evaluations": Json(report.atom_evaluations),
"atoms": Json(report.atoms),
"steps": Json(report.steps),
"cached_states": Json(report.cached_states),
"cache_hits": Json(report.cache_hits),
}),
"candidates": Json::array(
report.candidates.map(c => {
Json::object({
"expression": expression_to_json(c.expression),
"description": Json(explain(c.expression)),
"cost": Json(c.cost),
})
}),
),
"disagreements": Json::array(
suggestions.map(s => {
Json::object({
"row_index": Json(s.row_index),
"score": Json(s.score),
"groups": Json::array(
s.groups.map(g => {
Json::object({
"prediction": prediction_json(g.prediction),
"candidates": Json(g.candidates),
})
}),
),
})
}),
),
})
}
"validate_recipe" => {
let recipe = recipe_from_json(required(obj, "recipe"))
Json::object({
"ok": Json(true),
"recipe": recipe_to_json(recipe),
"descriptions": Json(recipe.outputs.map(o => explain(o.expression))),
})
}
"apply" => {
let recipe = recipe_from_json(required(obj, "recipe"))
let table = table_from_json(required(obj, "table"))
let result = apply_recipe(recipe, table)
let successful : Array[Array[String]] = []
let rows = result.rows.map(r => {
match r {
Ok(values) => {
successful.push(values)
Json::object({ "ok": Json(true), "values": Json(values) })
}
Err(message) =>
Json::object({ "ok": Json(false), "error": Json(message) })
}
})
let safe = match obj.get("spreadsheet_safe") {
Some(True) | None => true
Some(False) => false
_ => raise LoomError("spreadsheet_safe must be boolean")
}
// No partial CSV disguised as a complete result. JSON preserves row errors.
let csv = if result.failures == 0 {
Json(
render_csv(
{ columns: recipe.outputs.map(o => o.name), rows: successful, },
spreadsheet_safe=safe,
),
)
} else {
Json::null()
}
Json::object({
"ok": Json(true),
"failures": Json(result.failures),
"columns": Json(recipe.outputs.map(o => o.name)),
"rows": Json::array(rows),
"csv": csv,
})
}
_ => raise LoomError("unknown operation")
}
}
///|
/// Single JSON boundary shared by the Node CLI and browser worker. All data
/// parsing, search, recipe validation and transformation execute in MoonBit.
pub fn handle(request : String) -> String {
try {
if request.length() > 4000000 {
raise LoomError("request exceeds 4000000 UTF-16 units")
}
dispatch(parse_json_text(request)).stringify()
} catch {
LoomError(message) =>
Json::object({ "ok": Json(false), "error": Json(message) }).stringify()
}
}