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) {