diff --git a/.github/workflows/ci.yml b/.gitea/workflows/ci.yml similarity index 100% rename from .github/workflows/ci.yml rename to .gitea/workflows/ci.yml diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c277608 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,108 @@ +# 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](https://www.getoutline.com) 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`): +```sh +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 ` (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`): +```sh +OUTLINE_API_KEY=ol_api_... cargo test -- --ignored +``` + +Run from `cli/` (separate workspace, `cd cli` first): +```sh +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/.` 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`** — `Client` 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.()` 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/`.