Documentation navigationNavigare documentație
ExamplesExemple · API 0.8.0
contract-versioning
Each example is a fixture directory validated at build time against the released contracts: a JSON contract, a declarative transform, a template, a curl sequence and the expected output. Guides embed these files verbatim so documentation cannot drift from validated fixtures.Fiecare exemplu este un director de fixture-uri validat la build față de contractele publicate: un contract JSON, o transformare declarativă, un șablon, o secvență curl și rezultatul așteptat. Ghidurile includ aceste fișiere textual, astfel încât documentația nu poate devia de fixture-urile validate.
Files in this fixtureFișierele acestui fixture
contract-versioning/contract-v1.json · 377 bytes · sha256 9021dceda111180b…{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Person",
"type": "object",
"additionalProperties": false,
"required": ["first_name", "last_name", "email"],
"properties": {
"first_name": { "type": "string", "minLength": 1 },
"last_name": { "type": "string", "minLength": 1 },
"email": { "type": "string", "format": "email" }
}
}
contract-versioning/contract-v2.json · 505 bytes · sha256 78d18c0ad890c899…{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Person",
"type": "object",
"additionalProperties": false,
"required": ["first_name", "last_name", "email"],
"properties": {
"first_name": { "type": "string", "minLength": 1 },
"middle_name": { "type": "string", "minLength": 1 },
"last_name": { "type": "string", "minLength": 1 },
"email": { "type": "string", "format": "email" },
"phone": { "type": "string", "pattern": "^\\+[1-9][0-9]{6,14}$" }
}
}
contract-versioning/create-contract.request.json · 566 bytes · sha256 4a1971f8d1836231…{
"name": "Person",
"description": "Docs example: person intake contract, first published as version 1.",
"schema": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"title\": \"Person\",\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"required\": [\"first_name\", \"last_name\", \"email\"],\n \"properties\": {\n \"first_name\": { \"type\": \"string\", \"minLength\": 1 },\n \"last_name\": { \"type\": \"string\", \"minLength\": 1 },\n \"email\": { \"type\": \"string\", \"format\": \"email\" }\n }\n}\n"
}
contract-versioning/create-version.request.json · 29 bytes · sha256 c33d261b7a5878fd…{
"expected_revision": 1
}
contract-versioning/curl.sh · 3261 bytes · sha256 4f3553c061a83be5…#!/bin/sh
# Docula example: contract versioning.
# login -> create contract -> version 1 -> publish v1 -> validate v2 draft -> update draft -> version 2 -> publish v2 -> deprecate v1.
# Requires curl and jq. Environment: DOCULA_API, DOCULA_ORIGIN (defaults to DOCULA_API), DOCULA_USERNAME, DOCULA_PASSWORD.
set -eu
API=${DOCULA_API:?set DOCULA_API to the API origin, for example http://127.0.0.1:18081}
ORIGIN=${DOCULA_ORIGIN:-$API}
HERE=$(cd "$(dirname "$0")" && pwd)
JAR=$(mktemp)
trap 'rm -f "$JAR" "$JAR.body"' EXIT
CSRF=$(jq -n --arg u "${DOCULA_USERNAME:?}" --arg p "${DOCULA_PASSWORD:?}" '{username: $u, password: $p}' |
curl --fail --silent --show-error -c "$JAR" -H "Origin: $ORIGIN" -H 'Content-Type: application/json' \
--data-binary @- "$API/api/v1/auth/login" | jq -r '.csrf_token')
api() {
if [ "$#" -ge 3 ]; then
curl --fail --silent --show-error -b "$JAR" -X "$1" -H "Origin: $ORIGIN" -H "X-CSRF-Token: $CSRF" \
-H 'Content-Type: application/json' --data-binary @"$3" "$API$2"
else
curl --fail --silent --show-error -b "$JAR" -X "$1" -H "Origin: $ORIGIN" -H "X-CSRF-Token: $CSRF" "$API$2"
fi
}
# with_revision FILE REVISION: the API rejects stale expected_revision values with 412, so always read the live one.
with_revision() { jq --argjson rev "$2" '.expected_revision = $rev' "$1" > "$JAR.body"; printf '%s' "$JAR.body"; }
# 1. Create the draft (createContract) and freeze + publish version 1.
CONTRACT=$(api POST /api/v1/contracts "$HERE/create-contract.request.json")
CONTRACT_ID=$(printf '%s' "$CONTRACT" | jq -r '.contract.id')
REVISION=$(printf '%s' "$CONTRACT" | jq -r '.contract.draft_revision')
V1=$(api POST "/api/v1/contracts/$CONTRACT_ID/versions" "$(with_revision "$HERE/create-version.request.json" "$REVISION")")
V1_NUMBER=$(printf '%s' "$V1" | jq -r '.version.number')
api POST "/api/v1/contracts/$CONTRACT_ID/versions/$V1_NUMBER/publish" "$HERE/publish.request.json" > /dev/null
# 2. Check the version 2 schema before touching the draft (validateContractDraft is side-effect free).
api POST "/api/v1/contracts/$CONTRACT_ID/validate" "$HERE/validate-draft.request.json" | jq -e '.valid == true' > /dev/null
# 3. Replace the draft (updateContractDraft) with the current draft revision, then freeze + publish version 2.
REVISION=$(api GET "/api/v1/contracts/$CONTRACT_ID" | jq -r '.draft_revision')
UPDATED=$(api PUT "/api/v1/contracts/$CONTRACT_ID" "$(with_revision "$HERE/update-draft.request.json" "$REVISION")")
REVISION=$(printf '%s' "$UPDATED" | jq -r '.contract.draft_revision')
V2=$(api POST "/api/v1/contracts/$CONTRACT_ID/versions" "$(with_revision "$HERE/create-version.request.json" "$REVISION")")
V2_NUMBER=$(printf '%s' "$V2" | jq -r '.version.number')
api POST "/api/v1/contracts/$CONTRACT_ID/versions/$V2_NUMBER/publish" "$HERE/publish.request.json" > /dev/null
# 4. Deprecate version 1 (deprecateContractVersion). Published versions are immutable: deprecation only closes them to new releases.
api POST "/api/v1/contracts/$CONTRACT_ID/versions/$V1_NUMBER/deprecate" "$HERE/deprecate.request.json" | jq -e '.version.status == "deprecated"' > /dev/null
api GET "/api/v1/contracts/$CONTRACT_ID" | jq '{id, draft_revision, versions: [.versions[] | {number, status, content_hash}]}'
contract-versioning/deprecate.request.json · 3 bytes · sha256 ca3d163bab055381…{}
contract-versioning/fixture.json · 2643 bytes · sha256 31e849fea361d7f8…{
"schema": "docula.example-fixture/v1",
"guide": "/developers/guides/contracts-and-versioning",
"slug": "contract-versioning",
"title": "Contracts and versioning",
"summary": "Two schema revisions of the same contract, the request bodies for every contract operation the API-only workbench used (create, validate, update draft, version, publish, deprecate), and the curl sequence that moves the contract from draft to a deprecated version 1 and a published version 2.",
"operations": ["login", "createContract", "getContract", "validateContractDraft", "updateContractDraft", "createContractVersion", "publishContractVersion", "deprecateContractVersion"],
"related": ["declarative-transform", "operational-tasks-api"],
"files": [
{ "path": "contract-v1.json", "purpose": "Version 1: three required person fields.", "validate": { "kind": "json-schema-2020-12" } },
{ "path": "contract-v2.json", "purpose": "Version 2: adds optional middle_name and phone; every v1 input still validates.", "validate": { "kind": "json-schema-2020-12" } },
{ "path": "create-contract.request.json", "purpose": "createContract body; schema embeds contract-v1.json.", "validate": { "kind": "openapi", "schema": "#/components/schemas/CreateContractRequest" }, "embeds": [{ "field": "schema", "file": "contract-v1.json" }] },
{ "path": "validate-draft.request.json", "purpose": "validateContractDraft body; schema embeds contract-v2.json.", "validate": { "kind": "openapi", "schema": "#/components/schemas/ValidateContractRequest" }, "embeds": [{ "field": "schema", "file": "contract-v2.json" }] },
{ "path": "update-draft.request.json", "purpose": "updateContractDraft body; schema embeds contract-v2.json and carries the expected draft revision.", "validate": { "kind": "openapi", "schema": "#/components/schemas/UpdateContractRequest" }, "embeds": [{ "field": "schema", "file": "contract-v2.json" }] },
{ "path": "create-version.request.json", "purpose": "createContractVersion body with the expected draft revision.", "validate": { "kind": "openapi", "schema": "#/components/schemas/RevisionRequest" } },
{ "path": "publish.request.json", "purpose": "publishContractVersion body: an empty JSON object.", "validate": { "kind": "openapi", "schema": "#/components/schemas/EmptyObject" } },
{ "path": "deprecate.request.json", "purpose": "deprecateContractVersion body: an empty JSON object.", "validate": { "kind": "openapi", "schema": "#/components/schemas/EmptyObject" } },
{ "path": "curl.sh", "purpose": "Create, version, publish, revise, publish again and deprecate.", "validate": { "kind": "shell" } }
]
}
contract-versioning/publish.request.json · 3 bytes · sha256 ca3d163bab055381…{}
contract-versioning/update-draft.request.json · 769 bytes · sha256 f8eaf406b09478fe…{
"name": "Person",
"description": "Docs example: version 2 adds optional middle_name and phone without breaking version 1 inputs.",
"schema": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"title\": \"Person\",\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"required\": [\"first_name\", \"last_name\", \"email\"],\n \"properties\": {\n \"first_name\": { \"type\": \"string\", \"minLength\": 1 },\n \"middle_name\": { \"type\": \"string\", \"minLength\": 1 },\n \"last_name\": { \"type\": \"string\", \"minLength\": 1 },\n \"email\": { \"type\": \"string\", \"format\": \"email\" },\n \"phone\": { \"type\": \"string\", \"pattern\": \"^\\\\+[1-9][0-9]{6,14}$\" }\n }\n}\n",
"expected_revision": 1
}
contract-versioning/validate-draft.request.json · 608 bytes · sha256 6b3ecfb10efd7230…{
"schema": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"title\": \"Person\",\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"required\": [\"first_name\", \"last_name\", \"email\"],\n \"properties\": {\n \"first_name\": { \"type\": \"string\", \"minLength\": 1 },\n \"middle_name\": { \"type\": \"string\", \"minLength\": 1 },\n \"last_name\": { \"type\": \"string\", \"minLength\": 1 },\n \"email\": { \"type\": \"string\", \"format\": \"email\" },\n \"phone\": { \"type\": \"string\", \"pattern\": \"^\\\\+[1-9][0-9]{6,14}$\" }\n }\n}\n"
}
