Skip to documentation
SLOP

tiny.preserves.json

Reference tiny.preserves json

Defined in tiny.preserves.

Converts the package's values to JSON text, and JSON text back to values.

API (8)

Actions

Public operations.

Types and contracts

Public types and contracts.

No direct callersNo direct callstiny.preservesjson
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Source: lib/preserves/src/json.zig

zig
//! Converts the package's values to JSON text, and JSON text back to values. A caller exchanging//! values with JSON tools needs a JSON form for records, patterns and embedded values too, and//! needs to know which details a round trip loses.//!//! JSON has objects, arrays, strings, numbers, booleans and null only. It has no records, symbols,//! byte strings, sets or patterns. The standard library's JSON reader holds each number as a 64-bit//! integer or a double. The package keeps the JSON representation of the//! [Preserves](https://preserves.dev/) data language, alongside its text and binary syntaxes.//!//! Each value JSON lacks becomes a JSON object whose reserved member names it: `__record__`,//! `__discard__`, `__embedded__`, `$capture`, `$bind` and `$rest`. A record labeled with one of the//! ten labels in the file's protocol table becomes an object with a `"type"` member and named//! fields, and the object decodes back to that record. `Observe` and `Synced` are two of those ten//! labels. The mapping loses detail: symbols, byte strings and sets come back as strings and//! sequences, integers outside 64 bits change, and non-finite doubles become null. The decoder//! copies every string it keeps, so the decoded value owns all its memory and frees with `deinit`.const std = @import("std");const Allocator = std.mem.Allocator;const pretty = @import("pretty");const value_mod = @import("value.zig");const integer_mod = @import("integer.zig");const embedded_mod = @import("embedded.zig");const symbols_mod = @import("symbols.zig");const constructors_mod = @import("constructors.zig");const predicates_mod = @import("predicates.zig");const text_format = @import("text/root.zig");const parse_error_mod = @import("error.zig");pub const AnyEmbedded = embedded_mod.AnyEmbedded;/// The value type this file reads and writes: Preserves values whose embedded values hold any/// pointer (`AnyEmbedded`), for code that builds values for `toJsonString` or reads what/// `fromJsonString` returns. The type is the same type as `Value(AnyEmbedded)` from the package/// root.pub const Value = value_mod.Value(AnyEmbedded);pub const SignedInteger = integer_mod.SignedInteger;pub const ParseError = parse_error_mod.ParseError;/// The errors `toJsonString` returns, for a caller switching on these errors. The error set covers/// running out of memory, and a set or dictionary anywhere in the value that holds two equal/// elements or keys.pub const EncodeError = Allocator.Error || error{    DuplicateSetElement,    DuplicateDictionaryKey,};const JsonWriter = pretty.json.Writer;const WriteError = EncodeError || std.Io.Writer.Error;const any_constructors = constructors_mod.any_constructors;const ProtocolEntry = struct {    symbol_name: []const u8,    json_type: []const u8,    fields: []const []const u8,};const protocol_records = [_]ProtocolEntry{    .{ .symbol_name = "Observe", .json_type = "observe", .fields = &.{ "pattern", "observer" } },    .{ .symbol_name = "ReactorError", .json_type = "reactor.error", .fields = &.{ "stage", "facet", "reactor", "error" } },    .{ .symbol_name = "EntityRuntime", .json_type = "entity.runtime", .fields = &.{ "kind", "observe", "during" } },    .{ .symbol_name = "RequireService", .json_type = "require.service", .fields = &.{"name"} },    .{ .symbol_name = "RunService", .json_type = "run.service", .fields = &.{"name"} },    .{ .symbol_name = "ServiceState", .json_type = "service.state", .fields = &.{ "name", "state" } },    .{ .symbol_name = "ServiceObject", .json_type = "service.object", .fields = &.{ "name", "object" } },    .{ .symbol_name = "ServiceDependency", .json_type = "service.dependency", .fields = &.{ "depender", "dependee" } },    .{ .symbol_name = "RestartService", .json_type = "restart.service", .fields = &.{"name"} },    .{ .symbol_name = "Synced", .json_type = "syndicate.sync.synced", .fields = &.{} },};fn lookupProtocol(symbol_name: []const u8) ?ProtocolEntry {    for (&protocol_records) |entry| {        if (std.mem.eql(u8, entry.symbol_name, symbol_name)) return entry;    }    return null;}fn lookupProtocolByJsonType(json_type: []const u8) ?ProtocolEntry {    for (&protocol_records) |entry| {        if (std.mem.eql(u8, entry.json_type, json_type)) return entry;    }    return null;}/// Returns `value` as minified JSON text, allocated with `alloc`, for code that logs or sends a/// value to a JSON consumer. The caller owns the returned bytes and frees them with `alloc`./// Booleans, strings and sequences become their JSON counterparts, and sets become arrays. The/// symbol `null` becomes JSON null, and every other symbol becomes a JSON string. A byte string/// becomes a string of lowercase hexadecimal digits. If a double is non-finite, it becomes null. An/// integer too wide for 128 bits becomes a 64-bit number that differs from it. A dictionary becomes/// an object: string and symbol keys become member names, and other keys are written in the text/// syntax. A record whose label is a protocol name becomes `{"type": …}` with named fields, and/// each field past the named ones takes the name `f` plus its index. If a record has a symbol label/// and either zero fields or one dictionary field with string keys, the record becomes/// `{"type": label}` plus the dictionary's entries. Any other record becomes/// `{"__record__": label, "fields": […]}`. The discard pattern becomes `{"__discard__": true}`, and/// captures, binds and rest patterns become `$capture`, `$bind` and `$rest` objects. An embedded/// value becomes `{"__embedded__": "…"}` holding its pointer as a decimal string. The call frees/// what it wrote so far on any error.pub fn toJsonString(alloc: Allocator, value: Value) EncodeError![]const u8 {    var out: std.Io.Writer.Allocating = .init(alloc);    errdefer out.deinit();    var json = JsonWriter.init(&out.writer, .minified);    writeJson(alloc, &json, value) catch |err| switch (err) {        error.WriteFailed => return error.OutOfMemory,        else => return @errorCast(err),    };    return try out.toOwnedSlice();}fn writeJson(alloc: Allocator, json: *JsonWriter, value: Value) WriteError!void {    switch (value) {        .discard => {            const object = try json.object();            try object.field("__discard__", true);            try object.end();        },        .capture => |inner| {            const object = try json.object();            try object.writer.objectField("$capture");            try writeJson(alloc, object.writer, inner.*);            try object.end();        },        .bind => |binding| {            const object = try json.object();            try object.field("$bind", binding.name);            if (binding.pattern.* != .discard) {                try object.writer.objectField("pattern");                try writeJson(alloc, object.writer, binding.pattern.*);            }            try object.end();        },        .rest_pattern => |rest_pattern| {            const object = try json.object();            const rest = try object.object("$rest");            const prefix = try rest.array("prefix");            for (rest_pattern.prefix) |item| {                try writeJson(alloc, prefix.writer, item);            }            try prefix.end();            try rest.writer.objectField("rest");            try writeJson(alloc, rest.writer, rest_pattern.rest.*);            try rest.end();            try object.end();        },        .boolean => |actual| try json.write(actual),        .signed_integer => |actual| try writeSignedInteger(json, actual),        .double => |actual| try writeDouble(json, actual),        .string => |actual| try json.write(actual),        .byte_string => |actual| try json.hexString(actual),        .symbol => |name| {            if (std.mem.eql(u8, name, "null")) {                try json.write(null);            } else {                try json.write(name);            }        },        .record => |record| try writeRecord(alloc, json, record),        .sequence => |items| try writeArray(alloc, json, items),        .set => |items| {            if (!Value.setElementsDistinct(items)) return error.DuplicateSetElement;            try writeArray(alloc, json, items);        },        .dictionary => |entries| {            if (!Value.dictionaryKeysDistinct(entries)) {                return error.DuplicateDictionaryKey;            }            try writeDictionary(alloc, json, entries);        },        .embedded => |embedded| {            const object = try json.object();            var tmp: [24]u8 = undefined;            const pointer = std.fmt.bufPrint(                &tmp,                "{d}",                .{@intFromPtr(embedded.value)},            ) catch unreachable;            try object.field("__embedded__", pointer);            try object.end();        },    }}fn writeRecord(alloc: Allocator, json: *JsonWriter, record: Value.Record) WriteError!void {    const label_symbol: ?[]const u8 = switch (record.label.*) {        .symbol => |symbol| symbol,        else => null,    };    if (label_symbol) |label_name| {        if (lookupProtocol(label_name)) |protocol| {            const object = try json.object();            try object.field("type", protocol.json_type);            for (record.fields, 0..) |field, index| {                if (index < protocol.fields.len) {                    try object.writer.objectField(protocol.fields[index]);                } else {                    var tmp: [16]u8 = undefined;                    const field_name = std.fmt.bufPrint(                        &tmp,                        "f{d}",                        .{index},                    ) catch unreachable;                    try object.writer.objectField(field_name);                }                try writeJson(alloc, object.writer, field);            }            try object.end();            return;        }        if (predicates_mod.recordAttributes(AnyEmbedded, .{ .record = record })) |attributes| {            const object = try json.object();            try object.field("type", label_name);            for (attributes) |entry| {                try object.writer.objectField(entry.key.string);                try writeJson(alloc, object.writer, entry.value);            }            try object.end();            return;        }    }    const object = try json.object();    try object.writer.objectField("__record__");    try writeJson(alloc, object.writer, record.label.*);    const fields = try object.array("fields");    for (record.fields) |field| {        try writeJson(alloc, fields.writer, field);    }    try fields.end();    try object.end();}fn writeArray(alloc: Allocator, json: *JsonWriter, items: []const Value) WriteError!void {    const array = try json.array();    for (items) |item| {        try writeJson(alloc, array.writer, item);    }    try array.end();}fn writeDictionary(    alloc: Allocator,    json: *JsonWriter,    entries: []const Value.DictionaryEntry,) WriteError!void {    const object = try json.object();    for (entries) |entry| {        switch (entry.key) {            .string => |key| {                try object.writer.objectField(key);                try writeJson(alloc, object.writer, entry.value);            },            .symbol => |key| {                try object.writer.objectField(key);                try writeJson(alloc, object.writer, entry.value);            },            else => {                const key = text_format.toText(alloc, entry.key) catch |err| switch (err) {                    error.OutOfMemory => return error.OutOfMemory,                    error.DuplicateSetElement => return error.DuplicateSetElement,                    error.DuplicateDictionaryKey => return error.DuplicateDictionaryKey,                };                defer alloc.free(key);                try object.writer.objectField(key);                try writeJson(alloc, object.writer, entry.value);            },        }    }    try object.end();}fn writeSignedInteger(json: *JsonWriter, value: SignedInteger) std.Io.Writer.Error!void {    switch (value.repr) {        .i128 => |actual| try json.print("{d}", .{actual}),        .u128 => |actual| try json.print("{d}", .{actual}),        .big => try json.print("{d}", .{value.toI64Lossy()}),    }}fn writeDouble(json: *JsonWriter, value: f64) std.Io.Writer.Error!void {    if (std.math.isNan(value) or std.math.isInf(value)) {        try json.write(null);        return;    }    try json.print("{d}", .{value});}/// Parses `json_text` and returns the Preserves value it describes, allocated with `alloc`, for/// code that receives JSON text. The value owns all its memory, and the caller frees it with/// `deinit`. The mapping is the one `fromJsonValue` applies to the parsed JSON. Malformed JSON/// returns `error.OutOfMemory`, the only error the call reports. If memory runs out inside a/// `__record__`, `$rest`, `$capture`, `$bind` or `"type"` object, the call leaks the parts already/// built.pub fn fromJsonString(alloc: Allocator, json_text: []const u8) !Value {    const parsed = std.json.parseFromSlice(std.json.Value, alloc, json_text, .{}) catch return error.OutOfMemory;    defer parsed.deinit();    return jsonToValue(alloc, parsed.value);}/// Returns the Preserves value that `json_value` describes, allocated with `alloc`, for code that/// already holds a parsed `std.json.Value`, so the JSON is parsed once. The result copies every/// string it keeps, so it owns all its memory and borrows nothing from `json_value`. JSON null/// becomes the symbol `null`, an integer that fits 64 bits becomes an integer, any other finite/// number becomes a double, and a number outside those ranges becomes the symbol `NaN`. Strings/// stay strings, and arrays become sequences. The reserved member names are tried in order:/// `__discard__`, `__record__`, `__embedded__`, `$rest`, `$capture`, `$bind`, then `"type"`. Any/// other object becomes a dictionary with string keys. A `"type"` naming a protocol record becomes/// that record, and each missing field becomes the discard pattern. Any other `"type"` becomes a/// record labeled with that name and one dictionary field holding the other members. A `__record__`/// string label becomes a symbol label. An `__embedded__` member becomes an embedded value that/// owns the decoded member, and copying or freeing the outer value copies or frees it. Decoding the/// output of `toJsonString` for an embedded value yields an embedded value that holds the pointer's/// decimal string.pub fn fromJsonValue(alloc: Allocator, json_value: std.json.Value) !Value {    return jsonToValue(alloc, json_value);}fn jsonToValue(alloc: Allocator, jv: std.json.Value) Allocator.Error!Value {    return switch (jv) {        .null => Value{ .symbol = try alloc.dupe(u8, symbols_mod.SYM_NULL.name) },        .bool => |v| Value{ .boolean = v },        .integer => |v| Value{ .signed_integer = SignedInteger.fromI128(@as(i128, v)) },        .float => |v| Value{ .double = v },        .string => |s| blk: {            const dup = try alloc.dupe(u8, s);            break :blk Value{ .string = dup };        },        .array => |arr| blk: {            const items = try alloc.alloc(Value, arr.items.len);            var built: usize = 0;            errdefer {                for (items[0..built]) |*item| item.deinit(alloc);                alloc.free(items);            }            for (arr.items, 0..) |item, i| {                items[i] = try jsonToValue(alloc, item);                built = i + 1;            }            break :blk Value{ .sequence = items };        },        .object => |obj| try objectToValue(alloc, obj),        .number_string => Value{ .symbol = try alloc.dupe(u8, "NaN") },    };}fn objectToValue(alloc: Allocator, obj: std.json.ObjectMap) Allocator.Error!Value {    if (obj.get("__discard__")) |_| {        return Value{ .discard = {} };    }    if (obj.get("__record__")) |label_json| {        const label_val = try jsonToValue(alloc, label_json);        const label = switch (label_val) {            .string => |s| Value{ .symbol = s },            else => label_val,        };        const fields_json = obj.get("fields") orelse {            return any_constructors.record(alloc, label, &.{}) catch |err| switch (err) {                error.OutOfMemory => return error.OutOfMemory,            };        };        switch (fields_json) {            .array => |arr| {                const fields = try alloc.alloc(Value, arr.items.len);                defer alloc.free(fields);                for (arr.items, 0..) |item, i| {                    fields[i] = try jsonToValue(alloc, item);                }                return any_constructors.record(alloc, label, fields) catch |err| switch (err) {                    error.OutOfMemory => return error.OutOfMemory,                };            },            else => return any_constructors.record(alloc, label, &.{}) catch |err| switch (err) {                error.OutOfMemory => return error.OutOfMemory,            },        }    }    if (obj.get("__embedded__")) |inner_json| {        var inner = try jsonToValue(alloc, inner_json);        errdefer inner.deinit(alloc);        const ptr = try alloc.create(Value);        ptr.* = inner;        return Value{ .embedded = AnyEmbedded{            .value = @ptrCast(ptr),            .semantic_ops = embedded_mod.parsedEmbeddedOps(Value),            .deinit_fn = embedded_mod.parsedEmbeddedDeinit(Value),            .clone_fn = embedded_mod.parsedEmbeddedClone(Value),        } };    }    if (obj.get("$rest")) |rest_json| {        switch (rest_json) {            .object => |rest_obj| {                if (rest_obj.get("prefix")) |prefix_json| {                    if (rest_obj.get("rest")) |rest_val_json| {                        switch (prefix_json) {                            .array => |arr| {                                const prefix = try alloc.alloc(Value, arr.items.len);                                errdefer alloc.free(prefix);                                for (arr.items, 0..) |item, i| {                                    prefix[i] = try jsonToValue(alloc, item);                                }                                const rest_val = try jsonToValue(alloc, rest_val_json);                                const rest_ptr = try alloc.create(Value);                                rest_ptr.* = rest_val;                                return Value{ .rest_pattern = .{ .prefix = prefix, .rest = rest_ptr } };                            },                            else => {},                        }                    }                }            },            else => {},        }    }    if (obj.get("$capture")) |cap_json| {        const inner = try jsonToValue(alloc, cap_json);        return any_constructors.capture(alloc, inner) catch |err| switch (err) {            error.OutOfMemory => return error.OutOfMemory,        };    }    if (obj.get("$bind")) |bind_json| {        switch (bind_json) {            .string => |name| {                const name_dup = try alloc.dupe(u8, name);                const pat: Value = if (obj.get("pattern")) |pj|                    try jsonToValue(alloc, pj)                else                    Value{ .discard = {} };                return any_constructors.bindVal(alloc, name_dup, pat) catch |err| switch (err) {                    error.OutOfMemory => return error.OutOfMemory,                };            },            else => {},        }    }    if (obj.get("type")) |type_json| {        switch (type_json) {            .string => |type_name| {                if (lookupProtocolByJsonType(type_name)) |proto| {                    const fields = try alloc.alloc(Value, proto.fields.len);                    defer alloc.free(fields);                    for (proto.fields, 0..) |fname, i| {                        if (obj.get(fname)) |fv| {                            fields[i] = try jsonToValue(alloc, fv);                        } else {                            fields[i] = Value{ .discard = {} };                        }                    }                    const label_name = try alloc.dupe(u8, proto.symbol_name);                    return any_constructors.record(alloc, Value{ .symbol = label_name }, fields) catch |err| switch (err) {                        error.OutOfMemory => return error.OutOfMemory,                    };                }                const attr_count = if (obj.count() > 0) obj.count() - 1 else 0;                const entries = try alloc.alloc(Value.DictionaryEntry, attr_count);                errdefer alloc.free(entries);                var iter = obj.iterator();                var i: usize = 0;                while (iter.next()) |entry| {                    if (std.mem.eql(u8, entry.key_ptr.*, "type")) continue;                    const key_str = try alloc.dupe(u8, entry.key_ptr.*);                    entries[i] = .{                        .key = Value{ .string = key_str },                        .value = try jsonToValue(alloc, entry.value_ptr.*),                    };                    i += 1;                }                const tagged = Value{ .dictionary = entries };                const label_name = try alloc.dupe(u8, type_name);                const fields = [_]Value{tagged};                return any_constructors.record(alloc, Value{ .symbol = label_name }, &fields) catch |err| switch (err) {                    error.OutOfMemory => return error.OutOfMemory,                };            },            else => {},        }    }    const entries = try alloc.alloc(Value.DictionaryEntry, obj.count());    var built: usize = 0;    errdefer {        for (entries[0..built]) |*done| {            done.key.deinit(alloc);            done.value.deinit(alloc);        }        alloc.free(entries);    }    var iter = obj.iterator();    var i: usize = 0;    while (iter.next()) |entry| {        const key_str = try alloc.dupe(u8, entry.key_ptr.*);        errdefer alloc.free(key_str);        entries[i] = .{            .key = Value{ .string = key_str },            .value = try jsonToValue(alloc, entry.value_ptr.*),        };        i += 1;        built = i;    }    return Value{ .dictionary = entries };}test "toJsonString: primitives" {    const alloc = std.testing.allocator;    const b = try toJsonString(alloc, Value{ .boolean = true });    defer alloc.free(b);    try std.testing.expectEqualStrings("true", b);    const i = try toJsonString(alloc, Value{ .signed_integer = SignedInteger.fromI128(-42) });    defer alloc.free(i);    try std.testing.expectEqualStrings("-42", i);    const s = try toJsonString(alloc, Value{ .string = "hi" });    defer alloc.free(s);    try std.testing.expectEqualStrings("\"hi\"", s);    const n = try toJsonString(alloc, Value{ .symbol = "null" });    defer alloc.free(n);    try std.testing.expectEqualStrings("null", n);}test "toJsonString: NaN and Inf round to null" {    const alloc = std.testing.allocator;    const nan = try toJsonString(alloc, Value{ .double = std.math.nan(f64) });    defer alloc.free(nan);    try std.testing.expectEqualStrings("null", nan);    const inf = try toJsonString(alloc, Value{ .double = std.math.inf(f64) });    defer alloc.free(inf);    try std.testing.expectEqualStrings("null", inf);}test "toJsonString: byte_string as hex-packed string" {    const alloc = std.testing.allocator;    const bytes = [_]u8{ 0xde, 0xad, 0xbe, 0xef };    const j = try toJsonString(alloc, Value{ .byte_string = &bytes });    defer alloc.free(j);    try std.testing.expectEqualStrings("\"deadbeef\"", j);}test "toJsonString: nested dictionaries preserve wide values and escaping" {    const alloc = std.testing.allocator;    const bytes = [_]u8{ 0x00, 0xff };    var sequence = [_]Value{        .{ .byte_string = &bytes },        .{ .signed_integer = SignedInteger.fromU128(std.math.maxInt(u128)) },        .{ .double = 1.5 },    };    var set = [_]Value{        .{ .boolean = false },        .{ .boolean = true },    };    var entries = [_]Value.DictionaryEntry{        .{            .key = .{ .string = "nested\"\n\x01" },            .value = .{ .sequence = &sequence },        },        .{            .key = Value.initI128(7),            .value = .{ .set = &set },        },    };    const rendered = try toJsonString(alloc, Value.initDictionary(&entries));    defer alloc.free(rendered);    try std.testing.expectEqualStrings(        "{\"nested\\\"\\n\\u0001\":[\"00ff\",340282366920938463463374607431768211455,1.5],\"7\":[false,true]}",        rendered,    );}test "toJsonString: __discard__ envelope" {    const alloc = std.testing.allocator;    const j = try toJsonString(alloc, Value{ .discard = {} });    defer alloc.free(j);    try std.testing.expectEqualStrings("{\"__discard__\":true}", j);}test "toJsonString: Observe record uses protocol mapping" {    const alloc = std.testing.allocator;    var arena = std.heap.ArenaAllocator.init(alloc);    defer arena.deinit();    const a = arena.allocator();    const label = Value{ .symbol = "Observe" };    const fields = [_]Value{        .{ .discard = {} },        .{ .boolean = true },        .{ .string = "extra" },    };    const rec = try any_constructors.record(a, label, &fields);    const j = try toJsonString(alloc, rec);    defer alloc.free(j);    try std.testing.expectEqualStrings(        "{\"type\":\"observe\",\"pattern\":{\"__discard__\":true},\"observer\":true,\"f2\":\"extra\"}",        j,    );}test "toJsonString: embedded values use decimal pointer strings" {    const alloc = std.testing.allocator;    var marker: u8 = 0;    const rendered = try toJsonString(alloc, .{        .embedded = .{ .value = &marker },    });    defer alloc.free(rendered);    const parsed = try std.json.parseFromSlice(std.json.Value, alloc, rendered, .{});    defer parsed.deinit();    const pointer = parsed.value.object.get("__embedded__").?.string;    var expected_buffer: [24]u8 = undefined;    const expected = try std.fmt.bufPrint(        &expected_buffer,        "{d}",        .{@intFromPtr(&marker)},    );    try std.testing.expectEqualStrings(expected, pointer);}test "toJsonString: unknown symbol-label record uses __record__ envelope" {    const alloc = std.testing.allocator;    var arena = std.heap.ArenaAllocator.init(alloc);    defer arena.deinit();    const a = arena.allocator();    const label = Value{ .symbol = "Weird" };    const fields = [_]Value{ Value{ .signed_integer = SignedInteger.fromI128(1) }, Value{ .signed_integer = SignedInteger.fromI128(2) } };    const rec = try any_constructors.record(a, label, &fields);    const j = try toJsonString(alloc, rec);    defer alloc.free(j);    try std.testing.expectEqualStrings(        "{\"__record__\":\"Weird\",\"fields\":[1,2]}",        j,    );}test "fromJsonString: primitives" {    const alloc = std.testing.allocator;    var b = try fromJsonString(alloc, "true");    defer b.deinit(alloc);    try std.testing.expect(b == .boolean and b.boolean == true);    var n = try fromJsonString(alloc, "null");    defer n.deinit(alloc);    try std.testing.expect(n == .symbol and std.mem.eql(u8, n.symbol, "null"));    var i = try fromJsonString(alloc, "42");    defer i.deinit(alloc);    try std.testing.expect(i == .signed_integer);    try std.testing.expectEqual(@as(i128, 42), try i.signed_integer.toI128());    var s = try fromJsonString(alloc, "\"hello\"");    defer s.deinit(alloc);    try std.testing.expect(s == .string);    try std.testing.expectEqualStrings("hello", s.string);}test "fromJsonString: __discard__ round-trip" {    const alloc = std.testing.allocator;    var v = try fromJsonString(alloc, "{\"__discard__\":true}");    defer v.deinit(alloc);    try std.testing.expect(v == .discard);}test "fromJsonString: protocol type lookup" {    const alloc = std.testing.allocator;    var v = try fromJsonString(alloc, "{\"type\":\"observe\",\"pattern\":{\"__discard__\":true},\"observer\":true}");    defer v.deinit(alloc);    try std.testing.expect(v == .record);    try std.testing.expect(v.record.label.* == .symbol);    try std.testing.expectEqualStrings("Observe", v.record.label.*.symbol);    try std.testing.expectEqual(@as(usize, 2), v.record.fields.len);    try std.testing.expect(v.record.fields[0] == .discard);    try std.testing.expect(v.record.fields[1] == .boolean);    try std.testing.expectEqual(true, v.record.fields[1].boolean);}test "fromJsonString: unknown type becomes tagged dict record" {    const alloc = std.testing.allocator;    var v = try fromJsonString(alloc, "{\"type\":\"custom\",\"k\":1}");    defer v.deinit(alloc);    try std.testing.expect(v == .record);    try std.testing.expectEqualStrings("custom", v.record.label.*.symbol);    try std.testing.expectEqual(@as(usize, 1), v.record.fields.len);    try std.testing.expect(v.record.fields[0] == .dictionary);    try std.testing.expectEqual(@as(usize, 1), v.record.fields[0].dictionary.len);    try std.testing.expectEqualStrings("k", v.record.fields[0].dictionary[0].key.string);}test "fromJsonString: array maps to sequence" {    const alloc = std.testing.allocator;    var v = try fromJsonString(alloc, "[1,2,3]");    defer v.deinit(alloc);    try std.testing.expect(v == .sequence);    try std.testing.expectEqual(@as(usize, 3), v.sequence.len);}test "fromJsonString: plain object becomes dictionary" {    const alloc = std.testing.allocator;    var v = try fromJsonString(alloc, "{\"a\":1,\"b\":2}");    defer v.deinit(alloc);    try std.testing.expect(v == .dictionary);    try std.testing.expectEqual(@as(usize, 2), v.dictionary.len);}test "fromJsonString: __record__ round-trip" {    const alloc = std.testing.allocator;    var v = try fromJsonString(alloc, "{\"__record__\":\"Weird\",\"fields\":[1,2]}");    defer v.deinit(alloc);    try std.testing.expect(v == .record);    try std.testing.expectEqualStrings("Weird", v.record.label.*.symbol);    try std.testing.expectEqual(@as(usize, 2), v.record.fields.len);}test "toJsonString + fromJsonString: sequence round-trip" {    const alloc = std.testing.allocator;    var arena = std.heap.ArenaAllocator.init(alloc);    defer arena.deinit();    const a = arena.allocator();    const items = try a.alloc(Value, 3);    items[0] = Value{ .boolean = true };    items[1] = Value{ .signed_integer = SignedInteger.fromI128(99) };    items[2] = Value{ .string = "x" };    const seq = Value{ .sequence = items };    const text = try toJsonString(alloc, seq);    defer alloc.free(text);    try std.testing.expectEqualStrings("[true,99,\"x\"]", text);    var parsed = try fromJsonString(alloc, text);    defer parsed.deinit(alloc);    try std.testing.expect(parsed == .sequence);    try std.testing.expect(parsed.sequence[0].boolean == true);    try std.testing.expectEqual(@as(i128, 99), try parsed.sequence[1].signed_integer.toI128());    try std.testing.expectEqualStrings("x", parsed.sequence[2].string);}test "toJsonString rejects duplicate set elements and dictionary keys" {    const alloc = std.testing.allocator;    var set_items = [_]Value{ Value.initI128(1), Value.initI128(1) };    var entries = [_]Value.DictionaryEntry{        .{ .key = Value.initI128(1), .value = Value.initBoolean(true) },        .{ .key = Value.initI128(1), .value = Value.initBoolean(false) },    };    try std.testing.expectError(        error.DuplicateSetElement,        toJsonString(alloc, Value.initSet(&set_items)),    );    try std.testing.expectError(        error.DuplicateDictionaryKey,        toJsonString(alloc, Value.initDictionary(&entries)),    );}fn checkJsonEncodingAllocationFailures(allocator: Allocator) !void {    var values = [_]Value{        .{ .string = "owned\"\n" },        .{ .byte_string = "bytes" },    };    var entries = [_]Value.DictionaryEntry{        .{            .key = Value.initI128(7),            .value = .{ .sequence = &values },        },    };    const rendered = try toJsonString(        allocator,        Value.initDictionary(&entries),    );    defer allocator.free(rendered);}test "toJsonString releases every allocation failure path" {    try std.testing.checkAllAllocationFailures(        std.testing.allocator,        checkJsonEncodingAllocationFailures,        .{},    );}test "fromJsonValue handles pre-parsed std.json.Value" {    const alloc = std.testing.allocator;    const parsed = try std.json.parseFromSlice(std.json.Value, alloc, "[true,false]", .{});    defer parsed.deinit();    var v = try fromJsonValue(alloc, parsed.value);    defer v.deinit(alloc);    try std.testing.expect(v == .sequence);    try std.testing.expectEqual(@as(usize, 2), v.sequence.len);    try std.testing.expectEqual(true, v.sequence[0].boolean);    try std.testing.expectEqual(false, v.sequence[1].boolean);}test "fromJsonString: __embedded__ payloads are owned and cloneable" {    const alloc = std.testing.allocator;    var v = try fromJsonString(alloc, "{\"__embedded__\":{\"name\":\"demo\"}}");    defer v.deinit(alloc);    try std.testing.expect(v == .embedded);    try std.testing.expect(v.embedded.semantic_ops == embedded_mod.parsedEmbeddedOps(Value));    try std.testing.expect(v.embedded.deinit_fn != null);    try std.testing.expect(v.embedded.clone_fn != null);    var cloned = try v.embedded.clone(alloc);    defer cloned.deinit(alloc);    try std.testing.expect(v.embedded.value != cloned.value);    const cloned_payload: *const Value = @ptrCast(@alignCast(cloned.value));    try std.testing.expect(cloned_payload.* == .dictionary);    try std.testing.expectEqual(@as(usize, 1), cloned_payload.dictionary.len);    try std.testing.expectEqualStrings("name", cloned_payload.dictionary[0].key.string);    try std.testing.expectEqualStrings("demo", cloned_payload.dictionary[0].value.string);}fn checkEmbeddedJsonAllocationFailures(allocator: Allocator) !void {    var value = try fromJsonString(        allocator,        "{\"__embedded__\":{\"name\":\"demo\"}}",    );    defer value.deinit(allocator);}test "fromJsonString embedded payload releases every allocation failure path" {    try std.testing.checkAllAllocationFailures(        std.testing.allocator,        checkEmbeddedJsonAllocationFailures,        .{},    );}test "$capture / $bind / $rest round-trip through JSON" {    const alloc = std.testing.allocator;    var arena = std.heap.ArenaAllocator.init(alloc);    defer arena.deinit();    const a = arena.allocator();    const cap = try any_constructors.capture(a, Value{ .discard = {} });    const cap_json = try toJsonString(alloc, cap);    defer alloc.free(cap_json);    try std.testing.expectEqualStrings("{\"$capture\":{\"__discard__\":true}}", cap_json);    const bnd = try any_constructors.bindVal(a, "x", Value{ .discard = {} });    const bnd_json = try toJsonString(alloc, bnd);    defer alloc.free(bnd_json);    try std.testing.expectEqualStrings("{\"$bind\":\"x\"}", bnd_json);    const rest = try any_constructors.restPattern(        a,        &.{ Value.initI128(1), .{ .string = "tail" } },        .{ .discard = {} },    );    const rest_json = try toJsonString(alloc, rest);    defer alloc.free(rest_json);    try std.testing.expectEqualStrings(        "{\"$rest\":{\"prefix\":[1,\"tail\"],\"rest\":{\"__discard__\":true}}}",        rest_json,    );    var parsed_cap = try fromJsonString(alloc, "{\"$capture\":{\"__discard__\":true}}");    defer parsed_cap.deinit(alloc);    try std.testing.expect(parsed_cap == .capture);    try std.testing.expect(parsed_cap.capture.* == .discard);    var parsed_bnd = try fromJsonString(alloc, "{\"$bind\":\"y\"}");    defer parsed_bnd.deinit(alloc);    try std.testing.expect(parsed_bnd == .bind);    try std.testing.expectEqualStrings("y", parsed_bnd.bind.name);    try std.testing.expect(parsed_bnd.bind.pattern.* == .discard);    var parsed_rest = try fromJsonString(alloc, rest_json);    defer parsed_rest.deinit(alloc);    try std.testing.expect(parsed_rest == .rest_pattern);    try std.testing.expectEqual(@as(usize, 2), parsed_rest.rest_pattern.prefix.len);    try std.testing.expect(parsed_rest.rest_pattern.rest.* == .discard);}

Source: lib/preserves/src/root.zig:125

zig
pub const json = @import("json.zig");

Audit

Definitions3
Public names3
Members0
Version26.7.0
Revisiondaab053ee433