Skip to content

Releases: vkiryakov/zefir

v0.11.0

Choose a tag to compare

@vkiryakov vkiryakov released this 24 Oct 19:21
4414c59

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 _id field, the BSON names and the indexes are a typed odm_schema in the struct, and the rules are the validator's field_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.Filter and odm.Update build conditions at run time.
  • Tenants, shared and on servers of their own, at once. odm.Registry binds 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 of zefir.App.
  • Any driver. odm has no network code: it reaches a database only through its driver contract, odm.Driver. zefir-mongo's mongo.OdmDriver is the reference driver and passes the conformance suite, odm.testing.conformance, on mongod 9.0.
  • Tests without MongoDB. odm.testing.FakeDriver answers 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) and MiddlewareDoc are 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.Docs serves /openapi.json with its ETag and a Scalar page at /docs. zefir.openapi.testing.expectFile keeps openapi.json in the repository up to date, and zig build openapi rewrites 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, .body or .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, and http.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.0
const 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.openapi

A 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-jwt is now zefir-security, and JWT lives in it as security.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.X is now @import("mongo-bson").X, or @import("zefir").mongo_bson.X. Add .{ .name = "mongo-bson", .module = zefir.module("zefir-mongo-bson") } to the .imports of your module. mongo.testing.vectors is bson.testing.vectors.
  • The compile errors of the BSON codec name the types of the new module: types.DbPointer, not bson.types.DbPointer.
  • zefir-mongo impor...
Read more

v0.10.0

Choose a tag to compare

@vkiryakov vkiryakov released this 22 Oct 17:29
228a6b9

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.
  • The lifecycle.
    • start runs 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.
    • ShutdownTimeout when the deadline runs out, and run then ends the process.
    • wait, isReady, failure and Status. App's events go to std.log, in the scope zefir_app.
  • The beans of zefir.
    • The servers and clients of http, rpc, rpc-nats, nats and mongo are beans without wrappers, and redis.Client fits the same contract.
    • The servers of http and rpc, and rpc.Client, take their parts and typed Options, which ZLS completes. The servers and the clients of nats, mongo and redis stop with shutdown(io, deadline).
    • nats.Client gets start, wait and shutdown; mongo.Client gets start, shutdown and start_timeout.

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: init builds 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.generate makes keys for tests.
    • zefir-log writes a key as [REDACTED], and deinit wipes it from memory.
  • Claims.
    • The signer sets iss, aud, iat and exp, 15 minutes from now unless you choose another lifetime; sub comes with the call.
    • The verifier requires exp, iss and aud and checks the time with a leeway, 60 s by default. Then it fills your struct and checks its field_rules.
  • A refusal says why. Every reason has an error of its own, from BadSignature and Expired to WrongAudience and ClaimsRejected. jwt.Details names the claim or the rule at fault, for your log, and jwt.describe gives a short text for the answer to a client.
  • Safe by default.
    • The algorithm comes from the key, never from the token, and none never passes.
    • The key comes only from the verifier: jku, x5u, jwk and x5c in a header are never read.
    • A header with crit is refused.
    • ES256 and ES384 signatures are written, and accepted, with a low s only.
  • A plain library. jwt has no network code, no tasks and no global state; jwt.testing.FixedClock sets the time in tests.
  • HTTP and RPC. The guide builds both checks as code of the application: a middleware of zefir-http, JwtAuth, and an rpc.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.
    • start and shutdown let 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.raw sends any other command, except those that change the state of a connection.
  • Pipelines, transactions and scripts.
    • client.pipeline sends many commands in one exchange and gives a value or an error for each.
    • conn.transaction runs MULTI/EXEC with WATCH, and gives null when a watched key changed.
    • cmd.evalScript sends a Lua script by its SHA1, and its text after NOSCRIPT.
  • Pub/sub. client.subscribe gives a redis.Subscriber on 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.
    • Diagnostics keeps 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...

Read more

v0.9.0

Choose a tag to compare

