Files
henning 176667d038
CI / check (push) Successful in 3m24s
CI / test (push) Skipped
ci: move workflow from .github to .gitea directory for Gitea Actions
2026-07-30 17:43:30 +02:00

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 name outline) — 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 own Cargo.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.rsClient holds a reqwest::Client, base URL, and Auth, wrapped in an Arc (cheap to clone, shared connection pool). All real HTTP happens through Client::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 a const MethodDef (name + idempotent flag) at the top of its api/*.rs file and passed into one of these three.
  • envelope.rs — the {data, pagination, policies} wrapper every success response is deserialized into before being unwrapped by Client::rpc/rpc_list.
  • api/ — one module per resource (auth, documents, collections, users), each exposed as client.<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 a const MethodDef, add a method on the resource's *Api struct, and (for list/search) add a params struct implementing Paginated plus a builder.
  • page.rs — pagination is generic over any params type implementing Paginated (i.e. embeds PageParams via #[serde(flatten)]). Paginator::next_page advances offset itself using the returned item count (a page shorter than the requested limit means done) — it does not trust the API's nextPath hint, since that field isn't part of the published spec. paginate() also gets a into_stream() under the stream feature.
  • error.rs — single Error enum (Config, Transport, Decode, Api). Error::kind() gives a coarse ErrorKind (maps HTTP status and the API's own error string code, since Outline doesn't always use the "right" status code) for match-free handling; is_retryable() and retry_after() build on top of it.
  • models/ — response types, deliberately decoupled from Client so 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:
    • Id is a transparent String newtype, not uuid::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 — parses RateLimit-*/Retry-After response headers; attached to Error::Api so callers can back off correctly.
  • spec/spec3.json — a vendored copy of Outline's OpenAPI spec. tests/spec_coverage.rs asserts 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 — clap Parser/Subcommand definitions only; no logic.
  • commands/ — one module per subcommand, dispatched from commands/mod.rs.
  • config.rs — non-secret settings (currently just base_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/ (mode 0600) rather than /tmp, since documents may be confidential.

Testing conventions

  • tests/common/mod.rs::client_for spins up a wiremock::MockServer and returns a Client pointed 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.rs is the only suite that hits the real API; it's #[ignore]d and requires OUTLINE_API_KEY.
  • Unit tests for pure logic (e.g. base-URL normalization in builder.rs, config precedence in cli/src/config.rs) live inline in #[cfg(test)] mod tests next to the code they test, not under tests/.