///|
fn json_object(
value : Json,
allowed : Array[String],
context : String,
) -> Map[String, Json] raise JcampError {
guard value is Object(fields) else {
reject("JSON_OBJECT", context + " must be an object")
}
for key in fields.keys() {
guard allowed.contains(key) else {
reject("JSON_FIELD", context + " has unknown field: " + key)
}
}
fields
}
///|
fn json_field(
fields : Map[String, Json],
key : String,
) -> Json raise JcampError {
match fields.get(key) {
Some(value) => value
None => reject("JSON_REQUIRED", "missing JSON field: " + key)
}
}
///|
fn json_string(
fields : Map[String, Json],
key : String,
) -> String raise JcampError {
match json_field(fields, key) {
String(text) => text
_ => reject("JSON_TYPE", key + " must be a string")
}
}
///|
fn json_double(
fields : Map[String, Json],
key : String,
default : Double,
) -> Double raise JcampError {
match fields.get(key) {
Some(Number(n, ..)) => {
guard finite(n) else { reject("JSON_NUMBER", key + " must be finite") }
n
}
Some(_) => reject("JSON_TYPE", key + " must be a number")
None => default
}
}
///|
fn json_int(
fields : Map[String, Json],
key : String,
default : Int32,
) -> Int32 raise JcampError {
let n = json_double(fields, key, default.to_double())
guard n.floor() == n && n >= -2147483648.0 && n <= 2147483647.0 else {
reject("JSON_INTEGER", key + " must be a 32-bit integer")
}
n.to_int()
}
///|
pub fn spectrum_from_json(value : Json) -> Spectrum raise JcampError {
let fields = json_object(
value,
[
"title", "data_type", "x_units", "y_units", "points", "records", "source_line",
],
"spectrum",
)
guard json_field(fields, "points") is Array(input) else {
reject("JSON_TYPE", "points must be an array")
}
guard input.length() <= Limits::default().max_points else {
reject("LIMIT_POINTS", "JSON point count exceeds budget")
}
let points : Array[Point] = []
for item in input {
let p = json_object(item, ["x", "y"], "point")
guard json_field(p, "x") is Number(x, ..) && finite(x) else {
reject("JSON_NUMBER", "point x must be finite")
}
let y : Double? = match json_field(p, "y") {
Null => None
Number(value, ..) => {
guard finite(value) else {
reject("JSON_NUMBER", "point y must be finite or null")
}
Some(value)
}
_ => reject("JSON_TYPE", "point y must be a number or null")
}
points.push({ x, y, })
}
let records : Array[Record] = []
match fields.get("records") {
Some(Array(items)) => {
guard items.length() <= Limits::default().max_records else {
reject("LIMIT_RECORDS", "JSON record count exceeds budget")
}
for item in items {
let r = json_object(item, ["label", "value", "line"], "record")
let record : Record = {
label: json_string(r, "label"),
value: json_string(r, "value"),
line: json_int(r, "line", 0),
}
writable_record(record)
records.push(record)
}
}
Some(_) => reject("JSON_TYPE", "records must be an array")
None => ()
}
let result : Spectrum = {
title: json_string(fields, "title"),
data_type: json_string(fields, "data_type"),
x_units: json_string(fields, "x_units"),
y_units: json_string(fields, "y_units"),
points,
records,
source_line: json_int(fields, "source_line", 0),
}
checked_spectrum(result)
result
}
///|
pub fn export_json(spectra : Array[Spectrum]) -> Json raise JcampError {
guard !spectra.is_empty() && spectra.length() < Limits::default().max_blocks else {
reject(
"COLLECTION_SIZE", "JSON export requires a bounded nonempty collection",
)
}
let mut count = 0
for spectrum in spectra {
checked_spectrum(spectrum)
guard spectrum.points.length() <= Limits::default().max_points - count else {
reject("LIMIT_POINTS", "JSON export exceeds point budget")
}
count += spectrum.points.length()
}
{ "schema": "moonjcamp/spectra/1", "spectra": Json(spectra) }
}
///|
pub fn import_json(value : Json) -> Array[Spectrum] raise JcampError {
let fields = json_object(value, ["schema", "spectra"], "interchange document")
guard json_string(fields, "schema") == "moonjcamp/spectra/1" else {
reject("JSON_SCHEMA", "unsupported interchange schema")
}
guard json_field(fields, "spectra") is Array(values) &&
!values.is_empty() &&
values.length() < Limits::default().max_blocks else {
reject("COLLECTION_SIZE", "spectra must be a bounded nonempty array")
}
let result : Array[Spectrum] = []
let mut count = 0
for value in values {
let spectrum = spectrum_from_json(value)
guard spectrum.points.length() <= Limits::default().max_points - count else {
reject("LIMIT_POINTS", "JSON input exceeds total point budget")
}
count += spectrum.points.length()
result.push(spectrum)
}
result
}
///|
pub fn parse_interchange(text : String) -> Array[Spectrum] raise JcampError {
guard text.length() <= Limits::default().max_chars else {
reject("LIMIT_TEXT", "JSON input exceeds text budget")
}
let value = @json.parse(text[:], reject_duplicate_keys=true) catch {
_ => reject("JSON_SYNTAX", "invalid JSON or duplicate object key")
}
import_json(value)
}
///|
fn parse_write_options(value : Json) -> WriteOptions raise JcampError {
let fields = json_object(
value,
["encoding", "table", "points_per_line", "x_factor", "y_factor"],
"write options",
)
let encoding = match fields.get("encoding") {
None | Some(String("affn")) => Plain
Some(String("pac")) => Packed
Some(String("sqz")) => Squeezed
Some(String("difdup")) => Difference
_ => reject("WRITE_ENCODING", "encoding must be affn, pac, sqz or difdup")
}
let table_style = match fields.get("table") {
None | Some(String("auto")) => Automatic
Some(String("incremental")) => Incremental
Some(String("pairs")) => Pairs
_ => reject("WRITE_TABLE", "table must be auto, incremental or pairs")
}
{
encoding,
table_style,
points_per_line: json_int(fields, "points_per_line", 8),
x_factor: json_double(fields, "x_factor", 1.0),
y_factor: json_double(fields, "y_factor", 1.0),
}
}
///|
fn parse_read_options(value : Json) -> ParseOptions raise JcampError {
let fields = json_object(
value,
["numeric_mode", "max_points", "relative_tolerance", "absolute_tolerance"],
"read options",
)
let numeric_mode = match fields.get("numeric_mode") {
None | Some(String("auto")) => Auto
Some(String("affn")) => Affn
Some(String("asdf")) => Asdf
_ => reject("NUMERIC_MODE", "numeric_mode must be auto, affn or asdf")
}
let options = {
numeric_mode,
limits: {
..Limits::default(),
max_points: json_int(fields, "max_points", Limits::default().max_points),
},
relative_tolerance: json_double(
fields,
"relative_tolerance",
ParseOptions::default().relative_tolerance,
),
absolute_tolerance: json_double(
fields,
"absolute_tolerance",
ParseOptions::default().absolute_tolerance,
),
}
check_options(options)
options
}