Skip to content

Instantly share code, notes, and snippets.

@DutchGhost
Last active May 4, 2020 17:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DutchGhost/1c570533a017c22f517c2fc03b849e42 to your computer and use it in GitHub Desktop.
Save DutchGhost/1c570533a017c22f517c2fc03b849e42 to your computer and use it in GitHub Desktop.
parseth
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