6.1 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this is
Two crates:
outline-sdk(root, lib nameoutline) — an async Rust client library for the Outline knowledge base API. Pure API layer, no GUI/CLI code.cli/(outline-cli) — a minimal terminal client (login,search,get) built on top of the library via a path dependency. Separate Cargo workspace with its ownCargo.lock— commands run from the root do not build/test the CLI, and vice versa.
Commands
Run from the repo root (outline-sdk):
cargo fmt --check
cargo clippy --all-features --all-targets -- -D warnings
cargo test --all-features
cargo doc --no-deps --all-features
Run a single test: cargo test --all-features <test_name> (e.g. cargo test --all-features normalizes_bare_host).
Live smoke tests against the real API are #[ignore]d by default (see tests/live.rs):
OUTLINE_API_KEY=ol_api_... cargo test -- --ignored
Run from cli/ (separate workspace, cd cli first):
cargo build --release # binary at target/release/outline
cargo fmt --check
cargo clippy --all-features --all-targets -- -D warnings
CI (.github/workflows/ci.yml) runs fmt check, clippy (-D warnings), cargo test --all-features,
and cargo doc for the root crate only.
Architecture (root crate)
The Outline API is RPC-style: every endpoint is POST /api/<resource>.<action> with a JSON
body and a {ok, data, pagination, policies} envelope — there's no REST-y resource routing to
reason about. The whole crate is built around that one shape:
client.rs—Clientholds areqwest::Client, base URL, andAuth, wrapped in anArc(cheap to clone, shared connection pool). All real HTTP happens throughClient::send, which three thin wrappers sit on top of:rpc(single object →data),rpc_list(list →Page),rpc_ok(fire-and-forget, e.g.delete). Every endpoint is declared as aconst MethodDef(name +idempotentflag) at the top of itsapi/*.rsfile and passed into one of these three.envelope.rs— the{data, pagination, policies}wrapper every success response is deserialized into before being unwrapped byClient::rpc/rpc_list.api/— one module per resource (auth,documents,collections,users), each exposed asclient.<resource>()returning a*Api<'a>struct borrowing the client. List/search/create/ update calls return a builder (e.g.ListDocuments,SearchDocuments) with chained setter methods and a terminal.send()(single page) or.paginate()(see below). Adding a new endpoint means: add aconst MethodDef, add a method on the resource's*Apistruct, and (for list/search) add a params struct implementingPaginatedplus a builder.page.rs— pagination is generic over any params type implementingPaginated(i.e. embedsPageParamsvia#[serde(flatten)]).Paginator::next_pageadvancesoffsetitself using the returned item count (a page shorter than the requested limit means done) — it does not trust the API'snextPathhint, since that field isn't part of the published spec.paginate()also gets ainto_stream()under thestreamfeature.error.rs— singleErrorenum (Config,Transport,Decode,Api).Error::kind()gives a coarseErrorKind(maps HTTP status and the API's ownerrorstring code, since Outline doesn't always use the "right" status code) for match-free handling;is_retryable()andretry_after()build on top of it.models/— response types, deliberately decoupled fromClientso they can be held independently of a live connection (e.g. in GUI app state). Two forward-compatibility patterns used throughout, because the API is unversioned and self-hosted instances vary in age:Idis a transparentStringnewtype, notuuid::Uuid— an unexpected id format from an older/newer server must not fail deserialization of a whole response.- String enums carry an
Unknown(UnknownVariant)variant via#[serde(untagged)]instead of#[serde(other)], so an unrecognized value round-trips instead of causing a hard failure.
rate_limit.rs— parsesRateLimit-*/Retry-Afterresponse headers; attached toError::Apiso callers can back off correctly.spec/spec3.json— a vendored copy of Outline's OpenAPI spec.tests/spec_coverage.rsasserts every implemented RPC method name still exists as a path in it, catching typos and upstream renames without hitting the network.
Feature flags of note: stream (pagination as a futures_core::Stream), rustls-tls /
native-tls (mutually exclusive TLS backend choice), uuid (Id::as_uuid()), tracing,
multipart.
Architecture (cli/)
cli.rs— clapParser/Subcommanddefinitions only; no logic.commands/— one module per subcommand, dispatched fromcommands/mod.rs.config.rs— non-secret settings (currently justbase_url) in~/.config/outline-cli/config.toml. Precedence for the effective base URL:--base-url/$OUTLINE_URL> saved config > Outline Cloud default.secret.rs— the only module that talks to the FreeDesktop Secret Service (GNOME Keyring/KWallet) for the API key. The API key is never written to the config file. Swapping the secret backend later means rewriting only this file.- Rendered documents are cached in
~/.cache/outline-cli/(mode0600) rather than/tmp, since documents may be confidential.
Testing conventions
tests/common/mod.rs::client_forspins up awiremock::MockServerand returns aClientpointed at it with a dummy API key — the standard fixture for every non-live integration test.tests/fixtures/holds canned JSON response bodies.tests/live.rsis the only suite that hits the real API; it's#[ignore]d and requiresOUTLINE_API_KEY.- Unit tests for pure logic (e.g. base-URL normalization in
builder.rs, config precedence incli/src/config.rs) live inline in#[cfg(test)] mod testsnext to the code they test, not undertests/.