Skip to content

Commit c6ca205

Browse files
committed
feat: initial interface library
0 parents  commit c6ca205

File tree

10 files changed

+1476
-0
lines changed

10 files changed

+1476
-0
lines changed

.github/workflows/ci.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
16+
steps:
17+
- uses: actions/checkout@v3
18+
19+
- name: Install Zig
20+
uses: goto-bus-stop/setup-zig@v2
21+
with:
22+
version: 0.13.0
23+
24+
- name: Check Zig Version
25+
run: zig version
26+
27+
- name: Run tests
28+
run: zig build test

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
zig-cache
2+
zig-out

LICENSE

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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.

README.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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+
```
20+
TODO
21+
```
22+
23+
## Usage
24+
25+
1. Define an interface with required method signatures:
26+
27+
```zig
28+
const Repository = Interface(.{
29+
.create = fn(anytype, User) anyerror!u32,
30+
.findById = fn(anytype, u32) anyerror!?User,
31+
.update = fn(anytype, User) anyerror!void,
32+
.delete = fn(anytype, u32) anyerror!void,
33+
}, null);
34+
```
35+
36+
2. Implement the interface methods in your type:
37+
38+
```zig
39+
const InMemoryRepository = struct {
40+
allocator: std.mem.Allocator,
41+
users: std.AutoHashMap(u32, User),
42+
next_id: u32,
43+
44+
pub fn create(self: *InMemoryRepository, user: User) !u32 {
45+
var new_user = user;
46+
new_user.id = self.next_id;
47+
try self.users.put(self.next_id, new_user);
48+
self.next_id += 1;
49+
return new_user.id;
50+
}
51+
52+
// ... other Repository methods
53+
};
54+
```
55+
56+
3. Verify the implementation at compile time:
57+
58+
```zig
59+
// In functions that accept interface implementations:
60+
fn createUser(repo: anytype, name: []const u8, email: []const u8) !User {
61+
comptime Repository.satisfiedBy(@TypeOf(repo));
62+
// ... rest of implementation
63+
}
64+
65+
// Or verify directly:
66+
comptime Repository.satisfiedBy(InMemoryRepository);
67+
```
68+
69+
## Interface Embedding
70+
71+
Interfaces can embed other interfaces to combine their requirements:
72+
73+
```zig
74+
const Logger = Interface(.{
75+
.log = fn(anytype, []const u8) void,
76+
.getLogLevel = fn(anytype) u8,
77+
}, null);
78+
79+
const Metrics = Interface(.{
80+
.increment = fn(anytype, []const u8) void,
81+
.getValue = fn(anytype, []const u8) u64,
82+
}, .{ Logger }); // Embeds Logger interface
83+
84+
// Now implements both Metrics and Logger methods
85+
const MonitoredRepository = Interface(.{
86+
.create = fn(anytype, User) anyerror!u32,
87+
.findById = fn(anytype, u32) anyerror!?User,
88+
}, .{ Metrics });
89+
```
90+
91+
> Note: you can embed arbitrarily many interfaces!
92+
93+
## Error Reporting
94+
95+
The library provides detailed compile-time errors when implementations don't
96+
match:
97+
98+
```zig
99+
// Wrong parameter type ([]u8 vs []const u8)
100+
const BadImpl = struct {
101+
pub fn writeAll(self: @This(), data: []u8) !void {
102+
_ = self;
103+
_ = data;
104+
}
105+
};
106+
107+
// Results in compile error:
108+
// error: Method 'writeAll' parameter 1 has incorrect type:
109+
// └─ Expected: []const u8
110+
// └─ Got: []u8
111+
// └─ Hint: Consider making the parameter type const
112+
```
113+
114+
## Complex Types
115+
116+
The interface checker supports complex types including:
117+
118+
```zig
119+
const ComplexTypes = Interface(.{
120+
.process = fn(
121+
anytype,
122+
struct { config: Config, points: []const DataPoint },
123+
enum { ready, processing, error },
124+
[]const struct {
125+
timestamp: i64,
126+
data: ?[]const DataPoint,
127+
status: Status,
128+
}
129+
) anyerror!?ProcessingResult,
130+
}, null);
131+
```

build.zig

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
}

build.zig.zon

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
.{
2+
.name = "interface",
3+
// This is a [Semantic Version](https://semver.org/).
4+
// In a future version of Zig it will be used for package deduplication.
5+
.version = "0.0.1",
6+
7+
// This field is optional.
8+
// This is currently advisory only; Zig does not yet do anything
9+
// with this value.
10+
//.minimum_zig_version = "0.11.0",
11+
12+
// This field is optional.
13+
// Each dependency must either provide a `url` and `hash`, or a `path`.
14+
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
15+
// Once all dependencies are fetched, `zig build` no longer requires
16+
// internet connectivity.
17+
.dependencies = .{
18+
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
19+
//.example = .{
20+
// // When updating this field to a new URL, be sure to delete the corresponding
21+
// // `hash`, otherwise you are communicating that you expect to find the old hash at
22+
// // the new URL.
23+
// .url = "https://example.com/foo.tar.gz",
24+
//
25+
// // This is computed from the file contents of the directory of files that is
26+
// // obtained after fetching `url` and applying the inclusion rules given by
27+
// // `paths`.
28+
// //
29+
// // This field is the source of truth; packages do not come from a `url`; they
30+
// // come from a `hash`. `url` is just one of many possible mirrors for how to
31+
// // obtain a package matching this `hash`.
32+
// //
33+
// // Uses the [multihash](https://multiformats.io/multihash/) format.
34+
// .hash = "...",
35+
//
36+
// // When this is provided, the package is found in a directory relative to the
37+
// // build root. In this case the package's hash is irrelevant and therefore not
38+
// // computed. This field and `url` are mutually exclusive.
39+
// .path = "foo",
40+
41+
// // When this is set to `true`, a package is declared to be lazily
42+
// // fetched. This makes the dependency only get fetched if it is
43+
// // actually used.
44+
// .lazy = false,
45+
//},
46+
},
47+
48+
// Specifies the set of files and directories that are included in this package.
49+
// Only files and directories listed here are included in the `hash` that
50+
// is computed for this package.
51+
// Paths are relative to the build root. Use the empty string (`""`) to refer to
52+
// the build root itself.
53+
// A directory listed here means that all files within, recursively, are included.
54+
.paths = .{
55+
// This makes *all* files, recursively, included in this package. It is generally
56+
// better to explicitly list the files and directories instead, to insure that
57+
// fetching from tarballs, file system paths, and version control all result
58+
// in the same contents hash.
59+
"",
60+
// For example...
61+
//"build.zig",
62+
//"build.zig.zon",
63+
//"src",
64+
//"LICENSE",
65+
//"README.md",
66+
},
67+
}

0 commit comments

Comments
 (0)