Skip to content

Commit 3d1661e

Browse files
author
Luke Sneeringer
authored
feat: AIP-144 – Repeated fields (#31)
1 parent cec061e commit 3d1661e

4 files changed

Lines changed: 309 additions & 0 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
openapi: 3.0.3
3+
info:
4+
title: Library
5+
version: 1.0.0
6+
paths:
7+
'/publishers/{publisherId}/books/{bookId}:addAuthor':
8+
post:
9+
operationId: addAuthor
10+
description: Add an author to a book.
11+
requestBody:
12+
content:
13+
application/json:
14+
schema:
15+
properties:
16+
author:
17+
type: string
18+
description: The author to be added.
19+
required: true
20+
required: true
21+
responses:
22+
200:
23+
description: OK
24+
content:
25+
application/json:
26+
schema:
27+
$ref: '#/components/schemas/Book'
28+
'/publishers/{publisherId}/books/{bookId}:removeAuthor':
29+
post:
30+
operationId: removeAuthor
31+
description: Remove an author from a book.
32+
requestBody:
33+
content:
34+
application/json:
35+
schema:
36+
properties:
37+
author:
38+
type: string
39+
description: The author to be removed.
40+
required: true
41+
required: true
42+
responses:
43+
200:
44+
description: OK
45+
content:
46+
application/json:
47+
schema:
48+
$ref: '#/components/schemas/Book'
49+
components:
50+
schema:
51+
Book:
52+
description: A representation of a single book.
53+
properties:
54+
name:
55+
type: string
56+
description: |
57+
The name of the book.
58+
Format: publishers/{publisher}/books/{book}
59+
isbn:
60+
type: string
61+
description: |
62+
The ISBN (International Standard Book Number) for this book.
63+
title:
64+
type: string
65+
description: The title of the book.
66+
authors:
67+
type: array
68+
items:
69+
type: string
70+
description: The author or authors of the book.
71+
rating:
72+
type: float
73+
description: The rating assigned to the book.

aip/general/0144/add_remove.proto

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
syntax = "proto3";
2+
3+
import "google/api/annotations.proto";
4+
import "google/api/field_behavior.proto";
5+
import "google/api/resource.proto";
6+
7+
service Library {
8+
// Add an author to a book.
9+
rpc AddAuthor(AddAuthorRequest) returns (Book) {
10+
option (google.api.http) = {
11+
post: "/v1/{book=publishers/*/books/*}:addAuthor"
12+
body: "*"
13+
};
14+
}
15+
16+
// Remove an author from a book.
17+
rpc RemoveAuthor(RemoveAuthorRequest) returns (Book) {
18+
option (google.api.http) = {
19+
post: "/v1/{book=publishers/*/books/*}:removeAuthor"
20+
body: "*"
21+
};
22+
}
23+
}
24+
25+
// The request structure for the AddAuthor operation.
26+
message AddAuthorRequest {
27+
// The name of the book to add an author to.
28+
string book = 1 [
29+
(google.api.field_behavior) = REQUIRED,
30+
(google.api.resource_reference).type = "library.googleapis.com/Book"
31+
];
32+
33+
// The author to be added.
34+
string author = 2 [(google.api.field_behavior) = REQUIRED];
35+
}
36+
37+
// The request structure for the RemoveAuthor operation.
38+
message RemoveAuthorRequest {
39+
// The name of the book to remove an author from.
40+
string book = 1 [
41+
(google.api.field_behavior) = REQUIRED,
42+
(google.api.resource_reference).type = "library.googleapis.com/Book"
43+
];
44+
45+
// The author to be removed.
46+
string author = 2 [(google.api.field_behavior) = REQUIRED];
47+
}
48+
49+
50+
// A representation of a single book.
51+
message Book {
52+
option (google.api.resource) = {
53+
type: "library.googleapis.com/Book"
54+
pattern: "publishers/{publisher}/books/{book}"
55+
};
56+
57+
// The name of the book.
58+
// Format: publishers/{publisher}/books/{book}
59+
string name = 1;
60+
61+
// The ISBN (International Standard Book Number) for this book.
62+
string isbn = 2;
63+
64+
// The title of the book.
65+
string title = 3;
66+
67+
// The author or authors of the book.
68+
repeated string authors = 4;
69+
70+
// The rating assigned to the book.
71+
float rating = 5;
72+
}

aip/general/0144/aip.md.j2

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# Array fields
2+
3+
Representing lists of data in an API is trickier than it often appears. Users
4+
often need to modify lists in place, and longer data series within a single
5+
resource pose a challenge for pagination.
6+
7+
## Guidance
8+
9+
Resources **may** use array fields where appropriate.
10+
11+
```typescript
12+
interface Book {
13+
// The resource name for the book.
14+
name: string;
15+
16+
// The authors of the book.
17+
authors: string[];
18+
}
19+
```
20+
21+
- Array fields **must** use a plural field name.
22+
- If the English singular and plural words are identical ("moose", "info"),
23+
the dictionary word **must** be used rather than attempting to coin a new
24+
plural form.
25+
- Array fields **should** have an enforced upper bound that will not cause a
26+
single resource payload to become too large. A good rule of thumb is 100
27+
elements.
28+
- If an array data can not be bounded (in other words, if there is a chance
29+
that the array will be too large to be reasonably returned in a single
30+
request), the API **should** use a sub-resource instead.
31+
- Array fields **must not** represent the body of another resource inline.
32+
Instead, the field **should** be a array of strings providing the resource
33+
names of the associated resources.
34+
35+
**Note:** This document uses the term "array" to refer to a field on a resource
36+
that is a list of elements that have the same type. Some languages and IDLs use
37+
other terms, such as "list", "repeated", or "sequence", and these terms are all
38+
synonymous for the purposes of this document. The term "collection" is
39+
distinct, and refers to a group of resources under a single parent rather than
40+
a field on a resource.
41+
42+
### Scalars and structs
43+
44+
Array fields **should** use a scalar type (such as `string`) if they are
45+
certain that additional data will not be needed in the future, as using a
46+
struct type adds significant cognitive overhead and leads to more complicated
47+
code.
48+
49+
However, if additional data is likely to be needed in the future, array fields
50+
**should** use a struct instead of a scalar proactively, to avoid parallel
51+
array fields.
52+
53+
### Update strategies
54+
55+
A resource **may** use one of two strategies to enable updating a array field:
56+
direct update using the [standard `Update`][aip-134] method, or custom `Add`
57+
and `Remove` methods.
58+
59+
A standard `Update` method has one key limitation: the user is only able to
60+
update _the entire_ array. This means that the user is required to read the
61+
resource, make modifications to the array field value as needed, and send it
62+
back. This is fine for many situations, particularly when the array field is
63+
expected to have a small size (fewer than 10 or so) and race conditions are not
64+
an issue, or can be guarded against with [ETags][aip-154].
65+
66+
**Note:** Declarative-friendly resources (AIP-128) **must** use the standard
67+
`Update` method, and not introduce `Add` and `Remove` methods. If declarative
68+
tools need to reason about particular relationships while ignoring others,
69+
consider using a subresource instead.
70+
71+
If atomic modifications are required, and if the array is functionally a set
72+
(meaning that order does not matter, duplicate values are not meaningful, and
73+
non-comparable values such as `null` or `NaN` are not used), the API **should**
74+
define custom methods using the verbs `Add` and `Remove`:
75+
76+
{% tab proto %}
77+
78+
{% sample 'add_remove.proto', 'rpc AddAuthor', 'rpc RemoveAuthor' %}
79+
80+
- The data being added or removed **should** be a primitive (usually a
81+
`string`).
82+
- For more complex data structures with a primary key, the API **should** use
83+
a map with the `Update` method instead.
84+
- The RPC's name **must** begin with the word `Add` or `Remove`. The remainder
85+
of the RPC name **should** be the singular form of the field being added.
86+
- The response **should** be the resource itself, and **should** fully-populate
87+
the resource structure.
88+
- The HTTP method **must** be `POST`, as usual for [custom methods][aip-136].
89+
- The HTTP URI **must** end with `:add*` or `:remove*`, where `*` is the
90+
camel-case singular name of the field being added or removed.
91+
- The request field receiving the resource name **should** map to the URI path.
92+
- The HTTP variable **should** be the name of the resource (such as `book`)
93+
rather than `name` or `parent`.
94+
- That variable **should** be the only variable in the URI path.
95+
- The body clause in the `google.api.http` annotation **should** be `"*"`.
96+
- If the data being added in an `Add` operation is already present, the method
97+
**should** accept the request and make no changes (no-op), but **may** error
98+
with `ALREADY_EXISTS` if appropriate.
99+
- If the data being removed in a `Remove` operation is not present, the method
100+
**should** accept the request and make no changes (no-op), but **may** error
101+
with `NOT_FOUND` if appropriate.
102+
103+
{% tab oas %}
104+
105+
{% sample 'add_remove.oas.yaml', 'paths' %}
106+
107+
- The data being added or removed **should** be a primitive (usually a
108+
`string`).
109+
- For more complex data structures with a primary key, the API **should** use
110+
a map with the `Update` method instead.
111+
- The `operationId` **must** begin with the word `add` or `remove`. The
112+
remainder of the `operationId` **should** be the singular form of the field
113+
being added.
114+
- The response **should** be the resource itself, and **should** fully-populate
115+
the resource structure.
116+
- The HTTP method **must** be `POST`, as usual for [custom methods][aip-136].
117+
- The HTTP URI **must** end with `:add*` or `:remove*`, where `*` is the
118+
camel-case singular name of the field being added or removed.
119+
- If the data being added in an `Add` operation is already present, the method
120+
**should** accept the request and make no changes (no-op), but **may** error
121+
with `409 Conflict` if appropriate.
122+
- If the data being removed in a `Remove` operation is not present, the method
123+
**should** accept the request and make no changes (no-op), but **may** error
124+
with `404 Not Found` if appropriate.
125+
126+
{% endtabs %}
127+
128+
**Note:** If both of these strategies are too restrictive, consider using a
129+
subresource instead.
130+
131+
#### Request Structure
132+
133+
{% tab proto %}
134+
135+
{% sample 'add_remove.proto', 'message AddAuthorRequest', 'message RemoveAuthorRequest' %}
136+
137+
- A resource field **must** be included. It **should** be the name of the
138+
resource (such as `book`) rather than `name` or `parent`.
139+
- The field **should** be [annotated as required][aip-203].
140+
- If the field represents the name of another resource, it **should**
141+
identify the [resource type][aip-123] that it references.
142+
- A field for the value being added or removed **must** be included. It
143+
**should** be the singular name of the field.
144+
- The field **should** be [annotated as required][aip-203].
145+
- The request message **must not** contain any other required fields, and
146+
**should not** contain other optional fields except those described in this
147+
or another AIP.
148+
149+
{% tab oas %}
150+
151+
{% sample 'add_remove.oas.yaml', 'requestBody' %}
152+
153+
- A field for the value being added or removed **must** be included. It
154+
**should** be the singular name of the field.
155+
- The field **should** be designated as required.
156+
157+
{% endtabs %}

aip/general/0144/aip.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
id: 144
3+
state: approved
4+
created: 2020-03-19
5+
placement:
6+
category: fields
7+
order: 50

0 commit comments

Comments
 (0)