implement Command interface
This commit is contained in:
		| @@ -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); | ||||
| } | ||||
| @@ -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); | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
| } | ||||
|   | ||||
							
								
								
									
										134
									
								
								src/hncore/command/Interface.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								src/hncore/command/Interface.zig
									
									
									
									
									
										Normal 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); | ||||
| } | ||||
							
								
								
									
										7
									
								
								src/hncore/command/root.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/hncore/command/root.zig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| pub const Interface = @import("./Interface.zig"); | ||||
|  | ||||
| pub const task = @import("./task.zig"); | ||||
|  | ||||
| test { | ||||
|     @import("std").testing.refAllDecls(@This()); | ||||
| } | ||||
							
								
								
									
										67
									
								
								src/hncore/command/task.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/hncore/command/task.zig
									
									
									
									
									
										Normal 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); | ||||
| } | ||||
| @@ -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"); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user