diff --git a/codegen/src/main.zig b/codegen/src/main.zig index 12c3f57..d0b7657 100644 --- a/codegen/src/main.zig +++ b/codegen/src/main.zig @@ -68,10 +68,9 @@ fn generateServices(allocator: *std.mem.Allocator, comptime _: []const u8, file: defer allocator.free(json); const model = try smithy.parse(allocator, json); defer model.deinit(); - const ShapeInfo = @TypeOf(model.shapes[0]); // assume we have at least one shape - var shapes = std.StringHashMap(ShapeInfo).init(allocator); + var shapes = std.StringHashMap(smithy.ShapeInfo).init(allocator); defer shapes.deinit(); - var services = std.ArrayList(ShapeInfo).init(allocator); + var services = std.ArrayList(smithy.ShapeInfo).init(allocator); defer services.deinit(); for (model.shapes) |shape| { try shapes.put(shape.id, shape); @@ -131,7 +130,7 @@ fn generateServices(allocator: *std.mem.Allocator, comptime _: []const u8, file: // Operations for (service.shape.service.operations) |op| - try generateOperation(allocator, shapes.get(op).?, shapes, writer, constant_name); + try generateOperation(allocator, shapes.get(op).?, shapes, writer); } return constant_names.toOwnedSlice(); } @@ -155,67 +154,115 @@ fn constantName(allocator: *std.mem.Allocator, id: []const u8) ![]const u8 { // Not a special case - just snake it return try snake.fromPascalCase(allocator, id); } -fn generateOperation(allocator: *std.mem.Allocator, operation: smithy.ShapeInfo, shapes: anytype, writer: anytype, service: []const u8) !void { + +const GenerationState = struct { + type_stack: *std.ArrayList(*const smithy.ShapeInfo), + map_fields: *std.StringArrayHashMap(std.ArrayList([]const u8)), + allocator: *std.mem.Allocator, + indent_level: u64, + all_required: bool, +}; + +fn outputIndent(state: GenerationState, writer: anytype) !void { + const n_chars = 4 * state.indent_level; + try writer.writeByteNTimes(' ', n_chars); +} +fn generateOperation(allocator: *std.mem.Allocator, operation: smithy.ShapeInfo, shapes: std.StringHashMap(smithy.ShapeInfo), writer: anytype) !void { const snake_case_name = try snake.fromPascalCase(allocator, operation.name); defer allocator.free(snake_case_name); - const prefix = " "; var type_stack = std.ArrayList(*const smithy.ShapeInfo).init(allocator); defer type_stack.deinit(); + var map_fields = std.StringArrayHashMap(std.ArrayList([]const u8)).init(allocator); + defer { + for (map_fields.values()) |v| + v.deinit(); + map_fields.deinit(); + } + const state = GenerationState{ + .type_stack = &type_stack, + .map_fields = &map_fields, + .allocator = allocator, + .indent_level = 1, + .all_required = false, + }; + var child_state = state; + child_state.indent_level += 1; // indent should start at 4 spaces here const operation_name = avoidReserved(snake_case_name); try writer.print("pub const {s}: struct ", .{operation_name}); _ = try writer.write("{\n"); for (operation.shape.operation.traits) |trait| { if (trait == .http) { - _ = try writer.write(" pub const http_config = .{\n"); - try writer.print(" .method = \"{s}\",\n", .{trait.http.method}); - try writer.print(" .uri = \"{s}\",\n", .{trait.http.uri}); - try writer.print(" .success_code = {d},\n", .{trait.http.code}); - _ = try writer.write(" };\n\n"); + try outputIndent(state, writer); + _ = try writer.write("pub const http_config = .{\n"); + try outputIndent(child_state, writer); + try writer.print(".method = \"{s}\",\n", .{trait.http.method}); + try outputIndent(child_state, writer); + try writer.print(".uri = \"{s}\",\n", .{trait.http.uri}); + try outputIndent(child_state, writer); + try writer.print(".success_code = {d},\n", .{trait.http.code}); + try outputIndent(state, writer); + _ = try writer.write("};\n\n"); } } - try writer.print(" action_name: []const u8 = \"{s}\",\n", .{operation.name}); - _ = try writer.write(" Request: type = "); + try outputIndent(state, writer); + try writer.print("action_name: []const u8 = \"{s}\",\n", .{operation.name}); + try outputIndent(state, writer); + _ = try writer.write("Request: type = "); if (operation.shape.operation.input) |member| { - try generateTypeFor(allocator, member, shapes, writer, prefix, false, &type_stack, false); + try generateTypeFor(member, shapes, writer, state, false); _ = try writer.write("\n"); - try generateMetadataFunction(service, operation_name, prefix, writer); + try generateMetadataFunction(operation_name, state, writer); } else { _ = try writer.write("struct {\n"); - try generateMetadataFunction(service, operation_name, prefix, writer); + try generateMetadataFunction(operation_name, state, writer); } _ = try writer.write(",\n"); - _ = try writer.write(" Response: type = "); + try outputIndent(state, writer); + _ = try writer.write("Response: type = "); if (operation.shape.operation.output) |member| { - try generateTypeFor(allocator, member, shapes, writer, " ", false, &type_stack, true); + try generateTypeFor(member, shapes, writer, state, true); } else _ = try writer.write("struct {}"); // we want to maintain consistency with other ops _ = try writer.write(",\n"); if (operation.shape.operation.errors) |errors| { - _ = try writer.write(" ServiceError: type = error{\n"); + try outputIndent(state, writer); + _ = try writer.write("ServiceError: type = error{\n"); for (errors) |err| { const err_name = getErrorName(shapes.get(err).?.name); // need to remove "exception" - try writer.print(" {s},\n", .{err_name}); + try outputIndent(child_state, writer); + try writer.print("{s},\n", .{err_name}); } - _ = try writer.write(" },\n"); + try outputIndent(state, writer); + _ = try writer.write("},\n"); } _ = try writer.write("} = .{};\n"); } -fn generateMetadataFunction(_: []const u8, operation_name: []const u8, comptime prefix: []const u8, writer: anytype) !void { +fn generateMetadataFunction(operation_name: []const u8, state: GenerationState, writer: anytype) !void { // TODO: Shove these lines in here, and also the else portion // pub fn metaInfo(self: @This()) struct { service: @TypeOf(sts), action: @TypeOf(sts.get_caller_identity) } { // return .{ .service = sts, .action = sts.get_caller_identity }; // } // We want to add a short "get my parents" function into the response - try writer.print("{s} ", .{prefix}); + var child_state = state; + child_state.indent_level += 1; + try outputIndent(child_state, writer); _ = try writer.write("pub fn metaInfo() struct { "); try writer.print("service_metadata: @TypeOf(service_metadata), action: @TypeOf({s})", .{operation_name}); - _ = try writer.write(" } {\n" ++ prefix ++ " return .{ .service_metadata = service_metadata, "); + _ = try writer.write(" } {\n"); + child_state.indent_level += 1; + try outputIndent(child_state, writer); + _ = try writer.write("return .{ .service_metadata = service_metadata, "); try writer.print(".action = {s}", .{operation_name}); - _ = try writer.write(" };\n" ++ prefix ++ " }\n" ++ prefix ++ "}"); + _ = try writer.write(" };\n"); + child_state.indent_level -= 1; + try outputIndent(child_state, writer); + _ = try writer.write("}\n"); + try outputIndent(state, writer); + try writer.writeByte('}'); } fn getErrorName(err_name: []const u8) []const u8 { if (endsWith("Exception", err_name)) @@ -231,7 +278,7 @@ fn endsWith(item: []const u8, str: []const u8) bool { return std.mem.eql(u8, item, str[str.len - item.len ..]); } /// return type is anyerror!void as this is a recursive function, so the compiler cannot properly infer error types -fn generateTypeFor(allocator: *std.mem.Allocator, shape_id: []const u8, shapes: anytype, writer: anytype, prefix: []const u8, all_required: bool, type_stack: anytype, end_structure: bool) anyerror!void { +fn generateTypeFor(shape_id: []const u8, shapes: std.StringHashMap(smithy.ShapeInfo), writer: anytype, state: GenerationState, end_structure: bool) anyerror!void { if (shapes.get(shape_id) == null) { std.debug.print("Shape ID not found. This is most likely a bug. Shape ID: {s}\n", .{shape_id}); return error.InvalidType; @@ -242,7 +289,7 @@ fn generateTypeFor(allocator: *std.mem.Allocator, shape_id: []const u8, shapes: const shape = shape_info.shape; // Check for ourselves up the stack var self_occurences: u8 = 0; - for (type_stack.items) |i| { + for (state.type_stack.items) |i| { // NOTE: shapes.get isn't providing a consistent pointer - is it allocating each time? // we will therefore need to compare ids if (std.mem.eql(u8, i.*.id, shape_info.id)) @@ -264,60 +311,69 @@ fn generateTypeFor(allocator: *std.mem.Allocator, shape_id: []const u8, shapes: // type to properly reference. Realistically, AWS or the service // must be blocking deep recursion somewhere or this would be a great // DOS attack - try generateSimpleTypeFor("nothing", "[]const u8", writer, all_required); + try generateSimpleTypeFor("nothing", "[]const u8", writer); std.log.warn("Type cycle detected, limiting depth. Type: {s}", .{shape_id}); // std.log.info(" Type stack:\n", .{}); // for (type_stack.items) |i| // std.log.info(" {s}", .{i.*.id}); return; } - try type_stack.append(&shape_info); + try state.type_stack.append(&shape_info); switch (shape) { .structure => { - try generateComplexTypeFor(allocator, shape.structure.members, "struct", shapes, writer, prefix, all_required, type_stack); + try generateComplexTypeFor(shape_id, shape.structure.members, "struct", shapes, writer, state); if (end_structure) { // epilog - try writer.print("{s}", .{prefix}); + try outputIndent(state, writer); _ = try writer.write("}"); } }, .uniontype => { - try generateComplexTypeFor(allocator, shape.uniontype.members, "union", shapes, writer, prefix, all_required, type_stack); + try generateComplexTypeFor(shape_id, shape.uniontype.members, "union", shapes, writer, state); // epilog - try writer.print("{s}", .{prefix}); + try outputIndent(state, writer); _ = try writer.write("}"); }, - .string => |s| try generateSimpleTypeFor(s, "[]const u8", writer, all_required), - .integer => |s| try generateSimpleTypeFor(s, "i64", writer, all_required), + .string => |s| try generateSimpleTypeFor(s, "[]const u8", writer), + .integer => |s| try generateSimpleTypeFor(s, "i64", writer), .list => { _ = try writer.write("[]"); - try generateTypeFor(allocator, shape.list.member_target, shapes, writer, prefix, all_required, type_stack, true); + try generateTypeFor(shape.list.member_target, shapes, writer, state, true); }, .set => { _ = try writer.write("[]"); - try generateTypeFor(allocator, shape.set.member_target, shapes, writer, prefix, all_required, type_stack, true); + try generateTypeFor(shape.set.member_target, shapes, writer, state, true); }, - .timestamp => |s| try generateSimpleTypeFor(s, "i64", writer, all_required), - .blob => |s| try generateSimpleTypeFor(s, "[]const u8", writer, all_required), - .boolean => |s| try generateSimpleTypeFor(s, "bool", writer, all_required), - .double => |s| try generateSimpleTypeFor(s, "f64", writer, all_required), - .float => |s| try generateSimpleTypeFor(s, "f32", writer, all_required), - .long => |s| try generateSimpleTypeFor(s, "i64", writer, all_required), + .timestamp => |s| try generateSimpleTypeFor(s, "i64", writer), + .blob => |s| try generateSimpleTypeFor(s, "[]const u8", writer), + .boolean => |s| try generateSimpleTypeFor(s, "bool", writer), + .double => |s| try generateSimpleTypeFor(s, "f64", writer), + .float => |s| try generateSimpleTypeFor(s, "f32", writer), + .long => |s| try generateSimpleTypeFor(s, "i64", writer), .map => { + // TODO: We need this: + // + // pub fn jsonStringifyField(self: @This(), comptime field_name: []const u8, options: anytype, out_stream: anytype) !bool { + // if (std.mem.eql(u8, "tags", field_name)) + // return try serializeMap(self.tags, self.jsonFieldNameFor("tags"), options, out_stream); + // return false; + // } _ = try writer.write("[]struct {\n"); - const new_prefix = try std.fmt.allocPrint(allocator, " {s}", .{prefix}); - defer allocator.free(new_prefix); - try writer.print("{s} key: ", .{prefix}); - if (!all_required) try writeOptional(shape.map.traits, writer, null); - try generateTypeFor(allocator, shape.map.key, shapes, writer, prefix, all_required, type_stack, true); - if (!all_required) try writeOptional(shape.map.traits, writer, " = null"); + 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); + try generateTypeFor(shape.map.key, shapes, writer, state, true); + try writeOptional(shape.map.traits, writer, " = null"); _ = try writer.write(",\n"); - try writer.print("{s} value: ", .{prefix}); - if (!all_required) try writeOptional(shape.map.traits, writer, null); - try generateTypeFor(allocator, shape.map.key, shapes, writer, prefix, all_required, type_stack, true); - if (!all_required) try writeOptional(shape.map.traits, writer, " = null"); + try outputIndent(child_state, writer); + _ = try writer.write("value: "); + try writeOptional(shape.map.traits, writer, null); + try generateTypeFor(shape.map.key, shapes, writer, state, true); + try writeOptional(shape.map.traits, writer, " = null"); _ = try writer.write(",\n"); - _ = try writer.write(prefix); + try outputIndent(state, writer); _ = try writer.write("}"); }, else => { @@ -326,25 +382,19 @@ fn generateTypeFor(allocator: *std.mem.Allocator, shape_id: []const u8, shapes: // return error{UnimplementedShapeType}.UnimplementedShapeType; }, } - _ = type_stack.pop(); + _ = state.type_stack.pop(); } -// fn generateSimpleTypeFor(shape: anytype, type_name: []const u8, writer: anytype, _: bool) !void { -fn generateSimpleTypeFor(_: anytype, type_name: []const u8, writer: anytype, all_required: bool) !void { - // current compiler checks unused variables, but can't handle multiple unused - // function parameters. We don't want to change the signature in case we need to work with - // these in the future, so this stupid code is only here to trick the compiler - if (all_required or !all_required) - _ = try writer.write(type_name); // This had required stuff but the problem was elsewhere. Better to leave as function just in case +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 } - -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(shape_id: []const u8, members: []smithy.TypeMember, type_type_name: []const u8, shapes: std.StringHashMap(smithy.ShapeInfo), writer: anytype, state: GenerationState) anyerror!void { + _ = shape_id; const Mapping = struct { snake: []const u8, json: []const u8 }; - var json_field_name_mappings = try std.ArrayList(Mapping).initCapacity(allocator, members.len); + var json_field_name_mappings = try std.ArrayList(Mapping).initCapacity(state.allocator, members.len); defer { - for (json_field_name_mappings.items) |mapping| { - allocator.free(mapping.snake); - } + for (json_field_name_mappings.items) |mapping| + state.allocator.free(mapping.snake); json_field_name_mappings.deinit(); } // There is an httpQueryParams trait as well, but nobody is using it. API GW @@ -353,28 +403,26 @@ fn generateComplexTypeFor(allocator: *std.mem.Allocator, members: []smithy.TypeM // 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(allocator, members.len); + var http_query_mappings = try std.ArrayList(Mapping).initCapacity(state.allocator, members.len); defer { - for (http_query_mappings.items) |mapping| { - allocator.free(mapping.snake); - } + for (http_query_mappings.items) |mapping| + state.allocator.free(mapping.snake); http_query_mappings.deinit(); } - var http_header_mappings = try std.ArrayList(Mapping).initCapacity(allocator, members.len); + var http_header_mappings = try std.ArrayList(Mapping).initCapacity(state.allocator, members.len); defer { - for (http_header_mappings.items) |mapping| { - allocator.free(mapping.snake); - } + for (http_header_mappings.items) |mapping| + state.allocator.free(mapping.snake); http_header_mappings.deinit(); } // prolog. We'll rely on caller to get the spacing correct here _ = try writer.write(type_type_name); _ = try writer.write(" {\n"); + var child_state = state; + child_state.indent_level += 1; 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); + const snake_case_member = try snake.fromPascalCase(state.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 @@ -384,20 +432,21 @@ fn generateComplexTypeFor(allocator: *std.mem.Allocator, members: []smithy.TypeM switch (trait) { .json_name => { found_name_trait = true; - json_field_name_mappings.appendAssumeCapacity(.{ .snake = try allocator.dupe(u8, snake_case_member), .json = trait.json_name }); + json_field_name_mappings.appendAssumeCapacity(.{ .snake = try state.allocator.dupe(u8, snake_case_member), .json = trait.json_name }); }, - .http_query => http_query_mappings.appendAssumeCapacity(.{ .snake = try allocator.dupe(u8, snake_case_member), .json = trait.http_query }), - .http_header => http_header_mappings.appendAssumeCapacity(.{ .snake = try allocator.dupe(u8, snake_case_member), .json = trait.http_header }), + .http_query => http_query_mappings.appendAssumeCapacity(.{ .snake = try state.allocator.dupe(u8, snake_case_member), .json = trait.http_query }), + .http_header => http_header_mappings.appendAssumeCapacity(.{ .snake = try state.allocator.dupe(u8, snake_case_member), .json = trait.http_header }), else => {}, } } if (!found_name_trait) - json_field_name_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); - try generateTypeFor(allocator, member.target, shapes, writer, new_prefix, all_required, type_stack, true); - if (!all_required and !std.mem.eql(u8, "union", type_type_name)) + json_field_name_mappings.appendAssumeCapacity(.{ .snake = try state.allocator.dupe(u8, snake_case_member), .json = member.name }); + defer state.allocator.free(snake_case_member); + try outputIndent(child_state, writer); + try writer.print("{s}: ", .{avoidReserved(snake_case_member)}); + try writeOptional(member.traits, writer, null); + try generateTypeFor(member.target, shapes, writer, child_state, true); + if (!std.mem.eql(u8, "union", type_type_name)) try writeOptional(member.traits, writer, " = null"); _ = try writer.write(",\n"); } @@ -409,12 +458,10 @@ fn generateComplexTypeFor(allocator: *std.mem.Allocator, members: []smithy.TypeM // .marker = "Marker", // .max_items = "MaxItems", // }; - var constprefix = try std.fmt.allocPrint(allocator, "{s} ", .{prefix}); - defer allocator.free(constprefix); if (http_query_mappings.items.len > 0) _ = try writer.write("\n"); - try writeMappings(constprefix, "pub ", "http_query", http_query_mappings, writer); + try writeMappings(child_state, "pub ", "http_query", http_query_mappings, false, writer); if (http_query_mappings.items.len > 0 and http_header_mappings.items.len > 0) _ = try writer.write("\n"); - try writeMappings(constprefix, "pub ", "http_header", http_header_mappings, writer); + try writeMappings(child_state, "pub ", "http_header", http_header_mappings, false, writer); // Add in json mappings. The function looks like this: // @@ -426,23 +473,39 @@ fn generateComplexTypeFor(allocator: *std.mem.Allocator, members: []smithy.TypeM // return @field(mappings, field_name); // } // - var fieldnameprefix = try std.fmt.allocPrint(allocator, "{s} ", .{prefix}); - defer allocator.free(fieldnameprefix); - try writer.print("\n{s} pub fn jsonFieldNameFor(_: @This(), comptime field_name: []const u8) []const u8 ", .{prefix}); + try writer.writeByte('\n'); + try outputIndent(child_state, writer); + _ = try writer.write("pub fn jsonFieldNameFor(_: @This(), comptime field_name: []const u8) []const u8 "); _ = try writer.write("{\n"); - try writeMappings(fieldnameprefix, "", "mappings", json_field_name_mappings, writer); - try writer.print("{s} return @field(mappings, field_name);\n{s}", .{ prefix, prefix }); - _ = try writer.write(" }\n"); + var grandchild_state = child_state; + grandchild_state.indent_level += 1; + // We need to force output here becaseu we're referencing the field in the return statement below + try writeMappings(grandchild_state, "", "mappings", json_field_name_mappings, true, writer); + try outputIndent(grandchild_state, writer); + _ = try writer.write("return @field(mappings, field_name);\n"); + try outputIndent(child_state, writer); + _ = try writer.write("}\n"); + + // TODO: Deal with the jsonStringifyField stuff } -fn writeMappings(prefix: []const u8, @"pub": []const u8, mapping_name: []const u8, mappings: anytype, writer: anytype) !void { - if (mappings.items.len == 0) return; - try writer.print("{s}{s}const {s} = .", .{ prefix, @"pub", mapping_name }); - _ = try writer.write("{\n"); - for (mappings.items) |mapping| { - try writer.print("{s} .{s} = \"{s}\",\n", .{ prefix, avoidReserved(mapping.snake), mapping.json }); +fn writeMappings(state: GenerationState, @"pub": []const u8, mapping_name: []const u8, mappings: anytype, force_output: bool, writer: anytype) !void { + if (mappings.items.len == 0 and !force_output) return; + try outputIndent(state, writer); + if (mappings.items.len == 0) { + try writer.print("{s}const {s} = ", .{ @"pub", mapping_name }); + _ = try writer.write(".{};\n"); + return; } - _ = try writer.write(prefix); + try writer.print("{s}const {s} = .", .{ @"pub", mapping_name }); + _ = try writer.write("{\n"); + var child_state = state; + child_state.indent_level += 1; + for (mappings.items) |mapping| { + try outputIndent(child_state, writer); + try writer.print(".{s} = \"{s}\",\n", .{ avoidReserved(mapping.snake), mapping.json }); + } + try outputIndent(state, writer); _ = try writer.write("};\n"); }