Compare commits

..

10 Commits

8 changed files with 236 additions and 64 deletions

View File

@ -17,6 +17,12 @@ pub fn build(b: *Builder) !void {
// https://github.com/ziglang/zig/issues/855 // https://github.com/ziglang/zig/issues/855
exe.addPackagePath("smithy", "smithy/src/smithy.zig"); exe.addPackagePath("smithy", "smithy/src/smithy.zig");
// This bitfield workaround will end up requiring a bunch of headers that
// currently mean building in the docker container is the best way to build
// TODO: Determine if it's a good idea to copy these files out of our
// docker container to the local fs so we can just build even outside
// the container. And maybe, just maybe these even get committed to
// source control?
exe.addCSourceFile("src/bitfield-workaround.c", &[_][]const u8{"-std=c99"}); exe.addCSourceFile("src/bitfield-workaround.c", &[_][]const u8{"-std=c99"});
const c_include_dirs = .{ const c_include_dirs = .{
"./src/", "./src/",
@ -54,7 +60,10 @@ pub fn build(b: *Builder) !void {
// https://ziglang.org/builds/zig-linux-x86_64-0.9.0-dev.321+15a030ef3.tar.xz // https://ziglang.org/builds/zig-linux-x86_64-0.9.0-dev.321+15a030ef3.tar.xz
exe.linkage = .static; exe.linkage = .static;
const is_strip = b.option(bool, "strip", "strip exe") orelse true; // TODO: Strip doesn't actually fully strip the executable. If we're on
// linux we can run strip on the result, probably at the expense
// of busting cache logic
const is_strip = b.option(bool, "strip", "strip exe [true]") orelse true;
exe.strip = is_strip; exe.strip = is_strip;
const run_cmd = exe.run(); const run_cmd = exe.run();
@ -84,10 +93,19 @@ pub fn build(b: *Builder) !void {
} }
// TODO: Support > linux // TODO: Support > linux
// TODO: Get a better cache in place
if (std.builtin.os.tag == .linux) { if (std.builtin.os.tag == .linux) {
const codegen = b.step("gen", "Generate zig service code from smithy models"); 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.dependOn(&b.addSystemCommand(&.{ "/bin/sh", "-c", "cd codegen && zig build" }).step);
// Since codegen binary is built every time, if it's newer than our
// 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(&.{
"/bin/sh", "-c",
\\ [ ! -f src/models/service_manifest.zig ] || \
\\ [ src/models/service_manifest.zig -nt codegen/codegen ] || \
\\ rm src/models/service_manifest.zig
}).step);
codegen.dependOn(&b.addSystemCommand(&.{ codegen.dependOn(&b.addSystemCommand(&.{
"/bin/sh", "-c", "/bin/sh", "-c",
\\ mkdir -p src/models/ && \ \\ mkdir -p src/models/ && \

View File

@ -106,7 +106,7 @@ fn generateServices(allocator: *std.mem.Allocator, comptime _: []const u8, file:
// Service struct // Service struct
// name of the field will be snake_case of whatever comes in from // name of the field will be snake_case of whatever comes in from
// sdk_id. Not sure this will simple... // sdk_id. Not sure this will simple...
const constant_name = try snake.fromPascalCase(allocator, sdk_id); const constant_name = try constantName(allocator, sdk_id);
try constant_names.append(constant_name); try constant_names.append(constant_name);
try writer.print("const Self = @This();\n", .{}); try writer.print("const Self = @This();\n", .{});
try writer.print("pub const version: []const u8 = \"{s}\";\n", .{version}); try writer.print("pub const version: []const u8 = \"{s}\";\n", .{version});
@ -132,6 +132,26 @@ fn generateServices(allocator: *std.mem.Allocator, comptime _: []const u8, file:
} }
return constant_names.toOwnedSlice(); return constant_names.toOwnedSlice();
} }
fn constantName(allocator: *std.mem.Allocator, id: []const u8) ![]const u8 {
// There are some ids that don't follow consistent rules, so we'll
// look for the exceptions and, if not found, revert to the snake case
// algorithm
// 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", .{});
// 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, "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", .{});
// snake turns this into dev_ops, which is a little weird
if (std.mem.eql(u8, id, "DevOps Guru")) return try std.fmt.allocPrint(allocator, "devops_guru", .{});
if (std.mem.eql(u8, id, "FSx")) return try std.fmt.allocPrint(allocator, "fsx", .{});
// Not a special case - just snake it
return try snake.fromPascalCase(allocator, id);
}
fn generateOperation(allocator: *std.mem.Allocator, operation: smithy.ShapeInfo, shapes: anytype, writer: anytype, service: []const u8) !void { fn generateOperation(allocator: *std.mem.Allocator, operation: smithy.ShapeInfo, shapes: anytype, writer: anytype, service: []const u8) !void {
const snake_case_name = try snake.fromPascalCase(allocator, operation.name); const snake_case_name = try snake.fromPascalCase(allocator, operation.name);
defer allocator.free(snake_case_name); defer allocator.free(snake_case_name);

View File

@ -2,59 +2,97 @@ const std = @import("std");
const expectEqualStrings = std.testing.expectEqualStrings; const expectEqualStrings = std.testing.expectEqualStrings;
pub fn fromPascalCase(allocator: *std.mem.Allocator, name: []const u8) ![]u8 { pub fn fromPascalCase(allocator: *std.mem.Allocator, name: []const u8) ![]u8 {
const rc = try allocator.alloc(u8, name.len * 2); // This is overkill, but is > the maximum length possibly needed
errdefer allocator.free(rc);
var utf8_name = (std.unicode.Utf8View.init(name) catch unreachable).iterator(); var utf8_name = (std.unicode.Utf8View.init(name) catch unreachable).iterator();
var target_inx: u64 = 0; var target_inx: u64 = 0;
var previous_codepoint: ?u21 = null; var curr_char = (try isAscii(utf8_name.nextCodepoint())).?;
var cp = utf8_name.nextCodepoint(); target_inx = setNext(lowercase(curr_char), rc, target_inx);
if (cp == null) { var prev_char = curr_char;
return try allocator.dupeZ(u8, name); if (try isAscii(utf8_name.nextCodepoint())) |ch| {
} // TODO: fix bug if single letter uppercase curr_char = ch;
var codepoint = cp.?; } else {
const rc = try allocator.alloc(u8, name.len * 2); // This is overkill, but is > the maximum length possibly needed // Single character only - we're done here
while (utf8_name.nextCodepoint()) |next_codepoint| { _ = setNext(0, rc, target_inx);
if (codepoint > 0xff) return error{UnicodeNotSupported}.UnicodeNotSupported; return rc[0..target_inx];
if (next_codepoint > 0xff) return error{UnicodeNotSupported}.UnicodeNotSupported; }
const ascii_char = @truncate(u8, codepoint); while (try isAscii(utf8_name.nextCodepoint())) |next_char| {
if (next_codepoint == ' ') continue; // ignore all spaces in name if (next_char == ' ') {
if (ascii_char >= 'A' and ascii_char < 'Z') { // a space shouldn't be happening. But if it does, it clues us
const lowercase_char = ascii_char + ('a' - 'A'); // in pretty well:
if (previous_codepoint == null) { //
rc[target_inx] = lowercase_char; // MyStuff Is Awesome
target_inx = target_inx + 1; // |^
// |next_char
// ^
// prev_codepoint/ascii_prev_char (and target_inx)
target_inx = setNext(lowercase(curr_char), rc, target_inx);
target_inx = setNext('_', rc, target_inx);
curr_char = (try isAscii(utf8_name.nextCodepoint())).?;
target_inx = setNext(lowercase(curr_char), rc, target_inx);
prev_char = curr_char;
curr_char = (try isAscii(utf8_name.nextCodepoint())).?;
continue;
}
if (between(curr_char, 'A', 'Z')) {
if (isAcronym(curr_char, next_char)) {
// We could be in an acronym at the start of a word. This
// is the only case where we actually need to look back at the
// previous character, and if that's the case, throw in an
// underscore
// "SAMLMySAMLAcronymThing");
if (between(prev_char, 'a', 'z'))
target_inx = setNext('_', rc, target_inx);
//we are in an acronym - don't snake, just lower
target_inx = setNext(lowercase(curr_char), rc, target_inx);
} else { } else {
if (next_codepoint >= 'A' and next_codepoint <= 'Z' and previous_codepoint.? >= 'A' and previous_codepoint.? <= 'Z') { target_inx = setNext('_', rc, target_inx);
//we are in an acronym - don't snake, just lower target_inx = setNext(lowercase(curr_char), rc, target_inx);
rc[target_inx] = lowercase_char;
target_inx = target_inx + 1;
} else {
rc[target_inx] = '_';
rc[target_inx + 1] = lowercase_char;
target_inx = target_inx + 2;
}
} }
} else { } else {
// if (ascii_char == ' ') { target_inx = setNext(curr_char, rc, target_inx);
// rc[target_inx] = '_';
// } else {
rc[target_inx] = ascii_char;
// }
target_inx = target_inx + 1;
} }
previous_codepoint = codepoint; prev_char = curr_char;
codepoint = next_codepoint; curr_char = next_char;
} }
// work in the last codepoint - force lowercase // work in the last codepoint - force lowercase
rc[target_inx] = @truncate(u8, codepoint); target_inx = setNext(lowercase(curr_char), rc, target_inx);
if (rc[target_inx] >= 'A' and rc[target_inx] <= 'Z') {
const lowercase_char = rc[target_inx] + ('a' - 'A');
rc[target_inx] = lowercase_char;
}
target_inx = target_inx + 1;
rc[target_inx] = 0; rc[target_inx] = 0;
return rc[0..target_inx]; return rc[0..target_inx];
} }
fn isAcronym(char1: u8, char2: u8) bool {
return isAcronymChar(char1) and isAcronymChar(char2);
}
fn isAcronymChar(char: u8) bool {
return between(char, 'A', 'Z') or between(char, '0', '9');
}
fn isAscii(codepoint: ?u21) !?u8 {
if (codepoint) |cp| {
if (cp > 0xff) return error.UnicodeNotSupported;
return @truncate(u8, cp);
}
return null;
}
fn setNext(ascii: u8, slice: []u8, inx: u64) u64 {
slice[inx] = ascii;
return inx + 1;
}
fn lowercase(ascii: u8) u8 {
var lowercase_char = ascii;
if (between(ascii, 'A', 'Z'))
lowercase_char = ascii + ('a' - 'A');
return lowercase_char;
}
fn between(char: u8, from: u8, to: u8) bool {
return char >= from and char <= to;
}
test "converts from PascalCase to snake_case" { test "converts from PascalCase to snake_case" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
const snake_case = try fromPascalCase(allocator, "MyPascalCaseThing"); const snake_case = try fromPascalCase(allocator, "MyPascalCaseThing");
@ -73,3 +111,26 @@ test "spaces in the name" {
defer allocator.free(snake_case); defer allocator.free(snake_case);
try expectEqualStrings("api_gateway", snake_case); try expectEqualStrings("api_gateway", snake_case);
} }
test "S3" {
const allocator = std.testing.allocator;
const snake_case = try fromPascalCase(allocator, "S3");
defer allocator.free(snake_case);
try expectEqualStrings("s3", snake_case);
}
test "ec2" {
const allocator = std.testing.allocator;
const snake_case = try fromPascalCase(allocator, "EC2");
defer allocator.free(snake_case);
try expectEqualStrings("ec2", snake_case);
}
test "IoT 1Click Devices Service" {
const allocator = std.testing.allocator;
const snake_case = try fromPascalCase(allocator, "IoT 1Click Devices Service");
defer allocator.free(snake_case);
// NOTE: There is some debate amoung humans about what this should
// turn into. Should it be iot_1click_... or iot_1_click...?
try expectEqualStrings("iot_1_click_devices_service", snake_case);
}

View File

@ -84,12 +84,33 @@ pub const Aws = struct {
}); });
const continuation = if (buffer.items.len > 0) "&" else ""; 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_meta.version, continuation, buffer.items }); const query = if (service_meta.aws_protocol == .query)
try std.fmt.allocPrint(self.allocator, "", .{})
else // EC2
try std.fmt.allocPrint(self.allocator, "?Action={s}&Version={s}", .{
action.action_name,
service_meta.version,
});
defer self.allocator.free(query);
const body = if (service_meta.aws_protocol == .query)
try std.fmt.allocPrint(self.allocator, "Action={s}&Version={s}{s}{s}", .{
action.action_name,
service_meta.version,
continuation,
buffer.items,
})
else // EC2
try std.fmt.allocPrint(self.allocator, "{s}", .{buffer.items});
defer self.allocator.free(body); defer self.allocator.free(body);
const FullR = FullResponse(request); const FullR = FullResponse(request);
const response = try self.aws_http.callApi( const response = try self.aws_http.callApi(
service_meta.endpoint_prefix, service_meta.endpoint_prefix,
body, .{
.body = body,
.query = query,
},
.{ .{
.region = options.region, .region = options.region,
.dualstack = options.dualstack, .dualstack = options.dualstack,
@ -100,11 +121,36 @@ pub const Aws = struct {
defer response.deinit(); defer response.deinit();
if (response.response_code != 200) { if (response.response_code != 200) {
log.err("call failed! return status: {d}", .{response.response_code}); log.err("call failed! return status: {d}", .{response.response_code});
log.err("Request:\n |{s}\nResponse:\n |{s}", .{ body, response.body }); log.err("Request Query:\n |{s}\n", .{query});
log.err("Request Body:\n |{s}\n", .{body});
log.err("Response Headers:\n", .{});
for (response.headers) |h|
log.err("\t{s}:{s}\n", .{ h.name, h.value });
log.err("Response Body:\n |{s}", .{response.body});
return error.HttpFailure; return error.HttpFailure;
} }
// log.debug("Successful return from server:\n |{s}", .{response.body}); // EC2 ignores our accept type, but technically query protocol only
// TODO: Check status code for badness // returns XML as well. So, we'll ignore the protocol here and just
// look at the return type
var isJson: bool = undefined;
for (response.headers) |h| {
if (std.mem.eql(u8, "Content-Type", h.name)) {
if (std.mem.startsWith(u8, h.value, "application/json")) {
isJson = true;
} else if (std.mem.startsWith(u8, h.value, "text/xml")) {
isJson = false;
} else {
log.err("Unexpected content type: {s}", .{h.value});
return error.UnexpectedContentType;
}
break;
}
}
// TODO: Handle XML
if (!isJson) return error.XmlUnimplemented;
var stream = json.TokenStream.init(response.body); var stream = json.TokenStream.init(response.body);
const parser_options = json.ParseOptions{ const parser_options = json.ParseOptions{

View File

@ -74,13 +74,26 @@ const SigningOptions = struct {
service: []const u8, service: []const u8,
}; };
const HttpRequest = struct {
path: []const u8 = "/",
query: []const u8 = "",
body: []const u8 = "",
method: []const u8 = "POST",
// headers: []Header = .{},
};
const HttpResult = struct { const HttpResult = struct {
response_code: u16, // actually 3 digits can fit in u10 response_code: u16, // actually 3 digits can fit in u10
body: []const u8, body: []const u8,
headers: []Header,
allocator: *std.mem.Allocator, allocator: *std.mem.Allocator,
pub fn deinit(self: HttpResult) void { pub fn deinit(self: HttpResult) void {
self.allocator.free(self.body); self.allocator.free(self.body);
for (self.headers) |h| {
self.allocator.free(h.name);
self.allocator.free(h.value);
}
self.allocator.free(self.headers);
httplog.debug("http result deinit complete", .{}); httplog.debug("http result deinit complete", .{});
return; return;
} }
@ -235,16 +248,15 @@ pub const AwsHttp = struct {
/// It will calculate the appropriate endpoint and action parameters for the /// It will calculate the appropriate endpoint and action parameters for the
/// 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, body: []const u8, 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); const endpoint = try regionSubDomain(self.allocator, service, options.region, options.dualstack);
defer endpoint.deinit(); defer endpoint.deinit();
httplog.debug("Calling endpoint {s}", .{endpoint.uri}); httplog.debug("Calling endpoint {s}", .{endpoint.uri});
httplog.debug("Body\n====\n{s}\n====", .{body});
const signing_options: SigningOptions = .{ const signing_options: SigningOptions = .{
.region = options.region, .region = options.region,
.service = if (options.sigv4_service_name) |name| name else service, .service = if (options.sigv4_service_name) |name| name else service,
}; };
return try self.makeRequest(endpoint, "POST", "/", body, signing_options); return try self.makeRequest(endpoint, request, signing_options);
} }
/// makeRequest is a low level http/https function that can be used inside /// makeRequest is a low level http/https function that can be used inside
@ -265,15 +277,20 @@ pub const AwsHttp = struct {
/// Return value is an HttpResult, which will need the caller to deinit(). /// Return value is an HttpResult, which will need the caller to deinit().
/// HttpResult currently contains the body only. The addition of Headers /// HttpResult currently contains the body only. The addition of Headers
/// and return code would be a relatively minor change /// and return code would be a relatively minor change
pub fn makeRequest(self: Self, endpoint: EndPoint, method: []const u8, path: []const u8, body: []const u8, signing_options: ?SigningOptions) !HttpResult { pub fn makeRequest(self: Self, endpoint: EndPoint, request: HttpRequest, signing_options: ?SigningOptions) !HttpResult {
// Since we're going to pass these into C-land, we need to make sure // Since we're going to pass these into C-land, we need to make sure
// our inputs have sentinals // our inputs have sentinals
const method_z = try self.allocator.dupeZ(u8, method); const method_z = try self.allocator.dupeZ(u8, request.method);
defer self.allocator.free(method_z); defer self.allocator.free(method_z);
const path_z = try self.allocator.dupeZ(u8, path); // Path contains both path and query
const path_z = try std.fmt.allocPrintZ(self.allocator, "{s}{s}", .{ request.path, request.query });
defer self.allocator.free(path_z); defer self.allocator.free(path_z);
const body_z = try self.allocator.dupeZ(u8, body); const body_z = try self.allocator.dupeZ(u8, request.body);
defer self.allocator.free(body_z); defer self.allocator.free(body_z);
httplog.debug("Path: {s}", .{path_z});
httplog.debug("Method: {s}", .{request.method});
httplog.debug("body length: {d}", .{request.body.len});
httplog.debug("Body\n====\n{s}\n====", .{request.body});
// TODO: Try to re-encapsulate this // TODO: Try to re-encapsulate this
// var http_request = try createRequest(method, path, body); // var http_request = try createRequest(method, path, body);
@ -287,13 +304,11 @@ pub const AwsHttp = struct {
if (c.aws_http_message_set_request_path(http_request, c.aws_byte_cursor_from_c_str(@ptrCast([*c]const u8, path_z))) != c.AWS_OP_SUCCESS) if (c.aws_http_message_set_request_path(http_request, c.aws_byte_cursor_from_c_str(@ptrCast([*c]const u8, path_z))) != c.AWS_OP_SUCCESS)
return AwsError.SetRequestPathError; return AwsError.SetRequestPathError;
httplog.debug("body length: {d}", .{body.len});
const body_cursor = c.aws_byte_cursor_from_c_str(@ptrCast([*c]const u8, body_z)); const body_cursor = c.aws_byte_cursor_from_c_str(@ptrCast([*c]const u8, body_z));
const request_body = c.aws_input_stream_new_from_cursor(c_allocator, &body_cursor); const request_body = c.aws_input_stream_new_from_cursor(c_allocator, &body_cursor);
defer c.aws_input_stream_destroy(request_body); defer c.aws_input_stream_destroy(request_body);
if (body.len > 0) { if (request.body.len > 0)
c.aws_http_message_set_body_stream(http_request, request_body); c.aws_http_message_set_body_stream(http_request, request_body);
}
// End CreateRequest. This should return a struct with a deinit function that can do // End CreateRequest. This should return a struct with a deinit function that can do
// destroys, etc // destroys, etc
@ -305,7 +320,7 @@ pub const AwsHttp = struct {
var tls_connection_options: ?*c.aws_tls_connection_options = null; var tls_connection_options: ?*c.aws_tls_connection_options = null;
const host = try self.allocator.dupeZ(u8, endpoint.host); const host = try self.allocator.dupeZ(u8, endpoint.host);
defer self.allocator.free(host); defer self.allocator.free(host);
try self.addHeaders(http_request.?, host, body); try self.addHeaders(http_request.?, host, request.body);
if (std.mem.eql(u8, endpoint.scheme, "https")) { if (std.mem.eql(u8, endpoint.scheme, "https")) {
// TODO: Figure out why this needs to be inline vs function call // TODO: Figure out why this needs to be inline vs function call
// tls_connection_options = try self.setupTls(host); // tls_connection_options = try self.setupTls(host);
@ -449,6 +464,7 @@ pub const AwsHttp = struct {
const rc = HttpResult{ const rc = HttpResult{
.response_code = context.response_code.?, .response_code = context.response_code.?,
.body = final_body, .body = final_body,
.headers = context.headers.?.toOwnedSlice(),
.allocator = self.allocator, .allocator = self.allocator,
}; };
return rc; return rc;
@ -956,12 +972,12 @@ const RequestContext = struct {
pub fn appendToBody(self: *Self, fragment: []const u8) !void { pub fn appendToBody(self: *Self, fragment: []const u8) !void {
var orig_body: []const u8 = ""; var orig_body: []const u8 = "";
if (self.body) |b| { if (self.body) |b| {
orig_body = try self.allocator.dupeZ(u8, b); orig_body = try self.allocator.dupe(u8, b);
self.allocator.free(b); self.allocator.free(b);
self.body = null; self.body = null;
} }
defer self.allocator.free(orig_body); defer self.allocator.free(orig_body);
self.body = try std.fmt.allocPrintZ(self.allocator, "{s}{s}", .{ orig_body, fragment }); self.body = try std.fmt.allocPrint(self.allocator, "{s}{s}", .{ orig_body, fragment });
} }
pub fn addHeader(self: *Self, name: []const u8, value: []const u8) !void { pub fn addHeader(self: *Self, name: []const u8, value: []const u8) !void {

View File

@ -5,7 +5,8 @@ pub fn snakeToCamel(allocator: *std.mem.Allocator, name: []const u8) ![]u8 {
var utf8_name = (std.unicode.Utf8View.init(name) catch unreachable).iterator(); var utf8_name = (std.unicode.Utf8View.init(name) catch unreachable).iterator();
var target_inx: u64 = 0; var target_inx: u64 = 0;
var previous_ascii: u8 = 0; var previous_ascii: u8 = 0;
const rc = try allocator.alloc(u8, name.len); // This is slightly overkill, will need <= number of input chars // A single word will take the entire length plus our sentinel
const rc = try allocator.alloc(u8, name.len + 1);
while (utf8_name.nextCodepoint()) |cp| { while (utf8_name.nextCodepoint()) |cp| {
if (cp > 0xff) return error.UnicodeNotSupported; if (cp > 0xff) return error.UnicodeNotSupported;
const ascii_char = @truncate(u8, cp); const ascii_char = @truncate(u8, cp);
@ -38,3 +39,9 @@ test "converts from snake to camelCase" {
defer allocator.free(camel); defer allocator.free(camel);
try expectEqualStrings("accessKeyId", camel); try expectEqualStrings("accessKeyId", camel);
} }
test "single word" {
const allocator = std.testing.allocator;
const camel = try snakeToCamel(allocator, "word");
defer allocator.free(camel);
try expectEqualStrings("word", camel);
}

View File

@ -1625,6 +1625,8 @@ fn parseInternal(comptime T: type, token: Token, tokens: *TokenStream, options:
var r: T = undefined; var r: T = undefined;
var fields_seen = [_]bool{false} ** structInfo.fields.len; var fields_seen = [_]bool{false} ** structInfo.fields.len;
errdefer { errdefer {
// TODO: why so high here? This was needed for ec2 describe instances
@setEvalBranchQuota(100000);
inline for (structInfo.fields) |field, i| { inline for (structInfo.fields) |field, i| {
if (fields_seen[i] and !field.is_comptime) { if (fields_seen[i] and !field.is_comptime) {
parseFree(field.field_type, @field(r, field.name), options); parseFree(field.field_type, @field(r, field.name), options);

View File

@ -65,7 +65,7 @@ pub fn main() anyerror!void {
var client = aws.Aws.init(allocator); var client = aws.Aws.init(allocator);
defer client.deinit(); defer client.deinit();
const services = aws.Services(.{.sts}){}; const services = aws.Services(.{ .sts, .ec2 }){};
for (tests.items) |t| { for (tests.items) |t| {
std.log.info("===== Start Test: {s} =====", .{@tagName(t)}); std.log.info("===== Start Test: {s} =====", .{@tagName(t)});
@ -87,7 +87,9 @@ pub fn main() anyerror!void {
std.log.info("access key: {s}", .{access.response.credentials.access_key_id}); std.log.info("access key: {s}", .{access.response.credentials.access_key_id});
}, },
.ec2_query_no_input => { .ec2_query_no_input => {
// TODO: Find test const instances = try client.call(services.ec2.describe_instances.Request{}, options);
defer instances.deinit();
std.log.info("reservation count: {d}", .{instances.response.reservations.len});
}, },
} }
std.log.info("===== End Test: {s} =====\n", .{@tagName(t)}); std.log.info("===== End Test: {s} =====\n", .{@tagName(t)});