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.
fromJsonString: Parsesjson_textand returns the Preserves value it describes, allocated withalloc, for code that receives JSON text.fromJsonValue: Returns the Preserves value thatjson_valuedescribes, allocated withalloc, for code that already holds a parsedstd.json.Value, so the JSON is parsed once.toJsonString: Returnsvalueas minified JSON text, allocated withalloc, for code that logs or sends a value to a JSON consumer.
Types and contracts
Public types and contracts.
AnyEmbedded: An embedded value that points to a payload of the host program, with optional functions to compare, hash, free and copy it.EncodeError: The errorstoJsonStringreturns, for a caller switching on these errors.ParseError: The errorsparsereturns: one tag per kind of malformed text, plus running out of nesting depth or memory.SignedInteger: A signed integer of any size, stored in the smallest of three representations that holds it.Value: The value type this file reads and writes: Preserves values whose embedded values hold any pointer (AnyEmbedded), for code that builds values fortoJsonStringor reads whatfromJsonStringreturns.
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
| Definitions | 3 |
|---|---|
| Public names | 3 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |