diff --git a/src/aws.zig b/src/aws.zig index f7d9f42..f126e62 100644 --- a/src/aws.zig +++ b/src/aws.zig @@ -126,12 +126,15 @@ pub fn Request(comptime request_action: anytype) type { log.debug("Rest method: '{s}'", .{aws_request.method}); log.debug("Rest success code: '{d}'", .{Action.http_config.success_code}); log.debug("Rest raw uri: '{s}'", .{Action.http_config.uri}); + var al = std.ArrayList([]const u8).init(options.client.allocator); + defer al.deinit(); aws_request.path = try buildPath( options.client.allocator, Action.http_config.uri, ActionRequest, request, !std.mem.eql(u8, Self.service_meta.sdk_id, "S3"), + &al, ); defer options.client.allocator.free(aws_request.path); log.debug("Rest processed uri: '{s}'", .{aws_request.path}); @@ -163,7 +166,7 @@ pub fn Request(comptime request_action: anytype) type { defer nameAllocator.deinit(); if (Self.service_meta.aws_protocol == .rest_json_1) { if (std.mem.eql(u8, "PUT", aws_request.method) or std.mem.eql(u8, "POST", aws_request.method)) { - try json.stringify(request, .{ .whitespace = .{} }, buffer.writer()); + try json.stringify(request, .{ .whitespace = .{}, .emit_null = false, .exclude_fields = al.items }, buffer.writer()); } } aws_request.body = buffer.items; @@ -944,6 +947,7 @@ fn buildPath( comptime ActionRequest: type, request: anytype, encode_slash: bool, + replaced_fields: *std.ArrayList([]const u8), ) ![]const u8 { var buffer = try std.ArrayList(u8).initCapacity(allocator, raw_uri.len); // const writer = buffer.writer(); @@ -965,6 +969,7 @@ fn buildPath( const replacement_label = raw_uri[start..end]; inline for (std.meta.fields(ActionRequest)) |field| { if (std.mem.eql(u8, request.fieldNameFor(field.name), replacement_label)) { + try replaced_fields.append(replacement_label); var replacement_buffer = try std.ArrayList(u8).initCapacity(allocator, raw_uri.len); defer replacement_buffer.deinit(); var encoded_buffer = try std.ArrayList(u8).initCapacity(allocator, raw_uri.len); @@ -1254,23 +1259,27 @@ test "REST Json v1 serializes lists in queries" { } test "REST Json v1 buildpath substitutes" { const allocator = std.testing.allocator; + var al = std.ArrayList([]const u8).init(allocator); + defer al.deinit(); const svs = Services(.{.lambda}){}; const request = svs.lambda.list_functions.Request{ .max_items = 1, }; const input_path = "https://myhost/{MaxItems}/"; - const output_path = try buildPath(allocator, input_path, @TypeOf(request), request, true); + const output_path = try buildPath(allocator, input_path, @TypeOf(request), request, true, &al); defer allocator.free(output_path); try std.testing.expectEqualStrings("https://myhost/1/", output_path); } test "REST Json v1 buildpath handles restricted characters" { const allocator = std.testing.allocator; + var al = std.ArrayList([]const u8).init(allocator); + defer al.deinit(); const svs = Services(.{.lambda}){}; const request = svs.lambda.list_functions.Request{ .marker = ":", }; const input_path = "https://myhost/{Marker}/"; - const output_path = try buildPath(allocator, input_path, @TypeOf(request), request, true); + const output_path = try buildPath(allocator, input_path, @TypeOf(request), request, true, &al); defer allocator.free(output_path); try std.testing.expectEqualStrings("https://myhost/%3A/", output_path); } @@ -1970,7 +1979,6 @@ test "rest_json_1_work_with_lambda: lambda tagResource (only), to excercise zig try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); try std.testing.expectEqualStrings( \\{ - \\ "Resource": "arn:aws:lambda:us-west-2:550620852718:function:awsome-lambda-LambdaStackawsomeLambda", \\ "Tags": { \\ "Foo": "Bar" \\ } @@ -1981,6 +1989,45 @@ test "rest_json_1_work_with_lambda: lambda tagResource (only), to excercise zig // Response expectations try std.testing.expectEqualStrings("a521e152-6e32-4e67-9fb3-abc94e34551b", call.response_metadata.request_id); } +test "rest_json_1_url_parameters_not_in_request: lambda update_function_code" { + const allocator = std.testing.allocator; + var test_harness = TestSetup.init(.{ + .allocator = allocator, + .server_response = "{\"CodeSize\": 42}", + .server_response_status = .ok, + .server_response_headers = &.{ + .{ .name = "Content-Type", .value = "application/json" }, + .{ .name = "x-amzn-RequestId", .value = "a521e152-6e32-4e67-9fb3-abc94e34551b" }, + }, + }); + defer test_harness.deinit(); + const options = try test_harness.start(); + const lambda = (Services(.{.lambda}){}).lambda; + const architectures = [_][]const u8{"x86_64"}; + const arches: [][]const u8 = @constCast(architectures[0..]); + const req = services.lambda.update_function_code.Request{ + .function_name = "functionname", + .architectures = arches, + .zip_file = "zipfile", + }; + const call = try Request(lambda.update_function_code).call(req, options); + defer call.deinit(); + test_harness.stop(); + // Request expectations + try std.testing.expectEqual(std.http.Method.PUT, test_harness.request_options.request_method); + try std.testing.expectEqualStrings( + \\{ + \\ "ZipFile": "zipfile", + \\ "Architectures": [ + \\ "x86_64" + \\ ] + \\} + , test_harness.request_options.request_body); + // Due to 17015, we see %253A instead of %3A + try std.testing.expectEqualStrings("/2015-03-31/functions/functionname/code", test_harness.request_options.request_target); + // Response expectations + try std.testing.expectEqualStrings("a521e152-6e32-4e67-9fb3-abc94e34551b", call.response_metadata.request_id); +} test "ec2_query_no_input: EC2 describe regions" { const allocator = std.testing.allocator; var test_harness = TestSetup.init(.{ diff --git a/src/json.zig b/src/json.zig index b9a898c..b29343e 100644 --- a/src/json.zig +++ b/src/json.zig @@ -2756,6 +2756,10 @@ pub const StringifyOptions = struct { } }; + emit_null: bool = true, + + exclude_fields: ?[][]const u8 = null, + /// Controls the whitespace emitted whitespace: ?Whitespace = null, @@ -2855,7 +2859,7 @@ pub fn stringify( } try out_stream.writeByte('{'); - comptime var field_output = false; + var field_output = false; var child_options = options; if (child_options.whitespace) |*child_whitespace| { child_whitespace.indent_level += 1; @@ -2864,34 +2868,46 @@ pub fn stringify( // don't include void fields if (Field.type == void) continue; - if (!field_output) { - field_output = true; - } else { - try out_stream.writeByte(','); - } - if (child_options.whitespace) |child_whitespace| { - try out_stream.writeByte('\n'); - try child_whitespace.outputIndent(out_stream); - } - var field_written = false; - if (comptime std.meta.hasFn(T, "jsonStringifyField")) - field_written = try value.jsonStringifyField(Field.name, child_options, out_stream); + var output_this_field = true; + if (!options.emit_null and @typeInfo(Field.type) == .Optional and @field(value, Field.name) == null) output_this_field = false; - if (!field_written) { - if (comptime std.meta.hasFn(T, "fieldNameFor")) { - const name = value.fieldNameFor(Field.name); - try stringify(name, options, out_stream); - } else { - try stringify(Field.name, options, out_stream); - } - - try out_stream.writeByte(':'); - if (child_options.whitespace) |child_whitespace| { - if (child_whitespace.separator) { - try out_stream.writeByte(' '); + const final_name = if (comptime std.meta.hasFn(T, "fieldNameFor")) + value.fieldNameFor(Field.name) + else + Field.name; + if (options.exclude_fields) |exclude_fields| { + for (exclude_fields) |exclude_field| { + if (std.mem.eql(u8, final_name, exclude_field)) { + output_this_field = false; } } - try stringify(@field(value, Field.name), child_options, out_stream); + } + + if (!field_output) { + field_output = output_this_field; + } else { + if (output_this_field) try out_stream.writeByte(','); + } + if (child_options.whitespace) |child_whitespace| { + if (output_this_field) try out_stream.writeByte('\n'); + if (output_this_field) try child_whitespace.outputIndent(out_stream); + } + var field_written = false; + if (comptime std.meta.hasFn(T, "jsonStringifyField")) { + if (output_this_field) field_written = try value.jsonStringifyField(Field.name, child_options, out_stream); + } + + if (!field_written) { + if (output_this_field) { + try stringify(final_name, options, out_stream); + try out_stream.writeByte(':'); + } + if (child_options.whitespace) |child_whitespace| { + if (child_whitespace.separator) { + if (output_this_field) try out_stream.writeByte(' '); + } + } + if (output_this_field) try stringify(@field(value, Field.name), child_options, out_stream); } } if (field_output) {