@vkiryakov vkiryakov released this 20 Oct 20:12
7ef77d3

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 as maxTimeMS, and cancellation.
    • Cursors, and writes in batches cut to the server's limits.
    • close lets 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_sent or .may_have_executed.
    • Diagnostics keeps the server's code, name and labels.
    • IncompleteWrite and WriteFailed keep what a write did in partial.
    • There are no automatic retries; the guide says which errors are worth retrying.
  • BSON. Raw documents are checked when read. There are Builder, ObjectId and the other special types, and Relaxed Extended JSON for logs. The typed codec is bson.encode, bson.decode and bson.literal, and its errors name the field's path, at compile time and when decoding.
  • Database and Collection: insertOne and insertMany; find and findOne; updateOne, updateMany and replaceOne; deleteOne and deleteMany; findOneAndUpdate, findOneAndReplace and findOneAndDelete; aggregate; countDocuments and estimatedDocumentCount; distinct; indexes; collections; and runCommand.
  • mongo.testing: Script and FakeServer, 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.0

Breaking 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

Choose a tag to compare

@vkiryakov vkiryakov released this 18 Oct 10:17
3abd9bc

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.options and http.sse.get, with path parameters and catch-alls; 404, 405 with Allow; every GET answers HEAD too, and an OPTIONS request gets 204 with the methods of its path in Allow.
  • Input: ctx.path, ctx.query, ctx.form, ctx.headers, ctx.cookies and ctx.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; onResponse and onFinish phases, and typed ctx.locals.
  • Static files from a safe root, http.static: MIME types, ETag and Last-Modified with 304, ranges, and a fallback for single-page apps.
  • Server-sent events, http.sse.Manager: subscriptions by key, heartbeats, Last-Event-ID and bounded queues.
  • Each request runs in a task of its own: a deadline gives 504, a client that leaves cancels its request, .max_in_flight gives 503, and a stop waits .shutdown_grace for the requests in flight.
  • Two backends: http.native, on the network over std.Io, on zio in a service; and http.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.0

v0.7.0

Choose a tag to compare

@vkiryakov vkiryakov released this 17 Oct 10:24
ce5093b

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.nameOf and v.fieldNamed give 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) and v.transformWith(f), also inside v.each. v.transform(arena, &value) runs them before v.check or v.checkAll; v.TransformError(T) is its exact error set.
  • Existing field_rules work as before. A field whose Zig name has ., [ or ] is now accepted when v.rename gives it a clean outside name.

See the validator guide.

rpc-nats

zig fetch --save git+https://github.com/vkiryakov/zefir#v0.7.0

v0.6.0

Choose a tag to compare

@vkiryakov vkiryakov released this 16 Oct 17:07
41add92

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.Server and rpc.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-transport is 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.Server runs 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.timeout and Subscription.isDenied().
  • Breaking: Client.SubscribeError gains Timeout. An exhaustive switch on it without else needs a new branch; without .timeout the error never happens.
zig fetch --save git+https://github.com/vkiryakov/zefir#v0.6.0
const 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

Choose a tag to compare

@vkiryakov vkiryakov released this 14 Oct 16:07
1e0ca7c

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, and requestMany for 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.0
const 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

Choose a tag to compare

@vkiryakov vkiryakov released this 13 Oct 08:42
3c5e12d

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_rules lists the rules of each field, validateSelf checks across fields, and v.custom adds a predicate of your own.
  • v.check returns the first violation without allocating; v.checkAll collects them all, at most 100. A violation has a code, a message and a path such as items[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) with v.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 as v.isEmail(s).
  • .withMessage sets 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.0
const 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

Choose a tag to compare

@vkiryakov vkiryakov released this 13 Oct 00:08
f5b190b

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.Secret and any type marked zefir_sensitive) and by name (password, api_key, db_password, newPassword, …).
  • Levels and scopes that change while the service runs: setLevel and setScopeLevel.
  • Sync mode, where the call writes, and queued mode, where a lock-free queue and a writer thread keep stdout off the request path; flush and shutdown take 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.Secret carries the zefir_sensitive marker, 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.0
const 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

Choose a tag to compare

@vkiryakov vkiryakov released this 10 Oct 07:01
0288558

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 with withTrace, withDeadline (never extends a deadline) and withCancellation; ctx.check(now) returns error.Cancelled or error.DeadlineExceeded.
  • Deadline on the std.Io.Clock.awake clock, with saturating arithmetic and toTimeout() for std.Io waits.
  • CancellationToken: an atomic, cooperative flag, safe to cancel from any thread.
  • W3C Trace Context: TraceId, SpanId, TraceFlags, TraceState, TraceContext, and core.w3c to parse and write the traceparent and tracestate headers 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.0
const 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.