diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a53c34b..891b61c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,7 +67,7 @@ jobs: # github_token: ${{ secrets.GITHUB_TOKEN }} rust-validations: - name: Generated rust validations 🦀 + name: Generated rust 🦀 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -86,5 +86,10 @@ jobs: - name: Install rust toolchain run: rustup component add rustfmt clippy - - name: Type check the generated validation package - run: poetry run make verify-validations-rust + - name: Install openapi-generator + # Without it, verify.sh skips the leg that regenerates both halves, and only + # the committed crates get checked. + run: npm install -g @openapitools/openapi-generator-cli + + - name: Type check the generated rust + run: poetry run make verify-rust diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 91dcf99..92519f4 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -57,7 +57,7 @@ jobs: # github-token: ${{ secrets.GITHUB_TOKEN }} rust-validations: - name: Generated rust validations 🦀 + name: Generated rust 🦀 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -76,5 +76,10 @@ jobs: - name: Install rust toolchain run: rustup component add rustfmt clippy - - name: Type check the generated validation package - run: poetry run make verify-validations-rust + - name: Install openapi-generator + # Without it, verify.sh skips the leg that regenerates both halves, and only + # the committed crates get checked. + run: npm install -g @openapitools/openapi-generator-cli + + - name: Type check the generated rust + run: poetry run make verify-rust diff --git a/Makefile b/Makefile index c38e014..51aeaf2 100644 --- a/Makefile +++ b/Makefile @@ -5,10 +5,12 @@ RM := $(shell which RM) CLIENT_OUTDIR := $(shell mktemp -d) SERVER_CLIENT_OUTDIR := $(shell mktemp -d) -OPENAPI_GEN := $(shell which openapi-generator) +# brew installs it as openapi-generator, the npm wrapper as openapi-generator-cli. +OPENAPI_GEN := $(shell which openapi-generator || which openapi-generator-cli) FIRESTONE := $(shell which firestone) ADDRESSBOOK_DIR := examples/addressbook +SERVER_RS_DIR := examples/addressbook/server-rs RESOURCES := ${ADDRESSBOOK_DIR}/addressbook.yaml,${ADDRESSBOOK_DIR}/person.yaml,${ADDRESSBOOK_DIR}/postal_codes.yaml OPENAPI_DOC := ${ADDRESSBOOK_DIR}/openapi.yaml @@ -17,16 +19,17 @@ CLIENT_PKG := addressbook.client MAIN_FILE := ${ADDRESSBOOK_DIR}/main.py STREAMLIT_FILE := ${ADDRESSBOOK_DIR}/addressbook/webui/pages.py -.PHONY: gen-openapi gen-server gen-client gen-cli gen-validations gen-validations-rust verify-validations-rust +.PHONY: gen-openapi gen-server gen-server-rust gen-client gen-cli gen-validations gen-validations-rust verify-rust help: @echo "gen-openapi: Generate OpenAPI file from resources." @echo "gen-server: Generate FastAPI server code." + @echo "gen-server-rust: Generate the axum server and its implementation." @echo "gen-client: Generate Python client code." @echo "gen-cli: Generate CRUD Python (Click-based) CLI." @echo "gen-validations: Generate the python server side validation package." @echo "gen-validations-rust: Generate the rust server side validation package." - @echo "verify-validations-rust: Type check the generated rust validation package." + @echo "verify-rust: Type check firestone's generated rust." gen-openapi: ${FIRESTONE} ${FIRESTONE} generate \ @@ -80,6 +83,32 @@ gen-validations: ${FIRESTONE} validations \ --output-dir ${ADDRESSBOOK_DIR}/addressbook/validation +gen-server-rust: ${OPENAPI_GEN} ${FIRESTONE} gen-openapi + @echo "Generating the axum server from the OpenAPI document" + ${OPENAPI_GEN} generate \ + -i ${OPENAPI_DOC} \ + -g rust-axum \ + -o ${SERVER_RS_DIR}/api \ + --skip-validate-spec \ + -p packageName=addressbook_api,packageVersion=1.0.0 + + @echo "Generating the implementation of the traits it declares" + @# --force: this example is generator output, so it is refreshed wholesale. + @# A real project leaves it off, and keeps the Backend it wrote. + ${FIRESTONE} generate \ + --title 'Example person and addressbook API' \ + --description 'Example person and addressbook API' \ + --resources ${RESOURCES} \ + --version 1.0 \ + server \ + --pkg addressbook_server \ + --api-pkg addressbook_api \ + --output-dir ${SERVER_RS_DIR}/app \ + --force + + @echo "Formatting" + cd ${SERVER_RS_DIR} && cargo fmt --all + gen-validations-rust: ${FIRESTONE} ${FIRESTONE} generate \ --title 'Example person and addressbook API' \ @@ -90,7 +119,7 @@ gen-validations-rust: ${FIRESTONE} --language rust \ --output-dir ${ADDRESSBOOK_DIR}/validation-rs/src/validation -verify-validations-rust: ${FIRESTONE} +verify-rust: ${FIRESTONE} FIRESTONE=${FIRESTONE} test/rust/verify.sh gen-cli: $(FIRESTONE) @@ -117,6 +146,9 @@ gen-cli: $(FIRESTONE) --output-dir ${ADDRESSBOOK_DIR}/addressbook/cli \ --as-modules + @echo "Formatting the generated CLI" + black ${MAIN_FILE} ${ADDRESSBOOK_DIR}/addressbook/cli + gen-streamlit: $(FIRESTONE) @echo "Creating directory for ${STREAMLIT_FILE}" ${MKDIR} -pv $(shell ${DIRNAME} ${STREAMLIT_FILE}) diff --git a/README.md b/README.md index 1545852..5d539f7 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Firestone lives alongside a couple of sibling projects that share tooling and de | Python CLI | `firestone generate … cli` | Click-based CRUD utilities (`main.py` or modules) | Internal tooling, scripted batch jobs | | Streamlit UI | `firestone generate … streamlit` | Streamlit pages/modules | Lightweight admin dashboards over your API | | Validations | `firestone generate … validations` | Server side validation package (Python or Rust) | Enforcing cross-resource rules declared in the schema | +| Server impl | `firestone generate … server` | Implementation of an `openapi-generator -g rust-axum` server (Rust) | Standing an API up from the schema, rules enforced in the handlers | ## Quick Start diff --git a/docs/site/content/generation-guides/_index.md b/docs/site/content/generation-guides/_index.md index f3d6d80..ce9282c 100644 --- a/docs/site/content/generation-guides/_index.md +++ b/docs/site/content/generation-guides/_index.md @@ -14,6 +14,7 @@ From one resource schema, you can create: - **OpenAPI Specifications** - REST API documentation and contracts - **AsyncAPI Specifications** - WebSocket and event-driven API docs +- **Servers** - The implementation of a rust-axum server, with your rules enforced in the handlers - **Validation Packages** - Server side enforcement of the rules declared in your resources - **CLI Tools** - Command-line interfaces with full CRUD operations - **Streamlit UIs** - Interactive web interfaces for your APIs diff --git a/docs/site/content/generation-guides/server/_index.md b/docs/site/content/generation-guides/server/_index.md new file mode 100644 index 0000000..ccf9d58 --- /dev/null +++ b/docs/site/content/generation-guides/server/_index.md @@ -0,0 +1,22 @@ ++++ +title = "Server Generation" +weight = 45 +description = "Generate a runnable axum server from your resource definitions, with the rules already enforced." ++++ + +# Server Generation + +## Two Generators, One Document + +A resource file already says what the paths are, which methods each exposes, what a body looks like, which operations need authentication and what rules the data has to satisfy. Turning that into a running API takes two steps, and firestone only owns one of them: + +1. **`openapi-generator -g rust-axum`** turns the OpenAPI document firestone produces into the server: the router, the typed models, per-operation authentication and request validation. That is not firestone's job to duplicate. +2. **`firestone generate server`** implements the traits that server leaves behind - one required method per operation - wiring each to a `Backend` trait and enforcing the resources' validation rules on the way past. + +Both read the same document, so they cannot disagree about an operation id, a model name or a response variant. What is left for you is the `Backend`, which is the one thing neither generator can know. + +### 1. [Generating a Server](./generating) +Both commands, the crates they write, and what to run. + +### 2. [Implementing a Backend](./backend) +The one trait you write, and how the validation rules reach your storage through it. diff --git a/docs/site/content/generation-guides/server/backend.md b/docs/site/content/generation-guides/server/backend.md new file mode 100644 index 0000000..0c0945c --- /dev/null +++ b/docs/site/content/generation-guides/server/backend.md @@ -0,0 +1,103 @@ ++++ +title = "Implementing a Backend" +linkTitle = "Backend" +weight = 2 +description = "The one trait the generated server leaves you to write, and how the validation rules reach your storage through it." ++++ + +## The One Trait + +Firestone knows every route, model and rule in your API. It cannot know your storage, and should not guess, so that is the whole of what you write: + +```rust +#[async_trait] +pub trait Backend: Send + Sync + 'static { + async fn list(&self, resource: &str) -> Result, Error>; + async fn get(&self, resource: &str, key: &str) -> Result, Error>; + async fn create(&self, resource: &str, body: Value) -> Result<(String, Value), Error>; + async fn replace(&self, resource: &str, key: &str, body: Value) -> Result; + async fn patch(&self, resource: &str, key: &str, body: Value) -> Result; + async fn delete(&self, resource: &str, key: &str) -> Result<(), Error>; + + /// Find the first resource whose dotted `path` holds `value`. + async fn find(&self, resource: &str, path: &str, value: &Value) + -> Result, Error>; +} +``` + +It is deliberately resource-agnostic: `resource` is the `kind` from the schema, so one implementation serves every resource rather than one per kind. Bodies have already been through the generated models by the time they arrive, so a malformed body was refused before your code ran. + +`InMemory` is generated alongside the trait and wired into `main.rs`, so the server runs as it is. Swap it out and nothing else in the crate changes: + +```rust +let api = Api::new(Arc::new(MyPostgres::new(pool))); +``` + +## `find` Is What the Rules Use + +`find` is the one method that is not plain CRUD. It is how a validation rule reaches your storage: + +```yaml +person: + references: + kind: persons + key: first_name + value: person.first_name +``` + +becomes `find("persons", "first_name", "Ann")` when an address naming Ann is created. `path` is a **path into the resource**, not a column name — `person.first_name` is a perfectly ordinary rule — so translating it into a query is your job and belongs in one mapping table: + +```rust +const LOOKUPS: &[((&str, &str), &str)] = &[ + (("persons", "first_name"), "first_name = $1"), + (("addressbook", "person.first_name"), "person->>'first_name' = $1"), +]; +``` + +Raise on a pair you have not mapped rather than returning `None`, otherwise a typo in a schema becomes a rule that quietly rejects everything. + +## Transactions + +The generated handler validates and then writes, and those are two separate calls. A referenced resource can disappear in between, so for anything that matters: + +- Build the backend per request around the transaction that performs the write, so the lookups and the mutation see one snapshot. +- Take the locks the check implies, or run `SERIALIZABLE` and retry. +- Keep the foreign keys. Validation turns a constraint violation into a clear 422 naming the rule; it is not the guarantee itself. + +## Errors + +Return `Error` and the response shape is handled for you: + +| Variant | Status | +|---------|--------| +| `NotFound` | 404 | +| `Validation` | whatever the rule asked for, usually 422 | +| `NotImplemented` | 501 | +| `Internal` | 500 | + +A 401 is not in that list: the generated server refuses an unauthenticated request +to a secured operation before a handler runs, from the `security` block in the +schema, so it never reaches your backend. + +All of them leave as an RFC 9457 problem document served as `application/problem+json`, which is what the generated OpenAPI document declares: + +```json +{ + "type": "about:blank", + "title": "Validation failed", + "status": 422, + "detail": "No persons found with first_name 'Ann'.", + "violations": [ + { + "rule": "person_must_exist", + "resource": "addressbook", + "field": "person.first_name", + "message": "No persons found with first_name 'Ann'." + } + ] +} +``` + +## Next Steps + +- **[Generating a Server](./generating)** - both commands, and adding your own routes diff --git a/docs/site/content/generation-guides/server/generating.md b/docs/site/content/generation-guides/server/generating.md new file mode 100644 index 0000000..52d9b81 --- /dev/null +++ b/docs/site/content/generation-guides/server/generating.md @@ -0,0 +1,129 @@ ++++ +title = "Generating a Server" +linkTitle = "Generating" +weight = 1 +description = "Turn a set of resource files into an axum workspace that compiles, runs and answers." ++++ + +## The Two Commands + +First the server, from the OpenAPI document: + +```bash +firestone generate -t 'Addressbook API' -d 'Addressbook API' -v 1.0 \ + -r examples/addressbook/addressbook.yaml,examples/addressbook/person.yaml \ + openapi > openapi.yaml + +openapi-generator generate \ + -i openapi.yaml -g rust-axum -o server-rs/api --skip-validate-spec \ + -p packageName=addressbook_api,packageVersion=1.0.0 +``` + +Then the implementation of the traits it declares: + +```bash +firestone generate -t 'Addressbook API' -d 'Addressbook API' -v 1.0 \ + -r examples/addressbook/addressbook.yaml,examples/addressbook/person.yaml \ + server --pkg addressbook_server --api-pkg addressbook_api -o server-rs/app +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--output-dir`, `-o` | yes | The directory to write the crate to | +| `--pkg` | no | The crate name, `api_server` by default | +| `--api-pkg` | no | The crate name openapi-generator was given, `openapi` by default | +| `--no-validations` | no | Do not enforce the resources' rules in the handlers | +| `--force` | no | Overwrite the scaffolded files too, losing any edits to them | +| `--language`, `-l` | no | `rust`, the default and currently the only choice | + +`make gen-server-rust` does both for the addressbook example, and runs `cargo fmt` after, since neither generator formats its output. + +## The Workspace + +``` +server-rs/ +├── Cargo.toml # a workspace over the two halves +├── api/ # openapi-generator: router, models, auth, request validation +└── app/ + ├── Cargo.toml + └── src/ + ├── main.rs # config, graceful shutdown + ├── lib.rs # the Api struct, the ErrorHandler, router() + ├── handlers.rs # GENERATED: one method per operation + ├── backend.rs # the Backend trait, and an in-memory implementation + ├── auth.rs # what counts as an acceptable token + ├── error.rs # RFC 9457 problem responses + ├── resolver.rs # the rules' lookups, answered by the backend + └── validation/ # the generated validation package +``` + +## What Firestone Owns + +Some files follow the OpenAPI document exactly and are **rewritten every time**: + +| Rewritten | Why | +|-----------|-----| +| `src/handlers.rs` | one method per operation | +| `src/lib.rs` | declares the modules, which depend on whether the schema secures anything or carries rules | +| `src/resolver.rs`, `src/validation/` | the rules, and the adapter to your backend | +| `tests/api.rs` | derived from the same operations | + +Everything else is **scaffolded once and then yours** - `backend.rs`, `auth.rs`, `main.rs`, `error.rs` and `Cargo.toml` are left alone if they already exist, so the `Backend` you implement and the tokens you accept survive a regeneration. Put your own code in its own module rather than in a rewritten one. + +```console +$ firestone generate ... server -o server-rs/app +Wrote 1 file(s) to server-rs/app. +Kept 5 file(s) you own: Cargo.toml, src/backend.rs, src/error.rs, src/lib.rs, src/main.rs. +Pass --force to overwrite them. +``` + +Regenerate `api/` whenever the spec changes, and `app/` whenever either changes - it is safe to do so. + +## What Comes From Where + +| In the resource file | Who acts on it | +|----------------------|----------------| +| `methods.*` | openapi-generator: the routes | +| `security` | openapi-generator: a 401 on those operations, from your `ApiAuthBasic` | +| `schema.items.properties` | openapi-generator: the models, and request validation | +| `references`, `validations` | firestone: rules enforced in the handler before the backend is called | + +The auth split is worth knowing: **which** operations need a token is in the schema, and the generated server enforces it. **What counts as a valid token** is a deployment question, and lives in the `auth.rs` firestone writes, reading `API_TOKENS`. + +## Running It + +```console +$ cd server-rs && API_TOKENS=s3cret cargo run -p addressbook_server +INFO Addressbook API v1.0 listening on http://127.0.0.1:8080 +``` + +| Variable | Default | Meaning | +|----------|---------|---------| +| `BIND` | `127.0.0.1:8080` | The address to listen on | +| `API_TOKENS` | none | Comma separated bearer tokens the secured operations accept | +| `RUST_LOG` | `info` | The tracing filter | + +With no `API_TOKENS`, a secured operation refuses everything, which is the safe way round for a service that asked for authentication. + +## Adding Your Own Routes and Middleware + +`router(api)` hands back an ordinary [`axum::Router`](https://docs.rs/axum/latest/axum/struct.Router.html), so everything axum offers is available in `main.rs`: + +```rust +let app = router(api) + .merge(Router::new().route("/healthz", get(healthz))) + .layer(TraceLayer::new_for_http()) + .layer(your_middleware); +``` + +Firestone deliberately generates no middleware of its own. Rate limiting, CORS, compression and request ids are all `tower-http` layers, and which you want is a deployment decision rather than something a schema can say. + +## Known Gaps + +- Firestone writes a handler body only where the mapping is unambiguous. An operation with no request body, a collection `DELETE` or `PATCH`, and a `POST` or `PATCH` on an attribute all answer **501** until you implement them: a POST to an embedded collection appends and a PATCH merges, and neither is a replace. +- Query parameters are parsed and validated by the generated server but not applied; filtering is a storage concern and belongs in your `Backend`. +- `POST` mints a key through the backend. The in-memory one counts; yours will want something better. + +## Next Steps + +- **[Implementing a Backend](./backend)** - the one trait you write diff --git a/docs/site/content/reference/cli-command-reference.md b/docs/site/content/reference/cli-command-reference.md index 6195bd4..644580d 100644 --- a/docs/site/content/reference/cli-command-reference.md +++ b/docs/site/content/reference/cli-command-reference.md @@ -49,7 +49,7 @@ firestone generate [OPTIONS] ### Generators -Firestone supports five generators: `openapi`, `asyncapi`, `cli`, `streamlit`, and `validations`. +Firestone supports six generators: `openapi`, `asyncapi`, `cli`, `streamlit`, `validations`, and `server`. --- @@ -355,6 +355,57 @@ Neither package has a runtime dependency on firestone. Rules carrying a CEL `exp --- +## Server Generator + +Generates the implementation of a rust-axum server. The server itself comes from `openapi-generator -g rust-axum`, which turns firestone's OpenAPI document into the router, models, per-operation authentication and request validation; this implements the traits it leaves behind, wired to a `Backend` trait and enforcing the resources' validation rules. + +### Syntax + +```bash +firestone generate [COMMON_OPTIONS] server [OPTIONS] +``` + +### Options + +| Option | Short | Type | Required | Default | Description | +|--------|-------|------|----------|---------|-------------| +| `--output-dir` | `-o` | PATH | Yes | - | Directory to write the crate to, created if missing | +| `--pkg` | - | TEXT | No | `api_server` | The name of the generated crate | +| `--api-pkg` | - | TEXT | No | `openapi` | The crate name openapi-generator was given | +| `--no-validations` | - | FLAG | No | `false` | Do not wire the resources' rules into the handlers | +| `--language` | `-l` | CHOICE | No | `rust` | Target language; `rust` is currently the only choice | + +### Examples + +```bash +# 1. the server, from the OpenAPI document +openapi-generator generate -i openapi.yaml -g rust-axum \ + -o server-rs/api --skip-validate-spec \ + -p packageName=addressbook_api,packageVersion=1.0.0 + +# 2. the implementation of the traits it declares +firestone generate \ + -t "Addressbook API" -d "Addressbook API" -v 1.0 \ + -r examples/addressbook/addressbook.yaml,examples/addressbook/person.yaml \ + server --pkg addressbook_server --api-pkg addressbook_api -o server-rs/app + +cd server-rs && cargo fmt --all && cargo run -p addressbook_server +``` + +### Output + +``` +server-rs/ +├── api/ # openapi-generator: router, models, auth, validation +└── app/ # firestone + └── src/{main,lib,handlers,backend,auth,error,resolver}.rs + └── validation/ # when the resources declare rules +``` + +The generator does not format its output; run `cargo fmt` after generating. + +--- + ## Multiple Resources All generators support multiple resources via: @@ -440,6 +491,9 @@ firestone generate -r resources/ -t "My API" streamlit --backend-url http://loca # Generate the server side validation package firestone generate -r resources/ -t "My API" validations -o myapi/validation + +# Generate a runnable axum server +firestone generate -r resources/ -t "My API" server -o myapi/server-rs ``` ### CI/CD Integration diff --git a/examples/addressbook/addressbook.yaml b/examples/addressbook/addressbook.yaml index a694177..72b4dad 100644 --- a/examples/addressbook/addressbook.yaml +++ b/examples/addressbook/addressbook.yaml @@ -77,7 +77,13 @@ validations: methods: - put - patch - expr: 'self.is_valid == old.is_valid || (has(ctx.roles) && "admin" in ctx.roles)' + # Guarded with has(), because is_valid is optional: an attribute endpoint + # can hand the rule a resource that never had one. + expr: >- + !has(old.is_valid) + || !has(self.is_valid) + || self.is_valid == old.is_valid + || (has(ctx.roles) && "admin" in ctx.roles) error: status: 403 message: Only an admin may change is_valid on an address. @@ -106,6 +112,27 @@ validations: roles: - user expect: pass + # The case that used to 500: is_valid is optional, and an attribute + # endpoint can hand the rule a resource that never had one. + - self: + city: Ottawa + old: + city: Toronto + ctx: + roles: + - user + expect: pass + # And the case that used to 403: a partial update that never mentions + # is_valid is not a change to it. + - self: + city: Ottawa + old: + city: Toronto + is_valid: true + ctx: + roles: + - user + expect: pass # Metadata: end schema: type: array diff --git a/examples/addressbook/addressbook/cli/persons.py b/examples/addressbook/addressbook/cli/persons.py index 073e7e7..bffefe7 100644 --- a/examples/addressbook/addressbook/cli/persons.py +++ b/examples/addressbook/addressbook/cli/persons.py @@ -75,6 +75,13 @@ async def persons(ctx_obj): @click.option( "--last-name", help="The person's last name", type=str, show_default=True, required=False ) + @click.option( + "--timezone", + help="The person's timezone (e.g. UTC, America/New_York)", + type=str, + show_default=True, + required=False, + ) @click.option( "--uuid", help="A UUID associated to this person", @@ -85,7 +92,7 @@ async def persons(ctx_obj): @click.pass_obj @firestone_utils.click_coro @api_exc - async def persons_post(ctx_obj, age, first_name, hobbies, last_name, uuid): + async def persons_post(ctx_obj, age, first_name, hobbies, last_name, timezone, uuid): """Create a new person in this collection, a new UUID key will be created""" api_obj = ctx_obj["api_obj"] params = { @@ -93,6 +100,7 @@ async def persons_post(ctx_obj, age, first_name, hobbies, last_name, uuid): "first_name": first_name, "hobbies": hobbies, "last_name": last_name, + "timezone": timezone, "uuid": uuid, } req_body = create_person_model.CreatePerson(**params) @@ -198,11 +206,17 @@ async def persons_uuid_get(ctx_obj, last_name, uuid): @click.option("--first-name", help="The person's first name", type=str, required=False) @click.option("--hobbies", help="The person's hobbies", type=cli.StrList, required=False) @click.option("--last-name", help="The person's last name", type=str, required=False) + @click.option( + "--timezone", + help="The person's timezone (e.g. UTC, America/New_York)", + type=str, + required=False, + ) @click.argument("uuid", type=str) @click.pass_obj @firestone_utils.click_coro @api_exc - async def persons_uuid_put(ctx_obj, age, first_name, hobbies, last_name, uuid): + async def persons_uuid_put(ctx_obj, age, first_name, hobbies, last_name, timezone, uuid): """Put a new person in this collection, with the given UUId key""" api_obj = ctx_obj["api_obj"] params = { @@ -210,6 +224,7 @@ async def persons_uuid_put(ctx_obj, age, first_name, hobbies, last_name, uuid): "first_name": first_name, "hobbies": hobbies, "last_name": last_name, + "timezone": timezone, } req_body = update_person_model.UpdatePerson(**params) diff --git a/examples/addressbook/addressbook/validation/rules.py b/examples/addressbook/addressbook/validation/rules.py index dd23ee8..515cb2a 100644 --- a/examples/addressbook/addressbook/validation/rules.py +++ b/examples/addressbook/addressbook/validation/rules.py @@ -51,7 +51,7 @@ "patch", ], "refs": [], - "expr": 'self.is_valid == old.is_valid || (has(ctx.roles) && "admin" in ctx.roles)', + "expr": '!has(old.is_valid) || !has(self.is_valid) || self.is_valid == old.is_valid || (has(ctx.roles) && "admin" in ctx.roles)', "error": { "status": 403, "message": "Only an admin may change is_valid on an address.", @@ -102,6 +102,37 @@ }, "expect": "pass", }, + { + "self": { + "city": "Ottawa", + }, + "old": { + "city": "Toronto", + }, + "refs": {}, + "ctx": { + "roles": [ + "user", + ], + }, + "expect": "pass", + }, + { + "self": { + "city": "Ottawa", + }, + "old": { + "city": "Toronto", + "is_valid": True, + }, + "refs": {}, + "ctx": { + "roles": [ + "user", + ], + }, + "expect": "pass", + }, ], }, ], diff --git a/examples/addressbook/addressbook/validation/test_rules.py b/examples/addressbook/addressbook/validation/test_rules.py index 721fb63..5d60bf3 100644 --- a/examples/addressbook/addressbook/validation/test_rules.py +++ b/examples/addressbook/addressbook/validation/test_rules.py @@ -76,6 +76,47 @@ "expect": "pass", }, ], + [ + "addressbook", + "only_admins_may_invalidate", + "put", + { + "self": { + "city": "Ottawa", + }, + "old": { + "city": "Toronto", + }, + "refs": {}, + "ctx": { + "roles": [ + "user", + ], + }, + "expect": "pass", + }, + ], + [ + "addressbook", + "only_admins_may_invalidate", + "put", + { + "self": { + "city": "Ottawa", + }, + "old": { + "city": "Toronto", + "is_valid": True, + }, + "refs": {}, + "ctx": { + "roles": [ + "user", + ], + }, + "expect": "pass", + }, + ], [ "persons", "person_is_not_in_use", diff --git a/examples/addressbook/main.py b/examples/addressbook/main.py index 42fd6bb..eb0c1a0 100755 --- a/examples/addressbook/main.py +++ b/examples/addressbook/main.py @@ -160,7 +160,7 @@ async def addressbook(ctx_obj): "--country", help="The country of this address", type=str, show_default=True, required=True ) @click.option( - "--is-valid--no-is-valid", + "--is-valid/--no-is-valid", help="Address is valid or not", is_flag=True, show_default=True, @@ -311,7 +311,6 @@ async def addressbook_address_key_get(ctx_obj, address_key, city): @addressbook.command("update") @click.argument("address_key", type=str) -@click.argument("address_key", type=str) @click.option( "--addrtype", help="The address type, e.g. work or home", @@ -338,17 +337,7 @@ async def addressbook_address_key_get(ctx_obj, address_key, city): @firestone_utils.click_coro @api_exc async def addressbook_address_key_put( - ctx_obj, - address_key, - address_key, - addrtype, - city, - country, - is_valid, - people, - person, - state, - street, + ctx_obj, address_key, addrtype, city, country, is_valid, people, person, state, street ): """Update an existing address in this addressbook, with the given address key.""" api_obj = ctx_obj["api_obj"] @@ -364,7 +353,7 @@ async def addressbook_address_key_put( } req_body = update_addressbook_model.UpdateAddressbook(**params) - resp = await api_obj.addressbook_address_key_put(address_key, address_key, req_body) + resp = await api_obj.addressbook_address_key_put(address_key, req_body) _LOGGER.debug(f"resp: {resp}") if isinstance(resp, list): @@ -397,13 +386,20 @@ async def persons(ctx_obj): @click.option( "--last-name", help="The person's last name", type=str, show_default=True, required=False ) +@click.option( + "--timezone", + help="The person's timezone (e.g. UTC, America/New_York)", + type=str, + show_default=True, + required=False, +) @click.option( "--uuid", help="A UUID associated to this person", type=str, show_default=True, required=False ) @click.pass_obj @firestone_utils.click_coro @api_exc -async def persons_post(ctx_obj, age, first_name, hobbies, last_name, uuid): +async def persons_post(ctx_obj, age, first_name, hobbies, last_name, timezone, uuid): """Create a new person in this collection, a new UUID key will be created""" api_obj = ctx_obj["api_obj"] params = { @@ -411,6 +407,7 @@ async def persons_post(ctx_obj, age, first_name, hobbies, last_name, uuid): "first_name": first_name, "hobbies": hobbies, "last_name": last_name, + "timezone": timezone, "uuid": uuid, } req_body = create_person_model.CreatePerson(**params) @@ -520,12 +517,17 @@ async def persons_uuid_get(ctx_obj, last_name, uuid): @click.option("--first-name", help="The person's first name", type=str, required=False) @click.option("--hobbies", help="The person's hobbies", type=cli.StrList, required=False) @click.option("--last-name", help="The person's last name", type=str, required=False) -@click.argument("uuid", type=str) +@click.option( + "--timezone", + help="The person's timezone (e.g. UTC, America/New_York)", + type=str, + required=False, +) @click.argument("uuid", type=str) @click.pass_obj @firestone_utils.click_coro @api_exc -async def persons_uuid_put(ctx_obj, age, first_name, hobbies, last_name, uuid, uuid): +async def persons_uuid_put(ctx_obj, age, first_name, hobbies, last_name, timezone, uuid): """Put a new person in this collection, with the given UUId key""" api_obj = ctx_obj["api_obj"] params = { @@ -533,10 +535,11 @@ async def persons_uuid_put(ctx_obj, age, first_name, hobbies, last_name, uuid, u "first_name": first_name, "hobbies": hobbies, "last_name": last_name, + "timezone": timezone, } req_body = update_person_model.UpdatePerson(**params) - resp = await api_obj.persons_uuid_put(uuid, uuid, req_body) + resp = await api_obj.persons_uuid_put(uuid, req_body) _LOGGER.debug(f"resp: {resp}") if isinstance(resp, list): diff --git a/examples/addressbook/openapi.yaml b/examples/addressbook/openapi.yaml index fa68237..abc9691 100644 --- a/examples/addressbook/openapi.yaml +++ b/examples/addressbook/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.1.0" info: - title: Example person and addressbook API - description: Example person and addressbook API + title: "Example person and addressbook API" + description: "Example person and addressbook API" version: "1.0" # https://www.jvt.me/posts/2022/05/29/openapi-json-patch/ components: @@ -269,7 +269,7 @@ paths: schema: type: string responses: - default: + 200: description: Default HEAD response tags: - addressbook @@ -367,7 +367,7 @@ paths: schema: type: string responses: - default: + 200: description: Default HEAD response tags: - addressbook @@ -464,7 +464,7 @@ paths: schema: type: string responses: - default: + 200: description: Default HEAD response tags: - addressbook @@ -558,7 +558,7 @@ paths: schema: type: string responses: - default: + 200: description: Default HEAD response tags: - addressbook @@ -652,7 +652,7 @@ paths: schema: type: string responses: - default: + 200: description: Default HEAD response tags: - addressbook @@ -748,7 +748,7 @@ paths: schema: type: string responses: - default: + 200: description: Default HEAD response tags: - addressbook @@ -852,7 +852,7 @@ paths: - *id001 - *id002 responses: - default: + 200: description: Default HEAD response tags: - addressbook @@ -973,7 +973,7 @@ paths: schema: type: string responses: - default: + 200: description: Default HEAD response tags: - addressbook @@ -1067,7 +1067,7 @@ paths: schema: type: string responses: - default: + 200: description: Default HEAD response tags: - addressbook @@ -1226,7 +1226,7 @@ paths: schema: type: string responses: - default: + 200: description: Default HEAD response tags: - persons @@ -1370,7 +1370,7 @@ paths: schema: type: string responses: - default: + 200: description: Default HEAD response tags: - postal_codes @@ -1443,7 +1443,8 @@ x-firestone-validations: error: message: Only an admin may change is_valid on an address. status: 403 - expr: self.is_valid == old.is_valid || (has(ctx.roles) && "admin" in ctx.roles) + expr: '!has(old.is_valid) || !has(self.is_valid) || self.is_valid == old.is_valid + || (has(ctx.roles) && "admin" in ctx.roles)' kind: expr methods: - put diff --git a/examples/addressbook/server-rs/Cargo.lock b/examples/addressbook/server-rs/Cargo.lock new file mode 100644 index 0000000..79c675b --- /dev/null +++ b/examples/addressbook/server-rs/Cargo.lock @@ -0,0 +1,1987 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addressbook_api" +version = "1.0.0" +dependencies = [ + "ammonia", + "async-trait", + "axum", + "axum-extra", + "base64", + "bytes", + "chrono", + "frunk", + "frunk-enum-core", + "frunk-enum-derive", + "frunk_core 0.4.4", + "frunk_derives", + "http", + "lazy_static", + "regex", + "serde", + "serde_html_form", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", + "validator", +] + +[[package]] +name = "addressbook_server" +version = "1.0.0" +dependencies = [ + "addressbook_api", + "async-trait", + "axum", + "axum-extra", + "cel-interpreter", + "http", + "http-body-util", + "serde", + "serde_json", + "subtle", + "tokio", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "ammonia" +version = "4.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6d763210e2eb7670d1a5183a08bebefa3f97db2a738a684f2ce00bd49f681d" +dependencies = [ + "cssparser", + "html5ever", + "maplit", + "url", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "antlr4rust" +version = "0.3.0-rc2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d240d49ee89063f90fa0cb18aead41a5893cd544a1785983dc3bf5c3d5faa58b" +dependencies = [ + "better_any", + "bit-set", + "byteorder", + "lazy_static", + "murmur3", + "once_cell", + "parking_lot", + "typed-arena", + "uuid", +] + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde_core", + "serde_html_form", + "serde_path_to_error", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "better_any" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4372b9543397a4b86050cc5e7ee36953edf4bac9518e8a774c2da694977fb6e4" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cel-interpreter" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76c07820046cc8239526fceec6df147a979ae48644dbad274fc3ce38ab0973b" +dependencies = [ + "cel-parser", + "chrono", + "nom", + "paste", + "regex", + "serde", + "thiserror", +] + +[[package]] +name = "cel-parser" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "546fb134998490c5c47fc7a29c7535e725d2e403f172040e8f263d0b318bff5f" +dependencies = [ + "antlr4rust", + "lazy_static", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "cssparser" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98" +dependencies = [ + "dtoa-short", + "itoa", + "smallvec", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "encoding_rs" +version = "0.8.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "frunk" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28aef0f9aa070bce60767c12ba9cb41efeaf1a2bc6427f87b7d83f11239a16d7" +dependencies = [ + "frunk_core 0.4.4", + "frunk_derives", + "frunk_proc_macros", + "serde", +] + +[[package]] +name = "frunk-enum-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d155e9389142f77d2ee933d7243b887f91a3179f9bb6a6dc1aeea48b82bba8" +dependencies = [ + "frunk", +] + +[[package]] +name = "frunk-enum-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2c6130959f36116111ab30576a7e8961df52f864524c92d580b1162cb3d2b04" +dependencies = [ + "frunk_proc_macro_helpers", + "itertools", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "frunk_core" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "476eeaa382e3462b84da5d6ba3da97b5786823c2d0d3a0d04ef088d073da225c" +dependencies = [ + "serde", +] + +[[package]] +name = "frunk_core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd3c9ba2e323e8b19e77f15873f60974a7d82f89b80e50c53be44b8b92927c1" + +[[package]] +name = "frunk_derives" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b4095fc99e1d858e5b8c7125d2638372ec85aa0fe6c807105cf10b0265ca6c" +dependencies = [ + "frunk_proc_macro_helpers", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "frunk_proc_macro_helpers" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b70229a1347a20d4af9c06116cc452acef34f798668c6b69e97dd5c8a88052bd" +dependencies = [ + "frunk_core 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "frunk_proc_macros" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c63fe4306e13ece9d1ac23ce02f121ffefd42ae876ad03ec2caf07232bde3023" +dependencies = [ + "frunk_core 0.5.0", + "frunk_proc_macro_helpers", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "html5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "markup5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "multiversion" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" +dependencies = [ + "multiversion-macros", +] + +[[package]] +name = "multiversion-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 3.0.5", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "murmur3" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a198f9589efc03f544388dfc4a19fe8af4323662b62f598b8dcfdac62c14771c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-error-attr3" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e564d14133360e1ae169ffde5da25881b5fa47261665b8e5713c212c27799da" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error3" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f0d4471b3436c22106b21913b1dda531558918ae9b7ec55d58aa84b43552233" +dependencies = [ + "proc-macro-error-attr3", + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_html_form" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" +dependencies = [ + "form_urlencoded", + "indexmap", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "validator" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240e4b81c20a1d6d50d1d7265c658dfbd204e8b9ac4d80f3c931f39462196335" +dependencies = [ + "darling", + "proc-macro-error3", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.5", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web_atoms" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/examples/addressbook/server-rs/Cargo.toml b/examples/addressbook/server-rs/Cargo.toml new file mode 100644 index 0000000..ce81f29 --- /dev/null +++ b/examples/addressbook/server-rs/Cargo.toml @@ -0,0 +1,7 @@ +# A workspace holding the two halves of the generated server. +# +# api/ openapi-generator -g rust-axum, from the OpenAPI document firestone emits +# app/ firestone, implementing the traits that crate declares +[workspace] +members = ["api", "app"] +resolver = "2" diff --git a/examples/addressbook/server-rs/api/.gitignore b/examples/addressbook/server-rs/api/.gitignore new file mode 100644 index 0000000..a9d37c5 --- /dev/null +++ b/examples/addressbook/server-rs/api/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/examples/addressbook/server-rs/api/.openapi-generator-ignore b/examples/addressbook/server-rs/api/.openapi-generator-ignore new file mode 100644 index 0000000..7484ee5 --- /dev/null +++ b/examples/addressbook/server-rs/api/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/examples/addressbook/server-rs/api/.openapi-generator/FILES b/examples/addressbook/server-rs/api/.openapi-generator/FILES new file mode 100644 index 0000000..72a2436 --- /dev/null +++ b/examples/addressbook/server-rs/api/.openapi-generator/FILES @@ -0,0 +1,12 @@ +.gitignore +Cargo.toml +README.md +src/apis/addressbook.rs +src/apis/mod.rs +src/apis/persons.rs +src/apis/postal_codes.rs +src/header.rs +src/lib.rs +src/models.rs +src/server/mod.rs +src/types.rs diff --git a/examples/addressbook/server-rs/api/.openapi-generator/VERSION b/examples/addressbook/server-rs/api/.openapi-generator/VERSION new file mode 100644 index 0000000..6328c54 --- /dev/null +++ b/examples/addressbook/server-rs/api/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.17.0 diff --git a/examples/addressbook/server-rs/api/Cargo.toml b/examples/addressbook/server-rs/api/Cargo.toml new file mode 100644 index 0000000..87e1a25 --- /dev/null +++ b/examples/addressbook/server-rs/api/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "addressbook_api" +version = "1.0.0" +authors = ["OpenAPI Generator team and contributors"] +description = "Example person and addressbook API" +edition = "2024" + +[features] +default = ["server"] +server = [] +conversion = [ + "frunk", + "frunk_derives", + "frunk_core", + "frunk-enum-core", + "frunk-enum-derive", +] + +[dependencies] +ammonia = "4" +async-trait = "0.1" +axum = { version = "0.8", features = ["multipart"] } +axum-extra = { version = "0.10", features = ["cookie", "query"] } +base64 = "0.22" +bytes = "1" +chrono = { version = "0.4", features = ["serde"] } +frunk = { version = "0.4", optional = true } +frunk-enum-core = { version = "0.3", optional = true } +frunk-enum-derive = { version = "0.3", optional = true } +frunk_core = { version = "0.4", optional = true } +frunk_derives = { version = "0.4", optional = true } +http = "1" +lazy_static = "1" +regex = "1" +serde = { version = "1", features = ["derive"] } +serde_html_form = "0.2" +serde_json = { version = "1", features = ["raw_value"] } +tokio = { version = "1", default-features = false, features = [ + "signal", + "rt-multi-thread", +] } +tracing = { version = "0.1", features = ["attributes"] } +uuid = { version = "1", features = ["serde"] } +validator = { version = "0.20", features = ["derive"] } + +[dev-dependencies] +tracing-subscriber = "0.3" diff --git a/examples/addressbook/server-rs/api/README.md b/examples/addressbook/server-rs/api/README.md new file mode 100644 index 0000000..f2d997f --- /dev/null +++ b/examples/addressbook/server-rs/api/README.md @@ -0,0 +1,94 @@ +# Rust API for addressbook_api + +Example person and addressbook API + +## Overview + +This server was generated by the [openapi-generator] +(https://openapi-generator.tech) project. By using the +[OpenAPI-Spec](https://github.com/OAI/OpenAPI-Specification) from a remote +server, you can easily generate a server stub. + +To see how to make this your own, look here: [README]((https://openapi-generator.tech)) + +- API version: 1.0 +- Build date: 2026-09-15T07:48:36.698698-03:00[America/Halifax] +- Generator version: 7.17.0 + + + +This autogenerated project defines an API crate `addressbook_api` which contains: +* An `Api` trait defining the API in Rust. +* Data types representing the underlying data model. +* Axum router which accepts HTTP requests and invokes the appropriate `Api` method for each operation. + * Request validations (path, query, body params) are included. + +## Using the generated library + +The generated library has a few optional features that can be activated through Cargo. + +* `server` + * This defaults to enabled and creates the basic skeleton of a server implementation based on Axum. + * To create the server stack you'll need to provide an implementation of the API trait to provide the server function. +* `conversions` + * This defaults to disabled and creates extra derives on models to allow "transmogrification" between objects of structurally similar types. + +See https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-section for how to use features in your `Cargo.toml`. + +### Example + +```rust +struct ServerImpl { + // database: sea_orm::DbConn, +} + +#[allow(unused_variables)] +#[async_trait] +impl addressbook_api::apis::default::Api for ServerImpl { + // API implementation goes here +} + +impl addressbook_api::apis::ErrorHandler for ServerImpl {} + +pub async fn start_server(addr: &str) { + // initialize tracing + tracing_subscriber::fmt::init(); + + // Init Axum router + let app = addressbook_api::server::new(Arc::new(ServerImpl)); + + // Add layers to the router + let app = app.layer(...); + + // Run the server with graceful shutdown + let listener = TcpListener::bind(addr).await.unwrap(); + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) + .await + .unwrap(); +} + +async fn shutdown_signal() { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } +} +``` diff --git a/examples/addressbook/server-rs/api/src/apis/addressbook.rs b/examples/addressbook/server-rs/api/src/apis/addressbook.rs new file mode 100644 index 0000000..a548ca3 --- /dev/null +++ b/examples/addressbook/server-rs/api/src/apis/addressbook.rs @@ -0,0 +1,780 @@ +use async_trait::async_trait; +use axum::extract::*; +use axum_extra::extract::{CookieJar, Host}; +use bytes::Bytes; +use http::Method; +use serde::{Deserialize, Serialize}; + +use crate::{models, types::*}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyAddrtypeDeleteResponse { + /// Response for OK + Status200_ResponseForOK(String), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyAddrtypeGetResponse { + /// Response for OK + Status200_ResponseForOK(String), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyAddrtypeHeadResponse { + /// Default HEAD response + Status200_DefaultHEADResponse, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyAddrtypePutResponse { + /// Response for OK + Status200_ResponseForOK(String), + /// One or more validation rules failed + Status403_OneOrMoreValidationRulesFailed(models::ValidationProblem), + /// One or more validation rules failed + Status422_OneOrMoreValidationRulesFailed(models::ValidationProblem), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyCityDeleteResponse { + /// Response for OK + Status200_ResponseForOK(String), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyCityGetResponse { + /// Response for OK + Status200_ResponseForOK(String), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyCityHeadResponse { + /// Default HEAD response + Status200_DefaultHEADResponse, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyCityPutResponse { + /// Response for OK + Status200_ResponseForOK(String), + /// One or more validation rules failed + Status403_OneOrMoreValidationRulesFailed(models::ValidationProblem), + /// One or more validation rules failed + Status422_OneOrMoreValidationRulesFailed(models::ValidationProblem), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyCountryDeleteResponse { + /// Response for OK + Status200_ResponseForOK(String), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyCountryGetResponse { + /// Response for OK + Status200_ResponseForOK(String), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyCountryHeadResponse { + /// Default HEAD response + Status200_DefaultHEADResponse, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyCountryPutResponse { + /// Response for OK + Status200_ResponseForOK(String), + /// One or more validation rules failed + Status403_OneOrMoreValidationRulesFailed(models::ValidationProblem), + /// One or more validation rules failed + Status422_OneOrMoreValidationRulesFailed(models::ValidationProblem), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyDeleteResponse { + /// Response for OK + Status200_ResponseForOK(models::Addressbook), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyGetResponse { + /// Response for OK + Status200_ResponseForOK(models::Addressbook), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyHeadResponse { + /// Default HEAD response + Status200_DefaultHEADResponse, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyIsValidDeleteResponse { + /// Response for OK + Status200_ResponseForOK(bool), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyIsValidGetResponse { + /// Response for OK + Status200_ResponseForOK(bool), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyIsValidHeadResponse { + /// Default HEAD response + Status200_DefaultHEADResponse, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyIsValidPutResponse { + /// Response for OK + Status200_ResponseForOK(bool), + /// One or more validation rules failed + Status403_OneOrMoreValidationRulesFailed(models::ValidationProblem), + /// One or more validation rules failed + Status422_OneOrMoreValidationRulesFailed(models::ValidationProblem), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyPeopleDeleteResponse { + /// Response for OK + Status200_ResponseForOK(Vec), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyPeopleGetResponse { + /// Response for OK + Status200_ResponseForOK(Vec), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyPeopleHeadResponse { + /// Default HEAD response + Status200_DefaultHEADResponse, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyPeoplePutResponse { + /// Response for OK + Status200_ResponseForOK(Vec), + /// One or more validation rules failed + Status403_OneOrMoreValidationRulesFailed(models::ValidationProblem), + /// One or more validation rules failed + Status422_OneOrMoreValidationRulesFailed(models::ValidationProblem), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyPersonDeleteResponse { + /// Response for OK + Status200_ResponseForOK(models::Person), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyPersonGetResponse { + /// Response for OK + Status200_ResponseForOK(Vec), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyPersonHeadResponse { + /// Default HEAD response + Status200_DefaultHEADResponse, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyPersonPatchResponse { + /// Response for OK + Status200_ResponseForOK(models::Person), + /// One or more validation rules failed + Status403_OneOrMoreValidationRulesFailed(models::ValidationProblem), + /// One or more validation rules failed + Status422_OneOrMoreValidationRulesFailed(models::ValidationProblem), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyPersonPostResponse { + /// Response for CREATED + Status201_ResponseForCREATED(models::CreatePerson), + /// One or more validation rules failed + Status422_OneOrMoreValidationRulesFailed(models::ValidationProblem), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyPutResponse { + /// Response for OK + Status200_ResponseForOK(models::UpdateAddressbook), + /// One or more validation rules failed + Status403_OneOrMoreValidationRulesFailed(models::ValidationProblem), + /// One or more validation rules failed + Status422_OneOrMoreValidationRulesFailed(models::ValidationProblem), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyStateDeleteResponse { + /// Response for OK + Status200_ResponseForOK(String), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyStateGetResponse { + /// Response for OK + Status200_ResponseForOK(String), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyStateHeadResponse { + /// Default HEAD response + Status200_DefaultHEADResponse, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyStatePutResponse { + /// Response for OK + Status200_ResponseForOK(String), + /// One or more validation rules failed + Status403_OneOrMoreValidationRulesFailed(models::ValidationProblem), + /// One or more validation rules failed + Status422_OneOrMoreValidationRulesFailed(models::ValidationProblem), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyStreetDeleteResponse { + /// Response for OK + Status200_ResponseForOK(String), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyStreetGetResponse { + /// Response for OK + Status200_ResponseForOK(String), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyStreetHeadResponse { + /// Default HEAD response + Status200_DefaultHEADResponse, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookAddressKeyStreetPutResponse { + /// Response for OK + Status200_ResponseForOK(String), + /// One or more validation rules failed + Status403_OneOrMoreValidationRulesFailed(models::ValidationProblem), + /// One or more validation rules failed + Status422_OneOrMoreValidationRulesFailed(models::ValidationProblem), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookGetResponse { + /// Response for OK + Status200_ResponseForOK(Vec), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum AddressbookPostResponse { + /// Response for CREATED + Status201_ResponseForCREATED(models::CreateAddressbook), + /// One or more validation rules failed + Status422_OneOrMoreValidationRulesFailed(models::ValidationProblem), +} + +/// Addressbook +#[async_trait] +#[allow(clippy::ptr_arg)] +pub trait Addressbook: + super::ErrorHandler +{ + /// AddressbookAddressKeyAddrtypeDelete - DELETE /addressbook/{address_key}/addrtype + async fn addressbook_address_key_addrtype_delete( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyAddrtypeDeletePathParams, + ) -> Result; + + /// AddressbookAddressKeyAddrtypeGet - GET /addressbook/{address_key}/addrtype + async fn addressbook_address_key_addrtype_get( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyAddrtypeGetPathParams, + query_params: &models::AddressbookAddressKeyAddrtypeGetQueryParams, + ) -> Result; + + /// AddressbookAddressKeyAddrtypeHead - HEAD /addressbook/{address_key}/addrtype + async fn addressbook_address_key_addrtype_head( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyAddrtypeHeadPathParams, + ) -> Result; + + /// AddressbookAddressKeyAddrtypePut - PUT /addressbook/{address_key}/addrtype + async fn addressbook_address_key_addrtype_put( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyAddrtypePutPathParams, + body: &String, + ) -> Result; + + /// AddressbookAddressKeyCityDelete - DELETE /addressbook/{address_key}/city + async fn addressbook_address_key_city_delete( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyCityDeletePathParams, + ) -> Result; + + /// AddressbookAddressKeyCityGet - GET /addressbook/{address_key}/city + async fn addressbook_address_key_city_get( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyCityGetPathParams, + query_params: &models::AddressbookAddressKeyCityGetQueryParams, + ) -> Result; + + /// AddressbookAddressKeyCityHead - HEAD /addressbook/{address_key}/city + async fn addressbook_address_key_city_head( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyCityHeadPathParams, + ) -> Result; + + /// AddressbookAddressKeyCityPut - PUT /addressbook/{address_key}/city + async fn addressbook_address_key_city_put( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyCityPutPathParams, + body: &String, + ) -> Result; + + /// AddressbookAddressKeyCountryDelete - DELETE /addressbook/{address_key}/country + async fn addressbook_address_key_country_delete( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyCountryDeletePathParams, + ) -> Result; + + /// AddressbookAddressKeyCountryGet - GET /addressbook/{address_key}/country + async fn addressbook_address_key_country_get( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyCountryGetPathParams, + query_params: &models::AddressbookAddressKeyCountryGetQueryParams, + ) -> Result; + + /// AddressbookAddressKeyCountryHead - HEAD /addressbook/{address_key}/country + async fn addressbook_address_key_country_head( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyCountryHeadPathParams, + ) -> Result; + + /// AddressbookAddressKeyCountryPut - PUT /addressbook/{address_key}/country + async fn addressbook_address_key_country_put( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyCountryPutPathParams, + body: &String, + ) -> Result; + + /// AddressbookAddressKeyDelete - DELETE /addressbook/{address_key} + async fn addressbook_address_key_delete( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyDeletePathParams, + ) -> Result; + + /// AddressbookAddressKeyGet - GET /addressbook/{address_key} + async fn addressbook_address_key_get( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyGetPathParams, + query_params: &models::AddressbookAddressKeyGetQueryParams, + ) -> Result; + + /// AddressbookAddressKeyHead - HEAD /addressbook/{address_key} + async fn addressbook_address_key_head( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyHeadPathParams, + ) -> Result; + + /// AddressbookAddressKeyIsValidDelete - DELETE /addressbook/{address_key}/is_valid + async fn addressbook_address_key_is_valid_delete( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyIsValidDeletePathParams, + ) -> Result; + + /// AddressbookAddressKeyIsValidGet - GET /addressbook/{address_key}/is_valid + async fn addressbook_address_key_is_valid_get( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyIsValidGetPathParams, + query_params: &models::AddressbookAddressKeyIsValidGetQueryParams, + ) -> Result; + + /// AddressbookAddressKeyIsValidHead - HEAD /addressbook/{address_key}/is_valid + async fn addressbook_address_key_is_valid_head( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyIsValidHeadPathParams, + ) -> Result; + + /// AddressbookAddressKeyIsValidPut - PUT /addressbook/{address_key}/is_valid + async fn addressbook_address_key_is_valid_put( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyIsValidPutPathParams, + body: &bool, + ) -> Result; + + /// AddressbookAddressKeyPeopleDelete - DELETE /addressbook/{address_key}/people + async fn addressbook_address_key_people_delete( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyPeopleDeletePathParams, + ) -> Result; + + /// AddressbookAddressKeyPeopleGet - GET /addressbook/{address_key}/people + async fn addressbook_address_key_people_get( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyPeopleGetPathParams, + query_params: &models::AddressbookAddressKeyPeopleGetQueryParams, + ) -> Result; + + /// AddressbookAddressKeyPeopleHead - HEAD /addressbook/{address_key}/people + async fn addressbook_address_key_people_head( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyPeopleHeadPathParams, + ) -> Result; + + /// AddressbookAddressKeyPeoplePut - PUT /addressbook/{address_key}/people + async fn addressbook_address_key_people_put( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyPeoplePutPathParams, + body: &Vec, + ) -> Result; + + /// AddressbookAddressKeyPersonDelete - DELETE /addressbook/{address_key}/person + async fn addressbook_address_key_person_delete( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyPersonDeletePathParams, + query_params: &models::AddressbookAddressKeyPersonDeleteQueryParams, + ) -> Result; + + /// AddressbookAddressKeyPersonGet - GET /addressbook/{address_key}/person + async fn addressbook_address_key_person_get( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyPersonGetPathParams, + query_params: &models::AddressbookAddressKeyPersonGetQueryParams, + ) -> Result; + + /// AddressbookAddressKeyPersonHead - HEAD /addressbook/{address_key}/person + async fn addressbook_address_key_person_head( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyPersonHeadPathParams, + query_params: &models::AddressbookAddressKeyPersonHeadQueryParams, + ) -> Result; + + /// AddressbookAddressKeyPersonPatch - PATCH /addressbook/{address_key}/person + async fn addressbook_address_key_person_patch( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyPersonPatchPathParams, + query_params: &models::AddressbookAddressKeyPersonPatchQueryParams, + ) -> Result; + + /// AddressbookAddressKeyPersonPost - POST /addressbook/{address_key}/person + async fn addressbook_address_key_person_post( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyPersonPostPathParams, + query_params: &models::AddressbookAddressKeyPersonPostQueryParams, + body: &models::CreatePerson, + ) -> Result; + + /// AddressbookAddressKeyPut - PUT /addressbook/{address_key} + async fn addressbook_address_key_put( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyPutPathParams, + body: &models::UpdateAddressbook, + ) -> Result; + + /// AddressbookAddressKeyStateDelete - DELETE /addressbook/{address_key}/state + async fn addressbook_address_key_state_delete( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyStateDeletePathParams, + ) -> Result; + + /// AddressbookAddressKeyStateGet - GET /addressbook/{address_key}/state + async fn addressbook_address_key_state_get( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyStateGetPathParams, + query_params: &models::AddressbookAddressKeyStateGetQueryParams, + ) -> Result; + + /// AddressbookAddressKeyStateHead - HEAD /addressbook/{address_key}/state + async fn addressbook_address_key_state_head( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyStateHeadPathParams, + ) -> Result; + + /// AddressbookAddressKeyStatePut - PUT /addressbook/{address_key}/state + async fn addressbook_address_key_state_put( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyStatePutPathParams, + body: &String, + ) -> Result; + + /// AddressbookAddressKeyStreetDelete - DELETE /addressbook/{address_key}/street + async fn addressbook_address_key_street_delete( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyStreetDeletePathParams, + ) -> Result; + + /// AddressbookAddressKeyStreetGet - GET /addressbook/{address_key}/street + async fn addressbook_address_key_street_get( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyStreetGetPathParams, + query_params: &models::AddressbookAddressKeyStreetGetQueryParams, + ) -> Result; + + /// AddressbookAddressKeyStreetHead - HEAD /addressbook/{address_key}/street + async fn addressbook_address_key_street_head( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyStreetHeadPathParams, + ) -> Result; + + /// AddressbookAddressKeyStreetPut - PUT /addressbook/{address_key}/street + async fn addressbook_address_key_street_put( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyStreetPutPathParams, + body: &String, + ) -> Result; + + /// AddressbookGet - GET /addressbook + async fn addressbook_get( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + query_params: &models::AddressbookGetQueryParams, + ) -> Result; + + /// AddressbookPost - POST /addressbook + async fn addressbook_post( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + query_params: &models::AddressbookPostQueryParams, + body: &models::CreateAddressbook, + ) -> Result; +} diff --git a/examples/addressbook/server-rs/api/src/apis/mod.rs b/examples/addressbook/server-rs/api/src/apis/mod.rs new file mode 100644 index 0000000..e192467 --- /dev/null +++ b/examples/addressbook/server-rs/api/src/apis/mod.rs @@ -0,0 +1,45 @@ +pub mod addressbook; +pub mod persons; +pub mod postal_codes; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum BasicAuthKind { + Basic, + Bearer, +} + +/// API Key Authentication - Authentication Header. +/// For `Basic token` and `Bearer token` +#[async_trait::async_trait] +pub trait ApiAuthBasic { + type Claims; + + /// Extracting Claims from Header. Return None if the Claims are invalid. + async fn extract_claims_from_auth_header( + &self, + kind: BasicAuthKind, + headers: &axum::http::header::HeaderMap, + key: &str, + ) -> Option; +} + +// Error handler for unhandled errors. +#[async_trait::async_trait] +pub trait ErrorHandler { + #[allow(unused_variables)] + #[tracing::instrument(skip_all)] + async fn handle_error( + &self, + method: &::http::Method, + host: &axum_extra::extract::Host, + cookies: &axum_extra::extract::CookieJar, + error: E, + ) -> Result { + tracing::error!("Unhandled error: {:?}", error); + axum::response::Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .body(axum::body::Body::empty()) + .map_err(|_| http::StatusCode::INTERNAL_SERVER_ERROR) + } +} diff --git a/examples/addressbook/server-rs/api/src/apis/persons.rs b/examples/addressbook/server-rs/api/src/apis/persons.rs new file mode 100644 index 0000000..7d41fac --- /dev/null +++ b/examples/addressbook/server-rs/api/src/apis/persons.rs @@ -0,0 +1,131 @@ +use async_trait::async_trait; +use axum::extract::*; +use axum_extra::extract::{CookieJar, Host}; +use bytes::Bytes; +use http::Method; +use serde::{Deserialize, Serialize}; + +use crate::{models, types::*}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum PersonsGetResponse { + /// Response for OK + Status200_ResponseForOK(Vec), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum PersonsPostResponse { + /// Response for CREATED + Status201_ResponseForCREATED(models::CreatePerson), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum PersonsUuidDeleteResponse { + /// Response for OK + Status200_ResponseForOK(models::Person), + /// One or more validation rules failed + Status409_OneOrMoreValidationRulesFailed(models::ValidationProblem), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum PersonsUuidGetResponse { + /// Response for OK + Status200_ResponseForOK(models::Person), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum PersonsUuidHeadResponse { + /// Default HEAD response + Status200_DefaultHEADResponse, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum PersonsUuidPutResponse { + /// Response for OK + Status200_ResponseForOK(models::UpdatePerson), +} + +/// Persons +#[async_trait] +#[allow(clippy::ptr_arg)] +pub trait Persons: super::ErrorHandler { + type Claims; + + /// PersonsGet - GET /persons + async fn persons_get( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + query_params: &models::PersonsGetQueryParams, + ) -> Result; + + /// PersonsPost - POST /persons + async fn persons_post( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + claims: &Self::Claims, + query_params: &models::PersonsPostQueryParams, + body: &models::CreatePerson, + ) -> Result; + + /// PersonsUuidDelete - DELETE /persons/{uuid} + async fn persons_uuid_delete( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + claims: &Self::Claims, + path_params: &models::PersonsUuidDeletePathParams, + ) -> Result; + + /// PersonsUuidGet - GET /persons/{uuid} + async fn persons_uuid_get( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::PersonsUuidGetPathParams, + query_params: &models::PersonsUuidGetQueryParams, + ) -> Result; + + /// PersonsUuidHead - HEAD /persons/{uuid} + async fn persons_uuid_head( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::PersonsUuidHeadPathParams, + ) -> Result; + + /// PersonsUuidPut - PUT /persons/{uuid} + async fn persons_uuid_put( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + claims: &Self::Claims, + path_params: &models::PersonsUuidPutPathParams, + body: &models::UpdatePerson, + ) -> Result; +} diff --git a/examples/addressbook/server-rs/api/src/apis/postal_codes.rs b/examples/addressbook/server-rs/api/src/apis/postal_codes.rs new file mode 100644 index 0000000..086dcd9 --- /dev/null +++ b/examples/addressbook/server-rs/api/src/apis/postal_codes.rs @@ -0,0 +1,149 @@ +use async_trait::async_trait; +use axum::extract::*; +use axum_extra::extract::{CookieJar, Host}; +use bytes::Bytes; +use http::Method; +use serde::{Deserialize, Serialize}; + +use crate::{models, types::*}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum PostalCodesGetResponse { + /// Response for OK + Status200_ResponseForOK(Vec), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum PostalCodesPostResponse { + /// Response for CREATED + Status201_ResponseForCREATED(models::CreatePostalCode), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum PostalCodesUuidDeleteResponse { + /// Response for OK + Status200_ResponseForOK(models::PostalCode), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum PostalCodesUuidGetResponse { + /// Response for OK + Status200_ResponseForOK(models::PostalCode), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum PostalCodesUuidHeadResponse { + /// Default HEAD response + Status200_DefaultHEADResponse, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum PostalCodesUuidNameDeleteResponse { + /// Response for OK + Status200_ResponseForOK(String), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum PostalCodesUuidNameGetResponse { + /// Response for OK + Status200_ResponseForOK(String), +} + +/// PostalCodes +#[async_trait] +#[allow(clippy::ptr_arg)] +pub trait PostalCodes: + super::ErrorHandler +{ + type Claims; + + /// PostalCodesGet - GET /postal_codes + async fn postal_codes_get( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + query_params: &models::PostalCodesGetQueryParams, + ) -> Result; + + /// PostalCodesPost - POST /postal_codes + async fn postal_codes_post( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + claims: &Self::Claims, + query_params: &models::PostalCodesPostQueryParams, + body: &models::CreatePostalCode, + ) -> Result; + + /// PostalCodesUuidDelete - DELETE /postal_codes/{uuid} + async fn postal_codes_uuid_delete( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + claims: &Self::Claims, + path_params: &models::PostalCodesUuidDeletePathParams, + ) -> Result; + + /// PostalCodesUuidGet - GET /postal_codes/{uuid} + async fn postal_codes_uuid_get( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::PostalCodesUuidGetPathParams, + query_params: &models::PostalCodesUuidGetQueryParams, + ) -> Result; + + /// PostalCodesUuidHead - HEAD /postal_codes/{uuid} + async fn postal_codes_uuid_head( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::PostalCodesUuidHeadPathParams, + ) -> Result; + + /// PostalCodesUuidNameDelete - DELETE /postal_codes/{uuid}/name + async fn postal_codes_uuid_name_delete( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + claims: &Self::Claims, + path_params: &models::PostalCodesUuidNameDeletePathParams, + ) -> Result; + + /// PostalCodesUuidNameGet - GET /postal_codes/{uuid}/name + async fn postal_codes_uuid_name_get( + &self, + + method: &Method, + host: &Host, + cookies: &CookieJar, + path_params: &models::PostalCodesUuidNameGetPathParams, + query_params: &models::PostalCodesUuidNameGetQueryParams, + ) -> Result; +} diff --git a/examples/addressbook/server-rs/api/src/header.rs b/examples/addressbook/server-rs/api/src/header.rs new file mode 100644 index 0000000..970a99d --- /dev/null +++ b/examples/addressbook/server-rs/api/src/header.rs @@ -0,0 +1,188 @@ +use std::{convert::TryFrom, fmt, ops::Deref}; + +use chrono::{DateTime, Utc}; +use http::HeaderValue; + +/// A struct to allow homogeneous conversion into a HeaderValue. We can't +/// implement the From/Into trait on HeaderValue because we don't own +/// either of the types. +#[derive(Debug, Clone)] +pub(crate) struct IntoHeaderValue(pub T); + +// Generic implementations + +impl Deref for IntoHeaderValue { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +// Derive for each TryFrom in http::HeaderValue + +macro_rules! ihv_generate { + ($t:ident) => { + impl TryFrom for IntoHeaderValue<$t> { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> Result { + match hdr_value.to_str() { + Ok(hdr_value) => match hdr_value.parse::<$t>() { + Ok(hdr_value) => Ok(IntoHeaderValue(hdr_value)), + Err(e) => Err(format!( + r#"Unable to parse {} as a string: {e}"#, + stringify!($t) + )), + }, + Err(e) => Err(format!( + r#"Unable to parse header {hdr_value:?} as a string - {e}"# + )), + } + } + } + + impl TryFrom> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: IntoHeaderValue<$t>) -> Result { + Ok(hdr_value.0.into()) + } + } + }; +} + +ihv_generate!(u64); +ihv_generate!(i64); +ihv_generate!(i16); +ihv_generate!(u16); +ihv_generate!(u32); +ihv_generate!(usize); +ihv_generate!(isize); +ihv_generate!(i32); + +// Custom derivations + +// Vec + +impl TryFrom for IntoHeaderValue> { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> Result { + match hdr_value.to_str() { + Ok(hdr_value) => Ok(IntoHeaderValue( + hdr_value + .split(',') + .filter_map(|x| match x.trim() { + "" => None, + y => Some(y.to_string()), + }) + .collect(), + )), + Err(e) => Err(format!( + r#"Unable to parse header: {hdr_value:?} as a string - {e}"# + )), + } + } +} + +impl TryFrom>> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: IntoHeaderValue>) -> Result { + match HeaderValue::from_str(&hdr_value.0.join(", ")) { + Ok(hdr_value) => Ok(hdr_value), + Err(e) => Err(format!( + r#"Unable to convert {hdr_value:?} into a header - {e}"# + )), + } + } +} + +// String + +impl TryFrom for IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> Result { + match hdr_value.to_str() { + Ok(hdr_value) => Ok(IntoHeaderValue(hdr_value.to_string())), + Err(e) => Err(format!(r#"Unable to convert header {hdr_value:?} to {e}"#)), + } + } +} + +impl TryFrom> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: IntoHeaderValue) -> Result { + match HeaderValue::from_str(&hdr_value.0) { + Ok(hdr_value) => Ok(hdr_value), + Err(e) => Err(format!( + r#"Unable to convert {hdr_value:?} from a header {e}"# + )), + } + } +} + +// Bool + +impl TryFrom for IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> Result { + match hdr_value.to_str() { + Ok(hdr_value) => match hdr_value.parse() { + Ok(hdr_value) => Ok(IntoHeaderValue(hdr_value)), + Err(e) => Err(format!(r#"Unable to parse bool from {hdr_value} - {e}"#)), + }, + Err(e) => Err(format!( + r#"Unable to convert {hdr_value:?} from a header {e}"# + )), + } + } +} + +impl TryFrom> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: IntoHeaderValue) -> Result { + match HeaderValue::from_str(&hdr_value.0.to_string()) { + Ok(hdr_value) => Ok(hdr_value), + Err(e) => Err(format!( + r#"Unable to convert: {hdr_value:?} into a header: {e}"# + )), + } + } +} + +// DateTime + +impl TryFrom for IntoHeaderValue> { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> Result { + match hdr_value.to_str() { + Ok(hdr_value) => match DateTime::parse_from_rfc3339(hdr_value) { + Ok(date) => Ok(IntoHeaderValue(date.with_timezone(&Utc))), + Err(e) => Err(format!(r#"Unable to parse: {hdr_value} as date - {e}"#)), + }, + Err(e) => Err(format!( + r#"Unable to convert header {hdr_value:?} to string {e}"# + )), + } + } +} + +impl TryFrom>> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: IntoHeaderValue>) -> Result { + match HeaderValue::from_str(hdr_value.0.to_rfc3339().as_str()) { + Ok(hdr_value) => Ok(hdr_value), + Err(e) => Err(format!( + r#"Unable to convert {hdr_value:?} to a header: {e}"# + )), + } + } +} diff --git a/examples/addressbook/server-rs/api/src/lib.rs b/examples/addressbook/server-rs/api/src/lib.rs new file mode 100644 index 0000000..127b67d --- /dev/null +++ b/examples/addressbook/server-rs/api/src/lib.rs @@ -0,0 +1,28 @@ +#![allow( + missing_docs, + trivial_casts, + unused_variables, + unused_mut, + unused_extern_crates, + non_camel_case_types, + unused_imports, + unused_attributes +)] +#![allow( + clippy::derive_partial_eq_without_eq, + clippy::disallowed_names, + clippy::too_many_arguments +)] + +pub const BASE_PATH: &str = ""; +pub const API_VERSION: &str = "1.0"; + +#[cfg(feature = "server")] +pub mod server; + +pub mod apis; +pub mod models; +pub mod types; + +#[cfg(feature = "server")] +pub(crate) mod header; diff --git a/examples/addressbook/server-rs/api/src/models.rs b/examples/addressbook/server-rs/api/src/models.rs new file mode 100644 index 0000000..45117e6 --- /dev/null +++ b/examples/addressbook/server-rs/api/src/models.rs @@ -0,0 +1,2795 @@ +#![allow(unused_qualifications)] + +use http::HeaderValue; +use validator::Validate; + +#[cfg(feature = "server")] +use crate::header; +use crate::{models, types::*}; + +#[allow(dead_code)] +fn from_validation_error(e: validator::ValidationError) -> validator::ValidationErrors { + let mut errs = validator::ValidationErrors::new(); + errs.add("na", e); + errs +} + +#[allow(dead_code)] +pub fn check_xss_string(v: &str) -> std::result::Result<(), validator::ValidationError> { + if ammonia::is_html(v) { + std::result::Result::Err(validator::ValidationError::new("xss detected")) + } else { + std::result::Result::Ok(()) + } +} + +#[allow(dead_code)] +pub fn check_xss_vec_string(v: &[String]) -> std::result::Result<(), validator::ValidationError> { + if v.iter().any(|i| ammonia::is_html(i)) { + std::result::Result::Err(validator::ValidationError::new("xss detected")) + } else { + std::result::Result::Ok(()) + } +} + +#[allow(dead_code)] +pub fn check_xss_map_string( + v: &std::collections::HashMap, +) -> std::result::Result<(), validator::ValidationError> { + if v.keys().any(|k| ammonia::is_html(k)) || v.values().any(|v| ammonia::is_html(v)) { + std::result::Result::Err(validator::ValidationError::new("xss detected")) + } else { + std::result::Result::Ok(()) + } +} + +#[allow(dead_code)] +pub fn check_xss_map_nested( + v: &std::collections::HashMap, +) -> std::result::Result<(), validator::ValidationError> +where + T: validator::Validate, +{ + if v.keys().any(|k| ammonia::is_html(k)) || v.values().any(|v| v.validate().is_err()) { + std::result::Result::Err(validator::ValidationError::new("xss detected")) + } else { + std::result::Result::Ok(()) + } +} + +#[allow(dead_code)] +pub fn check_xss_map( + v: &std::collections::HashMap, +) -> std::result::Result<(), validator::ValidationError> { + if v.keys().any(|k| ammonia::is_html(k)) { + std::result::Result::Err(validator::ValidationError::new("xss detected")) + } else { + std::result::Result::Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyAddrtypeDeletePathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyAddrtypeGetPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyAddrtypeGetQueryParams { + /// Filter by city name + #[serde(rename = "city")] + #[serde(skip_serializing_if = "Option::is_none")] + pub city: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyAddrtypeHeadPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyAddrtypePutPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyCityDeletePathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyCityGetPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyCityGetQueryParams { + /// Filter by city name + #[serde(rename = "city")] + #[serde(skip_serializing_if = "Option::is_none")] + pub city: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyCityHeadPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyCityPutPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyCountryDeletePathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyCountryGetPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyCountryGetQueryParams { + /// Filter by city name + #[serde(rename = "city")] + #[serde(skip_serializing_if = "Option::is_none")] + pub city: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyCountryHeadPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyCountryPutPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyDeletePathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyGetPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyGetQueryParams { + /// Filter by city name + #[serde(rename = "city")] + #[serde(skip_serializing_if = "Option::is_none")] + pub city: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyHeadPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyIsValidDeletePathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyIsValidGetPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyIsValidGetQueryParams { + /// Filter by city name + #[serde(rename = "city")] + #[serde(skip_serializing_if = "Option::is_none")] + pub city: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyIsValidHeadPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyIsValidPutPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyPeopleDeletePathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyPeopleGetPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyPeopleGetQueryParams { + /// Filter by city name + #[serde(rename = "city")] + #[serde(skip_serializing_if = "Option::is_none")] + pub city: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyPeopleHeadPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyPeoplePutPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyPersonDeletePathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyPersonDeleteQueryParams { + /// Limit the number of responses back + #[serde(rename = "limit")] + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// The offset to start returning resources + #[serde(rename = "offset")] + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyPersonGetPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyPersonGetQueryParams { + /// Filter by last name + #[serde(rename = "last_name")] + #[serde(skip_serializing_if = "Option::is_none")] + pub last_name: Option, + /// Limit the number of responses back + #[serde(rename = "limit")] + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// The offset to start returning resources + #[serde(rename = "offset")] + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyPersonHeadPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyPersonHeadQueryParams { + /// Limit the number of responses back + #[serde(rename = "limit")] + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// The offset to start returning resources + #[serde(rename = "offset")] + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyPersonPatchPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyPersonPatchQueryParams { + /// Limit the number of responses back + #[serde(rename = "limit")] + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// The offset to start returning resources + #[serde(rename = "offset")] + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyPersonPostPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyPersonPostQueryParams { + /// Limit the number of responses back + #[serde(rename = "limit")] + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// The offset to start returning resources + #[serde(rename = "offset")] + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyPutPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyStateDeletePathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyStateGetPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyStateGetQueryParams { + /// Filter by city name + #[serde(rename = "city")] + #[serde(skip_serializing_if = "Option::is_none")] + pub city: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyStateHeadPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyStatePutPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyStreetDeletePathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyStreetGetPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyStreetGetQueryParams { + /// Filter by city name + #[serde(rename = "city")] + #[serde(skip_serializing_if = "Option::is_none")] + pub city: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyStreetHeadPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookAddressKeyStreetPutPathParams { + pub address_key: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookGetQueryParams { + /// Filter by city name + #[serde(rename = "city")] + #[serde(skip_serializing_if = "Option::is_none")] + pub city: Option, + /// Limit the number of responses back + #[serde(rename = "limit")] + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// The offset to start returning resources + #[serde(rename = "offset")] + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct AddressbookPostQueryParams { + /// Limit the number of responses back + #[serde(rename = "limit")] + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// The offset to start returning resources + #[serde(rename = "offset")] + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct PersonsGetQueryParams { + /// Filter by last name + #[serde(rename = "last_name")] + #[serde(skip_serializing_if = "Option::is_none")] + pub last_name: Option, + /// Limit the number of responses back + #[serde(rename = "limit")] + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// The offset to start returning resources + #[serde(rename = "offset")] + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct PersonsPostQueryParams { + /// Limit the number of responses back + #[serde(rename = "limit")] + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// The offset to start returning resources + #[serde(rename = "offset")] + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct PersonsUuidDeletePathParams { + pub uuid: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct PersonsUuidGetPathParams { + pub uuid: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct PersonsUuidGetQueryParams { + /// Filter by last name + #[serde(rename = "last_name")] + #[serde(skip_serializing_if = "Option::is_none")] + pub last_name: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct PersonsUuidHeadPathParams { + pub uuid: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct PersonsUuidPutPathParams { + pub uuid: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct PostalCodesGetQueryParams { + /// Filter by name + #[serde(rename = "name")] + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// Limit the number of responses back + #[serde(rename = "limit")] + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// The offset to start returning resources + #[serde(rename = "offset")] + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct PostalCodesPostQueryParams { + /// Limit the number of responses back + #[serde(rename = "limit")] + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// The offset to start returning resources + #[serde(rename = "offset")] + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct PostalCodesUuidDeletePathParams { + pub uuid: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct PostalCodesUuidGetPathParams { + pub uuid: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct PostalCodesUuidGetQueryParams { + /// Filter by name + #[serde(rename = "name")] + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct PostalCodesUuidHeadPathParams { + pub uuid: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct PostalCodesUuidNameDeletePathParams { + pub uuid: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct PostalCodesUuidNameGetPathParams { + pub uuid: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct PostalCodesUuidNameGetQueryParams { + /// Filter by name + #[serde(rename = "name")] + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct Addressbook { + #[serde(rename = "address_key")] + #[serde(skip_serializing_if = "Option::is_none")] + pub address_key: Option, + + /// The address type, e.g. work or home + /// Note: inline enums are not fully supported by openapi-generator + #[serde(rename = "addrtype")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub addrtype: Option, + + /// The city of this address + #[serde(rename = "city")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub city: Option, + + /// The country of this address + #[serde(rename = "country")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub country: Option, + + /// Address is valid or not + #[serde(rename = "is_valid")] + #[serde(skip_serializing_if = "Option::is_none")] + pub is_valid: Option, + + /// A list of people's names living there + #[serde(rename = "people")] + #[validate(custom(function = "check_xss_vec_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub people: Option>, + + #[serde(rename = "person")] + #[validate(nested)] + #[serde(skip_serializing_if = "Option::is_none")] + pub person: Option, + + /// The state of this address + #[serde(rename = "state")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub state: Option, + + /// The street and civic number of this address + #[serde(rename = "street")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub street: Option, +} + +impl Addressbook { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new() -> Addressbook { + Addressbook { + address_key: None, + addrtype: None, + city: None, + country: None, + is_valid: None, + people: None, + person: None, + state: None, + street: None, + } + } +} + +/// Converts the Addressbook value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for Addressbook { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + // Skipping address_key in query parameter serialization + self.addrtype + .as_ref() + .map(|addrtype| ["addrtype".to_string(), addrtype.to_string()].join(",")), + self.city + .as_ref() + .map(|city| ["city".to_string(), city.to_string()].join(",")), + self.country + .as_ref() + .map(|country| ["country".to_string(), country.to_string()].join(",")), + self.is_valid + .as_ref() + .map(|is_valid| ["is_valid".to_string(), is_valid.to_string()].join(",")), + self.people.as_ref().map(|people| { + [ + "people".to_string(), + people + .iter() + .map(|x| x.to_string()) + .collect::>() + .join(","), + ] + .join(",") + }), + // Skipping person in query parameter serialization + self.state + .as_ref() + .map(|state| ["state".to_string(), state.to_string()].join(",")), + self.street + .as_ref() + .map(|street| ["street".to_string(), street.to_string()].join(",")), + ]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a Addressbook value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for Addressbook { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub address_key: Vec, + pub addrtype: Vec, + pub city: Vec, + pub country: Vec, + pub is_valid: Vec, + pub people: Vec>, + pub person: Vec, + pub state: Vec, + pub street: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err( + "Missing value while parsing Addressbook".to_string(), + ); + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "address_key" => intermediate_rep.address_key.push( + ::from_str(val) + .map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "addrtype" => intermediate_rep.addrtype.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "city" => intermediate_rep.city.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "country" => intermediate_rep.country.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "is_valid" => intermediate_rep.is_valid.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + "people" => { + return std::result::Result::Err( + "Parsing a container in this style is not supported in Addressbook" + .to_string(), + ); + } + #[allow(clippy::redundant_clone)] + "person" => intermediate_rep.person.push( + ::from_str(val) + .map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "state" => intermediate_rep.state.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "street" => intermediate_rep.street.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing Addressbook".to_string(), + ); + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(Addressbook { + address_key: intermediate_rep.address_key.into_iter().next(), + addrtype: intermediate_rep.addrtype.into_iter().next(), + city: intermediate_rep.city.into_iter().next(), + country: intermediate_rep.country.into_iter().next(), + is_valid: intermediate_rep.is_valid.into_iter().next(), + people: intermediate_rep.people.into_iter().next(), + person: intermediate_rep.person.into_iter().next(), + state: intermediate_rep.state.into_iter().next(), + street: intermediate_rep.street.into_iter().next(), + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Invalid header value for Addressbook - value: {hdr_value} is invalid {e}"# + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + r#"Unable to convert header value '{value}' into Addressbook - {err}"# + )), + } + } + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Unable to convert header: {hdr_value:?} to string: {e}"# + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct CreateAddressbook { + #[serde(rename = "address_key")] + #[serde(skip_serializing_if = "Option::is_none")] + pub address_key: Option, + + /// The address type, e.g. work or home + /// Note: inline enums are not fully supported by openapi-generator + #[serde(rename = "addrtype")] + #[validate(custom(function = "check_xss_string"))] + pub addrtype: String, + + /// The city of this address + #[serde(rename = "city")] + #[validate(custom(function = "check_xss_string"))] + pub city: String, + + /// The country of this address + #[serde(rename = "country")] + #[validate(custom(function = "check_xss_string"))] + pub country: String, + + /// Address is valid or not + #[serde(rename = "is_valid")] + #[serde(skip_serializing_if = "Option::is_none")] + pub is_valid: Option, + + /// A list of people's names living there + #[serde(rename = "people")] + #[validate(custom(function = "check_xss_vec_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub people: Option>, + + #[serde(rename = "person")] + #[validate(nested)] + #[serde(skip_serializing_if = "Option::is_none")] + pub person: Option, + + /// The state of this address + #[serde(rename = "state")] + #[validate(custom(function = "check_xss_string"))] + pub state: String, + + /// The street and civic number of this address + #[serde(rename = "street")] + #[validate(custom(function = "check_xss_string"))] + pub street: String, +} + +impl CreateAddressbook { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new( + addrtype: String, + city: String, + country: String, + state: String, + street: String, + ) -> CreateAddressbook { + CreateAddressbook { + address_key: None, + addrtype, + city, + country, + is_valid: None, + people: None, + person: None, + state, + street, + } + } +} + +/// Converts the CreateAddressbook value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for CreateAddressbook { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + // Skipping address_key in query parameter serialization + Some("addrtype".to_string()), + Some(self.addrtype.to_string()), + Some("city".to_string()), + Some(self.city.to_string()), + Some("country".to_string()), + Some(self.country.to_string()), + self.is_valid + .as_ref() + .map(|is_valid| ["is_valid".to_string(), is_valid.to_string()].join(",")), + self.people.as_ref().map(|people| { + [ + "people".to_string(), + people + .iter() + .map(|x| x.to_string()) + .collect::>() + .join(","), + ] + .join(",") + }), + // Skipping person in query parameter serialization + Some("state".to_string()), + Some(self.state.to_string()), + Some("street".to_string()), + Some(self.street.to_string()), + ]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a CreateAddressbook value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for CreateAddressbook { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub address_key: Vec, + pub addrtype: Vec, + pub city: Vec, + pub country: Vec, + pub is_valid: Vec, + pub people: Vec>, + pub person: Vec, + pub state: Vec, + pub street: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err( + "Missing value while parsing CreateAddressbook".to_string(), + ); + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "address_key" => intermediate_rep.address_key.push( + ::from_str(val) + .map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "addrtype" => intermediate_rep.addrtype.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "city" => intermediate_rep.city.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "country" => intermediate_rep.country.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "is_valid" => intermediate_rep.is_valid.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + "people" => return std::result::Result::Err( + "Parsing a container in this style is not supported in CreateAddressbook" + .to_string(), + ), + #[allow(clippy::redundant_clone)] + "person" => intermediate_rep.person.push( + ::from_str(val) + .map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "state" => intermediate_rep.state.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "street" => intermediate_rep.street.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing CreateAddressbook".to_string(), + ); + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(CreateAddressbook { + address_key: intermediate_rep.address_key.into_iter().next(), + addrtype: intermediate_rep + .addrtype + .into_iter() + .next() + .ok_or_else(|| "addrtype missing in CreateAddressbook".to_string())?, + city: intermediate_rep + .city + .into_iter() + .next() + .ok_or_else(|| "city missing in CreateAddressbook".to_string())?, + country: intermediate_rep + .country + .into_iter() + .next() + .ok_or_else(|| "country missing in CreateAddressbook".to_string())?, + is_valid: intermediate_rep.is_valid.into_iter().next(), + people: intermediate_rep.people.into_iter().next(), + person: intermediate_rep.person.into_iter().next(), + state: intermediate_rep + .state + .into_iter() + .next() + .ok_or_else(|| "state missing in CreateAddressbook".to_string())?, + street: intermediate_rep + .street + .into_iter() + .next() + .ok_or_else(|| "street missing in CreateAddressbook".to_string())?, + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Invalid header value for CreateAddressbook - value: {hdr_value} is invalid {e}"# + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + r#"Unable to convert header value '{value}' into CreateAddressbook - {err}"# + )), + } + } + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Unable to convert header: {hdr_value:?} to string: {e}"# + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct CreatePerson { + /// The person's age + #[serde(rename = "age")] + #[serde(skip_serializing_if = "Option::is_none")] + pub age: Option, + + /// The person's first name + #[serde(rename = "first_name")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub first_name: Option, + + /// The person's hobbies + #[serde(rename = "hobbies")] + #[validate(custom(function = "check_xss_vec_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub hobbies: Option>, + + /// The person's last name + #[serde(rename = "last_name")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub last_name: Option, + + /// The person's timezone (e.g. UTC, America/New_York) + #[serde(rename = "timezone")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub timezone: Option, + + #[serde(rename = "uuid")] + #[serde(skip_serializing_if = "Option::is_none")] + pub uuid: Option, +} + +impl CreatePerson { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new() -> CreatePerson { + CreatePerson { + age: None, + first_name: None, + hobbies: None, + last_name: None, + timezone: Some(r#"UTC"#.to_string()), + uuid: None, + } + } +} + +/// Converts the CreatePerson value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for CreatePerson { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + self.age + .as_ref() + .map(|age| ["age".to_string(), age.to_string()].join(",")), + self.first_name + .as_ref() + .map(|first_name| ["first_name".to_string(), first_name.to_string()].join(",")), + self.hobbies.as_ref().map(|hobbies| { + [ + "hobbies".to_string(), + hobbies + .iter() + .map(|x| x.to_string()) + .collect::>() + .join(","), + ] + .join(",") + }), + self.last_name + .as_ref() + .map(|last_name| ["last_name".to_string(), last_name.to_string()].join(",")), + self.timezone + .as_ref() + .map(|timezone| ["timezone".to_string(), timezone.to_string()].join(",")), + // Skipping uuid in query parameter serialization + ]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a CreatePerson value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for CreatePerson { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub age: Vec, + pub first_name: Vec, + pub hobbies: Vec>, + pub last_name: Vec, + pub timezone: Vec, + pub uuid: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err( + "Missing value while parsing CreatePerson".to_string(), + ); + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "age" => intermediate_rep.age.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "first_name" => intermediate_rep.first_name.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + "hobbies" => { + return std::result::Result::Err( + "Parsing a container in this style is not supported in CreatePerson" + .to_string(), + ); + } + #[allow(clippy::redundant_clone)] + "last_name" => intermediate_rep.last_name.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "timezone" => intermediate_rep.timezone.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "uuid" => intermediate_rep.uuid.push( + ::from_str(val) + .map_err(|x| x.to_string())?, + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing CreatePerson".to_string(), + ); + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(CreatePerson { + age: intermediate_rep.age.into_iter().next(), + first_name: intermediate_rep.first_name.into_iter().next(), + hobbies: intermediate_rep.hobbies.into_iter().next(), + last_name: intermediate_rep.last_name.into_iter().next(), + timezone: intermediate_rep.timezone.into_iter().next(), + uuid: intermediate_rep.uuid.into_iter().next(), + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Invalid header value for CreatePerson - value: {hdr_value} is invalid {e}"# + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + r#"Unable to convert header value '{value}' into CreatePerson - {err}"# + )), + } + } + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Unable to convert header: {hdr_value:?} to string: {e}"# + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct CreatePostalCode { + /// The postal code's name/id + #[serde(rename = "name")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + #[serde(rename = "uuid")] + #[serde(skip_serializing_if = "Option::is_none")] + pub uuid: Option, +} + +impl CreatePostalCode { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new() -> CreatePostalCode { + CreatePostalCode { + name: None, + uuid: None, + } + } +} + +/// Converts the CreatePostalCode value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for CreatePostalCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + self.name + .as_ref() + .map(|name| ["name".to_string(), name.to_string()].join(",")), + // Skipping uuid in query parameter serialization + ]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a CreatePostalCode value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for CreatePostalCode { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub name: Vec, + pub uuid: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err( + "Missing value while parsing CreatePostalCode".to_string(), + ); + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "name" => intermediate_rep.name.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "uuid" => intermediate_rep.uuid.push( + ::from_str(val) + .map_err(|x| x.to_string())?, + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing CreatePostalCode".to_string(), + ); + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(CreatePostalCode { + name: intermediate_rep.name.into_iter().next(), + uuid: intermediate_rep.uuid.into_iter().next(), + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Invalid header value for CreatePostalCode - value: {hdr_value} is invalid {e}"# + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + r#"Unable to convert header value '{value}' into CreatePostalCode - {err}"# + )), + } + } + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Unable to convert header: {hdr_value:?} to string: {e}"# + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct Person { + /// The person's age + #[serde(rename = "age")] + #[serde(skip_serializing_if = "Option::is_none")] + pub age: Option, + + /// The person's first name + #[serde(rename = "first_name")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub first_name: Option, + + /// The person's hobbies + #[serde(rename = "hobbies")] + #[validate(custom(function = "check_xss_vec_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub hobbies: Option>, + + /// The person's last name + #[serde(rename = "last_name")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub last_name: Option, + + /// The person's timezone (e.g. UTC, America/New_York) + #[serde(rename = "timezone")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub timezone: Option, + + #[serde(rename = "uuid")] + #[serde(skip_serializing_if = "Option::is_none")] + pub uuid: Option, +} + +impl Person { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new() -> Person { + Person { + age: None, + first_name: None, + hobbies: None, + last_name: None, + timezone: Some(r#"UTC"#.to_string()), + uuid: None, + } + } +} + +/// Converts the Person value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for Person { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + self.age + .as_ref() + .map(|age| ["age".to_string(), age.to_string()].join(",")), + self.first_name + .as_ref() + .map(|first_name| ["first_name".to_string(), first_name.to_string()].join(",")), + self.hobbies.as_ref().map(|hobbies| { + [ + "hobbies".to_string(), + hobbies + .iter() + .map(|x| x.to_string()) + .collect::>() + .join(","), + ] + .join(",") + }), + self.last_name + .as_ref() + .map(|last_name| ["last_name".to_string(), last_name.to_string()].join(",")), + self.timezone + .as_ref() + .map(|timezone| ["timezone".to_string(), timezone.to_string()].join(",")), + // Skipping uuid in query parameter serialization + ]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a Person value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for Person { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub age: Vec, + pub first_name: Vec, + pub hobbies: Vec>, + pub last_name: Vec, + pub timezone: Vec, + pub uuid: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err( + "Missing value while parsing Person".to_string(), + ); + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "age" => intermediate_rep.age.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "first_name" => intermediate_rep.first_name.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + "hobbies" => { + return std::result::Result::Err( + "Parsing a container in this style is not supported in Person" + .to_string(), + ); + } + #[allow(clippy::redundant_clone)] + "last_name" => intermediate_rep.last_name.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "timezone" => intermediate_rep.timezone.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "uuid" => intermediate_rep.uuid.push( + ::from_str(val) + .map_err(|x| x.to_string())?, + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing Person".to_string(), + ); + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(Person { + age: intermediate_rep.age.into_iter().next(), + first_name: intermediate_rep.first_name.into_iter().next(), + hobbies: intermediate_rep.hobbies.into_iter().next(), + last_name: intermediate_rep.last_name.into_iter().next(), + timezone: intermediate_rep.timezone.into_iter().next(), + uuid: intermediate_rep.uuid.into_iter().next(), + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Invalid header value for Person - value: {hdr_value} is invalid {e}"# + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + r#"Unable to convert header value '{value}' into Person - {err}"# + )), + } + } + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Unable to convert header: {hdr_value:?} to string: {e}"# + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct PostalCode { + /// The postal code's name/id + #[serde(rename = "name")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + #[serde(rename = "uuid")] + #[serde(skip_serializing_if = "Option::is_none")] + pub uuid: Option, +} + +impl PostalCode { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new() -> PostalCode { + PostalCode { + name: None, + uuid: None, + } + } +} + +/// Converts the PostalCode value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for PostalCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + self.name + .as_ref() + .map(|name| ["name".to_string(), name.to_string()].join(",")), + // Skipping uuid in query parameter serialization + ]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a PostalCode value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for PostalCode { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub name: Vec, + pub uuid: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err( + "Missing value while parsing PostalCode".to_string(), + ); + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "name" => intermediate_rep.name.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "uuid" => intermediate_rep.uuid.push( + ::from_str(val) + .map_err(|x| x.to_string())?, + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing PostalCode".to_string(), + ); + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(PostalCode { + name: intermediate_rep.name.into_iter().next(), + uuid: intermediate_rep.uuid.into_iter().next(), + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Invalid header value for PostalCode - value: {hdr_value} is invalid {e}"# + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + r#"Unable to convert header value '{value}' into PostalCode - {err}"# + )), + } + } + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Unable to convert header: {hdr_value:?} to string: {e}"# + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct UpdateAddressbook { + #[serde(rename = "address_key")] + #[serde(skip_serializing_if = "Option::is_none")] + pub address_key: Option, + + /// The address type, e.g. work or home + /// Note: inline enums are not fully supported by openapi-generator + #[serde(rename = "addrtype")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub addrtype: Option, + + /// The city of this address + #[serde(rename = "city")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub city: Option, + + /// The country of this address + #[serde(rename = "country")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub country: Option, + + /// Address is valid or not + #[serde(rename = "is_valid")] + #[serde(skip_serializing_if = "Option::is_none")] + pub is_valid: Option, + + /// A list of people's names living there + #[serde(rename = "people")] + #[validate(custom(function = "check_xss_vec_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub people: Option>, + + #[serde(rename = "person")] + #[validate(nested)] + #[serde(skip_serializing_if = "Option::is_none")] + pub person: Option, + + /// The state of this address + #[serde(rename = "state")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub state: Option, + + /// The street and civic number of this address + #[serde(rename = "street")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub street: Option, +} + +impl UpdateAddressbook { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new() -> UpdateAddressbook { + UpdateAddressbook { + address_key: None, + addrtype: None, + city: None, + country: None, + is_valid: None, + people: None, + person: None, + state: None, + street: None, + } + } +} + +/// Converts the UpdateAddressbook value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for UpdateAddressbook { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + // Skipping address_key in query parameter serialization + self.addrtype + .as_ref() + .map(|addrtype| ["addrtype".to_string(), addrtype.to_string()].join(",")), + self.city + .as_ref() + .map(|city| ["city".to_string(), city.to_string()].join(",")), + self.country + .as_ref() + .map(|country| ["country".to_string(), country.to_string()].join(",")), + self.is_valid + .as_ref() + .map(|is_valid| ["is_valid".to_string(), is_valid.to_string()].join(",")), + self.people.as_ref().map(|people| { + [ + "people".to_string(), + people + .iter() + .map(|x| x.to_string()) + .collect::>() + .join(","), + ] + .join(",") + }), + // Skipping person in query parameter serialization + self.state + .as_ref() + .map(|state| ["state".to_string(), state.to_string()].join(",")), + self.street + .as_ref() + .map(|street| ["street".to_string(), street.to_string()].join(",")), + ]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a UpdateAddressbook value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for UpdateAddressbook { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub address_key: Vec, + pub addrtype: Vec, + pub city: Vec, + pub country: Vec, + pub is_valid: Vec, + pub people: Vec>, + pub person: Vec, + pub state: Vec, + pub street: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err( + "Missing value while parsing UpdateAddressbook".to_string(), + ); + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "address_key" => intermediate_rep.address_key.push( + ::from_str(val) + .map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "addrtype" => intermediate_rep.addrtype.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "city" => intermediate_rep.city.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "country" => intermediate_rep.country.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "is_valid" => intermediate_rep.is_valid.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + "people" => return std::result::Result::Err( + "Parsing a container in this style is not supported in UpdateAddressbook" + .to_string(), + ), + #[allow(clippy::redundant_clone)] + "person" => intermediate_rep.person.push( + ::from_str(val) + .map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "state" => intermediate_rep.state.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "street" => intermediate_rep.street.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing UpdateAddressbook".to_string(), + ); + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(UpdateAddressbook { + address_key: intermediate_rep.address_key.into_iter().next(), + addrtype: intermediate_rep.addrtype.into_iter().next(), + city: intermediate_rep.city.into_iter().next(), + country: intermediate_rep.country.into_iter().next(), + is_valid: intermediate_rep.is_valid.into_iter().next(), + people: intermediate_rep.people.into_iter().next(), + person: intermediate_rep.person.into_iter().next(), + state: intermediate_rep.state.into_iter().next(), + street: intermediate_rep.street.into_iter().next(), + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Invalid header value for UpdateAddressbook - value: {hdr_value} is invalid {e}"# + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + r#"Unable to convert header value '{value}' into UpdateAddressbook - {err}"# + )), + } + } + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Unable to convert header: {hdr_value:?} to string: {e}"# + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct UpdatePerson { + /// The person's age + #[serde(rename = "age")] + #[serde(skip_serializing_if = "Option::is_none")] + pub age: Option, + + /// The person's first name + #[serde(rename = "first_name")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub first_name: Option, + + /// The person's hobbies + #[serde(rename = "hobbies")] + #[validate(custom(function = "check_xss_vec_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub hobbies: Option>, + + /// The person's last name + #[serde(rename = "last_name")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub last_name: Option, + + /// The person's timezone (e.g. UTC, America/New_York) + #[serde(rename = "timezone")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub timezone: Option, + + #[serde(rename = "uuid")] + #[serde(skip_serializing_if = "Option::is_none")] + pub uuid: Option, +} + +impl UpdatePerson { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new() -> UpdatePerson { + UpdatePerson { + age: None, + first_name: None, + hobbies: None, + last_name: None, + timezone: Some(r#"UTC"#.to_string()), + uuid: None, + } + } +} + +/// Converts the UpdatePerson value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for UpdatePerson { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + self.age + .as_ref() + .map(|age| ["age".to_string(), age.to_string()].join(",")), + self.first_name + .as_ref() + .map(|first_name| ["first_name".to_string(), first_name.to_string()].join(",")), + self.hobbies.as_ref().map(|hobbies| { + [ + "hobbies".to_string(), + hobbies + .iter() + .map(|x| x.to_string()) + .collect::>() + .join(","), + ] + .join(",") + }), + self.last_name + .as_ref() + .map(|last_name| ["last_name".to_string(), last_name.to_string()].join(",")), + self.timezone + .as_ref() + .map(|timezone| ["timezone".to_string(), timezone.to_string()].join(",")), + // Skipping uuid in query parameter serialization + ]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a UpdatePerson value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for UpdatePerson { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub age: Vec, + pub first_name: Vec, + pub hobbies: Vec>, + pub last_name: Vec, + pub timezone: Vec, + pub uuid: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err( + "Missing value while parsing UpdatePerson".to_string(), + ); + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "age" => intermediate_rep.age.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "first_name" => intermediate_rep.first_name.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + "hobbies" => { + return std::result::Result::Err( + "Parsing a container in this style is not supported in UpdatePerson" + .to_string(), + ); + } + #[allow(clippy::redundant_clone)] + "last_name" => intermediate_rep.last_name.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "timezone" => intermediate_rep.timezone.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "uuid" => intermediate_rep.uuid.push( + ::from_str(val) + .map_err(|x| x.to_string())?, + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing UpdatePerson".to_string(), + ); + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(UpdatePerson { + age: intermediate_rep.age.into_iter().next(), + first_name: intermediate_rep.first_name.into_iter().next(), + hobbies: intermediate_rep.hobbies.into_iter().next(), + last_name: intermediate_rep.last_name.into_iter().next(), + timezone: intermediate_rep.timezone.into_iter().next(), + uuid: intermediate_rep.uuid.into_iter().next(), + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Invalid header value for UpdatePerson - value: {hdr_value} is invalid {e}"# + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + r#"Unable to convert header value '{value}' into UpdatePerson - {err}"# + )), + } + } + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Unable to convert header: {hdr_value:?} to string: {e}"# + )), + } + } +} + +/// An RFC 9457 problem detail, returned when one or more validation rules fail. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct ValidationProblem { + /// The first violation's message. + #[serde(rename = "detail")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + + /// The HTTP status code. + #[serde(rename = "status")] + pub status: i32, + + /// A short summary of the problem. + #[serde(rename = "title")] + #[validate(custom(function = "check_xss_string"))] + pub title: String, + + #[serde(rename = "type")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub r_type: Option, + + /// Every rule that failed for this request. + #[serde(rename = "violations")] + #[validate(nested)] + pub violations: Vec, +} + +impl ValidationProblem { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new( + status: i32, + title: String, + violations: Vec, + ) -> ValidationProblem { + ValidationProblem { + detail: None, + status, + title, + r_type: Some(r#"about:blank"#.to_string()), + violations, + } + } +} + +/// Converts the ValidationProblem value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for ValidationProblem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + self.detail + .as_ref() + .map(|detail| ["detail".to_string(), detail.to_string()].join(",")), + Some("status".to_string()), + Some(self.status.to_string()), + Some("title".to_string()), + Some(self.title.to_string()), + self.r_type + .as_ref() + .map(|r_type| ["type".to_string(), r_type.to_string()].join(",")), + // Skipping violations in query parameter serialization + ]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a ValidationProblem value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for ValidationProblem { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub detail: Vec, + pub status: Vec, + pub title: Vec, + pub r_type: Vec, + pub violations: Vec>, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err( + "Missing value while parsing ValidationProblem".to_string(), + ); + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "detail" => intermediate_rep.detail.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "status" => intermediate_rep.status.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "title" => intermediate_rep.title.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "type" => intermediate_rep.r_type.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + "violations" => return std::result::Result::Err( + "Parsing a container in this style is not supported in ValidationProblem" + .to_string(), + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing ValidationProblem".to_string(), + ); + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(ValidationProblem { + detail: intermediate_rep.detail.into_iter().next(), + status: intermediate_rep + .status + .into_iter() + .next() + .ok_or_else(|| "status missing in ValidationProblem".to_string())?, + title: intermediate_rep + .title + .into_iter() + .next() + .ok_or_else(|| "title missing in ValidationProblem".to_string())?, + r_type: intermediate_rep.r_type.into_iter().next(), + violations: intermediate_rep + .violations + .into_iter() + .next() + .ok_or_else(|| "violations missing in ValidationProblem".to_string())?, + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Invalid header value for ValidationProblem - value: {hdr_value} is invalid {e}"# + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + r#"Unable to convert header value '{value}' into ValidationProblem - {err}"# + )), + } + } + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Unable to convert header: {hdr_value:?} to string: {e}"# + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct ValidationProblemViolationsInner { + /// The field the rule applies to, if any. + #[serde(rename = "field")] + #[validate(custom(function = "check_xss_string"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub field: Option, + + /// Why the rule failed. + #[serde(rename = "message")] + #[validate(custom(function = "check_xss_string"))] + pub message: String, + + /// The resource validated. + #[serde(rename = "resource")] + #[validate(custom(function = "check_xss_string"))] + pub resource: String, + + /// The name of the rule. + #[serde(rename = "rule")] + #[validate(custom(function = "check_xss_string"))] + pub rule: String, +} + +impl ValidationProblemViolationsInner { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new( + message: String, + resource: String, + rule: String, + ) -> ValidationProblemViolationsInner { + ValidationProblemViolationsInner { + field: None, + message, + resource, + rule, + } + } +} + +/// Converts the ValidationProblemViolationsInner value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for ValidationProblemViolationsInner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + self.field + .as_ref() + .map(|field| ["field".to_string(), field.to_string()].join(",")), + Some("message".to_string()), + Some(self.message.to_string()), + Some("resource".to_string()), + Some(self.resource.to_string()), + Some("rule".to_string()), + Some(self.rule.to_string()), + ]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a ValidationProblemViolationsInner value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for ValidationProblemViolationsInner { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub field: Vec, + pub message: Vec, + pub resource: Vec, + pub rule: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err( + "Missing value while parsing ValidationProblemViolationsInner".to_string(), + ); + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "field" => intermediate_rep.field.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "message" => intermediate_rep.message.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "resource" => intermediate_rep.resource.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "rule" => intermediate_rep.rule.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing ValidationProblemViolationsInner" + .to_string(), + ); + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(ValidationProblemViolationsInner { + field: intermediate_rep.field.into_iter().next(), + message: intermediate_rep + .message + .into_iter() + .next() + .ok_or_else(|| "message missing in ValidationProblemViolationsInner".to_string())?, + resource: intermediate_rep + .resource + .into_iter() + .next() + .ok_or_else(|| { + "resource missing in ValidationProblemViolationsInner".to_string() + })?, + rule: intermediate_rep + .rule + .into_iter() + .next() + .ok_or_else(|| "rule missing in ValidationProblemViolationsInner".to_string())?, + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> + for HeaderValue +{ + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Invalid header value for ValidationProblemViolationsInner - value: {hdr_value} is invalid {e}"# + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom + for header::IntoHeaderValue +{ + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + r#"Unable to convert header value '{value}' into ValidationProblemViolationsInner - {err}"# + )), + } + } + std::result::Result::Err(e) => std::result::Result::Err(format!( + r#"Unable to convert header: {hdr_value:?} to string: {e}"# + )), + } + } +} diff --git a/examples/addressbook/server-rs/api/src/server/mod.rs b/examples/addressbook/server-rs/api/src/server/mod.rs new file mode 100644 index 0000000..a5bfcc7 --- /dev/null +++ b/examples/addressbook/server-rs/api/src/server/mod.rs @@ -0,0 +1,4867 @@ +use std::collections::HashMap; + +use axum::{body::Body, extract::*, response::Response, routing::*}; +use axum_extra::extract::{CookieJar, Host, Query as QueryExtra}; +use bytes::Bytes; +use http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, header::CONTENT_TYPE}; +use tracing::error; +use validator::{Validate, ValidationErrors}; + +use crate::{header, types::*}; + +#[allow(unused_imports)] +use crate::{apis, models}; + +#[allow(unused_imports)] +use crate::{ + models::check_xss_map, models::check_xss_map_nested, models::check_xss_map_string, + models::check_xss_string, models::check_xss_vec_string, +}; + +/// Setup API Server. +pub fn new(api_impl: I) -> Router +where + I: AsRef + Clone + Send + Sync + 'static, + A: apis::addressbook::Addressbook + + apis::persons::Persons + + apis::postal_codes::PostalCodes + + apis::ApiAuthBasic + + Send + + Sync + + 'static, + E: std::fmt::Debug + Send + Sync + 'static, + C: Send + Sync + 'static, +{ + // build our application with a route + Router::new() + .route( + "/addressbook", + get(addressbook_get::).post(addressbook_post::), + ) + .route( + "/addressbook/{address_key}", + delete(addressbook_address_key_delete::) + .get(addressbook_address_key_get::) + .head(addressbook_address_key_head::) + .put(addressbook_address_key_put::), + ) + .route( + "/addressbook/{address_key}/addrtype", + delete(addressbook_address_key_addrtype_delete::) + .get(addressbook_address_key_addrtype_get::) + .head(addressbook_address_key_addrtype_head::) + .put(addressbook_address_key_addrtype_put::), + ) + .route( + "/addressbook/{address_key}/city", + delete(addressbook_address_key_city_delete::) + .get(addressbook_address_key_city_get::) + .head(addressbook_address_key_city_head::) + .put(addressbook_address_key_city_put::), + ) + .route( + "/addressbook/{address_key}/country", + delete(addressbook_address_key_country_delete::) + .get(addressbook_address_key_country_get::) + .head(addressbook_address_key_country_head::) + .put(addressbook_address_key_country_put::), + ) + .route( + "/addressbook/{address_key}/is_valid", + delete(addressbook_address_key_is_valid_delete::) + .get(addressbook_address_key_is_valid_get::) + .head(addressbook_address_key_is_valid_head::) + .put(addressbook_address_key_is_valid_put::), + ) + .route( + "/addressbook/{address_key}/people", + delete(addressbook_address_key_people_delete::) + .get(addressbook_address_key_people_get::) + .head(addressbook_address_key_people_head::) + .put(addressbook_address_key_people_put::), + ) + .route( + "/addressbook/{address_key}/person", + delete(addressbook_address_key_person_delete::) + .get(addressbook_address_key_person_get::) + .head(addressbook_address_key_person_head::) + .patch(addressbook_address_key_person_patch::) + .post(addressbook_address_key_person_post::), + ) + .route( + "/addressbook/{address_key}/state", + delete(addressbook_address_key_state_delete::) + .get(addressbook_address_key_state_get::) + .head(addressbook_address_key_state_head::) + .put(addressbook_address_key_state_put::), + ) + .route( + "/addressbook/{address_key}/street", + delete(addressbook_address_key_street_delete::) + .get(addressbook_address_key_street_get::) + .head(addressbook_address_key_street_head::) + .put(addressbook_address_key_street_put::), + ) + .route( + "/persons", + get(persons_get::).post(persons_post::), + ) + .route( + "/persons/{uuid}", + delete(persons_uuid_delete::) + .get(persons_uuid_get::) + .head(persons_uuid_head::) + .put(persons_uuid_put::), + ) + .route( + "/postal_codes", + get(postal_codes_get::).post(postal_codes_post::), + ) + .route( + "/postal_codes/{uuid}", + delete(postal_codes_uuid_delete::) + .get(postal_codes_uuid_get::) + .head(postal_codes_uuid_head::), + ) + .route( + "/postal_codes/{uuid}/name", + delete(postal_codes_uuid_name_delete::) + .get(postal_codes_uuid_name_get::), + ) + .with_state(api_impl) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_addrtype_delete_validation( + path_params: models::AddressbookAddressKeyAddrtypeDeletePathParams, +) -> std::result::Result<(models::AddressbookAddressKeyAddrtypeDeletePathParams,), ValidationErrors> +{ + path_params.validate()?; + + Ok((path_params,)) +} +/// AddressbookAddressKeyAddrtypeDelete - DELETE /addressbook/{address_key}/addrtype +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_addrtype_delete( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_addrtype_delete_validation(path_params) + }) + .await + .unwrap(); + + let Ok((path_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_addrtype_delete(&method, &host, &cookies, &path_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyAddrtypeDeleteResponse::Status200_ResponseForOK + (body) + => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_addrtype_get_validation( + path_params: models::AddressbookAddressKeyAddrtypeGetPathParams, + query_params: models::AddressbookAddressKeyAddrtypeGetQueryParams, +) -> std::result::Result< + ( + models::AddressbookAddressKeyAddrtypeGetPathParams, + models::AddressbookAddressKeyAddrtypeGetQueryParams, + ), + ValidationErrors, +> { + path_params.validate()?; + query_params.validate()?; + + Ok((path_params, query_params)) +} +/// AddressbookAddressKeyAddrtypeGet - GET /addressbook/{address_key}/addrtype +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_addrtype_get( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_addrtype_get_validation(path_params, query_params) + }) + .await + .unwrap(); + + let Ok((path_params, query_params)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_addrtype_get(&method, &host, &cookies, &path_params, &query_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyAddrtypeGetResponse::Status200_ResponseForOK + (body) + => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_addrtype_head_validation( + path_params: models::AddressbookAddressKeyAddrtypeHeadPathParams, +) -> std::result::Result<(models::AddressbookAddressKeyAddrtypeHeadPathParams,), ValidationErrors> { + path_params.validate()?; + + Ok((path_params,)) +} +/// AddressbookAddressKeyAddrtypeHead - HEAD /addressbook/{address_key}/addrtype +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_addrtype_head( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_addrtype_head_validation(path_params) + }) + .await + .unwrap(); + + let Ok((path_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_addrtype_head(&method, &host, &cookies, &path_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyAddrtypeHeadResponse::Status200_DefaultHEADResponse + => { + let mut response = response.status(200); + response.body(Body::empty()) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[derive(validator::Validate)] +#[allow(dead_code)] +struct AddressbookAddressKeyAddrtypePutBodyValidator<'a> { + #[validate(custom(function = "check_xss_string"))] + body: &'a String, +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_addrtype_put_validation( + path_params: models::AddressbookAddressKeyAddrtypePutPathParams, + body: String, +) -> std::result::Result< + (models::AddressbookAddressKeyAddrtypePutPathParams, String), + ValidationErrors, +> { + path_params.validate()?; + let b = AddressbookAddressKeyAddrtypePutBodyValidator { body: &body }; + b.validate()?; + + Ok((path_params, body)) +} +/// AddressbookAddressKeyAddrtypePut - PUT /addressbook/{address_key}/addrtype +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_addrtype_put( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, + Json(body): Json, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_addrtype_put_validation(path_params, body) + }) + .await + .unwrap(); + + let Ok((path_params, body)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_addrtype_put(&method, &host, &cookies, &path_params, &body) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyAddrtypePutResponse::Status200_ResponseForOK + (body) + => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + apis::addressbook::AddressbookAddressKeyAddrtypePutResponse::Status403_OneOrMoreValidationRulesFailed + (body) + => { + let mut response = response.status(403); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/problem+json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + apis::addressbook::AddressbookAddressKeyAddrtypePutResponse::Status422_OneOrMoreValidationRulesFailed + (body) + => { + let mut response = response.status(422); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/problem+json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_city_delete_validation( + path_params: models::AddressbookAddressKeyCityDeletePathParams, +) -> std::result::Result<(models::AddressbookAddressKeyCityDeletePathParams,), ValidationErrors> { + path_params.validate()?; + + Ok((path_params,)) +} +/// AddressbookAddressKeyCityDelete - DELETE /addressbook/{address_key}/city +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_city_delete( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_city_delete_validation(path_params) + }) + .await + .unwrap(); + + let Ok((path_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_city_delete(&method, &host, &cookies, &path_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyCityDeleteResponse::Status200_ResponseForOK( + body, + ) => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_city_get_validation( + path_params: models::AddressbookAddressKeyCityGetPathParams, + query_params: models::AddressbookAddressKeyCityGetQueryParams, +) -> std::result::Result< + ( + models::AddressbookAddressKeyCityGetPathParams, + models::AddressbookAddressKeyCityGetQueryParams, + ), + ValidationErrors, +> { + path_params.validate()?; + query_params.validate()?; + + Ok((path_params, query_params)) +} +/// AddressbookAddressKeyCityGet - GET /addressbook/{address_key}/city +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_city_get( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_city_get_validation(path_params, query_params) + }) + .await + .unwrap(); + + let Ok((path_params, query_params)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_city_get(&method, &host, &cookies, &path_params, &query_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyCityGetResponse::Status200_ResponseForOK( + body, + ) => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_city_head_validation( + path_params: models::AddressbookAddressKeyCityHeadPathParams, +) -> std::result::Result<(models::AddressbookAddressKeyCityHeadPathParams,), ValidationErrors> { + path_params.validate()?; + + Ok((path_params,)) +} +/// AddressbookAddressKeyCityHead - HEAD /addressbook/{address_key}/city +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_city_head( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_city_head_validation(path_params) + }) + .await + .unwrap(); + + let Ok((path_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_city_head(&method, &host, &cookies, &path_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyCityHeadResponse::Status200_DefaultHEADResponse + => { + let mut response = response.status(200); + response.body(Body::empty()) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[derive(validator::Validate)] +#[allow(dead_code)] +struct AddressbookAddressKeyCityPutBodyValidator<'a> { + #[validate(custom(function = "check_xss_string"))] + body: &'a String, +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_city_put_validation( + path_params: models::AddressbookAddressKeyCityPutPathParams, + body: String, +) -> std::result::Result<(models::AddressbookAddressKeyCityPutPathParams, String), ValidationErrors> +{ + path_params.validate()?; + let b = AddressbookAddressKeyCityPutBodyValidator { body: &body }; + b.validate()?; + + Ok((path_params, body)) +} +/// AddressbookAddressKeyCityPut - PUT /addressbook/{address_key}/city +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_city_put( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, + Json(body): Json, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_city_put_validation(path_params, body) + }) + .await + .unwrap(); + + let Ok((path_params, body)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_city_put(&method, &host, &cookies, &path_params, &body) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyCityPutResponse::Status200_ResponseForOK + (body) + => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + apis::addressbook::AddressbookAddressKeyCityPutResponse::Status403_OneOrMoreValidationRulesFailed + (body) + => { + let mut response = response.status(403); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/problem+json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + apis::addressbook::AddressbookAddressKeyCityPutResponse::Status422_OneOrMoreValidationRulesFailed + (body) + => { + let mut response = response.status(422); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/problem+json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_country_delete_validation( + path_params: models::AddressbookAddressKeyCountryDeletePathParams, +) -> std::result::Result<(models::AddressbookAddressKeyCountryDeletePathParams,), ValidationErrors> +{ + path_params.validate()?; + + Ok((path_params,)) +} +/// AddressbookAddressKeyCountryDelete - DELETE /addressbook/{address_key}/country +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_country_delete( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_country_delete_validation(path_params) + }) + .await + .unwrap(); + + let Ok((path_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_country_delete(&method, &host, &cookies, &path_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyCountryDeleteResponse::Status200_ResponseForOK + (body) + => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_country_get_validation( + path_params: models::AddressbookAddressKeyCountryGetPathParams, + query_params: models::AddressbookAddressKeyCountryGetQueryParams, +) -> std::result::Result< + ( + models::AddressbookAddressKeyCountryGetPathParams, + models::AddressbookAddressKeyCountryGetQueryParams, + ), + ValidationErrors, +> { + path_params.validate()?; + query_params.validate()?; + + Ok((path_params, query_params)) +} +/// AddressbookAddressKeyCountryGet - GET /addressbook/{address_key}/country +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_country_get( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_country_get_validation(path_params, query_params) + }) + .await + .unwrap(); + + let Ok((path_params, query_params)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_country_get(&method, &host, &cookies, &path_params, &query_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyCountryGetResponse::Status200_ResponseForOK( + body, + ) => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_country_head_validation( + path_params: models::AddressbookAddressKeyCountryHeadPathParams, +) -> std::result::Result<(models::AddressbookAddressKeyCountryHeadPathParams,), ValidationErrors> { + path_params.validate()?; + + Ok((path_params,)) +} +/// AddressbookAddressKeyCountryHead - HEAD /addressbook/{address_key}/country +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_country_head( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_country_head_validation(path_params) + }) + .await + .unwrap(); + + let Ok((path_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_country_head(&method, &host, &cookies, &path_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyCountryHeadResponse::Status200_DefaultHEADResponse + => { + let mut response = response.status(200); + response.body(Body::empty()) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[derive(validator::Validate)] +#[allow(dead_code)] +struct AddressbookAddressKeyCountryPutBodyValidator<'a> { + #[validate(custom(function = "check_xss_string"))] + body: &'a String, +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_country_put_validation( + path_params: models::AddressbookAddressKeyCountryPutPathParams, + body: String, +) -> std::result::Result< + (models::AddressbookAddressKeyCountryPutPathParams, String), + ValidationErrors, +> { + path_params.validate()?; + let b = AddressbookAddressKeyCountryPutBodyValidator { body: &body }; + b.validate()?; + + Ok((path_params, body)) +} +/// AddressbookAddressKeyCountryPut - PUT /addressbook/{address_key}/country +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_country_put( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, + Json(body): Json, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_country_put_validation(path_params, body) + }) + .await + .unwrap(); + + let Ok((path_params, body)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_country_put(&method, &host, &cookies, &path_params, &body) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyCountryPutResponse::Status200_ResponseForOK + (body) + => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + apis::addressbook::AddressbookAddressKeyCountryPutResponse::Status403_OneOrMoreValidationRulesFailed + (body) + => { + let mut response = response.status(403); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/problem+json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + apis::addressbook::AddressbookAddressKeyCountryPutResponse::Status422_OneOrMoreValidationRulesFailed + (body) + => { + let mut response = response.status(422); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/problem+json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_delete_validation( + path_params: models::AddressbookAddressKeyDeletePathParams, +) -> std::result::Result<(models::AddressbookAddressKeyDeletePathParams,), ValidationErrors> { + path_params.validate()?; + + Ok((path_params,)) +} +/// AddressbookAddressKeyDelete - DELETE /addressbook/{address_key} +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_delete( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = + tokio::task::spawn_blocking(move || addressbook_address_key_delete_validation(path_params)) + .await + .unwrap(); + + let Ok((path_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_delete(&method, &host, &cookies, &path_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyDeleteResponse::Status200_ResponseForOK( + body, + ) => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_get_validation( + path_params: models::AddressbookAddressKeyGetPathParams, + query_params: models::AddressbookAddressKeyGetQueryParams, +) -> std::result::Result< + ( + models::AddressbookAddressKeyGetPathParams, + models::AddressbookAddressKeyGetQueryParams, + ), + ValidationErrors, +> { + path_params.validate()?; + query_params.validate()?; + + Ok((path_params, query_params)) +} +/// AddressbookAddressKeyGet - GET /addressbook/{address_key} +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_get( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_get_validation(path_params, query_params) + }) + .await + .unwrap(); + + let Ok((path_params, query_params)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_get(&method, &host, &cookies, &path_params, &query_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyGetResponse::Status200_ResponseForOK(body) => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_head_validation( + path_params: models::AddressbookAddressKeyHeadPathParams, +) -> std::result::Result<(models::AddressbookAddressKeyHeadPathParams,), ValidationErrors> { + path_params.validate()?; + + Ok((path_params,)) +} +/// AddressbookAddressKeyHead - HEAD /addressbook/{address_key} +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_head( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = + tokio::task::spawn_blocking(move || addressbook_address_key_head_validation(path_params)) + .await + .unwrap(); + + let Ok((path_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_head(&method, &host, &cookies, &path_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyHeadResponse::Status200_DefaultHEADResponse => { + let mut response = response.status(200); + response.body(Body::empty()) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_is_valid_delete_validation( + path_params: models::AddressbookAddressKeyIsValidDeletePathParams, +) -> std::result::Result<(models::AddressbookAddressKeyIsValidDeletePathParams,), ValidationErrors> +{ + path_params.validate()?; + + Ok((path_params,)) +} +/// AddressbookAddressKeyIsValidDelete - DELETE /addressbook/{address_key}/is_valid +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_is_valid_delete( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_is_valid_delete_validation(path_params) + }) + .await + .unwrap(); + + let Ok((path_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_is_valid_delete(&method, &host, &cookies, &path_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyIsValidDeleteResponse::Status200_ResponseForOK + (body) + => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_is_valid_get_validation( + path_params: models::AddressbookAddressKeyIsValidGetPathParams, + query_params: models::AddressbookAddressKeyIsValidGetQueryParams, +) -> std::result::Result< + ( + models::AddressbookAddressKeyIsValidGetPathParams, + models::AddressbookAddressKeyIsValidGetQueryParams, + ), + ValidationErrors, +> { + path_params.validate()?; + query_params.validate()?; + + Ok((path_params, query_params)) +} +/// AddressbookAddressKeyIsValidGet - GET /addressbook/{address_key}/is_valid +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_is_valid_get( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_is_valid_get_validation(path_params, query_params) + }) + .await + .unwrap(); + + let Ok((path_params, query_params)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_is_valid_get(&method, &host, &cookies, &path_params, &query_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyIsValidGetResponse::Status200_ResponseForOK( + body, + ) => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_is_valid_head_validation( + path_params: models::AddressbookAddressKeyIsValidHeadPathParams, +) -> std::result::Result<(models::AddressbookAddressKeyIsValidHeadPathParams,), ValidationErrors> { + path_params.validate()?; + + Ok((path_params,)) +} +/// AddressbookAddressKeyIsValidHead - HEAD /addressbook/{address_key}/is_valid +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_is_valid_head( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_is_valid_head_validation(path_params) + }) + .await + .unwrap(); + + let Ok((path_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_is_valid_head(&method, &host, &cookies, &path_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyIsValidHeadResponse::Status200_DefaultHEADResponse + => { + let mut response = response.status(200); + response.body(Body::empty()) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[derive(validator::Validate)] +#[allow(dead_code)] +struct AddressbookAddressKeyIsValidPutBodyValidator<'a> { + body: &'a bool, +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_is_valid_put_validation( + path_params: models::AddressbookAddressKeyIsValidPutPathParams, + body: bool, +) -> std::result::Result<(models::AddressbookAddressKeyIsValidPutPathParams, bool), ValidationErrors> +{ + path_params.validate()?; + let b = AddressbookAddressKeyIsValidPutBodyValidator { body: &body }; + b.validate()?; + + Ok((path_params, body)) +} +/// AddressbookAddressKeyIsValidPut - PUT /addressbook/{address_key}/is_valid +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_is_valid_put( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, + Json(body): Json, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_is_valid_put_validation(path_params, body) + }) + .await + .unwrap(); + + let Ok((path_params, body)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_is_valid_put(&method, &host, &cookies, &path_params, &body) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyIsValidPutResponse::Status200_ResponseForOK + (body) + => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + apis::addressbook::AddressbookAddressKeyIsValidPutResponse::Status403_OneOrMoreValidationRulesFailed + (body) + => { + let mut response = response.status(403); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/problem+json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + apis::addressbook::AddressbookAddressKeyIsValidPutResponse::Status422_OneOrMoreValidationRulesFailed + (body) + => { + let mut response = response.status(422); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/problem+json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_people_delete_validation( + path_params: models::AddressbookAddressKeyPeopleDeletePathParams, +) -> std::result::Result<(models::AddressbookAddressKeyPeopleDeletePathParams,), ValidationErrors> { + path_params.validate()?; + + Ok((path_params,)) +} +/// AddressbookAddressKeyPeopleDelete - DELETE /addressbook/{address_key}/people +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_people_delete( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_people_delete_validation(path_params) + }) + .await + .unwrap(); + + let Ok((path_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_people_delete(&method, &host, &cookies, &path_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyPeopleDeleteResponse::Status200_ResponseForOK + (body) + => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_people_get_validation( + path_params: models::AddressbookAddressKeyPeopleGetPathParams, + query_params: models::AddressbookAddressKeyPeopleGetQueryParams, +) -> std::result::Result< + ( + models::AddressbookAddressKeyPeopleGetPathParams, + models::AddressbookAddressKeyPeopleGetQueryParams, + ), + ValidationErrors, +> { + path_params.validate()?; + query_params.validate()?; + + Ok((path_params, query_params)) +} +/// AddressbookAddressKeyPeopleGet - GET /addressbook/{address_key}/people +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_people_get( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_people_get_validation(path_params, query_params) + }) + .await + .unwrap(); + + let Ok((path_params, query_params)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_people_get(&method, &host, &cookies, &path_params, &query_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyPeopleGetResponse::Status200_ResponseForOK( + body, + ) => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_people_head_validation( + path_params: models::AddressbookAddressKeyPeopleHeadPathParams, +) -> std::result::Result<(models::AddressbookAddressKeyPeopleHeadPathParams,), ValidationErrors> { + path_params.validate()?; + + Ok((path_params,)) +} +/// AddressbookAddressKeyPeopleHead - HEAD /addressbook/{address_key}/people +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_people_head( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_people_head_validation(path_params) + }) + .await + .unwrap(); + + let Ok((path_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_people_head(&method, &host, &cookies, &path_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyPeopleHeadResponse::Status200_DefaultHEADResponse + => { + let mut response = response.status(200); + response.body(Body::empty()) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[derive(validator::Validate)] +#[allow(dead_code)] +struct AddressbookAddressKeyPeoplePutBodyValidator<'a> { + #[validate(custom(function = "check_xss_vec_string"))] + body: &'a Vec, +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_people_put_validation( + path_params: models::AddressbookAddressKeyPeoplePutPathParams, + body: Vec, +) -> std::result::Result< + ( + models::AddressbookAddressKeyPeoplePutPathParams, + Vec, + ), + ValidationErrors, +> { + path_params.validate()?; + let b = AddressbookAddressKeyPeoplePutBodyValidator { body: &body }; + b.validate()?; + + Ok((path_params, body)) +} +/// AddressbookAddressKeyPeoplePut - PUT /addressbook/{address_key}/people +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_people_put( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, + Json(body): Json>, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_people_put_validation(path_params, body) + }) + .await + .unwrap(); + + let Ok((path_params, body)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_people_put(&method, &host, &cookies, &path_params, &body) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyPeoplePutResponse::Status200_ResponseForOK + (body) + => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + apis::addressbook::AddressbookAddressKeyPeoplePutResponse::Status403_OneOrMoreValidationRulesFailed + (body) + => { + let mut response = response.status(403); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/problem+json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + apis::addressbook::AddressbookAddressKeyPeoplePutResponse::Status422_OneOrMoreValidationRulesFailed + (body) + => { + let mut response = response.status(422); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/problem+json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_person_delete_validation( + path_params: models::AddressbookAddressKeyPersonDeletePathParams, + query_params: models::AddressbookAddressKeyPersonDeleteQueryParams, +) -> std::result::Result< + ( + models::AddressbookAddressKeyPersonDeletePathParams, + models::AddressbookAddressKeyPersonDeleteQueryParams, + ), + ValidationErrors, +> { + path_params.validate()?; + query_params.validate()?; + + Ok((path_params, query_params)) +} +/// AddressbookAddressKeyPersonDelete - DELETE /addressbook/{address_key}/person +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_person_delete( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_person_delete_validation(path_params, query_params) + }) + .await + .unwrap(); + + let Ok((path_params, query_params)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_person_delete( + &method, + &host, + &cookies, + &path_params, + &query_params, + ) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyPersonDeleteResponse::Status200_ResponseForOK + (body) + => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_person_get_validation( + path_params: models::AddressbookAddressKeyPersonGetPathParams, + query_params: models::AddressbookAddressKeyPersonGetQueryParams, +) -> std::result::Result< + ( + models::AddressbookAddressKeyPersonGetPathParams, + models::AddressbookAddressKeyPersonGetQueryParams, + ), + ValidationErrors, +> { + path_params.validate()?; + query_params.validate()?; + + Ok((path_params, query_params)) +} +/// AddressbookAddressKeyPersonGet - GET /addressbook/{address_key}/person +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_person_get( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_person_get_validation(path_params, query_params) + }) + .await + .unwrap(); + + let Ok((path_params, query_params)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_person_get(&method, &host, &cookies, &path_params, &query_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyPersonGetResponse::Status200_ResponseForOK( + body, + ) => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_person_head_validation( + path_params: models::AddressbookAddressKeyPersonHeadPathParams, + query_params: models::AddressbookAddressKeyPersonHeadQueryParams, +) -> std::result::Result< + ( + models::AddressbookAddressKeyPersonHeadPathParams, + models::AddressbookAddressKeyPersonHeadQueryParams, + ), + ValidationErrors, +> { + path_params.validate()?; + query_params.validate()?; + + Ok((path_params, query_params)) +} +/// AddressbookAddressKeyPersonHead - HEAD /addressbook/{address_key}/person +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_person_head( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_person_head_validation(path_params, query_params) + }) + .await + .unwrap(); + + let Ok((path_params, query_params)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_person_head(&method, &host, &cookies, &path_params, &query_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyPersonHeadResponse::Status200_DefaultHEADResponse + => { + let mut response = response.status(200); + response.body(Body::empty()) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_person_patch_validation( + path_params: models::AddressbookAddressKeyPersonPatchPathParams, + query_params: models::AddressbookAddressKeyPersonPatchQueryParams, +) -> std::result::Result< + ( + models::AddressbookAddressKeyPersonPatchPathParams, + models::AddressbookAddressKeyPersonPatchQueryParams, + ), + ValidationErrors, +> { + path_params.validate()?; + query_params.validate()?; + + Ok((path_params, query_params)) +} +/// AddressbookAddressKeyPersonPatch - PATCH /addressbook/{address_key}/person +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_person_patch( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_person_patch_validation(path_params, query_params) + }) + .await + .unwrap(); + + let Ok((path_params, query_params)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_person_patch(&method, &host, &cookies, &path_params, &query_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyPersonPatchResponse::Status200_ResponseForOK + (body) + => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + apis::addressbook::AddressbookAddressKeyPersonPatchResponse::Status403_OneOrMoreValidationRulesFailed + (body) + => { + let mut response = response.status(403); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/problem+json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + apis::addressbook::AddressbookAddressKeyPersonPatchResponse::Status422_OneOrMoreValidationRulesFailed + (body) + => { + let mut response = response.status(422); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/problem+json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[derive(validator::Validate)] +#[allow(dead_code)] +struct AddressbookAddressKeyPersonPostBodyValidator<'a> { + #[validate(nested)] + body: &'a models::CreatePerson, +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_person_post_validation( + path_params: models::AddressbookAddressKeyPersonPostPathParams, + query_params: models::AddressbookAddressKeyPersonPostQueryParams, + body: models::CreatePerson, +) -> std::result::Result< + ( + models::AddressbookAddressKeyPersonPostPathParams, + models::AddressbookAddressKeyPersonPostQueryParams, + models::CreatePerson, + ), + ValidationErrors, +> { + path_params.validate()?; + query_params.validate()?; + let b = AddressbookAddressKeyPersonPostBodyValidator { body: &body }; + b.validate()?; + + Ok((path_params, query_params, body)) +} +/// AddressbookAddressKeyPersonPost - POST /addressbook/{address_key}/person +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_person_post( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, + Json(body): Json, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_person_post_validation(path_params, query_params, body) + }) + .await + .unwrap(); + + let Ok((path_params, query_params, body)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_person_post( + &method, + &host, + &cookies, + &path_params, + &query_params, + &body, + ) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyPersonPostResponse::Status201_ResponseForCREATED + (body) + => { + let mut response = response.status(201); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + apis::addressbook::AddressbookAddressKeyPersonPostResponse::Status422_OneOrMoreValidationRulesFailed + (body) + => { + let mut response = response.status(422); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/problem+json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[derive(validator::Validate)] +#[allow(dead_code)] +struct AddressbookAddressKeyPutBodyValidator<'a> { + #[validate(nested)] + body: &'a models::UpdateAddressbook, +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_put_validation( + path_params: models::AddressbookAddressKeyPutPathParams, + body: models::UpdateAddressbook, +) -> std::result::Result< + ( + models::AddressbookAddressKeyPutPathParams, + models::UpdateAddressbook, + ), + ValidationErrors, +> { + path_params.validate()?; + let b = AddressbookAddressKeyPutBodyValidator { body: &body }; + b.validate()?; + + Ok((path_params, body)) +} +/// AddressbookAddressKeyPut - PUT /addressbook/{address_key} +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_put( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, + Json(body): Json, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_put_validation(path_params, body) + }) + .await + .unwrap(); + + let Ok((path_params, body)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_put(&method, &host, &cookies, &path_params, &body) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyPutResponse::Status200_ResponseForOK + (body) + => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + apis::addressbook::AddressbookAddressKeyPutResponse::Status403_OneOrMoreValidationRulesFailed + (body) + => { + let mut response = response.status(403); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/problem+json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + apis::addressbook::AddressbookAddressKeyPutResponse::Status422_OneOrMoreValidationRulesFailed + (body) + => { + let mut response = response.status(422); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/problem+json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_state_delete_validation( + path_params: models::AddressbookAddressKeyStateDeletePathParams, +) -> std::result::Result<(models::AddressbookAddressKeyStateDeletePathParams,), ValidationErrors> { + path_params.validate()?; + + Ok((path_params,)) +} +/// AddressbookAddressKeyStateDelete - DELETE /addressbook/{address_key}/state +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_state_delete( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_state_delete_validation(path_params) + }) + .await + .unwrap(); + + let Ok((path_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_state_delete(&method, &host, &cookies, &path_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyStateDeleteResponse::Status200_ResponseForOK + (body) + => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_state_get_validation( + path_params: models::AddressbookAddressKeyStateGetPathParams, + query_params: models::AddressbookAddressKeyStateGetQueryParams, +) -> std::result::Result< + ( + models::AddressbookAddressKeyStateGetPathParams, + models::AddressbookAddressKeyStateGetQueryParams, + ), + ValidationErrors, +> { + path_params.validate()?; + query_params.validate()?; + + Ok((path_params, query_params)) +} +/// AddressbookAddressKeyStateGet - GET /addressbook/{address_key}/state +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_state_get( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_state_get_validation(path_params, query_params) + }) + .await + .unwrap(); + + let Ok((path_params, query_params)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_state_get(&method, &host, &cookies, &path_params, &query_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyStateGetResponse::Status200_ResponseForOK( + body, + ) => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_state_head_validation( + path_params: models::AddressbookAddressKeyStateHeadPathParams, +) -> std::result::Result<(models::AddressbookAddressKeyStateHeadPathParams,), ValidationErrors> { + path_params.validate()?; + + Ok((path_params,)) +} +/// AddressbookAddressKeyStateHead - HEAD /addressbook/{address_key}/state +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_state_head( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_state_head_validation(path_params) + }) + .await + .unwrap(); + + let Ok((path_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_state_head(&method, &host, &cookies, &path_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyStateHeadResponse::Status200_DefaultHEADResponse + => { + let mut response = response.status(200); + response.body(Body::empty()) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[derive(validator::Validate)] +#[allow(dead_code)] +struct AddressbookAddressKeyStatePutBodyValidator<'a> { + #[validate(custom(function = "check_xss_string"))] + body: &'a String, +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_state_put_validation( + path_params: models::AddressbookAddressKeyStatePutPathParams, + body: String, +) -> std::result::Result<(models::AddressbookAddressKeyStatePutPathParams, String), ValidationErrors> +{ + path_params.validate()?; + let b = AddressbookAddressKeyStatePutBodyValidator { body: &body }; + b.validate()?; + + Ok((path_params, body)) +} +/// AddressbookAddressKeyStatePut - PUT /addressbook/{address_key}/state +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_state_put( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, + Json(body): Json, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_state_put_validation(path_params, body) + }) + .await + .unwrap(); + + let Ok((path_params, body)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_state_put(&method, &host, &cookies, &path_params, &body) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyStatePutResponse::Status200_ResponseForOK + (body) + => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + apis::addressbook::AddressbookAddressKeyStatePutResponse::Status403_OneOrMoreValidationRulesFailed + (body) + => { + let mut response = response.status(403); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/problem+json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + apis::addressbook::AddressbookAddressKeyStatePutResponse::Status422_OneOrMoreValidationRulesFailed + (body) + => { + let mut response = response.status(422); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/problem+json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_street_delete_validation( + path_params: models::AddressbookAddressKeyStreetDeletePathParams, +) -> std::result::Result<(models::AddressbookAddressKeyStreetDeletePathParams,), ValidationErrors> { + path_params.validate()?; + + Ok((path_params,)) +} +/// AddressbookAddressKeyStreetDelete - DELETE /addressbook/{address_key}/street +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_street_delete( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_street_delete_validation(path_params) + }) + .await + .unwrap(); + + let Ok((path_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_street_delete(&method, &host, &cookies, &path_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyStreetDeleteResponse::Status200_ResponseForOK + (body) + => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_street_get_validation( + path_params: models::AddressbookAddressKeyStreetGetPathParams, + query_params: models::AddressbookAddressKeyStreetGetQueryParams, +) -> std::result::Result< + ( + models::AddressbookAddressKeyStreetGetPathParams, + models::AddressbookAddressKeyStreetGetQueryParams, + ), + ValidationErrors, +> { + path_params.validate()?; + query_params.validate()?; + + Ok((path_params, query_params)) +} +/// AddressbookAddressKeyStreetGet - GET /addressbook/{address_key}/street +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_street_get( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_street_get_validation(path_params, query_params) + }) + .await + .unwrap(); + + let Ok((path_params, query_params)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_street_get(&method, &host, &cookies, &path_params, &query_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyStreetGetResponse::Status200_ResponseForOK( + body, + ) => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_street_head_validation( + path_params: models::AddressbookAddressKeyStreetHeadPathParams, +) -> std::result::Result<(models::AddressbookAddressKeyStreetHeadPathParams,), ValidationErrors> { + path_params.validate()?; + + Ok((path_params,)) +} +/// AddressbookAddressKeyStreetHead - HEAD /addressbook/{address_key}/street +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_street_head( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_street_head_validation(path_params) + }) + .await + .unwrap(); + + let Ok((path_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_street_head(&method, &host, &cookies, &path_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyStreetHeadResponse::Status200_DefaultHEADResponse + => { + let mut response = response.status(200); + response.body(Body::empty()) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[derive(validator::Validate)] +#[allow(dead_code)] +struct AddressbookAddressKeyStreetPutBodyValidator<'a> { + #[validate(custom(function = "check_xss_string"))] + body: &'a String, +} + +#[tracing::instrument(skip_all)] +fn addressbook_address_key_street_put_validation( + path_params: models::AddressbookAddressKeyStreetPutPathParams, + body: String, +) -> std::result::Result<(models::AddressbookAddressKeyStreetPutPathParams, String), ValidationErrors> +{ + path_params.validate()?; + let b = AddressbookAddressKeyStreetPutBodyValidator { body: &body }; + b.validate()?; + + Ok((path_params, body)) +} +/// AddressbookAddressKeyStreetPut - PUT /addressbook/{address_key}/street +#[tracing::instrument(skip_all)] +async fn addressbook_address_key_street_put( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, + Json(body): Json, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + addressbook_address_key_street_put_validation(path_params, body) + }) + .await + .unwrap(); + + let Ok((path_params, body)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_address_key_street_put(&method, &host, &cookies, &path_params, &body) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookAddressKeyStreetPutResponse::Status200_ResponseForOK + (body) + => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + apis::addressbook::AddressbookAddressKeyStreetPutResponse::Status403_OneOrMoreValidationRulesFailed + (body) + => { + let mut response = response.status(403); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/problem+json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + apis::addressbook::AddressbookAddressKeyStreetPutResponse::Status422_OneOrMoreValidationRulesFailed + (body) + => { + let mut response = response.status(422); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/problem+json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn addressbook_get_validation( + query_params: models::AddressbookGetQueryParams, +) -> std::result::Result<(models::AddressbookGetQueryParams,), ValidationErrors> { + query_params.validate()?; + + Ok((query_params,)) +} +/// AddressbookGet - GET /addressbook +#[tracing::instrument(skip_all)] +async fn addressbook_get( + method: Method, + host: Host, + cookies: CookieJar, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || addressbook_get_validation(query_params)) + .await + .unwrap(); + + let Ok((query_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_get(&method, &host, &cookies, &query_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookGetResponse::Status200_ResponseForOK(body) => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[derive(validator::Validate)] +#[allow(dead_code)] +struct AddressbookPostBodyValidator<'a> { + #[validate(nested)] + body: &'a models::CreateAddressbook, +} + +#[tracing::instrument(skip_all)] +fn addressbook_post_validation( + query_params: models::AddressbookPostQueryParams, + body: models::CreateAddressbook, +) -> std::result::Result< + ( + models::AddressbookPostQueryParams, + models::CreateAddressbook, + ), + ValidationErrors, +> { + query_params.validate()?; + let b = AddressbookPostBodyValidator { body: &body }; + b.validate()?; + + Ok((query_params, body)) +} +/// AddressbookPost - POST /addressbook +#[tracing::instrument(skip_all)] +async fn addressbook_post( + method: Method, + host: Host, + cookies: CookieJar, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, + Json(body): Json, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::addressbook::Addressbook + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = + tokio::task::spawn_blocking(move || addressbook_post_validation(query_params, body)) + .await + .unwrap(); + + let Ok((query_params, body)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .addressbook_post(&method, &host, &cookies, &query_params, &body) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::addressbook::AddressbookPostResponse::Status201_ResponseForCREATED + (body) + => { + let mut response = response.status(201); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + apis::addressbook::AddressbookPostResponse::Status422_OneOrMoreValidationRulesFailed + (body) + => { + let mut response = response.status(422); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/problem+json")); + } + + let body_content = tokio::task::spawn_blocking(move || + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })).await.unwrap()?; + response.body(Body::from(body_content)) + }, + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl.as_ref().handle_error(&method, &host, &cookies, why).await; + }, + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn persons_get_validation( + query_params: models::PersonsGetQueryParams, +) -> std::result::Result<(models::PersonsGetQueryParams,), ValidationErrors> { + query_params.validate()?; + + Ok((query_params,)) +} +/// PersonsGet - GET /persons +#[tracing::instrument(skip_all)] +async fn persons_get( + method: Method, + host: Host, + cookies: CookieJar, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::persons::Persons + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || persons_get_validation(query_params)) + .await + .unwrap(); + + let Ok((query_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .persons_get(&method, &host, &cookies, &query_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::persons::PersonsGetResponse::Status200_ResponseForOK(body) => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[derive(validator::Validate)] +#[allow(dead_code)] +struct PersonsPostBodyValidator<'a> { + #[validate(nested)] + body: &'a models::CreatePerson, +} + +#[tracing::instrument(skip_all)] +fn persons_post_validation( + query_params: models::PersonsPostQueryParams, + body: models::CreatePerson, +) -> std::result::Result<(models::PersonsPostQueryParams, models::CreatePerson), ValidationErrors> { + query_params.validate()?; + let b = PersonsPostBodyValidator { body: &body }; + b.validate()?; + + Ok((query_params, body)) +} +/// PersonsPost - POST /persons +#[tracing::instrument(skip_all)] +async fn persons_post( + method: Method, + host: Host, + cookies: CookieJar, + headers: HeaderMap, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, + Json(body): Json, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::persons::Persons + apis::ApiAuthBasic + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + // Authentication + let claims_in_auth_header = api_impl + .as_ref() + .extract_claims_from_auth_header(apis::BasicAuthKind::Bearer, &headers, "authorization") + .await; + let claims = None.or(claims_in_auth_header); + let Some(claims) = claims else { + return response_with_status_code_only(StatusCode::UNAUTHORIZED); + }; + + #[allow(clippy::redundant_closure)] + let validation = + tokio::task::spawn_blocking(move || persons_post_validation(query_params, body)) + .await + .unwrap(); + + let Ok((query_params, body)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .persons_post(&method, &host, &cookies, &claims, &query_params, &body) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::persons::PersonsPostResponse::Status201_ResponseForCREATED(body) => { + let mut response = response.status(201); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn persons_uuid_delete_validation( + path_params: models::PersonsUuidDeletePathParams, +) -> std::result::Result<(models::PersonsUuidDeletePathParams,), ValidationErrors> { + path_params.validate()?; + + Ok((path_params,)) +} +/// PersonsUuidDelete - DELETE /persons/{uuid} +#[tracing::instrument(skip_all)] +async fn persons_uuid_delete( + method: Method, + host: Host, + cookies: CookieJar, + headers: HeaderMap, + Path(path_params): Path, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::persons::Persons + apis::ApiAuthBasic + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + // Authentication + let claims_in_auth_header = api_impl + .as_ref() + .extract_claims_from_auth_header(apis::BasicAuthKind::Bearer, &headers, "authorization") + .await; + let claims = None.or(claims_in_auth_header); + let Some(claims) = claims else { + return response_with_status_code_only(StatusCode::UNAUTHORIZED); + }; + + #[allow(clippy::redundant_closure)] + let validation = + tokio::task::spawn_blocking(move || persons_uuid_delete_validation(path_params)) + .await + .unwrap(); + + let Ok((path_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .persons_uuid_delete(&method, &host, &cookies, &claims, &path_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::persons::PersonsUuidDeleteResponse::Status200_ResponseForOK(body) => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + apis::persons::PersonsUuidDeleteResponse::Status409_OneOrMoreValidationRulesFailed( + body, + ) => { + let mut response = response.status(409); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/problem+json"), + ); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn persons_uuid_get_validation( + path_params: models::PersonsUuidGetPathParams, + query_params: models::PersonsUuidGetQueryParams, +) -> std::result::Result< + ( + models::PersonsUuidGetPathParams, + models::PersonsUuidGetQueryParams, + ), + ValidationErrors, +> { + path_params.validate()?; + query_params.validate()?; + + Ok((path_params, query_params)) +} +/// PersonsUuidGet - GET /persons/{uuid} +#[tracing::instrument(skip_all)] +async fn persons_uuid_get( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::persons::Persons + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = + tokio::task::spawn_blocking(move || persons_uuid_get_validation(path_params, query_params)) + .await + .unwrap(); + + let Ok((path_params, query_params)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .persons_uuid_get(&method, &host, &cookies, &path_params, &query_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::persons::PersonsUuidGetResponse::Status200_ResponseForOK(body) => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn persons_uuid_head_validation( + path_params: models::PersonsUuidHeadPathParams, +) -> std::result::Result<(models::PersonsUuidHeadPathParams,), ValidationErrors> { + path_params.validate()?; + + Ok((path_params,)) +} +/// PersonsUuidHead - HEAD /persons/{uuid} +#[tracing::instrument(skip_all)] +async fn persons_uuid_head( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::persons::Persons + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || persons_uuid_head_validation(path_params)) + .await + .unwrap(); + + let Ok((path_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .persons_uuid_head(&method, &host, &cookies, &path_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::persons::PersonsUuidHeadResponse::Status200_DefaultHEADResponse => { + let mut response = response.status(200); + response.body(Body::empty()) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[derive(validator::Validate)] +#[allow(dead_code)] +struct PersonsUuidPutBodyValidator<'a> { + #[validate(nested)] + body: &'a models::UpdatePerson, +} + +#[tracing::instrument(skip_all)] +fn persons_uuid_put_validation( + path_params: models::PersonsUuidPutPathParams, + body: models::UpdatePerson, +) -> std::result::Result<(models::PersonsUuidPutPathParams, models::UpdatePerson), ValidationErrors> +{ + path_params.validate()?; + let b = PersonsUuidPutBodyValidator { body: &body }; + b.validate()?; + + Ok((path_params, body)) +} +/// PersonsUuidPut - PUT /persons/{uuid} +#[tracing::instrument(skip_all)] +async fn persons_uuid_put( + method: Method, + host: Host, + cookies: CookieJar, + headers: HeaderMap, + Path(path_params): Path, + State(api_impl): State, + Json(body): Json, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::persons::Persons + apis::ApiAuthBasic + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + // Authentication + let claims_in_auth_header = api_impl + .as_ref() + .extract_claims_from_auth_header(apis::BasicAuthKind::Bearer, &headers, "authorization") + .await; + let claims = None.or(claims_in_auth_header); + let Some(claims) = claims else { + return response_with_status_code_only(StatusCode::UNAUTHORIZED); + }; + + #[allow(clippy::redundant_closure)] + let validation = + tokio::task::spawn_blocking(move || persons_uuid_put_validation(path_params, body)) + .await + .unwrap(); + + let Ok((path_params, body)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .persons_uuid_put(&method, &host, &cookies, &claims, &path_params, &body) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::persons::PersonsUuidPutResponse::Status200_ResponseForOK(body) => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn postal_codes_get_validation( + query_params: models::PostalCodesGetQueryParams, +) -> std::result::Result<(models::PostalCodesGetQueryParams,), ValidationErrors> { + query_params.validate()?; + + Ok((query_params,)) +} +/// PostalCodesGet - GET /postal_codes +#[tracing::instrument(skip_all)] +async fn postal_codes_get( + method: Method, + host: Host, + cookies: CookieJar, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::postal_codes::PostalCodes + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || postal_codes_get_validation(query_params)) + .await + .unwrap(); + + let Ok((query_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .postal_codes_get(&method, &host, &cookies, &query_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::postal_codes::PostalCodesGetResponse::Status200_ResponseForOK(body) => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[derive(validator::Validate)] +#[allow(dead_code)] +struct PostalCodesPostBodyValidator<'a> { + #[validate(nested)] + body: &'a models::CreatePostalCode, +} + +#[tracing::instrument(skip_all)] +fn postal_codes_post_validation( + query_params: models::PostalCodesPostQueryParams, + body: models::CreatePostalCode, +) -> std::result::Result< + (models::PostalCodesPostQueryParams, models::CreatePostalCode), + ValidationErrors, +> { + query_params.validate()?; + let b = PostalCodesPostBodyValidator { body: &body }; + b.validate()?; + + Ok((query_params, body)) +} +/// PostalCodesPost - POST /postal_codes +#[tracing::instrument(skip_all)] +async fn postal_codes_post( + method: Method, + host: Host, + cookies: CookieJar, + headers: HeaderMap, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, + Json(body): Json, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::postal_codes::PostalCodes + + apis::ApiAuthBasic + + Send + + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + // Authentication + let claims_in_auth_header = api_impl + .as_ref() + .extract_claims_from_auth_header(apis::BasicAuthKind::Bearer, &headers, "authorization") + .await; + let claims = None.or(claims_in_auth_header); + let Some(claims) = claims else { + return response_with_status_code_only(StatusCode::UNAUTHORIZED); + }; + + #[allow(clippy::redundant_closure)] + let validation = + tokio::task::spawn_blocking(move || postal_codes_post_validation(query_params, body)) + .await + .unwrap(); + + let Ok((query_params, body)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .postal_codes_post(&method, &host, &cookies, &claims, &query_params, &body) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::postal_codes::PostalCodesPostResponse::Status201_ResponseForCREATED(body) => { + let mut response = response.status(201); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn postal_codes_uuid_delete_validation( + path_params: models::PostalCodesUuidDeletePathParams, +) -> std::result::Result<(models::PostalCodesUuidDeletePathParams,), ValidationErrors> { + path_params.validate()?; + + Ok((path_params,)) +} +/// PostalCodesUuidDelete - DELETE /postal_codes/{uuid} +#[tracing::instrument(skip_all)] +async fn postal_codes_uuid_delete( + method: Method, + host: Host, + cookies: CookieJar, + headers: HeaderMap, + Path(path_params): Path, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::postal_codes::PostalCodes + + apis::ApiAuthBasic + + Send + + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + // Authentication + let claims_in_auth_header = api_impl + .as_ref() + .extract_claims_from_auth_header(apis::BasicAuthKind::Bearer, &headers, "authorization") + .await; + let claims = None.or(claims_in_auth_header); + let Some(claims) = claims else { + return response_with_status_code_only(StatusCode::UNAUTHORIZED); + }; + + #[allow(clippy::redundant_closure)] + let validation = + tokio::task::spawn_blocking(move || postal_codes_uuid_delete_validation(path_params)) + .await + .unwrap(); + + let Ok((path_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .postal_codes_uuid_delete(&method, &host, &cookies, &claims, &path_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::postal_codes::PostalCodesUuidDeleteResponse::Status200_ResponseForOK(body) => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn postal_codes_uuid_get_validation( + path_params: models::PostalCodesUuidGetPathParams, + query_params: models::PostalCodesUuidGetQueryParams, +) -> std::result::Result< + ( + models::PostalCodesUuidGetPathParams, + models::PostalCodesUuidGetQueryParams, + ), + ValidationErrors, +> { + path_params.validate()?; + query_params.validate()?; + + Ok((path_params, query_params)) +} +/// PostalCodesUuidGet - GET /postal_codes/{uuid} +#[tracing::instrument(skip_all)] +async fn postal_codes_uuid_get( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::postal_codes::PostalCodes + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + postal_codes_uuid_get_validation(path_params, query_params) + }) + .await + .unwrap(); + + let Ok((path_params, query_params)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .postal_codes_uuid_get(&method, &host, &cookies, &path_params, &query_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::postal_codes::PostalCodesUuidGetResponse::Status200_ResponseForOK(body) => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn postal_codes_uuid_head_validation( + path_params: models::PostalCodesUuidHeadPathParams, +) -> std::result::Result<(models::PostalCodesUuidHeadPathParams,), ValidationErrors> { + path_params.validate()?; + + Ok((path_params,)) +} +/// PostalCodesUuidHead - HEAD /postal_codes/{uuid} +#[tracing::instrument(skip_all)] +async fn postal_codes_uuid_head( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::postal_codes::PostalCodes + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = + tokio::task::spawn_blocking(move || postal_codes_uuid_head_validation(path_params)) + .await + .unwrap(); + + let Ok((path_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .postal_codes_uuid_head(&method, &host, &cookies, &path_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::postal_codes::PostalCodesUuidHeadResponse::Status200_DefaultHEADResponse => { + let mut response = response.status(200); + response.body(Body::empty()) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn postal_codes_uuid_name_delete_validation( + path_params: models::PostalCodesUuidNameDeletePathParams, +) -> std::result::Result<(models::PostalCodesUuidNameDeletePathParams,), ValidationErrors> { + path_params.validate()?; + + Ok((path_params,)) +} +/// PostalCodesUuidNameDelete - DELETE /postal_codes/{uuid}/name +#[tracing::instrument(skip_all)] +async fn postal_codes_uuid_name_delete( + method: Method, + host: Host, + cookies: CookieJar, + headers: HeaderMap, + Path(path_params): Path, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::postal_codes::PostalCodes + + apis::ApiAuthBasic + + Send + + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + // Authentication + let claims_in_auth_header = api_impl + .as_ref() + .extract_claims_from_auth_header(apis::BasicAuthKind::Bearer, &headers, "authorization") + .await; + let claims = None.or(claims_in_auth_header); + let Some(claims) = claims else { + return response_with_status_code_only(StatusCode::UNAUTHORIZED); + }; + + #[allow(clippy::redundant_closure)] + let validation = + tokio::task::spawn_blocking(move || postal_codes_uuid_name_delete_validation(path_params)) + .await + .unwrap(); + + let Ok((path_params,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .postal_codes_uuid_name_delete(&method, &host, &cookies, &claims, &path_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::postal_codes::PostalCodesUuidNameDeleteResponse::Status200_ResponseForOK( + body, + ) => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[tracing::instrument(skip_all)] +fn postal_codes_uuid_name_get_validation( + path_params: models::PostalCodesUuidNameGetPathParams, + query_params: models::PostalCodesUuidNameGetQueryParams, +) -> std::result::Result< + ( + models::PostalCodesUuidNameGetPathParams, + models::PostalCodesUuidNameGetQueryParams, + ), + ValidationErrors, +> { + path_params.validate()?; + query_params.validate()?; + + Ok((path_params, query_params)) +} +/// PostalCodesUuidNameGet - GET /postal_codes/{uuid}/name +#[tracing::instrument(skip_all)] +async fn postal_codes_uuid_name_get( + method: Method, + host: Host, + cookies: CookieJar, + Path(path_params): Path, + QueryExtra(query_params): QueryExtra, + State(api_impl): State, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::postal_codes::PostalCodes + Send + Sync, + E: std::fmt::Debug + Send + Sync + 'static, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || { + postal_codes_uuid_name_get_validation(path_params, query_params) + }) + .await + .unwrap(); + + let Ok((path_params, query_params)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl + .as_ref() + .postal_codes_uuid_name_get(&method, &host, &cookies, &path_params, &query_params) + .await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::postal_codes::PostalCodesUuidNameGetResponse::Status200_ResponseForOK(body) => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(why) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + return api_impl + .as_ref() + .handle_error(&method, &host, &cookies, why) + .await; + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +#[allow(dead_code)] +#[inline] +fn response_with_status_code_only(code: StatusCode) -> Result { + Response::builder() + .status(code) + .body(Body::empty()) + .map_err(|_| code) +} diff --git a/examples/addressbook/server-rs/api/src/types.rs b/examples/addressbook/server-rs/api/src/types.rs new file mode 100644 index 0000000..2c8847a --- /dev/null +++ b/examples/addressbook/server-rs/api/src/types.rs @@ -0,0 +1,790 @@ +use std::{mem, str::FromStr}; + +use base64::{Engine, engine::general_purpose}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[allow(dead_code)] +pub struct Object(pub serde_json::Value); + +impl validator::Validate for Object { + fn validate(&self) -> Result<(), validator::ValidationErrors> { + Ok(()) + } +} + +impl FromStr for Object { + type Err = String; + + fn from_str(s: &str) -> Result { + Ok(Self(serde_json::Value::String(s.to_owned()))) + } +} + +/// Serde helper function to create a default `Option>` while +/// deserializing +pub fn default_optional_nullable() -> Option> { + None +} + +/// Serde helper function to deserialize into an `Option>` +pub fn deserialize_optional_nullable<'de, D, T>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, + T: Deserialize<'de>, +{ + Option::::deserialize(deserializer).map(|val| match val { + Some(inner) => Some(Nullable::Present(inner)), + None => Some(Nullable::Null), + }) +} + +/// The Nullable type. Represents a value which may be specified as null on an API. +/// Note that this is distinct from a value that is optional and not present! +/// +/// Nullable implements many of the same methods as the Option type (map, unwrap, etc). +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub enum Nullable { + /// Null value + Null, + /// Value is present + Present(T), +} + +impl Nullable { + ///////////////////////////////////////////////////////////////////////// + // Querying the contained values + ///////////////////////////////////////////////////////////////////////// + + /// Returns `true` if the Nullable is a `Present` value. + /// + /// # Examples + /// + /// ``` + /// # use addressbook_api::types::Nullable; + /// + /// let x: Nullable = Nullable::Present(2); + /// assert_eq!(x.is_present(), true); + /// + /// let x: Nullable = Nullable::Null; + /// assert_eq!(x.is_present(), false); + /// ``` + #[inline] + pub fn is_present(&self) -> bool { + match *self { + Nullable::Present(_) => true, + Nullable::Null => false, + } + } + + /// Returns `true` if the Nullable is a `Null` value. + /// + /// # Examples + /// + /// ``` + /// # use addressbook_api::types::Nullable; + /// + /// let x: Nullable = Nullable::Present(2); + /// assert_eq!(x.is_null(), false); + /// + /// let x: Nullable = Nullable::Null; + /// assert_eq!(x.is_null(), true); + /// ``` + #[inline] + pub fn is_null(&self) -> bool { + !self.is_present() + } + + ///////////////////////////////////////////////////////////////////////// + // Adapter for working with references + ///////////////////////////////////////////////////////////////////////// + + /// Converts from `Nullable` to `Nullable<&T>`. + /// + /// # Examples + /// + /// Convert an `Nullable<`[`String`]`>` into a `Nullable<`[`usize`]`>`, preserving the original. + /// The [`map`] method takes the `self` argument by value, consuming the original, + /// so this technique uses `as_ref` to first take a `Nullable` to a reference + /// to the value inside the original. + /// + /// [`map`]: enum.Nullable.html#method.map + /// [`String`]: ../../std/string/struct.String.html + /// [`usize`]: ../../std/primitive.usize.html + /// + /// ``` + /// # use addressbook_api::types::Nullable; + /// + /// let num_as_str: Nullable = Nullable::Present("10".to_string()); + /// // First, cast `Nullable` to `Nullable<&String>` with `as_ref`, + /// // then consume *that* with `map`, leaving `num_as_str` on the stack. + /// let num_as_int: Nullable = num_as_str.as_ref().map(|n| n.len()); + /// println!("still can print num_as_str: {:?}", num_as_str); + /// ``` + #[inline] + pub fn as_ref(&self) -> Nullable<&T> { + match *self { + Nullable::Present(ref x) => Nullable::Present(x), + Nullable::Null => Nullable::Null, + } + } + + /// Converts from `Nullable` to `Nullable<&mut T>`. + /// + /// # Examples + /// + /// ``` + /// # use addressbook_api::types::Nullable; + /// + /// let mut x = Nullable::Present(2); + /// match x.as_mut() { + /// Nullable::Present(v) => *v = 42, + /// Nullable::Null => {}, + /// } + /// assert_eq!(x, Nullable::Present(42)); + /// ``` + #[inline] + pub fn as_mut(&mut self) -> Nullable<&mut T> { + match *self { + Nullable::Present(ref mut x) => Nullable::Present(x), + Nullable::Null => Nullable::Null, + } + } + + ///////////////////////////////////////////////////////////////////////// + // Getting to contained values + ///////////////////////////////////////////////////////////////////////// + + /// Unwraps a Nullable, yielding the content of a `Nullable::Present`. + /// + /// # Panics + /// + /// Panics if the value is a [`Nullable::Null`] with a custom panic message provided by + /// `msg`. + /// + /// [`Nullable::Null`]: #variant.Null + /// + /// # Examples + /// + /// ``` + /// # use addressbook_api::types::Nullable; + /// + /// let x = Nullable::Present("value"); + /// assert_eq!(x.expect("the world is ending"), "value"); + /// ``` + /// + /// ```should_panic + /// # use addressbook_api::types::Nullable; + /// + /// let x: Nullable<&str> = Nullable::Null; + /// x.expect("the world is ending"); // panics with `the world is ending` + /// ``` + #[inline] + pub fn expect(self, msg: &str) -> T { + match self { + Nullable::Present(val) => val, + Nullable::Null => expect_failed(msg), + } + } + + /// Moves the value `v` out of the `Nullable` if it is `Nullable::Present(v)`. + /// + /// In general, because this function may panic, its use is discouraged. + /// Instead, prefer to use pattern matching and handle the `Nullable::Null` + /// case explicitly. + /// + /// # Panics + /// + /// Panics if the self value equals [`Nullable::Null`]. + /// + /// [`Nullable::Null`]: #variant.Null + /// + /// # Examples + /// + /// ``` + /// # use addressbook_api::types::Nullable; + /// + /// let x = Nullable::Present("air"); + /// assert_eq!(x.unwrap(), "air"); + /// ``` + /// + /// ```should_panic + /// # use addressbook_api::types::Nullable; + /// + /// let x: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.unwrap(), "air"); // fails + /// ``` + #[inline] + pub fn unwrap(self) -> T { + match self { + Nullable::Present(val) => val, + Nullable::Null => panic!("called `Nullable::unwrap()` on a `Nullable::Null` value"), + } + } + + /// Returns the contained value or a default. + /// + /// # Examples + /// + /// ``` + /// # use addressbook_api::types::Nullable; + /// + /// assert_eq!(Nullable::Present("car").unwrap_or("bike"), "car"); + /// assert_eq!(Nullable::Null.unwrap_or("bike"), "bike"); + /// ``` + #[inline] + pub fn unwrap_or(self, def: T) -> T { + match self { + Nullable::Present(x) => x, + Nullable::Null => def, + } + } + + /// Returns the contained value or computes it from a closure. + /// + /// # Examples + /// + /// ``` + /// # use addressbook_api::types::Nullable; + /// + /// let k = 10; + /// assert_eq!(Nullable::Present(4).unwrap_or_else(|| 2 * k), 4); + /// assert_eq!(Nullable::Null.unwrap_or_else(|| 2 * k), 20); + /// ``` + #[inline] + pub fn unwrap_or_else T>(self, f: F) -> T { + match self { + Nullable::Present(x) => x, + Nullable::Null => f(), + } + } + + ///////////////////////////////////////////////////////////////////////// + // Transforming contained values + ///////////////////////////////////////////////////////////////////////// + + /// Maps a `Nullable` to `Nullable` by applying a function to a contained value. + /// + /// # Examples + /// + /// Convert a `Nullable<`[`String`]`>` into a `Nullable<`[`usize`]`>`, consuming the original: + /// + /// [`String`]: ../../std/string/struct.String.html + /// [`usize`]: ../../std/primitive.usize.html + /// + /// ``` + /// # use addressbook_api::types::Nullable; + /// + /// let maybe_some_string = Nullable::Present(String::from("Hello, World!")); + /// // `Nullable::map` takes self *by value*, consuming `maybe_some_string` + /// let maybe_some_len = maybe_some_string.map(|s| s.len()); + /// + /// assert_eq!(maybe_some_len, Nullable::Present(13)); + /// ``` + #[inline] + pub fn map U>(self, f: F) -> Nullable { + match self { + Nullable::Present(x) => Nullable::Present(f(x)), + Nullable::Null => Nullable::Null, + } + } + + /// Applies a function to the contained value (if any), + /// or returns a `default` (if not). + /// + /// # Examples + /// + /// ``` + /// # use addressbook_api::types::Nullable; + /// + /// let x = Nullable::Present("foo"); + /// assert_eq!(x.map_or(42, |v| v.len()), 3); + /// + /// let x: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.map_or(42, |v| v.len()), 42); + /// ``` + #[inline] + pub fn map_or U>(self, default: U, f: F) -> U { + match self { + Nullable::Present(t) => f(t), + Nullable::Null => default, + } + } + + /// Applies a function to the contained value (if any), + /// or computes a `default` (if not). + /// + /// # Examples + /// + /// ``` + /// # use addressbook_api::types::Nullable; + /// + /// let k = 21; + /// + /// let x = Nullable::Present("foo"); + /// assert_eq!(x.map_or_else(|| 2 * k, |v| v.len()), 3); + /// + /// let x: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.map_or_else(|| 2 * k, |v| v.len()), 42); + /// ``` + #[inline] + pub fn map_or_else U, F: FnOnce(T) -> U>(self, default: D, f: F) -> U { + match self { + Nullable::Present(t) => f(t), + Nullable::Null => default(), + } + } + + /// Transforms the `Nullable` into a [`Result`], mapping `Nullable::Present(v)` to + /// [`Ok(v)`] and `Nullable::Null` to [`Err(err)`][Err]. + /// + /// [`Result`]: ../../std/result/enum.Result.html + /// [`Ok(v)`]: ../../std/result/enum.Result.html#variant.Ok + /// [Err]: ../../std/result/enum.Result.html#variant.Err + /// + /// # Examples + /// + /// ``` + /// # use addressbook_api::types::Nullable; + /// + /// let x = Nullable::Present("foo"); + /// assert_eq!(x.ok_or(0), Ok("foo")); + /// + /// let x: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.ok_or(0), Err(0)); + /// ``` + #[inline] + pub fn ok_or(self, err: E) -> Result { + match self { + Nullable::Present(v) => Ok(v), + Nullable::Null => Err(err), + } + } + + /// Transforms the `Nullable` into a [`Result`], mapping `Nullable::Present(v)` to + /// [`Ok(v)`] and `Nullable::Null` to [`Err(err())`][Err]. + /// + /// [`Result`]: ../../std/result/enum.Result.html + /// [`Ok(v)`]: ../../std/result/enum.Result.html#variant.Ok + /// [Err]: ../../std/result/enum.Result.html#variant.Err + /// + /// # Examples + /// + /// ``` + /// # use addressbook_api::types::Nullable; + /// + /// let x = Nullable::Present("foo"); + /// assert_eq!(x.ok_or_else(|| 0), Ok("foo")); + /// + /// let x: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.ok_or_else(|| 0), Err(0)); + /// ``` + #[inline] + pub fn ok_or_else E>(self, err: F) -> Result { + match self { + Nullable::Present(v) => Ok(v), + Nullable::Null => Err(err()), + } + } + + ///////////////////////////////////////////////////////////////////////// + // Boolean operations on the values, eager and lazy + ///////////////////////////////////////////////////////////////////////// + + /// Returns `Nullable::Null` if the Nullable is `Nullable::Null`, otherwise returns `optb`. + /// + /// # Examples + /// + /// ``` + /// # use addressbook_api::types::Nullable; + /// + /// let x = Nullable::Present(2); + /// let y: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.and(y), Nullable::Null); + /// + /// let x: Nullable = Nullable::Null; + /// let y = Nullable::Present("foo"); + /// assert_eq!(x.and(y), Nullable::Null); + /// + /// let x = Nullable::Present(2); + /// let y = Nullable::Present("foo"); + /// assert_eq!(x.and(y), Nullable::Present("foo")); + /// + /// let x: Nullable = Nullable::Null; + /// let y: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.and(y), Nullable::Null); + /// ``` + #[inline] + pub fn and(self, optb: Nullable) -> Nullable { + match self { + Nullable::Present(_) => optb, + Nullable::Null => Nullable::Null, + } + } + + /// Returns `Nullable::Null` if the Nullable is `Nullable::Null`, otherwise calls `f` with the + /// wrapped value and returns the result. + /// + /// Some languages call this operation flatmap. + /// + /// # Examples + /// + /// ``` + /// # use addressbook_api::types::Nullable; + /// + /// fn sq(x: u32) -> Nullable { Nullable::Present(x * x) } + /// fn nope(_: u32) -> Nullable { Nullable::Null } + /// + /// assert_eq!(Nullable::Present(2).and_then(sq).and_then(sq), Nullable::Present(16)); + /// assert_eq!(Nullable::Present(2).and_then(sq).and_then(nope), Nullable::Null); + /// assert_eq!(Nullable::Present(2).and_then(nope).and_then(sq), Nullable::Null); + /// assert_eq!(Nullable::Null.and_then(sq).and_then(sq), Nullable::Null); + /// ``` + #[inline] + pub fn and_then Nullable>(self, f: F) -> Nullable { + match self { + Nullable::Present(x) => f(x), + Nullable::Null => Nullable::Null, + } + } + + /// Returns the Nullable if it contains a value, otherwise returns `optb`. + /// + /// # Examples + /// + /// ``` + /// # use addressbook_api::types::Nullable; + /// + /// let x = Nullable::Present(2); + /// let y = Nullable::Null; + /// assert_eq!(x.or(y), Nullable::Present(2)); + /// + /// let x = Nullable::Null; + /// let y = Nullable::Present(100); + /// assert_eq!(x.or(y), Nullable::Present(100)); + /// + /// let x = Nullable::Present(2); + /// let y = Nullable::Present(100); + /// assert_eq!(x.or(y), Nullable::Present(2)); + /// + /// let x: Nullable = Nullable::Null; + /// let y = Nullable::Null; + /// assert_eq!(x.or(y), Nullable::Null); + /// ``` + #[inline] + pub fn or(self, optb: Nullable) -> Nullable { + match self { + Nullable::Present(_) => self, + Nullable::Null => optb, + } + } + + /// Returns the Nullable if it contains a value, otherwise calls `f` and + /// returns the result. + /// + /// # Examples + /// + /// ``` + /// # use addressbook_api::types::Nullable; + /// + /// fn nobody() -> Nullable<&'static str> { Nullable::Null } + /// fn vikings() -> Nullable<&'static str> { Nullable::Present("vikings") } + /// + /// assert_eq!(Nullable::Present("barbarians").or_else(vikings), + /// Nullable::Present("barbarians")); + /// assert_eq!(Nullable::Null.or_else(vikings), Nullable::Present("vikings")); + /// assert_eq!(Nullable::Null.or_else(nobody), Nullable::Null); + /// ``` + #[inline] + pub fn or_else Nullable>(self, f: F) -> Nullable { + match self { + Nullable::Present(_) => self, + Nullable::Null => f(), + } + } + + ///////////////////////////////////////////////////////////////////////// + // Misc + ///////////////////////////////////////////////////////////////////////// + + /// Takes the value out of the Nullable, leaving a `Nullable::Null` in its place. + /// + /// # Examples + /// + /// ``` + /// # use addressbook_api::types::Nullable; + /// + /// let mut x = Nullable::Present(2); + /// x.take(); + /// assert_eq!(x, Nullable::Null); + /// + /// let mut x: Nullable = Nullable::Null; + /// x.take(); + /// assert_eq!(x, Nullable::Null); + /// ``` + #[inline] + pub fn take(&mut self) -> Nullable { + mem::replace(self, Nullable::Null) + } +} + +impl Nullable<&T> { + /// Maps an `Nullable<&T>` to an `Nullable` by cloning the contents of the + /// Nullable. + /// + /// # Examples + /// + /// ``` + /// # use addressbook_api::types::Nullable; + /// + /// let x = 12; + /// let opt_x = Nullable::Present(&x); + /// assert_eq!(opt_x, Nullable::Present(&12)); + /// let cloned = opt_x.cloned(); + /// assert_eq!(cloned, Nullable::Present(12)); + /// ``` + pub fn cloned(self) -> Nullable { + self.map(Clone::clone) + } +} + +impl Nullable { + /// Returns the contained value or a default + /// + /// Consumes the `self` argument then, if `Nullable::Present`, returns the contained + /// value, otherwise if `Nullable::Null`, returns the default value for that + /// type. + /// + /// # Examples + /// + /// ``` + /// # use addressbook_api::types::Nullable; + /// + /// let x = Nullable::Present(42); + /// assert_eq!(42, x.unwrap_or_default()); + /// + /// let y: Nullable = Nullable::Null; + /// assert_eq!(0, y.unwrap_or_default()); + /// ``` + #[inline] + pub fn unwrap_or_default(self) -> T { + match self { + Nullable::Present(x) => x, + Nullable::Null => Default::default(), + } + } +} + +impl Default for Nullable { + /// Returns None. + #[inline] + fn default() -> Nullable { + Nullable::Null + } +} + +impl From for Nullable { + fn from(val: T) -> Nullable { + Nullable::Present(val) + } +} + +impl Serialize for Nullable +where + T: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + Nullable::Present(ref inner) => serializer.serialize_some(&inner), + Nullable::Null => serializer.serialize_none(), + } + } +} + +impl<'de, T> Deserialize<'de> for Nullable +where + T: serde::de::DeserializeOwned, +{ + fn deserialize(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + // In order to deserialize a required, but nullable, value, we first have to check whether + // the value is present at all. To do this, we deserialize to a serde_json::Value, which + // fails if the value is missing, or gives serde_json::Value::Null if the value is present. + // If that succeeds as null, we can easily return a Null. + // If that succeeds as some value, we deserialize that value and return a Present. + // If that errors, we return the error. + let presence: Result<::serde_json::Value, _> = + serde::Deserialize::deserialize(deserializer); + match presence { + Ok(serde_json::Value::Null) => Ok(Nullable::Null), + Ok(some_value) => serde_json::from_value(some_value) + .map(Nullable::Present) + .map_err(serde::de::Error::custom), + Err(x) => Err(x), + } + } +} + +impl validator::Validate for Nullable +where + T: validator::Validate, +{ + fn validate(&self) -> Result<(), validator::ValidationErrors> { + match self { + Self::Present(x) => x.validate(), + Self::Null => Ok(()), + } + } +} + +impl<'a, T> validator::ValidateArgs<'a> for Nullable +where + T: validator::ValidateArgs<'a>, +{ + type Args = T::Args; + fn validate_with_args(&self, args: Self::Args) -> Result<(), validator::ValidationErrors> { + match self { + Self::Present(x) => x.validate_with_args(args), + Self::Null => Ok(()), + } + } +} + +impl validator::ValidateEmail for Nullable +where + T: validator::ValidateEmail, +{ + fn as_email_string(&'_ self) -> Option> { + match self { + Self::Present(x) => x.as_email_string(), + Self::Null => None, + } + } +} + +impl validator::ValidateUrl for Nullable +where + T: validator::ValidateUrl, +{ + fn as_url_string(&'_ self) -> Option> { + match self { + Self::Present(x) => x.as_url_string(), + Self::Null => None, + } + } +} + +impl validator::ValidateContains for Nullable +where + T: validator::ValidateContains, +{ + fn validate_contains(&self, needle: &str) -> bool { + match self { + Self::Present(x) => x.validate_contains(needle), + Self::Null => true, + } + } +} + +impl validator::ValidateRequired for Nullable +where + T: validator::ValidateRequired, +{ + fn is_some(&self) -> bool { + self.is_present() + } +} + +impl validator::ValidateRegex for Nullable +where + T: validator::ValidateRegex, +{ + fn validate_regex(&self, regex: impl validator::AsRegex) -> bool { + match self { + Self::Present(x) => x.validate_regex(regex), + Self::Null => true, + } + } +} + +impl validator::ValidateRange for Nullable +where + T: validator::ValidateRange, +{ + fn greater_than(&self, max: I) -> Option { + use validator::ValidateRange; + match self { + Self::Present(x) => x.greater_than(max), + Self::Null => None, + } + } + fn less_than(&self, min: I) -> Option { + use validator::ValidateRange; + match self { + Self::Present(x) => x.less_than(min), + Self::Null => None, + } + } +} + +impl validator::ValidateLength for Nullable +where + T: validator::ValidateLength, + I: PartialEq + PartialOrd, +{ + fn length(&self) -> Option { + use validator::ValidateLength; + match self { + Self::Present(x) => x.length(), + Self::Null => None, + } + } +} + +impl From> for Option { + fn from(value: Nullable) -> Option { + match value { + Nullable::Present(x) => Some(x), + Nullable::Null => None, + } + } +} + +#[inline(never)] +#[cold] +fn expect_failed(msg: &str) -> ! { + panic!("{}", msg) +} + +#[derive(Debug, Clone, PartialEq, PartialOrd)] +/// Base64-encoded byte array +pub struct ByteArray(pub Vec); + +impl Serialize for ByteArray { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&general_purpose::STANDARD.encode(&self.0)) + } +} + +impl<'de> Deserialize<'de> for ByteArray { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match general_purpose::STANDARD.decode(s) { + Ok(bin) => Ok(ByteArray(bin)), + _ => Err(serde::de::Error::custom("invalid base64")), + } + } +} diff --git a/examples/addressbook/server-rs/app/Cargo.toml b/examples/addressbook/server-rs/app/Cargo.toml new file mode 100644 index 0000000..1a49021 --- /dev/null +++ b/examples/addressbook/server-rs/app/Cargo.toml @@ -0,0 +1,40 @@ +# Example person and addressbook API +# +# The glue between the generated server and your storage. The server itself is the +# sibling crate, generated by openapi-generator from the OpenAPI document; this one +# implements its traits. +[package] +name = "addressbook_server" +version = "1.0.0" +edition = "2021" +# The generated engine uses LazyLock, and Option::is_none_or in the +# validation runtime. +rust-version = "1.82" +description = "Example person and addressbook API" + +[dependencies] +addressbook_api = { path = "../api" } +axum = "0.8" +axum-extra = { version = "0.10", features = ["cookie"] } +http = "1" +tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"] } +tower-http = { version = "0.6", features = ["trace"] } +async-trait = "0.1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +# Constant-time token comparison, rather than hand rolling one in security code. +subtle = "2" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +cel-interpreter = { version = "0.10", optional = true } + +[features] +default = ["cel"] +# Only the rules carrying a CEL expression need this. +cel = ["dep:cel-interpreter"] + + +[dev-dependencies] +http-body-util = "0.1" +tokio = { version = "1", features = ["macros", "rt"] } +tower = { version = "0.5", features = ["util"] } diff --git a/examples/addressbook/server-rs/app/src/auth.rs b/examples/addressbook/server-rs/app/src/auth.rs new file mode 100644 index 0000000..32de23e --- /dev/null +++ b/examples/addressbook/server-rs/app/src/auth.rs @@ -0,0 +1,69 @@ +//! Who is asking. +//! +//! The generated server decides *which* operations need credentials, from the +//! `security` block in the resource files, and refuses those with a 401 when this +//! returns None. What counts as an acceptable credential is a deployment question, +//! so it lives here. + +use subtle::Choice; +use subtle::ConstantTimeEq; + +use addressbook_api::apis::ApiAuthBasic; +use addressbook_api::apis::BasicAuthKind; + +use crate::Api; + +/// Whatever a handler should know about the caller. +/// +/// The generated traits carry this through to every secured operation, so add to +/// it whatever your rules need: a subject, a tenant, a set of roles. +#[derive(Clone, Debug)] +pub struct Claims { + /// The token the caller presented. + pub token: String, +} + +/// Whether any configured token matches, compared in constant time. +/// +/// Every candidate is checked rather than stopping at the first match, and the +/// comparison itself comes from `subtle` rather than being hand rolled, so a wrong +/// token cannot be narrowed down by timing the response. +fn accepted(known: &[String], presented: &str) -> bool { + known + .iter() + .fold(Choice::from(0u8), |found, candidate| { + found | candidate.as_bytes().ct_eq(presented.as_bytes()) + }) + .into() +} + +#[async_trait::async_trait] +impl ApiAuthBasic for Api { + type Claims = Claims; + + async fn extract_claims_from_auth_header( + &self, + kind: BasicAuthKind, + headers: &axum::http::header::HeaderMap, + key: &str, + ) -> Option { + if !matches!(kind, BasicAuthKind::Bearer) { + return None; + } + + // RFC 7235 auth-scheme names are case insensitive, so 'bearer' is as valid + // as 'Bearer'. + let header = headers.get(key)?.to_str().ok()?; + let (scheme, token) = header.split_once(' ')?; + if !scheme.eq_ignore_ascii_case("bearer") { + return None; + } + let token = token.trim(); + + // No configured tokens means every secured request is refused, which is the + // safe way round for a service that asked for authentication. + accepted(&self.tokens, token).then(|| Claims { + token: token.to_string(), + }) + } +} diff --git a/examples/addressbook/server-rs/app/src/backend.rs b/examples/addressbook/server-rs/app/src/backend.rs new file mode 100644 index 0000000..03cf5e9 --- /dev/null +++ b/examples/addressbook/server-rs/app/src/backend.rs @@ -0,0 +1,195 @@ +//! Where the data actually lives. +//! +//! The generated server knows every route, model and rule in your API. It cannot +//! know your storage, and firestone will not guess, so this trait is the seam. +//! [`InMemory`] is generated alongside it so the server answers the moment it is +//! built; swap it for your own and nothing else in the crate changes. + +use std::collections::BTreeMap; +use std::sync::Mutex; + +use async_trait::async_trait; +use serde_json::Value; + +use crate::error::Error; + +/// The storage operations the generated handlers need. +#[async_trait] +pub trait Backend: Send + Sync + 'static { + /// List every resource of this kind. + async fn list(&self, resource: &str) -> Result, Error>; + + /// Get one resource by its key, or None if there is no such thing. + async fn get(&self, resource: &str, key: &str) -> Result, Error>; + + /// Store a new resource, returning the key it was given and the resource. + async fn create(&self, resource: &str, body: Value) -> Result<(String, Value), Error>; + + /// Replace a resource outright. + async fn replace(&self, resource: &str, key: &str, body: Value) -> Result; + + /// Merge a partial body over a resource. + async fn patch(&self, resource: &str, key: &str, body: Value) -> Result; + + /// Remove a resource. + async fn delete(&self, resource: &str, key: &str) -> Result<(), Error>; + + /// Find the first resource whose dotted `path` holds `value`. + /// + /// This is what the generated validation rules look up through, so a rule + /// saying "an address may only name a person who exists" reaches your storage + /// here. `path` is a path into the resource, not a column, so translating it + /// is yours. A real implementation would index rather than scan. + async fn find(&self, resource: &str, path: &str, value: &Value) + -> Result, Error>; +} + +/// A backend that keeps everything in memory, so the server runs as generated. +#[derive(Debug, Default)] +pub struct InMemory { + // One lock over both, so minting a key and storing under it cannot interleave. + state: Mutex, +} + +#[derive(Debug, Default)] +struct Store { + resources: BTreeMap>, + next: BTreeMap, +} + +impl InMemory { + /// A new, empty store. + pub fn new() -> Self { + Self::default() + } + + /// Seed a resource, which is handy in tests and for a runnable demo. + pub fn seed(&self, resource: &str, key: &str, body: Value) { + let mut state = self.state.lock().expect("the store is not poisoned"); + state + .resources + .entry(resource.to_string()) + .or_default() + .insert(key.to_string(), body); + } +} + +/// Look up a dotted path such as `person.first_name`. +pub fn resolve_path<'a>(data: &'a Value, path: &str) -> Option<&'a Value> { + let mut current = data; + for part in path.split('.') { + current = current.get(part)?; + } + + Some(current) +} + +/// Merge a partial body over a resource, nested objects included. +fn merge(current: &Value, body: &Value) -> Value { + let (Some(current_object), Some(body_object)) = (current.as_object(), body.as_object()) else { + return body.clone(); + }; + + let mut merged = current_object.clone(); + for (key, value) in body_object { + match merged.get(key) { + Some(existing) if existing.is_object() && value.is_object() => { + merged.insert(key.clone(), merge(existing, value)); + } + _ => { + merged.insert(key.clone(), value.clone()); + } + } + } + + Value::Object(merged) +} + +#[async_trait] +impl Backend for InMemory { + async fn list(&self, resource: &str) -> Result, Error> { + let state = self.state.lock().expect("the store is not poisoned"); + + Ok(state + .resources + .get(resource) + .map(|items| items.values().cloned().collect()) + .unwrap_or_default()) + } + + async fn get(&self, resource: &str, key: &str) -> Result, Error> { + let state = self.state.lock().expect("the store is not poisoned"); + + Ok(state + .resources + .get(resource) + .and_then(|items| items.get(key)) + .cloned()) + } + + async fn create(&self, resource: &str, body: Value) -> Result<(String, Value), Error> { + // Minting and storing under one lock, so two creates cannot be handed the + // same key. A real backend would let the database mint it. + let mut state = self.state.lock().expect("the store is not poisoned"); + let number = state.next.entry(resource.to_string()).or_insert(0); + let key = format!("{resource}-{number}"); + *number += 1; + + state + .resources + .entry(resource.to_string()) + .or_default() + .insert(key.clone(), body.clone()); + + Ok((key, body)) + } + + async fn replace(&self, resource: &str, key: &str, body: Value) -> Result { + self.seed(resource, key, body.clone()); + + Ok(body) + } + + async fn patch(&self, resource: &str, key: &str, body: Value) -> Result { + let current = self + .get(resource, key) + .await? + .ok_or_else(|| Error::not_found(resource, key))?; + let merged = merge(¤t, &body); + self.seed(resource, key, merged.clone()); + + Ok(merged) + } + + async fn delete(&self, resource: &str, key: &str) -> Result<(), Error> { + let mut state = self.state.lock().expect("the store is not poisoned"); + let removed = state + .resources + .get_mut(resource) + .and_then(|items| items.remove(key)) + .is_some(); + + if removed { + return Ok(()); + } + + Err(Error::not_found(resource, key)) + } + + async fn find( + &self, + resource: &str, + path: &str, + value: &Value, + ) -> Result, Error> { + let state = self.state.lock().expect("the store is not poisoned"); + let Some(items) = state.resources.get(resource) else { + return Ok(None); + }; + + Ok(items + .values() + .find(|item| resolve_path(item, path) == Some(value)) + .cloned()) + } +} diff --git a/examples/addressbook/server-rs/app/src/error.rs b/examples/addressbook/server-rs/app/src/error.rs new file mode 100644 index 0000000..512b94b --- /dev/null +++ b/examples/addressbook/server-rs/app/src/error.rs @@ -0,0 +1,105 @@ +//! What a handler can fail with, and how that reaches the caller. +//! +//! The generated server's typed responses only cover what the OpenAPI document +//! declares, so anything else - a resource that is not there, a backend that could +//! not answer - travels as the error type and is rendered by the [`ErrorHandler`] +//! implementation in `lib.rs`. + +use axum::http::header; +use axum::http::HeaderValue; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use axum::response::Response; +use serde_json::json; +use serde_json::Value; + +/// Anything a handler can fail with. +#[derive(Debug)] +pub enum Error { + /// No such resource. + NotFound { resource: String, key: String }, + /// The body did not survive the rules declared in the schema. + Validation(Value), + /// An operation firestone did not know how to answer. + NotImplemented(String), + /// This service's fault. + Internal(String), +} + +impl Error { + /// A resource that is not there. + pub fn not_found(resource: &str, key: &str) -> Self { + Error::NotFound { + resource: resource.to_string(), + key: key.to_string(), + } + } + + /// The status this failure asks for. + pub fn status(&self) -> StatusCode { + match self { + Error::NotFound { .. } => StatusCode::NOT_FOUND, + Error::Validation(problem) => problem + .get("status") + .and_then(Value::as_u64) + .and_then(|status| StatusCode::from_u16(status as u16).ok()) + .unwrap_or(StatusCode::UNPROCESSABLE_ENTITY), + Error::NotImplemented(_) => StatusCode::NOT_IMPLEMENTED, + Error::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + /// This failure as an RFC 9457 problem detail. + pub fn to_problem(&self) -> Value { + if let Error::Validation(problem) = self { + return problem.clone(); + } + + let (title, detail) = match self { + Error::NotFound { resource, key } => { + ("Not found", format!("No {resource} with key '{key}'.")) + } + Error::NotImplemented(detail) => ("Not implemented", detail.clone()), + Error::Internal(detail) => ("Internal server error", detail.clone()), + Error::Validation(_) => unreachable!("handled above"), + }; + + json!({ + "type": "about:blank", + "title": title, + "status": self.status().as_u16(), + "detail": detail, + }) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_problem()) + } +} + +impl std::error::Error for Error {} + +impl IntoResponse for Error { + fn into_response(self) -> Response { + let status = self.status(); + let body = self.to_problem(); + let content_type = ( + header::CONTENT_TYPE, + HeaderValue::from_static("application/problem+json"), + ); + + (status, [content_type], axum::Json(body)).into_response() + } +} + +/// Deserialize what the backend returned into whatever the response declares. +pub fn from_json(value: Value) -> Result { + serde_json::from_value(value).map_err(|err| Error::Internal(err.to_string())) +} + +/// Serialize a typed request body so the backend and the rules can work with it. +pub fn to_json(value: &T) -> Result { + serde_json::to_value(value).map_err(|err| Error::Internal(err.to_string())) +} diff --git a/examples/addressbook/server-rs/app/src/handlers.rs b/examples/addressbook/server-rs/app/src/handlers.rs new file mode 100644 index 0000000..d56c181 --- /dev/null +++ b/examples/addressbook/server-rs/app/src/handlers.rs @@ -0,0 +1,1256 @@ +//! The implementation of every trait the generated server declares. +//! +//! Generated by firestone from the same OpenAPI document as the server, so the two +//! cannot disagree about an operation id, a model name or a response variant. +//! Do not edit: change the resource files and regenerate. +//! +//! Each handler does the same three things - pull the request apart, enforce the +//! rules the schema declares, then hand off to the [`Backend`](crate::backend::Backend). +//! Only the last of those is yours to write. + +use async_trait::async_trait; +use axum_extra::extract::CookieJar; +use axum_extra::extract::Host; +use http::Method; +use serde_json::Value; + +use addressbook_api::apis; +use addressbook_api::models; + +use crate::error::from_json; +use crate::error::to_json; +use crate::error::Error; +use crate::resolver::validate_request; +use crate::resolver::validate_subject; +use crate::Api; + +#[async_trait] +impl apis::addressbook::Addressbook for Api { + /// DELETE /addressbook/{address_key}/addrtype + async fn addressbook_address_key_addrtype_delete( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyAddrtypeDeletePathParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyAddrtypeDeleteResponse as Response; + + let key = &path_params.address_key; + let current = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let removed = current + .get("addrtype") + .cloned() + .ok_or_else(|| Error::not_found("addressbook addrtype", key))?; + + let mut updated = current.clone(); + updated + .as_object_mut() + .ok_or_else(|| Error::Internal("the stored resource is not an object".into()))? + .remove("addrtype"); + validate_subject(self, "delete", "addressbook", &updated, Some(¤t)).await?; + self.backend.replace("addressbook", key, updated).await?; + + Ok(Response::Status200_ResponseForOK(from_json(removed)?)) + } + + /// GET /addressbook/{address_key}/addrtype + async fn addressbook_address_key_addrtype_get( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyAddrtypeGetPathParams, + _query_params: &models::AddressbookAddressKeyAddrtypeGetQueryParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyAddrtypeGetResponse as Response; + + let key = &path_params.address_key; + let found = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let value = found + .get("addrtype") + .cloned() + .ok_or_else(|| Error::not_found("addressbook addrtype", key))?; + + Ok(Response::Status200_ResponseForOK(from_json(value)?)) + } + + /// HEAD /addressbook/{address_key}/addrtype + async fn addressbook_address_key_addrtype_head( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _path_params: &models::AddressbookAddressKeyAddrtypeHeadPathParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyAddrtypeHeadResponse as Response; + + Ok(Response::Status200_DefaultHEADResponse) + } + + /// PUT /addressbook/{address_key}/addrtype + async fn addressbook_address_key_addrtype_put( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyAddrtypePutPathParams, + body: &String, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyAddrtypePutResponse as Response; + + let key = &path_params.address_key; + let current = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let value = to_json(body)?; + + let mut updated = current.clone(); + updated + .as_object_mut() + .ok_or_else(|| Error::Internal("the stored resource is not an object".into()))? + .insert("addrtype".to_string(), value.clone()); + // The rules are written against the whole resource, so they are given the + // resource as it will be rather than the attribute on its own. + validate_subject(self, "put", "addressbook", &updated, Some(¤t)).await?; + self.backend.replace("addressbook", key, updated).await?; + + Ok(Response::Status200_ResponseForOK(from_json(value)?)) + } + + /// DELETE /addressbook/{address_key}/city + async fn addressbook_address_key_city_delete( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyCityDeletePathParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyCityDeleteResponse as Response; + + let key = &path_params.address_key; + let current = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let removed = current + .get("city") + .cloned() + .ok_or_else(|| Error::not_found("addressbook city", key))?; + + let mut updated = current.clone(); + updated + .as_object_mut() + .ok_or_else(|| Error::Internal("the stored resource is not an object".into()))? + .remove("city"); + validate_subject(self, "delete", "addressbook", &updated, Some(¤t)).await?; + self.backend.replace("addressbook", key, updated).await?; + + Ok(Response::Status200_ResponseForOK(from_json(removed)?)) + } + + /// GET /addressbook/{address_key}/city + async fn addressbook_address_key_city_get( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyCityGetPathParams, + _query_params: &models::AddressbookAddressKeyCityGetQueryParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyCityGetResponse as Response; + + let key = &path_params.address_key; + let found = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let value = found + .get("city") + .cloned() + .ok_or_else(|| Error::not_found("addressbook city", key))?; + + Ok(Response::Status200_ResponseForOK(from_json(value)?)) + } + + /// HEAD /addressbook/{address_key}/city + async fn addressbook_address_key_city_head( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _path_params: &models::AddressbookAddressKeyCityHeadPathParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyCityHeadResponse as Response; + + Ok(Response::Status200_DefaultHEADResponse) + } + + /// PUT /addressbook/{address_key}/city + async fn addressbook_address_key_city_put( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyCityPutPathParams, + body: &String, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyCityPutResponse as Response; + + let key = &path_params.address_key; + let current = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let value = to_json(body)?; + + let mut updated = current.clone(); + updated + .as_object_mut() + .ok_or_else(|| Error::Internal("the stored resource is not an object".into()))? + .insert("city".to_string(), value.clone()); + // The rules are written against the whole resource, so they are given the + // resource as it will be rather than the attribute on its own. + validate_subject(self, "put", "addressbook", &updated, Some(¤t)).await?; + self.backend.replace("addressbook", key, updated).await?; + + Ok(Response::Status200_ResponseForOK(from_json(value)?)) + } + + /// DELETE /addressbook/{address_key}/country + async fn addressbook_address_key_country_delete( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyCountryDeletePathParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyCountryDeleteResponse as Response; + + let key = &path_params.address_key; + let current = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let removed = current + .get("country") + .cloned() + .ok_or_else(|| Error::not_found("addressbook country", key))?; + + let mut updated = current.clone(); + updated + .as_object_mut() + .ok_or_else(|| Error::Internal("the stored resource is not an object".into()))? + .remove("country"); + validate_subject(self, "delete", "addressbook", &updated, Some(¤t)).await?; + self.backend.replace("addressbook", key, updated).await?; + + Ok(Response::Status200_ResponseForOK(from_json(removed)?)) + } + + /// GET /addressbook/{address_key}/country + async fn addressbook_address_key_country_get( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyCountryGetPathParams, + _query_params: &models::AddressbookAddressKeyCountryGetQueryParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyCountryGetResponse as Response; + + let key = &path_params.address_key; + let found = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let value = found + .get("country") + .cloned() + .ok_or_else(|| Error::not_found("addressbook country", key))?; + + Ok(Response::Status200_ResponseForOK(from_json(value)?)) + } + + /// HEAD /addressbook/{address_key}/country + async fn addressbook_address_key_country_head( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _path_params: &models::AddressbookAddressKeyCountryHeadPathParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyCountryHeadResponse as Response; + + Ok(Response::Status200_DefaultHEADResponse) + } + + /// PUT /addressbook/{address_key}/country + async fn addressbook_address_key_country_put( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyCountryPutPathParams, + body: &String, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyCountryPutResponse as Response; + + let key = &path_params.address_key; + let current = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let value = to_json(body)?; + + let mut updated = current.clone(); + updated + .as_object_mut() + .ok_or_else(|| Error::Internal("the stored resource is not an object".into()))? + .insert("country".to_string(), value.clone()); + // The rules are written against the whole resource, so they are given the + // resource as it will be rather than the attribute on its own. + validate_subject(self, "put", "addressbook", &updated, Some(¤t)).await?; + self.backend.replace("addressbook", key, updated).await?; + + Ok(Response::Status200_ResponseForOK(from_json(value)?)) + } + + /// DELETE /addressbook/{address_key} + async fn addressbook_address_key_delete( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyDeletePathParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyDeleteResponse as Response; + + let key = &path_params.address_key; + let current = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + // A delete has no body, so the rules see the resource that is about to go. + validate_request(self, "delete", "addressbook", None, Some(¤t)).await?; + self.backend.delete("addressbook", key).await?; + tracing::info!(resource = "addressbook", key, "deleted"); + + Ok(Response::Status200_ResponseForOK(from_json(current)?)) + } + + /// GET /addressbook/{address_key} + async fn addressbook_address_key_get( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyGetPathParams, + _query_params: &models::AddressbookAddressKeyGetQueryParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyGetResponse as Response; + + let key = &path_params.address_key; + let found = self.backend.get("addressbook", key).await?; + + Ok(Response::Status200_ResponseForOK(from_json( + found.ok_or_else(|| Error::not_found("addressbook", key))?, + )?)) + } + + /// HEAD /addressbook/{address_key} + async fn addressbook_address_key_head( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _path_params: &models::AddressbookAddressKeyHeadPathParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyHeadResponse as Response; + + Ok(Response::Status200_DefaultHEADResponse) + } + + /// DELETE /addressbook/{address_key}/is_valid + async fn addressbook_address_key_is_valid_delete( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyIsValidDeletePathParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyIsValidDeleteResponse as Response; + + let key = &path_params.address_key; + let current = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let removed = current + .get("is_valid") + .cloned() + .ok_or_else(|| Error::not_found("addressbook is_valid", key))?; + + let mut updated = current.clone(); + updated + .as_object_mut() + .ok_or_else(|| Error::Internal("the stored resource is not an object".into()))? + .remove("is_valid"); + validate_subject(self, "delete", "addressbook", &updated, Some(¤t)).await?; + self.backend.replace("addressbook", key, updated).await?; + + Ok(Response::Status200_ResponseForOK(from_json(removed)?)) + } + + /// GET /addressbook/{address_key}/is_valid + async fn addressbook_address_key_is_valid_get( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyIsValidGetPathParams, + _query_params: &models::AddressbookAddressKeyIsValidGetQueryParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyIsValidGetResponse as Response; + + let key = &path_params.address_key; + let found = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let value = found + .get("is_valid") + .cloned() + .ok_or_else(|| Error::not_found("addressbook is_valid", key))?; + + Ok(Response::Status200_ResponseForOK(from_json(value)?)) + } + + /// HEAD /addressbook/{address_key}/is_valid + async fn addressbook_address_key_is_valid_head( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _path_params: &models::AddressbookAddressKeyIsValidHeadPathParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyIsValidHeadResponse as Response; + + Ok(Response::Status200_DefaultHEADResponse) + } + + /// PUT /addressbook/{address_key}/is_valid + async fn addressbook_address_key_is_valid_put( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyIsValidPutPathParams, + body: &bool, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyIsValidPutResponse as Response; + + let key = &path_params.address_key; + let current = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let value = to_json(body)?; + + let mut updated = current.clone(); + updated + .as_object_mut() + .ok_or_else(|| Error::Internal("the stored resource is not an object".into()))? + .insert("is_valid".to_string(), value.clone()); + // The rules are written against the whole resource, so they are given the + // resource as it will be rather than the attribute on its own. + validate_subject(self, "put", "addressbook", &updated, Some(¤t)).await?; + self.backend.replace("addressbook", key, updated).await?; + + Ok(Response::Status200_ResponseForOK(from_json(value)?)) + } + + /// DELETE /addressbook/{address_key}/people + async fn addressbook_address_key_people_delete( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyPeopleDeletePathParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyPeopleDeleteResponse as Response; + + let key = &path_params.address_key; + let current = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let removed = current + .get("people") + .cloned() + .ok_or_else(|| Error::not_found("addressbook people", key))?; + + let mut updated = current.clone(); + updated + .as_object_mut() + .ok_or_else(|| Error::Internal("the stored resource is not an object".into()))? + .remove("people"); + validate_subject(self, "delete", "addressbook", &updated, Some(¤t)).await?; + self.backend.replace("addressbook", key, updated).await?; + + Ok(Response::Status200_ResponseForOK(from_json(removed)?)) + } + + /// GET /addressbook/{address_key}/people + async fn addressbook_address_key_people_get( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyPeopleGetPathParams, + _query_params: &models::AddressbookAddressKeyPeopleGetQueryParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyPeopleGetResponse as Response; + + let key = &path_params.address_key; + let found = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let value = found + .get("people") + .cloned() + .ok_or_else(|| Error::not_found("addressbook people", key))?; + + Ok(Response::Status200_ResponseForOK(from_json(value)?)) + } + + /// HEAD /addressbook/{address_key}/people + async fn addressbook_address_key_people_head( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _path_params: &models::AddressbookAddressKeyPeopleHeadPathParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyPeopleHeadResponse as Response; + + Ok(Response::Status200_DefaultHEADResponse) + } + + /// PUT /addressbook/{address_key}/people + async fn addressbook_address_key_people_put( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyPeoplePutPathParams, + body: &Vec, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyPeoplePutResponse as Response; + + let key = &path_params.address_key; + let current = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let value = to_json(body)?; + + let mut updated = current.clone(); + updated + .as_object_mut() + .ok_or_else(|| Error::Internal("the stored resource is not an object".into()))? + .insert("people".to_string(), value.clone()); + // The rules are written against the whole resource, so they are given the + // resource as it will be rather than the attribute on its own. + validate_subject(self, "put", "addressbook", &updated, Some(¤t)).await?; + self.backend.replace("addressbook", key, updated).await?; + + Ok(Response::Status200_ResponseForOK(from_json(value)?)) + } + + /// DELETE /addressbook/{address_key}/person + async fn addressbook_address_key_person_delete( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyPersonDeletePathParams, + _query_params: &models::AddressbookAddressKeyPersonDeleteQueryParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyPersonDeleteResponse as Response; + + let key = &path_params.address_key; + let current = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let removed = current + .get("person") + .cloned() + .ok_or_else(|| Error::not_found("addressbook person", key))?; + + let mut updated = current.clone(); + updated + .as_object_mut() + .ok_or_else(|| Error::Internal("the stored resource is not an object".into()))? + .remove("person"); + validate_subject(self, "delete", "addressbook", &updated, Some(¤t)).await?; + self.backend.replace("addressbook", key, updated).await?; + + Ok(Response::Status200_ResponseForOK(from_json(removed)?)) + } + + /// GET /addressbook/{address_key}/person + async fn addressbook_address_key_person_get( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyPersonGetPathParams, + _query_params: &models::AddressbookAddressKeyPersonGetQueryParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyPersonGetResponse as Response; + + let key = &path_params.address_key; + let found = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let value = found + .get("person") + .cloned() + .ok_or_else(|| Error::not_found("addressbook person", key))?; + + Ok(Response::Status200_ResponseForOK(from_json(value)?)) + } + + /// HEAD /addressbook/{address_key}/person + async fn addressbook_address_key_person_head( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _path_params: &models::AddressbookAddressKeyPersonHeadPathParams, + _query_params: &models::AddressbookAddressKeyPersonHeadQueryParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyPersonHeadResponse as Response; + + Ok(Response::Status200_DefaultHEADResponse) + } + + /// PATCH /addressbook/{address_key}/person + async fn addressbook_address_key_person_patch( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _path_params: &models::AddressbookAddressKeyPersonPatchPathParams, + _query_params: &models::AddressbookAddressKeyPersonPatchQueryParams, + ) -> Result { + // Firestone does not generate a body for this operation: the document + // declares no request body or no success response, or the method means + // something a replace would get wrong. Implement it by hand if you need it. + Err(Error::NotImplemented( + "PATCH /addressbook/{address_key}/person is not generated; implement it by hand".into(), + )) + } + + /// POST /addressbook/{address_key}/person + async fn addressbook_address_key_person_post( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _path_params: &models::AddressbookAddressKeyPersonPostPathParams, + _query_params: &models::AddressbookAddressKeyPersonPostQueryParams, + _body: &models::CreatePerson, + ) -> Result { + // Firestone does not generate a body for this operation: the document + // declares no request body or no success response, or the method means + // something a replace would get wrong. Implement it by hand if you need it. + Err(Error::NotImplemented( + "POST /addressbook/{address_key}/person is not generated; implement it by hand".into(), + )) + } + + /// PUT /addressbook/{address_key} + async fn addressbook_address_key_put( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyPutPathParams, + body: &models::UpdateAddressbook, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyPutResponse as Response; + + let key = &path_params.address_key; + let body = to_json(body)?; + // Rules comparing against the current state need it, whether or not the + // backend would have. + let current = self.backend.get("addressbook", key).await?; + validate_request(self, "put", "addressbook", Some(&body), current.as_ref()).await?; + let stored = self.backend.replace("addressbook", key, body).await?; + + Ok(Response::Status200_ResponseForOK(from_json(stored)?)) + } + + /// DELETE /addressbook/{address_key}/state + async fn addressbook_address_key_state_delete( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyStateDeletePathParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyStateDeleteResponse as Response; + + let key = &path_params.address_key; + let current = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let removed = current + .get("state") + .cloned() + .ok_or_else(|| Error::not_found("addressbook state", key))?; + + let mut updated = current.clone(); + updated + .as_object_mut() + .ok_or_else(|| Error::Internal("the stored resource is not an object".into()))? + .remove("state"); + validate_subject(self, "delete", "addressbook", &updated, Some(¤t)).await?; + self.backend.replace("addressbook", key, updated).await?; + + Ok(Response::Status200_ResponseForOK(from_json(removed)?)) + } + + /// GET /addressbook/{address_key}/state + async fn addressbook_address_key_state_get( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyStateGetPathParams, + _query_params: &models::AddressbookAddressKeyStateGetQueryParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyStateGetResponse as Response; + + let key = &path_params.address_key; + let found = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let value = found + .get("state") + .cloned() + .ok_or_else(|| Error::not_found("addressbook state", key))?; + + Ok(Response::Status200_ResponseForOK(from_json(value)?)) + } + + /// HEAD /addressbook/{address_key}/state + async fn addressbook_address_key_state_head( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _path_params: &models::AddressbookAddressKeyStateHeadPathParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyStateHeadResponse as Response; + + Ok(Response::Status200_DefaultHEADResponse) + } + + /// PUT /addressbook/{address_key}/state + async fn addressbook_address_key_state_put( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyStatePutPathParams, + body: &String, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyStatePutResponse as Response; + + let key = &path_params.address_key; + let current = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let value = to_json(body)?; + + let mut updated = current.clone(); + updated + .as_object_mut() + .ok_or_else(|| Error::Internal("the stored resource is not an object".into()))? + .insert("state".to_string(), value.clone()); + // The rules are written against the whole resource, so they are given the + // resource as it will be rather than the attribute on its own. + validate_subject(self, "put", "addressbook", &updated, Some(¤t)).await?; + self.backend.replace("addressbook", key, updated).await?; + + Ok(Response::Status200_ResponseForOK(from_json(value)?)) + } + + /// DELETE /addressbook/{address_key}/street + async fn addressbook_address_key_street_delete( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyStreetDeletePathParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyStreetDeleteResponse as Response; + + let key = &path_params.address_key; + let current = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let removed = current + .get("street") + .cloned() + .ok_or_else(|| Error::not_found("addressbook street", key))?; + + let mut updated = current.clone(); + updated + .as_object_mut() + .ok_or_else(|| Error::Internal("the stored resource is not an object".into()))? + .remove("street"); + validate_subject(self, "delete", "addressbook", &updated, Some(¤t)).await?; + self.backend.replace("addressbook", key, updated).await?; + + Ok(Response::Status200_ResponseForOK(from_json(removed)?)) + } + + /// GET /addressbook/{address_key}/street + async fn addressbook_address_key_street_get( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyStreetGetPathParams, + _query_params: &models::AddressbookAddressKeyStreetGetQueryParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyStreetGetResponse as Response; + + let key = &path_params.address_key; + let found = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let value = found + .get("street") + .cloned() + .ok_or_else(|| Error::not_found("addressbook street", key))?; + + Ok(Response::Status200_ResponseForOK(from_json(value)?)) + } + + /// HEAD /addressbook/{address_key}/street + async fn addressbook_address_key_street_head( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _path_params: &models::AddressbookAddressKeyStreetHeadPathParams, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyStreetHeadResponse as Response; + + Ok(Response::Status200_DefaultHEADResponse) + } + + /// PUT /addressbook/{address_key}/street + async fn addressbook_address_key_street_put( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::AddressbookAddressKeyStreetPutPathParams, + body: &String, + ) -> Result { + use apis::addressbook::AddressbookAddressKeyStreetPutResponse as Response; + + let key = &path_params.address_key; + let current = self + .backend + .get("addressbook", key) + .await? + .ok_or_else(|| Error::not_found("addressbook", key))?; + let value = to_json(body)?; + + let mut updated = current.clone(); + updated + .as_object_mut() + .ok_or_else(|| Error::Internal("the stored resource is not an object".into()))? + .insert("street".to_string(), value.clone()); + // The rules are written against the whole resource, so they are given the + // resource as it will be rather than the attribute on its own. + validate_subject(self, "put", "addressbook", &updated, Some(¤t)).await?; + self.backend.replace("addressbook", key, updated).await?; + + Ok(Response::Status200_ResponseForOK(from_json(value)?)) + } + + /// GET /addressbook + async fn addressbook_get( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _query_params: &models::AddressbookGetQueryParams, + ) -> Result { + use apis::addressbook::AddressbookGetResponse as Response; + + let items = self.backend.list("addressbook").await?; + + Ok(Response::Status200_ResponseForOK(from_json(Value::Array( + items, + ))?)) + } + + /// POST /addressbook + async fn addressbook_post( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _query_params: &models::AddressbookPostQueryParams, + body: &models::CreateAddressbook, + ) -> Result { + use apis::addressbook::AddressbookPostResponse as Response; + + let body = to_json(body)?; + validate_request(self, "post", "addressbook", Some(&body), None).await?; + let (key, mut created) = self.backend.create("addressbook", body).await?; + tracing::info!(resource = "addressbook", key, "created"); + + // The backend mints the key, so the caller is told what it was: without it + // there is no way to GET, PUT or DELETE what was just created. + if let Some(object) = created.as_object_mut() { + object.insert("address_key".to_string(), Value::String(key)); + } + + Ok(Response::Status201_ResponseForCREATED(from_json(created)?)) + } +} + +#[async_trait] +impl apis::persons::Persons for Api { + type Claims = crate::auth::Claims; + + /// GET /persons + async fn persons_get( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _query_params: &models::PersonsGetQueryParams, + ) -> Result { + use apis::persons::PersonsGetResponse as Response; + + let items = self.backend.list("persons").await?; + + Ok(Response::Status200_ResponseForOK(from_json(Value::Array( + items, + ))?)) + } + + /// POST /persons + async fn persons_post( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _claims: &Self::Claims, + _query_params: &models::PersonsPostQueryParams, + body: &models::CreatePerson, + ) -> Result { + use apis::persons::PersonsPostResponse as Response; + + let body = to_json(body)?; + validate_request(self, "post", "persons", Some(&body), None).await?; + let (key, mut created) = self.backend.create("persons", body).await?; + tracing::info!(resource = "persons", key, "created"); + + // The backend mints the key, so the caller is told what it was: without it + // there is no way to GET, PUT or DELETE what was just created. + if let Some(object) = created.as_object_mut() { + object.insert("uuid".to_string(), Value::String(key)); + } + + Ok(Response::Status201_ResponseForCREATED(from_json(created)?)) + } + + /// DELETE /persons/{uuid} + async fn persons_uuid_delete( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _claims: &Self::Claims, + path_params: &models::PersonsUuidDeletePathParams, + ) -> Result { + use apis::persons::PersonsUuidDeleteResponse as Response; + + let key = &path_params.uuid; + let current = self + .backend + .get("persons", key) + .await? + .ok_or_else(|| Error::not_found("persons", key))?; + // A delete has no body, so the rules see the resource that is about to go. + validate_request(self, "delete", "persons", None, Some(¤t)).await?; + self.backend.delete("persons", key).await?; + tracing::info!(resource = "persons", key, "deleted"); + + Ok(Response::Status200_ResponseForOK(from_json(current)?)) + } + + /// GET /persons/{uuid} + async fn persons_uuid_get( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::PersonsUuidGetPathParams, + _query_params: &models::PersonsUuidGetQueryParams, + ) -> Result { + use apis::persons::PersonsUuidGetResponse as Response; + + let key = &path_params.uuid; + let found = self.backend.get("persons", key).await?; + + Ok(Response::Status200_ResponseForOK(from_json( + found.ok_or_else(|| Error::not_found("persons", key))?, + )?)) + } + + /// HEAD /persons/{uuid} + async fn persons_uuid_head( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _path_params: &models::PersonsUuidHeadPathParams, + ) -> Result { + use apis::persons::PersonsUuidHeadResponse as Response; + + Ok(Response::Status200_DefaultHEADResponse) + } + + /// PUT /persons/{uuid} + async fn persons_uuid_put( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _claims: &Self::Claims, + path_params: &models::PersonsUuidPutPathParams, + body: &models::UpdatePerson, + ) -> Result { + use apis::persons::PersonsUuidPutResponse as Response; + + let key = &path_params.uuid; + let body = to_json(body)?; + // Rules comparing against the current state need it, whether or not the + // backend would have. + let current = self.backend.get("persons", key).await?; + validate_request(self, "put", "persons", Some(&body), current.as_ref()).await?; + let stored = self.backend.replace("persons", key, body).await?; + + Ok(Response::Status200_ResponseForOK(from_json(stored)?)) + } +} + +#[async_trait] +impl apis::postal_codes::PostalCodes for Api { + type Claims = crate::auth::Claims; + + /// GET /postal_codes + async fn postal_codes_get( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _query_params: &models::PostalCodesGetQueryParams, + ) -> Result { + use apis::postal_codes::PostalCodesGetResponse as Response; + + let items = self.backend.list("postal_codes").await?; + + Ok(Response::Status200_ResponseForOK(from_json(Value::Array( + items, + ))?)) + } + + /// POST /postal_codes + async fn postal_codes_post( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _claims: &Self::Claims, + _query_params: &models::PostalCodesPostQueryParams, + body: &models::CreatePostalCode, + ) -> Result { + use apis::postal_codes::PostalCodesPostResponse as Response; + + let body = to_json(body)?; + let (key, mut created) = self.backend.create("postal_codes", body).await?; + tracing::info!(resource = "postal_codes", key, "created"); + + // The backend mints the key, so the caller is told what it was: without it + // there is no way to GET, PUT or DELETE what was just created. + if let Some(object) = created.as_object_mut() { + object.insert("uuid".to_string(), Value::String(key)); + } + + Ok(Response::Status201_ResponseForCREATED(from_json(created)?)) + } + + /// DELETE /postal_codes/{uuid} + async fn postal_codes_uuid_delete( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _claims: &Self::Claims, + path_params: &models::PostalCodesUuidDeletePathParams, + ) -> Result { + use apis::postal_codes::PostalCodesUuidDeleteResponse as Response; + + let key = &path_params.uuid; + let current = self + .backend + .get("postal_codes", key) + .await? + .ok_or_else(|| Error::not_found("postal_codes", key))?; + self.backend.delete("postal_codes", key).await?; + tracing::info!(resource = "postal_codes", key, "deleted"); + + Ok(Response::Status200_ResponseForOK(from_json(current)?)) + } + + /// GET /postal_codes/{uuid} + async fn postal_codes_uuid_get( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::PostalCodesUuidGetPathParams, + _query_params: &models::PostalCodesUuidGetQueryParams, + ) -> Result { + use apis::postal_codes::PostalCodesUuidGetResponse as Response; + + let key = &path_params.uuid; + let found = self.backend.get("postal_codes", key).await?; + + Ok(Response::Status200_ResponseForOK(from_json( + found.ok_or_else(|| Error::not_found("postal_codes", key))?, + )?)) + } + + /// HEAD /postal_codes/{uuid} + async fn postal_codes_uuid_head( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _path_params: &models::PostalCodesUuidHeadPathParams, + ) -> Result { + use apis::postal_codes::PostalCodesUuidHeadResponse as Response; + + Ok(Response::Status200_DefaultHEADResponse) + } + + /// DELETE /postal_codes/{uuid}/name + async fn postal_codes_uuid_name_delete( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + _claims: &Self::Claims, + path_params: &models::PostalCodesUuidNameDeletePathParams, + ) -> Result { + use apis::postal_codes::PostalCodesUuidNameDeleteResponse as Response; + + let key = &path_params.uuid; + let current = self + .backend + .get("postal_codes", key) + .await? + .ok_or_else(|| Error::not_found("postal_codes", key))?; + let removed = current + .get("name") + .cloned() + .ok_or_else(|| Error::not_found("postal_codes name", key))?; + + let mut updated = current.clone(); + updated + .as_object_mut() + .ok_or_else(|| Error::Internal("the stored resource is not an object".into()))? + .remove("name"); + self.backend.replace("postal_codes", key, updated).await?; + + Ok(Response::Status200_ResponseForOK(from_json(removed)?)) + } + + /// GET /postal_codes/{uuid}/name + async fn postal_codes_uuid_name_get( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, + path_params: &models::PostalCodesUuidNameGetPathParams, + _query_params: &models::PostalCodesUuidNameGetQueryParams, + ) -> Result { + use apis::postal_codes::PostalCodesUuidNameGetResponse as Response; + + let key = &path_params.uuid; + let found = self + .backend + .get("postal_codes", key) + .await? + .ok_or_else(|| Error::not_found("postal_codes", key))?; + let value = found + .get("name") + .cloned() + .ok_or_else(|| Error::not_found("postal_codes name", key))?; + + Ok(Response::Status200_ResponseForOK(from_json(value)?)) + } +} diff --git a/examples/addressbook/server-rs/app/src/lib.rs b/examples/addressbook/server-rs/app/src/lib.rs new file mode 100644 index 0000000..42f98aa --- /dev/null +++ b/examples/addressbook/server-rs/app/src/lib.rs @@ -0,0 +1,91 @@ +//! Example person and addressbook API +//! +//! Example person and addressbook API +//! +//! The router, models, per-operation authentication and request validation are the +//! sibling `addressbook_api` crate, generated by openapi-generator from the OpenAPI +//! document firestone produced. This crate implements the traits that crate +//! declares, so the only thing left to write is a [`backend::Backend`]. + +pub mod auth; +pub mod backend; +pub mod error; +pub mod handlers; +pub mod resolver; +pub mod validation; + +use std::sync::Arc; + +pub use error::Error; + +/// The implementation the generated server is built around. +/// +/// Handed to `addressbook_api::server::new`, which routes every operation in the +/// OpenAPI document to one of its methods. +#[derive(Clone)] +pub struct Api { + /// Where the data lives. + pub backend: Arc, + /// The bearer tokens the secured operations accept. + pub tokens: Vec, +} + +impl Api { + /// Build the implementation around a backend, reading API_TOKENS for the + /// credentials the secured operations accept. + pub fn new(backend: Arc) -> Self { + let tokens = std::env::var("API_TOKENS").unwrap_or_default(); + + Api::with_tokens( + backend, + tokens + .split(',') + .map(str::trim) + .filter(|token| !token.is_empty()) + .map(str::to_string), + ) + } + + /// The same, with the tokens given rather than read from the environment, so a + /// test or an embedding binary can supply its own. + pub fn with_tokens( + backend: Arc, + tokens: impl IntoIterator, + ) -> Self { + Api { + backend, + tokens: tokens.into_iter().collect(), + } + } +} + +impl AsRef for Api { + fn as_ref(&self) -> &Api { + self + } +} + +/// Render anything the typed responses cannot as an RFC 9457 problem document. +/// +/// The generated server calls this for an error a handler returned, which is how a +/// 404, or a rule the OpenAPI document did not declare a response for, still +/// reaches the caller with a sensible status. +#[async_trait::async_trait] +impl addressbook_api::apis::ErrorHandler for Api { + async fn handle_error( + &self, + _method: &::http::Method, + _host: &axum_extra::extract::Host, + _cookies: &axum_extra::extract::CookieJar, + error: Error, + ) -> Result { + tracing::info!("{error}"); + + Ok(axum::response::IntoResponse::into_response(error)) + } +} + +/// Build the router for every operation in the OpenAPI document. +pub fn router(api: Api) -> axum::Router { + addressbook_api::server::new(api) +} diff --git a/examples/addressbook/server-rs/app/src/main.rs b/examples/addressbook/server-rs/app/src/main.rs new file mode 100644 index 0000000..a0367b5 --- /dev/null +++ b/examples/addressbook/server-rs/app/src/main.rs @@ -0,0 +1,65 @@ +//! Example person and addressbook API +//! +//! The thin part: choose a backend, build the router, serve it. Anything you want +//! to add - your own routes, your own middleware - goes on the router here, and +//! `addressbook_server::router` hands back an ordinary `axum::Router` so all of axum is +//! available. See . + +use std::net::SocketAddr; +use std::sync::Arc; + +use addressbook_server::backend::InMemory; +use addressbook_server::router; +use addressbook_server::Api; +use tokio::net::TcpListener; +use tokio::signal; +use tower_http::trace::TraceLayer; + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "info,tower_http=debug".into()), + ) + .init(); + + let api = Api::new(Arc::new(InMemory::new())); + let app = router(api).layer(TraceLayer::new_for_http()); + + let bind: SocketAddr = std::env::var("BIND") + .unwrap_or_else(|_| "127.0.0.1:8080".to_string()) + .parse()?; + let listener = TcpListener::bind(bind).await?; + tracing::info!("Example person and addressbook API v1.0 listening on http://{bind}"); + + axum::serve(listener, app) + .with_graceful_shutdown(shutdown()) + .await?; + + Ok(()) +} + +/// Stop serving on Ctrl-C or SIGTERM, letting in-flight requests finish. +async fn shutdown() { + let interrupt = async { + signal::ctrl_c() + .await + .expect("the interrupt handler installs"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("the terminate handler installs") + .recv() + .await; + }; + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + () = interrupt => tracing::info!("interrupted, shutting down"), + () = terminate => tracing::info!("terminated, shutting down"), + } +} diff --git a/examples/addressbook/server-rs/app/src/resolver.rs b/examples/addressbook/server-rs/app/src/resolver.rs new file mode 100644 index 0000000..8b6e41b --- /dev/null +++ b/examples/addressbook/server-rs/app/src/resolver.rs @@ -0,0 +1,109 @@ +//! Connecting the generated validation rules to the backend. +//! +//! The rules say *what* has to exist; the [`Backend`](crate::backend::Backend) is +//! the only thing that knows how to look it up. This is the adapter, and it is +//! generated because both halves are. + +use async_trait::async_trait; +use serde_json::Value; + +use crate::error::Error; +use crate::validation::validate; +use crate::validation::Error as ValidationFailure; +use crate::validation::RefRequest; +use crate::validation::Request as ValidationRequest; +use crate::validation::Resolved; +use crate::validation::Resolver; +use crate::validation::ResolverError; +use crate::Api; + +/// Answers a rule's lookups out of the backend. +pub struct BackendResolver<'a> { + api: &'a Api, +} + +#[async_trait] +impl Resolver for BackendResolver<'_> { + async fn resolve( + &mut self, + requests: &[RefRequest], + _ctx: &Value, + ) -> Result { + let mut resolved = Resolved::new(); + for request in requests { + // 'key' is a path into the resource being looked up, not a column, so it + // goes to the backend as one. + if let Some(found) = self + .api + .backend + .find(&request.kind, &request.key, &request.value) + .await? + { + resolved.insert(request.id(), found); + } + } + + Ok(resolved) + } +} + +/// Run every rule that applies to this request. +pub async fn validate_request( + api: &Api, + op: &str, + resource: &str, + body: Option<&Value>, + old: Option<&Value>, +) -> Result<(), Error> { + let mut request = ValidationRequest::new(op, resource); + if let Some(body) = body { + request = request.body(body); + } + if let Some(old) = old { + request = request.old(old); + } + + run(api, request, op, resource).await +} + +/// Run the rules against a resource you have already worked out. +/// +/// An attribute endpoint changes one field, and the rules are written against the +/// whole resource, so they are handed the resource as it will be rather than being +/// asked to merge a fragment they would not understand. +pub async fn validate_subject( + api: &Api, + op: &str, + resource: &str, + subject: &Value, + old: Option<&Value>, +) -> Result<(), Error> { + let mut request = ValidationRequest::new(op, resource).subject(subject); + if let Some(old) = old { + request = request.old(old); + } + + run(api, request, op, resource).await +} + +async fn run( + api: &Api, + request: ValidationRequest<'_>, + op: &str, + resource: &str, +) -> Result<(), Error> { + let mut resolver = BackendResolver { api }; + match validate(request, Some(&mut resolver)).await { + Ok(()) => Ok(()), + Err(ValidationFailure::Validation(failed)) => { + tracing::info!( + resource, + op, + "rejected by {} rule(s)", + failed.violations.len() + ); + Err(Error::Validation(failed.to_problem())) + } + Err(other) => Err(Error::Internal(other.to_string())), + } +} diff --git a/examples/addressbook/server-rs/app/src/validation/mod.rs b/examples/addressbook/server-rs/app/src/validation/mod.rs new file mode 100644 index 0000000..b00f513 --- /dev/null +++ b/examples/addressbook/server-rs/app/src/validation/mod.rs @@ -0,0 +1,76 @@ +//! Declarative validations for this API. +//! +//! Generated by firestone from the `references` and `validations` blocks in the +//! resource schemas. Rules are data, in `rules.rs`; the engine is in `runtime.rs`; +//! the interface you implement is in `ports.rs`. +//! +//! Nothing here depends on firestone at runtime. Add to your `Cargo.toml`: +//! +//! ```toml +//! [package] +//! # The engine uses LazyLock and Option::is_none_or. +//! rust-version = "1.82" +//! +//! [dependencies] +//! async-trait = "0.1" +//! serde = { version = "1", features = ["derive"] } +//! serde_json = "1" +//! cel-interpreter = { version = "0.10", optional = true } +//! +//! [features] +//! default = ["cel"] +//! cel = ["dep:cel-interpreter"] +//! +//! [dev-dependencies] +//! tokio = { version = "1", features = ["macros", "rt"] } +//! ``` +//! +//! Typical use, from a server handler: +//! +//! ```ignore +//! use myapi::validation::{validate, Error, Request}; +//! +//! match validate(Request::new("post", "addressbook").body(&body), Some(&mut resolver)).await { +//! Ok(()) => {} +//! Err(Error::Validation(failed)) => { +//! return (StatusCode::from_u16(failed.status())?, Json(failed.to_problem())) +//! .into_response(); +//! } +//! Err(other) => return internal_error(other), +//! } +//! ``` +//! +//! Serve the problem document as `application/problem+json`: that is what the +//! generated OpenAPI document declares, and RFC 9457 puts its members at the top +//! level of the body rather than under a wrapper key. + +pub mod ports; +pub mod rules; +pub mod runtime; + +#[cfg(test)] +mod tests; + +pub use ports::Error; +pub use ports::RefRequest; +pub use ports::Resolved; +pub use ports::Resolver; +pub use ports::ResolverError; +pub use ports::ValidationError; +pub use ports::Violation; +pub use rules::Rule; +pub use rules::RULES; +pub use runtime::Request; + +/// Run every generated rule that applies to this request. +/// +/// # Errors +/// +/// [`Error::Validation`] when rules rejected the request, [`Error::Rule`] when a +/// rule could not be evaluated, [`Error::Resolver`] when a lookup failed. +pub async fn validate( + request: Request<'_>, + resolver: Option<&mut dyn Resolver>, +) -> Result<(), Error> { + runtime::validate_with_rules(&RULES, request, resolver).await +} diff --git a/examples/addressbook/server-rs/app/src/validation/ports.rs b/examples/addressbook/server-rs/app/src/validation/ports.rs new file mode 100644 index 0000000..ef97253 --- /dev/null +++ b/examples/addressbook/server-rs/app/src/validation/ports.rs @@ -0,0 +1,195 @@ +//! The interfaces of the validation package. +//! +//! This file is generated by firestone but never changes: it is the contract +//! between the generated rules and your application. The only thing you have to +//! write is a [`Resolver`]. + +use std::collections::HashMap; +use std::error::Error as StdError; +use std::fmt; + +use async_trait::async_trait; +use serde_json::json; +use serde_json::Value; + +/// What a resolver hands back: the resource found, keyed by [`RefRequest::id`]. +pub type Resolved = HashMap; + +/// Whatever went wrong while talking to your storage. +pub type ResolverError = Box; + +/// A single lookup a rule needs before it can be evaluated. +#[derive(Clone, Debug, PartialEq)] +pub struct RefRequest { + /// The resource kind to look up, e.g. `persons`. + pub kind: String, + /// The path into that resource to match on, e.g. `first_name`. + pub key: String, + /// The value to match, taken from the request body. + pub value: Value, +} + +impl RefRequest { + /// A stable identifier for this lookup, used to key the resolved results. + pub fn id(&self) -> String { + format!("{}/{}/{}", self.kind, self.key, self.value) + } +} + +/// Fetches the resources that validation rules depend on. +/// +/// Implement this against whatever you already have: a database, an ORM, a +/// cache, another service. Firestone works out *what* needs fetching; this is +/// the only place that knows *how*. +/// +/// `resolve` is handed every lookup needed by a single request at once, already +/// de-duplicated, so a request that touches ten rules still costs one round trip. +/// +/// `ctx` is the request context passed to `validate`, for whatever scoping a +/// lookup needs: the tenant, the caller's authorisation scope, a trace id. Rules +/// can read it too, so it is plain JSON; a database handle or transaction belongs +/// on the implementing struct rather than in there. +/// +/// `resolve` takes `&mut self` deliberately. Validation has to read through the +/// same transaction as the mutation it guards, or the check races the write, and a +/// transaction is normally held mutably. Building the resolver per request and +/// handing it to `validate` mutably is therefore the straightforward path; an +/// implementation that needs no mutation simply ignores it. +#[async_trait] +pub trait Resolver: Send { + /// Resolve the given lookups. + /// + /// Leave an entry out, or map it to `Value::Null`, for anything that does not + /// exist. + async fn resolve( + &mut self, + requests: &[RefRequest], + ctx: &Value, + ) -> Result; +} + +/// A single rule that failed. +#[derive(Clone, Debug, PartialEq)] +pub struct Violation { + /// The name of the rule. + pub rule: String, + /// The resource kind that was validated. + pub resource: String, + /// The field the rule applies to, if any. + pub field: Option, + /// Why the rule failed. + pub message: String, + /// The status this violation asks for. + pub status: u16, +} + +impl Violation { + /// This violation in the shape used by the problem document. + pub fn to_json(&self) -> Value { + let mut violation = json!({ + "rule": self.rule, + "resource": self.resource, + "message": self.message, + }); + if let Some(field) = &self.field { + violation["field"] = json!(field); + } + + violation + } +} + +/// One or more validation rules rejected the request. +#[derive(Clone, Debug)] +pub struct ValidationError { + /// Every rule that failed for this request, not just the first. + pub violations: Vec, +} + +impl ValidationError { + /// The status to respond with, the highest of any violation's. + pub fn status(&self) -> u16 { + self.violations + .iter() + .map(|violation| violation.status) + .max() + .unwrap_or(422) + } + + /// An RFC 9457 problem detail describing every violation. + /// + /// Serve it as `application/problem+json`, which is what the generated + /// OpenAPI document declares. + pub fn to_problem(&self) -> Value { + json!({ + "type": "about:blank", + "title": "Validation failed", + "status": self.status(), + "detail": self.violations.first().map(|v| v.message.clone()).unwrap_or_default(), + "violations": self.violations.iter().map(Violation::to_json).collect::>(), + }) + } +} + +impl fmt::Display for ValidationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let messages: Vec<&str> = self + .violations + .iter() + .map(|violation| violation.message.as_str()) + .collect(); + write!(f, "{}", messages.join("; ")) + } +} + +impl StdError for ValidationError {} + +/// Everything `validate` can go wrong with. +/// +/// The split matters: [`Error::Validation`] is a request to refuse, and the other +/// two are failures on your side that should not be reported as a rejected body. +#[derive(Debug)] +pub enum Error { + /// Rules rejected the request. Answer with [`ValidationError::to_problem`]. + Validation(ValidationError), + /// A rule could not be evaluated at all, which is a bug in the rule. Answer 500. + Rule(String), + /// The resolver could not answer. Answer 500, or 503 if it was transient. + Resolver(ResolverError), +} + +impl Error { + /// The status this failure asks for. + pub fn status(&self) -> u16 { + match self { + Error::Validation(error) => error.status(), + Error::Rule(_) | Error::Resolver(_) => 500, + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Validation(error) => write!(f, "{error}"), + Error::Rule(message) => write!(f, "rule could not be evaluated: {message}"), + Error::Resolver(error) => write!(f, "resolver failed: {error}"), + } + } +} + +impl StdError for Error { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + match self { + Error::Validation(error) => Some(error), + Error::Resolver(error) => Some(error.as_ref()), + Error::Rule(_) => None, + } + } +} + +impl From for Error { + fn from(error: ValidationError) -> Self { + Error::Validation(error) + } +} diff --git a/examples/addressbook/server-rs/app/src/validation/rules.rs b/examples/addressbook/server-rs/app/src/validation/rules.rs new file mode 100644 index 0000000..28cd3e3 --- /dev/null +++ b/examples/addressbook/server-rs/app/src/validation/rules.rs @@ -0,0 +1,86 @@ +//! The validation rules for this API. +//! +//! Generated by firestone. Do not edit: change the `references` and +//! `validations` blocks in your resource schemas and regenerate. +//! +//! Some of these rules carry a CEL expression, so this crate needs the `cel` +//! feature, which is on by default. + +use std::collections::HashMap; +use std::sync::LazyLock; + +use serde::Deserialize; +use serde_json::Value; + +/// A single lookup a rule declares. +#[derive(Clone, Debug, Deserialize)] +pub struct Ref { + /// The name the expression refers to this lookup by. + pub name: String, + /// The resource kind to look up. + pub kind: String, + /// The path into that resource to match on. + pub key: String, + /// The path in the request body holding the value to look up. + pub value: String, + /// Whether the rule still runs when this lookup finds nothing. + pub optional: bool, +} + +/// What to answer with when a rule fails. +#[derive(Clone, Debug, Deserialize)] +pub struct RuleError { + /// The HTTP status code. + pub status: u16, + /// The message, which may carry `{self.city}` style placeholders. + pub message: String, +} + +/// A case the rule must accept or reject, used by the generated tests. +#[derive(Clone, Debug, Deserialize)] +pub struct Example { + /// The request body. + #[serde(rename = "self")] + pub subject: Value, + /// The resource as it currently stands. + pub old: Option, + /// The resources the resolver would find, keyed by ref name. + pub refs: Value, + /// Anything else the rule reads. + pub ctx: Value, + /// Whether the rule should accept or reject this case. + pub expect: String, +} + +/// One validation rule. +#[derive(Clone, Debug, Deserialize)] +pub struct Rule { + /// Unique within the resource; violations are reported under it. + pub name: String, + /// What this rule enforces, and why. + pub description: Option, + /// The resource kind this rule belongs to. + pub resource: String, + /// `exists`, `immutable` or `expr`. + pub kind: String, + /// The field this rule is about, for the reported violation. + pub field: Option, + /// The HTTP methods this rule runs on. + pub methods: Vec, + /// The lookups this rule needs. + pub refs: Vec, + /// The CEL expression, for a rule of kind `expr`. + pub expr: Option, + /// What to answer with when this rule fails. + pub error: RuleError, + /// Cases this rule must accept or reject. + #[serde(default)] + pub examples: Vec, +} + +/// The rules as generated, before they are parsed. +const RULES_JSON: &str = "{\"addressbook\": [{\"name\": \"person_must_exist\", \"description\": \"The referenced persons must already exist.\", \"resource\": \"addressbook\", \"kind\": \"exists\", \"field\": \"person.first_name\", \"methods\": [\"post\", \"put\", \"patch\"], \"refs\": [{\"name\": \"person\", \"kind\": \"persons\", \"key\": \"first_name\", \"value\": \"person.first_name\", \"optional\": false}], \"expr\": null, \"error\": {\"status\": 422, \"message\": \"No persons found with first_name '{self.person.first_name}'.\"}, \"examples\": []}, {\"name\": \"only_admins_may_invalidate\", \"description\": \"Marking an address invalid is an administrative action.\", \"resource\": \"addressbook\", \"kind\": \"expr\", \"field\": null, \"methods\": [\"put\", \"patch\"], \"refs\": [], \"expr\": \"!has(old.is_valid) || !has(self.is_valid) || self.is_valid == old.is_valid || (has(ctx.roles) && \\\"admin\\\" in ctx.roles)\", \"error\": {\"status\": 403, \"message\": \"Only an admin may change is_valid on an address.\"}, \"examples\": [{\"self\": {\"is_valid\": false}, \"old\": {\"is_valid\": true}, \"refs\": {}, \"ctx\": {\"roles\": [\"admin\"]}, \"expect\": \"pass\"}, {\"self\": {\"is_valid\": false}, \"old\": {\"is_valid\": true}, \"refs\": {}, \"ctx\": {\"roles\": [\"user\"]}, \"expect\": \"fail\"}, {\"self\": {\"is_valid\": true}, \"old\": {\"is_valid\": true}, \"refs\": {}, \"ctx\": {\"roles\": [\"user\"]}, \"expect\": \"pass\"}, {\"self\": {\"city\": \"Ottawa\"}, \"old\": {\"city\": \"Toronto\"}, \"refs\": {}, \"ctx\": {\"roles\": [\"user\"]}, \"expect\": \"pass\"}, {\"self\": {\"city\": \"Ottawa\"}, \"old\": {\"city\": \"Toronto\", \"is_valid\": true}, \"refs\": {}, \"ctx\": {\"roles\": [\"user\"]}, \"expect\": \"pass\"}]}], \"persons\": [{\"name\": \"person_is_not_in_use\", \"description\": \"A person cannot be deleted while an address still points at them.\", \"resource\": \"persons\", \"kind\": \"expr\", \"field\": null, \"methods\": [\"delete\"], \"refs\": [{\"name\": \"address\", \"kind\": \"addressbook\", \"key\": \"person.first_name\", \"value\": \"first_name\", \"optional\": true}], \"expr\": \"!has(refs.address)\", \"error\": {\"status\": 409, \"message\": \"{self.first_name} still has an address in the addressbook.\"}, \"examples\": [{\"self\": {\"first_name\": \"Ann\"}, \"old\": null, \"refs\": {}, \"ctx\": {}, \"expect\": \"pass\"}, {\"self\": {\"first_name\": \"Bob\"}, \"old\": null, \"refs\": {\"address\": {\"address_key\": \"bob-home\"}}, \"ctx\": {}, \"expect\": \"fail\"}]}]}"; + +/// Every rule, keyed by the resource kind it applies to. +pub static RULES: LazyLock>> = + LazyLock::new(|| serde_json::from_str(RULES_JSON).expect("firestone generated invalid rules")); diff --git a/examples/addressbook/server-rs/app/src/validation/runtime.rs b/examples/addressbook/server-rs/app/src/validation/runtime.rs new file mode 100644 index 0000000..8d3e287 --- /dev/null +++ b/examples/addressbook/server-rs/app/src/validation/runtime.rs @@ -0,0 +1,501 @@ +//! The validation engine. +//! +//! This file is generated by firestone but never changes: all of the +//! per-project detail lives in `rules.rs`. + +use std::collections::HashMap; + +use serde_json::Value; + +use super::ports::Error; +use super::ports::RefRequest; +use super::ports::Resolved; +use super::ports::Resolver; +use super::ports::ValidationError; +use super::ports::Violation; +use super::rules::Ref; +use super::rules::Rule; + +/// What is being validated, and everything the rules need to judge it. +/// +/// Built with the chaining setters, so only what a rule actually reads has to be +/// supplied: +/// +/// ```ignore +/// let request = Request::new("patch", "addressbook").body(&body).old(¤t); +/// validate(request, &resolver).await?; +/// ``` +#[derive(Clone, Debug)] +pub struct Request<'a> { + /// The HTTP method being performed, lowercase. + pub op: &'a str, + /// The resource kind being operated on, as written in the schema. + pub resource: &'a str, + /// The request body. + pub body: Option<&'a Value>, + /// The resource as it currently stands, needed by PATCH, PUT and DELETE rules. + pub old: Option<&'a Value>, + /// Request scoped context: the caller's roles, the tenant, a trace id. + pub ctx: Option<&'a Value>, + /// The resource as it will be, when you would rather work that out yourself. + pub subject: Option<&'a Value>, +} + +impl<'a> Request<'a> { + /// Start a request for the given method and resource kind. + pub fn new(op: &'a str, resource: &'a str) -> Self { + Request { + op, + resource, + body: None, + old: None, + ctx: None, + subject: None, + } + } + + /// Set the request body. + pub fn body(mut self, body: &'a Value) -> Self { + self.body = Some(body); + self + } + + /// Set the resource as it currently stands. + pub fn old(mut self, old: &'a Value) -> Self { + self.old = Some(old); + self + } + + /// Set the request context, read by the rules and handed to the resolver. + pub fn ctx(mut self, ctx: &'a Value) -> Self { + self.ctx = Some(ctx); + self + } + + /// Set the resulting resource explicitly, instead of merging `body` over `old`. + /// + /// Needed for anything whose update semantics are not a plain merge, and for + /// endpoints that update one attribute rather than a whole resource. + pub fn subject(mut self, subject: &'a Value) -> Self { + self.subject = Some(subject); + self + } +} + +/// Look up a dotted path such as `person.first_name`. +/// +/// Returns `None` when any part of the path is absent, which is distinct from +/// finding a `null`. +pub fn resolve_path<'a>(data: &'a Value, path: &str) -> Option<&'a Value> { + let mut current = data; + for part in path.split('.') { + current = current.get(part)?; + } + + Some(current) +} + +/// Apply a partial body over the current resource, nested objects included. +/// +/// Nested objects are merged, so a body that touches one field of an embedded +/// object leaves the rest of that object alone. Everything else is replaced +/// outright: lists wholesale, and an explicit null as a null rather than as a +/// removal. That last point is where this differs from RFC 7386 JSON Merge Patch, +/// which deletes a member set to null. +/// +/// If your API implements merge patch properly, or JSON Patch, or anything else, +/// do not rely on this: work out the resulting document yourself and hand it to +/// [`Request::subject`]. +pub fn merge(old: &Value, body: &Value) -> Value { + let (Some(old_object), Some(body_object)) = (old.as_object(), body.as_object()) else { + return body.clone(); + }; + + let mut merged = old_object.clone(); + for (key, value) in body_object { + match merged.get(key) { + Some(current) if current.is_object() && value.is_object() => { + merged.insert(key.clone(), merge(current, value)); + } + _ => { + merged.insert(key.clone(), value.clone()); + } + } + } + + Value::Object(merged) +} + +fn is_empty(value: &Value) -> bool { + value.as_object().is_none_or(|object| object.is_empty()) +} + +/// Work out the object the rules are evaluated against. +/// +/// `op` is the normalised method, so that which rules run and what they run +/// against can never disagree. +fn subject_of(request: &Request<'_>, op: &str) -> Value { + if let Some(subject) = request.subject { + return subject.clone(); + } + + let body = request + .body + .cloned() + .unwrap_or_else(|| Value::Object(Default::default())); + let old = request + .old + .cloned() + .unwrap_or_else(|| Value::Object(Default::default())); + + if op == "delete" { + return if is_empty(&old) { body } else { old }; + } + if op == "patch" && !is_empty(&old) { + return merge(&old, &body); + } + + body +} + +/// Interpolate `{self.city}` style placeholders in an error message. +fn format_message(message: &str, scope: &Value) -> String { + let mut out = String::with_capacity(message.len()); + let mut rest = message; + + while let Some(start) = rest.find('{') { + out.push_str(&rest[..start]); + rest = &rest[start..]; + + let Some(end) = rest.find('}') else { + break; + }; + let path = &rest[1..end]; + let is_path = !path.is_empty() + && path.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') + && path + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.'); + + match is_path.then(|| resolve_path(scope, path)).flatten() { + Some(value) => out.push_str(&render(value)), + None => out.push_str(&rest[..=end]), + } + rest = &rest[end + 1..]; + } + out.push_str(rest); + + out +} + +/// Render a value the way it should read inside a message. +/// +/// Strings go in unquoted; everything else is rendered as JSON, so a boolean reads +/// as `true` and matches what the python package would have written for the same +/// rule. +fn render(value: &Value) -> String { + match value { + Value::String(text) => text.clone(), + other => other.to_string(), + } +} + +/// Whether an existence rule is checking a reference that did not change. +/// +/// A reference that is the same as the one already stored was checked when it was +/// stored, so an update that leaves it alone does not need to look it up again. +fn unchanged(rule: &Rule, subject: &Value, old: &Value) -> bool { + if rule.kind != "exists" || is_empty(old) { + return false; + } + + rule.refs.iter().all(|reference| { + resolve_path(old, &reference.value) == resolve_path(subject, &reference.value) + }) +} + +/// Get the first lookup this rule needed but did not get, if any. +/// +/// A rule that reads through a lookup cannot be evaluated when that lookup found +/// nothing, so rather than let the expression blow up, the rule simply fails. A +/// rule that is *about* absence declares its lookup `optional` and guards the +/// expression with `has()`. +fn unresolved<'a>(rule: &'a Rule, refs: &serde_json::Map) -> Option<&'a Ref> { + rule.refs + .iter() + .find(|reference| !reference.optional && !refs.contains_key(&reference.name)) +} + +/// Report a required lookup that found nothing, in terms of the lookup itself. +fn missing_violation(rule: &Rule, reference: &Ref, subject: &Value) -> Violation { + let value = resolve_path(subject, &reference.value) + .map(render) + .unwrap_or_else(|| "None".to_string()); + + Violation { + rule: rule.name.clone(), + resource: rule.resource.clone(), + field: rule.field.clone().or_else(|| Some(reference.value.clone())), + message: format!( + "No {} found with {} '{}'.", + reference.kind, reference.key, value + ), + status: rule.error.status, + } +} + +/// Work out every lookup the given rules need. +/// +/// A rule whose lookup value is not present in the subject is skipped rather than +/// failed, so a PATCH that does not touch a referenced field is left alone. +type Plan<'a> = Vec<(&'a Rule, HashMap)>; + +fn plan<'a>(rules: &[&'a Rule], subject: &Value, old: &Value) -> (Plan<'a>, Vec) { + let mut requests: Vec = Vec::new(); + let mut seen: Vec = Vec::new(); + let mut runnable: Plan<'a> = Vec::new(); + + for rule in rules { + if unchanged(rule, subject, old) { + continue; + } + + let mut lookups = HashMap::new(); + let mut pending = Vec::new(); + let mut skip = false; + for reference in &rule.refs { + let Some(value) = resolve_path(subject, &reference.value) else { + skip = true; + break; + }; + if value.is_null() { + skip = true; + break; + } + + let request = RefRequest { + kind: reference.kind.clone(), + key: reference.key.clone(), + value: value.clone(), + }; + lookups.insert(reference.name.clone(), request.id()); + pending.push(request); + } + + // Only once the whole rule is known to run, so a rule skipped on its second + // lookup does not leave the first one queued for a round trip nobody needs. + if !skip { + for request in pending { + let id = request.id(); + if !seen.contains(&id) { + seen.push(id); + requests.push(request); + } + } + runnable.push((rule, lookups)); + } + } + + (runnable, requests) +} + +#[cfg(feature = "cel")] +mod cel { + use std::collections::HashMap; + use std::sync::Arc; + use std::sync::LazyLock; + use std::sync::RwLock; + + use cel_interpreter::Context; + use cel_interpreter::Program; + use cel_interpreter::Value as CelValue; + use serde_json::Value; + + use super::Error; + use super::Rule; + + static PROGRAMS: LazyLock>>> = + LazyLock::new(|| RwLock::new(HashMap::new())); + + fn compiled(expr: &str, rule: &str) -> Result, Error> { + if let Some(program) = PROGRAMS + .read() + .ok() + .and_then(|cache| cache.get(expr).cloned()) + { + return Ok(program); + } + + let program = Program::compile(expr).map_err(|err| { + Error::Rule(format!( + "Rule '{rule}' has an invalid CEL expression: {err}" + )) + })?; + let program = Arc::new(program); + if let Ok(mut cache) = PROGRAMS.write() { + cache.insert(expr.to_string(), Arc::clone(&program)); + } + + Ok(program) + } + + /// Evaluate a rule's CEL expression against the given scope. + pub fn evaluate(rule: &Rule, scope: &Value) -> Result { + let expr = rule + .expr + .as_deref() + .ok_or_else(|| Error::Rule(format!("Rule '{}' has no expression", rule.name)))?; + let program = compiled(expr, &rule.name)?; + + let mut context = Context::default(); + for (name, value) in scope.as_object().into_iter().flatten() { + context + .add_variable(name.as_str(), value.clone()) + .map_err(|err| { + Error::Rule(format!( + "Rule '{}' could not be given {name}: {err}", + rule.name + )) + })?; + } + + match program.execute(&context) { + Ok(CelValue::Bool(passed)) => Ok(passed), + Ok(other) => Err(Error::Rule(format!( + "Rule '{}' evaluated to {other:?} rather than a boolean", + rule.name + ))), + Err(err) => Err(Error::Rule(format!( + "Rule '{}' could not be evaluated: {err}", + rule.name + ))), + } + } +} + +#[cfg(not(feature = "cel"))] +mod cel { + use serde_json::Value; + + use super::Error; + use super::Rule; + + /// Without the `cel` feature there is no way to evaluate an expression. + pub fn evaluate(rule: &Rule, _scope: &Value) -> Result { + Err(Error::Rule(format!( + "Rule '{}' carries a CEL expression, so this crate needs the 'cel' feature", + rule.name + ))) + } +} + +/// Evaluate a single rule that has had all of its lookups resolved. +fn passes(rule: &Rule, scope: &Value) -> Result { + match rule.kind.as_str() { + // Its lookup is required, so reaching here means the resource was found. + "exists" => Ok(true), + "immutable" => { + let old = &scope["old"]; + if is_empty(old) { + return Ok(true); + } + let field = rule.field.as_deref().unwrap_or_default(); + + Ok(resolve_path(old, field) == resolve_path(&scope["self"], field)) + } + "expr" => cel::evaluate(rule, scope), + other => Err(Error::Rule(format!( + "Rule '{}' has an unknown kind '{other}'", + rule.name + ))), + } +} + +/// Run every rule in `rules` that applies to this request. +/// +/// Prefer the `validate` in the parent module, which uses the generated rules. +pub async fn validate_with_rules( + rules: &HashMap>, + request: Request<'_>, + resolver: Option<&mut dyn Resolver>, +) -> Result<(), Error> { + let op = request.op.to_lowercase(); + let Some(all) = rules.get(request.resource) else { + return Ok(()); + }; + + let applicable: Vec<&Rule> = all + .iter() + .filter(|rule| rule.methods.iter().any(|m| m == &op)) + .collect(); + if applicable.is_empty() { + return Ok(()); + } + + let empty = Value::Object(Default::default()); + let ctx = request.ctx.cloned().unwrap_or_else(|| empty.clone()); + let old = request.old.cloned().unwrap_or_else(|| empty.clone()); + let subject = subject_of(&request, &op); + + let (runnable, requests) = plan(&applicable, &subject, &old); + + let mut resolved: Resolved = HashMap::new(); + if !requests.is_empty() { + // Rules reading only self, old and ctx need nothing fetched, so a resolver is + // only required once a lookup has actually been planned. + let Some(resolver) = resolver else { + return Err(Error::Rule(format!( + "Validating {op} on {} needs {} lookup(s) but no resolver was given", + request.resource, + requests.len() + ))); + }; + resolved = resolver + .resolve(&requests, &ctx) + .await + .map_err(Error::Resolver)?; + } + + let mut violations = Vec::new(); + for (rule, lookups) in runnable { + let mut refs = serde_json::Map::new(); + for (name, id) in &lookups { + match resolved.get(id) { + Some(found) if !found.is_null() => { + refs.insert(name.clone(), found.clone()); + } + _ => {} + } + } + + if let Some(reference) = unresolved(rule, &refs) { + violations.push(missing_violation(rule, reference, &subject)); + continue; + } + + let scope = serde_json::json!({ + "self": subject, + "old": old, + "ctx": ctx, + "refs": Value::Object(refs), + }); + + if passes(rule, &scope)? { + continue; + } + + violations.push(Violation { + rule: rule.name.clone(), + resource: rule.resource.clone(), + field: rule.field.clone(), + message: format_message(&rule.error.message, &scope), + status: rule.error.status, + }); + } + + if violations.is_empty() { + return Ok(()); + } + + Err(Error::Validation(ValidationError { violations })) +} diff --git a/examples/addressbook/server-rs/app/src/validation/tests.rs b/examples/addressbook/server-rs/app/src/validation/tests.rs new file mode 100644 index 0000000..6e1ec5b --- /dev/null +++ b/examples/addressbook/server-rs/app/src/validation/tests.rs @@ -0,0 +1,101 @@ +//! Tests for the validation rules, generated from the examples in the resource schemas. +//! +//! Generated by firestone. Do not edit: add or change `examples` on a rule and +//! regenerate. The rules run against an in-memory resolver, so no database, and +//! no server, is needed. + +use async_trait::async_trait; +use serde_json::Value; + +use super::ports::Error; +use super::ports::RefRequest; +use super::ports::Resolved; +use super::ports::Resolver; +use super::ports::ResolverError; +use super::rules::Rule; +use super::rules::RULES; +use super::runtime::validate_with_rules; +use super::runtime::Request; + +/// One entry per example: the resource, the rule, the method, and the example index. +const EXAMPLES: &str = "[[\"addressbook\", \"only_admins_may_invalidate\", \"put\", 0], [\"addressbook\", \"only_admins_may_invalidate\", \"put\", 1], [\"addressbook\", \"only_admins_may_invalidate\", \"put\", 2], [\"addressbook\", \"only_admins_may_invalidate\", \"put\", 3], [\"addressbook\", \"only_admins_may_invalidate\", \"put\", 4], [\"persons\", \"person_is_not_in_use\", \"delete\", 0], [\"persons\", \"person_is_not_in_use\", \"delete\", 1]]"; + +/// Returns the resources an example declares, instead of hitting a database. +struct FakeResolver { + rule: Rule, + refs: Value, +} + +#[async_trait] +impl Resolver for FakeResolver { + async fn resolve( + &mut self, + requests: &[RefRequest], + _ctx: &Value, + ) -> Result { + let mut resolved = Resolved::new(); + for request in requests { + for reference in &self.rule.refs { + if reference.kind != request.kind || reference.key != request.key { + continue; + } + if let Some(found) = self.refs.get(&reference.name) { + if !found.is_null() { + resolved.insert(request.id(), found.clone()); + } + } + } + } + + Ok(resolved) + } +} + +fn get_rule(resource: &str, name: &str) -> Rule { + RULES + .get(resource) + .and_then(|rules| rules.iter().find(|rule| rule.name == name)) + .unwrap_or_else(|| panic!("No rule named '{name}' on {resource}")) + .clone() +} + +/// Check that every rule accepts or rejects its examples as the schema says it should. +#[tokio::test] +async fn rule_examples() { + let cases: Vec<(String, String, String, usize)> = + serde_json::from_str(EXAMPLES).expect("firestone generated invalid examples"); + + for (resource, name, op, index) in cases { + let rule = get_rule(&resource, &name); + let example = rule.examples[index].clone(); + let mut resolver = FakeResolver { + rule: rule.clone(), + refs: example.refs.clone(), + }; + + let mut rules = std::collections::HashMap::new(); + rules.insert(resource.clone(), vec![rule]); + + let old = example.old.clone(); + let mut request = Request::new(&op, &resource) + .body(&example.subject) + .ctx(&example.ctx); + if let Some(old) = old.as_ref() { + request = request.old(old); + } + + let result = validate_with_rules(&rules, request, Some(&mut resolver)).await; + let case = format!("{resource}-{name}-{index}"); + + match (example.expect.as_str(), result) { + ("pass", Ok(())) => {} + ("pass", Err(err)) => panic!("{case}: expected to pass, got {err}"), + ("fail", Err(Error::Validation(failed))) => { + assert_eq!(failed.violations[0].rule, name, "{case}: wrong rule failed"); + } + ("fail", Ok(())) => panic!("{case}: expected to fail, but passed"), + ("fail", Err(err)) => panic!("{case}: expected a validation failure, got {err}"), + (other, _) => panic!("{case}: unknown expectation '{other}'"), + } + } +} diff --git a/examples/addressbook/server-rs/app/tests/api.rs b/examples/addressbook/server-rs/app/tests/api.rs new file mode 100644 index 0000000..f9b4be5 --- /dev/null +++ b/examples/addressbook/server-rs/app/tests/api.rs @@ -0,0 +1,282 @@ +//! Tests for the implementation firestone generated. +//! +//! Generated from the same OpenAPI document as the handlers, so a template change +//! that compiles but misbehaves fails here rather than in production. The router is +//! driven directly with `oneshot`, so nothing binds a port. + +use std::sync::Arc; + +use axum::body::Body; +use axum::http::Request; +use axum::http::StatusCode; +use http_body_util::BodyExt; +use serde_json::json; +use serde_json::Value; +use tower::ServiceExt; + +use addressbook_server::backend::InMemory; +use addressbook_server::router; +use addressbook_server::Api; + +const TOKEN: &str = "test-token"; + +/// A server with nothing stored and one accepted token. +fn server() -> (axum::Router, Arc) { + let backend = Arc::new(InMemory::new()); + + ( + router(Api::with_tokens(backend.clone(), [TOKEN.to_string()])), + backend, + ) +} + +async fn call( + app: axum::Router, + method: &str, + uri: &str, + scheme: Option<&str>, + body: Option, +) -> (StatusCode, Value) { + let mut request = Request::builder() + .method(method) + .uri(uri) + // The generated handlers extract Host, so a request without one is + // rejected before it reaches them. + .header("host", "localhost"); + if let Some(scheme) = scheme { + request = request.header("authorization", format!("{scheme} {TOKEN}")); + } + let request = match body { + Some(body) => request + .header("content-type", "application/json") + .body(Body::from(body.to_string())), + None => request.body(Body::empty()), + } + .expect("the request builds"); + + let response = app.oneshot(request).await.expect("the router answers"); + let status = response.status(); + let bytes = response + .into_body() + .collect() + .await + .expect("the body reads") + .to_bytes(); + + ( + status, + serde_json::from_slice(&bytes).unwrap_or(Value::Null), + ) +} + +/// A created addressbook comes back carrying the key it was given, and that key works. +/// +/// Without it the caller has no way to GET, PUT or DELETE what it just made. +#[tokio::test] +async fn addressbook_create_returns_a_usable_key() { + let (app, _backend) = server(); + + let body: Value = + serde_json::from_str("{\"addrtype\": \"work\", \"street\": \"x\", \"city\": \"x\", \"state\": \"x\", \"country\": \"x\"}").expect("the sample body parses"); + let (status, created) = call( + app.clone(), + "POST", + "/addressbook", + Some("Bearer"), + Some(body), + ) + .await; + assert_eq!(status, StatusCode::CREATED, "create answered {created}"); + + let key = created + .get("address_key") + .and_then(Value::as_str) + .expect("the created resource carries its key") + .to_string(); + + let (status, _) = call( + app, + "GET", + &format!("/addressbook/{}", key), + Some("Bearer"), + None, + ) + .await; + assert_eq!( + status, + StatusCode::OK, + "the key a create returned should fetch" + ); +} + +/// A created persons comes back carrying the key it was given, and that key works. +/// +/// Without it the caller has no way to GET, PUT or DELETE what it just made. +#[tokio::test] +async fn persons_create_returns_a_usable_key() { + let (app, _backend) = server(); + + let body: Value = serde_json::from_str("{}").expect("the sample body parses"); + let (status, created) = call(app.clone(), "POST", "/persons", Some("Bearer"), Some(body)).await; + assert_eq!(status, StatusCode::CREATED, "create answered {created}"); + + let key = created + .get("uuid") + .and_then(Value::as_str) + .expect("the created resource carries its key") + .to_string(); + + let (status, _) = call( + app, + "GET", + &format!("/persons/{}", key), + Some("Bearer"), + None, + ) + .await; + assert_eq!( + status, + StatusCode::OK, + "the key a create returned should fetch" + ); +} + +/// A created postal_codes comes back carrying the key it was given, and that key works. +/// +/// Without it the caller has no way to GET, PUT or DELETE what it just made. +#[tokio::test] +async fn postal_codes_create_returns_a_usable_key() { + let (app, _backend) = server(); + + let body: Value = serde_json::from_str("{}").expect("the sample body parses"); + let (status, created) = call( + app.clone(), + "POST", + "/postal_codes", + Some("Bearer"), + Some(body), + ) + .await; + assert_eq!(status, StatusCode::CREATED, "create answered {created}"); + + let key = created + .get("uuid") + .and_then(Value::as_str) + .expect("the created resource carries its key") + .to_string(); + + let (status, _) = call( + app, + "GET", + &format!("/postal_codes/{}", key), + Some("Bearer"), + None, + ) + .await; + assert_eq!( + status, + StatusCode::OK, + "the key a create returned should fetch" + ); +} + +/// A secured operation refuses a request with no credentials. +#[tokio::test] +async fn a_secured_operation_needs_a_token() { + let (app, _) = server(); + let (status, _) = call(app, "POST", "/persons", None, Some(json!({}))).await; + assert_eq!( + status, + StatusCode::UNAUTHORIZED, + "POST /persons should need a token" + ); + let (app, _) = server(); + let (status, _) = call(app, "DELETE", "/persons/test-key", None, Some(json!({}))).await; + assert_eq!( + status, + StatusCode::UNAUTHORIZED, + "DELETE /persons/test-key should need a token" + ); + let (app, _) = server(); + let (status, _) = call(app, "PUT", "/persons/test-key", None, Some(json!({}))).await; + assert_eq!( + status, + StatusCode::UNAUTHORIZED, + "PUT /persons/test-key should need a token" + ); + let (app, _) = server(); + let (status, _) = call(app, "POST", "/postal_codes", None, Some(json!({}))).await; + assert_eq!( + status, + StatusCode::UNAUTHORIZED, + "POST /postal_codes should need a token" + ); + let (app, _) = server(); + let (status, _) = call( + app, + "DELETE", + "/postal_codes/test-key", + None, + Some(json!({})), + ) + .await; + assert_eq!( + status, + StatusCode::UNAUTHORIZED, + "DELETE /postal_codes/test-key should need a token" + ); + let (app, _) = server(); + let (status, _) = call( + app, + "DELETE", + "/postal_codes/test-key/name", + None, + Some(json!({})), + ) + .await; + assert_eq!( + status, + StatusCode::UNAUTHORIZED, + "DELETE /postal_codes/test-key/name should need a token" + ); +} + +/// The auth-scheme name is case insensitive, as RFC 7235 has it. +#[tokio::test] +async fn a_lowercase_scheme_is_accepted() { + let (app, _) = server(); + let (status, _) = call(app, "POST", "/persons", Some("bearer"), Some(json!({}))).await; + + assert_ne!( + status, + StatusCode::UNAUTHORIZED, + "'bearer' is as valid as 'Bearer'" + ); +} + +/// A token that is not accepted is refused. +#[tokio::test] +async fn a_wrong_token_is_refused() { + let (app, _) = server(); + let request = Request::builder() + .method("POST") + .uri("/persons") + .header("host", "localhost") + .header("authorization", "Bearer not-the-token") + .header("content-type", "application/json") + .body(Body::from("{}")) + .expect("the request builds"); + let status = app.oneshot(request).await.expect("answers").status(); + + assert_eq!(status, StatusCode::UNAUTHORIZED); +} +/// A resource that is not there is a 404, served as an RFC 9457 problem document. +#[tokio::test] +async fn a_missing_resource_is_a_problem_document() { + let (app, _) = server(); + let (status, body) = call(app, "GET", "/addressbook/test-key", None, None).await; + + assert_eq!(status, StatusCode::NOT_FOUND); + assert_eq!(body["status"], 404); + assert_eq!(body["title"], "Not found"); +} diff --git a/examples/addressbook/validation-rs/src/validation/rules.rs b/examples/addressbook/validation-rs/src/validation/rules.rs index d622824..28cd3e3 100644 --- a/examples/addressbook/validation-rs/src/validation/rules.rs +++ b/examples/addressbook/validation-rs/src/validation/rules.rs @@ -79,7 +79,7 @@ pub struct Rule { } /// The rules as generated, before they are parsed. -const RULES_JSON: &str = "{\"addressbook\": [{\"name\": \"person_must_exist\", \"description\": \"The referenced persons must already exist.\", \"resource\": \"addressbook\", \"kind\": \"exists\", \"field\": \"person.first_name\", \"methods\": [\"post\", \"put\", \"patch\"], \"refs\": [{\"name\": \"person\", \"kind\": \"persons\", \"key\": \"first_name\", \"value\": \"person.first_name\", \"optional\": false}], \"expr\": null, \"error\": {\"status\": 422, \"message\": \"No persons found with first_name '{self.person.first_name}'.\"}, \"examples\": []}, {\"name\": \"only_admins_may_invalidate\", \"description\": \"Marking an address invalid is an administrative action.\", \"resource\": \"addressbook\", \"kind\": \"expr\", \"field\": null, \"methods\": [\"put\", \"patch\"], \"refs\": [], \"expr\": \"self.is_valid == old.is_valid || (has(ctx.roles) && \\\"admin\\\" in ctx.roles)\", \"error\": {\"status\": 403, \"message\": \"Only an admin may change is_valid on an address.\"}, \"examples\": [{\"self\": {\"is_valid\": false}, \"old\": {\"is_valid\": true}, \"refs\": {}, \"ctx\": {\"roles\": [\"admin\"]}, \"expect\": \"pass\"}, {\"self\": {\"is_valid\": false}, \"old\": {\"is_valid\": true}, \"refs\": {}, \"ctx\": {\"roles\": [\"user\"]}, \"expect\": \"fail\"}, {\"self\": {\"is_valid\": true}, \"old\": {\"is_valid\": true}, \"refs\": {}, \"ctx\": {\"roles\": [\"user\"]}, \"expect\": \"pass\"}]}], \"persons\": [{\"name\": \"person_is_not_in_use\", \"description\": \"A person cannot be deleted while an address still points at them.\", \"resource\": \"persons\", \"kind\": \"expr\", \"field\": null, \"methods\": [\"delete\"], \"refs\": [{\"name\": \"address\", \"kind\": \"addressbook\", \"key\": \"person.first_name\", \"value\": \"first_name\", \"optional\": true}], \"expr\": \"!has(refs.address)\", \"error\": {\"status\": 409, \"message\": \"{self.first_name} still has an address in the addressbook.\"}, \"examples\": [{\"self\": {\"first_name\": \"Ann\"}, \"old\": null, \"refs\": {}, \"ctx\": {}, \"expect\": \"pass\"}, {\"self\": {\"first_name\": \"Bob\"}, \"old\": null, \"refs\": {\"address\": {\"address_key\": \"bob-home\"}}, \"ctx\": {}, \"expect\": \"fail\"}]}]}"; +const RULES_JSON: &str = "{\"addressbook\": [{\"name\": \"person_must_exist\", \"description\": \"The referenced persons must already exist.\", \"resource\": \"addressbook\", \"kind\": \"exists\", \"field\": \"person.first_name\", \"methods\": [\"post\", \"put\", \"patch\"], \"refs\": [{\"name\": \"person\", \"kind\": \"persons\", \"key\": \"first_name\", \"value\": \"person.first_name\", \"optional\": false}], \"expr\": null, \"error\": {\"status\": 422, \"message\": \"No persons found with first_name '{self.person.first_name}'.\"}, \"examples\": []}, {\"name\": \"only_admins_may_invalidate\", \"description\": \"Marking an address invalid is an administrative action.\", \"resource\": \"addressbook\", \"kind\": \"expr\", \"field\": null, \"methods\": [\"put\", \"patch\"], \"refs\": [], \"expr\": \"!has(old.is_valid) || !has(self.is_valid) || self.is_valid == old.is_valid || (has(ctx.roles) && \\\"admin\\\" in ctx.roles)\", \"error\": {\"status\": 403, \"message\": \"Only an admin may change is_valid on an address.\"}, \"examples\": [{\"self\": {\"is_valid\": false}, \"old\": {\"is_valid\": true}, \"refs\": {}, \"ctx\": {\"roles\": [\"admin\"]}, \"expect\": \"pass\"}, {\"self\": {\"is_valid\": false}, \"old\": {\"is_valid\": true}, \"refs\": {}, \"ctx\": {\"roles\": [\"user\"]}, \"expect\": \"fail\"}, {\"self\": {\"is_valid\": true}, \"old\": {\"is_valid\": true}, \"refs\": {}, \"ctx\": {\"roles\": [\"user\"]}, \"expect\": \"pass\"}, {\"self\": {\"city\": \"Ottawa\"}, \"old\": {\"city\": \"Toronto\"}, \"refs\": {}, \"ctx\": {\"roles\": [\"user\"]}, \"expect\": \"pass\"}, {\"self\": {\"city\": \"Ottawa\"}, \"old\": {\"city\": \"Toronto\", \"is_valid\": true}, \"refs\": {}, \"ctx\": {\"roles\": [\"user\"]}, \"expect\": \"pass\"}]}], \"persons\": [{\"name\": \"person_is_not_in_use\", \"description\": \"A person cannot be deleted while an address still points at them.\", \"resource\": \"persons\", \"kind\": \"expr\", \"field\": null, \"methods\": [\"delete\"], \"refs\": [{\"name\": \"address\", \"kind\": \"addressbook\", \"key\": \"person.first_name\", \"value\": \"first_name\", \"optional\": true}], \"expr\": \"!has(refs.address)\", \"error\": {\"status\": 409, \"message\": \"{self.first_name} still has an address in the addressbook.\"}, \"examples\": [{\"self\": {\"first_name\": \"Ann\"}, \"old\": null, \"refs\": {}, \"ctx\": {}, \"expect\": \"pass\"}, {\"self\": {\"first_name\": \"Bob\"}, \"old\": null, \"refs\": {\"address\": {\"address_key\": \"bob-home\"}}, \"ctx\": {}, \"expect\": \"fail\"}]}]}"; /// Every rule, keyed by the resource kind it applies to. pub static RULES: LazyLock>> = diff --git a/examples/addressbook/validation-rs/src/validation/tests.rs b/examples/addressbook/validation-rs/src/validation/tests.rs index c5d5d8e..6e1ec5b 100644 --- a/examples/addressbook/validation-rs/src/validation/tests.rs +++ b/examples/addressbook/validation-rs/src/validation/tests.rs @@ -18,7 +18,7 @@ use super::runtime::validate_with_rules; use super::runtime::Request; /// One entry per example: the resource, the rule, the method, and the example index. -const EXAMPLES: &str = "[[\"addressbook\", \"only_admins_may_invalidate\", \"put\", 0], [\"addressbook\", \"only_admins_may_invalidate\", \"put\", 1], [\"addressbook\", \"only_admins_may_invalidate\", \"put\", 2], [\"persons\", \"person_is_not_in_use\", \"delete\", 0], [\"persons\", \"person_is_not_in_use\", \"delete\", 1]]"; +const EXAMPLES: &str = "[[\"addressbook\", \"only_admins_may_invalidate\", \"put\", 0], [\"addressbook\", \"only_admins_may_invalidate\", \"put\", 1], [\"addressbook\", \"only_admins_may_invalidate\", \"put\", 2], [\"addressbook\", \"only_admins_may_invalidate\", \"put\", 3], [\"addressbook\", \"only_admins_may_invalidate\", \"put\", 4], [\"persons\", \"person_is_not_in_use\", \"delete\", 0], [\"persons\", \"person_is_not_in_use\", \"delete\", 1]]"; /// Returns the resources an example declares, instead of hitting a database. struct FakeResolver { diff --git a/examples/addressbook_rs/src/cli/addressbook.rs b/examples/addressbook_rs/src/cli/addressbook.rs index 4eaaa07..0797172 100644 --- a/examples/addressbook_rs/src/cli/addressbook.rs +++ b/examples/addressbook_rs/src/cli/addressbook.rs @@ -165,8 +165,12 @@ pub async fn handle_addressbook_post( config.base_path = url.clone(); } if let Some(ref key) = ctx.api_key { + // A 'bearer' scheme and an 'apiKey' scheme are read from different fields by + // the generated client, and which one a resource declares is up to its + // schema, so both are set rather than guessing. + config.bearer_access_token = Some(key.clone()); config.api_key = Some(crate::apis::configuration::ApiKey { - prefix: Some("Bearer".to_string()), + prefix: None, key: key.clone(), }); } @@ -203,8 +207,12 @@ pub async fn handle_addressbook_get( config.base_path = url.clone(); } if let Some(ref key) = ctx.api_key { + // A 'bearer' scheme and an 'apiKey' scheme are read from different fields by + // the generated client, and which one a resource declares is up to its + // schema, so both are set rather than guessing. + config.bearer_access_token = Some(key.clone()); config.api_key = Some(crate::apis::configuration::ApiKey { - prefix: Some("Bearer".to_string()), + prefix: None, key: key.clone(), }); } @@ -227,8 +235,12 @@ pub async fn handle_addressbook_address_key_delete( config.base_path = url.clone(); } if let Some(ref key) = ctx.api_key { + // A 'bearer' scheme and an 'apiKey' scheme are read from different fields by + // the generated client, and which one a resource declares is up to its + // schema, so both are set rather than guessing. + config.bearer_access_token = Some(key.clone()); config.api_key = Some(crate::apis::configuration::ApiKey { - prefix: Some("Bearer".to_string()), + prefix: None, key: key.clone(), }); } @@ -262,8 +274,12 @@ pub async fn handle_addressbook_address_key_get( config.base_path = url.clone(); } if let Some(ref key) = ctx.api_key { + // A 'bearer' scheme and an 'apiKey' scheme are read from different fields by + // the generated client, and which one a resource declares is up to its + // schema, so both are set rather than guessing. + config.bearer_access_token = Some(key.clone()); config.api_key = Some(crate::apis::configuration::ApiKey { - prefix: Some("Bearer".to_string()), + prefix: None, key: key.clone(), }); } @@ -284,8 +300,12 @@ pub async fn handle_addressbook_address_key_put( config.base_path = url.clone(); } if let Some(ref key) = ctx.api_key { + // A 'bearer' scheme and an 'apiKey' scheme are read from different fields by + // the generated client, and which one a resource declares is up to its + // schema, so both are set rather than guessing. + config.bearer_access_token = Some(key.clone()); config.api_key = Some(crate::apis::configuration::ApiKey { - prefix: Some("Bearer".to_string()), + prefix: None, key: key.clone(), }); } diff --git a/examples/addressbook_rs/src/cli/contacts.rs b/examples/addressbook_rs/src/cli/contacts.rs index 22699ae..ac63557 100644 --- a/examples/addressbook_rs/src/cli/contacts.rs +++ b/examples/addressbook_rs/src/cli/contacts.rs @@ -156,8 +156,12 @@ pub async fn handle_contacts_post( config.base_path = url.clone(); } if let Some(ref key) = ctx.api_key { + // A 'bearer' scheme and an 'apiKey' scheme are read from different fields by + // the generated client, and which one a resource declares is up to its + // schema, so both are set rather than guessing. + config.bearer_access_token = Some(key.clone()); config.api_key = Some(crate::apis::configuration::ApiKey { - prefix: Some("Bearer".to_string()), + prefix: None, key: key.clone(), }); } @@ -208,8 +212,12 @@ pub async fn handle_contacts_get( config.base_path = url.clone(); } if let Some(ref key) = ctx.api_key { + // A 'bearer' scheme and an 'apiKey' scheme are read from different fields by + // the generated client, and which one a resource declares is up to its + // schema, so both are set rather than guessing. + config.bearer_access_token = Some(key.clone()); config.api_key = Some(crate::apis::configuration::ApiKey { - prefix: Some("Bearer".to_string()), + prefix: None, key: key.clone(), }); } @@ -231,8 +239,12 @@ pub async fn handle_contacts_contact_id_delete( config.base_path = url.clone(); } if let Some(ref key) = ctx.api_key { + // A 'bearer' scheme and an 'apiKey' scheme are read from different fields by + // the generated client, and which one a resource declares is up to its + // schema, so both are set rather than guessing. + config.bearer_access_token = Some(key.clone()); config.api_key = Some(crate::apis::configuration::ApiKey { - prefix: Some("Bearer".to_string()), + prefix: None, key: key.clone(), }); } @@ -266,8 +278,12 @@ pub async fn handle_contacts_contact_id_get( config.base_path = url.clone(); } if let Some(ref key) = ctx.api_key { + // A 'bearer' scheme and an 'apiKey' scheme are read from different fields by + // the generated client, and which one a resource declares is up to its + // schema, so both are set rather than guessing. + config.bearer_access_token = Some(key.clone()); config.api_key = Some(crate::apis::configuration::ApiKey { - prefix: Some("Bearer".to_string()), + prefix: None, key: key.clone(), }); } diff --git a/examples/addressbook_rs/src/cli/persons.rs b/examples/addressbook_rs/src/cli/persons.rs index 7c3fc1e..72596f3 100644 --- a/examples/addressbook_rs/src/cli/persons.rs +++ b/examples/addressbook_rs/src/cli/persons.rs @@ -134,8 +134,12 @@ pub async fn handle_persons_post( config.base_path = url.clone(); } if let Some(ref key) = ctx.api_key { + // A 'bearer' scheme and an 'apiKey' scheme are read from different fields by + // the generated client, and which one a resource declares is up to its + // schema, so both are set rather than guessing. + config.bearer_access_token = Some(key.clone()); config.api_key = Some(crate::apis::configuration::ApiKey { - prefix: Some("Bearer".to_string()), + prefix: None, key: key.clone(), }); } @@ -166,8 +170,12 @@ pub async fn handle_persons_get( config.base_path = url.clone(); } if let Some(ref key) = ctx.api_key { + // A 'bearer' scheme and an 'apiKey' scheme are read from different fields by + // the generated client, and which one a resource declares is up to its + // schema, so both are set rather than guessing. + config.bearer_access_token = Some(key.clone()); config.api_key = Some(crate::apis::configuration::ApiKey { - prefix: Some("Bearer".to_string()), + prefix: None, key: key.clone(), }); } @@ -190,8 +198,12 @@ pub async fn handle_persons_uuid_delete( config.base_path = url.clone(); } if let Some(ref key) = ctx.api_key { + // A 'bearer' scheme and an 'apiKey' scheme are read from different fields by + // the generated client, and which one a resource declares is up to its + // schema, so both are set rather than guessing. + config.bearer_access_token = Some(key.clone()); config.api_key = Some(crate::apis::configuration::ApiKey { - prefix: Some("Bearer".to_string()), + prefix: None, key: key.clone(), }); } @@ -225,8 +237,12 @@ pub async fn handle_persons_uuid_get( config.base_path = url.clone(); } if let Some(ref key) = ctx.api_key { + // A 'bearer' scheme and an 'apiKey' scheme are read from different fields by + // the generated client, and which one a resource declares is up to its + // schema, so both are set rather than guessing. + config.bearer_access_token = Some(key.clone()); config.api_key = Some(crate::apis::configuration::ApiKey { - prefix: Some("Bearer".to_string()), + prefix: None, key: key.clone(), }); } @@ -247,8 +263,12 @@ pub async fn handle_persons_uuid_put( config.base_path = url.clone(); } if let Some(ref key) = ctx.api_key { + // A 'bearer' scheme and an 'apiKey' scheme are read from different fields by + // the generated client, and which one a resource declares is up to its + // schema, so both are set rather than guessing. + config.bearer_access_token = Some(key.clone()); config.api_key = Some(crate::apis::configuration::ApiKey { - prefix: Some("Bearer".to_string()), + prefix: None, key: key.clone(), }); } diff --git a/examples/addressbook_rs/src/cli/postal_codes.rs b/examples/addressbook_rs/src/cli/postal_codes.rs index aa1c501..2bcf0ab 100644 --- a/examples/addressbook_rs/src/cli/postal_codes.rs +++ b/examples/addressbook_rs/src/cli/postal_codes.rs @@ -95,8 +95,12 @@ pub async fn handle_postal_codes_post( config.base_path = url.clone(); } if let Some(ref key) = ctx.api_key { + // A 'bearer' scheme and an 'apiKey' scheme are read from different fields by + // the generated client, and which one a resource declares is up to its + // schema, so both are set rather than guessing. + config.bearer_access_token = Some(key.clone()); config.api_key = Some(crate::apis::configuration::ApiKey { - prefix: Some("Bearer".to_string()), + prefix: None, key: key.clone(), }); } @@ -123,8 +127,12 @@ pub async fn handle_postal_codes_get( config.base_path = url.clone(); } if let Some(ref key) = ctx.api_key { + // A 'bearer' scheme and an 'apiKey' scheme are read from different fields by + // the generated client, and which one a resource declares is up to its + // schema, so both are set rather than guessing. + config.bearer_access_token = Some(key.clone()); config.api_key = Some(crate::apis::configuration::ApiKey { - prefix: Some("Bearer".to_string()), + prefix: None, key: key.clone(), }); } @@ -147,8 +155,12 @@ pub async fn handle_postal_codes_uuid_delete( config.base_path = url.clone(); } if let Some(ref key) = ctx.api_key { + // A 'bearer' scheme and an 'apiKey' scheme are read from different fields by + // the generated client, and which one a resource declares is up to its + // schema, so both are set rather than guessing. + config.bearer_access_token = Some(key.clone()); config.api_key = Some(crate::apis::configuration::ApiKey { - prefix: Some("Bearer".to_string()), + prefix: None, key: key.clone(), }); } @@ -182,8 +194,12 @@ pub async fn handle_postal_codes_uuid_get( config.base_path = url.clone(); } if let Some(ref key) = ctx.api_key { + // A 'bearer' scheme and an 'apiKey' scheme are read from different fields by + // the generated client, and which one a resource declares is up to its + // schema, so both are set rather than guessing. + config.bearer_access_token = Some(key.clone()); config.api_key = Some(crate::apis::configuration::ApiKey { - prefix: Some("Bearer".to_string()), + prefix: None, key: key.clone(), }); } diff --git a/firestone/__main__.py b/firestone/__main__.py index f618b25..b2fe3e5 100644 --- a/firestone/__main__.py +++ b/firestone/__main__.py @@ -401,6 +401,95 @@ def validations(rsrc_data, output_dir, no_tests, language): ) -if __name__ == "main": +@generate.command() +@click.option( + "--output-dir", + "-o", + help="Location of the directory to write the server crate to", + type=click.Path(file_okay=False, dir_okay=True, writable=True), + required=True, +) +@click.option( + "--pkg", + help="The name of the generated crate", + default="api_server", + show_default=True, +) +@click.option( + "--api-pkg", + help="The crate name openapi-generator was given for the server", + default="openapi", + show_default=True, +) +@click.option( + "--no-validations", + help="Do not wire the resources' validation rules into the handlers", + is_flag=True, +) +@click.option( + "--force", + help="Overwrite the scaffolded files too, losing any edits made to them", + is_flag=True, +) +@click.option( + "--language", + "-l", + help="The language to generate the server for (only rust today)", + type=click.Choice([LANG_RUST], case_sensitive=False), + default=LANG_RUST, + show_default=True, +) +@click.pass_obj +def server(rsrc_data, output_dir, pkg, api_pkg, no_validations, force, language): + """Generate the implementation of a rust-axum server. + + The server itself comes from openapi-generator, which turns the OpenAPI document + firestone produces into a router, typed models, per-operation authentication and + request validation. This generates the implementation of the traits it leaves + behind: every operation, wired to a Backend trait for you to implement, with the + resources' validation rules enforced along the way. + """ + del language # only rust for now, the option keeps the interface open + + files = firestone_spec.server_rust.generate( + rsrc_data["data"], + rsrc_data["title"], + rsrc_data["desc"], + rsrc_data["version"], + pkg=pkg, + api_pkg=api_pkg, + ruleset=rsrc_data["validations"], + with_validations=not no_validations, + ) + + written = 0 + kept = [] + for name, content in files.items(): + path = os.path.join(output_dir, name) + + # A scaffold is yours once it exists: the Backend you implemented and the + # tokens you accept must survive a regeneration. Only the files that follow + # the OpenAPI document exactly are rewritten every time. + scaffold = name in firestone_spec.server_rust.SCAFFOLD + if scaffold and not force and os.path.exists(path): + kept.append(name) + continue + + os.makedirs(os.path.dirname(path), exist_ok=True) + _LOGGER.info(f"Writing {path}") + with io.open(path, "w", encoding="utf-8") as fh: + fh.write(content) + written += 1 + + click.echo(f"Wrote {written} file(s) to {output_dir}.", err=True) + if kept: + click.echo( + f"Kept {len(kept)} file(s) you own: {', '.join(sorted(kept))}. " + "Pass --force to overwrite them.", + err=True, + ) + + +if __name__ == "__main__": # pylint: disable=no-value-for-parameter main() diff --git a/firestone/schema/_handlers.rs.jinja2 b/firestone/schema/_handlers.rs.jinja2 index eeda3f7..480775a 100644 --- a/firestone/schema/_handlers.rs.jinja2 +++ b/firestone/schema/_handlers.rs.jinja2 @@ -4,8 +4,12 @@ config.base_path = url.clone(); } if let Some(ref key) = ctx.api_key { + // A 'bearer' scheme and an 'apiKey' scheme are read from different fields by + // the generated client, and which one a resource declares is up to its + // schema, so both are set rather than guessing. + config.bearer_access_token = Some(key.clone()); config.api_key = Some({{ client_pkg }}::apis::configuration::ApiKey { - prefix: Some("Bearer".to_string()), + prefix: None, key: key.clone(), }); } diff --git a/firestone/schema/asyncapi.jinja2 b/firestone/schema/asyncapi.jinja2 index 12fbfd8..52f9d60 100644 --- a/firestone/schema/asyncapi.jinja2 +++ b/firestone/schema/asyncapi.jinja2 @@ -1,8 +1,8 @@ asyncapi: 2.5.0 defaultContentType: application/json info: - title: {{ title }} - description: {{ description }} + title: {{ title | tojson }} + description: {{ description | tojson }} version: '{{ version }}' {% if components -%} servers: diff --git a/firestone/schema/main.py.jinja2 b/firestone/schema/main.py.jinja2 index acf1667..81ef4cb 100644 --- a/firestone/schema/main.py.jinja2 +++ b/firestone/schema/main.py.jinja2 @@ -166,7 +166,7 @@ async def {{ rsrc["name"] }}(ctx_obj): {% for attr in op["attrs"]|sort(attribute='name') -%} @click.option("--{{ attr["name"].replace("_", "-") }}{%- if attr["type"] == "bool" -%} - --no-{{ attr["name"].replace("_", "-") }} + /--no-{{ attr["name"].replace("_", "-") }} {%- endif -%} ", help="{{ attr["description"] }}", {%- if attr["type"] == "bool" -%} diff --git a/firestone/schema/openapi.jinja2 b/firestone/schema/openapi.jinja2 index f58942e..1e4a7b2 100644 --- a/firestone/schema/openapi.jinja2 +++ b/firestone/schema/openapi.jinja2 @@ -1,7 +1,7 @@ openapi: "{{ openapi_version or '3.0.0' }}" info: - title: {{ title }} - description: {{ description }} + title: {{ title | tojson }} + description: {{ description | tojson }} version: "{{ version }}" # https://www.jvt.me/posts/2022/05/29/openapi-json-patch/ {%- if servers %} diff --git a/firestone/schema/server/rust/Cargo.toml.jinja2 b/firestone/schema/server/rust/Cargo.toml.jinja2 new file mode 100644 index 0000000..b9753cb --- /dev/null +++ b/firestone/schema/server/rust/Cargo.toml.jinja2 @@ -0,0 +1,44 @@ +# {{ title }} +# +# The glue between the generated server and your storage. The server itself is the +# sibling crate, generated by openapi-generator from the OpenAPI document; this one +# implements its traits. +[package] +name = "{{ pkg }}" +version = "{{ crate_version }}" +edition = "2021" +# The generated engine uses LazyLock, and Option::is_none_or in the +# validation runtime. +rust-version = "1.82" +description = {{ description_toml }} + +[dependencies] +{{ api_pkg }} = { path = "../api" } +axum = "0.8" +axum-extra = { version = "0.10", features = ["cookie"] } +http = "1" +tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"] } +tower-http = { version = "0.6", features = ["trace"] } +async-trait = "0.1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +{%- if secured %} +# Constant-time token comparison, rather than hand rolling one in security code. +subtle = "2" +{%- endif %} +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +{%- if validations %} +cel-interpreter = { version = "0.10", optional = true } +{%- endif %} +{% if validations %} +[features] +default = ["cel"] +# Only the rules carrying a CEL expression need this. +cel = ["dep:cel-interpreter"] +{% endif %} + +[dev-dependencies] +http-body-util = "0.1" +tokio = { version = "1", features = ["macros", "rt"] } +tower = { version = "0.5", features = ["util"] } diff --git a/firestone/schema/server/rust/src/auth.rs.jinja2 b/firestone/schema/server/rust/src/auth.rs.jinja2 new file mode 100644 index 0000000..13caa61 --- /dev/null +++ b/firestone/schema/server/rust/src/auth.rs.jinja2 @@ -0,0 +1,67 @@ +//! Who is asking. +//! +//! The generated server decides *which* operations need credentials, from the +//! `security` block in the resource files, and refuses those with a 401 when this +//! returns None. What counts as an acceptable credential is a deployment question, +//! so it lives here. + +use subtle::Choice; +use subtle::ConstantTimeEq; + +use {{ api_pkg }}::apis::ApiAuthBasic; +use {{ api_pkg }}::apis::BasicAuthKind; + +use crate::Api; + +/// Whatever a handler should know about the caller. +/// +/// The generated traits carry this through to every secured operation, so add to +/// it whatever your rules need: a subject, a tenant, a set of roles. +#[derive(Clone, Debug)] +pub struct Claims { + /// The token the caller presented. + pub token: String, +} + +/// Whether any configured token matches, compared in constant time. +/// +/// Every candidate is checked rather than stopping at the first match, and the +/// comparison itself comes from `subtle` rather than being hand rolled, so a wrong +/// token cannot be narrowed down by timing the response. +fn accepted(known: &[String], presented: &str) -> bool { + known.iter().fold(Choice::from(0u8), |found, candidate| { + found | candidate.as_bytes().ct_eq(presented.as_bytes()) + }) + .into() +} + +#[async_trait::async_trait] +impl ApiAuthBasic for Api { + type Claims = Claims; + + async fn extract_claims_from_auth_header( + &self, + kind: BasicAuthKind, + headers: &axum::http::header::HeaderMap, + key: &str, + ) -> Option { + if !matches!(kind, BasicAuthKind::Bearer) { + return None; + } + + // RFC 7235 auth-scheme names are case insensitive, so 'bearer' is as valid + // as 'Bearer'. + let header = headers.get(key)?.to_str().ok()?; + let (scheme, token) = header.split_once(' ')?; + if !scheme.eq_ignore_ascii_case("bearer") { + return None; + } + let token = token.trim(); + + // No configured tokens means every secured request is refused, which is the + // safe way round for a service that asked for authentication. + accepted(&self.tokens, token).then(|| Claims { + token: token.to_string(), + }) + } +} diff --git a/firestone/schema/server/rust/src/backend.rs.jinja2 b/firestone/schema/server/rust/src/backend.rs.jinja2 new file mode 100644 index 0000000..03cf5e9 --- /dev/null +++ b/firestone/schema/server/rust/src/backend.rs.jinja2 @@ -0,0 +1,195 @@ +//! Where the data actually lives. +//! +//! The generated server knows every route, model and rule in your API. It cannot +//! know your storage, and firestone will not guess, so this trait is the seam. +//! [`InMemory`] is generated alongside it so the server answers the moment it is +//! built; swap it for your own and nothing else in the crate changes. + +use std::collections::BTreeMap; +use std::sync::Mutex; + +use async_trait::async_trait; +use serde_json::Value; + +use crate::error::Error; + +/// The storage operations the generated handlers need. +#[async_trait] +pub trait Backend: Send + Sync + 'static { + /// List every resource of this kind. + async fn list(&self, resource: &str) -> Result, Error>; + + /// Get one resource by its key, or None if there is no such thing. + async fn get(&self, resource: &str, key: &str) -> Result, Error>; + + /// Store a new resource, returning the key it was given and the resource. + async fn create(&self, resource: &str, body: Value) -> Result<(String, Value), Error>; + + /// Replace a resource outright. + async fn replace(&self, resource: &str, key: &str, body: Value) -> Result; + + /// Merge a partial body over a resource. + async fn patch(&self, resource: &str, key: &str, body: Value) -> Result; + + /// Remove a resource. + async fn delete(&self, resource: &str, key: &str) -> Result<(), Error>; + + /// Find the first resource whose dotted `path` holds `value`. + /// + /// This is what the generated validation rules look up through, so a rule + /// saying "an address may only name a person who exists" reaches your storage + /// here. `path` is a path into the resource, not a column, so translating it + /// is yours. A real implementation would index rather than scan. + async fn find(&self, resource: &str, path: &str, value: &Value) + -> Result, Error>; +} + +/// A backend that keeps everything in memory, so the server runs as generated. +#[derive(Debug, Default)] +pub struct InMemory { + // One lock over both, so minting a key and storing under it cannot interleave. + state: Mutex, +} + +#[derive(Debug, Default)] +struct Store { + resources: BTreeMap>, + next: BTreeMap, +} + +impl InMemory { + /// A new, empty store. + pub fn new() -> Self { + Self::default() + } + + /// Seed a resource, which is handy in tests and for a runnable demo. + pub fn seed(&self, resource: &str, key: &str, body: Value) { + let mut state = self.state.lock().expect("the store is not poisoned"); + state + .resources + .entry(resource.to_string()) + .or_default() + .insert(key.to_string(), body); + } +} + +/// Look up a dotted path such as `person.first_name`. +pub fn resolve_path<'a>(data: &'a Value, path: &str) -> Option<&'a Value> { + let mut current = data; + for part in path.split('.') { + current = current.get(part)?; + } + + Some(current) +} + +/// Merge a partial body over a resource, nested objects included. +fn merge(current: &Value, body: &Value) -> Value { + let (Some(current_object), Some(body_object)) = (current.as_object(), body.as_object()) else { + return body.clone(); + }; + + let mut merged = current_object.clone(); + for (key, value) in body_object { + match merged.get(key) { + Some(existing) if existing.is_object() && value.is_object() => { + merged.insert(key.clone(), merge(existing, value)); + } + _ => { + merged.insert(key.clone(), value.clone()); + } + } + } + + Value::Object(merged) +} + +#[async_trait] +impl Backend for InMemory { + async fn list(&self, resource: &str) -> Result, Error> { + let state = self.state.lock().expect("the store is not poisoned"); + + Ok(state + .resources + .get(resource) + .map(|items| items.values().cloned().collect()) + .unwrap_or_default()) + } + + async fn get(&self, resource: &str, key: &str) -> Result, Error> { + let state = self.state.lock().expect("the store is not poisoned"); + + Ok(state + .resources + .get(resource) + .and_then(|items| items.get(key)) + .cloned()) + } + + async fn create(&self, resource: &str, body: Value) -> Result<(String, Value), Error> { + // Minting and storing under one lock, so two creates cannot be handed the + // same key. A real backend would let the database mint it. + let mut state = self.state.lock().expect("the store is not poisoned"); + let number = state.next.entry(resource.to_string()).or_insert(0); + let key = format!("{resource}-{number}"); + *number += 1; + + state + .resources + .entry(resource.to_string()) + .or_default() + .insert(key.clone(), body.clone()); + + Ok((key, body)) + } + + async fn replace(&self, resource: &str, key: &str, body: Value) -> Result { + self.seed(resource, key, body.clone()); + + Ok(body) + } + + async fn patch(&self, resource: &str, key: &str, body: Value) -> Result { + let current = self + .get(resource, key) + .await? + .ok_or_else(|| Error::not_found(resource, key))?; + let merged = merge(¤t, &body); + self.seed(resource, key, merged.clone()); + + Ok(merged) + } + + async fn delete(&self, resource: &str, key: &str) -> Result<(), Error> { + let mut state = self.state.lock().expect("the store is not poisoned"); + let removed = state + .resources + .get_mut(resource) + .and_then(|items| items.remove(key)) + .is_some(); + + if removed { + return Ok(()); + } + + Err(Error::not_found(resource, key)) + } + + async fn find( + &self, + resource: &str, + path: &str, + value: &Value, + ) -> Result, Error> { + let state = self.state.lock().expect("the store is not poisoned"); + let Some(items) = state.resources.get(resource) else { + return Ok(None); + }; + + Ok(items + .values() + .find(|item| resolve_path(item, path) == Some(value)) + .cloned()) + } +} diff --git a/firestone/schema/server/rust/src/error.rs.jinja2 b/firestone/schema/server/rust/src/error.rs.jinja2 new file mode 100644 index 0000000..512b94b --- /dev/null +++ b/firestone/schema/server/rust/src/error.rs.jinja2 @@ -0,0 +1,105 @@ +//! What a handler can fail with, and how that reaches the caller. +//! +//! The generated server's typed responses only cover what the OpenAPI document +//! declares, so anything else - a resource that is not there, a backend that could +//! not answer - travels as the error type and is rendered by the [`ErrorHandler`] +//! implementation in `lib.rs`. + +use axum::http::header; +use axum::http::HeaderValue; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use axum::response::Response; +use serde_json::json; +use serde_json::Value; + +/// Anything a handler can fail with. +#[derive(Debug)] +pub enum Error { + /// No such resource. + NotFound { resource: String, key: String }, + /// The body did not survive the rules declared in the schema. + Validation(Value), + /// An operation firestone did not know how to answer. + NotImplemented(String), + /// This service's fault. + Internal(String), +} + +impl Error { + /// A resource that is not there. + pub fn not_found(resource: &str, key: &str) -> Self { + Error::NotFound { + resource: resource.to_string(), + key: key.to_string(), + } + } + + /// The status this failure asks for. + pub fn status(&self) -> StatusCode { + match self { + Error::NotFound { .. } => StatusCode::NOT_FOUND, + Error::Validation(problem) => problem + .get("status") + .and_then(Value::as_u64) + .and_then(|status| StatusCode::from_u16(status as u16).ok()) + .unwrap_or(StatusCode::UNPROCESSABLE_ENTITY), + Error::NotImplemented(_) => StatusCode::NOT_IMPLEMENTED, + Error::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + /// This failure as an RFC 9457 problem detail. + pub fn to_problem(&self) -> Value { + if let Error::Validation(problem) = self { + return problem.clone(); + } + + let (title, detail) = match self { + Error::NotFound { resource, key } => { + ("Not found", format!("No {resource} with key '{key}'.")) + } + Error::NotImplemented(detail) => ("Not implemented", detail.clone()), + Error::Internal(detail) => ("Internal server error", detail.clone()), + Error::Validation(_) => unreachable!("handled above"), + }; + + json!({ + "type": "about:blank", + "title": title, + "status": self.status().as_u16(), + "detail": detail, + }) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_problem()) + } +} + +impl std::error::Error for Error {} + +impl IntoResponse for Error { + fn into_response(self) -> Response { + let status = self.status(); + let body = self.to_problem(); + let content_type = ( + header::CONTENT_TYPE, + HeaderValue::from_static("application/problem+json"), + ); + + (status, [content_type], axum::Json(body)).into_response() + } +} + +/// Deserialize what the backend returned into whatever the response declares. +pub fn from_json(value: Value) -> Result { + serde_json::from_value(value).map_err(|err| Error::Internal(err.to_string())) +} + +/// Serialize a typed request body so the backend and the rules can work with it. +pub fn to_json(value: &T) -> Result { + serde_json::to_value(value).map_err(|err| Error::Internal(err.to_string())) +} diff --git a/firestone/schema/server/rust/src/handlers.rs.jinja2 b/firestone/schema/server/rust/src/handlers.rs.jinja2 new file mode 100644 index 0000000..51b5785 --- /dev/null +++ b/firestone/schema/server/rust/src/handlers.rs.jinja2 @@ -0,0 +1,197 @@ +//! The implementation of every trait the generated server declares. +//! +//! Generated by firestone from the same OpenAPI document as the server, so the two +//! cannot disagree about an operation id, a model name or a response variant. +//! Do not edit: change the resource files and regenerate. +//! +//! Each handler does the same three things - pull the request apart, enforce the +//! rules the schema declares, then hand off to the [`Backend`](crate::backend::Backend). +//! Only the last of those is yours to write. + +use async_trait::async_trait; +use axum_extra::extract::CookieJar; +use axum_extra::extract::Host; +use http::Method; +{%- if needs_value %} +use serde_json::Value; +{%- endif %} + +use {{ api_pkg }}::apis; +{%- if needs_models %} +use {{ api_pkg }}::models; +{%- endif %} + +{% if needs_from_json %}use crate::error::from_json; +{% endif %}{% if needs_to_json %}use crate::error::to_json; +{% endif %}use crate::error::Error; +use crate::Api; +{%- if validations %} +use crate::resolver::validate_request; +use crate::resolver::validate_subject; +{%- endif %} +{% for tag, info in traits.items() %} +#[async_trait] +impl apis::{{ info.module }}::{{ info.trait }} for Api { +{%- if info.claims %} + type Claims = crate::auth::Claims; +{%- endif %} +{% for op in info.operations %} + /// {{ op.method|upper }} {{ op.path }} + async fn {{ op.name }}( + &self, + _method: &Method, + _host: &Host, + _cookies: &CookieJar, +{%- if op.claims %} + _claims: &Self::Claims, +{%- endif %} +{%- if op.path_params %} + {% if op.uses_key %}path_params{% else %}_path_params{% endif %}: &models::{{ op.name|pascal }}PathParams, +{%- endif %} +{%- if op.query_params %} + _query_params: &models::{{ op.name|pascal }}QueryParams, +{%- endif %} +{%- if op.body %} + {% if op.uses_body %}body{% else %}_body{% endif %}: &{{ op.body }}, +{%- endif %} + ) -> Result { +{%- if op.implemented %} + use apis::{{ info.module }}::{{ op.response }} as Response; +{%- endif %} +{%- if not op.action %} + + // Firestone does not generate a body for this operation: the document + // declares no request body or no success response, or the method means + // something a replace would get wrong. Implement it by hand if you need it. + Err(Error::NotImplemented( + "{{ op.method|upper }} {{ op.path }} is not generated; implement it by hand".into(), + )) +{%- elif op.action == "head" %} + + Ok(Response::{{ op.success.variant }}) +{%- elif op.action == "list" %} + + let items = self.backend.list("{{ op.resource }}").await?; + + Ok(Response::{{ op.success.variant }}(from_json(Value::Array(items))?)) +{%- elif op.action == "create" %} + + let body = to_json(body)?; +{%- if op.rules %} + validate_request(self, "post", "{{ op.resource }}", Some(&body), None).await?; +{%- endif %} + let (key, {% if info.key_field %}mut {% endif %}created) = self.backend.create("{{ op.resource }}", body).await?; + tracing::info!(resource = "{{ op.resource }}", key, "created"); +{%- if info.key_field %} + + // The backend mints the key, so the caller is told what it was: without it + // there is no way to GET, PUT or DELETE what was just created. + if let Some(object) = created.as_object_mut() { + object.insert("{{ info.key_field }}".to_string(), Value::String(key)); + } +{%- endif %} + + Ok(Response::{{ op.success.variant }}(from_json(created)?)) +{%- elif op.action == "fetch" %} + + let key = &path_params.{{ op.key }}; + let found = self.backend.get("{{ op.resource }}", key).await?; + + Ok(Response::{{ op.success.variant }}(from_json( + found.ok_or_else(|| Error::not_found("{{ op.resource }}", key))?, + )?)) +{%- elif op.action == "update" %} + + let key = &path_params.{{ op.key }}; + let body = to_json(body)?; +{%- if op.rules %} + // Rules comparing against the current state need it, whether or not the + // backend would have. + let current = self.backend.get("{{ op.resource }}", key).await?; + validate_request(self, "{{ op.method }}", "{{ op.resource }}", Some(&body), current.as_ref()).await?; +{%- endif %} + let stored = self.backend.{% if op.method == "put" %}replace{% else %}patch{% endif %}("{{ op.resource }}", key, body).await?; + + Ok(Response::{{ op.success.variant }}(from_json(stored)?)) +{%- elif op.action == "remove" %} + + let key = &path_params.{{ op.key }}; + let current = self + .backend + .get("{{ op.resource }}", key) + .await? + .ok_or_else(|| Error::not_found("{{ op.resource }}", key))?; +{%- if op.rules %} + // A delete has no body, so the rules see the resource that is about to go. + validate_request(self, "delete", "{{ op.resource }}", None, Some(¤t)).await?; +{%- endif %} + self.backend.delete("{{ op.resource }}", key).await?; + tracing::info!(resource = "{{ op.resource }}", key, "deleted"); + + Ok(Response::{{ op.success.variant }}({% if op.success.payload %}from_json(current)?{% endif %})) +{%- elif op.action == "attr_get" %} + + let key = &path_params.{{ op.key }}; + let found = self + .backend + .get("{{ op.resource }}", key) + .await? + .ok_or_else(|| Error::not_found("{{ op.resource }}", key))?; + let value = found + .get("{{ op.attr }}") + .cloned() + .ok_or_else(|| Error::not_found("{{ op.resource }} {{ op.attr }}", key))?; + + Ok(Response::{{ op.success.variant }}(from_json(value)?)) +{%- elif op.action == "attr_set" %} + + let key = &path_params.{{ op.key }}; + let current = self + .backend + .get("{{ op.resource }}", key) + .await? + .ok_or_else(|| Error::not_found("{{ op.resource }}", key))?; + let value = to_json(body)?; + + let mut updated = current.clone(); + updated + .as_object_mut() + .ok_or_else(|| Error::Internal("the stored resource is not an object".into()))? + .insert("{{ op.attr }}".to_string(), value.clone()); +{%- if op.rules %} + // The rules are written against the whole resource, so they are given the + // resource as it will be rather than the attribute on its own. + validate_subject(self, "{{ op.method }}", "{{ op.resource }}", &updated, Some(¤t)).await?; +{%- endif %} + self.backend.replace("{{ op.resource }}", key, updated).await?; + + Ok(Response::{{ op.success.variant }}({% if op.success.payload %}from_json(value)?{% endif %})) +{%- elif op.action == "attr_remove" %} + + let key = &path_params.{{ op.key }}; + let current = self + .backend + .get("{{ op.resource }}", key) + .await? + .ok_or_else(|| Error::not_found("{{ op.resource }}", key))?; + let removed = current + .get("{{ op.attr }}") + .cloned() + .ok_or_else(|| Error::not_found("{{ op.resource }} {{ op.attr }}", key))?; + + let mut updated = current.clone(); + updated + .as_object_mut() + .ok_or_else(|| Error::Internal("the stored resource is not an object".into()))? + .remove("{{ op.attr }}"); +{%- if op.rules %} + validate_subject(self, "{{ op.method }}", "{{ op.resource }}", &updated, Some(¤t)).await?; +{%- endif %} + self.backend.replace("{{ op.resource }}", key, updated).await?; + + Ok(Response::{{ op.success.variant }}({% if op.success.payload %}from_json(removed)?{% endif %})) +{%- endif %} + } +{% endfor -%} +} +{% endfor %} diff --git a/firestone/schema/server/rust/src/lib.rs.jinja2 b/firestone/schema/server/rust/src/lib.rs.jinja2 new file mode 100644 index 0000000..659b247 --- /dev/null +++ b/firestone/schema/server/rust/src/lib.rs.jinja2 @@ -0,0 +1,93 @@ +//! {{ title }} +//! +//! {{ description }} +//! +//! The router, models, per-operation authentication and request validation are the +//! sibling `{{ api_pkg }}` crate, generated by openapi-generator from the OpenAPI +//! document firestone produced. This crate implements the traits that crate +//! declares, so the only thing left to write is a [`backend::Backend`]. + +{% if secured %}pub mod auth; +{% endif %}pub mod backend; +pub mod error; +pub mod handlers; +{%- if validations %} +pub mod resolver; +pub mod validation; +{%- endif %} + +use std::sync::Arc; + +pub use error::Error; + +/// The implementation the generated server is built around. +/// +/// Handed to `{{ api_pkg }}::server::new`, which routes every operation in the +/// OpenAPI document to one of its methods. +#[derive(Clone)] +pub struct Api { + /// Where the data lives. + pub backend: Arc, + /// The bearer tokens the secured operations accept. + pub tokens: Vec, +} + +impl Api { + /// Build the implementation around a backend, reading API_TOKENS for the + /// credentials the secured operations accept. + pub fn new(backend: Arc) -> Self { + let tokens = std::env::var("API_TOKENS").unwrap_or_default(); + + Api::with_tokens( + backend, + tokens + .split(',') + .map(str::trim) + .filter(|token| !token.is_empty()) + .map(str::to_string), + ) + } + + /// The same, with the tokens given rather than read from the environment, so a + /// test or an embedding binary can supply its own. + pub fn with_tokens( + backend: Arc, + tokens: impl IntoIterator, + ) -> Self { + Api { + backend, + tokens: tokens.into_iter().collect(), + } + } +} + +impl AsRef for Api { + fn as_ref(&self) -> &Api { + self + } +} + +/// Render anything the typed responses cannot as an RFC 9457 problem document. +/// +/// The generated server calls this for an error a handler returned, which is how a +/// 404, or a rule the OpenAPI document did not declare a response for, still +/// reaches the caller with a sensible status. +#[async_trait::async_trait] +impl {{ api_pkg }}::apis::ErrorHandler for Api { + async fn handle_error( + &self, + _method: &::http::Method, + _host: &axum_extra::extract::Host, + _cookies: &axum_extra::extract::CookieJar, + error: Error, + ) -> Result { + tracing::info!("{error}"); + + Ok(axum::response::IntoResponse::into_response(error)) + } +} + +/// Build the router for every operation in the OpenAPI document. +pub fn router(api: Api) -> axum::Router { + {{ api_pkg }}::server::new(api) +} diff --git a/firestone/schema/server/rust/src/main.rs.jinja2 b/firestone/schema/server/rust/src/main.rs.jinja2 new file mode 100644 index 0000000..eba5877 --- /dev/null +++ b/firestone/schema/server/rust/src/main.rs.jinja2 @@ -0,0 +1,65 @@ +//! {{ title }} +//! +//! The thin part: choose a backend, build the router, serve it. Anything you want +//! to add - your own routes, your own middleware - goes on the router here, and +//! `{{ pkg }}::router` hands back an ordinary `axum::Router` so all of axum is +//! available. See . + +use std::net::SocketAddr; +use std::sync::Arc; + +use {{ pkg }}::backend::InMemory; +use {{ pkg }}::router; +use {{ pkg }}::Api; +use tokio::net::TcpListener; +use tokio::signal; +use tower_http::trace::TraceLayer; + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "info,tower_http=debug".into()), + ) + .init(); + + let api = Api::new(Arc::new(InMemory::new())); + let app = router(api).layer(TraceLayer::new_for_http()); + + let bind: SocketAddr = std::env::var("BIND") + .unwrap_or_else(|_| "127.0.0.1:8080".to_string()) + .parse()?; + let listener = TcpListener::bind(bind).await?; + tracing::info!("{{ title_fmt }} v{{ version }} listening on http://{bind}"); + + axum::serve(listener, app) + .with_graceful_shutdown(shutdown()) + .await?; + + Ok(()) +} + +/// Stop serving on Ctrl-C or SIGTERM, letting in-flight requests finish. +async fn shutdown() { + let interrupt = async { + signal::ctrl_c() + .await + .expect("the interrupt handler installs"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("the terminate handler installs") + .recv() + .await; + }; + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + () = interrupt => tracing::info!("interrupted, shutting down"), + () = terminate => tracing::info!("terminated, shutting down"), + } +} diff --git a/firestone/schema/server/rust/src/resolver.rs.jinja2 b/firestone/schema/server/rust/src/resolver.rs.jinja2 new file mode 100644 index 0000000..8b6e41b --- /dev/null +++ b/firestone/schema/server/rust/src/resolver.rs.jinja2 @@ -0,0 +1,109 @@ +//! Connecting the generated validation rules to the backend. +//! +//! The rules say *what* has to exist; the [`Backend`](crate::backend::Backend) is +//! the only thing that knows how to look it up. This is the adapter, and it is +//! generated because both halves are. + +use async_trait::async_trait; +use serde_json::Value; + +use crate::error::Error; +use crate::validation::validate; +use crate::validation::Error as ValidationFailure; +use crate::validation::RefRequest; +use crate::validation::Request as ValidationRequest; +use crate::validation::Resolved; +use crate::validation::Resolver; +use crate::validation::ResolverError; +use crate::Api; + +/// Answers a rule's lookups out of the backend. +pub struct BackendResolver<'a> { + api: &'a Api, +} + +#[async_trait] +impl Resolver for BackendResolver<'_> { + async fn resolve( + &mut self, + requests: &[RefRequest], + _ctx: &Value, + ) -> Result { + let mut resolved = Resolved::new(); + for request in requests { + // 'key' is a path into the resource being looked up, not a column, so it + // goes to the backend as one. + if let Some(found) = self + .api + .backend + .find(&request.kind, &request.key, &request.value) + .await? + { + resolved.insert(request.id(), found); + } + } + + Ok(resolved) + } +} + +/// Run every rule that applies to this request. +pub async fn validate_request( + api: &Api, + op: &str, + resource: &str, + body: Option<&Value>, + old: Option<&Value>, +) -> Result<(), Error> { + let mut request = ValidationRequest::new(op, resource); + if let Some(body) = body { + request = request.body(body); + } + if let Some(old) = old { + request = request.old(old); + } + + run(api, request, op, resource).await +} + +/// Run the rules against a resource you have already worked out. +/// +/// An attribute endpoint changes one field, and the rules are written against the +/// whole resource, so they are handed the resource as it will be rather than being +/// asked to merge a fragment they would not understand. +pub async fn validate_subject( + api: &Api, + op: &str, + resource: &str, + subject: &Value, + old: Option<&Value>, +) -> Result<(), Error> { + let mut request = ValidationRequest::new(op, resource).subject(subject); + if let Some(old) = old { + request = request.old(old); + } + + run(api, request, op, resource).await +} + +async fn run( + api: &Api, + request: ValidationRequest<'_>, + op: &str, + resource: &str, +) -> Result<(), Error> { + let mut resolver = BackendResolver { api }; + match validate(request, Some(&mut resolver)).await { + Ok(()) => Ok(()), + Err(ValidationFailure::Validation(failed)) => { + tracing::info!( + resource, + op, + "rejected by {} rule(s)", + failed.violations.len() + ); + Err(Error::Validation(failed.to_problem())) + } + Err(other) => Err(Error::Internal(other.to_string())), + } +} diff --git a/firestone/schema/server/rust/tests/api.rs.jinja2 b/firestone/schema/server/rust/tests/api.rs.jinja2 new file mode 100644 index 0000000..04a81e3 --- /dev/null +++ b/firestone/schema/server/rust/tests/api.rs.jinja2 @@ -0,0 +1,169 @@ +//! Tests for the implementation firestone generated. +//! +//! Generated from the same OpenAPI document as the handlers, so a template change +//! that compiles but misbehaves fails here rather than in production. The router is +//! driven directly with `oneshot`, so nothing binds a port. + +use std::sync::Arc; + +use axum::body::Body; +use axum::http::Request; +use axum::http::StatusCode; +use http_body_util::BodyExt; +{% if needs_json %}use serde_json::json; +{% endif %}use serde_json::Value; +use tower::ServiceExt; + +use {{ pkg }}::backend::InMemory; +use {{ pkg }}::router; +use {{ pkg }}::Api; + +const TOKEN: &str = "test-token"; + +/// A server with nothing stored and one accepted token. +fn server() -> (axum::Router, Arc) { + let backend = Arc::new(InMemory::new()); + + ( + router(Api::with_tokens(backend.clone(), [TOKEN.to_string()])), + backend, + ) +} + +async fn call( + app: axum::Router, + method: &str, + uri: &str, + scheme: Option<&str>, + body: Option, +) -> (StatusCode, Value) { + let mut request = Request::builder() + .method(method) + .uri(uri) + // The generated handlers extract Host, so a request without one is + // rejected before it reaches them. + .header("host", "localhost"); + if let Some(scheme) = scheme { + request = request.header("authorization", format!("{scheme} {TOKEN}")); + } + let request = match body { + Some(body) => request + .header("content-type", "application/json") + .body(Body::from(body.to_string())), + None => request.body(Body::empty()), + } + .expect("the request builds"); + + let response = app.oneshot(request).await.expect("the router answers"); + let status = response.status(); + let bytes = response + .into_body() + .collect() + .await + .expect("the body reads") + .to_bytes(); + + (status, serde_json::from_slice(&bytes).unwrap_or(Value::Null)) +} +{% for tag, info in traits.items() %} +{%- set create = info.operations | selectattr("action", "equalto", "create") | list | first %} +{%- set fetch = info.operations | selectattr("action", "equalto", "fetch") | list | first %} +{%- if create and fetch and info.key_field %} +/// A created {{ tag }} comes back carrying the key it was given, and that key works. +/// +/// Without it the caller has no way to GET, PUT or DELETE what it just made. +#[tokio::test] +async fn {{ info.module }}_create_returns_a_usable_key() { + let (app, _backend) = server(); + + let body: Value = + serde_json::from_str({{ info.sample_body | tojson }}).expect("the sample body parses"); + let (status, created) = call(app.clone(), "POST", "{{ create.path }}", Some("Bearer"), Some(body)).await; + assert_eq!(status, StatusCode::CREATED, "create answered {created}"); + + let key = created + .get("{{ info.key_field }}") + .and_then(Value::as_str) + .expect("the created resource carries its key") + .to_string(); + + let (status, _) = call( + app, + "GET", + &format!("{{ create.path }}/{}", key), + Some("Bearer"), + None, + ) + .await; + assert_eq!(status, StatusCode::OK, "the key a create returned should fetch"); +} +{% endif %} +{%- endfor %} +{%- set secured_ops = [] %} +{%- for tag, info in traits.items() %}{% for op in info.operations %} +{%- if op.claims and op.action %}{% set _ = secured_ops.append(op) %}{% endif %} +{%- endfor %}{% endfor %} +{%- if secured_ops %} +/// A secured operation refuses a request with no credentials. +#[tokio::test] +async fn a_secured_operation_needs_a_token() { +{%- for op in secured_ops %} + let (app, _) = server(); + let (status, _) = call(app, "{{ op.method|upper }}", "{{ op.test_path }}", None, Some(json!({}))).await; + assert_eq!( + status, + StatusCode::UNAUTHORIZED, + "{{ op.method|upper }} {{ op.test_path }} should need a token" + ); +{%- endfor %} +} + +/// The auth-scheme name is case insensitive, as RFC 7235 has it. +#[tokio::test] +async fn a_lowercase_scheme_is_accepted() { + let (app, _) = server(); + let (status, _) = call( + app, + "{{ secured_ops[0].method|upper }}", + "{{ secured_ops[0].test_path }}", + Some("bearer"), + Some(json!({})), + ) + .await; + + assert_ne!(status, StatusCode::UNAUTHORIZED, "'bearer' is as valid as 'Bearer'"); +} + +/// A token that is not accepted is refused. +#[tokio::test] +async fn a_wrong_token_is_refused() { + let (app, _) = server(); + let request = Request::builder() + .method("{{ secured_ops[0].method|upper }}") + .uri("{{ secured_ops[0].test_path }}") + .header("host", "localhost") + .header("authorization", "Bearer not-the-token") + .header("content-type", "application/json") + .body(Body::from("{}")) + .expect("the request builds"); + let status = app.oneshot(request).await.expect("answers").status(); + + assert_eq!(status, StatusCode::UNAUTHORIZED); +} +{%- endif %} +{%- set fetches = [] %} +{%- for tag, info in traits.items() %}{% for op in info.operations %} +{%- if op.action == "fetch" and not op.claims %}{% set _ = fetches.append(op) %}{% endif %} +{%- endfor %}{% endfor %} +{%- if fetches %} +/// A resource that is not there is a 404, served as an RFC 9457 problem document. +#[tokio::test] +async fn a_missing_resource_is_a_problem_document() { + let (app, _) = server(); + let (status, body) = call(app, "GET", "{{ fetches[0].test_path }}", None, None).await; + + assert_eq!(status, StatusCode::NOT_FOUND); + assert_eq!(body["status"], 404); + assert_eq!(body["title"], "Not found"); +} +{%- endif %} diff --git a/firestone/spec/__init__.py b/firestone/spec/__init__.py index 0864904..d646ae8 100644 --- a/firestone/spec/__init__.py +++ b/firestone/spec/__init__.py @@ -6,7 +6,16 @@ from firestone.spec import cli from firestone.spec import cli_rust from firestone.spec import openapi +from firestone.spec import server_rust from firestone.spec import streamlit from firestone.spec import validations -__all__ = ["asyncapi", "cli", "cli_rust", "openapi", "streamlit", "validations"] +__all__ = [ + "asyncapi", + "cli", + "cli_rust", + "openapi", + "server_rust", + "streamlit", + "validations", +] diff --git a/firestone/spec/_base.py b/firestone/spec/_base.py index 14a23c3..13ac363 100644 --- a/firestone/spec/_base.py +++ b/firestone/spec/_base.py @@ -30,6 +30,14 @@ def yaml_pretty(data, indent=2): JINJA_ENV.filters["yaml_pretty"] = yaml_pretty +def pascal(name: str) -> str: + """Convert a name to PascalCase, keeping inner capitals.""" + return "".join(part[0].upper() + part[1:] for part in str(name).split("_") if part) + + +JINJA_ENV.filters["pascal"] = pascal + + def to_singular(name: str) -> str: """Convert a plural resource name to its singular form. diff --git a/firestone/spec/openapi.py b/firestone/spec/openapi.py index 4714be2..824d687 100644 --- a/firestone/spec/openapi.py +++ b/firestone/spec/openapi.py @@ -50,7 +50,10 @@ def get_responses( ): """Set schema for a given operation type.""" if method == "head": - return {"default": {"description": "Default HEAD response"}} + # A real status rather than 'default': a HEAD answers 200 with no body, and + # a generator reading 'default' has no code to put on the response. rust-axum + # emits status 0 for it, which is not a status a response can carry. + return {http.client.OK.value: {"description": "Default HEAD response"}} resp_code_enum = http.client.OK if method == "post": @@ -545,7 +548,10 @@ def add_rsrc_components( if required: del components["schemas"][comp_name]["required"] - rscr_methods = methods.get("resource", []) + # add_resource_methods emits every collection method when methods.resource is + # absent, so the components have to be worked out the same way or the document + # ends up referencing a Create/Update schema it never defines. + rscr_methods = methods.get("resource") or RSRC_HTTP_METHODS rscr_inst_methods = methods.get("instance", []) _LOGGER.debug(f"rscr_methods: {rscr_methods}") _LOGGER.debug(f"rscr_inst_methods: {rscr_inst_methods}") diff --git a/firestone/spec/server_rust.py b/firestone/spec/server_rust.py new file mode 100644 index 0000000..c56a7b3 --- /dev/null +++ b/firestone/spec/server_rust.py @@ -0,0 +1,504 @@ +""" +Generate the glue between a rust-axum server and your storage. + +The server itself is not firestone's to write: ``openapi-generator -g rust-axum`` +already turns the OpenAPI document firestone produces into a router, typed models, +per-operation authentication and request validation. What it leaves behind is a +set of traits with one required method per operation, and for a real API that is +dozens of methods that have to exist before anything compiles. + +This generates those: every trait method, wired to a ``Backend`` trait that is the +one thing firestone cannot know, plus the adapter that lets the validation rules +reach the same backend. Everything here is derived from the same OpenAPI document +the server was generated from, so the two cannot disagree about a name. +""" + +import json +import logging +import re + +import yaml + +from firestone.spec import _base as spec_base +from firestone.spec import openapi as spec_openapi +from firestone.spec import validations as spec_validations + +_LOGGER = logging.getLogger(__name__) + +#: JSON Schema types mapped the way openapi-generator's rust-axum maps them. +TYPE_TO_RUST = { + "string": "String", + "integer": "i32", + "number": "f64", + "boolean": "bool", +} + +#: Formats that change the rust type. An int64 that came through as i32 would not +#: satisfy the trait the generated server declares. +FORMAT_TO_RUST = { + ("integer", "int32"): "i32", + ("integer", "int64"): "i64", + ("number", "float"): "f32", + ("number", "double"): "f64", +} + + +def rust_type(schema: dict) -> str: + """Get the rust type openapi-generator would choose for a scalar schema.""" + kind = schema.get("type") + fmt = schema.get("format") + + return FORMAT_TO_RUST.get((kind, fmt)) or TYPE_TO_RUST.get(kind) + + +#: The methods that carry a body, and so can be validated. +MUTATING = ["post", "put", "patch", "delete"] + + +#: Convert a name the way openapi-generator does, keeping inner capitals, so +#: ``create_postal_code`` becomes ``CreatePostalCode`` and ``OK`` stays ``OK``. +pascal = spec_base.pascal + + +def variant(code, description: str) -> str: + """Get the response enum variant openapi-generator generates for a response.""" + status = 0 if code == "default" else code + words = "".join(word[0].upper() + word[1:] for word in str(description).split() if word) + + return f"Status{status}_{words}" + + +def _body_type(operation: dict) -> str: + """Get the rust type of an operation's request body, if it has one.""" + body = operation.get("requestBody") + if not body: + return None + + schema = body.get("content", {}).get(spec_base.DEFAULT_CONTENT_TYPE, {}).get("schema", {}) + if "$ref" in schema: + return f"models::{pascal(schema['$ref'].rsplit('/', 1)[-1])}" + + if schema.get("type") == "array": + items = schema.get("items", {}) + if "$ref" in items: + return f"Vec" + + return f"Vec<{rust_type(items) or 'serde_json::Value'}>" + + # A type firestone does not map falls back to Value, which will not match what + # openapi-generator chose and so fails to compile rather than misbehaving. That + # is the right way round: firestone controls the schema this was built from. + return rust_type(schema) or "serde_json::Value" + + +def _success(operation: dict) -> dict: + """Get the response a handler returns when nothing went wrong.""" + for code in sorted(operation.get("responses", {}), key=str): + if str(code) in ("200", "201") or code == "default": + response = operation["responses"][code] + + return { + "variant": variant(code, response.get("description", "")), + "payload": bool(response.get("content")), + } + + return None + + +#: What a handler does, by path shape and method. An action absent from this table +#: is one firestone will not guess at: a POST to an embedded collection appends and +#: a PATCH merges, and neither is a replace. +_ACTIONS = { + ("collection", "get"): "list", + ("collection", "post"): "create", + ("instance", "get"): "fetch", + ("instance", "put"): "update", + ("instance", "patch"): "update", + ("instance", "delete"): "remove", + ("attribute", "get"): "attr_get", + ("attribute", "put"): "attr_set", + ("attribute", "delete"): "attr_remove", +} + +#: The actions that cannot run without a request body to apply. +_NEEDS_BODY = ["create", "update", "attr_set"] + + +def _action(shape: str, method: str, body: str, success: dict) -> str: + """Get what a handler for this operation should do, or None if firestone cannot say. + + The template dispatches on this rather than re-deriving it, so the set of + operations firestone claims to implement cannot drift from the set it actually + writes a body for. + """ + if not success: + return None + if method == "head": + return "head" + + action = _ACTIONS.get((shape, method)) + if action in _NEEDS_BODY and not body: + return None + + return action + + +#: The actions whose handler deserializes what the backend returned. +_READS = ["list", "create", "fetch", "update", "attr_get"] + +#: The actions whose handler serializes the request body. +_WRITES = _NEEDS_BODY + + +#: A value of each JSON Schema type, for the body the generated tests post. +_SAMPLES = {"string": "x", "integer": 1, "number": 1.5, "boolean": True, "array": []} + + +def _model(spec: dict, body_type: str) -> tuple: + """Resolve a request model to its properties and its required fields. + + Create models are ``allOf[base, {required: [...]}]``, so both halves matter. + """ + if not body_type or not body_type.startswith("models::"): + return {}, [] + + schemas = spec.get("components", {}).get("schemas", {}) + wanted = body_type.split("::", 1)[1] + model = next((schemas[name] for name in schemas if pascal(name) == wanted), None) + if not model: + return {}, [] + + required = [] + base = {} + for part in model.get("allOf", [model]): + required.extend(part.get("required", [])) + if "$ref" in part: + ref = part["$ref"].rsplit("/", 1)[-1] + base = schemas.get(ref, {}).get("properties", {}) + else: + base.update(part.get("properties", {})) + + return base, required + + +def _sample_body(spec: dict, body_type: str) -> dict: + """Build a body satisfying the required fields of a create's request model. + + The generated tests have to post something the request validation accepts, and + what that is comes from the document rather than from guesswork. + """ + base, required = _model(spec, body_type) + + body = {} + for name in required: + prop = base.get(name, {}) + if prop.get("enum"): + body[name] = prop["enum"][0] + else: + body[name] = _SAMPLES.get(prop.get("type", "string"), "x") + + return body + + +def _shape(path: str) -> dict: + """Work out what a path is, so a handler knows which backend call it maps to.""" + parts = [part for part in path.strip("/").split("/") if part] + keys = [part for part in parts if part.startswith("{")] + trailing = parts[-1] + + if not keys: + return {"kind": "collection", "attr": None} + if trailing.startswith("{"): + return {"kind": "instance", "attr": None} + + # A path segment after the key is an attribute of the resource, which the + # OpenAPI document exposes in its own right. + return {"kind": "attribute", "attr": trailing} + + +def _returnable_key(spec: dict, ops: list) -> str: + """Get the key attribute a create can hand back, if the model declares one. + + The response goes through the typed model, and serde drops a field the struct + does not have, so a key the item schema never declared cannot come back. A + resource that wants its key in responses declares it as a property, as the + addressbook example does. + """ + key = next((op["key"] for op in sorted(ops, key=lambda o: o["name"]) if op["key"]), None) + if not key: + return None + + create = next((op for op in ops if op["action"] == "create"), None) + properties, _ = _model(spec, create["body"] if create else None) + + return key if key in properties else None + + +def operations(spec: dict, ruleset: dict) -> dict: + """Describe every operation the generated server expects an implementation of. + + :param dict spec: the OpenAPI document, as generated by firestone + :param dict ruleset: the validation rules, so a handler knows what to enforce + :return: the operations, grouped by the trait that declares them + :rtype: dict + """ + traits = {} + + for path in spec.get("paths", {}): + for method in spec["paths"][path]: + operation = spec["paths"][path][method] + if not isinstance(operation, dict) or "operationId" not in operation: + continue + + tag = (operation.get("tags") or ["default"])[0] + params = operation.get("parameters") or [] + shape = _shape(path) + path_params = [param for param in params if param.get("in") == "path"] + + success = _success(operation) + body = _body_type(operation) + action = _action(shape["kind"], method, body, success) + traits.setdefault(tag, []).append( + { + "name": operation["operationId"], + "response": f"{pascal(operation['operationId'])}Response", + "method": method, + "path": path, + # The same path with its parameters filled in, so a generated + # test can ask for it without any string surgery of its own. + "test_path": re.sub(r"\{[^}]+\}", "test-key", path), + "resource": tag, + "shape": shape["kind"], + "attr": shape["attr"], + "key": path_params[-1]["name"] if path_params else None, + "claims": bool(operation.get("security")), + "path_params": bool(path_params), + "query_params": any(param.get("in") == "query" for param in params), + "body": body, + "success": success, + "rules": tag in ruleset and method in MUTATING, + # Whether firestone knows how to answer this operation. One it + # does not, such as a mutating operation the document declares + # no body for, is left explicit rather than guessed at. + "action": action, + "implemented": bool(action), + # Whether the generated body actually reads the key, so the + # argument is not bound to a name nothing uses. + "uses_key": bool(path_params) + and action not in (None, "head", "list", "create"), + # Whether the generated body reads the request body, so the + # argument is not bound to a name nothing uses. + "uses_body": bool(body) and action in _WRITES, + } + ) + + return { + tag: { + "module": tag, + "trait": pascal(tag), + # The attribute the resource is keyed by, taken from its instance path. + # A create returns the body it was given, so the key the backend minted + # has to be put into it before it goes back. + # A body the request validation will accept, for the generated tests. + "sample_body": json.dumps( + _sample_body( + spec, + next( + ( + op["body"] + for op in sorted(traits[tag], key=lambda o: o["name"]) + if op["action"] == "create" + ), + None, + ), + ) + ), + "key_field": _returnable_key(spec, traits[tag]), + # Only a trait with a secured operation declares the associated Claims + # type, so implementing it unconditionally does not compile. + "claims": any(op["claims"] for op in traits[tag]), + "operations": sorted(traits[tag], key=lambda op: op["name"]), + } + for tag in sorted(traits) + } + + +def _doc(text: str) -> str: + """Flatten text for a doc comment, where a newline would end the comment.""" + return " ".join(str(text).split()) + + +def _rust_format(text: str) -> str: + """Escape text for the inside of a rust format string. + + Quotes and backslashes have to be escaped as in any rust literal, and braces + doubled, or a title containing one is read as a format argument. + """ + escaped = json.dumps(str(text))[1:-1] + + return escaped.replace("{", "{{").replace("}", "}}") + + +def _context(spec: dict, ruleset: dict, names: dict) -> dict: + """Build what the templates are rendered with. + + :param dict spec: the OpenAPI document the server was generated from + :param dict ruleset: the rules to enforce, empty when they are turned off + :param dict names: the title, description, version and crate names + """ + traits = operations(spec, ruleset) + every = [op for trait in traits.values() for op in trait["operations"]] + + return { + **names, + "crate_version": crate_version(names["version"]), + "traits": traits, + "validations": bool(ruleset), + # An operation is only secured if the schema said so, and the generated + # server only declares ApiAuthBasic when something is, so generating an + # implementation of it unconditionally does not compile. + "secured": any(op["claims"] for op in every), + # Imported only where the handlers reach for them: an unused import is a + # warning, and the gate treats warnings as errors. + "needs_value": any(op["action"] == "list" for op in every) + or any( + trait["key_field"] and op["action"] == "create" + for trait in traits.values() + for op in trait["operations"] + ), + # models is reached for by the path and query parameter types in the + # signature as well as by a request body, so a schema with an instance path + # and no body still needs it. + "needs_models": any(op["body"] or op["path_params"] or op["query_params"] for op in every), + "needs_to_json": any(op["action"] in _WRITES for op in every), + # Whether there is anything to test. A resource with, say, only a create has + # no round trip to check and no credentials to refuse, and a test file of + # nothing but unused helpers does not compile. + # json! is only used by the create round trip and the secured-operation + # checks, so a file carrying neither must not import it. + "needs_json": any( + trait["key_field"] + and any(op["action"] == "create" for op in trait["operations"]) + and any(op["action"] == "fetch" for op in trait["operations"]) + for trait in traits.values() + ) + or any(op["claims"] and op["action"] for op in every), + "has_tests": any( + trait["key_field"] + and any(op["action"] == "create" for op in trait["operations"]) + and any(op["action"] == "fetch" for op in trait["operations"]) + for trait in traits.values() + ) + or any(op["claims"] and op["action"] for op in every) + or any(op["action"] == "fetch" and not op["claims"] for op in every), + "needs_from_json": any( + op["action"] in _READS + or (op["action"] in ("remove", "attr_set", "attr_remove") and op["success"]["payload"]) + for op in every + ), + } + + +def generate( + rsrc_data: list, + title: str, + desc: str, + version: str, + pkg: str = "api_server", + api_pkg: str = "openapi", + ruleset: dict = None, + with_validations: bool = True, +) -> dict: + """Generate the implementation crate for a rust-axum server. + + :param list rsrc_data: the resource data, as loaded from the resource files + :param str title: the title of the API + :param str desc: the description of the API + :param str version: the version of the API + :param str pkg: the name of the generated crate + :param str api_pkg: the crate name openapi-generator was given + :param dict ruleset: the validation rules, as returned by + :func:`firestone.spec.validations.extract` + :param bool with_validations: enforce those rules in the generated handlers + :return: a mapping of file name to file contents + :rtype: dict + """ + ruleset = (ruleset or {}) if with_validations else {} + + # Derived from the document the server was generated from, rather than from the + # resources directly, so firestone and openapi-generator cannot disagree about + # an operation id, a model name or a response variant. + spec = yaml.safe_load(spec_openapi.generate(rsrc_data, title, desc, desc, version)) + context = _context( + spec, + ruleset, + { + # User text, landing in a TOML string, a rust format string and a doc + # comment, so each gets a form that cannot break its surroundings. + "title": _doc(title), + "title_toml": json.dumps(title), + "title_fmt": _rust_format(title), + "description": _doc(desc), + "description_toml": json.dumps(desc), + "version": version, + "pkg": pkg, + "api_pkg": api_pkg, + }, + ) + _LOGGER.info( + f"Generating an implementation of {len(context['traits'])} trait(s) " + f"for {context['pkg']}" + ) + + files = { + name: spec_base.JINJA_ENV.get_template(f"server/rust/{name}.jinja2").render(**context) + for name in TEMPLATES + if (name != "src/auth.rs" or context["secured"]) + and (name != "tests/api.rs" or context["has_tests"]) + } + + if context["validations"]: + tmpl = spec_base.JINJA_ENV.get_template("server/rust/src/resolver.rs.jinja2") + files["src/resolver.rs"] = tmpl.render(**context) + + for name, content in spec_validations.generate( + ruleset, language=spec_validations.LANG_RUST + ).items(): + files[f"src/validation/{name}"] = content + + return {name: content.rstrip("\n") + "\n" for name, content in files.items()} + + +def crate_version(version: str) -> str: + """Get a semver crate version from the API version. + + An API version is often something like ``1.0``, which cargo will not take, so + it is padded out rather than rejected. + """ + parts = [part.lstrip("vV") for part in str(version).split(".")] + parts = [part for part in parts if part.isdigit()] + if not parts: + return "0.1.0" + + return ".".join((parts + ["0", "0"])[:3]) + + +#: Regenerated every time, because they follow the OpenAPI document exactly. +#: +#: lib.rs is here because the modules it declares depend on the document: turning +#: on security or validations adds auth.rs and resolver.rs, and a kept lib.rs would +#: not declare them, leaving a crate that does not compile. +GENERATED = ["src/handlers.rs", "src/lib.rs", "tests/api.rs"] + +#: Written once, then yours. Firestone will not overwrite these unless asked, so +#: the Backend you implement and the tokens you accept survive a regeneration. +SCAFFOLD = [ + "Cargo.toml", + "src/main.rs", + "src/error.rs", + "src/backend.rs", + "src/auth.rs", +] + +#: Every template, in the order they are rendered. +TEMPLATES = GENERATED + SCAFFOLD diff --git a/test/rust/verify.sh b/test/rust/verify.sh index 5b5798e..d3f3676 100755 --- a/test/rust/verify.sh +++ b/test/rust/verify.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# Type-check the generated rust validation package against a real toolchain. +# Type-check firestone's generated rust against a real toolchain. # # The python tests can only inspect the generated text; nothing there catches a # template that stops compiling. This regenerates into a throwaway crate and runs @@ -9,12 +9,25 @@ # 1. a ruleset with CEL expressions, built with the cel feature on # 2. a references-only ruleset, built with --no-default-features # -# It then runs the committed example crate, which adds hand written behaviour tests. +# It then runs the committed example crates, which add hand written tests, and the +# generated axum server. # -# Run it directly, or via `make verify-validations-rust`. +# Run it directly, or via `make verify-rust`. set -euo pipefail FIRESTONE="${FIRESTONE:-firestone}" + +# brew installs it as openapi-generator, the npm wrapper as openapi-generator-cli, +# so take whichever is on PATH rather than assuming one of them. +OPENAPI_GEN="${OPENAPI_GEN:-}" +if [ -z "${OPENAPI_GEN}" ]; then + for candidate in openapi-generator openapi-generator-cli; do + if command -v "${candidate}" >/dev/null 2>&1; then + OPENAPI_GEN="${candidate}" + break + fi + done +fi WORKDIR="$(mktemp -d)" trap 'rm -rf "${WORKDIR}"' EXIT @@ -105,4 +118,114 @@ cargo test --manifest-path "${REPO}/examples/addressbook/validation-rs/Cargo.tom cargo clippy --manifest-path "${REPO}/examples/addressbook/validation-rs/Cargo.toml" \ --all-targets --all-features -- -D warnings -echo "==> generated rust validations type check clean" +# The server is openapi-generator's; this is the implementation firestone writes for +# the traits it declares. Regenerated here so a template that stops compiling, or a +# name the two no longer agree on, fails. +echo "==> generated axum server implementation" +if [ -n "${OPENAPI_GEN}" ]; then + SERVER="${WORKDIR}/server" + mkdir -p "${SERVER}" + "${FIRESTONE}" generate \ + --title 'Server type check' --description 'Server type check' \ + --resources "${RESOURCES}" --version 1.0 \ + openapi > "${WORKDIR}/openapi.yaml" + "${OPENAPI_GEN}" generate -i "${WORKDIR}/openapi.yaml" -g rust-axum \ + -o "${SERVER}/api" --skip-validate-spec \ + -p packageName=type_check_api,packageVersion=1.0.0 >/dev/null + "${FIRESTONE}" generate \ + --title 'Server type check' --description 'Server type check' \ + --resources "${RESOURCES}" --version 1.0 \ + server --pkg type_check_server --api-pkg type_check_api \ + --output-dir "${SERVER}/app" + cat > "${SERVER}/Cargo.toml" <<'TOML' +[workspace] +members = ["api", "app"] +resolver = "2" +TOML + cargo clippy --manifest-path "${SERVER}/Cargo.toml" --all-targets -- -D warnings + cargo test --manifest-path "${SERVER}/Cargo.toml" + + # A schema whose operation set differs from the addressbook's exercises other + # branches of the handlers template, and other import sets. The example alone + # would let a two-line schema ship broken. + # Every shape declares methods.resource explicitly, because leaving it out + # defaults to all of them and so always produces a POST body: that hid a + # missing models import for any schema with path params and no body. + for shape in "resource: [post]" \ + "resource: [get]" \ + "resource: [get, delete]" \ + "resource: [get] + instance: [get]" \ + "resource: [get] + instance: [get, delete]" \ + "resource: [get] + instance_attrs: [get]" \ + "resource: [head] + instance: [head]" \ + "resource: [delete] + instance: [get]" \ + "instance: [get, delete]" \ + "instance: [head]" \ + "instance_attrs: [get, put, delete]"; do + # Printed so a failure in CI names the shape that produced it. + echo " shape: methods.${shape}" | tr '\n' ' ' + echo + cat > "${WORKDIR}/shape.yaml" < "${WORKDIR}/shape.json" + "${OPENAPI_GEN}" generate -i "${WORKDIR}/shape.json" -g rust-axum \ + -o "${WORKDIR}/shape/api" --skip-validate-spec \ + -p packageName=shape_api,packageVersion=1.0.0 >/dev/null + "${FIRESTONE}" generate --title S --description S --version 1.0 \ + --resources "${WORKDIR}/shape.yaml" \ + server --pkg shape_app --api-pkg shape_api \ + --output-dir "${WORKDIR}/shape/app" + cat > "${WORKDIR}/shape/Cargo.toml" <<'TOML' +[workspace] +members = ["api", "app"] +resolver = "2" +TOML + cargo clippy --manifest-path "${WORKDIR}/shape/Cargo.toml" \ + --all-targets -- -D warnings + # Not only clippy: a generated test that panics is a generator bug too. + cargo test --manifest-path "${WORKDIR}/shape/Cargo.toml" + done +elif [ -n "${CI:-}" ]; then + # CI installs it, so missing here means the step that does was dropped and this + # whole leg would quietly stop running. + echo "no openapi-generator on PATH, and this is CI" >&2 + exit 1 +else + echo " skipped: openapi-generator is not installed" +fi + +echo "==> committed example server" +cargo clippy --manifest-path "${REPO}/examples/addressbook/server-rs/Cargo.toml" \ + --all-targets --all-features -- -D warnings +cargo test --manifest-path "${REPO}/examples/addressbook/server-rs/Cargo.toml" +cargo fmt --manifest-path "${REPO}/examples/addressbook/server-rs/Cargo.toml" --all --check + +echo "==> committed example validation crate is formatted" +cargo fmt --manifest-path "${REPO}/examples/addressbook/validation-rs/Cargo.toml" --check + +echo "==> generated rust type check clean" diff --git a/test/spec/test_cli.py b/test/spec/test_cli.py new file mode 100644 index 0000000..c4eff6d --- /dev/null +++ b/test/spec/test_cli.py @@ -0,0 +1,80 @@ +# pylint: disable=duplicate-code +""" +Test the firestone.spec.cli module. +""" + +import ast +import unittest + +from firestone.spec import cli + + +def _resource() -> dict: + """Get a resource with a boolean property, which needs a click flag pair.""" + return { + "kind": "things", + "apiVersion": "v1", + "schema": { + "type": "array", + "key": {"name": "thing_key", "schema": {"type": "string"}}, + "items": { + "type": "object", + "properties": { + "name": {"type": "string", "description": "The name"}, + "is_valid": {"type": "boolean", "description": "Valid or not"}, + }, + }, + }, + "methods": {"resource": ["get", "post"], "instance": ["get", "put", "delete"]}, + } + + +class TestCliBooleans(unittest.TestCase): + """Test how the generated python CLI declares a boolean option.""" + + def test_single_file_output_parses(self): + """The generated CLI has to be importable, which a bad option breaks. + + A flag pair written without its '/' becomes one option named + --is-valid--no-is-valid, which click turns into an 'is_valid__no_is_valid' + parameter, and the handler then rejects at runtime. + """ + generated = cli.generate( + "pkg", "pkg.client", [_resource()], "Title", "Desc", "Summary", "1.0", False + ) + + ast.parse(generated) + + def test_flag_pair_is_declared(self): + """A boolean is a --x/--no-x pair, so it can be turned off as well as on.""" + generated = cli.generate( + "pkg", "pkg.client", [_resource()], "Title", "Desc", "Summary", "1.0", False + ) + + self.assertIn('"--is-valid/--no-is-valid"', generated) + self.assertNotIn('"--is-valid--no-is-valid"', generated) + + def test_modules_output_parses(self): + """The per-module output has to be importable too.""" + modules = cli.generate( + "pkg", "pkg.client", [_resource()], "Title", "Desc", "Summary", "1.0", True + ) + + for name, content in modules.items(): + ast.parse(content, filename=f"{name}.py") + + def test_both_outputs_agree(self): + """The single file and the modules declare the option the same way.""" + single = cli.generate( + "pkg", "pkg.client", [_resource()], "Title", "Desc", "Summary", "1.0", False + ) + modules = cli.generate( + "pkg", "pkg.client", [_resource()], "Title", "Desc", "Summary", "1.0", True + ) + + self.assertIn('"--is-valid/--no-is-valid"', single) + self.assertIn('"--is-valid/--no-is-valid"', modules["things"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/spec/test_cli_rust.py b/test/spec/test_cli_rust.py index b0499b2..6784937 100644 --- a/test/spec/test_cli_rust.py +++ b/test/spec/test_cli_rust.py @@ -1,3 +1,4 @@ +# pylint: disable=too-many-lines # pylint: disable=protected-access """ Test the firestone.spec.cli_rust module. @@ -551,6 +552,59 @@ def test_generation_creates_command_enum(self): self.assertIn("pub enum FooCommands", rust_code) +class TestCliRustAuth(unittest.TestCase): + """Test how the generated rust CLI presents its credentials.""" + + @staticmethod + def _generate() -> dict: + """Generate a CLI for a resource behind a bearer scheme.""" + rsrc_data = [ + { + "kind": "foo", + "apiVersion": "v1", + "security": { + "scheme": {"bearer_auth": {"type": "http", "scheme": "bearer"}}, + "resource": ["post"], + }, + "schema": { + "type": "array", + "key": {"name": "foo_key", "schema": {"type": "string"}}, + "items": {"type": "object", "properties": {"name": {"type": "string"}}}, + }, + "methods": {"resource": ["get", "post"], "instance": ["get"]}, + } + ] + + return cli_rust.generate( + "test_pkg", + "test_pkg", + rsrc_data, + "Test", + "Test CLI", + "Test", + "1.0", + True, + ) + + def test_bearer_token_is_set(self): + """The token reaches the field the generated client actually reads. + + A 'bearer' scheme is sent from configuration.bearer_access_token; the + api_key field is only consulted for an 'apiKey' scheme, so setting that + alone means --api-key is accepted and then silently dropped. + """ + module = self._generate()["foo"] + + self.assertIn("config.bearer_access_token = Some(key.clone());", module) + + def test_api_key_is_also_set(self): + """An apiKey scheme is covered too, since the schema decides which is used.""" + module = self._generate()["foo"] + + self.assertIn("config.api_key = Some(", module) + self.assertIn("prefix: None", module) + + class TestCliRustHelpers(unittest.TestCase): """Test helper functions in firestone.spec.cli_rust.""" diff --git a/test/spec/test_openapi.py b/test/spec/test_openapi.py index 3cf00ed..142d133 100644 --- a/test/spec/test_openapi.py +++ b/test/spec/test_openapi.py @@ -28,6 +28,24 @@ def test_head(self): ) self.assertIsNotNone(responses) + def test_head_declares_a_real_status(self): + """A HEAD answers 200, not 'default'. + + 'default' is a fallback shape rather than a status, so a server generator + has no code to put on the response: rust-axum resolves it to status 0, which + no response can carry, and every HEAD answers 500. + """ + responses = openapi.get_responses("head", None, None) + + self.assertNotIn("default", responses) + self.assertIn(http.client.OK.value, responses) + + def test_head_carries_no_content(self): + """A HEAD response has no body to describe.""" + responses = openapi.get_responses("head", None, None) + + self.assertNotIn("content", responses[http.client.OK.value]) + def test_get_comp_name(self): """Test firestone.spec.openapi.get_responses() 'get' with comp name.""" responses = openapi.get_responses( diff --git a/test/spec/test_server_rust.py b/test/spec/test_server_rust.py new file mode 100644 index 0000000..b99ad13 --- /dev/null +++ b/test/spec/test_server_rust.py @@ -0,0 +1,466 @@ +# pylint: disable=duplicate-code +""" +Test the firestone.spec.server_rust module. +""" + +import unittest + +import yaml + +from firestone.spec import openapi +from firestone.spec import server_rust +from firestone.spec import validations + + +def _resource(kind: str = "addressbook", **extra) -> dict: + """Get a minimal resource, with anything extra merged over it.""" + rsrc = { + "kind": kind, + "apiVersion": "v1", + "metadata": {"description": f"An example {kind}"}, + "methods": {"resource": ["get", "post"], "instance": ["get", "put", "delete"]}, + "schema": { + "type": "array", + "key": {"name": f"{kind}_key", "schema": {"type": "string"}}, + "items": { + "type": "object", + "properties": {"city": {"type": "string"}, "is_valid": {"type": "boolean"}}, + "required": ["city"], + }, + }, + } + rsrc.update(extra) + + return rsrc + + +def _spec(rsrc_data: list) -> dict: + """Get the OpenAPI document the server would be generated from.""" + return yaml.safe_load(openapi.generate(rsrc_data, "Title", "Desc", "Sum", "1.0")) + + +def _generate(rsrc_data: list, **kwargs) -> dict: + """Generate an implementation crate for the given resources.""" + return server_rust.generate(rsrc_data, "Title", "Desc", "1.0", **kwargs) + + +class TestNaming(unittest.TestCase): + """Test that firestone derives the names openapi-generator generates.""" + + def test_pascal_keeps_inner_capitals(self): + """CreatePostal_code is the component name, CreatePostalCode is the struct.""" + self.assertEqual(server_rust.pascal("create_postal_code"), "CreatePostalCode") + self.assertEqual(server_rust.pascal("CreatePostal_code"), "CreatePostalCode") + self.assertEqual(server_rust.pascal("persons_uuid_get"), "PersonsUuidGet") + + def test_variant_from_a_description(self): + """A response variant is Status_, capitals preserved.""" + self.assertEqual(server_rust.variant(200, "Response for OK"), "Status200_ResponseForOK") + self.assertEqual( + server_rust.variant(422, "One or more validation rules failed"), + "Status422_OneOrMoreValidationRulesFailed", + ) + + def test_default_is_status_zero(self): + """openapi-generator numbers a 'default' response 0.""" + self.assertEqual( + server_rust.variant("default", "Default HEAD response"), "Status0_DefaultHEADResponse" + ) + + def test_crate_version_is_padded(self): + """An API version cargo would refuse is padded rather than rejected.""" + self.assertEqual(server_rust.crate_version("1.0"), "1.0.0") + self.assertEqual(server_rust.crate_version("1.2.3"), "1.2.3") + self.assertEqual(server_rust.crate_version("v-next"), "0.1.0") + + +class TestOperations(unittest.TestCase): + """Test firestone.spec.server_rust.operations()""" + + def test_every_operation_is_described(self): + """Nothing the document declares is left without an implementation.""" + spec = _spec([_resource()]) + declared = sum( + 1 + for path in spec["paths"] + for method in spec["paths"][path] + if isinstance(spec["paths"][path][method], dict) + ) + described = sum( + len(trait["operations"]) for trait in server_rust.operations(spec, {}).values() + ) + + self.assertEqual(described, declared) + + def test_grouped_by_tag(self): + """Operations are grouped the way openapi-generator groups its traits.""" + traits = server_rust.operations(_spec([_resource()]), {}) + + self.assertEqual(list(traits), ["addressbook"]) + self.assertEqual(traits["addressbook"]["trait"], "Addressbook") + + def test_shapes(self): + """A path is a collection, an instance or one attribute of an instance.""" + traits = server_rust.operations(_spec([_resource()]), {}) + shapes = {op["name"]: op["shape"] for op in traits["addressbook"]["operations"]} + + self.assertEqual(shapes["addressbook_get"], "collection") + self.assertEqual(shapes["addressbook_addressbook_key_get"], "instance") + + def test_claims_follow_security(self): + """Only an operation the schema secured carries claims.""" + rsrc = _resource( + security={"scheme": {"bearer_auth": {"type": "http"}}, "resource": ["post"]} + ) + traits = server_rust.operations(_spec([rsrc]), {}) + claims = {op["name"]: op["claims"] for op in traits["addressbook"]["operations"]} + + self.assertTrue(claims["addressbook_post"]) + self.assertFalse(claims["addressbook_get"]) + + def test_trait_declares_claims_only_when_needed(self): + """A trait with no secured operation has no associated Claims type.""" + plain = server_rust.operations(_spec([_resource()]), {}) + secured = server_rust.operations( + _spec( + [ + _resource( + security={ + "scheme": {"bearer_auth": {"type": "http"}}, + "resource": ["post"], + } + ) + ] + ), + {}, + ) + + self.assertFalse(plain["addressbook"]["claims"]) + self.assertTrue(secured["addressbook"]["claims"]) + + def test_body_types(self): + """A body is the model openapi-generator names, or a plain rust type.""" + traits = server_rust.operations(_spec([_resource()]), {}) + bodies = {op["name"]: op["body"] for op in traits["addressbook"]["operations"]} + + self.assertEqual(bodies["addressbook_post"], "models::CreateAddressbook") + self.assertIsNone(bodies["addressbook_get"]) + + +class TestActions(unittest.TestCase): + """Test that firestone only claims the operations it writes a body for.""" + + def test_action_table(self): + """One place decides what a handler does, so the template cannot drift.""" + cases = [ + (("collection", "get", None, True), "list"), + (("collection", "post", "models::X", True), "create"), + (("collection", "post", None, True), None), + (("collection", "delete", None, True), None), + (("collection", "patch", None, True), None), + (("instance", "get", None, True), "fetch"), + (("instance", "put", "models::X", True), "update"), + (("instance", "patch", "models::X", True), "update"), + (("instance", "patch", None, True), None), + (("instance", "delete", None, True), "remove"), + (("attribute", "get", None, True), "attr_get"), + (("attribute", "put", "String", True), "attr_set"), + (("attribute", "post", "String", True), None), + (("attribute", "patch", "String", True), None), + (("attribute", "delete", None, True), "attr_remove"), + (("instance", "head", None, True), "head"), + (("instance", "get", None, None), None), + ] + for (shape, method, body, success), expected in cases: + # pylint: disable=protected-access + actual = server_rust._action( + shape, method, body, {"payload": True} if success else None + ) + self.assertEqual(actual, expected, f"{shape} {method} body={body}") + + def test_attribute_post_and_patch_are_not_a_replace(self): + """A POST appends and a PATCH merges; neither is silently a PUT.""" + # pylint: disable=protected-access + success = {"payload": True} + self.assertEqual(server_rust._action("attribute", "put", "String", success), "attr_set") + self.assertIsNone(server_rust._action("attribute", "post", "String", success)) + self.assertIsNone(server_rust._action("attribute", "patch", "String", success)) + + def test_unimplemented_operations_answer_501(self): + """An operation firestone does not write a body for says so.""" + rsrc = _resource(methods={"resource": ["get", "delete"]}) + handlers = _generate([rsrc])["src/handlers.rs"] + + self.assertIn("Error::NotImplemented(", handlers) + self.assertIn("is not generated; implement it by hand", handlers) + + def test_arguments_nothing_reads_are_bound_to_underscore(self): + """An unused argument is a warning, and the gate treats warnings as errors.""" + rsrc = _resource(methods={"instance": ["head"]}) + handlers = _generate([rsrc])["src/handlers.rs"] + + self.assertIn("_path_params: &models::", handlers) + self.assertNotIn("\n path_params: &models::", handlers) + + +class TestImports(unittest.TestCase): + """Test that handlers.rs imports only what its operations reach for.""" + + @staticmethod + def _imports(rsrc: dict) -> list: + return [ + line + for line in _generate([rsrc])["src/handlers.rs"].split("\n") + if line.startswith("use ") + ] + + def test_post_only_needs_no_value(self): + """serde_json::Value is only reached for by a collection list.""" + imports = self._imports(_resource(methods={"resource": ["post"]})) + + self.assertNotIn("use serde_json::Value;", imports) + + def test_get_only_needs_no_models_or_to_json(self): + """Neither is reached for when nothing carries a body.""" + imports = self._imports(_resource(methods={"resource": ["get"]})) + + self.assertNotIn("use openapi::models;", imports) + self.assertNotIn("use crate::error::to_json;", imports) + + def test_a_full_resource_needs_all_of_them(self): + """And they are imported where they are used.""" + imports = self._imports( + _resource(methods={"resource": ["get", "post"], "instance": ["get", "put"]}) + ) + + self.assertIn("use serde_json::Value;", imports) + self.assertIn("use openapi::models;", imports) + self.assertIn("use crate::error::to_json;", imports) + self.assertIn("use crate::error::from_json;", imports) + + def test_auth_is_only_generated_when_something_is_secured(self): + """The generated server only declares ApiAuthBasic when the schema secures + something, so implementing it unconditionally does not compile.""" + plain = _generate([_resource()]) + secured = _generate( + [ + _resource( + security={"scheme": {"bearer_auth": {"type": "http"}}, "resource": ["post"]} + ) + ] + ) + + self.assertNotIn("src/auth.rs", plain) + self.assertNotIn("pub mod auth;", plain["src/lib.rs"]) + self.assertIn("src/auth.rs", secured) + self.assertIn("pub mod auth;", secured["src/lib.rs"]) + self.assertIn("subtle", secured["Cargo.toml"]) + + +class TestTypes(unittest.TestCase): + """Test that firestone names the same rust types openapi-generator does.""" + + def test_format_decides_the_integer_width(self): + """An int64 sent as i32 would not satisfy the trait the server declares.""" + self.assertEqual(server_rust.rust_type({"type": "integer"}), "i32") + self.assertEqual(server_rust.rust_type({"type": "integer", "format": "int64"}), "i64") + self.assertEqual(server_rust.rust_type({"type": "number", "format": "float"}), "f32") + + def test_an_unmapped_type_has_no_rust_name(self): + """Falling back is the caller's decision, not this function's.""" + self.assertIsNone(server_rust.rust_type({"type": "who-knows"})) + + +class TestMetadata(unittest.TestCase): + """Test that a title or description cannot break what it is written into.""" + + @staticmethod + def _generate(title: str, desc: str) -> dict: + return server_rust.generate([_resource()], title, desc, "1.0") + + def test_a_quote_does_not_break_the_manifest(self): + """Cargo.toml is TOML, so the description is a TOML string.""" + files = self._generate("T", 'Line "one" two') + + self.assertIn('description = "Line \\"one\\" two"', files["Cargo.toml"]) + + def test_a_brace_does_not_break_the_format_string(self): + """The title goes inside a rust format string, where a brace is an argument.""" + files = self._generate("A {brace} title", "D") + + self.assertIn("A {{brace}} title", files["src/main.rs"]) + + def test_a_quote_does_not_break_the_format_string(self): + """And a quote would end the literal.""" + files = self._generate('A "quoted" title', "D") + + self.assertIn('A \\"quoted\\" title', files["src/main.rs"]) + + def test_a_newline_does_not_break_a_doc_comment(self): + """A doc comment ends at the newline, so the text is flattened.""" + files = self._generate("T", "Line one\nline two") + + self.assertIn("//! Line one line two", files["src/lib.rs"]) + + +class TestOwnership(unittest.TestCase): + """Test which files firestone owns and which are yours once they exist.""" + + def test_only_what_follows_the_document_is_regenerated(self): + """Everything else is scaffolded once, so your Backend survives.""" + self.assertEqual( + sorted(server_rust.GENERATED), + ["src/handlers.rs", "src/lib.rs", "tests/api.rs"], + ) + for name in ("src/backend.rs", "src/auth.rs", "src/main.rs", "Cargo.toml"): + self.assertIn(name, server_rust.SCAFFOLD) + + # lib.rs declares the modules, which depend on whether the schema secures + # anything: keeping it would leave a crate that does not compile. + self.assertNotIn("src/lib.rs", server_rust.SCAFFOLD) + + def test_every_template_is_one_or_the_other(self): + """A file that is neither would be written with no rule about it.""" + self.assertEqual( + sorted(server_rust.TEMPLATES), + sorted(server_rust.GENERATED + server_rust.SCAFFOLD), + ) + + +class TestGeneratedTests(unittest.TestCase): + """Test the test suite firestone generates alongside the implementation.""" + + def test_a_create_is_checked_for_its_key(self): + """The response of a create has to carry the key it was given.""" + rsrc = _resource() + rsrc["schema"]["items"]["properties"]["addressbook_key"] = {"type": "string"} + tests = _generate([rsrc])["tests/api.rs"] + + self.assertIn("addressbook_create_returns_a_usable_key", tests) + self.assertIn('.get("addressbook_key")', tests) + + def test_no_key_test_when_the_model_does_not_declare_one(self): + """serde drops a field the struct does not have, so the key cannot come back. + + Claiming it would generate a test that panics on every such schema. + """ + tests = _generate([_resource()])["tests/api.rs"] + + self.assertNotIn("create_returns_a_usable_key", tests) + + def test_no_seed_of_another_schemas_resources(self): + """A seed for the addressbook's persons is meaningless in another project.""" + rsrc = _resource() + rsrc["schema"]["items"]["properties"]["addressbook_key"] = {"type": "string"} + tests = _generate([rsrc])["tests/api.rs"] + + self.assertNotIn('seed("persons"', tests) + + def test_paths_are_filled_in(self): + """A test asks for a concrete path, not one with a parameter in it.""" + tests = _generate([_resource()])["tests/api.rs"] + + self.assertNotIn("{addressbook_key}", tests) + self.assertIn("/addressbook/test-key", tests) + + def test_the_sample_body_satisfies_the_model(self): + """A create test has to post something the request validation accepts.""" + rsrc = _resource() + rsrc["schema"]["items"]["properties"]["kind_of"] = { + "type": "string", + "enum": ["one", "two"], + } + rsrc["schema"]["items"]["required"] = ["city", "kind_of"] + rsrc["schema"]["items"]["properties"]["addressbook_key"] = {"type": "string"} + tests = _generate([rsrc])["tests/api.rs"] + + self.assertIn('\\"city\\": \\"x\\"', tests) + self.assertIn('\\"kind_of\\": \\"one\\"', tests) + + def test_auth_tests_only_when_something_is_secured(self): + """There is nothing to check when the schema secures nothing.""" + plain = _generate([_resource()])["tests/api.rs"] + secured = _generate( + [ + _resource( + security={"scheme": {"bearer_auth": {"type": "http"}}, "resource": ["post"]} + ) + ] + )["tests/api.rs"] + + self.assertNotIn("a_secured_operation_needs_a_token", plain) + self.assertIn("a_secured_operation_needs_a_token", secured) + self.assertIn("a_lowercase_scheme_is_accepted", secured) + + +class TestGenerate(unittest.TestCase): + """Test firestone.spec.server_rust.generate()""" + + def test_files(self): + """The crate that implements the generated server's traits.""" + files = _generate([_resource()]) + + for name in ( + "Cargo.toml", + "src/main.rs", + "src/lib.rs", + "src/error.rs", + "src/backend.rs", + "src/handlers.rs", + ): + self.assertIn(name, files) + + # auth.rs only when the schema secures something; see TestImports. + self.assertNotIn("src/auth.rs", files) + + def test_it_depends_on_the_generated_server(self): + """The implementation is a sibling crate of the openapi-generator output.""" + files = _generate([_resource()], api_pkg="addressbook_api") + + self.assertIn('addressbook_api = { path = "../api" }', files["Cargo.toml"]) + self.assertIn("use addressbook_api::apis;", files["src/handlers.rs"]) + + def test_every_operation_is_implemented(self): + """One method per operation, or the server does not compile.""" + spec = _spec([_resource()]) + files = _generate([_resource()]) + for path in spec["paths"]: + for method in spec["paths"][path]: + operation = spec["paths"][path][method] + if isinstance(operation, dict) and "operationId" in operation: + self.assertIn(f"async fn {operation['operationId']}(", files["src/handlers.rs"]) + + def test_no_validation_module_without_rules(self): + """A project with no rules gets no validation module and no CEL dependency.""" + files = _generate([_resource()]) + + self.assertNotIn("src/validation/mod.rs", files) + self.assertNotIn("src/resolver.rs", files) + self.assertNotIn("cel-interpreter", files["Cargo.toml"]) + + def test_validation_is_wired_in(self): + """A project with rules gets the package, the resolver and the handler calls.""" + rsrc = _resource(validations={"rules": [{"name": "r", "expr": "true"}]}) + ruleset = validations.extract([rsrc]) + files = _generate(validations.strip([rsrc]), ruleset=ruleset) + + self.assertIn("src/validation/mod.rs", files) + self.assertIn("src/resolver.rs", files) + self.assertIn("validate_request(self,", files["src/handlers.rs"]) + + def test_validation_can_be_turned_off(self): + """The rules can be left out even when the resources declare them.""" + rsrc = _resource(validations={"rules": [{"name": "r", "expr": "true"}]}) + ruleset = validations.extract([rsrc]) + files = _generate(validations.strip([rsrc]), ruleset=ruleset, with_validations=False) + + self.assertNotIn("src/validation/mod.rs", files) + self.assertNotIn("validate_request", files["src/handlers.rs"]) + + def test_generation_is_idempotent(self): + """Regenerating without changing a schema changes nothing.""" + self.assertEqual(_generate([_resource()]), _generate([_resource()])) + + +if __name__ == "__main__": + unittest.main()