From 17b0ae95510cb225368e9969e62492adb5d6f422 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Fri, 13 Aug 2021 10:28:29 -0700 Subject: [PATCH] provide smithy-based json serialization for request data This will use the actual structure name or the override from the trait as needed. Return value support is also enabled in the code generation but not in aws.zig. The current fuzzy checks should get most of the way there though --- codegen/src/main.zig | 46 ++++++++++++++++++++++++++++++++++++++++++++ src/aws.zig | 31 ++++++++++++++++++++++++++--- src/json.zig | 20 +++++++------------ 3 files changed, 81 insertions(+), 16 deletions(-) diff --git a/codegen/src/main.zig b/codegen/src/main.zig index 5d0e6db..02d2d9a 100644 --- a/codegen/src/main.zig +++ b/codegen/src/main.zig @@ -329,13 +329,35 @@ fn generateSimpleTypeFor(_: anytype, type_name: []const u8, writer: anytype, all } fn generateComplexTypeFor(allocator: *std.mem.Allocator, members: []smithy.TypeMember, type_type_name: []const u8, shapes: anytype, writer: anytype, prefix: []const u8, all_required: bool, type_stack: anytype) anyerror!void { + const Mapping = struct { snake: []const u8, json: []const u8 }; + var mappings = try std.ArrayList(Mapping).initCapacity(allocator, members.len); + defer { + for (mappings.items) |mapping| { + allocator.free(mapping.snake); + } + mappings.deinit(); + } // prolog. We'll rely on caller to get the spacing correct here _ = try writer.write(type_type_name); _ = try writer.write(" {\n"); for (members) |member| { const new_prefix = try std.fmt.allocPrint(allocator, " {s}", .{prefix}); defer allocator.free(new_prefix); + // This is our mapping const snake_case_member = try snake.fromPascalCase(allocator, member.name); + // So it looks like some services have duplicate names?! Check out "httpMethod" + // in API Gateway. Not sure what we're supposed to do there. Checking the go + // sdk, they move this particular duplicate to 'http_method' - not sure yet + // if this is a hard-coded exception` + var found_trait = false; + for (member.traits) |trait| { + if (trait == .json_name) { + found_trait = true; + mappings.appendAssumeCapacity(.{ .snake = try allocator.dupe(u8, snake_case_member), .json = trait.json_name }); + } + } + if (!found_trait) + mappings.appendAssumeCapacity(.{ .snake = try allocator.dupe(u8, snake_case_member), .json = member.name }); defer allocator.free(snake_case_member); try writer.print("{s} {s}: ", .{ prefix, avoidReserved(snake_case_member) }); if (!all_required) try writeOptional(member.traits, writer, null); @@ -344,6 +366,30 @@ fn generateComplexTypeFor(allocator: *std.mem.Allocator, members: []smithy.TypeM try writeOptional(member.traits, writer, " = null"); _ = try writer.write(",\n"); } + + // Add in json mappings. The function looks like this: + // + // pub fn jsonFieldNameFor(_: @This(), comptime field_name: []const u8) []const u8 { + // const mappings = .{ + // .exclusive_start_table_name = "ExclusiveStartTableName", + // .limit = "Limit", + // }; + // return @field(mappings, field_name); + // } + // + // TODO: There is a smithy trait that will specify the json name. We should be using + // this instead if applicable. + try writer.print("\n{s} pub fn jsonFieldNameFor(_: @This(), comptime field_name: []const u8) []const u8 ", .{prefix}); + _ = try writer.write("{\n"); + try writer.print("{s} const mappings = .", .{prefix}); + _ = try writer.write("{\n"); + for (mappings.items) |mapping| { + try writer.print("{s} .{s} = \"{s}\",\n", .{ prefix, avoidReserved(mapping.snake), mapping.json }); + } + _ = try writer.write(prefix); + _ = try writer.write(" };\n"); + try writer.print("{s} return @field(mappings, field_name);\n{s}", .{ prefix, prefix }); + _ = try writer.write(" }\n"); } fn writeOptional(traits: ?[]smithy.Trait, writer: anytype, value: ?[]const u8) !void { diff --git a/src/aws.zig b/src/aws.zig index 52ee81f..fbb9810 100644 --- a/src/aws.zig +++ b/src/aws.zig @@ -93,7 +93,7 @@ pub const Aws = struct { // var nameAllocator = std.heap.ArenaAllocator.init(self.allocator); defer nameAllocator.deinit(); - try json.stringify(request, .{ .whitespace = .{}, .allocator = &nameAllocator.allocator, .nameTransform = pascalTransformer }, buffer.writer()); + try json.stringify(request, .{ .whitespace = .{} }, buffer.writer()); var content_type: []const u8 = undefined; switch (service_meta.aws_protocol) { @@ -377,8 +377,33 @@ fn queryFieldTransformer(field_name: []const u8, encoding_options: url.EncodingO return try case.snakeToPascal(encoding_options.allocator.?, field_name); } -fn pascalTransformer(field_name: []const u8, options: json.StringifyOptions) anyerror![]const u8 { - return try case.snakeToPascal(options.allocator.?, field_name); +test "basic json request serialization" { + const allocator = std.testing.allocator; + const svs = Services(.{.dynamo_db}){}; + const request = svs.dynamo_db.list_tables.Request{ + .limit = 1, + }; + var buffer = std.ArrayList(u8).init(allocator); + defer buffer.deinit(); + + // The transformer needs to allocate stuff out of band, but we + // can guarantee we don't need the memory after this call completes, + // so we'll use an arena allocator to whack everything. + // TODO: Determine if sending in null values is ok, or if we need another + // tweak to the stringify function to exclude. According to the + // smithy spec, "A null value MAY be provided or omitted + // for a boxed member with no observable difference." But we're + // seeing a lot of differences here between spec and reality + // + var nameAllocator = std.heap.ArenaAllocator.init(allocator); + defer nameAllocator.deinit(); + try json.stringify(request, .{ .whitespace = .{} }, buffer.writer()); + try std.testing.expectEqualStrings( + \\{ + \\ "ExclusiveStartTableName": null, + \\ "Limit": 1 + \\} + , buffer.items); } // Use for debugging json responses of specific requests // test "dummy request" { diff --git a/src/json.zig b/src/json.zig index f61e831..d6399f3 100644 --- a/src/json.zig +++ b/src/json.zig @@ -2656,15 +2656,6 @@ pub const StringifyOptions = struct { string: StringOptions = StringOptions{ .String = .{} }, - nameTransform: fn ([]const u8, StringifyOptions) anyerror![]const u8 = nullTransform, - - /// Not used by stringify - might be needed for your name transformer - allocator: ?*std.mem.Allocator = null, - - fn nullTransform(name: []const u8, _: StringifyOptions) ![]const u8 { - return name; - } - /// Should []u8 be serialised as a string? or an array? pub const StringOptions = union(enum) { Array, @@ -2777,10 +2768,13 @@ pub fn stringify( try out_stream.writeByte('\n'); try child_whitespace.outputIndent(out_stream); } - const name = child_options.nameTransform(Field.name, options) catch { - return error.NameTransformationError; - }; - try stringify(name, options, out_stream); + if (comptime std.meta.trait.hasFn("jsonFieldNameFor")(T)) { + const name = value.jsonFieldNameFor(Field.name); + try stringify(name, options, out_stream); + } else { + try stringify(Field.name, options, out_stream); + } + try out_stream.writeByte(':'); if (child_options.whitespace) |child_whitespace| { if (child_whitespace.separator) {