///|
pub(all) enum MetadataEdit {
Set(String, String)
Remove(String)
} derive(@debug.Debug)
///|
pub extend MetadataEdit with @debug.Debug::{to_repr}
///|
fn protected_label(key : String) -> Bool {
[
"JCAMPDX", "DATATYPE", "DATACLASS", "END", "BLOCKS", "XYDATA", "XYPOINTS", "XYPAIRS",
"PEAKTABLE", "DATATABLE", "NTUPLES", "NPOINTS", "FIRSTX", "LASTX", "FIRSTY",
"LASTY", "MINX", "MAXX", "MINY", "MAXY", "DELTAX", "XFACTOR", "YFACTOR", "XUNITS",
"YUNITS",
].contains(key) ||
key.has_prefix("$MOONJCAMP.")
}
///|
pub fn edit_metadata(
spectrum : Spectrum,
edits : Array[MetadataEdit],
) -> Spectrum raise JcampError {
checked_spectrum(spectrum)
guard !edits.is_empty() && edits.length() <= 100 else {
reject("METADATA_EDITS", "metadata editing requires 1..100 changes")
}
let keys : Map[String, Bool] = Map([])
let mut records = spectrum.records.copy()
let mut title = spectrum.title
let changes : Array[Json] = []
for edit in edits {
let label = match edit {
Set(label, _) | Remove(label) => label
}
let key = canonical_label(label)
writable_record({ label, value: "", line: 0, })
guard key != "" && !keys.contains(key) else {
reject(
"METADATA_DUPLICATE", "metadata edit keys must be nonempty and unique",
)
}
guard !protected_label(key) else {
reject(
"METADATA_PROTECTED",
"derived, structural, unit and provenance labels require a dedicated operation: " +
label,
)
}
keys[key] = true
match edit {
Set(label, value) => {
writable_record({ label, value, line: 0, })
if key == "TITLE" {
guard trim(value) != "" && !value.contains("\n") else {
reject("METADATA_TITLE", "title must be nonempty and single-line")
}
title = value
}
records = set_record(records, label, value)
changes.push({ "action": "set", "label": label, "value": value })
}
Remove(_) => {
guard key != "TITLE" else {
reject("METADATA_TITLE", "TITLE cannot be removed")
}
records = remove_records(records, [key])
changes.push({ "action": "remove", "label": label })
}
}
guard records.length() <= Limits::default().max_records else {
reject("LIMIT_RECORDS", "metadata record budget exceeded")
}
}
let updated = transformed(
{ ..spectrum, title, records, },
spectrum.points.copy(),
{ "operation": "edit-metadata", "changes": Json(changes) },
)
// Use the same normalization and profile checks as a file written to disk.
{ ..read_spectra(write_spectrum(updated))[0], source_line: 0, }
}
///|
fn parse_metadata_edits(value : Json) -> Array[MetadataEdit] raise JcampError {
guard value is Array(items) && items.length() <= 100 else {
reject("METADATA_EDITS", "edits must be a bounded array")
}
let edits : Array[MetadataEdit] = []
for item in items {
let fields = json_object(
item,
["action", "label", "value"],
"metadata edit",
)
let label = json_string(fields, "label")
match json_string(fields, "action") {
"set" => edits.push(Set(label, json_string(fields, "value")))
"remove" => {
guard fields.get("value") is None else {
reject("JSON_FIELD", "remove edit must not include a value")
}
edits.push(Remove(label))
}
_ => reject("METADATA_ACTION", "metadata action must be set or remove")
}
}
edits
}