1const std = @import("std");
2const assert = std.debug.assert;
3const math = std.math;
4const mem = std.mem;
5const log = std.log.scoped(.yaml);
6
7const Allocator = mem.Allocator;
8const ArenaAllocator = std.heap.ArenaAllocator;
9
10pub const Tokenizer = @import("Tokenizer.zig");
11pub const parse = @import("parse.zig");
12
13const Node = parse.Node;
14const Tree = parse.Tree;
15const ParseError = parse.ParseError;
16
17pub const YamlError = error{
18 UnexpectedNodeType,
19 DuplicateMapKey,
20 OutOfMemory,
21 CannotEncodeValue,
22} || ParseError || std.fmt.ParseIntError;
23
24pub const List = []Value;
25pub const Map = std.StringHashMap(Value);
26
27pub const Value = union(enum) {
28 empty,
29 int: i64,
30 float: f64,
31 string: []const u8,
32 list: List,
33 map: Map,
34
35 pub fn asInt(self: Value) !i64 {
36 if (self != .int) return error.TypeMismatch;
37 return self.int;
38 }
39
40 pub fn asFloat(self: Value) !f64 {
41 if (self != .float) return error.TypeMismatch;
42 return self.float;
43 }
44
45 pub fn asString(self: Value) ![]const u8 {
46 if (self != .string) return error.TypeMismatch;
47 return self.string;
48 }
49
50 pub fn asList(self: Value) !List {
51 if (self != .list) return error.TypeMismatch;
52 return self.list;
53 }
54
55 pub fn asMap(self: Value) !Map {
56 if (self != .map) return error.TypeMismatch;
57 return self.map;
58 }
59
60 const StringifyArgs = struct {
61 indentation: usize = 0,
62 should_inline_first_key: bool = false,
63 };
64
65 pub fn stringify(self: Value, writer: anytype, args: StringifyArgs) anyerror!void {
66 switch (self) {
67 .empty => return,
68 .int => |int| return writer.print("{}", .{int}),
69 .float => |float| return writer.print("{d}", .{float}),
70 .string => |string| return writer.print("{s}", .{string}),
71 .list => |list| {
72 const len = list.len;
73 if (len == 0) return;
74
75 const first = list[0];
76 if (first.isCompound()) {
77 for (list, 0..) |elem, i| {
78 try writer.writeByteNTimes(' ', args.indentation);
79 try writer.writeAll("- ");
80 try elem.stringify(writer, .{
81 .indentation = args.indentation + 2,
82 .should_inline_first_key = true,
83 });
84 if (i < len - 1) {
85 try writer.writeByte('\n');
86 }
87 }
88 return;
89 }
90
91 try writer.writeAll("[ ");
92 for (list, 0..) |elem, i| {
93 try elem.stringify(writer, args);
94 if (i < len - 1) {
95 try writer.writeAll(", ");
96 }
97 }
98 try writer.writeAll(" ]");
99 },
100 .map => |map| {
101 const len = map.count();
102 if (len == 0) return;
103
104 var i: usize = 0;
105 var it = map.iterator();
106 while (it.next()) |entry| {
107 const key = entry.key_ptr.*;
108 const value = entry.value_ptr.*;
109
110 if (!args.should_inline_first_key or i != 0) {
111 try writer.writeByteNTimes(' ', args.indentation);
112 }
113 try writer.print("{s}: ", .{key});
114
115 const should_inline = blk: {
116 if (!value.isCompound()) break :blk true;
117 if (value == .list and value.list.len > 0 and !value.list[0].isCompound()) break :blk true;
118 break :blk false;
119 };
120
121 if (should_inline) {
122 try value.stringify(writer, args);
123 } else {
124 try writer.writeByte('\n');
125 try value.stringify(writer, .{
126 .indentation = args.indentation + 4,
127 });
128 }
129
130 if (i < len - 1) {
131 try writer.writeByte('\n');
132 }
133
134 i += 1;
135 }
136 },
137 }
138 }
139
140 fn isCompound(self: Value) bool {
141 return switch (self) {
142 .list, .map => true,
143 else => false,
144 };
145 }
146
147 fn fromNode(arena: Allocator, tree: *const Tree, node: *const Node) YamlError!Value {
148 if (node.cast(Node.Doc)) |doc| {
149 const inner = doc.value orelse {
150 // empty doc
151 return Value{ .empty = {} };
152 };
153 return Value.fromNode(arena, tree, inner);
154 } else if (node.cast(Node.Map)) |map| {
155 // TODO use ContextAdapted HashMap and do not duplicate keys, intern
156 // in a contiguous string buffer.
157 var out_map = std.StringHashMap(Value).init(arena);
158 try out_map.ensureUnusedCapacity(math.cast(u32, map.values.items.len) orelse return error.Overflow);
159
160 for (map.values.items) |entry| {
161 const key = try arena.dupe(u8, tree.getRaw(entry.key, entry.key));
162 const gop = out_map.getOrPutAssumeCapacity(key);
163 if (gop.found_existing) {
164 return error.DuplicateMapKey;
165 }
166 const value = if (entry.value) |value|
167 try Value.fromNode(arena, tree, value)
168 else
169 .empty;
170 gop.value_ptr.* = value;
171 }
172
173 return Value{ .map = out_map };
174 } else if (node.cast(Node.List)) |list| {
175 var out_list = std.array_list.Managed(Value).init(arena);
176 try out_list.ensureUnusedCapacity(list.values.items.len);
177
178 for (list.values.items) |elem| {
179 const value = try Value.fromNode(arena, tree, elem);
180 out_list.appendAssumeCapacity(value);
181 }
182
183 return Value{ .list = try out_list.toOwnedSlice() };
184 } else if (node.cast(Node.Value)) |value| {
185 const raw = tree.getRaw(node.start, node.end);
186
187 try_int: {
188 // TODO infer base for int
189 const int = std.fmt.parseInt(i64, raw, 10) catch break :try_int;
190 return Value{ .int = int };
191 }
192
193 try_float: {
194 const float = std.fmt.parseFloat(f64, raw) catch break :try_float;
195 return Value{ .float = float };
196 }
197
198 return Value{ .string = try arena.dupe(u8, value.string_value.items) };
199 } else {
200 log.debug("Unexpected node type: {}", .{node.tag});
201 return error.UnexpectedNodeType;
202 }
203 }
204
205 fn encode(arena: Allocator, input: anytype) YamlError!?Value {
206 switch (@typeInfo(@TypeOf(input))) {
207 .comptime_int,
208 .int,
209 => return Value{ .int = math.cast(i64, input) orelse return error.Overflow },
210
211 .float => return Value{ .float = math.lossyCast(f64, input) },
212
213 .@"struct" => |info| if (info.is_tuple) {
214 var list: std.ArrayList(Value) = try .initCapacity(arena);
215 defer list.deinit();
216
217 inline for (info.field_names) |field_name| {
218 if (try encode(arena, @field(input, field_name))) |value| {
219 list.appendAssumeCapacity(value);
220 }
221 }
222
223 return Value{ .list = try list.toOwnedSlice(arena) };
224 } else {
225 var map = Map.init(arena);
226 errdefer map.deinit();
227 try map.ensureTotalCapacity(info.field_names.len);
228
229 inline for (info.field_names) |field_name| {
230 if (try encode(arena, @field(input, field_name))) |value| {
231 const key = try arena.dupe(u8, field_name);
232 map.putAssumeCapacityNoClobber(key, value);
233 }
234 }
235
236 return Value{ .map = map };
237 },
238
239 .@"union" => |info| if (info.tag_type) |tag_type| {
240 inline for (info.field_names) |field_name| {
241 if (@field(tag_type, field_name) == input) {
242 return try encode(arena, @field(input, field_name));
243 }
244 } else unreachable;
245 } else return error.UntaggedUnion,
246
247 .array => return encode(arena, &input),
248
249 .pointer => |info| switch (info.size) {
250 .one => switch (@typeInfo(info.child)) {
251 .array => |child_info| {
252 const Slice = []const child_info.child;
253 return encode(arena, @as(Slice, input));
254 },
255 else => {
256 @compileError("Unhandled type: {s}" ++ @typeName(info.child));
257 },
258 },
259 .slice => {
260 if (info.child == u8) {
261 return Value{ .string = try arena.dupe(u8, input) };
262 }
263
264 var list: std.ArrayList(Value) = .initCapacity(input.len);
265 defer list.deinit(arena);
266
267 for (input) |elem| {
268 if (try encode(arena, elem)) |value| {
269 list.appendAssumeCapacity(value);
270 } else {
271 log.debug("Could not encode value in a list: {any}", .{elem});
272 return error.CannotEncodeValue;
273 }
274 }
275
276 return Value{ .list = try list.toOwnedSlice(arena) };
277 },
278 else => {
279 @compileError("Unhandled type: {s}" ++ @typeName(@TypeOf(input)));
280 },
281 },
282
283 // TODO we should probably have an option to encode `null` and also
284 // allow for some default value too.
285 .optional => return if (input) |val| encode(arena, val) else null,
286
287 .null => return null,
288
289 else => {
290 @compileError("Unhandled type: {s}" ++ @typeName(@TypeOf(input)));
291 },
292 }
293 }
294};
295
296pub const Yaml = struct {
297 arena: ArenaAllocator,
298 tree: ?Tree = null,
299 docs: std.array_list.Managed(Value),
300
301 pub fn deinit(self: *Yaml) void {
302 self.arena.deinit();
303 }
304
305 pub fn load(allocator: Allocator, source: []const u8) !Yaml {
306 var arena = ArenaAllocator.init(allocator);
307 errdefer arena.deinit();
308
309 var tree = Tree.init(arena.allocator());
310 try tree.parse(source);
311
312 var docs = std.array_list.Managed(Value).init(arena.allocator());
313 try docs.ensureTotalCapacityPrecise(tree.docs.items.len);
314
315 for (tree.docs.items) |node| {
316 const value = try Value.fromNode(arena.allocator(), &tree, node);
317 docs.appendAssumeCapacity(value);
318 }
319
320 return Yaml{
321 .arena = arena,
322 .tree = tree,
323 .docs = docs,
324 };
325 }
326
327 pub const Error = error{
328 Unimplemented,
329 TypeMismatch,
330 StructFieldMissing,
331 ArraySizeMismatch,
332 UntaggedUnion,
333 UnionTagMissing,
334 Overflow,
335 OutOfMemory,
336 };
337
338 pub fn parse(self: *Yaml, comptime T: type) Error!T {
339 if (self.docs.items.len == 0) {
340 if (@typeInfo(T) == .void) return {};
341 return error.TypeMismatch;
342 }
343
344 if (self.docs.items.len == 1) {
345 return self.parseValue(T, self.docs.items[0]);
346 }
347
348 switch (@typeInfo(T)) {
349 .array => |info| {
350 var parsed: T = undefined;
351 for (self.docs.items, 0..) |doc, i| {
352 parsed[i] = try self.parseValue(info.child, doc);
353 }
354 return parsed;
355 },
356 .pointer => |info| {
357 switch (info.size) {
358 .slice => {
359 var parsed = try self.arena.allocator().alloc(info.child, self.docs.items.len);
360 for (self.docs.items, 0..) |doc, i| {
361 parsed[i] = try self.parseValue(info.child, doc);
362 }
363 return parsed;
364 },
365 else => return error.TypeMismatch,
366 }
367 },
368 .@"union" => return error.Unimplemented,
369 else => return error.TypeMismatch,
370 }
371 }
372
373 fn parseValue(self: *Yaml, comptime T: type, value: Value) Error!T {
374 return switch (@typeInfo(T)) {
375 .int => math.cast(T, try value.asInt()) orelse return error.Overflow,
376 .float => if (value.asFloat()) |float| {
377 return math.lossyCast(T, float);
378 } else |_| {
379 return math.lossyCast(T, try value.asInt());
380 },
381 .@"struct" => self.parseStruct(T, try value.asMap()),
382 .@"union" => self.parseUnion(T, value),
383 .array => self.parseArray(T, try value.asList()),
384 .pointer => if (value.asList()) |list| {
385 return self.parsePointer(T, .{ .list = list });
386 } else |_| {
387 return self.parsePointer(T, .{ .string = try value.asString() });
388 },
389 .void => error.TypeMismatch,
390 .optional => unreachable,
391 else => error.Unimplemented,
392 };
393 }
394
395 fn parseUnion(self: *Yaml, comptime T: type, value: Value) Error!T {
396 const union_info = @typeInfo(T).@"union";
397
398 if (union_info.tag_type) |_| {
399 inline for (union_info.field_names, union_info.field_types) |field_name, field_type| {
400 if (self.parseValue(field_type, value)) |u_value| {
401 return @unionInit(T, field_name, u_value);
402 } else |err| {
403 if (@as(@TypeOf(err) || error{TypeMismatch}, err) != error.TypeMismatch) return err;
404 }
405 }
406 } else return error.UntaggedUnion;
407
408 return error.UnionTagMissing;
409 }
410
411 fn parseOptional(self: *Yaml, comptime T: type, value: ?Value) Error!T {
412 const unwrapped = value orelse return null;
413 const opt_info = @typeInfo(T).optional;
414 return @as(T, try self.parseValue(opt_info.child, unwrapped));
415 }
416
417 fn parseStruct(self: *Yaml, comptime T: type, map: Map) Error!T {
418 const struct_info = @typeInfo(T).@"struct";
419 var parsed: T = undefined;
420
421 inline for (struct_info.field_names, struct_info.field_types) |field_name, field_type| {
422 const value: ?Value = map.get(field_name) orelse blk: {
423 const field_name_ = try mem.replaceOwned(u8, self.arena.allocator(), field_name, "_", "-");
424 break :blk map.get(field_name_);
425 };
426
427 if (@typeInfo(field_type) == .optional) {
428 @field(parsed, field_name) = try self.parseOptional(field_type, value);
429 continue;
430 }
431
432 const unwrapped = value orelse {
433 log.debug("missing struct field: {s}: {s}", .{ field_name, @typeName(field_type) });
434 return error.StructFieldMissing;
435 };
436 @field(parsed, field_name) = try self.parseValue(field_type, unwrapped);
437 }
438
439 return parsed;
440 }
441
442 fn parsePointer(self: *Yaml, comptime T: type, value: Value) Error!T {
443 const ptr_info = @typeInfo(T).pointer;
444 const arena = self.arena.allocator();
445
446 switch (ptr_info.size) {
447 .slice => {
448 if (ptr_info.child == u8) {
449 return value.asString();
450 }
451
452 var parsed = try arena.alloc(ptr_info.child, value.list.len);
453 for (value.list, 0..) |elem, i| {
454 parsed[i] = try self.parseValue(ptr_info.child, elem);
455 }
456 return parsed;
457 },
458 else => return error.Unimplemented,
459 }
460 }
461
462 fn parseArray(self: *Yaml, comptime T: type, list: List) Error!T {
463 const array_info = @typeInfo(T).array;
464 if (array_info.len != list.len) return error.ArraySizeMismatch;
465
466 var parsed: T = undefined;
467 for (list, 0..) |elem, i| {
468 parsed[i] = try self.parseValue(array_info.child, elem);
469 }
470
471 return parsed;
472 }
473
474 pub fn stringify(self: Yaml, writer: anytype) !void {
475 for (self.docs.items, 0..) |doc, i| {
476 try writer.writeAll("---");
477 if (self.tree.?.getDirective(i)) |directive| {
478 try writer.print(" !{s}", .{directive});
479 }
480 try writer.writeByte('\n');
481 try doc.stringify(writer, .{});
482 try writer.writeByte('\n');
483 }
484 try writer.writeAll("...\n");
485 }
486};
487
488pub fn stringify(allocator: Allocator, input: anytype, writer: anytype) !void {
489 var arena = ArenaAllocator.init(allocator);
490 defer arena.deinit();
491
492 const maybe_value = try Value.encode(arena.allocator(), input);
493
494 if (maybe_value) |value| {
495 // TODO should we output as an explicit doc?
496 // How can allow the user to specify?
497 try value.stringify(writer, .{});
498 }
499}
500
501test {
502 std.testing.refAllDecls(Tokenizer);
503 std.testing.refAllDecls(parse);
504 _ = @import("yaml/test.zig");
505}