diff options
| -rw-r--r-- | README.md | 6 | ||||
| -rw-r--r-- | TODO | 22 | ||||
| -rw-r--r-- | fatcat-openapi2.yml | 198 | ||||
| -rw-r--r-- | notes/database_dumps_backups.txt | 53 | ||||
| -rw-r--r-- | notes/rust_libraries.txt (renamed from rust/NOTES.txt) | 0 | ||||
| -rw-r--r-- | python/fatcat/manifest_importer.py | 4 | ||||
| -rw-r--r-- | rust/HACKING.md | 58 | ||||
| -rw-r--r-- | rust/INSTALL.md | 36 | ||||
| -rw-r--r-- | rust/README.md | 85 | ||||
| -rw-r--r-- | rust/TODO | 9 | ||||
| -rw-r--r-- | rust/fatcat-api/README.md | 2 | ||||
| -rw-r--r-- | rust/fatcat-api/api.yaml | 198 | ||||
| -rw-r--r-- | rust/fatcat-api/api/swagger.yaml | 620 | ||||
| -rw-r--r-- | rust/fatcat-api/examples/client.rs | 68 | ||||
| -rw-r--r-- | rust/fatcat-api/examples/server_lib/server.rs | 109 | ||||
| -rw-r--r-- | rust/fatcat-api/src/client.rs | 683 | ||||
| -rw-r--r-- | rust/fatcat-api/src/lib.rs | 206 | ||||
| -rw-r--r-- | rust/fatcat-api/src/mimetypes.rs | 186 | ||||
| -rw-r--r-- | rust/fatcat-api/src/server.rs | 1083 | ||||
| -rw-r--r-- | rust/src/api_helpers.rs | 32 | ||||
| -rw-r--r-- | rust/src/api_server.rs | 775 | ||||
| -rw-r--r-- | rust/src/api_wrappers.rs | 113 | ||||
| -rw-r--r-- | rust/src/database_entity_crud.rs | 900 | ||||
| -rw-r--r-- | rust/src/database_models.rs | 102 | ||||
| -rw-r--r-- | rust/src/lib.rs | 5 | ||||
| -rw-r--r-- | rust/tests/test_api_server.rs | 65 | 
26 files changed, 4808 insertions, 810 deletions
| @@ -31,7 +31,7 @@ released, while the API server and web interface are strong copyleft (AGPLv3).  ## Status  - HTTP API -    - [ ] base32 encoding of UUID identifiers +    - [x] base32 encoding of UUID identifiers      - [x] inverse many-to-many helpers (files-by-release, release-by-creator)  - SQL Schema      - [x] Basic entities @@ -53,7 +53,9 @@ released, while the API server and web interface are strong copyleft (AGPLv3).  ## Identifiers -Fatcat entity identifiers are in "boring"  +Fatcat entity identifiers are 128-bit UUIDs encoded in base32 format. Revision +ids are also UUIDs, and encoded in normal UUID fashion, to disambiguate from +edity identifiers.  Python helpers for conversion: @@ -1,6 +1,7 @@  ## Next Up +- some significant slow-down has happened? transactions, or regexes?  summer roadmap:  - PUT/UPDATE, DELETE, and merge code paths  - faster UPDATE-free bulk import code path @@ -19,12 +20,28 @@ importers:  - manifest: multiple URLs per SHA1  - pubmed (medline), if not in CORE      => and/or, use pubmed ID lookups on crossref import +- core  - semantic scholar (up to 39 million; author de-dupe) +- wikidata (if they have a dump) +- crossref: relations ("is-preprint-of") +- crossref: filter works +    => content-type whitelist +    => title length and title/slug blacklist +    => at least one author (?) +    => make this a method on Release object +    => or just set release_stub as "stub"?  bugs:  - test: release pointing to a collection that has been deleted/redirected    => UI crash? +july roadmap: +- complete and test this round of schema changes +- container import (extra?): lang, region, subject +- re-run imports +- basic API+webface creation, editing, merging, editgroup approval +- elastic schema/transform for releases; bulk and continuous scripts +  ## Schema / Alignment / Scope  - "container" -> "venue"? @@ -55,6 +72,11 @@ name ref: https://www.w3.org/International/questions/qa-personal-names  - batch inserts automerge: create editgroup and changelog, mark all edits as    accepted, all in a single transaction +## API + +- hydrate entities in API +    ? "expand" query param +  ## Other  - basic python hbase/elastic matcher diff --git a/fatcat-openapi2.yml b/fatcat-openapi2.yml index ea17e982..a8919216 100644 --- a/fatcat-openapi2.yml +++ b/fatcat-openapi2.yml @@ -473,19 +473,47 @@ paths:          in: path          type: string          required: true -      - name: expand -        in: query -        type: string -        required: false -        description: "List of sub-entities to expand in response. For now, only 'all' accepted."      get:        operationId: "get_container" +      parameters: +        - name: expand +          in: query +          type: string +          required: false +          description: "List of sub-entities to expand in response. For now, only 'all' accepted."        responses:          200:            description: Found Entity            schema:              $ref: "#/definitions/container_entity"          <<: *ENTITYRESPONSES +    put: +      operationId: "update_container" +      parameters: +        - name: entity +          in: body +          required: true +          schema: +            $ref: "#/definitions/container_entity" +      responses: +        200: +          description: Updated Entity +          schema: +            $ref: "#/definitions/entity_edit" +        <<: *ENTITYRESPONSES +    delete: +      operationId: "delete_container" +      parameters: +        - name: editgroup +          in: query +          required: false +          type: string +      responses: +        200: +          description: Deleted Entity +          schema: +            $ref: "#/definitions/entity_edit" +        <<: *ENTITYRESPONSES    /container/{id}/history:      parameters:        - name: id @@ -571,19 +599,47 @@ paths:          in: path          type: string          required: true -      - name: expand -        in: query -        type: string -        required: false -        description: "List of sub-entities to expand in response. For now, only 'all' accepted."      get:        operationId: "get_creator" +      parameters: +        - name: expand +          in: query +          type: string +          required: false +          description: "List of sub-entities to expand in response. For now, only 'all' accepted."        responses:          200:            description: Found Entity            schema:              $ref: "#/definitions/creator_entity"          <<: *ENTITYRESPONSES +    put: +      operationId: "update_creator" +      parameters: +        - name: entity +          in: body +          required: true +          schema: +            $ref: "#/definitions/creator_entity" +      responses: +        200: +          description: Updated Entity +          schema: +            $ref: "#/definitions/entity_edit" +        <<: *ENTITYRESPONSES +    delete: +      operationId: "delete_creator" +      parameters: +        - name: editgroup +          in: query +          required: false +          type: string +      responses: +        200: +          description: Deleted Entity +          schema: +            $ref: "#/definitions/entity_edit" +        <<: *ENTITYRESPONSES    /creator/{id}/history:      parameters:        - name: id @@ -685,19 +741,47 @@ paths:          in: path          type: string          required: true -      - name: expand -        in: query -        type: string -        required: false -        description: "List of sub-entities to expand in response. For now, only 'all' accepted."      get:        operationId: "get_file" +      parameters: +        - name: expand +          in: query +          type: string +          required: false +          description: "List of sub-entities to expand in response. For now, only 'all' accepted."        responses:          200:            description: Found Entity            schema:              $ref: "#/definitions/file_entity"          <<: *ENTITYRESPONSES +    put: +      operationId: "update_file" +      parameters: +        - name: entity +          in: body +          required: true +          schema: +            $ref: "#/definitions/file_entity" +      responses: +        200: +          description: Updated Entity +          schema: +            $ref: "#/definitions/entity_edit" +        <<: *ENTITYRESPONSES +    delete: +      operationId: "delete_file" +      parameters: +        - name: editgroup +          in: query +          required: false +          type: string +      responses: +        200: +          description: Deleted Entity +          schema: +            $ref: "#/definitions/entity_edit" +        <<: *ENTITYRESPONSES    /file/{id}/history:      parameters:        - name: id @@ -783,19 +867,47 @@ paths:          in: path          type: string          required: true -      - name: expand -        in: query -        type: string -        required: false -        description: "List of sub-entities to expand in response. For now, only 'all' accepted."      get:        operationId: "get_release" +      parameters: +        - name: expand +          in: query +          type: string +          required: false +          description: "List of sub-entities to expand in response. For now, only 'all' accepted."        responses:          200:            description: Found Entity            schema:              $ref: "#/definitions/release_entity"          <<: *ENTITYRESPONSES +    put: +      operationId: "update_release" +      parameters: +        - name: entity +          in: body +          required: true +          schema: +            $ref: "#/definitions/release_entity" +      responses: +        200: +          description: Updated Entity +          schema: +            $ref: "#/definitions/entity_edit" +        <<: *ENTITYRESPONSES +    delete: +      operationId: "delete_release" +      parameters: +        - name: editgroup +          in: query +          required: false +          type: string +      responses: +        200: +          description: Deleted Entity +          schema: +            $ref: "#/definitions/entity_edit" +        <<: *ENTITYRESPONSES    /release/{id}/history:      parameters:        - name: id @@ -897,19 +1009,47 @@ paths:          in: path          type: string          required: true -      - name: expand -        in: query -        type: string -        required: false -        description: "List of sub-entities to expand in response. For now, only 'all' accepted."      get:        operationId: "get_work" +      parameters: +        - name: expand +          in: query +          type: string +          required: false +          description: "List of sub-entities to expand in response. For now, only 'all' accepted."        responses:          200:            description: Found Entity            schema:              $ref: "#/definitions/work_entity"          <<: *ENTITYRESPONSES +    put: +      operationId: "update_work" +      parameters: +        - name: entity +          in: body +          required: true +          schema: +            $ref: "#/definitions/work_entity" +      responses: +        200: +          description: Updated Entity +          schema: +            $ref: "#/definitions/entity_edit" +        <<: *ENTITYRESPONSES +    delete: +      operationId: "delete_work" +      parameters: +        - name: editgroup +          in: query +          required: false +          type: string +      responses: +        200: +          description: Deleted Entity +          schema: +            $ref: "#/definitions/entity_edit" +        <<: *ENTITYRESPONSES    /work/{id}/history:      parameters:        - name: id @@ -1055,10 +1195,18 @@ paths:            description: Unmergable            schema:              $ref: "#/definitions/error_response" +        400: +          description: Bad Request +          schema: +            $ref: "#/definitions/error_response"          404:            description: Not Found            schema:              $ref: "#/definitions/error_response" +        409: +          description: Edit Conflict +          schema: +            $ref: "#/definitions/error_response"          500:            description: Generic Error            schema: diff --git a/notes/database_dumps_backups.txt b/notes/database_dumps_backups.txt new file mode 100644 index 00000000..60d4bba0 --- /dev/null +++ b/notes/database_dumps_backups.txt @@ -0,0 +1,53 @@ + +## Dumps and Backups + +There are a few different database dump formats folks might want: + +- raw native database backups, for disaster recovery (would include +  volatile/unsupported schema details, user API credentials, full history, +  in-process edits, comments, etc) +- a sanitized version of the above: roughly per-table dumps of the full state +  of the database. Could use per-table SQL expressions with sub-queries to pull +  in small tables ("partial transform") and export JSON for each table; would +  be extra work to maintain, so not pursuing for now. +- full history, full public schema exports, in a form that might be used to +  mirror or enitrely fork the project. Propose supplying the full "changelog" +  in API schema format, in a single file to capture all entity history, without +  "hydrating" any inter-entity references. Rely on separate dumps of +  non-entity, non-versioned tables (editors, abstracts, etc). Note that a +  variant of this could use the public interface, in particular to do +  incremental updates (though that wouldn't capture schema changes). +- transformed exports of the current state of the database (aka, without +  history). Useful for data analysis, search engines, etc. Propose supplying +  just the Release table in a fully "hydrated" state to start. Unclear if +  should be on a work or release basis; will go with release for now. Harder to +  do using public interface because of the need for transaction locking. + +## Full Postgres Backup + +Backing up the entire database using `pg_dump`, with parallelism 1 (use more on +larger machine with fast disks; try 4 or 8?), assuming the database name is +'fatcat', and the current user has access: + +    pg_dump -j1 -Fd -f test-dump fatcat + +## Identifier Dumps + +The `extras/quick_dump.sql` script will dump abstracts and identifiers as TSV +files to `/tmp/`. Pretty quick; takes about 15 GB of disk space (uncompressed). + +## Releases Export + +    # simple command +    ./fatcat_export.py releases /tmp/fatcat_ident_releases.tsv /tmp/releases-dump.json + +    # usual command +    time ./fatcat_export.py releases /tmp/fatcat_ident_releases.tsv - | pv -l | wc + +## Changelog Export + +    # simple command +    ./fatcat_export.py changelog /tmp/changelog-dump.json + +    # usual command +    time ./fatcat_export.py changelog - | pv -l | wc diff --git a/rust/NOTES.txt b/notes/rust_libraries.txt index 7e6f33eb..7e6f33eb 100644 --- a/rust/NOTES.txt +++ b/notes/rust_libraries.txt diff --git a/python/fatcat/manifest_importer.py b/python/fatcat/manifest_importer.py index 7762d132..2965d0ef 100644 --- a/python/fatcat/manifest_importer.py +++ b/python/fatcat/manifest_importer.py @@ -66,7 +66,7 @@ class FatcatManifestImporter(FatcatImporter):          total_count = int(list(db.execute("SELECT COUNT(*) FROM files_metadata;"))[0][0])          print("{} rows to process".format(total_count)) -        eg = self.api.create_editgroup(fatcat_client.Editgroup(editor_id=1)) +        eg = self.api.create_editgroup(fatcat_client.Editgroup(editor_id="aaaaaaaaaaaabkvkaaaaaaaaae"))          i = 0          j = -1          for row in db.execute(QUERY): @@ -81,7 +81,7 @@ class FatcatManifestImporter(FatcatImporter):              self.create_entity(fe, editgroup_id=eg.id)              if i > 0 and (i % size) == 0:                  self.api.accept_editgroup(eg.id) -                eg = self.api.create_editgroup(fatcat_client.Editgroup(editor_id=1)) +                eg = self.api.create_editgroup(fatcat_client.Editgroup(editor_id="aaaaaaaaaaaabkvkaaaaaaaaae"))                  print("Finished a batch; row {} of {} ({:.2f}%).\tTotal inserted: {}".format(                      j, total_count, 100.0*j/total_count, i))              i = i + 1 diff --git a/rust/HACKING.md b/rust/HACKING.md new file mode 100644 index 00000000..622a4b5a --- /dev/null +++ b/rust/HACKING.md @@ -0,0 +1,58 @@ + +## Code Structure + +Almost all of the rust code is either auto-generated or "glue" between the +swagger API spec on one side and the SQL schema on the other. + +- `./migrations/*/up.sql`: SQL schema +- `./src/database_schema.rs`: autogenerated per-table Diesel schemas +- `./src/database_models.rs`: hand- and macro-generated Rust structs matching +  Diesel schemas, and a small number of row-level helpers +- `./src/database_entity_crud.rs`: "struct-relational-mapping"; trait +  implementations of CRUD (create, read, update, delete) actions for each +  entity model, hitting the database (and building on `database_model` structs) +- `./src/api_server.rs`: one function for each API endpoint, with rust-style +  arguments and return types. mostly calls in to `database_entity_crud`. +- `./src/api_wrappers.rs`: hand- and macro-generated wrapper functions, one per +  API endpoint, that map between API request and return types, and +  rust-idiomatic request and return types (plus API models). +- `./fatcat-api`: autogenerated API models and endpoint types/signatures +- `../fatcat-openapi2.yaml`:  + +When deciding to use this structure, it wasn't expected that either +`api_wrappers.rs` or `database_models.rs` would need to hand-maintained; both +are verbose and implemented in a very mechanical fashion. The return type +mapping in `api_wrappers` might be necessary, but `database_models.rs` in +particular feels unnecessary; other projects have attempted to completely +automate generation of this file, but it doesn't sound reliable. In particular, +both regular "Row" (queriable) and "NewRow" (insertable) structs need to be +defined. + +## Test Structure + +- `./tests/test_api_server.rs`: Iron (web framework) level raw HTTP JSON +  request/response tests of API endpoints. + +## Updating Schemas + +Regenerate API schemas after editing the fatcat-openapi2 schema. This will, as +a side-effect, also run `cargo fmt` on the whole project, so don't run it with +your editor open! + +    cargo install cargo-swagger  # uses docker +    ./codegen_openapi2.sh + +Update Rust database schema (after changing raw SQL schema): + +    diesel database reset +    diesel print-schema > src/database_schema.rs + +Debug SQL schema errors (if diesel commands fail): + +    psql fatcat_test < migrations/2018-05-12-001226_init/up.sql + +## Direct API Interaction + +Creating entities via API: + +    http --json post localhost:9411/v0/container name=asdf issn=1234-5678 diff --git a/rust/INSTALL.md b/rust/INSTALL.md new file mode 100644 index 00000000..c2b86c51 --- /dev/null +++ b/rust/INSTALL.md @@ -0,0 +1,36 @@ + +Canonical IA production/QA ansible scripts are in the journal-infra repo. These +directions are likely to end up out-of-date. + +## Simple Deployment + +To install manually, on a bare server, as root: + +    adduser fatcat +    apt install postgresql-9.6 postgresql-contrib postgresql-client-9.6 \ +        nginx build-essential git pkg-config libssl-dev libpq-dev \ +        htop screen +    mkdir -p /srv/fatcat +    chown fatcat:fatcat /srv/fatcat + +    # setup new postgres user +    su - postgres +    createuser -P -s fatcat     # strong random password +    # DELETE: createdb fatcat + +    # as fatcat user +    su - fatcat +    ssh-keygen +    curl https://sh.rustup.rs -sSf | sh +    source $HOME/.cargo/env +    cargo install diesel_cli --no-default-features --features "postgres" +    cd /srv/fatcat +    git clone git@git.archive.org:webgroup/fatcat +    cd rust +    cargo build +    echo "DATABASE_URL=postgres://fatcat@localhost/fatcat" > .env +    diesel database reset + +    # as fatcat, in a screen or something +    cd /srv/fatcat/fatcat/rust +    cargo run diff --git a/rust/README.md b/rust/README.md index a6873345..c061a1f9 100644 --- a/rust/README.md +++ b/rust/README.md @@ -29,87 +29,4 @@ Tests:      cargo test -- --test-threads 1 -## Simple Deployment - -Canonical ansible scripts are in the journal-infra repo. To install manually, -on a bare server, as root: - -    adduser fatcat -    apt install postgresql-9.6 postgresql-contrib postgresql-client-9.6 \ -        nginx build-essential git pkg-config libssl-dev libpq-dev \ -        htop screen -    mkdir -p /srv/fatcat -    chown fatcat:fatcat /srv/fatcat - -    # setup new postgres user -    su - postgres -    createuser -P -s fatcat     # strong random password -    # DELETE: createdb fatcat - -    # as fatcat user -    su - fatcat -    ssh-keygen -    curl https://sh.rustup.rs -sSf | sh -    source $HOME/.cargo/env -    cargo install diesel_cli --no-default-features --features "postgres" -    cd /srv/fatcat -    git clone git@git.archive.org:webgroup/fatcat -    cd rust -    cargo build -    echo "DATABASE_URL=postgres://fatcat@localhost/fatcat" > .env -    diesel database reset - -    # as fatcat, in a screen or something -    cd /srv/fatcat/fatcat/rust -    cargo run - -### Dumps and Backups - -There are a few different databaase dump formats folks might want: - -- raw native database backups, for disaster recovery (would include -  volatile/unsupported schema details, user API credentials, full history, -  in-process edits, comments, etc) -- a sanitized version of the above: roughly per-table dumps of the full state -  of the database. Could use per-table SQL expressions with sub-queries to pull -  in small tables ("partial transform") and export JSON for each table; would -  be extra work to maintain, so not pursuing for now. -- full history, full public schema exports, in a form that might be used to -  mirror or enitrely fork the project. Propose supplying the full "changelog" -  in API schema format, in a single file to capture all entity history, without -  "hydrating" any inter-entity references. Rely on separate dumps of -  non-entity, non-versioned tables (editors, abstracts, etc). Note that a -  variant of this could use the public interface, in particular to do -  incremental updates (though that wouldn't capture schema changes). -- transformed exports of the current state of the database (aka, without -  history). Useful for data analysis, search engines, etc. Propose supplying -  just the Release table in a fully "hydrated" state to start. Unclear if -  should be on a work or release basis; will go with release for now. Harder to -  do using public interface because of the need for transaction locking. - -Backing up the entire database using `pg_dump`, with parallelism 1 (use more on -larger machine with fast disks; try 4 or 8?), assuming the database name is -'fatcat', and the current user has access: - -    pg_dump -j1 -Fd -f test-dump fatcat - -### Special Tricks - -Regenerate API schemas (this will, as a side-effect, also run `cargo fmt` on -the whole project, so don't run it with your editor open): - -    cargo install cargo-swagger  # uses docker -    ./codegen_openapi2.sh - -Regenerate SQL schema: - -    diesel database reset -    diesel print-schema > src/database_schema.rs - -Debugging SQL schema errors: - -    psql fatcat_test < migrations/2018-05-12-001226_init/up.sql - -Creating entities via API: - -    http --json post localhost:9411/v0/container name=asdf issn=1234-5678 +See `HACKING` for some more advanced tips and commands. @@ -1,4 +1,13 @@ +- fatcat_api -> fatcat_api_schema (or spec? models? types?) +- fatcat -> fatcat-api-server +- refactor rev creation (from an entity) into it's own function, across the board +    => attach to database module structs? +    => should make cockroachdb compatible (single use of CTE) +    => usable from both "update" and "create" +- editgroup param to update +    => also for creation? for consistency +  - editor_id vs. editor username; return editor_id (in addition to name?)  later: diff --git a/rust/fatcat-api/README.md b/rust/fatcat-api/README.md index 7e4a2ec8..4be4939f 100644 --- a/rust/fatcat-api/README.md +++ b/rust/fatcat-api/README.md @@ -13,7 +13,7 @@ To see how to make this your own, look here:  [README](https://github.com/swagger-api/swagger-codegen/blob/master/README.md)  - API version: 0.1.0 -- Build date: 2018-08-22T00:54:09.323Z +- Build date: 2018-09-01T01:21:49.948Z  This autogenerated project defines an API crate `fatcat` which contains:  * An `Api` trait defining the API in Rust. diff --git a/rust/fatcat-api/api.yaml b/rust/fatcat-api/api.yaml index ea17e982..a8919216 100644 --- a/rust/fatcat-api/api.yaml +++ b/rust/fatcat-api/api.yaml @@ -473,19 +473,47 @@ paths:          in: path          type: string          required: true -      - name: expand -        in: query -        type: string -        required: false -        description: "List of sub-entities to expand in response. For now, only 'all' accepted."      get:        operationId: "get_container" +      parameters: +        - name: expand +          in: query +          type: string +          required: false +          description: "List of sub-entities to expand in response. For now, only 'all' accepted."        responses:          200:            description: Found Entity            schema:              $ref: "#/definitions/container_entity"          <<: *ENTITYRESPONSES +    put: +      operationId: "update_container" +      parameters: +        - name: entity +          in: body +          required: true +          schema: +            $ref: "#/definitions/container_entity" +      responses: +        200: +          description: Updated Entity +          schema: +            $ref: "#/definitions/entity_edit" +        <<: *ENTITYRESPONSES +    delete: +      operationId: "delete_container" +      parameters: +        - name: editgroup +          in: query +          required: false +          type: string +      responses: +        200: +          description: Deleted Entity +          schema: +            $ref: "#/definitions/entity_edit" +        <<: *ENTITYRESPONSES    /container/{id}/history:      parameters:        - name: id @@ -571,19 +599,47 @@ paths:          in: path          type: string          required: true -      - name: expand -        in: query -        type: string -        required: false -        description: "List of sub-entities to expand in response. For now, only 'all' accepted."      get:        operationId: "get_creator" +      parameters: +        - name: expand +          in: query +          type: string +          required: false +          description: "List of sub-entities to expand in response. For now, only 'all' accepted."        responses:          200:            description: Found Entity            schema:              $ref: "#/definitions/creator_entity"          <<: *ENTITYRESPONSES +    put: +      operationId: "update_creator" +      parameters: +        - name: entity +          in: body +          required: true +          schema: +            $ref: "#/definitions/creator_entity" +      responses: +        200: +          description: Updated Entity +          schema: +            $ref: "#/definitions/entity_edit" +        <<: *ENTITYRESPONSES +    delete: +      operationId: "delete_creator" +      parameters: +        - name: editgroup +          in: query +          required: false +          type: string +      responses: +        200: +          description: Deleted Entity +          schema: +            $ref: "#/definitions/entity_edit" +        <<: *ENTITYRESPONSES    /creator/{id}/history:      parameters:        - name: id @@ -685,19 +741,47 @@ paths:          in: path          type: string          required: true -      - name: expand -        in: query -        type: string -        required: false -        description: "List of sub-entities to expand in response. For now, only 'all' accepted."      get:        operationId: "get_file" +      parameters: +        - name: expand +          in: query +          type: string +          required: false +          description: "List of sub-entities to expand in response. For now, only 'all' accepted."        responses:          200:            description: Found Entity            schema:              $ref: "#/definitions/file_entity"          <<: *ENTITYRESPONSES +    put: +      operationId: "update_file" +      parameters: +        - name: entity +          in: body +          required: true +          schema: +            $ref: "#/definitions/file_entity" +      responses: +        200: +          description: Updated Entity +          schema: +            $ref: "#/definitions/entity_edit" +        <<: *ENTITYRESPONSES +    delete: +      operationId: "delete_file" +      parameters: +        - name: editgroup +          in: query +          required: false +          type: string +      responses: +        200: +          description: Deleted Entity +          schema: +            $ref: "#/definitions/entity_edit" +        <<: *ENTITYRESPONSES    /file/{id}/history:      parameters:        - name: id @@ -783,19 +867,47 @@ paths:          in: path          type: string          required: true -      - name: expand -        in: query -        type: string -        required: false -        description: "List of sub-entities to expand in response. For now, only 'all' accepted."      get:        operationId: "get_release" +      parameters: +        - name: expand +          in: query +          type: string +          required: false +          description: "List of sub-entities to expand in response. For now, only 'all' accepted."        responses:          200:            description: Found Entity            schema:              $ref: "#/definitions/release_entity"          <<: *ENTITYRESPONSES +    put: +      operationId: "update_release" +      parameters: +        - name: entity +          in: body +          required: true +          schema: +            $ref: "#/definitions/release_entity" +      responses: +        200: +          description: Updated Entity +          schema: +            $ref: "#/definitions/entity_edit" +        <<: *ENTITYRESPONSES +    delete: +      operationId: "delete_release" +      parameters: +        - name: editgroup +          in: query +          required: false +          type: string +      responses: +        200: +          description: Deleted Entity +          schema: +            $ref: "#/definitions/entity_edit" +        <<: *ENTITYRESPONSES    /release/{id}/history:      parameters:        - name: id @@ -897,19 +1009,47 @@ paths:          in: path          type: string          required: true -      - name: expand -        in: query -        type: string -        required: false -        description: "List of sub-entities to expand in response. For now, only 'all' accepted."      get:        operationId: "get_work" +      parameters: +        - name: expand +          in: query +          type: string +          required: false +          description: "List of sub-entities to expand in response. For now, only 'all' accepted."        responses:          200:            description: Found Entity            schema:              $ref: "#/definitions/work_entity"          <<: *ENTITYRESPONSES +    put: +      operationId: "update_work" +      parameters: +        - name: entity +          in: body +          required: true +          schema: +            $ref: "#/definitions/work_entity" +      responses: +        200: +          description: Updated Entity +          schema: +            $ref: "#/definitions/entity_edit" +        <<: *ENTITYRESPONSES +    delete: +      operationId: "delete_work" +      parameters: +        - name: editgroup +          in: query +          required: false +          type: string +      responses: +        200: +          description: Deleted Entity +          schema: +            $ref: "#/definitions/entity_edit" +        <<: *ENTITYRESPONSES    /work/{id}/history:      parameters:        - name: id @@ -1055,10 +1195,18 @@ paths:            description: Unmergable            schema:              $ref: "#/definitions/error_response" +        400: +          description: Bad Request +          schema: +            $ref: "#/definitions/error_response"          404:            description: Not Found            schema:              $ref: "#/definitions/error_response" +        409: +          description: Edit Conflict +          schema: +            $ref: "#/definitions/error_response"          500:            description: Generic Error            schema: diff --git a/rust/fatcat-api/api/swagger.yaml b/rust/fatcat-api/api/swagger.yaml index 3b8ed6e3..0b1ca88a 100644 --- a/rust/fatcat-api/api/swagger.yaml +++ b/rust/fatcat-api/api/swagger.yaml @@ -208,6 +208,127 @@ paths:        path: "/container/:id"        HttpMethod: "Get"        httpmethod: "get" +    put: +      operationId: "update_container" +      parameters: +      - name: "id" +        in: "path" +        required: true +        type: "string" +        formatString: "\\\"{}\\\"" +        example: "\"id_example\".to_string()" +      - in: "body" +        name: "entity" +        required: true +        schema: +          $ref: "#/definitions/container_entity" +        uppercase_data_type: "CONTAINERENTITY" +        refName: "container_entity" +        formatString: "{:?}" +        example: "???" +        model_key: "editgroup_edits" +        uppercase_operation_id: "UPDATE_CONTAINER" +        consumesJson: true +      responses: +        200: +          description: "Updated Entity" +          schema: +            $ref: "#/definitions/entity_edit" +          x-responseId: "UpdatedEntity" +          x-uppercaseResponseId: "UPDATED_ENTITY" +          uppercase_operation_id: "UPDATE_CONTAINER" +          uppercase_data_type: "ENTITYEDIT" +          producesJson: true +        400: +          description: "Bad Request" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "BadRequest" +          x-uppercaseResponseId: "BAD_REQUEST" +          uppercase_operation_id: "UPDATE_CONTAINER" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        404: +          description: "Not Found" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "NotFound" +          x-uppercaseResponseId: "NOT_FOUND" +          uppercase_operation_id: "UPDATE_CONTAINER" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        500: +          description: "Generic Error" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "GenericError" +          x-uppercaseResponseId: "GENERIC_ERROR" +          uppercase_operation_id: "UPDATE_CONTAINER" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +      operation_id: "update_container" +      uppercase_operation_id: "UPDATE_CONTAINER" +      path: "/container/:id" +      HttpMethod: "Put" +      httpmethod: "put" +      noClientExample: true +    delete: +      operationId: "delete_container" +      parameters: +      - name: "id" +        in: "path" +        required: true +        type: "string" +        formatString: "\\\"{}\\\"" +        example: "\"id_example\".to_string()" +      - name: "editgroup" +        in: "query" +        required: false +        type: "string" +        formatString: "{:?}" +        example: "Some(\"editgroup_example\".to_string())" +      responses: +        200: +          description: "Deleted Entity" +          schema: +            $ref: "#/definitions/entity_edit" +          x-responseId: "DeletedEntity" +          x-uppercaseResponseId: "DELETED_ENTITY" +          uppercase_operation_id: "DELETE_CONTAINER" +          uppercase_data_type: "ENTITYEDIT" +          producesJson: true +        400: +          description: "Bad Request" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "BadRequest" +          x-uppercaseResponseId: "BAD_REQUEST" +          uppercase_operation_id: "DELETE_CONTAINER" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        404: +          description: "Not Found" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "NotFound" +          x-uppercaseResponseId: "NOT_FOUND" +          uppercase_operation_id: "DELETE_CONTAINER" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        500: +          description: "Generic Error" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "GenericError" +          x-uppercaseResponseId: "GENERIC_ERROR" +          uppercase_operation_id: "DELETE_CONTAINER" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +      operation_id: "delete_container" +      uppercase_operation_id: "DELETE_CONTAINER" +      path: "/container/:id" +      HttpMethod: "Delete" +      httpmethod: "delete"    /container/{id}/history:      get:        operationId: "get_container_history" @@ -519,6 +640,127 @@ paths:        path: "/creator/:id"        HttpMethod: "Get"        httpmethod: "get" +    put: +      operationId: "update_creator" +      parameters: +      - name: "id" +        in: "path" +        required: true +        type: "string" +        formatString: "\\\"{}\\\"" +        example: "\"id_example\".to_string()" +      - in: "body" +        name: "entity" +        required: true +        schema: +          $ref: "#/definitions/creator_entity" +        uppercase_data_type: "CREATORENTITY" +        refName: "creator_entity" +        formatString: "{:?}" +        example: "???" +        model_key: "editgroup_edits" +        uppercase_operation_id: "UPDATE_CREATOR" +        consumesJson: true +      responses: +        200: +          description: "Updated Entity" +          schema: +            $ref: "#/definitions/entity_edit" +          x-responseId: "UpdatedEntity" +          x-uppercaseResponseId: "UPDATED_ENTITY" +          uppercase_operation_id: "UPDATE_CREATOR" +          uppercase_data_type: "ENTITYEDIT" +          producesJson: true +        400: +          description: "Bad Request" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "BadRequest" +          x-uppercaseResponseId: "BAD_REQUEST" +          uppercase_operation_id: "UPDATE_CREATOR" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        404: +          description: "Not Found" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "NotFound" +          x-uppercaseResponseId: "NOT_FOUND" +          uppercase_operation_id: "UPDATE_CREATOR" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        500: +          description: "Generic Error" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "GenericError" +          x-uppercaseResponseId: "GENERIC_ERROR" +          uppercase_operation_id: "UPDATE_CREATOR" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +      operation_id: "update_creator" +      uppercase_operation_id: "UPDATE_CREATOR" +      path: "/creator/:id" +      HttpMethod: "Put" +      httpmethod: "put" +      noClientExample: true +    delete: +      operationId: "delete_creator" +      parameters: +      - name: "id" +        in: "path" +        required: true +        type: "string" +        formatString: "\\\"{}\\\"" +        example: "\"id_example\".to_string()" +      - name: "editgroup" +        in: "query" +        required: false +        type: "string" +        formatString: "{:?}" +        example: "Some(\"editgroup_example\".to_string())" +      responses: +        200: +          description: "Deleted Entity" +          schema: +            $ref: "#/definitions/entity_edit" +          x-responseId: "DeletedEntity" +          x-uppercaseResponseId: "DELETED_ENTITY" +          uppercase_operation_id: "DELETE_CREATOR" +          uppercase_data_type: "ENTITYEDIT" +          producesJson: true +        400: +          description: "Bad Request" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "BadRequest" +          x-uppercaseResponseId: "BAD_REQUEST" +          uppercase_operation_id: "DELETE_CREATOR" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        404: +          description: "Not Found" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "NotFound" +          x-uppercaseResponseId: "NOT_FOUND" +          uppercase_operation_id: "DELETE_CREATOR" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        500: +          description: "Generic Error" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "GenericError" +          x-uppercaseResponseId: "GENERIC_ERROR" +          uppercase_operation_id: "DELETE_CREATOR" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +      operation_id: "delete_creator" +      uppercase_operation_id: "DELETE_CREATOR" +      path: "/creator/:id" +      HttpMethod: "Delete" +      httpmethod: "delete"    /creator/{id}/history:      get:        operationId: "get_creator_history" @@ -884,6 +1126,127 @@ paths:        path: "/file/:id"        HttpMethod: "Get"        httpmethod: "get" +    put: +      operationId: "update_file" +      parameters: +      - name: "id" +        in: "path" +        required: true +        type: "string" +        formatString: "\\\"{}\\\"" +        example: "\"id_example\".to_string()" +      - in: "body" +        name: "entity" +        required: true +        schema: +          $ref: "#/definitions/file_entity" +        uppercase_data_type: "FILEENTITY" +        refName: "file_entity" +        formatString: "{:?}" +        example: "???" +        model_key: "editgroup_edits" +        uppercase_operation_id: "UPDATE_FILE" +        consumesJson: true +      responses: +        200: +          description: "Updated Entity" +          schema: +            $ref: "#/definitions/entity_edit" +          x-responseId: "UpdatedEntity" +          x-uppercaseResponseId: "UPDATED_ENTITY" +          uppercase_operation_id: "UPDATE_FILE" +          uppercase_data_type: "ENTITYEDIT" +          producesJson: true +        400: +          description: "Bad Request" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "BadRequest" +          x-uppercaseResponseId: "BAD_REQUEST" +          uppercase_operation_id: "UPDATE_FILE" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        404: +          description: "Not Found" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "NotFound" +          x-uppercaseResponseId: "NOT_FOUND" +          uppercase_operation_id: "UPDATE_FILE" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        500: +          description: "Generic Error" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "GenericError" +          x-uppercaseResponseId: "GENERIC_ERROR" +          uppercase_operation_id: "UPDATE_FILE" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +      operation_id: "update_file" +      uppercase_operation_id: "UPDATE_FILE" +      path: "/file/:id" +      HttpMethod: "Put" +      httpmethod: "put" +      noClientExample: true +    delete: +      operationId: "delete_file" +      parameters: +      - name: "id" +        in: "path" +        required: true +        type: "string" +        formatString: "\\\"{}\\\"" +        example: "\"id_example\".to_string()" +      - name: "editgroup" +        in: "query" +        required: false +        type: "string" +        formatString: "{:?}" +        example: "Some(\"editgroup_example\".to_string())" +      responses: +        200: +          description: "Deleted Entity" +          schema: +            $ref: "#/definitions/entity_edit" +          x-responseId: "DeletedEntity" +          x-uppercaseResponseId: "DELETED_ENTITY" +          uppercase_operation_id: "DELETE_FILE" +          uppercase_data_type: "ENTITYEDIT" +          producesJson: true +        400: +          description: "Bad Request" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "BadRequest" +          x-uppercaseResponseId: "BAD_REQUEST" +          uppercase_operation_id: "DELETE_FILE" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        404: +          description: "Not Found" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "NotFound" +          x-uppercaseResponseId: "NOT_FOUND" +          uppercase_operation_id: "DELETE_FILE" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        500: +          description: "Generic Error" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "GenericError" +          x-uppercaseResponseId: "GENERIC_ERROR" +          uppercase_operation_id: "DELETE_FILE" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +      operation_id: "delete_file" +      uppercase_operation_id: "DELETE_FILE" +      path: "/file/:id" +      HttpMethod: "Delete" +      httpmethod: "delete"    /file/{id}/history:      get:        operationId: "get_file_history" @@ -1192,6 +1555,127 @@ paths:        path: "/release/:id"        HttpMethod: "Get"        httpmethod: "get" +    put: +      operationId: "update_release" +      parameters: +      - name: "id" +        in: "path" +        required: true +        type: "string" +        formatString: "\\\"{}\\\"" +        example: "\"id_example\".to_string()" +      - in: "body" +        name: "entity" +        required: true +        schema: +          $ref: "#/definitions/release_entity" +        uppercase_data_type: "RELEASEENTITY" +        refName: "release_entity" +        formatString: "{:?}" +        example: "???" +        model_key: "editgroup_edits" +        uppercase_operation_id: "UPDATE_RELEASE" +        consumesJson: true +      responses: +        200: +          description: "Updated Entity" +          schema: +            $ref: "#/definitions/entity_edit" +          x-responseId: "UpdatedEntity" +          x-uppercaseResponseId: "UPDATED_ENTITY" +          uppercase_operation_id: "UPDATE_RELEASE" +          uppercase_data_type: "ENTITYEDIT" +          producesJson: true +        400: +          description: "Bad Request" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "BadRequest" +          x-uppercaseResponseId: "BAD_REQUEST" +          uppercase_operation_id: "UPDATE_RELEASE" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        404: +          description: "Not Found" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "NotFound" +          x-uppercaseResponseId: "NOT_FOUND" +          uppercase_operation_id: "UPDATE_RELEASE" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        500: +          description: "Generic Error" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "GenericError" +          x-uppercaseResponseId: "GENERIC_ERROR" +          uppercase_operation_id: "UPDATE_RELEASE" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +      operation_id: "update_release" +      uppercase_operation_id: "UPDATE_RELEASE" +      path: "/release/:id" +      HttpMethod: "Put" +      httpmethod: "put" +      noClientExample: true +    delete: +      operationId: "delete_release" +      parameters: +      - name: "id" +        in: "path" +        required: true +        type: "string" +        formatString: "\\\"{}\\\"" +        example: "\"id_example\".to_string()" +      - name: "editgroup" +        in: "query" +        required: false +        type: "string" +        formatString: "{:?}" +        example: "Some(\"editgroup_example\".to_string())" +      responses: +        200: +          description: "Deleted Entity" +          schema: +            $ref: "#/definitions/entity_edit" +          x-responseId: "DeletedEntity" +          x-uppercaseResponseId: "DELETED_ENTITY" +          uppercase_operation_id: "DELETE_RELEASE" +          uppercase_data_type: "ENTITYEDIT" +          producesJson: true +        400: +          description: "Bad Request" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "BadRequest" +          x-uppercaseResponseId: "BAD_REQUEST" +          uppercase_operation_id: "DELETE_RELEASE" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        404: +          description: "Not Found" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "NotFound" +          x-uppercaseResponseId: "NOT_FOUND" +          uppercase_operation_id: "DELETE_RELEASE" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        500: +          description: "Generic Error" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "GenericError" +          x-uppercaseResponseId: "GENERIC_ERROR" +          uppercase_operation_id: "DELETE_RELEASE" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +      operation_id: "delete_release" +      uppercase_operation_id: "DELETE_RELEASE" +      path: "/release/:id" +      HttpMethod: "Delete" +      httpmethod: "delete"    /release/{id}/history:      get:        operationId: "get_release_history" @@ -1554,6 +2038,127 @@ paths:        path: "/work/:id"        HttpMethod: "Get"        httpmethod: "get" +    put: +      operationId: "update_work" +      parameters: +      - name: "id" +        in: "path" +        required: true +        type: "string" +        formatString: "\\\"{}\\\"" +        example: "\"id_example\".to_string()" +      - in: "body" +        name: "entity" +        required: true +        schema: +          $ref: "#/definitions/work_entity" +        uppercase_data_type: "WORKENTITY" +        refName: "work_entity" +        formatString: "{:?}" +        example: "???" +        model_key: "editgroup_edits" +        uppercase_operation_id: "UPDATE_WORK" +        consumesJson: true +      responses: +        200: +          description: "Updated Entity" +          schema: +            $ref: "#/definitions/entity_edit" +          x-responseId: "UpdatedEntity" +          x-uppercaseResponseId: "UPDATED_ENTITY" +          uppercase_operation_id: "UPDATE_WORK" +          uppercase_data_type: "ENTITYEDIT" +          producesJson: true +        400: +          description: "Bad Request" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "BadRequest" +          x-uppercaseResponseId: "BAD_REQUEST" +          uppercase_operation_id: "UPDATE_WORK" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        404: +          description: "Not Found" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "NotFound" +          x-uppercaseResponseId: "NOT_FOUND" +          uppercase_operation_id: "UPDATE_WORK" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        500: +          description: "Generic Error" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "GenericError" +          x-uppercaseResponseId: "GENERIC_ERROR" +          uppercase_operation_id: "UPDATE_WORK" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +      operation_id: "update_work" +      uppercase_operation_id: "UPDATE_WORK" +      path: "/work/:id" +      HttpMethod: "Put" +      httpmethod: "put" +      noClientExample: true +    delete: +      operationId: "delete_work" +      parameters: +      - name: "id" +        in: "path" +        required: true +        type: "string" +        formatString: "\\\"{}\\\"" +        example: "\"id_example\".to_string()" +      - name: "editgroup" +        in: "query" +        required: false +        type: "string" +        formatString: "{:?}" +        example: "Some(\"editgroup_example\".to_string())" +      responses: +        200: +          description: "Deleted Entity" +          schema: +            $ref: "#/definitions/entity_edit" +          x-responseId: "DeletedEntity" +          x-uppercaseResponseId: "DELETED_ENTITY" +          uppercase_operation_id: "DELETE_WORK" +          uppercase_data_type: "ENTITYEDIT" +          producesJson: true +        400: +          description: "Bad Request" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "BadRequest" +          x-uppercaseResponseId: "BAD_REQUEST" +          uppercase_operation_id: "DELETE_WORK" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        404: +          description: "Not Found" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "NotFound" +          x-uppercaseResponseId: "NOT_FOUND" +          uppercase_operation_id: "DELETE_WORK" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        500: +          description: "Generic Error" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "GenericError" +          x-uppercaseResponseId: "GENERIC_ERROR" +          uppercase_operation_id: "DELETE_WORK" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +      operation_id: "delete_work" +      uppercase_operation_id: "DELETE_WORK" +      path: "/work/:id" +      HttpMethod: "Delete" +      httpmethod: "delete"    /work/{id}/history:      get:        operationId: "get_work_history" @@ -1888,11 +2493,11 @@ paths:            uppercase_data_type: "SUCCESS"            producesJson: true          400: -          description: "Unmergable" +          description: "Bad Request"            schema:              $ref: "#/definitions/error_response" -          x-responseId: "Unmergable" -          x-uppercaseResponseId: "UNMERGABLE" +          x-responseId: "BadRequest" +          x-uppercaseResponseId: "BAD_REQUEST"            uppercase_operation_id: "ACCEPT_EDITGROUP"            uppercase_data_type: "ERRORRESPONSE"            producesJson: true @@ -1905,6 +2510,15 @@ paths:            uppercase_operation_id: "ACCEPT_EDITGROUP"            uppercase_data_type: "ERRORRESPONSE"            producesJson: true +        409: +          description: "Edit Conflict" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "EditConflict" +          x-uppercaseResponseId: "EDIT_CONFLICT" +          uppercase_operation_id: "ACCEPT_EDITGROUP" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true          500:            description: "Generic Error"            schema: diff --git a/rust/fatcat-api/examples/client.rs b/rust/fatcat-api/examples/client.rs index 34653196..cc94af11 100644 --- a/rust/fatcat-api/examples/client.rs +++ b/rust/fatcat-api/examples/client.rs @@ -13,10 +13,11 @@ use clap::{App, Arg};  #[allow(unused_imports)]  use fatcat::{      AcceptEditgroupResponse, ApiError, ApiNoContext, ContextWrapperExt, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse, -    CreateEditgroupResponse, CreateFileBatchResponse, CreateFileResponse, CreateReleaseBatchResponse, CreateReleaseResponse, CreateWorkBatchResponse, CreateWorkResponse, GetChangelogEntryResponse, -    GetChangelogResponse, GetContainerHistoryResponse, GetContainerResponse, GetCreatorHistoryResponse, GetCreatorReleasesResponse, GetCreatorResponse, GetEditgroupResponse, -    GetEditorChangelogResponse, GetEditorResponse, GetFileHistoryResponse, GetFileResponse, GetReleaseFilesResponse, GetReleaseHistoryResponse, GetReleaseResponse, GetStatsResponse, -    GetWorkHistoryResponse, GetWorkReleasesResponse, GetWorkResponse, LookupContainerResponse, LookupCreatorResponse, LookupFileResponse, LookupReleaseResponse, +    CreateEditgroupResponse, CreateFileBatchResponse, CreateFileResponse, CreateReleaseBatchResponse, CreateReleaseResponse, CreateWorkBatchResponse, CreateWorkResponse, DeleteContainerResponse, +    DeleteCreatorResponse, DeleteFileResponse, DeleteReleaseResponse, DeleteWorkResponse, GetChangelogEntryResponse, GetChangelogResponse, GetContainerHistoryResponse, GetContainerResponse, +    GetCreatorHistoryResponse, GetCreatorReleasesResponse, GetCreatorResponse, GetEditgroupResponse, GetEditorChangelogResponse, GetEditorResponse, GetFileHistoryResponse, GetFileResponse, +    GetReleaseFilesResponse, GetReleaseHistoryResponse, GetReleaseResponse, GetStatsResponse, GetWorkHistoryResponse, GetWorkReleasesResponse, GetWorkResponse, LookupContainerResponse, +    LookupCreatorResponse, LookupFileResponse, LookupReleaseResponse, UpdateContainerResponse, UpdateCreatorResponse, UpdateFileResponse, UpdateReleaseResponse, UpdateWorkResponse,  };  #[allow(unused_imports)]  use futures::{future, stream, Future, Stream}; @@ -33,6 +34,11 @@ fn main() {                      "CreateFileBatch",                      "CreateReleaseBatch",                      "CreateWorkBatch", +                    "DeleteContainer", +                    "DeleteCreator", +                    "DeleteFile", +                    "DeleteRelease", +                    "DeleteWork",                      "GetChangelog",                      "GetChangelogEntry",                      "GetContainer", @@ -145,6 +151,31 @@ fn main() {              println!("{:?} (X-Span-ID: {:?})", result, client.context().x_span_id.clone().unwrap_or(String::from("<none>")));          } +        Some("DeleteContainer") => { +            let result = client.delete_container("id_example".to_string(), Some("editgroup_example".to_string())).wait(); +            println!("{:?} (X-Span-ID: {:?})", result, client.context().x_span_id.clone().unwrap_or(String::from("<none>"))); +        } + +        Some("DeleteCreator") => { +            let result = client.delete_creator("id_example".to_string(), Some("editgroup_example".to_string())).wait(); +            println!("{:?} (X-Span-ID: {:?})", result, client.context().x_span_id.clone().unwrap_or(String::from("<none>"))); +        } + +        Some("DeleteFile") => { +            let result = client.delete_file("id_example".to_string(), Some("editgroup_example".to_string())).wait(); +            println!("{:?} (X-Span-ID: {:?})", result, client.context().x_span_id.clone().unwrap_or(String::from("<none>"))); +        } + +        Some("DeleteRelease") => { +            let result = client.delete_release("id_example".to_string(), Some("editgroup_example".to_string())).wait(); +            println!("{:?} (X-Span-ID: {:?})", result, client.context().x_span_id.clone().unwrap_or(String::from("<none>"))); +        } + +        Some("DeleteWork") => { +            let result = client.delete_work("id_example".to_string(), Some("editgroup_example".to_string())).wait(); +            println!("{:?} (X-Span-ID: {:?})", result, client.context().x_span_id.clone().unwrap_or(String::from("<none>"))); +        } +          Some("GetChangelog") => {              let result = client.get_changelog(Some(789)).wait();              println!("{:?} (X-Span-ID: {:?})", result, client.context().x_span_id.clone().unwrap_or(String::from("<none>"))); @@ -260,6 +291,35 @@ fn main() {              println!("{:?} (X-Span-ID: {:?})", result, client.context().x_span_id.clone().unwrap_or(String::from("<none>")));          } +        // Disabled because there's no example. +        // Some("UpdateContainer") => { +        //     let result = client.update_container("id_example".to_string(), ???).wait(); +        //     println!("{:?} (X-Span-ID: {:?})", result, client.context().x_span_id.clone().unwrap_or(String::from("<none>"))); +        //  }, + +        // Disabled because there's no example. +        // Some("UpdateCreator") => { +        //     let result = client.update_creator("id_example".to_string(), ???).wait(); +        //     println!("{:?} (X-Span-ID: {:?})", result, client.context().x_span_id.clone().unwrap_or(String::from("<none>"))); +        //  }, + +        // Disabled because there's no example. +        // Some("UpdateFile") => { +        //     let result = client.update_file("id_example".to_string(), ???).wait(); +        //     println!("{:?} (X-Span-ID: {:?})", result, client.context().x_span_id.clone().unwrap_or(String::from("<none>"))); +        //  }, + +        // Disabled because there's no example. +        // Some("UpdateRelease") => { +        //     let result = client.update_release("id_example".to_string(), ???).wait(); +        //     println!("{:?} (X-Span-ID: {:?})", result, client.context().x_span_id.clone().unwrap_or(String::from("<none>"))); +        //  }, + +        // Disabled because there's no example. +        // Some("UpdateWork") => { +        //     let result = client.update_work("id_example".to_string(), ???).wait(); +        //     println!("{:?} (X-Span-ID: {:?})", result, client.context().x_span_id.clone().unwrap_or(String::from("<none>"))); +        //  },          _ => panic!("Invalid operation provided"),      }  } diff --git a/rust/fatcat-api/examples/server_lib/server.rs b/rust/fatcat-api/examples/server_lib/server.rs index 60e19847..ab08f594 100644 --- a/rust/fatcat-api/examples/server_lib/server.rs +++ b/rust/fatcat-api/examples/server_lib/server.rs @@ -12,10 +12,11 @@ use swagger;  use fatcat::models;  use fatcat::{      AcceptEditgroupResponse, Api, ApiError, Context, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse, CreateEditgroupResponse, -    CreateFileBatchResponse, CreateFileResponse, CreateReleaseBatchResponse, CreateReleaseResponse, CreateWorkBatchResponse, CreateWorkResponse, GetChangelogEntryResponse, GetChangelogResponse, -    GetContainerHistoryResponse, GetContainerResponse, GetCreatorHistoryResponse, GetCreatorReleasesResponse, GetCreatorResponse, GetEditgroupResponse, GetEditorChangelogResponse, GetEditorResponse, -    GetFileHistoryResponse, GetFileResponse, GetReleaseFilesResponse, GetReleaseHistoryResponse, GetReleaseResponse, GetStatsResponse, GetWorkHistoryResponse, GetWorkReleasesResponse, -    GetWorkResponse, LookupContainerResponse, LookupCreatorResponse, LookupFileResponse, LookupReleaseResponse, +    CreateFileBatchResponse, CreateFileResponse, CreateReleaseBatchResponse, CreateReleaseResponse, CreateWorkBatchResponse, CreateWorkResponse, DeleteContainerResponse, DeleteCreatorResponse, +    DeleteFileResponse, DeleteReleaseResponse, DeleteWorkResponse, GetChangelogEntryResponse, GetChangelogResponse, GetContainerHistoryResponse, GetContainerResponse, GetCreatorHistoryResponse, +    GetCreatorReleasesResponse, GetCreatorResponse, GetEditgroupResponse, GetEditorChangelogResponse, GetEditorResponse, GetFileHistoryResponse, GetFileResponse, GetReleaseFilesResponse, +    GetReleaseHistoryResponse, GetReleaseResponse, GetStatsResponse, GetWorkHistoryResponse, GetWorkReleasesResponse, GetWorkResponse, LookupContainerResponse, LookupCreatorResponse, +    LookupFileResponse, LookupReleaseResponse, UpdateContainerResponse, UpdateCreatorResponse, UpdateFileResponse, UpdateReleaseResponse, UpdateWorkResponse,  };  #[derive(Copy, Clone)] @@ -154,6 +155,61 @@ impl Api for Server {          Box::new(futures::failed("Generic failure".into()))      } +    fn delete_container(&self, id: String, editgroup: Option<String>, context: &Context) -> Box<Future<Item = DeleteContainerResponse, Error = ApiError> + Send> { +        let context = context.clone(); +        println!( +            "delete_container(\"{}\", {:?}) - X-Span-ID: {:?}", +            id, +            editgroup, +            context.x_span_id.unwrap_or(String::from("<none>")).clone() +        ); +        Box::new(futures::failed("Generic failure".into())) +    } + +    fn delete_creator(&self, id: String, editgroup: Option<String>, context: &Context) -> Box<Future<Item = DeleteCreatorResponse, Error = ApiError> + Send> { +        let context = context.clone(); +        println!( +            "delete_creator(\"{}\", {:?}) - X-Span-ID: {:?}", +            id, +            editgroup, +            context.x_span_id.unwrap_or(String::from("<none>")).clone() +        ); +        Box::new(futures::failed("Generic failure".into())) +    } + +    fn delete_file(&self, id: String, editgroup: Option<String>, context: &Context) -> Box<Future<Item = DeleteFileResponse, Error = ApiError> + Send> { +        let context = context.clone(); +        println!( +            "delete_file(\"{}\", {:?}) - X-Span-ID: {:?}", +            id, +            editgroup, +            context.x_span_id.unwrap_or(String::from("<none>")).clone() +        ); +        Box::new(futures::failed("Generic failure".into())) +    } + +    fn delete_release(&self, id: String, editgroup: Option<String>, context: &Context) -> Box<Future<Item = DeleteReleaseResponse, Error = ApiError> + Send> { +        let context = context.clone(); +        println!( +            "delete_release(\"{}\", {:?}) - X-Span-ID: {:?}", +            id, +            editgroup, +            context.x_span_id.unwrap_or(String::from("<none>")).clone() +        ); +        Box::new(futures::failed("Generic failure".into())) +    } + +    fn delete_work(&self, id: String, editgroup: Option<String>, context: &Context) -> Box<Future<Item = DeleteWorkResponse, Error = ApiError> + Send> { +        let context = context.clone(); +        println!( +            "delete_work(\"{}\", {:?}) - X-Span-ID: {:?}", +            id, +            editgroup, +            context.x_span_id.unwrap_or(String::from("<none>")).clone() +        ); +        Box::new(futures::failed("Generic failure".into())) +    } +      fn get_changelog(&self, limit: Option<i64>, context: &Context) -> Box<Future<Item = GetChangelogResponse, Error = ApiError> + Send> {          let context = context.clone();          println!("get_changelog({:?}) - X-Span-ID: {:?}", limit, context.x_span_id.unwrap_or(String::from("<none>")).clone()); @@ -316,4 +372,49 @@ impl Api for Server {          println!("lookup_release(\"{}\") - X-Span-ID: {:?}", doi, context.x_span_id.unwrap_or(String::from("<none>")).clone());          Box::new(futures::failed("Generic failure".into()))      } + +    fn update_container(&self, id: String, entity: models::ContainerEntity, context: &Context) -> Box<Future<Item = UpdateContainerResponse, Error = ApiError> + Send> { +        let context = context.clone(); +        println!( +            "update_container(\"{}\", {:?}) - X-Span-ID: {:?}", +            id, +            entity, +            context.x_span_id.unwrap_or(String::from("<none>")).clone() +        ); +        Box::new(futures::failed("Generic failure".into())) +    } + +    fn update_creator(&self, id: String, entity: models::CreatorEntity, context: &Context) -> Box<Future<Item = UpdateCreatorResponse, Error = ApiError> + Send> { +        let context = context.clone(); +        println!( +            "update_creator(\"{}\", {:?}) - X-Span-ID: {:?}", +            id, +            entity, +            context.x_span_id.unwrap_or(String::from("<none>")).clone() +        ); +        Box::new(futures::failed("Generic failure".into())) +    } + +    fn update_file(&self, id: String, entity: models::FileEntity, context: &Context) -> Box<Future<Item = UpdateFileResponse, Error = ApiError> + Send> { +        let context = context.clone(); +        println!("update_file(\"{}\", {:?}) - X-Span-ID: {:?}", id, entity, context.x_span_id.unwrap_or(String::from("<none>")).clone()); +        Box::new(futures::failed("Generic failure".into())) +    } + +    fn update_release(&self, id: String, entity: models::ReleaseEntity, context: &Context) -> Box<Future<Item = UpdateReleaseResponse, Error = ApiError> + Send> { +        let context = context.clone(); +        println!( +            "update_release(\"{}\", {:?}) - X-Span-ID: {:?}", +            id, +            entity, +            context.x_span_id.unwrap_or(String::from("<none>")).clone() +        ); +        Box::new(futures::failed("Generic failure".into())) +    } + +    fn update_work(&self, id: String, entity: models::WorkEntity, context: &Context) -> Box<Future<Item = UpdateWorkResponse, Error = ApiError> + Send> { +        let context = context.clone(); +        println!("update_work(\"{}\", {:?}) - X-Span-ID: {:?}", id, entity, context.x_span_id.unwrap_or(String::from("<none>")).clone()); +        Box::new(futures::failed("Generic failure".into())) +    }  } diff --git a/rust/fatcat-api/src/client.rs b/rust/fatcat-api/src/client.rs index 628d8894..6f61f773 100644 --- a/rust/fatcat-api/src/client.rs +++ b/rust/fatcat-api/src/client.rs @@ -36,10 +36,11 @@ use swagger::{ApiError, Context, XSpanId};  use models;  use {      AcceptEditgroupResponse, Api, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse, CreateEditgroupResponse, CreateFileBatchResponse, -    CreateFileResponse, CreateReleaseBatchResponse, CreateReleaseResponse, CreateWorkBatchResponse, CreateWorkResponse, GetChangelogEntryResponse, GetChangelogResponse, GetContainerHistoryResponse, -    GetContainerResponse, GetCreatorHistoryResponse, GetCreatorReleasesResponse, GetCreatorResponse, GetEditgroupResponse, GetEditorChangelogResponse, GetEditorResponse, GetFileHistoryResponse, -    GetFileResponse, GetReleaseFilesResponse, GetReleaseHistoryResponse, GetReleaseResponse, GetStatsResponse, GetWorkHistoryResponse, GetWorkReleasesResponse, GetWorkResponse, -    LookupContainerResponse, LookupCreatorResponse, LookupFileResponse, LookupReleaseResponse, +    CreateFileResponse, CreateReleaseBatchResponse, CreateReleaseResponse, CreateWorkBatchResponse, CreateWorkResponse, DeleteContainerResponse, DeleteCreatorResponse, DeleteFileResponse, +    DeleteReleaseResponse, DeleteWorkResponse, GetChangelogEntryResponse, GetChangelogResponse, GetContainerHistoryResponse, GetContainerResponse, GetCreatorHistoryResponse, +    GetCreatorReleasesResponse, GetCreatorResponse, GetEditgroupResponse, GetEditorChangelogResponse, GetEditorResponse, GetFileHistoryResponse, GetFileResponse, GetReleaseFilesResponse, +    GetReleaseHistoryResponse, GetReleaseResponse, GetStatsResponse, GetWorkHistoryResponse, GetWorkReleasesResponse, GetWorkResponse, LookupContainerResponse, LookupCreatorResponse, +    LookupFileResponse, LookupReleaseResponse, UpdateContainerResponse, UpdateCreatorResponse, UpdateFileResponse, UpdateReleaseResponse, UpdateWorkResponse,  };  /// Convert input into a base path, e.g. "http://example:123". Also checks the scheme as it goes. @@ -191,7 +192,7 @@ impl Api for Client {                      response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?;                      let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; -                    Ok(AcceptEditgroupResponse::Unmergable(body)) +                    Ok(AcceptEditgroupResponse::BadRequest(body))                  }                  404 => {                      let mut buf = String::new(); @@ -200,6 +201,13 @@ impl Api for Client {                      Ok(AcceptEditgroupResponse::NotFound(body))                  } +                409 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(AcceptEditgroupResponse::EditConflict(body)) +                }                  500 => {                      let mut buf = String::new();                      response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; @@ -1008,6 +1016,346 @@ impl Api for Client {          Box::new(futures::done(result))      } +    fn delete_container(&self, param_id: String, param_editgroup: Option<String>, context: &Context) -> Box<Future<Item = DeleteContainerResponse, Error = ApiError> + Send> { +        // Query parameters +        let query_editgroup = param_editgroup.map_or_else(String::new, |query| format!("editgroup={editgroup}&", editgroup = query.to_string())); + +        let url = format!( +            "{}/v0/container/{id}?{editgroup}", +            self.base_path, +            id = utf8_percent_encode(¶m_id.to_string(), PATH_SEGMENT_ENCODE_SET), +            editgroup = utf8_percent_encode(&query_editgroup, QUERY_ENCODE_SET) +        ); + +        let hyper_client = (self.hyper_client)(); +        let request = hyper_client.request(hyper::method::Method::Delete, &url); +        let mut custom_headers = hyper::header::Headers::new(); + +        context.x_span_id.as_ref().map(|header| custom_headers.set(XSpanId(header.clone()))); + +        let request = request.headers(custom_headers); + +        // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +        fn parse_response(mut response: hyper::client::response::Response) -> Result<DeleteContainerResponse, ApiError> { +            match response.status.to_u16() { +                200 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::EntityEdit>(&buf)?; + +                    Ok(DeleteContainerResponse::DeletedEntity(body)) +                } +                400 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(DeleteContainerResponse::BadRequest(body)) +                } +                404 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(DeleteContainerResponse::NotFound(body)) +                } +                500 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(DeleteContainerResponse::GenericError(body)) +                } +                code => { +                    let mut buf = [0; 100]; +                    let debug_body = match response.read(&mut buf) { +                        Ok(len) => match str::from_utf8(&buf[..len]) { +                            Ok(body) => Cow::from(body), +                            Err(_) => Cow::from(format!("<Body was not UTF8: {:?}>", &buf[..len].to_vec())), +                        }, +                        Err(e) => Cow::from(format!("<Failed to read body: {}>", e)), +                    }; +                    Err(ApiError(format!("Unexpected response code {}:\n{:?}\n\n{}", code, response.headers, debug_body))) +                } +            } +        } + +        let result = request.send().map_err(|e| ApiError(format!("No response received: {}", e))).and_then(parse_response); +        Box::new(futures::done(result)) +    } + +    fn delete_creator(&self, param_id: String, param_editgroup: Option<String>, context: &Context) -> Box<Future<Item = DeleteCreatorResponse, Error = ApiError> + Send> { +        // Query parameters +        let query_editgroup = param_editgroup.map_or_else(String::new, |query| format!("editgroup={editgroup}&", editgroup = query.to_string())); + +        let url = format!( +            "{}/v0/creator/{id}?{editgroup}", +            self.base_path, +            id = utf8_percent_encode(¶m_id.to_string(), PATH_SEGMENT_ENCODE_SET), +            editgroup = utf8_percent_encode(&query_editgroup, QUERY_ENCODE_SET) +        ); + +        let hyper_client = (self.hyper_client)(); +        let request = hyper_client.request(hyper::method::Method::Delete, &url); +        let mut custom_headers = hyper::header::Headers::new(); + +        context.x_span_id.as_ref().map(|header| custom_headers.set(XSpanId(header.clone()))); + +        let request = request.headers(custom_headers); + +        // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +        fn parse_response(mut response: hyper::client::response::Response) -> Result<DeleteCreatorResponse, ApiError> { +            match response.status.to_u16() { +                200 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::EntityEdit>(&buf)?; + +                    Ok(DeleteCreatorResponse::DeletedEntity(body)) +                } +                400 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(DeleteCreatorResponse::BadRequest(body)) +                } +                404 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(DeleteCreatorResponse::NotFound(body)) +                } +                500 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(DeleteCreatorResponse::GenericError(body)) +                } +                code => { +                    let mut buf = [0; 100]; +                    let debug_body = match response.read(&mut buf) { +                        Ok(len) => match str::from_utf8(&buf[..len]) { +                            Ok(body) => Cow::from(body), +                            Err(_) => Cow::from(format!("<Body was not UTF8: {:?}>", &buf[..len].to_vec())), +                        }, +                        Err(e) => Cow::from(format!("<Failed to read body: {}>", e)), +                    }; +                    Err(ApiError(format!("Unexpected response code {}:\n{:?}\n\n{}", code, response.headers, debug_body))) +                } +            } +        } + +        let result = request.send().map_err(|e| ApiError(format!("No response received: {}", e))).and_then(parse_response); +        Box::new(futures::done(result)) +    } + +    fn delete_file(&self, param_id: String, param_editgroup: Option<String>, context: &Context) -> Box<Future<Item = DeleteFileResponse, Error = ApiError> + Send> { +        // Query parameters +        let query_editgroup = param_editgroup.map_or_else(String::new, |query| format!("editgroup={editgroup}&", editgroup = query.to_string())); + +        let url = format!( +            "{}/v0/file/{id}?{editgroup}", +            self.base_path, +            id = utf8_percent_encode(¶m_id.to_string(), PATH_SEGMENT_ENCODE_SET), +            editgroup = utf8_percent_encode(&query_editgroup, QUERY_ENCODE_SET) +        ); + +        let hyper_client = (self.hyper_client)(); +        let request = hyper_client.request(hyper::method::Method::Delete, &url); +        let mut custom_headers = hyper::header::Headers::new(); + +        context.x_span_id.as_ref().map(|header| custom_headers.set(XSpanId(header.clone()))); + +        let request = request.headers(custom_headers); + +        // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +        fn parse_response(mut response: hyper::client::response::Response) -> Result<DeleteFileResponse, ApiError> { +            match response.status.to_u16() { +                200 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::EntityEdit>(&buf)?; + +                    Ok(DeleteFileResponse::DeletedEntity(body)) +                } +                400 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(DeleteFileResponse::BadRequest(body)) +                } +                404 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(DeleteFileResponse::NotFound(body)) +                } +                500 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(DeleteFileResponse::GenericError(body)) +                } +                code => { +                    let mut buf = [0; 100]; +                    let debug_body = match response.read(&mut buf) { +                        Ok(len) => match str::from_utf8(&buf[..len]) { +                            Ok(body) => Cow::from(body), +                            Err(_) => Cow::from(format!("<Body was not UTF8: {:?}>", &buf[..len].to_vec())), +                        }, +                        Err(e) => Cow::from(format!("<Failed to read body: {}>", e)), +                    }; +                    Err(ApiError(format!("Unexpected response code {}:\n{:?}\n\n{}", code, response.headers, debug_body))) +                } +            } +        } + +        let result = request.send().map_err(|e| ApiError(format!("No response received: {}", e))).and_then(parse_response); +        Box::new(futures::done(result)) +    } + +    fn delete_release(&self, param_id: String, param_editgroup: Option<String>, context: &Context) -> Box<Future<Item = DeleteReleaseResponse, Error = ApiError> + Send> { +        // Query parameters +        let query_editgroup = param_editgroup.map_or_else(String::new, |query| format!("editgroup={editgroup}&", editgroup = query.to_string())); + +        let url = format!( +            "{}/v0/release/{id}?{editgroup}", +            self.base_path, +            id = utf8_percent_encode(¶m_id.to_string(), PATH_SEGMENT_ENCODE_SET), +            editgroup = utf8_percent_encode(&query_editgroup, QUERY_ENCODE_SET) +        ); + +        let hyper_client = (self.hyper_client)(); +        let request = hyper_client.request(hyper::method::Method::Delete, &url); +        let mut custom_headers = hyper::header::Headers::new(); + +        context.x_span_id.as_ref().map(|header| custom_headers.set(XSpanId(header.clone()))); + +        let request = request.headers(custom_headers); + +        // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +        fn parse_response(mut response: hyper::client::response::Response) -> Result<DeleteReleaseResponse, ApiError> { +            match response.status.to_u16() { +                200 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::EntityEdit>(&buf)?; + +                    Ok(DeleteReleaseResponse::DeletedEntity(body)) +                } +                400 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(DeleteReleaseResponse::BadRequest(body)) +                } +                404 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(DeleteReleaseResponse::NotFound(body)) +                } +                500 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(DeleteReleaseResponse::GenericError(body)) +                } +                code => { +                    let mut buf = [0; 100]; +                    let debug_body = match response.read(&mut buf) { +                        Ok(len) => match str::from_utf8(&buf[..len]) { +                            Ok(body) => Cow::from(body), +                            Err(_) => Cow::from(format!("<Body was not UTF8: {:?}>", &buf[..len].to_vec())), +                        }, +                        Err(e) => Cow::from(format!("<Failed to read body: {}>", e)), +                    }; +                    Err(ApiError(format!("Unexpected response code {}:\n{:?}\n\n{}", code, response.headers, debug_body))) +                } +            } +        } + +        let result = request.send().map_err(|e| ApiError(format!("No response received: {}", e))).and_then(parse_response); +        Box::new(futures::done(result)) +    } + +    fn delete_work(&self, param_id: String, param_editgroup: Option<String>, context: &Context) -> Box<Future<Item = DeleteWorkResponse, Error = ApiError> + Send> { +        // Query parameters +        let query_editgroup = param_editgroup.map_or_else(String::new, |query| format!("editgroup={editgroup}&", editgroup = query.to_string())); + +        let url = format!( +            "{}/v0/work/{id}?{editgroup}", +            self.base_path, +            id = utf8_percent_encode(¶m_id.to_string(), PATH_SEGMENT_ENCODE_SET), +            editgroup = utf8_percent_encode(&query_editgroup, QUERY_ENCODE_SET) +        ); + +        let hyper_client = (self.hyper_client)(); +        let request = hyper_client.request(hyper::method::Method::Delete, &url); +        let mut custom_headers = hyper::header::Headers::new(); + +        context.x_span_id.as_ref().map(|header| custom_headers.set(XSpanId(header.clone()))); + +        let request = request.headers(custom_headers); + +        // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +        fn parse_response(mut response: hyper::client::response::Response) -> Result<DeleteWorkResponse, ApiError> { +            match response.status.to_u16() { +                200 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::EntityEdit>(&buf)?; + +                    Ok(DeleteWorkResponse::DeletedEntity(body)) +                } +                400 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(DeleteWorkResponse::BadRequest(body)) +                } +                404 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(DeleteWorkResponse::NotFound(body)) +                } +                500 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(DeleteWorkResponse::GenericError(body)) +                } +                code => { +                    let mut buf = [0; 100]; +                    let debug_body = match response.read(&mut buf) { +                        Ok(len) => match str::from_utf8(&buf[..len]) { +                            Ok(body) => Cow::from(body), +                            Err(_) => Cow::from(format!("<Body was not UTF8: {:?}>", &buf[..len].to_vec())), +                        }, +                        Err(e) => Cow::from(format!("<Failed to read body: {}>", e)), +                    }; +                    Err(ApiError(format!("Unexpected response code {}:\n{:?}\n\n{}", code, response.headers, debug_body))) +                } +            } +        } + +        let result = request.send().map_err(|e| ApiError(format!("No response received: {}", e))).and_then(parse_response); +        Box::new(futures::done(result)) +    } +      fn get_changelog(&self, param_limit: Option<i64>, context: &Context) -> Box<Future<Item = GetChangelogResponse, Error = ApiError> + Send> {          // Query parameters          let query_limit = param_limit.map_or_else(String::new, |query| format!("limit={limit}&", limit = query.to_string())); @@ -2436,6 +2784,331 @@ impl Api for Client {          let result = request.send().map_err(|e| ApiError(format!("No response received: {}", e))).and_then(parse_response);          Box::new(futures::done(result))      } + +    fn update_container(&self, param_id: String, param_entity: models::ContainerEntity, context: &Context) -> Box<Future<Item = UpdateContainerResponse, Error = ApiError> + Send> { +        let url = format!("{}/v0/container/{id}", self.base_path, id = utf8_percent_encode(¶m_id.to_string(), PATH_SEGMENT_ENCODE_SET)); + +        let body = serde_json::to_string(¶m_entity).expect("impossible to fail to serialize"); + +        let hyper_client = (self.hyper_client)(); +        let request = hyper_client.request(hyper::method::Method::Put, &url); +        let mut custom_headers = hyper::header::Headers::new(); + +        let request = request.body(&body); + +        custom_headers.set(ContentType(mimetypes::requests::UPDATE_CONTAINER.clone())); +        context.x_span_id.as_ref().map(|header| custom_headers.set(XSpanId(header.clone()))); + +        let request = request.headers(custom_headers); + +        // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +        fn parse_response(mut response: hyper::client::response::Response) -> Result<UpdateContainerResponse, ApiError> { +            match response.status.to_u16() { +                200 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::EntityEdit>(&buf)?; + +                    Ok(UpdateContainerResponse::UpdatedEntity(body)) +                } +                400 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(UpdateContainerResponse::BadRequest(body)) +                } +                404 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(UpdateContainerResponse::NotFound(body)) +                } +                500 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(UpdateContainerResponse::GenericError(body)) +                } +                code => { +                    let mut buf = [0; 100]; +                    let debug_body = match response.read(&mut buf) { +                        Ok(len) => match str::from_utf8(&buf[..len]) { +                            Ok(body) => Cow::from(body), +                            Err(_) => Cow::from(format!("<Body was not UTF8: {:?}>", &buf[..len].to_vec())), +                        }, +                        Err(e) => Cow::from(format!("<Failed to read body: {}>", e)), +                    }; +                    Err(ApiError(format!("Unexpected response code {}:\n{:?}\n\n{}", code, response.headers, debug_body))) +                } +            } +        } + +        let result = request.send().map_err(|e| ApiError(format!("No response received: {}", e))).and_then(parse_response); +        Box::new(futures::done(result)) +    } + +    fn update_creator(&self, param_id: String, param_entity: models::CreatorEntity, context: &Context) -> Box<Future<Item = UpdateCreatorResponse, Error = ApiError> + Send> { +        let url = format!("{}/v0/creator/{id}", self.base_path, id = utf8_percent_encode(¶m_id.to_string(), PATH_SEGMENT_ENCODE_SET)); + +        let body = serde_json::to_string(¶m_entity).expect("impossible to fail to serialize"); + +        let hyper_client = (self.hyper_client)(); +        let request = hyper_client.request(hyper::method::Method::Put, &url); +        let mut custom_headers = hyper::header::Headers::new(); + +        let request = request.body(&body); + +        custom_headers.set(ContentType(mimetypes::requests::UPDATE_CREATOR.clone())); +        context.x_span_id.as_ref().map(|header| custom_headers.set(XSpanId(header.clone()))); + +        let request = request.headers(custom_headers); + +        // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +        fn parse_response(mut response: hyper::client::response::Response) -> Result<UpdateCreatorResponse, ApiError> { +            match response.status.to_u16() { +                200 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::EntityEdit>(&buf)?; + +                    Ok(UpdateCreatorResponse::UpdatedEntity(body)) +                } +                400 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(UpdateCreatorResponse::BadRequest(body)) +                } +                404 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(UpdateCreatorResponse::NotFound(body)) +                } +                500 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(UpdateCreatorResponse::GenericError(body)) +                } +                code => { +                    let mut buf = [0; 100]; +                    let debug_body = match response.read(&mut buf) { +                        Ok(len) => match str::from_utf8(&buf[..len]) { +                            Ok(body) => Cow::from(body), +                            Err(_) => Cow::from(format!("<Body was not UTF8: {:?}>", &buf[..len].to_vec())), +                        }, +                        Err(e) => Cow::from(format!("<Failed to read body: {}>", e)), +                    }; +                    Err(ApiError(format!("Unexpected response code {}:\n{:?}\n\n{}", code, response.headers, debug_body))) +                } +            } +        } + +        let result = request.send().map_err(|e| ApiError(format!("No response received: {}", e))).and_then(parse_response); +        Box::new(futures::done(result)) +    } + +    fn update_file(&self, param_id: String, param_entity: models::FileEntity, context: &Context) -> Box<Future<Item = UpdateFileResponse, Error = ApiError> + Send> { +        let url = format!("{}/v0/file/{id}", self.base_path, id = utf8_percent_encode(¶m_id.to_string(), PATH_SEGMENT_ENCODE_SET)); + +        let body = serde_json::to_string(¶m_entity).expect("impossible to fail to serialize"); + +        let hyper_client = (self.hyper_client)(); +        let request = hyper_client.request(hyper::method::Method::Put, &url); +        let mut custom_headers = hyper::header::Headers::new(); + +        let request = request.body(&body); + +        custom_headers.set(ContentType(mimetypes::requests::UPDATE_FILE.clone())); +        context.x_span_id.as_ref().map(|header| custom_headers.set(XSpanId(header.clone()))); + +        let request = request.headers(custom_headers); + +        // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +        fn parse_response(mut response: hyper::client::response::Response) -> Result<UpdateFileResponse, ApiError> { +            match response.status.to_u16() { +                200 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::EntityEdit>(&buf)?; + +                    Ok(UpdateFileResponse::UpdatedEntity(body)) +                } +                400 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(UpdateFileResponse::BadRequest(body)) +                } +                404 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(UpdateFileResponse::NotFound(body)) +                } +                500 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(UpdateFileResponse::GenericError(body)) +                } +                code => { +                    let mut buf = [0; 100]; +                    let debug_body = match response.read(&mut buf) { +                        Ok(len) => match str::from_utf8(&buf[..len]) { +                            Ok(body) => Cow::from(body), +                            Err(_) => Cow::from(format!("<Body was not UTF8: {:?}>", &buf[..len].to_vec())), +                        }, +                        Err(e) => Cow::from(format!("<Failed to read body: {}>", e)), +                    }; +                    Err(ApiError(format!("Unexpected response code {}:\n{:?}\n\n{}", code, response.headers, debug_body))) +                } +            } +        } + +        let result = request.send().map_err(|e| ApiError(format!("No response received: {}", e))).and_then(parse_response); +        Box::new(futures::done(result)) +    } + +    fn update_release(&self, param_id: String, param_entity: models::ReleaseEntity, context: &Context) -> Box<Future<Item = UpdateReleaseResponse, Error = ApiError> + Send> { +        let url = format!("{}/v0/release/{id}", self.base_path, id = utf8_percent_encode(¶m_id.to_string(), PATH_SEGMENT_ENCODE_SET)); + +        let body = serde_json::to_string(¶m_entity).expect("impossible to fail to serialize"); + +        let hyper_client = (self.hyper_client)(); +        let request = hyper_client.request(hyper::method::Method::Put, &url); +        let mut custom_headers = hyper::header::Headers::new(); + +        let request = request.body(&body); + +        custom_headers.set(ContentType(mimetypes::requests::UPDATE_RELEASE.clone())); +        context.x_span_id.as_ref().map(|header| custom_headers.set(XSpanId(header.clone()))); + +        let request = request.headers(custom_headers); + +        // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +        fn parse_response(mut response: hyper::client::response::Response) -> Result<UpdateReleaseResponse, ApiError> { +            match response.status.to_u16() { +                200 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::EntityEdit>(&buf)?; + +                    Ok(UpdateReleaseResponse::UpdatedEntity(body)) +                } +                400 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(UpdateReleaseResponse::BadRequest(body)) +                } +                404 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(UpdateReleaseResponse::NotFound(body)) +                } +                500 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(UpdateReleaseResponse::GenericError(body)) +                } +                code => { +                    let mut buf = [0; 100]; +                    let debug_body = match response.read(&mut buf) { +                        Ok(len) => match str::from_utf8(&buf[..len]) { +                            Ok(body) => Cow::from(body), +                            Err(_) => Cow::from(format!("<Body was not UTF8: {:?}>", &buf[..len].to_vec())), +                        }, +                        Err(e) => Cow::from(format!("<Failed to read body: {}>", e)), +                    }; +                    Err(ApiError(format!("Unexpected response code {}:\n{:?}\n\n{}", code, response.headers, debug_body))) +                } +            } +        } + +        let result = request.send().map_err(|e| ApiError(format!("No response received: {}", e))).and_then(parse_response); +        Box::new(futures::done(result)) +    } + +    fn update_work(&self, param_id: String, param_entity: models::WorkEntity, context: &Context) -> Box<Future<Item = UpdateWorkResponse, Error = ApiError> + Send> { +        let url = format!("{}/v0/work/{id}", self.base_path, id = utf8_percent_encode(¶m_id.to_string(), PATH_SEGMENT_ENCODE_SET)); + +        let body = serde_json::to_string(¶m_entity).expect("impossible to fail to serialize"); + +        let hyper_client = (self.hyper_client)(); +        let request = hyper_client.request(hyper::method::Method::Put, &url); +        let mut custom_headers = hyper::header::Headers::new(); + +        let request = request.body(&body); + +        custom_headers.set(ContentType(mimetypes::requests::UPDATE_WORK.clone())); +        context.x_span_id.as_ref().map(|header| custom_headers.set(XSpanId(header.clone()))); + +        let request = request.headers(custom_headers); + +        // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +        fn parse_response(mut response: hyper::client::response::Response) -> Result<UpdateWorkResponse, ApiError> { +            match response.status.to_u16() { +                200 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::EntityEdit>(&buf)?; + +                    Ok(UpdateWorkResponse::UpdatedEntity(body)) +                } +                400 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(UpdateWorkResponse::BadRequest(body)) +                } +                404 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(UpdateWorkResponse::NotFound(body)) +                } +                500 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(UpdateWorkResponse::GenericError(body)) +                } +                code => { +                    let mut buf = [0; 100]; +                    let debug_body = match response.read(&mut buf) { +                        Ok(len) => match str::from_utf8(&buf[..len]) { +                            Ok(body) => Cow::from(body), +                            Err(_) => Cow::from(format!("<Body was not UTF8: {:?}>", &buf[..len].to_vec())), +                        }, +                        Err(e) => Cow::from(format!("<Failed to read body: {}>", e)), +                    }; +                    Err(ApiError(format!("Unexpected response code {}:\n{:?}\n\n{}", code, response.headers, debug_body))) +                } +            } +        } + +        let result = request.send().map_err(|e| ApiError(format!("No response received: {}", e))).and_then(parse_response); +        Box::new(futures::done(result)) +    }  }  #[derive(Debug)] diff --git a/rust/fatcat-api/src/lib.rs b/rust/fatcat-api/src/lib.rs index 5de3647b..fc1ae2a1 100644 --- a/rust/fatcat-api/src/lib.rs +++ b/rust/fatcat-api/src/lib.rs @@ -36,10 +36,12 @@ pub use swagger::{ApiError, Context, ContextWrapper};  pub enum AcceptEditgroupResponse {      /// Merged Successfully      MergedSuccessfully(models::Success), -    /// Unmergable -    Unmergable(models::ErrorResponse), +    /// Bad Request +    BadRequest(models::ErrorResponse),      /// Not Found      NotFound(models::ErrorResponse), +    /// Edit Conflict +    EditConflict(models::ErrorResponse),      /// Generic Error      GenericError(models::ErrorResponse),  } @@ -175,6 +177,66 @@ pub enum CreateWorkBatchResponse {  }  #[derive(Debug, PartialEq)] +pub enum DeleteContainerResponse { +    /// Deleted Entity +    DeletedEntity(models::EntityEdit), +    /// Bad Request +    BadRequest(models::ErrorResponse), +    /// Not Found +    NotFound(models::ErrorResponse), +    /// Generic Error +    GenericError(models::ErrorResponse), +} + +#[derive(Debug, PartialEq)] +pub enum DeleteCreatorResponse { +    /// Deleted Entity +    DeletedEntity(models::EntityEdit), +    /// Bad Request +    BadRequest(models::ErrorResponse), +    /// Not Found +    NotFound(models::ErrorResponse), +    /// Generic Error +    GenericError(models::ErrorResponse), +} + +#[derive(Debug, PartialEq)] +pub enum DeleteFileResponse { +    /// Deleted Entity +    DeletedEntity(models::EntityEdit), +    /// Bad Request +    BadRequest(models::ErrorResponse), +    /// Not Found +    NotFound(models::ErrorResponse), +    /// Generic Error +    GenericError(models::ErrorResponse), +} + +#[derive(Debug, PartialEq)] +pub enum DeleteReleaseResponse { +    /// Deleted Entity +    DeletedEntity(models::EntityEdit), +    /// Bad Request +    BadRequest(models::ErrorResponse), +    /// Not Found +    NotFound(models::ErrorResponse), +    /// Generic Error +    GenericError(models::ErrorResponse), +} + +#[derive(Debug, PartialEq)] +pub enum DeleteWorkResponse { +    /// Deleted Entity +    DeletedEntity(models::EntityEdit), +    /// Bad Request +    BadRequest(models::ErrorResponse), +    /// Not Found +    NotFound(models::ErrorResponse), +    /// Generic Error +    GenericError(models::ErrorResponse), +} + +#[derive(Debug, PartialEq)]  pub enum GetChangelogResponse {      /// Success      Success(Vec<models::ChangelogEntry>), @@ -436,6 +498,66 @@ pub enum LookupReleaseResponse {      GenericError(models::ErrorResponse),  } +#[derive(Debug, PartialEq)] +pub enum UpdateContainerResponse { +    /// Updated Entity +    UpdatedEntity(models::EntityEdit), +    /// Bad Request +    BadRequest(models::ErrorResponse), +    /// Not Found +    NotFound(models::ErrorResponse), +    /// Generic Error +    GenericError(models::ErrorResponse), +} + +#[derive(Debug, PartialEq)] +pub enum UpdateCreatorResponse { +    /// Updated Entity +    UpdatedEntity(models::EntityEdit), +    /// Bad Request +    BadRequest(models::ErrorResponse), +    /// Not Found +    NotFound(models::ErrorResponse), +    /// Generic Error +    GenericError(models::ErrorResponse), +} + +#[derive(Debug, PartialEq)] +pub enum UpdateFileResponse { +    /// Updated Entity +    UpdatedEntity(models::EntityEdit), +    /// Bad Request +    BadRequest(models::ErrorResponse), +    /// Not Found +    NotFound(models::ErrorResponse), +    /// Generic Error +    GenericError(models::ErrorResponse), +} + +#[derive(Debug, PartialEq)] +pub enum UpdateReleaseResponse { +    /// Updated Entity +    UpdatedEntity(models::EntityEdit), +    /// Bad Request +    BadRequest(models::ErrorResponse), +    /// Not Found +    NotFound(models::ErrorResponse), +    /// Generic Error +    GenericError(models::ErrorResponse), +} + +#[derive(Debug, PartialEq)] +pub enum UpdateWorkResponse { +    /// Updated Entity +    UpdatedEntity(models::EntityEdit), +    /// Bad Request +    BadRequest(models::ErrorResponse), +    /// Not Found +    NotFound(models::ErrorResponse), +    /// Generic Error +    GenericError(models::ErrorResponse), +} +  /// API  pub trait Api {      fn accept_editgroup(&self, id: String, context: &Context) -> Box<Future<Item = AcceptEditgroupResponse, Error = ApiError> + Send>; @@ -492,6 +614,16 @@ pub trait Api {          context: &Context,      ) -> Box<Future<Item = CreateWorkBatchResponse, Error = ApiError> + Send>; +    fn delete_container(&self, id: String, editgroup: Option<String>, context: &Context) -> Box<Future<Item = DeleteContainerResponse, Error = ApiError> + Send>; + +    fn delete_creator(&self, id: String, editgroup: Option<String>, context: &Context) -> Box<Future<Item = DeleteCreatorResponse, Error = ApiError> + Send>; + +    fn delete_file(&self, id: String, editgroup: Option<String>, context: &Context) -> Box<Future<Item = DeleteFileResponse, Error = ApiError> + Send>; + +    fn delete_release(&self, id: String, editgroup: Option<String>, context: &Context) -> Box<Future<Item = DeleteReleaseResponse, Error = ApiError> + Send>; + +    fn delete_work(&self, id: String, editgroup: Option<String>, context: &Context) -> Box<Future<Item = DeleteWorkResponse, Error = ApiError> + Send>; +      fn get_changelog(&self, limit: Option<i64>, context: &Context) -> Box<Future<Item = GetChangelogResponse, Error = ApiError> + Send>;      fn get_changelog_entry(&self, id: i64, context: &Context) -> Box<Future<Item = GetChangelogEntryResponse, Error = ApiError> + Send>; @@ -537,6 +669,16 @@ pub trait Api {      fn lookup_file(&self, sha1: String, context: &Context) -> Box<Future<Item = LookupFileResponse, Error = ApiError> + Send>;      fn lookup_release(&self, doi: String, context: &Context) -> Box<Future<Item = LookupReleaseResponse, Error = ApiError> + Send>; + +    fn update_container(&self, id: String, entity: models::ContainerEntity, context: &Context) -> Box<Future<Item = UpdateContainerResponse, Error = ApiError> + Send>; + +    fn update_creator(&self, id: String, entity: models::CreatorEntity, context: &Context) -> Box<Future<Item = UpdateCreatorResponse, Error = ApiError> + Send>; + +    fn update_file(&self, id: String, entity: models::FileEntity, context: &Context) -> Box<Future<Item = UpdateFileResponse, Error = ApiError> + Send>; + +    fn update_release(&self, id: String, entity: models::ReleaseEntity, context: &Context) -> Box<Future<Item = UpdateReleaseResponse, Error = ApiError> + Send>; + +    fn update_work(&self, id: String, entity: models::WorkEntity, context: &Context) -> Box<Future<Item = UpdateWorkResponse, Error = ApiError> + Send>;  }  /// API without a `Context` @@ -580,6 +722,16 @@ pub trait ApiNoContext {      fn create_work_batch(&self, entity_list: &Vec<models::WorkEntity>, autoaccept: Option<bool>, editgroup: Option<String>) -> Box<Future<Item = CreateWorkBatchResponse, Error = ApiError> + Send>; +    fn delete_container(&self, id: String, editgroup: Option<String>) -> Box<Future<Item = DeleteContainerResponse, Error = ApiError> + Send>; + +    fn delete_creator(&self, id: String, editgroup: Option<String>) -> Box<Future<Item = DeleteCreatorResponse, Error = ApiError> + Send>; + +    fn delete_file(&self, id: String, editgroup: Option<String>) -> Box<Future<Item = DeleteFileResponse, Error = ApiError> + Send>; + +    fn delete_release(&self, id: String, editgroup: Option<String>) -> Box<Future<Item = DeleteReleaseResponse, Error = ApiError> + Send>; + +    fn delete_work(&self, id: String, editgroup: Option<String>) -> Box<Future<Item = DeleteWorkResponse, Error = ApiError> + Send>; +      fn get_changelog(&self, limit: Option<i64>) -> Box<Future<Item = GetChangelogResponse, Error = ApiError> + Send>;      fn get_changelog_entry(&self, id: i64) -> Box<Future<Item = GetChangelogEntryResponse, Error = ApiError> + Send>; @@ -625,6 +777,16 @@ pub trait ApiNoContext {      fn lookup_file(&self, sha1: String) -> Box<Future<Item = LookupFileResponse, Error = ApiError> + Send>;      fn lookup_release(&self, doi: String) -> Box<Future<Item = LookupReleaseResponse, Error = ApiError> + Send>; + +    fn update_container(&self, id: String, entity: models::ContainerEntity) -> Box<Future<Item = UpdateContainerResponse, Error = ApiError> + Send>; + +    fn update_creator(&self, id: String, entity: models::CreatorEntity) -> Box<Future<Item = UpdateCreatorResponse, Error = ApiError> + Send>; + +    fn update_file(&self, id: String, entity: models::FileEntity) -> Box<Future<Item = UpdateFileResponse, Error = ApiError> + Send>; + +    fn update_release(&self, id: String, entity: models::ReleaseEntity) -> Box<Future<Item = UpdateReleaseResponse, Error = ApiError> + Send>; + +    fn update_work(&self, id: String, entity: models::WorkEntity) -> Box<Future<Item = UpdateWorkResponse, Error = ApiError> + Send>;  }  /// Trait to extend an API to make it easy to bind it to a context. @@ -706,6 +868,26 @@ impl<'a, T: Api> ApiNoContext for ContextWrapper<'a, T> {          self.api().create_work_batch(entity_list, autoaccept, editgroup, &self.context())      } +    fn delete_container(&self, id: String, editgroup: Option<String>) -> Box<Future<Item = DeleteContainerResponse, Error = ApiError> + Send> { +        self.api().delete_container(id, editgroup, &self.context()) +    } + +    fn delete_creator(&self, id: String, editgroup: Option<String>) -> Box<Future<Item = DeleteCreatorResponse, Error = ApiError> + Send> { +        self.api().delete_creator(id, editgroup, &self.context()) +    } + +    fn delete_file(&self, id: String, editgroup: Option<String>) -> Box<Future<Item = DeleteFileResponse, Error = ApiError> + Send> { +        self.api().delete_file(id, editgroup, &self.context()) +    } + +    fn delete_release(&self, id: String, editgroup: Option<String>) -> Box<Future<Item = DeleteReleaseResponse, Error = ApiError> + Send> { +        self.api().delete_release(id, editgroup, &self.context()) +    } + +    fn delete_work(&self, id: String, editgroup: Option<String>) -> Box<Future<Item = DeleteWorkResponse, Error = ApiError> + Send> { +        self.api().delete_work(id, editgroup, &self.context()) +    } +      fn get_changelog(&self, limit: Option<i64>) -> Box<Future<Item = GetChangelogResponse, Error = ApiError> + Send> {          self.api().get_changelog(limit, &self.context())      } @@ -797,6 +979,26 @@ impl<'a, T: Api> ApiNoContext for ContextWrapper<'a, T> {      fn lookup_release(&self, doi: String) -> Box<Future<Item = LookupReleaseResponse, Error = ApiError> + Send> {          self.api().lookup_release(doi, &self.context())      } + +    fn update_container(&self, id: String, entity: models::ContainerEntity) -> Box<Future<Item = UpdateContainerResponse, Error = ApiError> + Send> { +        self.api().update_container(id, entity, &self.context()) +    } + +    fn update_creator(&self, id: String, entity: models::CreatorEntity) -> Box<Future<Item = UpdateCreatorResponse, Error = ApiError> + Send> { +        self.api().update_creator(id, entity, &self.context()) +    } + +    fn update_file(&self, id: String, entity: models::FileEntity) -> Box<Future<Item = UpdateFileResponse, Error = ApiError> + Send> { +        self.api().update_file(id, entity, &self.context()) +    } + +    fn update_release(&self, id: String, entity: models::ReleaseEntity) -> Box<Future<Item = UpdateReleaseResponse, Error = ApiError> + Send> { +        self.api().update_release(id, entity, &self.context()) +    } + +    fn update_work(&self, id: String, entity: models::WorkEntity) -> Box<Future<Item = UpdateWorkResponse, Error = ApiError> + Send> { +        self.api().update_work(id, entity, &self.context()) +    }  }  #[cfg(feature = "client")] diff --git a/rust/fatcat-api/src/mimetypes.rs b/rust/fatcat-api/src/mimetypes.rs index 53b582dc..2c54a313 100644 --- a/rust/fatcat-api/src/mimetypes.rs +++ b/rust/fatcat-api/src/mimetypes.rs @@ -10,7 +10,7 @@ pub mod responses {      }      /// Create Mime objects for the response content types for AcceptEditgroup      lazy_static! { -        pub static ref ACCEPT_EDITGROUP_UNMERGABLE: Mime = mime!(Application / Json); +        pub static ref ACCEPT_EDITGROUP_BAD_REQUEST: Mime = mime!(Application / Json);      }      /// Create Mime objects for the response content types for AcceptEditgroup      lazy_static! { @@ -18,6 +18,10 @@ pub mod responses {      }      /// Create Mime objects for the response content types for AcceptEditgroup      lazy_static! { +        pub static ref ACCEPT_EDITGROUP_EDIT_CONFLICT: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for AcceptEditgroup +    lazy_static! {          pub static ref ACCEPT_EDITGROUP_GENERIC_ERROR: Mime = mime!(Application / Json);      }      /// Create Mime objects for the response content types for CreateContainer @@ -192,6 +196,86 @@ pub mod responses {      lazy_static! {          pub static ref CREATE_WORK_BATCH_GENERIC_ERROR: Mime = mime!(Application / Json);      } +    /// Create Mime objects for the response content types for DeleteContainer +    lazy_static! { +        pub static ref DELETE_CONTAINER_DELETED_ENTITY: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for DeleteContainer +    lazy_static! { +        pub static ref DELETE_CONTAINER_BAD_REQUEST: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for DeleteContainer +    lazy_static! { +        pub static ref DELETE_CONTAINER_NOT_FOUND: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for DeleteContainer +    lazy_static! { +        pub static ref DELETE_CONTAINER_GENERIC_ERROR: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for DeleteCreator +    lazy_static! { +        pub static ref DELETE_CREATOR_DELETED_ENTITY: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for DeleteCreator +    lazy_static! { +        pub static ref DELETE_CREATOR_BAD_REQUEST: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for DeleteCreator +    lazy_static! { +        pub static ref DELETE_CREATOR_NOT_FOUND: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for DeleteCreator +    lazy_static! { +        pub static ref DELETE_CREATOR_GENERIC_ERROR: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for DeleteFile +    lazy_static! { +        pub static ref DELETE_FILE_DELETED_ENTITY: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for DeleteFile +    lazy_static! { +        pub static ref DELETE_FILE_BAD_REQUEST: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for DeleteFile +    lazy_static! { +        pub static ref DELETE_FILE_NOT_FOUND: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for DeleteFile +    lazy_static! { +        pub static ref DELETE_FILE_GENERIC_ERROR: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for DeleteRelease +    lazy_static! { +        pub static ref DELETE_RELEASE_DELETED_ENTITY: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for DeleteRelease +    lazy_static! { +        pub static ref DELETE_RELEASE_BAD_REQUEST: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for DeleteRelease +    lazy_static! { +        pub static ref DELETE_RELEASE_NOT_FOUND: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for DeleteRelease +    lazy_static! { +        pub static ref DELETE_RELEASE_GENERIC_ERROR: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for DeleteWork +    lazy_static! { +        pub static ref DELETE_WORK_DELETED_ENTITY: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for DeleteWork +    lazy_static! { +        pub static ref DELETE_WORK_BAD_REQUEST: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for DeleteWork +    lazy_static! { +        pub static ref DELETE_WORK_NOT_FOUND: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for DeleteWork +    lazy_static! { +        pub static ref DELETE_WORK_GENERIC_ERROR: Mime = mime!(Application / Json); +    }      /// Create Mime objects for the response content types for GetChangelog      lazy_static! {          pub static ref GET_CHANGELOG_SUCCESS: Mime = mime!(Application / Json); @@ -532,6 +616,86 @@ pub mod responses {      lazy_static! {          pub static ref LOOKUP_RELEASE_GENERIC_ERROR: Mime = mime!(Application / Json);      } +    /// Create Mime objects for the response content types for UpdateContainer +    lazy_static! { +        pub static ref UPDATE_CONTAINER_UPDATED_ENTITY: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for UpdateContainer +    lazy_static! { +        pub static ref UPDATE_CONTAINER_BAD_REQUEST: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for UpdateContainer +    lazy_static! { +        pub static ref UPDATE_CONTAINER_NOT_FOUND: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for UpdateContainer +    lazy_static! { +        pub static ref UPDATE_CONTAINER_GENERIC_ERROR: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for UpdateCreator +    lazy_static! { +        pub static ref UPDATE_CREATOR_UPDATED_ENTITY: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for UpdateCreator +    lazy_static! { +        pub static ref UPDATE_CREATOR_BAD_REQUEST: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for UpdateCreator +    lazy_static! { +        pub static ref UPDATE_CREATOR_NOT_FOUND: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for UpdateCreator +    lazy_static! { +        pub static ref UPDATE_CREATOR_GENERIC_ERROR: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for UpdateFile +    lazy_static! { +        pub static ref UPDATE_FILE_UPDATED_ENTITY: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for UpdateFile +    lazy_static! { +        pub static ref UPDATE_FILE_BAD_REQUEST: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for UpdateFile +    lazy_static! { +        pub static ref UPDATE_FILE_NOT_FOUND: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for UpdateFile +    lazy_static! { +        pub static ref UPDATE_FILE_GENERIC_ERROR: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for UpdateRelease +    lazy_static! { +        pub static ref UPDATE_RELEASE_UPDATED_ENTITY: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for UpdateRelease +    lazy_static! { +        pub static ref UPDATE_RELEASE_BAD_REQUEST: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for UpdateRelease +    lazy_static! { +        pub static ref UPDATE_RELEASE_NOT_FOUND: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for UpdateRelease +    lazy_static! { +        pub static ref UPDATE_RELEASE_GENERIC_ERROR: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for UpdateWork +    lazy_static! { +        pub static ref UPDATE_WORK_UPDATED_ENTITY: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for UpdateWork +    lazy_static! { +        pub static ref UPDATE_WORK_BAD_REQUEST: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for UpdateWork +    lazy_static! { +        pub static ref UPDATE_WORK_NOT_FOUND: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for UpdateWork +    lazy_static! { +        pub static ref UPDATE_WORK_GENERIC_ERROR: Mime = mime!(Application / Json); +    }  } @@ -581,5 +745,25 @@ pub mod requests {      lazy_static! {          pub static ref CREATE_WORK_BATCH: Mime = mime!(Application / Json);      } +    /// Create Mime objects for the request content types for UpdateContainer +    lazy_static! { +        pub static ref UPDATE_CONTAINER: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the request content types for UpdateCreator +    lazy_static! { +        pub static ref UPDATE_CREATOR: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the request content types for UpdateFile +    lazy_static! { +        pub static ref UPDATE_FILE: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the request content types for UpdateRelease +    lazy_static! { +        pub static ref UPDATE_RELEASE: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the request content types for UpdateWork +    lazy_static! { +        pub static ref UPDATE_WORK: Mime = mime!(Application / Json); +    }  } diff --git a/rust/fatcat-api/src/server.rs b/rust/fatcat-api/src/server.rs index 1ba9a218..04d10e14 100644 --- a/rust/fatcat-api/src/server.rs +++ b/rust/fatcat-api/src/server.rs @@ -38,10 +38,11 @@ use swagger::{ApiError, Context, XSpanId};  use models;  use {      AcceptEditgroupResponse, Api, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse, CreateEditgroupResponse, CreateFileBatchResponse, -    CreateFileResponse, CreateReleaseBatchResponse, CreateReleaseResponse, CreateWorkBatchResponse, CreateWorkResponse, GetChangelogEntryResponse, GetChangelogResponse, GetContainerHistoryResponse, -    GetContainerResponse, GetCreatorHistoryResponse, GetCreatorReleasesResponse, GetCreatorResponse, GetEditgroupResponse, GetEditorChangelogResponse, GetEditorResponse, GetFileHistoryResponse, -    GetFileResponse, GetReleaseFilesResponse, GetReleaseHistoryResponse, GetReleaseResponse, GetStatsResponse, GetWorkHistoryResponse, GetWorkReleasesResponse, GetWorkResponse, -    LookupContainerResponse, LookupCreatorResponse, LookupFileResponse, LookupReleaseResponse, +    CreateFileResponse, CreateReleaseBatchResponse, CreateReleaseResponse, CreateWorkBatchResponse, CreateWorkResponse, DeleteContainerResponse, DeleteCreatorResponse, DeleteFileResponse, +    DeleteReleaseResponse, DeleteWorkResponse, GetChangelogEntryResponse, GetChangelogResponse, GetContainerHistoryResponse, GetContainerResponse, GetCreatorHistoryResponse, +    GetCreatorReleasesResponse, GetCreatorResponse, GetEditgroupResponse, GetEditorChangelogResponse, GetEditorResponse, GetFileHistoryResponse, GetFileResponse, GetReleaseFilesResponse, +    GetReleaseHistoryResponse, GetReleaseResponse, GetStatsResponse, GetWorkHistoryResponse, GetWorkReleasesResponse, GetWorkResponse, LookupContainerResponse, LookupCreatorResponse, +    LookupFileResponse, LookupReleaseResponse, UpdateContainerResponse, UpdateCreatorResponse, UpdateFileResponse, UpdateReleaseResponse, UpdateWorkResponse,  };  header! { (Warning, "Warning") => [String] } @@ -130,11 +131,11 @@ where                              Ok(response)                          } -                        AcceptEditgroupResponse::Unmergable(body) => { +                        AcceptEditgroupResponse::BadRequest(body) => {                              let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize");                              let mut response = Response::with((status::Status::from_u16(400), body_string)); -                            response.headers.set(ContentType(mimetypes::responses::ACCEPT_EDITGROUP_UNMERGABLE.clone())); +                            response.headers.set(ContentType(mimetypes::responses::ACCEPT_EDITGROUP_BAD_REQUEST.clone()));                              context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); @@ -150,6 +151,16 @@ where                              Ok(response)                          } +                        AcceptEditgroupResponse::EditConflict(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(409), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::ACCEPT_EDITGROUP_EDIT_CONFLICT.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        }                          AcceptEditgroupResponse::GenericError(body) => {                              let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); @@ -1356,6 +1367,466 @@ where      );      let api_clone = api.clone(); +    router.delete( +        "/v0/container/:id", +        move |req: &mut Request| { +            let mut context = Context::default(); + +            // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +            fn handle_request<T>(req: &mut Request, api: &T, context: &mut Context) -> Result<Response, Response> +            where +                T: Api, +            { +                context.x_span_id = Some(req.headers.get::<XSpanId>().map(XSpanId::to_string).unwrap_or_else(|| self::uuid::Uuid::new_v4().to_string())); +                context.auth_data = req.extensions.remove::<AuthData>(); +                context.authorization = req.extensions.remove::<Authorization>(); + +                // Path parameters +                let param_id = { +                    let param = req.extensions +                        .get::<Router>() +                        .ok_or_else(|| Response::with((status::InternalServerError, "An internal error occurred".to_string())))? +                        .find("id") +                        .ok_or_else(|| Response::with((status::BadRequest, "Missing path parameter id".to_string())))?; +                    percent_decode(param.as_bytes()) +                        .decode_utf8() +                        .map_err(|_| Response::with((status::BadRequest, format!("Couldn't percent-decode path parameter as UTF-8: {}", param))))? +                        .parse() +                        .map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse path parameter id: {}", e))))? +                }; + +                // Query parameters (note that non-required or collection query parameters will ignore garbage values, rather than causing a 400 response) +                let query_params = req.get::<UrlEncodedQuery>().unwrap_or_default(); +                let param_editgroup = query_params.get("editgroup").and_then(|list| list.first()).and_then(|x| x.parse::<String>().ok()); + +                match api.delete_container(param_id, param_editgroup, context).wait() { +                    Ok(rsp) => match rsp { +                        DeleteContainerResponse::DeletedEntity(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(200), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::DELETE_CONTAINER_DELETED_ENTITY.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                        DeleteContainerResponse::BadRequest(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(400), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::DELETE_CONTAINER_BAD_REQUEST.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                        DeleteContainerResponse::NotFound(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(404), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::DELETE_CONTAINER_NOT_FOUND.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                        DeleteContainerResponse::GenericError(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(500), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::DELETE_CONTAINER_GENERIC_ERROR.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                    }, +                    Err(_) => { +                        // Application code returned an error. This should not happen, as the implementation should +                        // return a valid response. +                        Err(Response::with((status::InternalServerError, "An internal error occurred".to_string()))) +                    } +                } +            } + +            handle_request(req, &api_clone, &mut context).or_else(|mut response| { +                context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                Ok(response) +            }) +        }, +        "DeleteContainer", +    ); + +    let api_clone = api.clone(); +    router.delete( +        "/v0/creator/:id", +        move |req: &mut Request| { +            let mut context = Context::default(); + +            // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +            fn handle_request<T>(req: &mut Request, api: &T, context: &mut Context) -> Result<Response, Response> +            where +                T: Api, +            { +                context.x_span_id = Some(req.headers.get::<XSpanId>().map(XSpanId::to_string).unwrap_or_else(|| self::uuid::Uuid::new_v4().to_string())); +                context.auth_data = req.extensions.remove::<AuthData>(); +                context.authorization = req.extensions.remove::<Authorization>(); + +                // Path parameters +                let param_id = { +                    let param = req.extensions +                        .get::<Router>() +                        .ok_or_else(|| Response::with((status::InternalServerError, "An internal error occurred".to_string())))? +                        .find("id") +                        .ok_or_else(|| Response::with((status::BadRequest, "Missing path parameter id".to_string())))?; +                    percent_decode(param.as_bytes()) +                        .decode_utf8() +                        .map_err(|_| Response::with((status::BadRequest, format!("Couldn't percent-decode path parameter as UTF-8: {}", param))))? +                        .parse() +                        .map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse path parameter id: {}", e))))? +                }; + +                // Query parameters (note that non-required or collection query parameters will ignore garbage values, rather than causing a 400 response) +                let query_params = req.get::<UrlEncodedQuery>().unwrap_or_default(); +                let param_editgroup = query_params.get("editgroup").and_then(|list| list.first()).and_then(|x| x.parse::<String>().ok()); + +                match api.delete_creator(param_id, param_editgroup, context).wait() { +                    Ok(rsp) => match rsp { +                        DeleteCreatorResponse::DeletedEntity(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(200), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::DELETE_CREATOR_DELETED_ENTITY.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                        DeleteCreatorResponse::BadRequest(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(400), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::DELETE_CREATOR_BAD_REQUEST.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                        DeleteCreatorResponse::NotFound(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(404), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::DELETE_CREATOR_NOT_FOUND.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                        DeleteCreatorResponse::GenericError(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(500), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::DELETE_CREATOR_GENERIC_ERROR.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                    }, +                    Err(_) => { +                        // Application code returned an error. This should not happen, as the implementation should +                        // return a valid response. +                        Err(Response::with((status::InternalServerError, "An internal error occurred".to_string()))) +                    } +                } +            } + +            handle_request(req, &api_clone, &mut context).or_else(|mut response| { +                context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                Ok(response) +            }) +        }, +        "DeleteCreator", +    ); + +    let api_clone = api.clone(); +    router.delete( +        "/v0/file/:id", +        move |req: &mut Request| { +            let mut context = Context::default(); + +            // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +            fn handle_request<T>(req: &mut Request, api: &T, context: &mut Context) -> Result<Response, Response> +            where +                T: Api, +            { +                context.x_span_id = Some(req.headers.get::<XSpanId>().map(XSpanId::to_string).unwrap_or_else(|| self::uuid::Uuid::new_v4().to_string())); +                context.auth_data = req.extensions.remove::<AuthData>(); +                context.authorization = req.extensions.remove::<Authorization>(); + +                // Path parameters +                let param_id = { +                    let param = req.extensions +                        .get::<Router>() +                        .ok_or_else(|| Response::with((status::InternalServerError, "An internal error occurred".to_string())))? +                        .find("id") +                        .ok_or_else(|| Response::with((status::BadRequest, "Missing path parameter id".to_string())))?; +                    percent_decode(param.as_bytes()) +                        .decode_utf8() +                        .map_err(|_| Response::with((status::BadRequest, format!("Couldn't percent-decode path parameter as UTF-8: {}", param))))? +                        .parse() +                        .map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse path parameter id: {}", e))))? +                }; + +                // Query parameters (note that non-required or collection query parameters will ignore garbage values, rather than causing a 400 response) +                let query_params = req.get::<UrlEncodedQuery>().unwrap_or_default(); +                let param_editgroup = query_params.get("editgroup").and_then(|list| list.first()).and_then(|x| x.parse::<String>().ok()); + +                match api.delete_file(param_id, param_editgroup, context).wait() { +                    Ok(rsp) => match rsp { +                        DeleteFileResponse::DeletedEntity(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(200), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::DELETE_FILE_DELETED_ENTITY.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                        DeleteFileResponse::BadRequest(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(400), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::DELETE_FILE_BAD_REQUEST.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                        DeleteFileResponse::NotFound(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(404), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::DELETE_FILE_NOT_FOUND.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                        DeleteFileResponse::GenericError(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(500), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::DELETE_FILE_GENERIC_ERROR.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                    }, +                    Err(_) => { +                        // Application code returned an error. This should not happen, as the implementation should +                        // return a valid response. +                        Err(Response::with((status::InternalServerError, "An internal error occurred".to_string()))) +                    } +                } +            } + +            handle_request(req, &api_clone, &mut context).or_else(|mut response| { +                context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                Ok(response) +            }) +        }, +        "DeleteFile", +    ); + +    let api_clone = api.clone(); +    router.delete( +        "/v0/release/:id", +        move |req: &mut Request| { +            let mut context = Context::default(); + +            // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +            fn handle_request<T>(req: &mut Request, api: &T, context: &mut Context) -> Result<Response, Response> +            where +                T: Api, +            { +                context.x_span_id = Some(req.headers.get::<XSpanId>().map(XSpanId::to_string).unwrap_or_else(|| self::uuid::Uuid::new_v4().to_string())); +                context.auth_data = req.extensions.remove::<AuthData>(); +                context.authorization = req.extensions.remove::<Authorization>(); + +                // Path parameters +                let param_id = { +                    let param = req.extensions +                        .get::<Router>() +                        .ok_or_else(|| Response::with((status::InternalServerError, "An internal error occurred".to_string())))? +                        .find("id") +                        .ok_or_else(|| Response::with((status::BadRequest, "Missing path parameter id".to_string())))?; +                    percent_decode(param.as_bytes()) +                        .decode_utf8() +                        .map_err(|_| Response::with((status::BadRequest, format!("Couldn't percent-decode path parameter as UTF-8: {}", param))))? +                        .parse() +                        .map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse path parameter id: {}", e))))? +                }; + +                // Query parameters (note that non-required or collection query parameters will ignore garbage values, rather than causing a 400 response) +                let query_params = req.get::<UrlEncodedQuery>().unwrap_or_default(); +                let param_editgroup = query_params.get("editgroup").and_then(|list| list.first()).and_then(|x| x.parse::<String>().ok()); + +                match api.delete_release(param_id, param_editgroup, context).wait() { +                    Ok(rsp) => match rsp { +                        DeleteReleaseResponse::DeletedEntity(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(200), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::DELETE_RELEASE_DELETED_ENTITY.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                        DeleteReleaseResponse::BadRequest(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(400), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::DELETE_RELEASE_BAD_REQUEST.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                        DeleteReleaseResponse::NotFound(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(404), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::DELETE_RELEASE_NOT_FOUND.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                        DeleteReleaseResponse::GenericError(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(500), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::DELETE_RELEASE_GENERIC_ERROR.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                    }, +                    Err(_) => { +                        // Application code returned an error. This should not happen, as the implementation should +                        // return a valid response. +                        Err(Response::with((status::InternalServerError, "An internal error occurred".to_string()))) +                    } +                } +            } + +            handle_request(req, &api_clone, &mut context).or_else(|mut response| { +                context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                Ok(response) +            }) +        }, +        "DeleteRelease", +    ); + +    let api_clone = api.clone(); +    router.delete( +        "/v0/work/:id", +        move |req: &mut Request| { +            let mut context = Context::default(); + +            // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +            fn handle_request<T>(req: &mut Request, api: &T, context: &mut Context) -> Result<Response, Response> +            where +                T: Api, +            { +                context.x_span_id = Some(req.headers.get::<XSpanId>().map(XSpanId::to_string).unwrap_or_else(|| self::uuid::Uuid::new_v4().to_string())); +                context.auth_data = req.extensions.remove::<AuthData>(); +                context.authorization = req.extensions.remove::<Authorization>(); + +                // Path parameters +                let param_id = { +                    let param = req.extensions +                        .get::<Router>() +                        .ok_or_else(|| Response::with((status::InternalServerError, "An internal error occurred".to_string())))? +                        .find("id") +                        .ok_or_else(|| Response::with((status::BadRequest, "Missing path parameter id".to_string())))?; +                    percent_decode(param.as_bytes()) +                        .decode_utf8() +                        .map_err(|_| Response::with((status::BadRequest, format!("Couldn't percent-decode path parameter as UTF-8: {}", param))))? +                        .parse() +                        .map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse path parameter id: {}", e))))? +                }; + +                // Query parameters (note that non-required or collection query parameters will ignore garbage values, rather than causing a 400 response) +                let query_params = req.get::<UrlEncodedQuery>().unwrap_or_default(); +                let param_editgroup = query_params.get("editgroup").and_then(|list| list.first()).and_then(|x| x.parse::<String>().ok()); + +                match api.delete_work(param_id, param_editgroup, context).wait() { +                    Ok(rsp) => match rsp { +                        DeleteWorkResponse::DeletedEntity(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(200), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::DELETE_WORK_DELETED_ENTITY.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                        DeleteWorkResponse::BadRequest(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(400), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::DELETE_WORK_BAD_REQUEST.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                        DeleteWorkResponse::NotFound(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(404), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::DELETE_WORK_NOT_FOUND.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                        DeleteWorkResponse::GenericError(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(500), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::DELETE_WORK_GENERIC_ERROR.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                    }, +                    Err(_) => { +                        // Application code returned an error. This should not happen, as the implementation should +                        // return a valid response. +                        Err(Response::with((status::InternalServerError, "An internal error occurred".to_string()))) +                    } +                } +            } + +            handle_request(req, &api_clone, &mut context).or_else(|mut response| { +                context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                Ok(response) +            }) +        }, +        "DeleteWork", +    ); + +    let api_clone = api.clone();      router.get(          "/v0/changelog",          move |req: &mut Request| { @@ -3312,6 +3783,606 @@ where          },          "LookupRelease",      ); + +    let api_clone = api.clone(); +    router.put( +        "/v0/container/:id", +        move |req: &mut Request| { +            let mut context = Context::default(); + +            // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +            fn handle_request<T>(req: &mut Request, api: &T, context: &mut Context) -> Result<Response, Response> +            where +                T: Api, +            { +                context.x_span_id = Some(req.headers.get::<XSpanId>().map(XSpanId::to_string).unwrap_or_else(|| self::uuid::Uuid::new_v4().to_string())); +                context.auth_data = req.extensions.remove::<AuthData>(); +                context.authorization = req.extensions.remove::<Authorization>(); + +                // Path parameters +                let param_id = { +                    let param = req.extensions +                        .get::<Router>() +                        .ok_or_else(|| Response::with((status::InternalServerError, "An internal error occurred".to_string())))? +                        .find("id") +                        .ok_or_else(|| Response::with((status::BadRequest, "Missing path parameter id".to_string())))?; +                    percent_decode(param.as_bytes()) +                        .decode_utf8() +                        .map_err(|_| Response::with((status::BadRequest, format!("Couldn't percent-decode path parameter as UTF-8: {}", param))))? +                        .parse() +                        .map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse path parameter id: {}", e))))? +                }; + +                // Body parameters (note that non-required body parameters will ignore garbage +                // values, rather than causing a 400 response). Produce warning header and logs for +                // any unused fields. + +                let param_entity = req.get::<bodyparser::Raw>() +                    .map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse body parameter entity - not valid UTF-8: {}", e))))?; + +                let mut unused_elements = Vec::new(); + +                let param_entity = if let Some(param_entity_raw) = param_entity { +                    let deserializer = &mut serde_json::Deserializer::from_str(¶m_entity_raw); + +                    let param_entity: Option<models::ContainerEntity> = +                        serde_ignored::deserialize(deserializer, |path| { +                            warn!("Ignoring unknown field in body: {}", path); +                            unused_elements.push(path.to_string()); +                        }).map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse body parameter entity - doesn't match schema: {}", e))))?; + +                    param_entity +                } else { +                    None +                }; +                let param_entity = param_entity.ok_or_else(|| Response::with((status::BadRequest, "Missing required body parameter entity".to_string())))?; + +                match api.update_container(param_id, param_entity, context).wait() { +                    Ok(rsp) => match rsp { +                        UpdateContainerResponse::UpdatedEntity(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(200), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::UPDATE_CONTAINER_UPDATED_ENTITY.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                            if !unused_elements.is_empty() { +                                response.headers.set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); +                            } +                            Ok(response) +                        } +                        UpdateContainerResponse::BadRequest(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(400), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::UPDATE_CONTAINER_BAD_REQUEST.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                            if !unused_elements.is_empty() { +                                response.headers.set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); +                            } +                            Ok(response) +                        } +                        UpdateContainerResponse::NotFound(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(404), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::UPDATE_CONTAINER_NOT_FOUND.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                            if !unused_elements.is_empty() { +                                response.headers.set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); +                            } +                            Ok(response) +                        } +                        UpdateContainerResponse::GenericError(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(500), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::UPDATE_CONTAINER_GENERIC_ERROR.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                            if !unused_elements.is_empty() { +                                response.headers.set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); +                            } +                            Ok(response) +                        } +                    }, +                    Err(_) => { +                        // Application code returned an error. This should not happen, as the implementation should +                        // return a valid response. +                        Err(Response::with((status::InternalServerError, "An internal error occurred".to_string()))) +                    } +                } +            } + +            handle_request(req, &api_clone, &mut context).or_else(|mut response| { +                context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                Ok(response) +            }) +        }, +        "UpdateContainer", +    ); + +    let api_clone = api.clone(); +    router.put( +        "/v0/creator/:id", +        move |req: &mut Request| { +            let mut context = Context::default(); + +            // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +            fn handle_request<T>(req: &mut Request, api: &T, context: &mut Context) -> Result<Response, Response> +            where +                T: Api, +            { +                context.x_span_id = Some(req.headers.get::<XSpanId>().map(XSpanId::to_string).unwrap_or_else(|| self::uuid::Uuid::new_v4().to_string())); +                context.auth_data = req.extensions.remove::<AuthData>(); +                context.authorization = req.extensions.remove::<Authorization>(); + +                // Path parameters +                let param_id = { +                    let param = req.extensions +                        .get::<Router>() +                        .ok_or_else(|| Response::with((status::InternalServerError, "An internal error occurred".to_string())))? +                        .find("id") +                        .ok_or_else(|| Response::with((status::BadRequest, "Missing path parameter id".to_string())))?; +                    percent_decode(param.as_bytes()) +                        .decode_utf8() +                        .map_err(|_| Response::with((status::BadRequest, format!("Couldn't percent-decode path parameter as UTF-8: {}", param))))? +                        .parse() +                        .map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse path parameter id: {}", e))))? +                }; + +                // Body parameters (note that non-required body parameters will ignore garbage +                // values, rather than causing a 400 response). Produce warning header and logs for +                // any unused fields. + +                let param_entity = req.get::<bodyparser::Raw>() +                    .map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse body parameter entity - not valid UTF-8: {}", e))))?; + +                let mut unused_elements = Vec::new(); + +                let param_entity = if let Some(param_entity_raw) = param_entity { +                    let deserializer = &mut serde_json::Deserializer::from_str(¶m_entity_raw); + +                    let param_entity: Option<models::CreatorEntity> = +                        serde_ignored::deserialize(deserializer, |path| { +                            warn!("Ignoring unknown field in body: {}", path); +                            unused_elements.push(path.to_string()); +                        }).map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse body parameter entity - doesn't match schema: {}", e))))?; + +                    param_entity +                } else { +                    None +                }; +                let param_entity = param_entity.ok_or_else(|| Response::with((status::BadRequest, "Missing required body parameter entity".to_string())))?; + +                match api.update_creator(param_id, param_entity, context).wait() { +                    Ok(rsp) => match rsp { +                        UpdateCreatorResponse::UpdatedEntity(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(200), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::UPDATE_CREATOR_UPDATED_ENTITY.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                            if !unused_elements.is_empty() { +                                response.headers.set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); +                            } +                            Ok(response) +                        } +                        UpdateCreatorResponse::BadRequest(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(400), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::UPDATE_CREATOR_BAD_REQUEST.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                            if !unused_elements.is_empty() { +                                response.headers.set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); +                            } +                            Ok(response) +                        } +                        UpdateCreatorResponse::NotFound(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(404), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::UPDATE_CREATOR_NOT_FOUND.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                            if !unused_elements.is_empty() { +                                response.headers.set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); +                            } +                            Ok(response) +                        } +                        UpdateCreatorResponse::GenericError(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(500), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::UPDATE_CREATOR_GENERIC_ERROR.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                            if !unused_elements.is_empty() { +                                response.headers.set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); +                            } +                            Ok(response) +                        } +                    }, +                    Err(_) => { +                        // Application code returned an error. This should not happen, as the implementation should +                        // return a valid response. +                        Err(Response::with((status::InternalServerError, "An internal error occurred".to_string()))) +                    } +                } +            } + +            handle_request(req, &api_clone, &mut context).or_else(|mut response| { +                context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                Ok(response) +            }) +        }, +        "UpdateCreator", +    ); + +    let api_clone = api.clone(); +    router.put( +        "/v0/file/:id", +        move |req: &mut Request| { +            let mut context = Context::default(); + +            // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +            fn handle_request<T>(req: &mut Request, api: &T, context: &mut Context) -> Result<Response, Response> +            where +                T: Api, +            { +                context.x_span_id = Some(req.headers.get::<XSpanId>().map(XSpanId::to_string).unwrap_or_else(|| self::uuid::Uuid::new_v4().to_string())); +                context.auth_data = req.extensions.remove::<AuthData>(); +                context.authorization = req.extensions.remove::<Authorization>(); + +                // Path parameters +                let param_id = { +                    let param = req.extensions +                        .get::<Router>() +                        .ok_or_else(|| Response::with((status::InternalServerError, "An internal error occurred".to_string())))? +                        .find("id") +                        .ok_or_else(|| Response::with((status::BadRequest, "Missing path parameter id".to_string())))?; +                    percent_decode(param.as_bytes()) +                        .decode_utf8() +                        .map_err(|_| Response::with((status::BadRequest, format!("Couldn't percent-decode path parameter as UTF-8: {}", param))))? +                        .parse() +                        .map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse path parameter id: {}", e))))? +                }; + +                // Body parameters (note that non-required body parameters will ignore garbage +                // values, rather than causing a 400 response). Produce warning header and logs for +                // any unused fields. + +                let param_entity = req.get::<bodyparser::Raw>() +                    .map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse body parameter entity - not valid UTF-8: {}", e))))?; + +                let mut unused_elements = Vec::new(); + +                let param_entity = if let Some(param_entity_raw) = param_entity { +                    let deserializer = &mut serde_json::Deserializer::from_str(¶m_entity_raw); + +                    let param_entity: Option<models::FileEntity> = +                        serde_ignored::deserialize(deserializer, |path| { +                            warn!("Ignoring unknown field in body: {}", path); +                            unused_elements.push(path.to_string()); +                        }).map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse body parameter entity - doesn't match schema: {}", e))))?; + +                    param_entity +                } else { +                    None +                }; +                let param_entity = param_entity.ok_or_else(|| Response::with((status::BadRequest, "Missing required body parameter entity".to_string())))?; + +                match api.update_file(param_id, param_entity, context).wait() { +                    Ok(rsp) => match rsp { +                        UpdateFileResponse::UpdatedEntity(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(200), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::UPDATE_FILE_UPDATED_ENTITY.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                            if !unused_elements.is_empty() { +                                response.headers.set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); +                            } +                            Ok(response) +                        } +                        UpdateFileResponse::BadRequest(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(400), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::UPDATE_FILE_BAD_REQUEST.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                            if !unused_elements.is_empty() { +                                response.headers.set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); +                            } +                            Ok(response) +                        } +                        UpdateFileResponse::NotFound(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(404), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::UPDATE_FILE_NOT_FOUND.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                            if !unused_elements.is_empty() { +                                response.headers.set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); +                            } +                            Ok(response) +                        } +                        UpdateFileResponse::GenericError(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(500), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::UPDATE_FILE_GENERIC_ERROR.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                            if !unused_elements.is_empty() { +                                response.headers.set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); +                            } +                            Ok(response) +                        } +                    }, +                    Err(_) => { +                        // Application code returned an error. This should not happen, as the implementation should +                        // return a valid response. +                        Err(Response::with((status::InternalServerError, "An internal error occurred".to_string()))) +                    } +                } +            } + +            handle_request(req, &api_clone, &mut context).or_else(|mut response| { +                context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                Ok(response) +            }) +        }, +        "UpdateFile", +    ); + +    let api_clone = api.clone(); +    router.put( +        "/v0/release/:id", +        move |req: &mut Request| { +            let mut context = Context::default(); + +            // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +            fn handle_request<T>(req: &mut Request, api: &T, context: &mut Context) -> Result<Response, Response> +            where +                T: Api, +            { +                context.x_span_id = Some(req.headers.get::<XSpanId>().map(XSpanId::to_string).unwrap_or_else(|| self::uuid::Uuid::new_v4().to_string())); +                context.auth_data = req.extensions.remove::<AuthData>(); +                context.authorization = req.extensions.remove::<Authorization>(); + +                // Path parameters +                let param_id = { +                    let param = req.extensions +                        .get::<Router>() +                        .ok_or_else(|| Response::with((status::InternalServerError, "An internal error occurred".to_string())))? +                        .find("id") +                        .ok_or_else(|| Response::with((status::BadRequest, "Missing path parameter id".to_string())))?; +                    percent_decode(param.as_bytes()) +                        .decode_utf8() +                        .map_err(|_| Response::with((status::BadRequest, format!("Couldn't percent-decode path parameter as UTF-8: {}", param))))? +                        .parse() +                        .map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse path parameter id: {}", e))))? +                }; + +                // Body parameters (note that non-required body parameters will ignore garbage +                // values, rather than causing a 400 response). Produce warning header and logs for +                // any unused fields. + +                let param_entity = req.get::<bodyparser::Raw>() +                    .map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse body parameter entity - not valid UTF-8: {}", e))))?; + +                let mut unused_elements = Vec::new(); + +                let param_entity = if let Some(param_entity_raw) = param_entity { +                    let deserializer = &mut serde_json::Deserializer::from_str(¶m_entity_raw); + +                    let param_entity: Option<models::ReleaseEntity> = +                        serde_ignored::deserialize(deserializer, |path| { +                            warn!("Ignoring unknown field in body: {}", path); +                            unused_elements.push(path.to_string()); +                        }).map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse body parameter entity - doesn't match schema: {}", e))))?; + +                    param_entity +                } else { +                    None +                }; +                let param_entity = param_entity.ok_or_else(|| Response::with((status::BadRequest, "Missing required body parameter entity".to_string())))?; + +                match api.update_release(param_id, param_entity, context).wait() { +                    Ok(rsp) => match rsp { +                        UpdateReleaseResponse::UpdatedEntity(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(200), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::UPDATE_RELEASE_UPDATED_ENTITY.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                            if !unused_elements.is_empty() { +                                response.headers.set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); +                            } +                            Ok(response) +                        } +                        UpdateReleaseResponse::BadRequest(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(400), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::UPDATE_RELEASE_BAD_REQUEST.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                            if !unused_elements.is_empty() { +                                response.headers.set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); +                            } +                            Ok(response) +                        } +                        UpdateReleaseResponse::NotFound(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(404), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::UPDATE_RELEASE_NOT_FOUND.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                            if !unused_elements.is_empty() { +                                response.headers.set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); +                            } +                            Ok(response) +                        } +                        UpdateReleaseResponse::GenericError(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(500), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::UPDATE_RELEASE_GENERIC_ERROR.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                            if !unused_elements.is_empty() { +                                response.headers.set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); +                            } +                            Ok(response) +                        } +                    }, +                    Err(_) => { +                        // Application code returned an error. This should not happen, as the implementation should +                        // return a valid response. +                        Err(Response::with((status::InternalServerError, "An internal error occurred".to_string()))) +                    } +                } +            } + +            handle_request(req, &api_clone, &mut context).or_else(|mut response| { +                context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                Ok(response) +            }) +        }, +        "UpdateRelease", +    ); + +    let api_clone = api.clone(); +    router.put( +        "/v0/work/:id", +        move |req: &mut Request| { +            let mut context = Context::default(); + +            // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +            fn handle_request<T>(req: &mut Request, api: &T, context: &mut Context) -> Result<Response, Response> +            where +                T: Api, +            { +                context.x_span_id = Some(req.headers.get::<XSpanId>().map(XSpanId::to_string).unwrap_or_else(|| self::uuid::Uuid::new_v4().to_string())); +                context.auth_data = req.extensions.remove::<AuthData>(); +                context.authorization = req.extensions.remove::<Authorization>(); + +                // Path parameters +                let param_id = { +                    let param = req.extensions +                        .get::<Router>() +                        .ok_or_else(|| Response::with((status::InternalServerError, "An internal error occurred".to_string())))? +                        .find("id") +                        .ok_or_else(|| Response::with((status::BadRequest, "Missing path parameter id".to_string())))?; +                    percent_decode(param.as_bytes()) +                        .decode_utf8() +                        .map_err(|_| Response::with((status::BadRequest, format!("Couldn't percent-decode path parameter as UTF-8: {}", param))))? +                        .parse() +                        .map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse path parameter id: {}", e))))? +                }; + +                // Body parameters (note that non-required body parameters will ignore garbage +                // values, rather than causing a 400 response). Produce warning header and logs for +                // any unused fields. + +                let param_entity = req.get::<bodyparser::Raw>() +                    .map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse body parameter entity - not valid UTF-8: {}", e))))?; + +                let mut unused_elements = Vec::new(); + +                let param_entity = if let Some(param_entity_raw) = param_entity { +                    let deserializer = &mut serde_json::Deserializer::from_str(¶m_entity_raw); + +                    let param_entity: Option<models::WorkEntity> = +                        serde_ignored::deserialize(deserializer, |path| { +                            warn!("Ignoring unknown field in body: {}", path); +                            unused_elements.push(path.to_string()); +                        }).map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse body parameter entity - doesn't match schema: {}", e))))?; + +                    param_entity +                } else { +                    None +                }; +                let param_entity = param_entity.ok_or_else(|| Response::with((status::BadRequest, "Missing required body parameter entity".to_string())))?; + +                match api.update_work(param_id, param_entity, context).wait() { +                    Ok(rsp) => match rsp { +                        UpdateWorkResponse::UpdatedEntity(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(200), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::UPDATE_WORK_UPDATED_ENTITY.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                            if !unused_elements.is_empty() { +                                response.headers.set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); +                            } +                            Ok(response) +                        } +                        UpdateWorkResponse::BadRequest(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(400), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::UPDATE_WORK_BAD_REQUEST.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                            if !unused_elements.is_empty() { +                                response.headers.set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); +                            } +                            Ok(response) +                        } +                        UpdateWorkResponse::NotFound(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(404), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::UPDATE_WORK_NOT_FOUND.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                            if !unused_elements.is_empty() { +                                response.headers.set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); +                            } +                            Ok(response) +                        } +                        UpdateWorkResponse::GenericError(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(500), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::UPDATE_WORK_GENERIC_ERROR.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                            if !unused_elements.is_empty() { +                                response.headers.set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); +                            } +                            Ok(response) +                        } +                    }, +                    Err(_) => { +                        // Application code returned an error. This should not happen, as the implementation should +                        // return a valid response. +                        Err(Response::with((status::InternalServerError, "An internal error occurred".to_string()))) +                    } +                } +            } + +            handle_request(req, &api_clone, &mut context).or_else(|mut response| { +                context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                Ok(response) +            }) +        }, +        "UpdateWork", +    );  }  /// Middleware to extract authentication data from request diff --git a/rust/src/api_helpers.rs b/rust/src/api_helpers.rs index 020aad76..6c9a4e5f 100644 --- a/rust/src/api_helpers.rs +++ b/rust/src/api_helpers.rs @@ -6,6 +6,9 @@ use diesel::prelude::*;  use errors::*;  use regex::Regex;  use uuid::Uuid; +use std::str::FromStr; + +pub type DbConn = diesel::r2d2::PooledConnection<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;  /// This function should always be run within a transaction  pub fn get_or_create_editgroup(editor_id: Uuid, conn: &PgConnection) -> Result<Uuid> { @@ -34,10 +37,7 @@ pub fn accept_editgroup(editgroup_id: Uuid, conn: &PgConnection) -> Result<Chang          .count()          .get_result(conn)?;      if count > 0 { -        bail!( -            "editgroup {} has already been accepted", -            editgroup_id.to_string() -        ); +        return Err(ErrorKind::EditgroupAlreadyAccepted(uuid2fcid(&editgroup_id)).into());      }      // for each entity type... @@ -90,6 +90,30 @@ pub fn accept_editgroup(editgroup_id: Uuid, conn: &PgConnection) -> Result<Chang      Ok(entry)  } +pub struct FatCatId(Uuid); + +impl ToString for FatCatId { +    fn to_string(&self) -> String { +        uuid2fcid(&self.to_uuid()) +    } +} + +impl FromStr for FatCatId { +    type Err = Error; +    fn from_str(s: &str) -> Result<FatCatId> { +        fcid2uuid(s).map(|u| FatCatId(u)) +    } +} + +impl FatCatId { +    pub fn to_uuid(&self) -> Uuid { +        self.0 +    } +    pub fn from_uuid(u: &Uuid) -> FatCatId { +        FatCatId(u.clone()) +    } +} +  /// Convert fatcat IDs (base32 strings) to UUID  pub fn fcid2uuid(fcid: &str) -> Result<Uuid> {      if fcid.len() != 26 { diff --git a/rust/src/api_server.rs b/rust/src/api_server.rs index 88fd7063..602fbfb7 100644 --- a/rust/src/api_server.rs +++ b/rust/src/api_server.rs @@ -17,11 +17,11 @@ use fatcat_api::models::*;  use sha1::Sha1;  use uuid::Uuid;  use ConnectionPool; - -type DbConn = diesel::r2d2::PooledConnection<diesel::r2d2::ConnectionManager<diesel::PgConnection>>; +use database_entity_crud::{EntityCrud, EditContext}; +use std::str::FromStr;  macro_rules! entity_batch_handler { -    ($post_handler:ident, $post_batch_handler:ident, $model:ident) => { +    ($post_batch_handler:ident, $model:ident) => {          pub fn $post_batch_handler(              &self,              entity_list: &[models::$model], @@ -29,8 +29,6 @@ macro_rules! entity_batch_handler {              editgroup: Option<String>,              conn: &DbConn,          ) -> Result<Vec<EntityEdit>> { -            let mut ret: Vec<EntityEdit> = vec![]; -            let editor_id = Uuid::parse_str("00000000-0000-0000-AAAA-000000000001")?; // TODO: auth              // editgroup override logic based on parameters              let eg_id: Option<Uuid> = match (editgroup, autoaccept) {                  (Some(eg_string), _) => Some(fcid2uuid(&eg_string)?), @@ -51,43 +49,17 @@ macro_rules! entity_batch_handler {                  // actual wrapped function call here                  ret.push(self.$post_handler(e, autoaccept, conn)?);              } +            let mut edit_context = make_edit_context(conn, eg_id)?; +            edit_context.autoaccept = autoaccept; +            let model_list: Vec<&models::$model> = entity_list.iter().map(|e| e).collect(); +            let edits = $model::db_create_batch(conn, &edit_context, model_list.as_slice())?;              if autoaccept {                  // if autoaccept, eg_id is always Some                  let _clr: ChangelogRow = diesel::insert_into(changelog::table)                      .values((changelog::editgroup_id.eq(eg_id.unwrap()),))                      .get_result(conn)?;              } -            Ok(ret) -        } -    } -} - -macro_rules! entity_history_handler { -    ($history_handler:ident, $edit_row_type:ident, $edit_table:ident) => { -        pub fn $history_handler( -            &self, -            id: &Uuid, -            limit: Option<i64>, -            conn: &DbConn, -        ) -> Result<Vec<EntityHistoryEntry>> { -            let limit = limit.unwrap_or(50); - -            let rows: Vec<(EditgroupRow, ChangelogRow, $edit_row_type)> = editgroup::table -                .inner_join(changelog::table) -                .inner_join($edit_table::table) -                .filter($edit_table::ident_id.eq(id)) -                .order(changelog::id.desc()) -                .limit(limit) -                .get_results(conn)?; - -            let history: Vec<EntityHistoryEntry> = rows.into_iter() -                .map(|(eg_row, cl_row, e_row)| EntityHistoryEntry { -                    edit: e_row.into_model().expect("edit row to model"), -                    editgroup: eg_row.into_model_partial(), -                    changelog_entry: cl_row.into_model(), -                }) -                .collect(); -            Ok(history) +            edits.into_iter().map(|e| e.into_model()).collect()          }      }  } @@ -103,227 +75,23 @@ macro_rules! count_entity {      }};  } -#[derive(Clone)] -pub struct Server { -    pub db_pool: ConnectionPool, -} - -fn container_row2entity( -    ident: Option<ContainerIdentRow>, -    rev: ContainerRevRow, -) -> Result<ContainerEntity> { -    let (state, ident_id, redirect_id) = match ident { -        Some(i) => ( -            Some(i.state().unwrap().shortname()), -            Some(uuid2fcid(&i.id)), -            i.redirect_id.map(|u| uuid2fcid(&u)), -        ), -        None => (None, None, None), -    }; -    Ok(ContainerEntity { -        issnl: rev.issnl, -        wikidata_qid: rev.wikidata_qid, -        publisher: rev.publisher, -        name: rev.name, -        abbrev: rev.abbrev, -        coden: rev.coden, -        state: state, -        ident: ident_id, -        revision: Some(rev.id.to_string()), -        redirect: redirect_id, -        extra: rev.extra_json, -        editgroup_id: None, -    }) -} - -fn creator_row2entity(ident: Option<CreatorIdentRow>, rev: CreatorRevRow) -> Result<CreatorEntity> { -    let (state, ident_id, redirect_id) = match ident { -        Some(i) => ( -            Some(i.state().unwrap().shortname()), -            Some(uuid2fcid(&i.id)), -            i.redirect_id.map(|u| uuid2fcid(&u)), -        ), -        None => (None, None, None), -    }; -    Ok(CreatorEntity { -        display_name: rev.display_name, -        given_name: rev.given_name, -        surname: rev.surname, -        orcid: rev.orcid, -        wikidata_qid: rev.wikidata_qid, -        state: state, -        ident: ident_id, -        revision: Some(rev.id.to_string()), -        redirect: redirect_id, -        editgroup_id: None, -        extra: rev.extra_json, -    }) -} - -fn file_row2entity( -    ident: Option<FileIdentRow>, -    rev: FileRevRow, -    conn: &DbConn, -) -> Result<FileEntity> { -    let (state, ident_id, redirect_id) = match ident { -        Some(i) => ( -            Some(i.state().unwrap().shortname()), -            Some(uuid2fcid(&i.id)), -            i.redirect_id.map(|u| uuid2fcid(&u)), -        ), -        None => (None, None, None), +fn make_edit_context(conn: &DbConn, editgroup_id: Option<FatCatId>) -> Result<EditContext> { +    let editor_id = Uuid::parse_str("00000000-0000-0000-AAAA-000000000001")?; // TODO: auth +    let editgroup_id = match editgroup_id { +        None => FatCatId::from_uuid(&get_or_create_editgroup(editor_id, conn)?), +        Some(param) => param,      }; - -    let releases: Vec<String> = file_release::table -        .filter(file_release::file_rev.eq(rev.id)) -        .get_results(conn)? -        .into_iter() -        .map(|r: FileReleaseRow| uuid2fcid(&r.target_release_ident_id)) -        .collect(); - -    let urls: Vec<FileEntityUrls> = file_rev_url::table -        .filter(file_rev_url::file_rev.eq(rev.id)) -        .get_results(conn)? -        .into_iter() -        .map(|r: FileRevUrlRow| FileEntityUrls { -            rel: r.rel, -            url: r.url, -        }) -        .collect(); - -    Ok(FileEntity { -        sha1: rev.sha1, -        sha256: rev.sha256, -        md5: rev.md5, -        size: rev.size.map(|v| v as i64), -        urls: Some(urls), -        mimetype: rev.mimetype, -        releases: Some(releases), -        state: state, -        ident: ident_id, -        revision: Some(rev.id.to_string()), -        redirect: redirect_id, -        editgroup_id: None, -        extra: rev.extra_json, +    Ok(EditContext { +        editor_id: FatCatId::from_uuid(&editor_id), +        editgroup_id: editgroup_id, +        extra_json: None, +        autoapprove: false,      })  } -fn release_row2entity( -    ident: Option<ReleaseIdentRow>, -    rev: ReleaseRevRow, -    conn: &DbConn, -) -> Result<ReleaseEntity> { -    let (state, ident_id, redirect_id) = match ident { -        Some(i) => ( -            Some(i.state().unwrap().shortname()), -            Some(uuid2fcid(&i.id)), -            i.redirect_id.map(|u| uuid2fcid(&u)), -        ), -        None => (None, None, None), -    }; - -    let refs: Vec<ReleaseRef> = release_ref::table -        .filter(release_ref::release_rev.eq(rev.id)) -        .order(release_ref::index_val.asc()) -        .get_results(conn) -        .expect("fetch release refs") -        .into_iter() -        .map(|r: ReleaseRefRow| ReleaseRef { -            index: r.index_val, -            key: r.key, -            extra: r.extra_json, -            container_title: r.container_title, -            year: r.year, -            title: r.title, -            locator: r.locator, -            target_release_id: r.target_release_ident_id.map(|v| uuid2fcid(&v)), -        }) -        .collect(); - -    let contribs: Vec<ReleaseContrib> = release_contrib::table -        .filter(release_contrib::release_rev.eq(rev.id)) -        .order(( -            release_contrib::role.asc(), -            release_contrib::index_val.asc(), -        )) -        .get_results(conn) -        .expect("fetch release refs") -        .into_iter() -        .map(|c: ReleaseContribRow| ReleaseContrib { -            index: c.index_val, -            raw_name: c.raw_name, -            role: c.role, -            extra: c.extra_json, -            creator_id: c.creator_ident_id.map(|v| uuid2fcid(&v)), -            creator: None, -        }) -        .collect(); - -    let abstracts: Vec<ReleaseEntityAbstracts> = release_rev_abstract::table -        .inner_join(abstracts::table) -        .filter(release_rev_abstract::release_rev.eq(rev.id)) -        .get_results(conn)? -        .into_iter() -        .map( -            |r: (ReleaseRevAbstractRow, AbstractsRow)| ReleaseEntityAbstracts { -                sha1: Some(r.0.abstract_sha1), -                mimetype: r.0.mimetype, -                lang: r.0.lang, -                content: Some(r.1.content), -            }, -        ) -        .collect(); - -    Ok(ReleaseEntity { -        title: rev.title, -        release_type: rev.release_type, -        release_status: rev.release_status, -        release_date: rev.release_date -            .map(|v| chrono::DateTime::from_utc(v.and_hms(0, 0, 0), chrono::Utc)), -        doi: rev.doi, -        pmid: rev.pmid, -        pmcid: rev.pmcid, -        isbn13: rev.isbn13, -        core_id: rev.core_id, -        wikidata_qid: rev.wikidata_qid, -        volume: rev.volume, -        issue: rev.issue, -        pages: rev.pages, -        files: None, -        container: None, -        container_id: rev.container_ident_id.map(|u| uuid2fcid(&u)), -        publisher: rev.publisher, -        language: rev.language, -        work_id: Some(uuid2fcid(&rev.work_ident_id)), -        refs: Some(refs), -        contribs: Some(contribs), -        abstracts: Some(abstracts), -        state: state, -        ident: ident_id, -        revision: Some(rev.id.to_string()), -        redirect: redirect_id, -        editgroup_id: None, -        extra: rev.extra_json, -    }) -} - -fn work_row2entity(ident: Option<WorkIdentRow>, rev: WorkRevRow) -> Result<WorkEntity> { -    let (state, ident_id, redirect_id) = match ident { -        Some(i) => ( -            Some(i.state().unwrap().shortname()), -            Some(uuid2fcid(&i.id)), -            i.redirect_id.map(|u| uuid2fcid(&u)), -        ), -        None => (None, None, None), -    }; -    Ok(WorkEntity { -        state: state, -        ident: ident_id, -        revision: Some(rev.id.to_string()), -        redirect: redirect_id, -        editgroup_id: None, -        extra: rev.extra_json, -    }) +#[derive(Clone)] +pub struct Server { +    pub db_pool: ConnectionPool,  }  impl Server { @@ -333,13 +101,7 @@ impl Server {          _expand: Option<String>,          conn: &DbConn,      ) -> Result<ContainerEntity> { -        // TODO: handle Deletions -        let (ident, rev): (ContainerIdentRow, ContainerRevRow) = container_ident::table -            .find(id) -            .inner_join(container_rev::table) -            .first(conn)?; - -        container_row2entity(Some(ident), rev) +        ContainerEntity::db_get(conn, FatCatId::from_uuid(id))      }      pub fn lookup_container_handler(&self, issnl: &str, conn: &DbConn) -> Result<ContainerEntity> { @@ -354,7 +116,7 @@ impl Server {              .filter(container_ident::redirect_id.is_null())              .first(conn)?; -        container_row2entity(Some(ident), rev) +        ContainerEntity::db_from_row(conn, rev, Some(ident))      }      pub fn get_creator_handler( @@ -363,12 +125,8 @@ impl Server {          _expand: Option<String>,          conn: &DbConn,      ) -> Result<CreatorEntity> { -        let (ident, rev): (CreatorIdentRow, CreatorRevRow) = creator_ident::table -            .find(id) -            .inner_join(creator_rev::table) -            .first(conn)?; -        creator_row2entity(Some(ident), rev) +        CreatorEntity::db_get(conn, FatCatId::from_uuid(id))      }      pub fn lookup_creator_handler(&self, orcid: &str, conn: &DbConn) -> Result<CreatorEntity> { @@ -383,7 +141,7 @@ impl Server {              .filter(creator_ident::redirect_id.is_null())              .first(conn)?; -        creator_row2entity(Some(ident), rev) +        CreatorEntity::db_from_row(conn, rev, Some(ident))      }      pub fn get_creator_releases_handler( @@ -402,8 +160,9 @@ impl Server {              .filter(release_ident::redirect_id.is_null())              .load(conn)?; +        // TODO: from_rows, not from_row?          rows.into_iter() -            .map(|(rev, ident, _)| release_row2entity(Some(ident), rev, conn)) +            .map(|(rev, ident, _)| ReleaseEntity::db_from_row(conn, rev, Some(ident)))              .collect()      } @@ -413,12 +172,7 @@ impl Server {          _expand: Option<String>,          conn: &DbConn,      ) -> Result<FileEntity> { -        let (ident, rev): (FileIdentRow, FileRevRow) = file_ident::table -            .find(id) -            .inner_join(file_rev::table) -            .first(conn)?; - -        file_row2entity(Some(ident), rev, conn) +        FileEntity::db_get(conn, FatCatId::from_uuid(id))      }      pub fn lookup_file_handler(&self, sha1: &str, conn: &DbConn) -> Result<FileEntity> { @@ -432,7 +186,7 @@ impl Server {              .filter(file_ident::redirect_id.is_null())              .first(conn)?; -        file_row2entity(Some(ident), rev, conn) +        FileEntity::db_from_row(conn, rev, Some(ident))      }      pub fn get_release_handler( @@ -441,12 +195,8 @@ impl Server {          expand: Option<String>,          conn: &DbConn,      ) -> Result<ReleaseEntity> { -        let (ident, rev): (ReleaseIdentRow, ReleaseRevRow) = release_ident::table -            .find(id) -            .inner_join(release_rev::table) -            .first(conn)?; -        let mut release = release_row2entity(Some(ident), rev, conn)?; +        let mut release = ReleaseEntity::db_get(conn, FatCatId::from_uuid(id))?;          // For now, if there is any expand param we do them all          if expand.is_some() { @@ -457,7 +207,6 @@ impl Server {                      Some(self.get_container_handler(&fcid2uuid(&cid)?, None, conn)?);              }          } -          Ok(release)      } @@ -473,22 +222,23 @@ impl Server {              .filter(release_ident::redirect_id.is_null())              .first(conn)?; -        release_row2entity(Some(ident), rev, conn) +        ReleaseEntity::db_from_row(conn, rev, Some(ident))      }      pub fn get_release_files_handler(&self, id: &str, conn: &DbConn) -> Result<Vec<FileEntity>> { -        let id = fcid2uuid(&id)?; + +        let ident = FatCatId::from_str(id)?;          let rows: Vec<(FileRevRow, FileIdentRow, FileReleaseRow)> = file_rev::table              .inner_join(file_ident::table)              .inner_join(file_release::table) -            .filter(file_release::target_release_ident_id.eq(&id)) +            .filter(file_release::target_release_ident_id.eq(&ident.to_uuid()))              .filter(file_ident::is_live.eq(true))              .filter(file_ident::redirect_id.is_null())              .load(conn)?;          rows.into_iter() -            .map(|(rev, ident, _)| file_row2entity(Some(ident), rev, conn)) +            .map(|(rev, ident, _)| FileEntity::db_from_row(conn, rev, Some(ident)))              .collect()      } @@ -498,12 +248,7 @@ impl Server {          _expand: Option<String>,          conn: &DbConn,      ) -> Result<WorkEntity> { -        let (ident, rev): (WorkIdentRow, WorkRevRow) = work_ident::table -            .find(id) -            .inner_join(work_rev::table) -            .first(conn)?; - -        work_row2entity(Some(ident), rev) +        WorkEntity::db_get(conn, FatCatId::from_uuid(id))      }      pub fn get_work_releases_handler(&self, id: &str, conn: &DbConn) -> Result<Vec<ReleaseEntity>> { @@ -517,379 +262,139 @@ impl Server {              .load(conn)?;          rows.into_iter() -            .map(|(rev, ident)| release_row2entity(Some(ident), rev, conn)) +            .map(|(rev, ident)| ReleaseEntity::db_from_row(conn, rev, Some(ident)))              .collect()      }      pub fn create_container_handler(          &self,          entity: models::ContainerEntity, -        autoaccept: bool,          conn: &DbConn,      ) -> Result<EntityEdit> { -        let editor_id = Uuid::parse_str("00000000-0000-0000-AAAA-000000000001")?; // TODO: auth -        let editgroup_id: Uuid = match entity.editgroup_id { -            None => get_or_create_editgroup(editor_id, conn)?, -            Some(param) => fcid2uuid(¶m)?, -        }; -        if let Some(ref extid) = entity.wikidata_qid { -            check_wikidata_qid(extid)?; -        } -        if let Some(ref extid) = entity.issnl { -            check_issn(extid)?; -        } - -        let edit: ContainerEditRow = diesel::sql_query( -            "WITH rev AS ( INSERT INTO container_rev (name, publisher, issnl, wikidata_qid, abbrev, coden, extra_json) -                        VALUES ($1, $2, $3, $4, $5, $6, $7) -                        RETURNING id ), -                ident AS ( INSERT INTO container_ident (is_live, rev_id) -                            VALUES ($8, (SELECT rev.id FROM rev)) -                            RETURNING id ) -            INSERT INTO container_edit (editgroup_id, ident_id, rev_id) VALUES -                ($9, (SELECT ident.id FROM ident), (SELECT rev.id FROM rev)) -            RETURNING *", -        ).bind::<diesel::sql_types::Text, _>(entity.name) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.publisher) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.issnl) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.wikidata_qid) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.abbrev) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.coden) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Json>, _>(entity.extra) -            .bind::<diesel::sql_types::Bool, _>(autoaccept) -            .bind::<diesel::sql_types::Uuid, _>(editgroup_id) -            .get_result(conn)?; +        let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?)?; +        let edit = entity.db_create(conn, &edit_context)?; +        edit.into_model() +    } +    pub fn update_container_handler( +        &self, +        id: &Uuid, +        entity: models::ContainerEntity, +        conn: &DbConn, +    ) -> Result<EntityEdit> { +        let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?)?; +        let edit = entity.db_update(conn, &edit_context, FatCatId::from_uuid(id))?; +        edit.into_model() +    } +    pub fn delete_container_handler(&self, id: &Uuid, editgroup_id: Option<Uuid>, conn: &DbConn) -> Result<EntityEdit> { +        let edit_context = make_edit_context(conn, editgroup_id.map(|u| FatCatId::from_uuid(&u)))?; +        let edit = ContainerEntity::db_delete(conn, &edit_context, FatCatId::from_uuid(id))?;          edit.into_model()      }      pub fn create_creator_handler(          &self,          entity: models::CreatorEntity, -        autoaccept: bool,          conn: &DbConn,      ) -> Result<EntityEdit> { -        let editor_id = Uuid::parse_str("00000000-0000-0000-AAAA-000000000001")?; // TODO: auth -        let editgroup_id = match entity.editgroup_id { -            None => get_or_create_editgroup(editor_id, conn).expect("current editgroup"), -            Some(param) => fcid2uuid(¶m)?, -        }; -        if let Some(ref extid) = entity.orcid { -            check_orcid(extid)?; -        } -        if let Some(ref extid) = entity.wikidata_qid { -            check_wikidata_qid(extid)?; -        } +        let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?)?; +        let edit = entity.db_create(conn, &edit_context)?; +        edit.into_model() -        let edit: CreatorEditRow = diesel::sql_query( -            "WITH rev AS ( INSERT INTO creator_rev (display_name, given_name, surname, orcid, wikidata_qid, extra_json) -                        VALUES ($1, $2, $3, $4, $5, $6) -                        RETURNING id ), -                ident AS ( INSERT INTO creator_ident (is_live, rev_id) -                            VALUES ($7, (SELECT rev.id FROM rev)) -                            RETURNING id ) -            INSERT INTO creator_edit (editgroup_id, ident_id, rev_id) VALUES -                ($8, (SELECT ident.id FROM ident), (SELECT rev.id FROM rev)) -            RETURNING *", -        ).bind::<diesel::sql_types::Text, _>(entity.display_name) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.given_name) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.surname) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.orcid) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.wikidata_qid) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Json>, _>(entity.extra) -            .bind::<diesel::sql_types::Bool, _>(autoaccept) -            .bind::<diesel::sql_types::Uuid, _>(editgroup_id) -            .get_result(conn)?; +    } +    pub fn update_creator_handler( +        &self, +        id: &Uuid, +        entity: models::CreatorEntity, +        conn: &DbConn, +    ) -> Result<EntityEdit> { +        let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?)?; +        let edit = entity.db_update(conn, &edit_context, FatCatId::from_uuid(id))?; +        edit.into_model() +    } +    pub fn delete_creator_handler(&self, id: &Uuid, editgroup_id: Option<Uuid>, conn: &DbConn) -> Result<EntityEdit> { +        let edit_context = make_edit_context(conn, editgroup_id.map(|u| FatCatId::from_uuid(&u)))?; +        let edit = CreatorEntity::db_delete(conn, &edit_context, FatCatId::from_uuid(id))?;          edit.into_model()      }      pub fn create_file_handler(          &self,          entity: models::FileEntity, -        autoaccept: bool,          conn: &DbConn,      ) -> Result<EntityEdit> { -        let editor_id = Uuid::parse_str("00000000-0000-0000-AAAA-000000000001")?; // TODO: auth -        let editgroup_id = match entity.editgroup_id { -            None => get_or_create_editgroup(editor_id, conn).expect("current editgroup"), -            Some(param) => fcid2uuid(¶m)?, -        }; - -        let edit: FileEditRow = -            diesel::sql_query( -                "WITH rev AS ( INSERT INTO file_rev (size, sha1, sha256, md5, mimetype, extra_json) -                        VALUES ($1, $2, $3, $4, $5, $6) -                        RETURNING id ), -                ident AS ( INSERT INTO file_ident (is_live, rev_id) -                            VALUES ($7, (SELECT rev.id FROM rev)) -                            RETURNING id ) -            INSERT INTO file_edit (editgroup_id, ident_id, rev_id) VALUES -                ($8, (SELECT ident.id FROM ident), (SELECT rev.id FROM rev)) -            RETURNING *", -            ).bind::<diesel::sql_types::Nullable<diesel::sql_types::Int8>, _>(entity.size) -                .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.sha1) -                .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.sha256) -                .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.md5) -                .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.mimetype) -                .bind::<diesel::sql_types::Nullable<diesel::sql_types::Json>, _>(entity.extra) -                .bind::<diesel::sql_types::Bool, _>(autoaccept) -                .bind::<diesel::sql_types::Uuid, _>(editgroup_id) -                .get_result(conn)?; - -        let _releases: Option<Vec<FileReleaseRow>> = match entity.releases { -            None => None, -            Some(release_list) => { -                if release_list.is_empty() { -                    Some(vec![]) -                } else { -                    let release_rows: Vec<FileReleaseRow> = release_list -                        .iter() -                        .map(|r| FileReleaseRow { -                            file_rev: edit.rev_id.unwrap(), -                            target_release_ident_id: fcid2uuid(r) -                                .expect("invalid fatcat identifier"), -                        }) -                        .collect(); -                    let release_rows: Vec<FileReleaseRow> = insert_into(file_release::table) -                        .values(release_rows) -                        .get_results(conn) -                        .expect("error inserting file_releases"); -                    Some(release_rows) -                } -            } -        }; - -        let _urls: Option<Vec<FileRevUrlRow>> = match entity.urls { -            None => None, -            Some(url_list) => { -                if url_list.is_empty() { -                    Some(vec![]) -                } else { -                    let url_rows: Vec<FileRevUrlNewRow> = url_list -                        .into_iter() -                        .map(|u| FileRevUrlNewRow { -                            file_rev: edit.rev_id.unwrap(), -                            rel: u.rel, -                            url: u.url, -                        }) -                        .collect(); -                    let url_rows: Vec<FileRevUrlRow> = insert_into(file_rev_url::table) -                        .values(url_rows) -                        .get_results(conn) -                        .expect("error inserting file_rev_url"); -                    Some(url_rows) -                } -            } -        }; +        let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?)?; +        let edit = entity.db_create(conn, &edit_context)?; +        edit.into_model() +    } +    pub fn update_file_handler( +        &self, +        id: &Uuid, +        entity: models::FileEntity, +        conn: &DbConn, +    ) -> Result<EntityEdit> { +        let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?)?; +        let edit = entity.db_update(conn, &edit_context, FatCatId::from_uuid(id))?; +        edit.into_model() +    } +    pub fn delete_file_handler(&self, id: &Uuid, editgroup_id: Option<Uuid>, conn: &DbConn) -> Result<EntityEdit> { +        let edit_context = make_edit_context(conn, editgroup_id.map(|u| FatCatId::from_uuid(&u)))?; +        let edit = FileEntity::db_delete(conn, &edit_context, FatCatId::from_uuid(id))?;          edit.into_model()      }      pub fn create_release_handler(          &self,          entity: models::ReleaseEntity, -        autoaccept: bool,          conn: &DbConn,      ) -> Result<EntityEdit> { -        let editor_id = Uuid::parse_str("00000000-0000-0000-AAAA-000000000001")?; // TODO: auth -        let editgroup_id = match entity.editgroup_id { -            None => get_or_create_editgroup(editor_id, conn).expect("current editgroup"), -            Some(param) => fcid2uuid(¶m)?, -        }; -        if let Some(ref extid) = entity.doi { -            check_doi(extid)?; -        } -        if let Some(ref extid) = entity.pmid { -            check_pmid(extid)?; -        } -        if let Some(ref extid) = entity.pmcid { -            check_pmcid(extid)?; -        } -        if let Some(ref extid) = entity.wikidata_qid { -            check_wikidata_qid(extid)?; -        } - -        let work_id = match entity.work_id { -            Some(work_id) => fcid2uuid(&work_id)?, -            None => { -                // If a work_id wasn't passed, create a new work under the current editgroup -                let work_model = models::WorkEntity { -                    ident: None, -                    revision: None, -                    redirect: None, -                    state: None, -                    editgroup_id: Some(uuid2fcid(&editgroup_id)), -                    extra: None, -                }; -                let new_entity = self.create_work_handler(work_model, autoaccept, conn)?; -                fcid2uuid(&new_entity.ident)? -            } -        }; - -        let container_id: Option<Uuid> = match entity.container_id { -            Some(id) => Some(fcid2uuid(&id)?), -            None => None, -        }; - -        let edit: ReleaseEditRow = diesel::sql_query( -            "WITH rev AS ( INSERT INTO release_rev (title, release_type, release_status, release_date, doi, pmid, pmcid, wikidata_qid, isbn13, core_id, volume, issue, pages, work_ident_id, container_ident_id, publisher, language, extra_json) -                        VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) -                        RETURNING id ), -                ident AS ( INSERT INTO release_ident (is_live, rev_id) -                            VALUES ($19, (SELECT rev.id FROM rev)) -                            RETURNING id ) -            INSERT INTO release_edit (editgroup_id, ident_id, rev_id) VALUES -                ($20, (SELECT ident.id FROM ident), (SELECT rev.id FROM rev)) -            RETURNING *", -        ).bind::<diesel::sql_types::Text, _>(entity.title) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.release_type) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.release_status) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Date>, _>( -                entity.release_date.map(|v| v.naive_utc().date())) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.doi) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.pmid) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.pmcid) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.wikidata_qid) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.isbn13) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.core_id) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.volume) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.issue) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.pages) -            .bind::<diesel::sql_types::Uuid, _>(work_id) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Uuid>, _>(container_id) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.publisher) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.language) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Json>, _>(entity.extra) -            .bind::<diesel::sql_types::Bool, _>(autoaccept) -            .bind::<diesel::sql_types::Uuid, _>(editgroup_id) -            .get_result(conn)?; - -        let _refs: Option<Vec<ReleaseRefRow>> = match entity.refs { -            None => None, -            Some(ref_list) => { -                if ref_list.is_empty() { -                    Some(vec![]) -                } else { -                    let ref_rows: Vec<ReleaseRefNewRow> = ref_list -                        .iter() -                        .map(|r| ReleaseRefNewRow { -                            release_rev: edit.rev_id.unwrap(), -                            target_release_ident_id: r.target_release_id -                                .clone() -                                .map(|v| fcid2uuid(&v).expect("valid fatcat identifier")), -                            index_val: r.index, -                            key: r.key.clone(), -                            container_title: r.container_title.clone(), -                            year: r.year, -                            title: r.title.clone(), -                            locator: r.locator.clone(), -                            extra_json: r.extra.clone(), -                        }) -                        .collect(); -                    let ref_rows: Vec<ReleaseRefRow> = insert_into(release_ref::table) -                        .values(ref_rows) -                        .get_results(conn) -                        .expect("error inserting release_refs"); -                    Some(ref_rows) -                } -            } -        }; - -        let _contribs: Option<Vec<ReleaseContribRow>> = match entity.contribs { -            None => None, -            Some(contrib_list) => { -                if contrib_list.is_empty() { -                    Some(vec![]) -                } else { -                    let contrib_rows: Vec<ReleaseContribNewRow> = contrib_list -                        .iter() -                        .map(|c| ReleaseContribNewRow { -                            release_rev: edit.rev_id.unwrap(), -                            creator_ident_id: c.creator_id -                                .clone() -                                .map(|v| fcid2uuid(&v).expect("valid fatcat identifier")), -                            raw_name: c.raw_name.clone(), -                            index_val: c.index, -                            role: c.role.clone(), -                            extra_json: c.extra.clone(), -                        }) -                        .collect(); -                    let contrib_rows: Vec<ReleaseContribRow> = insert_into(release_contrib::table) -                        .values(contrib_rows) -                        .get_results(conn) -                        .expect("error inserting release_contribs"); -                    Some(contrib_rows) -                } -            } -        }; - -        if let Some(abstract_list) = entity.abstracts { -            // For rows that specify content, we need to insert the abstract if it doesn't exist -            // already -            let new_abstracts: Vec<AbstractsRow> = abstract_list -                .iter() -                .filter(|ea| ea.content.is_some()) -                .map(|c| AbstractsRow { -                    sha1: Sha1::from(c.content.clone().unwrap()).hexdigest(), -                    content: c.content.clone().unwrap(), -                }) -                .collect(); -            if !new_abstracts.is_empty() { -                // Sort of an "upsert"; only inserts new abstract rows if they don't already exist -                insert_into(abstracts::table) -                    .values(&new_abstracts) -                    .on_conflict(abstracts::sha1) -                    .do_nothing() -                    .execute(conn)?; -            } -            let release_abstract_rows: Vec<ReleaseRevAbstractNewRow> = abstract_list -                .into_iter() -                .map(|c| ReleaseRevAbstractNewRow { -                    release_rev: edit.rev_id.unwrap(), -                    abstract_sha1: match c.content { -                        Some(ref content) => Sha1::from(content).hexdigest(), -                        None => c.sha1.expect("either abstract_sha1 or content is required"), -                    }, -                    lang: c.lang, -                    mimetype: c.mimetype, -                }) -                .collect(); -            insert_into(release_rev_abstract::table) -                .values(release_abstract_rows) -                .execute(conn)?; -        } +        let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?)?; +        let edit = entity.db_create(conn, &edit_context)?; +        edit.into_model() +    } +    pub fn update_release_handler( +        &self, +        id: &Uuid, +        entity: models::ReleaseEntity, +        conn: &DbConn, +    ) -> Result<EntityEdit> { +        let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?)?; +        let edit = entity.db_update(conn, &edit_context, FatCatId::from_uuid(id))?; +        edit.into_model() +    } +    pub fn delete_release_handler(&self, id: &Uuid, editgroup_id: Option<Uuid>, conn: &DbConn) -> Result<EntityEdit> { +        let edit_context = make_edit_context(conn, editgroup_id.map(|u| FatCatId::from_uuid(&u)))?; +        let edit = ReleaseEntity::db_delete(conn, &edit_context, FatCatId::from_uuid(id))?;          edit.into_model()      }      pub fn create_work_handler(          &self,          entity: models::WorkEntity, -        autoaccept: bool,          conn: &DbConn,      ) -> Result<EntityEdit> { -        let editor_id = Uuid::parse_str("00000000-0000-0000-AAAA-000000000001")?; // TODO: auth -        let editgroup_id = match entity.editgroup_id { -            None => get_or_create_editgroup(editor_id, conn).expect("current editgroup"), -            Some(param) => fcid2uuid(¶m)?, -        }; +        let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?)?; +        let edit = entity.db_create(conn, &edit_context)?; +        edit.into_model() +    } + +    pub fn update_work_handler( +        &self, +        id: &Uuid, +        entity: models::WorkEntity, +        conn: &DbConn, +    ) -> Result<EntityEdit> { +        let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?)?; +        let edit = entity.db_update(conn, &edit_context, FatCatId::from_uuid(id))?; +        edit.into_model() +    } -        let edit: WorkEditRow = -            diesel::sql_query( -                "WITH rev AS ( INSERT INTO work_rev (extra_json) -                        VALUES ($1) -                        RETURNING id ), -                ident AS ( INSERT INTO work_ident (is_live, rev_id) -                            VALUES ($2, (SELECT rev.id FROM rev)) -                            RETURNING id ) -            INSERT INTO work_edit (editgroup_id, ident_id, rev_id) VALUES -                ($3, (SELECT ident.id FROM ident), (SELECT rev.id FROM rev)) -            RETURNING *", -            ).bind::<diesel::sql_types::Nullable<diesel::sql_types::Json>, _>(entity.extra) -                .bind::<diesel::sql_types::Bool, _>(autoaccept) -                .bind::<diesel::sql_types::Uuid, _>(editgroup_id) -                .get_result(conn)?; +    pub fn delete_work_handler(&self, id: &Uuid, editgroup_id: Option<Uuid>, conn: &DbConn) -> Result<EntityEdit> { +        let edit_context = make_edit_context(conn, editgroup_id.map(|u| FatCatId::from_uuid(&u)))?; +        let edit = WorkEntity::db_delete(conn, &edit_context, FatCatId::from_uuid(id))?;          edit.into_model()      } @@ -910,8 +415,7 @@ impl Server {                  editgroup::description.eq(entity.description),                  editgroup::extra_json.eq(entity.extra),              )) -            .get_result(conn) -            .expect("error creating edit group"); +            .get_result(conn)?;          Ok(Editgroup {              id: Some(uuid2fcid(&row.id)), @@ -1124,30 +628,33 @@ impl Server {      }      entity_batch_handler!( -        create_container_handler,          create_container_batch_handler,          ContainerEntity      );      entity_batch_handler!( -        create_creator_handler,          create_creator_batch_handler,          CreatorEntity      ); -    entity_batch_handler!(create_file_handler, create_file_batch_handler, FileEntity); +    entity_batch_handler!(create_file_batch_handler, FileEntity);      entity_batch_handler!( -        create_release_handler,          create_release_batch_handler,          ReleaseEntity      ); -    entity_batch_handler!(create_work_handler, create_work_batch_handler, WorkEntity); +    entity_batch_handler!(create_work_batch_handler, WorkEntity); -    entity_history_handler!( -        get_container_history_handler, -        ContainerEditRow, -        container_edit -    ); -    entity_history_handler!(get_creator_history_handler, CreatorEditRow, creator_edit); -    entity_history_handler!(get_file_history_handler, FileEditRow, file_edit); -    entity_history_handler!(get_release_history_handler, ReleaseEditRow, release_edit); -    entity_history_handler!(get_work_history_handler, WorkEditRow, work_edit); +    pub fn get_container_history_handler(&self, id: &Uuid, limit: Option<i64>, conn: &DbConn,) -> Result<Vec<EntityHistoryEntry>> { +        ContainerEntity::db_get_history(conn, FatCatId::from_uuid(id), limit) +    } +    pub fn get_creator_history_handler(&self, id: &Uuid, limit: Option<i64>, conn: &DbConn,) -> Result<Vec<EntityHistoryEntry>> { +        CreatorEntity::db_get_history(conn, FatCatId::from_uuid(id), limit) +    } +    pub fn get_file_history_handler(&self, id: &Uuid, limit: Option<i64>, conn: &DbConn,) -> Result<Vec<EntityHistoryEntry>> { +        FileEntity::db_get_history(conn, FatCatId::from_uuid(id), limit) +    } +    pub fn get_release_history_handler(&self, id: &Uuid, limit: Option<i64>, conn: &DbConn,) -> Result<Vec<EntityHistoryEntry>> { +        ReleaseEntity::db_get_history(conn, FatCatId::from_uuid(id), limit) +    } +    pub fn get_work_history_handler(&self, id: &Uuid, limit: Option<i64>, conn: &DbConn,) -> Result<Vec<EntityHistoryEntry>> { +        WorkEntity::db_get_history(conn, FatCatId::from_uuid(id), limit) +    }  } diff --git a/rust/src/api_wrappers.rs b/rust/src/api_wrappers.rs index da6139d2..6272814e 100644 --- a/rust/src/api_wrappers.rs +++ b/rust/src/api_wrappers.rs @@ -19,8 +19,9 @@ macro_rules! wrap_entity_handlers {      // The only stable approach I know of would be: https://github.com/dtolnay/mashup      ($get_fn:ident, $get_handler:ident, $get_resp:ident, $post_fn:ident, $post_handler:ident,              $post_resp:ident, $post_batch_fn:ident, $post_batch_handler:ident, -            $post_batch_resp:ident, $get_history_fn:ident, $get_history_handler:ident, -            $get_history_resp:ident, $model:ident) => { +            $post_batch_resp:ident, $update_fn:ident, $update_handler:ident, $update_resp:ident, +            $delete_fn:ident, $delete_handler:ident, $delete_resp:ident, $get_history_fn:ident, +            $get_history_handler:ident, $get_history_resp:ident, $model:ident) => {          fn $get_fn(              &self, @@ -108,6 +109,79 @@ macro_rules! wrap_entity_handlers {              Box::new(futures::done(Ok(ret)))          } +        fn $update_fn( +            &self, +            id: String, +            entity: models::$model, +            _context: &Context, +        ) -> Box<Future<Item = $update_resp, Error = ApiError> + Send> { +            let id = if let Ok(parsed) = fcid2uuid(&id) { parsed } else { +                return Box::new(futures::done(Ok($update_resp::BadRequest(ErrorResponse { +                    message: ErrorKind::InvalidFatcatId(id).to_string() })))); +            }; +            let conn = self.db_pool.get().expect("db_pool error"); +            let ret = match conn.transaction(|| self.$update_handler(&id, entity, &conn)) { +                Ok(edit) => +                    $update_resp::UpdatedEntity(edit), +                Err(Error(ErrorKind::Diesel(::diesel::result::Error::NotFound), _)) => +                    $update_resp::NotFound(ErrorResponse { message: format!("No such entity {}: {}", stringify!($model), id) }), +                Err(Error(ErrorKind::Diesel(e), _)) => +                    $update_resp::BadRequest(ErrorResponse { message: e.to_string() }), +                Err(Error(ErrorKind::Uuid(e), _)) => +                    $update_resp::BadRequest(ErrorResponse { message: e.to_string() }), +                Err(Error(ErrorKind::InvalidFatcatId(e), _)) => +                    $update_resp::BadRequest(ErrorResponse { +                        message: ErrorKind::InvalidFatcatId(e).to_string() }), +                Err(Error(ErrorKind::MalformedExternalId(e), _)) => +                    $update_resp::BadRequest(ErrorResponse { message: e.to_string() }), +                Err(e) => { +                    error!("{}", e); +                    $update_resp::GenericError(ErrorResponse { message: e.to_string() }) +                }, +            }; +            Box::new(futures::done(Ok(ret))) +        } + +        fn $delete_fn( +            &self, +            id: String, +            editgroup_id: Option<String>, +            _context: &Context, +        ) -> Box<Future<Item = $delete_resp, Error = ApiError> + Send> { +            let id = if let Ok(parsed) = fcid2uuid(&id) { parsed } else { +                return Box::new(futures::done(Ok($delete_resp::BadRequest(ErrorResponse { +                    message: ErrorKind::InvalidFatcatId(id).to_string() })))); +            }; +            let editgroup_id = match editgroup_id { +                Some(raw) => if let Ok(parsed) = fcid2uuid(&raw) { Some(parsed) } else { +                    return Box::new(futures::done(Ok($delete_resp::BadRequest(ErrorResponse { +                        message: ErrorKind::InvalidFatcatId(raw).to_string() })))) +                } +                None => None +            }; +            let conn = self.db_pool.get().expect("db_pool error"); +            let ret = match conn.transaction(|| self.$delete_handler(&id, editgroup_id, &conn)) { +                Ok(edit) => +                    $delete_resp::DeletedEntity(edit), +                Err(Error(ErrorKind::Diesel(::diesel::result::Error::NotFound), _)) => +                    $delete_resp::NotFound(ErrorResponse { message: format!("No such entity {}: {}", stringify!($model), id) }), +                Err(Error(ErrorKind::Diesel(e), _)) => +                    $delete_resp::BadRequest(ErrorResponse { message: e.to_string() }), +                Err(Error(ErrorKind::Uuid(e), _)) => +                    $delete_resp::BadRequest(ErrorResponse { message: e.to_string() }), +                Err(Error(ErrorKind::InvalidFatcatId(e), _)) => +                    $delete_resp::BadRequest(ErrorResponse { +                        message: ErrorKind::InvalidFatcatId(e).to_string() }), +                Err(Error(ErrorKind::MalformedExternalId(e), _)) => +                    $delete_resp::BadRequest(ErrorResponse { message: e.to_string() }), +                Err(e) => { +                    error!("{}", e); +                    $delete_resp::GenericError(ErrorResponse { message: e.to_string() }) +                }, +            }; +            Box::new(futures::done(Ok(ret))) +        } +          fn $get_history_fn(              &self,              id: String, @@ -177,6 +251,12 @@ impl Api for Server {          create_container_batch,          create_container_batch_handler,          CreateContainerBatchResponse, +        update_container, +        update_container_handler, +        UpdateContainerResponse, +        delete_container, +        delete_container_handler, +        DeleteContainerResponse,          get_container_history,          get_container_history_handler,          GetContainerHistoryResponse, @@ -193,6 +273,12 @@ impl Api for Server {          create_creator_batch,          create_creator_batch_handler,          CreateCreatorBatchResponse, +        update_creator, +        update_creator_handler, +        UpdateCreatorResponse, +        delete_creator, +        delete_creator_handler, +        DeleteCreatorResponse,          get_creator_history,          get_creator_history_handler,          GetCreatorHistoryResponse, @@ -208,6 +294,12 @@ impl Api for Server {          create_file_batch,          create_file_batch_handler,          CreateFileBatchResponse, +        update_file, +        update_file_handler, +        UpdateFileResponse, +        delete_file, +        delete_file_handler, +        DeleteFileResponse,          get_file_history,          get_file_history_handler,          GetFileHistoryResponse, @@ -223,6 +315,12 @@ impl Api for Server {          create_release_batch,          create_release_batch_handler,          CreateReleaseBatchResponse, +        update_release, +        update_release_handler, +        UpdateReleaseResponse, +        delete_release, +        delete_release_handler, +        DeleteReleaseResponse,          get_release_history,          get_release_history_handler,          GetReleaseHistoryResponse, @@ -238,6 +336,12 @@ impl Api for Server {          create_work_batch,          create_work_batch_handler,          CreateWorkBatchResponse, +        update_work, +        update_work_handler, +        UpdateWorkResponse, +        delete_work, +        delete_work_handler, +        DeleteWorkResponse,          get_work_history,          get_work_history_handler,          GetWorkHistoryResponse, @@ -310,6 +414,11 @@ impl Api for Server {                      message: format!("No such editgroup: {}", id),                  })              } +            Err(Error(ErrorKind::EditgroupAlreadyAccepted(e), _)) => { +                AcceptEditgroupResponse::BadRequest(ErrorResponse { +                    message: ErrorKind::EditgroupAlreadyAccepted(e).to_string(), +                }) +            }              Err(e) => AcceptEditgroupResponse::GenericError(ErrorResponse {                  message: e.to_string(),              }), diff --git a/rust/src/database_entity_crud.rs b/rust/src/database_entity_crud.rs new file mode 100644 index 00000000..0f5c5f9d --- /dev/null +++ b/rust/src/database_entity_crud.rs @@ -0,0 +1,900 @@ + +use sha1::Sha1; +use chrono; +use diesel::prelude::*; +use diesel::{self, insert_into}; +use database_schema::*; +use database_models::*; +use errors::*; +use fatcat_api::models::*; +use api_helpers::*; +use uuid::Uuid; +use std::marker::Sized; +use std::str::FromStr; +use serde_json; + +pub struct EditContext { +    pub editor_id: FatCatId, +    pub editgroup_id: FatCatId, +    pub extra_json: Option<serde_json::Value>, +    pub autoapprove: bool, +} + +/* One goal here is to abstract the non-entity-specific bits into generic traits or functions, + * instead of macros. + * + * Notably: + * + *   db_get + *   db_get_rev + *   db_create + *   db_create_batch + *   db_update + *   db_delete + *   db_get_history + * + * For now, these will probably be macros, until we can level up our trait/generics foo. + */ + +// Associated Type, not parametric +pub trait EntityCrud where Self: Sized { +    // TODO: could these be generic structs? Or do they need to be bound to a specific table? +    type EditRow; // EntityEditRow +    type EditNewRow; +    type IdentRow; // EntityIdentRow +    type IdentNewRow; +    type RevRow; + +    fn parse_editgroup_id(&self) -> Result<Option<FatCatId>>; + +    // Generic Methods +    fn db_get(conn: &DbConn, ident: FatCatId) -> Result<Self>; +    fn db_get_rev(conn: &DbConn, rev_id: Uuid) -> Result<Self>; +    fn db_create(&self, conn: &DbConn, edit_context: &EditContext) -> Result<Self::EditRow>; +    fn db_create_batch(conn: &DbConn, edit_context: &EditContext, models: &[&Self]) -> Result<Vec<Self::EditRow>>; +    fn db_update(&self, conn: &DbConn, edit_context: &EditContext, ident: FatCatId) -> Result<Self::EditRow>; +    fn db_delete(conn: &DbConn, edit_context: &EditContext, ident: FatCatId) -> Result<Self::EditRow>; +    fn db_get_history(conn: &DbConn, ident: FatCatId, limit: Option<i64>) -> Result<Vec<EntityHistoryEntry>>; + +    // Entity-specific Methods +    fn db_from_row(conn: &DbConn, rev_row: Self::RevRow, ident_row: Option<Self::IdentRow>) -> Result<Self>; +    fn db_insert_rev(&self, conn: &DbConn) -> Result<Uuid>; +    fn db_insert_revs(conn: &DbConn, models: &[&Self]) -> Result<Vec<Uuid>>; +} + +// TODO: this could be a separate trait on all entities? +macro_rules! generic_parse_editgroup_id{ +    () => { +        fn parse_editgroup_id(&self) -> Result<Option<FatCatId>> { +            match &self.editgroup_id { +                Some(s) => Ok(Some(FatCatId::from_str(&s)?)), +                None => Ok(None), +            } +        } +    } +} + +macro_rules! generic_db_get { +    ($ident_table: ident, $rev_table: ident) => { +        fn db_get(conn: &DbConn, ident: FatCatId) -> Result<Self> { +            let (ident, rev): (Self::IdentRow, Self::RevRow) = $ident_table::table +                .find(ident.to_uuid()) +                .inner_join($rev_table::table) +                .first(conn)?; + +            Self::db_from_row(conn, rev, Some(ident)) +        } +    } +} + +macro_rules! generic_db_get_rev { +    ($rev_table: ident) => { +        fn db_get_rev(conn: &DbConn, rev_id: Uuid) -> Result<Self> { +            let rev = $rev_table::table +                .find(rev_id) +                .first(conn)?; +             +            Self::db_from_row(conn, rev, None) +        } +    } +} + +macro_rules! generic_db_create { +    // TODO: this path should call generic_db_create_batch +    ($ident_table: ident, $edit_table: ident) => { +        fn db_create(&self, conn: &DbConn, edit_context: &EditContext) -> Result<Self::EditRow> { +            let rev_id = self.db_insert_rev(conn)?; +            let ident: Uuid = insert_into($ident_table::table) +                .values($ident_table::rev_id.eq(&rev_id)) +                .returning($ident_table::id) +                .get_result(conn)?; +            let edit: Self::EditRow = insert_into($edit_table::table) +                .values(( +                    $edit_table::editgroup_id.eq(edit_context.editgroup_id.to_uuid()), +                    $edit_table::rev_id.eq(&rev_id), +                    $edit_table::ident_id.eq(&ident), +                )) +                .get_result(conn)?; +            Ok(edit) +        } +    } +} + +macro_rules! generic_db_create_batch { +    ($ident_table: ident, $edit_table: ident) => { +        fn db_create_batch(conn: &DbConn, edit_context: &EditContext, models: &[&Self]) -> Result<Vec<Self::EditRow>> { +            let rev_ids: Vec<Uuid> = Self::db_insert_revs(conn, models)?; +            let ident_ids: Vec<Uuid> = insert_into($ident_table::table) +                .values(rev_ids.iter() +                    .map(|rev_id| Self::IdentNewRow { +                        rev_id: Some(rev_id.clone()), +                        is_live: edit_context.autoapprove, +                        redirect_id: None, +                    }) +                    .collect::<Vec<Self::IdentNewRow>>()) +                .returning($ident_table::id) +                .get_results(conn)?; +            let edits: Vec<Self::EditRow> = insert_into($edit_table::table) +                .values(rev_ids.into_iter().zip(ident_ids.into_iter()) +                    .map(|(rev_id, ident_id)| Self::EditNewRow { +                        editgroup_id: edit_context.editgroup_id.to_uuid(), +                        rev_id: Some(rev_id), +                        ident_id: ident_id, +                        redirect_id: None, +                        prev_rev: None, +                        extra_json: edit_context.extra_json.clone(), +                    }) +                    .collect::<Vec<Self::EditNewRow>>()) +                .get_results(conn)?; +            Ok(edits) +        } +    } +} + +macro_rules! generic_db_update { +    ($ident_table: ident, $edit_table: ident) => { +        fn db_update(&self, conn: &DbConn, edit_context: &EditContext, ident: FatCatId) -> Result<Self::EditRow> { +            let current: Self::IdentRow = $ident_table::table.find(ident.to_uuid()).first(conn)?; +            if current.is_live != true { +                // TODO: what if isn't live? 4xx not 5xx +                bail!("can't delete an entity that doesn't exist yet"); +            } +            if current.rev_id.is_none() { +                // TODO: what if it's already deleted? 4xx not 5xx +                bail!("entity was already deleted"); +            } + +            let rev_id = self.db_insert_rev(conn)?; +            let edit: Self::EditRow = insert_into($edit_table::table) +                .values(( +                    $edit_table::editgroup_id.eq(edit_context.editgroup_id.to_uuid()), +                    $edit_table::ident_id.eq(&ident.to_uuid()), +                    $edit_table::rev_id.eq(&rev_id), +                    $edit_table::prev_rev.eq(current.rev_id.unwrap()), +                    $edit_table::extra_json.eq(&self.extra), +                )) +                .get_result(conn)?; + +            Ok(edit) +        } +    } +} + +macro_rules! generic_db_delete { +    ($ident_table: ident, $edit_table:ident) => { +        fn db_delete(conn: &DbConn, edit_context: &EditContext, ident: FatCatId) -> Result<Self::EditRow> { + +            let current: Self::IdentRow = $ident_table::table.find(ident.to_uuid()).first(conn)?; +            if current.is_live != true { +                // TODO: what if isn't live? 4xx not 5xx +                bail!("can't delete an entity that doesn't exist yet"); +            } +            if current.rev_id.is_none() { +                // TODO: what if it's already deleted? 4xx not 5xx +                bail!("entity was already deleted"); +            } +            let edit: Self::EditRow = insert_into($edit_table::table) +                .values(( +                    $edit_table::editgroup_id.eq(edit_context.editgroup_id.to_uuid()), +                    $edit_table::ident_id.eq(ident.to_uuid()), +                    $edit_table::rev_id.eq(None::<Uuid>), +                    $edit_table::redirect_id.eq(None::<Uuid>), +                    $edit_table::prev_rev.eq(current.rev_id), +                    $edit_table::extra_json.eq(&edit_context.extra_json), +                )) +                .get_result(conn)?; + +            Ok(edit) +        } +    } +} + +macro_rules! generic_db_get_history { +    ($edit_table:ident) => { +        fn db_get_history(conn: &DbConn, ident: FatCatId, limit: Option<i64>) -> Result<Vec<EntityHistoryEntry>> { +            let limit = limit.unwrap_or(50); // TODO: make a static + +            let rows: Vec<(EditgroupRow, ChangelogRow, Self::EditRow)> = editgroup::table +                .inner_join(changelog::table) +                .inner_join($edit_table::table) +                .filter($edit_table::ident_id.eq(ident.to_uuid())) +                .order(changelog::id.desc()) +                .limit(limit) +                .get_results(conn)?; + +            let history: Result<Vec<EntityHistoryEntry>> = rows.into_iter() +                .map(|(eg_row, cl_row, e_row)| Ok(EntityHistoryEntry { +                    edit: e_row.into_model()?, +                    editgroup: eg_row.into_model_partial(), +                    changelog_entry: cl_row.into_model(), +                })) +                .collect(); +            history +        } +    } +} + +macro_rules! generic_db_insert_rev { +    () => { +        fn db_insert_rev(&self, conn: &DbConn) -> Result<Uuid> { +            Self::db_insert_revs(conn, &vec![self]).map(|id_list| id_list[0]) +        } +    } +} + +impl EntityCrud for ContainerEntity { +    type EditRow = ContainerEditRow; +    type EditNewRow = ContainerEditNewRow; +    type IdentRow = ContainerIdentRow; +    type IdentNewRow = ContainerIdentNewRow; +    type RevRow = ContainerRevRow; + +    generic_parse_editgroup_id!(); +    generic_db_get!(container_ident, container_rev); +    generic_db_get_rev!(container_rev); +    generic_db_create!(container_ident, container_edit); +    generic_db_create_batch!(container_ident, container_edit); +    generic_db_update!(container_ident, container_edit); +    generic_db_delete!(container_ident, container_edit); +    generic_db_get_history!(container_edit); +    generic_db_insert_rev!(); + +    fn db_from_row(_conn: &DbConn, rev_row: Self::RevRow, ident_row: Option<Self::IdentRow>) -> Result<Self> { + +        let (state, ident_id, redirect_id) = match ident_row { +            Some(i) => ( +                Some(i.state().unwrap().shortname()), +                Some(FatCatId::from_uuid(&i.id).to_string()), +                i.redirect_id.map(|u| FatCatId::from_uuid(&u).to_string()), +            ), +            None => (None, None, None), +        }; + +        Ok(ContainerEntity { +            issnl: rev_row.issnl, +            wikidata_qid: rev_row.wikidata_qid, +            publisher: rev_row.publisher, +            name: rev_row.name, +            abbrev: rev_row.abbrev, +            coden: rev_row.coden, +            state: state, +            ident: ident_id, +            revision: Some(rev_row.id.to_string()), +            redirect: redirect_id, +            extra: rev_row.extra_json, +            editgroup_id: None, +        }) +    } + +    fn db_insert_revs(conn: &DbConn, models: &[&Self]) -> Result<Vec<Uuid>> { + +        // first verify external identifier syntax +        for entity in models { +            if let Some(ref extid) = entity.wikidata_qid { +                check_wikidata_qid(extid)?; +            } +            if let Some(ref extid) = entity.issnl { +                check_issn(extid)?; +            } +        } + +        let rev_ids: Vec<Uuid> = insert_into(container_rev::table) +            .values(models.iter() +                .map(|model| ContainerRevNewRow { +                    name: model.name.clone(), +                    publisher: model.publisher.clone(), +                    issnl: model.issnl.clone(), +                    wikidata_qid: model.wikidata_qid.clone(), +                    abbrev: model.abbrev.clone(), +                    coden: model.coden.clone(), +                    extra_json: model.extra.clone() +                }) +                .collect::<Vec<ContainerRevNewRow>>()) +            .returning(container_rev::id) +            .get_results(conn)?; +        Ok(rev_ids) +    } +} + +impl EntityCrud for CreatorEntity { +    type EditRow = CreatorEditRow; +    type EditNewRow = CreatorEditNewRow; +    type IdentRow = CreatorIdentRow; +    type IdentNewRow = CreatorIdentNewRow; +    type RevRow = CreatorRevRow; + +    generic_parse_editgroup_id!(); +    generic_db_get!(creator_ident, creator_rev); +    generic_db_get_rev!(creator_rev); +    generic_db_create!(creator_ident, creator_edit); +    generic_db_create_batch!(creator_ident, creator_edit); +    generic_db_update!(creator_ident, creator_edit); +    generic_db_delete!(creator_ident, creator_edit); +    generic_db_get_history!(creator_edit); +    generic_db_insert_rev!(); + +    fn db_from_row(_conn: &DbConn, rev_row: Self::RevRow, ident_row: Option<Self::IdentRow>) -> Result<Self> { +        let (state, ident_id, redirect_id) = match ident_row { +            Some(i) => ( +                Some(i.state().unwrap().shortname()), +                Some(FatCatId::from_uuid(&i.id).to_string()), +                i.redirect_id.map(|u| FatCatId::from_uuid(&u).to_string()), +            ), +            None => (None, None, None), +        }; +        Ok(CreatorEntity { +            display_name: rev_row.display_name, +            given_name: rev_row.given_name, +            surname: rev_row.surname, +            orcid: rev_row.orcid, +            wikidata_qid: rev_row.wikidata_qid, +            state: state, +            ident: ident_id, +            revision: Some(rev_row.id.to_string()), +            redirect: redirect_id, +            editgroup_id: None, +            extra: rev_row.extra_json, +        }) +    } + +    fn db_insert_revs(conn: &DbConn, models: &[&Self]) -> Result<Vec<Uuid>> { + +        // first verify external identifier syntax +        for entity in models { +            if let Some(ref extid) = entity.orcid { +                check_orcid(extid)?; +            } +            if let Some(ref extid) = entity.wikidata_qid { +                check_wikidata_qid(extid)?; +            } +        } + +        let rev_ids: Vec<Uuid> = insert_into(creator_rev::table) +            .values(models.iter() +                .map(|model| CreatorRevNewRow { +                    display_name: model.display_name.clone(), +                    given_name: model.given_name.clone(), +                    surname: model.surname.clone(), +                    orcid: model.orcid.clone(), +                    wikidata_qid: model.wikidata_qid.clone(), +                    extra_json: model.extra.clone() +                }) +                .collect::<Vec<CreatorRevNewRow>>()) +            .returning(creator_rev::id) +            .get_results(conn)?; +        Ok(rev_ids) +    } +} + +impl EntityCrud for FileEntity { +    type EditRow = FileEditRow; +    type EditNewRow = FileEditNewRow; +    type IdentRow = FileIdentRow; +    type IdentNewRow = FileIdentNewRow; +    type RevRow = FileRevRow; + +    generic_parse_editgroup_id!(); +    generic_db_get!(file_ident, file_rev); +    generic_db_get_rev!(file_rev); +    generic_db_create!(file_ident, file_edit); +    generic_db_create_batch!(file_ident, file_edit); +    generic_db_update!(file_ident, file_edit); +    generic_db_delete!(file_ident, file_edit); +    generic_db_get_history!(file_edit); +    generic_db_insert_rev!(); + +    fn db_from_row(conn: &DbConn, rev_row: Self::RevRow, ident_row: Option<Self::IdentRow>) -> Result<Self> { +        let (state, ident_id, redirect_id) = match ident_row { +            Some(i) => ( +                Some(i.state().unwrap().shortname()), +                Some(FatCatId::from_uuid(&i.id).to_string()), +                i.redirect_id.map(|u| FatCatId::from_uuid(&u).to_string()), +            ), +            None => (None, None, None), +        }; + +        let releases: Vec<FatCatId> = file_release::table +            .filter(file_release::file_rev.eq(rev_row.id)) +            .get_results(conn)? +            .into_iter() +            .map(|r: FileReleaseRow| FatCatId::from_uuid(&r.target_release_ident_id)) +            .collect(); + +        let urls: Vec<FileEntityUrls> = file_rev_url::table +            .filter(file_rev_url::file_rev.eq(rev_row.id)) +            .get_results(conn)? +            .into_iter() +            .map(|r: FileRevUrlRow| FileEntityUrls { +                rel: r.rel, +                url: r.url, +            }) +            .collect(); + +        Ok(FileEntity { +            sha1: rev_row.sha1, +            sha256: rev_row.sha256, +            md5: rev_row.md5, +            size: rev_row.size.map(|v| v as i64), +            urls: Some(urls), +            mimetype: rev_row.mimetype, +            releases: Some(releases.iter().map(|fcid| fcid.to_string()).collect()), +            state: state, +            ident: ident_id, +            revision: Some(rev_row.id.to_string()), +            redirect: redirect_id, +            editgroup_id: None, +            extra: rev_row.extra_json, +        }) +    } + +    fn db_insert_revs(conn: &DbConn, models: &[&Self]) -> Result<Vec<Uuid>> { + +        let rev_ids: Vec<Uuid> = insert_into(file_rev::table) +            .values(models.iter() +                .map(|model| FileRevNewRow { +                    size: model.size, +                    sha1: model.sha1.clone(), +                    sha256: model.sha256.clone(), +                    md5: model.md5.clone(), +                    mimetype: model.mimetype.clone(), +                    extra_json: model.extra.clone() +                }) +                .collect::<Vec<FileRevNewRow>>()) +            .returning(file_rev::id) +            .get_results(conn)?; + +        let mut file_release_rows: Vec<FileReleaseRow> = vec![]; +        let mut file_url_rows: Vec<FileRevUrlNewRow> = vec![]; + +        for (model, rev_id) in models.iter().zip(rev_ids.iter()) { +            match &model.releases { +                None => (), +                Some(release_list) => { +                    let these_release_rows: Result<Vec<FileReleaseRow>> = release_list +                        .iter() +                        .map(|r| Ok(FileReleaseRow { +                            file_rev: rev_id.clone(), +                            target_release_ident_id: FatCatId::from_str(r)?.to_uuid(), +                        })) +                        .collect(); +                    file_release_rows.extend(these_release_rows?); +                } +            }; + +            match &model.urls { +                None => (), +                Some(url_list) => { +                    let these_url_rows: Vec<FileRevUrlNewRow> = url_list +                        .into_iter() +                        .map(|u| FileRevUrlNewRow { +                            file_rev: rev_id.clone(), +                            rel: u.rel.clone(), +                            url: u.url.clone(), +                        }) +                        .collect(); +                    file_url_rows.extend(these_url_rows); +                } +            }; +        } + +        if !file_release_rows.is_empty() { +            // TODO: shouldn't it be "file_rev_release"? +            insert_into(file_release::table) +                .values(file_release_rows) +                .execute(conn)?; +        } + +        if !file_url_rows.is_empty() { +            insert_into(file_rev_url::table) +                .values(file_url_rows) +                .execute(conn)?; +        } + +        Ok(rev_ids) +    } +} + +impl EntityCrud for ReleaseEntity { +    type EditRow = ReleaseEditRow; +    type EditNewRow = ReleaseEditNewRow; +    type IdentRow = ReleaseIdentRow; +    type IdentNewRow = ReleaseIdentNewRow; +    type RevRow = ReleaseRevRow; + +    generic_parse_editgroup_id!(); +    generic_db_get!(release_ident, release_rev); +    generic_db_get_rev!(release_rev); +    //generic_db_create!(release_ident, release_edit); +    //generic_db_create_batch!(release_ident, release_edit); +    generic_db_update!(release_ident, release_edit); +    generic_db_delete!(release_ident, release_edit); +    generic_db_get_history!(release_edit); +    generic_db_insert_rev!(); + +    fn db_create(&self, conn: &DbConn, edit_context: &EditContext) -> Result<Self::EditRow> { +        let mut edits = Self::db_create_batch(conn, edit_context, &vec![self])?; +        // probably a more elegant way to destroy the vec and take first element +        Ok(edits.pop().unwrap()) +    } + +    fn db_create_batch(conn: &DbConn, edit_context: &EditContext, models: &[&Self]) -> Result<Vec<Self::EditRow>> { +        // This isn't the generic implementation because we need to create Work entities for each +        // of the release entities passed (at least in the common case) + +        // Generate the set of new work entities to insert (usually one for each release, but some +        // releases might be pointed to a work already) +        let mut new_work_models: Vec<&WorkEntity> = vec![]; +        for entity in models { +            if entity.work_id.is_none() { +                new_work_models.push(&WorkEntity { +                    ident: None, +                    revision: None, +                    redirect: None, +                    state: None, +                    editgroup_id: None, +                    extra: None, +                }); +            }; +        } + +        // create the works, then pluck the list of idents from the result +        let new_work_edits = WorkEntity::db_create_batch(conn, edit_context, new_work_models.as_slice())?; +        let mut new_work_ids: Vec<Uuid> = new_work_edits.iter().map(|edit| edit.ident_id).collect(); + +        // Copy all the release models, and ensure that each has work_id set, using the new work +        // idents. There should be one new work ident for each release missing one. +        let models_with_work_ids: Vec<Self> = models.iter().map(|model| { +            let mut model = (*model).clone(); +            if model.work_id.is_none() { +                model.work_id = Some(FatCatId::from_uuid(&new_work_ids.pop().unwrap()).to_string()) +            } +            model +        }).collect(); +        let model_refs: Vec<&Self> = models_with_work_ids.iter().map(|s| s).collect(); +        let models = model_refs.as_slice(); + +        // The rest here is copy/pasta from the generic (how to avoid copypasta?) +        let rev_ids: Vec<Uuid> = Self::db_insert_revs(conn, models)?; +        let ident_ids: Vec<Uuid> = insert_into(release_ident::table) +            .values(rev_ids.iter() +                .map(|rev_id| Self::IdentNewRow { +                    rev_id: Some(rev_id.clone()), +                    is_live: edit_context.autoapprove, +                    redirect_id: None, +                }) +                .collect::<Vec<Self::IdentNewRow>>()) +            .returning(release_ident::id) +            .get_results(conn)?; +        let edits: Vec<Self::EditRow> = insert_into(release_edit::table) +            .values(rev_ids.into_iter().zip(ident_ids.into_iter()) +                .map(|(rev_id, ident_id)| Self::EditNewRow { +                    editgroup_id: edit_context.editgroup_id.to_uuid(), +                    rev_id: Some(rev_id), +                    ident_id: ident_id, +                    redirect_id: None, +                    prev_rev: None, +                    extra_json: edit_context.extra_json.clone(), +                }) +                .collect::<Vec<Self::EditNewRow>>()) +            .get_results(conn)?; +        Ok(edits) +    } + +    fn db_from_row(conn: &DbConn, rev_row: Self::RevRow, ident_row: Option<Self::IdentRow>) -> Result<Self> { +        let (state, ident_id, redirect_id) = match ident_row { +            Some(i) => ( +                Some(i.state().unwrap().shortname()), +                Some(FatCatId::from_uuid(&i.id).to_string()), +                i.redirect_id.map(|u| FatCatId::from_uuid(&u).to_string()), +            ), +            None => (None, None, None), +        }; + +        let refs: Vec<ReleaseRef> = release_ref::table +            .filter(release_ref::release_rev.eq(rev_row.id)) +            .order(release_ref::index_val.asc()) +            .get_results(conn)? +            .into_iter() +            .map(|r: ReleaseRefRow| ReleaseRef { +                index: r.index_val, +                key: r.key, +                extra: r.extra_json, +                container_title: r.container_title, +                year: r.year, +                title: r.title, +                locator: r.locator, +                target_release_id: r.target_release_ident_id.map(|v| FatCatId::from_uuid(&v).to_string()), +            }) +            .collect(); + +        let contribs: Vec<ReleaseContrib> = release_contrib::table +            .filter(release_contrib::release_rev.eq(rev_row.id)) +            .order(( +                release_contrib::role.asc(), +                release_contrib::index_val.asc(), +            )) +            .get_results(conn)? +            .into_iter() +            .map(|c: ReleaseContribRow| ReleaseContrib { +                index: c.index_val, +                raw_name: c.raw_name, +                role: c.role, +                extra: c.extra_json, +                creator_id: c.creator_ident_id.map(|v| FatCatId::from_uuid(&v).to_string()), +                creator: None, +            }) +            .collect(); + +        let abstracts: Vec<ReleaseEntityAbstracts> = release_rev_abstract::table +            .inner_join(abstracts::table) +            .filter(release_rev_abstract::release_rev.eq(rev_row.id)) +            .get_results(conn)? +            .into_iter() +            .map( +                |r: (ReleaseRevAbstractRow, AbstractsRow)| ReleaseEntityAbstracts { +                    sha1: Some(r.0.abstract_sha1), +                    mimetype: r.0.mimetype, +                    lang: r.0.lang, +                    content: Some(r.1.content), +                }, +            ) +            .collect(); + +        Ok(ReleaseEntity { +            title: rev_row.title, +            release_type: rev_row.release_type, +            release_status: rev_row.release_status, +            release_date: rev_row.release_date +                .map(|v| chrono::DateTime::from_utc(v.and_hms(0, 0, 0), chrono::Utc)), +            doi: rev_row.doi, +            pmid: rev_row.pmid, +            pmcid: rev_row.pmcid, +            isbn13: rev_row.isbn13, +            core_id: rev_row.core_id, +            wikidata_qid: rev_row.wikidata_qid, +            volume: rev_row.volume, +            issue: rev_row.issue, +            pages: rev_row.pages, +            files: None, +            container: None, +            container_id: rev_row.container_ident_id.map(|u| FatCatId::from_uuid(&u).to_string()), +            publisher: rev_row.publisher, +            language: rev_row.language, +            work_id: Some(FatCatId::from_uuid(&rev_row.work_ident_id).to_string()), +            refs: Some(refs), +            contribs: Some(contribs), +            abstracts: Some(abstracts), +            state: state, +            ident: ident_id, +            revision: Some(rev_row.id.to_string()), +            redirect: redirect_id, +            editgroup_id: None, +            extra: rev_row.extra_json, +        }) +    } + +    fn db_insert_revs(conn: &DbConn, models: &[&Self]) -> Result<Vec<Uuid>> { + +        // first verify external identifier syntax +        for entity in models { +            if let Some(ref extid) = entity.doi { +                check_doi(extid)?; +            } +            if let Some(ref extid) = entity.pmid { +                check_pmid(extid)?; +            } +            if let Some(ref extid) = entity.pmcid { +                check_pmcid(extid)?; +            } +            if let Some(ref extid) = entity.wikidata_qid { +                check_wikidata_qid(extid)?; +            } +        } + +        let rev_ids: Vec<Uuid> = insert_into(release_rev::table) +            .values(models.iter() +                .map(|model| Ok(ReleaseRevNewRow { +                    title: model.title.clone(), +                    release_type: model.release_type.clone(), +                    release_status: model.release_status.clone(), +                    release_date: model.release_date.map(|v| v.naive_utc().date()), +                    doi: model.doi.clone(), +                    pmid: model.pmid.clone(), +                    pmcid: model.pmcid.clone(), +                    wikidata_qid: model.wikidata_qid.clone(), +                    isbn13: model.isbn13.clone(), +                    core_id: model.core_id.clone(), +                    volume: model.volume.clone(), +                    issue: model.issue.clone(), +                    pages: model.pages.clone(), +                    work_ident_id: match model.work_id.clone() { +                        None => bail!("release_revs must have a work_id by the time they are inserted; this is an internal soundness error"), +                        Some(s) => FatCatId::from_str(&s)?.to_uuid(), +                    }, +                    container_ident_id: match model.container_id.clone() { +                        None => None, +                        Some(s) => Some(FatCatId::from_str(&s)?.to_uuid()), +                    }, +                    publisher: model.publisher.clone(), +                    language: model.language.clone(), +                    extra_json: model.extra.clone() +                })) +                .collect::<Result<Vec<ReleaseRevNewRow>>>()?) +            .returning(release_rev::id) +            .get_results(conn)?; + +        let mut release_ref_rows: Vec<ReleaseRefNewRow> = vec![]; +        let mut release_contrib_rows: Vec<ReleaseContribNewRow> = vec![]; + +        for (model, rev_id) in models.iter().zip(rev_ids.iter()) { +            match &model.refs { +                None => (), +                Some(ref_list) => { +                    let these_ref_rows: Vec<ReleaseRefNewRow> = ref_list +                        .iter() +                        .map(|r| Ok(ReleaseRefNewRow { +                            release_rev: rev_id.clone(), +                            target_release_ident_id: match r.target_release_id.clone() { +                                None => None, +                                Some(v) => Some(FatCatId::from_str(&v)?.to_uuid()), +                            }, +                            index_val: r.index, +                            key: r.key.clone(), +                            container_title: r.container_title.clone(), +                            year: r.year, +                            title: r.title.clone(), +                            locator: r.locator.clone(), +                            extra_json: r.extra.clone(), +                        })) +                        .collect::<Result<Vec<ReleaseRefNewRow>>>()?; +                    release_ref_rows.extend(these_ref_rows); +                } +            }; + +            match &model.contribs { +                None => (), +                Some(contrib_list) => { +                    let these_contrib_rows: Vec<ReleaseContribNewRow> = contrib_list +                        .iter() +                        .map(|c| Ok(ReleaseContribNewRow { +                            release_rev: rev_id.clone(), +                            creator_ident_id: match c.creator_id.clone() { +                                None => None, +                                Some(v) => Some(FatCatId::from_str(&v)?.to_uuid()), +                            }, +                            raw_name: c.raw_name.clone(), +                            index_val: c.index, +                            role: c.role.clone(), +                            extra_json: c.extra.clone(), +                        })) +                        .collect::<Result<Vec<ReleaseContribNewRow>>>()?; +                    release_contrib_rows.extend(these_contrib_rows); +                } +            }; + +            // TODO: this part still isn't parallelized +            if let Some(abstract_list) = &model.abstracts { +                // For rows that specify content, we need to insert the abstract if it doesn't exist +                // already +                let new_abstracts: Vec<AbstractsRow> = abstract_list +                    .iter() +                    .filter(|ea| ea.content.is_some()) +                    .map(|c| AbstractsRow { +                        sha1: Sha1::from(c.content.clone().unwrap()).hexdigest(), +                        content: c.content.clone().unwrap(), +                    }) +                    .collect(); +                if !new_abstracts.is_empty() { +                    // Sort of an "upsert"; only inserts new abstract rows if they don't already exist +                    insert_into(abstracts::table) +                        .values(&new_abstracts) +                        .on_conflict(abstracts::sha1) +                        .do_nothing() +                        .execute(conn)?; +                } +                let release_abstract_rows: Vec<ReleaseRevAbstractNewRow> = abstract_list +                    .into_iter() +                    .map(|c| Ok(ReleaseRevAbstractNewRow { +                        release_rev: rev_id.clone(), +                        abstract_sha1: match c.content { +                            Some(ref content) => Sha1::from(content).hexdigest(), +                            None => match c.sha1.clone() { +                                Some(v) => v, +                                None => { bail!("either abstract_sha1 or content is required") } +                            }, +                        }, +                        lang: c.lang.clone(), +                        mimetype: c.mimetype.clone(), +                    })) +                    .collect::<Result<Vec<ReleaseRevAbstractNewRow>>>()?; +                insert_into(release_rev_abstract::table) +                    .values(release_abstract_rows) +                    .execute(conn)?; +            } +        } + +        if !release_ref_rows.is_empty() { +            insert_into(release_ref::table) +                .values(release_ref_rows) +                .execute(conn)?; +        } + +        if !release_contrib_rows.is_empty() { +            insert_into(release_contrib::table) +                .values(release_contrib_rows) +                .execute(conn)?; +        } + +        Ok(rev_ids) +    } +} + +impl EntityCrud for WorkEntity { +    type EditRow = WorkEditRow; +    type EditNewRow = WorkEditNewRow; +    type IdentRow = WorkIdentRow; +    type IdentNewRow = WorkIdentNewRow; +    type RevRow = WorkRevRow; + +    generic_parse_editgroup_id!(); +    generic_db_get!(work_ident, work_rev); +    generic_db_get_rev!(work_rev); +    generic_db_create!(work_ident, work_edit); +    generic_db_create_batch!(work_ident, work_edit); +    generic_db_update!(work_ident, work_edit); +    generic_db_delete!(work_ident, work_edit); +    generic_db_get_history!(work_edit); +    generic_db_insert_rev!(); + +    fn db_from_row(_conn: &DbConn, rev_row: Self::RevRow, ident_row: Option<Self::IdentRow>) -> Result<Self> { + +        let (state, ident_id, redirect_id) = match ident_row { +            Some(i) => ( +                Some(i.state().unwrap().shortname()), +                Some(FatCatId::from_uuid(&i.id).to_string()), +                i.redirect_id.map(|u| FatCatId::from_uuid(&u).to_string()), +            ), +            None => (None, None, None), +        }; + +        Ok(WorkEntity { +            state: state, +            ident: ident_id, +            revision: Some(rev_row.id.to_string()), +            redirect: redirect_id, +            editgroup_id: None, +            extra: rev_row.extra_json, +        }) +    } + +    fn db_insert_revs(conn: &DbConn, models: &[&Self]) -> Result<Vec<Uuid>> { +        let rev_ids: Vec<Uuid> = insert_into(work_rev::table) +            .values(models.iter() +                .map(|model| WorkRevNewRow { extra_json: model.extra.clone() } ) +                .collect::<Vec<WorkRevNewRow>>()) +            .returning(work_rev::id) +            .get_results(conn)?; +        Ok(rev_ids) +    } +} + diff --git a/rust/src/database_models.rs b/rust/src/database_models.rs index 47e00bcf..2d6788eb 100644 --- a/rust/src/database_models.rs +++ b/rust/src/database_models.rs @@ -37,7 +37,9 @@ pub trait EntityEditRow {  // Helper for constructing tables  macro_rules! entity_structs { -    ($edit_table:expr, $edit_struct:ident, $ident_table:expr, $ident_struct:ident) => { +    ($edit_table:expr, $edit_struct:ident, $edit_new_struct:ident, $ident_table:expr, +    $ident_struct:ident, $ident_new_struct:ident) => { +          #[derive(Debug, Queryable, Identifiable, Associations, AsChangeset, QueryableByName)]          #[table_name = $edit_table]          pub struct $edit_struct { @@ -51,6 +53,17 @@ macro_rules! entity_structs {              pub extra_json: Option<serde_json::Value>,          } +        #[derive(Debug, Associations, AsChangeset, QueryableByName, Insertable)] +        #[table_name = $edit_table] +        pub struct $edit_new_struct { +            pub editgroup_id: Uuid, +            pub ident_id: Uuid, +            pub rev_id: Option<Uuid>, +            pub redirect_id: Option<Uuid>, +            pub prev_rev: Option<Uuid>, +            pub extra_json: Option<serde_json::Value>, +        } +          impl EntityEditRow for $edit_struct {              /// Go from a row (SQL model) to an API model              fn into_model(self) -> Result<EntityEdit> { @@ -75,6 +88,14 @@ macro_rules! entity_structs {              pub redirect_id: Option<Uuid>,          } +        #[derive(Debug, Associations, AsChangeset, Insertable)] +        #[table_name = $ident_table] +        pub struct $ident_new_struct { +            pub is_live: bool, +            pub rev_id: Option<Uuid>, +            pub redirect_id: Option<Uuid>, +        } +          impl EntityIdentRow for $ident_struct {              fn state(&self) -> Result<EntityState> {                  if !self.is_live { @@ -104,11 +125,25 @@ pub struct ContainerRevRow {      pub coden: Option<String>,  } +#[derive(Debug, Associations, AsChangeset, Insertable)] +#[table_name = "container_rev"] +pub struct ContainerRevNewRow { +    pub extra_json: Option<serde_json::Value>, +    pub name: String, +    pub publisher: Option<String>, +    pub issnl: Option<String>, +    pub wikidata_qid: Option<String>, +    pub abbrev: Option<String>, +    pub coden: Option<String>, +} +  entity_structs!(      "container_edit",      ContainerEditRow, +    ContainerEditNewRow,      "container_ident", -    ContainerIdentRow +    ContainerIdentRow, +    ContainerIdentNewRow  );  #[derive(Debug, Queryable, Identifiable, Associations, AsChangeset)] @@ -123,11 +158,24 @@ pub struct CreatorRevRow {      pub wikidata_qid: Option<String>,  } +#[derive(Debug, Associations, AsChangeset, Insertable)] +#[table_name = "creator_rev"] +pub struct CreatorRevNewRow { +    pub extra_json: Option<serde_json::Value>, +    pub display_name: String, +    pub given_name: Option<String>, +    pub surname: Option<String>, +    pub orcid: Option<String>, +    pub wikidata_qid: Option<String>, +} +  entity_structs!(      "creator_edit",      CreatorEditRow, +    CreatorEditNewRow,      "creator_ident", -    CreatorIdentRow +    CreatorIdentRow, +    CreatorIdentNewRow  );  #[derive(Debug, Queryable, Identifiable, Associations, AsChangeset)] @@ -159,7 +207,18 @@ pub struct FileRevRow {      pub mimetype: Option<String>,  } -entity_structs!("file_edit", FileEditRow, "file_ident", FileIdentRow); +#[derive(Debug, Associations, AsChangeset, Insertable)] +#[table_name = "file_rev"] +pub struct FileRevNewRow { +    pub extra_json: Option<serde_json::Value>, +    pub size: Option<i64>, +    pub sha1: Option<String>, +    pub sha256: Option<String>, +    pub md5: Option<String>, +    pub mimetype: Option<String>, +} + +entity_structs!("file_edit", FileEditRow, FileEditNewRow, "file_ident", FileIdentRow, FileIdentNewRow);  #[derive(Debug, Queryable, Identifiable, Associations, AsChangeset)]  #[table_name = "release_rev"] @@ -185,11 +244,36 @@ pub struct ReleaseRevRow {      pub language: Option<String>,  } +#[derive(Debug, Associations, AsChangeset, Insertable)] +#[table_name = "release_rev"] +pub struct ReleaseRevNewRow { +    pub extra_json: Option<serde_json::Value>, +    pub work_ident_id: Uuid, +    pub container_ident_id: Option<Uuid>, +    pub title: String, +    pub release_type: Option<String>, +    pub release_status: Option<String>, +    pub release_date: Option<chrono::NaiveDate>, +    pub doi: Option<String>, +    pub pmid: Option<String>, +    pub pmcid: Option<String>, +    pub wikidata_qid: Option<String>, +    pub isbn13: Option<String>, +    pub core_id: Option<String>, +    pub volume: Option<String>, +    pub issue: Option<String>, +    pub pages: Option<String>, +    pub publisher: Option<String>, +    pub language: Option<String>, +} +  entity_structs!(      "release_edit",      ReleaseEditRow, +    ReleaseEditNewRow,      "release_ident", -    ReleaseIdentRow +    ReleaseIdentRow, +    ReleaseIdentNewRow  );  #[derive(Debug, Queryable, Identifiable, Associations, AsChangeset)] @@ -199,7 +283,13 @@ pub struct WorkRevRow {      pub extra_json: Option<serde_json::Value>,  } -entity_structs!("work_edit", WorkEditRow, "work_ident", WorkIdentRow); +#[derive(Debug, Associations, AsChangeset, Insertable)] +#[table_name = "work_rev"] +pub struct WorkRevNewRow { +    pub extra_json: Option<serde_json::Value>, +} + +entity_structs!("work_edit", WorkEditRow, WorkEditNewRow, "work_ident", WorkIdentRow, WorkIdentNewRow);  #[derive(Debug, Queryable, Identifiable, Associations, AsChangeset)]  #[table_name = "release_rev_abstract"] diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 50a7d410..a938486b 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -27,6 +27,7 @@ pub mod api_server;  pub mod api_wrappers;  pub mod database_models;  pub mod database_schema; +pub mod database_entity_crud;  mod errors {      // Create the Error, ErrorKind, ResultExt, and Result types @@ -47,6 +48,10 @@ mod errors {                  description("external identifier doesn't match required pattern")                  display("external identifier doesn't match required pattern")              } +            EditgroupAlreadyAccepted(id: String) { +                description("editgroup was already accepted") +                display("attempted to accept an editgroup which was already accepted: {}", id) +            }          }      }  } diff --git a/rust/tests/test_api_server.rs b/rust/tests/test_api_server.rs index 340fb996..54639228 100644 --- a/rust/tests/test_api_server.rs +++ b/rust/tests/test_api_server.rs @@ -469,6 +469,71 @@ fn test_post_work() {  }  #[test] +fn test_update_work() { +    let (headers, router, conn) = setup(); + +    check_response( +        request::post( +            "http://localhost:9411/v0/work", +            headers.clone(), +            r#"{ +                "extra": { "source": "other speculation" } +            }"#, +            &router, +        ), +        status::Created, +        None, +    ); + +    let editor_id = Uuid::parse_str("00000000-0000-0000-AAAA-000000000001").unwrap(); +    let editgroup_id = get_or_create_editgroup(editor_id, &conn).unwrap(); +    check_response( +        request::post( +            &format!( +                "http://localhost:9411/v0/editgroup/{}/accept", +                uuid2fcid(&editgroup_id) +            ), +            headers.clone(), +            "", +            &router, +        ), +        status::Ok, +        None, +    ); +} + +#[test] +fn test_delete_work() { +    let (headers, router, conn) = setup(); + +    check_response( +        request::delete( +            "http://localhost:9411/v0/work/aaaaaaaaaaaaavkvaaaaaaaaai", +            headers.clone(), +            &router, +        ), +        status::Ok, +        None, +    ); + +    let editor_id = Uuid::parse_str("00000000-0000-0000-AAAA-000000000001").unwrap(); +    let editgroup_id = get_or_create_editgroup(editor_id, &conn).unwrap(); +    check_response( +        request::post( +            &format!( +                "http://localhost:9411/v0/editgroup/{}/accept", +                uuid2fcid(&editgroup_id) +            ), +            headers.clone(), +            "", +            &router, +        ), +        status::Ok, +        None, +    ); +} + +#[test]  fn test_accept_editgroup() {      let (headers, router, conn) = setup(); | 
