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",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 useswasm_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 */);
}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::ResultThis keeps the library crates lightweight. A GUI or web consumer of panproto-core never pulls in miette’s terminal formatting code.
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 |
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.”)
# Errorssection: list each error variant that can be returned# Examplesection: preferred for complex APIs, using/// ```ignoreif 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
Buildersuffix for builders andErrorsuffix for errors - Functions: snake_case, with
check_prefix for validation functions andcmd_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
featuressparingly; only enable what each crate needs - No
unsafedependencies without explicit justification - MIT license only for all dependencies