2022-02-11 17:22:32 +00:00
|
|
|
const std = @import("std");
|
|
|
|
const xml = @import("xml.zig");
|
2022-02-16 22:01:29 +00:00
|
|
|
const date = @import("date.zig");
|
2022-02-11 17:22:32 +00:00
|
|
|
|
2022-02-11 21:36:38 +00:00
|
|
|
const log = std.log.scoped(.xml_shaper);
|
|
|
|
|
2024-03-01 04:41:03 +00:00
|
|
|
pub const Element = xml.Element;
|
|
|
|
|
2022-02-16 22:01:29 +00:00
|
|
|
pub fn Parsed(comptime T: type) type {
|
2022-02-11 17:22:32 +00:00
|
|
|
return struct {
|
2022-02-16 22:01:29 +00:00
|
|
|
// Forcing an arean allocator isn't my favorite choice here, but
|
|
|
|
// is the simplest way to handle deallocation in the event of
|
|
|
|
// an error
|
|
|
|
allocator: std.heap.ArenaAllocator,
|
2022-02-11 17:22:32 +00:00
|
|
|
parsed_value: T,
|
2022-02-16 22:01:29 +00:00
|
|
|
document: xml.Document,
|
2022-02-11 17:22:32 +00:00
|
|
|
|
|
|
|
const Self = @This();
|
|
|
|
|
2022-02-16 22:01:29 +00:00
|
|
|
pub fn init(allocator: std.heap.ArenaAllocator, parsedObj: T, document: xml.Document) Self {
|
2022-02-11 17:22:32 +00:00
|
|
|
return .{
|
|
|
|
.allocator = allocator,
|
|
|
|
.parsed_value = parsedObj,
|
2022-02-16 22:01:29 +00:00
|
|
|
.document = document,
|
2022-02-11 17:22:32 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn deinit(self: Self) void {
|
2022-02-16 22:01:29 +00:00
|
|
|
self.allocator.deinit();
|
|
|
|
// deinitObject(self.allocator, self.parsed_value);
|
|
|
|
// self.document.deinit();
|
2022-02-11 17:22:32 +00:00
|
|
|
}
|
2022-02-16 22:01:29 +00:00
|
|
|
};
|
|
|
|
}
|
2022-02-11 17:22:32 +00:00
|
|
|
|
2022-02-16 22:01:29 +00:00
|
|
|
// This is dead code and can be removed with the move to ArenaAllocator
|
|
|
|
fn deinitObject(allocator: std.mem.Allocator, obj: anytype) void {
|
|
|
|
switch (@typeInfo(@TypeOf(obj))) {
|
|
|
|
.Optional => if (obj) |o| deinitObject(allocator, o),
|
|
|
|
.Union => |union_info| {
|
|
|
|
inline for (union_info.fields) |field| {
|
|
|
|
std.debug.print("{s}", field); // need to find active field and deinit it
|
|
|
|
}
|
|
|
|
},
|
|
|
|
.Struct => |struct_info| {
|
|
|
|
inline for (struct_info.fields) |field| {
|
|
|
|
deinitObject(allocator, @field(obj, field.name));
|
|
|
|
}
|
|
|
|
},
|
|
|
|
.Array => {}, // Not implemented below
|
|
|
|
.Pointer => |ptr_info| {
|
|
|
|
switch (ptr_info.size) {
|
|
|
|
.One => {
|
|
|
|
deinitObject(allocator, obj.*);
|
|
|
|
allocator.free(obj);
|
2022-02-11 17:22:32 +00:00
|
|
|
},
|
2022-02-16 22:01:29 +00:00
|
|
|
.Many => {},
|
|
|
|
.C => {},
|
|
|
|
.Slice => {
|
|
|
|
for (obj) |child|
|
|
|
|
deinitObject(allocator, child);
|
|
|
|
allocator.free(obj);
|
2022-02-11 17:22:32 +00:00
|
|
|
},
|
|
|
|
}
|
2022-02-16 22:01:29 +00:00
|
|
|
},
|
|
|
|
//.Bool, .Float, .ComptimeFloat, .Int, .ComptimeInt, .Enum, .Opaque => {}, // no allocations here
|
|
|
|
else => {},
|
|
|
|
}
|
2022-02-11 17:22:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// should we just use json parse options?
|
|
|
|
pub const ParseOptions = struct {
|
|
|
|
allocator: ?std.mem.Allocator = null,
|
2023-08-04 17:06:54 +00:00
|
|
|
match_predicate_ptr: ?*const fn (a: []const u8, b: []const u8, options: xml.PredicateOptions) anyerror!bool = null,
|
2024-03-01 04:41:03 +00:00
|
|
|
/// 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,
|
2022-02-11 17:22:32 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
pub fn parse(comptime T: type, source: []const u8, options: ParseOptions) !Parsed(T) {
|
|
|
|
if (options.allocator == null)
|
|
|
|
return error.AllocatorRequired; // we are only leaving it be null for compatibility with json
|
|
|
|
const allocator = options.allocator.?;
|
2022-02-16 22:01:29 +00:00
|
|
|
var arena_allocator = std.heap.ArenaAllocator.init(allocator);
|
|
|
|
const aa = arena_allocator.allocator();
|
|
|
|
errdefer arena_allocator.deinit();
|
|
|
|
const parsed = try xml.parse(aa, source);
|
|
|
|
errdefer parsed.deinit();
|
|
|
|
const opts = ParseOptions{
|
|
|
|
.allocator = aa,
|
2023-08-05 19:41:04 +00:00
|
|
|
.match_predicate_ptr = options.match_predicate_ptr,
|
2022-02-16 22:01:29 +00:00
|
|
|
};
|
|
|
|
|
2024-03-01 04:41:03 +00:00
|
|
|
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);
|
2022-02-11 17:22:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn parseInternal(comptime T: type, element: *xml.Element, options: ParseOptions) !T {
|
|
|
|
switch (@typeInfo(T)) {
|
|
|
|
.Bool => {
|
|
|
|
if (std.ascii.eqlIgnoreCase("true", element.children.items[0].CharData))
|
|
|
|
return true;
|
|
|
|
if (std.ascii.eqlIgnoreCase("false", element.children.items[0].CharData))
|
|
|
|
return false;
|
|
|
|
return error.UnexpectedToken;
|
|
|
|
},
|
|
|
|
.Float, .ComptimeFloat => {
|
2022-02-16 22:01:29 +00:00
|
|
|
return std.fmt.parseFloat(T, element.children.items[0].CharData) catch |e| {
|
|
|
|
if (log_parse_traces) {
|
|
|
|
std.log.err(
|
2023-08-05 20:29:23 +00:00
|
|
|
"Could not parse '{s}' as float in element '{s}': {any}",
|
2022-02-16 22:01:29 +00:00
|
|
|
.{
|
|
|
|
element.children.items[0].CharData,
|
|
|
|
element.tag,
|
|
|
|
e,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
if (@errorReturnTrace()) |trace| {
|
|
|
|
std.debug.dumpStackTrace(trace.*);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return e;
|
|
|
|
};
|
2022-02-11 17:22:32 +00:00
|
|
|
},
|
|
|
|
.Int, .ComptimeInt => {
|
2022-02-16 22:01:29 +00:00
|
|
|
// 2021-10-05T16:39:45.000Z
|
|
|
|
return std.fmt.parseInt(T, element.children.items[0].CharData, 10) catch |e| {
|
|
|
|
if (element.children.items[0].CharData[element.children.items[0].CharData.len - 1] == 'Z') {
|
|
|
|
// We have an iso8601 in an integer field (we think)
|
|
|
|
// Try to coerce this into our type
|
|
|
|
const timestamp = try date.parseIso8601ToTimestamp(element.children.items[0].CharData);
|
2023-08-05 19:41:04 +00:00
|
|
|
return std.math.cast(T, timestamp).?;
|
2022-02-16 22:01:29 +00:00
|
|
|
}
|
|
|
|
if (log_parse_traces) {
|
|
|
|
std.log.err(
|
2023-08-05 20:29:23 +00:00
|
|
|
"Could not parse '{s}' as integer in element '{s}': {any}",
|
2022-02-16 22:01:29 +00:00
|
|
|
.{
|
|
|
|
element.children.items[0].CharData,
|
|
|
|
element.tag,
|
|
|
|
e,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
if (@errorReturnTrace()) |trace| {
|
|
|
|
std.debug.dumpStackTrace(trace.*);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return e;
|
|
|
|
};
|
2022-02-11 17:22:32 +00:00
|
|
|
},
|
|
|
|
.Optional => |optional_info| {
|
|
|
|
if (element.children.items.len == 0) {
|
|
|
|
// This is almost certainly incomplete. Empty strings? xsi:nil?
|
|
|
|
return null;
|
2022-05-28 22:50:07 +00:00
|
|
|
}
|
|
|
|
if (element.children.items.len > 0) {
|
2022-02-11 17:22:32 +00:00
|
|
|
// return try parseInternal(optional_info.child, element.elements().next().?, options);
|
|
|
|
return try parseInternal(optional_info.child, element, options);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
.Enum => |enum_info| {
|
|
|
|
_ = enum_info;
|
|
|
|
// const numeric: ?enum_info.tag_type = std.fmt.parseInt(enum_info.tag_type, element.children.items[0].CharData, 10) catch null;
|
|
|
|
// if (numeric) |num| {
|
|
|
|
// return std.meta.intToEnum(T, num);
|
|
|
|
// } else {
|
|
|
|
// // json parser handles escaping - could this happen here or does chardata handle?
|
|
|
|
// return std.meta.stringToEnum(T, element.CharData);
|
|
|
|
// }
|
|
|
|
},
|
|
|
|
.Union => |union_info| {
|
|
|
|
if (union_info.tag_type) |_| {
|
|
|
|
// try each of the union fields until we find one that matches
|
|
|
|
// inline for (union_info.fields) |u_field| {
|
|
|
|
// // take a copy of tokens so we can withhold mutations until success
|
|
|
|
// var tokens_copy = tokens.*;
|
2023-08-05 19:41:04 +00:00
|
|
|
// if (parseInternal(u_field.type, token, &tokens_copy, options)) |value| {
|
2022-02-11 17:22:32 +00:00
|
|
|
// tokens.* = tokens_copy;
|
|
|
|
// return @unionInit(T, u_field.name, value);
|
|
|
|
// } else |err| {
|
|
|
|
// // Bubble up error.OutOfMemory
|
|
|
|
// // Parsing some types won't have OutOfMemory in their
|
|
|
|
// // error-sets, for the condition to be valid, merge it in.
|
|
|
|
// if (@as(@TypeOf(err) || error{OutOfMemory}, err) == error.OutOfMemory) return err;
|
|
|
|
// // Bubble up AllocatorRequired, as it indicates missing option
|
|
|
|
// if (@as(@TypeOf(err) || error{AllocatorRequired}, err) == error.AllocatorRequired) return err;
|
|
|
|
// // otherwise continue through the `inline for`
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
return error.NoUnionMembersMatched;
|
|
|
|
}
|
2022-05-28 22:50:07 +00:00
|
|
|
@compileError("Unable to parse into untagged union '" ++ @typeName(T) ++ "'");
|
2022-02-11 17:22:32 +00:00
|
|
|
},
|
|
|
|
.Struct => |struct_info| {
|
|
|
|
var r: T = undefined;
|
|
|
|
var fields_seen = [_]bool{false} ** struct_info.fields.len;
|
|
|
|
var fields_set: u64 = 0;
|
|
|
|
// errdefer {
|
|
|
|
// // TODO: why so high here? This was needed for ec2 describe instances
|
|
|
|
// @setEvalBranchQuota(100000);
|
|
|
|
// inline for (struct_info.fields) |field, i| {
|
|
|
|
// if (fields_seen[i] and !field.is_comptime) {
|
2023-08-05 19:41:04 +00:00
|
|
|
// parseFree(field.type, @field(r, field.name), options);
|
2022-02-11 17:22:32 +00:00
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
|
|
|
// XML parser provides CharData for whitespace around elements.
|
|
|
|
// We shall ignore extra data for the moment as a performance thing
|
|
|
|
// if (element.children.items.len > struct_info.fields.len) {
|
|
|
|
// std.debug.print("element children: {d}, struct fields: {d}\n", .{ element.children.items.len, struct_info.fields.len });
|
|
|
|
// for (element.children.items) |child, i| {
|
|
|
|
// switch (child) {
|
|
|
|
// .CharData => std.debug.print("{d}: {s}\n", .{ i, child }),
|
|
|
|
// .Comment => {},
|
|
|
|
// .Element => {},
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// return error.MoreElementsThanFields;
|
|
|
|
// }
|
|
|
|
|
2022-02-11 21:36:38 +00:00
|
|
|
log.debug("Processing fields in struct: {s}", .{@typeName(T)});
|
2023-08-04 17:07:58 +00:00
|
|
|
inline for (struct_info.fields, 0..) |field, i| {
|
upgrade to nominated zig 2024.3.0-mach (0.12.0-dev.3180+83e578a18)
There were significant changes to the way HTTP operates since 0.11,
effecting client operations, but more substantially, the server
implementation, which effected the test harness.
std.http.Headers was removed, including the getFirstValue function, which
needed to be replicated. On the plus side, a std.http.Header struct was
added, identical to our own structure, so I have removed out own header
in favor of stdlib.
On the Http client side, I have switched to use the fetch API. Proxy
support is built in, but we are using (mostly) our own implementation
for now, with the remaining conversion left as a TODO item. Raw URIs are
now supported, so the workaround for issue 17015 has been removed. Large
payloads should also be fixed, but this has not been tested.
The standard library now adds the content-length header
(unconditionally), which is a decision of dubious nature. I have removed
the addition of content-length, which also means it is not present
during signing. This should be allowed.
Dependency loop on fieldTransformer was fixed. This should have been
a problem on zig 0.11, but was not. This effected the API for the json
parsing, but we were not using that. At the call site, these did not
need to be specified as references.
With the http server no longer doing all the allocations it once was,
the test harness now has a lot more allocations to perform. To alleviate
the bookeeping, this was moved to an Arena allocator. The client,
which is really what is under test, continues to use the allocator
passed.
2024-04-02 16:13:45 +00:00
|
|
|
var name: []const u8 = field.name;
|
2022-02-11 21:36:38 +00:00
|
|
|
var found_value = false;
|
upgrade to nominated zig 2024.3.0-mach (0.12.0-dev.3180+83e578a18)
There were significant changes to the way HTTP operates since 0.11,
effecting client operations, but more substantially, the server
implementation, which effected the test harness.
std.http.Headers was removed, including the getFirstValue function, which
needed to be replicated. On the plus side, a std.http.Header struct was
added, identical to our own structure, so I have removed out own header
in favor of stdlib.
On the Http client side, I have switched to use the fetch API. Proxy
support is built in, but we are using (mostly) our own implementation
for now, with the remaining conversion left as a TODO item. Raw URIs are
now supported, so the workaround for issue 17015 has been removed. Large
payloads should also be fixed, but this has not been tested.
The standard library now adds the content-length header
(unconditionally), which is a decision of dubious nature. I have removed
the addition of content-length, which also means it is not present
during signing. This should be allowed.
Dependency loop on fieldTransformer was fixed. This should have been
a problem on zig 0.11, but was not. This effected the API for the json
parsing, but we were not using that. At the call site, these did not
need to be specified as references.
With the http server no longer doing all the allocations it once was,
the test harness now has a lot more allocations to perform. To alleviate
the bookeeping, this was moved to an Arena allocator. The client,
which is really what is under test, continues to use the allocator
passed.
2024-04-02 16:13:45 +00:00
|
|
|
if (comptime std.meta.hasFn(T, "fieldNameFor"))
|
2022-02-11 17:22:32 +00:00
|
|
|
name = r.fieldNameFor(field.name);
|
2022-02-11 21:36:38 +00:00
|
|
|
log.debug("Field name: {s}, Element: {s}, Adjusted field name: {s}", .{ field.name, element.tag, name });
|
2022-02-11 17:22:32 +00:00
|
|
|
var iterator = element.findChildrenByTag(name);
|
2023-08-05 19:41:04 +00:00
|
|
|
if (options.match_predicate_ptr) |predicate_ptr| {
|
|
|
|
iterator.predicate = predicate_ptr;
|
2022-02-11 17:22:32 +00:00
|
|
|
iterator.predicate_options = .{ .allocator = options.allocator.? };
|
|
|
|
}
|
|
|
|
if (try iterator.next()) |child| {
|
|
|
|
// I don't know that we would use comptime here. I'm also
|
|
|
|
// not sure the nuance of setting this...
|
|
|
|
// if (field.is_comptime) {
|
2023-08-05 19:41:04 +00:00
|
|
|
// if (!try parsesTo(field.type, field.default_value.?, tokens, options)) {
|
2022-02-11 17:22:32 +00:00
|
|
|
// return error.UnexpectedValue;
|
|
|
|
// }
|
|
|
|
// } else {
|
2022-02-11 21:36:38 +00:00
|
|
|
log.debug("Found child element {s}", .{child.tag});
|
2022-02-16 22:01:29 +00:00
|
|
|
// TODO: how do we errdefer this?
|
2023-08-05 19:41:04 +00:00
|
|
|
@field(r, field.name) = try parseInternal(field.type, child, options);
|
2022-02-11 17:22:32 +00:00
|
|
|
fields_seen[i] = true;
|
|
|
|
fields_set = fields_set + 1;
|
2022-02-11 21:36:38 +00:00
|
|
|
found_value = true;
|
2022-02-11 17:22:32 +00:00
|
|
|
}
|
2023-08-05 19:41:04 +00:00
|
|
|
if (@typeInfo(field.type) == .Optional) {
|
2022-05-28 22:49:17 +00:00
|
|
|
// Test "compiler assertion failure 2"
|
|
|
|
// Zig compiler bug circa 0.9.0. Using "and !found_value"
|
|
|
|
// in the if statement above will trigger assertion failure
|
|
|
|
if (!found_value) {
|
2024-03-01 04:41:03 +00:00
|
|
|
log.debug("Child element not found, but field optional. Setting {s}=null", .{field.name});
|
2023-08-05 19:41:04 +00:00
|
|
|
// @compileLog("Optional: Field name ", field.name, ", type ", field.type);
|
2022-05-28 22:49:17 +00:00
|
|
|
@field(r, field.name) = null;
|
|
|
|
fields_set = fields_set + 1;
|
|
|
|
found_value = true;
|
|
|
|
}
|
2022-02-16 22:01:29 +00:00
|
|
|
}
|
2022-02-11 21:36:38 +00:00
|
|
|
// Using this else clause breaks zig, so we'll use a boolean instead
|
2022-02-16 22:01:29 +00:00
|
|
|
if (!found_value) {
|
|
|
|
log.err("Could not find a value for field {s}. Looking for {s} in element {s}", .{ field.name, name, element.tag });
|
|
|
|
return error.NoValueForField;
|
|
|
|
}
|
2022-02-11 21:36:38 +00:00
|
|
|
// } else {
|
|
|
|
// return error.NoValueForField;
|
|
|
|
// }
|
2022-02-11 17:22:32 +00:00
|
|
|
}
|
|
|
|
if (fields_set != struct_info.fields.len)
|
|
|
|
return error.FieldElementMismatch; // see fields_seen for details
|
|
|
|
return r;
|
|
|
|
},
|
|
|
|
.Array => //|array_info| {
|
|
|
|
return error.ArrayNotImplemented,
|
|
|
|
// switch (token) {
|
|
|
|
// .ArrayBegin => {
|
|
|
|
// var r: T = undefined;
|
|
|
|
// var i: usize = 0;
|
|
|
|
// errdefer {
|
|
|
|
// while (true) : (i -= 1) {
|
|
|
|
// parseFree(arrayInfo.child, r[i], options);
|
|
|
|
// if (i == 0) break;
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// while (i < r.len) : (i += 1) {
|
|
|
|
// r[i] = try parse(arrayInfo.child, tokens, options);
|
|
|
|
// }
|
|
|
|
// const tok = (try tokens.next()) orelse return error.UnexpectedEndOfJson;
|
|
|
|
// switch (tok) {
|
|
|
|
// .ArrayEnd => {},
|
|
|
|
// else => return error.UnexpectedToken,
|
|
|
|
// }
|
|
|
|
// return r;
|
|
|
|
// },
|
|
|
|
// .String => |stringToken| {
|
|
|
|
// if (arrayInfo.child != u8) return error.UnexpectedToken;
|
|
|
|
// var r: T = undefined;
|
|
|
|
// const source_slice = stringToken.slice(tokens.slice, tokens.i - 1);
|
|
|
|
// switch (stringToken.escapes) {
|
|
|
|
// .None => mem.copy(u8, &r, source_slice),
|
|
|
|
// .Some => try unescapeValidString(&r, source_slice),
|
|
|
|
// }
|
|
|
|
// return r;
|
|
|
|
// },
|
|
|
|
// else => return error.UnexpectedToken,
|
|
|
|
// }
|
|
|
|
// },
|
|
|
|
.Pointer => |ptr_info| {
|
|
|
|
const allocator = options.allocator orelse return error.AllocatorRequired;
|
|
|
|
switch (ptr_info.size) {
|
|
|
|
.One => {
|
|
|
|
const r: T = try allocator.create(ptr_info.child);
|
|
|
|
errdefer allocator.free(r);
|
|
|
|
r.* = try parseInternal(ptr_info.child, element, options);
|
|
|
|
return r;
|
|
|
|
},
|
|
|
|
.Slice => {
|
|
|
|
// TODO: Detect and deal with arrays. This will require two
|
|
|
|
// passes through the element children - one to
|
|
|
|
// determine if it is an array, one to parse the elements
|
|
|
|
// <Items>
|
|
|
|
// <Item>foo</Item>
|
|
|
|
// <Item>bar</Item>
|
|
|
|
// <Items>
|
|
|
|
if (ptr_info.child != u8) {
|
2022-02-11 21:36:38 +00:00
|
|
|
log.debug("type = {s}, ptr_info.child == {s}, element = {s}", .{ @typeName(T), @typeName(ptr_info.child), element.tag });
|
|
|
|
var iterator = element.elements();
|
|
|
|
var children = std.ArrayList(ptr_info.child).init(allocator);
|
|
|
|
defer children.deinit();
|
|
|
|
while (iterator.next()) |child_element| {
|
|
|
|
try children.append(try parseInternal(ptr_info.child, child_element, options));
|
2022-02-11 17:22:32 +00:00
|
|
|
}
|
2022-02-11 21:36:38 +00:00
|
|
|
return children.toOwnedSlice();
|
|
|
|
// var inx: usize = 0;
|
|
|
|
// while (inx < children.len) {
|
|
|
|
// switch (element.children.items[inx]) {
|
|
|
|
// .Element => children[inx] = try parseInternal(ptr_info.child, element.children.items[inx].Element, options),
|
|
|
|
// .CharData => children[inx] = try allocator.dupe(u8, element.children.items[inx].CharData),
|
|
|
|
// .Comment => children[inx] = try allocator.dupe(u8, element.children.items[inx].Comment), // This might be an error...
|
|
|
|
// }
|
|
|
|
// inx += 1;
|
|
|
|
// }
|
2022-02-11 17:22:32 +00:00
|
|
|
}
|
|
|
|
return try allocator.dupe(u8, element.children.items[0].CharData);
|
|
|
|
},
|
|
|
|
.Many => {
|
|
|
|
return error.ManyPointerSizeNotImplemented;
|
|
|
|
},
|
|
|
|
.C => {
|
|
|
|
return error.CPointerSizeNotImplemented;
|
|
|
|
},
|
|
|
|
}
|
|
|
|
},
|
|
|
|
else => @compileError("Unable to parse into type '" ++ @typeName(T) ++ "'"),
|
|
|
|
// }
|
|
|
|
// },
|
|
|
|
// else => @compileError("Unable to parse into type '" ++ @typeName(T) ++ "'"),
|
|
|
|
}
|
|
|
|
unreachable;
|
|
|
|
}
|
|
|
|
pub fn fuzzyEqual(a: []const u8, b: []const u8, options: xml.PredicateOptions) !bool {
|
|
|
|
const allocator = options.allocator orelse return error.AllocatorRequired;
|
|
|
|
// std.debug.print("raw: a = '{s}', b = '{s}'\n", .{ a, b });
|
|
|
|
const lower_a = try std.ascii.allocLowerString(allocator, a);
|
|
|
|
defer allocator.free(lower_a);
|
|
|
|
const lower_b = try std.ascii.allocLowerString(allocator, b);
|
|
|
|
defer allocator.free(lower_b);
|
|
|
|
// std.debug.print("lower: a = '{s}', b = '{s}'\n", .{ lower_a, lower_b });
|
|
|
|
const normal_a = normalize(lower_a);
|
|
|
|
const normal_b = normalize(lower_b);
|
|
|
|
|
|
|
|
// std.debug.print("normal: a = '{s}', b = '{s}'\n", .{ normal_a, normal_b });
|
|
|
|
return std.mem.eql(u8, normal_a, normal_b);
|
|
|
|
}
|
|
|
|
|
|
|
|
fn normalize(val: []u8) []u8 {
|
2024-01-09 23:16:29 +00:00
|
|
|
var underscores: usize = 0;
|
2023-08-04 17:07:58 +00:00
|
|
|
for (val, 0..) |ch, i| {
|
2022-02-11 17:22:32 +00:00
|
|
|
if (ch == '_') {
|
|
|
|
underscores = underscores + 1;
|
|
|
|
} else {
|
|
|
|
val[i - underscores] = ch;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return val[0 .. val.len - underscores];
|
|
|
|
}
|
|
|
|
|
|
|
|
const testing = std.testing;
|
|
|
|
test "can parse a simple type" {
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
// defer allocator.free(snake_case);
|
|
|
|
const data =
|
|
|
|
\\<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
\\<Example xmlns="http://example.example.com/doc/2016-11-15/">
|
|
|
|
\\ <fooBar>bar</fooBar>
|
|
|
|
\\</Example>
|
|
|
|
;
|
|
|
|
const Example = struct {
|
|
|
|
foo_bar: []const u8,
|
|
|
|
};
|
|
|
|
// std.debug.print("{s}", .{data});
|
2023-08-05 20:29:23 +00:00
|
|
|
const parsed_data = try parse(Example, data, .{ .allocator = allocator, .match_predicate_ptr = fuzzyEqual });
|
2022-02-11 17:22:32 +00:00
|
|
|
defer parsed_data.deinit();
|
|
|
|
try testing.expectEqualStrings("bar", parsed_data.parsed_value.foo_bar);
|
|
|
|
}
|
|
|
|
|
|
|
|
test "can parse a boolean type" {
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
// defer allocator.free(snake_case);
|
|
|
|
const data =
|
|
|
|
\\<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
\\<Example xmlns="http://example.example.com/doc/2016-11-15/">
|
|
|
|
\\ <fooBar>true</fooBar>
|
|
|
|
\\</Example>
|
|
|
|
;
|
|
|
|
const Example = struct {
|
|
|
|
foo_bar: bool,
|
|
|
|
};
|
|
|
|
// std.debug.print("{s}", .{data});
|
2023-08-05 20:29:23 +00:00
|
|
|
const parsed_data = try parse(Example, data, .{ .allocator = allocator, .match_predicate_ptr = fuzzyEqual });
|
2022-02-11 17:22:32 +00:00
|
|
|
defer parsed_data.deinit();
|
|
|
|
try testing.expectEqual(true, parsed_data.parsed_value.foo_bar);
|
|
|
|
}
|
|
|
|
test "can parse an integer type" {
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
// defer allocator.free(snake_case);
|
|
|
|
const data =
|
|
|
|
\\<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
\\<Example xmlns="http://example.example.com/doc/2016-11-15/">
|
|
|
|
\\ <fooBar>42</fooBar>
|
|
|
|
\\</Example>
|
|
|
|
;
|
|
|
|
const Example = struct {
|
|
|
|
foo_bar: u8,
|
|
|
|
};
|
|
|
|
// std.debug.print("{s}", .{data});
|
2023-08-05 20:29:23 +00:00
|
|
|
const parsed_data = try parse(Example, data, .{ .allocator = allocator, .match_predicate_ptr = fuzzyEqual });
|
2022-02-11 17:22:32 +00:00
|
|
|
defer parsed_data.deinit();
|
|
|
|
try testing.expectEqual(@as(u8, 42), parsed_data.parsed_value.foo_bar);
|
|
|
|
}
|
|
|
|
test "can parse an optional boolean type" {
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
const data =
|
|
|
|
\\<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
\\<Example xmlns="http://example.example.com/doc/2016-11-15/">
|
|
|
|
\\ <fooBar>true</fooBar>
|
|
|
|
\\</Example>
|
|
|
|
;
|
2022-02-11 21:36:38 +00:00
|
|
|
const ExampleDoesNotMatter = struct {
|
2022-02-11 17:22:32 +00:00
|
|
|
foo_bar: ?bool = null,
|
|
|
|
};
|
2023-08-05 20:29:23 +00:00
|
|
|
const parsed_data = try parse(ExampleDoesNotMatter, data, .{ .allocator = allocator, .match_predicate_ptr = fuzzyEqual });
|
2022-02-11 17:22:32 +00:00
|
|
|
defer parsed_data.deinit();
|
|
|
|
try testing.expectEqual(@as(?bool, true), parsed_data.parsed_value.foo_bar);
|
|
|
|
}
|
2022-02-11 21:36:38 +00:00
|
|
|
|
2022-02-16 22:01:29 +00:00
|
|
|
test "can coerce 8601 date to integer" {
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
const data =
|
|
|
|
\\<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
\\<Example xmlns="http://example.example.com/doc/2016-11-15/">
|
|
|
|
\\ <fooBar>2021-10-05T16:39:45.000Z</fooBar>
|
|
|
|
\\</Example>
|
|
|
|
;
|
|
|
|
const ExampleDoesNotMatter = struct {
|
|
|
|
foo_bar: ?i64 = null,
|
|
|
|
};
|
2023-08-05 20:29:23 +00:00
|
|
|
const parsed_data = try parse(ExampleDoesNotMatter, data, .{ .allocator = allocator, .match_predicate_ptr = fuzzyEqual });
|
2022-02-16 22:01:29 +00:00
|
|
|
defer parsed_data.deinit();
|
|
|
|
try testing.expectEqual(@as(i64, 1633451985), parsed_data.parsed_value.foo_bar.?);
|
|
|
|
}
|
2022-05-28 22:50:38 +00:00
|
|
|
// This is the simplest test so far that breaks zig (circa 0.9.0)
|
|
|
|
// See "Using this else clause breaks zig, so we'll use a boolean instead"
|
2022-02-11 21:36:38 +00:00
|
|
|
test "can parse a boolean type (two fields)" {
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
const data =
|
|
|
|
\\<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
\\<Example xmlns="http://example.example.com/doc/2016-11-15/">
|
|
|
|
\\ <fooBar>true</fooBar>
|
|
|
|
\\ <fooBaz>true</fooBaz>
|
|
|
|
\\</Example>
|
|
|
|
;
|
|
|
|
const ExampleDoesNotMatter = struct {
|
|
|
|
foo_bar: bool,
|
|
|
|
foo_baz: bool,
|
|
|
|
};
|
2023-08-05 20:29:23 +00:00
|
|
|
const parsed_data = try parse(ExampleDoesNotMatter, data, .{ .allocator = allocator, .match_predicate_ptr = fuzzyEqual });
|
2022-02-11 21:36:38 +00:00
|
|
|
defer parsed_data.deinit();
|
|
|
|
try testing.expectEqual(@as(bool, true), parsed_data.parsed_value.foo_bar);
|
|
|
|
}
|
2022-02-16 22:01:29 +00:00
|
|
|
var log_parse_traces = true;
|
|
|
|
test "can error without leaking memory" {
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
const data =
|
|
|
|
\\<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
\\<Example xmlns="http://example.example.com/doc/2016-11-15/">
|
|
|
|
\\ <fooBar>true</fooBar>
|
|
|
|
\\ <fooBaz>12.345</fooBaz>
|
|
|
|
\\</Example>
|
|
|
|
;
|
|
|
|
const ExampleDoesNotMatter = struct {
|
|
|
|
foo_bar: bool,
|
|
|
|
foo_baz: u64,
|
|
|
|
};
|
|
|
|
log_parse_traces = false;
|
|
|
|
defer log_parse_traces = true;
|
|
|
|
try std.testing.expectError(
|
|
|
|
error.InvalidCharacter,
|
2023-08-05 20:29:23 +00:00
|
|
|
parse(ExampleDoesNotMatter, data, .{ .allocator = allocator, .match_predicate_ptr = fuzzyEqual }),
|
2022-02-16 22:01:29 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-02-11 17:22:32 +00:00
|
|
|
test "can parse a nested type" {
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
const data =
|
|
|
|
\\<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
\\<Example xmlns="http://example.example.com/doc/2016-11-15/">
|
|
|
|
\\ <foo>
|
|
|
|
\\ <bar>baz</bar>
|
|
|
|
\\ </foo>
|
|
|
|
\\</Example>
|
|
|
|
;
|
|
|
|
const Example = struct {
|
|
|
|
foo: struct {
|
|
|
|
bar: []const u8,
|
|
|
|
},
|
|
|
|
};
|
2023-08-05 20:29:23 +00:00
|
|
|
const parsed_data = try parse(Example, data, .{ .allocator = allocator, .match_predicate_ptr = fuzzyEqual });
|
2022-02-11 17:22:32 +00:00
|
|
|
defer parsed_data.deinit();
|
|
|
|
try testing.expectEqualStrings("baz", parsed_data.parsed_value.foo.bar);
|
|
|
|
}
|
2022-02-11 21:36:38 +00:00
|
|
|
test "can parse a nested type - two fields" {
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
const data =
|
|
|
|
\\<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
\\<Example xmlns="http://example.example.com/doc/2016-11-15/">
|
|
|
|
\\ <foo>
|
|
|
|
\\ <bar>baz</bar>
|
|
|
|
\\ <qux>baz</qux>
|
|
|
|
\\ </foo>
|
|
|
|
\\</Example>
|
|
|
|
;
|
|
|
|
const Example = struct {
|
|
|
|
foo: struct {
|
|
|
|
bar: []const u8,
|
|
|
|
qux: []const u8,
|
|
|
|
},
|
|
|
|
};
|
2023-08-05 20:29:23 +00:00
|
|
|
const parsed_data = try parse(Example, data, .{ .allocator = allocator, .match_predicate_ptr = fuzzyEqual });
|
2022-02-11 21:36:38 +00:00
|
|
|
defer parsed_data.deinit();
|
|
|
|
try testing.expectEqualStrings("baz", parsed_data.parsed_value.foo.bar);
|
|
|
|
try testing.expectEqualStrings("baz", parsed_data.parsed_value.foo.qux);
|
|
|
|
}
|
2022-02-11 17:22:32 +00:00
|
|
|
|
|
|
|
const service_metadata: struct {
|
|
|
|
version: []const u8 = "2016-11-15",
|
|
|
|
sdk_id: []const u8 = "EC2",
|
|
|
|
arn_namespace: []const u8 = "ec2",
|
|
|
|
endpoint_prefix: []const u8 = "ec2",
|
|
|
|
sigv4_name: []const u8 = "ec2",
|
|
|
|
name: []const u8 = "AmazonEC2",
|
|
|
|
} = .{};
|
|
|
|
|
|
|
|
const describe_regions: struct {
|
|
|
|
action_name: []const u8 = "DescribeRegions",
|
|
|
|
Request: type = struct {
|
|
|
|
// filters: ?[]Filter = null,
|
|
|
|
region_names: ?[][]const u8 = null,
|
|
|
|
dry_run: ?bool = null,
|
|
|
|
all_regions: ?bool = null,
|
|
|
|
|
|
|
|
pub fn fieldNameFor(_: @This(), comptime field_name: []const u8) []const u8 {
|
|
|
|
const mappings = .{
|
|
|
|
.filters = "Filter",
|
|
|
|
.region_names = "RegionName",
|
|
|
|
.dry_run = "dryRun",
|
|
|
|
.all_regions = "AllRegions",
|
|
|
|
};
|
|
|
|
return @field(mappings, field_name);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn metaInfo() struct { service_metadata: @TypeOf(service_metadata), action: @TypeOf(describe_regions) } {
|
|
|
|
return .{ .service_metadata = service_metadata, .action = describe_regions };
|
|
|
|
}
|
|
|
|
},
|
|
|
|
Response: type = struct {
|
|
|
|
regions: ?[]struct {
|
2022-02-11 21:36:38 +00:00
|
|
|
// Having two of these causes the zig compiler bug
|
|
|
|
// Only one of them works fine. This leads me to believe that
|
|
|
|
// it has something to do with the inline for
|
2022-02-11 17:22:32 +00:00
|
|
|
endpoint: ?[]const u8 = null,
|
|
|
|
region_name: ?[]const u8 = null,
|
|
|
|
|
|
|
|
pub fn fieldNameFor(_: @This(), comptime field_name: []const u8) []const u8 {
|
|
|
|
const mappings = .{
|
|
|
|
.endpoint = "regionEndpoint",
|
|
|
|
.region_name = "regionName",
|
|
|
|
.opt_in_status = "optInStatus",
|
|
|
|
};
|
|
|
|
return @field(mappings, field_name);
|
|
|
|
}
|
|
|
|
} = null,
|
|
|
|
|
|
|
|
pub fn fieldNameFor(_: @This(), comptime field_name: []const u8) []const u8 {
|
|
|
|
const mappings = .{
|
|
|
|
.regions = "regionInfo",
|
|
|
|
};
|
|
|
|
return @field(mappings, field_name);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
} = .{};
|
|
|
|
|
2022-02-11 21:36:38 +00:00
|
|
|
test "can parse something serious" {
|
|
|
|
// std.testing.log_level = .debug;
|
|
|
|
log.debug("", .{});
|
|
|
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
const data =
|
|
|
|
\\<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
\\<DescribeRegionsResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
|
|
|
|
\\ <requestId>8d6bfc99-978b-4146-ba23-2e5fe5b65406</requestId>
|
|
|
|
\\ <regionInfo>
|
|
|
|
\\ <item>
|
|
|
|
\\ <regionName>eu-north-1</regionName>
|
|
|
|
\\ <regionEndpoint>ec2.eu-north-1.amazonaws.com</regionEndpoint>
|
|
|
|
\\ </item>
|
|
|
|
\\ <item>
|
|
|
|
\\ <regionName>ap-south-1</regionName>
|
|
|
|
\\ <regionEndpoint>ec2.ap-south-1.amazonaws.com</regionEndpoint>
|
|
|
|
\\ </item>
|
|
|
|
\\ </regionInfo>
|
|
|
|
\\</DescribeRegionsResponse>
|
|
|
|
;
|
|
|
|
// const ServerResponse = struct { DescribeRegionsResponse: describe_regions.Response, };
|
2024-03-01 04:41:03 +00:00
|
|
|
const parsed_data = try parse(describe_regions.Response, data, .{ .allocator = allocator, .elementToParse = findResult });
|
2022-02-11 21:36:38 +00:00
|
|
|
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.?);
|
|
|
|
}
|
2024-03-01 04:41:03 +00:00
|
|
|
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 =
|
|
|
|
\\<GetAccessKeyInfoResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
|
|
|
|
\\ <GetAccessKeyInfoResult>
|
|
|
|
\\ <Account>123456789012</Account>
|
|
|
|
\\ </GetAccessKeyInfoResult>
|
|
|
|
\\ <ResponseMetadata>
|
|
|
|
\\ <RequestId>ec85bf29-1ef0-459a-930e-6446dd14a286</RequestId>
|
|
|
|
\\ </ResponseMetadata>
|
|
|
|
\\</GetAccessKeyInfoResponse>
|
|
|
|
;
|
|
|
|
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.?);
|
|
|
|
}
|
2022-05-28 22:49:17 +00:00
|
|
|
|
|
|
|
test "compiler assertion failure 2" {
|
|
|
|
// std.testing.log_level = .debug;
|
|
|
|
// log.debug("", .{});
|
|
|
|
// Actually, we only care here that the code compiles
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
const Response: type = struct {
|
|
|
|
key_group_list: ?struct {
|
|
|
|
quantity: i64, // Making this optional will make the code compile
|
|
|
|
items: ?[]struct {
|
|
|
|
key_group: []const u8,
|
|
|
|
} = null,
|
|
|
|
pub fn fieldNameFor(_: @This(), comptime field_name: []const u8) []const u8 {
|
|
|
|
const mappings = .{
|
|
|
|
.quantity = "Quantity",
|
|
|
|
.items = "Items",
|
|
|
|
};
|
|
|
|
return @field(mappings, field_name);
|
|
|
|
}
|
|
|
|
} = null,
|
|
|
|
|
|
|
|
pub fn fieldNameFor(_: @This(), comptime field_name: []const u8) []const u8 {
|
|
|
|
const mappings = .{
|
|
|
|
.key_group_list = "KeyGroupList",
|
|
|
|
};
|
|
|
|
return @field(mappings, field_name);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
const data =
|
|
|
|
\\<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
\\<AnythingAtAll xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
|
|
|
|
\\ <KeyGroupList>
|
|
|
|
\\ <Quantity>42</Quantity>
|
|
|
|
\\ </KeyGroupList>
|
|
|
|
\\</AnythingAtAll>
|
|
|
|
;
|
|
|
|
const parsed_data = try parse(Response, data, .{ .allocator = allocator });
|
|
|
|
defer parsed_data.deinit();
|
|
|
|
try testing.expect(parsed_data.parsed_value.key_group_list.?.quantity == 42);
|
|
|
|
}
|