Repository navigation
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: the sessions, the profiles, the orders and the best customers of a coffee shop, step by step, every program compiled and every run shown as it printed.
Also in this release
- The module table of the README says which modules are only partly built, and what each one lacks.
- nats paces its reconnects by the life of the last connection up to the moment its end is known. Its teardown, slow on a busy host, no longer counts.
rpc.testing.MemoryTransportholds a call for its wholesetDelay, even where a sleep of theIoends early, as std's does on Windows.
zig fetch --save git+https://github.com/vkiryakov/zefir#v0.10.0const zefir = b.dependency("zefir", .{ .target = target, .optimize = optimize });
// in your module's .imports:
.{ .name = "zefir", .module = zefir.module("zefir") }, // zefir.App and zefir.app
.{ .name = "jwt", .module = zefir.module("zefir-jwt") },
.{ .name = "redis", .module = zefir.module("zefir-redis") },A service under App, and the Redis client, also need zio; the Installation sections of the App guide and the redis guide show how to add it.
Breaking changes
The servers and clients take their parts and their options apart, and stop by a deadline, so that zefir.App can run them (#76).
http
http.Server.init(gpa, options)is nowinit(gpa, parts, options: http.Server.Options). The parts are.controllers(required),.middleware,.error_handlerand.strict_errors; everything else is inOptions.http.Server.Settingsis no longer public..backend = bis now.listen = .{ .backend = b }, and.listenis required. New:.listen = .{ .port = N }and.listen = .{ .native = .{ … } }, where the server builds and frees its ownhttp.native.Backend..staticis a slice:.static = &.{ http.static(…) }.shutdown(io)is nowshutdown(io, deadline). It returns bydeadline − 250 ms, and the deadline cuts.shutdown_grace. The old cap, the grace plus 1 s plusfinish_timeout, is gone.WaitError, and with itrunandShutdownError, gainserror.Unavailableanderror.Unexpected(http.Backend.FatalError): a broken backend stops its server.http.Backend.Handlerhas a new required field,fail.http.testing.chainNamestakes the type of the parts. Compile errors say "unknown part …" instead of "unknown option".
rpc
rpc.Server.init(gpa, options)is nowinit(gpa, parts, options: rpc.Server.Options). The parts are.handlers(required),.middlewareand.authenticator.rpc.Client.init(gpa, options)is nowinit(gpa, parts, options: rpc.Client.Options). It returnsrpc.Client.InitError, withInvalidConfigurationfor a zeromax_pendingormax_response_bytes, or adefault_timeoutof zero or less.rpc.testing.Server.init(gpa, io, options)is nowinit(gpa, io, parts, options), andt.client(options)is nowt.client(parts, options), which returnsrpc.Client.InitErrortoo.rpc.Server.shutdown(io)andrpc.RawServer.shutdown(io)are nowshutdown(io, deadline), and return bydeadline − 250 ms.rpc.Serverlowerslimits.default_timeouttolimits.max_timeoutinstead of refusing it.
rpc-nats
rpc_nats.Transport.init(gpa, conn, options)is nowinit(gpa, client: *nats.Client, options). The old form isinitConn(gpa, conn, options).
nats
nats.ErrorgainsConnectionLostandReconnectFailed. An exhaustiveswitchon it withoutelseneeds new branches.
mongo
mongo.Client.Optionshasstart_timeout, 10 s by default, andinitrefuses astart_timeoutof zero or less withInvalidArgument.
core
- core's promises are narrowed to its primitives.
Context,Deadline,CancellationToken, the trace types andErrorCodenever allocate, read the clock or do I/O;zefir.Appallocates, reads the clock and runs tasks.