31  Coding Conventions and API Design

The panproto codebase follows a consistent set of conventions and API design patterns. Following these keeps the code predictable and the review process efficient.

31.1 Rust edition and toolchain

The workspace targets Rust Edition 2024, which requires rustc 1.85 or later. The workspace Cargo.toml enforces this:

    "crates/panproto-grammars",
    "crates/panproto-parse",
    "crates/panproto-project",
    "crates/panproto-git",
    "crates/panproto-llvm",
    "crates/panproto-xrpc",
    "crates/git-remote-cospan",
Important

Edition 2024 enables several language changes including the new use<> capture syntax and refined impl Trait behavior. If your local toolchain is older than 1.85, rustup update stable will get you there.

31.2 Workspace lints

Strict linting is configured at the workspace level. Every crate inherits these settings:

toml = "0.8"
globset = "0.4"

# Hashing
blake3 = "1"

# Benchmarking
divan = "0.1.21"

# Tree-sitter parsing
tree-sitter = "0.25"

# Workspace crates
panproto-gat = { version = "0.26.0", path = "crates/panproto-gat" }
panproto-schema = { version = "0.26.0", path = "crates/panproto-schema" }
panproto-inst = { version = "0.26.0", path = "crates/panproto-inst" }
panproto-mig = { version = "0.26.0", path = "crates/panproto-mig" }
panproto-lens = { version = "0.26.0", path = "crates/panproto-lens" }
panproto-lens-dsl = { version = "0.26.0", path = "crates/panproto-lens-dsl" }
panproto-check = { version = "0.26.0", path = "crates/panproto-check" }

Key policies:

  • unsafe_code = "deny": No unsafe code anywhere. The WASM boundary uses wasm_bindgen’s safe abstractions.
  • unwrap_used = "deny": No .unwrap() in library code. Use ?, .ok_or(), or pattern matching. Test modules annotate #[allow(clippy::unwrap_used)] where appropriate.
  • pedantic + nursery: Both clippy lint groups are enabled as warnings, catching style issues and potential bugs early.
  • missing_docs = "warn": Every public item should be documented.

The release profile enables LTO, single codegen unit, and symbol stripping for minimal binary size. The release-wasm profile inherits from release but uses opt-level = "z" for aggressive size optimization.

31.3 Immutable fluent Builder pattern

Both SchemaBuilder (Rust) and SchemaBuilder/MigrationBuilder (TypeScript) use the immutable fluent builder pattern. In Rust, SchemaBuilder takes self by value and returns Result<Self, SchemaError>:

use panproto_expr::Expr;

/// A builder for incrementally constructing a validated [`Schema`].
///
/// # Example
///
/// ```ignore
/// let schema = SchemaBuilder::new(&protocol)
///     .vertex("post", "record", Some("app.bsky.feed.post"))?
///     .vertex("post:body", "object", None)?
///     .edge("post", "post:body", "record-schema", None)?
///     .build()?;
/// ```
pub struct SchemaBuilder {
    protocol: Protocol,
    vertices: HashMap<Name, Vertex>,
    edges: Vec<Edge>,
    hyper_edges: HashMap<Name, HyperEdge>,
    constraints: HashMap<Name, Vec<Constraint>>,
    required: HashMap<Name, Vec<Edge>>,
    nsids: HashMap<Name, Name>,
    edge_set: FxHashSet<(Name, Name, Name, Option<Name>)>,
    coercions: HashMap<(Name, Name), CoercionSpec>,
    mergers: HashMap<Name, Expr>,
    defaults: HashMap<Name, Expr>,
    policies: HashMap<Name, Expr>,
}

