///|
fn run_request(request : Json) -> Json raise JcampError {
guard request is Object(fields) else {
reject("REQUEST_OBJECT", "request must be an object")
}
let operation = json_string(fields, "operation")
let read = parse_read_options(fields.get("read_options").unwrap_or({}))
let write = parse_write_options(fields.get("write_options").unwrap_or({}))
match operation {
"read" => {
ignore(
json_object(
request,
["operation", "text", "read_options"],
"read request",
),
)
export_json(read_spectra(json_string(fields, "text"), options=read))
}
"audit" => {
ignore(
json_object(
request,
["operation", "text", "read_options"],
"audit request",
),
)
Json(audit(json_string(fields, "text"), options=read))
}
"inspect" => {
ignore(
json_object(
request,
["operation", "text", "read_options"],
"inspect request",
),
)
Json(inspect(json_string(fields, "text"), options=read))
}
"summary" => {
ignore(
json_object(
request,
["operation", "text", "read_options", "policy"],
"summary request",
),
)
let policy = parse_quality_policy(fields.get("policy").unwrap_or({}))
let summaries : Array[Summary] = []
for spectrum in read_spectra(json_string(fields, "text"), options=read) {
summaries.push(summarize(spectrum, policy~))
}
{
"spectra": Json(summaries),
"accepted": summaries.iter().all(s => s.accepted),
}
}
"batch" => {
ignore(
json_object(
request,
["operation", "inputs", "read_options", "policy"],
"batch request",
),
)
guard json_field(fields, "inputs") is Array(values) &&
values.length() <= read.limits.max_blocks else {
reject("BATCH_SIZE", "inputs must be a bounded array")
}
let inputs : Array[BatchInput] = []
for value in values {
let input = json_object(value, ["name", "text"], "batch input")
inputs.push({
name: json_string(input, "name"),
text: json_string(input, "text"),
})
}
Json(
inspect_batch(
inputs,
policy=parse_quality_policy(fields.get("policy").unwrap_or({})),
options=read,
),
)
}
"to-csv" => {
ignore(
json_object(
request,
["operation", "text", "read_options"],
"CSV export request",
),
)
let spectra = read_spectra(json_string(fields, "text"), options=read)
guard spectra.length() == 1 else {
reject("CSV_SINGLE", "split a compound document before CSV export")
}
Json(export_csv(spectra[0]))
}
"from-csv" => {
ignore(
json_object(
request,
["operation", "text", "metadata", "metadata_text", "write_options"],
"CSV import request",
),
)
{
"text": write_spectrum(
import_csv(
json_string(fields, "text"),
nested_json(fields, "metadata"),
),
options=write,
),
}
}
"transform" => {
ignore(
json_object(
request,
[
"operation", "text", "actions", "actions_text", "read_options", "write_options",
],
"transform request",
),
)
guard nested_json(fields, "actions") is Array(actions) &&
!actions.is_empty() &&
actions.length() <= 100 else {
reject(
"TRANSFORM_ACTIONS", "actions must contain between 1 and 100 operations",
)
}
let spectra = read_spectra(json_string(fields, "text"), options=read)
let result : Array[Spectrum] = []
let mut total = 0
for spectrum in spectra {
let mut value = spectrum
for action in actions {
value = apply_action(value, action)
}
guard value.points.length() <= read.limits.max_points - total else {
reject("LIMIT_POINTS", "transformed collection exceeds point budget")
}
total += value.points.length()
result.push(value)
}
{
"text": if result.length() == 1 {
write_spectrum(result[0], options=write)
} else {
write_collection(result, options=write)
},
}
}
"write" => {
ignore(
json_object(
request,
["operation", "spectrum", "write_options"],
"write request",
),
)
{
"text": write_spectrum(
spectrum_from_json(json_field(fields, "spectrum")),
options=write,
),
}
}
"verify" => {
ignore(
json_object(
request,
["operation", "texts", "read_options"],
"verify request",
),
)
guard json_field(fields, "texts") is [String(left), String(right)] else {
reject("COMPARE_FILES", "verify requires exactly two text inputs")
}
Json(compare_documents(left, right, options=read))
}
"from-json" => {
ignore(
json_object(
request,
["operation", "text", "write_options"],
"from-json request",
),
)
let spectra = parse_interchange(json_string(fields, "text"))
{
"text": if spectra.length() == 1 {
write_spectrum(spectra[0], options=write)
} else {
write_collection(spectra, options=write)
},
}
}
"rewrite" => {
ignore(
json_object(
request,
["operation", "text", "read_options", "write_options"],
"rewrite request",
),
)
{
"text": rewrite_document(
parse_document(json_string(fields, "text"), options=read),
options=write,
read_options=read,
),
}
}
"split" => {
ignore(
json_object(
request,
["operation", "text", "read_options"],
"split request",
),
)
let document = parse_document(json_string(fields, "text"), options=read)
let files : Array[Json] = []
for i, part in split_document(document, read_options=read) {
files.push({
"name": "spectrum-" + (i + 1).to_string() + ".jdx",
"text": render_document(part, read_options=read),
})
}
{ "files": Json(files) }
}
"combine" => {
ignore(
json_object(request, ["operation", "texts", "title"], "combine request"),
)
guard json_field(fields, "texts") is Array(texts) &&
!texts.is_empty() &&
texts.length() < Limits::default().max_blocks else {
reject("REQUEST_FILES", "texts must be a bounded nonempty list")
}
let documents : Array[Document] = []
let mut chars = 0
for text in texts {
guard text is String(text) else {
reject("JSON_TYPE", "each source must be text")
}
guard text.length() <= Limits::default().max_chars - chars else {
reject("LIMIT_TEXT", "combined input exceeds text budget")
}
chars += text.length()
documents.push(parse_document(text))
}
let title = match fields.get("title") {
Some(String(title)) => title
None => "Combined documents"
_ => reject("JSON_TYPE", "title must be a string")
}
{ "text": render_document(combine_documents(documents, title~)) }
}
_ => reject("REQUEST_OPERATION", "unknown operation: " + operation)
}
}
///|
fn nested_json(
fields : Map[String, Json],
key : String,
) -> Json raise JcampError {
match (fields.get(key), fields.get(key + "_text")) {
(Some(value), None) => value
(None, Some(String(text))) => {
guard text.length() <= Limits::default().max_chars else {
reject("LIMIT_TEXT", "nested JSON exceeds text budget")
}
@json.parse(text[:], reject_duplicate_keys=true) catch {
_ =>
reject("JSON_SYNTAX", "invalid nested JSON or duplicate object key")
}
}
_ =>
reject(
"JSON_FIELD",
"provide exactly one of " + key + " or " + key + "_text",
)
}
}
///|
fn apply_action(
spectrum : Spectrum,
action : Json,
) -> Spectrum raise JcampError {
guard action is Object(fields) else {
reject("JSON_OBJECT", "transform action must be an object")
}
let operation = json_string(fields, "operation")
fn required_double(key : String) -> Double raise JcampError {
ignore(json_field(fields, key))
json_double(fields, key, 0.0)
}
match operation {
"edit-metadata" => {
ignore(json_object(action, ["operation", "edits"], "metadata action"))
edit_metadata(spectrum, parse_metadata_edits(json_field(fields, "edits")))
}
"crop" => {
ignore(
json_object(action, ["operation", "minimum", "maximum"], "crop action"),
)
crop(spectrum, required_double("minimum"), required_double("maximum"))
}
"resample" => {
ignore(
json_object(
action,
["operation", "start", "end", "count"],
"resample action",
),
)
ignore(json_field(fields, "count"))
resample(
spectrum,
required_double("start"),
required_double("end"),
json_int(fields, "count", 0),
)
}
"convert-x" => {
ignore(
json_object(action, ["operation", "target"], "X conversion action"),
)
convert_x(spectrum, json_string(fields, "target"))
}
"convert-y" => {
ignore(
json_object(action, ["operation", "target"], "Y conversion action"),
)
convert_y(spectrum, json_string(fields, "target"))
}
_ => reject("TRANSFORM_OPERATION", "unknown transform: " + operation)
}
}
///|
pub fn handle_request(request : Json) -> Json {
{ "ok": true, "value": run_request(request) } catch {
Invalid(diagnostic) => { "ok": false, "error": Json(diagnostic) }
}
}
///|
pub fn protocol(text : String) -> String {
let result : Json = try {
guard text.length() <= 2 * Limits::default().max_chars else {
reject("LIMIT_TEXT", "protocol input exceeds budget")
}
let request = @json.parse(text[:], reject_duplicate_keys=true) catch {
_ => reject("JSON_SYNTAX", "invalid request JSON or duplicate object key")
}
handle_request(request)
} catch {
Invalid(diagnostic) => { "ok": false, "error": Json(diagnostic) }
}
result.stringify()
}