///|
priv suberror StudyDecodeError {
StudyDecodeError(String, String)
}
///|
fn object(
value : Json,
path : String,
keys : Array[String],
) -> Map[String, Json] raise StudyDecodeError {
guard value is Object(fields) else {
raise StudyDecodeError(path, "Expected an object.")
}
for key, _ in fields {
if !keys.contains(key) {
raise StudyDecodeError(path + "." + key, "Unknown field.")
}
}
fields
}
///|
fn required(
fields : Map[String, Json],
key : String,
path : String,
) -> Json raise StudyDecodeError {
match fields.get(key) {
Some(value) => value
None =>
raise StudyDecodeError(path + "." + key, "Required field is missing.")
}
}
///|
fn string_field(
fields : Map[String, Json],
key : String,
path : String,
) -> String raise StudyDecodeError {
match required(fields, key, path) {
String(value) => value
_ => raise StudyDecodeError(path + "." + key, "Expected a string.")
}
}
///|
fn mm_field(
fields : Map[String, Json],
key : String,
path : String,
) -> Int raise StudyDecodeError {
let value = string_field(fields, key, path)
match parse_diameter_mm(value) {
Ok(mm) => mm
Err(message) => raise StudyDecodeError(path + "." + key, message)
}
}
///|
fn decode_study(value : Json) -> @lesion.Study raise StudyDecodeError {
let fields = object(value, "$", [
"schema_version", "study_id", "baseline_date", "technique_id", "slice_thickness_mm",
"targets",
])
match required(fields, "schema_version", "$") {
Number(version, ..) if version == 1.0 => ()
_ =>
raise StudyDecodeError(
"$.schema_version", "Only schema_version 1 is supported.",
)
}
let targets_json = match required(fields, "targets", "$") {
Array(values) => values
_ => raise StudyDecodeError("$.targets", "Expected an array.")
}
let targets : Array[@lesion.TargetLesion] = []
for i, target in targets_json {
let path = "$.targets[\{i}]"
let fields = object(target, path, ["id", "organ", "kind", "baseline_mm"])
let kind : @lesion.LesionKind = match string_field(fields, "kind", path) {
"non_nodal" => NonNodal
"lymph_node" => LymphNode
_ =>
raise StudyDecodeError(path + ".kind", "Use non_nodal or lymph_node.")
}
targets.push({
id: string_field(fields, "id", path),
organ: string_field(fields, "organ", path),
kind,
baseline_tenths_mm: mm_field(fields, "baseline_mm", path),
})
}
{
id: string_field(fields, "study_id", "$"),
baseline_date: string_field(fields, "baseline_date", "$"),
technique_id: string_field(fields, "technique_id", "$"),
slice_thickness_tenths_mm: mm_field(fields, "slice_thickness_mm", "$"),
targets,
}
}
///|
/// Decode a versioned study profile. Call validate_study for clinical-contract
/// validation after schema parsing. Numeric diameters must be decimal strings.
pub fn parse_study_json(
text : String,
) -> Result[@lesion.Study, Array[Diagnostic]] {
let value = @json.parse(text) catch {
error => return Err([diagnostic("json_syntax", error.to_string())])
}
try decode_study(value) catch {
StudyDecodeError(path, message) =>
Err([diagnostic("study_schema", message, field=path)])
} noraise {
study => Ok(study)
}
}