Compare commits
12 Commits
f009bb5c37
...
357c583e9b
Author | SHA1 | Date | |
---|---|---|---|
357c583e9b | |||
73d056e845 | |||
652a04d0c3 | |||
46e3a4de11 | |||
803581f647 | |||
611849bde6 | |||
f4e72ba961 | |||
fed45c90ee | |||
c1880e2d43 | |||
51232cb45e | |||
e4e5288ef6 | |||
f5e1019a39 |
14
README.md
14
README.md
|
@ -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/)
|
||||
|
||||
This SDK currently supports all AWS services. restXml protocol support (4
|
||||
services including S3), is still new, with lots of gaps. See TODO list below.
|
||||
This SDK currently supports all AWS services except S3. See TODO list below.
|
||||
|
||||
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
|
||||
|
@ -43,14 +42,15 @@ for posterity, and supports x86_64 linux. The old branch is deprecated.
|
|||
|
||||
## Limitations
|
||||
|
||||
There are many nuances of AWS V4 signature calculation. Parts of S3 are not supported
|
||||
because it uses many of these test cases. WebIdentityToken is not yet
|
||||
implemented.
|
||||
There are many nuances of AWS V4 signature calculation. S3 is not supported
|
||||
because it uses many of these edge cases. Also endpoint calculation is special
|
||||
for S3. WebIdentityToken is not yet implemented.
|
||||
|
||||
TODO List:
|
||||
|
||||
* Complete development of [AWS restXml protocol](https://awslabs.github.io/smithy/1.0/spec/aws/aws-restxml-protocol.html).
|
||||
Includes S3. Total service count 4.
|
||||
* Implement initial S3 support. This involves:
|
||||
* 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)
|
||||
* Implement sigv4a signing
|
||||
* Implement jitter/exponential backoff
|
||||
|
|
|
@ -202,7 +202,7 @@ fn run(allocator: std.mem.Allocator, argv: []const []const u8, cwd: ?[]const u8,
|
|||
try writer.print("{s}\"{s}\"", .{ prefix, arg });
|
||||
prefix = " ";
|
||||
}
|
||||
std.log.info("[RUN] {s}", .{msg.items});
|
||||
// std.log.debug("[RUN] {s}", .{msg.items});
|
||||
}
|
||||
|
||||
const result = try std.ChildProcess.exec(.{
|
||||
|
|
15
build.zig
15
build.zig
|
@ -67,11 +67,13 @@ pub fn build(b: *Builder) !void {
|
|||
var test_step = try tst.addTestStep(b, mode, exe.packages.items);
|
||||
test_step.dependOn(&version.step);
|
||||
|
||||
var codegen: ?*std.build.Step = null;
|
||||
if (target.getOs().tag == .linux) {
|
||||
// TODO: Support > linux with RunStep
|
||||
// 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.dependOn(&b.addSystemCommand(&.{ "/bin/sh", "-c", "cd codegen && zig build" }).step);
|
||||
codegen = b.step("gen", "Generate zig service code from smithy models");
|
||||
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
|
||||
// with models
|
||||
|
@ -79,20 +81,19 @@ pub fn build(b: *Builder) !void {
|
|||
// service manifest we know it needs to be regenerated. So this step
|
||||
// will remove the service manifest if codegen has been touched, thereby
|
||||
// triggering the re-gen
|
||||
codegen.dependOn(&b.addSystemCommand(&.{
|
||||
cg.dependOn(&b.addSystemCommand(&.{
|
||||
"/bin/sh", "-c",
|
||||
\\ [ ! -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
|
||||
}).step);
|
||||
codegen.dependOn(&b.addSystemCommand(&.{
|
||||
cg.dependOn(&b.addSystemCommand(&.{
|
||||
"/bin/sh", "-c",
|
||||
\\ mkdir -p src/models/ && \
|
||||
\\ [ -f src/models/service_manifest.zig ] || \
|
||||
\\ ( cd codegen/models && ../codegen *.json && mv *.zig ../../src/models )
|
||||
}).step);
|
||||
b.getInstallStep().dependOn(codegen);
|
||||
test_step.dependOn(codegen);
|
||||
exe.step.dependOn(cg);
|
||||
}
|
||||
|
||||
exe.install();
|
||||
|
|
|
@ -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
|
||||
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 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, "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", .{});
|
||||
|
@ -579,6 +580,7 @@ fn generateComplexTypeFor(shape_id: []const u8, members: []smithy.TypeMember, ty
|
|||
_ = try writer.write(" {\n");
|
||||
var child_state = state;
|
||||
child_state.indent_level += 1;
|
||||
var payload: ?[]const u8 = null;
|
||||
for (members) |member| {
|
||||
// This is our mapping
|
||||
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_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 => {},
|
||||
}
|
||||
}
|
||||
|
@ -639,6 +649,12 @@ fn generateComplexTypeFor(shape_id: []const u8, members: []smithy.TypeMember, ty
|
|||
// 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 outputIndent(child_state, writer);
|
||||
_ = try writer.write("pub fn fieldNameFor(_: @This(), comptime field_name: []const u8) []const u8 {\n");
|
||||
|
|
|
@ -95,6 +95,7 @@ pub const TraitType = enum {
|
|||
http_header,
|
||||
http_label,
|
||||
http_query,
|
||||
http_payload,
|
||||
json_name,
|
||||
xml_name,
|
||||
required,
|
||||
|
@ -128,6 +129,7 @@ pub const Trait = union(TraitType) {
|
|||
http_header: []const u8,
|
||||
http_label: []const u8,
|
||||
http_query: []const u8,
|
||||
http_payload: struct {},
|
||||
required: struct {},
|
||||
documentation: []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 };
|
||||
if (std.mem.eql(u8, trait_type, "smithy.api#httpHeader"))
|
||||
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?
|
||||
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#httpChecksumRequired
|
||||
\\smithy.api#httpLabel
|
||||
\\smithy.api#httpPayload
|
||||
\\smithy.api#httpPrefixHeaders
|
||||
\\smithy.api#httpQueryParams
|
||||
\\smithy.api#httpResponseCode
|
||||
|
|
108
src/aws.zig
108
src/aws.zig
|
@ -268,6 +268,13 @@ pub fn Request(comptime action: anytype) type {
|
|||
else
|
||||
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{
|
||||
.allocator = options.client.allocator,
|
||||
.allow_camel_case_conversion = true, // new option
|
||||
|
@ -288,22 +295,61 @@ pub fn Request(comptime action: anytype) type {
|
|||
|
||||
var stream = json.TokenStream.init(response.body);
|
||||
|
||||
const parsed_response = 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;
|
||||
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(
|
||||
\\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;
|
||||
};
|
||||
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
|
||||
// the code setting the response about 10 lines down will trigger
|
||||
// an error because the first field may not be a struct when
|
||||
|
@ -372,7 +418,16 @@ pub fn Request(comptime action: anytype) type {
|
|||
//
|
||||
// Big thing is that requestid, which we'll need to fetch "manually"
|
||||
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();
|
||||
var free_rid = false;
|
||||
// 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
|
||||
var host_id: ?[]const u8 = null;
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
// const writer = buffer.writer();
|
||||
defer buffer.deinit();
|
||||
var in_var = false;
|
||||
var in_label = false;
|
||||
var start: usize = 0;
|
||||
for (raw_uri) |c, inx| {
|
||||
switch (c) {
|
||||
'{' => {
|
||||
in_var = true;
|
||||
in_label = true;
|
||||
start = inx + 1;
|
||||
},
|
||||
'}' => {
|
||||
in_var = false;
|
||||
const replacement_var = raw_uri[start..inx];
|
||||
in_label = false;
|
||||
// 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| {
|
||||
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);
|
||||
defer replacement_buffer.deinit();
|
||||
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);
|
||||
} else {},
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ const EndPoint = struct {
|
|||
|
||||
fn deinit(self: EndPoint) void {
|
||||
self.allocator.free(self.uri);
|
||||
self.allocator.free(self.host);
|
||||
}
|
||||
};
|
||||
pub const AwsHttp = struct {
|
||||
|
@ -94,14 +95,31 @@ pub const AwsHttp = struct {
|
|||
/// service called, and will set up the signing options. The return
|
||||
/// value is simply a raw 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();
|
||||
log.debug("Calling endpoint {s}", .{endpoint.uri});
|
||||
// TODO: Should we allow customization here?
|
||||
const creds = try credentials.getCredentials(self.allocator, .{});
|
||||
defer creds.deinit();
|
||||
const signing_config: signing.Config = .{
|
||||
.region = options.region,
|
||||
.region = getRegion(service, options.region),
|
||||
.service = options.sigv4_service_name orelse service,
|
||||
.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 });
|
||||
defer self.allocator.free(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);
|
||||
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);
|
||||
|
||||
// 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:", .{});
|
||||
var resp_headers = try std.ArrayList(Header).initCapacity(self.allocator, req.headers.list.items.len);
|
||||
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 {
|
||||
try headers.append(.{ .name = "Accept", .value = "application/json" });
|
||||
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");
|
||||
if (environment_override) |override| {
|
||||
const uri = try allocator.dupeZ(u8, override);
|
||||
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.
|
||||
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)) {
|
||||
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 host = uri["https://".len..];
|
||||
const host = try allocator.dupe(u8, uri["https://".len..]);
|
||||
log.debug("host: {s}, scheme: {s}, port: {}", .{ host, "https", 443 });
|
||||
return EndPoint{
|
||||
.uri = uri,
|
||||
|
@ -291,7 +328,7 @@ fn endPointFromUri(allocator: std.mem.Allocator, uri: []const u8) !EndPoint {
|
|||
if (host_end == 0) {
|
||||
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 });
|
||||
return EndPoint{
|
||||
|
@ -302,3 +339,25 @@ fn endPointFromUri(allocator: std.mem.Allocator, uri: []const u8) !EndPoint {
|
|||
.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);
|
||||
}
|
||||
|
|
11
src/main.zig
11
src/main.zig
|
@ -49,6 +49,7 @@ const Tests = enum {
|
|||
rest_json_1_query_with_input,
|
||||
rest_json_1_work_with_lambda,
|
||||
rest_xml_no_input,
|
||||
rest_xml_anything_but_s3,
|
||||
};
|
||||
|
||||
pub fn main() anyerror!void {
|
||||
|
@ -89,7 +90,7 @@ pub fn main() anyerror!void {
|
|||
};
|
||||
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| {
|
||||
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("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)});
|
||||
}
|
||||
|
|
|
@ -145,7 +145,8 @@ fn parseInternal(comptime T: type, element: *xml.Element, options: ParseOptions)
|
|||
if (element.children.items.len == 0) {
|
||||
// This is almost certainly incomplete. Empty strings? xsi:nil?
|
||||
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, options);
|
||||
}
|
||||
|
@ -180,9 +181,8 @@ fn parseInternal(comptime T: type, element: *xml.Element, options: ParseOptions)
|
|||
// }
|
||||
// }
|
||||
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| {
|
||||
var r: T = undefined;
|
||||
|
@ -239,11 +239,16 @@ fn parseInternal(comptime T: type, element: *xml.Element, options: ParseOptions)
|
|||
fields_set = fields_set + 1;
|
||||
found_value = true;
|
||||
}
|
||||
if (@typeInfo(field.field_type) == .Optional and !found_value) {
|
||||
// @compileLog("Optional: Field name ", field.name, ", type ", field.field_type);
|
||||
@field(r, field.name) = null;
|
||||
fields_set = fields_set + 1;
|
||||
found_value = true;
|
||||
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);
|
||||
@field(r, field.name) = null;
|
||||
fields_set = fields_set + 1;
|
||||
found_value = true;
|
||||
}
|
||||
}
|
||||
// Using this else clause breaks zig, so we'll use a boolean instead
|
||||
if (!found_value) {
|
||||
|
@ -474,7 +479,8 @@ test "can coerce 8601 date to integer" {
|
|||
defer parsed_data.deinit();
|
||||
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)" {
|
||||
const allocator = std.testing.allocator;
|
||||
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("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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user