lib/preserves/src/json.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 //! Converts the package's values to JSON text, and JSON text back to values. A caller exchanging
2 //! values with JSON tools needs a JSON form for records, patterns and embedded values too, and
3 //! needs to know which details a round trip loses.
4 //!
5 //! JSON has objects, arrays, strings, numbers, booleans and null only. It has no records, symbols,
6 //! byte strings, sets or patterns. The standard library's JSON reader holds each number as a 64-bit
7 //! integer or a double. The package keeps the JSON representation of the
8 //! [Preserves](https://preserves.dev/) data language, alongside its text and binary syntaxes.
9 //!
10 //! Each value JSON lacks becomes a JSON object whose reserved member names it: `__record__`,
11 //! `__discard__`, `__embedded__`, `$capture`, `$bind` and `$rest`. A record labeled with one of the
12 //! ten labels in the file's protocol table becomes an object with a `"type"` member and named
13 //! fields, and the object decodes back to that record. `Observe` and `Synced` are two of those ten
14 //! labels. The mapping loses detail: symbols, byte strings and sets come back as strings and
15 //! sequences, integers outside 64 bits change, and non-finite doubles become null. The decoder
16 //! copies every string it keeps, so the decoded value owns all its memory and frees with `deinit`.
17 const std = @import("std");
18 const Allocator = std.mem.Allocator;
19 const pretty = @import("pretty");
20
21 const value_mod = @import("value.zig");
22 const integer_mod = @import("integer.zig");
23 const embedded_mod = @import("embedded.zig");
24 const symbols_mod = @import("symbols.zig");
25 const constructors_mod = @import("constructors.zig");
26 const predicates_mod = @import("predicates.zig");
27 const text_format = @import("text/root.zig");
28 const parse_error_mod = @import("error.zig");
29
30 pub const AnyEmbedded = embedded_mod.AnyEmbedded;
31 /// The value type this file reads and writes: Preserves values whose embedded values hold any
32 /// pointer (`AnyEmbedded`), for code that builds values for `toJsonString` or reads what
33 /// `fromJsonString` returns. The type is the same type as `Value(AnyEmbedded)` from the package
34 /// root.
35 pub const Value = value_mod.Value(AnyEmbedded);
36 pub const SignedInteger = integer_mod.SignedInteger;
37 pub const ParseError = parse_error_mod.ParseError;
38 /// The errors `toJsonString` returns, for a caller switching on these errors. The error set covers
39 /// running out of memory, and a set or dictionary anywhere in the value that holds two equal
40 /// elements or keys.
41 pub const EncodeError = Allocator.Error || error{
42 DuplicateSetElement,
43 DuplicateDictionaryKey,
44 };
45
46 const JsonWriter = pretty.json.Writer;
47 const WriteError = EncodeError || std.Io.Writer.Error;
48 const any_constructors = constructors_mod.any_constructors;
49
50 const ProtocolEntry = struct {
51 symbol_name: []const u8,
52 json_type: []const u8,
53 fields: []const []const u8,
54 };
55
56 const protocol_records = [_]ProtocolEntry{
57 .{ .symbol_name = "Observe", .json_type = "observe", .fields = &.{ "pattern", "observer" } },
58 .{ .symbol_name = "ReactorError", .json_type = "reactor.error", .fields = &.{ "stage", "facet", "reactor", "error" } },
59 .{ .symbol_name = "EntityRuntime", .json_type = "entity.runtime", .fields = &.{ "kind", "observe", "during" } },
60 .{ .symbol_name = "RequireService", .json_type = "require.service", .fields = &.{"name"} },
61 .{ .symbol_name = "RunService", .json_type = "run.service", .fields = &.{"name"} },
62 .{ .symbol_name = "ServiceState", .json_type = "service.state", .fields = &.{ "name", "state" } },
63 .{ .symbol_name = "ServiceObject", .json_type = "service.object", .fields = &.{ "name", "object" } },
64 .{ .symbol_name = "ServiceDependency", .json_type = "service.dependency", .fields = &.{ "depender", "dependee" } },
65 .{ .symbol_name = "RestartService", .json_type = "restart.service", .fields = &.{"name"} },
66 .{ .symbol_name = "Synced", .json_type = "syndicate.sync.synced", .fields = &.{} },
67 };
68
69 fn lookupProtocol(symbol_name: []const u8) ?ProtocolEntry {
70 for (&protocol_records) |entry| {
71 if (std.mem.eql(u8, entry.symbol_name, symbol_name)) return entry;
72 }
73 return null;
74 }
75
76 fn lookupProtocolByJsonType(json_type: []const u8) ?ProtocolEntry {
77 for (&protocol_records) |entry| {
78 if (std.mem.eql(u8, entry.json_type, json_type)) return entry;
79 }
80 return null;
81 }
82
83 /// Returns `value` as minified JSON text, allocated with `alloc`, for code that logs or sends a
84 /// value to a JSON consumer. The caller owns the returned bytes and frees them with `alloc`.
85 /// Booleans, strings and sequences become their JSON counterparts, and sets become arrays. The
86 /// symbol `null` becomes JSON null, and every other symbol becomes a JSON string. A byte string
87 /// becomes a string of lowercase hexadecimal digits. If a double is non-finite, it becomes null. An
88 /// integer too wide for 128 bits becomes a 64-bit number that differs from it. A dictionary becomes
89 /// an object: string and symbol keys become member names, and other keys are written in the text
90 /// syntax. A record whose label is a protocol name becomes `{"type": …}` with named fields, and
91 /// each field past the named ones takes the name `f` plus its index. If a record has a symbol label
92 /// and either zero fields or one dictionary field with string keys, the record becomes
93 /// `{"type": label}` plus the dictionary's entries. Any other record becomes
94 /// `{"__record__": label, "fields": […]}`. The discard pattern becomes `{"__discard__": true}`, and
95 /// captures, binds and rest patterns become `$capture`, `$bind` and `$rest` objects. An embedded
96 /// value becomes `{"__embedded__": "…"}` holding its pointer as a decimal string. The call frees
97 /// what it wrote so far on any error.
98 pub fn toJsonString(alloc: Allocator, value: Value) EncodeError![]const u8 {
99 var out: std.Io.Writer.Allocating = .init(alloc);
100 errdefer out.deinit();
101 var json = JsonWriter.init(&out.writer, .minified);
102 writeJson(alloc, &json, value) catch |err| switch (err) {
103 error.WriteFailed => return error.OutOfMemory,
104 else => return @errorCast(err),
105 };
106 return try out.toOwnedSlice();
107 }
108
109 fn writeJson(alloc: Allocator, json: *JsonWriter, value: Value) WriteError!void {
110 switch (value) {
111 .discard => {
112 const object = try json.object();
113 try object.field("__discard__", true);
114 try object.end();
115 },
116 .capture => |inner| {
117 const object = try json.object();
118 try object.writer.objectField("$capture");
119 try writeJson(alloc, object.writer, inner.*);
120 try object.end();
121 },
122 .bind => |binding| {
123 const object = try json.object();
124 try object.field("$bind", binding.name);
125 if (binding.pattern.* != .discard) {
126 try object.writer.objectField("pattern");
127 try writeJson(alloc, object.writer, binding.pattern.*);
128 }
129 try object.end();
130 },
131 .rest_pattern => |rest_pattern| {
132 const object = try json.object();
133 const rest = try object.object("$rest");
134 const prefix = try rest.array("prefix");
135 for (rest_pattern.prefix) |item| {
136 try writeJson(alloc, prefix.writer, item);
137 }
138 try prefix.end();
139 try rest.writer.objectField("rest");
140 try writeJson(alloc, rest.writer, rest_pattern.rest.*);
141 try rest.end();
142 try object.end();
143 },
144 .boolean => |actual| try json.write(actual),
145 .signed_integer => |actual| try writeSignedInteger(json, actual),
146 .double => |actual| try writeDouble(json, actual),
147 .string => |actual| try json.write(actual),
148 .byte_string => |actual| try json.hexString(actual),
149 .symbol => |name| {
150 if (std.mem.eql(u8, name, "null")) {
151 try json.write(null);
152 } else {
153 try json.write(name);
154 }
155 },
156 .record => |record| try writeRecord(alloc, json, record),
157 .sequence => |items| try writeArray(alloc, json, items),
158 .set => |items| {
159 if (!Value.setElementsDistinct(items)) return error.DuplicateSetElement;
160 try writeArray(alloc, json, items);
161 },
162 .dictionary => |entries| {
163 if (!Value.dictionaryKeysDistinct(entries)) {
164 return error.DuplicateDictionaryKey;
165 }
166 try writeDictionary(alloc, json, entries);
167 },
168 .embedded => |embedded| {
169 const object = try json.object();
170 var tmp: [24]u8 = undefined;
171 const pointer = std.fmt.bufPrint(
172 &tmp,
173 "{d}",
174 .{@intFromPtr(embedded.value)},
175 ) catch unreachable;
176 try object.field("__embedded__", pointer);
177 try object.end();
178 },
179 }
180 }
181
182 fn writeRecord(alloc: Allocator, json: *JsonWriter, record: Value.Record) WriteError!void {
183 const label_symbol: ?[]const u8 = switch (record.label.*) {
184 .symbol => |symbol| symbol,
185 else => null,
186 };
187
188 if (label_symbol) |label_name| {
189 if (lookupProtocol(label_name)) |protocol| {
190 const object = try json.object();
191 try object.field("type", protocol.json_type);
192 for (record.fields, 0..) |field, index| {
193 if (index < protocol.fields.len) {
194 try object.writer.objectField(protocol.fields[index]);
195 } else {
196 var tmp: [16]u8 = undefined;
197 const field_name = std.fmt.bufPrint(
198 &tmp,
199 "f{d}",
200 .{index},
201 ) catch unreachable;
202 try object.writer.objectField(field_name);
203 }
204 try writeJson(alloc, object.writer, field);
205 }
206 try object.end();
207 return;
208 }
209 if (predicates_mod.recordAttributes(AnyEmbedded, .{ .record = record })) |attributes| {
210 const object = try json.object();
211 try object.field("type", label_name);
212 for (attributes) |entry| {
213 try object.writer.objectField(entry.key.string);
214 try writeJson(alloc, object.writer, entry.value);
215 }
216 try object.end();
217 return;
218 }
219 }
220
221 const object = try json.object();
222 try object.writer.objectField("__record__");
223 try writeJson(alloc, object.writer, record.label.*);
224 const fields = try object.array("fields");
225 for (record.fields) |field| {
226 try writeJson(alloc, fields.writer, field);
227 }
228 try fields.end();
229 try object.end();
230 }
231
232 fn writeArray(alloc: Allocator, json: *JsonWriter, items: []const Value) WriteError!void {
233 const array = try json.array();
234 for (items) |item| {
235 try writeJson(alloc, array.writer, item);
236 }
237 try array.end();
238 }
239
240 fn writeDictionary(
241 alloc: Allocator,
242 json: *JsonWriter,
243 entries: []const Value.DictionaryEntry,
244 ) WriteError!void {
245 const object = try json.object();
246 for (entries) |entry| {
247 switch (entry.key) {
248 .string => |key| {
249 try object.writer.objectField(key);
250 try writeJson(alloc, object.writer, entry.value);
251 },
252 .symbol => |key| {
253 try object.writer.objectField(key);
254 try writeJson(alloc, object.writer, entry.value);
255 },
256 else => {
257 const key = text_format.toText(alloc, entry.key) catch |err| switch (err) {
258 error.OutOfMemory => return error.OutOfMemory,
259 error.DuplicateSetElement => return error.DuplicateSetElement,
260 error.DuplicateDictionaryKey => return error.DuplicateDictionaryKey,
261 };
262 defer alloc.free(key);
263 try object.writer.objectField(key);
264 try writeJson(alloc, object.writer, entry.value);
265 },
266 }
267 }
268 try object.end();
269 }
270
271 fn writeSignedInteger(json: *JsonWriter, value: SignedInteger) std.Io.Writer.Error!void {
272 switch (value.repr) {
273 .i128 => |actual| try json.print("{d}", .{actual}),
274 .u128 => |actual| try json.print("{d}", .{actual}),
275 .big => try json.print("{d}", .{value.toI64Lossy()}),
276 }
277 }
278
279 fn writeDouble(json: *JsonWriter, value: f64) std.Io.Writer.Error!void {
280 if (std.math.isNan(value) or std.math.isInf(value)) {
281 try json.write(null);
282 return;
283 }
284 try json.print("{d}", .{value});
285 }
286
287 /// Parses `json_text` and returns the Preserves value it describes, allocated with `alloc`, for
288 /// code that receives JSON text. The value owns all its memory, and the caller frees it with
289 /// `deinit`. The mapping is the one `fromJsonValue` applies to the parsed JSON. Malformed JSON
290 /// returns `error.OutOfMemory`, the only error the call reports. If memory runs out inside a
291 /// `__record__`, `$rest`, `$capture`, `$bind` or `"type"` object, the call leaks the parts already
292 /// built.
293 pub fn fromJsonString(alloc: Allocator, json_text: []const u8) !Value {
294 const parsed = std.json.parseFromSlice(std.json.Value, alloc, json_text, .{}) catch return error.OutOfMemory;
295 defer parsed.deinit();
296 return jsonToValue(alloc, parsed.value);
297 }
298
299 /// Returns the Preserves value that `json_value` describes, allocated with `alloc`, for code that
300 /// already holds a parsed `std.json.Value`, so the JSON is parsed once. The result copies every
301 /// string it keeps, so it owns all its memory and borrows nothing from `json_value`. JSON null
302 /// becomes the symbol `null`, an integer that fits 64 bits becomes an integer, any other finite
303 /// number becomes a double, and a number outside those ranges becomes the symbol `NaN`. Strings
304 /// stay strings, and arrays become sequences. The reserved member names are tried in order:
305 /// `__discard__`, `__record__`, `__embedded__`, `$rest`, `$capture`, `$bind`, then `"type"`. Any
306 /// other object becomes a dictionary with string keys. A `"type"` naming a protocol record becomes
307 /// that record, and each missing field becomes the discard pattern. Any other `"type"` becomes a
308 /// record labeled with that name and one dictionary field holding the other members. A `__record__`
309 /// string label becomes a symbol label. An `__embedded__` member becomes an embedded value that
310 /// owns the decoded member, and copying or freeing the outer value copies or frees it. Decoding the
311 /// output of `toJsonString` for an embedded value yields an embedded value that holds the pointer's
312 /// decimal string.
313 pub fn fromJsonValue(alloc: Allocator, json_value: std.json.Value) !Value {
314 return jsonToValue(alloc, json_value);
315 }
316
317 fn jsonToValue(alloc: Allocator, jv: std.json.Value) Allocator.Error!Value {
318 return switch (jv) {
319 .null => Value{ .symbol = try alloc.dupe(u8, symbols_mod.SYM_NULL.name) },
320 .bool => |v| Value{ .boolean = v },
321 .integer => |v| Value{ .signed_integer = SignedInteger.fromI128(@as(i128, v)) },
322 .float => |v| Value{ .double = v },
323 .string => |s| blk: {
324 const dup = try alloc.dupe(u8, s);
325 break :blk Value{ .string = dup };
326 },
327 .array => |arr| blk: {
328 const items = try alloc.alloc(Value, arr.items.len);
329 var built: usize = 0;
330 errdefer {
331 for (items[0..built]) |*item| item.deinit(alloc);
332 alloc.free(items);
333 }
334 for (arr.items, 0..) |item, i| {
335 items[i] = try jsonToValue(alloc, item);
336 built = i + 1;
337 }
338 break :blk Value{ .sequence = items };
339 },
340 .object => |obj| try objectToValue(alloc, obj),
341 .number_string => Value{ .symbol = try alloc.dupe(u8, "NaN") },
342 };
343 }
344
345 fn objectToValue(alloc: Allocator, obj: std.json.ObjectMap) Allocator.Error!Value {
346 if (obj.get("__discard__")) |_| {
347 return Value{ .discard = {} };
348 }
349 if (obj.get("__record__")) |label_json| {
350 const label_val = try jsonToValue(alloc, label_json);
351 const label = switch (label_val) {
352 .string => |s| Value{ .symbol = s },
353 else => label_val,
354 };
355 const fields_json = obj.get("fields") orelse {
356 return any_constructors.record(alloc, label, &.{}) catch |err| switch (err) {
357 error.OutOfMemory => return error.OutOfMemory,
358 };
359 };
360 switch (fields_json) {
361 .array => |arr| {
362 const fields = try alloc.alloc(Value, arr.items.len);
363 defer alloc.free(fields);
364 for (arr.items, 0..) |item, i| {
365 fields[i] = try jsonToValue(alloc, item);
366 }
367 return any_constructors.record(alloc, label, fields) catch |err| switch (err) {
368 error.OutOfMemory => return error.OutOfMemory,
369 };
370 },
371 else => return any_constructors.record(alloc, label, &.{}) catch |err| switch (err) {
372 error.OutOfMemory => return error.OutOfMemory,
373 },
374 }
375 }
376 if (obj.get("__embedded__")) |inner_json| {
377 var inner = try jsonToValue(alloc, inner_json);
378 errdefer inner.deinit(alloc);
379 const ptr = try alloc.create(Value);
380 ptr.* = inner;
381 return Value{ .embedded = AnyEmbedded{
382 .value = @ptrCast(ptr),
383 .semantic_ops = embedded_mod.parsedEmbeddedOps(Value),
384 .deinit_fn = embedded_mod.parsedEmbeddedDeinit(Value),
385 .clone_fn = embedded_mod.parsedEmbeddedClone(Value),
386 } };
387 }
388 if (obj.get("$rest")) |rest_json| {
389 switch (rest_json) {
390 .object => |rest_obj| {
391 if (rest_obj.get("prefix")) |prefix_json| {
392 if (rest_obj.get("rest")) |rest_val_json| {
393 switch (prefix_json) {
394 .array => |arr| {
395 const prefix = try alloc.alloc(Value, arr.items.len);
396 errdefer alloc.free(prefix);
397 for (arr.items, 0..) |item, i| {
398 prefix[i] = try jsonToValue(alloc, item);
399 }
400 const rest_val = try jsonToValue(alloc, rest_val_json);
401 const rest_ptr = try alloc.create(Value);
402 rest_ptr.* = rest_val;
403 return Value{ .rest_pattern = .{ .prefix = prefix, .rest = rest_ptr } };
404 },
405 else => {},
406 }
407 }
408 }
409 },
410 else => {},
411 }
412 }
413 if (obj.get("$capture")) |cap_json| {
414 const inner = try jsonToValue(alloc, cap_json);
415 return any_constructors.capture(alloc, inner) catch |err| switch (err) {
416 error.OutOfMemory => return error.OutOfMemory,
417 };
418 }
419 if (obj.get("$bind")) |bind_json| {
420 switch (bind_json) {
421 .string => |name| {
422 const name_dup = try alloc.dupe(u8, name);
423 const pat: Value = if (obj.get("pattern")) |pj|
424 try jsonToValue(alloc, pj)
425 else
426 Value{ .discard = {} };
427 return any_constructors.bindVal(alloc, name_dup, pat) catch |err| switch (err) {
428 error.OutOfMemory => return error.OutOfMemory,
429 };
430 },
431 else => {},
432 }
433 }
434 if (obj.get("type")) |type_json| {
435 switch (type_json) {
436 .string => |type_name| {
437 if (lookupProtocolByJsonType(type_name)) |proto| {
438 const fields = try alloc.alloc(Value, proto.fields.len);
439 defer alloc.free(fields);
440 for (proto.fields, 0..) |fname, i| {
441 if (obj.get(fname)) |fv| {
442 fields[i] = try jsonToValue(alloc, fv);
443 } else {
444 fields[i] = Value{ .discard = {} };
445 }
446 }
447 const label_name = try alloc.dupe(u8, proto.symbol_name);
448 return any_constructors.record(alloc, Value{ .symbol = label_name }, fields) catch |err| switch (err) {
449 error.OutOfMemory => return error.OutOfMemory,
450 };
451 }
452 const attr_count = if (obj.count() > 0) obj.count() - 1 else 0;
453 const entries = try alloc.alloc(Value.DictionaryEntry, attr_count);
454 errdefer alloc.free(entries);
455 var iter = obj.iterator();
456 var i: usize = 0;
457 while (iter.next()) |entry| {
458 if (std.mem.eql(u8, entry.key_ptr.*, "type")) continue;
459 const key_str = try alloc.dupe(u8, entry.key_ptr.*);
460 entries[i] = .{
461 .key = Value{ .string = key_str },
462 .value = try jsonToValue(alloc, entry.value_ptr.*),
463 };
464 i += 1;
465 }
466 const tagged = Value{ .dictionary = entries };
467 const label_name = try alloc.dupe(u8, type_name);
468 const fields = [_]Value{tagged};
469 return any_constructors.record(alloc, Value{ .symbol = label_name }, &fields) catch |err| switch (err) {
470 error.OutOfMemory => return error.OutOfMemory,
471 };
472 },
473 else => {},
474 }
475 }
476 const entries = try alloc.alloc(Value.DictionaryEntry, obj.count());
477 var built: usize = 0;
478 errdefer {
479 for (entries[0..built]) |*done| {
480 done.key.deinit(alloc);
481 done.value.deinit(alloc);
482 }
483 alloc.free(entries);
484 }
485 var iter = obj.iterator();
486 var i: usize = 0;
487 while (iter.next()) |entry| {
488 const key_str = try alloc.dupe(u8, entry.key_ptr.*);
489 errdefer alloc.free(key_str);
490 entries[i] = .{
491 .key = Value{ .string = key_str },
492 .value = try jsonToValue(alloc, entry.value_ptr.*),
493 };
494 i += 1;
495 built = i;
496 }
497 return Value{ .dictionary = entries };
498 }
499
500 test "toJsonString: primitives" {
501 const alloc = std.testing.allocator;
502
503 const b = try toJsonString(alloc, Value{ .boolean = true });
504 defer alloc.free(b);
505 try std.testing.expectEqualStrings("true", b);
506
507 const i = try toJsonString(alloc, Value{ .signed_integer = SignedInteger.fromI128(-42) });
508 defer alloc.free(i);
509 try std.testing.expectEqualStrings("-42", i);
510
511 const s = try toJsonString(alloc, Value{ .string = "hi" });
512 defer alloc.free(s);
513 try std.testing.expectEqualStrings("\"hi\"", s);
514
515 const n = try toJsonString(alloc, Value{ .symbol = "null" });
516 defer alloc.free(n);
517 try std.testing.expectEqualStrings("null", n);
518 }
519
520 test "toJsonString: NaN and Inf round to null" {
521 const alloc = std.testing.allocator;
522 const nan = try toJsonString(alloc, Value{ .double = std.math.nan(f64) });
523 defer alloc.free(nan);
524 try std.testing.expectEqualStrings("null", nan);
525
526 const inf = try toJsonString(alloc, Value{ .double = std.math.inf(f64) });
527 defer alloc.free(inf);
528 try std.testing.expectEqualStrings("null", inf);
529 }
530
531 test "toJsonString: byte_string as hex-packed string" {
532 const alloc = std.testing.allocator;
533 const bytes = [_]u8{ 0xde, 0xad, 0xbe, 0xef };
534 const j = try toJsonString(alloc, Value{ .byte_string = &bytes });
535 defer alloc.free(j);
536 try std.testing.expectEqualStrings("\"deadbeef\"", j);
537 }
538
539 test "toJsonString: nested dictionaries preserve wide values and escaping" {
540 const alloc = std.testing.allocator;
541 const bytes = [_]u8{ 0x00, 0xff };
542 var sequence = [_]Value{
543 .{ .byte_string = &bytes },
544 .{ .signed_integer = SignedInteger.fromU128(std.math.maxInt(u128)) },
545 .{ .double = 1.5 },
546 };
547 var set = [_]Value{
548 .{ .boolean = false },
549 .{ .boolean = true },
550 };
551 var entries = [_]Value.DictionaryEntry{
552 .{
553 .key = .{ .string = "nested\"\n\x01" },
554 .value = .{ .sequence = &sequence },
555 },
556 .{
557 .key = Value.initI128(7),
558 .value = .{ .set = &set },
559 },
560 };
561
562 const rendered = try toJsonString(alloc, Value.initDictionary(&entries));
563 defer alloc.free(rendered);
564 try std.testing.expectEqualStrings(
565 "{\"nested\\\"\\n\\u0001\":[\"00ff\",340282366920938463463374607431768211455,1.5],\"7\":[false,true]}",
566 rendered,
567 );
568 }
569
570 test "toJsonString: __discard__ envelope" {
571 const alloc = std.testing.allocator;
572 const j = try toJsonString(alloc, Value{ .discard = {} });
573 defer alloc.free(j);
574 try std.testing.expectEqualStrings("{\"__discard__\":true}", j);
575 }
576
577 test "toJsonString: Observe record uses protocol mapping" {
578 const alloc = std.testing.allocator;
579 var arena = std.heap.ArenaAllocator.init(alloc);
580 defer arena.deinit();
581 const a = arena.allocator();
582
583 const label = Value{ .symbol = "Observe" };
584 const fields = [_]Value{
585 .{ .discard = {} },
586 .{ .boolean = true },
587 .{ .string = "extra" },
588 };
589 const rec = try any_constructors.record(a, label, &fields);
590 const j = try toJsonString(alloc, rec);
591 defer alloc.free(j);
592 try std.testing.expectEqualStrings(
593 "{\"type\":\"observe\",\"pattern\":{\"__discard__\":true},\"observer\":true,\"f2\":\"extra\"}",
594 j,
595 );
596 }
597
598 test "toJsonString: embedded values use decimal pointer strings" {
599 const alloc = std.testing.allocator;
600 var marker: u8 = 0;
601 const rendered = try toJsonString(alloc, .{
602 .embedded = .{ .value = &marker },
603 });
604 defer alloc.free(rendered);
605
606 const parsed = try std.json.parseFromSlice(std.json.Value, alloc, rendered, .{});
607 defer parsed.deinit();
608 const pointer = parsed.value.object.get("__embedded__").?.string;
609 var expected_buffer: [24]u8 = undefined;
610 const expected = try std.fmt.bufPrint(
611 &expected_buffer,
612 "{d}",
613 .{@intFromPtr(&marker)},
614 );
615 try std.testing.expectEqualStrings(expected, pointer);
616 }
617
618 test "toJsonString: unknown symbol-label record uses __record__ envelope" {
619 const alloc = std.testing.allocator;
620 var arena = std.heap.ArenaAllocator.init(alloc);
621 defer arena.deinit();
622 const a = arena.allocator();
623
624 const label = Value{ .symbol = "Weird" };
625 const fields = [_]Value{ Value{ .signed_integer = SignedInteger.fromI128(1) }, Value{ .signed_integer = SignedInteger.fromI128(2) } };
626 const rec = try any_constructors.record(a, label, &fields);
627 const j = try toJsonString(alloc, rec);
628 defer alloc.free(j);
629 try std.testing.expectEqualStrings(
630 "{\"__record__\":\"Weird\",\"fields\":[1,2]}",
631 j,
632 );
633 }
634
635 test "fromJsonString: primitives" {
636 const alloc = std.testing.allocator;
637
638 var b = try fromJsonString(alloc, "true");
639 defer b.deinit(alloc);
640 try std.testing.expect(b == .boolean and b.boolean == true);
641
642 var n = try fromJsonString(alloc, "null");
643 defer n.deinit(alloc);
644 try std.testing.expect(n == .symbol and std.mem.eql(u8, n.symbol, "null"));
645
646 var i = try fromJsonString(alloc, "42");
647 defer i.deinit(alloc);
648 try std.testing.expect(i == .signed_integer);
649 try std.testing.expectEqual(@as(i128, 42), try i.signed_integer.toI128());
650
651 var s = try fromJsonString(alloc, "\"hello\"");
652 defer s.deinit(alloc);
653 try std.testing.expect(s == .string);
654 try std.testing.expectEqualStrings("hello", s.string);
655 }
656
657 test "fromJsonString: __discard__ round-trip" {
658 const alloc = std.testing.allocator;
659 var v = try fromJsonString(alloc, "{\"__discard__\":true}");
660 defer v.deinit(alloc);
661 try std.testing.expect(v == .discard);
662 }
663
664 test "fromJsonString: protocol type lookup" {
665 const alloc = std.testing.allocator;
666 var v = try fromJsonString(alloc, "{\"type\":\"observe\",\"pattern\":{\"__discard__\":true},\"observer\":true}");
667 defer v.deinit(alloc);
668 try std.testing.expect(v == .record);
669 try std.testing.expect(v.record.label.* == .symbol);
670 try std.testing.expectEqualStrings("Observe", v.record.label.*.symbol);
671 try std.testing.expectEqual(@as(usize, 2), v.record.fields.len);
672 try std.testing.expect(v.record.fields[0] == .discard);
673 try std.testing.expect(v.record.fields[1] == .boolean);
674 try std.testing.expectEqual(true, v.record.fields[1].boolean);
675 }
676
677 test "fromJsonString: unknown type becomes tagged dict record" {
678 const alloc = std.testing.allocator;
679 var v = try fromJsonString(alloc, "{\"type\":\"custom\",\"k\":1}");
680 defer v.deinit(alloc);
681 try std.testing.expect(v == .record);
682 try std.testing.expectEqualStrings("custom", v.record.label.*.symbol);
683 try std.testing.expectEqual(@as(usize, 1), v.record.fields.len);
684 try std.testing.expect(v.record.fields[0] == .dictionary);
685 try std.testing.expectEqual(@as(usize, 1), v.record.fields[0].dictionary.len);
686 try std.testing.expectEqualStrings("k", v.record.fields[0].dictionary[0].key.string);
687 }
688
689 test "fromJsonString: array maps to sequence" {
690 const alloc = std.testing.allocator;
691 var v = try fromJsonString(alloc, "[1,2,3]");
692 defer v.deinit(alloc);
693 try std.testing.expect(v == .sequence);
694 try std.testing.expectEqual(@as(usize, 3), v.sequence.len);
695 }
696
697 test "fromJsonString: plain object becomes dictionary" {
698 const alloc = std.testing.allocator;
699 var v = try fromJsonString(alloc, "{\"a\":1,\"b\":2}");
700 defer v.deinit(alloc);
701 try std.testing.expect(v == .dictionary);
702 try std.testing.expectEqual(@as(usize, 2), v.dictionary.len);
703 }
704
705 test "fromJsonString: __record__ round-trip" {
706 const alloc = std.testing.allocator;
707 var v = try fromJsonString(alloc, "{\"__record__\":\"Weird\",\"fields\":[1,2]}");
708 defer v.deinit(alloc);
709 try std.testing.expect(v == .record);
710 try std.testing.expectEqualStrings("Weird", v.record.label.*.symbol);
711 try std.testing.expectEqual(@as(usize, 2), v.record.fields.len);
712 }
713
714 test "toJsonString + fromJsonString: sequence round-trip" {
715 const alloc = std.testing.allocator;
716 var arena = std.heap.ArenaAllocator.init(alloc);
717 defer arena.deinit();
718 const a = arena.allocator();
719
720 const items = try a.alloc(Value, 3);
721 items[0] = Value{ .boolean = true };
722 items[1] = Value{ .signed_integer = SignedInteger.fromI128(99) };
723 items[2] = Value{ .string = "x" };
724 const seq = Value{ .sequence = items };
725
726 const text = try toJsonString(alloc, seq);
727 defer alloc.free(text);
728 try std.testing.expectEqualStrings("[true,99,\"x\"]", text);
729
730 var parsed = try fromJsonString(alloc, text);
731 defer parsed.deinit(alloc);
732 try std.testing.expect(parsed == .sequence);
733 try std.testing.expect(parsed.sequence[0].boolean == true);
734 try std.testing.expectEqual(@as(i128, 99), try parsed.sequence[1].signed_integer.toI128());
735 try std.testing.expectEqualStrings("x", parsed.sequence[2].string);
736 }
737
738 test "toJsonString rejects duplicate set elements and dictionary keys" {
739 const alloc = std.testing.allocator;
740 var set_items = [_]Value{ Value.initI128(1), Value.initI128(1) };
741 var entries = [_]Value.DictionaryEntry{
742 .{ .key = Value.initI128(1), .value = Value.initBoolean(true) },
743 .{ .key = Value.initI128(1), .value = Value.initBoolean(false) },
744 };
745
746 try std.testing.expectError(
747 error.DuplicateSetElement,
748 toJsonString(alloc, Value.initSet(&set_items)),
749 );
750 try std.testing.expectError(
751 error.DuplicateDictionaryKey,
752 toJsonString(alloc, Value.initDictionary(&entries)),
753 );
754 }
755
756 fn checkJsonEncodingAllocationFailures(allocator: Allocator) !void {
757 var values = [_]Value{
758 .{ .string = "owned\"\n" },
759 .{ .byte_string = "bytes" },
760 };
761 var entries = [_]Value.DictionaryEntry{
762 .{
763 .key = Value.initI128(7),
764 .value = .{ .sequence = &values },
765 },
766 };
767 const rendered = try toJsonString(
768 allocator,
769 Value.initDictionary(&entries),
770 );
771 defer allocator.free(rendered);
772 }
773
774 test "toJsonString releases every allocation failure path" {
775 try std.testing.checkAllAllocationFailures(
776 std.testing.allocator,
777 checkJsonEncodingAllocationFailures,
778 .{},
779 );
780 }
781
782 test "fromJsonValue handles pre-parsed std.json.Value" {
783 const alloc = std.testing.allocator;
784 const parsed = try std.json.parseFromSlice(std.json.Value, alloc, "[true,false]", .{});
785 defer parsed.deinit();
786 var v = try fromJsonValue(alloc, parsed.value);
787 defer v.deinit(alloc);
788 try std.testing.expect(v == .sequence);
789 try std.testing.expectEqual(@as(usize, 2), v.sequence.len);
790 try std.testing.expectEqual(true, v.sequence[0].boolean);
791 try std.testing.expectEqual(false, v.sequence[1].boolean);
792 }
793
794 test "fromJsonString: __embedded__ payloads are owned and cloneable" {
795 const alloc = std.testing.allocator;
796
797 var v = try fromJsonString(alloc, "{\"__embedded__\":{\"name\":\"demo\"}}");
798 defer v.deinit(alloc);
799
800 try std.testing.expect(v == .embedded);
801 try std.testing.expect(v.embedded.semantic_ops == embedded_mod.parsedEmbeddedOps(Value));
802 try std.testing.expect(v.embedded.deinit_fn != null);
803 try std.testing.expect(v.embedded.clone_fn != null);
804
805 var cloned = try v.embedded.clone(alloc);
806 defer cloned.deinit(alloc);
807
808 try std.testing.expect(v.embedded.value != cloned.value);
809 const cloned_payload: *const Value = @ptrCast(@alignCast(cloned.value));
810 try std.testing.expect(cloned_payload.* == .dictionary);
811 try std.testing.expectEqual(@as(usize, 1), cloned_payload.dictionary.len);
812 try std.testing.expectEqualStrings("name", cloned_payload.dictionary[0].key.string);
813 try std.testing.expectEqualStrings("demo", cloned_payload.dictionary[0].value.string);
814 }
815
816 fn checkEmbeddedJsonAllocationFailures(allocator: Allocator) !void {
817 var value = try fromJsonString(
818 allocator,
819 "{\"__embedded__\":{\"name\":\"demo\"}}",
820 );
821 defer value.deinit(allocator);
822 }
823
824 test "fromJsonString embedded payload releases every allocation failure path" {
825 try std.testing.checkAllAllocationFailures(
826 std.testing.allocator,
827 checkEmbeddedJsonAllocationFailures,
828 .{},
829 );
830 }
831
832 test "$capture / $bind / $rest round-trip through JSON" {
833 const alloc = std.testing.allocator;
834 var arena = std.heap.ArenaAllocator.init(alloc);
835 defer arena.deinit();
836 const a = arena.allocator();
837
838 const cap = try any_constructors.capture(a, Value{ .discard = {} });
839 const cap_json = try toJsonString(alloc, cap);
840 defer alloc.free(cap_json);
841 try std.testing.expectEqualStrings("{\"$capture\":{\"__discard__\":true}}", cap_json);
842
843 const bnd = try any_constructors.bindVal(a, "x", Value{ .discard = {} });
844 const bnd_json = try toJsonString(alloc, bnd);
845 defer alloc.free(bnd_json);
846 try std.testing.expectEqualStrings("{\"$bind\":\"x\"}", bnd_json);
847
848 const rest = try any_constructors.restPattern(
849 a,
850 &.{ Value.initI128(1), .{ .string = "tail" } },
851 .{ .discard = {} },
852 );
853 const rest_json = try toJsonString(alloc, rest);
854 defer alloc.free(rest_json);
855 try std.testing.expectEqualStrings(
856 "{\"$rest\":{\"prefix\":[1,\"tail\"],\"rest\":{\"__discard__\":true}}}",
857 rest_json,
858 );
859
860 var parsed_cap = try fromJsonString(alloc, "{\"$capture\":{\"__discard__\":true}}");
861 defer parsed_cap.deinit(alloc);
862 try std.testing.expect(parsed_cap == .capture);
863 try std.testing.expect(parsed_cap.capture.* == .discard);
864
865 var parsed_bnd = try fromJsonString(alloc, "{\"$bind\":\"y\"}");
866 defer parsed_bnd.deinit(alloc);
867 try std.testing.expect(parsed_bnd == .bind);
868 try std.testing.expectEqualStrings("y", parsed_bnd.bind.name);
869 try std.testing.expect(parsed_bnd.bind.pattern.* == .discard);
870
871 var parsed_rest = try fromJsonString(alloc, rest_json);
872 defer parsed_rest.deinit(alloc);
873 try std.testing.expect(parsed_rest == .rest_pattern);
874 try std.testing.expectEqual(@as(usize, 2), parsed_rest.rest_pattern.prefix.len);
875 try std.testing.expect(parsed_rest.rest_pattern.rest.* == .discard);
876 }