This repository has no description
1//! JSON <-> DAG-CBOR bridge.
2//!
3//! atproto's data model has no float type, so floats are encoded as their
4//! decimal string form on the way into a record. API responses never round
5//! trip through CBOR (they're served from the appview), so this lossiness
6//! only affects the canonical record body.
7
8const std = @import("std");
9const zat = @import("zat");
10
11pub fn jsonToCbor(arena: std.mem.Allocator, v: std.json.Value) error{OutOfMemory}!zat.cbor.Value {
12 return switch (v) {
13 .null => .null,
14 .bool => |b| .{ .boolean = b },
15 .integer => |i| if (i >= 0)
16 .{ .unsigned = @intCast(i) }
17 else
18 .{ .negative = i },
19 .float => |f| .{ .text = try std.fmt.allocPrint(arena, "{d}", .{f}) },
20 .number_string => |s| .{ .text = s },
21 .string => |s| .{ .text = s },
22 .array => |arr| blk: {
23 const items = try arena.alloc(zat.cbor.Value, arr.items.len);
24 for (arr.items, 0..) |item, i| items[i] = try jsonToCbor(arena, item);
25 break :blk .{ .array = items };
26 },
27 .object => |obj| blk: {
28 const entries = try arena.alloc(zat.cbor.Value.MapEntry, obj.count());
29 var it = obj.iterator();
30 var i: usize = 0;
31 while (it.next()) |e| : (i += 1) {
32 entries[i] = .{ .key = e.key_ptr.*, .value = try jsonToCbor(arena, e.value_ptr.*) };
33 }
34 break :blk .{ .map = entries };
35 },
36 };
37}
38
39/// write a decoded cbor value as JSON. cids render as {"$link": "<base32...>"}
40/// is out of scope for now — records written by this server contain none.
41pub fn cborToJson(jw: *std.json.Stringify, v: zat.cbor.Value) !void {
42 switch (v) {
43 .null => try jw.write(null),
44 .boolean => |b| try jw.write(b),
45 .unsigned => |u| try jw.write(u),
46 .negative => |n| try jw.write(n),
47 .text => |s| try jw.write(s),
48 .bytes => |b| {
49 var buf: [512]u8 = undefined;
50 const enc = std.base64.standard.Encoder;
51 if (enc.calcSize(b.len) <= buf.len) {
52 try jw.write(enc.encode(&buf, b));
53 } else {
54 try jw.write("<bytes>");
55 }
56 },
57 .cid => try jw.write("<cid>"),
58 .array => |items| {
59 try jw.beginArray();
60 for (items) |item| try cborToJson(jw, item);
61 try jw.endArray();
62 },
63 .map => |entries| {
64 try jw.beginObject();
65 for (entries) |e| {
66 try jw.objectField(e.key);
67 try cborToJson(jw, e.value);
68 }
69 try jw.endObject();
70 },
71 }
72}
73
74test "json -> cbor -> json round trip" {
75 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
76 defer arena.deinit();
77 const alloc = arena.allocator();
78
79 const parsed = try std.json.parseFromSliceLeaky(std.json.Value, alloc,
80 \\{"name":"test","count":3,"nested":{"ok":true},"items":[1,2]}
81 , .{});
82
83 const cb = try jsonToCbor(alloc, parsed);
84 const encoded = try zat.cbor.encodeAlloc(alloc, cb);
85 const decoded = try zat.cbor.decodeAll(alloc, encoded);
86
87 try std.testing.expectEqualStrings("test", decoded.getString("name").?);
88 try std.testing.expectEqual(@as(i64, 3), decoded.getInt("count").?);
89}