2021-06-14 23:12:55 +00:00
|
|
|
const std = @import("std");
|
|
|
|
|
2023-08-05 19:41:04 +00:00
|
|
|
fn defaultTransformer(allocator: std.mem.Allocator, field_name: []const u8, options: EncodingOptions) anyerror![]const u8 {
|
|
|
|
_ = options;
|
|
|
|
_ = allocator;
|
2021-06-24 01:14:59 +00:00
|
|
|
return field_name;
|
2021-06-14 23:12:55 +00:00
|
|
|
}
|
|
|
|
|
2023-08-05 19:41:04 +00:00
|
|
|
pub const fieldNameTransformerFn = *const fn (std.mem.Allocator, []const u8, EncodingOptions) anyerror![]const u8;
|
2021-06-24 01:14:59 +00:00
|
|
|
|
|
|
|
pub const EncodingOptions = struct {
|
2023-08-05 19:41:04 +00:00
|
|
|
field_name_transformer: fieldNameTransformerFn = &defaultTransformer,
|
2021-06-24 01:14:59 +00:00
|
|
|
};
|
|
|
|
|
2023-08-05 19:41:04 +00:00
|
|
|
pub fn encode(allocator: std.mem.Allocator, obj: anytype, writer: anytype, comptime options: EncodingOptions) !void {
|
|
|
|
_ = try encodeInternal(allocator, "", "", true, obj, writer, options);
|
2021-06-24 01:14:59 +00:00
|
|
|
}
|
|
|
|
|
2023-08-05 19:41:04 +00:00
|
|
|
fn encodeStruct(
|
|
|
|
allocator: std.mem.Allocator,
|
|
|
|
parent: []const u8,
|
|
|
|
first: bool,
|
|
|
|
obj: anytype,
|
|
|
|
writer: anytype,
|
|
|
|
comptime options: EncodingOptions,
|
|
|
|
) !bool {
|
2021-06-24 01:14:59 +00:00
|
|
|
var rc = first;
|
2021-06-14 23:12:55 +00:00
|
|
|
inline for (@typeInfo(@TypeOf(obj)).Struct.fields) |field| {
|
2023-08-05 19:41:04 +00:00
|
|
|
const field_name = try options.field_name_transformer(allocator, field.name, options);
|
2021-06-24 01:14:59 +00:00
|
|
|
defer if (options.field_name_transformer.* != defaultTransformer)
|
2023-08-05 19:41:04 +00:00
|
|
|
allocator.free(field_name);
|
2021-06-24 01:14:59 +00:00
|
|
|
// @compileLog(@typeInfo(field.field_type).Pointer);
|
2023-08-05 19:41:04 +00:00
|
|
|
rc = try encodeInternal(allocator, parent, field_name, rc, @field(obj, field.name), writer, options);
|
2021-06-24 01:14:59 +00:00
|
|
|
}
|
|
|
|
return rc;
|
|
|
|
}
|
|
|
|
|
2023-08-05 19:41:04 +00:00
|
|
|
pub fn encodeInternal(
|
|
|
|
allocator: std.mem.Allocator,
|
|
|
|
parent: []const u8,
|
|
|
|
field_name: []const u8,
|
|
|
|
first: bool,
|
|
|
|
obj: anytype,
|
|
|
|
writer: anytype,
|
|
|
|
comptime options: EncodingOptions,
|
|
|
|
) !bool {
|
2021-06-24 01:14:59 +00:00
|
|
|
// @compileLog(@typeInfo(@TypeOf(obj)));
|
|
|
|
var rc = first;
|
|
|
|
switch (@typeInfo(@TypeOf(obj))) {
|
|
|
|
.Optional => if (obj) |o| {
|
2023-08-05 19:41:04 +00:00
|
|
|
rc = try encodeInternal(allocator, parent, field_name, first, o, writer, options);
|
2021-06-24 01:14:59 +00:00
|
|
|
},
|
|
|
|
.Pointer => |ti| if (ti.size == .One) {
|
2023-08-05 19:41:04 +00:00
|
|
|
rc = try encodeInternal(allocator, parent, field_name, first, obj.*, writer, options);
|
2021-06-24 01:14:59 +00:00
|
|
|
} else {
|
|
|
|
if (!first) _ = try writer.write("&");
|
2023-08-05 23:26:09 +00:00
|
|
|
try writer.print("{s}{s}={any}", .{ parent, field_name, obj });
|
2021-06-24 01:14:59 +00:00
|
|
|
rc = false;
|
|
|
|
},
|
|
|
|
.Struct => if (std.mem.eql(u8, "", field_name)) {
|
2023-08-05 19:41:04 +00:00
|
|
|
rc = try encodeStruct(allocator, parent, first, obj, writer, options);
|
2021-06-24 01:14:59 +00:00
|
|
|
} else {
|
|
|
|
// TODO: It would be lovely if we could concat at compile time or allocPrint at runtime
|
|
|
|
// XOR have compile time allocator support. Alas, neither are possible:
|
|
|
|
// https://github.com/ziglang/zig/issues/868: Comptime detection (feels like foot gun)
|
|
|
|
// https://github.com/ziglang/zig/issues/1291: Comptime allocator
|
|
|
|
const new_parent = try std.fmt.allocPrint(allocator, "{s}{s}.", .{ parent, field_name });
|
|
|
|
defer allocator.free(new_parent);
|
2023-08-05 19:41:04 +00:00
|
|
|
rc = try encodeStruct(allocator, new_parent, first, obj, writer, options);
|
2021-06-24 01:14:59 +00:00
|
|
|
// try encodeStruct(parent ++ field_name ++ ".", first, obj, writer, options);
|
|
|
|
},
|
|
|
|
.Array => {
|
|
|
|
if (!first) _ = try writer.write("&");
|
|
|
|
try writer.print("{s}{s}={s}", .{ parent, field_name, obj });
|
|
|
|
rc = false;
|
|
|
|
},
|
|
|
|
.Int, .ComptimeInt, .Float, .ComptimeFloat => {
|
|
|
|
if (!first) _ = try writer.write("&");
|
|
|
|
try writer.print("{s}{s}={d}", .{ parent, field_name, obj });
|
|
|
|
rc = false;
|
|
|
|
},
|
|
|
|
// BUGS! any doesn't work - a lot. Check this out:
|
|
|
|
// https://github.com/ziglang/zig/blob/master/lib/std/fmt.zig#L424
|
|
|
|
else => {
|
|
|
|
if (!first) _ = try writer.write("&");
|
|
|
|
try writer.print("{s}{s}={any}", .{ parent, field_name, obj });
|
|
|
|
rc = false;
|
|
|
|
},
|
2021-06-14 23:12:55 +00:00
|
|
|
}
|
2021-06-24 01:14:59 +00:00
|
|
|
return rc;
|
2021-06-14 23:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-06-24 01:14:59 +00:00
|
|
|
fn testencode(expected: []const u8, value: anytype, options: EncodingOptions) !void {
|
2021-06-14 23:12:55 +00:00
|
|
|
const ValidationWriter = struct {
|
|
|
|
const Self = @This();
|
|
|
|
pub const Writer = std.io.Writer(*Self, Error, write);
|
|
|
|
pub const Error = error{
|
|
|
|
TooMuchData,
|
|
|
|
DifferentData,
|
|
|
|
};
|
|
|
|
|
|
|
|
expected_remaining: []const u8,
|
|
|
|
|
|
|
|
fn init(exp: []const u8) Self {
|
|
|
|
return .{ .expected_remaining = exp };
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn writer(self: *Self) Writer {
|
|
|
|
return .{ .context = self };
|
|
|
|
}
|
|
|
|
|
|
|
|
fn write(self: *Self, bytes: []const u8) Error!usize {
|
2021-06-24 01:14:59 +00:00
|
|
|
// std.debug.print("{s}\n", .{bytes});
|
2021-06-14 23:12:55 +00:00
|
|
|
if (self.expected_remaining.len < bytes.len) {
|
2021-12-23 16:51:48 +00:00
|
|
|
std.log.warn(
|
2021-06-14 23:12:55 +00:00
|
|
|
\\====== expected this output: =========
|
|
|
|
\\{s}
|
|
|
|
\\======== instead found this: =========
|
|
|
|
\\{s}
|
|
|
|
\\======================================
|
|
|
|
, .{
|
|
|
|
self.expected_remaining,
|
|
|
|
bytes,
|
|
|
|
});
|
|
|
|
return error.TooMuchData;
|
|
|
|
}
|
|
|
|
if (!std.mem.eql(u8, self.expected_remaining[0..bytes.len], bytes)) {
|
2021-12-23 16:51:48 +00:00
|
|
|
std.log.warn(
|
2021-06-14 23:12:55 +00:00
|
|
|
\\====== expected this output: =========
|
|
|
|
\\{s}
|
|
|
|
\\======== instead found this: =========
|
|
|
|
\\{s}
|
|
|
|
\\======================================
|
|
|
|
, .{
|
|
|
|
self.expected_remaining[0..bytes.len],
|
|
|
|
bytes,
|
|
|
|
});
|
|
|
|
return error.DifferentData;
|
|
|
|
}
|
|
|
|
self.expected_remaining = self.expected_remaining[bytes.len..];
|
|
|
|
return bytes.len;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
var vos = ValidationWriter.init(expected);
|
|
|
|
try encode(value, vos.writer(), options);
|
|
|
|
if (vos.expected_remaining.len > 0) return error.NotEnoughData;
|
|
|
|
}
|
|
|
|
|
2021-06-24 01:14:59 +00:00
|
|
|
test "can urlencode an object" {
|
2021-06-14 23:12:55 +00:00
|
|
|
try testencode(
|
|
|
|
"Action=GetCallerIdentity&Version=2021-01-01",
|
|
|
|
.{ .Action = "GetCallerIdentity", .Version = "2021-01-01" },
|
|
|
|
.{},
|
|
|
|
);
|
|
|
|
}
|
2021-06-24 01:14:59 +00:00
|
|
|
test "can urlencode an object with integer" {
|
|
|
|
try testencode(
|
|
|
|
"Action=GetCallerIdentity&Duration=32",
|
|
|
|
.{ .Action = "GetCallerIdentity", .Duration = 32 },
|
|
|
|
.{},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
const UnsetValues = struct {
|
|
|
|
action: ?[]const u8 = null,
|
|
|
|
duration: ?i64 = null,
|
|
|
|
val1: ?i64 = null,
|
|
|
|
val2: ?[]const u8 = null,
|
|
|
|
};
|
|
|
|
test "can urlencode an object with unset values" {
|
|
|
|
// var buffer = std.ArrayList(u8).init(std.testing.allocator);
|
|
|
|
// defer buffer.deinit();
|
|
|
|
// const writer = buffer.writer();
|
|
|
|
// try encode(
|
|
|
|
// UnsetValues{ .action = "GetCallerIdentity", .duration = 32 },
|
|
|
|
// writer,
|
|
|
|
// .{ .allocator = std.testing.allocator },
|
|
|
|
// );
|
|
|
|
// std.debug.print("{s}", .{buffer.items});
|
|
|
|
try testencode(
|
|
|
|
"action=GetCallerIdentity&duration=32",
|
|
|
|
UnsetValues{ .action = "GetCallerIdentity", .duration = 32 },
|
|
|
|
.{},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
test "can urlencode a complex object" {
|
2021-06-14 23:12:55 +00:00
|
|
|
try testencode(
|
|
|
|
"Action=GetCallerIdentity&Version=2021-01-01&complex.innermember=foo",
|
|
|
|
.{ .Action = "GetCallerIdentity", .Version = "2021-01-01", .complex = .{ .innermember = "foo" } },
|
2021-06-24 01:14:59 +00:00
|
|
|
.{ .allocator = std.testing.allocator },
|
2021-06-14 23:12:55 +00:00
|
|
|
);
|
|
|
|
}
|