acbf7ea
use utoipa::OpenApi;
#[derive(OpenApi)]
#[openapi(info(title = "gitdata api", version = "1.0"))]
pub struct WebApiDoc;
const CONVENTIONS: &str = r#"
## Conventions
### Response envelope
Every JSON response uses the same envelope:
| field | meaning |
|---|---|
| `code` | always equal to the HTTP status code |
| `message` | human-readable result / error summary |
| `data` | payload, `null` for errors and empty results |
| `error` | field-level / technical detail (e.g. validation errors), may be `null` |
| `error_code` | stable machine-readable code on 4xx (e.g. `conflict`, `rate_limited`), may be `null` |
| `request_id` / `log_id` | correlation ids, also returned as `X-Request-Id` / `X-Log-Id` headers |
Successful `POST` creation returns `200` (not `201`); empty results use `200` with `data: null`.
- `POST /auth/logout` is idempotent: repeating it always succeeds with `200`.
### Authentication
- `session_cookie`: KenOAuth access token carried in the `gitdata_session`
cookie. `GET /auth/login` starts the OIDC authorization-code + PKCE flow;
`POST /auth/refresh` rotates the access/refresh pair; `POST /auth/logout`
revokes the KenOAuth session and clears both cookies.
- `basic_access_key`: HTTP Basic auth with a KenOAuth-issued access key.
**Scope: git repository endpoints only** (`/repos/*`, `/graphql`); it is NOT
accepted by chat/issues/space/user endpoints.
### URL & parameter style
- Paths are `kebab-case`, plural nouns, nesting expresses ownership.
- Query/path/JSON field names are `snake_case`; enum values are `snake_case`.
- All endpoints are versioned under `/api/v1`. `/api/v1/internal/*` is guarded
by the `x-gitdata-service-key` header and is NOT documented here.
- State-transition actions use `POST {resource}/{id}/{verb}` (close/reopen);
idempotent whole-resource state uses `PUT` (default branch, pin, read-state);
partial updates use `PATCH`.
### Pagination
- Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
response `data` is `{ items, total, limit, offset }`.
- Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
response contains `has_more`.
### PATCH semantics (three-state fields)
Update commands use `Option<Option<T>>` for clearable fields:
- field **absent** → unchanged
- field `null` → cleared
- field value → set
Example: `{"milestone_id": null}` unassigns the milestone; omitting
`milestone_id` leaves it untouched.
### 2FA
TOTP enrollment, recovery codes and sensitive-operation verification are
handled by KenOAuth; GitData only proxies the account-management endpoints.
### Operations
- `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
- `GET /readyz` — readiness probe: database + redis check, `503` when down.
- Runtime metrics are pushed to the configured OTLP collector (30s interval);
there is no scrape endpoint.
- Cross-origin browser access is disabled by default; the API expects a
same-origin reverse proxy. Enable `web.cors_allow_origins` to allowlist
other origins.
### Abuse controls
- Creation endpoints are subject to per-user rolling 24h quotas
(`[quota]` config; 0 disables). Exceeding a quota returns `429`
with `error_code: "rate_limited"`.
### Idempotency
- `POST /chat/rooms/{room_id}/messages` accepts `nonce`: unique per room;
retrying with the same nonce returns the original message instead of a duplicate.
### Resource references
- User references in responses embed a `user` object (`UserDto`) wherever a
single person is shown; raw uuid fields are named `*_id` consistently.
- Viewer-specific state is embedded, not fetched separately: `RepoDto.me`
carries `starred`/`watch_mode`, `IssueDto.viewer_watch` carries the caller
watch mode. `PUT /repos/{space}/{repo}/star|unstar` style reads do not exist.
### Identifiers
- Issues are addressable by UUID **or** board-scoped number
(e.g. `/issues/{space}/boards/{board}/issues/123`).
- Binary uploads use raw request bodies with a content-type header
(e.g. `PUT /me/avatar` accepts `image/*` bytes, not base64 JSON).
"#;
pub fn api_doc() -> utoipa::openapi::OpenApi {
let mut doc = WebApiDoc::openapi();
let dbg = std::env::var("OPENAPI_DEBUG").is_ok();
macro_rules! m {
($name:literal, $expr:expr) => {{
if dbg {
eprintln!("merge: {}", $name);
}
doc.merge($expr);
if dbg {
eprintln!("done: {}", $name);
}
}};
}
m!("auth", auth::openapi::AuthApiDoc::openapi());
m!("chat", chat::openapi::ChatApiDoc::openapi());
m!("issues", issues::openapi::IssuesApiDoc::openapi());
m!("space", space::openapi::SpaceApiDoc::openapi());
m!("user", user::openapi::UserApiDoc::openapi());
m!("git", git::openapi::GitApiDoc::openapi());
m!("pulls", pulls::openapi::PullsApiDoc::openapi());
m!("cleanup", crate::cleanup::CleanupApiDoc::openapi());
doc.servers = Some(vec![utoipa::openapi::Server::new("/api/v1")]);
doc.info.description = Some(CONVENTIONS.to_string());
doc.info.license = None;
{
use utoipa::openapi::security::{
ApiKey, ApiKeyValue, Http, HttpAuthScheme, SecurityScheme,
};
let components = doc
.components
.get_or_insert_with(utoipa::openapi::Components::default);
components.add_security_scheme(
"session_cookie",
SecurityScheme::ApiKey(ApiKey::Cookie(ApiKeyValue::new("gitdata_session"))),
);
components.add_security_scheme(
"basic_access_key",
SecurityScheme::Http(Http::new(HttpAuthScheme::Basic)),
);
}
doc.security = Some(vec![
utoipa::openapi::security::SecurityRequirement::new("session_cookie", Vec::<String>::new()),
utoipa::openapi::security::SecurityRequirement::new(
"basic_access_key",
Vec::<String>::new(),
),
]);
doc
}