From 3374b029bea44fed6ad5a46a7a05c269ce61af58 Mon Sep 17 00:00:00 2001 From: tv Date: Mon, 26 Oct 2026 21:43:42 +0100 Subject: replace utoipa by static openapi.yaml --- Cargo.toml | 1 - docs/openapi.yaml | 1697 +++++++++++++++++++++++++++++++++++++++++++++ src/api/mod.rs | 59 +- src/api/peers.rs | 40 +- src/api/store.rs | 74 +- src/state/peer.rs | 2 +- src/transport/firewall.rs | 3 +- src/transport/traffic.rs | 7 +- 8 files changed, 1729 insertions(+), 154 deletions(-) create mode 100644 docs/openapi.yaml diff --git a/Cargo.toml b/Cargo.toml index 14cc7f5..0bde0c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,5 @@ tokio = { version = "1.53", features = ["full"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } url = "2.5" -utoipa = { version = "6", features = ["axum_extras"] } utoipa-swagger-ui = { version = "10", features = ["axum"] } uuid = { version = "1", features = ["v4"] } diff --git a/docs/openapi.yaml b/docs/openapi.yaml new file mode 100644 index 0000000..f81ed4c --- /dev/null +++ b/docs/openapi.yaml @@ -0,0 +1,1697 @@ +openapi: 3.1.0 +info: + title: continuum + version: 0.1.0 + description: | + HTTP API of a continuum node: a replicated Automerge key-value store that syncs + over Mycelium. + + Every endpoint requires HTTP basic authentication (users from `--htpasswd`, + Argon2 hashes), except `/health`, `/ready`, the Swagger UI (`/swagger-ui`) and + this description (`/openapi.yaml`). + `/debug/*` exists only with `--debug-api`. + + Written by hand, and served by every node as it is (`/openapi.yaml`). + + Conventions: + * Store paths are slash-separated keys; empty segments are invalid. Keys + containing `/` can't be addressed through the path. + * Timestamps in remake and quarantine records are Unix milliseconds. + * Change hashes and epoch IDs are lowercase hex (64 characters). + * Request bodies are limited to 2 MiB, except `POST /admin/remakes` (64 MiB). + +servers: + - url: http://127.0.0.1:3000 + +security: + - basic: [] + +tags: + - name: Store + description: Reading and writing the replicated document. + - name: Peers + description: Other nodes this node knows. + - name: Admin + description: Node operation (log level, statistics, document export). + - name: Remakes + description: | + Replacing the swarm's document with a new epoch (compaction, replacement, + import), and what happened to local writes that a remake couldn't place. + - name: Debug + description: Test hooks, only with `--debug-api`. + +paths: + /store: + get: + tags: [Store] + summary: Read the whole store + description: | + The merged view of the document as JSON (an empty object if empty), or with + `Accept: text/plain` as text. Includes protocol data under `_continuum`. + + While the node rejoins a newer epoch, reads are served from the previous + document, marked with `x-continuum-stale`. + parameters: + - $ref: '#/components/parameters/conflicts' + - $ref: '#/components/parameters/heads' + responses: + '200': + $ref: '#/components/responses/Value' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + description: Reading or serializing failed. + '503': + description: Node is not ready (and has no stale view to serve). + + /store/{key}: + parameters: + - $ref: '#/components/parameters/key' + get: + tags: [Store] + summary: Read a value + description: | + The value at the path, as JSON, or with `Accept: text/plain` as text (strings + unquoted, anything else as JSON). Concurrent writes resolve as follows: + concurrent maps merge, anything else keeps the newest write. + parameters: + - $ref: '#/components/parameters/conflicts' + - $ref: '#/components/parameters/heads' + responses: + '200': + $ref: '#/components/responses/Value' + '400': + description: Invalid path. + '401': + $ref: '#/components/responses/Unauthorized' + '404': + description: No value at this path. + '500': + description: Reading or serializing failed. + '503': + description: Node is not ready (and has no stale view to serve). + put: + tags: [Store] + summary: Write a value + description: | + Writes the value, creating intermediate maps. One request is one Automerge + change. Answers with the value written (JSON, or text with + `Accept: text/plain`). + + JSON maps and arrays become Automerge maps and lists; numbers become integers + when they fit, floats otherwise; strings are plain strings (not text objects). + requestBody: + required: true + content: + application/json: + schema: {} + example: { "theme": "dark", "fontSize": 14 } + text/plain: + schema: + type: string + example: dark + responses: + '200': + $ref: '#/components/responses/Value' + '400': + description: Invalid path, malformed JSON or invalid UTF-8. + '401': + $ref: '#/components/responses/Unauthorized' + '403': + description: Protocol data (`_continuum`) and the user isn't in `protocol-writers`, or epoch metadata or the swarm ID (`_continuum/epochs`, `_continuum/swarm`, never writable). + '409': + description: The path crosses an existing non-map value. + '415': + description: Content-Type is neither `application/json` nor `text/plain` (absent means JSON). + '500': + description: Writing failed. + '503': + description: Node is not ready. + delete: + tags: [Store] + summary: Delete a value + parameters: + - name: prune + in: query + description: Also remove ancestor maps left empty. + schema: + type: boolean + default: false + responses: + '204': + description: Deleted. + '400': + description: Invalid path. + '401': + $ref: '#/components/responses/Unauthorized' + '403': + description: Protocol data (`_continuum`) and the user isn't in `protocol-writers`, or epoch metadata or the swarm ID (`_continuum/epochs`, `_continuum/swarm`, never writable). + '404': + description: No value at this path. + '500': + description: Deleting failed. + '503': + description: Node is not ready. + + /stream: + get: + tags: [Store] + summary: Follow the whole store + description: | + As `/stream/{key}`, for the store root (as `GET /store` reads it): one event + with the whole store, then one whenever anything in it changes. Events have an + empty `key`. + responses: + '200': + description: Event stream. + content: + application/x-ndjson: + schema: + $ref: '#/components/schemas/StreamEvent' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/Error' + '503': + $ref: '#/components/responses/Error' + + /stream/{key}: + parameters: + - $ref: '#/components/parameters/key' + get: + tags: [Store] + summary: Follow a value + description: | + Newline-delimited JSON: one event with the current value, then one whenever + the resolved value changes (by a local write or by sync). The connection stays + open. + responses: + '200': + description: Event stream. + content: + application/x-ndjson: + schema: + $ref: '#/components/schemas/StreamEvent' + example: | + {"key":"settings/theme","value":"dark"} + {"key":"settings/theme","deleted":true} + '400': + description: Invalid path. + '401': + $ref: '#/components/responses/Unauthorized' + '500': + description: Reading the initial value failed. + '503': + description: Node is not ready. + + /peers: + get: + tags: [Peers] + summary: List known peers + responses: + '200': + description: Peers, sorted by public key. + content: + application/json: + schema: + $ref: '#/components/schemas/PeerList' + '401': + $ref: '#/components/responses/Unauthorized' + + /peers/{peer}: + delete: + tags: [Peers] + summary: Forget a peer swarm-wide + description: | + Writes a forget record into the document and removes the peer from the member + list, so every node drops it. A forgotten node that is still alive registers + again on its next message. + parameters: + - name: peer + in: path + required: true + description: The peer's public key. + schema: + type: string + - name: force + in: query + description: '`true` or `alive`: also forget a peer that is currently alive.' + schema: + type: string + responses: + '204': + description: Forget recorded. + '400': + description: Empty key, this node's own key, or an unknown `force`. + '401': + $ref: '#/components/responses/Unauthorized' + '409': + description: The peer is alive; pass `force=alive`. + '500': + description: Writing the record failed. + '503': + description: Node is not ready. + + /admin/log: + get: + tags: [Admin] + summary: Current log filter + responses: + '200': + description: The filter in `RUST_LOG` syntax. + content: + text/plain: + schema: + type: string + example: info,continuum::swarm::sync=debug + '401': + $ref: '#/components/responses/Unauthorized' + '500': + description: The filter can't be read. + put: + tags: [Admin] + summary: Replace the log filter + description: Takes effect immediately; not kept across restarts. + requestBody: + required: true + content: + text/plain: + schema: + type: string + example: info,continuum::swarm::sync=trace + responses: + '200': + description: The filter now in effect. + content: + text/plain: + schema: + type: string + '400': + description: Not UTF-8, or not a valid filter (the reason is in `error`); the old filter stays. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + $ref: '#/components/responses/Unauthorized' + + /health: + get: + tags: [Admin] + summary: Liveness + description: The HTTP API is up. Unauthenticated. + security: [] + responses: + '200': + description: Up. + content: + text/plain: + schema: + type: string + example: "ok\n" + + /ready: + get: + tags: [Admin] + summary: Readiness + description: | + The node serves reads and writes: `200` when ready, `503` while joining or + rejoining. Unauthenticated. + security: [] + responses: + '200': + description: Ready. + content: + text/plain: + schema: + type: string + example: "ready\n" + '503': + description: Not ready (joining or rejoining). + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /admin/stats: + get: + tags: [Admin] + summary: Node statistics + description: | + JSON by default. When `Accept` names `application/openmetrics-text` or + `text/plain` (as Prometheus does), the Prometheus text format (0.0.4) instead, + with per-peer series labelled `peer` (the public key). Aliases are only in + `continuum_info` and `continuum_peer_info`, for joining. + responses: + '200': + description: Statistics. + content: + application/json: + schema: + $ref: '#/components/schemas/Stats' + text/plain; version=0.0.4: + schema: + type: string + example: | + # HELP continuum_uptime_seconds Seconds since this node started. + # TYPE continuum_uptime_seconds gauge + continuum_uptime_seconds 3600 + '401': + $ref: '#/components/responses/Unauthorized' + + /admin/document: + get: + tags: [Admin] + summary: Export the document + description: | + The whole local document as saved by Automerge (history and `_continuum` + included): a backup, and input to an `import` remake. + responses: + '200': + description: The saved document. + headers: + Content-Disposition: + schema: + type: string + example: attachment; filename="continuum--3.automerge" + X-Continuum-Swarm: + description: The swarm ID. + schema: + type: string + X-Continuum-Epoch: + description: '`:`.' + schema: + type: string + X-Continuum-Heads: + description: The document's heads (hex, comma-separated). + schema: + type: string + content: + application/octet-stream: + schema: + type: string + format: binary + '401': + $ref: '#/components/responses/Unauthorized' + '429': + description: Another export is in progress (one at a time; each holds copies of the whole document). + '500': + description: Saving failed. + '503': + description: Node is not ready. + + /admin/config: + get: + tags: [Admin] + summary: Effective configuration + description: | + Every option with its value and where it came from (`default`, `file` with the + file, or `argument`), as `--print-config` prints it. Imported values (`env:`, + `file:`, `cmd:`) are left out; `imported` lists their importers. + responses: + '200': + description: The configuration. + content: + application/json: + schema: + type: object + required: [files, options] + properties: + files: + type: array + items: + type: string + options: + type: object + additionalProperties: + type: object + required: [source] + properties: + source: + type: string + enum: [default, file, argument] + file: + type: string + imported: + type: array + items: + type: string + value: {} + example: + files: [/etc/continuum.toml] + options: + neighbours: { source: default, value: 12 } + alias: { source: argument, imported: ['env:HOSTNAME'] } + '401': + $ref: '#/components/responses/Unauthorized' + + /admin/remakes: + get: + tags: [Remakes] + summary: List remakes + description: All remake records in the local document, oldest proposal first. + responses: + '200': + description: Remakes. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RemakeReport' + '401': + $ref: '#/components/responses/Unauthorized' + post: + tags: [Remakes] + summary: Propose a remake + description: | + Proposes a remake of the current epoch; this node coordinates it and votes for + it. The electorate is fixed now: the members that renewed their alive stamp + within this node's `--electorate-window`. Each member votes for at most + one remake of its epoch (again only once that one is aborted). The coordinator commits as soon as a majority of + the electorate voted for it, and aborts once that is out of reach or + `settle` passed without it. Every node switches to the new epoch after the + switch delay. Writes made after the base are carried over (translated by the + kind's rules; what no rule covers is quarantined on the node that made it). + + An `import` can also be uploaded as the raw document with + `Content-Type: application/octet-stream`; its options then go in the query + (`rules`, `baseHeads`, `settle`, `switchDelay`). Those query parameters + are refused with a JSON body. + parameters: + - $ref: '#/components/parameters/wait' + - $ref: '#/components/parameters/remakeForce' + - name: rules + in: query + description: 'Octet-stream import only: the rules as JSON, e.g. `[{"from":"","to":""}]`.' + schema: + type: string + - name: baseHeads + in: query + description: Octet-stream import only, comma-separated hex. + schema: + type: string + - name: settle + in: query + description: Octet-stream import only; a duration (seconds, or e.g. `500ms`, `8m`). + schema: + type: string + - name: switchDelay + in: query + description: Octet-stream import only; a duration. + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RemakeRequest' + examples: + compact: + value: {} + replace: + value: + kind: replace + spec: + document: { "accounts": {} } + rules: + - { "from": "users", "to": "accounts" } + - { "from": "tmp", "drop": true } + restore: + summary: Import a backup, keeping later writes at their paths + value: + kind: import + spec: + document: + rules: + - { "from": "", "to": "" } + application/octet-stream: + schema: + type: string + format: binary + responses: + '200': + description: With `wait=true`, the final report. + content: + application/json: + schema: + $ref: '#/components/schemas/RemakeReport' + '202': + description: Proposed. + headers: + Location: + schema: + type: string + example: /admin/remakes/0f8e… + content: + application/json: + schema: + $ref: '#/components/schemas/RemakeReport' + '400': + $ref: '#/components/responses/Error' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + description: With `wait=true`, the record vanished. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '409': + description: | + Another remake of this epoch is still open (`remake` holds its ID); an + expired one still holds votes or can't be superseded yet (`expired` lists + each with `id`, `coordinator`, `coordinatorActive`, `expiredAt`, + `abortableAt` and `tally`; `force` names the override, if any); or this + node can't vote for a new one: it voted for another remake of this epoch, + or may have before it restarted. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '413': + description: Request larger than 64 MiB. + '500': + $ref: '#/components/responses/Error' + '503': + $ref: '#/components/responses/Error' + delete: + tags: [Remakes] + summary: Abort the remake in progress + description: | + Aborts whichever remake of the local epoch is still undecided (the oldest if + several), as `DELETE /admin/remakes/{id}` does. + parameters: + - $ref: '#/components/parameters/wait' + - name: force + in: query + description: 'As for `DELETE /admin/remakes/{id}`: `true`, or a comma-separated list of `coordinatorActive` and `coordinatorInactive`.' + schema: + type: string + responses: + '200': + description: Aborted (or decided otherwise meanwhile). + content: + application/json: + schema: + $ref: '#/components/schemas/RemakeReport' + '202': + description: Abort requested; the coordinator applies it within about a second. + content: + application/json: + schema: + $ref: '#/components/schemas/RemakeReport' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/Error' + '409': + $ref: '#/components/responses/AlreadyDecided' + '500': + $ref: '#/components/responses/Error' + + /admin/remakes/{id}: + parameters: + - $ref: '#/components/parameters/remakeId' + get: + tags: [Remakes] + summary: Report one remake + parameters: + - name: wait + in: query + description: Long-poll until decided (bounded by the proposal's expiry, unless adopted). + schema: + type: boolean + default: false + responses: + '200': + description: The report. + content: + application/json: + schema: + $ref: '#/components/schemas/RemakeReport' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/Error' + delete: + tags: [Remakes] + summary: Abort a remake + description: | + Only while undecided; a committed remake can't be undone. An undecided one + whose coordinator this node doesn't reach answers `409` (the coordinator + could commit first), unless `force=coordinatorInactive`. An expired remake + is aborted by this node (`200`), as its coordinator is gone, once a grace + period has passed since it expired (`409` with `abortableAt` before), and, + if this node sees its coordinator as an active peer, only with + `force=coordinatorActive`. + parameters: + - $ref: '#/components/parameters/wait' + - name: force + in: query + description: '`true`, or a comma-separated list of `coordinatorActive` (abort an expired remake whose coordinator this node sees as an active peer) and `coordinatorInactive` (request the abort of an undecided remake whose coordinator this node doesn''t reach).' + schema: + type: string + responses: + '200': + description: Aborted (or decided otherwise meanwhile). + content: + application/json: + schema: + $ref: '#/components/schemas/RemakeReport' + '202': + description: Abort requested. + content: + application/json: + schema: + $ref: '#/components/schemas/RemakeReport' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/Error' + '409': + $ref: '#/components/responses/AlreadyDecided' + '500': + $ref: '#/components/responses/Error' + + /admin/remakes/{id}/adopt: + parameters: + - $ref: '#/components/parameters/remakeId' + post: + tags: [Remakes] + summary: Adopt a remake + description: | + This node becomes the coordinator of an expired remake (its coordinator is + gone), or of one adopted before. An adopted remake has no voting period: it + commits once a majority of its electorate voted for it, and aborts once that + is out of reach or on request. Adopt on one node only. + parameters: + - $ref: '#/components/parameters/wait' + responses: + '200': + description: With `wait=true`, the final report. + content: + application/json: + schema: + $ref: '#/components/schemas/RemakeReport' + '202': + description: Adopted. + content: + application/json: + schema: + $ref: '#/components/schemas/RemakeReport' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/Error' + '409': + description: | + Still in its voting period, already decided, or not a remake of this + node's epoch. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + $ref: '#/components/responses/Error' + '503': + $ref: '#/components/responses/Error' + + /admin/epochs: + get: + tags: [Remakes] + summary: Epochs on this node + responses: + '200': + description: The local epoch, archived ones, and a catch-up with a newer epoch in progress. + content: + application/json: + schema: + $ref: '#/components/schemas/Epochs' + '401': + $ref: '#/components/responses/Unauthorized' + + /admin/quarantine: + get: + tags: [Remakes] + summary: List quarantined writes + description: | + This node's own writes that a remake had no place for, kept in memory on this + node only. + responses: + '200': + description: Entries. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/QuarantineSummary' + '401': + $ref: '#/components/responses/Unauthorized' + + /admin/quarantine/{id}: + parameters: + - $ref: '#/components/parameters/quarantineId' + get: + tags: [Remakes] + summary: Show quarantined writes + responses: + '200': + description: The entry and every path it holds, with the value the old document has there. + content: + application/json: + schema: + type: object + required: [entry, writes] + properties: + entry: + $ref: '#/components/schemas/QuarantineSummary' + writes: + type: array + items: + $ref: '#/components/schemas/QuarantinedWrite' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/Error' + '500': + $ref: '#/components/responses/Error' + delete: + tags: [Remakes] + summary: Discard quarantined writes + responses: + '204': + description: Discarded. + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/Error' + + /admin/quarantine/{id}/document: + parameters: + - $ref: '#/components/parameters/quarantineId' + get: + tags: [Remakes] + summary: Download the old document + responses: + '200': + description: The old epoch's document, as saved by Automerge. + headers: + Content-Disposition: + schema: + type: string + example: attachment; filename="quarantine-.automerge" + content: + application/octet-stream: + schema: + type: string + format: binary + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/Error' + '500': + $ref: '#/components/responses/Error' + + /admin/quarantine/{id}/replay: + parameters: + - $ref: '#/components/parameters/quarantineId' + post: + tags: [Remakes] + summary: Replay quarantined writes + description: | + Writes them into the current epoch at their old paths, as fresh writes (they + win over what is there), and removes the entry. If none can be written, the + entry stays. + responses: + '200': + description: Replayed. + content: + application/json: + schema: + type: object + required: [replayed, failed] + properties: + replayed: + type: integer + failed: + type: integer + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/Error' + '409': + description: None of the writes could be replayed (`failed` holds the count). + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '503': + $ref: '#/components/responses/Error' + + /debug/firewall: + get: + tags: [Debug] + summary: Current firewall rules + responses: + '200': + description: Rules. + content: + application/json: + schema: + $ref: '#/components/schemas/FirewallRules' + '401': + $ref: '#/components/responses/Unauthorized' + put: + tags: [Debug] + summary: Replace the firewall rules + description: Drops messages from (`inbound`) or to (`outbound`) these peers, like a lossy network. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FirewallRules' + responses: + '204': + description: Replaced. + '401': + $ref: '#/components/responses/Unauthorized' + '422': + description: Invalid body. + +components: + securitySchemes: + basic: + type: http + scheme: basic + + parameters: + key: + name: key + in: path + required: true + description: Slash-separated store path, e.g. `settings/theme`. + schema: + type: string + conflicts: + name: conflicts + in: query + description: | + Answer `{"value": …, "conflicts": [...]}` (always JSON), listing every + conflict resolved on the way. + schema: + type: boolean + default: false + heads: + name: heads + in: query + description: Return the document's heads at the time of the read in `x-continuum-heads` (e.g. as the base of a remake). + schema: + type: boolean + default: false + wait: + name: wait + in: query + description: Wait until the remake is decided and answer with the final report. + schema: + type: boolean + default: false + remakeForce: + name: force + in: query + description: | + Checks to override: `true` for all, or a comma-separated list of + `switchDelay` (accept one below a keepalive interval; tests only) and + `supersede` (abort expired remakes of this epoch that still hold votes, and + propose in their place) and `coordinatorActive` (supersede even though this + node sees their coordinator as an active peer). + schema: + type: string + examples: + all: + value: 'true' + supersede: + value: supersede + remakeId: + name: id + in: path + required: true + schema: + type: string + format: uuid + quarantineId: + name: id + in: path + required: true + schema: + type: string + format: uuid + + responses: + Unauthorized: + description: Missing or wrong credentials. + headers: + WWW-Authenticate: + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + Value: + description: The value. + headers: + x-continuum-heads: + description: With `heads=true`, the heads at the time of the read (sorted hex, comma-separated). + schema: + type: string + x-continuum-stale: + description: Served from the previous epoch's document while rejoining; the value is that epoch (`:`). + schema: + type: string + ETag: + description: SHA-256 of the body. + schema: + type: string + content: + application/json: + schema: + oneOf: + - description: The value. + - $ref: '#/components/schemas/ValueWithConflicts' + text/plain: + schema: + type: string + Error: + description: Error. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + AlreadyDecided: + description: Already committed (can't be undone) or already over; `remake` holds the report. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + schemas: + Error: + type: object + required: [error] + properties: + error: + type: string + additionalProperties: true + description: >- + The body of every error answer (4xx and 5xx), on every endpoint. `error` is a + human-readable message; some errors add fields (`remake`, `failed`, `force`). + + StreamEvent: + type: object + required: [key] + properties: + key: + type: string + value: + description: The current value; absent when deleted. + deleted: + type: boolean + const: true + + ValueWithConflicts: + type: object + required: [value, conflicts] + properties: + value: {} + conflicts: + type: array + items: + $ref: '#/components/schemas/Conflict' + + Conflict: + type: object + required: [path, resolution, candidates] + properties: + path: + type: array + items: + type: string + ancestor: + type: boolean + description: On the way to the requested path (candidates without values). + resolution: + type: string + enum: [mergedMaps, newestWins] + candidates: + type: array + items: + type: object + required: [opId, selected] + properties: + opId: + type: string + selected: + type: boolean + value: {} + + Epoch: + type: object + required: [generation, id] + properties: + generation: + type: integer + minimum: 0 + id: + type: string + description: Hex snapshot hash; all zeros for the genesis epoch. + + PeerList: + type: object + required: [localEpoch, localHeads, peers] + properties: + localAlias: + type: [string, 'null'] + localEpoch: + $ref: '#/components/schemas/Epoch' + localHeads: + type: integer + description: 'Heads of the local document (more than one: concurrent changes not yet merged).' + peers: + type: array + items: + $ref: '#/components/schemas/Peer' + + Peer: + type: object + required: [publicKey, status, handshakeComplete, hasPendingDelivery, neighbour, traffic, incidents] + properties: + publicKey: + type: string + alias: + type: [string, 'null'] + description: The name the peer gave itself (`--alias`); not unique. + validatedSwarm: + type: [string, 'null'] + status: + type: string + enum: [active, dormant] + handshakeComplete: + type: boolean + lastSeenSecsAgo: + type: [integer, 'null'] + dormantForSecs: + type: [integer, 'null'] + hasPendingDelivery: + type: boolean + epoch: + oneOf: + - $ref: '#/components/schemas/Epoch' + - type: 'null' + sameEpoch: + type: [boolean, 'null'] + heads: + type: [integer, 'null'] + description: Heads in its last keepalive; null before one or when it sent a digest (more than 32). + neighbour: + type: boolean + description: We keep it alive and sync with it on our own; other peers we only answer. + rttMs: + type: [number, 'null'] + description: Effective round-trip time (smoothed, raised by unanswered requests); null without a recent sample. + traffic: + $ref: '#/components/schemas/Traffic' + incidents: + $ref: '#/components/schemas/Incidents' + syncThroughput: + oneOf: + - $ref: '#/components/schemas/Throughput' + - type: 'null' + + Counter: + type: object + required: [messages, bytes] + properties: + messages: + type: integer + bytes: + type: integer + description: Payload bytes (without Mycelium framing). + + ByKind: + type: object + required: [hello, helloReply, sync, ping, pong, blob] + properties: + hello: + $ref: '#/components/schemas/Counter' + helloReply: + $ref: '#/components/schemas/Counter' + sync: + $ref: '#/components/schemas/Counter' + ping: + $ref: '#/components/schemas/Counter' + pong: + $ref: '#/components/schemas/Counter' + blob: + $ref: '#/components/schemas/Counter' + description: Blob requests and answers (chunked downloads, e.g. of an epoch's snapshot). + + Traffic: + type: object + description: Since start. Every PONG answers a PING, so received pongs over sent pings is the keepalive answer ratio. + required: [sent, received, refused] + properties: + sent: + $ref: '#/components/schemas/ByKind' + received: + $ref: '#/components/schemas/ByKind' + refused: + $ref: '#/components/schemas/ByKind' + description: Sends Mycelium refused (not counted as sent). + + Incidents: + type: object + required: [replyTimeouts, heals, dormancies, dormantMs] + properties: + replyTimeouts: + type: integer + description: Our sync messages unanswered until the reply timeout (then resent). + heals: + type: integer + description: Re-handshakes because documents differed while sync was idle. + dormancies: + type: integer + dormantMs: + type: integer + description: Time spent dormant up to the last return (the current dormancy isn't included). + + Throughput: + type: object + required: [bytesPerSec, samples, secsAgo] + properties: + bytesPerSec: + type: number + description: Smoothed, from sync exchanges of at least 32 KiB. + samples: + type: integer + secsAgo: + type: integer + description: Age of the last sample. + + Stats: + type: object + required: [publicKey, uptimeSecs, ready, epoch, document, catchUp, remakes, sync, relay, topology, stability, staleHelloReplies, traffic, incidents, propagation, disk] + properties: + publicKey: + type: string + alias: + type: [string, 'null'] + uptimeSecs: + type: integer + ready: + type: boolean + epoch: + $ref: '#/components/schemas/Epoch' + document: + type: object + required: [heads, maxHeads] + properties: + heads: + type: integer + maxHeads: + type: integer + description: Most heads after applying a sync message, since start. + savedBytes: + type: [integer, 'null'] + description: Size as saved (compressed), measured in the background; null until first measured. + savedBytesSecsAgo: + type: [integer, 'null'] + savedBytesTookMs: + type: [integer, 'null'] + catchUp: + type: object + required: [sources] + properties: + sources: + type: integer + description: Catch-up sources chosen (more than one means it switched). + doneAfterSecs: + type: [number, 'null'] + description: From start until the initial sync was done; null until then and on the genesis node. + syncBytes: + type: [integer, 'null'] + remakes: + type: object + description: Remakes and their aftermath here (`docs/measurements.md`, "Remakes"). + required: [undecided, committed, rejoining, catchUpAttempts, quarantined, archivedEpochs] + properties: + undecided: + type: integer + committed: + type: integer + description: More than one is a double commit. + rejoining: + type: boolean + catchUpAttempts: + type: integer + quarantined: + type: integer + oldestQuarantineSecs: + type: [integer, 'null'] + archivedEpochs: + type: integer + sync: + type: object + required: [divergedPeers] + properties: + lastFromNeighbourSecs: + type: [integer, 'null'] + divergedPeers: + type: integer + relay: + type: object + required: [announceRounds, pulls, pullsQueued, blobRequestsDropped] + properties: + announceRounds: + type: integer + pulls: + type: integer + pullsQueued: + type: integer + blobRequestsDropped: + type: integer + topology: + type: object + required: [k] + properties: + k: + type: integer + view: + oneOf: + - type: 'null' + - type: object + required: [members, reachable, stable, neighbours, stableNeighbours, fullMesh] + properties: + members: + type: integer + description: Registered members, this node included. + reachable: + type: integer + description: Members other than this node not judged unreachable. + stable: + type: integer + neighbours: + type: integer + stableNeighbours: + type: integer + fullMesh: + type: boolean + stability: + type: object + required: [tier, connectedFraction, observedSecs] + properties: + tier: + type: string + enum: [stable, transient] + connectedFraction: + type: number + description: Fraction of the observed time (up to a day) with a live peer. + observedSecs: + type: integer + staleHelloReplies: + type: integer + disk: + type: object + description: | + This node's own files, by kind (`state`: the state file, `document`: the + document file, `backup`: a backup recovered from, `remake`: the input of a + genesis remake), since it started. + additionalProperties: + type: object + required: [reads, readBytes, writes, writtenBytes, appends, appendedBytes, errors] + properties: + reads: + type: integer + readBytes: + type: integer + writes: + type: integer + writtenBytes: + type: integer + description: Whole files written. + appends: + type: integer + appendedBytes: + type: integer + errors: + type: integer + description: Failed reads and writes (a missing file is no failure). + traffic: + $ref: '#/components/schemas/Traffic' + description: All peers, including ones no longer listed. + incidents: + $ref: '#/components/schemas/Incidents' + description: The peers currently listed. + propagation: + type: object + description: Time from a change's commit elsewhere until applied here, once caught up (includes clock skew). + required: [count, sumMs, ahead, buckets] + properties: + count: + type: integer + sumMs: + type: integer + ahead: + type: integer + description: Changes stamped later than our clock on arrival. + buckets: + type: array + description: Not cumulative; the last bucket has no bound. + items: + type: object + required: [count] + properties: + leMs: + type: [integer, 'null'] + count: + type: integer + + Retention: + type: object + description: Kept until every given limit has passed (as reports show it). + properties: + epochs: + type: integer + ageMs: + type: integer + + RetentionRequest: + type: object + description: Kept until every given limit has passed (as a request gives it). + properties: + epochs: + type: integer + age: + oneOf: [{ type: string }, { type: number }] + description: A duration (seconds, or e.g. `7d`, `12h`). + + Rule: + type: object + description: | + Where writes made after the base go. Paths are slash-separated strings (`""` + is the root and matches everything) or arrays of segments. The longest + matching `from` wins; the rest of the path is appended to `to`. Exactly one of + `to` and `drop`. Paths not covered are quarantined. + required: [from] + properties: + from: + $ref: '#/components/schemas/RulePath' + to: + $ref: '#/components/schemas/RulePath' + drop: + type: boolean + + RulePath: + oneOf: + - type: string + - type: array + items: + type: string + + RemakeRequest: + type: object + additionalProperties: false + properties: + kind: + type: string + enum: [compact, replace, import] + default: compact + spec: + description: | + `compact`: none. `replace`: `{document: , rules}`. + `import`: `{document: , rules}`; its + `_continuum` and history are left out, Automerge types are kept. + oneOf: + - type: 'null' + - type: object + required: [document] + additionalProperties: false + properties: + document: + oneOf: + - type: object + - type: string + contentEncoding: base64 + rules: + type: array + items: + $ref: '#/components/schemas/Rule' + baseHeads: + type: array + items: + type: string + description: | + Heads (hex) whose content the new document accounts for; the current heads + if absent. Not for `compact`, whose base is the coordinator's heads when it + commits (`400`). + replayWindow: + $ref: '#/components/schemas/RetentionRequest' + description: 'How stale a returning node''s writes may be and still be replayed. Default: 1 epoch, 7 days.' + archiveRetention: + $ref: '#/components/schemas/RetentionRequest' + description: 'How long nodes keep the old document. Default: 2 epochs, 30 days.' + settle: + oneOf: [{ type: string }, { type: number }] + description: How long votes are collected, a duration (seconds, or e.g. `500ms`, `8m`); default twice the dormancy threshold. + switchDelay: + oneOf: [{ type: string }, { type: number }] + description: A duration; default one keepalive interval, at least that without `force=switchDelay`. + + RemakeReport: + type: object + required: [id, phase, proposal] + properties: + id: + type: string + format: uuid + phase: + type: string + enum: [proposed, expired, committed, aborted] + proposal: + type: object + required: [coordinator, fromEpoch, policy, settleMs, proposedAt, settleUntil, electorate, quorum, kind] + properties: + coordinator: + type: string + fromEpoch: + $ref: '#/components/schemas/Epoch' + baseHeads: + type: array + description: Absent for `compact`; its base is in the decision. + items: + type: string + policy: + type: object + properties: + replayWindow: + $ref: '#/components/schemas/Retention' + archiveRetention: + $ref: '#/components/schemas/Retention' + settleMs: + type: integer + proposedAt: + type: integer + settleUntil: + type: integer + switchDelayMs: + type: integer + electorate: + type: array + description: Members whose votes count, fixed at proposal time. + items: + type: string + quorum: + type: integer + description: Votes needed to commit (a majority of the electorate). + kind: + type: string + content: + type: object + description: The new document of a replace or import remake, by reference; it stays on the coordinator and every voter downloads it. + required: [blake3, size] + properties: + blake3: + type: string + description: BLAKE3 hash of the content, hex. + size: + type: integer + translation: + description: The kind's rules, as stored. + decision: + oneOf: + - type: object + required: [outcome, toEpoch, snapshotBytes, decidedAt] + properties: + outcome: + const: committed + toEpoch: + $ref: '#/components/schemas/Epoch' + baseHeads: + type: array + description: The base fixed at the commit (`compact`). + items: + type: string + snapshotBytes: + type: integer + decidedAt: + type: integer + - type: object + required: [outcome, reason, decidedAt] + properties: + outcome: + const: aborted + reason: + type: string + decidedAt: + type: integer + abortRequested: + type: object + properties: + by: + type: string + at: + type: integer + adoptedBy: + type: object + description: The node that took over coordinating, if any. + properties: + by: + type: string + at: + type: integer + contested: + type: boolean + description: | + Present (true) when decisions written concurrently disagree; `decision` is + then the commit, and nodes don't switch to it on their own. + votes: + type: object + description: | + Votes for this remake by member public key, from the votes register (only + electorate members count; one that also voted for another remake counts + for neither). + additionalProperties: + type: object + required: [remake, at] + properties: + remake: + type: string + at: + type: integer + local: + type: object + description: What happened to this remake on the answering node. + properties: + switchedAt: + type: integer + replayedPaths: + type: integer + failedPaths: + type: integer + quarantine: + type: string + description: ID of the quarantine entry with this node's writes that weren't replayed. + error: + type: string + description: Why the switch didn't happen (yet). + + Epochs: + type: object + required: [current, archived, quarantine] + properties: + current: + $ref: '#/components/schemas/Epoch' + archived: + type: array + items: + type: object + required: [generation, id, archivedAt] + properties: + generation: + type: integer + id: + type: string + archivedAt: + type: integer + catchUp: + oneOf: + - type: 'null' + - type: object + required: [target, attempts, rejoining] + properties: + target: + type: string + description: '`:`.' + attempts: + type: integer + rejoining: + type: boolean + description: Syncing the target epoch (reads are served stale meanwhile). + error: + type: string + quarantine: + type: array + description: IDs of quarantine entries. + items: + type: string + + QuarantineSummary: + type: object + required: [id, epoch, target, createdAt, reason, paths] + properties: + id: + type: string + epoch: + type: string + description: The epoch the writes were made in (`:`). + target: + type: string + description: The epoch the node rejoined. + createdAt: + type: integer + reason: + type: string + paths: + type: integer + + QuarantinedWrite: + type: object + required: [path] + properties: + path: + type: array + items: + type: string + value: + description: The old document's value there. + deleted: + type: boolean + const: true + error: + type: string + + FirewallRules: + type: object + properties: + inbound: + type: array + items: + type: string + uniqueItems: true + outbound: + type: array + items: + type: string + uniqueItems: true diff --git a/src/api/mod.rs b/src/api/mod.rs index bf7b06b..20237e1 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,4 +1,4 @@ -//! HTTP API: router, OpenAPI document and shared response helpers. +//! HTTP API: router, the API description and shared response helpers. pub(crate) mod auth; mod debug; @@ -24,7 +24,6 @@ use axum::{ use serde_json::{Value as JsonValue, json}; use sha2::{Digest, Sha256}; use tracing::{error, warn}; -use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; use crate::api::auth::{AuthState, AuthenticatedUser, basic_auth}; @@ -32,8 +31,8 @@ use crate::document::meta; use crate::document::store::StoreError; use crate::state::AppState; -/// Builds the complete HTTP router. Everything except Swagger, `/health` and `/ready` is -/// authenticated. +/// Builds the complete HTTP router. Everything except the API description (`/swagger-ui`, +/// `/openapi.yaml`), `/health` and `/ready` is authenticated. pub(crate) fn router(state: AppState, auth_state: AuthState, debug_api: bool) -> Router { let mut api: Router = Router::new() .route("/store", get(store::get_store_root)) @@ -65,7 +64,11 @@ pub(crate) fn router(state: AppState, auth_state: AuthState, debug_api: bool) -> } api.layer(middleware::from_fn_with_state(auth_state, basic_auth)) - .merge(SwaggerUi::new("/swagger-ui").url("/openapi.json", ApiDoc::openapi())) + // The hand-written spec (`docs/openapi.yaml`), and the Swagger UI showing it. + .merge( + SwaggerUi::new("/swagger-ui").config(utoipa_swagger_ui::Config::from("/openapi.yaml")), + ) + .route("/openapi.yaml", get(openapi_spec)) .route("/health", get(health::health)) .route("/ready", get(health::ready)) // Outermost, so authentication failures get the error body too. @@ -125,6 +128,14 @@ async fn json_errors(request: Request, next: Next) -> Response { Response::from_parts(parts, Body::from(body)) } +/// `GET /openapi.yaml`: the API description (`docs/openapi.yaml`, built in). +async fn openapi_spec() -> impl IntoResponse { + ( + [(CONTENT_TYPE, HeaderValue::from_static("application/yaml"))], + include_str!("../../docs/openapi.yaml"), + ) +} + /// `GET /admin/config`: the effective configuration with each value's source; /// imported values are left out. async fn get_config(State(state): State) -> Json { @@ -231,41 +242,3 @@ fn response_for_value( Ok(response) } - -// --- openapi spec ------------------------------------------------------------ -// TODO spec is mostly out of sync - -#[derive(OpenApi)] -#[openapi( - paths( - store::get_store_root, - store::get_store_entry, - store::put_store_entry, - store::delete_store_entry, - peers::list_peers, - peers::delete_peer, - ), - components( - schemas( - peers::PeerStatus, - peers::PeerListResponse, - peers::PeerLivenessStatus, - peers::EpochInfo, - crate::transport::traffic::PeerTraffic, - crate::transport::traffic::ByKind, - crate::transport::traffic::Counter, - crate::state::Incidents, - peers::ThroughputInfo, - ) - ), - tags( - (name = "Store", description = "Key-value store operations"), - (name = "Peers", description = "Peer discovery and retirement") - ), - info( - title = "Automerge Replication API", - version = "1.0.0", - description = "HTTP API for a replicated Automerge key-value store" - ) -)] -struct ApiDoc; diff --git a/src/api/peers.rs b/src/api/peers.rs index eb522b0..9af24c0 100644 --- a/src/api/peers.rs +++ b/src/api/peers.rs @@ -8,7 +8,6 @@ use axum::{ use serde::{Deserialize, Serialize}; use tokio::time::Instant; use tracing::warn; -use utoipa::ToSchema; use super::store_status; use crate::document::replica::Epoch; @@ -18,14 +17,14 @@ use crate::state::{AppState, HelloState, Incidents, Liveness}; use crate::swarm::peer::forgotten_path; use crate::transport::traffic::PeerTraffic; -#[derive(Debug, Serialize, ToSchema)] +#[derive(Debug, Serialize)] #[serde(rename_all = "lowercase")] pub(crate) enum PeerLivenessStatus { Active, Dormant, } -#[derive(Debug, Serialize, ToSchema)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct EpochInfo { generation: u64, @@ -42,7 +41,7 @@ impl From for EpochInfo { } } -#[derive(Debug, Serialize, ToSchema)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct PeerStatus { public_key: String, @@ -75,7 +74,7 @@ pub(crate) struct PeerStatus { sync_throughput: Option, } -#[derive(Debug, Serialize, ToSchema)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ThroughputInfo { bytes_per_sec: f64, @@ -84,7 +83,7 @@ pub(crate) struct ThroughputInfo { secs_ago: u64, } -#[derive(Debug, Serialize, ToSchema)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct PeerListResponse { /// Our own alias (`--alias`). @@ -102,18 +101,7 @@ pub(crate) struct DeletePeerQuery { force: Option, } -#[utoipa::path( - get, - path = "/peers", - tag = "Peers", - responses( - ( - status = 200, - description = "Lists known peers and their local replication status", - body = PeerListResponse - ) - ) -)] +/// `GET /peers` (`docs/openapi.yaml`). pub(crate) async fn list_peers( State(state): State, ) -> Result, StatusCode> { @@ -182,21 +170,7 @@ pub(crate) async fn list_peers( })) } -#[utoipa::path( - delete, - path = "/peers/{peer}", - tag = "Peers", - params( - ("peer" = String, Path, description = "Peer public key"), - ("force" = Option, Query, description = "`true` or `alive`: also forget a peer that is currently alive; it returns on its next message") - ), - responses( - (status = 204, description = "Swarm-wide forget recorded"), - (status = 400, description = "Not a public key, or the local node's own key"), - (status = 409, description = "Peer is alive; pass force=alive"), - (status = 503, description = "Node is not ready") - ) -)] +/// `DELETE /peers/{pk}`: forgets a peer swarm-wide. pub(crate) async fn delete_peer( State(state): State, Path(peer): Path, diff --git a/src/api/store.rs b/src/api/store.rs index c8a072f..a05bc23 100644 --- a/src/api/store.rs +++ b/src/api/store.rs @@ -48,20 +48,7 @@ fn conflicts_response( response_for_value(&body, None) // always JSON } -#[utoipa::path( - get, - path = "/store", - tag = "Store", - params( - ("conflicts" = Option, Query, description = "Wrap the value and list every conflict resolved on the way"), - ("heads" = Option, Query, description = "Return the document heads at the time of the read in x-continuum-heads"), - ), - responses( - (status = 200, description = "Returns the complete store as JSON or text, depending on Accept"), - (status = 503, description = "Node is not ready"), - (status = 500, description = "Failed to read or serialize the document") - ) -)] +/// `GET /store`: the whole store (`docs/openapi.yaml`). pub(crate) async fn get_store_root( State(state): State, headers: HeaderMap, @@ -96,23 +83,7 @@ pub(crate) async fn get_store_root( mark_source(response, source, heads) } -#[utoipa::path( - get, - path = "/store/{key}", - tag = "Store", - params( - ("key" = String, Path, description = "Slash-separated store path, for example settings/theme"), - ("conflicts" = Option, Query, description = "Wrap the value and list every conflict resolved on the way"), - ("heads" = Option, Query, description = "Return the document heads at the time of the read in x-continuum-heads"), - ), - responses( - (status = 200, description = "Returns the value as JSON or text, depending on Accept"), - (status = 400, description = "Invalid store path"), - (status = 404, description = "Value does not exist"), - (status = 500, description = "Failed to read or serialize the value"), - (status = 503, description = "Node is not ready (and not rejoining with a stale view)") - ) -)] +/// `GET /store/{key}`. pub(crate) async fn get_store_entry( State(state): State, Path(key): Path, @@ -181,28 +152,7 @@ fn mark_source( Ok(response) } -#[utoipa::path( - put, - path = "/store/{key}", - tag = "Store", - params( - ("key" = String, Path, description = "Slash-separated store path") - ), - request_body( - content = String, - description = "Value to write. Use application/json for JSON values or text/plain for strings.", - content_type = "application/json" - ), - responses( - (status = 200, description = "Value written successfully"), - (status = 400, description = "Invalid path, malformed JSON, invalid UTF-8, or unsupported JSON value"), - (status = 403, description = "Protocol data (_continuum) and the user isn't a protocol writer, or epoch metadata"), - (status = 409, description = "Path conflicts with an existing non-map value"), - (status = 415, description = "Unsupported Content-Type"), - (status = 500, description = "Internal error or synchronization notification failure"), - (status = 503, description = "Node is not ready") - ) -)] +/// `PUT /store/{key}`. pub(crate) async fn put_store_entry( State(state): State, Extension(user): Extension, @@ -244,23 +194,7 @@ pub(crate) async fn put_store_entry( response_for_value(&value, headers.get(ACCEPT)) } -#[utoipa::path( - delete, - path = "/store/{key}", - tag = "Store", - params( - ("key" = String, Path, description = "Slash-separated store path"), - ("prune" = Option, Query, description = "Remove empty ancestor maps after deletion") - ), - responses( - (status = 204, description = "Value deleted successfully"), - (status = 400, description = "Invalid store path"), - (status = 403, description = "Protocol data (_continuum) and the user isn't a protocol writer, or epoch metadata"), - (status = 404, description = "Value does not exist"), - (status = 500, description = "Deletion or synchronization notification failed"), - (status = 503, description = "Node is not ready") - ) -)] +/// `DELETE /store/{key}`. #[instrument(skip(state), fields(path, prune = query.prune))] pub(crate) async fn delete_store_entry( State(state): State, diff --git a/src/state/peer.rs b/src/state/peer.rs index 944b886..344bcdb 100644 --- a/src/state/peer.rs +++ b/src/state/peer.rs @@ -134,7 +134,7 @@ pub(crate) struct PeerState { /// Trouble with a peer, counted since it was first listed. Lost keepalives and refused /// sends are in the traffic counters (`transport::traffic`): every PONG answers a PING. -#[derive(Debug, Default, Clone, Copy, serde::Serialize, utoipa::ToSchema)] +#[derive(Debug, Default, Clone, Copy, serde::Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct Incidents { /// Our sync messages left unanswered until the reply timeout (then sent again). diff --git a/src/transport/firewall.rs b/src/transport/firewall.rs index 23d91fe..037e739 100644 --- a/src/transport/firewall.rs +++ b/src/transport/firewall.rs @@ -4,13 +4,12 @@ use std::sync::{Arc, RwLock, RwLockReadGuard}; use anyhow::Result; use async_trait::async_trait; use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; use crate::locks::RwLockExt; use crate::transport::EncodedMessage; use crate::transport::{MessageHandler, ParsedMessage, Transport}; -#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct FirewallRules { /// Messages from these peers are dropped before the application sees them. #[serde(default)] diff --git a/src/transport/traffic.rs b/src/transport/traffic.rs index 78c5fee..7d245ac 100644 --- a/src/transport/traffic.rs +++ b/src/transport/traffic.rs @@ -14,12 +14,11 @@ use std::sync::{Arc, Mutex, MutexGuard}; use anyhow::Result; use async_trait::async_trait; use serde::Serialize; -use utoipa::ToSchema; use crate::locks::MutexExt; use crate::transport::{EncodedMessage, MessageHandler, MessageKind, ParsedMessage, Transport}; -#[derive(Debug, Default, Clone, Copy, Serialize, ToSchema)] +#[derive(Debug, Default, Clone, Copy, Serialize)] pub struct Counter { pub messages: u64, pub bytes: u64, @@ -32,7 +31,7 @@ impl Counter { } } -#[derive(Debug, Default, Clone, Copy, Serialize, ToSchema)] +#[derive(Debug, Default, Clone, Copy, Serialize)] #[serde(rename_all = "camelCase")] pub struct ByKind { pub hello: Counter, @@ -56,7 +55,7 @@ impl ByKind { } } -#[derive(Debug, Default, Clone, Copy, Serialize, ToSchema)] +#[derive(Debug, Default, Clone, Copy, Serialize)] #[serde(rename_all = "camelCase")] pub struct PeerTraffic { /// Handed to Mycelium (it accepted them). -- cgit v1.3.1