diff options
author | Lassi Pulkkinen <lassi@pulk.fi> | 2024-10-31 03:11:21 +0200 |
---|---|---|
committer | Lassi Pulkkinen <lassi@pulk.fi> | 2024-10-31 03:51:35 +0200 |
commit | ae44478b30d890fe0fb04022f44d474dcdcc3f9d (patch) | |
tree | 5f462459ae4b47d22114eed717d1382d08cf4dfe /dejson |
Diffstat (limited to 'dejson')
-rw-r--r-- | dejson/dejson.ha | 286 |
1 files changed, 286 insertions, 0 deletions
diff --git a/dejson/dejson.ha b/dejson/dejson.ha new file mode 100644 index 0000000..74e2752 --- /dev/null +++ b/dejson/dejson.ha @@ -0,0 +1,286 @@ +use ascii; +use encoding::json; +use fmt; +use io; +use math; +use memio; +use strings; +use types; + +export type deser = struct { + val: *json::value, + link: ((*deser, (str | size)) | void), +}; + +export type error = !str; + +export fn newdeser(val: *json::value) deser = + deser { val = val, link = void }; + +export fn fail(de: *deser, fmt: str, args: fmt::field...) error = { + let path: [](str | size) = []; + defer free(path); + for (let de_ = de; true) match (de_.link) { + case let link: (*deser, (str | size)) => + append(path, link.1); + de_ = link.0; + case void => break; + }; + + let err = memio::dynamic(); + + if (len(path) == 0 || !(path[len(path) - 1] is str)) { + memio::concat(&err, ".")!; + }; + for (let i = len(path); i > 0) { + i -= 1; + match (path[i]) { + case let s: str => + memio::concat(&err, ".")!; + printfield(&err, s)!; + case let n: size => + fmt::fprintf(&err, "[{}]", n)!; + }; + }; + + memio::concat(&err, ": ")!; + fmt::fprintf(&err, fmt, args...)!; + + return memio::string(&err)!; +}; + +export fn printfield(out: io::handle, s: str) (void | io::error) = { + let valid = true; + let first = true; + let it = strings::iter(s); + for (true) match (strings::next(&it)) { + case let ch: rune => + if (!ascii::isalpha(ch) && ch != '_' + && (first || !ascii::isdigit(ch))) { + json::dump(out, s)?; + return; + }; + first = false; + case done => break; + }; + memio::concat(out, s)?; + return; +}; + +export fn strfield(s: str) str = { + let out = memio::dynamic(); + printfield(&out, s)!; + return memio::string(&out)!; +}; + +export fn typename(val: *json::value) str = { + match (*val) { + case f64 => + return "number"; + case str => + return "string"; + case bool => + return "boolean"; + case json::_null => + return "null"; + case []json::value => + return "array"; + case json::object => + return "object"; + }; +}; + +export fn optfield(de: *deser, name: str) (deser | void | error) = { + const obj = object(de)?; + match (json::get(obj, name)) { + case let val: *json::value => + return deser { + val = val, + link = (de, name), + }; + case void => void; + }; +}; + +export fn field(de: *deser, name: str) (deser | error) = { + match (optfield(de, name)) { + case let de_field: deser => + return de_field; + case void => + let fieldname = strfield(name); + defer free(fieldname); + return fail(de, "Field {} is required but missing", fieldname); + }; +}; + +export fn index(de: *deser, i: size) (deser | error) = { + const arr = array(de)?; + if (i < len(arr)) { + return deser { + val = &arr[i], + link = (de, i), + }; + } else { + return fail(de, "Array index {} is required but missing", i); + }; +}; + +export fn length(de: *deser) (size | error) = + len(array(de)?); + +export fn assert_length(de: *deser, n: size) (void | error) = { + if (length(de)? != n) + return fail(de, "Array has {} elements, {} expected", + length(de)?, n); +}; + +export fn count(de: *deser) (size | error) = + json::count(object(de)?); + +export fn assert_fields(de: *deser, names: str...) (void | error) = { + const obj = object(de)?; + let err = memio::dynamic(); + defer io::close(&err)!; + let n = 0z; + let it = json::iter(obj); + for :fields (true) { + match (json::next(&it)) { + case let entry: (const str, const *json::value) => + for (let i = 0z; i < len(names); i += 1) { + if (entry.0 == names[i]) { + continue :fields; + }; + }; + if (n != 0) { + memio::concat(&err, ", ")!; + }; + printfield(&err, entry.0)!; + n += 1; + case void => break; + }; + }; + switch (n) { + case 0 => + return; + case 1 => + return fail(de, "Unknown field {}", memio::string(&err)!); + case => + return fail(de, "Unknown fields {}", memio::string(&err)!); + }; +}; + +export type iterator = struct { + iter: json::iterator, + deser: *deser, +}; + +export fn iter(de: *deser) (iterator | error) = { + const obj = object(de)?; + return iterator { + iter = json::iter(obj), + deser = de, + }; +}; + +export fn next(it: *iterator) ((str, deser) | void) = { + match (json::next(&it.iter)) { + case let entry: (const str, const *json::value) => + return (entry.0, deser { + val = entry.1, + link = (it.deser, entry.0), + }); + case void => void; + }; +}; + +export fn object(de: *deser) (*json::object | error) = { + if (*de.val is json::object) { + return &(de.val: *struct { t: int, val: json::object }).val; + } else { + return fail(de, "Expected object, found {}", typename(de.val)); + }; +}; + +export fn array(de: *deser) ([]json::value | error) = { + match (*de.val) { + case let val: []json::value => + return val; + case => + return fail(de, "Expected array, found {}", typename(de.val)); + }; +}; + +export fn string(de: *deser) (str | error) = { + match (*de.val) { + case let val: str => + return val; + case => + return fail(de, "Expected string, found {}", typename(de.val)); + }; +}; + +export fn boolean(de: *deser) (bool | error) = { + match (*de.val) { + case let val: bool => + return val; + case => + return fail(de, "Expected boolean, found {}", typename(de.val)); + }; +}; + +export fn number(de: *deser) (f64 | error) = { + match (*de.val) { + case let val: f64 => + return val; + case => + return fail(de, "Expected number, found {}", typename(de.val)); + }; +}; + +export fn number_frange(de: *deser, a: f64, b: f64) (f64 | error) = { + const val = number(de)?; + if (val < a || val > b) + return fail(de, "Number {} is outside the allowed range [{},{}]", + val, a, b); + return val; +}; + +export fn number_irange(de: *deser, a: i64, b: i64) (i64 | error) = { + const val = number(de)?; + if (math::absf64(val) > 9007199254740991.0) + return fail(de, + "Number {} is too large to be safely converted from f64 to an integer", + val); + const (ival, frac) = math::modfracf64(val); + if (frac != 0.0) + return fail(de, "Number {} is not an integer", val); + const ival = ival: i64; + if (ival < a || ival > b) + return fail(de, "Number {} is outside the allowed range [{},{}]", + val, a, b); + return ival; +}; + +export fn number_u8(de: *deser) (u8 | error) = + number_irange(de, 0, types::U8_MAX: i64)?: u8; + +export fn number_u16(de: *deser) (u16 | error) = + number_irange(de, 0, types::U16_MAX: i64)?: u16; + +export fn number_u32(de: *deser) (u32 | error) = + number_irange(de, 0, types::U32_MAX: i64)?: u32; + +export fn number_u64(de: *deser) (u64 | error) = + number_irange(de, 0, types::I64_MAX)?: u64; + +export fn number_i8(de: *deser) (i8 | error) = + number_irange(de, types::I8_MIN, types::I8_MAX)?: i8; + +export fn number_i16(de: *deser) (i16 | error) = + number_irange(de, types::I16_MIN, types::I16_MAX)?: i16; + +export fn number_i32(de: *deser) (i32 | error) = + number_irange(de, types::I32_MIN, types::I32_MAX)?: i32; + +export fn number_i64(de: *deser) (i64 | error) = + number_irange(de, types::I64_MIN, types::I64_MAX); |