From f4c306a2dfc1d54904466659cae50bbf04bfe5b2 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Fri, 18 Apr 2025 15:04:43 -0700 Subject: [PATCH] support rest_xml with non-string payloads (e.g. S3 create bucket) --- src/aws.zig | 40 ++++++++++- src/xml_serializer.zig | 148 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 176 insertions(+), 12 deletions(-) diff --git a/src/aws.zig b/src/aws.zig index a3da71c..33a0b16 100644 --- a/src/aws.zig +++ b/src/aws.zig @@ -236,6 +236,8 @@ pub fn Request(comptime request_action: anytype) type { } } aws_request.body = buffer.items; + var rest_xml_body: ?[]const u8 = null; + defer if (rest_xml_body) |b| options.client.allocator.free(b); if (Self.service_meta.aws_protocol == .rest_xml) { if (std.mem.eql(u8, "PUT", aws_request.method) or std.mem.eql(u8, "POST", aws_request.method)) { if (@hasDecl(ActionRequest, "http_payload")) { @@ -255,8 +257,37 @@ pub fn Request(comptime request_action: anytype) type { body_assigned = true; } - if (!body_assigned) - return error.XmlSerializationNotImplemented; + if (!body_assigned) { + const sm = ActionRequest.metaInfo().service_metadata; + if (!std.mem.eql(u8, sm.endpoint_prefix, "s3")) + // Because the attributes below are most likely only + // applicable to s3, we are better off to fail + // early. This portion of the code base should + // only be executed for s3 as no other known + // service uses this protocol + return error.NotImplemented; + + const attrs = try std.fmt.allocPrint( + options.client.allocator, + "xmlns=\"http://{s}.amazonaws.com/doc/{s}/\"", + .{ sm.endpoint_prefix, sm.version }, + ); + defer options.client.allocator.free(attrs); // once serialized, the value should be copied over + + // Need to serialize this + rest_xml_body = try xml_serializer.stringifyAlloc( + options.client.allocator, + payload, + .{ + .whitespace = .indent_2, + .root_name = request.fieldNameFor(ActionRequest.http_payload), + .root_attributes = attrs, + .emit_null_optional_fields = false, + .include_declaration = false, + }, + ); + aws_request.body = rest_xml_body.?; + } } else { return error.NotImplemented; } @@ -2305,7 +2336,10 @@ test "rest_xml_with_input_s3: S3 create bucket" { \\ , test_harness.request_options.request_body); // Response expectations - try std.testing.expectEqualStrings("9PEYBAZ9J7TPRX43", call.response_metadata.request_id); + try std.testing.expectEqualStrings( + "9PEYBAZ9J7TPRX43, host_id: u7lzgW0tIyRP15vSUsVOXxJ37OfVCO8lZmLIVuqeq5EE4tNp9qebb5fy+/kendlZpR4YQE+y4Xg=", + call.response_metadata.request_id, + ); } test "rest_xml_no_input: S3 list buckets" { const allocator = std.testing.allocator; diff --git a/src/xml_serializer.zig b/src/xml_serializer.zig index 23b2f59..91ba069 100644 --- a/src/xml_serializer.zig +++ b/src/xml_serializer.zig @@ -20,6 +20,9 @@ pub const StringifyOptions = struct { /// Root element name to use when serializing a value that doesn't have a natural name root_name: ?[]const u8 = "root", + /// Root attributes (e.g. xmlns="...") that will be added to the root element node only + root_attributes: []const u8 = "", + /// Function to determine the element name for an array item based on the element /// name of the array containing the elements. See arrayElementPluralToSingluarTransformation /// and arrayElementNoopTransformation functions for examples @@ -58,7 +61,10 @@ pub fn stringify( // Start serialization with the root element const root_name = options.root_name; - try serializeValue(value, root_name, options, writer.any(), 0); + if (@typeInfo(@TypeOf(value)) != .optional or value == null) + try serializeValue(value, root_name, options, writer.any(), 0) + else + try serializeValue(value.?, root_name, options, writer.any(), 0); } /// Serializes a value to XML and returns an allocated string @@ -84,18 +90,21 @@ fn serializeValue( ) !void { const T = @TypeOf(value); - try writeIndent(writer, depth, options.whitespace); + // const output_indent = !(!options.emit_null_optional_fields and @typeInfo(@TypeOf(value)) == .optional and value == null); + const output_indent = options.emit_null_optional_fields or @typeInfo(@TypeOf(value)) != .optional or value != null; + + if (output_indent and element_name != null) + try writeIndent(writer, depth, options.whitespace); - // const write_outer_element = - // @typeInfo(T) != .optional or - // options.emit_strings_as_arrays == false or - // (@typeInfo(T) == .optional and element_name != null) or - // (options.emit_strings_as_arrays and (@typeInfo(T) != .array or @typeInfo(T).array.child != u8)); // Start element tag if (@typeInfo(T) != .optional and @typeInfo(T) != .array) { if (element_name) |n| { try writer.writeAll("<"); try writer.writeAll(n); + if (depth == 0 and options.root_attributes.len > 0) { + try writer.writeByte(' '); + try writer.writeAll(options.root_attributes); + } try writer.writeAll(">"); } } @@ -197,8 +206,9 @@ fn serializeValue( else field.name; // TODO: field mapping + const field_value = @field(value, field.name); try serializeValue( - @field(value, field.name), + field_value, field_name, options, writer, @@ -206,7 +216,11 @@ fn serializeValue( ); if (options.whitespace != .minified) { - try writer.writeByte('\n'); + if (!options.emit_null_optional_fields and @typeInfo(@TypeOf(field_value)) == .optional and field_value == null) { + // Skip writing anything + } else { + try writer.writeByte('\n'); + } } } @@ -661,3 +675,119 @@ test "structs with custom field names" { , result); } } + +test "structs with optional values" { + const testing = std.testing; + const allocator = testing.allocator; + + const Person = struct { + first_name: []const u8, + middle_name: ?[]const u8 = null, + last_name: []const u8, + }; + + const person = Person{ + .first_name = "John", + .last_name = "Doe", + }; + + { + const result = try stringifyAlloc( + allocator, + person, + .{ + .whitespace = .indent_2, + .emit_null_optional_fields = false, + .root_attributes = "xmlns=\"http://example.com/blah/xxxx/\"", + }, + ); + defer allocator.free(result); + try testing.expectEqualStrings( + \\ + \\ + \\ John + \\ Doe + \\ + , result); + } +} + +test "optional structs with value" { + const testing = std.testing; + const allocator = testing.allocator; + + const Person = struct { + first_name: []const u8, + middle_name: ?[]const u8 = null, + last_name: []const u8, + }; + + const person: ?Person = Person{ + .first_name = "John", + .last_name = "Doe", + }; + + { + const result = try stringifyAlloc( + allocator, + person, + .{ + .whitespace = .indent_2, + .emit_null_optional_fields = false, + .root_attributes = "xmlns=\"http://example.com/blah/xxxx/\"", + }, + ); + defer allocator.free(result); + try testing.expectEqualStrings( + \\ + \\ + \\ John + \\ Doe + \\ + , result); + } +} + +test "nested optional structs with value" { + const testing = std.testing; + const allocator = testing.allocator; + + const Name = struct { + first_name: []const u8, + middle_name: ?[]const u8 = null, + last_name: []const u8, + }; + + const Person = struct { + name: ?Name, + }; + + const person: ?Person = Person{ + .name = .{ + .first_name = "John", + .last_name = "Doe", + }, + }; + + { + const result = try stringifyAlloc( + allocator, + person, + .{ + .whitespace = .indent_2, + .emit_null_optional_fields = false, + .root_attributes = "xmlns=\"http://example.com/blah/xxxx/\"", + }, + ); + defer allocator.free(result); + try testing.expectEqualStrings( + \\ + \\ + \\ + \\ John + \\ Doe + \\ + \\ + , result); + } +}