provide smithy-based json serialization for request data
All checks were successful
continuous-integration/drone/push Build is passing

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
This commit is contained in:
Emil Lerch 2021-08-13 10:28:29 -07:00
parent 5c29120a44
commit 17b0ae9551
Signed by: lobo
GPG Key ID: A7B62D657EF764F8
3 changed files with 81 additions and 16 deletions

View File

@ -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 { 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 // prolog. We'll rely on caller to get the spacing correct here
_ = try writer.write(type_type_name); _ = try writer.write(type_type_name);
_ = try writer.write(" {\n"); _ = try writer.write(" {\n");
for (members) |member| { for (members) |member| {
const new_prefix = try std.fmt.allocPrint(allocator, " {s}", .{prefix}); const new_prefix = try std.fmt.allocPrint(allocator, " {s}", .{prefix});
defer allocator.free(new_prefix); defer allocator.free(new_prefix);
// This is our mapping
const snake_case_member = try snake.fromPascalCase(allocator, member.name); 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); defer allocator.free(snake_case_member);
try writer.print("{s} {s}: ", .{ prefix, avoidReserved(snake_case_member) }); try writer.print("{s} {s}: ", .{ prefix, avoidReserved(snake_case_member) });
if (!all_required) try writeOptional(member.traits, writer, null); 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 writeOptional(member.traits, writer, " = null");
_ = try writer.write(",\n"); _ = 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 { fn writeOptional(traits: ?[]smithy.Trait, writer: anytype, value: ?[]const u8) !void {

View File

@ -93,7 +93,7 @@ pub const Aws = struct {
// //
var nameAllocator = std.heap.ArenaAllocator.init(self.allocator); var nameAllocator = std.heap.ArenaAllocator.init(self.allocator);
defer nameAllocator.deinit(); 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; var content_type: []const u8 = undefined;
switch (service_meta.aws_protocol) { 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); return try case.snakeToPascal(encoding_options.allocator.?, field_name);
} }
fn pascalTransformer(field_name: []const u8, options: json.StringifyOptions) anyerror![]const u8 { test "basic json request serialization" {
return try case.snakeToPascal(options.allocator.?, field_name); 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 // Use for debugging json responses of specific requests
// test "dummy request" { // test "dummy request" {

View File

@ -2656,15 +2656,6 @@ pub const StringifyOptions = struct {
string: StringOptions = StringOptions{ .String = .{} }, 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? /// Should []u8 be serialised as a string? or an array?
pub const StringOptions = union(enum) { pub const StringOptions = union(enum) {
Array, Array,
@ -2777,10 +2768,13 @@ pub fn stringify(
try out_stream.writeByte('\n'); try out_stream.writeByte('\n');
try child_whitespace.outputIndent(out_stream); try child_whitespace.outputIndent(out_stream);
} }
const name = child_options.nameTransform(Field.name, options) catch { if (comptime std.meta.trait.hasFn("jsonFieldNameFor")(T)) {
return error.NameTransformationError; const name = value.jsonFieldNameFor(Field.name);
};
try stringify(name, options, out_stream); try stringify(name, options, out_stream);
} else {
try stringify(Field.name, options, out_stream);
}
try out_stream.writeByte(':'); try out_stream.writeByte(':');
if (child_options.whitespace) |child_whitespace| { if (child_options.whitespace) |child_whitespace| {
if (child_whitespace.separator) { if (child_whitespace.separator) {