From 8263ccad82507457083177a4c516741b092a45e6 Mon Sep 17 00:00:00 2001 From: falsycat Date: Mon, 5 May 2025 09:56:28 +0900 Subject: [PATCH] implement Command interface --- src/hncore/Command.zig | 51 ------------ src/hncore/CommandHistory.zig | 59 +++++--------- src/hncore/Task.zig | 12 ++- src/hncore/command/Interface.zig | 134 +++++++++++++++++++++++++++++++ src/hncore/command/root.zig | 7 ++ src/hncore/command/task.zig | 67 ++++++++++++++++ src/hncore/root.zig | 3 +- 7 files changed, 237 insertions(+), 96 deletions(-) delete mode 100644 src/hncore/Command.zig create mode 100644 src/hncore/command/Interface.zig create mode 100644 src/hncore/command/root.zig create mode 100644 src/hncore/command/task.zig diff --git a/src/hncore/Command.zig b/src/hncore/Command.zig deleted file mode 100644 index 746479e..0000000 --- a/src/hncore/Command.zig +++ /dev/null @@ -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); -} diff --git a/src/hncore/CommandHistory.zig b/src/hncore/CommandHistory.zig index eef780e..1f1ec19 100644 --- a/src/hncore/CommandHistory.zig +++ b/src/hncore/CommandHistory.zig @@ -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); } diff --git a/src/hncore/Task.zig b/src/hncore/Task.zig index 25787c2..b94de1f 100644 --- a/src/hncore/Task.zig +++ b/src/hncore/Task.zig @@ -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); } diff --git a/src/hncore/command/Interface.zig b/src/hncore/command/Interface.zig new file mode 100644 index 0000000..e1b230c --- /dev/null +++ b/src/hncore/command/Interface.zig @@ -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); +} diff --git a/src/hncore/command/root.zig b/src/hncore/command/root.zig new file mode 100644 index 0000000..e8889de --- /dev/null +++ b/src/hncore/command/root.zig @@ -0,0 +1,7 @@ +pub const Interface = @import("./Interface.zig"); + +pub const task = @import("./task.zig"); + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/src/hncore/command/task.zig b/src/hncore/command/task.zig new file mode 100644 index 0000000..1e08674 --- /dev/null +++ b/src/hncore/command/task.zig @@ -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); +} diff --git a/src/hncore/root.zig b/src/hncore/root.zig index 1b62c22..70eae07 100644 --- a/src/hncore/root.zig +++ b/src/hncore/root.zig @@ -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");