From bbec89a749394345b1f631be9824b030a7607793 Mon Sep 17 00:00:00 2001 From: Kinflou Date: Mon, 9 Nov 2026 22:29:03 +0800 Subject: [PATCH 1/2] Add the comline-catalog Rust crate rust/ is a no_std, dependency-free crate holding the catalog as Rust constants and tables: guarantees, facts with their values, the official encodings with aliases and frozen spellings, features, sets and SPEC_VERSION. Its types and lookups are hand-written; its data, rust/src/catalog.rs, is generated from the CUE (catalog/rust.cue) by specs/check.sh, which fails on a stale copy. CI tests it on stable, checks it on Rust 1.82 and builds it for wasm32. --- .github/workflows/check.yml | 17 ++ README.md | 3 +- catalog/readme.md | 15 +- catalog/rust.cue | 185 +++++++++++++ rust/.gitignore | 2 + rust/Cargo.toml | 13 + rust/src/catalog.rs | 518 ++++++++++++++++++++++++++++++++++++ rust/src/lib.rs | 234 ++++++++++++++++ specs/check.sh | 20 +- 9 files changed, 999 insertions(+), 8 deletions(-) create mode 100644 catalog/rust.cue create mode 100644 rust/.gitignore create mode 100644 rust/Cargo.toml create mode 100644 rust/src/catalog.rs create mode 100644 rust/src/lib.rs diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index c6a5782..d1ac8da 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -37,3 +37,20 @@ jobs: - run: specs/check.sh --check - name: VERSION is a spec version run: grep -Eqx '[0-9]+\.[0-9]+' VERSION + + # The comline-catalog crate (rust/): its tests, its minimum Rust, and a + # no_std build. specs/check.sh above keeps its generated data current. + rust: + name: rust crate + runs-on: ubuntu-latest + defaults: + run: + working-directory: rust + steps: + - uses: actions/checkout@v4 + - run: rustup toolchain install stable 1.82.0 --profile minimal --component clippy + - run: rustup target add wasm32-unknown-unknown --toolchain stable + - run: cargo +stable test + - run: cargo +stable clippy --all-targets -- -D warnings + - run: cargo +1.82.0 check + - run: cargo +stable build --target wasm32-unknown-unknown diff --git a/README.md b/README.md index 4a3b536..2d28c77 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ program has to agree on: the guarantees, the facts and the values each may take, the official encodings with their aliases, and the wire features, with the sets they form. The profiles' facts and `features.json` come from it, and [`catalog/gen/catalog.json`](catalog/gen/catalog.json) holds it as JSON for -readers without CUE. See [the proposal](https://github.com/ComlineProject/docs/pull/162) +readers without CUE, and the `comline-catalog` crate ([`rust/`](rust)) as +Rust constants. See [the proposal](https://github.com/ComlineProject/docs/pull/162) and [core#219](https://github.com/ComlineProject/core/issues/219) for what will read it. diff --git a/catalog/readme.md b/catalog/readme.md index c8b6abd..c0dfa6b 100644 --- a/catalog/readme.md +++ b/catalog/readme.md @@ -32,8 +32,15 @@ What an entry may say, and the rules across entries, are in their values from here, so a profile is held to the catalog's domains. - [`features.json`](../features.json) is generated from `features`. - [`gen/catalog.json`](gen/catalog.json) is the whole catalog as JSON, for - readers without CUE: the compiler, the runtimes, generators, the language - server, the Observatory and the docs. + readers without CUE: the Observatory, the docs, generators in other + languages. +- The **`comline-catalog`** Rust crate ([`rust/`](../rust)) holds it as + constants and tables, `no_std` and without dependencies: core, the runtime + and the language server depend on it by git rev. Its types and lookups are + written by hand in `rust/src/lib.rs`; its data, `rust/src/catalog.rs`, is + generated by [`rust.cue`](rust.cue). An id's constant is its name in upper + case, with anything but letters and digits as `_`: `packed@1` is + `encoding::PACKED_1`. - `specs/check.sh` checks that `profiles/` holds exactly the encodings with a profile, each of its encoding's kind, and `vectors/` exactly the features. @@ -43,8 +50,8 @@ A third party's encoding isn't an entry here: its package declares it in ## Changing it Edit the `.cue` files, then run `specs/check.sh` to regenerate -`features.json` and `gen/catalog.json`. CI runs `specs/check.sh --check`, -which fails when either is stale. +`features.json`, `gen/catalog.json` and `rust/src/catalog.rs`. CI runs `specs/check.sh --check`, +which fails when any of them is stale. Order matters where it shows: facts are listed in the order profiles list them, and features in the order `features.json` does. diff --git a/catalog/rust.cue b/catalog/rust.cue new file mode 100644 index 0000000..6289daa --- /dev/null +++ b/catalog/rust.cue @@ -0,0 +1,185 @@ +package catalog + +import ( + "list" + "regexp" + "strings" +) + +// `rust/src/catalog.rs`: the catalog as Rust data, for the `comline-catalog` +// crate. `rust/src/lib.rs` defines the types it fills. Strings are raw +// literals, so no text may hold `"#`. + +_rust: { + // A Rust raw string literal. + str: { + #in: string + out: "r#\"\(#in)\"#" + } + opt: { + #in: string | *null + let v = #in + out: [if v != null {"Some(\((str & {#in: v}).out))"}, "None"][0] + } + list: { + #in: [...string] + out: "&[" + strings.Join([for s in #in {(str & {#in: s}).out}], ", ") + "]" + } + // A constant's name: `packed@1` -> `PACKED_1`. + const: { + #in: string + out: strings.ToUpper(regexp.ReplaceAll("[^a-zA-Z0-9]+", #in, "_")) + } + kind: { + #in: "format" | "framing" | "handshake" + out: {format: "Kind::Format", framing: "Kind::Framing", handshake: "Kind::Handshake"}[#in] + } + status: { + #in: "stable" | "experimental" | "deprecated" + out: {stable: "Status::Stable", experimental: "Status::Experimental", deprecated: "Status::Deprecated"}[#in] + } + lifecycle: { + #in: {...} + let e = #in + out: """ + status: \((status & {#in: e.status}).out), + replaced_by: \((opt & {#in: [if e.replaced_by != _|_ {e.replaced_by}, null][0]}).out), + since: \((opt & {#in: [if e.since != _|_ {e.since}, null][0]}).out), + """ + } +} + +// No text may end a raw string early. +_raw_safe: [ + for k, entries in {"guarantees": guarantees, "facts": facts, "encodings": encodings, "features": features} + for id, e in entries + for f, v in e + if (v & string) != _|_ + if strings.Contains(v, "\"#") {"\(k).\(id).\(f)"}, +] & [] + +// Constant names stay distinct within each kind. +_unique_consts: { + for k, entries in {"guarantees": guarantees, "facts": facts, "encodings": encodings, "features": features} { + (k): list.UniqueItems() & [for id, _ in entries {(_rust.const & {#in: id}).out}] + } +} + +_ids: { + #in: {...} + out: strings.Join([for id, _ in #in {" pub const \((_rust.const & {#in: id}).out): &str = \((_rust.str & {#in: id}).out);"}], "\n") +} + +_set: { + #name: string + #in: [...string] + let items = #in + out: " pub const \(#name): &[&str] = \((_rust.list & {#in: items}).out);" +} + +rust_catalog: """ + // Generated from the catalog's CUE by `specs/check.sh`: don't edit by hand. + + use crate::{Encoding, Fact, Feature, Guarantee, Kind, Origin, Status, Value}; + + pub const SPEC_VERSION: &str = \((_rust.str & {#in: _spec_version}).out); + + /// Every guarantee, in the catalog's order. + pub const GUARANTEES: &[Guarantee] = &[ + \(strings.Join([for id, g in guarantees {""" + Guarantee { + id: \((_rust.str & {#in: id}).out), + title: \((_rust.str & {#in: g.title}).out), + description: \((_rust.str & {#in: g.description}).out), + comes_from: \({handshake: "Origin::Handshake", encoding: "Origin::Encoding", policy: "Origin::Policy", type: "Origin::Type"}[g.comes_from]), + \((_rust.lifecycle & {#in: g}).out) + }, + """}], "\n")) + ]; + + /// Every fact, in the order profiles list them. + pub const FACTS: &[Fact] = &[ + \(strings.Join([for id, f in facts {""" + Fact { + id: \((_rust.str & {#in: id}).out), + applies_to: \((_rust.kind & {#in: f.applies_to}).out), + title: \((_rust.str & {#in: f.title}).out), + description: \((_rust.str & {#in: f.description}).out), + values: &[\(strings.Join([for v, meaning in f.values {"Value { id: \((_rust.str & {#in: v}).out), meaning: \((_rust.str & {#in: meaning}).out) }"}], ", "))], + establishes: \((_rust.opt & {#in: [if f.establishes != _|_ {f.establishes}, null][0]}).out), + \((_rust.lifecycle & {#in: f}).out) + }, + """}], "\n")) + ]; + + /// Every official encoding, by its wire name. + pub const ENCODINGS: &[Encoding] = &[ + \(strings.Join([for id, e in encodings {""" + Encoding { + id: \((_rust.str & {#in: id}).out), + kind: \((_rust.kind & {#in: e.kind}).out), + title: \((_rust.str & {#in: e.title}).out), + description: \((_rust.str & {#in: e.description}).out), + aliases: \((_rust.list & {#in: e.aliases}).out), + frozen_as: \((_rust.opt & {#in: [if e.frozen_as != _|_ {e.frozen_as}, null][0]}).out), + profile: \(e.profile), + feature: \((_rust.opt & {#in: [if e.feature != _|_ {e.feature}, null][0]}).out), + tags: \((_rust.list & {#in: e.tags}).out), + \((_rust.lifecycle & {#in: e}).out) + }, + """}], "\n")) + ]; + + /// Every wire feature, in the order `features.json` lists them. + pub const FEATURES: &[Feature] = &[ + \(strings.Join([for id, f in features {""" + Feature { + id: \((_rust.str & {#in: id}).out), + title: \((_rust.str & {#in: f.title}).out), + description: \((_rust.str & {#in: f.description}).out), + \((_rust.lifecycle & {#in: f}).out) + }, + """}], "\n")) + ]; + + /// Guarantee ids. + pub mod guarantee { + \((_ids & {#in: guarantees}).out) + } + + /// Fact ids. + pub mod fact { + \((_ids & {#in: facts}).out) + } + + /// Encoding wire names. + pub mod encoding { + \((_ids & {#in: encodings}).out) + } + + /// Feature ids. + pub mod feature { + \((_ids & {#in: features}).out) + } + + /// The catalog's sets. + pub mod sets { + \((_set & {#name: "GUARANTEES", #in: sets.guarantees}).out) + \((_set & {#name: "FORMATS", #in: sets.formats}).out) + \((_set & {#name: "FRAMINGS", #in: sets.framings}).out) + \((_set & {#name: "HANDSHAKES", #in: sets.handshakes}).out) + \((_set & {#name: "OFFICIAL", #in: sets.official}).out) + \((_set & {#name: "PROFILED", #in: sets.profiled}).out) + \((_set & {#name: "FORMAT_FACTS", #in: sets.format_facts}).out) + \((_set & {#name: "FRAMING_FACTS", #in: sets.framing_facts}).out) + \((_set & {#name: "HANDSHAKE_FACTS", #in: sets.handshake_facts}).out) + \((_set & {#name: "FEATURES", #in: sets.features}).out) + /// Every alias, with the wire name it resolves to. + pub const ALIASES: &[(&str, &str)] = &[\(strings.Join([for a, id in sets.aliases {"(\((_rust.str & {#in: a}).out), \((_rust.str & {#in: id}).out))"}], ", "))]; + } + + """ + +// The spec's version, from `VERSION`: `specs/check.sh` passes it as +// `-t version=...`. +_spec_version: *"" | =~"^[0-9]+\\.[0-9]+$" @tag(version) diff --git a/rust/.gitignore b/rust/.gitignore new file mode 100644 index 0000000..2c96eb1 --- /dev/null +++ b/rust/.gitignore @@ -0,0 +1,2 @@ +target/ +Cargo.lock diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..9bc2601 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "comline-catalog" +version = "0.10.0" +edition = "2021" +rust-version = "1.82" +license = "MPL-2.0" +description = "The Comline specification's catalog of shared names, as Rust constants" +repository = "https://github.com/ComlineProject/specification" +publish = false + +# No dependencies, and no_std: core's IR-only build, the runtime and the +# language server can all use it. +[dependencies] diff --git a/rust/src/catalog.rs b/rust/src/catalog.rs new file mode 100644 index 0000000..69f777b --- /dev/null +++ b/rust/src/catalog.rs @@ -0,0 +1,518 @@ +// Generated from the catalog's CUE by `specs/check.sh`: don't edit by hand. + +use crate::{Encoding, Fact, Feature, Guarantee, Kind, Origin, Status, Value}; + +pub const SPEC_VERSION: &str = r#"0.10"#; + +/// Every guarantee, in the catalog's order. +pub const GUARANTEES: &[Guarantee] = &[ + Guarantee { + id: r#"schema_agreement"#, + title: r#"schema agreement"#, + description: r#"Both ends were built from compatible schemas, which the handshake checks."#, + comes_from: Origin::Handshake, + status: Status::Stable, + replaced_by: None, + since: None, + }, + Guarantee { + id: r#"encoding_agreement"#, + title: r#"encoding agreement"#, + description: r#"Both ends use the same format and framing, which the handshake checks."#, + comes_from: Origin::Handshake, + status: Status::Stable, + replaced_by: None, + since: None, + }, + Guarantee { + id: r#"encoding_conformance"#, + title: r#"encoding conformance"#, + description: r#"An implementation encodes what its name promises, proven by conformance vectors."#, + comes_from: Origin::Encoding, + status: Status::Stable, + replaced_by: None, + since: None, + }, + Guarantee { + id: r#"version_honesty"#, + title: r#"version honesty"#, + description: r#"A version bump says what the change breaks, as the version comparison works it out."#, + comes_from: Origin::Policy, + status: Status::Stable, + replaced_by: None, + since: None, + }, + Guarantee { + id: r#"bounded_size"#, + title: r#"bounded size"#, + description: r#"Every value has a maximum size, so a decoder refuses an oversized one before allocating it."#, + comes_from: Origin::Type, + status: Status::Stable, + replaced_by: None, + since: None, + }, +]; + +/// Every fact, in the order profiles list them. +pub const FACTS: &[Fact] = &[ + Fact { + id: r#"struct_fields"#, + applies_to: Kind::Format, + title: r#"struct fields"#, + description: r#"How a struct's fields are written."#, + values: &[Value { id: r#"by_position"#, meaning: r#"back to back, in declaration order"# }, Value { id: r#"by_name"#, meaning: r#"each keyed by its name"# }], + establishes: None, + status: Status::Stable, + replaced_by: None, + since: None, + }, + Fact { + id: r#"unknown_field"#, + applies_to: Kind::Format, + title: r#"unknown field"#, + description: r#"What a reader does with a field its type doesn't have."#, + values: &[Value { id: r#"rejected"#, meaning: r#"refuses the value"# }, Value { id: r#"ignored"#, meaning: r#"skips the field"# }], + establishes: None, + status: Status::Stable, + replaced_by: None, + since: None, + }, + Fact { + id: r#"missing_optional"#, + applies_to: Kind::Format, + title: r#"missing optional"#, + description: r#"What a reader does when an optional field isn't there."#, + values: &[Value { id: r#"rejected"#, meaning: r#"refuses the value"# }, Value { id: r#"absent"#, meaning: r#"reads it as absent"# }], + establishes: None, + status: Status::Stable, + replaced_by: None, + since: None, + }, + Fact { + id: r#"enum_variants"#, + applies_to: Kind::Format, + title: r#"enum variants"#, + description: r#"How an enum's variant is written."#, + values: &[Value { id: r#"by_name"#, meaning: r#"by its name"# }, Value { id: r#"by_index"#, meaning: r#"by its position in the declaration"# }], + establishes: None, + status: Status::Stable, + replaced_by: None, + since: None, + }, + Fact { + id: r#"unknown_variant"#, + applies_to: Kind::Format, + title: r#"unknown variant"#, + description: r#"What a reader does with a variant its type doesn't have."#, + values: &[Value { id: r#"rejected"#, meaning: r#"refuses the value"# }, Value { id: r#"tolerated"#, meaning: r#"reads it without failing"# }], + establishes: None, + status: Status::Stable, + replaced_by: None, + since: None, + }, + Fact { + id: r#"integer_width"#, + applies_to: Kind::Format, + title: r#"integer width"#, + description: r#"Whether an integer reads into another width."#, + values: &[Value { id: r#"by_value"#, meaning: r#"any width the value fits"# }, Value { id: r#"exact"#, meaning: r#"only the width it was written as"# }], + establishes: None, + status: Status::Stable, + replaced_by: None, + since: None, + }, + Fact { + id: r#"output"#, + applies_to: Kind::Format, + title: r#"output"#, + description: r#"The grammar a format writes in, which a framing's `payload` must take."#, + values: &[Value { id: r#"bytes"#, meaning: r#"bytes in no grammar a framing reads"# }, Value { id: r#"json"#, meaning: r#"JSON text"# }], + establishes: None, + status: Status::Stable, + replaced_by: None, + since: Some(r#"0.7"#), + }, + Fact { + id: r#"calls"#, + applies_to: Kind::Framing, + title: r#"calls"#, + description: r#"How a request names the function it calls."#, + values: &[Value { id: r#"by_index"#, meaning: r#"by the function's position"# }, Value { id: r#"by_name"#, meaning: r#"by the function's name"# }, Value { id: r#"by_id"#, meaning: r#"by the protocol's and function's wire numbers"# }], + establishes: None, + status: Status::Stable, + replaced_by: None, + since: None, + }, + Fact { + id: r#"directions"#, + applies_to: Kind::Framing, + title: r#"directions"#, + description: r#"Whether a frame says if it's a request or a response."#, + values: &[Value { id: r#"both"#, meaning: r#"it does, so calls can go both ways on one transport"# }, Value { id: r#"one"#, meaning: r#"it doesn't"# }], + establishes: None, + status: Status::Stable, + replaced_by: None, + since: Some(r#"0.10"#), + }, + Fact { + id: r#"errors"#, + applies_to: Kind::Framing, + title: r#"errors"#, + description: r#"How an error response names its error."#, + values: &[Value { id: r#"by_ordinal"#, meaning: r#"by its position in the declaration"# }, Value { id: r#"by_name"#, meaning: r#"by its name"# }], + establishes: None, + status: Status::Stable, + replaced_by: None, + since: None, + }, + Fact { + id: r#"payload"#, + applies_to: Kind::Framing, + title: r#"payload"#, + description: r#"What a framing needs of a payload, which a format's `output` must fit."#, + values: &[Value { id: r#"opaque"#, meaning: r#"any bytes, carried as they are"# }, Value { id: r#"json"#, meaning: r#"JSON text, written into the frame"# }], + establishes: None, + status: Status::Stable, + replaced_by: None, + since: Some(r#"0.7"#), + }, + Fact { + id: r#"schema_agreement"#, + applies_to: Kind::Handshake, + title: r#"schema agreement"#, + description: r#"Whether the handshake checks both ends were built from compatible schemas."#, + values: &[Value { id: r#"true"#, meaning: r#"it does, and refuses a peer that wasn't"# }, Value { id: r#"false"#, meaning: r#"it doesn't"# }], + establishes: Some(r#"schema_agreement"#), + status: Status::Stable, + replaced_by: None, + since: Some(r#"0.2"#), + }, + Fact { + id: r#"encoding_agreement"#, + applies_to: Kind::Handshake, + title: r#"encoding agreement"#, + description: r#"Whether the handshake checks both ends use the same format and framing."#, + values: &[Value { id: r#"true"#, meaning: r#"it does, and refuses a peer that doesn't"# }, Value { id: r#"false"#, meaning: r#"it doesn't"# }], + establishes: Some(r#"encoding_agreement"#), + status: Status::Stable, + replaced_by: None, + since: Some(r#"0.2"#), + }, +]; + +/// Every official encoding, by its wire name. +pub const ENCODINGS: &[Encoding] = &[ + Encoding { + id: r#"packed@1"#, + kind: Kind::Format, + title: r#"packed"#, + description: r#"Fixed-width fields back to back, varint lengths, no tags."#, + aliases: &[], + frozen_as: None, + profile: true, + feature: Some(r#"format-packed"#), + tags: &[r#"official"#], + status: Status::Stable, + replaced_by: None, + since: None, + }, + Encoding { + id: r#"json@1"#, + kind: Kind::Format, + title: r#"JSON"#, + description: r#"Values as UTF-8 JSON: a struct as an object."#, + aliases: &[], + frozen_as: None, + profile: true, + feature: Some(r#"format-json"#), + tags: &[r#"official"#], + status: Status::Stable, + replaced_by: None, + since: None, + }, + Encoding { + id: r#"msgpack@1"#, + kind: Kind::Format, + title: r#"MessagePack"#, + description: r#"Values as MessagePack: a struct as an array."#, + aliases: &[], + frozen_as: None, + profile: true, + feature: Some(r#"format-msgpack"#), + tags: &[r#"official"#], + status: Status::Stable, + replaced_by: None, + since: None, + }, + Encoding { + id: r#"jsonrpc@1"#, + kind: Kind::Framing, + title: r#"JSON-RPC"#, + description: r#"JSON-RPC 2.0 requests and responses."#, + aliases: &[r#"jsonrpc"#, r#"json-rpc"#, r#"jsonrpc-2.0"#], + frozen_as: Some(r#"jsonrpc"#), + profile: true, + feature: Some(r#"framing-jsonrpc"#), + tags: &[r#"official"#], + status: Status::Stable, + replaced_by: None, + since: None, + }, + Encoding { + id: r#"datagram@1"#, + kind: Kind::Framing, + title: r#"datagram"#, + description: r#"Comline's own binary framing of requests and responses."#, + aliases: &[r#"datagram"#, r#"comline.datagram"#], + frozen_as: Some(r#"datagram"#), + profile: true, + feature: Some(r#"framing-datagram"#), + tags: &[r#"official"#], + status: Status::Stable, + replaced_by: None, + since: None, + }, + Encoding { + id: r#"datagram@2"#, + kind: Kind::Framing, + title: r#"datagram@2"#, + description: r#"The datagram framing with a request/reply kind and a protocol number, for calls both ways and several protocols on one connection."#, + aliases: &[], + frozen_as: None, + profile: true, + feature: Some(r#"framing-datagram2"#), + tags: &[r#"official"#], + status: Status::Stable, + replaced_by: None, + since: None, + }, + Encoding { + id: r#"datagram@3"#, + kind: Kind::Framing, + title: r#"datagram@3"#, + description: r#"The datagram framing that addresses a call by its protocol's and function's wire numbers."#, + aliases: &[], + frozen_as: None, + profile: true, + feature: Some(r#"framing-datagram3"#), + tags: &[r#"official"#], + status: Status::Stable, + replaced_by: None, + since: Some(r#"0.4"#), + }, + Encoding { + id: r#"comline@1"#, + kind: Kind::Handshake, + title: r#"comline@1"#, + description: r#"The fixed frame each end sends first: magic, version, IR hash, wire format, framing and capabilities."#, + aliases: &[], + frozen_as: None, + profile: true, + feature: Some(r#"handshake"#), + tags: &[r#"official"#], + status: Status::Stable, + replaced_by: None, + since: None, + }, + Encoding { + id: r#"comline@2"#, + kind: Kind::Handshake, + title: r#"comline@2"#, + description: r#"The compatibility-checking handshake: lines, shape hashes and wire numbers instead of an exact IR match."#, + aliases: &[], + frozen_as: None, + profile: true, + feature: Some(r#"handshake-comline2"#), + tags: &[r#"official"#], + status: Status::Stable, + replaced_by: None, + since: Some(r#"0.4"#), + }, + Encoding { + id: r#"none"#, + kind: Kind::Handshake, + title: r#"no handshake"#, + description: r#"No frame and nothing checked. Unversioned, since there is nothing to evolve; it establishes neither agreement."#, + aliases: &[], + frozen_as: None, + profile: false, + feature: None, + tags: &[r#"official"#], + status: Status::Stable, + replaced_by: None, + since: None, + }, +]; + +/// Every wire feature, in the order `features.json` lists them. +pub const FEATURES: &[Feature] = &[ + Feature { + id: r#"name-hash"#, + title: r#"name hash"#, + description: r#"FNV-1a 64 of a wire format or framing name, as the handshake carries it"#, + status: Status::Stable, + replaced_by: None, + since: None, + }, + Feature { + id: r#"handshake"#, + title: r#"handshake"#, + description: r#"the fixed 31-byte frame each side sends first: magic, version, IR hash, wire format, framing, capabilities"#, + status: Status::Stable, + replaced_by: None, + since: None, + }, + Feature { + id: r#"envelope"#, + title: r#"envelope"#, + description: r#"a response's outcome: ok with a payload, an error ordinal with its body, or a provider fault"#, + status: Status::Stable, + replaced_by: None, + since: None, + }, + Feature { + id: r#"framing-datagram"#, + title: r#"datagram framing"#, + description: r#"Comline's own binary framing of requests and responses"#, + status: Status::Stable, + replaced_by: None, + since: None, + }, + Feature { + id: r#"framing-datagram2"#, + title: r#"datagram@2 framing"#, + description: r#"the datagram framing with a request/reply kind and a protocol number, for calls in both directions and several protocols on one connection"#, + status: Status::Stable, + replaced_by: None, + since: None, + }, + Feature { + id: r#"framing-datagram3"#, + title: r#"datagram@3 framing"#, + description: r#"the datagram framing that addresses a call by its protocol's and function's wire numbers, both varints (calls = by_id); replies as datagram@2"#, + status: Status::Stable, + replaced_by: None, + since: Some(r#"0.4"#), + }, + Feature { + id: r#"handshake-comline2"#, + title: r#"comline@2 handshake"#, + description: r#"the compatibility-checking handshake: its hello, answer and verdict messages, and its negotiation steps under every parameter, with every refusal reason"#, + status: Status::Stable, + replaced_by: None, + since: Some(r#"0.4"#), + }, + Feature { + id: r#"stream"#, + title: r#"stream framing"#, + description: r#"frames on a byte stream (TCP, a pipe): each behind a u32 little-endian length prefix, refused over the limit, by default 16 MiB (16777216 bytes) in every runtime"#, + status: Status::Stable, + replaced_by: None, + since: None, + }, + Feature { + id: r#"framing-jsonrpc"#, + title: r#"JSON-RPC framing"#, + description: r#"JSON-RPC 2.0 requests and responses, a fault as -32603"#, + status: Status::Stable, + replaced_by: None, + since: None, + }, + Feature { + id: r#"format-json"#, + title: r#"JSON wire format"#, + description: r#"values as UTF-8 JSON: a struct as an object"#, + status: Status::Stable, + replaced_by: None, + since: None, + }, + Feature { + id: r#"format-msgpack"#, + title: r#"MessagePack wire format"#, + description: r#"values as MessagePack: a struct as an array"#, + status: Status::Stable, + replaced_by: None, + since: None, + }, + Feature { + id: r#"format-packed"#, + title: r#"packed wire format"#, + description: r#"values as packed@1: fixed-width fields back to back, varint lengths, no tags"#, + status: Status::Stable, + replaced_by: None, + since: None, + }, +]; + +/// Guarantee ids. +pub mod guarantee { + pub const SCHEMA_AGREEMENT: &str = r#"schema_agreement"#; + pub const ENCODING_AGREEMENT: &str = r#"encoding_agreement"#; + pub const ENCODING_CONFORMANCE: &str = r#"encoding_conformance"#; + pub const VERSION_HONESTY: &str = r#"version_honesty"#; + pub const BOUNDED_SIZE: &str = r#"bounded_size"#; +} + +/// Fact ids. +pub mod fact { + pub const STRUCT_FIELDS: &str = r#"struct_fields"#; + pub const UNKNOWN_FIELD: &str = r#"unknown_field"#; + pub const MISSING_OPTIONAL: &str = r#"missing_optional"#; + pub const ENUM_VARIANTS: &str = r#"enum_variants"#; + pub const UNKNOWN_VARIANT: &str = r#"unknown_variant"#; + pub const INTEGER_WIDTH: &str = r#"integer_width"#; + pub const OUTPUT: &str = r#"output"#; + pub const CALLS: &str = r#"calls"#; + pub const DIRECTIONS: &str = r#"directions"#; + pub const ERRORS: &str = r#"errors"#; + pub const PAYLOAD: &str = r#"payload"#; + pub const SCHEMA_AGREEMENT: &str = r#"schema_agreement"#; + pub const ENCODING_AGREEMENT: &str = r#"encoding_agreement"#; +} + +/// Encoding wire names. +pub mod encoding { + pub const PACKED_1: &str = r#"packed@1"#; + pub const JSON_1: &str = r#"json@1"#; + pub const MSGPACK_1: &str = r#"msgpack@1"#; + pub const JSONRPC_1: &str = r#"jsonrpc@1"#; + pub const DATAGRAM_1: &str = r#"datagram@1"#; + pub const DATAGRAM_2: &str = r#"datagram@2"#; + pub const DATAGRAM_3: &str = r#"datagram@3"#; + pub const COMLINE_1: &str = r#"comline@1"#; + pub const COMLINE_2: &str = r#"comline@2"#; + pub const NONE: &str = r#"none"#; +} + +/// Feature ids. +pub mod feature { + pub const NAME_HASH: &str = r#"name-hash"#; + pub const HANDSHAKE: &str = r#"handshake"#; + pub const ENVELOPE: &str = r#"envelope"#; + pub const FRAMING_DATAGRAM: &str = r#"framing-datagram"#; + pub const FRAMING_DATAGRAM2: &str = r#"framing-datagram2"#; + pub const FRAMING_DATAGRAM3: &str = r#"framing-datagram3"#; + pub const HANDSHAKE_COMLINE2: &str = r#"handshake-comline2"#; + pub const STREAM: &str = r#"stream"#; + pub const FRAMING_JSONRPC: &str = r#"framing-jsonrpc"#; + pub const FORMAT_JSON: &str = r#"format-json"#; + pub const FORMAT_MSGPACK: &str = r#"format-msgpack"#; + pub const FORMAT_PACKED: &str = r#"format-packed"#; +} + +/// The catalog's sets. +pub mod sets { + pub const GUARANTEES: &[&str] = &[r#"schema_agreement"#, r#"encoding_agreement"#, r#"encoding_conformance"#, r#"version_honesty"#, r#"bounded_size"#]; + pub const FORMATS: &[&str] = &[r#"packed@1"#, r#"json@1"#, r#"msgpack@1"#]; + pub const FRAMINGS: &[&str] = &[r#"jsonrpc@1"#, r#"datagram@1"#, r#"datagram@2"#, r#"datagram@3"#]; + pub const HANDSHAKES: &[&str] = &[r#"comline@1"#, r#"comline@2"#, r#"none"#]; + pub const OFFICIAL: &[&str] = &[r#"packed@1"#, r#"json@1"#, r#"msgpack@1"#, r#"jsonrpc@1"#, r#"datagram@1"#, r#"datagram@2"#, r#"datagram@3"#, r#"comline@1"#, r#"comline@2"#, r#"none"#]; + pub const PROFILED: &[&str] = &[r#"packed@1"#, r#"json@1"#, r#"msgpack@1"#, r#"jsonrpc@1"#, r#"datagram@1"#, r#"datagram@2"#, r#"datagram@3"#, r#"comline@1"#, r#"comline@2"#]; + pub const FORMAT_FACTS: &[&str] = &[r#"struct_fields"#, r#"unknown_field"#, r#"missing_optional"#, r#"enum_variants"#, r#"unknown_variant"#, r#"integer_width"#, r#"output"#]; + pub const FRAMING_FACTS: &[&str] = &[r#"calls"#, r#"directions"#, r#"errors"#, r#"payload"#]; + pub const HANDSHAKE_FACTS: &[&str] = &[r#"schema_agreement"#, r#"encoding_agreement"#]; + pub const FEATURES: &[&str] = &[r#"name-hash"#, r#"handshake"#, r#"envelope"#, r#"framing-datagram"#, r#"framing-datagram2"#, r#"framing-datagram3"#, r#"handshake-comline2"#, r#"stream"#, r#"framing-jsonrpc"#, r#"format-json"#, r#"format-msgpack"#, r#"format-packed"#]; + /// Every alias, with the wire name it resolves to. + pub const ALIASES: &[(&str, &str)] = &[(r#"jsonrpc"#, r#"jsonrpc@1"#), (r#"json-rpc"#, r#"jsonrpc@1"#), (r#"jsonrpc-2.0"#, r#"jsonrpc@1"#), (r#"datagram"#, r#"datagram@1"#), (r#"comline.datagram"#, r#"datagram@1"#)]; +} + diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..cd3531b --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,234 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! The Comline specification's catalog of shared names, as Rust constants: +//! the guarantees, the facts and the values each may take, the official +//! encodings with their aliases, and the wire features, with the sets they +//! form. `catalog/` in the specification is the source; [`catalog`]'s data is +//! generated from it by `specs/check.sh`, and CI fails when it's stale. +//! +//! The ids are published names: they never change. A rename adds an entry and +//! deprecates the old one ([`Status::Deprecated`], `replaced_by`). + +#![no_std] + +mod catalog; + +pub use catalog::{ + encoding, fact, feature, guarantee, sets, ENCODINGS, FACTS, FEATURES, GUARANTEES, + SPEC_VERSION, +}; + +/// What a fact applies to, and what kind of encoding a name is. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Kind { + Format, + Framing, + Handshake, +} + +impl Kind { + /// As the catalog and profiles write it: `format`, `framing`, `handshake`. + pub const fn as_str(self) -> &'static str { + match self { + Kind::Format => "format", + Kind::Framing => "framing", + Kind::Handshake => "handshake", + } + } +} + +/// What gives a guarantee up. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Origin { + /// The handshake. + Handshake, + /// The format and framing. + Encoding, + /// A handshake policy. + Policy, + /// A type. + Type, +} + +/// Where an entry stands. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Status { + Stable, + Experimental, + /// Kept for what already uses it; `replaced_by` names what to use instead. + Deprecated, +} + +/// A guarantee Comline gives between peers, which a protocol can give up +/// with `@unverified`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Guarantee { + pub id: &'static str, + pub title: &'static str, + pub description: &'static str, + pub comes_from: Origin, + pub status: Status, + pub replaced_by: Option<&'static str>, + /// The spec version that introduced it, where the changelog names one. + pub since: Option<&'static str>, +} + +/// One value a fact may take, and what it means. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Value { + pub id: &'static str, + pub meaning: &'static str, +} + +/// A fact an encoding's profile states. A handshake's facts are booleans +/// (`"true"`, `"false"`), and `true` establishes the guarantee named in +/// `establishes`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Fact { + pub id: &'static str, + pub applies_to: Kind, + pub title: &'static str, + pub description: &'static str, + pub values: &'static [Value], + pub establishes: Option<&'static str>, + pub status: Status, + pub replaced_by: Option<&'static str>, + pub since: Option<&'static str>, +} + +impl Fact { + /// Whether `value` is one this fact may take. + pub fn allows(&self, value: &str) -> bool { + self.values.iter().any(|v| v.id == value) + } +} + +/// An official encoding, by its wire name. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Encoding { + pub id: &'static str, + pub kind: Kind, + pub title: &'static str, + pub description: &'static str, + /// Other spellings a schema may write, resolved to `id`. + pub aliases: &'static [&'static str], + /// The spelling core freezes into the IR, where it isn't `id`. + pub frozen_as: Option<&'static str>, + /// Whether `profiles/.json` states its facts. + pub profile: bool, + /// The feature whose vectors prove it. + pub feature: Option<&'static str>, + pub tags: &'static [&'static str], + pub status: Status, + pub replaced_by: Option<&'static str>, + pub since: Option<&'static str>, +} + +impl Encoding { + /// The spelling core freezes: `frozen_as`, or else the wire name. + pub const fn frozen(&self) -> &'static str { + match self.frozen_as { + Some(spelling) => spelling, + None => self.id, + } + } +} + +/// A wire feature: a set of vector cases, `vectors/.json`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Feature { + pub id: &'static str, + pub title: &'static str, + pub description: &'static str, + pub status: Status, + pub replaced_by: Option<&'static str>, + pub since: Option<&'static str>, +} + +/// The guarantee `id`. +pub fn guarantee(id: &str) -> Option<&'static Guarantee> { + GUARANTEES.iter().find(|g| g.id == id) +} + +/// The fact `id`. +pub fn fact(id: &str) -> Option<&'static Fact> { + FACTS.iter().find(|f| f.id == id) +} + +/// The facts a profile of `kind` states, in the order profiles list them. +pub fn facts_of(kind: Kind) -> impl Iterator { + FACTS.iter().filter(move |f| f.applies_to == kind) +} + +/// The official encoding whose wire name is `id`. +pub fn encoding(id: &str) -> Option<&'static Encoding> { + ENCODINGS.iter().find(|e| e.id == id) +} + +/// The official encoding `name` spells: its wire name or one of its aliases. +pub fn resolve(name: &str) -> Option<&'static Encoding> { + encoding(name).or_else(|| ENCODINGS.iter().find(|e| e.aliases.contains(&name))) +} + +/// The feature `id`. +pub fn feature(id: &str) -> Option<&'static Feature> { + FEATURES.iter().find(|f| f.id == id) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_id_constant_resolves() { + for id in sets::GUARANTEES { + assert!(guarantee(id).is_some(), "{id}"); + } + for id in sets::OFFICIAL { + assert!(encoding(id).is_some(), "{id}"); + } + for id in sets::FEATURES { + assert!(feature(id).is_some(), "{id}"); + } + assert_eq!(guarantee(guarantee::BOUNDED_SIZE).unwrap().title, "bounded size"); + assert_eq!(encoding(encoding::PACKED_1).unwrap().kind, Kind::Format); + } + + #[test] + fn aliases_resolve_to_their_wire_name() { + for (alias, id) in sets::ALIASES { + assert_eq!(resolve(alias).map(|e| e.id), Some(*id), "{alias}"); + } + assert_eq!(resolve("json-rpc").unwrap().id, encoding::JSONRPC_1); + assert_eq!(resolve(encoding::DATAGRAM_1).unwrap().frozen(), "datagram"); + assert_eq!(resolve(encoding::PACKED_1).unwrap().frozen(), encoding::PACKED_1); + assert!(resolve("packed@2").is_none()); + } + + #[test] + fn facts_match_their_sets_in_order() { + let ids = |kind| facts_of(kind).map(|f| f.id); + assert!(ids(Kind::Format).eq(sets::FORMAT_FACTS.iter().copied())); + assert!(ids(Kind::Framing).eq(sets::FRAMING_FACTS.iter().copied())); + assert!(ids(Kind::Handshake).eq(sets::HANDSHAKE_FACTS.iter().copied())); + assert!(fact(fact::CALLS).unwrap().allows("by_id")); + assert!(!fact(fact::CALLS).unwrap().allows("by_guess")); + } + + #[test] + fn handshake_facts_establish_guarantees() { + for f in facts_of(Kind::Handshake) { + let g = f.establishes.expect("a handshake fact establishes a guarantee"); + assert!(guarantee(g).is_some(), "{g}"); + assert!(f.allows("true") && f.allows("false")); + } + } + + #[test] + fn the_spec_version_is_a_version() { + let (major, minor) = SPEC_VERSION.split_once('.').unwrap(); + assert!(major.parse::().is_ok() && minor.parse::().is_ok()); + } +} diff --git a/specs/check.sh b/specs/check.sh index 4981511..73fb484 100755 --- a/specs/check.sh +++ b/specs/check.sh @@ -1,9 +1,9 @@ #!/bin/sh # Generates gen/*.schema.json (and benches/bench.schema.json, which ships with # the benches so a vendored copy of them is whole) from the specs, and -# features.json and catalog/gen/catalog.json from the catalog, and vets every -# corpus file against them. `--check` fails instead of writing when a -# generated file is stale. +# features.json, catalog/gen/catalog.json and rust/src/catalog.rs from the +# catalog, and vets every corpus file against them. `--check` fails instead of +# writing when a generated file is stale. set -eu cd "$(dirname "$0")" @@ -58,6 +58,20 @@ cue vet -c ../catalog || failed=1 export_json features_json ../features.json export_json catalog ../catalog/gen/catalog.json +# The catalog as Rust data, for the comline-catalog crate (rust/), with the +# spec's version from VERSION. Tabs become spaces, as Rust is written. +rust_catalog() { + cue export ../catalog -t "version=$(cat ../VERSION)" -e rust_catalog --out text | expand -t 4 +} +if $check; then + if ! rust_catalog | cmp -s - ../rust/src/catalog.rs; then + echo "../rust/src/catalog.rs is out of date: run specs/check.sh" >&2 + failed=1 + fi +else + rust_catalog > ../rust/src/catalog.rs +fi + # profiles/ and vectors/ hold exactly what the catalog names. same_names() { # if [ "$(printf '%s\n' "$2" | sort)" != "$(printf '%s\n' "$3" | sort)" ]; then From 32bb16982638e8ddd1b2ec695c3c737d5e940341 Mon Sep 17 00:00:00 2001 From: Kinflou Date: Mon, 9 Nov 2026 22:29:17 +0800 Subject: [PATCH 2/2] catalog readme: reflow --- catalog/readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/catalog/readme.md b/catalog/readme.md index c0dfa6b..245b52a 100644 --- a/catalog/readme.md +++ b/catalog/readme.md @@ -50,8 +50,8 @@ A third party's encoding isn't an entry here: its package declares it in ## Changing it Edit the `.cue` files, then run `specs/check.sh` to regenerate -`features.json`, `gen/catalog.json` and `rust/src/catalog.rs`. CI runs `specs/check.sh --check`, -which fails when any of them is stale. +`features.json`, `gen/catalog.json` and `rust/src/catalog.rs`. CI runs +`specs/check.sh --check`, which fails when any of them is stale. Order matters where it shows: facts are listed in the order profiles list them, and features in the order `features.json` does.