From 21710663e043d70a04ee5f66cf57da159bcb3f27 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Tue, 12 Nov 2024 21:54:06 -0800 Subject: [PATCH 1/5] feat(137): add apply AEP add a new standard method to document apply / PUT. This AEP is useful for: - APIs that would like to offer a PUT endpoint, but has lacked guidance from the AEPs. - APIs that would like to expose a declarative-friendly API endpoint (the integration of which is simpler than apply). Apply can also serve as an alternative to expose a create and update endpoint. --- aep/general/0137/aep.md.j2 | 143 ++++ aep/general/0137/aep.yaml | 8 + aep/general/example.oas.yaml | 1276 +++++++++++++++++++++++----------- aep/general/example.proto | 147 ---- 4 files changed, 1003 insertions(+), 571 deletions(-) create mode 100644 aep/general/0137/aep.md.j2 create mode 100644 aep/general/0137/aep.yaml diff --git a/aep/general/0137/aep.md.j2 b/aep/general/0137/aep.md.j2 new file mode 100644 index 00000000..443e962a --- /dev/null +++ b/aep/general/0137/aep.md.j2 @@ -0,0 +1,143 @@ +# Apply + +In REST APIs, it is customary to make a `PUT` request to a collection's URI (for +example, `/v1/publishers/{publisher}/books`) in order to create or update a new +resource within that collection. + +Resource-oriented design (AEP-121) honors this pattern through the `Apply` +method. These RPCs accept the parent collection and the resource to create or +update, and return the final resource. + +## Guidance + +APIs **should** provide an apply method for resources unless it is not valuable +for users to do so. + +### Operation + +Apply methods are specified using the following pattern: + +- The HTTP verb **must** be `PUT`. +- Some resources take longer to be applied than is reasonable for a regular API + request. In this situation, the API **should** use a [long-running + operation](/long-running-operations). + +{% tab proto %} + +{% sample '../example.proto', 'rpc ApplyBook' %} + +- The RPC's name **must** begin with the word `Apply`. The remainder of the + RPC name **should** be the singular form of the resource being applied. + - The request message **must** match the RPC name, with a `Request` suffix. +- The collection's parent resource **must** be called `parent`, and + **should** be the only variable in the URI path. + - The collection identifier (`books` in the above example) **must** be a + literal string. +- There **must** be a `body` key in the `google.api.http` annotation, and it + **must** map to the resource field in the request message. + - All remaining fields **should** map to URI query parameters. +- There **should** be exactly one `google.api.method_signature` annotation, + with a value of `"parent,{resource},id"`, or "`"parent,{resource}"` if the + resource ID is not required. +- The operation **must** have [strong consistency][]. + +{% tab oas %} + +```http +PUT /v1/publishers/{publisher}/books/{book} HTTP/2 +Host: bookstore.example.com +Accept: application/json +{ + "title": "Pride and Prejudice", + "author": "Jane Austen" +} +``` + +{% endtabs %} + +### Requests + +Apply methods implement a common request message pattern: + +- A `path` **must** be supported, and map to the URI path. +- The resource **must** be included and **must** map to the POST body. +- The request message **must not** contain any other required fields and + **should not** contain other optional fields except those described in this + or another AEP. + +{% tab proto %} + +{% sample '../example.proto', 'message ApplyBookRequest' %} + +- A `path` field **must** be included, that is the path of the resource to apply. + - The field **must** be [annotated as `REQUIRED`][aep-203]. + - The field **must** identify the [resource type][aep-4] of the resource + being applied. +- The request message **must not** contain any other required fields and + **should not** contain other optional fields except those described in this + or another AEP. + +{% tab oas %} + +{% sample '../example.oas.yaml', '$.paths./publishers/{publisher}/books/{book}.put.requestBody' %} + +- The request body **must** be the resource being applied. + +{% endtabs %} + +### Responses + +- The response **must** be the resource itself. There is no separate response + schema. + - The response **should** include the fully-populated resource, and **must** + include any fields that were provided unless they are input only (see + AEP-203). + +{% tab proto %} + +{% sample '../example.proto', 'message Book' %} + +{% tab oas %} + +{% sample '../example.oas.yaml', '$.paths./publishers/{publisher}/books/{book}.put.responses.200' %} + +{% endtabs %} + +### Errors + +See [errors][], in particular [when to use PERMISSION_DENIED and NOT_FOUND +errors][permission-denied]. + +## Interface Definitions + +{% tab proto %} + +{% sample '../example.proto', 'rpc ApplyBook' %} + +{% sample '../example.proto', 'message ApplyBookRequest' %} + +{% sample '../example.proto', 'message Book' %} + +{% tab oas %} + +{% sample '../example.oas.yaml', '$.paths./publishers/{publisher}/books/{book}.put' %} + +{% endtabs %} + +## Further reading + +- For ensuring idempotency in `Apply` methods, see AEP-155. +- For naming resources involving Unicode, see AEP-210. + +[aep-121]: ./0121.md +[aep-122]: ./0122.md +[aep-4]: ./0004.md +[aep-155]: ./0155.md +[aep-203]: ./0203.md +[aep-210]: ./0210.md +[data plane]: ./0111.md#data-plane +[errors]: ./0193.md +[field_behavior]: ./203.md +[Declarative clients]: ./0003.md#declarative-clients +[permission-denied]: ./0193.md#permission-denied +[strong consistency]: ./0121.md#strong-consistency diff --git a/aep/general/0137/aep.yaml b/aep/general/0137/aep.yaml new file mode 100644 index 00000000..67cd17f7 --- /dev/null +++ b/aep/general/0137/aep.yaml @@ -0,0 +1,8 @@ +--- +id: 137 +state: approved +slug: apply +created: 2024-11-13 +placement: + category: standard-methods + order: 50 diff --git a/aep/general/example.oas.yaml b/aep/general/example.oas.yaml index dd36f7e2..58233319 100644 --- a/aep/general/example.oas.yaml +++ b/aep/general/example.oas.yaml @@ -1,424 +1,852 @@ -components: - schemas: - book: - properties: - author: - items: - type: object - type: array - edition: - format: int32 - type: integer - etag: - type: string - id: - readOnly: true - type: string - x-terraform-id: true - path: - readOnly: true - type: string - price: - format: float - type: number - published: - type: boolean - required: - - price - - published - type: object - x-aep-resource: - parents: - - publisher - patterns: - - /publishers/{publisher}/books/{book} - plural: books - singular: book - book-edition: - properties: - displayname: - type: string - id: - readOnly: true - type: string - x-terraform-id: true - path: - readOnly: true - type: string - required: - - displayname - type: object - x-aep-resource: - parents: - - book - patterns: - - /publishers/{publisher}/books/{book}/editions/{book-edition} - plural: book-editions - singular: book-edition - isbn: - properties: - id: - readOnly: true - type: string - x-terraform-id: true - path: - readOnly: true - type: string - type: object - x-aep-resource: - patterns: - - /isbns/{isbn} - plural: isbns - singular: isbn - publisher: - properties: - description: - type: string - etag: - type: string - id: - readOnly: true - type: string - x-terraform-id: true - path: - readOnly: true - type: string - type: object - x-aep-resource: - patterns: - - /publishers/{publisher} - plural: publishers - singular: publisher -info: - title: bookstore.example.com - version: version not set -openapi: 3.1.0 -paths: - /isbns: - post: - parameters: - - in: query - name: id - required: true - type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/isbn' - required: true - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/isbn' - description: Successful response - /isbns/{isbn}: - get: - parameters: - - in: path - name: isbn - required: true - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/isbn' - description: Successful response - /publishers: - get: - parameters: - - in: query - name: max_page_size - required: true - type: integer - - in: query - name: page_token - required: true - type: string - responses: - '200': - content: - application/json: - schema: - properties: - results: - items: - $ref: '#/components/schemas/publisher' - type: array - type: object - description: Successful response - post: - parameters: - - in: query - name: id - required: true - type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/publisher' - required: true - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/publisher' - description: Successful response - /publishers/{publisher}: - delete: - parameters: - - in: path - name: publisher - required: true - type: string - responses: - '200': - content: null - description: '' - get: - parameters: - - in: path - name: publisher - required: true - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/publisher' - description: Successful response - patch: - parameters: - - in: path - name: publisher - required: true - type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/publisher' - required: true - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/publisher' - description: Successful response - /publishers/{publisher}/books: - get: - parameters: - - in: path - name: publisher - required: true - type: string - - in: query - name: max_page_size - required: true - type: integer - - in: query - name: page_token - required: true - type: string - responses: - '200': - content: - application/json: - schema: - properties: - results: - items: - $ref: '#/components/schemas/book' - type: array - unreachable: - items: - type: string - type: array - type: object - description: Successful response - post: - parameters: - - in: path - name: publisher - required: true - type: string - - in: query - name: id - required: true - type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/book' - required: true - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/book' - description: Successful response - /publishers/{publisher}/books/{book}: - delete: - parameters: - - in: path - name: publisher - required: true - type: string - - in: path - name: book - required: true - type: string - responses: - '200': - content: null - description: '' - get: - parameters: - - in: path - name: publisher - required: true - type: string - - in: path - name: book - required: true - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/book' - description: Successful response - patch: - parameters: - - in: path - name: publisher - required: true - type: string - - in: path - name: book - required: true - type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/book' - required: true - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/book' - description: Successful response - /publishers/{publisher}/books/{book}/editions: - get: - parameters: - - in: path - name: publisher - required: true - type: string - - in: path - name: book - required: true - type: string - - in: query - name: max_page_size - required: true - type: integer - - in: query - name: page_token - required: true - type: string - responses: - '200': - content: - application/json: - schema: - properties: - results: - items: - $ref: '#/components/schemas/book-edition' - type: array - type: object - description: Successful response - post: - parameters: - - in: path - name: publisher - required: true - type: string - - in: path - name: book - required: true - type: string - - in: query - name: id - required: true - type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/book-edition' - required: true - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/book-edition' - description: Successful response - /publishers/{publisher}/books/{book}/editions/{book-edition}: - delete: - parameters: - - in: path - name: publisher - required: true - type: string - - in: path - name: book - required: true - type: string - - in: path - name: book-edition - required: true - type: string - responses: - '200': - content: null - description: '' - get: - parameters: - - in: path - name: publisher - required: true - type: string - - in: path - name: book - required: true - type: string - - in: path - name: book-edition - required: true - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/book-edition' - description: Successful response -servers: - - url: http://localhost:8081 +{ + 'openapi': '3.1.0', + 'servers': [{ 'url': 'http://localhost:8081' }], + 'info': { 'title': 'bookstore.example.com', 'version': 'version not set' }, + 'paths': + { + '/isbns': + { + 'get': + { + 'responses': + { + '200': + { + 'description': 'Successful response', + 'content': + { + 'application/json': + { + 'schema': + { + 'type': 'object', + 'properties': + { + 'results': + { + 'type': 'array', + 'items': + { + '$ref': '#/components/schemas/isbn', + }, + }, + }, + }, + }, + }, + }, + }, + 'parameters': + [ + { + 'in': 'query', + 'name': 'max_page_size', + 'required': true, + 'type': 'integer', + }, + { + 'in': 'query', + 'name': 'page_token', + 'required': true, + 'type': 'string', + }, + ], + }, + 'post': + { + 'responses': + { + '200': + { + 'description': 'Successful response', + 'content': + { + 'application/json': + { + 'schema': + { '$ref': '#/components/schemas/isbn' }, + }, + }, + }, + }, + 'requestBody': + { + 'required': true, + 'content': + { + 'application/json': + { 'schema': { '$ref': '#/components/schemas/isbn' } }, + }, + }, + }, + }, + '/isbns/{isbn}': + { + 'get': + { + 'responses': + { + '200': + { + 'description': 'Successful response', + 'content': + { + 'application/json': + { + 'schema': + { '$ref': '#/components/schemas/isbn' }, + }, + }, + }, + }, + 'parameters': + [ + { + 'in': 'path', + 'name': 'isbn', + 'required': true, + 'type': 'string', + }, + ], + }, + }, + '/publishers': + { + 'get': + { + 'responses': + { + '200': + { + 'description': 'Successful response', + 'content': + { + 'application/json': + { + 'schema': + { + 'type': 'object', + 'properties': + { + 'results': + { + 'type': 'array', + 'items': + { + '$ref': '#/components/schemas/publisher', + }, + }, + }, + }, + }, + }, + }, + }, + 'parameters': + [ + { + 'in': 'query', + 'name': 'max_page_size', + 'required': true, + 'type': 'integer', + }, + { + 'in': 'query', + 'name': 'page_token', + 'required': true, + 'type': 'string', + }, + ], + }, + 'post': + { + 'responses': + { + '200': + { + 'description': 'Successful response', + 'content': + { + 'application/json': + { + 'schema': + { '$ref': '#/components/schemas/publisher' }, + }, + }, + }, + }, + 'parameters': + [ + { + 'in': 'query', + 'name': 'id', + 'required': true, + 'type': 'string', + }, + ], + 'requestBody': + { + 'required': true, + 'content': + { + 'application/json': + { + 'schema': + { '$ref': '#/components/schemas/publisher' }, + }, + }, + }, + }, + }, + '/publishers/{publisher}': + { + 'delete': + { + 'responses': { '200': { 'description': '', 'content': null } }, + 'parameters': + [ + { + 'in': 'path', + 'name': 'publisher', + 'required': true, + 'type': 'string', + }, + { 'in': 'query', 'name': 'force', 'type': 'boolean' }, + ], + }, + 'get': + { + 'responses': + { + '200': + { + 'description': 'Successful response', + 'content': + { + 'application/json': + { + 'schema': + { '$ref': '#/components/schemas/publisher' }, + }, + }, + }, + }, + 'parameters': + [ + { + 'in': 'path', + 'name': 'publisher', + 'required': true, + 'type': 'string', + }, + ], + }, + 'patch': + { + 'responses': + { + '200': + { + 'description': 'Successful response', + 'content': + { + 'application/json': + { + 'schema': + { '$ref': '#/components/schemas/publisher' }, + }, + }, + }, + }, + 'parameters': + [ + { + 'in': 'path', + 'name': 'publisher', + 'required': true, + 'type': 'string', + }, + ], + 'requestBody': + { + 'required': true, + 'content': + { + 'application/json': + { + 'schema': + { '$ref': '#/components/schemas/publisher' }, + }, + }, + }, + }, + 'put': + { + 'responses': + { + '200': + { + 'description': 'Successful response', + 'content': + { + 'application/json': + { + 'schema': + { '$ref': '#/components/schemas/publisher' }, + }, + }, + }, + }, + 'parameters': + [ + { + 'in': 'path', + 'name': 'publisher', + 'required': true, + 'type': 'string', + }, + ], + 'requestBody': + { + 'required': true, + 'content': + { + 'application/json': + { + 'schema': + { '$ref': '#/components/schemas/publisher' }, + }, + }, + }, + }, + }, + '/publishers/{publisher}/books': + { + 'get': + { + 'responses': + { + '200': + { + 'description': 'Successful response', + 'content': + { + 'application/json': + { + 'schema': + { + 'type': 'object', + 'properties': + { + 'results': + { + 'type': 'array', + 'items': + { + '$ref': '#/components/schemas/book', + }, + }, + }, + }, + }, + }, + }, + }, + 'parameters': + [ + { + 'in': 'path', + 'name': 'publisher', + 'required': true, + 'type': 'string', + }, + { + 'in': 'query', + 'name': 'max_page_size', + 'required': true, + 'type': 'integer', + }, + { + 'in': 'query', + 'name': 'page_token', + 'required': true, + 'type': 'string', + }, + ], + }, + 'post': + { + 'responses': + { + '200': + { + 'description': 'Successful response', + 'content': + { + 'application/json': + { + 'schema': + { '$ref': '#/components/schemas/book' }, + }, + }, + }, + }, + 'parameters': + [ + { + 'in': 'path', + 'name': 'publisher', + 'required': true, + 'type': 'string', + }, + { + 'in': 'query', + 'name': 'id', + 'required': true, + 'type': 'string', + }, + ], + 'requestBody': + { + 'required': true, + 'content': + { + 'application/json': + { 'schema': { '$ref': '#/components/schemas/book' } }, + }, + }, + }, + }, + '/publishers/{publisher}/books/{book}': + { + 'delete': + { + 'responses': { '200': { 'description': '', 'content': null } }, + 'parameters': + [ + { + 'in': 'path', + 'name': 'publisher', + 'required': true, + 'type': 'string', + }, + { + 'in': 'path', + 'name': 'book', + 'required': true, + 'type': 'string', + }, + { 'in': 'query', 'name': 'force', 'type': 'boolean' }, + ], + }, + 'get': + { + 'responses': + { + '200': + { + 'description': 'Successful response', + 'content': + { + 'application/json': + { + 'schema': + { '$ref': '#/components/schemas/book' }, + }, + }, + }, + }, + 'parameters': + [ + { + 'in': 'path', + 'name': 'publisher', + 'required': true, + 'type': 'string', + }, + { + 'in': 'path', + 'name': 'book', + 'required': true, + 'type': 'string', + }, + ], + }, + 'patch': + { + 'responses': + { + '200': + { + 'description': 'Successful response', + 'content': + { + 'application/json': + { + 'schema': + { '$ref': '#/components/schemas/book' }, + }, + }, + }, + }, + 'parameters': + [ + { + 'in': 'path', + 'name': 'publisher', + 'required': true, + 'type': 'string', + }, + { + 'in': 'path', + 'name': 'book', + 'required': true, + 'type': 'string', + }, + ], + 'requestBody': + { + 'required': true, + 'content': + { + 'application/json': + { 'schema': { '$ref': '#/components/schemas/book' } }, + }, + }, + }, + 'put': + { + 'responses': + { + '200': + { + 'description': 'Successful response', + 'content': + { + 'application/json': + { + 'schema': + { '$ref': '#/components/schemas/book' }, + }, + }, + }, + }, + 'parameters': + [ + { + 'in': 'path', + 'name': 'publisher', + 'required': true, + 'type': 'string', + }, + { + 'in': 'path', + 'name': 'book', + 'required': true, + 'type': 'string', + }, + ], + 'requestBody': + { + 'required': true, + 'content': + { + 'application/json': + { 'schema': { '$ref': '#/components/schemas/book' } }, + }, + }, + }, + }, + '/publishers/{publisher}/books/{book}/editions': + { + 'get': + { + 'responses': + { + '200': + { + 'description': 'Successful response', + 'content': + { + 'application/json': + { + 'schema': + { + 'type': 'object', + 'properties': + { + 'results': + { + 'type': 'array', + 'items': + { + '$ref': '#/components/schemas/book-edition', + }, + }, + }, + }, + }, + }, + }, + }, + 'parameters': + [ + { + 'in': 'path', + 'name': 'publisher', + 'required': true, + 'type': 'string', + }, + { + 'in': 'path', + 'name': 'book', + 'required': true, + 'type': 'string', + }, + { + 'in': 'query', + 'name': 'max_page_size', + 'required': true, + 'type': 'integer', + }, + { + 'in': 'query', + 'name': 'page_token', + 'required': true, + 'type': 'string', + }, + ], + }, + 'post': + { + 'responses': + { + '200': + { + 'description': 'Successful response', + 'content': + { + 'application/json': + { + 'schema': + { + '$ref': '#/components/schemas/book-edition', + }, + }, + }, + }, + }, + 'parameters': + [ + { + 'in': 'path', + 'name': 'publisher', + 'required': true, + 'type': 'string', + }, + { + 'in': 'path', + 'name': 'book', + 'required': true, + 'type': 'string', + }, + { + 'in': 'query', + 'name': 'id', + 'required': true, + 'type': 'string', + }, + ], + 'requestBody': + { + 'required': true, + 'content': + { + 'application/json': + { + 'schema': + { '$ref': '#/components/schemas/book-edition' }, + }, + }, + }, + }, + }, + '/publishers/{publisher}/books/{book}/editions/{book-edition}': + { + 'delete': + { + 'responses': { '200': { 'description': '', 'content': null } }, + 'parameters': + [ + { + 'in': 'path', + 'name': 'publisher', + 'required': true, + 'type': 'string', + }, + { + 'in': 'path', + 'name': 'book', + 'required': true, + 'type': 'string', + }, + { + 'in': 'path', + 'name': 'book-edition', + 'required': true, + 'type': 'string', + }, + ], + }, + 'get': + { + 'responses': + { + '200': + { + 'description': 'Successful response', + 'content': + { + 'application/json': + { + 'schema': + { + '$ref': '#/components/schemas/book-edition', + }, + }, + }, + }, + }, + 'parameters': + [ + { + 'in': 'path', + 'name': 'publisher', + 'required': true, + 'type': 'string', + }, + { + 'in': 'path', + 'name': 'book', + 'required': true, + 'type': 'string', + }, + { + 'in': 'path', + 'name': 'book-edition', + 'required': true, + 'type': 'string', + }, + ], + }, + }, + '/publishers/{publisher}:archive': + { + 'post': + { + 'responses': + { + '200': + { + 'description': 'Successful response', + 'content': + { + 'application/json': + { + 'schema': + { '$ref': '#/components/schemas/publisher' }, + }, + }, + }, + }, + 'parameters': + [ + { + 'in': 'path', + 'name': 'publisher', + 'required': true, + 'type': 'string', + }, + ], + 'requestBody': + { + 'required': true, + 'content': { 'application/json': { 'schema': {} } }, + }, + }, + }, + }, + 'components': + { + 'schemas': + { + 'book': + { + 'type': 'object', + 'required': ['isbn', 'price', 'published'], + 'properties': + { + 'author': { 'type': 'array', 'items': { 'type': 'object' } }, + 'edition': { 'type': 'integer', 'format': 'int32' }, + 'isbn': { 'type': 'array', 'items': { 'type': 'string' } }, + 'path': { 'type': 'string', 'readOnly': true }, + 'price': { 'type': 'number', 'format': 'float' }, + 'published': { 'type': 'boolean' }, + }, + 'x-aep-resource': + { + 'singular': 'book', + 'plural': 'books', + 'patterns': ['/publishers/{publisher}/books/{book}'], + 'parents': ['publisher'], + }, + }, + 'book-edition': + { + 'type': 'object', + 'required': ['displayname'], + 'properties': + { + 'displayname': { 'type': 'string' }, + 'path': { 'type': 'string', 'readOnly': true }, + }, + 'x-aep-resource': + { + 'singular': 'book-edition', + 'plural': 'book-editions', + 'patterns': + [ + '/publishers/{publisher}/books/{book}/editions/{book-edition}', + ], + 'parents': ['book'], + }, + }, + 'isbn': + { + 'type': 'object', + 'properties': { 'path': { 'type': 'string', 'readOnly': true } }, + 'x-aep-resource': + { + 'singular': 'isbn', + 'plural': 'isbns', + 'patterns': ['/isbns/{isbn}'], + }, + }, + 'publisher': + { + 'type': 'object', + 'properties': + { + 'description': { 'type': 'string' }, + 'path': { 'type': 'string', 'readOnly': true }, + }, + 'x-aep-resource': + { + 'singular': 'publisher', + 'plural': 'publishers', + 'patterns': ['/publishers/{publisher}'], + }, + }, + }, + }, +} diff --git a/aep/general/example.proto b/aep/general/example.proto index 12dec3e7..cf4ec078 100644 --- a/aep/general/example.proto +++ b/aep/general/example.proto @@ -100,47 +100,6 @@ service Bookstore { option (google.api.method_signature) = "parent"; } - // An aep-compliant Create method for hil-device. - rpc CreateHilDevice ( CreateHilDeviceRequest ) returns ( HilDevice ) { - option (google.api.http) = { - post: "/{parent=hil-devices}", - body: "hil_device" - }; - - option (google.api.method_signature) = "hil_device"; - } - - // An aep-compliant Get method for hil-device. - rpc GetHilDevice ( GetHilDeviceRequest ) returns ( HilDevice ) { - option (google.api.http) = { get: "/{path=hil-devices/*}" }; - - option (google.api.method_signature) = "path"; - } - - // An aep-compliant Update method for hil-device. - rpc UpdateHilDevice ( UpdateHilDeviceRequest ) returns ( HilDevice ) { - option (google.api.http) = { - patch: "/{path=hil-devices/*}", - body: "hil_device" - }; - - option (google.api.method_signature) = "hil_device,update_mask"; - } - - // An aep-compliant Delete method for hil-device. - rpc DeleteHilDevice ( DeleteHilDeviceRequest ) returns ( google.protobuf.Empty ) { - option (google.api.http) = { delete: "/{path=hil-devices/*}" }; - - option (google.api.method_signature) = "path"; - } - - // An aep-compliant List method for hil-devices. - rpc ListHilDevices ( ListHilDevicesRequest ) returns ( ListHilDevicesResponse ) { - option (google.api.http) = { get: "/{parent=hil-devices}" }; - - option (google.api.method_signature) = "parent"; - } - // An aep-compliant Create method for isbn. rpc CreateIsbn ( CreateIsbnRequest ) returns ( Isbn ) { option (google.api.http) = { post: "/{parent=isbns}", body: "isbn" }; @@ -249,9 +208,6 @@ message Book { // Field for path. string path = 10000; - - // Field for id. - string id = 10001; } // A Create request for a book resource. @@ -357,9 +313,6 @@ message BookEdition { // Field for path. string path = 10000; - - // Field for id. - string id = 10001; } // A Create request for a book-edition resource. @@ -419,97 +372,6 @@ message ListBookEditionsResponse { string next_page_token = 10011; } -// A HilDevice. -message HilDevice { - option (google.api.resource) = { - type: "bookstore.example.com/hil-device", - pattern: [ "hil-devices/{hil-device}" ], - plural: "hil-devices", - singular: "hil-device" - }; - - // Field for name. - string name = 1; - - // Field for path. - string path = 10000; - - // Field for id. - string id = 10001; -} - -// A Create request for a hil-device resource. -message CreateHilDeviceRequest { - // A field for the parent of hil-device - string parent = 10013 [ - (google.api.field_behavior) = REQUIRED, - (google.api.resource_reference) = { } - ]; - - // An id that uniquely identifies the resource within the collection - string id = 10014; - - // The resource to perform the operation on. - HilDevice hil_device = 10015 [(google.api.field_behavior) = REQUIRED]; -} - -// Request message for the Gethil-device method -message GetHilDeviceRequest { - // The globally unique identifier for the resource - string path = 10018 [ - (google.api.field_behavior) = REQUIRED, - (google.api.resource_reference) = { type: "bookstore.example.com/hil-device" } - ]; -} - -// Request message for the UpdateHilDevice method -message UpdateHilDeviceRequest { - // The globally unique identifier for the resource - string path = 10018 [ - (google.api.field_behavior) = REQUIRED, - (google.api.resource_reference) = { type: "bookstore.example.com/hil-device" } - ]; - - // The resource to perform the operation on. - HilDevice hil_device = 10015 [(google.api.field_behavior) = REQUIRED]; - - // The update mask for the resource - google.protobuf.FieldMask update_mask = 10012; -} - -// Request message for the DeleteHilDevice method -message DeleteHilDeviceRequest { - // The globally unique identifier for the resource - string path = 10018 [ - (google.api.field_behavior) = REQUIRED, - (google.api.resource_reference) = { type: "bookstore.example.com/hil-device" } - ]; -} - -// Request message for the Listhil-device method -message ListHilDevicesRequest { - // A field for the parent of hil-device - string parent = 10013 [ - (google.api.field_behavior) = REQUIRED, - (google.api.resource_reference) = { } - ]; - - // The page token indicating the starting point of the page - string page_token = 10010; - - // The maximum number of resources to return in a single page. - int32 max_page_size = 10017; -} - -// Response message for the Listhil-device method -message ListHilDevicesResponse { - // A list of hil-devices - repeated HilDevice results = 10016; - - // The page token indicating the ending point of this response. - string next_page_token = 10011; -} - // A Isbn. message Isbn { option (google.api.resource) = { @@ -521,9 +383,6 @@ message Isbn { // Field for path. string path = 10000; - - // Field for id. - string id = 10001; } // A Create request for a isbn resource. @@ -534,9 +393,6 @@ message CreateIsbnRequest { (google.api.resource_reference) = { } ]; - // An id that uniquely identifies the resource within the collection - string id = 10014; - // The resource to perform the operation on. Isbn isbn = 10015 [(google.api.field_behavior) = REQUIRED]; } @@ -588,9 +444,6 @@ message Publisher { // Field for path. string path = 10000; - - // Field for id. - string id = 10001; } // A Create request for a publisher resource. From 74a6cfabfecbc4b7c718370b0f6e4f16054b4d78 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Sat, 16 Nov 2024 13:22:05 -0800 Subject: [PATCH 2/5] Apply suggestions from code review Co-authored-by: Richard Frankel --- aep/general/0137/aep.md.j2 | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/aep/general/0137/aep.md.j2 b/aep/general/0137/aep.md.j2 index 443e962a..d5de478b 100644 --- a/aep/general/0137/aep.md.j2 +++ b/aep/general/0137/aep.md.j2 @@ -1,12 +1,12 @@ # Apply In REST APIs, it is customary to make a `PUT` request to a collection's URI (for -example, `/v1/publishers/{publisher}/books`) in order to create or update a new +example, `/v1/publishers/{publisher}/books`) in order to create or replace a new resource within that collection. Resource-oriented design (AEP-121) honors this pattern through the `Apply` method. These RPCs accept the parent collection and the resource to create or -update, and return the final resource. +replace, and then return, the final resource. ## Guidance @@ -22,24 +22,25 @@ Apply methods are specified using the following pattern: request. In this situation, the API **should** use a [long-running operation](/long-running-operations). -{% tab proto %} - -{% sample '../example.proto', 'rpc ApplyBook' %} - -- The RPC's name **must** begin with the word `Apply`. The remainder of the - RPC name **should** be the singular form of the resource being applied. - - The request message **must** match the RPC name, with a `Request` suffix. +- The method's name **must** begin with the word `Apply`. The remainder of the + method name **should** be the singular form of the resource being applied. - The collection's parent resource **must** be called `parent`, and **should** be the only variable in the URI path. - The collection identifier (`books` in the above example) **must** be a literal string. +- The operation **must** have [strong consistency][]. + +{% tab proto %} + +{% sample '../example.proto', 'rpc ApplyBook' %} + +- The request message **must** match the method name, with a `Request` suffix. - There **must** be a `body` key in the `google.api.http` annotation, and it **must** map to the resource field in the request message. - All remaining fields **should** map to URI query parameters. - There **should** be exactly one `google.api.method_signature` annotation, with a value of `"parent,{resource},id"`, or "`"parent,{resource}"` if the resource ID is not required. -- The operation **must** have [strong consistency][]. {% tab oas %} @@ -59,9 +60,9 @@ Accept: application/json Apply methods implement a common request message pattern: -- A `path` **must** be supported, and map to the URI path. +- A `path` field **must** be supported, and map to the URI path. - The resource **must** be included and **must** map to the POST body. -- The request message **must not** contain any other required fields and +- The request schema **must not** contain any other required fields and **should not** contain other optional fields except those described in this or another AEP. @@ -69,7 +70,7 @@ Apply methods implement a common request message pattern: {% sample '../example.proto', 'message ApplyBookRequest' %} -- A `path` field **must** be included, that is the path of the resource to apply. +- A `path` field specifying the path of the resource **must** be included. - The field **must** be [annotated as `REQUIRED`][aep-203]. - The field **must** identify the [resource type][aep-4] of the resource being applied. From 0306fb7faf396e5360efdcbcf383bd535d6d739d Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Sat, 16 Nov 2024 13:28:49 -0800 Subject: [PATCH 3/5] applied more suggestions from review --- aep/general/0137/aep.md.j2 | 61 ++++++++++++++------------------------ 1 file changed, 22 insertions(+), 39 deletions(-) diff --git a/aep/general/0137/aep.md.j2 b/aep/general/0137/aep.md.j2 index d5de478b..6882b89e 100644 --- a/aep/general/0137/aep.md.j2 +++ b/aep/general/0137/aep.md.j2 @@ -1,16 +1,16 @@ # Apply -In REST APIs, it is customary to make a `PUT` request to a collection's URI (for -example, `/v1/publishers/{publisher}/books`) in order to create or replace a new -resource within that collection. +In REST APIs, it is customary to make a `PUT` request to a resource's URI (for +example, `/v1/publishers/{publisher}/books/{book}`) in order to create or +replace a resource. -Resource-oriented design (AEP-121) honors this pattern through the `Apply` -method. These RPCs accept the parent collection and the resource to create or -replace, and then return, the final resource. +[Resource-oriented design](/resources) honors this pattern through the `Apply` +method. These operations accept the resource and it's path, which it uses to +create or replace the resource. The operation returns the final resource. ## Guidance -APIs **should** provide an apply method for resources unless it is not valuable +APIs **should** provide an apply method for a resource unless it is not valuable for users to do so. ### Operation @@ -21,14 +21,8 @@ Apply methods are specified using the following pattern: - Some resources take longer to be applied than is reasonable for a regular API request. In this situation, the API **should** use a [long-running operation](/long-running-operations). - -- The method's name **must** begin with the word `Apply`. The remainder of the - method name **should** be the singular form of the resource being applied. -- The collection's parent resource **must** be called `parent`, and - **should** be the only variable in the URI path. - - The collection identifier (`books` in the above example) **must** be a - literal string. -- The operation **must** have [strong consistency][]. +- The operation **must** have [strong consistency](/resources#strong-consistency). +- The HTTP URI path of the `PUT` method **must** be the resource path. {% tab proto %} @@ -41,6 +35,10 @@ Apply methods are specified using the following pattern: - There **should** be exactly one `google.api.method_signature` annotation, with a value of `"parent,{resource},id"`, or "`"parent,{resource}"` if the resource ID is not required. +- The method's name **must** begin with the word `Apply`. The remainder of the + method name **should** be the singular form of the resource being applied. +- The request's `path` field **must** map to the URI path. + - The `path` field **must** be the only variable in the URI path. {% tab oas %} @@ -60,8 +58,7 @@ Accept: application/json Apply methods implement a common request message pattern: -- A `path` field **must** be supported, and map to the URI path. -- The resource **must** be included and **must** map to the POST body. +- The resource **must** be included and **must** map to the HTTP request body. - The request schema **must not** contain any other required fields and **should not** contain other optional fields except those described in this or another AEP. @@ -71,12 +68,9 @@ Apply methods implement a common request message pattern: {% sample '../example.proto', 'message ApplyBookRequest' %} - A `path` field specifying the path of the resource **must** be included. - - The field **must** be [annotated as `REQUIRED`][aep-203]. - - The field **must** identify the [resource type][aep-4] of the resource + - The field **must** be [annotated as `REQUIRED`](/field-behavior-documentation). + - The field **must** identify the [resource type](/resource-types) of the resource being applied. -- The request message **must not** contain any other required fields and - **should not** contain other optional fields except those described in this - or another AEP. {% tab oas %} @@ -92,7 +86,7 @@ Apply methods implement a common request message pattern: schema. - The response **should** include the fully-populated resource, and **must** include any fields that were provided unless they are input only (see - AEP-203). + [field behavior](/field-behavior-documentation)). {% tab proto %} @@ -106,8 +100,8 @@ Apply methods implement a common request message pattern: ### Errors -See [errors][], in particular [when to use PERMISSION_DENIED and NOT_FOUND -errors][permission-denied]. +See [errors](/errors), in particular [when to use PERMISSION_DENIED and NOT_FOUND +errors](/errors#permission-denied). ## Interface Definitions @@ -127,18 +121,7 @@ errors][permission-denied]. ## Further reading -- For ensuring idempotency in `Apply` methods, see AEP-155. -- For naming resources involving Unicode, see AEP-210. - -[aep-121]: ./0121.md -[aep-122]: ./0122.md -[aep-4]: ./0004.md -[aep-155]: ./0155.md -[aep-203]: ./0203.md -[aep-210]: ./0210.md -[data plane]: ./0111.md#data-plane -[errors]: ./0193.md -[field_behavior]: ./203.md -[Declarative clients]: ./0003.md#declarative-clients -[permission-denied]: ./0193.md#permission-denied +- For ensuring idempotency in `Apply` methods, see [idempotency](/idempotency). +- For naming resources involving Unicode, see [unicode](/unicode). + [strong consistency]: ./0121.md#strong-consistency From f077feb443bddf070e68ff7a8c2b58061a83a46c Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Thu, 21 Nov 2024 21:36:18 -0800 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Richard Gibson --- aep/general/0137/aep.md.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aep/general/0137/aep.md.j2 b/aep/general/0137/aep.md.j2 index 6882b89e..437baa7a 100644 --- a/aep/general/0137/aep.md.j2 +++ b/aep/general/0137/aep.md.j2 @@ -5,7 +5,7 @@ example, `/v1/publishers/{publisher}/books/{book}`) in order to create or replace a resource. [Resource-oriented design](/resources) honors this pattern through the `Apply` -method. These operations accept the resource and it's path, which it uses to +method. These operations accept the resource and its path, which it uses to create or replace the resource. The operation returns the final resource. ## Guidance From bebd872786ebab0f7f26ad77fbc962ca860519a7 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Thu, 21 Nov 2024 21:48:39 -0800 Subject: [PATCH 5/5] mention patch and put patterns in each AEP. --- aep/general/0134/aep.md.j2 | 3 +++ aep/general/0137/aep.md.j2 | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/aep/general/0134/aep.md.j2 b/aep/general/0134/aep.md.j2 index d2c75d46..7fb5b101 100644 --- a/aep/general/0134/aep.md.j2 +++ b/aep/general/0134/aep.md.j2 @@ -8,6 +8,9 @@ Resource-oriented design (AEP-121) honors this pattern through the `Update` method (which mirrors the REST `PATCH` behavior). These methods accept the URI representing that resource and return the resource. +Also see the [apply](/apply) method, with guidance on how to implement `PUT` +requests. + ## Guidance APIs **should** provide an update method for resources unless it is not diff --git a/aep/general/0137/aep.md.j2 b/aep/general/0137/aep.md.j2 index 437baa7a..6093093c 100644 --- a/aep/general/0137/aep.md.j2 +++ b/aep/general/0137/aep.md.j2 @@ -8,10 +8,15 @@ replace a resource. method. These operations accept the resource and its path, which it uses to create or replace the resource. The operation returns the final resource. +Also see the [update](/update) method, with guidance on how to implement `PATCH` +requests. + ## Guidance APIs **should** provide an apply method for a resource unless it is not valuable -for users to do so. +for users to do so. Clients should also consider using [update](/update) methods +instead to ensure forwards compatible requests (see [PATCH and +PUT](#patch-and-put)). ### Operation @@ -103,6 +108,14 @@ Apply methods implement a common request message pattern: See [errors](/errors), in particular [when to use PERMISSION_DENIED and NOT_FOUND errors](/errors#permission-denied). +### PATCH and PUT + +Note that `PUT` requests with fields missing in the resource may result in +overwriting values in the resource with existing values. For that reason, +AEP-compliant APIs generally use the `PATCH` HTTP verb. + +See [AEP-134's patch and put](/134#patch-and-put) for more information. + ## Interface Definitions {% tab proto %}