refactor: generate types for maps
TODO: handle json stringify for generated map types
This commit is contained in:
parent
522ab72296
commit
934323acf1
4 changed files with 113 additions and 96 deletions
|
@ -467,7 +467,9 @@ fn generateAdditionalTypes(allocator: std.mem.Allocator, file_state: FileGenerat
|
|||
.allocator = allocator,
|
||||
.indent_level = 0,
|
||||
};
|
||||
const type_name = avoidReserved(t.name);
|
||||
const type_name = try getTypeName(allocator, t);
|
||||
defer allocator.free(type_name);
|
||||
|
||||
try writer.print("\npub const {s} = ", .{type_name});
|
||||
try file_state.additional_types_generated.putNoClobber(t.name, {});
|
||||
_ = try generateTypeFor(t.id, writer, state, true);
|
||||
|
@ -637,6 +639,18 @@ fn endsWith(item: []const u8, str: []const u8) bool {
|
|||
return std.mem.eql(u8, item, str[str.len - item.len ..]);
|
||||
}
|
||||
|
||||
fn getTypeName(allocator: std.mem.Allocator, shape: smithy.ShapeInfo) ![]const u8 {
|
||||
const type_name = avoidReserved(shape.name);
|
||||
|
||||
switch (shape.shape) {
|
||||
.map => {
|
||||
const map_type_name = avoidReserved(shape.name);
|
||||
return try std.fmt.allocPrint(allocator, "{s}KeyValue", .{map_type_name[0 .. map_type_name.len - 1]});
|
||||
},
|
||||
else => return allocator.dupe(u8, type_name),
|
||||
}
|
||||
}
|
||||
|
||||
fn reuseCommonType(shape: smithy.ShapeInfo, writer: anytype, state: GenerationState) !bool {
|
||||
// We want to return if we're at the top level of the stack. There are three
|
||||
// reasons for this:
|
||||
|
@ -651,12 +665,21 @@ fn reuseCommonType(shape: smithy.ShapeInfo, writer: anytype, state: GenerationSt
|
|||
// can at least see the top level.
|
||||
// 3. When we come through at the end, we want to make sure we're writing
|
||||
// something or we'll have an infinite loop!
|
||||
|
||||
switch (shape.shape) {
|
||||
.structure, .uniontype, .map => {},
|
||||
else => return false,
|
||||
}
|
||||
|
||||
const type_name = try getTypeName(state.allocator, shape);
|
||||
defer state.allocator.free(type_name);
|
||||
|
||||
if (state.type_stack.items.len == 1) return false;
|
||||
var rc = false;
|
||||
if (state.file_state.shape_references.get(shape.id)) |r| {
|
||||
if (r > 1 and (shape.shape == .structure or shape.shape == .uniontype)) {
|
||||
if (r > 1) {
|
||||
rc = true;
|
||||
_ = try writer.write(avoidReserved(shape.name)); // This can't possibly be this easy...
|
||||
_ = try writer.write(type_name); // This can't possibly be this easy...
|
||||
if (state.file_state.additional_types_generated.getEntry(shape.name) == null)
|
||||
try state.file_state.additional_types_to_generate.append(shape);
|
||||
}
|
||||
|
@ -755,34 +778,14 @@ fn generateTypeFor(shape_id: []const u8, writer: anytype, state: GenerationState
|
|||
.double => |s| try generateSimpleTypeFor(s, "f64", writer),
|
||||
.float => |s| try generateSimpleTypeFor(s, "f32", writer),
|
||||
.long => |s| try generateSimpleTypeFor(s, "i64", writer),
|
||||
.map => {
|
||||
_ = try writer.write("[]struct {\n");
|
||||
var child_state = state;
|
||||
child_state.indent_level += 1;
|
||||
try outputIndent(child_state, writer);
|
||||
_ = try writer.write("key: ");
|
||||
try writeOptional(shape.map.traits, writer, null);
|
||||
var sub_maps = std.ArrayList([]const u8).init(state.allocator);
|
||||
defer sub_maps.deinit();
|
||||
if (try generateTypeFor(shape.map.key, writer, child_state, true))
|
||||
try sub_maps.append("key");
|
||||
try writeOptional(shape.map.traits, writer, " = null");
|
||||
_ = try writer.write(",\n");
|
||||
try outputIndent(child_state, writer);
|
||||
_ = try writer.write("value: ");
|
||||
try writeOptional(shape.map.traits, writer, null);
|
||||
if (try generateTypeFor(shape.map.value, writer, child_state, true))
|
||||
try sub_maps.append("value");
|
||||
try writeOptional(shape.map.traits, writer, " = null");
|
||||
_ = try writer.write(",\n");
|
||||
if (sub_maps.items.len > 0) {
|
||||
_ = try writer.write("\n");
|
||||
try writeStringify(state, sub_maps.items, writer);
|
||||
.map => |m| {
|
||||
if (!try reuseCommonType(shape_info, std.io.null_writer, state)) {
|
||||
try generateMapTypeFor(m, writer, state);
|
||||
rc = true;
|
||||
} else {
|
||||
try writer.writeAll("[]");
|
||||
_ = try reuseCommonType(shape_info, writer, state);
|
||||
}
|
||||
try outputIndent(state, writer);
|
||||
_ = try writer.write("}");
|
||||
|
||||
rc = true;
|
||||
},
|
||||
else => {
|
||||
std.log.err("encountered unimplemented shape type {s} for shape_id {s}. Generated code will not compile", .{ @tagName(shape), shape_id });
|
||||
|
@ -793,41 +796,59 @@ fn generateTypeFor(shape_id: []const u8, writer: anytype, state: GenerationState
|
|||
return rc;
|
||||
}
|
||||
|
||||
fn generateMapTypeFor(map: anytype, writer: anytype, state: GenerationState) anyerror!void {
|
||||
_ = try writer.write("struct {\n");
|
||||
|
||||
var child_state = state;
|
||||
child_state.indent_level += 1;
|
||||
|
||||
_ = try writer.write("key: ");
|
||||
try writeOptional(map.traits, writer, null);
|
||||
|
||||
_ = try generateTypeFor(map.key, writer, child_state, true);
|
||||
|
||||
try writeOptional(map.traits, writer, " = null");
|
||||
_ = try writer.write(",\n");
|
||||
|
||||
_ = try writer.write("value: ");
|
||||
try writeOptional(map.traits, writer, null);
|
||||
|
||||
_ = try generateTypeFor(map.value, writer, child_state, true);
|
||||
|
||||
try writeOptional(map.traits, writer, " = null");
|
||||
_ = try writer.write(",\n");
|
||||
_ = try writer.write("}");
|
||||
}
|
||||
|
||||
fn generateSimpleTypeFor(_: anytype, type_name: []const u8, writer: anytype) !void {
|
||||
_ = try writer.write(type_name); // This had required stuff but the problem was elsewhere. Better to leave as function just in case
|
||||
}
|
||||
|
||||
const Mapping = struct { snake: []const u8, original: []const u8 };
|
||||
fn generateComplexTypeFor(shape_id: []const u8, members: []smithy.TypeMember, type_type_name: []const u8, writer: anytype, state: GenerationState) anyerror!void {
|
||||
_ = shape_id;
|
||||
const Mapping = struct { snake: []const u8, original: []const u8 };
|
||||
var field_name_mappings = try std.ArrayList(Mapping).initCapacity(state.allocator, members.len);
|
||||
defer {
|
||||
for (field_name_mappings.items) |mapping|
|
||||
state.allocator.free(mapping.snake);
|
||||
field_name_mappings.deinit();
|
||||
}
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(state.allocator);
|
||||
defer arena.deinit();
|
||||
const allocator = arena.allocator();
|
||||
|
||||
var field_name_mappings = try std.ArrayList(Mapping).initCapacity(allocator, members.len);
|
||||
defer field_name_mappings.deinit();
|
||||
// There is an httpQueryParams trait as well, but nobody is using it. API GW
|
||||
// pretends to, but it's an empty map
|
||||
//
|
||||
// Same with httpPayload
|
||||
//
|
||||
// httpLabel is interesting - right now we just assume anything can be used - do we need to track this?
|
||||
var http_query_mappings = try std.ArrayList(Mapping).initCapacity(state.allocator, members.len);
|
||||
defer {
|
||||
for (http_query_mappings.items) |mapping|
|
||||
state.allocator.free(mapping.snake);
|
||||
http_query_mappings.deinit();
|
||||
}
|
||||
var http_header_mappings = try std.ArrayList(Mapping).initCapacity(state.allocator, members.len);
|
||||
defer {
|
||||
for (http_header_mappings.items) |mapping|
|
||||
state.allocator.free(mapping.snake);
|
||||
http_header_mappings.deinit();
|
||||
}
|
||||
var map_fields = std.ArrayList([]const u8).init(state.allocator);
|
||||
defer {
|
||||
for (map_fields.items) |f| state.allocator.free(f);
|
||||
map_fields.deinit();
|
||||
}
|
||||
var http_query_mappings = try std.ArrayList(Mapping).initCapacity(allocator, members.len);
|
||||
defer http_query_mappings.deinit();
|
||||
|
||||
var http_header_mappings = try std.ArrayList(Mapping).initCapacity(allocator, members.len);
|
||||
defer http_header_mappings.deinit();
|
||||
|
||||
var map_fields = std.ArrayList([]const u8).init(allocator);
|
||||
defer map_fields.deinit();
|
||||
|
||||
// prolog. We'll rely on caller to get the spacing correct here
|
||||
_ = try writer.write(type_type_name);
|
||||
_ = try writer.write(" {\n");
|
||||
|
@ -836,7 +857,7 @@ fn generateComplexTypeFor(shape_id: []const u8, members: []smithy.TypeMember, ty
|
|||
var payload: ?[]const u8 = null;
|
||||
for (members) |member| {
|
||||
// This is our mapping
|
||||
const snake_case_member = try constantName(state.allocator, member.name);
|
||||
const snake_case_member = try constantName(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
|
||||
|
@ -846,34 +867,34 @@ fn generateComplexTypeFor(shape_id: []const u8, members: []smithy.TypeMember, ty
|
|||
switch (trait) {
|
||||
.json_name => |n| {
|
||||
found_name_trait = true;
|
||||
field_name_mappings.appendAssumeCapacity(.{ .snake = try state.allocator.dupe(u8, snake_case_member), .original = n });
|
||||
field_name_mappings.appendAssumeCapacity(.{ .snake = try allocator.dupe(u8, snake_case_member), .original = n });
|
||||
},
|
||||
.xml_name => |n| {
|
||||
found_name_trait = true;
|
||||
field_name_mappings.appendAssumeCapacity(.{ .snake = try state.allocator.dupe(u8, snake_case_member), .original = n });
|
||||
field_name_mappings.appendAssumeCapacity(.{ .snake = try allocator.dupe(u8, snake_case_member), .original = n });
|
||||
},
|
||||
.http_query => |n| http_query_mappings.appendAssumeCapacity(.{ .snake = try state.allocator.dupe(u8, snake_case_member), .original = n }),
|
||||
.http_header => http_header_mappings.appendAssumeCapacity(.{ .snake = try state.allocator.dupe(u8, snake_case_member), .original = trait.http_header }),
|
||||
.http_query => |n| http_query_mappings.appendAssumeCapacity(.{ .snake = try allocator.dupe(u8, snake_case_member), .original = n }),
|
||||
.http_header => http_header_mappings.appendAssumeCapacity(.{ .snake = try allocator.dupe(u8, snake_case_member), .original = trait.http_header }),
|
||||
.http_payload => {
|
||||
// Don't assert as that will be optimized for Release* builds
|
||||
// We'll continue here and treat the above as a warning
|
||||
if (payload) |first| {
|
||||
std.log.err("Found multiple httpPayloads in violation of smithy spec! Ignoring '{s}' and using '{s}'", .{ first, snake_case_member });
|
||||
}
|
||||
payload = try state.allocator.dupe(u8, snake_case_member);
|
||||
payload = try allocator.dupe(u8, snake_case_member);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
if (!found_name_trait)
|
||||
field_name_mappings.appendAssumeCapacity(.{ .snake = try state.allocator.dupe(u8, snake_case_member), .original = member.name });
|
||||
defer state.allocator.free(snake_case_member);
|
||||
field_name_mappings.appendAssumeCapacity(.{ .snake = try allocator.dupe(u8, snake_case_member), .original = member.name });
|
||||
|
||||
try outputIndent(child_state, writer);
|
||||
const member_name = avoidReserved(snake_case_member);
|
||||
try writer.print("{s}: ", .{member_name});
|
||||
try writeOptional(member.traits, writer, null);
|
||||
if (try generateTypeFor(member.target, writer, child_state, true))
|
||||
try map_fields.append(try std.fmt.allocPrint(state.allocator, "{s}", .{member_name}));
|
||||
try map_fields.append(try std.fmt.allocPrint(allocator, "{s}", .{member_name}));
|
||||
|
||||
if (!std.mem.eql(u8, "union", type_type_name))
|
||||
try writeOptional(member.traits, writer, " = null");
|
||||
|
|
|
@ -14,35 +14,15 @@ const testing = std.testing;
|
|||
const mem = std.mem;
|
||||
const maxInt = std.math.maxInt;
|
||||
|
||||
pub fn serializeMap(map: anytype, key: []const u8, options: anytype, out_stream: anytype) !bool {
|
||||
pub fn serializeMap(map: anytype, key: []const u8, options: anytype, out_stream: anytype) !void {
|
||||
if (@typeInfo(@TypeOf(map)) == .optional) {
|
||||
if (map == null)
|
||||
return false
|
||||
else
|
||||
return serializeMapInternal(map.?, key, options, out_stream);
|
||||
if (map) |m| serializeMapInternal(m, key, options, out_stream);
|
||||
} else {
|
||||
serializeMapInternal(map, key, options, out_stream);
|
||||
}
|
||||
return serializeMapInternal(map, key, options, out_stream);
|
||||
}
|
||||
|
||||
fn serializeMapInternal(map: anytype, key: []const u8, options: anytype, out_stream: anytype) !bool {
|
||||
if (map.len == 0) {
|
||||
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('{');
|
||||
try out_stream.writeByte('}');
|
||||
return true;
|
||||
}
|
||||
// TODO: Map might be [][]struct{key, value} rather than []struct{key, value}
|
||||
fn serializeMapKey(key: []const u8, options: anytype, out_stream: anytype) !void {
|
||||
var child_options = options;
|
||||
if (child_options.whitespace) |*child_ws|
|
||||
child_ws.indent_level += 1;
|
||||
|
@ -55,36 +35,52 @@ fn serializeMapInternal(map: anytype, key: []const u8, options: anytype, out_str
|
|||
try out_stream.writeByte(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serializeMapAsObject(map: anytype, options: anytype, out_stream: anytype) !void {
|
||||
if (map.len == 0) {
|
||||
try out_stream.writeByte('{');
|
||||
try out_stream.writeByte('}');
|
||||
}
|
||||
|
||||
// TODO: Map might be [][]struct{key, value} rather than []struct{key, value}
|
||||
|
||||
try out_stream.writeByte('{');
|
||||
if (options.whitespace) |_|
|
||||
try out_stream.writeByte('\n');
|
||||
for (map, 0..) |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|
|
||||
if (options.whitespace) |ws|
|
||||
try ws.outputIndent(out_stream);
|
||||
try out_stream.writeByte('"');
|
||||
try jsonEscape(tag.key.?, child_options, out_stream);
|
||||
try jsonEscape(tag.key.?, options, out_stream);
|
||||
_ = try out_stream.write("\":");
|
||||
if (child_options.whitespace) |ws| {
|
||||
if (options.whitespace) |ws| {
|
||||
if (ws.separator) {
|
||||
try out_stream.writeByte(' ');
|
||||
}
|
||||
}
|
||||
try out_stream.writeByte('"');
|
||||
try jsonEscape(tag.value.?, child_options, out_stream);
|
||||
try jsonEscape(tag.value.?, options, out_stream);
|
||||
try out_stream.writeByte('"');
|
||||
if (i < map.len - 1) {
|
||||
try out_stream.writeByte(',');
|
||||
}
|
||||
if (child_options.whitespace) |_|
|
||||
if (options.whitespace) |_|
|
||||
try out_stream.writeByte('\n');
|
||||
}
|
||||
if (options.whitespace) |ws|
|
||||
try ws.outputIndent(out_stream);
|
||||
try out_stream.writeByte('}');
|
||||
return true;
|
||||
}
|
||||
|
||||
fn serializeMapInternal(map: anytype, key: []const u8, options: anytype, out_stream: anytype) !bool {
|
||||
var child_options = options;
|
||||
try serializeMapKey(key, &child_options, out_stream);
|
||||
return try serializeMapAsObject(map, child_options, out_stream);
|
||||
}
|
||||
|
||||
// code within jsonEscape lifted from json.zig in stdlib
|
||||
fn jsonEscape(value: []const u8, options: anytype, out_stream: anytype) !void {
|
||||
var i: usize = 0;
|
||||
|
|
|
@ -1389,7 +1389,7 @@ test "custom serialization for map objects" {
|
|||
defer tags.deinit();
|
||||
tags.appendAssumeCapacity(.{ .key = "Foo", .value = "Bar" });
|
||||
tags.appendAssumeCapacity(.{ .key = "Baz", .value = "Qux" });
|
||||
const req = services.lambda.tag_resource.Request{ .resource = "hello", .tags = tags.items };
|
||||
const req = services.lambda.TagResourceRequest{ .resource = "hello", .tags = tags.items };
|
||||
try json.stringify(req, .{ .whitespace = .{} }, buffer.writer());
|
||||
try std.testing.expectEqualStrings(
|
||||
\\{
|
||||
|
|
|
@ -192,7 +192,7 @@ pub fn main() anyerror!void {
|
|||
const func = fns[0];
|
||||
const arn = func.function_arn.?;
|
||||
// This is a bit ugly. Maybe a helper function in the library would help?
|
||||
var tags = try std.ArrayList(@typeInfo(try typeForField(services.lambda.tag_resource.Request, "tags")).pointer.child).initCapacity(allocator, 1);
|
||||
var tags = try std.ArrayList(aws.services.lambda.TagKeyValue).initCapacity(allocator, 1);
|
||||
defer tags.deinit();
|
||||
tags.appendAssumeCapacity(.{ .key = "Foo", .value = "Bar" });
|
||||
const req = services.lambda.tag_resource.Request{ .resource = arn, .tags = tags.items };
|
||||
|
|
Loading…
Add table
Reference in a new issue