wip: started a refactoring
This commit is contained in:
parent
75ccf913ac
commit
4970b1b1f6
6 changed files with 524 additions and 169 deletions
34
build.zig
34
build.zig
|
|
@ -130,14 +130,11 @@ pub fn build(b: *std.Build) void {
|
|||
// Creates an executable that will run `test` blocks from the provided module.
|
||||
// Here `mod` needs to define a target, which is why earlier we made sure to
|
||||
// set the releative field.
|
||||
const mod_tests = b.addTest(.{
|
||||
const run_mod_tests = b.addRunArtifact(b.addTest(.{
|
||||
.root_module = mod,
|
||||
});
|
||||
}));
|
||||
|
||||
// A run step that will run the test executable.
|
||||
const run_mod_tests = b.addRunArtifact(mod_tests);
|
||||
|
||||
const bot_tests = b.addTest(.{
|
||||
const run_bot_tests = b.addRunArtifact(b.addTest(.{
|
||||
.root_module = b.addModule("bot", .{
|
||||
.target = target,
|
||||
.root_source_file = b.path("src/bot.zig"),
|
||||
|
|
@ -148,19 +145,28 @@ pub fn build(b: *std.Build) void {
|
|||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
}));
|
||||
|
||||
const run_bot_tests = b.addRunArtifact(bot_tests);
|
||||
const run_parser_tests = b.addRunArtifact(b.addTest(.{
|
||||
.root_module = b.addModule("parser", .{
|
||||
.target = target,
|
||||
.root_source_file = b.path("src/parser.zig"),
|
||||
}),
|
||||
}));
|
||||
|
||||
const run_buffer_tests = b.addRunArtifact(b.addTest(.{
|
||||
.root_module = b.addModule("buffer", .{
|
||||
.target = target,
|
||||
.root_source_file = b.path("src/buffer.zig"),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Creates an executable that will run `test` blocks from the executable's
|
||||
// root module. Note that test executables only test one module at a time,
|
||||
// hence why we have to create two separate ones.
|
||||
const exe_tests = b.addTest(.{
|
||||
const run_exe_tests = b.addRunArtifact(b.addTest(.{
|
||||
.root_module = exe.root_module,
|
||||
});
|
||||
|
||||
// A run step that will run the second test executable.
|
||||
const run_exe_tests = b.addRunArtifact(exe_tests);
|
||||
}));
|
||||
|
||||
// A top level step for running all tests. dependOn can be called multiple
|
||||
// times and since the two run steps do not depend on one another, this will
|
||||
|
|
@ -168,7 +174,9 @@ pub fn build(b: *std.Build) void {
|
|||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&run_mod_tests.step);
|
||||
test_step.dependOn(&run_exe_tests.step);
|
||||
test_step.dependOn(&run_parser_tests.step);
|
||||
test_step.dependOn(&run_bot_tests.step);
|
||||
test_step.dependOn(&run_buffer_tests.step);
|
||||
|
||||
// Just like flags, top level steps are also listed in the `--help` menu.
|
||||
//
|
||||
|
|
|
|||
288
src/bot.zig
288
src/bot.zig
|
|
@ -1,54 +1,8 @@
|
|||
const std = @import("std");
|
||||
const zircon = @import("zircon");
|
||||
|
||||
pub const Parser = struct {
|
||||
original: []const u8,
|
||||
rest: []const u8,
|
||||
end_idx: usize,
|
||||
|
||||
fn seek(self: *const Parser, skip: usize) Parser {
|
||||
return .{ .original = self.original, .rest = self.rest[skip..], .end_idx = self.end_idx + skip };
|
||||
}
|
||||
|
||||
fn init(s: []const u8) Parser {
|
||||
return .{
|
||||
.original = s,
|
||||
.end_idx = 0,
|
||||
.rest = s,
|
||||
};
|
||||
}
|
||||
|
||||
fn consume_char(self: *const Parser, c: u8) ?Parser {
|
||||
if (self.rest[0] != c) {
|
||||
return null;
|
||||
}
|
||||
return self.seek(1);
|
||||
}
|
||||
|
||||
fn consume_str(self: *const Parser, s: []const u8) ?Parser {
|
||||
const len = s.len;
|
||||
if (self.rest.len < len) {
|
||||
return null;
|
||||
}
|
||||
if (!std.mem.eql(u8, self.rest[0..len], s)) {
|
||||
return null;
|
||||
}
|
||||
return self.seek(len);
|
||||
}
|
||||
|
||||
fn take_until_char(self: *const Parser, c: u8) struct { Parser, []const u8 } {
|
||||
const idx = std.mem.indexOfScalar(u8, self.rest, c) orelse unreachable;
|
||||
return .{ self.seek(idx), self.rest[0..idx] };
|
||||
}
|
||||
|
||||
fn take_char(self: *const Parser) struct { Parser, u8 } {
|
||||
return .{ self.seek(1), self.rest[0] };
|
||||
}
|
||||
|
||||
fn parsed(self: *const Parser) []const u8 {
|
||||
return self.original[0..self.end_idx];
|
||||
}
|
||||
};
|
||||
const Buffer = @import("buffer.zig").Buffer;
|
||||
const Parser = @import("parser.zig").Parser;
|
||||
|
||||
/// AdminCommand are commands useful for debugging zigeru, since they
|
||||
/// are more spammy than others they are separated and only sent to
|
||||
|
|
@ -132,56 +86,70 @@ pub const Message = struct {
|
|||
author: []const u8,
|
||||
content: []const u8,
|
||||
|
||||
pub fn init_owned(allocator: std.mem.Allocator, timestamp: i64, author: []const u8, targets: []const u8, content: []const u8) Error!Message {
|
||||
return .{
|
||||
.timestamp = timestamp,
|
||||
.targets = try allocator.dupe(u8, targets),
|
||||
.author = try allocator.dupe(u8, author),
|
||||
.content = try allocator.dupe(u8, content),
|
||||
};
|
||||
pub fn init_owned(
|
||||
allocator: std.mem.Allocator,
|
||||
timestamp: i64,
|
||||
author: []const u8,
|
||||
targets: []const u8,
|
||||
content: []const u8,
|
||||
) Error!*Message {
|
||||
const message = try allocator.create(Message);
|
||||
message.timestamp = timestamp;
|
||||
message.targets = try allocator.dupe(u8, targets);
|
||||
message.author = try allocator.dupe(u8, author);
|
||||
message.content = try allocator.dupe(u8, content);
|
||||
return message;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const Message, allocator: std.mem.Allocator) void {
|
||||
allocator.free(self.author);
|
||||
allocator.free(self.content);
|
||||
allocator.free(self.targets);
|
||||
allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Bot = struct {
|
||||
backlog: [1024]?*const Message,
|
||||
sent_messages: std.ArrayList([]u8),
|
||||
top: usize,
|
||||
bottom: usize,
|
||||
backlog: Buffer(*const Message, 1024),
|
||||
sent_messages: Buffer([]u8, 1024),
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
fn deinit_backlog_slot(allocator: std.mem.Allocator, item: *const Message) void {
|
||||
item.deinit(allocator);
|
||||
}
|
||||
|
||||
fn deinit_sent_message(allocator: std.mem.Allocator, item: []u8) void {
|
||||
allocator.free(item);
|
||||
}
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) Error!Bot {
|
||||
return Bot{
|
||||
.backlog = .{null} ** 1024,
|
||||
.sent_messages = try std.ArrayList([]u8).initCapacity(allocator, 10),
|
||||
.top = 0,
|
||||
.bottom = 0,
|
||||
var bot = Bot{
|
||||
.allocator = allocator,
|
||||
.backlog = undefined,
|
||||
.sent_messages = undefined,
|
||||
};
|
||||
bot.backlog = .init_with_closure(allocator, &Bot.deinit_backlog_slot);
|
||||
bot.sent_messages = .init_with_closure(allocator, &Bot.deinit_sent_message);
|
||||
return bot;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Bot) void {
|
||||
for (self.sent_messages.items) |item| {
|
||||
self.allocator.free(item);
|
||||
}
|
||||
self.sent_messages.deinit(self.allocator);
|
||||
|
||||
for (self.backlog) |slot| {
|
||||
if (slot) |message| {
|
||||
message.deinit(self.allocator);
|
||||
}
|
||||
}
|
||||
self.sent_messages.deinit();
|
||||
self.backlog.deinit();
|
||||
}
|
||||
|
||||
pub fn execute(self: *Bot, cmd: *const Command, prefix: ?zircon.Prefix, targets: []const u8) Error!zircon.Message {
|
||||
pub fn execute(
|
||||
self: *Bot,
|
||||
cmd: *const Command,
|
||||
prefix: ?zircon.Prefix,
|
||||
targets: []const u8,
|
||||
) Error!zircon.Message {
|
||||
switch (cmd.*) {
|
||||
.substitute => |command| {
|
||||
const prev_msg = self.previous_message_by_author(command.author, targets) orelse return Error.NoMessage;
|
||||
const prev_msg = self.previous_message_by_author(
|
||||
command.author,
|
||||
targets,
|
||||
) orelse return Error.NoMessage;
|
||||
const output = try std.mem.replaceOwned(
|
||||
u8,
|
||||
self.allocator,
|
||||
|
|
@ -190,27 +158,40 @@ pub const Bot = struct {
|
|||
command.replacement,
|
||||
);
|
||||
defer self.allocator.free(output);
|
||||
const quoted_output = try std.fmt.allocPrint(self.allocator, "{s}: \"{s}\"", .{ command.author, output });
|
||||
try self.sent_messages.append(self.allocator, quoted_output);
|
||||
return zircon.Message{
|
||||
.PRIVMSG = .{ .targets = targets, .prefix = prefix, .text = quoted_output },
|
||||
};
|
||||
const quoted_output = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"{s}: \"{s}\"",
|
||||
.{ command.author, output },
|
||||
);
|
||||
self.sent_messages.append(quoted_output);
|
||||
return zircon.Message{ .PRIVMSG = .{
|
||||
.targets = targets,
|
||||
.prefix = prefix,
|
||||
.text = quoted_output,
|
||||
} };
|
||||
},
|
||||
.help => {
|
||||
return zircon.Message{
|
||||
.PRIVMSG = .{ .targets = targets, .prefix = prefix, .text = HELP_MESSAGE },
|
||||
};
|
||||
return zircon.Message{ .PRIVMSG = .{
|
||||
.targets = targets,
|
||||
.prefix = prefix,
|
||||
.text = HELP_MESSAGE,
|
||||
} };
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute_admin(self: *Bot, cmd: *const AdminCommand, prefix: ?zircon.Prefix, targets: []const u8) Error!zircon.Message {
|
||||
pub fn execute_admin(
|
||||
self: *Bot,
|
||||
cmd: *const AdminCommand,
|
||||
prefix: ?zircon.Prefix,
|
||||
targets: []const u8,
|
||||
) Error!zircon.Message {
|
||||
switch (cmd.*) {
|
||||
.status => {
|
||||
const msg = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"heard messages: {}, sent messages: {}, top: {}, bottom: {}",
|
||||
.{ self.no_messages(), self.sent_messages.items.len, self.top, self.bottom },
|
||||
"heard messages: {}, sent messages: {}",
|
||||
.{ self.no_messages(), self.sent_messages.items.len },
|
||||
);
|
||||
return .{ .PRIVMSG = .{ .targets = targets, .prefix = prefix, .text = msg } };
|
||||
},
|
||||
|
|
@ -219,20 +200,19 @@ pub const Bot = struct {
|
|||
return .{ .JOIN = .{ .prefix = prefix, .channels = msg.channel } };
|
||||
},
|
||||
.backlog => |backlog| {
|
||||
if (self.top == self.bottom) {
|
||||
if (self.backlog.insertions() == 0) {
|
||||
return Error.NoMessage;
|
||||
}
|
||||
if (backlog.history > self.no_messages()) {
|
||||
return Error.NoMessage;
|
||||
}
|
||||
const idx = self.previous_idx(self.top - backlog.history);
|
||||
if (self.backlog[idx]) |message| {
|
||||
if (self.backlog.get_backwards(backlog.history)) |message| {
|
||||
const quoted_output = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"backlog {}: author: \"{s}\", content: \"{s}\"",
|
||||
.{ backlog.history, message.author, message.content },
|
||||
);
|
||||
try self.sent_messages.append(self.allocator, quoted_output);
|
||||
self.sent_messages.append(quoted_output);
|
||||
return .{
|
||||
.PRIVMSG = .{ .targets = targets, .prefix = prefix, .text = quoted_output },
|
||||
};
|
||||
|
|
@ -245,52 +225,82 @@ pub const Bot = struct {
|
|||
}
|
||||
|
||||
pub fn hear(self: *Bot, msg: *const Message) void {
|
||||
self.backlog[self.top] = msg;
|
||||
self.top = (self.top + 1) % self.backlog.len;
|
||||
if (self.top == self.bottom) {
|
||||
self.bottom = (self.bottom + 1) % self.backlog.len;
|
||||
// free old message
|
||||
self.allocator.free(self.backlog[self.top].?.author);
|
||||
self.allocator.free(self.backlog[self.top].?.content);
|
||||
self.backlog[self.top] = null;
|
||||
}
|
||||
self.backlog.append(msg);
|
||||
}
|
||||
|
||||
pub fn no_messages(self: *Bot) usize {
|
||||
if (self.top < self.bottom) {
|
||||
// we've bounded around, the backlog is full.
|
||||
return self.backlog.len;
|
||||
}
|
||||
return self.top - self.bottom;
|
||||
}
|
||||
|
||||
fn previous_idx(self: *Bot, idx: usize) usize {
|
||||
if (idx == 0) {
|
||||
return self.backlog.len - 1;
|
||||
}
|
||||
return (idx - 1) % self.backlog.len;
|
||||
return self.backlog.len();
|
||||
}
|
||||
|
||||
fn previous_message_by_author(self: *Bot, author: []const u8, targets: []const u8) ?*const Message {
|
||||
var idx = self.previous_idx(self.top);
|
||||
while (true) : (idx = self.previous_idx(idx)) {
|
||||
if (self.backlog[idx] == null) {
|
||||
return null;
|
||||
}
|
||||
const message = self.backlog[idx] orelse unreachable;
|
||||
if (std.mem.eql(u8, message.author, author) and std.mem.eql(u8, message.targets, targets)) {
|
||||
var iter = self.backlog.iterate_reverse();
|
||||
while (iter.prev()) |message| {
|
||||
if (std.mem.eql(u8, message.author, author) and
|
||||
std.mem.eql(u8, message.targets, targets))
|
||||
{
|
||||
return message;
|
||||
}
|
||||
if (idx == self.bottom) {
|
||||
// reached the start of the list
|
||||
break;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
test "hear_wraps" {
|
||||
test "deiniting an owned Message leaks no memory" {
|
||||
const allocator = std.testing.allocator;
|
||||
const testMessage = try Message.init_owned(
|
||||
allocator,
|
||||
12345,
|
||||
"jassob",
|
||||
"#test",
|
||||
"All your codebase are belong to us.\n",
|
||||
);
|
||||
testMessage.deinit(allocator);
|
||||
}
|
||||
|
||||
test "deiniting an inited Bot leaks no memory" {
|
||||
var bot = try Bot.init(std.testing.allocator);
|
||||
bot.deinit();
|
||||
}
|
||||
|
||||
test "hear and deinit has no leaks" {
|
||||
const allocator = std.testing.allocator;
|
||||
var bot = try Bot.init(allocator);
|
||||
defer bot.deinit();
|
||||
|
||||
const testMessage = try Message.init_owned(
|
||||
allocator,
|
||||
12345,
|
||||
"jassob",
|
||||
"#test",
|
||||
"All your codebase are belong to us.\n",
|
||||
);
|
||||
bot.hear(testMessage);
|
||||
|
||||
try std.testing.expectEqual(0, bot.backlog.top);
|
||||
try std.testing.expectEqual(1, bot.no_messages());
|
||||
}
|
||||
|
||||
test "a few hears and deinit has no leaks" {
|
||||
const allocator = std.testing.allocator;
|
||||
var bot = try Bot.init(allocator);
|
||||
defer bot.deinit();
|
||||
|
||||
for (0..2) |i| {
|
||||
const testMessage = try Message.init_owned(
|
||||
std.testing.allocator,
|
||||
@intCast(i),
|
||||
"jassob",
|
||||
"#test",
|
||||
"All your codebase are belong to us.\n",
|
||||
);
|
||||
bot.hear(testMessage);
|
||||
}
|
||||
|
||||
try std.testing.expectEqual(1, bot.backlog.top);
|
||||
try std.testing.expect(bot.no_messages() == 2);
|
||||
}
|
||||
|
||||
test "hear wraps" {
|
||||
var bot = try Bot.init(std.testing.allocator);
|
||||
defer bot.deinit();
|
||||
|
||||
|
|
@ -302,19 +312,27 @@ test "hear_wraps" {
|
|||
"#test",
|
||||
"All your codebase are belong to us.\n",
|
||||
);
|
||||
bot.hear(&testMessage);
|
||||
bot.hear(testMessage);
|
||||
}
|
||||
|
||||
try std.testing.expect(bot.top == 1);
|
||||
try std.testing.expect(bot.bottom == 2);
|
||||
try std.testing.expect(bot.no_messages() == 1024);
|
||||
try std.testing.expectEqual(0, bot.backlog.top);
|
||||
try std.testing.expectEqual(1, bot.backlog.bottom());
|
||||
try std.testing.expectEqual(1024, bot.no_messages());
|
||||
}
|
||||
|
||||
test "execute_substitution_no_previous_message" {
|
||||
var bot = try Bot.init(std.testing.allocator);
|
||||
defer bot.deinit();
|
||||
const cmd = Command{ .substitute = .{ .author = "jassob", .needle = "What", .replacement = "what" } };
|
||||
try std.testing.expectError(Error.NoMessage, bot.execute(&cmd, null, "#test"));
|
||||
const cmd = Command{ .substitute = .{
|
||||
.author = "jassob",
|
||||
.needle = "What",
|
||||
.replacement = "what",
|
||||
} };
|
||||
try std.testing.expectError(Error.NoMessage, bot.execute(
|
||||
&cmd,
|
||||
null,
|
||||
"#test",
|
||||
));
|
||||
}
|
||||
|
||||
test "execute_substitution" {
|
||||
|
|
@ -329,7 +347,7 @@ test "execute_substitution" {
|
|||
"#test",
|
||||
"What",
|
||||
);
|
||||
bot.hear(&msg);
|
||||
bot.hear(msg);
|
||||
|
||||
// execute substitution
|
||||
const cmd = Command{
|
||||
|
|
@ -345,3 +363,11 @@ test "execute_substitution" {
|
|||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
test "parse_botcommands" {
|
||||
const cmd = AdminCommand.parse("!join badchannel") orelse unreachable;
|
||||
try std.testing.expectEqual(
|
||||
AdminCommand{ .err = .{ .message = "channels must start with \"#\"" } },
|
||||
cmd,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
269
src/buffer.zig
Normal file
269
src/buffer.zig
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
const std = @import("std");
|
||||
|
||||
// Buffer represents a non-allocating, circular buffer of a fixed size.
|
||||
//
|
||||
// It is possible to store owned data in Buffer and have it
|
||||
// automatically deinited when it is overwritten by supplying a custom
|
||||
// ItemDeinitClosure, using .init_with_closure.
|
||||
pub fn Buffer(comptime T: type, comptime length: usize) type {
|
||||
// TODO(jassob): Refactor buffer to use a top and len instead of
|
||||
// top and bottom.
|
||||
//
|
||||
// Idea: bottom is always 0 if len != length and otherwise it is
|
||||
// always top + 1.
|
||||
return struct {
|
||||
items: [length]?T,
|
||||
top: usize,
|
||||
insertions: usize,
|
||||
deinit_func: ?*const fn (allocator: std.mem.Allocator, item: T) void,
|
||||
allocator: ?std.mem.Allocator,
|
||||
|
||||
pub fn init() Buffer(T, length) {
|
||||
return .{
|
||||
.items = .{null} ** length,
|
||||
.top = 0,
|
||||
.insertions = 0,
|
||||
.deinit_func = null,
|
||||
.allocator = null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn init_with_closure(
|
||||
allocator: std.mem.Allocator,
|
||||
deinit_func: *const fn (allocator: std.mem.Allocator, item: T) void,
|
||||
) Buffer(T, length) {
|
||||
var buf = Buffer(T, length).init();
|
||||
buf.allocator = allocator;
|
||||
buf.deinit_func = deinit_func;
|
||||
return buf;
|
||||
}
|
||||
|
||||
fn can_deinit(self: *const Buffer(T, length)) bool {
|
||||
return self.allocator != null and self.deinit_func != null;
|
||||
}
|
||||
|
||||
fn deinit_item(self: *Buffer(T, length), item: T) void {
|
||||
if (!self.can_deinit()) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
self.deinit_func.?(self.allocator.?, item);
|
||||
}
|
||||
|
||||
fn next(self: *const Buffer(T, length)) usize {
|
||||
return (self.top + 1) % length;
|
||||
}
|
||||
|
||||
fn prev(self: *const Buffer(T, length)) usize {
|
||||
if (self.top == 0) {
|
||||
return length - 1;
|
||||
}
|
||||
return self.top - 1;
|
||||
}
|
||||
|
||||
fn reached_end(self: *const Buffer(T, length)) bool {
|
||||
return self.insertions >= length;
|
||||
}
|
||||
|
||||
pub fn append(self: *Buffer(T, length), item: T) void {
|
||||
if (self.insertions > 0) {
|
||||
self.top = self.next();
|
||||
}
|
||||
if (self.insertions >= length) {
|
||||
// free old bottom item
|
||||
self.deinit_item(self.items[self.top].?);
|
||||
self.items[self.top] = null;
|
||||
}
|
||||
self.items[self.top] = item;
|
||||
self.insertions = self.insertions + 1;
|
||||
}
|
||||
|
||||
pub fn len(self: *const Buffer(T, length)) usize {
|
||||
return @min(self.insertions, length);
|
||||
}
|
||||
|
||||
pub fn bottom(self: *const Buffer(T, length)) usize {
|
||||
if (self.reached_end()) {
|
||||
return self.next();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
pub fn get(self: *const Buffer(T, length), index: usize) ?T {
|
||||
const idx = (self.bottom() + index) % self.items.len;
|
||||
if (index > self.insertions()) {
|
||||
return null;
|
||||
}
|
||||
return self.items[idx];
|
||||
}
|
||||
|
||||
pub fn get_backwards(self: *const Buffer(T, length), offset: usize) ?T {
|
||||
if (offset >= self.insertions()) {
|
||||
return null;
|
||||
}
|
||||
return self.items[(self.top - offset) % self.items.len];
|
||||
}
|
||||
|
||||
pub fn iterate(self: *const Buffer(T, length)) BufferIterator(T, length) {
|
||||
return .{
|
||||
.buffer = self,
|
||||
.index = self.bottom(),
|
||||
.seen_first_item = false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn iterate_reverse(self: *const Buffer(T, length)) BufferIterator(T, length) {
|
||||
return .{
|
||||
.buffer = self,
|
||||
.index = self.top,
|
||||
.seen_first_item = false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Buffer(T, length)) void {
|
||||
if (!self.can_deinit()) {
|
||||
return;
|
||||
}
|
||||
for (self.items, 0..) |item, idx| {
|
||||
if (item != null) {
|
||||
self.deinit_item(item.?);
|
||||
self.items[idx] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn BufferIterator(comptime T: type, comptime length: usize) type {
|
||||
return struct {
|
||||
buffer: *const Buffer(T, length),
|
||||
index: usize,
|
||||
seen_first_item: bool,
|
||||
|
||||
pub fn next(self: *BufferIterator(T, length)) ?T {
|
||||
if (self.seen_first_item and self.index == self.buffer.bottom()) {
|
||||
// We've reached the top, return null.
|
||||
return null;
|
||||
}
|
||||
const item = self.buffer.items[self.index];
|
||||
self.index = (self.index + 1) % length;
|
||||
if (!self.seen_first_item) {
|
||||
self.seen_first_item = true;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
pub fn prev(self: *BufferIterator(T, length)) ?T {
|
||||
if (self.seen_first_item and self.index == self.buffer.top) {
|
||||
// We've reached the top, return null.
|
||||
return null;
|
||||
}
|
||||
const item = self.buffer.items[self.index];
|
||||
if (self.index == 0) {
|
||||
self.index = length - 1;
|
||||
} else {
|
||||
self.index = self.index - 1;
|
||||
}
|
||||
if (!self.seen_first_item) {
|
||||
self.seen_first_item = true;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test "init is empty" {
|
||||
const buffer = Buffer(u8, 10).init();
|
||||
// len is 0 in initial buffer
|
||||
try std.testing.expectEqual(0, buffer.len());
|
||||
var iter = buffer.iterate();
|
||||
if (iter.next()) |_| {
|
||||
@panic("unexpected message");
|
||||
}
|
||||
}
|
||||
|
||||
test "reached_end is true after we've inserted length items" {
|
||||
var buffer = Buffer(u8, 2).init();
|
||||
buffer.append(1);
|
||||
try std.testing.expect(!buffer.reached_end());
|
||||
buffer.append(2);
|
||||
try std.testing.expectEqual(2, buffer.insertions);
|
||||
try std.testing.expect(buffer.reached_end());
|
||||
}
|
||||
|
||||
test "append adds item" {
|
||||
var buffer = Buffer(u8, 10).init();
|
||||
buffer.append(10);
|
||||
|
||||
try std.testing.expectEqual(1, buffer.len());
|
||||
var iter = buffer.iterate();
|
||||
try std.testing.expectEqual(10, iter.next().?);
|
||||
try std.testing.expectEqual(null, iter.next());
|
||||
}
|
||||
|
||||
test "append bounds" {
|
||||
var buffer = Buffer(usize, 10).init();
|
||||
|
||||
// append 0 - 9 to buffer
|
||||
for (0..10) |i| {
|
||||
buffer.append(i);
|
||||
}
|
||||
// we expect 0 - 9 to exist inside buffer
|
||||
var iter = buffer.iterate();
|
||||
for (0..10) |i| {
|
||||
try std.testing.expectEqual(i, iter.next().?);
|
||||
}
|
||||
|
||||
// append 11, will overwrite 0.
|
||||
buffer.append(11);
|
||||
|
||||
// reset iterator
|
||||
iter = buffer.iterate();
|
||||
|
||||
// values 1-9 are present
|
||||
for (1..10) |i| {
|
||||
try std.testing.expectEqual(i, iter.next().?);
|
||||
}
|
||||
// and so is value 11
|
||||
try std.testing.expectEqual(11, iter.next().?);
|
||||
}
|
||||
|
||||
fn deinit_byte_slice(allocator: std.mem.Allocator, item: []u8) void {
|
||||
allocator.free(item);
|
||||
}
|
||||
|
||||
test "deiniting allocating buffer does not leak" {
|
||||
// create closure
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
// create buffer with closure
|
||||
var buffer = Buffer([]u8, 3).init_with_closure(allocator, &deinit_byte_slice);
|
||||
defer buffer.deinit();
|
||||
|
||||
// add less elements than length
|
||||
buffer.append(try std.fmt.allocPrint(allocator, "item {}", .{1}));
|
||||
buffer.append(try std.fmt.allocPrint(allocator, "item {}", .{2}));
|
||||
buffer.append(try std.fmt.allocPrint(allocator, "item {}", .{3}));
|
||||
|
||||
try std.testing.expectEqual(3, buffer.len());
|
||||
}
|
||||
|
||||
test "wrapping allocating buffer does not leak" {
|
||||
// create closure
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
// create buffer with closure
|
||||
var buffer = Buffer([]u8, 3).init_with_closure(allocator, &deinit_byte_slice);
|
||||
defer buffer.deinit();
|
||||
|
||||
// add an element more than length
|
||||
buffer.append(try std.fmt.allocPrint(allocator, "item {}", .{1}));
|
||||
buffer.append(try std.fmt.allocPrint(allocator, "item {}", .{2}));
|
||||
buffer.append(try std.fmt.allocPrint(allocator, "item {}", .{3}));
|
||||
buffer.append(try std.fmt.allocPrint(allocator, "item {}", .{4}));
|
||||
|
||||
// length is still 3 because we have removed the first element
|
||||
try std.testing.expectEqual(3, buffer.len());
|
||||
try std.testing.expectEqual(4, buffer.insertions);
|
||||
try std.testing.expect(buffer.reached_end());
|
||||
}
|
||||
51
src/main.zig
51
src/main.zig
|
|
@ -1,13 +1,12 @@
|
|||
const std = @import("std");
|
||||
|
||||
const zircon = @import("zircon");
|
||||
const zigeru = @import("zigeru");
|
||||
|
||||
const Bot = zigeru.bot.Bot;
|
||||
const Error = zigeru.bot.Error;
|
||||
const BotCommand = zigeru.bot.Command;
|
||||
const AdminCommand = zigeru.bot.AdminCommand;
|
||||
const BotMessage = zigeru.bot.Message;
|
||||
const zircon = @import("zircon");
|
||||
|
||||
var debug_allocator = std.heap.DebugAllocator(.{}).init;
|
||||
|
||||
|
|
@ -121,26 +120,28 @@ pub const BotAdapter = struct {
|
|||
}
|
||||
};
|
||||
|
||||
test "substitute" {
|
||||
var bot_adapter = try BotAdapter.init(std.testing.allocator);
|
||||
defer bot_adapter.deinit();
|
||||
const prefix = zircon.Prefix{ .nick = "jassob", .user = "jassob", .host = "localhost" };
|
||||
const msg = zircon.Message{
|
||||
.PRIVMSG = .{
|
||||
.prefix = prefix,
|
||||
.targets = "#test",
|
||||
.text = "hello world",
|
||||
},
|
||||
};
|
||||
_ = bot_adapter.callback(msg);
|
||||
const cmd_msg = zircon.Message{
|
||||
.PRIVMSG = .{
|
||||
.prefix = prefix,
|
||||
.targets = "#test",
|
||||
.text = "s/world/zig/",
|
||||
},
|
||||
};
|
||||
const response = bot_adapter.callback(cmd_msg);
|
||||
try std.testing.expect(response != null);
|
||||
try std.testing.expectEqualStrings("jassob: \"hello zig\"", response.?.PRIVMSG.text);
|
||||
}
|
||||
// test "substitute" {
|
||||
// var bot_adapter = try BotAdapter.init(std.testing.allocator);
|
||||
// defer bot_adapter.deinit();
|
||||
// const prefix = zircon.Prefix{ .nick = "jassob", .user = "jassob", .host = "localhost" };
|
||||
// const msg = zircon.Message{
|
||||
// .PRIVMSG = .{
|
||||
// .prefix = prefix,
|
||||
// .targets = "#test",
|
||||
// .text = "hello world",
|
||||
// },
|
||||
// };
|
||||
// if (bot_adapter.callback(msg)) |_| {
|
||||
// @panic("unexpected response");
|
||||
// }
|
||||
// const cmd_msg = zircon.Message{
|
||||
// .PRIVMSG = .{
|
||||
// .prefix = prefix,
|
||||
// .targets = "#test",
|
||||
// .text = "s/world/zig/",
|
||||
// },
|
||||
// };
|
||||
// const response = bot_adapter.callback(cmd_msg);
|
||||
// try std.testing.expect(response != null);
|
||||
// try std.testing.expectEqualStrings("jassob: \"hello zig\"", response.?.PRIVMSG.text);
|
||||
// }
|
||||
|
|
|
|||
50
src/parser.zig
Normal file
50
src/parser.zig
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub const Parser = struct {
|
||||
original: []const u8,
|
||||
rest: []const u8,
|
||||
end_idx: usize,
|
||||
|
||||
pub fn seek(self: *const Parser, skip: usize) Parser {
|
||||
return .{ .original = self.original, .rest = self.rest[skip..], .end_idx = self.end_idx + skip };
|
||||
}
|
||||
|
||||
pub fn init(s: []const u8) Parser {
|
||||
return .{
|
||||
.original = s,
|
||||
.end_idx = 0,
|
||||
.rest = s,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn consume_char(self: *const Parser, c: u8) ?Parser {
|
||||
if (self.rest[0] != c) {
|
||||
return null;
|
||||
}
|
||||
return self.seek(1);
|
||||
}
|
||||
|
||||
pub fn consume_str(self: *const Parser, s: []const u8) ?Parser {
|
||||
const len = s.len;
|
||||
if (self.rest.len < len) {
|
||||
return null;
|
||||
}
|
||||
if (!std.mem.eql(u8, self.rest[0..len], s)) {
|
||||
return null;
|
||||
}
|
||||
return self.seek(len);
|
||||
}
|
||||
|
||||
pub fn take_until_char(self: *const Parser, c: u8) struct { Parser, []const u8 } {
|
||||
const idx = std.mem.indexOfScalar(u8, self.rest, c) orelse unreachable;
|
||||
return .{ self.seek(idx), self.rest[0..idx] };
|
||||
}
|
||||
|
||||
pub fn take_char(self: *const Parser) struct { Parser, u8 } {
|
||||
return .{ self.seek(1), self.rest[0] };
|
||||
}
|
||||
|
||||
pub fn parsed(self: *const Parser) []const u8 {
|
||||
return self.original[0..self.end_idx];
|
||||
}
|
||||
};
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
//! By convention, root.zig is the root source file when making a library.
|
||||
|
||||
pub const bot = @import("bot.zig");
|
||||
pub const buffer = @import("buffer.zig");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue