diff --git a/AGENTS.md b/AGENTS.md index 388c014..c63d9cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,7 @@ uv for environments and locking, hatchling for the build, pytest, mypy --strict, | Generate `src/didit/types`, `src/didit/resources`, `tests/generated` from `openapi/` | `uv run python codegen/generate.py` | | Check the committed generated code is current (CI) | `uv run python codegen/generate.py --check` | | Sync `openapi/` with docs.didit.me (spec-sync workflow) | `uv run python codegen/spec_sync.py --summary spec-sync-summary.md` | +| Regenerate `openapi/breaking-vectors.json` (the shared breaking-change cases, DESIGN.md 3.1) | `uv run python codegen/breaking_vectors.py` | | Test (unit, generated, contract, webhooks, Authentication, codegen) | `uv run pytest` | | Test with the core coverage gate | `uv run pytest --cov --cov-report=term-missing --cov-fail-under=90` | | Live sandbox smoke tests (only with `DIDIT_SANDBOX_API_KEY`) | `uv run pytest -m live` | @@ -31,6 +32,7 @@ uv for environments and locking, hatchling for the build, pytest, mypy --strict, - Generated (never edit by hand): `src/didit/types/`, `src/didit/resources/`, `tests/generated/`. The generator is `codegen/didit_codegen/`; its tests are in `codegen/tests/`. - Core (hand-written): everything else in `src/didit/` (`_base_client.py` transport and retries, `_client.py` options, `_exceptions.py`, `_pagination.py`, `_files.py`, `_response.py`, `_models.py`), `src/didit/webhooks/` and `src/didit/authentication/`. +- `openapi/breaking-vectors.json` (written by `codegen/breaking_vectors.py`, never by hand) holds the breaking-change rules of DESIGN.md 3.1 as cases. The other six server SDKs copy it byte for byte, so a rule change lands here first: change DESIGN.md 3.1, `codegen/spec_sync.py` and the cases together, regenerate, then copy the file to the other six. - `openapi/deprecated-operations.json` (written by `codegen/spec_sync.py`) keeps methods whose operation was removed or renamed upstream as deprecated methods until `remove_in` (the next minor version); `tests/test_contract.py` fails once that version is reached, and the entry is then deleted. ## Rules diff --git a/CLAUDE.md b/CLAUDE.md index 388c014..c63d9cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,6 +20,7 @@ uv for environments and locking, hatchling for the build, pytest, mypy --strict, | Generate `src/didit/types`, `src/didit/resources`, `tests/generated` from `openapi/` | `uv run python codegen/generate.py` | | Check the committed generated code is current (CI) | `uv run python codegen/generate.py --check` | | Sync `openapi/` with docs.didit.me (spec-sync workflow) | `uv run python codegen/spec_sync.py --summary spec-sync-summary.md` | +| Regenerate `openapi/breaking-vectors.json` (the shared breaking-change cases, DESIGN.md 3.1) | `uv run python codegen/breaking_vectors.py` | | Test (unit, generated, contract, webhooks, Authentication, codegen) | `uv run pytest` | | Test with the core coverage gate | `uv run pytest --cov --cov-report=term-missing --cov-fail-under=90` | | Live sandbox smoke tests (only with `DIDIT_SANDBOX_API_KEY`) | `uv run pytest -m live` | @@ -31,6 +32,7 @@ uv for environments and locking, hatchling for the build, pytest, mypy --strict, - Generated (never edit by hand): `src/didit/types/`, `src/didit/resources/`, `tests/generated/`. The generator is `codegen/didit_codegen/`; its tests are in `codegen/tests/`. - Core (hand-written): everything else in `src/didit/` (`_base_client.py` transport and retries, `_client.py` options, `_exceptions.py`, `_pagination.py`, `_files.py`, `_response.py`, `_models.py`), `src/didit/webhooks/` and `src/didit/authentication/`. +- `openapi/breaking-vectors.json` (written by `codegen/breaking_vectors.py`, never by hand) holds the breaking-change rules of DESIGN.md 3.1 as cases. The other six server SDKs copy it byte for byte, so a rule change lands here first: change DESIGN.md 3.1, `codegen/spec_sync.py` and the cases together, regenerate, then copy the file to the other six. - `openapi/deprecated-operations.json` (written by `codegen/spec_sync.py`) keeps methods whose operation was removed or renamed upstream as deprecated methods until `remove_in` (the next minor version); `tests/test_contract.py` fails once that version is reached, and the entry is then deleted. ## Rules diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index db51488..1156836 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,9 @@ CI runs `uv run python codegen/generate.py --check`, which regenerates into a te The spec sync runs daily. When upstream changed, it regenerates the code, runs the checks and opens a pull request titled `(spec): sync API spec YYYY-MM-DD`. The type is `feat(spec)` when a caller can observe the change (a minor release, even when it can break typed code) and `chore(spec)` when only descriptions, examples or other documentation changed (no release on its own); never `!` or a `BREAKING CHANGE:` footer (DESIGN sections 3.1 and 13). A download that is not an OpenAPI 3 document with paths is refused and nothing is written. -The body opens with "Breaking changes", or "None detected": every field or parameter whose type changed or that can now be null, removed operation, field or enum value, and newly required parameter or request field, in parameters, request bodies and 2xx responses, following `$ref`s and array items. Then it lists added, removed, renamed and changed operations and schemas. It ends with a `BEGIN_COMMIT_OVERRIDE` block that release-please reads instead of the merged commit: the sync line plus one nested `api(spec):` commit per breaking change, which `release-please-config.json` lists under "API changes that can break typed code". Merge sync pull requests with **Squash and merge** and keep that block: after a merge commit release-please reads it once per commit and every line appears twice. When the lines do not fit in a pull request description (hundreds of breaking changes), the description says so and leaves the block out; then squash with the sync commit's message, which holds every line. +The body opens with "Breaking changes", or "None detected": every change the rules of DESIGN section 3.1 count, in parameters, request bodies and 2xx responses, following `$ref`s and array items. Then it lists added, removed, renamed and changed operations and schemas. It ends with a `BEGIN_COMMIT_OVERRIDE` block that release-please reads instead of the merged commit: the sync line plus one nested `api(spec):` commit per breaking change, which `release-please-config.json` lists under "API changes that can break typed code". Merge sync pull requests with **Squash and merge** and keep that block: after a merge commit release-please reads it once per commit and every line appears twice. When the lines do not fit in a pull request description (hundreds of breaking changes), the description says so and leaves the block out; then squash with the sync commit's message, which holds every line. + +The rules are held by `openapi/breaking-vectors.json`, which the other six server SDKs copy byte for byte and run through their own detectors. `codegen/breaking_vectors.py` writes it from hand-written cases: two minimal documents, before and after, with the expected commit type and the counted changes, each reviewed against DESIGN section 3.1. It refuses to write the file while `codegen/spec_sync.py` disagrees with a case, and `codegen/tests/test_breaking_vectors.py` fails while the committed file is not what it writes. To change a rule, change DESIGN section 3.1, the detector and the cases in one pull request, run `uv run python codegen/breaking_vectors.py`, and then copy the file to the other six repositories. `openapi/authentication.json` is synced like the others, but the `didit.authentication` module stays hand-written (DESIGN section 10). When a sync adds, removes or renames one of its server operations, the body lists it under "Authentication module" and `tests/test_authentication_contract.py` fails until the module follows on the sync branch. diff --git a/codegen/breaking_vectors.py b/codegen/breaking_vectors.py new file mode 100644 index 0000000..e5bb8b4 --- /dev/null +++ b/codegen/breaking_vectors.py @@ -0,0 +1,1006 @@ +#!/usr/bin/env python3 +"""Generate ``openapi/breaking-vectors.json``: the breaking-change rules of DESIGN.md 3.1 as cases. + + uv run python codegen/breaking_vectors.py # write the file + uv run python codegen/breaking_vectors.py --check # fail when the file is not current + +The file is the contract of the spec sync's breaking-change detector in all seven server +SDKs. It is generated here and copied byte for byte into the other six repositories, +whose tests run every case through their own detector; a rule change lands here first. + +Every case below is written by hand: two minimal OpenAPI documents, ``before`` and +``after``, whose operations carry ``x-didit-sdk`` (``server`` true or false), and the +expected result, checked against DESIGN.md 3.1 rather than copied from the detector. The +generator runs ``spec_sync.classify`` on each case and refuses to write the file while the +detector disagrees, so the file and this repository's detector never differ. + +The file is ``{"version": 1, "rules": "docs/DESIGN.md section 3.1", "generator": +"codegen/breaking_vectors.py", "cases": [...]}``. Each case has: + +- ``name``: unique, kebab-case. +- ``rule``: the DESIGN.md 3.1 bullet it covers, by its opening words: ``Responses``, + ``Requests``, ``Operations``, ``Unions``, ``Success statuses`` or ``Release notes`` + (the bullet "Every counted change reaches the release notes"). +- ``before`` and ``after``: the upstream ``verification.json`` before and after the change. +- ``expect.type``: ``feat(spec)`` or ``chore(spec)``. +- ``expect.breaking``: the counted changes, distinct and sorted by code point (all ASCII); + empty when the change is only listed. + +A counted change is one line, `` : ``: + +- ````: ``response`` (any 2xx body), ``request body``, ``query``, ``path`` or + ``header``. +- ````: a dotted path; ``[]`` is an array's items, ``{}`` a map's values, and + ``(oneOf N)`` or ``(anyOf N)`` the Nth variant (from 1) of the union after the change. + A parameter's field starts with its name (``ids[]``). A whole body has no ```` + (``GET /things/ response: can now be null``). +- ````: ``removed``, ``type -> ``, ``can now be null``, ``no longer + required``, ``now required`` (also a new required parameter or field), or ``enum value + removed`` with the value as JSON (``enum value "closed" removed``). +- Two kinds belong to the operation and have no `` ``: `` : + body now required`` and `` : operation removed``. +- A type is a JSON type name, ``string (date)``, ``string (date-time)`` or ``string + (binary)`` for the typed formats, or ``any`` for a schema without a type. Several types + are joined with ``|`` in sorted order. A union's variants, or a method's 2xx bodies, + give one type each, so ``type object|object -> object`` is an object variant (or the only + body of a removed status) that went away, and ``null`` is a method whose 2xx statuses + have no body any more (``type string -> null``). +- A 2xx status that keeps its code but loses or gains its body is read as that status + removed with its body and added without one (or the reverse). +""" + +from __future__ import annotations + +import argparse +import copy +import json +import pathlib +import re +import sys +from typing import Any + +import spec_sync + +REPO = pathlib.Path(__file__).resolve().parents[1] +OUTPUT = REPO / "openapi" / "breaking-vectors.json" +GENERATOR = "codegen/breaking_vectors.py" +# The rule of each case -> how its bullet opens in DESIGN.md 3.1. +RULES = { + "Responses": "- Responses (", + "Requests": "- Requests (", + "Operations": "- Operations:", + "Unions": "- Unions (", + "Success statuses": "- Success statuses:", + "Release notes": "- Every counted change reaches the release notes", +} +NAME = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*") +# A field: names, ".", "[]", "{}" and "(oneOf N)" / "(anyOf N)". +FIELD = r"(?:[^\s:()]+|\((?:oneOf|anyOf) [1-9][0-9]*\))+" +KIND = ( + r"(?:removed|type \S.* -> \S.*|can now be null|no longer required|now required" + r"|enum value .+ removed)" +) +BREAKING = re.compile( + r"(?:GET|PUT|POST|DELETE|PATCH|HEAD|OPTIONS) /\S*" + r"(?:: (?:body now required|operation removed)" + rf"|(?: response| request body)(?: {FIELD})?: {KIND}" + rf"| (?:query|path|header) {FIELD}: {KIND})" +) +Json = Any + +# Building blocks ---------------------------------------------------------------------------- + +STRING: Json = {"type": "string"} +INTEGER: Json = {"type": "integer"} + + +def document(paths: Json, schemas: Json = None) -> Json: + doc: Json = { + "openapi": "3.1.0", + "info": {"title": "Breaking-change vectors", "version": "1.0.0"}, + "paths": paths, + } + if schemas: + doc["components"] = {"schemas": schemas} + return doc + + +def naming(method: str, resource: str = "things", *, server: bool = True) -> Json: + auth = "api_key" if server else "device_token" + return {"resource": resource, "method": method, "auth": auth, "server": server} + + +def ok(schema: Json, description: str = "OK") -> Json: + return {"description": description, "content": {"application/json": {"schema": schema}}} + + +def operation( + method: str, + *, + resource: str = "things", + server: bool = True, + parameters: Json = None, + body: Json = None, + body_required: bool = False, + response: Json = None, + responses: Json = None, +) -> Json: + """One operation named ``resource.method``; a 200 JSON ``response`` unless ``responses``.""" + op: Json = {"x-didit-sdk": naming(method, resource, server=server)} + if parameters: + op["parameters"] = parameters + if body is not None: + op["requestBody"] = {"content": {"application/json": {"schema": body}}} + if body_required: + op["requestBody"]["required"] = True + op["responses"] = responses if responses is not None else {"200": ok(response or THING)} + return op + + +def obj(properties: Json, required: tuple[str, ...] = ()) -> Json: + out: Json = {"type": "object", "properties": properties} + if required: + out["required"] = list(required) + return out + + +def param(name: str, where: str = "query", schema: Json = None, *, required: bool = False) -> Json: + out: Json = {"name": name, "in": where, "schema": schema or STRING} + if required or where == "path": + out["required"] = True + return out + + +def enum(*values: str) -> Json: + return {"type": "string", "enum": list(values)} + + +def ref(name: str) -> Json: + return {"$ref": f"#/components/schemas/{name}"} + + +def edit(schema: Json, **properties: Json) -> Json: + """``schema`` with these properties replaced (None removes one, from ``required`` too).""" + out = copy.deepcopy(schema) + for name, value in properties.items(): + if value is None: + del out["properties"][name] + if name in out.get("required", []): + out["required"].remove(name) + else: + out["properties"][name] = value + return out + + +def requiring(schema: Json, *names: str) -> Json: + return {**copy.deepcopy(schema), "required": list(names)} + + +ID = param("id", "path") +THING = obj( + {"id": STRING, "name": STRING, "status": enum("active", "closed")}, + required=("id", "name", "status"), +) +JOB = obj({"job_id": STRING}, required=("job_id",)) +NEW_THING = obj({"name": STRING, "note": STRING, "amount": INTEGER}, required=("name",)) +CARD = obj({"kind": enum("card"), "last4": STRING}, required=("kind", "last4")) +BANK = obj({"kind": enum("bank"), "iban": STRING}, required=("kind", "iban")) +WALLET = obj({"kind": enum("wallet"), "address": STRING}, required=("kind", "address")) + + +def get_thing(response: Json = None, **options: Json) -> Json: + """``GET /things/{id}/``, method ``things.retrieve``, returning ``response``.""" + parameters = [ID, *options.pop("parameters", [])] + return document( + { + "/things/{id}/": { + "get": operation("retrieve", parameters=parameters, response=response, **options) + } + } + ) + + +def list_things(*parameters: Json) -> Json: + """``GET /things/``, method ``things.list``, with these query parameters.""" + response = obj({"results": {"type": "array", "items": THING}}) + return document( + {"/things/": {"get": operation("list", parameters=list(parameters), response=response)}} + ) + + +def create_thing(body: Json = None, *, body_required: bool = False, **options: Json) -> Json: + """``POST /things/``, method ``things.create``, sending ``body``.""" + op = operation( + "create", body=NEW_THING if body is None else body, body_required=body_required, **options + ) + return document({"/things/": {"post": op}}) + + +def get_payment(response: Json) -> Json: + """``GET /payments/{id}/``, method ``payments.retrieve``, returning ``response``.""" + op = operation("retrieve", resource="payments", parameters=[ID], response=response) + return document({"/payments/{id}/": {"get": op}}) + + +def create_with_statuses(responses: Json) -> Json: + """``POST /things/`` with these responses (a status without a body is ``None``).""" + full = { + status: ok(schema) if schema is not None else {"description": "No content"} + for status, schema in responses.items() + } + return create_thing(responses=full) + + +def case( + name: str, + rule: str, + before: Json, + after: Json, + breaking: list[str] | tuple[str, ...] = (), + *, + commit_type: str = "feat(spec)", +) -> Json: + return { + "name": name, + "rule": rule, + "before": before, + "after": after, + "expect": {"type": commit_type, "breaking": sorted(breaking)}, + } + + +# The cases ---------------------------------------------------------------------------------- + + +def _responses() -> list[Json]: + rule = "Responses" + thing = "GET /things/{id}/ response" + dated = edit(THING, created_at=STRING, born_on={"type": "string", "format": "date"}) + owned = edit(THING, owner=obj({"email": STRING})) + nullable_email = {"anyOf": [STRING, {"type": "null"}]} + scored = edit(THING, score={"type": "number"}, metadata={}) + files = edit(THING, file={"type": "string", "format": "binary"}) + counted = edit(THING, counts={"type": "object", "additionalProperties": INTEGER}) + listed = obj({"results": {"type": "array", "items": ref("Thing")}}) + extended = {"allOf": [ref("Base"), obj({"label": STRING})]} + base = obj({"id": STRING, "created_at": STRING}, required=("id",)) + documented = copy.deepcopy(get_thing()) + get = documented["paths"]["/things/{id}/"]["get"] + get["summary"] = "Retrieve a thing" + get["description"] = "Returns one thing." + get["responses"]["200"] = ok( + { + **edit(THING, name={**STRING, "description": "The name.", "example": "Box"}), + "title": "Thing", + }, + description="The thing", + ) + return [ + case( + "response-field-removed", + rule, + get_thing(), + get_thing(edit(THING, name=None)), + [f"{thing} name: removed"], + ), + case( + "response-field-type-changed", + rule, + get_thing(), + get_thing(edit(THING, id=INTEGER)), + [f"{thing} id: type string -> integer"], + ), + case( + "response-field-can-now-be-null", + rule, + get_thing(), + get_thing(edit(THING, name={"type": ["string", "null"]})), + [f"{thing} name: can now be null"], + ), + case( + "response-nested-field-can-now-be-null", + rule, + get_thing(owned), + get_thing(edit(owned, owner=obj({"email": nullable_email}))), + [f"{thing} owner.email: can now be null"], + ), + case( + "response-field-no-longer-required", + rule, + get_thing(), + get_thing(requiring(THING, "id", "status")), + [f"{thing} name: no longer required"], + ), + case( + "response-enum-value-removed", + rule, + get_thing(), + get_thing(edit(THING, status=enum("active"))), + [f'{thing} status: enum value "closed" removed'], + ), + case( + "response-typed-format-gained-and-changed", + rule, + get_thing(dated), + get_thing( + edit( + dated, + created_at={"type": "string", "format": "date-time"}, + born_on={"type": "string", "format": "date-time"}, + ) + ), + [ + f"{thing} born_on: type string (date) -> string (date-time)", + f"{thing} created_at: type string -> string (date-time)", + ], + ), + case( + "response-typed-format-lost", + rule, + get_thing(files), + get_thing(edit(files, file=STRING)), + [f"{thing} file: type string (binary) -> string"], + ), + case( + "response-value-becomes-untyped-or-typed", + rule, + get_thing(scored), + get_thing( + edit(scored, score={"description": "Any score."}, metadata=obj({"source": STRING})) + ), + [f"{thing} metadata: type any -> object", f"{thing} score: type number -> any"], + ), + case( + "response-map-value-type-changed", + rule, + get_thing(counted), + get_thing(edit(counted, counts={"type": "object", "additionalProperties": STRING})), + [f"{thing} counts{{}}: type integer -> string"], + ), + case( + "response-ref-and-array-items", + rule, + document({"/things/": {"get": operation("list", response=listed)}}, {"Thing": THING}), + document( + {"/things/": {"get": operation("list", response=listed)}}, + {"Thing": edit(THING, name=None)}, + ), + ["GET /things/ response results[].name: removed"], + ), + case( + "response-all-of", + rule, + document( + { + "/things/{id}/": { + "get": operation("retrieve", parameters=[ID], response=extended) + } + }, + {"Base": base}, + ), + document( + { + "/things/{id}/": { + "get": operation("retrieve", parameters=[ID], response=extended) + } + }, + {"Base": edit(base, id=INTEGER, created_at=None)}, + ), + [f"{thing} created_at: removed", f"{thing} id: type string -> integer"], + ), + case( + "response-field-added-is-listed", + rule, + get_thing(), + get_thing(edit(THING, color=STRING)), + ), + case( + "response-enum-value-added-is-listed", + rule, + get_thing(), + get_thing(edit(THING, status=enum("active", "closed", "archived"))), + ), + case( + "response-field-now-always-present-is-listed", + rule, + get_thing(requiring(THING, "id", "status")), + get_thing(), + ), + case( + "response-value-no-longer-null-is-listed", + rule, + get_thing(edit(THING, name={"type": ["string", "null"]})), + get_thing(), + ), + case( + "response-documentation-only", rule, get_thing(), documented, commit_type="chore(spec)" + ), + ] + + +def _requests() -> list[Json]: + rule = "Requests" + things, create = "GET /things/", "POST /things/" + limit = param("limit", schema=INTEGER) + status = param("status", schema=enum("active", "closed")) + key = param("Idempotency-Key", "header") + tags = {"type": "array", "items": enum("a", "b")} + tagged = edit(NEW_THING, tags=tags) + archive = operation("archive", parameters=[ID], response=JOB) + reason = obj({"reason": STRING}, required=("reason",)) + archive_with_body = operation( + "archive", parameters=[ID], response=JOB, body=reason, body_required=True + ) + documented = copy.deepcopy(list_things(limit)) + documented["paths"]["/things/"]["get"]["parameters"][0].update( + description="How many things to return.", example=10 + ) + return [ + case( + "query-parameter-removed", + rule, + list_things(limit), + list_things(), + [f"{things} query limit: removed"], + ), + case( + "header-parameter-removed", + rule, + create_thing(parameters=[key]), + create_thing(), + [f"{create} header Idempotency-Key: removed"], + ), + case( + "request-body-field-removed", + rule, + create_thing(), + create_thing(edit(NEW_THING, note=None)), + [f"{create} request body note: removed"], + ), + case( + "query-parameter-type-changed", + rule, + list_things(limit), + list_things(param("limit", schema=STRING)), + [f"{things} query limit: type integer -> string"], + ), + case( + "path-parameter-type-changed", + rule, + get_thing(), + document( + { + "/things/{id}/": { + "get": operation("retrieve", parameters=[param("id", "path", INTEGER)]) + } + } + ), + ["GET /things/{id}/ path id: type string -> integer"], + ), + case( + "request-body-field-type-changed", + rule, + create_thing(), + create_thing( + edit( + NEW_THING, amount={"type": "number"}, note={"type": "string", "format": "date"} + ) + ), + [ + f"{create} request body amount: type integer -> number", + f"{create} request body note: type string -> string (date)", + ], + ), + case( + "new-required-parameter", + rule, + list_things(), + list_things(param("cursor", required=True)), + [f"{things} query cursor: now required"], + ), + case( + "new-required-body-field", + rule, + create_thing(), + create_thing(requiring(edit(NEW_THING, currency=STRING), "name", "currency")), + [f"{create} request body currency: now required"], + ), + case( + "optional-parameter-becomes-required", + rule, + list_things(limit), + list_things(param("limit", schema=INTEGER, required=True)), + [f"{things} query limit: now required"], + ), + case( + "optional-body-field-becomes-required", + rule, + create_thing(), + create_thing(requiring(NEW_THING, "name", "note")), + [f"{create} request body note: now required"], + ), + case( + "body-becomes-required", + rule, + create_thing(), + create_thing(body_required=True), + [f"{create}: body now required"], + ), + case( + "new-required-body", + rule, + document({"/things/{id}/archive/": {"post": archive}}), + document({"/things/{id}/archive/": {"post": archive_with_body}}), + [ + "POST /things/{id}/archive/ request body reason: now required", + "POST /things/{id}/archive/: body now required", + ], + ), + case( + "query-enum-value-removed", + rule, + list_things(status), + list_things(param("status", schema=enum("active"))), + [f'{things} query status: enum value "closed" removed'], + ), + case( + "request-body-array-item-enum-value-removed", + rule, + create_thing(tagged), + create_thing(edit(tagged, tags={"type": "array", "items": enum("a")})), + [f'{create} request body tags[]: enum value "b" removed'], + ), + case( + "request-body-ref-field-removed", + rule, + document( + {"/things/": {"post": operation("create", body=ref("NewThing"))}}, + {"NewThing": NEW_THING}, + ), + document( + {"/things/": {"post": operation("create", body=ref("NewThing"))}}, + {"NewThing": edit(NEW_THING, note=None)}, + ), + [f"{create} request body note: removed"], + ), + case( + "parameter-can-now-be-null-is-listed", + rule, + list_things(limit), + list_things(param("limit", schema={"type": ["integer", "null"]})), + ), + case( + "body-field-can-now-be-null-is-listed", + rule, + create_thing(), + create_thing(edit(NEW_THING, note={"anyOf": [STRING, {"type": "null"}]})), + ), + case( + "parameter-no-longer-required-is-listed", + rule, + list_things(param("limit", schema=INTEGER, required=True)), + list_things(limit), + ), + case( + "body-field-no-longer-required-is-listed", + rule, + create_thing(), + create_thing(requiring(NEW_THING)), + ), + case( + "body-no-longer-required-is-listed", + rule, + create_thing(body_required=True), + create_thing(), + ), + case("optional-parameter-added-is-listed", rule, list_things(), list_things(limit)), + case( + "optional-body-field-added-is-listed", + rule, + create_thing(), + create_thing(edit(NEW_THING, color=STRING)), + ), + case( + "request-enum-value-added-is-listed", + rule, + list_things(status), + list_things(param("status", schema=enum("active", "closed", "archived"))), + ), + case( + "request-documentation-only", + rule, + list_things(limit), + documented, + commit_type="chore(spec)", + ), + ] + + +def _operations() -> list[Json]: + rule = "Operations" + retrieve = operation("retrieve", parameters=[ID]) + delete = operation("delete", parameters=[ID], response=JOB) + device = operation( + "retrieve", resource="devices", server=False, parameters=[ID, param("fields")] + ) + device_after = operation( + "retrieve", + resource="devices", + server=False, + parameters=[ID], + response=edit(THING, name=None), + ) + renamed = copy.deepcopy(retrieve) + renamed["x-didit-sdk"]["method"] = "get" + gone = operation("retrieve", parameters=[ID], response=edit(THING, name=None)) + return [ + case( + "operation-removed", + rule, + document({"/things/{id}/": {"get": retrieve, "delete": delete}}), + document({"/things/{id}/": {"get": retrieve}}), + ["DELETE /things/{id}/: operation removed"], + ), + case( + "operation-no-longer-on-server-sdks", + rule, + document({"/things/{id}/": {"get": retrieve}}), + document( + { + "/things/{id}/": { + "get": {**retrieve, "x-didit-sdk": naming("retrieve", server=False)} + } + } + ), + ["GET /things/{id}/: operation removed"], + ), + case( + "server-false-operation-removed-is-listed", + rule, + document({"/things/{id}/": {"get": retrieve}, "/device/things/{id}/": {"get": device}}), + document({"/things/{id}/": {"get": retrieve}}), + ), + case( + "server-false-operation-changes-are-listed", + rule, + document({"/device/things/{id}/": {"get": device}}), + document({"/device/things/{id}/": {"get": device_after}}), + ), + case( + "operation-added-is-listed", + rule, + document({"/things/{id}/": {"get": retrieve}}), + document({"/things/{id}/": {"get": retrieve, "delete": delete}}), + ), + case( + "moved-operation-changes-count-under-its-new-path", + rule, + document({"/things/{id}/": {"get": retrieve}}), + document({"/items/{id}/": {"get": gone}}), + ["GET /items/{id}/ response name: removed"], + ), + case( + "moved-operation-unchanged-is-listed", + rule, + document({"/things/{id}/": {"get": retrieve}}), + document({"/items/{id}/": {"get": retrieve}}), + ), + case( + "renamed-method-is-listed", + rule, + document({"/things/{id}/": {"get": retrieve}}), + document({"/things/{id}/": {"get": renamed}}), + ), + ] + + +def _unions() -> list[Json]: + rule = "Unions" + payment = "GET /payments/{id}/ response" + bank = edit(BANK, iban=INTEGER) + titled = {**CARD, "title": "Card payment"} + by_email = obj({"email": STRING}, required=("email",)) + by_phone = obj({"phone": STRING, "country_code": STRING}, required=("phone",)) + contact = document( + { + "/contacts/": { + "post": operation( + "create", resource="contacts", body={"anyOf": [by_email, by_phone]} + ) + } + } + ) + contact_after = document( + { + "/contacts/": { + "post": operation( + "create", + resource="contacts", + body={"anyOf": [by_email, edit(by_phone, country_code=None)]}, + ) + } + } + ) + return [ + case( + "union-variant-added-and-paired-variant-changed", + rule, + get_payment({"oneOf": [CARD, BANK]}), + get_payment({"oneOf": [CARD, bank, WALLET]}), + [f"{payment} (oneOf 2).iban: type string -> integer"], + ), + case( + "union-variants-reordered-and-paired-variant-changed", + rule, + get_payment({"oneOf": [CARD, BANK]}), + get_payment({"oneOf": [bank, CARD]}), + [f"{payment} (oneOf 1).iban: type string -> integer"], + ), + case( + "union-untyped-variant-added-and-paired-variant-changed", + rule, + get_payment({"anyOf": [CARD, BANK]}), + get_payment({"anyOf": [edit(CARD, last4=INTEGER), BANK, {"description": "Other."}]}), + [ + f"{payment} (anyOf 1).last4: type string -> integer", + f"{payment}: type object -> any", + ], + ), + case( + "union-variant-removed", + rule, + get_payment({"oneOf": [CARD, BANK]}), + get_payment({"oneOf": [CARD]}), + [f"{payment}: type object|object -> object"], + ), + case( + "schema-becomes-union-compared-with-its-variant", + rule, + get_payment(CARD), + get_payment({"oneOf": [edit(CARD, last4=INTEGER), BANK]}), + [f"{payment} (oneOf 1).last4: type string -> integer"], + ), + case( + "request-union-variant-field-removed", + rule, + contact, + contact_after, + ["POST /contacts/ request body (anyOf 2).country_code: removed"], + ), + case( + "union-variant-added-is-listed", + rule, + get_payment({"oneOf": [CARD, BANK]}), + get_payment({"oneOf": [CARD, BANK, WALLET]}), + ), + case( + "union-variants-reordered-is-listed", + rule, + get_payment({"oneOf": [CARD, BANK]}), + get_payment({"oneOf": [BANK, CARD]}), + ), + case( + "union-variant-title-renamed-is-listed", + rule, + get_payment({"oneOf": [titled, BANK]}), + get_payment({"oneOf": [{**titled, "title": "Card charge"}, BANK]}), + ), + ] + + +def _statuses() -> list[Json]: + rule = "Success statuses" + create = "POST /things/ response" + return [ + case( + "added-status-without-body", + rule, + create_with_statuses({"200": THING}), + create_with_statuses({"200": THING, "204": None}), + [f"{create}: can now be null"], + ), + case( + "removed-status-whose-body-no-other-returns", + rule, + create_with_statuses({"200": THING, "202": JOB}), + create_with_statuses({"200": THING}), + [f"{create}: type object|object -> object"], + ), + case( + "removed-status-whose-body-has-another-type", + rule, + create_with_statuses({"200": STRING, "202": INTEGER}), + create_with_statuses({"200": STRING}), + [f"{create}: type integer|string -> string"], + ), + case( + "only-success-status-changed-compares-the-bodies", + rule, + create_with_statuses({"200": THING}), + create_with_statuses({"201": edit(THING, name=None)}), + [f"{create} name: removed"], + ), + case( + "only-success-status-replaced-by-one-without-body", + rule, + create_with_statuses({"200": STRING}), + create_with_statuses({"204": None}), + [f"{create}: can now be null", f"{create}: type string -> null"], + ), + case( + "removed-status-leaves-only-statuses-without-body", + rule, + create_with_statuses({"200": STRING, "204": None}), + create_with_statuses({"204": None}), + [f"{create}: type string -> null"], + ), + case( + "status-loses-its-body", + rule, + create_with_statuses({"200": THING}), + create_with_statuses({"200": None}), + [f"{create}: can now be null", f"{create}: type object -> null"], + ), + case( + "added-status-with-another-body-is-listed", + rule, + create_with_statuses({"200": THING}), + create_with_statuses({"200": THING, "202": JOB}), + ), + case( + "added-status-with-the-same-body-is-listed", + rule, + create_with_statuses({"200": THING}), + create_with_statuses({"200": THING, "201": THING}), + ), + case( + "removed-status-whose-body-another-returns-is-listed", + rule, + create_with_statuses({"200": THING, "201": THING}), + create_with_statuses({"200": THING}), + ), + case( + "removed-status-without-body-is-listed", + rule, + create_with_statuses({"200": THING, "204": None}), + create_with_statuses({"200": THING}), + ), + case( + "status-gains-a-body-is-listed", + rule, + create_with_statuses({"204": None}), + create_with_statuses({"204": THING}), + ), + case( + "only-success-status-changed-is-listed", + rule, + create_with_statuses({"200": THING}), + create_with_statuses({"201": THING}), + ), + ] + + +def _release_notes() -> list[Json]: + rule = "Release notes" + wide = obj({f"field_{i:02d}": STRING for i in range(30)}) + retrieve = operation("retrieve", parameters=[ID, param("expand")]) + delete = operation("delete", parameters=[ID], response=JOB) + create = operation("create", body=NEW_THING) + after_retrieve = operation( + "retrieve", parameters=[ID], response=edit(THING, id=INTEGER, color=STRING) + ) + after_create = operation( + "create", body=edit(NEW_THING, note={"type": ["string", "null"]}, amount=None) + ) + return [ + case( + "every-counted-change-is-listed", + rule, + get_thing(wide), + get_thing(obj({})), + [f"GET /things/{{id}}/ response field_{i:02d}: removed" for i in range(30)], + ), + case( + "counted-changes-of-every-operation-are-listed", + rule, + document( + {"/things/": {"post": create}, "/things/{id}/": {"get": retrieve, "delete": delete}} + ), + document( + {"/things/": {"post": after_create}, "/things/{id}/": {"get": after_retrieve}} + ), + [ + "DELETE /things/{id}/: operation removed", + "GET /things/{id}/ query expand: removed", + "GET /things/{id}/ response id: type string -> integer", + "POST /things/ request body amount: removed", + ], + ), + ] + + +def cases() -> list[Json]: + return [ + *_responses(), + *_requests(), + *_operations(), + *_unions(), + *_statuses(), + *_release_notes(), + ] + + +# The file ----------------------------------------------------------------------------------- + + +def _check(found: list[Json]) -> list[str]: + """Every problem with the cases: their form, and each result the detector disagrees with.""" + problems: list[str] = [] + names = [c["name"] for c in found] + problems += [ + f"{n}: the name is used twice" for n in sorted({n for n in names if names.count(n) > 1}) + ] + for rule in RULES: + if sum(c["rule"] == rule for c in found) < 2: + problems.append(f"rule {rule!r} has fewer than two cases") + for c in found: + expect = c["expect"] + if not NAME.fullmatch(c["name"]): + problems.append(f"{c['name']}: the name is not kebab-case") + if c["rule"] not in RULES: + problems.append(f"{c['name']}: unknown rule {c['rule']!r}") + for line in expect["breaking"]: + if not line.isascii() or not BREAKING.fullmatch(line): + problems.append(f"{c['name']}: {line!r} is not a normalized breaking change") + if expect["breaking"] and expect["type"] != "feat(spec)": + problems.append(f"{c['name']}: a counted change is always feat(spec)") + found_type, found_breaking = spec_sync.classify(c["before"], c["after"]) + if (found_type, found_breaking) != (expect["type"], expect["breaking"]): + problems.append( + f"{c['name']}: the detector gives {found_type} {json.dumps(found_breaking, indent=1)}, " + f"the case expects {expect['type']} {json.dumps(expect['breaking'], indent=1)}" + ) + return problems + + +def render() -> str: + """The file's text; raises SystemExit when a case is malformed or the detector disagrees.""" + found = cases() + problems = _check(found) + if problems: + raise SystemExit("breaking vectors not written:\n" + "\n".join(problems)) + data = { + "version": 1, + "rules": "docs/DESIGN.md section 3.1", + "generator": GENERATOR, + "cases": found, + } + return json.dumps(data, indent=1) + "\n" + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + "--check", action="store_true", help="fail when the committed file is not current" + ) + args = parser.parse_args(argv) + text = render() + if args.check: + current = OUTPUT.read_bytes() if OUTPUT.exists() else b"" + if current != text.encode(): + print( + f"{OUTPUT.relative_to(REPO)} is not current: run uv run python {GENERATOR}", + file=sys.stderr, + ) + return 1 + return 0 + # LF on every platform: .gitattributes checks openapi/*.json out byte for byte. + OUTPUT.write_text(text, "utf-8", newline="\n") + print(f"wrote {OUTPUT.relative_to(REPO)} ({len(json.loads(text)['cases'])} cases)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/codegen/spec_sync.py b/codegen/spec_sync.py index 057a389..b4758da 100644 --- a/codegen/spec_sync.py +++ b/codegen/spec_sync.py @@ -23,11 +23,10 @@ caller can observe changed, ``chore`` when only documentation did. Never ``!`` or a ``BREAKING CHANGE:`` footer: a change that mirrors the API is a minor version; - writes a Markdown summary that opens with "Breaking changes", every change that - can break typed code (a field or parameter whose type changed or that can now be - null, a removed operation, field or enum value, a newly required parameter or - request field) in parameters, request bodies and 2xx responses, following - ``$ref``s and array items, then lists added, removed, renamed and changed - operations and schemas; + the rules of DESIGN section 3.1 count (``openapi/breaking-vectors.json`` checks them, + see ``codegen/breaking_vectors.py``) in parameters, request bodies and 2xx + responses, following ``$ref``s and array items, then lists added, removed, renamed + and changed operations and schemas; - writes the commit message release-please reads (``--commit-message``): the sync line, then one nested ``api(spec):`` commit per breaking change, which ``release-please-config.json`` lists under "API changes that can break typed code". @@ -47,6 +46,7 @@ import pathlib import re import sys +import tempfile import urllib.request from typing import Any @@ -95,6 +95,9 @@ # String formats with their own type in the generated code (datetime, date, bytes or a # file): adding, removing or changing one changes the field's type. TYPED_FORMATS = frozenset({"date-time", "date", "binary"}) +PARAMETER_LOCATIONS = frozenset({"path", "query", "header", "cookie"}) +# Normalized kinds of the operation itself: no `` `` (BreakingChange.normalized). +OPERATION_KINDS = frozenset({"operation removed", "body now required"}) MAX_NOTES = 25 MAX_DEPTH = 32 NO_DIFFERENCE = "descriptions, examples or other details changed" @@ -235,18 +238,29 @@ def observable(doc: Json) -> Json: # Schema comparison ---------------------------------------------------------------------- +def _target(where: str, path: str) -> str: + """Where a change is, as the summary and the release notes say it.""" + if where in PARAMETER_LOCATIONS: + return f"{where} parameter `{path}`" + return f"{where} `{path}`" if path else where + + @dataclasses.dataclass(frozen=True) class Change: - """One difference of an operation; ``breaking`` when it is in the DESIGN 3.1 list.""" + """One difference of an operation; ``breaking`` when DESIGN 3.1 counts it.""" - where: str # "response", "request body", "parameter `limit` (query)", "operation" - path: str # the field, for example "registry_checks[].company.registry_data"; "" for all + where: str # "response", "request body", "query" (a parameter's location), "operation" + # The field, for example "registry_checks[].company.registry_data", or a parameter's + # name and the field in it ("limit", "ids[]"); "" for a whole body. + path: str kind: str # "can now be null", "type changed from string to object", "removed", ... breaking: bool = False + # A counted change's kind as the shared vectors spell it: "removed", "type string -> + # object", "now required" (see BreakingChange.normalized); "" when not counted. + code: str = "" def text(self) -> str: - target = f"{self.where} `{self.path}`" if self.path else self.where - return f"{target}: {self.kind}" + return f"{_target(self.where, self.path)}: {self.kind}" @dataclasses.dataclass(frozen=True) @@ -257,12 +271,22 @@ class BreakingChange: where: str path: str kind: str + code: str = "" def describe(self) -> str: # No "<" or ">": release-please escapes them in the release notes. - target = " ".join(p for p in (self.where, f"`{self.path}`" if self.path else "") if p) + target = _target(self.where, self.path) return f"`{self.operation}`{' ' + target if target else ''}: {self.kind}" + def normalized(self) -> str: + """The line ``openapi/breaking-vectors.json`` compares in all seven SDKs: + `` : ``, without ```` for a whole body + and without `` `` for the operation itself.""" + if self.code in OPERATION_KINDS: + return f"{self.operation}: {self.code}" + target = " ".join(p for p in (self.where, self.path) if p) + return f"{self.operation} {target}: {self.code}" + def _null_only(schema: Json) -> bool: return isinstance(schema, dict) and ( @@ -359,18 +383,29 @@ def _kinds(schema: Json, depth: int = 0) -> frozenset[str]: return frozenset(kinds) -def _type_names(schema: Json, kinds: frozenset[str]) -> str: - """``kinds`` as the summary names them; a string with a typed format is its own type, - and a schema that does not say allows any value.""" +def _type_names(schema: Json, kinds: frozenset[str], sep: str = " or ") -> str: + """``kinds`` as the summary names them (the vectors join them with ``|``); a string + with a typed format is its own type, and a schema that does not say allows any value.""" if not kinds: return "any" fmt = schema.get("format") - return " or ".join( + return sep.join( f"string ({fmt})" if kind == "string" and fmt in TYPED_FORMATS else kind for kind in sorted(kinds) ) +def _union_names(schemas: list[Json]) -> str: + """The types of a union's variants (or of a method's 2xx bodies) as the vectors name + them: sorted, and repeated per variant, so that losing one of two objects reads + ``object|object -> object``.""" + names = [] + for schema in schemas: + single = _single(schema) if isinstance(schema, dict) else {} + names.extend(_type_names(single, _kinds(single), "|").split("|")) + return "|".join(sorted(names)) + + def _enum(schema: Json) -> list[Json] | None: values = schema.get("enum") return [v for v in values if v is not None] if isinstance(values, list) else None @@ -406,25 +441,27 @@ def diff_schema( ) -> None: """Append a :class:`Change` for every difference between two dereferenced schemas. - Breaking (DESIGN 3.1, the same in requests and responses): a type change in either - direction, a value that can now be null, a removed field, enum value or union - variant, and in a request (``request``) a field that is newly required. Everything - else is listed, not counted. + Counted (DESIGN 3.1), in requests and responses: a type change in either direction + (a typed string format and "any" included), a removed field, enum value or union + variant. Only in a response: a value that can now be null and a field that is no + longer required. Only in a request (``request``): a field that is newly required. + Everything else is listed, not counted. """ if depth > MAX_DEPTH or _shape(old) == _shape(new): return start = len(out) - # A body that appears or disappears is compared field by field, not as a type change. + # A request body that appears or disappears is compared field by field, not as a type + # change (a response's is a success-status change, _diff_statuses). present = old is not NO_SCHEMA and new is not NO_SCHEMA - def note(kind: str, breaking: bool = False, at: str = path) -> None: - out.append(Change(where, at, kind, breaking)) + def note(kind: str, breaking: bool = False, at: str = path, code: str = "") -> None: + out.append(Change(where, at, kind, breaking, code if breaking else "")) - if not isinstance(old, dict) or not isinstance(new, dict): - note("schema replaced", True) - return + # A schema that is not an object (``true``) allows any value. + old, new = (s if isinstance(s, dict) else {} for s in (old, new)) if _nullable(new) and not _nullable(old): - note("can now be null", True) + # A request that accepts null only widens what a caller may send. + note("can now be null", not request, code="can now be null") elif _nullable(old) and not _nullable(new): note("no longer accepts null" if request else "can no longer be null") old, new = _single(old), _single(new) @@ -432,9 +469,13 @@ def note(kind: str, breaking: bool = False, at: str = path) -> None: before, after = _type_names(old, old_kinds), _type_names(new, new_kinds) # "any" (a schema that does not say, typed ``object``) against a type counts too. if present and (old_kinds or new_kinds) and before != after: - note(f"type changed from {before} to {after}", True) - if old_kinds and new_kinds: - # A union's variants can change too; the type change already covers those removed. + code = "type {} -> {}".format( + _type_names(old, old_kinds, "|"), _type_names(new, new_kinds, "|") + ) + note(f"type changed from {before} to {after}", True, code=code) + # A union's variants can change too, also when an untyped variant made it "any"; the + # type change already covers those removed. A schema untyped as a whole has none. + if (old_kinds or _variants(old)) and (new_kinds or _variants(new)): _diff_variants( old, new, where, path, request=request, out=out, depth=depth, retyped=True ) @@ -444,7 +485,8 @@ def note(kind: str, breaking: bool = False, at: str = path) -> None: if old_enum is not None and new_enum is not None: for value in old_enum: if value not in new_enum: - note(f"enum value {json.dumps(value, ensure_ascii=False)} removed", True) + kind = f"enum value {json.dumps(value, ensure_ascii=False)} removed" + note(kind, True, code=kind) added = [v for v in new_enum if v not in old_enum] if added: note(f"enum values added {json.dumps(added, ensure_ascii=False)}") @@ -458,16 +500,21 @@ def note(kind: str, breaking: bool = False, at: str = path) -> None: # A union's fields live in its variants, compared below. if (old_props and new_props) or not (_variants(old) or _variants(new)): for name in sorted(set(old_props) - set(new_props)): - note("removed", True, _join(path, name)) + note("removed", True, _join(path, name), "removed") for name in sorted(set(new_props) - set(old_props)): required = request and name in new_req - note("added (required)" if required else "added", required, _join(path, name)) + kind = "added (required)" if required else "added" + note(kind, required, _join(path, name), "now required") for name in sorted(set(old_props) & set(new_props)): at = _join(path, name) if name in new_req and name not in old_req: - note("now required" if request else "now always present", request, at) + note( + "now required" if request else "now always present", request, at, "now required" + ) elif name in old_req and name not in new_req: - note("no longer required" if request else "no longer always present", False, at) + # A response field that may be missing breaks code that reads it. + kind = "no longer required" if request else "no longer always present" + note(kind, not request, at, "no longer required") diff_schema( old_props[name], new_props[name], where, at, request=request, out=out, depth=depth + 1, @@ -571,7 +618,8 @@ def _diff_variants( out.append(Change(where, at, f"variant type renamed from {before} to {after}")) if removed and not retyped: kind = f"type changed: {len(removed)} {keyword} variant(s) removed" - out.append(Change(where, path, kind, True)) + code = f"type {_union_names(old_vars)} -> {_union_names(new_vars)}" + out.append(Change(where, path, kind, True, code)) if added: out.append(Change(where, path, f"{len(added)} {keyword} variant(s) added")) order = [j for _, j in sorted(pairs)] @@ -579,10 +627,6 @@ def _diff_variants( out.append(Change(where, path, f"{keyword} variants reordered")) -def _parameter_where(key: tuple[str, str]) -> str: - return f"parameter `{key[1]}` ({key[0]})" - - def _parameters(doc: Json, op: Json) -> dict[tuple[str, str], Json]: found: dict[tuple[str, str], Json] = {} for parameter in dereference(doc, op.get("parameters") or [], ref_titles=False): @@ -632,11 +676,16 @@ def _diff_content( def _diff_statuses( - old: dict[str, dict[str, Json]], new: dict[str, dict[str, Json]] + old: dict[str, dict[str, Json]], + new: dict[str, dict[str, Json]], + removed: list[str], + added: list[str], ) -> list[Change]: - """The success statuses only one side has. The method returns a union of every 2xx - body, nullable when one has none: a status without a body makes it nullable, and a - removed status whose body no other one returns removes a variant of it (DESIGN 3.1).""" + """The success statuses not compared body by body: ``removed`` (only ``old`` has them, + or their body disappeared) and ``added`` (only ``new`` has them, or their body appeared). + The method returns a union of every 2xx body, nullable when one has none: a status + without a body makes it nullable, and a removed status whose body no other one returns + removes a variant of it, ``null`` when no body is left (DESIGN 3.1).""" found: list[Change] = [] def bodies(responses: dict[str, dict[str, Json]]) -> list[Json]: @@ -645,65 +694,103 @@ def bodies(responses: dict[str, dict[str, Json]]) -> list[Json]: def returns_null(responses: dict[str, dict[str, Json]]) -> bool: return any(not content for content in responses.values()) - for status in sorted(set(new) - set(old)): + def types(responses: dict[str, dict[str, Json]]) -> str: + distinct: dict[str, Json] = {} + for content in responses.values(): + if content: + main = content.get("application/json", content[min(content)]) + distinct.setdefault(json.dumps(_shape(content, names=True), sort_keys=True), main) + return _union_names(list(distinct.values())) or "null" + + for status in added: if not new[status] and bodies(old) and not returns_null(old): - found.append( - Change("response", "", f"can now be null (status {status} has no body)", True) - ) + label = "lost its body" if status in old else "has no body" + kind = f"can now be null (status {status} {label})" + found.append(Change("response", "", kind, True, "can now be null")) + elif status in old: + continue # a body that appeared or disappeared: said with the old side below elif new[status] and _shape(new[status], names=True) not in bodies(old): found.append(Change("response", "", f"status {status} added with another body")) else: found.append(Change("response", "", f"status {status} added")) - for status in sorted(set(old) - set(new)): + for status in removed: + change = "removed" + if status in new: + change = "lost its body" if old[status] else "now has a body" if not old[status] and bodies(new) and not returns_null(new): - found.append(Change("response", "", f"can no longer be null (status {status} removed)")) + found.append( + Change("response", "", f"can no longer be null (status {status} {change})") + ) elif old[status] and _shape(old[status], names=True) not in bodies(new): - kind = f"type changed: status {status} removed, and no other status returns its body" - found.append(Change("response", "", kind, True)) + kind = f"type changed: status {status} {change}, and no other status returns its body" + found.append(Change("response", "", kind, True, f"type {types(old)} -> {types(new)}")) else: - found.append(Change("response", "", f"status {status} removed")) + found.append(Change("response", "", f"status {status} {change}")) return found def describe_change(old_doc: Json, old: Json, new_doc: Json, new: Json) -> list[Change]: """Every difference of one operation (or of a method that moved to another one).""" found: list[Change] = [] + # A parameter is ``where`` (its location) and ``path`` (its name, then the field in it). old_params, new_params = _parameters(old_doc, old), _parameters(new_doc, new) - for key in sorted(set(old_params) - set(new_params)): - found.append(Change(_parameter_where(key), "", "removed")) + for where, name in sorted(set(old_params) - set(new_params)): + found.append(Change(where, name, "removed", True, "removed")) for key in sorted(set(new_params) - set(old_params)): required = new_params[key].get("required") is True - found.append( - Change(_parameter_where(key), "", "added (required)" if required else "added", required) - ) + kind = "added (required)" if required else "added" + found.append(Change(*key, kind, required, "now required" if required else "")) for key in sorted(set(old_params) & set(new_params)): - where = _parameter_where(key) - if old_params[key].get("required") is not True and new_params[key].get("required") is True: - found.append(Change(where, "", "now required", True)) + was, now = (params[key].get("required") is True for params in (old_params, new_params)) + if now and not was: + found.append(Change(*key, "now required", True, "now required")) + elif was and not now: + found.append(Change(*key, "no longer required")) diff_schema( - old_params[key].get("schema") or {}, new_params[key].get("schema") or {}, where, + old_params[key].get("schema") or {}, new_params[key].get("schema") or {}, *key, request=True, out=found, ) # fmt: skip old_body, new_body = old.get("requestBody") or {}, new.get("requestBody") or {} - if dereference(old_doc, old_body).get("required") is not True and ( - dereference(new_doc, new_body).get("required") is True - ): - found.append(Change("request body", "", "now required")) + was, now = ( + dereference(d, b).get("required") is True + for d, b in ((old_doc, old_body), (new_doc, new_body)) + ) + if now and not was: + found.append(Change("request body", "", "now required", True, "body now required")) + elif was and not now: + found.append(Change("request body", "", "no longer required")) _diff_content( _content(old_doc, old_body), _content(new_doc, new_body), "request body", request=True, out=found, ) # fmt: skip - # Every 2xx response both have; when the only success status changed (200 to 201), - # the old body against the new one; otherwise the statuses only one side has. + # Every 2xx response both have with a body on both sides or on neither; when the only + # success status changed (200 to 201), the old body against the new one. The other + # statuses, and a body that appeared or disappeared, change what the method returns. old_responses, new_responses = _successes(old_doc, old), _successes(new_doc, new) - statuses = [(s, s) for s in old_responses if s in new_responses] - if not statuses and len(old_responses) == 1 and len(new_responses) == 1: + statuses = [ + (s, s) + for s in old_responses + if s in new_responses and bool(old_responses[s]) == bool(new_responses[s]) + ] + if ( + not statuses + and len(old_responses) == 1 == len(new_responses) + and bool(next(iter(old_responses.values()))) == bool(next(iter(new_responses.values()))) + ): statuses = [(next(iter(old_responses)), next(iter(new_responses)))] found.append(Change("response", "", f"status {statuses[0][0]} -> {statuses[0][1]}")) else: - found.extend(_diff_statuses(old_responses, new_responses)) + compared = {s for s, _ in statuses} + found.extend( + _diff_statuses( + old_responses, + new_responses, + [s for s in old_responses if s not in compared], + [s for s in new_responses if s not in compared], + ) + ) for before, after in statuses: _diff_content( old_responses[before], new_responses[after], "response", request=False, out=found @@ -833,6 +920,12 @@ def old_naming(key: str) -> Json: def new_naming(key: str) -> Json: return new_ops[key][1].get("x-didit-sdk") or old_overlay.get(key) + def exposed(key: str) -> bool: + """Whether a server SDK method calls the operation before and after the sync: + the changes of an operation with ``server: false`` are listed, never counted + (DESIGN 3.1). One without naming counts: the generator fails until it is named.""" + return all(n is None or bool(n.get("server")) for n in (old_naming(key), new_naming(key))) + added = sorted(set(new_ops) - set(old_ops)) removed = sorted(set(old_ops) - set(new_ops)) common = sorted(set(old_ops) & set(new_ops)) @@ -885,7 +978,9 @@ def keep(naming: Json, key: str, reason: str, replacement: str | None) -> None: kept.append(method) def removed_operation(key: str, detail: str) -> None: - breaking.append(BreakingChange(key, "", "", f"operation removed{detail}")) + breaking.append( + BreakingChange(key, "", "", f"operation removed{detail}", "operation removed") + ) lines.append(f"## Added operations ({len(added)})") for key in added: @@ -987,7 +1082,7 @@ def removed_operation(key: str, detail: str) -> None: f"- `{method}`{marker} moved from `{old_key}` to `{new_key}`: " + "; ".join(found) ) breaking.extend( - BreakingChange(new_key, c.where, c.path, c.kind) for c in changes if c.breaking + BreakingChange(new_key, c.where, c.path, c.kind, c.code) for c in changes if c.breaking ) lines.append(f"## Moved or reassigned methods ({len(moves)})") lines.extend(moves) @@ -1015,9 +1110,13 @@ def removed_operation(key: str, detail: str) -> None: if old != new: documentation_only.append(key) continue + if not exposed(key): + changes = [dataclasses.replace(c, breaking=False, code="") for c in changes] marker = " **(breaking)**" if any(c.breaking for c in changes) else "" changes_lines.append(f"- `{key}`{marker}: " + "; ".join(notes(changes))) - breaking.extend(BreakingChange(key, c.where, c.path, c.kind) for c in changes if c.breaking) + breaking.extend( + BreakingChange(key, c.where, c.path, c.kind, c.code) for c in changes if c.breaking + ) lines.append(f"## Changed operations ({len(changes_lines) + len(documentation_only)})") lines.extend(changes_lines) if documentation_only: @@ -1134,6 +1233,22 @@ def removed_operation(key: str, detail: str) -> None: return SyncResult(True, summary, today.isoformat(), commit_type, breaking) +def classify(before: Json, after: Json) -> tuple[str, list[str]]: + """What the sync makes of ``verification.json`` changing from ``before`` to ``after`` + (two complete documents, every operation named by its own ``x-didit-sdk``): the + commit type, ``feat(spec)`` or ``chore(spec)``, and the counted breaking changes, + normalized, distinct and sorted. ``openapi/breaking-vectors.json`` holds the expected + result of each of its cases (``codegen/breaking_vectors.py``).""" + now = dt.datetime(2026, 1, 1, tzinfo=dt.timezone.utc) + with tempfile.TemporaryDirectory() as tmp: + openapi_dir = pathlib.Path(tmp) + (openapi_dir / "verification.json").write_bytes(json.dumps(before).encode()) + (openapi_dir / "overlay.json").write_bytes(json.dumps({"operations": {}}).encode()) + downloads = {"verification.json": json.dumps(after).encode()} + result = sync(openapi_dir, downloads, today=now.date(), now=now, version="1.0.0") + return f"{result.commit_type}(spec)", sorted({b.normalized() for b in result.breaking}) + + def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter diff --git a/codegen/tests/test_breaking_vectors.py b/codegen/tests/test_breaking_vectors.py new file mode 100644 index 0000000..604fed0 --- /dev/null +++ b/codegen/tests/test_breaking_vectors.py @@ -0,0 +1,170 @@ +"""The shared breaking-change vectors, ``openapi/breaking-vectors.json`` (DESIGN.md 3.1). + +The other six server SDKs copy the file byte for byte and run every case through their own +detector. Here every case runs through ``spec_sync``, and the file must be exactly what +``codegen/breaking_vectors.py`` writes. +""" + +from __future__ import annotations + +import json +import pathlib +import re +import sys +from typing import Any + +import pytest + +CODEGEN = pathlib.Path(__file__).resolve().parents[1] +REPO = CODEGEN.parent +VECTORS_PATH = REPO / "openapi" / "breaking-vectors.json" +sys.path.insert(0, str(CODEGEN)) + +import breaking_vectors # noqa: E402 +import spec_sync # noqa: E402 + +VECTORS: Any = json.loads(VECTORS_PATH.read_text("utf-8")) +CASES: list[Any] = VECTORS["cases"] +BY_NAME = {case["name"]: case for case in CASES} + + +def test_the_file_names_its_rules_and_its_generator() -> None: + assert list(VECTORS) == ["version", "rules", "generator", "cases"] + assert VECTORS["version"] == 1 + assert VECTORS["rules"] == "docs/DESIGN.md section 3.1" + assert VECTORS["generator"] == "codegen/breaking_vectors.py" + assert (REPO / VECTORS["generator"]).is_file() + + +@pytest.mark.parametrize("case", CASES, ids=[case["name"] for case in CASES]) +def test_every_case_passes(case: Any) -> None: + assert list(case) == ["name", "rule", "before", "after", "expect"] + expect = case["expect"] + assert spec_sync.classify(case["before"], case["after"]) == ( + expect["type"], + expect["breaking"], + ) + + +def test_the_file_is_what_the_generator_writes_every_time() -> None: + first, second = breaking_vectors.render(), breaking_vectors.render() + assert first == second + assert VECTORS_PATH.read_bytes() == first.encode() + assert breaking_vectors.main(["--check"]) == 0 + + +def test_every_rule_is_a_design_bullet_with_at_least_two_cases() -> None: + design = (REPO / "docs" / "DESIGN.md").read_text("utf-8") + section = design.split("### 3.1 Spec sync", 1)[1].split("\n## ", 1)[0] + for rule, opening in breaking_vectors.RULES.items(): + assert f"\n{opening}" in section, rule + assert sum(case["rule"] == rule for case in CASES) >= 2, rule + assert {case["rule"] for case in CASES} == set(breaking_vectors.RULES) + + +def test_names_are_unique_and_every_line_is_normalized_and_sorted() -> None: + assert len(BY_NAME) == len(CASES) + for case in CASES: + assert re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", case["name"]), case["name"] + expect = case["expect"] + assert expect["type"] in ("feat(spec)", "chore(spec)") + assert expect["breaking"] == sorted(set(expect["breaking"])), case["name"] + for line in expect["breaking"]: + assert line.isascii(), line + assert breaking_vectors.BREAKING.fullmatch(line), line + + +def _counted(side: str) -> set[str]: + """The kinds counted somewhere in the file on one side: "response", "request" (the body + and the parameters) or "operation".""" + found: set[str] = set() + for case in CASES: + for line in case["expect"]["breaking"]: + target, kind = line.split(": ", 1) + where = target.split(" ", 2)[2] if target.count(" ") >= 2 else "operation" + if where.startswith("response"): + where = "response" + elif where != "operation": + where = "request" + if where == side: + found.add(re.sub(r"^(type|enum value) .*", r"\1", kind)) + return found + + +def test_each_counted_kind_is_covered_in_responses_and_in_requests() -> None: + assert _counted("response") == { + "removed", + "type", + "can now be null", + "no longer required", + "enum value", + } + assert _counted("request") == {"removed", "type", "now required", "enum value"} + assert _counted("operation") == {"body now required", "operation removed"} + + +# The issue's coverage list: every item names the cases that cover it. +COVERAGE = { + "listed, not counted": [ + "response-field-added-is-listed", + "response-enum-value-added-is-listed", + "parameter-can-now-be-null-is-listed", + "body-field-can-now-be-null-is-listed", + "parameter-no-longer-required-is-listed", + "body-field-no-longer-required-is-listed", + "operation-added-is-listed", + "union-variant-title-renamed-is-listed", + "added-status-with-another-body-is-listed", + ], + "documentation only": ["response-documentation-only", "request-documentation-only"], + "$ref": ["response-ref-and-array-items", "request-body-ref-field-removed"], + "array items": ["response-ref-and-array-items", "request-body-array-item-enum-value-removed"], + "allOf": ["response-all-of"], + "union with an added variant": ["union-variant-added-and-paired-variant-changed"], + "union with a reordered variant": ["union-variants-reordered-and-paired-variant-changed"], + "server: false": [ + "server-false-operation-removed-is-listed", + "server-false-operation-changes-are-listed", + ], + "moved operation": ["moved-operation-changes-count-under-its-new-path"], +} + + +@pytest.mark.parametrize("item", COVERAGE) +def test_the_file_covers(item: str) -> None: + for name in COVERAGE[item]: + case = BY_NAME[name] + documents = json.dumps([case["before"], case["after"]]) + breaking = case["expect"]["breaking"] + if item == "listed, not counted": + assert (case["expect"]["type"], breaking) == ("feat(spec)", []), name + elif item == "documentation only": + assert (case["expect"]["type"], breaking) == ("chore(spec)", []), name + elif item == "$ref": + assert '"$ref"' in documents, name + assert breaking, name + elif item == "array items": + assert any("[]" in line for line in breaking), name + elif item == "allOf": + assert '"allOf"' in documents, name + assert breaking, name + elif item.startswith("union"): + assert any("(oneOf " in line for line in breaking), name + elif item == "server: false": + assert '"server": false' in documents, name + assert not breaking, name + else: + assert set(case["before"]["paths"]).isdisjoint(case["after"]["paths"]), name + assert breaking, name + + +def test_the_real_api_change_of_2026_11_04_gives_the_same_list_in_vector_form() -> None: + # The first-round fixtures, normalized the way the vectors spell them. + fixtures = CODEGEN / "tests" / "fixtures" + before = json.loads((fixtures / "verification-9fe57fc69299601e59cc7a64c1db370fea5c7461.json").read_bytes()) # fmt: skip + after = json.loads((fixtures / "verification-e002a815773bbd1599256e2774046ee76520c6c8.json").read_bytes()) # fmt: skip + commit_type, breaking = spec_sync.classify(before, after) + assert commit_type == "feat(spec)" + assert len(breaking) == 14 + assert "POST /v3/email/risk/ response email.enrichment.reason_codes[]: type string -> object" in breaking # fmt: skip + assert "POST /v3/kyb/ubo/ response kyb_registry.registry_data: can now be null" in breaking diff --git a/codegen/tests/test_codegen.py b/codegen/tests/test_codegen.py index e917741..453a0bb 100644 --- a/codegen/tests/test_codegen.py +++ b/codegen/tests/test_codegen.py @@ -420,7 +420,7 @@ def test_a_moved_operation_reports_what_changed(openapi: pathlib.Path) -> None: _section(summary, "Moved or reassigned methods") ) assert _breaking(summary).strip().splitlines() == [ - "- `GET /v4/billing/balance/` parameter `currency` (query): added (required)", + "- `GET /v4/billing/balance/` query parameter `currency`: added (required)", "- `GET /v4/billing/balance/` response: type changed from object to array", ] assert _read(openapi / "deprecated-operations.json")["methods"] == {} @@ -566,11 +566,11 @@ def test_sync_reports_changes_and_drops_overlay_entries(openapi: pathlib.Path) - summary = result.summary assert result.changed assert "`GET /v3/new-thing/`: **no SDK naming**" in summary - assert "`POST /v3/session/` **(breaking)**: parameter `dry_run` (query): added (required)" in ( + assert "`POST /v3/session/` **(breaking)**: query parameter `dry_run`: added (required)" in ( summary ) assert _breaking(summary).strip() == ( - "- `POST /v3/session/` parameter `dry_run` (query): added (required)" + "- `POST /v3/session/` query parameter `dry_run`: added (required)" ) assert "Dropped 1 overlay entries" in summary assert "POST /v3/session/" not in _read(openapi / "overlay.json")["operations"] diff --git a/codegen/tests/test_spec_sync.py b/codegen/tests/test_spec_sync.py index 33735d1..09579f9 100644 --- a/codegen/tests/test_spec_sync.py +++ b/codegen/tests/test_spec_sync.py @@ -221,7 +221,8 @@ def diff(old: Any, new: Any) -> list[tuple[str, str, bool]]: ("", "type changed from number to integer", True) ] assert diff({"type": "string"}, {"type": "object", "properties": {}}) == [("", OBJECT, True)] - # A value that can now be null, however the document says it. + # A value that can now be null, however the document says it: counted in a response, + # listed in a request (it only widens what a caller may send). for nullable in ( {"type": "string", "nullable": True}, {"type": ["string", "null"]}, @@ -229,8 +230,8 @@ def diff(old: Any, new: Any) -> list[tuple[str, str, bool]]: {"oneOf": [{"type": "string"}, {"enum": [None]}]}, {"allOf": [{"type": "string"}], "nullable": True}, ): - assert diff({"type": "string"}, nullable) == [("", NULL, True)], nullable - assert diff(STATUS, {**STATUS, "enum": ["a", "b", None]}) == [("", NULL, True)] + assert diff({"type": "string"}, nullable) == [("", NULL, not request_side)], nullable + assert diff(STATUS, {**STATUS, "enum": ["a", "b", None]}) == [("", NULL, not request_side)] # A removed field or enum value, nested in objects and array items. assert diff(OBJ, {"type": "object", "properties": {}}) == [("a", "removed", True)] assert diff(STATUS, {**STATUS, "enum": ["a"]}) == [("", 'enum value "b" removed', True)] @@ -264,12 +265,12 @@ def test_a_string_format_with_its_own_type_is_a_type_change(request_side: bool) date = {"type": "string", "format": "date"} stamp = {"type": ["string", "null"], "format": "date-time"} assert _diff(date, stamp, request=request_side) == [ - ("", NULL, True), + ("", NULL, not request_side), ("", "type changed from string (date) to string (date-time)", True), ] listed = {"type": "array", "items": plain} assert _diff(listed, {**listed, "items": stamp}, request=request_side) == [ - ("[]", NULL, True), + ("[]", NULL, not request_side), ("[]", "type changed from string to string (date-time)", True), ] @@ -288,9 +289,11 @@ def test_a_schema_that_allows_any_value_is_a_type_change(request_side: bool) -> assert _diff(listed, {"type": "array"}, request=request_side) == [ ("[]", "type changed from string to any", True) ] + # An untyped variant makes the union "any", and the variant it replaced is compared too. either = {"oneOf": [{"type": "string"}, {"type": "integer"}]} assert _diff(either, {"oneOf": [{"type": "string"}, {}]}, request=request_side) == [ - ("", "type changed from integer or string to any", True) + ("", "type changed from integer or string to any", True), + ("(oneOf 2)", "type changed from integer to any", True), ] @@ -347,17 +350,19 @@ def test_a_variant_title_names_its_generated_type() -> None: assert _diff(a, {**a, "title": "Chosen items"}) == [] -def test_newly_required_request_fields_count_and_response_requiredness_does_not() -> None: +def test_required_counts_where_it_narrows_what_a_caller_sends_or_gets() -> None: optional = {**OBJ, "required": []} + # A request field that is newly required, or new and required, counts. assert _diff(optional, OBJ, request=True) == [("a", "now required", True)] added = {"type": "object", "properties": {**OBJ["properties"], "b": {"type": "string"}}} assert _diff(optional, {**added, "required": ["b"]}, request=True) == [ ("b", "added (required)", True) ] assert _diff(optional, added, request=True) == [("b", "added", False)] - assert _diff(optional, OBJ) == [("a", "now always present", False)] - assert _diff(OBJ, optional) == [("a", "no longer always present", False)] assert _diff(OBJ, optional, request=True) == [("a", "no longer required", False)] + # A response field that may now be missing counts; one always present is listed. + assert _diff(OBJ, optional) == [("a", "no longer always present", True)] + assert _diff(optional, OBJ) == [("a", "now always present", False)] def test_listed_but_not_counted() -> None: @@ -423,25 +428,35 @@ def _json(schema: Any) -> dict[str, Any]: def test_parameters() -> None: limit = {"name": "limit", "in": "query", "schema": {"type": "integer"}} - where = "parameter `limit` (query)" - def changes(old: Any, new: Any) -> list[tuple[str, str, bool]]: + def changes(old: Any, new: Any) -> list[tuple[str, str, str, bool]]: found = spec_sync.describe_change({}, old, {}, new) - return [(c.where, c.kind, c.breaking) for c in found] + return [(c.where, c.path, c.kind, c.breaking) for c in found] required_null = {**limit, "required": True, "schema": {"type": "integer", "nullable": True}} assert changes(_op(limit), _op(required_null)) == [ - (where, "now required", True), - (where, NULL, True), + ("query", "limit", "now required", True), + ("query", "limit", NULL, False), + ] + assert changes(_op({**limit, "required": True}), _op(limit)) == [ + ("query", "limit", "no longer required", False) ] assert changes(_op(limit), _op({**limit, "schema": {"type": "string"}})) == [ - (where, "type changed from integer to string", True) + ("query", "limit", "type changed from integer to string", True) ] assert changes(_op(limit), _op({**limit, "name": "page"})) == [ - (where, "removed", False), - ("parameter `page` (query)", "added", False), + ("query", "limit", "removed", True), + ("query", "page", "added", False), + ] + assert changes(_op(), _op({**limit, "required": True})) == [ + ("query", "limit", "added (required)", True) ] - assert changes(_op(), _op({**limit, "required": True})) == [(where, "added (required)", True)] + ids = {"name": "ids", "in": "query", "schema": {"type": "array", "items": STATUS}} + narrowed = {**ids, "schema": {"type": "array", "items": {**STATUS, "enum": ["a"]}}} + assert changes(_op(ids), _op(narrowed)) == [("query", "ids[]", 'enum value "b" removed', True)] + # The summary and the release notes name the parameter and its location. + removed = spec_sync.describe_change({}, _op(limit), {}, _op()) + assert [c.text() for c in removed] == ["query parameter `limit`: removed"] def test_every_success_response_is_compared_and_reported_once() -> None: @@ -477,6 +492,16 @@ def changes(old: Any, new: Any) -> list[tuple[str, bool]]: same = {**one, "202": _json(OBJ)} assert changes(one, same) == [("status 202 added", False)] assert changes(same, one) == [("status 202 removed", False)] + # A status that loses its body is removed with it and added without one, and the + # reverse; with no body left, the method returns None. + empty = {"200": no_body["204"]} + assert changes(one, empty) == [ + ("can now be null (status 200 lost its body)", True), + ("type changed: status 200 lost its body, and no other status returns its body", True), + ] + assert changes(empty, one) == [("can no longer be null (status 200 now has a body)", False)] + found = spec_sync.describe_change({}, _op(responses=one), {}, _op(responses=no_body)) + assert [c.code for c in found] == ["can now be null", "type object -> null"] def test_a_body_that_appears_is_compared_field_by_field() -> None: @@ -487,6 +512,15 @@ def test_a_body_that_appears_is_compared_field_by_field() -> None: ] +def test_a_request_body_that_becomes_required_counts() -> None: + old = _op(body=OBJ) + new = {**old, "requestBody": {**old["requestBody"], "required": True}} + found = spec_sync.describe_change({}, old, {}, new) + assert [(c.text(), c.breaking, c.code) for c in found] == [ + ("request body: now required", True, "body now required") + ] + + def test_references_are_followed() -> None: doc = {"components": {"schemas": {"Obj": OBJ}}} new_doc = {"components": {"schemas": {"Obj": {**OBJ, "properties": {}}}}} diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 22e9dd7..eb201f7 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -63,6 +63,14 @@ The commit never carries `!` or a `BREAKING CHANGE:` footer: a change that mirro The pull request body opens with a "Breaking changes" section, then lists added, removed and changed operations and schemas. "Breaking changes" lists every change that can break typed code, or says "None detected": a field or parameter whose type changed, a field that can now be null, a removed operation, field or enum value, and a newly required parameter or request field. Every repository detects exactly that list, so the seven pull requests for one upstream change report the same breaking changes. +The rules, checked in every repository by the shared vectors in `openapi/breaking-vectors.json` (generated in sdk-python and copied byte for byte into the other six; a rule change lands there first): + +- Responses (the body of every 2xx status): a removed field, a field whose type changed, a field that can now be null or is no longer required, and a removed enum value count. A typed string format (`date`, `date-time`, `binary`) gained, lost or changed, and a typed value becoming untyped or the reverse, are type changes. An added field or enum value is listed, not counted: enums are open and unknown values must be accepted. +- Requests (path, query and header parameters, and the body): a removed parameter or field, a parameter or field whose type changed, a new required parameter or field, an optional one becoming required, a body becoming required, and a removed enum value count. A parameter or field that can now be null or stops being required is listed, not counted, because it only widens what a caller may send. +- Operations: a removed operation counts when its SDK method goes; a moved operation's changes count under its new path; an added operation is listed. Operations with `server: false` are listed and never counted. +- Unions (`oneOf`, `anyOf`): each old variant is paired with the variant it became, and changes inside a paired variant follow the rules above even when other variants are added, removed or reordered. A renamed variant title is listed, not counted. +- Success statuses: a method returns every 2xx body. An added 2xx status without a body counts as "can now be null"; a removed status whose body no other 2xx status returns counts as a type change; any other status change is listed. +- Every counted change reaches the release notes, with no cap. The release notes repeat that list under "API changes that can break typed code". A new server operation without SDK naming fails the generator with a message that names the operation; the fix is an entry in `overlay.json` (or upstream). The sync never deletes a public SDK method silently: a removed operation is marked deprecated for one minor version first, and the pull request says so. diff --git a/openapi/breaking-vectors.json b/openapi/breaking-vectors.json new file mode 100644 index 0000000..e5c4d9c --- /dev/null +++ b/openapi/breaking-vectors.json @@ -0,0 +1,11171 @@ +{ + "version": 1, + "rules": "docs/DESIGN.md section 3.1", + "generator": "codegen/breaking_vectors.py", + "cases": [ + { + "name": "response-field-removed", + "rule": "Responses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /things/{id}/ response name: removed" + ] + } + }, + { + "name": "response-field-type-changed", + "rule": "Responses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /things/{id}/ response id: type string -> integer" + ] + } + }, + { + "name": "response-field-can-now-be-null", + "rule": "Responses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /things/{id}/ response name: can now be null" + ] + } + }, + { + "name": "response-nested-field-can-now-be-null", + "rule": "Responses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + }, + "owner": { + "type": "object", + "properties": { + "email": { + "type": "string" + } + } + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + }, + "owner": { + "type": "object", + "properties": { + "email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + } + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /things/{id}/ response owner.email: can now be null" + ] + } + }, + { + "name": "response-field-no-longer-required", + "rule": "Responses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /things/{id}/ response name: no longer required" + ] + } + }, + { + "name": "response-enum-value-removed", + "rule": "Responses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /things/{id}/ response status: enum value \"closed\" removed" + ] + } + }, + { + "name": "response-typed-format-gained-and-changed", + "rule": "Responses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + }, + "created_at": { + "type": "string" + }, + "born_on": { + "type": "string", + "format": "date" + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "born_on": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /things/{id}/ response born_on: type string (date) -> string (date-time)", + "GET /things/{id}/ response created_at: type string -> string (date-time)" + ] + } + }, + { + "name": "response-typed-format-lost", + "rule": "Responses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + }, + "file": { + "type": "string", + "format": "binary" + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + }, + "file": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /things/{id}/ response file: type string (binary) -> string" + ] + } + }, + { + "name": "response-value-becomes-untyped-or-typed", + "rule": "Responses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + }, + "score": { + "type": "number" + }, + "metadata": {} + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + }, + "score": { + "description": "Any score." + }, + "metadata": { + "type": "object", + "properties": { + "source": { + "type": "string" + } + } + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /things/{id}/ response metadata: type any -> object", + "GET /things/{id}/ response score: type number -> any" + ] + } + }, + { + "name": "response-map-value-type-changed", + "rule": "Responses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + }, + "counts": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + }, + "counts": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /things/{id}/ response counts{}: type integer -> string" + ] + } + }, + { + "name": "response-ref-and-array-items", + "rule": "Responses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Thing" + } + } + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Thing": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Thing" + } + } + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Thing": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "status" + ] + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /things/ response results[].name: removed" + ] + } + }, + { + "name": "response-all-of", + "rule": "Responses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Base" + }, + { + "type": "object", + "properties": { + "label": { + "type": "string" + } + } + } + ] + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Base": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "created_at": { + "type": "string" + } + }, + "required": [ + "id" + ] + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Base" + }, + { + "type": "object", + "properties": { + "label": { + "type": "string" + } + } + } + ] + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Base": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + }, + "required": [ + "id" + ] + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /things/{id}/ response created_at: removed", + "GET /things/{id}/ response id: type string -> integer" + ] + } + }, + { + "name": "response-field-added-is-listed", + "rule": "Responses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + }, + "color": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "response-enum-value-added-is-listed", + "rule": "Responses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed", + "archived" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "response-field-now-always-present-is-listed", + "rule": "Responses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "response-value-no-longer-null-is-listed", + "rule": "Responses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "response-documentation-only", + "rule": "Responses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "The thing", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string", + "description": "The name.", + "example": "Box" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ], + "title": "Thing" + } + } + } + } + }, + "summary": "Retrieve a thing", + "description": "Returns one thing." + } + } + } + }, + "expect": { + "type": "chore(spec)", + "breaking": [] + } + }, + { + "name": "query-parameter-removed", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /things/ query limit: removed" + ] + } + }, + { + "name": "header-parameter-removed", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "Idempotency-Key", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "POST /things/ header Idempotency-Key: removed" + ] + } + }, + { + "name": "request-body-field-removed", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "POST /things/ request body note: removed" + ] + } + }, + { + "name": "query-parameter-type-changed", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /things/ query limit: type integer -> string" + ] + } + }, + { + "name": "path-parameter-type-changed", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "integer" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /things/{id}/ path id: type string -> integer" + ] + } + }, + { + "name": "request-body-field-type-changed", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string", + "format": "date" + }, + "amount": { + "type": "number" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "POST /things/ request body amount: type integer -> number", + "POST /things/ request body note: type string -> string (date)" + ] + } + }, + { + "name": "new-required-parameter", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "cursor", + "in": "query", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /things/ query cursor: now required" + ] + } + }, + { + "name": "new-required-body-field", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + }, + "currency": { + "type": "string" + } + }, + "required": [ + "name", + "currency" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "POST /things/ request body currency: now required" + ] + } + }, + { + "name": "optional-parameter-becomes-required", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /things/ query limit: now required" + ] + } + }, + { + "name": "optional-body-field-becomes-required", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name", + "note" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "POST /things/ request body note: now required" + ] + } + }, + { + "name": "body-becomes-required", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "POST /things/: body now required" + ] + } + }, + { + "name": "new-required-body", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/archive/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "archive", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "job_id": { + "type": "string" + } + }, + "required": [ + "job_id" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/archive/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "archive", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "reason": { + "type": "string" + } + }, + "required": [ + "reason" + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "job_id": { + "type": "string" + } + }, + "required": [ + "job_id" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "POST /things/{id}/archive/ request body reason: now required", + "POST /things/{id}/archive/: body now required" + ] + } + }, + { + "name": "query-enum-value-removed", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "status", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "status", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "active" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /things/ query status: enum value \"closed\" removed" + ] + } + }, + { + "name": "request-body-array-item-enum-value-removed", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + }, + "tags": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "a", + "b" + ] + } + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + }, + "tags": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "a" + ] + } + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "POST /things/ request body tags[]: enum value \"b\" removed" + ] + } + }, + { + "name": "request-body-ref-field-removed", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewThing" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "NewThing": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewThing" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "NewThing": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "POST /things/ request body note: removed" + ] + } + }, + { + "name": "parameter-can-now-be-null-is-listed", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": [ + "integer", + "null" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "body-field-can-now-be-null-is-listed", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "parameter-no-longer-required-is-listed", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "body-field-no-longer-required-is-listed", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "body-no-longer-required-is-listed", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "optional-parameter-added-is-listed", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "optional-body-field-added-is-listed", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + }, + "color": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "request-enum-value-added-is-listed", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "status", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "status", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "active", + "closed", + "archived" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "request-documentation-only", + "rule": "Requests", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "list", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer" + }, + "description": "How many things to return.", + "example": 10 + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "chore(spec)", + "breaking": [] + } + }, + { + "name": "operation-removed", + "rule": "Operations", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + }, + "delete": { + "x-didit-sdk": { + "resource": "things", + "method": "delete", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "job_id": { + "type": "string" + } + }, + "required": [ + "job_id" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "DELETE /things/{id}/: operation removed" + ] + } + }, + { + "name": "operation-no-longer-on-server-sdks", + "rule": "Operations", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "device_token", + "server": false + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /things/{id}/: operation removed" + ] + } + }, + { + "name": "server-false-operation-removed-is-listed", + "rule": "Operations", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + }, + "/device/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "devices", + "method": "retrieve", + "auth": "device_token", + "server": false + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "fields", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "server-false-operation-changes-are-listed", + "rule": "Operations", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/device/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "devices", + "method": "retrieve", + "auth": "device_token", + "server": false + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "fields", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/device/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "devices", + "method": "retrieve", + "auth": "device_token", + "server": false + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "operation-added-is-listed", + "rule": "Operations", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + }, + "delete": { + "x-didit-sdk": { + "resource": "things", + "method": "delete", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "job_id": { + "type": "string" + } + }, + "required": [ + "job_id" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "moved-operation-changes-count-under-its-new-path", + "rule": "Operations", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/items/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /items/{id}/ response name: removed" + ] + } + }, + { + "name": "moved-operation-unchanged-is-listed", + "rule": "Operations", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/items/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "renamed-method-is-listed", + "rule": "Operations", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "get", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "union-variant-added-and-paired-variant-changed", + "rule": "Unions", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/payments/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "payments", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "card" + ] + }, + "last4": { + "type": "string" + } + }, + "required": [ + "kind", + "last4" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "bank" + ] + }, + "iban": { + "type": "string" + } + }, + "required": [ + "kind", + "iban" + ] + } + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/payments/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "payments", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "card" + ] + }, + "last4": { + "type": "string" + } + }, + "required": [ + "kind", + "last4" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "bank" + ] + }, + "iban": { + "type": "integer" + } + }, + "required": [ + "kind", + "iban" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "wallet" + ] + }, + "address": { + "type": "string" + } + }, + "required": [ + "kind", + "address" + ] + } + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /payments/{id}/ response (oneOf 2).iban: type string -> integer" + ] + } + }, + { + "name": "union-variants-reordered-and-paired-variant-changed", + "rule": "Unions", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/payments/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "payments", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "card" + ] + }, + "last4": { + "type": "string" + } + }, + "required": [ + "kind", + "last4" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "bank" + ] + }, + "iban": { + "type": "string" + } + }, + "required": [ + "kind", + "iban" + ] + } + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/payments/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "payments", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "bank" + ] + }, + "iban": { + "type": "integer" + } + }, + "required": [ + "kind", + "iban" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "card" + ] + }, + "last4": { + "type": "string" + } + }, + "required": [ + "kind", + "last4" + ] + } + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /payments/{id}/ response (oneOf 1).iban: type string -> integer" + ] + } + }, + { + "name": "union-untyped-variant-added-and-paired-variant-changed", + "rule": "Unions", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/payments/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "payments", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "card" + ] + }, + "last4": { + "type": "string" + } + }, + "required": [ + "kind", + "last4" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "bank" + ] + }, + "iban": { + "type": "string" + } + }, + "required": [ + "kind", + "iban" + ] + } + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/payments/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "payments", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "card" + ] + }, + "last4": { + "type": "integer" + } + }, + "required": [ + "kind", + "last4" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "bank" + ] + }, + "iban": { + "type": "string" + } + }, + "required": [ + "kind", + "iban" + ] + }, + { + "description": "Other." + } + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /payments/{id}/ response (anyOf 1).last4: type string -> integer", + "GET /payments/{id}/ response: type object -> any" + ] + } + }, + { + "name": "union-variant-removed", + "rule": "Unions", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/payments/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "payments", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "card" + ] + }, + "last4": { + "type": "string" + } + }, + "required": [ + "kind", + "last4" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "bank" + ] + }, + "iban": { + "type": "string" + } + }, + "required": [ + "kind", + "iban" + ] + } + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/payments/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "payments", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "card" + ] + }, + "last4": { + "type": "string" + } + }, + "required": [ + "kind", + "last4" + ] + } + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /payments/{id}/ response: type object|object -> object" + ] + } + }, + { + "name": "schema-becomes-union-compared-with-its-variant", + "rule": "Unions", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/payments/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "payments", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "card" + ] + }, + "last4": { + "type": "string" + } + }, + "required": [ + "kind", + "last4" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/payments/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "payments", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "card" + ] + }, + "last4": { + "type": "integer" + } + }, + "required": [ + "kind", + "last4" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "bank" + ] + }, + "iban": { + "type": "string" + } + }, + "required": [ + "kind", + "iban" + ] + } + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /payments/{id}/ response (oneOf 1).last4: type string -> integer" + ] + } + }, + { + "name": "request-union-variant-field-removed", + "rule": "Unions", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/contacts/": { + "post": { + "x-didit-sdk": { + "resource": "contacts", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "email": { + "type": "string" + } + }, + "required": [ + "email" + ] + }, + { + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "country_code": { + "type": "string" + } + }, + "required": [ + "phone" + ] + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/contacts/": { + "post": { + "x-didit-sdk": { + "resource": "contacts", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "email": { + "type": "string" + } + }, + "required": [ + "email" + ] + }, + { + "type": "object", + "properties": { + "phone": { + "type": "string" + } + }, + "required": [ + "phone" + ] + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "POST /contacts/ request body (anyOf 2).country_code: removed" + ] + } + }, + { + "name": "union-variant-added-is-listed", + "rule": "Unions", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/payments/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "payments", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "card" + ] + }, + "last4": { + "type": "string" + } + }, + "required": [ + "kind", + "last4" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "bank" + ] + }, + "iban": { + "type": "string" + } + }, + "required": [ + "kind", + "iban" + ] + } + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/payments/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "payments", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "card" + ] + }, + "last4": { + "type": "string" + } + }, + "required": [ + "kind", + "last4" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "bank" + ] + }, + "iban": { + "type": "string" + } + }, + "required": [ + "kind", + "iban" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "wallet" + ] + }, + "address": { + "type": "string" + } + }, + "required": [ + "kind", + "address" + ] + } + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "union-variants-reordered-is-listed", + "rule": "Unions", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/payments/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "payments", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "card" + ] + }, + "last4": { + "type": "string" + } + }, + "required": [ + "kind", + "last4" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "bank" + ] + }, + "iban": { + "type": "string" + } + }, + "required": [ + "kind", + "iban" + ] + } + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/payments/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "payments", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "bank" + ] + }, + "iban": { + "type": "string" + } + }, + "required": [ + "kind", + "iban" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "card" + ] + }, + "last4": { + "type": "string" + } + }, + "required": [ + "kind", + "last4" + ] + } + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "union-variant-title-renamed-is-listed", + "rule": "Unions", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/payments/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "payments", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "card" + ] + }, + "last4": { + "type": "string" + } + }, + "required": [ + "kind", + "last4" + ], + "title": "Card payment" + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "bank" + ] + }, + "iban": { + "type": "string" + } + }, + "required": [ + "kind", + "iban" + ] + } + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/payments/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "payments", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "card" + ] + }, + "last4": { + "type": "string" + } + }, + "required": [ + "kind", + "last4" + ], + "title": "Card charge" + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "bank" + ] + }, + "iban": { + "type": "string" + } + }, + "required": [ + "kind", + "iban" + ] + } + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "added-status-without-body", + "rule": "Success statuses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + }, + "204": { + "description": "No content" + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "POST /things/ response: can now be null" + ] + } + }, + { + "name": "removed-status-whose-body-no-other-returns", + "rule": "Success statuses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + }, + "202": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "job_id": { + "type": "string" + } + }, + "required": [ + "job_id" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "POST /things/ response: type object|object -> object" + ] + } + }, + { + "name": "removed-status-whose-body-has-another-type", + "rule": "Success statuses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "202": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "POST /things/ response: type integer|string -> string" + ] + } + }, + { + "name": "only-success-status-changed-compares-the-bodies", + "rule": "Success statuses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "201": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "POST /things/ response name: removed" + ] + } + }, + { + "name": "only-success-status-replaced-by-one-without-body", + "rule": "Success statuses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "204": { + "description": "No content" + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "POST /things/ response: can now be null", + "POST /things/ response: type string -> null" + ] + } + }, + { + "name": "removed-status-leaves-only-statuses-without-body", + "rule": "Success statuses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "204": { + "description": "No content" + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "204": { + "description": "No content" + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "POST /things/ response: type string -> null" + ] + } + }, + { + "name": "status-loses-its-body", + "rule": "Success statuses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "No content" + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "POST /things/ response: can now be null", + "POST /things/ response: type object -> null" + ] + } + }, + { + "name": "added-status-with-another-body-is-listed", + "rule": "Success statuses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + }, + "202": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "job_id": { + "type": "string" + } + }, + "required": [ + "job_id" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "added-status-with-the-same-body-is-listed", + "rule": "Success statuses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + }, + "201": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "removed-status-whose-body-another-returns-is-listed", + "rule": "Success statuses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + }, + "201": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "removed-status-without-body-is-listed", + "rule": "Success statuses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + }, + "204": { + "description": "No content" + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "status-gains-a-body-is-listed", + "rule": "Success statuses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "204": { + "description": "No content" + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "204": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "only-success-status-changed-is-listed", + "rule": "Success statuses", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "201": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [] + } + }, + { + "name": "every-counted-change-is-listed", + "rule": "Release notes", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "field_00": { + "type": "string" + }, + "field_01": { + "type": "string" + }, + "field_02": { + "type": "string" + }, + "field_03": { + "type": "string" + }, + "field_04": { + "type": "string" + }, + "field_05": { + "type": "string" + }, + "field_06": { + "type": "string" + }, + "field_07": { + "type": "string" + }, + "field_08": { + "type": "string" + }, + "field_09": { + "type": "string" + }, + "field_10": { + "type": "string" + }, + "field_11": { + "type": "string" + }, + "field_12": { + "type": "string" + }, + "field_13": { + "type": "string" + }, + "field_14": { + "type": "string" + }, + "field_15": { + "type": "string" + }, + "field_16": { + "type": "string" + }, + "field_17": { + "type": "string" + }, + "field_18": { + "type": "string" + }, + "field_19": { + "type": "string" + }, + "field_20": { + "type": "string" + }, + "field_21": { + "type": "string" + }, + "field_22": { + "type": "string" + }, + "field_23": { + "type": "string" + }, + "field_24": { + "type": "string" + }, + "field_25": { + "type": "string" + }, + "field_26": { + "type": "string" + }, + "field_27": { + "type": "string" + }, + "field_28": { + "type": "string" + }, + "field_29": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "GET /things/{id}/ response field_00: removed", + "GET /things/{id}/ response field_01: removed", + "GET /things/{id}/ response field_02: removed", + "GET /things/{id}/ response field_03: removed", + "GET /things/{id}/ response field_04: removed", + "GET /things/{id}/ response field_05: removed", + "GET /things/{id}/ response field_06: removed", + "GET /things/{id}/ response field_07: removed", + "GET /things/{id}/ response field_08: removed", + "GET /things/{id}/ response field_09: removed", + "GET /things/{id}/ response field_10: removed", + "GET /things/{id}/ response field_11: removed", + "GET /things/{id}/ response field_12: removed", + "GET /things/{id}/ response field_13: removed", + "GET /things/{id}/ response field_14: removed", + "GET /things/{id}/ response field_15: removed", + "GET /things/{id}/ response field_16: removed", + "GET /things/{id}/ response field_17: removed", + "GET /things/{id}/ response field_18: removed", + "GET /things/{id}/ response field_19: removed", + "GET /things/{id}/ response field_20: removed", + "GET /things/{id}/ response field_21: removed", + "GET /things/{id}/ response field_22: removed", + "GET /things/{id}/ response field_23: removed", + "GET /things/{id}/ response field_24: removed", + "GET /things/{id}/ response field_25: removed", + "GET /things/{id}/ response field_26: removed", + "GET /things/{id}/ response field_27: removed", + "GET /things/{id}/ response field_28: removed", + "GET /things/{id}/ response field_29: removed" + ] + } + }, + { + "name": "counted-changes-of-every-operation-are-listed", + "rule": "Release notes", + "before": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "integer" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + }, + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "expand", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + }, + "delete": { + "x-didit-sdk": { + "resource": "things", + "method": "delete", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "job_id": { + "type": "string" + } + }, + "required": [ + "job_id" + ] + } + } + } + } + } + } + } + } + }, + "after": { + "openapi": "3.1.0", + "info": { + "title": "Breaking-change vectors", + "version": "1.0.0" + }, + "paths": { + "/things/": { + "post": { + "x-didit-sdk": { + "resource": "things", + "method": "create", + "auth": "api_key", + "server": true + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "note": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + }, + "/things/{id}/": { + "get": { + "x-didit-sdk": { + "resource": "things", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "closed" + ] + }, + "color": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "status" + ] + } + } + } + } + } + } + } + } + }, + "expect": { + "type": "feat(spec)", + "breaking": [ + "DELETE /things/{id}/: operation removed", + "GET /things/{id}/ query expand: removed", + "GET /things/{id}/ response id: type string -> integer", + "POST /things/ request body amount: removed" + ] + } + } + ] +}