A compile-time interface checker for Zig that enables interface-based design with comprehensive type checking and detailed error reporting.
This library provides a way to define and verify interfaces in Zig at compile time. It supports:
- Type-safe interface definitions with detailed error reporting
- Interface embedding (composition)
- Complex type validation including structs, enums, arrays, and slices
- Comprehensive compile-time error messages with helpful hints
- Flexible error union compatibility with
anyerror
Add or update this library as a dependency in your zig project run the following command:
zig fetch --save git+https://github.com/nilslice/zig-interface
Afterwards add the library as a dependency to any module in your build.zig:
// ...
const interface_dependency = b.dependency("interface", .{
.target = target,
.optimize = optimize,
});
const exe = b.addExecutable(.{
.name = "main",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
// import the exposed `interface` module from the dependency
exe.root_module.addImport("interface", interface_dependency.module("interface"));
// ...
In the end you can import the interface
module. For example:
const Interface = @import("interface").Interface;
const Repository = Interface(.{
.create = fn(anytype, User) anyerror!u32,
.findById = fn(anytype, u32) anyerror!?User,
.update = fn(anytype, User) anyerror!void,
.delete = fn(anytype, u32) anyerror!void,
}, null);
- Define an interface with required method signatures:
const Repository = Interface(.{
.create = fn(anytype, User) anyerror!u32,
.findById = fn(anytype, u32) anyerror!?User,
.update = fn(anytype, User) anyerror!void,
.delete = fn(anytype, u32) anyerror!void,
}, null);
- Implement the interface methods in your type:
const InMemoryRepository = struct {
allocator: std.mem.Allocator,
users: std.AutoHashMap(u32, User),
next_id: u32,
pub fn create(self: *InMemoryRepository, user: User) !u32 {
var new_user = user;
new_user.id = self.next_id;
try self.users.put(self.next_id, new_user);
self.next_id += 1;
return new_user.id;
}
// ... other Repository methods
};
- Verify the implementation at compile time:
// In functions that accept interface implementations:
fn createUser(repo: anytype, name: []const u8, email: []const u8) !User {
comptime Repository.satisfiedBy(@TypeOf(repo));
// ... rest of implementation
}
// Or verify directly:
comptime Repository.satisfiedBy(InMemoryRepository);
Interfaces can embed other interfaces to combine their requirements:
const Logger = Interface(.{
.log = fn(anytype, []const u8) void,
.getLogLevel = fn(anytype) u8,
}, null);
const Metrics = Interface(.{
.increment = fn(anytype, []const u8) void,
.getValue = fn(anytype, []const u8) u64,
}, .{ Logger }); // Embeds Logger interface
// Now implements both Metrics and Logger methods
const MonitoredRepository = Interface(.{
.create = fn(anytype, User) anyerror!u32,
.findById = fn(anytype, u32) anyerror!?User,
}, .{ Metrics });
Note: you can embed arbitrarily many interfaces!
The library provides detailed compile-time errors when implementations don't match:
// Wrong parameter type ([]u8 vs []const u8)
const BadImpl = struct {
pub fn writeAll(self: @This(), data: []u8) !void {
_ = self;
_ = data;
}
};
// Results in compile error:
// error: Method 'writeAll' parameter 1 has incorrect type:
// └─ Expected: []const u8
// └─ Got: []u8
// └─ Hint: Consider making the parameter type const
The interface checker supports complex types including:
const ComplexTypes = Interface(.{
.process = fn(
anytype,
struct { config: Config, points: []const DataPoint },
enum { ready, processing, error },
[]const struct {
timestamp: i64,
data: ?[]const DataPoint,
status: Status,
}
) anyerror!?ProcessingResult,
}, null);