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/)
|
[![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
|
||||||
|
|
|
@ -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(.{
|
||||||
|
|
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);
|
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();
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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
|
||||||
|
|
82
src/aws.zig
82
src/aws.zig
|
@ -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 {},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
11
src/main.zig
11
src/main.zig
|
@ -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)});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user