Repository navigation
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-mongoimportszefir-odm, and with itzefir-validator: its reference driver of odm,mongo.OdmDriver, lives there. The API of zefir-mongo does not change, and the code of the driver compiles only when an application uses it.
odm
- The placeholder module
zefir-orm, andzefir.orm, are gone. In their place iszefir-odm(zefir.odm).
http
- The SSE manager is typed.
http.sse.Manager(Events)takes aunion(enum)of the events, whose variants name the events and carry their data;publishtakes.{ .event = .{ .@"todo.created" = todo } }, with.idand.retrybeside it; and everyhttp.sse.getroute declares.events. An event the union does not have, or data of another type, does not build. Manager(Events).PublishErrorholds the transform errors of every variant:InvalidEvent,EventTooLarge,OutOfMemory, and the union ofv.TransformErrorof each variant's type. A handler that publishes a variant of plain data still gets the transform errors of the other variants in its error set, and under the strict error check (.strict_errors, on by default)mapErrormust map them.- 205 Reset Content goes without a body, with
Content-Length: 0(RFC 9110, 15.3.6), as 204 and 304 go: what the handler wrote, or the value of itshttp.Json(T), is left out. - New compile errors come with the new declarations of a route. Among them,
.jsonor.content_typeon a 204, 205 or 304 entry of.responsesis refused, since those responses never have a body:.no_content = .{}is the whole entry.