diff --git a/codegen/src/json.zig b/codegen/src/json.zig new file mode 100644 index 0000000..ddaaed2 --- /dev/null +++ b/codegen/src/json.zig @@ -0,0 +1,123 @@ +const std = @import("std"); +// options is a json.Options, but since we're using our hacked json.zig we don't want to +// specifically call this out +pub fn serializeMap(map: anytype, key: []const u8, options: anytype, out_stream: anytype) !bool { + if (map.len == 0) return true; + var child_options = options; + if (child_options.whitespace) |*child_ws| + child_ws.indent_level += 1; + + try out_stream.writeByte('"'); + try out_stream.writeAll(key); + _ = try out_stream.write("\":"); + if (options.whitespace) |ws| { + if (ws.separator) { + try out_stream.writeByte(' '); + } + } + try out_stream.writeByte('{'); + if (options.whitespace) |_| + try out_stream.writeByte('\n'); + for (map) |tag, i| { + if (tag.key == null or tag.value == null) continue; + // TODO: Deal with escaping and general "json.stringify" the values... + if (child_options.whitespace) |ws| + try ws.outputIndent(out_stream); + try out_stream.writeByte('"'); + try jsonEscape(tag.key.?, child_options, out_stream); + _ = try out_stream.write("\":"); + if (child_options.whitespace) |ws| { + if (ws.separator) { + try out_stream.writeByte(' '); + } + } + try out_stream.writeByte('"'); + try jsonEscape(tag.value.?, child_options, out_stream); + try out_stream.writeByte('"'); + if (i < map.len - 1) { + try out_stream.writeByte(','); + } + if (child_options.whitespace) |_| + try out_stream.writeByte('\n'); + } + if (options.whitespace) |ws| + try ws.outputIndent(out_stream); + try out_stream.writeByte('}'); + return true; +} +// code within jsonEscape lifted from json.zig in stdlib +fn jsonEscape(value: []const u8, options: anytype, out_stream: anytype) !void { + var i: usize = 0; + while (i < value.len) : (i += 1) { + switch (value[i]) { + // normal ascii character + 0x20...0x21, 0x23...0x2E, 0x30...0x5B, 0x5D...0x7F => |c| try out_stream.writeByte(c), + // only 2 characters that *must* be escaped + '\\' => try out_stream.writeAll("\\\\"), + '\"' => try out_stream.writeAll("\\\""), + // solidus is optional to escape + '/' => { + if (options.string.String.escape_solidus) { + try out_stream.writeAll("\\/"); + } else { + try out_stream.writeByte('/'); + } + }, + // control characters with short escapes + // TODO: option to switch between unicode and 'short' forms? + 0x8 => try out_stream.writeAll("\\b"), + 0xC => try out_stream.writeAll("\\f"), + '\n' => try out_stream.writeAll("\\n"), + '\r' => try out_stream.writeAll("\\r"), + '\t' => try out_stream.writeAll("\\t"), + else => { + const ulen = std.unicode.utf8ByteSequenceLength(value[i]) catch unreachable; + // control characters (only things left with 1 byte length) should always be printed as unicode escapes + if (ulen == 1 or options.string.String.escape_unicode) { + const codepoint = std.unicode.utf8Decode(value[i .. i + ulen]) catch unreachable; + try outputUnicodeEscape(codepoint, out_stream); + } else { + try out_stream.writeAll(value[i .. i + ulen]); + } + i += ulen - 1; + }, + } + } +} +// outputUnicodeEscape and assert lifted from json.zig in stdlib +fn outputUnicodeEscape( + codepoint: u21, + out_stream: anytype, +) !void { + if (codepoint <= 0xFFFF) { + // If the character is in the Basic Multilingual Plane (U+0000 through U+FFFF), + // then it may be represented as a six-character sequence: a reverse solidus, followed + // by the lowercase letter u, followed by four hexadecimal digits that encode the character's code point. + try out_stream.writeAll("\\u"); + try std.fmt.formatIntValue(codepoint, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream); + } else { + assert(codepoint <= 0x10FFFF); + // To escape an extended character that is not in the Basic Multilingual Plane, + // the character is represented as a 12-character sequence, encoding the UTF-16 surrogate pair. + const high = @intCast(u16, (codepoint - 0x10000) >> 10) + 0xD800; + const low = @intCast(u16, codepoint & 0x3FF) + 0xDC00; + try out_stream.writeAll("\\u"); + try std.fmt.formatIntValue(high, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream); + try out_stream.writeAll("\\u"); + try std.fmt.formatIntValue(low, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream); + } +} + +/// This function invokes undefined behavior when `ok` is `false`. +/// In Debug and ReleaseSafe modes, calls to this function are always +/// generated, and the `unreachable` statement triggers a panic. +/// In ReleaseFast and ReleaseSmall modes, calls to this function are +/// optimized away, and in fact the optimizer is able to use the assertion +/// in its heuristics. +/// Inside a test block, it is best to use the `std.testing` module rather +/// than this function, because this function may not detect a test failure +/// in ReleaseFast and ReleaseSmall mode. Outside of a test block, this assert +/// function is the correct function to use. +pub fn assert(ok: bool) void { + if (!ok) unreachable; // assertion failure +} diff --git a/codegen/src/main.zig b/codegen/src/main.zig index d0b7657..6e5e397 100644 --- a/codegen/src/main.zig +++ b/codegen/src/main.zig @@ -1,6 +1,7 @@ const std = @import("std"); const smithy = @import("smithy"); const snake = @import("snake.zig"); +const json_zig = @embedFile("json.zig"); pub fn main() anyerror!void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); @@ -10,6 +11,9 @@ pub fn main() anyerror!void { const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); const stdout = std.io.getStdOut().writer(); + const json_file = try std.fs.cwd().createFile("json.zig", .{}); + defer json_file.close(); + try json_file.writer().writeAll(json_zig); const manifest_file = try std.fs.cwd().createFile("service_manifest.zig", .{}); defer manifest_file.close(); const manifest = manifest_file.writer(); @@ -42,6 +46,8 @@ fn processFile(arg: []const u8, stdout: anytype, manifest: anytype) !void { file = try std.fs.cwd().createFile(filename, .{ .truncate = true }); errdefer file.close(); writer = &file.writer(); + _ = try writer.write("const std = @import(\"std\");\n"); + _ = try writer.write("const serializeMap = @import(\"json.zig\").serializeMap;\n"); _ = try writer.write("const smithy = @import(\"smithy\");\n\n"); std.log.info("Processing file: {s}", .{arg}); const service_names = generateServicesForFilePath(allocator, ";", arg, writer) catch |err| {