support request data in query protocol calls

This should complete the query protocol calls. However, there are likely
gaps in implementation for the transformation of request parameters
to url encoded body data
This commit is contained in:
Emil Lerch 2021-06-14 16:12:55 -07:00
parent 9a0908bc63
commit bd3605e387
Signed by: lobo
GPG Key ID: A7B62D657EF764F8
4 changed files with 163 additions and 7 deletions

View File

@ -2,6 +2,8 @@ const std = @import("std");
const awshttp = @import("awshttp.zig");
const json = @import("json.zig");
const url = @import("url.zig");
const case = @import("case.zig");
const servicemodel = @import("servicemodel.zig");
const log = std.log.scoped(.aws);
@ -66,11 +68,30 @@ pub const Aws = struct {
// Call using query protocol. This is documented as an XML protocol, but
// throwing a JSON accept header seems to work
fn callQuery(self: Self, comptime request: anytype, service: anytype, action: anytype, options: Options) !FullResponse(request) {
var buffer = std.ArrayList(u8).init(self.allocator);
defer buffer.deinit();
const writer = buffer.writer();
const transformer = struct {
allocator: *std.mem.Allocator,
const This = @This();
pub fn transform(this: This, name: []const u8) ![]const u8 {
return try case.snakeToPascal(this.allocator, name);
}
pub fn transform_deinit(this: This, name: []const u8) void {
this.allocator.free(name);
}
}{ .allocator = self.allocator };
try url.encode(request, writer, .{ .field_name_transformer = transformer });
const continuation = if (buffer.items.len > 0) "&" else "";
const body = try std.fmt.allocPrint(self.allocator, "Action={s}&Version={s}{s}{s}\n", .{ action.action_name, service.version, continuation, buffer.items });
defer self.allocator.free(body);
const FullR = FullResponse(request);
const response = try self.aws_http.callApi(
service.endpoint_prefix,
service.version,
action.action_name,
body,
.{
.region = options.region,
.dualstack = options.dualstack,
@ -80,7 +101,7 @@ pub const Aws = struct {
defer response.deinit();
if (response.response_code != 200) {
log.err("call failed! return status: {d}", .{response.response_code});
log.err("{s}", .{response.body});
log.err("Request:\n |{s}\nResponse:\n |{s}", .{ body, response.body });
return error.HttpFailure;
}
// TODO: Check status code for badness

View File

@ -232,12 +232,11 @@ pub const AwsHttp = struct {
/// It will calculate the appropriate endpoint and action parameters for the
/// 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, version: []const u8, action: []const u8, options: Options) !HttpResult {
pub fn callApi(self: Self, service: []const u8, body: []const u8, options: Options) !HttpResult {
const endpoint = try regionSubDomain(self.allocator, service, options.region, options.dualstack);
defer endpoint.deinit();
const body = try std.fmt.allocPrint(self.allocator, "Action={s}&Version={s}\n", .{ action, version });
defer self.allocator.free(body);
httplog.debug("Calling {s}.{s}, endpoint {s}", .{ service, action, endpoint.uri });
httplog.debug("Calling endpoint {s}", .{endpoint.uri});
httplog.debug("Body\n====\n{s}\n====", .{body});
const signing_options: SigningOptions = .{
.region = options.region,
.service = if (options.sigv4_service_name) |name| name else service,

40
src/case.zig Normal file
View File

@ -0,0 +1,40 @@
const std = @import("std");
const expectEqualStrings = std.testing.expectEqualStrings;
pub fn snakeToCamel(allocator: *std.mem.Allocator, name: []const u8) ![]u8 {
var utf8_name = (std.unicode.Utf8View.init(name) catch unreachable).iterator();
var target_inx: u64 = 0;
var previous_ascii: u8 = 0;
const rc = try allocator.alloc(u8, name.len); // This is slightly overkill, will need <= number of input chars
while (utf8_name.nextCodepoint()) |cp| {
if (cp > 0xff) return error.UnicodeNotSupported;
const ascii_char = @truncate(u8, cp);
if (ascii_char != '_') {
if (previous_ascii == '_' and ascii_char >= 'a' and ascii_char <= 'z') {
const uppercase_char = ascii_char - ('a' - 'A');
rc[target_inx] = uppercase_char;
} else {
rc[target_inx] = ascii_char;
}
target_inx = target_inx + 1;
}
previous_ascii = ascii_char;
}
rc[target_inx] = 0; // add zero sentinel
return rc[0..target_inx];
}
pub fn snakeToPascal(allocator: *std.mem.Allocator, name: []const u8) ![]u8 {
const rc = try snakeToCamel(allocator, name);
if (rc[0] >= 'a' and rc[0] <= 'z') {
const uppercase_char = rc[0] - ('a' - 'A');
rc[0] = uppercase_char;
}
return rc;
}
test "converts from snake to camelCase" {
const allocator = std.testing.allocator;
const camel = try snakeToCamel(allocator, "access_key_id");
defer allocator.free(camel);
try expectEqualStrings("accessKeyId", camel);
}

96
src/url.zig Normal file
View File

@ -0,0 +1,96 @@
const std = @import("std");
pub fn encode(obj: anytype, writer: anytype, options: anytype) !void {
try encodeStruct("", obj, writer, options);
}
fn encodeStruct(parent: []const u8, obj: anytype, writer: anytype, options: anytype) !void {
var first = true;
inline for (@typeInfo(@TypeOf(obj)).Struct.fields) |field| {
const field_name = if (@hasField(@TypeOf(options), "field_name_transformer")) try options.field_name_transformer.transform(field.name) else field.name;
defer {
if (@hasField(@TypeOf(options), "field_name_transformer"))
options.field_name_transformer.transform_deinit(field_name);
}
if (!first) _ = try writer.write("&");
switch (@typeInfo(field.field_type)) {
.Struct => {
try encodeStruct(field_name ++ ".", @field(obj, field.name), writer);
},
else => try writer.print("{s}{s}={s}", .{ parent, field_name, @field(obj, field.name) }),
}
first = false;
}
}
fn testencode(expected: []const u8, value: anytype, options: anytype) !void {
const ValidationWriter = struct {
const Self = @This();
pub const Writer = std.io.Writer(*Self, Error, write);
pub const Error = error{
TooMuchData,
DifferentData,
};
expected_remaining: []const u8,
fn init(exp: []const u8) Self {
return .{ .expected_remaining = exp };
}
pub fn writer(self: *Self) Writer {
return .{ .context = self };
}
fn write(self: *Self, bytes: []const u8) Error!usize {
// std.debug.print("{s}", .{bytes});
if (self.expected_remaining.len < bytes.len) {
std.debug.warn(
\\====== expected this output: =========
\\{s}
\\======== instead found this: =========
\\{s}
\\======================================
, .{
self.expected_remaining,
bytes,
});
return error.TooMuchData;
}
if (!std.mem.eql(u8, self.expected_remaining[0..bytes.len], bytes)) {
std.debug.warn(
\\====== expected this output: =========
\\{s}
\\======== instead found this: =========
\\{s}
\\======================================
, .{
self.expected_remaining[0..bytes.len],
bytes,
});
return error.DifferentData;
}
self.expected_remaining = self.expected_remaining[bytes.len..];
return bytes.len;
}
};
var vos = ValidationWriter.init(expected);
try encode(value, vos.writer(), options);
if (vos.expected_remaining.len > 0) return error.NotEnoughData;
}
test "can url encode an object" {
try testencode(
"Action=GetCallerIdentity&Version=2021-01-01",
.{ .Action = "GetCallerIdentity", .Version = "2021-01-01" },
.{},
);
}
test "can url encode a complex object" {
try testencode(
"Action=GetCallerIdentity&Version=2021-01-01&complex.innermember=foo",
.{ .Action = "GetCallerIdentity", .Version = "2021-01-01", .complex = .{ .innermember = "foo" } },
.{},
);
}