implement Command interface

This commit is contained in:
falsycat 2025-05-05 09:56:28 +09:00
parent 88252a151b
commit 8263ccad82
7 changed files with 237 additions and 96 deletions

View File

@ -1,51 +0,0 @@
//!
const std = @import("std");
///
pub const Self = @This();
///
pub const Error = error {
///
AllocationFailure,
///
InvalidState,
///
Unknown,
};
///
pub const VTable = struct {
///
deinit: *const fn(ctx: *anyopaque) void,
///
apply: *const fn(ctx: *anyopaque) Error!void,
///
revert: *const fn(ctx: *anyopaque) Error!void,
};
///
ctx: *anyopaque,
///
vtable: *const VTable,
///
pub fn deinit(self: Self) void {
self.vtable.deinit(self.ctx);
}
///
pub fn apply(self: Self) !void {
return self.vtable.apply(self.ctx);
}
///
pub fn revert(self: Self) !void {
return self.vtable.revert(self.ctx);
}

View File

@ -1,12 +1,12 @@
const std = @import("std");
const Command = @import("./Command.zig");
const CommandIF = @import("./command/root.zig").Interface;
///
const Self = @This();
///
const CommandList = std.ArrayList(Command);
const CommandList = std.ArrayList(CommandIF);
///
commands: CommandList,
@ -31,7 +31,7 @@ pub fn deinit(self: *Self) void {
}
///
pub fn exec(self: *Self, command: Command) !void {
pub fn exec(self: *Self, command: CommandIF) !void {
if (self.head >= self.commands.items.len) {
try self.commands.append(command);
} else {
@ -74,25 +74,8 @@ pub fn isRedoAvailable(self: *const Self) bool {
}
test {
const Mock = struct {
const vt = Command.VTable {
.deinit = deinit_,
.apply = apply_,
.revert = revert_,
};
count: i32 = 0,
fn deinit_(_: *anyopaque) void { }
fn apply_(ctx: *anyopaque) Command.Error!void {
var self: *@This() = @ptrCast(@alignCast(ctx));
self.count += 1;
}
fn revert_(ctx: *anyopaque) Command.Error!void {
var self: *@This() = @ptrCast(@alignCast(ctx));
self.count -= 1;
}
};
var mock1 = Mock {};
var mock2 = Mock {};
var value1: i32 = 0;
var value2: i32 = 0;
var history = init(std.testing.allocator);
defer history.deinit();
@ -102,48 +85,50 @@ test {
try std.testing.expectError(error.NoCommand, history.undo());
try std.testing.expectError(error.NoCommand, history.redo());
try history.exec(Command { .ctx = &mock1, .vtable = &Mock.vt, });
try history.exec(
try CommandIF.make(std.testing.allocator, CommandIF.Mock { .target = &value1, }));
try std.testing.expectEqual(history.isUndoAvailable(), true);
try std.testing.expectEqual(history.isRedoAvailable(), false);
try std.testing.expectEqual(mock1.count, 1);
try std.testing.expectEqual(value1, 1);
try history.undo();
try std.testing.expectEqual(history.isUndoAvailable(), false);
try std.testing.expectEqual(history.isRedoAvailable(), true);
try std.testing.expectEqual(mock1.count, 0);
try std.testing.expectEqual(value1, 0);
try history.redo();
try std.testing.expectEqual(history.isUndoAvailable(), true);
try std.testing.expectEqual(history.isRedoAvailable(), false);
try std.testing.expectEqual(mock1.count, 1);
try std.testing.expectEqual(value1, 1);
try history.exec(Command { .ctx = &mock2, .vtable = &Mock.vt, });
try history.exec(
try CommandIF.make(std.testing.allocator, CommandIF.Mock { .target = &value2, }));
try std.testing.expectEqual(history.isUndoAvailable(), true);
try std.testing.expectEqual(history.isRedoAvailable(), false);
try std.testing.expectEqual(mock1.count, 1);
try std.testing.expectEqual(mock2.count, 1);
try std.testing.expectEqual(value1, 1);
try std.testing.expectEqual(value2, 1);
try history.undo();
try std.testing.expectEqual(history.isUndoAvailable(), true);
try std.testing.expectEqual(history.isRedoAvailable(), true);
try std.testing.expectEqual(mock1.count, 1);
try std.testing.expectEqual(mock2.count, 0);
try std.testing.expectEqual(value1, 1);
try std.testing.expectEqual(value2, 0);
try history.redo();
try std.testing.expectEqual(history.isUndoAvailable(), true);
try std.testing.expectEqual(history.isRedoAvailable(), false);
try std.testing.expectEqual(mock1.count, 1);
try std.testing.expectEqual(mock2.count, 1);
try std.testing.expectEqual(value1, 1);
try std.testing.expectEqual(value2, 1);
try history.undo();
try std.testing.expectEqual(history.isUndoAvailable(), true);
try std.testing.expectEqual(history.isRedoAvailable(), true);
try std.testing.expectEqual(mock1.count, 1);
try std.testing.expectEqual(mock2.count, 0);
try std.testing.expectEqual(value1, 1);
try std.testing.expectEqual(value2, 0);
try history.undo();
try std.testing.expectEqual(history.isUndoAvailable(), false);
try std.testing.expectEqual(history.isRedoAvailable(), true);
try std.testing.expectEqual(mock1.count, 0);
try std.testing.expectEqual(mock2.count, 0);
try std.testing.expectEqual(value1, 0);
try std.testing.expectEqual(value2, 0);
}

View File

@ -38,16 +38,14 @@ pub fn deinit(self: *Self) void {
}
///
pub fn setSummary(self: *Self, v: []const u8) ![:0]const u8 {
pub fn setSummary(self: *Self, v: []const u8) !void {
self.alloc.free(self.summary);
self.summary = try self.alloc.dupeZ(u8, v);
return self.summary;
}
///
pub fn setDetails(self: *Self, v: []const u8) ![:0]const u8 {
pub fn setDetails(self: *Self, v: []const u8) !void {
self.alloc.free(self.details);
self.details = try self.alloc.dupeZ(u8, v);
return self.details;
}
test {
@ -57,15 +55,15 @@ test {
try std.testing.expectEqualStrings("", task.summary);
try std.testing.expectEqualStrings("", task.details);
_ = try task.setSummary("helloworld");
try task.setSummary("helloworld");
try std.testing.expectEqualStrings("helloworld", task.summary);
try std.testing.expectEqualStrings("", task.details);
_ = try task.setDetails("good afternoon");
try task.setDetails("good afternoon");
try std.testing.expectEqualStrings("helloworld", task.summary);
try std.testing.expectEqualStrings("good afternoon", task.details);
_ = try task.setSummary("goodbye");
try task.setSummary("goodbye");
try std.testing.expectEqualStrings("goodbye", task.summary);
try std.testing.expectEqualStrings("good afternoon", task.details);
}

View File

@ -0,0 +1,134 @@
//!
const std = @import("std");
///
pub const Self = @This();
///
pub const Error = error {
///
OutOfMemory,
///
InvalidState,
///
Unknown,
};
///
pub const VTable = struct {
///
deinit: *const fn(ctx: *anyopaque) void,
///
apply: *const fn(ctx: *anyopaque) Error!void,
///
revert: *const fn(ctx: *anyopaque) Error!void,
};
///
pub const Mock = struct {
target : ?*i32 = null,
destroyed: ?*bool = null,
pub fn deinit(self: *@This(), _: anytype) void {
if (self.destroyed) |ptr| {
ptr.* = true;
}
}
pub fn apply(self: *@This(), _: anytype) !void {
if (self.target) |ptr| {
ptr.* += 1;
}
}
pub fn revert(self: *@This(), _: anytype) !void {
if (self.target) |ptr| {
ptr.* -= 1;
}
}
};
///
ctx: *anyopaque,
///
vtable: *const VTable,
///
pub fn deinit(self: Self) void {
self.vtable.deinit(self.ctx);
}
///
pub fn apply(self: Self) !void {
return self.vtable.apply(self.ctx);
}
///
pub fn revert(self: Self) !void {
return self.vtable.revert(self.ctx);
}
///
pub fn make(alloc: std.mem.Allocator, ctx: anytype) !Self {
const Target = @TypeOf(ctx);
const Wrapper = struct {
const vtable = VTable {
.deinit = @This().del,
.apply = @This().apply,
.revert = @This().revert,
};
alloc: std.mem.Allocator,
ctx : Target,
fn new(alloc2: std.mem.Allocator, ctx2: Target) !*@This() {
const self = try alloc2.create(@This());
self.* = .{
.alloc = alloc2,
.ctx = ctx2,
};
return self;
}
fn del(selfp: *anyopaque) void {
const self: *@This() = @ptrCast(@alignCast(selfp));
self.ctx.deinit(self.alloc);
self.alloc.destroy(self);
}
fn apply(selfp: *anyopaque) Error!void {
const self: *@This() = @ptrCast(@alignCast(selfp));
return self.ctx.apply(self.alloc);
}
fn revert(selfp: *anyopaque) Error!void {
const self: *@This() = @ptrCast(@alignCast(selfp));
return self.ctx.revert(self.alloc);
}
};
return Self {
.ctx = try Wrapper.new(alloc, ctx),
.vtable = &Wrapper.vtable,
};
}
test {
var value: i32 = 0;
var destroyed = false;
{
const cmd = try Self.make(std.testing.allocator, Mock {
.target = &value,
.destroyed = &destroyed,
});
defer cmd.deinit();
try cmd.apply();
try std.testing.expectEqual(value, 1);
try cmd.revert();
try std.testing.expectEqual(value, 0);
}
try std.testing.expect(destroyed);
}

View File

@ -0,0 +1,7 @@
pub const Interface = @import("./Interface.zig");
pub const task = @import("./task.zig");
test {
@import("std").testing.refAllDecls(@This());
}

View File

@ -0,0 +1,67 @@
const std = @import("std");
const Interface = @import("./Interface.zig");
const Task = @import("../Task.zig");
///
const SetSummary = struct {
///
const Self = @This();
///
target: *Task,
///
summary: [:0]const u8,
///
pub fn init(alloc: std.mem.Allocator, task: *Task, summary: []const u8) !Interface {
const summaryDup = try alloc.dupeZ(u8, summary);
errdefer alloc.free(summaryDup);
return try Interface.make(alloc, Self {
.target = task,
.summary = summaryDup,
});
}
///
pub fn deinit(self: *Self, alloc: std.mem.Allocator) void {
alloc.free(self.summary);
}
///
pub fn apply(self: *Self, alloc: std.mem.Allocator) Interface.Error!void {
const old = try alloc.dupeZ(u8, self.target.summary);
errdefer alloc.free(old);
try self.target.setSummary(self.summary);
alloc.free(self.summary);
self.summary = old;
}
///
pub fn revert(self: *Self, alloc: std.mem.Allocator) Interface.Error!void {
return self.apply(alloc);
}
};
test {
const TaskStore = @import("../TaskStore.zig");
var tasks = TaskStore.init(std.testing.allocator);
defer tasks.deinit();
var task = try tasks.add();
try task.setSummary("helloworld");
var sut = try SetSummary.init(std.testing.allocator, task, "goodbye");
defer sut.deinit();
try sut.apply();
try std.testing.expectEqualStrings("goodbye", task.summary);
try sut.revert();
try std.testing.expectEqualStrings("helloworld", task.summary);
}

View File

@ -1,4 +1,5 @@
pub const Command = @import("./Command.zig");
pub const command = @import("./command/root.zig");
pub const CommandHistory = @import("./CommandHistory.zig");
pub const Task = @import("./Task.zig");
pub const TaskStore = @import("./TaskStore.zig");