support rest_xml with non-string payloads (e.g. S3 create bucket)
All checks were successful
AWS-Zig Build / build-zig-amd64-host (push) Successful in 7m17s

This commit is contained in:
Emil Lerch 2025-04-18 15:04:43 -07:00
parent 52f99bb35f
commit f4c306a2df
Signed by: lobo
GPG key ID: A7B62D657EF764F8
2 changed files with 176 additions and 12 deletions

View file

@ -236,6 +236,8 @@ pub fn Request(comptime request_action: anytype) type {
} }
} }
aws_request.body = buffer.items; 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 (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 (std.mem.eql(u8, "PUT", aws_request.method) or std.mem.eql(u8, "POST", aws_request.method)) {
if (@hasDecl(ActionRequest, "http_payload")) { if (@hasDecl(ActionRequest, "http_payload")) {
@ -255,8 +257,37 @@ pub fn Request(comptime request_action: anytype) type {
body_assigned = true; body_assigned = true;
} }
if (!body_assigned) if (!body_assigned) {
return error.XmlSerializationNotImplemented; 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 { } else {
return error.NotImplemented; return error.NotImplemented;
} }
@ -2305,7 +2336,10 @@ test "rest_xml_with_input_s3: S3 create bucket" {
\\</CreateBucketConfiguration> \\</CreateBucketConfiguration>
, test_harness.request_options.request_body); , test_harness.request_options.request_body);
// Response expectations // 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" { test "rest_xml_no_input: S3 list buckets" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;

View file

@ -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 element name to use when serializing a value that doesn't have a natural name
root_name: ?[]const u8 = "root", 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 /// Function to determine the element name for an array item based on the element
/// name of the array containing the elements. See arrayElementPluralToSingluarTransformation /// name of the array containing the elements. See arrayElementPluralToSingluarTransformation
/// and arrayElementNoopTransformation functions for examples /// and arrayElementNoopTransformation functions for examples
@ -58,7 +61,10 @@ pub fn stringify(
// Start serialization with the root element // Start serialization with the root element
const root_name = options.root_name; 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 /// Serializes a value to XML and returns an allocated string
@ -84,18 +90,21 @@ fn serializeValue(
) !void { ) !void {
const T = @TypeOf(value); const T = @TypeOf(value);
// 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); 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 // Start element tag
if (@typeInfo(T) != .optional and @typeInfo(T) != .array) { if (@typeInfo(T) != .optional and @typeInfo(T) != .array) {
if (element_name) |n| { if (element_name) |n| {
try writer.writeAll("<"); try writer.writeAll("<");
try writer.writeAll(n); 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(">"); try writer.writeAll(">");
} }
} }
@ -197,8 +206,9 @@ fn serializeValue(
else else
field.name; // TODO: field mapping field.name; // TODO: field mapping
const field_value = @field(value, field.name);
try serializeValue( try serializeValue(
@field(value, field.name), field_value,
field_name, field_name,
options, options,
writer, writer,
@ -206,9 +216,13 @@ fn serializeValue(
); );
if (options.whitespace != .minified) { if (options.whitespace != .minified) {
if (!options.emit_null_optional_fields and @typeInfo(@TypeOf(field_value)) == .optional and field_value == null) {
// Skip writing anything
} else {
try writer.writeByte('\n'); try writer.writeByte('\n');
} }
} }
}
try writeIndent(writer, depth, options.whitespace); try writeIndent(writer, depth, options.whitespace);
}, },
@ -661,3 +675,119 @@ test "structs with custom field names" {
, result); , 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(
\\<?xml version="1.0" encoding="UTF-8"?>
\\<root xmlns="http://example.com/blah/xxxx/">
\\ <first_name>John</first_name>
\\ <last_name>Doe</last_name>
\\</root>
, 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(
\\<?xml version="1.0" encoding="UTF-8"?>
\\<root xmlns="http://example.com/blah/xxxx/">
\\ <first_name>John</first_name>
\\ <last_name>Doe</last_name>
\\</root>
, 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(
\\<?xml version="1.0" encoding="UTF-8"?>
\\<root xmlns="http://example.com/blah/xxxx/">
\\ <name>
\\ <first_name>John</first_name>
\\ <last_name>Doe</last_name>
\\ </name>
\\</root>
, result);
}
}