From 9f21ddabcc13355e12b5542bc6dc47b8228b9cfa Mon Sep 17 00:00:00 2001 From: Luke Sneeringer Date: Wed, 19 May 2021 11:55:45 -0700 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20AIP-162=20=E2=80=93=20Resource=20re?= =?UTF-8?q?visions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aip/general/0162/aip.md.j2 | 280 +++++++++++++++++++++++++++++++ aip/general/0162/aip.yaml | 7 + aip/general/0162/revisions.proto | 202 ++++++++++++++++++++++ 3 files changed, 489 insertions(+) create mode 100644 aip/general/0162/aip.md.j2 create mode 100644 aip/general/0162/aip.yaml create mode 100644 aip/general/0162/revisions.proto diff --git a/aip/general/0162/aip.md.j2 b/aip/general/0162/aip.md.j2 new file mode 100644 index 00000000..64e6eaa7 --- /dev/null +++ b/aip/general/0162/aip.md.j2 @@ -0,0 +1,280 @@ +# Resource Revisions + +Some APIs need to have resources with a revision history, where users can +reason about the state of the resource over time. There are several reasons for +this: + +- Users may want to be able to roll back to a previous revision, or diff + against a previous revision. +- An API may create data which is derived in some way from a resource at a + given point in time. In these cases, it may be desirable to snapshot the + resource for reference later. + +**Note:** We use the word _revision_ to refer to a historical reference for a +particular resource, and intentionally avoid the term _version_, which refers +to the version of an API as a whole. + +## Guidance + +APIs **may** store a revision history for a resource if it is useful to users. + +APIs implementing resources with a revision history **must** provide a +`revision_id` field on the resource: + +```typescript +interface Book { + // The ID of the book. + id: string; + + // Other fields… + + // The revision ID of the book. + // A new revision is committed whenever the book is changed in any way. + // The format is an 8-character hexadecimal string. + readonly revisionId: string; + + // The timestamp that the revision was created. + readonly revisionCreateTime: string; // ISO 8601 +} +``` + +- The resource **must** contain a `revision_id` field, which **should** be a + string and contain a short, automatically-generated random string. A good + rule of thumb is the last eight characters of a UUID4. + - The `revision_id` field **must** document when new revisions are created + (see [committing revisions](#committing-revisions) below). + - The `revision_id` field **should** document the format of revision IDs. +- The resource **must** contain a `revision_create_time` field, which + **should** be a timestamp (see AIP-142). + +**Note:** A randomly generated string is preferred over other operations, such +as an auto-incrementing integer, because there is often a need to delete or +revert revisions, and a randomly generated string holds up better in those +situations. + +### Referencing revisions + +When it is necessary to refer to a specific revision of a resource, APIs +**must** use the following syntax: `{resource_id}@{revision_id}`. For example: + + publishers/123/books/les-miserables@c7cfa2a8 + +**Note:** The `@` character is selected because it is the only character +permitted by [RFC 1738 §2.2][] for special meaning within a URI scheme that is +not already used elsewhere. + +APIs **should** generally accept a resource reference at a particular revision +in any place where they ordinarily accept the resource ID. However, they **must +not** accept a revision in situations that mutate the resource, and should +error with `400 Bad Request` if one is given. + +**Important:** APIs **must not** require a revision ID, and **must** default to +the current revision if one is not provided, except in operations specifically +dealing with the revision history (such as rollback) where failing to require +it would not make sense (or lead to dangerous mistakes). + +### Getting a revision + +APIs implementing resource revisions **should** accept a resource ID with a +revision ID in the standard `Get` operation (AIP-131): + +{% tab proto %} + +{% sample 'revisions.proto', 'message GetBookRequest' %} + +{% endtabs %} + +If the user passes a revision ID that does not exist, the API **must** fail +with a `404 Not Found` error. + +APIs **must** return a `id` value corresponding to what the user sent. If the +user sent a resource ID with no revision ID, the returned `id` string **must +not** include the revision ID either. Similarly, if the user sent a resource ID +with a revision ID, the returned `id` string **must** explicitly include it. + +### Tagging revisions + +APIs implementing resource revisions **may** provide a mechanism for users to +tag a specific revision with a user provided name by implementing a "Tag +Revision" custom operation: + +{% tab proto %} + +{% sample 'revisions.proto', 'rpc TagBookRevision', 'message TagBookRevisionRequest' %} + +{% endtabs %} + +- The `id` field **should** require an explicit revision ID to be provided. + - The field **should** be [annotated as required][aip-203]. + - The field **should** identify the [resource type][aip-123] that it + references. +- The `tag` field **should** be [annotated as required][aip-203]. + - Additionally, tags **should** restrict letters to lower-case. +- Once a revision is tagged, the API **must** support using the tag in place of + the revision ID in `id` fields. + - If the user sends a tag, the API **must** return the tag in the resource's + `id` field, but the revision ID in the resource's `revision_id` field. + - If the user sends a revision ID, the API **must** return the revision ID in + both the `id` field and the `revision_id` field. +- If the user calls the `Tag` operation with an existing tag, the request + **must** succeed and the tag updated to point to the new requested revision + ID. This allows users to write code against specific tags (e.g. `published`) + and the revision can change in the background with no code change. + +### Listing revisions + +APIs implementing resource revisions **should** provide a custom operation for +listing the revision history for a resource, with a structure similar to +standard `List` operations (AIP-132): + +{% tab proto %} + +{% sample 'revisions.proto', 'rpc ListBookRevisions', 'message ListBookRevisionsRequest' %} + +{% endtabs %} + +While revision listing operations are mostly similar to standard `List` +operations (AIP-132), the following important differences apply: + +- The first field in the request message **must** be called `id` rather than + `parent` (this is listing revisions for a specific book, not a collection of + books). +- The URI **must** end with `:listRevisions`. +- Revisions **must** be ordered in reverse chronological order. An `order_by` + field **should not** be provided. +- The returned resources **must** include an explicit revision ID in the + resource's `id` field. + - If providing the full resource is expensive or infeasible, the revision + object **may** only populate the `id`, `revision_id`, and + `revision_create_time` fields instead. The `id` field **must** include the + resource ID and an explicit revision ID, which can be used for an explicit + `Get` request. The API **must** document that it will do this. + +### Child resources + +Resources with a revision history **may** have child resources. If they do, +there are two potential variants: + +- Child resources where each child resource is a child of the parent resource + as a whole. +- Child resources where each child resource is a child of _a single revision + of_ the parent resource. + +If a child resource is a child of a single revision, the child resource's name +**must** always explicitly include the parent's resource ID: + + publishers/123/books/les-miserables@c7cfa2a8/pages/42 + +In `List` requests for such resources, the service **should** default to the +latest revision of the parent if the user does not specify one, but **must** +explicitly include the parent's revision ID in the `id` field of resources in +the response. + +If necessary, APIs **may** explicitly support listing child resources across +parent revisions by accepting the `@-` syntax. For example: + + GET /v1/publishers/123/books/les-miserables@-/pages + +APIs **should not** include multiple levels of resources with revisions, as +this quickly becomes difficult to reason about. + +### Committing revisions + +Depending on the resource, different APIs may have different strategies for +when to commit a new revision, such as: + +- Commit a new revision any time that there is a change +- Commit a new revision when something important happens +- Commit a new revision when the user specifically asks + +APIs **may** use any of these strategies. APIs that want to commit a revision +on user request **should** handle this with a `Commit` custom operation: + +{% tab proto %} + +{% sample 'revisions.proto', 'rpc CommitBook', 'message CommitBookRequest' %} + +{% endtabs %} + +- The operation **must** use the `POST` HTTP method. +- The operation **should** return the resource, and the resource ID **must** + include the revision ID. +- The request message **must** include the `id` field. + - The field **should** be [annotated as required][aip-203]. + - The field **should** identify the [resource type][aip-123] that it + references. + +### Rollback + +A common use case for a resource with a revision history is the ability to roll +back to a given revision. APIs **should** handle this with a `Rollback` custom +operation: + +{% tab proto %} + +{% sample 'revisions.proto', 'rpc RollbackBook', 'message RollbackBookRequest' %} + +{% endtabs %} + +- The operation **must** use the `POST` HTTP method. +- The operation **should** return the resource, and the resource ID **must** + include the revision ID. +- The request message **must** have a `id` field to identify the resource being + rolled back. + - The field **should** be [annotated as required][aip-203]. + - The field **should** identify the [resource type][aip-123] that it + references. +- The request message **must** include a `revision_id` field. + - The API **must** fail the request with `NOT_FOUND` if the revision does not + exist on that resource. + - The field **should** be [annotated as required][aip-203]. + +**Note:** When rolling back, the API should return a _new_ revision of the +resource with a _new_ revision ID, rather than reusing the original ID. This +avoids problems with representing the same revision being active for multiple +ranges of time. + +### Deleting revisions + +Revisions are sometimes expensive to store, and there are valid use cases to +want to remove one or more revisions from a resource's revision history. + +APIs **may** define a operation to delete revisions, with a structure similar +to `Delete` (AIP-135) operations: + +{% tab proto %} + +{% sample 'revisions.proto', 'rpc DeleteBookRevision', 'message DeleteBookRevisionRequest' %} + +{% endtabs %} + +- The request message **must** have a `id` field to identify the resource + revision being deleted. + - The explicit revision ID **must** be required (the operation **must** fail + with `INVALID_ARGUMENT` if it is not provided, and **must not** default to + the latest revision). + - The field **should** be [annotated as required][aip-203]. + - The field **should** identify the [resource type][aip-123] that it + references. +- The API **must not** overload the `DeleteBook` operation to serve both + purposes (this could lead to dangerous or confusing mistakes). +- If the resource supports soft delete, then revisions of that resource + **should** also support soft delete. +- The operation **must not** cause the _resource_ to be deleted. + - Because a resource **should not** exist with zero revisions, the operation + **must** fail with `FAILED_PRECONDITION` if the user attempts to delete the + only revision. + +### Character Collision + +Most resource IDs have a restrictive set of characters, but some are very open. +For example, Google Cloud Storage allows the `@` character in filenames, which +are part of the resource ID, and therefore uses the `#` character to indicate a +revision. + +APIs **should not** permit the `@` character in resource IDs, and if APIs that +do permit it need to support resources with revisions, they **should** pick an +appropriate separator depending on how and where the API is used, and **must** +clearly document it. + +[rfc 1738 §2.2]: https://tools.ietf.org/html/rfc1738 diff --git a/aip/general/0162/aip.yaml b/aip/general/0162/aip.yaml new file mode 100644 index 00000000..ce8cc64c --- /dev/null +++ b/aip/general/0162/aip.yaml @@ -0,0 +1,7 @@ +--- +id: 162 +state: approved +created: 2019-09-17 +placement: + category: design-patterns + order: 88 diff --git a/aip/general/0162/revisions.proto b/aip/general/0162/revisions.proto new file mode 100644 index 00000000..b7363010 --- /dev/null +++ b/aip/general/0162/revisions.proto @@ -0,0 +1,202 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/field_behavior.proto"; +import "google/api/resource.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; + +// A library with revision-controlled books. +service Library { + // Retrieve a single book. + rpc GetBook(GetBookRequest) returns (Book) { + option (google.api.http) = { + get: "/v1/{id=publishers/*/books/*}" + }; + option (google.api.method_signature) = "id"; + } + + // List all revisions of a single book. + rpc ListBookRevisions(ListBookRevisionsRequest) + returns (ListBookRevisionsResponse) { + option (google.api.http) = { + get: "/v1/{id=publishers/*/books/*}:listRevisions" + }; + option (google.api.method_signature) = "id"; + } + + // Tag a single book revision with a user-specified tag. + // The tag may then be used in place of the canonical revision ID. + // + // If a tag is sent that already exists, the tag will be removed from its + // original revision and assigned to the provided revision. + rpc TagBookRevision(TagBookRevisionRequest) returns (Book) { + option (google.api.http) = { + post: "/v1/{id=publishers/*/books/*}:tagRevision" + body: "*" + }; + option (google.api.method_signature) = "id,tag"; + } + + // Delete a single revision of this book. + rpc DeleteBookRevision(DeleteBookRevisionRequest) + returns (google.protobuf.Empty) { + option (google.api.http) = { + delete: "/v1/{id=publishers/*/books/*}:deleteRevision" + }; + } + + // Save a new, discrete revision snapshot of the given book. + rpc CommitBook(CommitBookRequest) returns (Book) { + option (google.api.http) = { + post: "/v1/{id=publishers/*/books/*}:commit" + body: "*" + }; + option (google.api.method_signature) = "id"; + } + + // Rollback the primary revision of a book to a previous revision. + // + // This creates a new revision, with a new revision ID, with the contents + // of the provided revision, as the most recent revision in the sequence. + rpc RollbackBook(RollbackBookRequest) returns (Book) { + option (google.api.http) = { + post: "/v1/{id=publishers/*/books/*}:rollback" + body: "*" + }; + option (google.api.method_signature) = "id,revision_id"; + } +} + +// Request structure for book retreival. +message GetBookRequest { + // The ID of the book. + // Example: publishers/123/books/les-miserables + // + // In order to retrieve a previous revision of the book, also provide + // the revision ID. + // Example: publishers/123/books/les-miserables@c7cfa2a8 + string id = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = { + type: "library.googleapis.com/Book" + }]; +} + +// Request structure for listing book revisions. +message ListBookRevisionsRequest { + // The name of the book to list revisions for. + string id = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = { + type: "library.googleapis.com/Book" + }]; + + // The maximum number of revisions to return per page. + int32 page_size = 2; + + // The page token, received from a previous ListBookRevisions call. + // Provide this to retrieve the subsequent page. + string page_token = 3; +} + +// Response structure for listing book revisions. +message ListBookRevisionsResponse { + // The revisions of the book. + repeated Book books = 1; + + // A token that can be sent as `page_token` to retrieve the next page. + // If this field is omitted, there are no subsequent pages. + string next_page_token = 2; +} + +// Request structure for tagging a book revision. +message TagBookRevisionRequest { + // The name of the book to be tagged, including the revision ID. + string id = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = { + type: "library.googleapis.com/Book" + }]; + + // The tag to apply. The tag should be at most 40 characters, and match + // `[a-z][a-z0-9-]{3,38}[a-z0-9]`. + string tag = 2 [(google.api.field_behavior) = REQUIRED]; +} + +// Request structure for deleting a single book revision. +message DeleteBookRevisionRequest { + // The name of the book revision to be deleted, with a revision ID explicitly + // included. + // + // Example: publishers/123/books/les-miserables@c7cfa2a8 + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = { + type: "library.googleapis.com/Book" + }]; +} + +// Request structure for committing a new book revision. +message CommitBookRequest { + string id = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = { + type: "library.googleapis.com/Book" + }]; +} + +// Request structure for rolling back a book to a previous revision. +message RollbackBookRequest { + // The book being rolled back. + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = { + type: "library.googleapis.com/Book" + }]; + + // The revision ID to roll back to. + // It must be a revision of the same book. + // + // Example: c7cfa2a8 + string revision_id = 2 [(google.api.field_behavior) = REQUIRED]; +} + +// A representation of a single book. +message Book { + option (google.api.resource) = { + type: "library.googleapis.com/Book" + pattern: "publishers/{publisher}/books/{book}" + }; + + // The ID of the book. + string id = 1; + + // Other fields... + + // The revision ID of the book. + // A new revision is committed whenever the book is changed in any way. + // The format is an 8-character hexadecimal string. + string revision_id = 5 [ + (google.api.field_behavior) = IMMUTABLE, + (google.api.field_behavior) = OUTPUT_ONLY]; + + // The timestamp that the revision was created. + google.protobuf.Timestamp revision_create_time = 6 + [(google.api.field_behavior) = OUTPUT_ONLY]; +} From 32627fb776731aef333ae6cbe2b495c0d1da694c Mon Sep 17 00:00:00 2001 From: Luke Sneeringer Date: Wed, 19 May 2021 14:15:58 -0700 Subject: [PATCH 2/5] Working on the OAS examples. --- aip/general/0131/get.oas.yaml | 2 +- aip/general/0131/get.proto | 2 +- aip/general/0162/aip.md.j2 | 8 +++ aip/general/0162/revisions.oas.yaml | 106 ++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 aip/general/0162/revisions.oas.yaml diff --git a/aip/general/0131/get.oas.yaml b/aip/general/0131/get.oas.yaml index 86e9c77e..631edf7e 100644 --- a/aip/general/0131/get.oas.yaml +++ b/aip/general/0131/get.oas.yaml @@ -7,7 +7,7 @@ paths: /publishers/{publisherId}/books/{bookId}: get: operationId: getBook - description: Get a single book. + description: Retrieve a single book. responses: 200: description: OK diff --git a/aip/general/0131/get.proto b/aip/general/0131/get.proto index 01f25f9e..845c1036 100644 --- a/aip/general/0131/get.proto +++ b/aip/general/0131/get.proto @@ -20,7 +20,7 @@ import "google/api/field_behavior.proto"; import "google/api/resource.proto"; service Library { - // Get a single book. + // Retrieve a single book. rpc GetBook(GetBookRequest) returns (Book) { option (google.api.http) = { get: "/v1/{name=publishers/*/books/*}" diff --git a/aip/general/0162/aip.md.j2 b/aip/general/0162/aip.md.j2 index 64e6eaa7..999c053d 100644 --- a/aip/general/0162/aip.md.j2 +++ b/aip/general/0162/aip.md.j2 @@ -82,6 +82,10 @@ revision ID in the standard `Get` operation (AIP-131): {% sample 'revisions.proto', 'message GetBookRequest' %} +{% tab oas %} + +{% sample 'revisions.oas.yaml', '/publishers/{publisherId}/books/{bookId}' %} + {% endtabs %} If the user passes a revision ID that does not exist, the API **must** fail @@ -102,6 +106,10 @@ Revision" custom operation: {% sample 'revisions.proto', 'rpc TagBookRevision', 'message TagBookRevisionRequest' %} +{% tab oas %} + +{% sample 'revisions.oas.yaml', '/publishers/{publisherId}/books/{bookId}:tagRevision' %} + {% endtabs %} - The `id` field **should** require an explicit revision ID to be provided. diff --git a/aip/general/0162/revisions.oas.yaml b/aip/general/0162/revisions.oas.yaml new file mode 100644 index 00000000..5791494a --- /dev/null +++ b/aip/general/0162/revisions.oas.yaml @@ -0,0 +1,106 @@ +--- +openapi: 3.0.3 +info: + title: Library + version: 1.0.0 +paths: + /publishers/{publisherId}/books/{bookId}: + get: + operationId: getBook + description: Retrieve a single book. + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Book' + /publishers/{publisherId}/books/{bookId}:listRevisions: + get: + operationId: listBookRevisions + description: List all revisions of a single book. + parameters: + - name: maxPageSize + in: query + schema: + type: int32 + description: The maximum number of revisions to return per page. + - name: pageToken + in: query + schema: + type: string + description: | + The page token, received from a previous ListBookRevisions call. + Provide this to retrieve the subsequent page. + responses: + 200: + description: OK + content: + application/json: + description: Response structure for listing book revisions. + properties: + books: + type: array + items: + $ref: '#/components/schemas/Book' + description: The revisions of the book. + nextPageToken: + type: string + description: | + A token that can be sent as `pageToken` to retrieve the + next page. + + If this field is omitted, there are no subsequent pages. + /publishers/{publisherId}/books/{bookId}:tagRevision: + post: + operationId: tagBookRevision + description: | + Tag a single book revision with a user-specified tag. + The tag may then be used in place of the canonical revision ID. + + If a tag is sent that already exists, the tag will be removed from its + original revision and assigned to the provided revision. + requestBody: + content: + application/json: + description: Request structure for tagging a book revision. + properties: + id: + type: string + description: | + The ID of the book to be tagged, including the revision ID. + required: true + tag: + type: string + description: | + The tag to apply. The tag should be at most 40 characters, + and match `[a-z][a-z0-9-]{3,38}[a-z0-9]`. + required: true + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Book' +components: + schema: + Book: + description: A representation of a single book. + properties: + id: + type: string + description: | + The resource ID of the book. + Format: publishers/{publisher}/books/{book} + # Other fields... + revisionId: + type: string + description: | + The revision ID of the book. + A new revision is committed whenever the book is changed in any way. + The format is an 8-character hexadecimal string. + revisionCreateTime: + type: string + format: datetime + description: The timestamp when the revision was created. From ee71d9c56b6bd81521396ce97c70f7abde1c9f8b Mon Sep 17 00:00:00 2001 From: Luke Sneeringer Date: Mon, 24 May 2021 12:24:22 -0700 Subject: [PATCH 3/5] Complete the OpenAPI examples. --- aip/general/0162/aip.md.j2 | 18 +++++++++- aip/general/0162/revisions.oas.yaml | 54 +++++++++++++++++++++++++---- 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/aip/general/0162/aip.md.j2 b/aip/general/0162/aip.md.j2 index 999c053d..27d757b7 100644 --- a/aip/general/0162/aip.md.j2 +++ b/aip/general/0162/aip.md.j2 @@ -108,7 +108,7 @@ Revision" custom operation: {% tab oas %} -{% sample 'revisions.oas.yaml', '/publishers/{publisherId}/books/{bookId}:tagRevision' %} +{% sample 'revisions.oas.yaml', '/publishers/{publisherId}/books/{bookId}@{revisionId}:tagRevision' %} {% endtabs %} @@ -139,6 +139,10 @@ standard `List` operations (AIP-132): {% sample 'revisions.proto', 'rpc ListBookRevisions', 'message ListBookRevisionsRequest' %} +{% tab oas %} + +{% sample 'revisions.oas.yaml', '/publishers/{publisherId}/books/{bookId}:listRevisions' %} + {% endtabs %} While revision listing operations are mostly similar to standard `List` @@ -202,6 +206,10 @@ on user request **should** handle this with a `Commit` custom operation: {% sample 'revisions.proto', 'rpc CommitBook', 'message CommitBookRequest' %} +{% tab oas %} + +{% sample 'revisions.oas.yaml', '/publishers/{publisherId}/books/{bookId}:commit' %} + {% endtabs %} - The operation **must** use the `POST` HTTP method. @@ -222,6 +230,10 @@ operation: {% sample 'revisions.proto', 'rpc RollbackBook', 'message RollbackBookRequest' %} +{% tab oas %} + +{% sample 'revisions.oas.yaml', '/publishers/{publisherId}/books/{bookId}:rollback' %} + {% endtabs %} - The operation **must** use the `POST` HTTP method. @@ -254,6 +266,10 @@ to `Delete` (AIP-135) operations: {% sample 'revisions.proto', 'rpc DeleteBookRevision', 'message DeleteBookRevisionRequest' %} +{% tab oas %} + +{% sample 'revisions.oas.yaml', '/publishers/{publisherId}/books/{bookId}@{revisionId}:deleteRevision' %} + {% endtabs %} - The request message **must** have a `id` field to identify the resource diff --git a/aip/general/0162/revisions.oas.yaml b/aip/general/0162/revisions.oas.yaml index 5791494a..6a21e5d7 100644 --- a/aip/general/0162/revisions.oas.yaml +++ b/aip/general/0162/revisions.oas.yaml @@ -51,7 +51,7 @@ paths: next page. If this field is omitted, there are no subsequent pages. - /publishers/{publisherId}/books/{bookId}:tagRevision: + /publishers/{publisherId}/books/{bookId}@{revisionId}:tagRevision: post: operationId: tagBookRevision description: | @@ -65,11 +65,6 @@ paths: application/json: description: Request structure for tagging a book revision. properties: - id: - type: string - description: | - The ID of the book to be tagged, including the revision ID. - required: true tag: type: string description: | @@ -83,6 +78,53 @@ paths: application/json: schema: $ref: '#/components/schemas/Book' + /publishers/{publisherId}/books/{bookId}@{revisionId}:deleteRevision: + delete: + operationId: deleteBookRevision + description: Delete a single revision of this book. + responses: + 200: + description: OK + /publishers/{publisherId}/books/{bookId}:commit: + post: + operationId: commitBook + description: Save a new, discrete revision snapshot of the given book. + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Book' + /publishers/{publisherId}/books/{bookId}:rollback: + post: + operationId: rollbackBook + description: | + Rollback the primary revision of a book to a previous revision. + + This creates a new revision, with a new revision ID, with the contents + of the provided revision, as the most recent revision in the sequence. + requestBody: + content: + application/json: + description: | + Request structure for rolling back a book to a previous revision. + properties: + revisionId: + type: string + description: | + The revision ID to roll back to. + It must be a revision of the same book. + + Example: c7cfa2a8 + required: true + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Book' components: schema: Book: From 0269294c74ce5aa97cad8bbc59fb184a6a4749fb Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Sun, 30 Oct 2022 13:27:53 -0500 Subject: [PATCH 4/5] Improve/fix oas example --- aip/general/0162/revisions.oas.yaml | 139 ++++++++++++++++++++-------- 1 file changed, 98 insertions(+), 41 deletions(-) diff --git a/aip/general/0162/revisions.oas.yaml b/aip/general/0162/revisions.oas.yaml index 6a21e5d7..d95b4739 100644 --- a/aip/general/0162/revisions.oas.yaml +++ b/aip/general/0162/revisions.oas.yaml @@ -5,17 +5,24 @@ info: version: 1.0.0 paths: /publishers/{publisherId}/books/{bookId}: + parameters: + - $ref: "#/components/parameters/PublisherId" + - $ref: "#/components/parameters/BookId" get: operationId: getBook description: Retrieve a single book. responses: - 200: + '200': description: OK content: application/json: schema: $ref: '#/components/schemas/Book' + /publishers/{publisherId}/books/{bookId}:listRevisions: + parameters: + - $ref: "#/components/parameters/PublisherId" + - $ref: "#/components/parameters/BookId" get: operationId: listBookRevisions description: List all revisions of a single book. @@ -23,7 +30,8 @@ paths: - name: maxPageSize in: query schema: - type: int32 + type: integer + format: int32 description: The maximum number of revisions to return per page. - name: pageToken in: query @@ -33,25 +41,32 @@ paths: The page token, received from a previous ListBookRevisions call. Provide this to retrieve the subsequent page. responses: - 200: + '200': description: OK content: application/json: - description: Response structure for listing book revisions. - properties: - books: - type: array - items: - $ref: '#/components/schemas/Book' - description: The revisions of the book. - nextPageToken: - type: string - description: | - A token that can be sent as `pageToken` to retrieve the - next page. + schema: + description: Response structure for listing book revisions. + type: object + properties: + books: + type: array + items: + $ref: '#/components/schemas/Book' + description: The revisions of the book. + nextPageToken: + type: string + description: | + A token that can be sent as `pageToken` to retrieve the + next page. + + If this field is omitted, there are no subsequent pages. - If this field is omitted, there are no subsequent pages. /publishers/{publisherId}/books/{bookId}@{revisionId}:tagRevision: + parameters: + - $ref: "#/components/parameters/PublisherId" + - $ref: "#/components/parameters/BookId" + - $ref: "#/components/parameters/RevisionId" post: operationId: tagBookRevision description: | @@ -63,40 +78,56 @@ paths: requestBody: content: application/json: - description: Request structure for tagging a book revision. - properties: - tag: - type: string - description: | - The tag to apply. The tag should be at most 40 characters, - and match `[a-z][a-z0-9-]{3,38}[a-z0-9]`. - required: true + schema: + description: Request structure for tagging a book revision. + type: object + properties: + tag: + type: string + description: | + The tag to apply. + maxLength: 40 + pattern: "^[a-z][a-z0-9-]*[a-z0-9]$" + required: ["tag"] responses: - 200: + '200': description: OK content: application/json: schema: $ref: '#/components/schemas/Book' + /publishers/{publisherId}/books/{bookId}@{revisionId}:deleteRevision: + parameters: + - $ref: "#/components/parameters/PublisherId" + - $ref: "#/components/parameters/BookId" + - $ref: "#/components/parameters/RevisionId" delete: operationId: deleteBookRevision description: Delete a single revision of this book. responses: - 200: - description: OK + '204': + description: Revision deleted + /publishers/{publisherId}/books/{bookId}:commit: + parameters: + - $ref: "#/components/parameters/PublisherId" + - $ref: "#/components/parameters/BookId" post: operationId: commitBook description: Save a new, discrete revision snapshot of the given book. responses: - 200: + '200': description: OK content: application/json: schema: $ref: '#/components/schemas/Book' + /publishers/{publisherId}/books/{bookId}:rollback: + parameters: + - $ref: "#/components/parameters/PublisherId" + - $ref: "#/components/parameters/BookId" post: operationId: rollbackBook description: | @@ -105,28 +136,54 @@ paths: This creates a new revision, with a new revision ID, with the contents of the provided revision, as the most recent revision in the sequence. requestBody: + required: true content: application/json: - description: | - Request structure for rolling back a book to a previous revision. - properties: - revisionId: - type: string - description: | - The revision ID to roll back to. - It must be a revision of the same book. + schema: + description: | + Request structure for rolling back a book to a previous revision. + type: object + properties: + revisionId: + type: string + description: | + The revision ID to roll back to. + It must be a revision of the same book. + example: c7cfa2a8 + required: ["revisionId"] - Example: c7cfa2a8 - required: true responses: - 200: + '200': description: OK content: application/json: schema: $ref: '#/components/schemas/Book' + components: - schema: + parameters: + PublisherId: + name: publisherId + in: path + description: The id of the book publisher. + required: true + schema: + type: string + BookId: + name: bookId + in: path + description: The id of the book. + required: true + schema: + type: string + RevisionId: + name: revisionId + in: path + description: The id of a revision of the book. + required: true + schema: + type: string + schemas: Book: description: A representation of a single book. properties: @@ -144,5 +201,5 @@ components: The format is an 8-character hexadecimal string. revisionCreateTime: type: string - format: datetime + format: date-time description: The timestamp when the revision was created. From 2bbeec09bc1f44f5a8c7e4aa4907f2e7085c71cf Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Tue, 15 Nov 2022 12:53:16 -0600 Subject: [PATCH 5/5] Apply suggestions from code review Changes as agreed in working meeting on 11/15/2022. --- aip/general/0162/aip.md.j2 | 13 ++++++++++--- aip/general/0162/aip.yaml | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/aip/general/0162/aip.md.j2 b/aip/general/0162/aip.md.j2 index 27d757b7..4f4bc678 100644 --- a/aip/general/0162/aip.md.j2 +++ b/aip/general/0162/aip.md.j2 @@ -1,5 +1,8 @@ # Resource Revisions +| This AIP is pending final review. See the [#todos][] section for a list of known issues to be resolved. | +| :- | + Some APIs need to have resources with a revision history, where users can reason about the state of the resource over time. There are several reasons for this: @@ -60,7 +63,7 @@ When it is necessary to refer to a specific revision of a resource, APIs publishers/123/books/les-miserables@c7cfa2a8 **Note:** The `@` character is selected because it is the only character -permitted by [RFC 1738 §2.2][] for special meaning within a URI scheme that is +permitted by [RFC 3987][] for special meaning within a URI scheme that is not already used elsewhere. APIs **should** generally accept a resource reference at a particular revision @@ -234,9 +237,9 @@ operation: {% sample 'revisions.oas.yaml', '/publishers/{publisherId}/books/{bookId}:rollback' %} +- The operation **must** use the `POST` HTTP method. {% endtabs %} -- The operation **must** use the `POST` HTTP method. - The operation **should** return the resource, and the resource ID **must** include the revision ID. - The request message **must** have a `id` field to identify the resource being @@ -301,4 +304,8 @@ do permit it need to support resources with revisions, they **should** pick an appropriate separator depending on how and where the API is used, and **must** clearly document it. -[rfc 1738 §2.2]: https://tools.ietf.org/html/rfc1738 +## Todos + +- Revisit the selection of the `@` character as the delimiter for the version. + +[rfc 3987]: https://tools.ietf.org/html/rfc3987 diff --git a/aip/general/0162/aip.yaml b/aip/general/0162/aip.yaml index ce8cc64c..18ebcbbb 100644 --- a/aip/general/0162/aip.yaml +++ b/aip/general/0162/aip.yaml @@ -1,6 +1,6 @@ --- id: 162 -state: approved +state: pending created: 2019-09-17 placement: category: design-patterns