Interfaces: import proto, oxl gen, ORM relations
English translation of docs/interfaces.md (the Russian text is the original). A guide to К17 and К7 ([С §16.3, §5.9, §12.8]); the decisions taken are D-4740..D-4799 in decisions.md, the design is in the section "import proto and oxl gen" of ARCHITECTURE.md.
import proto#
import proto "proto/inventory.proto" { Inventory } // the names in braces are optional: without them everything is imported
use std.os { args }
fn main() io, net {
let inv = Inventory.connect(args()[1], tls: ClientTls{})
match inv.Reserve(sku: "HAM-1", qty: 2) { // the request fields are the rpc parameters
ReserveReply r => print("{r.left}")
NetError e => print("failed {e}")
}
}- The path is looked up next to the source, in the package directory, in
src/and inproto/;import "x/y.proto"inside a .proto is looked up from the same root.OXL_DUMP_FFI=1 oxl build …prints the generated declarations. message→data(@tag(N)= the field number, default values so that a message without zero fields is accepted),enum→@open enum,oneof→ optional fields (and the form ofoxl exportfor anenumwith fields is again anenum),map→Map,repeated→List.service→pub extern servicewith the path/package.Service/method. The rpc parameters are the fields of the request message; a response consisting of one field with number 1 is that field, any other response is the message type.option idempotency_level = IDEMPOTENT | NO_SIDE_EFFECTS→@idempotent/@readonly.option (oxl.error) = trueon a message → an@errortype;option (oxl.raises) = "A, B"on an rpc →-> R | A | B.oxl export --protowrites these options itself (oxl/options.proto).@servebeforeimport prototurns the services into ordinaryservices with@publicrpcs — they can be implemented throughimpl.- Not supported:
proto2,edition,fixed*,sfixed*,Any,Struct; client/bidi streaming of a foreign .proto is skipped with the warningimport-skip.
Round-trip test: oxl export --proto → import proto → a call of a live service gives the same bytes (ctest proto_import_roundtrip); a foreign server — ctest proto_import_interop.
oxl gen#
oxl gen --py [path] --out dir [--name module] [--all]
oxl gen --ts [path] --out dir
oxl gen --go [path] --out dir
oxl gen --kotlin [path] --out dirWrites <package>_client.py|ts and oxl_runtime.py|ts (transport: grpcio / @grpc/grpc-js). Only pub(api) services (--all — all of them).
--go (D-8230) writes a Go module (go.mod, <package>_client.go, oxlrt/oxlrt.go) with no dependencies but the standard library (Go 1.24, gRPC over net/http):
methods take a context.Context, errors are error values (errors.As(err, &shop.NotFound{})), streams are iter.Seq / iter.Seq2.
--kotlin (D-8231) writes <Package>Client.kt and OxlRuntime.kt (gRPC for the JVM: io.grpc:grpc-api, grpc-stub and a channel such as grpc-netty-shaded): data class, enum class, sealed class,
errors are OxlExceptions. cpp is after 1.0. The Kotlin client is covered by a golden file and structural checks; the test compiles it when kotlinc and $OXL_KOTLIN_CP exist.
import shop_client as shop
with shop.CatalogClient("127.0.0.1:7000", token=jwt, timeout=2.0) as c:
p = c.get("HAM-1") # Product (dataclass)
try:
c.reserve(shop.Line(sku="HAM-1", qty=100))
except shop.OutOfStock as e: # @error types are exceptions
print(e.available)
for p in c.list(shop.Category.Tools): ... # server stream
c.worth(iter(["HAM-1", "SAW-4"])) # client stream; bidi — an iterator on input and on outputimport { CatalogClient, NotFound, result } from "./shop_client.ts";
const c = new CatalogClient("127.0.0.1:7000", { token: jwt, timeoutMs: 2000 });
const r = await result(c.get("NOPE-0")); // { ok: false, error: NotFound }
for await (const p of c.list(Category.Tools)) console.log(p.name);- Refined types (
type Sku = str where len(1..=16)) are checked on the client before sending and after receiving (RefinementError). Timeout,Unavailable,Cancelled,AccessDenied,RpcErrorare used instead ofNetError.- TypeScript uses erasable syntax only: it runs on
node≥ 22.18, Deno, Bun without compilation; 64-bit numbers arebigint.
Table relations and queries#
use std.orm { Db, ref, many, Ref, Many }
@table("customers")
data Customer { @pk @auto id: i64; name: str; orders: many<Order> = Many<Order>{} }
@table("orders")
data Order { @pk @auto id: i64; customer: ref<Customer>; total: i64 }
let big = from o in db.Order where o.customer.id == 7 && o.total > 100 order by o.id load(customer)
let withOrders = from c in db.Customer order by c.id load(orders)
let perRegion = from s in db.Sale group by s.region into g select (g.key, g.count())ref<T>is a column holding the key (i64) of a row ofT; the value isRef<Customer>{ id: 7 }; afterload(customer)the row is ino.customer.loaded[0].many<T>has no column;Thas exactly onereffield to this table; afterload(orders)the rows are inc.orders.rows.loadruns one query per field (a semi-join on the samewhere/skip/take), identically on SQLite and PostgreSQL.builtin://— not yet.group byover a table:where/order byare in SQL, the groups are built over the fetched rows (D-4783).
Source: docs/interfaces.en.md