From 6df02b10748b82f7a7300e399e48dc6f98647cf7 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Thu, 29 Feb 2024 20:41:03 -0800 Subject: [PATCH] switch sqs query test (json) with sts query test (xml) and fix response parsing --- src/aws.zig | 57 +++++++++++++++++++++++++++++++---------- src/xml_shaper.zig | 63 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 105 insertions(+), 15 deletions(-) diff --git a/src/aws.zig b/src/aws.zig index 121e920..66242b4 100644 --- a/src/aws.zig +++ b/src/aws.zig @@ -457,6 +457,30 @@ pub fn Request(comptime request_action: anytype) type { } } + fn findResult(element: *xml_shaper.Element, options: xml_shaper.ParseOptions) *xml_shaper.Element { + _ = options; + // We're looking for a very specific pattern here. We want only two direct + // children. The first one must end with "Result", and the second should + // be our ResponseMetadata node + var children = element.elements(); + var found_metadata = false; + var result_child: ?*xml_shaper.Element = null; + var inx: usize = 0; + while (children.next()) |child| : (inx += 1) { + if (std.mem.eql(u8, child.tag, "ResponseMetadata")) { + found_metadata = true; + continue; + } + if (std.mem.endsWith(u8, child.tag, "Result")) { + result_child = child; + continue; + } + if (inx > 1) return element; + return element; // It should only be those two + } + return result_child orelse element; + } + fn xmlReturn(request: awshttp.HttpRequest, options: Options, result: awshttp.HttpResult) !FullResponseType { // Server shape be all like: // @@ -481,7 +505,7 @@ pub fn Request(comptime request_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 xml_options = xml_shaper.ParseOptions{ .allocator = options.client.allocator, .elementToParse = findResult }; var body: []const u8 = result.body; var free_body = false; if (result.body.len < 20) { @@ -1584,24 +1608,31 @@ test "query_no_input: sts getCallerIdentity comptime" { try std.testing.expectEqualStrings("123456789012", call.response.account.?); try std.testing.expectEqualStrings("8f0d54da-1230-40f7-b4ac-95015c4b84cd", call.response_metadata.request_id); } -test "query_with_input: sqs listQueues runtime" { - if (true) return error.SkipZigTest; // sqs switched from query to json recently +test "query_with_input: sts getAccessKeyInfo runtime" { + // sqs switched from query to json in aws sdk for go v2 commit f5a08768ef820ff5efd62a49ba50c61c9ca5dbcb const allocator = std.testing.allocator; var test_harness = TestSetup.init(allocator, .{ .allocator = allocator, .server_response = - \\{"ListQueuesResponse":{"ListQueuesResult":{"NextExclusiveStartQueueName":null,"NextToken":null,"queueUrls":null},"ResponseMetadata":{"RequestId":"a85e390b-b866-590e-8cae-645f2bbe59c5"}}} + \\ + \\ + \\ 123456789012 + \\ + \\ + \\ ec85bf29-1ef0-459a-930e-6446dd14a286 + \\ + \\ , .server_response_headers = @constCast(&[_][2][]const u8{ - .{ "Content-Type", "application/json" }, - .{ "x-amzn-RequestId", "a85e390b-b866-590e-8cae-645f2bbe59c5" }, + .{ "Content-Type", "text/xml" }, + .{ "x-amzn-RequestId", "ec85bf29-1ef0-459a-930e-6446dd14a286" }, }), }); defer test_harness.deinit(); const options = try test_harness.start(); - const sqs = (Services(.{.sqs}){}).sqs; - const call = try test_harness.client.call(sqs.list_queues.Request{ - .queue_name_prefix = "s", + const sts = (Services(.{.sts}){}).sts; + const call = try test_harness.client.call(sts.get_access_key_info.Request{ + .access_key_id = "ASIAYAM4POHXJNKTYFUN", }, options); defer call.deinit(); test_harness.stop(); @@ -1609,12 +1640,12 @@ test "query_with_input: sqs listQueues runtime" { try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); try std.testing.expectEqualStrings("/", test_harness.request_options.request_target); try std.testing.expectEqualStrings( - \\Action=ListQueues&Version=2012-11-05&QueueNamePrefix=s + \\Action=GetAccessKeyInfo&Version=2011-06-15&AccessKeyId=ASIAYAM4POHXJNKTYFUN , test_harness.request_options.request_body); // Response expectations - // TODO: We can get a lot better with this under test - try std.testing.expect(call.response.queue_urls == null); - try std.testing.expectEqualStrings("a85e390b-b866-590e-8cae-645f2bbe59c5", call.response_metadata.request_id); + try std.testing.expect(call.response.account != null); + try std.testing.expectEqualStrings("123456789012", call.response.account.?); + try std.testing.expectEqualStrings("ec85bf29-1ef0-459a-930e-6446dd14a286", call.response_metadata.request_id); } test "json_1_0_query_with_input: dynamodb listTables runtime" { const allocator = std.testing.allocator; diff --git a/src/xml_shaper.zig b/src/xml_shaper.zig index fd51594..f97d188 100644 --- a/src/xml_shaper.zig +++ b/src/xml_shaper.zig @@ -4,6 +4,8 @@ const date = @import("date.zig"); const log = std.log.scoped(.xml_shaper); +pub const Element = xml.Element; + pub fn Parsed(comptime T: type) type { return struct { // Forcing an arean allocator isn't my favorite choice here, but @@ -70,6 +72,8 @@ fn deinitObject(allocator: std.mem.Allocator, obj: anytype) void { pub const ParseOptions = struct { allocator: ?std.mem.Allocator = null, match_predicate_ptr: ?*const fn (a: []const u8, b: []const u8, options: xml.PredicateOptions) anyerror!bool = null, + /// defines a function to use to locate an element other than the root of the document for parsing + elementToParse: ?*const fn (element: *Element, options: ParseOptions) *Element = null, }; pub fn parse(comptime T: type, source: []const u8, options: ParseOptions) !Parsed(T) { @@ -86,7 +90,8 @@ pub fn parse(comptime T: type, source: []const u8, options: ParseOptions) !Parse .match_predicate_ptr = options.match_predicate_ptr, }; - return Parsed(T).init(arena_allocator, try parseInternal(T, parsed.root, opts), parsed); + const root = if (options.elementToParse) |e| e(parsed.root, opts) else parsed.root; + return Parsed(T).init(arena_allocator, try parseInternal(T, root, opts), parsed); } fn parseInternal(comptime T: type, element: *xml.Element, options: ParseOptions) !T { @@ -244,6 +249,7 @@ fn parseInternal(comptime T: type, element: *xml.Element, options: ParseOptions) // Zig compiler bug circa 0.9.0. Using "and !found_value" // in the if statement above will trigger assertion failure if (!found_value) { + log.debug("Child element not found, but field optional. Setting {s}=null", .{field.name}); // @compileLog("Optional: Field name ", field.name, ", type ", field.type); @field(r, field.name) = null; fields_set = fields_set + 1; @@ -625,12 +631,65 @@ test "can parse something serious" { \\ ; // const ServerResponse = struct { DescribeRegionsResponse: describe_regions.Response, }; - const parsed_data = try parse(describe_regions.Response, data, .{ .allocator = allocator }); + const parsed_data = try parse(describe_regions.Response, data, .{ .allocator = allocator, .elementToParse = findResult }); defer parsed_data.deinit(); try testing.expect(parsed_data.parsed_value.regions != null); 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.?); } +const StsGetAccesskeyInfoResponse: type = struct { + account: ?[]const u8 = null, + + pub fn fieldNameFor(_: @This(), comptime field_name: []const u8) []const u8 { + const mappings = .{ + .account = "Account", + }; + return @field(mappings, field_name); + } +}; +fn findResult(element: *xml.Element, options: ParseOptions) *xml.Element { + _ = options; + // We're looking for a very specific pattern here. We want only two direct + // children. The first one must end with "Result", and the second should + // be our ResponseMetadata node + var children = element.elements(); + var found_metadata = false; + var result_child: ?*xml.Element = null; + var inx: usize = 0; + while (children.next()) |child| : (inx += 1) { + if (std.mem.eql(u8, child.tag, "ResponseMetadata")) { + found_metadata = true; + continue; + } + if (std.mem.endsWith(u8, child.tag, "Result")) { + result_child = child; + continue; + } + if (inx > 1) return element; + return element; // It should only be those two + } + return result_child orelse element; +} +test "can parse a result within a response" { + log.debug("", .{}); + + const allocator = std.testing.allocator; + const data = + \\ + \\ + \\ 123456789012 + \\ + \\ + \\ ec85bf29-1ef0-459a-930e-6446dd14a286 + \\ + \\ + ; + const parsed_data = try parse(StsGetAccesskeyInfoResponse, data, .{ .allocator = allocator, .elementToParse = findResult }); + defer parsed_data.deinit(); + // Response expectations + try std.testing.expect(parsed_data.parsed_value.account != null); + try std.testing.expectEqualStrings("123456789012", parsed_data.parsed_value.account.?); +} test "compiler assertion failure 2" { // std.testing.log_level = .debug;