2022-12-01 03:21:55 +00:00
|
|
|
const std = @import("std");
|
|
|
|
const util = @import("./lib.zig");
|
|
|
|
|
|
|
|
const FieldRef = []const []const u8;
|
|
|
|
|
|
|
|
const QueryStringOptions = struct {
|
|
|
|
fn isScalar(comptime T: type) bool {
|
|
|
|
if (comptime std.meta.trait.is(.Optional)(T) and isScalar(std.meta.Child(T))) return true;
|
|
|
|
if (comptime std.meta.trait.isZigString(T)) return true;
|
|
|
|
if (comptime std.meta.trait.isIntegral(T)) return true;
|
|
|
|
if (comptime std.meta.trait.isFloat(T)) return true;
|
|
|
|
if (comptime std.meta.trait.is(.Enum)(T)) return true;
|
|
|
|
if (comptime std.meta.trait.is(.EnumLiteral)(T)) return true;
|
|
|
|
if (comptime std.meta.trait.hasFn("parse")(T)) return true;
|
|
|
|
if (T == bool) return true;
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const embed_unions = true;
|
|
|
|
const ParsedField = ?[]const u8;
|
|
|
|
|
|
|
|
fn deserializeScalar(comptime T: type, maybe_value: ParsedField) !T {
|
|
|
|
const is_optional = comptime std.meta.trait.is(.Optional)(T);
|
|
|
|
if (maybe_value) |value| {
|
|
|
|
const Eff = if (is_optional) std.meta.Child(T) else T;
|
|
|
|
|
|
|
|
// Treat all empty values as nulls if possible
|
|
|
|
if (value.len == 0 and is_optional) return null;
|
|
|
|
|
|
|
|
// TODO:
|
|
|
|
//const decoded = try decodeString(alloc, value);
|
|
|
|
const decoded = value;
|
|
|
|
|
|
|
|
if (comptime std.meta.trait.isZigString(Eff)) return decoded;
|
|
|
|
|
|
|
|
// TOOD:
|
|
|
|
//defer alloc.free(decoded);
|
|
|
|
|
|
|
|
if (comptime std.meta.trait.isIntegral(Eff)) return try std.fmt.parseInt(Eff, decoded, 0);
|
|
|
|
if (comptime std.meta.trait.isFloat(Eff)) return try std.fmt.parseFloat(Eff, decoded);
|
|
|
|
if (comptime std.meta.trait.is(.Enum)(Eff)) {
|
|
|
|
//_ = std.ascii.lowerString(decoded, decoded);
|
|
|
|
return std.meta.stringToEnum(Eff, decoded) orelse return error.InvalidEnumValue;
|
|
|
|
}
|
|
|
|
if (Eff == bool) {
|
|
|
|
//_ = std.ascii.lowerString(decoded, decoded);
|
|
|
|
return bool_map.get(decoded) orelse return error.InvalidBool;
|
|
|
|
}
|
|
|
|
if (comptime std.meta.trait.hasFn("parse")(Eff)) return try Eff.parse(decoded);
|
|
|
|
|
|
|
|
@compileError("Invalid type " ++ @typeName(T));
|
|
|
|
} else {
|
|
|
|
// Parameter is present, but no associated value
|
|
|
|
return if (is_optional)
|
|
|
|
null
|
|
|
|
else if (T == bool)
|
|
|
|
true
|
|
|
|
else
|
|
|
|
error.MissingValue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const FormDataField = struct {
|
|
|
|
filename: ?[]const u8 = null,
|
|
|
|
charset: ?[]const u8 = null,
|
|
|
|
value: []const u8,
|
|
|
|
};
|
|
|
|
|
|
|
|
const FormFile = struct {
|
|
|
|
filename: []const u8,
|
|
|
|
value: []const u8,
|
|
|
|
};
|
|
|
|
|
|
|
|
const FormDataOptions = struct {
|
|
|
|
const embed_unions = true;
|
|
|
|
|
|
|
|
fn isScalar(comptime T: type) bool {
|
|
|
|
if (comptime std.meta.trait.is(.Optional)(T) and isScalar(std.meta.Child(T))) return true;
|
|
|
|
if (comptime std.meta.trait.isZigString(T)) return true;
|
|
|
|
if (comptime std.meta.trait.isIntegral(T)) return true;
|
|
|
|
if (comptime std.meta.trait.isFloat(T)) return true;
|
|
|
|
if (comptime std.meta.trait.is(.Enum)(T)) return true;
|
|
|
|
if (comptime std.meta.trait.is(.EnumLiteral)(T)) return true;
|
|
|
|
if (comptime std.meta.trait.hasFn("parse")(T)) return true;
|
|
|
|
if (T == bool) return true;
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const ParsedField = FormDataField;
|
|
|
|
|
|
|
|
fn deserializeScalar(comptime T: type, field: FormDataField) !T {
|
|
|
|
// TODO: allocation??
|
|
|
|
|
|
|
|
if (T == FormFile) {
|
|
|
|
return FormFile{
|
|
|
|
.filename = field.filename orelse "untitled",
|
|
|
|
.data = field.value,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const decoded = field.value;
|
|
|
|
|
|
|
|
if (comptime std.meta.trait.isZigString(T)) return decoded;
|
|
|
|
|
|
|
|
// TOOD:
|
|
|
|
//defer alloc.free(decoded);
|
|
|
|
|
|
|
|
if (comptime std.meta.trait.isIntegral(T)) return try std.fmt.parseInt(T, decoded, 0);
|
|
|
|
if (comptime std.meta.trait.isFloat(T)) return try std.fmt.parseFloat(T, decoded);
|
|
|
|
if (comptime std.meta.trait.is(.Enum)(T)) {
|
|
|
|
//_ = std.ascii.lowerString(decoded, decoded);
|
|
|
|
return std.meta.stringToEnum(T, decoded) orelse return error.InvalidEnumValue;
|
|
|
|
}
|
|
|
|
if (T == bool) {
|
|
|
|
//_ = std.ascii.lowerString(decoded, decoded);
|
|
|
|
return bool_map.get(decoded) orelse return error.InvalidBool;
|
|
|
|
}
|
|
|
|
if (comptime std.meta.trait.hasFn("parse")(T)) return try T.parse(decoded);
|
|
|
|
|
|
|
|
@compileError("Invalid type " ++ @typeName(T));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
fn getRecursiveFieldList(comptime T: type, comptime prefix: FieldRef, comptime options: SerializationOptions) []const FieldRef {
|
|
|
|
comptime {
|
|
|
|
if (std.meta.trait.is(.Union)(T) and prefix.len == 0 and options.embed_unions) {
|
|
|
|
@compileError("Cannot embed a union into nothing");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (options.isScalar(T)) return &.{prefix};
|
|
|
|
if (std.meta.trait.is(.Optional)(T)) return getRecursiveFieldList(std.meta.Child(T), prefix, options);
|
|
|
|
|
|
|
|
const eff_prefix: FieldRef = if (std.meta.trait.is(.Union)(T) and options.embed_unions)
|
|
|
|
prefix[0 .. prefix.len - 1]
|
|
|
|
else
|
|
|
|
prefix;
|
|
|
|
|
|
|
|
var fields: []const FieldRef = &.{};
|
|
|
|
|
|
|
|
for (std.meta.fields(T)) |f| {
|
|
|
|
const new_prefix = eff_prefix ++ &[_][]const u8{f.name};
|
|
|
|
const F = f.field_type;
|
|
|
|
fields = fields ++ getRecursiveFieldList(F, new_prefix, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
return fields;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const SerializationOptions = struct {
|
|
|
|
embed_unions: bool = true,
|
|
|
|
isScalar: fn (type) bool = QueryStringOptions.isScalar,
|
|
|
|
};
|
|
|
|
|
|
|
|
fn Intermediary(comptime Result: type, comptime From: type, comptime options: SerializationOptions) type {
|
|
|
|
const field_refs = getRecursiveFieldList(Result, &.{}, options);
|
|
|
|
|
|
|
|
var fields: [field_refs.len]std.builtin.Type.StructField = undefined;
|
|
|
|
for (field_refs) |ref, i| {
|
|
|
|
//@compileLog(i, ref, util.comptimeJoin(".", ref));
|
|
|
|
fields[i] = .{
|
|
|
|
.name = util.comptimeJoin(".", ref),
|
|
|
|
.field_type = ?From,
|
|
|
|
.default_value = &@as(?From, null),
|
|
|
|
.is_comptime = false,
|
|
|
|
.alignment = @alignOf(?From),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return @Type(.{ .Struct = .{
|
|
|
|
.layout = .Auto,
|
|
|
|
.fields = &fields,
|
|
|
|
.decls = &.{},
|
|
|
|
.is_tuple = false,
|
|
|
|
} });
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn Deserializer(comptime Result: type, comptime From: type) type {
|
|
|
|
return DeserializerContext(Result, From, struct {
|
|
|
|
const options = SerializationOptions{};
|
|
|
|
const deserializeScalar = QueryStringOptions.deserializeScalar;
|
|
|
|
const isScalar = QueryStringOptions.isScalar;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
fn DeserializerContext(comptime Result: type, comptime From: type, comptime Context: type) type {
|
|
|
|
return struct {
|
|
|
|
const Data = Intermediary(Result, From, Context.options);
|
|
|
|
|
|
|
|
data: Data = .{},
|
|
|
|
context: Context = .{},
|
|
|
|
|
|
|
|
pub fn setSerializedField(self: *@This(), key: []const u8, value: From) !void {
|
|
|
|
const field = std.meta.stringToEnum(std.meta.FieldEnum(Data), key);
|
|
|
|
inline for (comptime std.meta.fieldNames(Data)) |field_name| {
|
|
|
|
@setEvalBranchQuota(10000);
|
|
|
|
const f = comptime std.meta.stringToEnum(std.meta.FieldEnum(Data), field_name);
|
|
|
|
if (field == f) {
|
|
|
|
@field(self.data, field_name) = value;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return error.UnknownField;
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn finish(self: *@This()) !Result {
|
2022-12-01 04:01:17 +00:00
|
|
|
return (try self.deserialize(Result, &.{})) orelse error.MissingField;
|
2022-12-01 03:21:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn getSerializedField(self: *@This(), comptime field_ref: FieldRef) ?From {
|
|
|
|
//inline for (comptime std.meta.fieldNames(Data)) |f| @compileLog(f.ptr);
|
|
|
|
return @field(self.data, util.comptimeJoin(".", field_ref));
|
|
|
|
}
|
|
|
|
|
|
|
|
fn deserialize(self: *@This(), comptime T: type, comptime field_ref: FieldRef) !?T {
|
|
|
|
if (comptime Context.isScalar(T)) {
|
|
|
|
return try Context.deserializeScalar(T, self.getSerializedField(field_ref) orelse return null);
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (@typeInfo(T)) {
|
|
|
|
// At most one of any union field can be active at a time, and it is embedded
|
|
|
|
// in its parent container
|
|
|
|
.Union => |info| {
|
|
|
|
var result: ?T = null;
|
|
|
|
// TODO: errdefer cleanup
|
|
|
|
const union_ref: FieldRef = if (Context.options.embed_unions) field_ref[0 .. field_ref.len - 1] else field_ref;
|
|
|
|
inline for (info.fields) |field| {
|
|
|
|
const F = field.field_type;
|
|
|
|
const new_field_ref = union_ref ++ &[_][]const u8{field.name};
|
|
|
|
const maybe_value = try self.deserialize(F, new_field_ref);
|
|
|
|
if (maybe_value) |value| {
|
|
|
|
// TODO: errdefer cleanup
|
2022-12-01 04:01:17 +00:00
|
|
|
if (result != null) return error.DuplicateUnionMember;
|
2022-12-01 03:21:55 +00:00
|
|
|
result = @unionInit(T, field.name, value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
},
|
|
|
|
|
|
|
|
.Struct => |info| {
|
|
|
|
var result: T = undefined;
|
|
|
|
|
|
|
|
var any_explicit = false;
|
|
|
|
var any_missing = false;
|
|
|
|
inline for (info.fields) |field| {
|
|
|
|
const F = field.field_type;
|
|
|
|
const new_field_ref = field_ref ++ &[_][]const u8{field.name};
|
|
|
|
const maybe_value = try self.deserialize(F, new_field_ref);
|
|
|
|
if (maybe_value) |v| {
|
|
|
|
@field(result, field.name) = v;
|
|
|
|
any_explicit = true;
|
|
|
|
} else if (field.default_value) |ptr| {
|
|
|
|
if (@sizeOf(F) != 0) {
|
|
|
|
@field(result, field.name) = @ptrCast(*const F, @alignCast(field.alignment, ptr)).*;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
any_missing = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (any_missing) {
|
2022-12-01 04:01:17 +00:00
|
|
|
return if (any_explicit) error.MissingField else null;
|
2022-12-01 03:21:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
},
|
|
|
|
|
|
|
|
// Specifically non-scalar optionals
|
|
|
|
.Optional => |info| return try self.deserialize(info.child, field_ref),
|
|
|
|
|
|
|
|
else => @compileError("Unsupported type"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const bool_map = std.ComptimeStringMap(bool, .{
|
|
|
|
.{ "true", true },
|
|
|
|
.{ "t", true },
|
|
|
|
.{ "yes", true },
|
|
|
|
.{ "y", true },
|
|
|
|
.{ "1", true },
|
|
|
|
|
|
|
|
.{ "false", false },
|
|
|
|
.{ "f", false },
|
|
|
|
.{ "no", false },
|
|
|
|
.{ "n", false },
|
|
|
|
.{ "0", false },
|
|
|
|
});
|
|
|
|
|
2022-12-01 04:01:17 +00:00
|
|
|
test "Deserializer" {
|
2022-12-01 03:21:55 +00:00
|
|
|
|
2022-12-01 04:01:17 +00:00
|
|
|
// Happy case - simple
|
2022-12-01 03:21:55 +00:00
|
|
|
{
|
2022-12-01 04:01:17 +00:00
|
|
|
const T = struct { foo: usize, bar: bool };
|
|
|
|
|
2022-12-01 03:21:55 +00:00
|
|
|
var ds = Deserializer(T, []const u8){};
|
|
|
|
try ds.setSerializedField("foo", "123");
|
|
|
|
try ds.setSerializedField("bar", "true");
|
|
|
|
try std.testing.expectEqual(T{ .foo = 123, .bar = true }, try ds.finish());
|
|
|
|
}
|
2022-12-01 04:01:17 +00:00
|
|
|
|
|
|
|
// Returns error if nonexistent field set
|
|
|
|
{
|
|
|
|
const T = struct { foo: usize, bar: bool };
|
|
|
|
|
|
|
|
var ds = Deserializer(T, []const u8){};
|
|
|
|
try std.testing.expectError(error.UnknownField, ds.setSerializedField("baz", "123"));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Substruct dereferencing
|
|
|
|
{
|
|
|
|
const T = struct {
|
|
|
|
foo: struct { bar: bool, baz: bool },
|
|
|
|
};
|
|
|
|
|
|
|
|
var ds = Deserializer(T, []const u8){};
|
|
|
|
try ds.setSerializedField("foo.bar", "true");
|
|
|
|
try ds.setSerializedField("foo.baz", "true");
|
|
|
|
try std.testing.expectEqual(T{ .foo = .{ .bar = true, .baz = true } }, try ds.finish());
|
|
|
|
}
|
|
|
|
|
|
|
|
// Union embedding
|
|
|
|
{
|
|
|
|
const T = struct {
|
|
|
|
foo: union(enum) { bar: bool, baz: bool },
|
|
|
|
};
|
|
|
|
|
|
|
|
var ds = Deserializer(T, []const u8){};
|
|
|
|
try ds.setSerializedField("bar", "true");
|
|
|
|
try std.testing.expectEqual(T{ .foo = .{ .bar = true } }, try ds.finish());
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns error if multiple union fields specified
|
|
|
|
{
|
|
|
|
const T = struct {
|
|
|
|
foo: union(enum) { bar: bool, baz: bool },
|
|
|
|
};
|
|
|
|
|
|
|
|
var ds = Deserializer(T, []const u8){};
|
|
|
|
try ds.setSerializedField("bar", "true");
|
|
|
|
try ds.setSerializedField("baz", "true");
|
|
|
|
try std.testing.expectError(error.DuplicateUnionMember, ds.finish());
|
|
|
|
}
|
|
|
|
|
|
|
|
// Uses default values if fields aren't provided
|
|
|
|
{
|
|
|
|
const T = struct { foo: usize = 123, bar: bool = true };
|
|
|
|
|
|
|
|
var ds = Deserializer(T, []const u8){};
|
|
|
|
try std.testing.expectEqual(T{ .foo = 123, .bar = true }, try ds.finish());
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns an error if fields aren't provided and no default exists
|
2022-12-01 03:21:55 +00:00
|
|
|
{
|
2022-12-01 04:01:17 +00:00
|
|
|
const T = struct { foo: usize, bar: bool };
|
|
|
|
|
2022-12-01 03:21:55 +00:00
|
|
|
var ds = Deserializer(T, []const u8){};
|
|
|
|
try ds.setSerializedField("foo", "123");
|
2022-12-01 04:01:17 +00:00
|
|
|
try std.testing.expectError(error.MissingField, ds.finish());
|
2022-12-01 03:21:55 +00:00
|
|
|
}
|
|
|
|
|
2022-12-01 04:01:17 +00:00
|
|
|
// Handles optional containers
|
|
|
|
{
|
|
|
|
const T = struct {
|
|
|
|
foo: ?struct { bar: usize = 3, baz: usize } = null,
|
|
|
|
qux: ?union(enum) { quux: usize } = null,
|
|
|
|
};
|
|
|
|
|
|
|
|
var ds = Deserializer(T, []const u8){};
|
|
|
|
try std.testing.expectEqual(T{ .foo = null, .qux = null }, try ds.finish());
|
|
|
|
}
|
|
|
|
|
|
|
|
{
|
|
|
|
const T = struct {
|
|
|
|
foo: ?struct { bar: usize = 3, baz: usize } = null,
|
|
|
|
qux: ?union(enum) { quux: usize } = null,
|
|
|
|
};
|
|
|
|
|
|
|
|
var ds = Deserializer(T, []const u8){};
|
|
|
|
try ds.setSerializedField("foo.baz", "3");
|
|
|
|
try ds.setSerializedField("quux", "3");
|
|
|
|
try std.testing.expectEqual(T{ .foo = .{ .bar = 3, .baz = 3 }, .qux = .{ .quux = 3 } }, try ds.finish());
|
|
|
|
}
|
|
|
|
|
|
|
|
{
|
|
|
|
const T = struct {
|
|
|
|
foo: ?struct { bar: usize = 3, baz: usize } = null,
|
|
|
|
qux: ?union(enum) { quux: usize } = null,
|
|
|
|
};
|
|
|
|
|
|
|
|
var ds = Deserializer(T, []const u8){};
|
|
|
|
try ds.setSerializedField("foo.bar", "3");
|
|
|
|
try ds.setSerializedField("quux", "3");
|
|
|
|
try std.testing.expectError(error.MissingField, ds.finish());
|
|
|
|
}
|
2022-12-01 03:21:55 +00:00
|
|
|
}
|