diff --git a/ipa/general/0103.mdx b/ipa/general/0103.mdx index 5a37917..5960f7b 100644 --- a/ipa/general/0103.mdx +++ b/ipa/general/0103.mdx @@ -10,30 +10,408 @@ that a service can perform on behalf of the consumer. ## Guidance -- API authors **should** choose from the defined categories in the following - order: - - Standard methods (on collections and resources) - - Custom methods (on collections, resources, or stateless) -- Standard methods **must not** cause side effects - - In such scenarios where a side effect is necessary, a custom method - **should** be used - - A side effect specifically includes mutating - [client-owned fields](0111.mdx#single-owner-fields) of any resource other - than the target of the request. The dependent mutation **must** be performed - through the dependent resource's own endpoint; if the operation is - fundamentally multi-resource by design, it **must** be modeled as a custom - method per the rule above. - - Side effects limited to read-only or - [effective values](0111.mdx#effective-values) of another resource **may** - occur in standard methods. -- Standard methods **must** guarantee - [atomicity](https://en.wikipedia.org/wiki/Atomicity_%28database_systems%29) - - In cases where atomicity cannot be guaranteed, consider in the following - order: - - A new sub-resource with the appropriate methods - - A [singleton-resource](0113.mdx) for the corresponding section - - [Custom methods](0109.mdx), for example, an `Add` or `Remove` operation - for repeated fields + + + + +API authors **should** choose from the defined categories in the following +order: + +- Standard methods (on collections and resources) +- Custom methods (on collections, resources, or stateless) + + + + + +```yaml +paths: + /orders/{orderId}: + delete: + operationId: deleteOrder +``` + + + Cancelling an order is removal of a resource, so the standard `delete` method + covers it. A standard method is the first category that fits, so it is the one + used. + + + + + + +```yaml +paths: + /orders/{orderId}:cancel: + post: + operationId: cancelOrder +``` + + + A custom `:cancel` method is introduced for an operation that a standard + `delete` already expresses. The custom method is chosen ahead of the standard + one, reversing the intended order. + + + + + + + Enumerate every operation under `paths` and pair it with the resource path + it acts on. + + + For each operation, determine the action it performs: create, read, list, + update, delete, or something else. + + + When the action is a plain create/read/list/update/delete on a collection or + resource, confirm it is expressed as the matching standard method rather + than a custom method. + + + Report any custom method whose behavior a standard method on the same + resource would already cover. + + + + + + + + + +Standard methods **must not** cause side effects. + + + + + +```yaml +paths: + /projects/{projectId}: + patch: + operationId: updateProject + responses: + "200": + description: The updated project. +``` + + + The `update` method changes only the project named in the path. No other + resource is mutated as a consequence, so the standard method stays free of + side effects. + + + + + + +```yaml +paths: + /projects/{projectId}: + patch: + operationId: updateProject + description: >- + Updates the project and also deactivates every team that belongs to it. +``` + + + Updating one project mutates a different set of resources, the teams. A + standard method that reaches beyond its target hides the second effect from + anyone reading the contract. + + + + + + + List the standard methods in the spec (create, read, list, update, delete on + collections and resources). + + + For each one, identify the target resource named in the path. + + + Inspect the implementation or the documented behavior to find every resource + the operation mutates, not only the target. + + + Report any standard method that mutates a resource other than its target, + since that is a side effect. + + + + + + + + + +In scenarios where a side effect is necessary, a custom method **should** be +used. + + + + + +```yaml +paths: + /projects/{projectId}:archive: + post: + operationId: archiveProject + description: >- + Archives the project and disables its teams. +``` + + + The operation deliberately touches more than its target, so it is expressed as + a custom method. The custom verb signals up front that the call carries an + effect a plain update would not. + + + + + + +```yaml +paths: + /projects/{projectId}: + patch: + operationId: updateProject + description: >- + Updates the project and disables its teams. +``` + + + The same effect is folded into a standard `update`. The required side effect + is hidden behind a method that callers expect to be effect-free. + + + + + + + Start from the operations flagged as carrying a necessary side effect by + IPA-103-must-not-cause-side-effects. + + + For each, check whether it is modeled as a standard method or a custom + method. + + + Report any that carry a deliberate side effect yet are still expressed as a + standard method. + + + + + + + + + +A side effect specifically includes mutating +[client-owned fields](0111.mdx#single-owner-fields) of any resource other than +the target of the request. The dependent mutation **must** be performed through +the dependent resource's own endpoint. + + + + + +```yaml +paths: + /projects/{projectId}: + patch: + operationId: updateProject + /teams/{teamId}: + patch: + operationId: updateTeam +``` + + + Changing a team's client-owned fields goes through the team's own `update` + endpoint. Each resource owns the writes to its own fields. + + + + + + +```yaml +paths: + /projects/{projectId}: + patch: + operationId: updateProject + description: >- + Also rewrites the name field on each team in the project. +``` + + + The project update writes client-owned fields on teams. Those writes belong to + the team endpoint, so routing them through the project endpoint hides the + mutation from anyone working with the team resource. + + + + + + + For each operation, identify the target resource named in its path. + + + From the implementation or documented behavior, list the client-owned fields + the operation writes on any resource. + + + Flag any operation that writes client-owned fields on a resource other than + its target. + + + Confirm that an endpoint owned by the dependent resource exists and that the + mutation is routed through it instead. + + + + + + + + + +If the operation is fundamentally multi-resource by design, it **must** be +modeled as a custom method. + + + + + +```yaml +paths: + /projects/{projectId}:transfer: + post: + operationId: transferProject + description: >- + Moves the project to another owner and reassigns its teams. +``` + + + The operation touches several resources by design, so it is a custom method. + The custom verb makes the multi-resource scope explicit. + + + + + + +```yaml +paths: + /projects/{projectId}: + patch: + operationId: updateProject + description: >- + Moves the project to another owner and reassigns its teams. +``` + + + A genuinely multi-resource operation is squeezed into a standard `update`. The + standard method advertises a single-resource change while doing much more. + + + + + + + For each operation, determine from the implementation or documented behavior + how many distinct resources it mutates. + + + Identify operations whose purpose inherently spans more than one resource. + + + Report any such operation that is modeled as a standard method rather than a + custom method. + + + + + + + + + Side effects limited to read-only or [effective + values](0111.mdx#effective-values) of another resource **may** occur in + standard methods. + + + + +Standard methods **must** guarantee +[atomicity](https://en.wikipedia.org/wiki/Atomicity_%28database_systems%29). + + + + + +```yaml +paths: + /orders/{orderId}: + patch: + operationId: updateOrder + description: >- + Applies all field changes together; a failure leaves the order + unchanged. +``` + + + The update either lands in full or not at all. A partial result is never + observable, which is what atomicity guarantees. + + + + + + +```yaml +paths: + /orders/{orderId}: + patch: + operationId: updateOrder + description: >- + Updates each field in turn; a mid-way failure leaves some fields + changed. +``` + + + A failure partway through leaves the order in a mixed state. The standard + method exposes a half-applied result, which atomicity forbids. + + + + + + + List the standard methods that mutate state (create, update, delete). + + + For each, inspect the implementation or documented behavior to see whether a + partial failure can leave the resource half-changed. + + + Report any mutating standard method that can produce an observable partial + result. When atomicity cannot be guaranteed, the operation belongs in a + sub-resource, a [singleton resource](0113.mdx), or a [custom + method](0109.mdx) instead. + + + + + + + + If a standard method is unsuitable, then custom methods offer a lesser, but still valuable level of consistency, helping the user reason about the scope of @@ -47,22 +425,358 @@ Selecting a custom method may be valuable for: ### Response bodies -- Every endpoint **must** support a versioned JSON content type (e.g. - `application/vnd.atlas.YYYY-MM-DD+json`), per [IPA-900](../sdks/0900.mdx). + + + + +Every endpoint **must** support a versioned JSON content type (e.g. +`application/vnd.atlas.YYYY-MM-DD+json`), per [IPA-900](../sdks/0900.mdx). + + + + + +```yaml +paths: + /orders/{orderId}: + get: + operationId: getOrder + responses: + "200": + content: + application/vnd.example.2023-01-01+json: + schema: + $ref: "#/components/schemas/Order" +``` + + + The response is offered under a dated, versioned JSON media type, so the + payload shape can evolve under a new date without breaking existing callers. + + + + + + +```yaml +paths: + /orders/{orderId}: + get: + operationId: getOrder + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Order" +``` + + + The response uses a bare, unversioned JSON media type. There is no version + handle, so a later change to the shape cannot be rolled out without breaking + callers pinned to the old shape. + + + + + + + For each operation under `paths`, collect the media type keys declared under + every response's `content`. + + + For each operation, check that at least one media type is a versioned JSON + type carrying a version token and ending in `+json`. + + + Report any operation whose responses offer no versioned JSON media type. + + + + + + + + Additional content types (e.g. `+csv`) **may** be offered alongside JSON. -- When a response body is returned as a JSON content type, it **must** be a JSON - object with a fixed set of named properties at the root. - - Top-level arrays, primitives, or objects with dynamic (unknown) keys at the - root are prohibited because they cannot be consistently typed by - schema-based clients and tooling. - - Collections **must** be wrapped in an envelope object per - [IPA-110](0110.mdx) (e.g. - `{"results": [...], "links": [...], "totalCount": 42}`). + + + + +When a response body is returned as a JSON content type, it **must** be a JSON +object with a fixed set of named properties at the root. Top-level arrays, +primitives, or objects with dynamic (unknown) keys at the root are prohibited +because they cannot be consistently typed by schema-based clients and tooling. + + + + + +```yaml +components: + schemas: + OrderList: + type: object + properties: + results: + type: array + items: + $ref: "#/components/schemas/Order" + totalCount: + type: integer +``` + + + The root is an object with named, declared properties. A schema-based client + can generate one stable type for the response. + + + + + + +```yaml +components: + schemas: + OrderList: + type: array + items: + $ref: "#/components/schemas/Order" +``` + + + The root is a bare array. A top-level array leaves no place to add fields + later and cannot be typed as a named object, so it is prohibited at the root. + + + + + + + For each operation under `paths`, find the schema of every JSON response + body, following any `$ref`. + + + Confirm the root schema declares `type: object` with a named `properties` + map. + + + Flag any root schema that is an array, a primitive, or an object that + declares only `additionalProperties` with no named properties. + + + + + + + + + +Collections **must** be wrapped in an envelope object per [IPA-110](0110.mdx) +(e.g. `{"results": [...], "links": [...], "totalCount": 42}`). + + + + + +```yaml +paths: + /orders: + get: + operationId: listOrders + responses: + "200": + content: + application/json: + schema: + type: object + properties: + results: + type: array + items: + $ref: "#/components/schemas/Order" + totalCount: + type: integer +``` + + + The list response wraps the array in an envelope object that also carries + pagination fields. The array sits under a named property, so the envelope can + grow without changing the root type. + + + + + + +```yaml +paths: + /orders: + get: + operationId: listOrders + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Order" +``` + + + The list response returns the array directly. With no envelope there is + nowhere to carry pagination links or a total count, and the root cannot be + extended later. + + + + + + + Identify the operations that return a collection — typically `list` + operations on a collection path. + + + For each, inspect the JSON response schema and confirm the array is held + under a named property of a root object rather than returned at the root. + + + Confirm the envelope carries the pagination fields required by IPA-110. + + + Report any collection response whose array is returned directly at the root. + + + + + + + + ### Naming -- The method name is the - [Operation ID](https://swagger.io/docs/specification/v3_0/paths-and-operations/#operationid) - (`operationId`) in the OpenAPI Specification -- Operation IDs **must** be unique -- Operation IDs **must** be in `camelCase` +The method name is the +[Operation ID](https://swagger.io/docs/specification/v3_0/paths-and-operations/#operationid) +(`operationId`) in the OpenAPI Specification. + + + + + +Operation IDs **must** be unique. + + + + + +```yaml +paths: + /orders: + get: + operationId: listOrders + /orders/{orderId}: + get: + operationId: getOrder +``` + + + Each operation carries a distinct `operationId`, so generated SDKs produce one + method per operation with no name collisions. + + + + + + +```yaml +paths: + /orders: + get: + operationId: getOrders + /orders/{orderId}: + get: + operationId: getOrders +``` + + + Two operations share `getOrders`. A duplicate id makes the mapping from + operation to generated method ambiguous. + + + + + + + Collect the `operationId` of every operation under `paths`. + + + Compare the values and find any that appear more than once. + + + Report each repeated `operationId` along with the operations that share it. + + + + + + + + + +Operation IDs **must** be in `camelCase`. + + + + + +```yaml +paths: + /orders/{orderId}: + get: + operationId: getOrder +``` + + + The `operationId` is camelCase: a lowercase first letter and no separators. + SDK generators map it straight to an idiomatic method name. + + + + + + +```yaml +paths: + /orders/{orderId}: + get: + operationId: Get_Order +``` + + + The `operationId` uses an uppercase first letter and an underscore separator. + It is neither camelCase nor a clean basis for a generated method name. + + + + + + + For each operation under `paths`, read its `operationId`. + + + Check that the value starts with a lowercase letter and contains only + letters and digits, with no spaces, underscores, or hyphens. + + Report any `operationId` that is not camelCase. + + + + + + +