feat: generate canonical workflow contract
This commit is contained in:
@@ -1,3 +1,11 @@
|
||||
from .generate import generate_manifest
|
||||
from .io import (
|
||||
DEFAULT_MANIFEST_PATH,
|
||||
ManifestDriftError,
|
||||
canonical_manifest_json,
|
||||
check_manifest,
|
||||
write_manifest,
|
||||
)
|
||||
from .model import ContractManifest, JsonSchema, JsonValue, ManifestError
|
||||
from .normalize import manifest_from_openrpc
|
||||
|
||||
@@ -7,4 +15,10 @@ __all__ = [
|
||||
"JsonValue",
|
||||
"ManifestError",
|
||||
"manifest_from_openrpc",
|
||||
"generate_manifest",
|
||||
"DEFAULT_MANIFEST_PATH",
|
||||
"ManifestDriftError",
|
||||
"canonical_manifest_json",
|
||||
"check_manifest",
|
||||
"write_manifest",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import cast
|
||||
|
||||
from wf_server import build_local_static_workflow_server
|
||||
from wf_transport_rpc_http import create_rpc_app
|
||||
|
||||
from .model import ContractManifest
|
||||
from .normalize import manifest_from_openrpc
|
||||
|
||||
|
||||
def generate_manifest() -> ContractManifest:
|
||||
"""Compose the real server against an isolated store and normalize OpenRPC."""
|
||||
with TemporaryDirectory(prefix="wf-contract-manifest-") as directory:
|
||||
server = build_local_static_workflow_server(Path(directory) / "store")
|
||||
document = cast(dict[str, object], create_rpc_app(server).get_openrpc())
|
||||
# Normalization deliberately drops framework metadata that could carry
|
||||
# process-local paths or transport details.
|
||||
return manifest_from_openrpc(document)
|
||||
@@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from .model import ContractManifest
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
|
||||
DEFAULT_MANIFEST_PATH = REPOSITORY_ROOT / "contracts" / "workflow-api.manifest.json"
|
||||
|
||||
|
||||
class ManifestDriftError(RuntimeError):
|
||||
"""Indicate that the checked manifest differs from the generated contract."""
|
||||
|
||||
|
||||
def canonical_manifest_json(manifest: ContractManifest) -> str:
|
||||
try:
|
||||
return json.dumps(manifest, ensure_ascii=False, indent=2) + "\n"
|
||||
except (TypeError, ValueError) as error:
|
||||
raise ValueError(f"manifest is not canonically serializable: {error}") from error
|
||||
|
||||
|
||||
def write_manifest(
|
||||
manifest: ContractManifest, path: Path = DEFAULT_MANIFEST_PATH
|
||||
) -> Path:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(canonical_manifest_json(manifest), encoding="utf-8", newline="\n")
|
||||
return path
|
||||
|
||||
|
||||
def check_manifest(
|
||||
manifest: ContractManifest, path: Path = DEFAULT_MANIFEST_PATH
|
||||
) -> None:
|
||||
expected = canonical_manifest_json(manifest).encode("utf-8")
|
||||
try:
|
||||
actual = path.read_bytes()
|
||||
except FileNotFoundError as error:
|
||||
raise ManifestDriftError(
|
||||
f"{path} is missing; run `python -m wf_contract_manifest write`"
|
||||
) from error
|
||||
if actual != expected:
|
||||
raise ManifestDriftError(
|
||||
f"{path} is stale; run `python -m wf_contract_manifest write`"
|
||||
)
|
||||
Reference in New Issue
Block a user