impl SchemaBuilder {
    /// Create a new builder for the given protocol.
    #[must_use]
    pub fn new(protocol: &Protocol) -> Self {
        Self {
            protocol: protocol.clone(),
            vertices: HashMap::new(),
            edges: Vec::new(),
            hyper_edges: HashMap::new(),

Each method consumes the builder and returns a new one (or an error). This ensures:

  • No partial construction: if a step fails, the builder is consumed and can’t be reused in an inconsistent state
  • Type-level linearity: the compiler prevents using a builder after it has been moved
  • Composability: methods chain naturally with ?

In TypeScript, the same pattern uses readonly fields and returns new instances:

vertex(id: string, kind: string, options?: VertexOptions): SchemaBuilder {
  // ... validate ...
  return new SchemaBuilder(/* new state */);
}
Tip

Mark all builder constructors and pure functions with #[must_use]. This catches the common mistake of calling a builder method without binding the result.

31.4 Error design

The codebase uses a two-tier error strategy:

31.4.1 Library crates: thiserror

Every library crate defines its own error enum with thiserror:

#[derive(Debug, thiserror::Error)]
pub enum SchemaError {
    #[error("duplicate vertex: {0}")]
    DuplicateVertex(String),
    #[error("vertex not found: {0}")]
    VertexNotFound(String),
    // ...
}

These errors are structured, matchable, and carry no formatting dependencies. They compose naturally with ? across crate boundaries.

31.4.2 CLI crate: miette

Only panproto-cli uses miette for user-facing diagnostics. Library errors are converted via .into_diagnostic():

let schema = load_json(path)?;  // returns miette::Result

This keeps the library crates lightweight. A GUI or web consumer of panproto-core never pulls in miette’s terminal formatting code.

NoteWhy Two Error Crates?

thiserror generates zero-overhead error types with structured fields. miette generates rich terminal output with colors, source labels, and suggestions. Library consumers need the first; CLI users need the second. Keeping them separate respects that boundary.

31.5 #[must_use] on builders and pure functions

All builder types and pure functions are annotated with #[must_use]:

#[must_use]
pub fn new(protocol: &Protocol) -> Self { ... }

This generates a compiler warning if the return value is discarded, which catches bugs like:

builder.vertex("post", "record", None)?;  // Warning: unused result
// Should be: builder = builder.vertex(...)?;

31.6 Collection choices

The codebase makes deliberate collection choices based on usage patterns:

Collection Use Case Rationale
SmallVec<T, 4> Adjacency lists (outgoing/incoming edges) Most vertices have fewer than 4 edges; avoids heap allocation
FxHashSet Hot internal paths (edge deduplication, surviving vertex sets) Faster hashing than std::collections::HashSet for string keys
HashMap Public API surfaces Standard, unsurprising, no extra dependency for consumers
BTreeSet / BTreeMap When deterministic iteration order is needed Used in CLI output and test assertions
Note

SmallVec uses the 2.0.0-alpha series with serde support. FxHashSet comes from the rustc-hash crate, the same hasher used inside the Rust compiler.

31.7 Documentation standards

Every public item must have a doc comment. The conventions are:

  • First line: one-sentence summary in imperative mood (“Add a vertex to the schema.”)
  • # Errors section: list each error variant that can be returned
  • # Example section: preferred for complex APIs, using /// ```ignore if the example requires external setup
  • Cross-references: use [name] links to other items in the same crate
/// Add a binary edge to the schema.
///
/// Validates that both `src` and `tgt` vertices exist and that
/// the edge kind satisfies the protocol's edge rules.
///
/// # Errors
///
/// Returns [`SchemaError::VertexNotFound`] if either endpoint
/// is missing, or [`SchemaError::UnknownEdgeKind`] if the kind
/// is not recognized by the protocol.
pub fn edge(self, src: &str, tgt: &str, kind: &str, name: Option<&str>)
    -> Result<Self, SchemaError>

31.8 Naming conventions

  • Crates: panproto-<name> (kebab-case)
  • Modules: snake_case, matching the domain concept (schema, migration, existence)
  • Types: PascalCase, with Builder suffix for builders and Error suffix for errors
  • Functions: snake_case, with check_ prefix for validation functions and cmd_ prefix for CLI handlers
  • TypeScript: camelCase for functions and variables, PascalCase for classes and interfaces, matching standard JS conventions

31.9 Dependency policy

  • Prefer workspace-level dependency declarations in the root Cargo.toml
  • Pin major versions but allow minor/patch updates
  • Use features sparingly; only enable what each crate needs
  • No unsafe dependencies without explicit justification
  • MIT license only for all dependencies