summaryrefslogtreecommitdiff
path: root/dejson/dejson.ha
diff options
context:
space:
mode:
Diffstat (limited to 'dejson/dejson.ha')
-rw-r--r--dejson/dejson.ha286
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);