Last active
May 4, 2020 17:27
-
-
Save DutchGhost/1c570533a017c22f517c2fc03b849e42 to your computer and use it in GitHub Desktop.
parseth
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const std = @import("std"); | |
const testing = std.testing; | |
const meta = std.meta; | |
pub const Type = union(enum) { | |
Character, | |
Number, | |
String, | |
}; | |
const Any = struct {}; | |
const Unknown = struct {}; | |
fn starts_with(comptime T: type, slice: []const T, pattern: []const T) bool { | |
if (slice.len < pattern.len) return false; | |
return std.mem.eql(T, slice[0..pattern.len], pattern); | |
} | |
const ParseError = error{Invalid}; | |
pub fn parsing(comptime T: type, comptime Description: var) type { | |
return struct { | |
fn parseCharacter(comptime fieldName: []const u8, result: *T, s: []const u8) ParseError!usize { | |
comptime var is_skip = starts_with(u8, fieldName, "skip"); | |
const filter = @field(Description, fieldName).filter; | |
const matches = switch (@TypeOf(filter)) { | |
Any, @TypeOf(null) => true, | |
comptime_int => filter == s[0], | |
fn (*const u8) bool => filter(&s[0]), | |
else => @compileError("Type `" ++ @typeName(filter) ++ "` is not handled."), | |
}; | |
if (matches) { | |
if (!is_skip) { | |
@field(result, fieldName) = s[0]; | |
} | |
return 1; | |
} else { | |
return ParseError.Invalid; | |
} | |
} | |
fn parseNumber(comptime fieldName: []const u8, result: *T, s: []const u8) ParseError!usize { | |
comptime var is_skip = starts_with(u8, fieldName, "skip"); | |
const digits = @field(Description, fieldName).digits; | |
const radix = @field(Description, fieldName).radix; | |
const numberOfDigits = switch (@TypeOf(digits)) { | |
comptime_int => digits, | |
Unknown, @TypeOf(null) => brk: { | |
var index: usize = 0; | |
for (s) |c, idx| { | |
const value = switch (c) { | |
'0'...'9' => c - '0', | |
'A'...'Z' => c - 'A' + 10, | |
'a'...'z' => c - 'a' + 10, | |
else => break, | |
}; | |
if (value >= radix) break; | |
index = idx; | |
} | |
break :brk index + 1; | |
}, | |
else => unreachable, | |
}; | |
if (numberOfDigits < 1) return ParseError.Invalid; | |
const numType = @TypeOf(@field(result, fieldName)); | |
const num = std.fmt.parseInt(numType, s[0..numberOfDigits], radix) catch return ParseError.Invalid; | |
if (!is_skip) { | |
@field(result, fieldName) = num; | |
} | |
return numberOfDigits; | |
} | |
fn parseString(comptime fieldName: []const u8, result: *T, s: []const u8) ParseError!usize { | |
comptime var is_skip = starts_with(u8, fieldName, "skip"); | |
const filter = @field(Description, fieldName).filter; | |
var index = @as(usize, 0); | |
const matches = switch (@TypeOf(filter)) { | |
fn (*const u8) bool => brk: { | |
for (s) |c| { | |
if (!filter(&c)) break; | |
index += 1; | |
} | |
break :brk true; | |
}, | |
else => brk: { | |
comptime var tinfo = @typeInfo(@TypeOf(filter)); | |
if (tinfo == .Array or tinfo == .Pointer) { | |
index = filter.len; | |
break :brk std.mem.eql(u8, filter, s[0..index]); | |
} else { | |
unreachable; | |
} | |
}, | |
}; | |
if (matches) { | |
if (!is_skip) { | |
@field(result, fieldName) = s[0..index]; | |
} | |
return index; | |
} else { | |
return ParseError.Invalid; | |
} | |
} | |
pub fn parse(s: []const u8) ParseError!T { | |
var slice = s; | |
var r: T = undefined; | |
comptime var parsedFields: usize = 0; | |
inline for (meta.fields(@TypeOf(Description))) |field| { | |
if (@hasField(T, field.name)) { | |
parsedFields += 1; | |
} | |
switch (@field(Description, field.name).kind) { | |
.Character => { | |
var ammount = try parseCharacter(field.name, &r, slice); | |
slice = slice[ammount..]; | |
}, | |
.Number => { | |
var ammount = try parseNumber(field.name, &r, slice); | |
slice = slice[ammount..]; | |
}, | |
.String => { | |
var ammount = try parseString(field.name, &r, slice); | |
slice = slice[ammount..]; | |
}, | |
else => unreachable, | |
} | |
} | |
// If not all fields are handled, we just error out :) | |
if (comptime meta.fields(T).len != parsedFields) @compileError("Must handle all fields"); | |
return r; | |
} | |
}; | |
} | |
fn isWhitespace(c: *const u8) bool { | |
return c.* == ' '; | |
} | |
test "basic parse" { | |
const x = struct { | |
c: u8, | |
n: u8, | |
pub usingnamespace parsing(@This(), .{ | |
.skip = .{ | |
.kind = .Character, | |
.filter = isWhitespace, | |
}, | |
.skip1 = .{ | |
.kind = .Character, | |
.filter = Any{}, | |
}, | |
.c = .{ | |
.kind = .Character, | |
.filter = null, | |
}, | |
.skip2 = .{ | |
.kind = .Character, | |
.filter = isWhitespace, | |
}, | |
.n = .{ | |
.kind = .Number, | |
.radix = 16, | |
.digits = null, | |
}, | |
}); | |
}; | |
var parsed = try x.parse(" a2 FF"); | |
std.debug.assert(parsed.c == '2'); | |
std.debug.assert(parsed.n == 255); | |
} | |
test "parse rgb" { | |
const RGB = struct { | |
R: usize, | |
G: usize, | |
B: usize, | |
pub usingnamespace parsing(@This(), .{ | |
.skip = .{ | |
.kind = .String, | |
.filter = "rgb(", | |
}, | |
.R = .{ | |
.kind = .Number, | |
.radix = 16, | |
.digits = 6, | |
}, | |
.skip1 = .{ | |
.kind = .String, | |
.filter = ", ", | |
}, | |
.G = .{ | |
.kind = .Number, | |
.radix = 16, | |
.digits = 6, | |
}, | |
.skip2 = .{ | |
.kind = .String, | |
.filter = ", ", | |
}, | |
.B = .{ | |
.kind = .Number, | |
.radix = 16, | |
.digits = 6, | |
}, | |
.skip3 = .{ | |
.kind = .Character, | |
.filter = ')', | |
}, | |
}); | |
}; | |
const rgb = comptime try RGB.parse("rgb(0000FF, 000000, 00000F)"); | |
std.debug.assert(rgb.R == 255); | |
std.debug.assert(rgb.G == 0); | |
std.debug.assert(rgb.B == 15); | |
} | |
fn tillDash(c: *const u8) bool { | |
return c.* != '-'; | |
} | |
test "parse random" { | |
const room = struct { | |
e1: []const u8, | |
e2: []const u8, | |
e3: []const u8, | |
e4: []const u8, | |
id: usize, | |
checksum: []const u8, | |
pub usingnamespace parsing(@This(), .{ | |
.e1 = .{ | |
.kind = .String, | |
.filter = tillDash, | |
}, | |
.skip = .{ | |
.kind = .Character, | |
.filter = '-', | |
}, | |
.e2 = .{ | |
.kind = .String, | |
.filter = tillDash, | |
}, | |
.skip2 = .{ | |
.kind = .Character, | |
.filter = '-', | |
}, | |
.e3 = .{ | |
.kind = .String, | |
.filter = tillDash, | |
}, | |
.skip3 = .{ | |
.kind = .Character, | |
.filter = '-', | |
}, | |
.e4 = .{ | |
.kind = .String, | |
.filter = tillDash, | |
}, | |
.skip4 = .{ | |
.kind = .Character, | |
.filter = '-', | |
}, | |
.id = .{ | |
.kind = .Number, | |
.radix = 10, | |
.digits = null, | |
}, | |
.checksum = .{ | |
.kind = .String, | |
.filter = tillDash, | |
}, | |
}); | |
}; | |
const r = comptime try room.parse("aaaa-bbb-z-y-123[abxyz]"); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment