├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── src └── interface.zig └── test ├── complex.zig ├── embedded.zig └── simple.zig /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | 2 | name: CI 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test-example: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, macos-latest, windows-latest] 15 | zig-version: ["0.14.1"] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Install Zig 21 | uses: goto-bus-stop/setup-zig@v2 22 | with: 23 | version: ${{ matrix.zig-version }} 24 | 25 | - name: Check Zig Version 26 | run: zig version 27 | 28 | - name: Run tests 29 | run: zig build test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache 2 | .zig-out -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Steve Manuel 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the “Software”), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zig Interfaces & Validation 2 | 3 | A compile-time interface checker for Zig that enables interface-based design 4 | with comprehensive type checking and detailed error reporting. 5 | 6 | ## Features 7 | 8 | This library provides a way to define and verify interfaces in Zig at compile 9 | time. It supports: 10 | 11 | - Type-safe interface definitions with detailed error reporting 12 | - Interface embedding (composition) 13 | - Complex type validation including structs, enums, arrays, and slices 14 | - Comprehensive compile-time error messages with helpful hints 15 | - Flexible error union compatibility with `anyerror` 16 | 17 | ## Install 18 | 19 | Add or update this library as a dependency in your zig project run the following command: 20 | 21 | ```sh 22 | zig fetch --save git+https://github.com/nilslice/zig-interface 23 | ``` 24 | 25 | Afterwards add the library as a dependency to any module in your _build.zig_: 26 | 27 | ```zig 28 | // ... 29 | const interface_dependency = b.dependency("interface", .{ 30 | .target = target, 31 | .optimize = optimize, 32 | }); 33 | 34 | const exe = b.addExecutable(.{ 35 | .name = "main", 36 | .root_source_file = b.path("src/main.zig"), 37 | .target = target, 38 | .optimize = optimize, 39 | }); 40 | // import the exposed `interface` module from the dependency 41 | exe.root_module.addImport("interface", interface_dependency.module("interface")); 42 | // ... 43 | ``` 44 | 45 | In the end you can import the `interface` module. For example: 46 | 47 | ```zig 48 | const Interface = @import("interface").Interface; 49 | 50 | const Repository = Interface(.{ 51 | .create = fn(anytype, User) anyerror!u32, 52 | .findById = fn(anytype, u32) anyerror!?User, 53 | .update = fn(anytype, User) anyerror!void, 54 | .delete = fn(anytype, u32) anyerror!void, 55 | }, null); 56 | ``` 57 | 58 | ## Usage 59 | 60 | 1. Define an interface with required method signatures: 61 | 62 | ```zig 63 | const Repository = Interface(.{ 64 | .create = fn(anytype, User) anyerror!u32, 65 | .findById = fn(anytype, u32) anyerror!?User, 66 | .update = fn(anytype, User) anyerror!void, 67 | .delete = fn(anytype, u32) anyerror!void, 68 | }, null); 69 | ``` 70 | 71 | 2. Implement the interface methods in your type: 72 | 73 | ```zig 74 | const InMemoryRepository = struct { 75 | allocator: std.mem.Allocator, 76 | users: std.AutoHashMap(u32, User), 77 | next_id: u32, 78 | 79 | pub fn create(self: *InMemoryRepository, user: User) !u32 { 80 | var new_user = user; 81 | new_user.id = self.next_id; 82 | try self.users.put(self.next_id, new_user); 83 | self.next_id += 1; 84 | return new_user.id; 85 | } 86 | 87 | // ... other Repository methods 88 | }; 89 | ``` 90 | 91 | 3. Verify the implementation at compile time: 92 | 93 | ```zig 94 | // In functions that accept interface implementations: 95 | fn createUser(repo: anytype, name: []const u8, email: []const u8) !User { 96 | comptime Repository.satisfiedBy(@TypeOf(repo)); 97 | // ... rest of implementation 98 | } 99 | 100 | // Or verify directly: 101 | comptime Repository.satisfiedBy(InMemoryRepository); 102 | ``` 103 | 104 | ## Interface Embedding 105 | 106 | Interfaces can embed other interfaces to combine their requirements: 107 | 108 | ```zig 109 | const Logger = Interface(.{ 110 | .log = fn(anytype, []const u8) void, 111 | .getLogLevel = fn(anytype) u8, 112 | }, null); 113 | 114 | const Metrics = Interface(.{ 115 | .increment = fn(anytype, []const u8) void, 116 | .getValue = fn(anytype, []const u8) u64, 117 | }, .{ Logger }); // Embeds Logger interface 118 | 119 | // Now implements both Metrics and Logger methods 120 | const MonitoredRepository = Interface(.{ 121 | .create = fn(anytype, User) anyerror!u32, 122 | .findById = fn(anytype, u32) anyerror!?User, 123 | }, .{ Metrics }); 124 | ``` 125 | 126 | > Note: you can embed arbitrarily many interfaces! 127 | 128 | ## Error Reporting 129 | 130 | The library provides detailed compile-time errors when implementations don't 131 | match: 132 | 133 | ```zig 134 | // Wrong parameter type ([]u8 vs []const u8) 135 | const BadImpl = struct { 136 | pub fn writeAll(self: @This(), data: []u8) !void { 137 | _ = self; 138 | _ = data; 139 | } 140 | }; 141 | 142 | // Results in compile error: 143 | // error: Method 'writeAll' parameter 1 has incorrect type: 144 | // └─ Expected: []const u8 145 | // └─ Got: []u8 146 | // └─ Hint: Consider making the parameter type const 147 | ``` 148 | 149 | ## Complex Types 150 | 151 | The interface checker supports complex types including: 152 | 153 | ```zig 154 | const ComplexTypes = Interface(.{ 155 | .process = fn( 156 | anytype, 157 | struct { config: Config, points: []const DataPoint }, 158 | enum { ready, processing, error }, 159 | []const struct { 160 | timestamp: i64, 161 | data: ?[]const DataPoint, 162 | status: Status, 163 | } 164 | ) anyerror!?ProcessingResult, 165 | }, null); 166 | ``` 167 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | // Although this function looks imperative, note that its job is to 4 | // declaratively construct a build graph that will be executed by an external 5 | // runner. 6 | pub fn build(b: *std.Build) void { 7 | // Standard target options allows the person running `zig build` to choose 8 | // what target to build for. Here we do not override the defaults, which 9 | // means any target is allowed, and the default is native. Other options 10 | // for restricting supported target set are available. 11 | const target = b.standardTargetOptions(.{}); 12 | 13 | // Standard optimization options allow the person running `zig build` to select 14 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not 15 | // set a preferred release mode, allowing the user to decide how to optimize. 16 | const optimize = b.standardOptimizeOption(.{}); 17 | 18 | const interface_lib = b.addModule("interface", .{ 19 | .root_source_file = b.path("src/interface.zig"), 20 | }); 21 | 22 | // Creates a step for unit testing. This only builds the test executable 23 | // but does not run it. 24 | const simple_unit_tests = b.addTest(.{ 25 | .root_source_file = b.path("test/simple.zig"), 26 | .target = target, 27 | .optimize = optimize, 28 | }); 29 | simple_unit_tests.root_module.addImport("interface", interface_lib); 30 | const run_simple_unit_tests = b.addRunArtifact(simple_unit_tests); 31 | 32 | const complex_unit_tests = b.addTest(.{ 33 | .root_source_file = b.path("test/complex.zig"), 34 | .target = target, 35 | .optimize = optimize, 36 | }); 37 | complex_unit_tests.root_module.addImport("interface", interface_lib); 38 | const run_complex_unit_tests = b.addRunArtifact(complex_unit_tests); 39 | 40 | const embedded_unit_tests = b.addTest(.{ 41 | .root_source_file = b.path("test/embedded.zig"), 42 | .target = target, 43 | .optimize = optimize, 44 | }); 45 | embedded_unit_tests.root_module.addImport("interface", interface_lib); 46 | const run_embedded_unit_tests = b.addRunArtifact(embedded_unit_tests); 47 | 48 | // Similar to creating the run step earlier, this exposes a `test` step to 49 | // the `zig build --help` menu, providing a way for the user to request 50 | // running the unit tests. 51 | const test_step = b.step("test", "Run unit tests"); 52 | test_step.dependOn(&run_simple_unit_tests.step); 53 | test_step.dependOn(&run_complex_unit_tests.step); 54 | test_step.dependOn(&run_embedded_unit_tests.step); 55 | } 56 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .fingerprint = 0x34f4ecdd27565918, 3 | .name = .interface, 4 | // This is a [Semantic Version](https://semver.org/). 5 | // In a future version of Zig it will be used for package deduplication. 6 | .version = "0.0.2", 7 | 8 | // This field is optional. 9 | // This is currently advisory only; Zig does not yet do anything 10 | // with this value. 11 | //.minimum_zig_version = "0.11.0", 12 | 13 | // This field is optional. 14 | // Each dependency must either provide a `url` and `hash`, or a `path`. 15 | // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. 16 | // Once all dependencies are fetched, `zig build` no longer requires 17 | // internet connectivity. 18 | .dependencies = .{ 19 | // See `zig fetch --save ` for a command-line interface for adding dependencies. 20 | //.example = .{ 21 | // // When updating this field to a new URL, be sure to delete the corresponding 22 | // // `hash`, otherwise you are communicating that you expect to find the old hash at 23 | // // the new URL. 24 | // .url = "https://example.com/foo.tar.gz", 25 | // 26 | // // This is computed from the file contents of the directory of files that is 27 | // // obtained after fetching `url` and applying the inclusion rules given by 28 | // // `paths`. 29 | // // 30 | // // This field is the source of truth; packages do not come from a `url`; they 31 | // // come from a `hash`. `url` is just one of many possible mirrors for how to 32 | // // obtain a package matching this `hash`. 33 | // // 34 | // // Uses the [multihash](https://multiformats.io/multihash/) format. 35 | // .hash = "...", 36 | // 37 | // // When this is provided, the package is found in a directory relative to the 38 | // // build root. In this case the package's hash is irrelevant and therefore not 39 | // // computed. This field and `url` are mutually exclusive. 40 | // .path = "foo", 41 | 42 | // // When this is set to `true`, a package is declared to be lazily 43 | // // fetched. This makes the dependency only get fetched if it is 44 | // // actually used. 45 | // .lazy = false, 46 | //}, 47 | }, 48 | 49 | // Specifies the set of files and directories that are included in this package. 50 | // Only files and directories listed here are included in the `hash` that 51 | // is computed for this package. 52 | // Paths are relative to the build root. Use the empty string (`""`) to refer to 53 | // the build root itself. 54 | // A directory listed here means that all files within, recursively, are included. 55 | .paths = .{ 56 | // This makes *all* files, recursively, included in this package. It is generally 57 | // better to explicitly list the files and directories instead, to insure that 58 | // fetching from tarballs, file system paths, and version control all result 59 | // in the same contents hash. 60 | "", 61 | // For example... 62 | //"build.zig", 63 | //"build.zig.zon", 64 | //"src", 65 | //"LICENSE", 66 | //"README.md", 67 | }, 68 | } 69 | -------------------------------------------------------------------------------- /src/interface.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// Compares two types structurally to determine if they're compatible 4 | fn isTypeCompatible(comptime T1: type, comptime T2: type) bool { 5 | const info1 = @typeInfo(T1); 6 | const info2 = @typeInfo(T2); 7 | 8 | // If types are identical, they're compatible 9 | if (T1 == T2) return true; 10 | 11 | // If type categories don't match, they're not compatible 12 | if (@intFromEnum(info1) != @intFromEnum(info2)) return false; 13 | 14 | return switch (info1) { 15 | .@"struct" => |s1| blk: { 16 | const s2 = @typeInfo(T2).@"struct"; 17 | if (s1.fields.len != s2.fields.len) break :blk false; 18 | if (s1.is_tuple != s2.is_tuple) break :blk false; 19 | 20 | for (s1.fields, s2.fields) |f1, f2| { 21 | if (!std.mem.eql(u8, f1.name, f2.name)) break :blk false; 22 | if (!isTypeCompatible(f1.type, f2.type)) break :blk false; 23 | } 24 | break :blk true; 25 | }, 26 | .@"enum" => |e1| blk: { 27 | const e2 = @typeInfo(T2).@"enum"; 28 | if (e1.fields.len != e2.fields.len) break :blk false; 29 | 30 | for (e1.fields, e2.fields) |f1, f2| { 31 | if (!std.mem.eql(u8, f1.name, f2.name)) break :blk false; 32 | if (f1.value != f2.value) break :blk false; 33 | } 34 | break :blk true; 35 | }, 36 | .array => |a1| blk: { 37 | const a2 = @typeInfo(T2).array; 38 | if (a1.len != a2.len) break :blk false; 39 | break :blk isTypeCompatible(a1.child, a2.child); 40 | }, 41 | .pointer => |p1| blk: { 42 | const p2 = @typeInfo(T2).pointer; 43 | if (p1.size != p2.size) break :blk false; 44 | if (p1.is_const != p2.is_const) break :blk false; 45 | if (p1.is_volatile != p2.is_volatile) break :blk false; 46 | break :blk isTypeCompatible(p1.child, p2.child); 47 | }, 48 | .optional => |o1| blk: { 49 | const o2 = @typeInfo(T2).optional; 50 | break :blk isTypeCompatible(o1.child, o2.child); 51 | }, 52 | else => T1 == T2, 53 | }; 54 | } 55 | 56 | /// Generates helpful hints for type mismatches 57 | fn generateTypeHint(comptime expected: type, comptime got: type) ?[]const u8 { 58 | const exp_info = @typeInfo(expected); 59 | const got_info = @typeInfo(got); 60 | 61 | // Check for common slice constness issues 62 | if (exp_info == .Pointer and got_info == .Pointer) { 63 | const exp_ptr = exp_info.Pointer; 64 | const got_ptr = got_info.Pointer; 65 | if (exp_ptr.is_const and !got_ptr.is_const) { 66 | return "Consider making the parameter type const (e.g., []const u8 instead of []u8)"; 67 | } 68 | } 69 | 70 | // Check for optional vs non-optional mismatches 71 | if (exp_info == .Optional and got_info != .Optional) { 72 | return "The expected type is optional. Consider wrapping the parameter in '?'"; 73 | } 74 | if (exp_info != .Optional and got_info == .Optional) { 75 | return "The expected type is non-optional. Remove the '?' from the parameter type"; 76 | } 77 | 78 | // Check for enum type mismatches 79 | if (exp_info == .Enum and got_info == .Enum) { 80 | return "Check that the enum values and field names match exactly"; 81 | } 82 | 83 | // Check for struct field mismatches 84 | if (exp_info == .Struct and got_info == .Struct) { 85 | const exp_s = exp_info.Struct; 86 | const got_s = got_info.Struct; 87 | if (exp_s.fields.len != got_s.fields.len) { 88 | return "The structs have different numbers of fields"; 89 | } 90 | // Could add more specific field comparison hints here 91 | return "Check that all struct field names and types match exactly"; 92 | } 93 | 94 | // Generic catch-all for pointer size mismatches 95 | if (exp_info == .Pointer and got_info == .Pointer) { 96 | const exp_ptr = exp_info.Pointer; 97 | const got_ptr = got_info.Pointer; 98 | if (exp_ptr.size != got_ptr.size) { 99 | return "Check pointer type (single item vs slice vs many-item)"; 100 | } 101 | } 102 | 103 | return null; 104 | } 105 | 106 | /// Formats type mismatch errors with helpful hints 107 | fn formatTypeMismatch( 108 | comptime expected: type, 109 | comptime got: type, 110 | indent: []const u8, 111 | ) []const u8 { 112 | var result = std.fmt.comptimePrint( 113 | "{s}Expected: {s}\n{s}Got: {s}", 114 | .{ 115 | indent, 116 | @typeName(expected), 117 | indent, 118 | @typeName(got), 119 | }, 120 | ); 121 | 122 | // Add hint if available 123 | if (generateTypeHint(expected, got)) |hint| { 124 | result = result ++ std.fmt.comptimePrint("\n {s}Hint: {s}", .{ indent, hint }); 125 | } 126 | 127 | return result; 128 | } 129 | 130 | /// Creates a verifiable interface type that can be used to define method requirements 131 | /// for other types. Interfaces can embed other interfaces, combining their requirements. 132 | /// 133 | /// The interface consists of method signatures that implementing types must match exactly. 134 | /// Method signatures must use `anytype` for the self parameter to allow any implementing type. 135 | /// 136 | /// Supports: 137 | /// - Complex types (structs, enums, arrays, slices) 138 | /// - Error unions with specific or `anyerror` 139 | /// - Optional types and comptime checking 140 | /// - Interface embedding (combining multiple interfaces) 141 | /// - Detailed error reporting for mismatched implementations 142 | /// 143 | /// Params: 144 | /// methods: A struct of function signatures that define the interface 145 | /// embedded: A tuple of other interfaces to embed, or null for no embedding 146 | /// 147 | /// Example: 148 | /// ``` 149 | /// const Writer = Interface(.{ 150 | /// .writeAll = fn(anytype, []const u8) anyerror!void, 151 | /// }, null); 152 | /// 153 | /// const Logger = Interface(.{ 154 | /// .log = fn(anytype, []const u8) void, 155 | /// }, .{ Writer }); // Embeds Writer interface 156 | /// 157 | /// // Usage in functions: 158 | /// fn write(w: anytype, data: []const u8) !void { 159 | /// comptime Writer.satisfiedBy(@TypeOf(w)); 160 | /// try w.writeAll(data); 161 | /// } 162 | /// ``` 163 | /// 164 | /// Common incompatibilities reported: 165 | /// - Missing required methods 166 | /// - Wrong parameter counts or types 167 | /// - Incorrect return types 168 | /// - Method name conflicts in embedded interfaces 169 | /// - Non-const slices where const is required 170 | /// 171 | pub fn Interface(comptime methods: anytype, comptime embedded: anytype) type { 172 | const embedded_interfaces = switch (@typeInfo(@TypeOf(embedded))) { 173 | .null => embedded, 174 | .@"struct" => |s| if (s.is_tuple) embedded else .{embedded}, 175 | else => .{embedded}, 176 | }; 177 | 178 | // Handle the case where null is passed for embedded_interfaces 179 | const has_embeds = @TypeOf(embedded_interfaces) != @TypeOf(null); 180 | 181 | return struct { 182 | const Self = @This(); 183 | const name = @typeName(Self); 184 | 185 | // Store these at the type level so they're accessible to helper functions 186 | const Methods = @TypeOf(methods); 187 | const Embeds = @TypeOf(embedded_interfaces); 188 | 189 | /// Represents all possible interface implementation problems 190 | const Incompatibility = union(enum) { 191 | missing_method: []const u8, 192 | wrong_param_count: struct { 193 | method: []const u8, 194 | expected: usize, 195 | got: usize, 196 | }, 197 | param_type_mismatch: struct { 198 | method: []const u8, 199 | param_index: usize, 200 | expected: type, 201 | got: type, 202 | }, 203 | return_type_mismatch: struct { 204 | method: []const u8, 205 | expected: type, 206 | got: type, 207 | }, 208 | ambiguous_method: struct { 209 | method: []const u8, 210 | interfaces: []const []const u8, 211 | }, 212 | }; 213 | 214 | /// Collects all method names from this interface and its embedded interfaces 215 | fn collectMethodNames() []const []const u8 { 216 | comptime { 217 | var method_count: usize = 0; 218 | 219 | // Count methods from primary interface 220 | for (std.meta.fields(Methods)) |_| { 221 | method_count += 1; 222 | } 223 | 224 | // Count methods from embedded interfaces 225 | if (has_embeds) { 226 | for (std.meta.fields(Embeds)) |embed_field| { 227 | const embed = @field(embedded_interfaces, embed_field.name); 228 | method_count += embed.collectMethodNames().len; 229 | } 230 | } 231 | 232 | // Now create array of correct size 233 | var names: [method_count][]const u8 = undefined; 234 | var index: usize = 0; 235 | 236 | // Add primary interface methods 237 | for (std.meta.fields(Methods)) |field| { 238 | names[index] = field.name; 239 | index += 1; 240 | } 241 | 242 | // Add embedded interface methods 243 | if (has_embeds) { 244 | for (std.meta.fields(Embeds)) |embed_field| { 245 | const embed = @field(embedded_interfaces, embed_field.name); 246 | const embed_methods = embed.collectMethodNames(); 247 | @memcpy(names[index..][0..embed_methods.len], embed_methods); 248 | index += embed_methods.len; 249 | } 250 | } 251 | 252 | return &names; 253 | } 254 | } 255 | 256 | /// Checks if a method exists in multiple interfaces and returns the list of interfaces if so 257 | fn findMethodConflicts(comptime method_name: []const u8) ?[]const []const u8 { 258 | comptime { 259 | var interface_count: usize = 0; 260 | 261 | // Count primary interface 262 | if (@hasDecl(Methods, method_name)) { 263 | interface_count += 1; 264 | } 265 | 266 | // Count embedded interfaces 267 | if (has_embeds) { 268 | for (std.meta.fields(Embeds)) |embed_field| { 269 | const embed = @field(embedded_interfaces, embed_field.name); 270 | if (embed.hasMethod(method_name)) { 271 | interface_count += 1; 272 | } 273 | } 274 | } 275 | 276 | if (interface_count <= 1) return null; 277 | 278 | var interfaces: [interface_count][]const u8 = undefined; 279 | var index: usize = 0; 280 | 281 | // Add primary interface 282 | if (@hasDecl(Methods, method_name)) { 283 | interfaces[index] = name; 284 | index += 1; 285 | } 286 | 287 | // Add embedded interfaces 288 | if (has_embeds) { 289 | for (std.meta.fields(Embeds)) |embed_field| { 290 | const embed = @field(embedded_interfaces, embed_field.name); 291 | if (embed.hasMethod(method_name)) { 292 | interfaces[index] = @typeName(@TypeOf(embed)); 293 | index += 1; 294 | } 295 | } 296 | } 297 | 298 | return &interfaces; 299 | } 300 | } 301 | 302 | /// Checks if this interface has a specific method 303 | fn hasMethod(comptime method_name: []const u8) bool { 304 | comptime { 305 | // Check primary interface 306 | if (@hasDecl(Methods, method_name)) { 307 | return true; 308 | } 309 | 310 | // Check embedded interfaces 311 | if (has_embeds) { 312 | for (std.meta.fields(Embeds)) |embed_field| { 313 | const embed = @field(embedded_interfaces, embed_field.name); 314 | if (embed.hasMethod(method_name)) { 315 | return true; 316 | } 317 | } 318 | } 319 | 320 | return false; 321 | } 322 | } 323 | 324 | fn isCompatibleErrorSet(comptime Expected: type, comptime Actual: type) bool { 325 | const exp_info = @typeInfo(Expected); 326 | const act_info = @typeInfo(Actual); 327 | 328 | if (exp_info != .error_union or act_info != .error_union) { 329 | return Expected == Actual; 330 | } 331 | 332 | if (exp_info.error_union.error_set == anyerror) { 333 | return exp_info.error_union.payload == act_info.error_union.payload; 334 | } 335 | return Expected == Actual; 336 | } 337 | 338 | pub fn incompatibilities(comptime Type: type) []const Incompatibility { 339 | comptime { 340 | var problems: []const Incompatibility = &.{}; 341 | 342 | // First check for method ambiguity across all interfaces 343 | for (Self.collectMethodNames()) |method_name| { 344 | if (Self.findMethodConflicts(method_name)) |conflicting_interfaces| { 345 | problems = problems ++ &[_]Incompatibility{.{ 346 | .ambiguous_method = .{ 347 | .method = method_name, 348 | .interfaces = conflicting_interfaces, 349 | }, 350 | }}; 351 | } 352 | } 353 | 354 | // If we have ambiguous methods, return early 355 | if (problems.len > 0) return problems; 356 | 357 | // Check primary interface methods 358 | for (std.meta.fields(@TypeOf(methods))) |field| { 359 | if (!@hasDecl(Type, field.name)) { 360 | problems = problems ++ &[_]Incompatibility{.{ 361 | .missing_method = field.name, 362 | }}; 363 | continue; 364 | } 365 | 366 | const impl_fn = @TypeOf(@field(Type, field.name)); 367 | const expected_fn = @field(methods, field.name); 368 | 369 | const impl_info = @typeInfo(impl_fn).@"fn"; 370 | const expected_info = @typeInfo(expected_fn).@"fn"; 371 | 372 | if (impl_info.params.len != expected_info.params.len) { 373 | problems = problems ++ &[_]Incompatibility{.{ 374 | .wrong_param_count = .{ 375 | .method = field.name, 376 | .expected = expected_info.params.len, 377 | .got = impl_info.params.len, 378 | }, 379 | }}; 380 | } else { 381 | for (impl_info.params[1..], expected_info.params[1..], 0..) |impl_param, expected_param, i| { 382 | if (!isTypeCompatible(impl_param.type.?, expected_param.type.?)) { 383 | problems = problems ++ &[_]Incompatibility{.{ 384 | .param_type_mismatch = .{ 385 | .method = field.name, 386 | .param_index = i + 1, 387 | .expected = expected_param.type.?, 388 | .got = impl_param.type.?, 389 | }, 390 | }}; 391 | } 392 | } 393 | } 394 | 395 | if (!isCompatibleErrorSet(expected_info.return_type.?, impl_info.return_type.?)) { 396 | problems = problems ++ &[_]Incompatibility{.{ 397 | .return_type_mismatch = .{ 398 | .method = field.name, 399 | .expected = expected_info.return_type.?, 400 | .got = impl_info.return_type.?, 401 | }, 402 | }}; 403 | } 404 | } 405 | 406 | // Check embedded interfaces 407 | if (has_embeds) { 408 | for (std.meta.fields(@TypeOf(embedded_interfaces))) |embed_field| { 409 | const embed = @field(embedded_interfaces, embed_field.name); 410 | const embed_problems = embed.incompatibilities(Type); 411 | problems = problems ++ embed_problems; 412 | } 413 | } 414 | 415 | return problems; 416 | } 417 | } 418 | 419 | fn formatIncompatibility(incompatibility: Incompatibility) []const u8 { 420 | const indent = " └─ "; 421 | return switch (incompatibility) { 422 | .missing_method => |method| std.fmt.comptimePrint("Missing required method: {s}\n{s}Add the method with the correct signature to your implementation", .{ method, indent }), 423 | 424 | .wrong_param_count => |info| std.fmt.comptimePrint("Method '{s}' has incorrect number of parameters:\n" ++ 425 | "{s}Expected {d} parameters\n" ++ 426 | "{s}Got {d} parameters\n" ++ 427 | " {s}Hint: Remember that the first parameter should be the self/receiver type", .{ 428 | info.method, 429 | indent, 430 | info.expected, 431 | indent, 432 | info.got, 433 | indent, 434 | }), 435 | 436 | .param_type_mismatch => |info| std.fmt.comptimePrint("Method '{s}' parameter {d} has incorrect type:\n{s}", .{ 437 | info.method, 438 | info.param_index, 439 | formatTypeMismatch(info.expected, info.got, indent), 440 | }), 441 | 442 | .return_type_mismatch => |info| std.fmt.comptimePrint("Method '{s}' return type is incorrect:\n{s}", .{ 443 | info.method, 444 | formatTypeMismatch(info.expected, info.got, indent), 445 | }), 446 | 447 | .ambiguous_method => |info| std.fmt.comptimePrint("Method '{s}' is ambiguous - it appears in multiple interfaces: {s}\n" ++ 448 | " {s}Hint: This method needs to be uniquely implemented or the ambiguity resolved", .{ 449 | info.method, 450 | info.interfaces, 451 | indent, 452 | }), 453 | }; 454 | } 455 | 456 | pub fn satisfiedBy(comptime Type: type) void { 457 | comptime { 458 | const problems = incompatibilities(Type); 459 | if (problems.len > 0) { 460 | const title = "Type '{s}' does not implement interface '{s}':\n"; 461 | 462 | // First compute the total size needed for our error message 463 | var total_len: usize = std.fmt.count(title, .{ 464 | @typeName(Type), 465 | name, 466 | }); 467 | 468 | // Add space for each problem's length 469 | for (1.., problems) |i, problem| { 470 | total_len += std.fmt.count("{d}. {s}\n", .{ i, formatIncompatibility(problem) }); 471 | } 472 | 473 | // Now create a fixed-size array of the exact size we need 474 | var errors: [total_len]u8 = undefined; 475 | var written: usize = 0; 476 | 477 | written += (std.fmt.bufPrint(errors[written..], title, .{ 478 | @typeName(Type), 479 | name, 480 | }) catch unreachable).len; 481 | 482 | // Write each problem 483 | for (1.., problems) |i, problem| { 484 | written += (std.fmt.bufPrint(errors[written..], "{d}. {s}\n", .{ i, formatIncompatibility(problem) }) catch unreachable).len; 485 | } 486 | 487 | @compileError(errors[0..written]); 488 | } 489 | } 490 | } 491 | }; 492 | } 493 | 494 | test "expected usage of embedded interfaces" { 495 | const Logger = Interface(.{ 496 | .log = fn (anytype, []const u8) void, 497 | }, .{}); 498 | 499 | const Writer = Interface(.{ 500 | .write = fn (anytype, []const u8) anyerror!void, 501 | }, .{Logger}); 502 | 503 | const Implementation = struct { 504 | pub fn write(self: @This(), data: []const u8) !void { 505 | _ = self; 506 | _ = data; 507 | } 508 | 509 | pub fn log(self: @This(), msg: []const u8) void { 510 | _ = self; 511 | _ = msg; 512 | } 513 | }; 514 | 515 | comptime Writer.satisfiedBy(Implementation); 516 | 517 | try std.testing.expect(Writer.incompatibilities(Implementation).len == 0); 518 | } 519 | 520 | test "expected failure case of embedded interfaces" { 521 | const Logger = Interface(.{ 522 | .log = fn (anytype, []const u8, u8) void, 523 | .missing = fn (anytype) void, 524 | }, .{}); 525 | 526 | const Writer = Interface(.{ 527 | .write = fn (anytype, []const u8) anyerror!void, 528 | }, .{Logger}); 529 | 530 | const Implementation = struct { 531 | pub fn write(self: @This(), data: []const u8) !void { 532 | _ = self; 533 | _ = data; 534 | } 535 | 536 | pub fn log(self: @This(), msg: []const u8) void { 537 | _ = self; 538 | _ = msg; 539 | } 540 | }; 541 | 542 | try std.testing.expect(Writer.incompatibilities(Implementation).len == 2); 543 | } 544 | -------------------------------------------------------------------------------- /test/complex.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Interface = @import("interface").Interface; 3 | 4 | test "complex type support" { 5 | const ComplexTypes = Interface(.{ 6 | .complexMethod = fn (anytype, struct { a: []const u8, b: ?i32 }, enum { a, b, c }, []const struct { x: u32, y: ?[]const u8 }) anyerror!void, 7 | }, null); 8 | 9 | // Correct implementation 10 | const GoodImpl = struct { 11 | pub fn complexMethod( 12 | self: @This(), 13 | first: struct { a: []const u8, b: ?i32 }, 14 | second: enum { a, b, c }, 15 | third: []const struct { x: u32, y: ?[]const u8 }, 16 | ) !void { 17 | _ = self; 18 | _ = first; 19 | _ = second; 20 | _ = third; 21 | } 22 | }; 23 | 24 | // Should compile without error 25 | comptime ComplexTypes.satisfiedBy(GoodImpl); 26 | 27 | // Bad implementation - mismatched struct field type 28 | const BadImpl1 = struct { 29 | pub fn complexMethod( 30 | self: @This(), 31 | first: struct { a: []u8, b: ?i32 }, // []u8 instead of []const u8 32 | second: enum { a, b, c }, 33 | third: []const struct { x: u32, y: ?[]const u8 }, 34 | ) !void { 35 | _ = self; 36 | _ = first; 37 | _ = second; 38 | _ = third; 39 | } 40 | }; 41 | 42 | // Bad implementation - missing enum value 43 | const BadImpl2 = struct { 44 | pub fn complexMethod( 45 | self: @This(), 46 | first: struct { a: []const u8, b: ?i32 }, 47 | second: enum { a, b }, // missing 'c' 48 | third: []const struct { x: u32, y: ?[]const u8 }, 49 | ) !void { 50 | _ = self; 51 | _ = first; 52 | _ = second; 53 | _ = third; 54 | } 55 | }; 56 | 57 | // Bad implementation - different struct field name 58 | const BadImpl3 = struct { 59 | pub fn complexMethod( 60 | self: @This(), 61 | first: struct { a: []const u8, b: ?i32 }, 62 | second: enum { a, b, c }, 63 | third: []const struct { x: u32, y_value: ?[]const u8 }, // y_value instead of y 64 | ) !void { 65 | _ = self; 66 | _ = first; 67 | _ = second; 68 | _ = third; 69 | } 70 | }; 71 | 72 | try std.testing.expect(ComplexTypes.incompatibilities(BadImpl1).len > 0); 73 | try std.testing.expect(ComplexTypes.incompatibilities(BadImpl2).len > 0); 74 | try std.testing.expect(ComplexTypes.incompatibilities(BadImpl3).len > 0); 75 | } 76 | 77 | test "complex type support with embedding" { 78 | // Define all complex types we'll use 79 | const Config = struct { 80 | a: []const u8, 81 | b: ?i32, 82 | }; 83 | 84 | const Status = enum { a, b, c }; 85 | 86 | const DataPoint = struct { 87 | x: u32, 88 | y: ?[]const u8, 89 | }; 90 | 91 | const ProcessingMode = enum { ready, processing, unknown }; 92 | 93 | const HistoryEntry = struct { 94 | timestamp: i64, 95 | data: ?[]const DataPoint, 96 | status: Status, 97 | }; 98 | 99 | const ProcessingResult = struct { 100 | result: []const DataPoint, 101 | status: Status, 102 | }; 103 | 104 | const ProcessingInput = struct { 105 | config: Config, 106 | points: []const DataPoint, 107 | }; 108 | 109 | // Base interfaces with complex types 110 | const Configurable = Interface(.{ 111 | .configure = fn (anytype, Config) anyerror!void, 112 | .getConfig = fn (anytype) Config, 113 | }, null); 114 | 115 | const StatusProvider = Interface(.{ 116 | .getStatus = fn (anytype) Status, 117 | .setStatus = fn (anytype, Status) anyerror!void, 118 | }, null); 119 | 120 | const DataHandler = Interface(.{ 121 | .processData = fn (anytype, []const DataPoint) anyerror!void, 122 | .getLastPoint = fn (anytype) ?DataPoint, 123 | }, null); 124 | 125 | // Complex interface that embeds all the above and adds its own complex methods 126 | const ComplexTypes = Interface(.{ 127 | .complexMethod = fn (anytype, Config, Status, []const DataPoint) anyerror!void, 128 | .superComplex = fn (anytype, ProcessingInput, ProcessingMode, []const HistoryEntry) anyerror!?ProcessingResult, 129 | }, .{ Configurable, StatusProvider, DataHandler }); 130 | 131 | // Correct implementation 132 | const GoodImpl = struct { 133 | current_config: Config = .{ .a = "", .b = null }, 134 | current_status: Status = .a, 135 | last_point: ?DataPoint = null, 136 | 137 | // Configurable implementation 138 | pub fn configure(self: *@This(), cfg: Config) !void { 139 | self.current_config = cfg; 140 | } 141 | 142 | pub fn getConfig(self: @This()) Config { 143 | return self.current_config; 144 | } 145 | 146 | // StatusProvider implementation 147 | pub fn getStatus(self: @This()) Status { 148 | return self.current_status; 149 | } 150 | 151 | pub fn setStatus(self: *@This(), status: Status) !void { 152 | self.current_status = status; 153 | } 154 | 155 | // DataHandler implementation 156 | pub fn processData(self: *@This(), points: []const DataPoint) !void { 157 | if (points.len > 0) { 158 | self.last_point = points[points.len - 1]; 159 | } 160 | } 161 | 162 | pub fn getLastPoint(self: @This()) ?DataPoint { 163 | return self.last_point; 164 | } 165 | 166 | // ComplexTypes implementation 167 | pub fn complexMethod( 168 | self: *@This(), 169 | config: Config, 170 | status: Status, 171 | points: []const DataPoint, 172 | ) !void { 173 | try self.configure(config); 174 | try self.setStatus(status); 175 | try self.processData(points); 176 | } 177 | 178 | pub fn superComplex( 179 | self: *@This(), 180 | input: ProcessingInput, 181 | mode: ProcessingMode, 182 | history: []const HistoryEntry, 183 | ) !?ProcessingResult { 184 | _ = self; 185 | _ = input; 186 | _ = mode; 187 | _ = history; 188 | return null; 189 | } 190 | }; 191 | 192 | // Should compile without error 193 | comptime ComplexTypes.satisfiedBy(GoodImpl); 194 | comptime Configurable.satisfiedBy(GoodImpl); 195 | comptime StatusProvider.satisfiedBy(GoodImpl); 196 | comptime DataHandler.satisfiedBy(GoodImpl); 197 | 198 | // Bad implementation - missing embedded interface methods 199 | const BadImpl1 = struct { 200 | pub fn complexMethod( 201 | self: *@This(), 202 | config: Config, 203 | status: Status, 204 | points: []const DataPoint, 205 | ) !void { 206 | _ = self; 207 | _ = config; 208 | _ = status; 209 | _ = points; 210 | } 211 | 212 | pub fn superComplex( 213 | self: *@This(), 214 | input: ProcessingInput, 215 | mode: ProcessingMode, 216 | history: []const HistoryEntry, 217 | ) !?ProcessingResult { 218 | _ = self; 219 | _ = input; 220 | _ = mode; 221 | _ = history; 222 | return null; 223 | } 224 | }; 225 | 226 | // Bad implementation - wrong embedded interface method signature 227 | const BadImpl2 = struct { 228 | pub fn configure(self: *@This(), cfg: Config) !void { 229 | _ = self; 230 | _ = cfg; 231 | } 232 | 233 | pub fn getConfig(self: @This()) ?Config { // Wrong return type 234 | _ = self; 235 | return null; 236 | } 237 | 238 | pub fn getStatus(self: @This()) Status { 239 | _ = self; 240 | return .a; 241 | } 242 | 243 | pub fn setStatus(self: *@This(), status: Status) !void { 244 | _ = self; 245 | _ = status; 246 | } 247 | 248 | pub fn processData(self: *@This(), points: []DataPoint) !void { // Missing const 249 | _ = self; 250 | _ = points; 251 | } 252 | 253 | pub fn getLastPoint(self: @This()) ?DataPoint { 254 | _ = self; 255 | return null; 256 | } 257 | 258 | pub fn complexMethod( 259 | self: *@This(), 260 | config: Config, 261 | status: Status, 262 | points: []const DataPoint, 263 | ) !void { 264 | _ = self; 265 | _ = config; 266 | _ = status; 267 | _ = points; 268 | } 269 | 270 | pub fn superComplex( 271 | self: *@This(), 272 | input: ProcessingInput, 273 | mode: ProcessingMode, 274 | history: []const HistoryEntry, 275 | ) !?ProcessingResult { 276 | _ = self; 277 | _ = input; 278 | _ = mode; 279 | _ = history; 280 | return null; 281 | } 282 | }; 283 | 284 | // Test that bad implementations are caught 285 | try std.testing.expect(ComplexTypes.incompatibilities(BadImpl1).len > 0); 286 | try std.testing.expect(ComplexTypes.incompatibilities(BadImpl2).len > 0); 287 | } 288 | -------------------------------------------------------------------------------- /test/embedded.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Interface = @import("interface").Interface; 3 | 4 | const User = struct { 5 | id: u32, 6 | name: []const u8, 7 | email: []const u8, 8 | }; 9 | 10 | test "interface embedding" { 11 | // Base interfaces 12 | const Logger = Interface(.{ 13 | .log = fn (anytype, []const u8) void, 14 | .getLogLevel = fn (anytype) u8, 15 | }, null); 16 | 17 | const Metrics = Interface(.{ 18 | .increment = fn (anytype, []const u8) void, 19 | .getValue = fn (anytype, []const u8) u64, 20 | }, .{Logger}); 21 | 22 | // Complex interface that embeds both Logger and Metrics 23 | const MonitoredRepository = Interface(.{ 24 | .create = fn (anytype, User) anyerror!u32, 25 | .findById = fn (anytype, u32) anyerror!?User, 26 | .update = fn (anytype, User) anyerror!void, 27 | .delete = fn (anytype, u32) anyerror!void, 28 | }, .{Metrics}); 29 | 30 | // Implementation that satisfies all interfaces 31 | const TrackedRepository = struct { 32 | allocator: std.mem.Allocator, 33 | users: std.AutoHashMap(u32, User), 34 | next_id: u32, 35 | log_level: u8, 36 | metrics: std.StringHashMap(u64), 37 | 38 | const Self = @This(); 39 | 40 | pub fn init(allocator: std.mem.Allocator) !Self { 41 | return .{ 42 | .allocator = allocator, 43 | .users = std.AutoHashMap(u32, User).init(allocator), 44 | .next_id = 1, 45 | .log_level = 0, 46 | .metrics = std.StringHashMap(u64).init(allocator), 47 | }; 48 | } 49 | 50 | pub fn deinit(self: *Self) void { 51 | self.metrics.deinit(); 52 | self.users.deinit(); 53 | } 54 | 55 | // Logger interface implementation 56 | pub fn log(self: Self, message: []const u8) void { 57 | _ = self; 58 | _ = message; 59 | // In real code: actual logging 60 | } 61 | 62 | pub fn getLogLevel(self: Self) u8 { 63 | return self.log_level; 64 | } 65 | 66 | // Metrics interface implementation 67 | pub fn increment(self: *Self, key: []const u8) void { 68 | if (self.metrics.get(key)) |value| { 69 | self.metrics.put(key, value + 1) catch {}; 70 | } else { 71 | self.metrics.put(key, 1) catch {}; 72 | } 73 | } 74 | 75 | pub fn getValue(self: Self, key: []const u8) u64 { 76 | return self.metrics.get(key) orelse 0; 77 | } 78 | 79 | // Repository interface implementation 80 | pub fn create(self: *Self, user: User) !u32 { 81 | self.log("Creating new user"); 82 | self.increment("users.created"); 83 | 84 | var new_user = user; 85 | new_user.id = self.next_id; 86 | try self.users.put(self.next_id, new_user); 87 | self.next_id += 1; 88 | return new_user.id; 89 | } 90 | 91 | pub fn findById(self: *Self, id: u32) !?User { 92 | self.increment("users.lookup"); 93 | return self.users.get(id); 94 | } 95 | 96 | pub fn update(self: *Self, user: User) !void { 97 | self.log("Updating user"); 98 | self.increment("users.updated"); 99 | 100 | if (!self.users.contains(user.id)) { 101 | return error.UserNotFound; 102 | } 103 | try self.users.put(user.id, user); 104 | } 105 | 106 | pub fn delete(self: *Self, id: u32) !void { 107 | self.log("Deleting user"); 108 | self.increment("users.deleted"); 109 | 110 | if (!self.users.remove(id)) { 111 | return error.UserNotFound; 112 | } 113 | } 114 | }; 115 | 116 | // Test that our implementation satisfies all interfaces 117 | comptime MonitoredRepository.satisfiedBy(TrackedRepository); 118 | comptime Logger.satisfiedBy(TrackedRepository); 119 | comptime Metrics.satisfiedBy(TrackedRepository); 120 | 121 | // Test the actual implementation 122 | var repo = try TrackedRepository.init(std.testing.allocator); 123 | defer repo.deinit(); 124 | 125 | // Create a user and verify metrics 126 | const user = User{ .id = 0, .name = "Test User", .email = "test@example.com" }; 127 | const id = try repo.create(user); 128 | try std.testing.expectEqual(@as(u64, 1), repo.getValue("users.created")); 129 | 130 | // Look up the user and verify metrics 131 | const found = try repo.findById(id); 132 | try std.testing.expect(found != null); 133 | try std.testing.expectEqual(@as(u64, 1), repo.getValue("users.lookup")); 134 | 135 | // Test logging level 136 | try std.testing.expectEqual(@as(u8, 0), repo.getLogLevel()); 137 | } 138 | 139 | test "interface embedding with conflicts" { 140 | // Two interfaces with conflicting method names 141 | const BasicLogger = Interface(.{ 142 | .log = fn (anytype, []const u8) void, 143 | }, null); 144 | 145 | const MetricLogger = Interface(.{ 146 | .log = fn (anytype, []const u8, u64) void, 147 | }, null); 148 | 149 | // This should fail to compile due to conflicting 'log' methods 150 | const ConflictingLogger = Interface(.{ 151 | .write = fn (anytype, []const u8) void, 152 | }, .{ BasicLogger, MetricLogger }); 153 | 154 | // Implementation that tries to satisfy both 155 | const BadImplementation = struct { 156 | pub fn write(self: @This(), message: []const u8) void { 157 | _ = self; 158 | _ = message; 159 | } 160 | 161 | pub fn log(self: @This(), message: []const u8) void { 162 | _ = self; 163 | _ = message; 164 | } 165 | }; 166 | 167 | // This should fail compilation with an ambiguous method error 168 | comptime { 169 | if (ConflictingLogger.incompatibilities(BadImplementation).len == 0) { 170 | @compileError("Should have detected conflicting 'log' methods"); 171 | } 172 | } 173 | } 174 | 175 | test "nested interface embedding" { 176 | // Base interface 177 | const Closer = Interface(.{ 178 | .close = fn (anytype) void, 179 | }, null); 180 | 181 | // Mid-level interface that embeds Closer 182 | const Writer = Interface(.{ 183 | .write = fn (anytype, []const u8) anyerror!void, 184 | }, .{Closer}); 185 | 186 | // Top-level interface that embeds Writer 187 | const FileWriter = Interface(.{ 188 | .flush = fn (anytype) anyerror!void, 189 | }, .{Writer}); 190 | 191 | // Implementation that satisfies all interfaces 192 | const Implementation = struct { 193 | pub fn close(self: @This()) void { 194 | _ = self; 195 | } 196 | 197 | pub fn write(self: @This(), data: []const u8) !void { 198 | _ = self; 199 | _ = data; 200 | } 201 | 202 | pub fn flush(self: @This()) !void { 203 | _ = self; 204 | } 205 | }; 206 | 207 | // Should satisfy all interfaces 208 | comptime FileWriter.satisfiedBy(Implementation); 209 | comptime Writer.satisfiedBy(Implementation); 210 | comptime Closer.satisfiedBy(Implementation); 211 | } 212 | -------------------------------------------------------------------------------- /test/simple.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Interface = @import("interface").Interface; 3 | 4 | // First define our data type 5 | const User = struct { 6 | id: u32, 7 | name: []const u8, 8 | email: []const u8, 9 | }; 10 | 11 | // Define our Repository interface with multiple methods 12 | // Note the anytype to indicate pointer methods 13 | const Repository = Interface(.{ 14 | .create = fn (anytype, User) anyerror!u32, 15 | .findById = fn (anytype, u32) anyerror!?User, 16 | .update = fn (anytype, User) anyerror!void, 17 | .delete = fn (anytype, u32) anyerror!void, 18 | .findByEmail = fn (anytype, []const u8) anyerror!?User, 19 | }, null); 20 | 21 | // Implement a simple in-memory repository 22 | pub const InMemoryRepository = struct { 23 | allocator: std.mem.Allocator, 24 | users: std.AutoHashMap(u32, User), 25 | next_id: u32, 26 | 27 | pub fn init(allocator: std.mem.Allocator) InMemoryRepository { 28 | return .{ 29 | .allocator = allocator, 30 | .users = std.AutoHashMap(u32, User).init(allocator), 31 | .next_id = 1, 32 | }; 33 | } 34 | 35 | pub fn deinit(self: *InMemoryRepository) void { 36 | self.users.deinit(); 37 | } 38 | 39 | // Repository implementation methods 40 | pub fn create(self: *InMemoryRepository, user: User) !u32 { 41 | var new_user = user; 42 | new_user.id = self.next_id; 43 | try self.users.put(self.next_id, new_user); 44 | self.next_id += 1; 45 | return new_user.id; 46 | } 47 | 48 | pub fn findById(self: InMemoryRepository, id: u32) !?User { 49 | return self.users.get(id); 50 | } 51 | 52 | pub fn update(self: *InMemoryRepository, user: User) !void { 53 | if (!self.users.contains(user.id)) { 54 | return error.UserNotFound; 55 | } 56 | try self.users.put(user.id, user); 57 | } 58 | 59 | pub fn delete(self: *InMemoryRepository, id: u32) !void { 60 | if (!self.users.remove(id)) { 61 | return error.UserNotFound; 62 | } 63 | } 64 | 65 | pub fn findByEmail(self: InMemoryRepository, email: []const u8) !?User { 66 | var it = self.users.valueIterator(); 67 | while (it.next()) |user| { 68 | if (std.mem.eql(u8, user.email, email)) { 69 | return user.*; 70 | } 71 | } 72 | return null; 73 | } 74 | }; 75 | 76 | // Function that works with any Repository implementation 77 | fn createUser(repo: anytype, name: []const u8, email: []const u8) !User { 78 | comptime Repository.satisfiedBy(@TypeOf(repo.*)); // Required to be called by function author 79 | 80 | const user = User{ 81 | .id = 0, 82 | .name = name, 83 | .email = email, 84 | }; 85 | 86 | const id = try repo.create(user); 87 | return User{ 88 | .id = id, 89 | .name = name, 90 | .email = email, 91 | }; 92 | } 93 | 94 | test "repository interface" { 95 | var repo = InMemoryRepository.init(std.testing.allocator); 96 | defer repo.deinit(); 97 | 98 | // Verify at comptime that our implementation satisfies the interface 99 | comptime Repository.satisfiedBy(@TypeOf(repo)); // Required to be called by function author 100 | // or, can pass the concrete struct type directly: 101 | comptime Repository.satisfiedBy(InMemoryRepository); 102 | 103 | // Test create and findById 104 | const user1 = try createUser(&repo, "John Doe", "john@example.com"); 105 | const found = try repo.findById(user1.id); 106 | try std.testing.expect(found != null); 107 | try std.testing.expectEqualStrings("John Doe", found.?.name); 108 | 109 | // Test findByEmail 110 | const by_email = try repo.findByEmail("john@example.com"); 111 | try std.testing.expect(by_email != null); 112 | try std.testing.expectEqual(user1.id, by_email.?.id); 113 | 114 | // Test update 115 | var updated_user = user1; 116 | updated_user.name = "Johnny Doe"; 117 | try repo.update(updated_user); 118 | const found_updated = try repo.findById(user1.id); 119 | try std.testing.expect(found_updated != null); 120 | try std.testing.expectEqualStrings("Johnny Doe", found_updated.?.name); 121 | 122 | // Test delete 123 | try repo.delete(user1.id); 124 | const not_found = try repo.findById(user1.id); 125 | try std.testing.expect(not_found == null); 126 | 127 | // Test error cases 128 | try std.testing.expectError(error.UserNotFound, repo.update(User{ 129 | .id = 999, 130 | .name = "Not Found", 131 | .email = "none@example.com", 132 | })); 133 | try std.testing.expectError(error.UserNotFound, repo.delete(999)); 134 | } 135 | --------------------------------------------------------------------------------