diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..c3e25bf1 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,152 @@ +# AGENTS.md + +This repository contains the **API Enhancement Proposals (AEPs)** — the design +specification documents for the [AEP project](https://aep.dev). It is a +content-driven repository: the primary artifacts are Markdown/Jinja2 documents +and YAML metadata, not application code. + +## Repository layout + +``` +aep/general// # One directory per AEP (zero-padded, 4-digit number) + aep.yaml # AEP metadata (id, state, slug, category, etc.) + aep.md.j2 # AEP content (Markdown + Jinja2 template syntax) +config/ # Site configuration (hero, header, urls, site) +scripts/ # Tooling: fix.py, validate_links.py, build.sh, serve.sh +pages/general/ # Static site pages (adopting, faq, licensing, etc.) +blog/ # Blog posts +.github/workflows/ # CI: lint.yaml, test.yaml, publish_site.yaml +``` + +## AEP file structure + +Every AEP lives in `aep/general//` and contains exactly two files: + +### `aep.yaml` — metadata + +Required fields: + +- `id`: integer AEP number (no zero-padding) +- `state`: one of `draft`, `reviewing`, `approved`, `final`, `replaced`, + `withdrawn` +- `slug`: URL-friendly short name +- `created`: ISO-8601 date (`yyyy-mm-dd`) +- `placement.category`: one of `meta`, `general`, `resources`, + `standard-methods`, `fields`, `types`, `design-patterns`, `batch-methods`, + `best-practices`, `protobuf` + +Optional fields: `updated`, `placement.order`, `redirect_from`, `js_scripts`. + +### `aep.md.j2` — content + +Markdown with Jinja2 template directives. Key conventions: + +- Starts with a single `#` title (a noun, not an imperative). +- Introduction paragraph (no heading) → `## Guidance` section. +- Optional trailing sections in order: `## Rationale`, `## History`, + `## Further reading`, `## Changelog`. +- Only use heading levels `##` and `###` (never `#` beyond the title). +- Use RFC-2119 keywords (`**must**`, `**should**`, `**may**`) in lower-case + bold. +- Use `{% tab proto %}` / `{% tab oas %}` / `{% endtabs %}` for + protocol-specific examples. +- Use `{% sample '', '' %}` to include code from example files. +- Cross-reference AEPs as `AEP-N` (no zero-padding) in prose, with links using + relative paths like `./0008.md` or `../0008.md`. +- Links must NOT end in `.md` (the linter strips `.md` suffixes). Exception: + external GitHub links. +- Do not use self-reference links like `[aep-123][aep-123]` — use plain text. +- Do not use reference-style links with AEP identifiers like + `[aep-123]: ./0123`. +- Wrap reference-style link definitions that would break prettier in + `` / `` blocks. + +## Formatting and linting + +Formatting is enforced by CI. Always run before committing: + +```bash +make lint +``` + +This runs three checks: + +1. **Prettier** (`npm run check`): Markdown/YAML/JSON formatting. + - `printWidth: 79`, `proseWrap: always`, `singleQuote: true`, + `trailingComma: es5`. + - `.md.j2` files are parsed as Markdown. +2. **`scripts/fix.py`**: Validates and fixes AEP-specific link rules. + - No `.md` suffixes in internal links. + - All AEP cross-references point to existing AEPs. + - No self-reference links, no AEP-identifier reference-style links. + - HTTP URLs are well-formed. +3. **`scripts/validate_links.py`**: Read-only link validation (same rules as + fix.py but never modifies files). + +Use `make check` for a read-only lint pass (CI uses this). + +## Development + +### Prerequisites + +- Node.js + npm +- Python 3 + +### Install dependencies + +```bash +make install # runs npm install +``` + +### Local preview + +```bash +./scripts/serve.sh +``` + +This clones the [site-generator](https://github.com/aep-dev/site-generator) (if +not already present as a sibling directory), builds the site, and starts a dev +server on port 4321. + +### Build + +```bash +./scripts/build.sh +``` + +Builds the full site including the site-generator, api-linter, and +aep-openapi-linter (uses sibling directories if present, otherwise clones to +`/tmp`). + +## CI workflows + +| Workflow | Trigger | What it does | +| ------------------- | ----------- | ------------------------------------------------- | +| `lint.yaml` | PR → main | `make check` (prettier + fix.py + validate_links) | +| `test.yaml` | PR → main | `./scripts/build.sh` (full site build) | +| `publish_site.yaml` | push → main | Triggers site-generator repository dispatch | + +## Content conventions + +- AEPs should be concise — roughly two printed pages. +- A single AEP covers a single topic. +- API design examples should be presented in both OpenAPI (OAS 3.1) and + protocol buffers. +- Use snake_case for parameter and property names in examples. +- Error codes in prose use the format `{error_code} / {http_status_code}` (e.g. + `OK / 200`). +- Example files (`example.oas.yaml`, `example.proto`) in `aep/general/` are + code-generated by [aepc](https://github.com/aep-dev/aepc) — do not edit them + by hand. + +## Common pitfalls + +- **Don't link to `.md` files** in AEP content (except external GitHub links). + Links like `./0131.md` will be auto-fixed to `./0131` by `fix.py`, but will + fail `make check`. +- **Don't forget `make lint`** before committing. Prettier reformats prose + wrapping at 79 columns, and fix.py catches link issues. +- **Don't edit `example.oas.yaml` or `example.proto`** — these are generated + from aepc. +- **Zero-pad directory names** but not prose references. Directory: `0008/`. + Prose: `AEP-8`. diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 9ef24a58..eea486db 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -9,7 +9,7 @@ approval privileges to the aep-dev organization. | --------------- | ------------------------------------------------ | -------------- | | Alfred Fuller | [@alfus](https://github.com/alfus) | Buf | | Alex Stephen | [@rambleraptor](https://github.com/rambleraptor) | Google | -| Dan Hudlow | [@hudlow](https://github.com/hudlow) | IBM | +| Dan Hudlow | [@hudlow](https://github.com/hudlow) | | | Mike Kistler | [@mkistler](https://github.com/mkistler) | Microsoft | | Mak Ahmad | [@makahmad](https://github.com/makahmad) | | | Marsh Gardiner | [@earth2marsh](https://github.com/earth2marsh) | Rubrik | diff --git a/aep/general/0004/aep.md.j2 b/aep/general/0004/aep.md.j2 index 87c9ef39..06af1a4f 100644 --- a/aep/general/0004/aep.md.j2 +++ b/aep/general/0004/aep.md.j2 @@ -58,7 +58,7 @@ message UserEvent { plural: "user-events" // define one or more patterns, e.g. if a resource has more than one parent. pattern: "projects/{project_id}/user-events/{user_event_id}" - pattern: "folder/{folder_id}/user-events/{user_event_d}" + pattern: "folder/{folder_id}/user-events/{user_event_id}" pattern: "users/{user_id}/events/{user_event_id}" }; @@ -91,11 +91,12 @@ element = variable | literal; variable = "{", literal, "}"; ``` -Where `literal` matches the regex `[a-z][a-z0-9\-_]*[a-z0-9]`. +Where `literal` matches the regex `[a-z](?:[a-z0-9\-_]*[a-z0-9])?`. - Patterns **must** match the possible [paths](/paths) of the resource. - Pattern variables (the segments within braces) **must** match the singular of the resource whose id is being matched by that value, suffixed with `_id`. +- Pattern variables (the segments within braces) **must** use snake case. #### Pattern uniqueness diff --git a/aep/general/0133/aep.md.j2 b/aep/general/0133/aep.md.j2 index ce0714b6..126a1820 100644 --- a/aep/general/0133/aep.md.j2 +++ b/aep/general/0133/aep.md.j2 @@ -90,8 +90,9 @@ Create methods implement a common request message pattern: {% tab oas %} -{% sample '../example.oas.yaml', '$.paths./publishers/{publisher_id}/books.post.responses.200' %} +{% sample '../example.oas.yaml', '$.paths./publishers/{publisher_id}/books.post.responses.201' %} +- The response **must** return a `201 Created` HTTP status code. - The response **must** be the resource itself. There is no separate response schema. - The response **should** include the fully-populated resource, and **must** diff --git a/aep/general/0134/aep.md.j2 b/aep/general/0134/aep.md.j2 index f1d6ce32..b0535b8e 100644 --- a/aep/general/0134/aep.md.j2 +++ b/aep/general/0134/aep.md.j2 @@ -117,7 +117,7 @@ Update methods implement a common request pattern: {% tab oas %} -{% sample '../example.oas.yaml', '$.paths./publishers.post.responses.200' %} +{% sample '../example.oas.yaml', '$.paths./publishers.post.responses.201' %} {% endtabs %} diff --git a/aep/general/0136/aep.md.j2 b/aep/general/0136/aep.md.j2 index 5255d692..01228d52 100644 --- a/aep/general/0136/aep.md.j2 +++ b/aep/general/0136/aep.md.j2 @@ -25,7 +25,8 @@ apply consistently: - The HTTP URI **must** use a `:` character followed by the custom verb (`:archive` in the above example), and the verb in the URI **must** match the verb in the name of the RPC. - - If word separation is required, `kebab-case` **must** be used. + - If word separation is required, `kebab-case` **must** be used (e.g. + `:translate-text`, not `:translateText`). - The name of the RPC **should** be a verb followed by a noun. - The name of the RPC **must not** contain prepositions ("for", "with", etc.). @@ -106,7 +107,7 @@ permanent effect on data within the API. {% tab oas %} -{% sample 'translate.oas.yaml', '$.paths./projects/{projectId}:translateText' %} +{% sample 'translate.oas.yaml', '$.paths./projects/{project_id}:translate-text' %} {% endtabs %} @@ -116,7 +117,8 @@ permanent effect on data within the API. used. - The URI **should** place both the verb and noun after the `:` separator (avoid a "faux collection key" in the URI in this case, as there is no - collection). For example, `:translateText` is preferable to `text:translate`. + collection). For example, `:translate-text` is preferable to + `text:translate`. - Stateless methods **must** use `POST` if they involve billing. ### Usage in declarative clients diff --git a/aep/general/0136/translate.oas.yaml b/aep/general/0136/translate.oas.yaml index 2974bc7a..dfe741fb 100644 --- a/aep/general/0136/translate.oas.yaml +++ b/aep/general/0136/translate.oas.yaml @@ -4,7 +4,7 @@ info: title: Library version: 1.0.0 paths: - /projects/{projectId}:translateText: + /projects/{project_id}:translate-text: post: operationId: translateText description: Translates the provided text from one language to another. @@ -37,7 +37,7 @@ paths: application/json: schema: description: | - Response structure for the translateText operation. + Response structure for the translate-text operation. properties: translated_text: type: string diff --git a/aep/general/0136/translate.proto b/aep/general/0136/translate.proto index 37f6e26d..2f7362e2 100644 --- a/aep/general/0136/translate.proto +++ b/aep/general/0136/translate.proto @@ -22,7 +22,7 @@ service Translate { // Translates the provided text from one language to another. rpc TranslateText(TranslateTextRequest) returns (TranslateTextResponse) { option (google.api.http) = { - post: "/v1/{project=projects/*}:translateText" + post: "/v1/{project=projects/*}:translate-text" body: "*" }; } diff --git a/aep/general/0137/aep.md.j2 b/aep/general/0137/aep.md.j2 index 684146d4..f0fae325 100644 --- a/aep/general/0137/aep.md.j2 +++ b/aep/general/0137/aep.md.j2 @@ -108,7 +108,7 @@ Apply methods implement a common request message pattern: {% tab oas %} -{% sample '../example.oas.yaml', '$.paths./publishers/{publisher_id}/books/{book_id}.put.responses.200' %} +{% sample '../example.oas.yaml', '$.paths./publishers/{publisher_id}/books/{book_id}.put.responses' %} - If the resource is created, the response **must** return a `201` status code. - If the resource is updated, the response **must** return a `200` status code. diff --git a/aep/general/0231/aep.md.j2 b/aep/general/0231/aep.md.j2 index 75f6771f..d5c7b1ad 100644 --- a/aep/general/0231/aep.md.j2 +++ b/aep/general/0231/aep.md.j2 @@ -11,7 +11,7 @@ APIs **may** support batch get to retrieve a consistent set of resources. - The method's name **must** begin with `BatchGet`. The remainder of the method name **must** be the plural form of the resource being retrieved. - The HTTP verb **must** be `GET`. -- The HTTP URI **must** end with `:batchGet`. +- The HTTP URI **must** end with `:batch-get`. - The URI path **must** represent the collection for the resource, matching the collection used for simple CRUD operations. If the operation spans parents, a [wilcard](./reading-across-collections) **may** be accepted. @@ -45,7 +45,7 @@ pattern: ```proto rpc BatchGetBooks(BatchGetBooksRequest) returns (BatchGetBooksResponse) { option (google.api.http) = { - get: "/v1/{parent=publishers/*}/books:batchGet" + get: "/v1/{parent=publishers/*}/books:batch-get" }; } @@ -121,7 +121,7 @@ message BatchGetBooksResponse { {% tab oas %} -{% sample 'batchget.oas.yaml', '$.paths./publishers/{publisherId}/books:BatchGet' %} +{% sample 'batchget.oas.yaml', '$.paths./publishers/{publisherId}/books:batch-get' %} Example response body: diff --git a/aep/general/0231/batchget.oas.yaml b/aep/general/0231/batchget.oas.yaml index 2c0577a7..f9f937ee 100644 --- a/aep/general/0231/batchget.oas.yaml +++ b/aep/general/0231/batchget.oas.yaml @@ -3,7 +3,7 @@ info: title: Library version: 1.0.0 paths: - /publishers/{publisherId}/books:BatchGet: + /publishers/{publisherId}/books:batch-get: parameters: - name: publisherId in: path diff --git a/aep/general/example.oas.yaml b/aep/general/example.oas.yaml index e519e320..fb6a23ff 100644 --- a/aep/general/example.oas.yaml +++ b/aep/general/example.oas.yaml @@ -196,7 +196,7 @@ paths: $ref: '#/components/schemas/isbn' required: true responses: - '200': + '201': content: application/json: schema: @@ -269,7 +269,7 @@ paths: $ref: '#/components/schemas/publisher' required: true responses: - '200': + '201': content: application/json: schema: @@ -354,7 +354,13 @@ paths: application/json: schema: $ref: '#/components/schemas/publisher' - description: Successful response + description: Successful response (updated) + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/publisher' + description: Successful response (created) /publishers/{publisher_id}/books: get: description: List method for book @@ -411,7 +417,7 @@ paths: $ref: '#/components/schemas/book' required: true responses: - '200': + '201': content: application/json: schema: @@ -516,7 +522,13 @@ paths: application/json: schema: $ref: '#/components/schemas/book' - description: Successful response + description: Successful response (updated) + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/book' + description: Successful response (created) /publishers/{publisher_id}/books/{book_id}/editions: get: description: List method for book-edition @@ -579,7 +591,7 @@ paths: $ref: '#/components/schemas/book-edition' required: true responses: - '200': + '201': content: application/json: schema: diff --git a/scripts/build.sh b/scripts/build.sh index 84393ccf..dd639d20 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -7,22 +7,21 @@ set -x export AEP_LOCATION="${PWD}" -declare -A repos - repos=( - [site_generator]="site-generator" - [api_linter]="api-linter" - [aep_openapi_linter]="aep-openapi-linter" + "site_generator:site-generator" + "api_linter:api-linter" + "aep_openapi_linter:aep-openapi-linter" ) -for varName in "${!repos[@]}"; do - repoName="${repos[$varName]}" - if [ -d ${AEP_LOCATION}/../${repoName} ]; then - eval "$varName=${AEP_LOCATION}/../${repoName}" +for entry in "${repos[@]}"; do + varName="${entry%%:*}" + repoName="${entry#*:}" + if [ -d "${AEP_LOCATION}/../${repoName}" ]; then + printf -v "$varName" '%s' "${AEP_LOCATION}/../${repoName}" else - eval "$varName=/tmp/${repoName}" - if [ ! -d "${varName}" ]; then - git clone https://github.com/aep-dev/$repoName.git "${!varName}" + printf -v "$varName" '%s' "/tmp/${repoName}" + if [ ! -d "${!varName}" ]; then + git clone "https://github.com/aep-dev/${repoName}.git" "${!varName}" fi fi done