Skip to content

Commit

Permalink
Adopt AEP-122: Resource paths (#63)
Browse files Browse the repository at this point in the history
* Adopt AEP-122: Resouce paths

This is an adaptation of Roblox's AIP-122, which is an adaptation of Google's AIP-122.

This primary difference from Google's AIP-122 is the change from "name" to "path" for the key concept defined by the AEP.

Some notes on this PR:

* This PR currently has protobuf-only guidance.  If the content is generally acceptable to the working group, I'm happy to add the relevant OAS tabs.
* The proposed new numbering scheme is ignored, because there are existing AEPs that don't use it.  If and when we change the numbering scheme, a separate PR will be required anyway.
* The PR uses apis.example.com/<api> (Roblox pattern) rather than "<api>.exampleapis.com" (Google pattern).  No strong feeling about this either way.

* Update aep/general/0122/aep.md

Co-authored-by: Yusuke Tsutsumi <[email protected]>

* Update aep/general/0122/aep.md

Co-authored-by: Yusuke Tsutsumi <[email protected]>

* Address PR feedback from @toumorokoshi.

* Clarify wording around resource path uniqueness.

* Add information about how to annotate valid ID formats.

* Linkify `pattern`.

* Update aep/general/0122/aep.md

Co-authored-by: Yusuke Tsutsumi <[email protected]>

* Address comments from @toumorokoshi.

* Fix reference to JSON Schema keywords.

* Add a slug to AIP-122.

* Update aep/general/0122/aep.yaml

Co-authored-by: Yusuke Tsutsumi <[email protected]>

---------

Co-authored-by: Yusuke Tsutsumi <[email protected]>
Co-authored-by: Yusuke Tsutsumi <[email protected]>
  • Loading branch information
3 people authored Nov 17, 2023
1 parent 378bf24 commit 7fefa19
Show file tree
Hide file tree
Showing 2 changed files with 333 additions and 0 deletions.
324 changes: 324 additions & 0 deletions aep/general/0122/aep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
# Resource paths

Most APIs expose _resources_ (their primary nouns) which users are able to
create, retrieve, and manipulate. Additionally, resources have _paths_: each
resource has a unique (within the API service) identifying path that users use
to reference that resource, and these paths are what users should _store_ as
the canonical identifier for the resources.

## Guidance

All resource paths defined by an API **must** be unique within that API. (See
the section on [full resource paths](#full-resource-paths) below for more
information on referring to resources across APIs.)

Resource paths are formatted according to the [URI path schema][], but without
the leading slash:

publishers/123/books/les-miserables users/vhugo1802

- Resource path components **must** alternate between collection identifiers
(example: `publishers`, `books`, `users`) and resource IDs (example: `123`,
`les-miserables`, `vhugo1802`), _except_ when [singleton resources][] are
present.
- Resource paths **must** use the `/` character to separate individual segments
of the resource path.
- Each segment of a resource path **must not** contain a `/` character.
- Resource paths **should** only use characters available in DNS names, as
defined by [RFC-1123](https://tools.ietf.org/html/rfc1123).
- Additionally, resource IDs **should not** use upper-case letters.
- If additional characters are necessary, resource paths **should not** use
characters that require URL-escaping, or characters outside of ASCII.
- If Unicode characters can not be avoided, resource paths **must** be stored
in Normalization Form C (see [AEP-210][]).
- Each resource **must** expose a `path` field that contains its resource path.
- Resources **may** provide the resource ID, i.e. the last segment of the
path, as a separate field named `id`.
- Resources **must not** expose tuples, self-links, or other forms of
resource identification.
- All ID fields **must** be strings.

**Note:** Resource paths as described here are used within the scope of a
single API (or else in situations where the owning API is clear from the
context), and are only required to be unique within that scope. For this
reason, they are sometimes called _relative resource paths_ to distinguish them
from _full resource paths_ (discussed below).

[aep-210]: ./0210.md
[uri path schema]: https://datatracker.ietf.org/doc/html/rfc3986#appendix-A
[singleton resources]: https://aep.dev/singletons

### Collection identifiers

The collection identifier segments in a resource path **must** be the plural
form of the noun used for the resource. (For example, a collection of
`Publisher` resources is called `publishers` in the resource path.)

- Collection identifiers **must** be concise American English terms.
- Collection identifiers **must** be in `kebab-case`.
- Collection identifiers **must** begin with a lower-cased letter and contain
only lower-case ASCII letters, numbers. and hyphens (`/[a-z][a-z0-9-]*/`).
- Collection identifiers **must** be plural.
- In situations where there is no plural word ("info"), or where the singular
and plural terms are the same ("moose"), the non-pluralized (singular) form
is correct. Collection segments **must not** "coin" words by adding "s" in
such cases (e.g. avoid "infos").

#### Nested collections

If a resource path contains multiple levels of a hierarchy, and a parent
collection's path is used as a prefix for the child resource's path, the child
collection's path **may** omit the prefix. For example, given a collection of
`UserEvent` resources that would normally be nested underneath `users`:

```
users/vhugo1802/user-events/birthday-dinner-226/user-event-guests/123
```

An API **may** use the less-redundant form:

```
users/vhugo1802/events/birthday-dinner-226/guests/123
```

In this situation, the _message_ is still called `UserEvent` or
`UserEventGuest`; only the collection name is shortened.

**Note:** APIs wishing to do this **must** follow this format consistently
throughout the API, or else not at all.

### Resource ID segments

A resource ID segment identifies the resource within its parent collection. In
the resource path `publishers/123/books/les-miserables`, `123` is the resource
ID for the publisher, and `les-miserables` is the resource ID for the book.

- Resource IDs **may** be either always set by users (required on resource
creation), optionally set by users (optional on resource creation,
server-generated if unset), or never set by users (not accepted at resource
creation). They **must** be immutable once created.
- If resource IDs are user-settable, the API **must** document and/or annotate
the field with the allowed formats. User-settable resource IDs **should**
conform to [RFC-1034][]; which restricts to letters, numbers, and hyphen,
with the first character a letter, the last a letter or a number, and a 63
character maximum.
- Additionally, user-settable resource IDs **should** restrict letters to
lower-case (`^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$`).
- Characters outside of ASCII **should not** be permitted; however, if
Unicode characters are necessary, APIs **must** follow guidance in
[AEP-210][].
- User-settable IDs **should not** be permitted to be a UUID (or any value
that syntactically appears to be a UUID).
- Field annotations **should** use [protovalidate][] in protobuf and [JSON
Schema keywords][] like [`pattern`][] with OAS/JSON Schema.
- If resource IDs are not user-settable, the API **should** document the basic
format, and any upper boundaries (for example, "at most 63 characters").
- For more information, see the [create][] standard method.

<!-- prettier-ignore-start -->
[aep-128]: ./0128.md
[create]: ./0133.md#user-specified-ids
[rfc-1034]: https://tools.ietf.org/html/rfc1034
[JSON Schema keywords]: https://swagger.io/docs/specification/data-models/keywords/
<!-- prettier-ignore-end -->

### Resource ID aliases

It is sometimes valuable to provide an alias for common lookup patterns for
resource IDs. For example, an API with `users` at the top of its resource
hierarchy may wish to provide `users/me` as a shortcut for retrieving
information for the authenticated user.

APIs **may** provide programmatic aliases for common lookup patterns. However,
all data returned from the API **must** use the canonical resource path.

### Full resource paths

In most cases, resource paths are used within a single API only, or else they
are used in contexts where the owning API is clear (for example,
`string pubsub_topic`).

However, sometimes it is necessary for services to refer to resources in an
arbitrary API. In this situation, the service **should** use the _full resource
path_, a schemeless URI with the owning API's service endpoint, followed by the
relative resource path:

```
//apis.example.com/library/publishers/123/books/les-miserables
//apis.example.com/calendar/users/vhugo1802
```

**Note:** The full resource path **should not** be used for cross-API
references where the owning API is clear; it is only used if a field refers to
resources in multiple APIs where ambiguity is possible.

### Resource URIs

The full resource path is a schemeless URI, but slightly distinct from the full
URIs we use to access a resource. The latter adds two components: the protocol
(HTTPS) and the API version:

```
https://apis.example.com/library/v1/publishers/123/books/les-miserables
https://apis.example.com/calendar/v3/users/vhugo1802
```

The version is not included in the full resource path because the full resource
path is expected to persist from version to version. Even though the API
surface may change between major versions, multiple major versions of the same
API are expected to use the same underlying data.

**Note:** The correlation between the full resource path and the service's
hostname is by convention. In particular, one service is able to have multiple
hostnames (example use cases include regionalization or staging environments),
and the full resource path does not change between these.

### Fields representing resource paths

When defining a resource, the first field **should** be the resource path,
which **must** be of type `string` and **must** be called `path` for the
resource path. The message **should** include a `google.api.resource`
annotation declaring the type (see [AEP-123][] for more on this).

```proto
// A representation of a book in the library.
message Book {
option (google.api.resource) = {
type: "apis.example.com/library/Book"
pattern: "publishers/{publisher}/books/{book}"
};
// The resource path of the book.
// Format: publishers/{publisher}/books/{book}
string path = 1;
// Other fields...
}
```

When defining a method that retrieves or acts on an already-existing resource
(such as `GetBook` or `ArchiveBook`), the first field of the request message
**should** be the resource path, which **must** be of type `string` and
**must** be called `path` for the resource path. The field **should** also be
annotated with the `google.api.resource_reference` annotation, referencing the
resource type ([AEP-123][]).

```proto
// Request message for ArchiveBook
message ArchiveBookRequest {
// The book to archive.
// Format: publishers/{publisher}/books/{book}
string path = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {
type: "apis.example.com/library/Book"
}];
// Other fields...
}
```

**Note:** Fields **must not** be called `path` except for this purpose. For
other use cases, either use a different term or prepend an adjective (for
example: `file_path`).

[aep-123]: ./0123.md

### Fields representing a resource's parent

When defining a method that retrieves resources from a collection or adds a new
resource to a collection (such as `ListBooks` or `CreateBook`), the first field
of the request message **should** be of type `string` and **should** be called
`parent` for the resource path of the collection. The `parent` field **should**
also be annotated with the `google.api.resource_reference` annotation,
referencing the parent's resource type ([AEP-123][]).

```proto
// Request message for ListBooks.
message ListBooksRequest {
// The publisher to list books from.
// Format: publishers/{publisher_id}
string parent = 1 [(google.api.resource_reference) = {
type: "apis.example.com/library/Publisher"
}];
// Other fields (e.g. max_page_size, page_token, filter, etc.)...
}
```

If there is more than one possible parent type, the `parent` field **should**
be annotated with the `child_type` key on `google.api.resource_reference`
instead:

```proto
// Request message for ListBooks.
message ListBooksRequest {
// The parent to list books from.
// Format:
// - publishers/{publisher_id}
// - authors/{author_id}
string parent = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {
child_type: "apis.example.com/library/Book"
}];
// Other fields (e.g. max_page_size, page_token, filter, etc.)...
}
```

**Note:** Fields **should not** be called `parent` except for this purpose. For
other use cases, use a synonymous term if possible.

### Fields representing another resource

When referencing a resource path for a different resource, the field **should**
be of type `string` for the resource path, and the field name **should** be
equivalent to the corresponding message's name in snake case.

- Field names **may** include a leading adjective if appropriate (such as
`string dusty_book`).
- Field names **should not** use the `_path` suffix unless the field would be
ambiguous without it (e.g., `crypto_key_path`)
- Fields representing another resource **should** provide the
`google.api.resource_reference` annotation with the resource type being
referenced.

```proto
// A representation of a book in a library.
message Book {
option (google.api.resource) = {
type: "apis.example.com/library/Book"
pattern: "publishers/{publisher}/books/{book}"
};
// Path of the book.
// Format is `publishers/{publisher}/books/{book}`
string path = 1;
// The shelf where the book currently sits.
// Format is `shelves/{shelf}`.
string shelf = 2 [(google.api.resource_reference) = {
type: "apis.example.com/library/Shelf"
}];
// Other fields...
}
```

**Note:** When referring to other resources in this way, we use the resource
path as the value, not just the ID component. Services **should** use the
resource path to reference resources when possible. If using the ID component
alone is strictly necessary, the field **should** use an `_id` suffix (e.g.
`shelf_id`).

## Further reading

- For evolving resource paths over time, see
[AEP-180](./0180.md#changing-resource-paths).
- For resource types, see [AEP-123][].

<!-- prettier-ignore-start -->
[protovalidate]: https://github.com/bufbuild/protovalidate
[`pattern`]: https://json-schema.org/understanding-json-schema/reference/string#regexp
<!-- prettier-ignore-end -->
9 changes: 9 additions & 0 deletions aep/general/0122/aep.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
id: 122
slug: resource-paths
state: approved
created: 2023-10-11
placement:
category: resources
order: 20
redirect_from: /resource-paths

0 comments on commit 7fefa19

Please sign in to comment.