Repository navigation
Releases: vkiryakov/zefir
Release list
v0.11.0
Zefir 0.11.0 adds three modules: zefir-odm, a typed ODM for MongoDB for services with many tenants; zefir-mongo-bson, the BSON of zefir-mongo as a module of its own; and zefir-openapi, which builds the OpenAPI document of a zefir-http server from the types the server runs on. zefir-jwt becomes the first part of zefir-security, as security.jwt.
odm
zefir-odm maps the documents of MongoDB to plain Zig structs. A filter, an update or a sort is a literal that the compiler checks against the model, and what a query finds comes back as that struct. One registry keeps the connections and the tenants of a service:
// A session on the tenant's database, for this task, and the
// collection of User in it.
var acme = try registry.tenant(io, "acme");
defer acme.deinit(io);
const users = acme.collection(User);
// A customer; odm makes the id, and says which.
const ann = try users.insertOne(io, ctx, .{ .email = "ann@example.com", .name = "Ann", .age = 30 });
// The adults, the oldest first. The cursor brings them batch by batch,
// each one decoded into a User in the arena.
var adults = try users.find(io, ctx, .{ .age = .{ .gte = 18 } }, .{ .sort = .{ .age = .desc } });
defer adults.deinit(io);
while (try adults.next(io, ctx, arena)) |user| app.info("Adult", .{ .name = user.name, .age = user.age });- Models are plain structs. No base type and no generated code: the collection, the
_idfield, the BSON names and the indexes are a typedodm_schemain the struct, and the rules are the validator'sfield_rules. - Queries that the compiler checks. A field that is not there, an operator that does not fit its type, a value of the wrong type, or an update that would leave a document the model cannot read stops the build, with a message that says what to write instead.
odm.Filterandodm.Updatebuild conditions at run time. - Tenants, shared and on servers of their own, at once.
odm.Registrybinds each tenant to a connection, one server and one login, and a database of its own. Tenants come and go by events with versions while the service runs. - Credentials that change on the fly. A connection moves to a new login: new sessions go to the new generation, and the old one drains by leases, with no background tasks. No request fails.
- Errors that say whether to retry. Each says whether the server may have run the call; the details are in the diagnostics of the session.
- Shutdown within a deadline,
stats(), and logs through zefir-log. The registry is a bean ofzefir.App. - Any driver. odm has no network code: it reaches a database only through its driver contract,
odm.Driver. zefir-mongo'smongo.OdmDriveris the reference driver and passes the conformance suite,odm.testing.conformance, on mongod 9.0. - Tests without MongoDB.
odm.testing.FakeDriveranswers what a test tells it to, and keeps a journal of what odm sent.
Not in this version yet, as the list at the top of the guide says, among others: TLS and mongodb+srv://, replica sets and sharded clusters (zefir-mongo connects to one standalone mongod, so keep the servers of tenants in a network you trust), transactions and change streams, and a typed language of aggregations (aggregateRaw takes BSON as it is).
The end-to-end tests in tests/odm run against mongod 9.0 in CI: the conformance suite, tenants, credential rotation, documents and the lifecycle, with stress tests besides.
See the odm guide: a quick start, models, queries, tenants, rotation, errors, zefir.App, tests and a chapter for authors of drivers, every program run against mongod and its output shown as it printed.
mongo-bson
The BSON of zefir-mongo moves into a module of its own, zefir-mongo-bson (zefir.mongo_bson), which imports nothing and is shared by zefir-mongo and zefir-odm: Zig values to BSON and back, raw documents, Extended JSON and the test vectors. New in it: bson.encodeKeyed, bson.decodeKeyed and bson.FieldKey, for fields whose BSON names differ from their Zig names, and bson.codec.
openapi
zefir-openapi builds the OpenAPI document of a zefir-http server when the service compiles, 3.1.2 by default and 3.2.1 on request, from the same types the server runs on: the routes of the controllers and what they declare, what the handlers return, the DTOs with their field_rules, the errors of each route as mapError answers them, and the middleware of each route. What the compiler cannot see, the texts, each type declares next to its code:
// A route declares what it reads, and the handler reads exactly that.
const Get = http.get("/{id}", .{ .path = dto.TodoIdPath });
pub const openapi: zefir.openapi.ControllerDoc(@This()) = .{
.tag = "Todos",
.routes = .{ .get = .{ .summary = "Get a todo" } },
};
pub fn get(self: *TodosController, ctx: *http.Context) !http.Json(dto.TodoResponse) {
const in = try ctx.input(Get); // in.path is a dto.TodoIdPath, as Get declares
const todo = self.repo.find(in.path.id) orelse return error.TodoNotFound;
return .{ .value = .from(todo) };
}
// Where the server is built: the document comes from the type of the same parts.
s.docs = try .init(gpa, comptime zefir.openapi.document(@TypeOf(parts), .{
.info = .{ .title = "Todos API", .version = "1.0.0" },
.prefix = prefix,
}));- The document cannot drift from the server. It cannot list a route that is not there, leave out an error that a route returns, or forget a rule of a field. A route it cannot describe, or a text that does not fit, stops the build.
- Typed texts.
zefir.openapi.Doc(T),ControllerDoc(C)andMiddlewareDocare made from the types themselves, so a field that is not there, or an example of the wrong type, is an ordinary error of Zig at the line of your code. - Serving and keeping it. The controller
zefir.openapi.Docsserves/openapi.jsonwith its ETag and a Scalar page at/docs.zefir.openapi.testing.expectFilekeepsopenapi.jsonin the repository up to date, andzig build openapirewrites it. - The same program gives the same bytes, so the file changes only when the API does.
What it needs from the modules it stands on is new in them too:
- zefir-validator:
v.schemaOf(rule, T)gives each of the 96 kinds of rule as JSON Schema keywords, with its accuracy and a sentence for the description; a new kind of rule without a mapping does not build. - zefir-http: a route declares its input (
.path,.query,.headers,.cookies,.bodyor.form) and its statuses (.responses), and a path and its DTO are checked against each other at compile time.ctx.input(Route)reads all a route declares in one call, server-sent events are typed, andhttp.describe(Parts)describes a server at compile time. In a Debug build, the server checks each input call and each response of a route of a documented controller against what the route declares. A route without the new options works as before.
The document is checked outside Zig as well. In tests/openapi, the regular expressions of v.schemaOf run against the validator in ajv (ECMA-262) and Go (RE2), on 89 357 vectors; every golden document and the documents of both guides are checked against the official JSON Schema of OAS 3.1 and 3.2; and the bodies the golden applications read and write are checked against their schemas. In tests/http, Schemathesis runs against the live service of the http guide and its document.
See the openapi guide, and the chapter on the document in the http guide.
security
JWT is one part of security, so zefir-jwt is no longer a module of its own: it is the first part of the planned module zefir-security, and lives there as security.jwt. The code of JWT is the same as in v0.10.0; only the imports change, see the breaking changes below. The guide moved to src/security/examples/README.md.
Also in this release
- zefir-validator has
v.closest. - The README's module table has one row for zefir-security, partial since it holds only JWT for now, and shows zefir-scheduler as in progress.
zig fetch --save git+https://github.com/vkiryakov/zefir#v0.11.0const zefir = b.dependency("zefir", .{ .target = target, .optimize = optimize });
// in your module's .imports:
.{ .name = "odm", .module = zefir.module("zefir-odm") },
.{ .name = "mongo", .module = zefir.module("zefir-mongo") }, // mongo.OdmDriver
.{ .name = "mongo-bson", .module = zefir.module("zefir-mongo-bson") },
.{ .name = "security", .module = zefir.module("zefir-security") }, // security.jwt
.{ .name = "zefir", .module = zefir.module("zefir") }, // zefir.openapiA service with odm, and one that serves its document, also needs zio; the Installation sections of the odm guide and the openapi guide show how to add it.
Breaking changes
security
- The module
zefir-jwtis nowzefir-security, and JWT lives in it assecurity.jwt:zefir.module("zefir-jwt")→zefir.module("zefir-security");@import("jwt")→@import("security").jwt;@import("zefir").jwt→@import("zefir").security.jwt;- the build step
zig build test-jwt→zig build test-security.
mongo and mongo-bson
- The BSON of zefir-mongo is a module of its own,
zefir-mongo-bson:mongo.bson.Xis now@import("mongo-bson").X, or@import("zefir").mongo_bson.X. Add.{ .name = "mongo-bson", .module = zefir.module("zefir-mongo-bson") }to the.importsof your module.mongo.testing.vectorsisbson.testing.vectors. - The compile errors of the BSON codec name the types of the new module:
types.DbPointer, notbson.types.DbPointer. zefir-mongoimpor...
v0.10.0
Zefir 0.10.0 adds zefir.App, a container that builds a service out of its modules and runs the lifecycle of its servers and clients, and two modules: zefir-jwt, which signs and verifies the tokens of zefir services, and zefir-redis, a Redis and Valkey client for one standalone server.
App
zefir.App(Root), in zefir-core, builds a service out of modules, structs that declare what they are made of. App builds every part once and gives each the parts it asks for by their types. It starts what has to start, watches the servers, and stops everything within one deadline. The whole graph is checked when the service compiles:
pub const AppModule = struct {
pub const module: zefir.app.Module = .{
.imports = &.{ users.UsersModule, orders.OrdersModule },
.providers = &.{ShopErrors},
.factories = &.{ Bus, Servers },
.controllers = &.{HealthController},
// Ready objects from main: App hands them out, and never starts or
// destroys them.
.values = &.{ *const Config, *const log.Logger },
};
// ...
};
// In main: App matches the ready objects by type, and builds the rest.
var app = try zefir.App(AppModule).init(init.gpa, io, .{ &config, &logger }, .{});
defer app.deinit();
// Starts every bean, waits until the stop is over, and returns; a signal
// calls app.requestStop().
try app.run();- The graph.
- Modules with providers, factories, controllers and handlers, exports and visibility, ready values from
main, and overrides for tests. - A dependency that nothing provides, a part that a module does not see, or a cycle is a compile error that names the module, the part and the fix.
- Modules with providers, factories, controllers and handlers, exports and visibility, ready values from
- The lifecycle.
startruns in the order the beans were built, and rolls back when one fails. App then watches the servers, and one that breaks stops the application.requestStop, from a signal or your code, begins a stop in two phases under one deadline,shutdown_timeout, 25 s by default. The servers stop taking work, then they all shut down together, then the clients one by one, the last built first.ShutdownTimeoutwhen the deadline runs out, andrunthen ends the process.wait,isReady,failureandStatus. App's events go tostd.log, in the scopezefir_app.
- The beans of zefir.
- The servers and clients of http, rpc, rpc-nats, nats and mongo are beans without wrappers, and
redis.Clientfits the same contract. - The servers of http and rpc, and
rpc.Client, take their parts and typedOptions, which ZLS completes. The servers and the clients of nats, mongo and redis stop withshutdown(io, deadline). nats.Clientgetsstart,waitandshutdown;mongo.Clientgetsstart,shutdownandstart_timeout.
- The servers and clients of http, rpc, rpc-nats, nats and mongo are beans without wrappers, and
Not in this version yet, as the list at the top of the guide says:
- a context of the application, such as one that cancels background work at the stop: a bean with work of its own ends it in its
shutdown; - a restart: an App runs once;
- a deadline of App's own for the start: each bean bounds its own
start; - lazy beans:
initbuilds every bean of every module the root reaches.
The end-to-end tests in tests/app run on zio, in CI on Linux, macOS and Windows: two servers in one App, stops under load, fail-fast, and signals and ShutdownTimeout in child processes.
See the App guide: a shop of three modules with an HTTP server and an rpc server, every file shown as it is and every run as it printed. The guides of http, rpc, rpc-nats, nats and mongo each have a section "Under zefir.App".
jwt
zefir-jwt signs and verifies the JSON Web Tokens that zefir services issue themselves: RFC 7519 tokens in the compact form of JWS, with HS256, HS384, HS512, ES256, ES384 and EdDSA. Keys come from the settings, and the claims of your application arrive in a struct of your own, checked by the rules of zefir-validator:
var key: jwt.SigningKey = try .fromPem(.EdDSA, config.auth.signing_key.expose(), .{ .id = config.auth.key_id });
defer key.deinit();
const signer: jwt.Signer = try .init(&key, .{ .issuer = "orders", .audiences = &.{"orders"} });
const keys = [_]jwt.Key{key.verificationKey()};
const verifier: jwt.Verifier = try .init(.{ .keys = &keys, .issuer = "orders", .audiences = &.{"orders"} });
// The token of user-7, a buyer, for 15 minutes.
const token = try signer.sign(Claims, io, gpa, &.{ .roles = &.{"buyer"} }, .{ .subject = "user-7" });
defer gpa.free(token);
// A route checks it; what it gives lives in the arena of the request.
const verified = try verifier.verify(Claims, io, arena, token, .{});- Keys.
- HMAC secrets, and EdDSA, ES256 and ES384 keys in PEM or DER. Each key is bound to one algorithm when it loads.
- A verifier holds several keys and picks one by
kid, so a service can change its key while its replicas switch one by one. SigningKey.generatemakes keys for tests.- zefir-log writes a key as
[REDACTED], anddeinitwipes it from memory.
- Claims.
- The signer sets
iss,aud,iatandexp, 15 minutes from now unless you choose another lifetime;subcomes with the call. - The verifier requires
exp,issandaudand checks the time with a leeway, 60 s by default. Then it fills your struct and checks itsfield_rules.
- The signer sets
- A refusal says why. Every reason has an error of its own, from
BadSignatureandExpiredtoWrongAudienceandClaimsRejected.jwt.Detailsnames the claim or the rule at fault, for your log, andjwt.describegives a short text for the answer to a client. - Safe by default.
- The algorithm comes from the key, never from the token, and
nonenever passes. - The key comes only from the verifier:
jku,x5u,jwkandx5cin a header are never read. - A header with
critis refused. - ES256 and ES384 signatures are written, and accepted, with a low s only.
- The algorithm comes from the key, never from the token, and
- A plain library. jwt has no network code, no tasks and no global state;
jwt.testing.FixedClocksets the time in tests. - HTTP and RPC. The guide builds both checks as code of the application: a middleware of zefir-http,
JwtAuth, and anrpc.Authenticator,JwtAuthenticator.
Not in this version yet, as the list at the top of the guide says:
- tokens of other issuers, such as Keycloak, Auth0 or Google. Even an ES256 or ES384 token that another library signed passes only with a low s;
- JWKS by URL, OIDC discovery and RSA;
- JWE, nested tokens, JWK and JWKS, and keys from X.509 certificates;
- ES512 and Ed448;
- revocation and replay protection: a token lives until it expires.
See the jwt guide: the orders and billing services of a shop, every program compiled and every run shown as it printed.
redis
redis.Client talks to one standalone server, Redis 7.2 or later or Valkey 8 or later, over RESP3, on std.Io. A command is a value, and its reply arrives as a Zig type:
var client: redis.Client = try .init(gpa, io, .{ .url = config.redis.url, .password = password });
defer client.deinit();
try client.connect(io, ctx, &diagnostics);
defer client.close(io);
// A session: its token names the customer, for half an hour.
try client.run(io, op, cmd.set("session:7f3a9c", "42", .{ .ttl = .fromSeconds(30 * 60) }), .{});
const user = try client.run(io, op, cmd.get([]const u8, "session:7f3a9c"), .{ .allocator = arena });
// The profile: a hash with a field for each field of Profile.
try client.run(io, op, cmd.hset("user:42", Profile{ .name = "Ann", .age = 30, .plan = .pro }), .{});
const profile = (try client.run(io, op, cmd.hgetall(Profile, "user:42"), .{ .allocator = arena })) orelse
return error.ProfileGone;- The client.
- A pool of connections that every task shares, first come first served. A connection that idled gets a PING before a call takes it.
- Deadlines and cancellation from
core.Context. A blocking command's timeout on the server comes from the deadline of the call. - A connection with an unread reply never goes back to the pool.
startandshutdownlet zefir.App start the client and stop it with the rest of a service.
- About 90 commands in
redis.cmd: strings, keys, hashes, lists, sets, sorted sets, streams with consumer groups, scripts and pub/sub. A hash reads into a struct of yours.cmd.rawsends any other command, except those that change the state of a connection. - Pipelines, transactions and scripts.
client.pipelinesends many commands in one exchange and gives a value or an error for each.conn.transactionruns MULTI/EXEC with WATCH, and givesnullwhen a watched key changed.cmd.evalScriptsends a Lua script by its SHA1, and its text after NOSCRIPT.
- Pub/sub.
client.subscribegives aredis.Subscriberon a connection of its own. - Errors say whether the command may have run.
redis.outcome(err)tells which failed calls are safe to make again.Diagnosticskeeps the server's code and text.- There are no automatic retries; the guide says which errors are worth retrying.
redis.testing:FakeServer, a scripted server on loopback, to test an application without Redis.
Not in this version yet, as the list at the top of the guide says:
- Sentinel and Cluster: a client refuses a node of a cluster or a Sentinel when it connects;
- TLS, so it needs a trusted network. Tokens that expire, as the clouds' IAM gives them, need TLS too;
- servers older than Redis 7.2 or Valkey 8;
- functions (
FUNCTION,FCALL), the commands of modules such as JSON and Search, and caching on the client (CLIENT TRACKING).
On Windows under zio, close doesn't interrupt a read that is already waiting.
The end-to-end tests run against Redis 7.2.16, Redis 8.10.2 and Valkey 9.1.2 in Docker, in CI too.
See the [redis guide](https://github.com/vkir...
v0.9.0
Zefir 0.9.0 adds zefir-mongo, an async MongoDB driver for a standalone mongod.
mongo
mongo.Client talks to one standalone mongod, 7.0 or later, on std.Io. Database and Collection give the operations of the database. A document can be a struct of your own, an anonymous literal or raw BSON:
var client: mongo.Client = try .init(gpa, io, .{ .uri = config.mongo.uri, .password = password });
defer client.deinit();
try client.connect(io, ctx, &diagnostics);
defer client.close(io);
var ids: bson.ObjectId.Generator = .init(io);
const db: mongo.Database = .init(&client, "shop", &ids);
const users = db.collection("users");
// The insert makes the _id, and says which.
const ann = try users.insertOne(io, op, User{ .email = "ann@example.com", .name = "Ann", .age = 30 }, .{ .allocator = arena });
// The adults, the oldest first, batch by batch.
var adults = try users.find(io, op, .{ .age = .{ .@"$gte" = 18 } }, .{ .allocator = arena, .sort = bson.literal(.{ .age = -1 }) });
defer adults.deinit(io);
while (try adults.next(io, op)) |doc| {
const user = try bson.decode(User, arena, doc, .{});
// ...
}- The client.
- OP_MSG with the 2026 handshake.
- SCRAM-SHA-256 and SCRAM-SHA-1, with a cache of the keys.
- A pool of connections: a FIFO line for calls that wait, and a clear, a pause and a probe after a failure.
- Deadlines from
core.Context, sent to the server asmaxTimeMS, and cancellation. - Cursors, and writes in batches cut to the server's limits.
closelets the calls in flight finish, then cuts them, and kills the open cursors.
- Errors say whether the command may have run.
mongo.outcome(err)gives.not_sentor.may_have_executed.Diagnosticskeeps the server's code, name and labels.IncompleteWriteandWriteFailedkeep what a write did inpartial.- There are no automatic retries; the guide says which errors are worth retrying.
- BSON. Raw documents are checked when read. There are
Builder,ObjectIdand the other special types, and Relaxed Extended JSON for logs. The typed codec isbson.encode,bson.decodeandbson.literal, and its errors name the field's path, at compile time and when decoding. DatabaseandCollection: insertOne and insertMany; find and findOne; updateOne, updateMany and replaceOne; deleteOne and deleteMany; findOneAndUpdate, findOneAndReplace and findOneAndDelete; aggregate; countDocuments and estimatedDocumentCount; distinct; indexes; collections; andrunCommand.mongo.testing:ScriptandFakeServer, a scripted mongod on loopback, to test an application without MongoDB.
Not in this version yet, as the list at the top of the guide says:
- replica sets and sharded clusters;
- TLS, so it needs a trusted network;
- authentication other than SCRAM, and SCRAM-SHA-256 with a password that is not printable ASCII;
- sessions, transactions, change streams and compression.
The connection string is strict. On Windows under zio, close doesn't interrupt a read that is already waiting.
The end-to-end tests run against mongod 7.0.45, 8.0.34 and 9.0.4 in Docker, in CI too.
See the mongo guide: the customers of a shop, step by step, every program compiled and every run shown as it printed.
zig fetch --save git+https://github.com/vkiryakov/zefir#v0.9.0Breaking changes
- zefir-rpc-log is gone (#68). zefir has one logger, zefir-log. An access log of rpc calls is middleware that the application writes on it; the rpc guide shows one in its "Access logs" section.
v0.8.0
Zefir 0.8.0 adds zefir-http, an HTTP server with routes checked at compile time.
http
A service is made of controllers: structs whose methods handle requests and which declare their routes. The compiler knows every route of the service: it checks each handler's signature, and every error a handler may return, when it builds the service. What a request brings comes into structs of your own by the field_rules of zefir-validator:
// The body of POST: the title is trimmed, then checked.
const NewTodo = struct {
title: []const u8,
pub const field_rules = .{ .title = .{ v.trim, v.notBlank, v.maxLength(200) } };
};
const TodosV1Controller = struct {
pub const http_routes = http.routes(.{
.prefix = "/v1/todos",
.methods = .{
.list = http.get("", .{}),
.create = http.post("", .{}),
},
});
pub fn create(self: *TodosV1Controller, ctx: *http.Context) !http.Json(Todo) {
// Parsed, trimmed and checked: a body that does not fit never gets here.
const body = try ctx.json(NewTodo);
// ...
return .{ .value = todo, .status = 201 };
}
};A body that does not fit is a 422 in application/problem+json that names every field.
- Routes:
http.get,http.post,http.put,http.patch,http.delete,http.head,http.optionsandhttp.sse.get, with path parameters and catch-alls; 404, 405 withAllow; every GET answers HEAD too, and an OPTIONS request gets 204 with the methods of its path inAllow. - Input:
ctx.path,ctx.query,ctx.form,ctx.headers,ctx.cookiesandctx.json(T)read into your structs with their outside names, transforms and rules, and report every violation at once. - Output:
http.Json(T)by the same names and transforms, text and HTML, cookies, redirects, files and streamed responses. - One error handler turns every error into a response, problem+json by default; the compiler checks that every error a handler may return becomes a response.
- Middleware of your own, on the server, the controller and the route, in the order you write them;
onResponseandonFinishphases, and typedctx.locals. - Static files from a safe root,
http.static: MIME types,ETagandLast-Modifiedwith 304, ranges, and a fallback for single-page apps. - Server-sent events,
http.sse.Manager: subscriptions by key, heartbeats,Last-Event-IDand bounded queues. - Each request runs in a task of its own: a deadline gives 504, a client that leaves cancels its request,
.max_in_flightgives 503, and a stop waits.shutdown_gracefor the requests in flight. - Two backends:
http.native, on the network overstd.Io, on zio in a service; andhttp.testing, the same server in memory for tests, with no network. - A strict HTTP/1.1 parser of its own (RFC 9112), with limits on heads, targets and bodies,
Expect: 100-continue, pipelining and HTTP/1.0.
Not in this version yet, as the TODO list at the top of the guide says: WebSocket, templates, multipart/form-data uploads, OpenAPI, http.Patch(T), ready-made middleware, HTTP/2, HTTP/3 and TLS, compression.
On a shared GitHub runner, 4 vCPUs of an AMD EPYC 7763, a server on zio with four executors answers plain text at about 122 000 requests per second over 64 connections kept alive; the guide's benchmark shows the whole run.
See the http guide: a todo service built step by step, every example compiled and every run shown as it printed. For a first look, the README has http in a minute.
zig fetch --save git+https://github.com/vkiryakov/zefir#v0.8.0v0.7.0
Zefir 0.7.0 adds outside names and transforms to zefir-validator.
validator
The entry of a field in field_rules can now carry, in this order, an outside name, transforms and rules:
pub const field_rules = .{
.user_name = .{ v.rename("userName"), v.trim, v.notBlank, v.maxLength(100) },
.email = .{ v.trim, v.toLower, v.email },
.tags = v.each(.{ v.trim, v.notBlank }),
};v.rename("userName"): the field's outside name. Violation paths use it (userName,speakers[1].displayName).v.nameOfandv.fieldNamedgive the names to a codec of your own; no zefir module reads or writes JSON by them yet.- Transforms:
v.trim,v.trimStart,v.trimEnd,v.toLower,v.toUpper(ASCII letters only) andv.transformWith(f), also insidev.each.v.transform(arena, &value)runs them beforev.checkorv.checkAll;v.TransformError(T)is its exact error set. - Existing
field_ruleswork as before. A field whose Zig name has.,[or]is now accepted whenv.renamegives it a clean outside name.
See the validator guide.
rpc-nats
- Writing a transport: how rpc-nats is built, as a guide to writing a transport of your own.
zig fetch --save git+https://github.com/vkiryakov/zefir#v0.7.0v0.6.0
Zefir 0.6.0 adds rpc: typed calls between Zig services, on transports you can plug in.
rpc
zefir-rpc describes a service once, as a contract written in Zig, and the compiler checks the server and its clients against the same types. Calls travel as JSON over a transport: NATS in production, memory in tests, or one you write yourself.
- An engine that runs handlers with limits, deadlines that travel into nested calls, cancellation, authentication, middleware on three levels and a graceful stop, the same way on every transport.
- A contract,
rpc.Serverandrpc.Client; JSON and validation with zefir-validator on both sides; failures of a service come as values with stable codes. - Transports on vtables, a memory transport and a public conformance suite, so a transport can live in a package of its own:
examples/rpc-custom-transportis one. zefir-rpc-nats: request-reply over NATS, errors as in ADR-32, NATS micro discovery.zefir-rpc-log: access logs on zefir-log, one record per call, never bodies or credentials.rpc.testing.Serverruns your handlers with everything around them in memory.
Not implemented yet: transports over HTTP (Connect) and TCP, TLS (zefir-nats speaks plain TCP, so run rpc over NATS only in a network you trust), streaming, codecs other than JSON, retries.
See the rpc guide and the rpc-nats guide: every chapter with a program the build compiles and runs, and its real output.
nats
Client.publishMessageTimeout,SubscribeOptions.timeoutandSubscription.isDenied().- Breaking:
Client.SubscribeErrorgainsTimeout. An exhaustiveswitchon it withoutelseneeds a new branch; without.timeoutthe error never happens.
zig fetch --save git+https://github.com/vkiryakov/zefir#v0.6.0const zefir = b.dependency("zefir", .{ .target = target, .optimize = optimize });
// in your module's .imports:
.{ .name = "rpc", .module = zefir.module("zefir-rpc") },
.{ .name = "rpc-nats", .module = zefir.module("zefir-rpc-nats") },The NATS transport also needs zio; the rpc-nats guide's Installation shows how to add it.
Until 1.0, breaking changes may happen in any minor release.
v0.5.0
Zefir 0.5.0 adds nats: a Core NATS client written against std.Io.
nats
zefir-nats connects a Zig service to a NATS server: publish and subscribe, request and reply. It runs on any std.Io with real concurrency; zio is the runtime for services.
- Publish with headers and back-pressure; subscriptions with wildcards, queue groups, auto-unsubscribe and three overflow policies.
- Request and reply through one shared inbox subscription:
request,respond, andrequestManyfor scatter and gather. A request with no responders fails at once. - Bounded memory: every queue has a limit, and what does not fit waits for room or is dropped and counted.
- Reconnect with backoff, to the same server or to another one of the cluster, with the subscriptions and the inbox restored and a buffer for publishes; lame duck mode; a two-phase drain; events and statistics.
- Authentication: none, or a user and a password.
Not implemented yet: JetStream, TLS and mTLS, token, NKey and JWT authentication, WebSocket and the services API.
See the nats guide: a quick start in two programs, a sender and a receiver; every feature with a run against a real server; and the benchmark.
Other changes
- CI tests a pull request only on the modules it affects (#42).
- Documentation: the validator guide is in English, with an example for every rule (#39); log says it builds for 64-bit targets only (#41); the module list is grouped by area (#44).
zig fetch --save git+https://github.com/vkiryakov/zefir#v0.5.0const zefir = b.dependency("zefir", .{ .target = target, .optimize = optimize });
// in your module's .imports:
.{ .name = "nats", .module = zefir.module("zefir-nats") },The nats examples also need zio; the guide's Installation shows how to add it.
Until 1.0, breaking changes may happen in any minor release.
v0.4.0
Zefir 0.4.0 adds validator: validation rules declared on your structs.
validator
zefir-validator checks values against rules declared next to the struct.
pub const field_ruleslists the rules of each field,validateSelfchecks across fields, andv.customadds a predicate of your own.v.checkreturns the first violation without allocating;v.checkAllcollects them all, at most 100. A violation has a code, a message and a path such asitems[2].name.- A rule that does not fit its field is a compile error, and so are two rules in one tuple that no value can pass, such as
v.min(10)withv.max(5). - 96 rules in eight groups: common (
equals,oneOf,empty, …), numbers (min,max,range,positive,divisibleBy,finite, …), lists (minItems,maxItems,uniqueItems,uniqueBy,containsAll,containsNone,each), strings (minLength,maxLength,notBlank,alpha,contains,strongPassword, …), formats (email,httpUrl,fqdn,ip,port,decimal, …), encodings (json,jwt,base64,semVer,mimeType, colors, …), identifiers (uuid,iban,bic,creditCard,isbn,ean,btcAddress,ethereumAddress, …) and dates (date,rfc3339,iso8601,minDate,maxDate,militaryTime). Most rules have a public predicate, such asv.isEmail(s). .withMessagesets a message, with placeholders.
See the validator guide. From this release on, the behaviour of a shipped rule is a contract: see Compatibility in the guide.
If you tried the early preview in the v0.3.0 tag: two rules in one tuple that no value can pass are now a compile error.
Other changes
- log: the queue's detectors tell a lost wake-up from a slow thread (#32).
zig fetch --save git+https://github.com/vkiryakov/zefir#v0.4.0const zefir = b.dependency("zefir", .{ .target = target, .optimize = optimize });
// in your module's .imports:
.{ .name = "validator", .module = zefir.module("zefir-validator") },Until 1.0, breaking changes may happen in any minor release.
v0.3.0
Zefir 0.3.0 adds log, structured logging for services.
log
zefir-log writes one line of JSON per record on stdout, for log collectors and log stores.
- Constant messages and typed fields, checked at compile time: a value that cannot be logged, a field without a name or a reserved name is a compile error that names the field.
- Secrets come out as
[REDACTED]: by type (confy.Secretand any type markedzefir_sensitive) and by name (password,api_key,db_password,newPassword, …). - Levels and scopes that change while the service runs:
setLevelandsetScopeLevel. - Sync mode, where the call writes, and queued mode, where a lock-free queue and a writer thread keep stdout off the request path;
flushandshutdowntake a timeout. - Trace and span ids from a
core.Context, the call site from@src(), and std.log routed into the same stream. - Subscribers for alerts, on threads of their own; interceptors that drop noise; emergency records to stderr; counters in
stats().
See the log guide: a tutorial with diagrams and complete examples, from the first record to alerts in Slack.
validator
zefir-validator ships in 0.4.0. The module in this tag is an early preview: it is not covered by the compatibility promise until 0.4.0.
Other changes
- confy:
confy.Secretcarries thezefir_sensitivemarker, so log writes it as[REDACTED]. Its{f}now prints[REDACTED]; it printed[redacted]before. - core:
core.isSensitive(T)tells whether a type carries the marker, plus the fixes from the core review. - CI: the tests also run under ThreadSanitizer on Linux and macOS, and both runs must pass for every pull request.
rpc and orm are still placeholders without an API.
Requirements
Zig 0.17.0.
Installation
zig fetch --save git+https://github.com/vkiryakov/zefir#v0.3.0const zefir = b.dependency("zefir", .{ .target = target, .optimize = optimize });
// in your module's .imports:
.{ .name = "log", .module = zefir.module("zefir-log") },Until 1.0, breaking changes may happen in any minor release.
v0.2.0
Zefir 0.2.0 adds core, the execution context every Zefir module will share.
core
zefir-core carries an operation's deadline, its cancellation token and its W3C trace identity through your code, without allocating, reading a clock or doing I/O.
Context: derive it withwithTrace,withDeadline(never extends a deadline) andwithCancellation;ctx.check(now)returnserror.Cancelledorerror.DeadlineExceeded.Deadlineon thestd.Io.Clock.awakeclock, with saturating arithmetic andtoTimeout()forstd.Iowaits.CancellationToken: an atomic, cooperative flag, safe to cancel from any thread.- W3C Trace Context:
TraceId,SpanId,TraceFlags,TraceState,TraceContext, andcore.w3cto parse and write thetraceparentandtracestateheaders for any transport, checked against the official W3C test suite. ErrorCode: the 16 canonical error classes of gRPC, Connect and Twirp, for transports to map to their own statuses.
See the core docs for passing contexts, ownership, cancellation rules for std.Io and the W3C headers.
confy is unchanged since v0.1.0. logger, rpc and orm are still placeholders without an API.
Requirements
Zig 0.17.0.
Installation
zig fetch --save git+https://github.com/vkiryakov/zefir#v0.2.0const zefir = b.dependency("zefir", .{ .target = target, .optimize = optimize });
// in your module's .imports:
.{ .name = "core", .module = zefir.module("zefir-core") },Until 1.0, breaking changes may happen in any minor release.