rest json v1 basic requests (responses tbd)
All checks were successful
continuous-integration/drone/push Build is passing

This provides initial implementation with some basic calls
working. Only GET requests have been verified so far.
Http header support missing. POST/PUT might work, but
have not been tested. Currently the demo tests are
failing due to a response deserialization issue that
is still being debugged
This commit is contained in:
Emil Lerch 2021-08-13 18:12:05 -07:00
parent 6f53ed6dcf
commit 8e853e9a82
Signed by: lobo
GPG Key ID: A7B62D657EF764F8
4 changed files with 196 additions and 49 deletions

View File

@ -340,12 +340,32 @@ 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);
var json_field_name_mappings = try std.ArrayList(Mapping).initCapacity(allocator, members.len);
defer {
for (mappings.items) |mapping| {
for (json_field_name_mappings.items) |mapping| {
allocator.free(mapping.snake);
}
mappings.deinit();
json_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(allocator, members.len);
defer {
for (http_query_mappings.items) |mapping| {
allocator.free(mapping.snake);
}
http_query_mappings.deinit();
}
var http_header_mappings = try std.ArrayList(Mapping).initCapacity(allocator, members.len);
defer {
for (http_header_mappings.items) |mapping| {
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);
@ -359,15 +379,20 @@ fn generateComplexTypeFor(allocator: *std.mem.Allocator, members: []smithy.TypeM
// 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;
var found_name_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 });
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 });
},
.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 }),
else => {},
}
}
if (!found_trait)
mappings.appendAssumeCapacity(.{ .snake = try allocator.dupe(u8, snake_case_member), .json = member.name });
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);
@ -377,6 +402,20 @@ fn generateComplexTypeFor(allocator: *std.mem.Allocator, members: []smithy.TypeM
_ = try writer.write(",\n");
}
// Add in http query metadata (only relevant to REST JSON APIs - do we care?
// pub const http_query = .{
// .master_region = "MasterRegion",
// .function_version = "FunctionVersion",
// .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);
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);
// Add in json mappings. The function looks like this:
//
// pub fn jsonFieldNameFor(_: @This(), comptime field_name: []const u8) []const u8 {
@ -387,19 +426,24 @@ fn generateComplexTypeFor(allocator: *std.mem.Allocator, members: []smithy.TypeM
// 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.
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.write("{\n");
try writer.print("{s} const mappings = .", .{prefix});
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");
}
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 });
}
_ = 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");
_ = try writer.write("};\n");
}
fn writeOptional(traits: ?[]smithy.Trait, writer: anytype, value: ?[]const u8) !void {

View File

@ -92,6 +92,9 @@ pub const TraitType = enum {
aws_protocol,
ec2_query_name,
http,
http_header,
http_label,
http_query,
json_name,
required,
documentation,
@ -120,6 +123,9 @@ pub const Trait = union(TraitType) {
uri: []const u8,
code: i64 = 200,
},
http_header: []const u8,
http_label: []const u8,
http_query: []const u8,
required: struct {},
documentation: []const u8,
pattern: []const u8,
@ -559,6 +565,10 @@ fn getTrait(trait_type: []const u8, value: std.json.Value) SmithyParseError!?Tra
}
if (std.mem.eql(u8, trait_type, "smithy.api#jsonName"))
return Trait{ .json_name = value.String };
if (std.mem.eql(u8, trait_type, "smithy.api#httpQuery"))
return Trait{ .http_query = value.String };
if (std.mem.eql(u8, trait_type, "smithy.api#httpHeader"))
return Trait{ .http_header = value.String };
// TODO: Maybe care about these traits?
if (std.mem.eql(u8, trait_type, "smithy.api#title"))
@ -583,14 +593,11 @@ fn getTrait(trait_type: []const u8, value: std.json.Value) SmithyParseError!?Tra
\\smithy.api#eventPayload
\\smithy.api#externalDocumentation
\\smithy.api#hostLabel
\\smithy.api#http
\\smithy.api#httpError
\\smithy.api#httpChecksumRequired
\\smithy.api#httpHeader
\\smithy.api#httpLabel
\\smithy.api#httpPayload
\\smithy.api#httpPrefixHeaders
\\smithy.api#httpQuery
\\smithy.api#httpQueryParams
\\smithy.api#httpResponseCode
\\smithy.api#idempotencyToken

View File

@ -11,6 +11,7 @@ const log = std.log.scoped(.aws);
pub const Options = struct {
region: []const u8 = "aws-global",
dualstack: bool = false,
success_http_code: i64 = 200,
};
/// Using this constant may blow up build times. Recommed using Services()
@ -65,12 +66,45 @@ pub const Aws = struct {
switch (service_meta.aws_protocol) {
.query => return self.callQuery(request, service_meta, action, options),
// .query, .ec2_query => return self.callQuery(request, service_meta, action, options),
.rest_json_1, .json_1_0, .json_1_1 => return self.callJson(request, service_meta, action, options),
.json_1_0, .json_1_1 => return self.callJson(request, service_meta, action, options),
.rest_json_1 => return self.callRestJson(request, service_meta, action, options),
.ec2_query, .rest_xml => @compileError("XML responses may be blocked on a zig compiler bug scheduled to be fixed in 0.9.0"),
}
}
/// Calls using one of the json protocols (rest_json_1, json_1_0, json_1_1
/// Rest Json is the most complex and so we handle this seperately
fn callRestJson(self: Self, comptime request: anytype, comptime service_meta: anytype, action: anytype, options: Options) !FullResponse(request) {
const Action = @TypeOf(action);
var aws_request: awshttp.HttpRequest = .{
.method = Action.http_config.method,
.content_type = "application/json",
.path = Action.http_config.uri,
};
log.debug("Rest JSON v1 method: {s}", .{aws_request.method});
log.debug("Rest JSON v1 success code: {d}", .{Action.http_config.success_code});
log.debug("Rest JSON v1 raw uri: {s}", .{Action.http_config.uri});
aws_request.query = try buildQuery(self.allocator, request);
log.debug("Rest JSON v1 query: {s}", .{aws_request.query});
defer self.allocator.free(aws_request.query);
// We don't know if we need a body...guessing here, this should cover most
var buffer = std.ArrayList(u8).init(self.allocator);
defer buffer.deinit();
var nameAllocator = std.heap.ArenaAllocator.init(self.allocator);
defer nameAllocator.deinit();
if (std.mem.eql(u8, "PUT", aws_request.method) or std.mem.eql(u8, "POST", aws_request.method)) {
try json.stringify(request, .{ .whitespace = .{} }, buffer.writer());
}
return try self.callAws(request, service_meta, aws_request, .{
.success_http_code = Action.http_config.success_code,
.region = options.region,
.dualstack = options.dualstack,
});
}
/// Calls using one of the json protocols (json_1_0, json_1_1)
fn callJson(self: Self, comptime request: anytype, comptime service_meta: anytype, action: anytype, options: Options) !FullResponse(request) {
const target =
try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{
@ -97,7 +131,6 @@ pub const Aws = struct {
var content_type: []const u8 = undefined;
switch (service_meta.aws_protocol) {
.rest_json_1 => content_type = "application/json",
.json_1_0 => content_type = "application/x-amz-json-1.0",
.json_1_1 => content_type = "application/x-amz-json-1.1",
else => unreachable,
@ -377,6 +410,53 @@ fn queryFieldTransformer(field_name: []const u8, encoding_options: url.EncodingO
return try case.snakeToPascal(encoding_options.allocator.?, field_name);
}
fn buildQuery(allocator: *std.mem.Allocator, comptime request: anytype) ![]const u8 {
// query should look something like this:
// pub const http_query = .{
// .master_region = "MasterRegion",
// .function_version = "FunctionVersion",
// .marker = "Marker",
// };
const query_arguments = @TypeOf(request).http_query;
var buffer = std.ArrayList(u8).init(allocator);
const writer = buffer.writer();
defer buffer.deinit();
var has_begun = false;
inline for (@typeInfo(@TypeOf(query_arguments)).Struct.fields) |arg| {
const val = @field(request, arg.name);
if (@typeInfo(@TypeOf(val)) == .Optional) {
if (val) |v| {
try addQueryArg(@field(query_arguments, arg.name), v, writer, !has_begun);
has_begun = true;
}
} else {
try addQueryArg(@field(query_arguments, arg.name), val, writer, !has_begun);
has_begun = true;
}
}
return buffer.toOwnedSlice();
}
fn addQueryArg(key: []const u8, value: anytype, writer: anytype, start: bool) !void {
if (start)
_ = try writer.write("?")
else
_ = try writer.write("&");
// TODO: url escaping
try writer.print("{s}=", .{key});
try json.stringify(value, .{}, writer);
}
test "REST Json v1 builds proper queries" {
const allocator = std.testing.allocator;
const svs = Services(.{.lambda}){};
const request = svs.lambda.list_functions.Request{
.max_items = 1,
};
const query = try buildQuery(allocator, request);
defer allocator.free(query);
try std.testing.expectEqualStrings("?MaxItems=1", query);
}
test "basic json request serialization" {
const allocator = std.testing.allocator;
const svs = Services(.{.dynamo_db}){};

View File

@ -31,6 +31,8 @@ const Tests = enum {
json_1_0_query_no_input,
json_1_1_query_with_input,
json_1_1_query_no_input,
rest_json_1_query_no_input,
rest_json_1_query_with_input,
};
pub fn main() anyerror!void {
@ -69,53 +71,67 @@ pub fn main() anyerror!void {
var client = aws.Aws.init(allocator);
defer client.deinit();
const services = aws.Services(.{ .sts, .ec2, .dynamo_db, .ecs }){};
const services = aws.Services(.{ .sts, .ec2, .dynamo_db, .ecs, .lambda }){};
for (tests.items) |t| {
std.log.info("===== Start Test: {s} =====", .{@tagName(t)});
switch (t) {
.query_no_input => {
const resp = try client.call(services.sts.get_caller_identity.Request{}, options);
defer resp.deinit();
std.log.info("arn: {s}", .{resp.response.arn});
std.log.info("id: {s}", .{resp.response.user_id});
std.log.info("account: {s}", .{resp.response.account});
std.log.info("requestId: {s}", .{resp.response_metadata.request_id});
const call = try client.call(services.sts.get_caller_identity.Request{}, options);
defer call.deinit();
std.log.info("arn: {s}", .{call.response.arn});
std.log.info("id: {s}", .{call.response.user_id});
std.log.info("account: {s}", .{call.response.account});
std.log.info("requestId: {s}", .{call.response_metadata.request_id});
},
.query_with_input => {
// TODO: Find test without sensitive info
const access = try client.call(services.sts.get_session_token.Request{
const call = try client.call(services.sts.get_session_token.Request{
.duration_seconds = 900,
}, options);
defer access.deinit();
std.log.info("access key: {s}", .{access.response.credentials.?.access_key_id});
defer call.deinit();
std.log.info("call key: {s}", .{call.response.credentials.?.access_key_id});
},
.json_1_0_query_with_input => {
const tables = try client.call(services.dynamo_db.list_tables.Request{
const call = try client.call(services.dynamo_db.list_tables.Request{
.limit = 1,
}, options);
defer tables.deinit();
std.log.info("request id: {s}", .{tables.response_metadata.request_id});
std.log.info("account has tables: {b}", .{tables.response.table_names.?.len > 0});
defer call.deinit();
std.log.info("request id: {s}", .{call.response_metadata.request_id});
std.log.info("account has call: {b}", .{call.response.table_names.?.len > 0});
},
.json_1_0_query_no_input => {
const limits = try client.call(services.dynamo_db.describe_limits.Request{}, options);
defer limits.deinit();
std.log.info("account read capacity limit: {d}", .{limits.response.account_max_read_capacity_units});
const call = try client.call(services.dynamo_db.describe_limits.Request{}, options);
defer call.deinit();
std.log.info("account read capacity limit: {d}", .{call.response.account_max_read_capacity_units});
},
.json_1_1_query_with_input => {
const clusters = try client.call(services.ecs.list_clusters.Request{
const call = try client.call(services.ecs.list_clusters.Request{
.max_results = 1,
}, options);
defer clusters.deinit();
std.log.info("request id: {s}", .{clusters.response_metadata.request_id});
std.log.info("account has clusters: {b}", .{clusters.response.cluster_arns.?.len > 0});
defer call.deinit();
std.log.info("request id: {s}", .{call.response_metadata.request_id});
std.log.info("account has call: {b}", .{call.response.cluster_arns.?.len > 0});
},
.json_1_1_query_no_input => {
const clusters = try client.call(services.ecs.list_clusters.Request{}, options);
defer clusters.deinit();
std.log.info("request id: {s}", .{clusters.response_metadata.request_id});
std.log.info("account has clusters: {b}", .{clusters.response.cluster_arns.?.len > 0});
const call = try client.call(services.ecs.list_clusters.Request{}, options);
defer call.deinit();
std.log.info("request id: {s}", .{call.response_metadata.request_id});
std.log.info("account has call: {b}", .{call.response.cluster_arns.?.len > 0});
},
.rest_json_1_query_with_input => {
const call = try client.call(services.lambda.list_functions.Request{
.max_items = 1,
}, options);
defer call.deinit();
std.log.info("request id: {s}", .{call.response_metadata.request_id});
std.log.info("account has call: {b}", .{call.response.functions.?.len > 0});
},
.rest_json_1_query_no_input => {
const call = try client.call(services.lambda.list_functions.Request{}, options);
defer call.deinit();
std.log.info("request id: {s}", .{call.response_metadata.request_id});
std.log.info("account has call: {b}", .{call.response.functions.?.len > 0});
},
.ec2_query_no_input => {
std.log.err("EC2 Test disabled due to compiler bug", .{});