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:
parent
9a0908bc63
commit
bd3605e387
27
src/aws.zig
27
src/aws.zig
|
@ -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
|
||||
|
|
|
@ -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
40
src/case.zig
Normal 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
96
src/url.zig
Normal 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" } },
|
||||
.{},
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user