Compare commits

...

12 Commits

9 changed files with 253 additions and 57 deletions

View File

@ -2,8 +2,7 @@
[![Build Status](https://drone.lerch.org/api/badges/lobo/aws-sdk-for-zig/status.svg?ref=refs/heads/master)](https://drone.lerch.org/api/badges/lobo/aws-sdk-for-zig/) [![Build Status](https://drone.lerch.org/api/badges/lobo/aws-sdk-for-zig/status.svg?ref=refs/heads/master)](https://drone.lerch.org/api/badges/lobo/aws-sdk-for-zig/)
This SDK currently supports all AWS services. restXml protocol support (4 This SDK currently supports all AWS services except S3. See TODO list below.
services including S3), is still new, with lots of gaps. See TODO list below.
Current executable size for the demo is 1.7M (90k of which is the AWS PEM file, Current executable size for the demo is 1.7M (90k of which is the AWS PEM file,
and approximately 600K for XML services) after compiling with -Drelease-safe and and approximately 600K for XML services) after compiling with -Drelease-safe and
@ -43,14 +42,15 @@ for posterity, and supports x86_64 linux. The old branch is deprecated.
## Limitations ## Limitations
There are many nuances of AWS V4 signature calculation. Parts of S3 are not supported There are many nuances of AWS V4 signature calculation. S3 is not supported
because it uses many of these test cases. WebIdentityToken is not yet because it uses many of these edge cases. Also endpoint calculation is special
implemented. for S3. WebIdentityToken is not yet implemented.
TODO List: TODO List:
* Complete development of [AWS restXml protocol](https://awslabs.github.io/smithy/1.0/spec/aws/aws-restxml-protocol.html). * Implement initial S3 support. This involves:
Includes S3. Total service count 4. * Implementation of AWS SigV4 signature calculation for S3, which is unique
* Implementation of S3 endpoint calculation, which is also unique to this service
* Bump to zig 0.9.1. iguanaTLS, used in zFetch is still [working out 0.9.1 issues](https://github.com/alexnask/iguanaTLS/pull/29) * Bump to zig 0.9.1. iguanaTLS, used in zFetch is still [working out 0.9.1 issues](https://github.com/alexnask/iguanaTLS/pull/29)
* Implement sigv4a signing * Implement sigv4a signing
* Implement jitter/exponential backoff * Implement jitter/exponential backoff

View File

@ -202,7 +202,7 @@ fn run(allocator: std.mem.Allocator, argv: []const []const u8, cwd: ?[]const u8,
try writer.print("{s}\"{s}\"", .{ prefix, arg }); try writer.print("{s}\"{s}\"", .{ prefix, arg });
prefix = " "; prefix = " ";
} }
std.log.info("[RUN] {s}", .{msg.items}); // std.log.debug("[RUN] {s}", .{msg.items});
} }
const result = try std.ChildProcess.exec(.{ const result = try std.ChildProcess.exec(.{

View File

@ -67,11 +67,13 @@ pub fn build(b: *Builder) !void {
var test_step = try tst.addTestStep(b, mode, exe.packages.items); var test_step = try tst.addTestStep(b, mode, exe.packages.items);
test_step.dependOn(&version.step); test_step.dependOn(&version.step);
var codegen: ?*std.build.Step = null;
if (target.getOs().tag == .linux) { if (target.getOs().tag == .linux) {
// TODO: Support > linux with RunStep // TODO: Support > linux with RunStep
// std.build.RunStep.create(null,null).cwd(std.fs.path.resolve(b.build_root, "codegen")).addArgs(...) // std.build.RunStep.create(null,null).cwd(std.fs.path.resolve(b.build_root, "codegen")).addArgs(...)
const codegen = b.step("gen", "Generate zig service code from smithy models"); codegen = b.step("gen", "Generate zig service code from smithy models");
codegen.dependOn(&b.addSystemCommand(&.{ "/bin/sh", "-c", "cd codegen && zig build" }).step); const cg = codegen.?;
cg.dependOn(&b.addSystemCommand(&.{ "/bin/sh", "-c", "cd codegen && zig build" }).step);
// This can probably be triggered instead by GitRepoStep cloning the repo // This can probably be triggered instead by GitRepoStep cloning the repo
// with models // with models
@ -79,20 +81,19 @@ pub fn build(b: *Builder) !void {
// service manifest we know it needs to be regenerated. So this step // service manifest we know it needs to be regenerated. So this step
// will remove the service manifest if codegen has been touched, thereby // will remove the service manifest if codegen has been touched, thereby
// triggering the re-gen // triggering the re-gen
codegen.dependOn(&b.addSystemCommand(&.{ cg.dependOn(&b.addSystemCommand(&.{
"/bin/sh", "-c", "/bin/sh", "-c",
\\ [ ! -f src/models/service_manifest.zig ] || \ \\ [ ! -f src/models/service_manifest.zig ] || \
\\ [ src/models/service_manifest.zig -nt codegen/codegen ] || \ \\ [ $(find codegen -type f -newer src/models/service_manifest.zig -print -quit |wc -c) = '0' ] || \
\\ rm src/models/service_manifest.zig \\ rm src/models/service_manifest.zig
}).step); }).step);
codegen.dependOn(&b.addSystemCommand(&.{ cg.dependOn(&b.addSystemCommand(&.{
"/bin/sh", "-c", "/bin/sh", "-c",
\\ mkdir -p src/models/ && \ \\ mkdir -p src/models/ && \
\\ [ -f src/models/service_manifest.zig ] || \ \\ [ -f src/models/service_manifest.zig ] || \
\\ ( cd codegen/models && ../codegen *.json && mv *.zig ../../src/models ) \\ ( cd codegen/models && ../codegen *.json && mv *.zig ../../src/models )
}).step); }).step);
b.getInstallStep().dependOn(codegen); exe.step.dependOn(cg);
test_step.dependOn(codegen);
} }
exe.install(); exe.install();

View File

@ -257,9 +257,10 @@ fn constantName(allocator: std.mem.Allocator, id: []const u8) ![]const u8 {
// This one might be a bug in snake, but it's the only example so HPDL // This one might be a bug in snake, but it's the only example so HPDL
if (std.mem.eql(u8, id, "SESv2")) return try std.fmt.allocPrint(allocator, "ses_v2", .{}); if (std.mem.eql(u8, id, "SESv2")) return try std.fmt.allocPrint(allocator, "ses_v2", .{});
if (std.mem.eql(u8, id, "CloudFront")) return try std.fmt.allocPrint(allocator, "cloudfront", .{});
// IoT is an acryonym, but snake wouldn't know that. Interestingly not all // IoT is an acryonym, but snake wouldn't know that. Interestingly not all
// iot services are capitalizing that way. // iot services are capitalizing that way.
if (std.mem.eql(u8, id, "IoTSiteWise")) return try std.fmt.allocPrint(allocator, "iot_site_wise", .{}); //sitewise? if (std.mem.eql(u8, id, "IoTSiteWise")) return try std.fmt.allocPrint(allocator, "iot_sitewise", .{});
if (std.mem.eql(u8, id, "IoTFleetHub")) return try std.fmt.allocPrint(allocator, "iot_fleet_hub", .{}); if (std.mem.eql(u8, id, "IoTFleetHub")) return try std.fmt.allocPrint(allocator, "iot_fleet_hub", .{});
if (std.mem.eql(u8, id, "IoTSecureTunneling")) return try std.fmt.allocPrint(allocator, "iot_secure_tunneling", .{}); if (std.mem.eql(u8, id, "IoTSecureTunneling")) return try std.fmt.allocPrint(allocator, "iot_secure_tunneling", .{});
if (std.mem.eql(u8, id, "IoTThingsGraph")) return try std.fmt.allocPrint(allocator, "iot_things_graph", .{}); if (std.mem.eql(u8, id, "IoTThingsGraph")) return try std.fmt.allocPrint(allocator, "iot_things_graph", .{});
@ -579,6 +580,7 @@ fn generateComplexTypeFor(shape_id: []const u8, members: []smithy.TypeMember, ty
_ = try writer.write(" {\n"); _ = try writer.write(" {\n");
var child_state = state; var child_state = state;
child_state.indent_level += 1; child_state.indent_level += 1;
var payload: ?[]const u8 = null;
for (members) |member| { for (members) |member| {
// This is our mapping // This is our mapping
const snake_case_member = try snake.fromPascalCase(state.allocator, member.name); const snake_case_member = try snake.fromPascalCase(state.allocator, member.name);
@ -599,6 +601,14 @@ fn generateComplexTypeFor(shape_id: []const u8, members: []smithy.TypeMember, ty
}, },
.http_query => http_query_mappings.appendAssumeCapacity(.{ .snake = try state.allocator.dupe(u8, snake_case_member), .original = trait.http_query }), .http_query => http_query_mappings.appendAssumeCapacity(.{ .snake = try state.allocator.dupe(u8, snake_case_member), .original = trait.http_query }),
.http_header => http_header_mappings.appendAssumeCapacity(.{ .snake = try state.allocator.dupe(u8, snake_case_member), .original = trait.http_header }), .http_header => http_header_mappings.appendAssumeCapacity(.{ .snake = try state.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);
},
else => {}, else => {},
} }
} }
@ -639,6 +649,12 @@ fn generateComplexTypeFor(shape_id: []const u8, members: []smithy.TypeMember, ty
// return @field(mappings, field_name); // return @field(mappings, field_name);
// } // }
// //
if (payload) |load| {
try writer.writeByte('\n');
try outputIndent(child_state, writer);
try writer.print("pub const http_payload: []const u8 = \"{s}\";", .{load});
}
try writer.writeByte('\n'); try writer.writeByte('\n');
try outputIndent(child_state, writer); try outputIndent(child_state, writer);
_ = try writer.write("pub fn fieldNameFor(_: @This(), comptime field_name: []const u8) []const u8 {\n"); _ = try writer.write("pub fn fieldNameFor(_: @This(), comptime field_name: []const u8) []const u8 {\n");

View File

@ -95,6 +95,7 @@ pub const TraitType = enum {
http_header, http_header,
http_label, http_label,
http_query, http_query,
http_payload,
json_name, json_name,
xml_name, xml_name,
required, required,
@ -128,6 +129,7 @@ pub const Trait = union(TraitType) {
http_header: []const u8, http_header: []const u8,
http_label: []const u8, http_label: []const u8,
http_query: []const u8, http_query: []const u8,
http_payload: struct {},
required: struct {}, required: struct {},
documentation: []const u8, documentation: []const u8,
pattern: []const u8, pattern: []const u8,
@ -573,6 +575,8 @@ fn getTrait(trait_type: []const u8, value: std.json.Value) SmithyParseError!?Tra
return Trait{ .http_query = value.String }; return Trait{ .http_query = value.String };
if (std.mem.eql(u8, trait_type, "smithy.api#httpHeader")) if (std.mem.eql(u8, trait_type, "smithy.api#httpHeader"))
return Trait{ .http_header = value.String }; return Trait{ .http_header = value.String };
if (std.mem.eql(u8, trait_type, "smithy.api#httpPayload"))
return Trait{ .http_payload = .{} };
// TODO: Maybe care about these traits? // TODO: Maybe care about these traits?
if (std.mem.eql(u8, trait_type, "smithy.api#title")) if (std.mem.eql(u8, trait_type, "smithy.api#title"))
@ -600,7 +604,6 @@ fn getTrait(trait_type: []const u8, value: std.json.Value) SmithyParseError!?Tra
\\smithy.api#httpError \\smithy.api#httpError
\\smithy.api#httpChecksumRequired \\smithy.api#httpChecksumRequired
\\smithy.api#httpLabel \\smithy.api#httpLabel
\\smithy.api#httpPayload
\\smithy.api#httpPrefixHeaders \\smithy.api#httpPrefixHeaders
\\smithy.api#httpQueryParams \\smithy.api#httpQueryParams
\\smithy.api#httpResponseCode \\smithy.api#httpResponseCode

View File

@ -268,6 +268,13 @@ pub fn Request(comptime action: anytype) type {
else else
ServerResponse(action); ServerResponse(action);
const NullType: type = u0; // This is a small hack, yes...
const SRawResponse = if (Self.service_meta.aws_protocol != .query and
std.meta.fields(SResponse).len == 1)
std.meta.fields(SResponse)[0].field_type
else
NullType;
const parser_options = json.ParseOptions{ const parser_options = json.ParseOptions{
.allocator = options.client.allocator, .allocator = options.client.allocator,
.allow_camel_case_conversion = true, // new option .allow_camel_case_conversion = true, // new option
@ -288,7 +295,37 @@ pub fn Request(comptime action: anytype) type {
var stream = json.TokenStream.init(response.body); var stream = json.TokenStream.init(response.body);
const parsed_response = json.parse(SResponse, &stream, parser_options) catch |e| { const start = std.mem.indexOf(u8, response.body, "\"") orelse 0; // Should never be 0
if (start == 0) log.warn("Response body missing json key?!", .{});
var end = std.mem.indexOf(u8, response.body[start + 1 ..], "\"") orelse 0;
if (end == 0) log.warn("Response body only has one double quote?!", .{});
end = end + start + 1;
const key = response.body[start + 1 .. end];
log.debug("First json key: {s}", .{key});
const foundNormalJsonResponse = std.mem.eql(u8, key, action.action_name ++ "Response");
const parsed_response_ptr = blk: {
if (SRawResponse == NullType or foundNormalJsonResponse)
break :blk &(json.parse(SResponse, &stream, parser_options) catch |e| {
log.err(
\\Call successful, but unexpected response from service.
\\This could be the result of a bug or a stale set of code generated
\\service models.
\\
\\Model Type: {s}
\\
\\Response from server:
\\
\\{s}
\\
, .{ SResponse, response.body });
return e;
});
log.debug("Appears server has provided a raw response", .{});
const ptr = try options.client.allocator.create(SResponse);
@field(ptr.*, std.meta.fields(SResponse)[0].name) =
json.parse(SRawResponse, &stream, parser_options) catch |e| {
log.err( log.err(
\\Call successful, but unexpected response from service. \\Call successful, but unexpected response from service.
\\This could be the result of a bug or a stale set of code generated \\This could be the result of a bug or a stale set of code generated
@ -303,6 +340,15 @@ pub fn Request(comptime action: anytype) type {
, .{ SResponse, response.body }); , .{ SResponse, response.body });
return e; return e;
}; };
break :blk ptr;
};
// This feels like it should result in a use after free, but it
// seems to be working?
defer if (!(SRawResponse == NullType or foundNormalJsonResponse))
options.client.allocator.destroy(parsed_response_ptr);
const parsed_response = parsed_response_ptr.*;
// TODO: Figure out this hack // TODO: Figure out this hack
// the code setting the response about 10 lines down will trigger // the code setting the response about 10 lines down will trigger
@ -372,7 +418,16 @@ pub fn Request(comptime action: anytype) type {
// //
// Big thing is that requestid, which we'll need to fetch "manually" // Big thing is that requestid, which we'll need to fetch "manually"
const xml_options = xml_shaper.ParseOptions{ .allocator = options.client.allocator }; const xml_options = xml_shaper.ParseOptions{ .allocator = options.client.allocator };
const parsed = try xml_shaper.parse(action.Response, result.body, xml_options); var body: []const u8 = result.body;
var free_body = false;
if (std.mem.lastIndexOf(u8, result.body[result.body.len - 20 ..], "Response>") == null) {
free_body = true;
// chop the "<?xml version="1.0"?>" from the front
const start = if (std.mem.indexOf(u8, result.body, "?>")) |i| i else 0;
body = try std.fmt.allocPrint(options.client.allocator, "<ActionResponse>{s}</ActionResponse>", .{body[start..]});
}
defer if (free_body) options.client.allocator.free(body);
const parsed = try xml_shaper.parse(action.Response, body, xml_options);
errdefer parsed.deinit(); errdefer parsed.deinit();
var free_rid = false; var free_rid = false;
// This needs to get into FullResponseType somehow: defer parsed.deinit(); // This needs to get into FullResponseType somehow: defer parsed.deinit();
@ -390,10 +445,13 @@ pub fn Request(comptime action: anytype) type {
// so we'll use that // so we'll use that
var host_id: ?[]const u8 = null; var host_id: ?[]const u8 = null;
for (result.headers) |header| { for (result.headers) |header| {
if (std.ascii.eqlIgnoreCase(header.name, "x-amz-request-id")) { if (std.ascii.eqlIgnoreCase(header.name, "x-amzn-requestid")) { // CloudFront
rid = header.value; rid = header.value;
} }
if (std.ascii.eqlIgnoreCase(header.name, "x-amz-id-2")) { if (std.ascii.eqlIgnoreCase(header.name, "x-amz-request-id")) { // S3
rid = header.value;
}
if (std.ascii.eqlIgnoreCase(header.name, "x-amz-id-2")) { // S3
host_id = header.value; host_id = header.value;
} }
} }
@ -527,19 +585,23 @@ fn buildPath(allocator: std.mem.Allocator, raw_uri: []const u8, comptime ActionR
var buffer = try std.ArrayList(u8).initCapacity(allocator, raw_uri.len); var buffer = try std.ArrayList(u8).initCapacity(allocator, raw_uri.len);
// const writer = buffer.writer(); // const writer = buffer.writer();
defer buffer.deinit(); defer buffer.deinit();
var in_var = false; var in_label = false;
var start: usize = 0; var start: usize = 0;
for (raw_uri) |c, inx| { for (raw_uri) |c, inx| {
switch (c) { switch (c) {
'{' => { '{' => {
in_var = true; in_label = true;
start = inx + 1; start = inx + 1;
}, },
'}' => { '}' => {
in_var = false; in_label = false;
const replacement_var = raw_uri[start..inx]; // The label may be "greedy" (uses a '+' at the end), but
// it's not clear if that effects this processing
var end = inx;
if (raw_uri[inx - 1] == '+') end -= 1;
const replacement_label = raw_uri[start..end];
inline for (std.meta.fields(ActionRequest)) |field| { inline for (std.meta.fields(ActionRequest)) |field| {
if (std.mem.eql(u8, request.fieldNameFor(field.name), replacement_var)) { if (std.mem.eql(u8, request.fieldNameFor(field.name), replacement_label)) {
var replacement_buffer = try std.ArrayList(u8).initCapacity(allocator, raw_uri.len); var replacement_buffer = try std.ArrayList(u8).initCapacity(allocator, raw_uri.len);
defer replacement_buffer.deinit(); defer replacement_buffer.deinit();
var encoded_buffer = try std.ArrayList(u8).initCapacity(allocator, raw_uri.len); var encoded_buffer = try std.ArrayList(u8).initCapacity(allocator, raw_uri.len);
@ -557,7 +619,7 @@ fn buildPath(allocator: std.mem.Allocator, raw_uri: []const u8, comptime ActionR
} }
} }
}, },
else => if (!in_var) { else => if (!in_label) {
try buffer.append(c); try buffer.append(c);
} else {}, } else {},
} }

View File

@ -60,6 +60,7 @@ const EndPoint = struct {
fn deinit(self: EndPoint) void { fn deinit(self: EndPoint) void {
self.allocator.free(self.uri); self.allocator.free(self.uri);
self.allocator.free(self.host);
} }
}; };
pub const AwsHttp = struct { pub const AwsHttp = struct {
@ -94,14 +95,31 @@ pub const AwsHttp = struct {
/// service called, and will set up the signing options. The return /// service called, and will set up the signing options. The return
/// value is simply a raw HttpResult /// value is simply a raw HttpResult
pub fn callApi(self: Self, service: []const u8, request: HttpRequest, options: Options) !HttpResult { pub fn callApi(self: Self, service: []const u8, request: HttpRequest, options: Options) !HttpResult {
const endpoint = try regionSubDomain(self.allocator, service, options.region, options.dualstack); // This function or regionSubDomain needs altering for virtual host
// addressing (for S3). Botocore, and I suspect other SDKs, have
// hardcoded exceptions for S3:
// https://github.com/boto/botocore/blob/f2b0dbb800b8dc2a3541334d5ca1190faf900150/botocore/utils.py#L2160-L2181
// Boto assumes virtual host addressing unless the endpoint url is configured
//
// NOTE: There are 4 rest_xml services. They are:
// * CloudFront
// * Route53
// * S3
// * S3 control
//
// All 4 are non-standard. Route53 and CloudFront are global endpoints
// S3 uses virtual host addressing (except when it doesn't), and
// S3 control uses <account-id>.s3-control.<region>.amazonaws.com
//
// So this regionSubDomain call needs to handle generic customization
const endpoint = try endpointForRequest(self.allocator, service, options.region, options.dualstack);
defer endpoint.deinit(); defer endpoint.deinit();
log.debug("Calling endpoint {s}", .{endpoint.uri}); log.debug("Calling endpoint {s}", .{endpoint.uri});
// TODO: Should we allow customization here? // TODO: Should we allow customization here?
const creds = try credentials.getCredentials(self.allocator, .{}); const creds = try credentials.getCredentials(self.allocator, .{});
defer creds.deinit(); defer creds.deinit();
const signing_config: signing.Config = .{ const signing_config: signing.Config = .{
.region = options.region, .region = getRegion(service, options.region),
.service = options.sigv4_service_name orelse service, .service = options.sigv4_service_name orelse service,
.credentials = creds, .credentials = creds,
}; };
@ -161,6 +179,11 @@ pub const AwsHttp = struct {
const url = try std.fmt.allocPrint(self.allocator, "{s}{s}{s}", .{ endpoint.uri, request.path, request.query }); const url = try std.fmt.allocPrint(self.allocator, "{s}{s}{s}", .{ endpoint.uri, request.path, request.query });
defer self.allocator.free(url); defer self.allocator.free(url);
log.debug("Request url: {s}", .{url}); log.debug("Request url: {s}", .{url});
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// PLEASE READ!! IF YOU ARE LOOKING AT THIS LINE OF CODE DUE TO A
// SEGFAULT IN INIT, IT IS PROBABLY BECAUSE THE HOST DOES NOT EXIST
// https://github.com/ziglang/zig/issues/11358
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
var req = try zfetch.Request.init(self.allocator, url, self.trust_chain); var req = try zfetch.Request.init(self.allocator, url, self.trust_chain);
defer req.deinit(); defer req.deinit();
@ -168,7 +191,7 @@ pub const AwsHttp = struct {
try req.do(method, headers, if (request_cp.body.len == 0) null else request_cp.body); try req.do(method, headers, if (request_cp.body.len == 0) null else request_cp.body);
// TODO: Timeout - is this now above us? // TODO: Timeout - is this now above us?
log.debug("request_complete. Response code {d}: {s}", .{ req.status.code, req.status.reason }); log.debug("Request Complete. Response code {d}: {s}", .{ req.status.code, req.status.reason });
log.debug("Response headers:", .{}); log.debug("Response headers:", .{});
var resp_headers = try std.ArrayList(Header).initCapacity(self.allocator, req.headers.list.items.len); var resp_headers = try std.ArrayList(Header).initCapacity(self.allocator, req.headers.list.items.len);
defer resp_headers.deinit(); defer resp_headers.deinit();
@ -204,6 +227,11 @@ pub const AwsHttp = struct {
} }
}; };
fn getRegion(service: []const u8, region: []const u8) []const u8 {
if (std.mem.eql(u8, service, "cloudfront")) return "us-east-1";
return region;
}
fn addHeaders(allocator: std.mem.Allocator, headers: *std.ArrayList(base.Header), host: []const u8, body: []const u8, content_type: []const u8, additional_headers: []Header) !?[]const u8 { fn addHeaders(allocator: std.mem.Allocator, headers: *std.ArrayList(base.Header), host: []const u8, body: []const u8, content_type: []const u8, additional_headers: []Header) !?[]const u8 {
try headers.append(.{ .name = "Accept", .value = "application/json" }); try headers.append(.{ .name = "Accept", .value = "application/json" });
try headers.append(.{ .name = "Host", .value = host }); try headers.append(.{ .name = "Host", .value = host });
@ -225,15 +253,24 @@ fn getEnvironmentVariable(allocator: std.mem.Allocator, key: []const u8) !?[]con
}; };
} }
fn regionSubDomain(allocator: std.mem.Allocator, service: []const u8, region: []const u8, useDualStack: bool) !EndPoint { fn endpointForRequest(allocator: std.mem.Allocator, service: []const u8, region: []const u8, use_dual_stack: bool) !EndPoint {
const environment_override = try getEnvironmentVariable(allocator, "AWS_ENDPOINT_URL"); const environment_override = try getEnvironmentVariable(allocator, "AWS_ENDPOINT_URL");
if (environment_override) |override| { if (environment_override) |override| {
const uri = try allocator.dupeZ(u8, override); const uri = try allocator.dupeZ(u8, override);
return endPointFromUri(allocator, uri); return endPointFromUri(allocator, uri);
} }
if (std.mem.eql(u8, service, "cloudfront")) {
return EndPoint{
.uri = try allocator.dupe(u8, "https://cloudfront.amazonaws.com"),
.host = try allocator.dupe(u8, "cloudfront.amazonaws.com"),
.scheme = "https",
.port = 443,
.allocator = allocator,
};
}
// Fallback to us-east-1 if global endpoint does not exist. // Fallback to us-east-1 if global endpoint does not exist.
const realregion = if (std.mem.eql(u8, region, "aws-global")) "us-east-1" else region; const realregion = if (std.mem.eql(u8, region, "aws-global")) "us-east-1" else region;
const dualstack = if (useDualStack) ".dualstack" else ""; const dualstack = if (use_dual_stack) ".dualstack" else "";
const domain = switch (std.hash_map.hashString(region)) { const domain = switch (std.hash_map.hashString(region)) {
US_ISO_EAST_1_HASH => "c2s.ic.gov", US_ISO_EAST_1_HASH => "c2s.ic.gov",
@ -243,7 +280,7 @@ fn regionSubDomain(allocator: std.mem.Allocator, service: []const u8, region: []
}; };
const uri = try std.fmt.allocPrintZ(allocator, "https://{s}{s}.{s}.{s}", .{ service, dualstack, realregion, domain }); const uri = try std.fmt.allocPrintZ(allocator, "https://{s}{s}.{s}.{s}", .{ service, dualstack, realregion, domain });
const host = uri["https://".len..]; const host = try allocator.dupe(u8, uri["https://".len..]);
log.debug("host: {s}, scheme: {s}, port: {}", .{ host, "https", 443 }); log.debug("host: {s}, scheme: {s}, port: {}", .{ host, "https", 443 });
return EndPoint{ return EndPoint{
.uri = uri, .uri = uri,
@ -291,7 +328,7 @@ fn endPointFromUri(allocator: std.mem.Allocator, uri: []const u8) !EndPoint {
if (host_end == 0) { if (host_end == 0) {
host_end = uri.len; host_end = uri.len;
} }
host = uri[host_start..host_end]; host = try allocator.dupe(u8, uri[host_start..host_end]);
log.debug("host: {s}, scheme: {s}, port: {}", .{ host, scheme, port }); log.debug("host: {s}, scheme: {s}, port: {}", .{ host, scheme, port });
return EndPoint{ return EndPoint{
@ -302,3 +339,25 @@ fn endPointFromUri(allocator: std.mem.Allocator, uri: []const u8) !EndPoint {
.port = port, .port = port,
}; };
} }
test "endpointForRequest standard operation" {
const allocator = std.testing.allocator;
const service = "dynamodb";
const region = "us-west-2";
const use_dual_stack = false;
const endpoint = try endpointForRequest(allocator, service, region, use_dual_stack);
defer endpoint.deinit();
try std.testing.expectEqualStrings("https://dynamodb.us-west-2.amazonaws.com", endpoint.uri);
}
test "endpointForRequest for cloudfront" {
const allocator = std.testing.allocator;
const service = "cloudfront";
const region = "us-west-2";
const use_dual_stack = false;
const endpoint = try endpointForRequest(allocator, service, region, use_dual_stack);
defer endpoint.deinit();
try std.testing.expectEqualStrings("https://cloudfront.amazonaws.com", endpoint.uri);
}

View File

@ -49,6 +49,7 @@ const Tests = enum {
rest_json_1_query_with_input, rest_json_1_query_with_input,
rest_json_1_work_with_lambda, rest_json_1_work_with_lambda,
rest_xml_no_input, rest_xml_no_input,
rest_xml_anything_but_s3,
}; };
pub fn main() anyerror!void { pub fn main() anyerror!void {
@ -89,7 +90,7 @@ pub fn main() anyerror!void {
}; };
defer client.deinit(); defer client.deinit();
const services = aws.Services(.{ .sts, .ec2, .dynamo_db, .ecs, .lambda, .sqs, .s3 }){}; const services = aws.Services(.{ .sts, .ec2, .dynamo_db, .ecs, .lambda, .sqs, .s3, .cloudfront }){};
for (tests.items) |t| { for (tests.items) |t| {
std.log.info("===== Start Test: {s} =====", .{@tagName(t)}); std.log.info("===== Start Test: {s} =====", .{@tagName(t)});
@ -221,6 +222,14 @@ pub fn main() anyerror!void {
std.log.info("request id: {s}", .{result.response_metadata.request_id}); std.log.info("request id: {s}", .{result.response_metadata.request_id});
std.log.info("bucket count: {d}", .{result.response.buckets.?.len}); std.log.info("bucket count: {d}", .{result.response.buckets.?.len});
}, },
.rest_xml_anything_but_s3 => {
const result = try client.call(services.cloudfront.list_key_groups.Request{}, options);
defer result.deinit();
std.log.info("request id: {s}", .{result.response_metadata.request_id});
const list = result.response.key_group_list.?;
std.log.info("key group list max: {d}", .{list.max_items});
std.log.info("key group quantity: {d}", .{list.quantity});
},
} }
std.log.info("===== End Test: {s} =====\n", .{@tagName(t)}); std.log.info("===== End Test: {s} =====\n", .{@tagName(t)});
} }

View File

@ -145,7 +145,8 @@ fn parseInternal(comptime T: type, element: *xml.Element, options: ParseOptions)
if (element.children.items.len == 0) { if (element.children.items.len == 0) {
// This is almost certainly incomplete. Empty strings? xsi:nil? // This is almost certainly incomplete. Empty strings? xsi:nil?
return null; return null;
} else { }
if (element.children.items.len > 0) {
// return try parseInternal(optional_info.child, element.elements().next().?, options); // return try parseInternal(optional_info.child, element.elements().next().?, options);
return try parseInternal(optional_info.child, element, options); return try parseInternal(optional_info.child, element, options);
} }
@ -180,9 +181,8 @@ fn parseInternal(comptime T: type, element: *xml.Element, options: ParseOptions)
// } // }
// } // }
return error.NoUnionMembersMatched; return error.NoUnionMembersMatched;
} else {
@compileError("Unable to parse into untagged union '" ++ @typeName(T) ++ "'");
} }
@compileError("Unable to parse into untagged union '" ++ @typeName(T) ++ "'");
}, },
.Struct => |struct_info| { .Struct => |struct_info| {
var r: T = undefined; var r: T = undefined;
@ -239,12 +239,17 @@ fn parseInternal(comptime T: type, element: *xml.Element, options: ParseOptions)
fields_set = fields_set + 1; fields_set = fields_set + 1;
found_value = true; found_value = true;
} }
if (@typeInfo(field.field_type) == .Optional and !found_value) { if (@typeInfo(field.field_type) == .Optional) {
// Test "compiler assertion failure 2"
// Zig compiler bug circa 0.9.0. Using "and !found_value"
// in the if statement above will trigger assertion failure
if (!found_value) {
// @compileLog("Optional: Field name ", field.name, ", type ", field.field_type); // @compileLog("Optional: Field name ", field.name, ", type ", field.field_type);
@field(r, field.name) = null; @field(r, field.name) = null;
fields_set = fields_set + 1; fields_set = fields_set + 1;
found_value = true; found_value = true;
} }
}
// Using this else clause breaks zig, so we'll use a boolean instead // Using this else clause breaks zig, so we'll use a boolean instead
if (!found_value) { if (!found_value) {
log.err("Could not find a value for field {s}. Looking for {s} in element {s}", .{ field.name, name, element.tag }); log.err("Could not find a value for field {s}. Looking for {s} in element {s}", .{ field.name, name, element.tag });
@ -474,7 +479,8 @@ test "can coerce 8601 date to integer" {
defer parsed_data.deinit(); defer parsed_data.deinit();
try testing.expectEqual(@as(i64, 1633451985), parsed_data.parsed_value.foo_bar.?); try testing.expectEqual(@as(i64, 1633451985), parsed_data.parsed_value.foo_bar.?);
} }
// This is the simplest test so far that breaks zig // This is the simplest test so far that breaks zig (circa 0.9.0)
// See "Using this else clause breaks zig, so we'll use a boolean instead"
test "can parse a boolean type (two fields)" { test "can parse a boolean type (two fields)" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
const data = const data =
@ -642,3 +648,43 @@ test "can parse something serious" {
try testing.expectEqualStrings("eu-north-1", parsed_data.parsed_value.regions.?[0].region_name.?); try testing.expectEqualStrings("eu-north-1", parsed_data.parsed_value.regions.?[0].region_name.?);
try testing.expectEqualStrings("ec2.eu-north-1.amazonaws.com", parsed_data.parsed_value.regions.?[0].endpoint.?); try testing.expectEqualStrings("ec2.eu-north-1.amazonaws.com", parsed_data.parsed_value.regions.?[0].endpoint.?);
} }
test "compiler assertion failure 2" {
// std.testing.log_level = .debug;
// log.debug("", .{});
// Actually, we only care here that the code compiles
const allocator = std.testing.allocator;
const Response: type = struct {
key_group_list: ?struct {
quantity: i64, // Making this optional will make the code compile
items: ?[]struct {
key_group: []const u8,
} = null,
pub fn fieldNameFor(_: @This(), comptime field_name: []const u8) []const u8 {
const mappings = .{
.quantity = "Quantity",
.items = "Items",
};
return @field(mappings, field_name);
}
} = null,
pub fn fieldNameFor(_: @This(), comptime field_name: []const u8) []const u8 {
const mappings = .{
.key_group_list = "KeyGroupList",
};
return @field(mappings, field_name);
}
};
const data =
\\<?xml version="1.0" encoding="UTF-8"?>
\\<AnythingAtAll xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
\\ <KeyGroupList>
\\ <Quantity>42</Quantity>
\\ </KeyGroupList>
\\</AnythingAtAll>
;
const parsed_data = try parse(Response, data, .{ .allocator = allocator });
defer parsed_data.deinit();
try testing.expect(parsed_data.parsed_value.key_group_list.?.quantity == 42);
}