Code Generation Pipeline
Edit on GitHubMulti-target code emission from verified intermediate representation
Last updated:
Research and design document for Stage 5 of the spec-to-REST compiler. Covers complete file manifests, template architecture, database migration generation, OpenAPI emission, Dafny integration, infrastructure code, quality assurance, incremental regeneration, extensibility, and comparison with existing generators.
Status. The shipped codegen lives in
modules/codegen/and emits thepython-fastapi-postgrestarget via Handlebars templates, see the live Python + FastAPI + PostgreSQL target reference. The Dafny integration (Phase 6, #27–32) is not yet implemented; the generated services raiseNotImplementedErrorfor non-trivial operation bodies until that work lands. Multi-target emission (Go/chi #33, TS/Express #35) is Phase 7, also not yet onmain. Python-shaped code samples below are design sketches; the implementation language is Scala 3 (see Architecture).
Table of contents
- What Gets Generated (Complete File Manifest)
- Template Architecture
- Database Migration Generation
- OpenAPI Spec Generation
- Dafny Output Integration
- Infrastructure Code Generation
- Quality Assurance of Generated Code
- Incremental Regeneration
- Extensibility
- Comparison with Existing Generators
1. What gets generated
The code generation pipeline receives four inputs:
- IR from the spec parser (entities, state, operations, invariants)
- Convention engine outputs (HTTP method mapping, DB schema, validation rules)
- Verified Dafny code for non-trivial operations (compiled to target language)
- Test specifications derived from the spec (properties, state machines, conformance config)
It produces a complete, runnable project: not stubs or skeletons, but working code you can
docker-compose up and start using immediately.
The following subsections show the full file manifest and complete generated code for the URL shortener spec from the comprehensive analysis (Section 7.2 of document 00).
1.1 Python-fastapi-postgres
Directory structure
url_shortener/
pyproject.toml
[project]
name = "url-shortener"
version = "0.1.0"
description = "URL Shortener service — generated by spec-to-rest compiler"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.115.0,<1.0.0",
"uvicorn[standard]>=0.30.0,<1.0.0",
"sqlalchemy>=2.0.0,<3.0.0",
"alembic>=1.13.0,<2.0.0",
"psycopg[binary]>=3.2.0,<4.0.0",
"pydantic>=2.9.0,<3.0.0",
"pydantic-settings>=2.5.0,<3.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.3.0,<9.0.0",
"pytest-asyncio>=0.24.0,<1.0.0",
"httpx>=0.27.0,<1.0.0",
"hypothesis>=6.112.0,<7.0.0",
"schemathesis~=4.15",
"ruff>=0.7.0,<1.0.0",
"mypy>=1.11.0,<2.0.0",
]
[build-system]
requires = ["setuptools>=75.0"]
build-backend = "setuptools.build_meta"
[tool.ruff]
target-version = "py311"
line-length = 99
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP", "B", "SIM", "RUF"]
[tool.mypy]
python_version = "3.11"
strict = true
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]Dockerfile
# --- Stage 1: build dependencies ---
FROM python:3.11-slim AS builder
WORKDIR /build
COPY pyproject.toml ./
RUN pip install --no-cache-dir --prefix=/install .
# --- Stage 2: runtime ---
FROM python:3.11-slim
RUN adduser --disabled-password --gecos "" appuser
WORKDIR /app
COPY --from=builder /install /usr/local
COPY alembic.ini ./
COPY alembic/ ./alembic/
COPY app/ ./app/
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]docker-compose.yml
services:
app:
build: .
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql+psycopg://shortener:shortener@db:5432/shortener
BASE_URL: http://localhost:8000
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:17-alpine
environment:
POSTGRES_USER: shortener
POSTGRES_PASSWORD: shortener
POSTGRES_DB: shortener
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U shortener"]
interval: 5s
timeout: 3s
retries: 10
migrate:
build: .
command: ["python", "-m", "alembic", "upgrade", "head"]
environment:
DATABASE_URL: postgresql+psycopg://shortener:shortener@db:5432/shortener
depends_on:
db:
condition: service_healthy
volumes:
pgdata:.env.example
DATABASE_URL=postgresql+psycopg://shortener:shortener@localhost:5432/shortener
BASE_URL=http://localhost:8000
LOG_LEVEL=infoalembic.ini
[alembic]
script_location = alembic
sqlalchemy.url = %(DATABASE_URL)s
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%SMakefile
.PHONY: help run test lint typecheck migrate docker-up docker-down
help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
run: ## Run the development server
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
test: ## Run all tests
pytest tests/ -v --tb=short
lint: ## Lint with ruff
ruff check app/ tests/
ruff format --check app/ tests/
typecheck: ## Type-check with mypy
mypy app/
migrate: ## Run database migrations
alembic upgrade head
docker-up: ## Start all services with docker-compose
docker compose up --build -d
docker-down: ## Stop all services
docker compose down -valembic/Alembic DB migrations
env.py
import os
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
from app.models.url_shortener import Base
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_metadata = Base.metadata
database_url = os.environ.get("DATABASE_URL")
if database_url:
config.set_main_option("sqlalchemy.url", database_url)
def run_migrations_offline() -> None:
url = config.get_main_option("sqlalchemy.url")
context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()versions/one file per spec change
001_initial_schema.py
"""Initial schema for UrlShortener service.
Generated by spec-to-rest compiler from the service specification.
Revision ID: 001
Create Date: 2026-04-05
"""
from alembic import op
import sqlalchemy as sa
revision = "001"
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"short_codes",
sa.Column("code", sa.String(length=10), nullable=False),
sa.Column("url", sa.Text(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(),
nullable=False),
sa.PrimaryKeyConstraint("code"),
sa.CheckConstraint("length(code) >= 6 AND length(code) <= 10", name="ck_code_length"),
sa.CheckConstraint("code ~ '^[a-zA-Z0-9]+$'", name="ck_code_pattern"),
sa.CheckConstraint("url ~ '^https?://'", name="ck_url_valid_uri"),
)
op.create_index("ix_short_codes_created_at", "short_codes", ["created_at"])
def downgrade() -> None:
op.drop_index("ix_short_codes_created_at", table_name="short_codes")
op.drop_table("short_codes")app/FastAPI service code
main.py
"""FastAPI application for UrlShortener service.
Generated by spec-to-rest compiler. Do not edit directly — modify the spec and
regenerate. See the 'extensions/' directory for safe customization hooks.
"""
import logging
from contextlib import asynccontextmanager
from collections.abc import AsyncIterator
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from app.config import settings
from app.database import engine
from app.routers import url_shortener
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
logger.info("Starting UrlShortener service")
yield
await engine.dispose()
logger.info("Shutting down UrlShortener service")
app = FastAPI(
title="UrlShortener",
version="0.1.0",
description="URL Shortener service — generated from formal specification",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(url_shortener.router)
@app.get("/health", tags=["infrastructure"])
async def health_check() -> dict[str, str]:
return {"status": "ok"}
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception) -> JSONResponse:
logger.exception("Unhandled exception on %s %s", request.method, request.url.path)
return JSONResponse(status_code=500, content={"detail": "Internal server error"})config.py
"""Application configuration loaded from environment variables.
Generated by spec-to-rest compiler.
"""
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str = "postgresql+psycopg://shortener:shortener@localhost:5432/shortener"
base_url: str = "http://localhost:8000"
log_level: str = "info"
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
settings = Settings()database.py
"""SQLAlchemy async engine and session factory.
Generated by spec-to-rest compiler.
"""
from collections.abc import AsyncIterator
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from app.config import settings
engine = create_async_engine(
settings.database_url,
echo=False,
pool_size=10,
max_overflow=20,
pool_pre_ping=True,
)
async_session_factory = async_sessionmaker(engine, expire_on_commit=False)
async def get_session() -> AsyncIterator[AsyncSession]:
async with async_session_factory() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raisemodels/SQLAlchemy ORM (one per entity)
__init__.py
from app.models.url_shortener import Base, StoreEntry
__all__ = ["Base", "StoreEntry"]url_shortener.py
"""SQLAlchemy ORM models for UrlShortener.
Generated from spec state declarations:
state {
store: ShortCode -> lone LongURL
created_at: ShortCode -> DateTime
}
"""
from datetime import datetime
from sqlalchemy import CheckConstraint, DateTime, String, Text, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class StoreEntry(Base):
"""Represents one entry in the `store` relation: ShortCode -> lone LongURL.
Invariants enforced at the database level:
- len(code) >= 6 and len(code) <= 10
- code matches [a-zA-Z0-9]+
- url is a valid URI (basic prefix check)
"""
__tablename__ = "store"
code: Mapped[str] = mapped_column(
String(10), primary_key=True, nullable=False
)
url: Mapped[str] = mapped_column(
Text, nullable=False
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
__table_args__ = (
CheckConstraint("length(code) >= 6 AND length(code) <= 10", name="ck_code_length"),
CheckConstraint("code ~ '^[a-zA-Z0-9]+$'", name="ck_code_pattern"),
CheckConstraint("url ~ '^https?://'", name="ck_url_valid_uri"),
)
def __repr__(self) -> str:
return f"StoreEntry(code={self.code!r}, url={self.url!r})"schemas/Pydantic Read / Create / Update DTOs
__init__.py
from app.schemas.url_shortener import (
ErrorResponse,
ShortenRequest,
ShortenResponse,
ResolveResponse,
)
__all__ = ["ErrorResponse", "ShortenRequest", "ShortenResponse", "ResolveResponse"]url_shortener.py
"""Pydantic schemas for UrlShortener request/response models.
Generated from spec entity and operation declarations.
"""
import re
from datetime import datetime
from pydantic import BaseModel, Field, field_validator
class ShortenRequest(BaseModel):
"""Input schema for the Shorten operation.
Derived from:
operation Shorten { input: url: LongURL }
requires: isValidURI(url.value)
"""
url: str = Field(
...,
description="The long URL to shorten",
examples=["https://example.com/very/long/path?query=1"],
)
@field_validator("url")
@classmethod
def validate_url(cls, v: str) -> str:
if not re.match(r"^https?://\S+$", v):
msg = "Must be a valid HTTP or HTTPS URL"
raise ValueError(msg)
return v
class ShortenResponse(BaseModel):
"""Output schema for the Shorten operation.
Derived from:
operation Shorten { output: code: ShortCode, short_url: String }
"""
code: str = Field(
...,
min_length=6,
max_length=10,
pattern=r"^[a-zA-Z0-9]+$",
description="The generated short code",
)
short_url: str = Field(
...,
description="The full short URL (base_url + / + code)",
)
created_at: datetime = Field(
...,
description="Timestamp when the short URL was created",
)
class ResolveResponse(BaseModel):
"""Output schema for the Resolve operation.
Derived from:
operation Resolve { output: url: LongURL }
"""
url: str = Field(..., description="The original long URL")
class ErrorResponse(BaseModel):
"""Standard error response body."""
detail: str = Field(..., description="Human-readable error description")routers/HTTP route handlers
url_shortener.py
"""FastAPI route handlers for UrlShortener.
Generated from spec operations via the convention engine:
Shorten (mutates state, has input) -> POST /shorten
Resolve (reads state, no mutation) -> GET /{code} (302 redirect per convention override)
Delete (removes from state) -> DELETE /{code}
Convention overrides applied:
Resolve.http_status_success = 302
Resolve.http_header "Location" = output.url
"""
from fastapi import APIRouter, Depends, HTTPException, Response
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_session
from app.schemas.url_shortener import (
ErrorResponse,
ResolveResponse,
ShortenRequest,
ShortenResponse,
)
from app.services.url_shortener import UrlShortenerService
router = APIRouter(tags=["url_shortener"])
@router.post(
"/shorten",
response_model=ShortenResponse,
status_code=201,
responses={422: {"model": ErrorResponse, "description": "Validation error"}},
summary="Shorten a URL",
description="Creates a new short code mapping to the provided long URL.",
)
async def shorten(
body: ShortenRequest,
session: AsyncSession = Depends(get_session),
) -> ShortenResponse:
service = UrlShortenerService(session)
return await service.shorten(body)
@router.get(
"/{code}",
status_code=302,
responses={
302: {"description": "Redirect to the long URL"},
404: {"model": ErrorResponse, "description": "Short code not found"},
},
summary="Resolve a short code",
description="Looks up the short code and redirects to the original URL.",
)
async def resolve(
code: str,
session: AsyncSession = Depends(get_session),
) -> Response:
service = UrlShortenerService(session)
result = await service.resolve(code)
if result is None:
raise HTTPException(status_code=404, detail=f"Short code '{code}' not found")
return Response(status_code=302, headers={"Location": result.url})
@router.delete(
"/{code}",
status_code=204,
responses={404: {"model": ErrorResponse, "description": "Short code not found"}},
summary="Delete a short code",
description="Removes the short code mapping from the store.",
)
async def delete(
code: str,
session: AsyncSession = Depends(get_session),
) -> Response:
service = UrlShortenerService(session)
deleted = await service.delete(code)
if not deleted:
raise HTTPException(status_code=404, detail=f"Short code '{code}' not found")
return Response(status_code=204)services/business logic invoked by routers
url_shortener.py
"""Business logic service for UrlShortener.
Generated by spec-to-rest compiler.
For CRUD operations, the convention engine emits the implementation directly.
For non-trivial logic (e.g., short code generation), the Dafny-verified output
is integrated here. The generate_short_code function below comes from the
verified Dafny compilation; the rest is convention-engine boilerplate.
"""
import secrets
import string
from datetime import datetime, timezone
from sqlalchemy import select, delete as sa_delete
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.models.url_shortener import StoreEntry
from app.schemas.url_shortener import (
ResolveResponse,
ShortenRequest,
ShortenResponse,
)
from app.validators.url_shortener import validate_short_code, validate_long_url
# ---------------------------------------------------------------------------
# Dafny-verified short code generation
# ---------------------------------------------------------------------------
# The function below is the adapter wrapping Dafny-compiled output.
# Dafny ensures:
# - len(result) >= 6 and len(result) <= 10
# - result matches [a-zA-Z0-9]+
# - result is distinct from all codes in the provided existing_codes set
#
# The Dafny source guarantees these postconditions are met for every call.
# See Section 5 (Dafny Output Integration) for the full compilation pipeline.
# ---------------------------------------------------------------------------
_ALPHABET = string.ascii_letters + string.digits # [a-zA-Z0-9]
def generate_short_code(existing_codes: set[str], length: int = 8) -> str:
"""Generate a fresh short code not present in existing_codes.
Verified postconditions (from Dafny):
ensures len(result) >= 6 and len(result) <= 10
ensures result matches [a-zA-Z0-9]+
ensures result not in existing_codes
"""
if length < 6 or length > 10:
length = 8
for _ in range(1000):
candidate = "".join(secrets.choice(_ALPHABET) for _ in range(length))
if candidate not in existing_codes:
return candidate
# Fallback: extend length within invariant bounds
for fallback_len in range(6, 11):
candidate = "".join(secrets.choice(_ALPHABET) for _ in range(fallback_len))
if candidate not in existing_codes:
return candidate
raise RuntimeError("Failed to generate a unique short code after exhaustive attempts")
class UrlShortenerService:
"""Service layer implementing UrlShortener operations.
Each method corresponds to a spec operation and enforces the spec's
requires/ensures contract.
"""
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def shorten(self, request: ShortenRequest) -> ShortenResponse:
"""Implements operation Shorten.
requires: isValidURI(url.value)
ensures:
code not in pre(store)
store'[code] = url
short_url = base_url + "/" + code.value
#store' = #store + 1
"""
validate_long_url(request.url)
# Gather existing codes to satisfy the Dafny-verified uniqueness precondition
result = await self._session.execute(select(StoreEntry.code))
existing_codes = {row[0] for row in result.fetchall()}
code = generate_short_code(existing_codes)
now = datetime.now(timezone.utc)
entry = StoreEntry(code=code, url=request.url, created_at=now)
self._session.add(entry)
await self._session.flush()
short_url = f"{settings.base_url}/{code}"
return ShortenResponse(code=code, short_url=short_url, created_at=now)
async def resolve(self, code: str) -> ResolveResponse | None:
"""Implements operation Resolve.
requires: code in store
ensures:
url = store[code]
store' = store (state unchanged)
"""
validate_short_code(code)
result = await self._session.execute(
select(StoreEntry).where(StoreEntry.code == code)
)
entry = result.scalar_one_or_none()
if entry is None:
return None
return ResolveResponse(url=entry.url)
async def delete(self, code: str) -> bool:
"""Implements operation Delete.
requires: code in store
ensures:
code not in store'
#store' = #store - 1
"""
validate_short_code(code)
result = await self._session.execute(
sa_delete(StoreEntry).where(StoreEntry.code == code)
)
return result.rowcount > 0 # type: ignore[return-value]validators/checks lifted from `requires` clauses
url_shortener.py
"""Validators derived from spec requires clauses and entity invariants.
Generated by spec-to-rest compiler.
Source constraints:
entity ShortCode {
invariant: len(value) >= 6 and len(value) <= 10
invariant: value matches /^[a-zA-Z0-9]+$/
}
entity LongURL {
invariant: isValidURI(value)
}
"""
import re
from fastapi import HTTPException
_SHORT_CODE_PATTERN = re.compile(r"^[a-zA-Z0-9]+$")
_URL_PATTERN = re.compile(r"^https?://\S+$")
def validate_short_code(value: str) -> None:
"""Validate a ShortCode value against entity invariants.
Raises HTTPException 422 if invalid.
"""
if len(value) < 6 or len(value) > 10:
raise HTTPException(
status_code=422,
detail=f"Short code must be 6-10 characters, got {len(value)}",
)
if not _SHORT_CODE_PATTERN.match(value):
raise HTTPException(
status_code=422,
detail="Short code must contain only alphanumeric characters [a-zA-Z0-9]",
)
def validate_long_url(value: str) -> None:
"""Validate a LongURL value against entity invariants.
Raises HTTPException 422 if invalid.
"""
if not _URL_PATTERN.match(value):
raise HTTPException(
status_code=422,
detail="URL must be a valid HTTP or HTTPS URI",
)tests/
conftest.py
"""Pytest fixtures for UrlShortener test suite.
Generated by spec-to-rest compiler.
"""
import asyncio
from collections.abc import AsyncIterator
import pytest
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from app.main import app
from app.database import get_session
from app.models.url_shortener import Base
TEST_DATABASE_URL = "sqlite+aiosqlite:///test.db"
test_engine = create_async_engine(TEST_DATABASE_URL, echo=False)
test_session_factory = async_sessionmaker(test_engine, expire_on_commit=False)
@pytest.fixture(autouse=True)
async def setup_database():
async with test_engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
async with test_engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
async def override_get_session() -> AsyncIterator[AsyncSession]:
async with test_session_factory() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
app.dependency_overrides[get_session] = override_get_session
@pytest.fixture
async def client() -> AsyncIterator[AsyncClient]:
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield actest_api.py
"""API integration tests derived from spec operations.
Generated by spec-to-rest compiler.
Each test corresponds to a spec operation and validates the requires/ensures contract
at the HTTP level.
"""
import pytest
from httpx import AsyncClient
class TestShortenOperation:
"""Tests for POST /shorten (operation Shorten)."""
async def test_shorten_valid_url(self, client: AsyncClient) -> None:
"""ensures: code not in pre(store), store'[code] = url, #store' = #store + 1"""
response = await client.post("/shorten", json={"url": "https://example.com/long"})
assert response.status_code == 201
data = response.json()
assert "code" in data
assert "short_url" in data
assert len(data["code"]) >= 6
assert len(data["code"]) <= 10
assert data["short_url"].endswith(data["code"])
async def test_shorten_invalid_url_returns_422(self, client: AsyncClient) -> None:
"""requires: isValidURI(url.value) -> 422 on violation"""
response = await client.post("/shorten", json={"url": "not-a-valid-url"})
assert response.status_code == 422
async def test_shorten_missing_url_returns_422(self, client: AsyncClient) -> None:
response = await client.post("/shorten", json={})
assert response.status_code == 422
async def test_shorten_produces_unique_codes(self, client: AsyncClient) -> None:
"""ensures: code not in pre(store) — each code is fresh"""
codes = set()
for _ in range(20):
resp = await client.post("/shorten", json={"url": "https://example.com/"})
assert resp.status_code == 201
codes.add(resp.json()["code"])
assert len(codes) == 20
class TestResolveOperation:
"""Tests for GET /{code} (operation Resolve)."""
async def test_resolve_existing_code_redirects(self, client: AsyncClient) -> None:
"""ensures: url = store[code], convention override: 302 + Location header"""
create = await client.post("/shorten", json={"url": "https://example.com/target"})
code = create.json()["code"]
response = await client.get(f"/{code}", follow_redirects=False)
assert response.status_code == 302
assert response.headers["location"] == "https://example.com/target"
async def test_resolve_nonexistent_code_returns_404(self, client: AsyncClient) -> None:
"""requires: code in store -> 404 when violated"""
response = await client.get("/aBcDeFgH", follow_redirects=False)
assert response.status_code == 404
async def test_resolve_does_not_mutate_state(self, client: AsyncClient) -> None:
"""ensures: store' = store (state unchanged)"""
create = await client.post("/shorten", json={"url": "https://example.com/stable"})
code = create.json()["code"]
await client.get(f"/{code}", follow_redirects=False)
await client.get(f"/{code}", follow_redirects=False)
response = await client.get(f"/{code}", follow_redirects=False)
assert response.status_code == 302
assert response.headers["location"] == "https://example.com/stable"
class TestDeleteOperation:
"""Tests for DELETE /{code} (operation Delete)."""
async def test_delete_existing_code(self, client: AsyncClient) -> None:
"""ensures: code not in store', #store' = #store - 1"""
create = await client.post("/shorten", json={"url": "https://example.com/gone"})
code = create.json()["code"]
response = await client.delete(f"/{code}")
assert response.status_code == 204
# Verify removal
resolve = await client.get(f"/{code}", follow_redirects=False)
assert resolve.status_code == 404
async def test_delete_nonexistent_code_returns_404(self, client: AsyncClient) -> None:
"""requires: code in store -> 404 when violated"""
response = await client.delete("/aBcDeFgH")
assert response.status_code == 404
class TestHealthCheck:
"""Infrastructure endpoint test."""
async def test_health(self, client: AsyncClient) -> None:
response = await client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}test_properties.py
"""Property-based tests derived from spec ensures clauses.
Generated by spec-to-rest compiler.
Uses Hypothesis to check that postconditions hold for arbitrary valid inputs.
"""
import re
import string
from hypothesis import given, settings, strategies as st
from app.services.url_shortener import generate_short_code
alphanumeric = st.text(alphabet=string.ascii_letters + string.digits, min_size=6, max_size=10)
class TestShortCodeGeneration:
"""Properties of generate_short_code — from Dafny verified postconditions."""
@given(
existing=st.frozensets(alphanumeric, max_size=50),
length=st.integers(min_value=6, max_value=10),
)
@settings(max_examples=200)
def test_code_length_within_bounds(self, existing: frozenset[str], length: int) -> None:
"""ensures: len(result) >= 6 and len(result) <= 10"""
code = generate_short_code(set(existing), length)
assert 6 <= len(code) <= 10
@given(
existing=st.frozensets(alphanumeric, max_size=50),
length=st.integers(min_value=6, max_value=10),
)
@settings(max_examples=200)
def test_code_matches_pattern(self, existing: frozenset[str], length: int) -> None:
"""ensures: result matches [a-zA-Z0-9]+"""
code = generate_short_code(set(existing), length)
assert re.fullmatch(r"[a-zA-Z0-9]+", code)
@given(
existing=st.frozensets(alphanumeric, max_size=50),
length=st.integers(min_value=6, max_value=10),
)
@settings(max_examples=200)
def test_code_is_fresh(self, existing: frozenset[str], length: int) -> None:
"""ensures: result not in existing_codes"""
code = generate_short_code(set(existing), length)
assert code not in existing
class TestStoreInvariants:
"""Global invariants from the spec:
invariant: all c in store | isValidURI(store[c].value)
invariant: all c in store | c in created_at
These are enforced at the DB level via CHECK constraints and NOT NULL columns.
This test class validates the invariant logic in isolation.
"""
@given(url=st.text(min_size=1, max_size=200))
def test_valid_uri_invariant(self, url: str) -> None:
"""invariant: all c in store | isValidURI(store[c].value)
Any URL stored must match ^https?://
"""
is_valid = bool(re.match(r"^https?://\S+$", url))
# This invariant means invalid URLs should never pass validation
if not is_valid:
from fastapi import HTTPException
import pytest
from app.validators.url_shortener import validate_long_url
with pytest.raises(HTTPException) as exc_info:
validate_long_url(url)
assert exc_info.value.status_code == 422test_conformance.py
"""Schemathesis conformance test configuration.
Generated by spec-to-rest compiler.
Run with:
schemathesis run openapi.yaml --base-url http://localhost:8000 --stateful=links
Or use this file as a pytest-based Schemathesis runner:
pytest tests/test_conformance.py -v
"""
import schemathesis
schema = schemathesis.from_path(
"openapi.yaml",
base_url="http://localhost:8000",
)
@schema.parametrize()
def test_api_conformance(case):
"""Every endpoint must conform to the OpenAPI schema.
Schemathesis generates random valid inputs and checks:
- Response status codes match declared responses
- Response bodies match declared schemas
- No 500 errors for valid inputs
- Content-Type headers are correct
"""
case.call_and_validate()openapi.yaml
(Full generated spec shown in Section 4 below.)
1.2 Go-chi-postgres
Directory structure
url_shortener/
go.mod
module github.com/generated/url-shortener
go 1.22
require (
github.com/go-chi/chi/v5 v5.1.0
github.com/jackc/pgx/v5 v5.7.1
github.com/golang-migrate/migrate/v4 v4.18.1
github.com/caarlos0/env/v11 v11.2.0
github.com/stretchr/testify v1.9.0
pgregory.net/rapid v1.1.0
)cmd/Go-idiom entry-point binaries
server/
main.go
// Package main is the entry point for the UrlShortener service.
//
// Generated by spec-to-rest compiler.
package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/generated/url-shortener/internal/config"
"github.com/generated/url-shortener/internal/database"
"github.com/generated/url-shortener/internal/handlers"
"github.com/generated/url-shortener/internal/services"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
slog.SetDefault(logger)
cfg, err := config.Load()
if err != nil {
slog.Error("failed to load config", "error", err)
os.Exit(1)
}
pool, err := database.Connect(context.Background(), cfg.DatabaseURL)
if err != nil {
slog.Error("failed to connect to database", "error", err)
os.Exit(1)
}
defer pool.Close()
svc := services.NewUrlShortenerService(pool, cfg.BaseURL)
h := handlers.NewUrlShortenerHandler(svc)
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(30 * time.Second))
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok"}`))
})
r.Post("/shorten", h.Shorten)
r.Get("/{code}", h.Resolve)
r.Delete("/{code}", h.Delete)
server := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.Port),
Handler: r,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
done := make(chan os.Signal, 1)
signal.Notify(done, os.Interrupt, syscall.SIGTERM)
go func() {
slog.Info("starting server", "port", cfg.Port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("server error", "error", err)
os.Exit(1)
}
}()
<-done
slog.Info("shutting down")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = server.Shutdown(ctx)
}internal/private packages (un-importable externally)
config/
config.go
// Package config loads application configuration from environment variables.
//
// Generated by spec-to-rest compiler.
package config
import "github.com/caarlos0/env/v11"
type Config struct {
DatabaseURL string `env:"DATABASE_URL" envDefault:"postgres://shortener:shortener@localhost:5432/shortener?sslmode=disable"`
BaseURL string `env:"BASE_URL" envDefault:"http://localhost:8080"`
Port int `env:"PORT" envDefault:"8080"`
LogLevel string `env:"LOG_LEVEL" envDefault:"info"`
}
func Load() (*Config, error) {
cfg := &Config{}
if err := env.Parse(cfg); err != nil {
return nil, err
}
return cfg, nil
}database/
models/struct definitions (one per entity)
url_shortener.go
// Package models defines data types for UrlShortener.
//
// Generated from spec entity and state declarations.
package models
import "time"
// StoreEntry represents one row of the `store` relation: ShortCode -> lone LongURL.
type StoreEntry struct {
Code string `json:"code" db:"code"`
URL string `json:"url" db:"url"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
// ShortenRequest is the input for operation Shorten.
type ShortenRequest struct {
URL string `json:"url"`
}
// ShortenResponse is the output for operation Shorten.
type ShortenResponse struct {
Code string `json:"code"`
ShortURL string `json:"short_url"`
CreatedAt time.Time `json:"created_at"`
}
// ErrorResponse is the standard error body.
type ErrorResponse struct {
Detail string `json:"detail"`
}handlers/chi route handlers
url_shortener.go
// Package handlers contains HTTP route handlers for UrlShortener.
//
// Generated from spec operations via the convention engine.
package handlers
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/generated/url-shortener/internal/models"
"github.com/generated/url-shortener/internal/services"
"github.com/generated/url-shortener/internal/validators"
)
type UrlShortenerHandler struct {
svc *services.UrlShortenerService
}
func NewUrlShortenerHandler(svc *services.UrlShortenerService) *UrlShortenerHandler {
return &UrlShortenerHandler{svc: svc}
}
func (h *UrlShortenerHandler) Shorten(w http.ResponseWriter, r *http.Request) {
var req models.ShortenRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Invalid JSON body")
return
}
if err := validators.ValidateLongURL(req.URL); err != nil {
writeError(w, http.StatusUnprocessableEntity, err.Error())
return
}
resp, err := h.svc.Shorten(r.Context(), req)
if err != nil {
writeError(w, http.StatusInternalServerError, "Failed to shorten URL")
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(resp)
}
func (h *UrlShortenerHandler) Resolve(w http.ResponseWriter, r *http.Request) {
code := chi.URLParam(r, "code")
if err := validators.ValidateShortCode(code); err != nil {
writeError(w, http.StatusUnprocessableEntity, err.Error())
return
}
entry, err := h.svc.Resolve(r.Context(), code)
if err != nil {
writeError(w, http.StatusInternalServerError, "Failed to resolve code")
return
}
if entry == nil {
writeError(w, http.StatusNotFound, "Short code not found")
return
}
// Convention override: Resolve.http_status_success = 302
http.Redirect(w, r, entry.URL, http.StatusFound)
}
func (h *UrlShortenerHandler) Delete(w http.ResponseWriter, r *http.Request) {
code := chi.URLParam(r, "code")
if err := validators.ValidateShortCode(code); err != nil {
writeError(w, http.StatusUnprocessableEntity, err.Error())
return
}
deleted, err := h.svc.Delete(r.Context(), code)
if err != nil {
writeError(w, http.StatusInternalServerError, "Failed to delete code")
return
}
if !deleted {
writeError(w, http.StatusNotFound, "Short code not found")
return
}
w.WriteHeader(http.StatusNoContent)
}
func writeError(w http.ResponseWriter, status int, detail string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(models.ErrorResponse{Detail: detail})
}services/business logic invoked by handlers
url_shortener.go
// Package services contains business logic for UrlShortener.
//
// Generated by spec-to-rest compiler.
package services
import (
"context"
"crypto/rand"
"errors"
"fmt"
"math/big"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/generated/url-shortener/internal/models"
)
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
type UrlShortenerService struct {
pool *pgxpool.Pool
baseURL string
}
func NewUrlShortenerService(pool *pgxpool.Pool, baseURL string) *UrlShortenerService {
return &UrlShortenerService{pool: pool, baseURL: baseURL}
}
// generateShortCode produces a fresh alphanumeric code of the given length.
// Dafny-verified postconditions:
// - len(result) >= 6 && len(result) <= 10
// - result matches [a-zA-Z0-9]+
// - result is not in the current store
func generateShortCode(length int) (string, error) {
if length < 6 || length > 10 {
length = 8
}
b := make([]byte, length)
for i := range b {
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphabet))))
if err != nil {
return "", fmt.Errorf("crypto/rand failed: %w", err)
}
b[i] = alphabet[n.Int64()]
}
return string(b), nil
}
func (s *UrlShortenerService) Shorten(ctx context.Context, req models.ShortenRequest) (*models.ShortenResponse, error) {
// Retry loop to guarantee code uniqueness (Dafny postcondition: code not in pre(store))
for attempts := 0; attempts < 10; attempts++ {
code, err := generateShortCode(8)
if err != nil {
return nil, err
}
now := time.Now().UTC()
tag, err := s.pool.Exec(ctx,
`INSERT INTO store (code, url, created_at) VALUES ($1, $2, $3) ON CONFLICT (code) DO NOTHING`,
code, req.URL, now,
)
if err != nil {
return nil, fmt.Errorf("insert failed: %w", err)
}
if tag.RowsAffected() == 1 {
return &models.ShortenResponse{
Code: code,
ShortURL: fmt.Sprintf("%s/%s", s.baseURL, code),
CreatedAt: now,
}, nil
}
// Conflict: code already exists, retry with new code
}
return nil, fmt.Errorf("failed to generate unique short code after 10 attempts")
}
func (s *UrlShortenerService) Resolve(ctx context.Context, code string) (*models.StoreEntry, error) {
row := s.pool.QueryRow(ctx, `SELECT code, url, created_at FROM store WHERE code = $1`, code)
var entry models.StoreEntry
err := row.Scan(&entry.Code, &entry.URL, &entry.CreatedAt)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, fmt.Errorf("query failed: %w", err)
}
return &entry, nil
}
func (s *UrlShortenerService) Delete(ctx context.Context, code string) (bool, error) {
tag, err := s.pool.Exec(ctx, `DELETE FROM store WHERE code = $1`, code)
if err != nil {
return false, fmt.Errorf("delete failed: %w", err)
}
return tag.RowsAffected() > 0, nil
}validators/checks lifted from `requires` clauses
migrations/raw SQL up/down migrations
001_initial_schema.up.sql
CREATE TABLE store (
code VARCHAR(10) NOT NULL PRIMARY KEY,
url TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT ck_code_length CHECK (length(code) >= 6 AND length(code) <= 10),
CONSTRAINT ck_code_pattern CHECK (code ~ '^[a-zA-Z0-9]+$'),
CONSTRAINT ck_url_valid CHECK (url ~ '^https?://')
);
CREATE INDEX ix_store_created_at ON store (created_at);001_initial_schema.down.sql
DROP TABLE IF EXISTS store;tests/
1.3 Typescript-express-prisma
Directory structure
url_shortener/
package.json
{
"name": "url-shortener",
"version": "0.1.0",
"description": "URL Shortener service — generated by spec-to-rest compiler",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts",
"test": "vitest run",
"lint": "eslint src/ tests/",
"format": "prettier --check src/ tests/",
"migrate": "prisma migrate deploy",
"generate": "prisma generate"
},
"dependencies": {
"express": "^4.21.0",
"@prisma/client": "^5.20.0",
"zod": "^3.23.0",
"dotenv": "^16.4.0"
},
"devDependencies": {
"typescript": "^5.6.0",
"tsx": "^4.19.0",
"@types/express": "^5.0.0",
"@types/node": "^22.0.0",
"prisma": "^5.20.0",
"vitest": "^2.1.0",
"supertest": "^7.0.0",
"@types/supertest": "^6.0.0",
"eslint": "^9.12.0",
"prettier": "^3.3.0",
"fast-check": "^3.22.0"
}
}prisma/Prisma schema + generated client
schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
/// Represents the `store` relation: ShortCode -> lone LongURL
/// Generated from spec state declaration.
model Store {
code String @id @db.VarChar(10)
url String @db.Text
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
@@map("store")
}src/Express service code
index.ts
import { createApp } from "./app";
import { config } from "./config";
async function main(): Promise<void> {
const app = createApp();
app.listen(config.port, () => {
console.log(`UrlShortener service listening on port ${config.port}`);
});
process.on("SIGTERM", () => {
console.log("SIGTERM received, shutting down");
process.exit(0);
});
}
main().catch((err) => {
console.error("Failed to start server:", err);
process.exit(1);
});config.ts
import "dotenv/config";
export const config = {
databaseUrl:
process.env.DATABASE_URL ?? "postgresql://shortener:shortener@localhost:5432/shortener",
baseUrl: process.env.BASE_URL ?? "http://localhost:3000",
port: parseInt(process.env.PORT ?? "3000", 10),
} as const;app.ts
import express, { type Express, type Request, type Response, type NextFunction } from "express";
import { urlShortenerRouter } from "./routes/urlShortener";
export function createApp(): Express {
const app = express();
app.use(express.json());
app.get("/health", (_req: Request, res: Response) => {
res.json({ status: "ok" });
});
app.use("/", urlShortenerRouter);
// Global error handler
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error("Unhandled error:", err);
res.status(500).json({ detail: "Internal server error" });
});
return app;
}routes/Express route handlers
urlShortener.ts
/** Express route handlers for UrlShortener.
*
* Generated from spec operations via the convention engine.
*/
import { Router, type Request, type Response } from "express";
import { ShortenRequestSchema, ShortCodeSchema } from "../validators/urlShortener";
import * as service from "../services/urlShortener";
export const urlShortenerRouter = Router();
// POST /shorten — operation Shorten (mutates state, has input)
urlShortenerRouter.post("/shorten", async (req: Request, res: Response) => {
const parsed = ShortenRequestSchema.safeParse(req.body);
if (!parsed.success) {
res.status(422).json({ detail: parsed.error.issues[0].message });
return;
}
try {
const result = await service.shorten(parsed.data);
res.status(201).json(result);
} catch (err) {
console.error("Shorten error:", err);
res.status(500).json({ detail: "Failed to shorten URL" });
}
});
// GET /:code — operation Resolve (reads state, no mutation; convention override: 302)
urlShortenerRouter.get("/:code", async (req: Request, res: Response) => {
const parsed = ShortCodeSchema.safeParse(req.params.code);
if (!parsed.success) {
res.status(422).json({ detail: parsed.error.issues[0].message });
return;
}
try {
const result = await service.resolve(parsed.data);
if (!result) {
res.status(404).json({ detail: "Short code not found" });
return;
}
// Convention override: Resolve.http_status_success = 302
res.redirect(302, result.url);
} catch (err) {
console.error("Resolve error:", err);
res.status(500).json({ detail: "Failed to resolve code" });
}
});
// DELETE /:code — operation Delete (removes from state)
urlShortenerRouter.delete("/:code", async (req: Request, res: Response) => {
const parsed = ShortCodeSchema.safeParse(req.params.code);
if (!parsed.success) {
res.status(422).json({ detail: parsed.error.issues[0].message });
return;
}
try {
const deleted = await service.deleteCode(parsed.data);
if (!deleted) {
res.status(404).json({ detail: "Short code not found" });
return;
}
res.status(204).end();
} catch (err) {
console.error("Delete error:", err);
res.status(500).json({ detail: "Failed to delete code" });
}
});services/business logic invoked by routes
urlShortener.ts
/** Business logic service for UrlShortener.
*
* Generated by spec-to-rest compiler.
*/
import { PrismaClient } from "@prisma/client";
import crypto from "node:crypto";
import { config } from "../config";
import type { ShortenRequest, ShortenResponse, ResolveResponse } from "../types/urlShortener";
const ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const prisma = new PrismaClient();
/**
* Generate a fresh short code.
*
* Dafny-verified postconditions:
* - length >= 6 && length <= 10
* - matches /^[a-zA-Z0-9]+$/
*/
function generateShortCode(length: number = 8): string {
const clampedLen = Math.max(6, Math.min(10, length));
const bytes = crypto.randomBytes(clampedLen);
let result = "";
for (let i = 0; i < clampedLen; i++) {
result += ALPHABET[bytes[i] % ALPHABET.length];
}
return result;
}
export async function shorten(req: ShortenRequest): Promise<ShortenResponse> {
for (let attempt = 0; attempt < 10; attempt++) {
const code = generateShortCode(8);
try {
const entry = await prisma.store.create({
data: { code, url: req.url },
});
return {
code: entry.code,
short_url: `${config.baseUrl}/${entry.code}`,
created_at: entry.createdAt.toISOString(),
};
} catch (err: unknown) {
// Unique constraint violation — retry with new code
if (isPrismaUniqueViolation(err)) continue;
throw err;
}
}
throw new Error("Failed to generate unique short code after 10 attempts");
}
export async function resolve(code: string): Promise<ResolveResponse | null> {
const entry = await prisma.store.findUnique({ where: { code } });
if (!entry) return null;
return { url: entry.url };
}
export async function deleteCode(code: string): Promise<boolean> {
try {
await prisma.store.delete({ where: { code } });
return true;
} catch (err: unknown) {
if (isPrismaNotFound(err)) return false;
throw err;
}
}
function isPrismaUniqueViolation(err: unknown): boolean {
return (err as { code?: string })?.code === "P2002";
}
function isPrismaNotFound(err: unknown): boolean {
return (err as { code?: string })?.code === "P2025";
}validators/checks lifted from `requires` clauses
urlShortener.ts
/** Validators derived from spec requires clauses and entity invariants.
*
* Generated by spec-to-rest compiler.
*/
import { z } from "zod";
export const ShortCodeSchema = z
.string()
.min(6, "Short code must be at least 6 characters")
.max(10, "Short code must be at most 10 characters")
.regex(/^[a-zA-Z0-9]+$/, "Short code must be alphanumeric");
export const LongURLSchema = z
.string()
.regex(/^https?:\/\/\S+$/, "Must be a valid HTTP or HTTPS URL");
export const ShortenRequestSchema = z.object({
url: LongURLSchema,
});types/TypeScript interfaces (one per entity)
urlShortener.ts
/** Request/response types for UrlShortener.
*
* Generated from spec entity and operation declarations.
*/
export interface ShortenRequest {
url: string;
}
export interface ShortenResponse {
code: string;
short_url: string;
created_at: string;
}
export interface ResolveResponse {
url: string;
}
export interface ErrorResponse {
detail: string;
}tests/
2. Template architecture
2.1 Template engine selection
Shipped reality. The
python-fastapi-postgrestarget ships with Handlebars templates, not Jinja2. This section captures the original research-time recommendation; the implementation chose Handlebars because the codegen module is JVM/Scala-resident and Handlebars-Java integrates directly without a Python sidecar. The trade-off discussion below still applies if a future target wants to revisit the choice.
We use a hybrid approach: Jinja2 as the primary template engine with a thin orchestration layer written in Python.
Why Jinja2
| Criterion | Jinja2 | Go templates | StringTemplate | Custom |
|---|---|---|---|---|
| Familiarity | High (Python dev audience) | Moderate | Low | None |
| Expressiveness | Rich (filters, macros, inheritance) | Limited | Moderate | Unlimited |
| Whitespace control | Excellent ({%- -%}) | Adequate | Good | Manual |
| Template inheritance | Built-in (extends/block) | No | No | Custom |
| Library ecosystem | Huge | Moderate | Small | N/A |
| Performance | Adequate (one-shot generation) | Fast | Fast | Varies |
Code generation is a batch process that runs once and produces files on disk. Template rendering speed is irrelevant, a URL shortener spec generates ~25 files in under 100ms regardless of engine. Expressiveness and developer familiarity dominate the choice.
2.2 IR-to-template variable mapping
The IR produced by the spec parser is a Scala 3 ADT (sealed abstract classes + final case
classes) extracted from Isabelle by Code_Target_Scala. The Handlebars engine receives a
ProfiledService view that wraps the IR with the convention-engine annotations. The shipped
types live in
modules/ir/src/main/scala/specrest/ir/generated/SpecRestGenerated.scala
(extracted; do not hand-edit) and modules/profile/.../Types.scala. The conceptual shape
(extracted Scala uses positional letters a/b/c/… instead of English field names, see
IR.thy
for the source-of-truth definitions):
final case class ServiceIR(
name: String, // "UrlShortener"
entities: List[EntityDecl], // UrlMapping
enums: List[EnumDecl],
typeAliases: List[TypeAliasDecl], // ShortCode, LongURL, BaseURL
state: Option[StateDecl], // store, metadata, base_url
operations: List[OperationDecl], // Shorten, Resolve, Delete, ListAll
invariants: List[InvariantDecl], // global invariants
temporals: List[TemporalDecl],
facts: List[FactDecl],
functions: List[FunctionDecl],
predicates: List[PredicateDecl],
transitions: List[TransitionDecl],
conventions: Option[ConventionsDecl] // user-specified overrides
)
final case class OperationDecl(
name: String, // "Shorten"
inputs: List[ParamDecl] = Nil, // url: LongURL
outputs: List[ParamDecl] = Nil, // code: ShortCode, short_url: String
requires: List[Expr] = Nil, // isValidURI(url)
ensures: List[Expr] = Nil, // code not in pre(store), ...
span: Option[Span] = None
)
// HTTP-shape annotation produced by the convention engine and added by
// modules/profile/.../Annotate.buildProfiledService before templates render.
final case class EndpointSpec(
operationName: String, // "Shorten"
method: HttpMethod, // POST
path: String, // "/shorten"
pathParams: List[ParamSpec],
queryParams: List[ParamSpec],
bodyParams: List[ParamSpec],
successStatus: Int // 201
)The convention engine annotates each OperationIR with HTTP mapping decisions before templates are
rendered. Templates then access these annotations directly:
@router.{{ op.http_method | lower }}(
"{{ op.http_path }}",
status_code={{ op.http_success_status }},
)
async def {{ op.name | snake_case }}(
{%- for input in op.inputs %}
{{ input.name }}: {{ input | python_type }},
{%- endfor %}
session: AsyncSession = Depends(get_session),
) -> {{ op | python_response_type }}:2.3 Convention engine feed-in
The convention engine makes decisions that templates consume:
| Convention Decision | Template Variable | Example Value |
|---|---|---|
| HTTP method | op.http_method | "POST" |
| URL path | op.http_path | "/shorten" |
| Success status code | op.http_success_status | 201 |
| Error mappings | op.http_error_responses | [(422, "Validation error")] |
| DB table name | state.table_name | "store" |
| Column types | field.db_type | "VARCHAR(10)" |
| Index decisions | state.indexes | [("created_at", "btree")] |
| Constraint SQL | entity.check_constraints | ["length(code) >= 6"] |
Convention decisions are made once, stored on the IR, and consumed by every target's templates. This means adding a new convention (e.g., "all list endpoints support cursor pagination") only requires changing the convention engine, all targets inherit the new behavior.
2.4 Dafny output splicing
When the LLM synthesis engine produces verified Dafny code for an operation, it is compiled to the target language and spliced into the service layer:
Dafny source -> dafny build --target py -> Python module
|
Template renders -> service layer template -> imports Dafny module
|
Adapter wraps -> convert Dafny types -> native Python typesThe template detects whether an operation has Dafny output available:
{% if op.dafny_module %}
from {{ op.dafny_module }} import {{ op.dafny_function }} as _dafny_{{ op.name | snake_case }}
{% endif %}
class {{ service.name }}Service:
async def {{ op.name | snake_case }}(self, ...) -> ...:
{% if op.dafny_module %}
# Verified implementation from Dafny
result = _dafny_{{ op.name | snake_case }}({{ op.inputs | dafny_args }})
return {{ op | adapt_dafny_result("result") }}
{% else %}
# Convention-generated CRUD implementation
{{ op | crud_implementation }}
{% endif %}2.5 Cross-cutting concerns
Cross-cutting concerns (logging, error handling, auth, rate limiting) are handled via template composition rather than inheritance:
templates/
_base/
python-fastapi/
go-chi/
typescript-express/
Each target's main template includes the relevant cross-cutting fragments. The base fragments are
parameterized to work across targets, they receive the same IR context but produce
language-specific output.
2.6 Template organization and quality
Structure
templates/
_shared/Language-agnostic partials
python-fastapi-postgres/
go-chi-postgres/
typescript-express-prisma/
manifest.json drives the generation process:
{
"target": "python-fastapi-postgres",
"files": [
{ "template": "pyproject.toml.j2", "output": "pyproject.toml", "per": "service" },
{ "template": "dockerfile.j2", "output": "Dockerfile", "per": "service" },
{ "template": "main.py.j2", "output": "app/main.py", "per": "service" },
{ "template": "config.py.j2", "output": "app/config.py", "per": "service" },
{ "template": "database.py.j2", "output": "app/database.py", "per": "service" },
{ "template": "model.py.j2", "output": "app/models/{{ svc }}.py", "per": "service" },
{ "template": "schema.py.j2", "output": "app/schemas/{{ svc }}.py", "per": "service" },
{ "template": "router.py.j2", "output": "app/routers/{{ svc }}.py", "per": "service" },
{ "template": "service.py.j2", "output": "app/services/{{ svc }}.py", "per": "service" },
{ "template": "validator.py.j2", "output": "app/validators/{{ svc }}.py", "per": "service" },
{
"template": "migration.py.j2",
"output": "alembic/versions/001_initial_schema.py",
"per": "service"
},
{ "template": "test_api.py.j2", "output": "tests/test_api.py", "per": "service" },
{ "template": "test_properties.py.j2", "output": "tests/test_properties.py", "per": "service" }
]
}Preventing template rot
-
Template tests: Each template has a golden-file test. The test renders the template with the URL shortener IR and compares the output to the checked-in golden file. Any change to a template that alters output triggers a diff review.
-
Compilation gate: After rendering, the generated project is compiled/type-checked as a CI step. For Python:
ruff check + mypy. For Go:go build + go vet. For TypeScript:tsc --noEmit. Templates that produce code with lint errors fail CI. -
Integration gate: The generated project is started in Docker, and the generated tests are run against it. Templates that produce code with test failures fail CI.
3. Database migration generation
3.1 From spec state declarations to DDL
The spec's state block is the single source of truth for the database schema. The convention
engine translates each state relation into DDL.
Mapping rules
| Spec Construct | DDL Output |
|---|---|
store: ShortCode -> lone LongURL | Table with FK or inline columns. lone = nullable FK / nullable column. Here, since LongURL is a value entity, it becomes an inline column. |
ShortCode (left side of ->) | Primary key column |
LongURL (right side with lone) | NOT NULL column (lone means at most one, and since this is the value of the mapping, presence is guaranteed) |
created_at: ShortCode -> DateTime | Additional column on the same table (same key domain) |
invariant: len(value) >= 6 | CHECK (length(code) >= 6) |
invariant: value matches /^[a-zA-Z0-9]+$/ | CHECK (code ~ '^[a-zA-Z0-9]+$') |
invariant: isValidURI(value) | CHECK (url ~ '^https?://') |
all c in store | c in created_at | NOT NULL on created_at + default |
Column type mapping
| Spec Type | PostgreSQL | SQLite (test) | MySQL |
|---|---|---|---|
String (unbounded) | TEXT | TEXT | TEXT |
String (with len constraint) | VARCHAR(max_len) | TEXT | VARCHAR(max_len) |
Int | INTEGER | INTEGER | INT |
DateTime | TIMESTAMPTZ | TEXT | DATETIME |
Bool | BOOLEAN | INTEGER | TINYINT(1) |
Decimal | NUMERIC(p,s) | REAL | DECIMAL(p,s) |
| Entity reference | FK column (_id INTEGER REFERENCES ...) | Same | Same |
Implementation note (#58). The Python target emits SQLAlchemy generic types, not raw DDL, so the concrete column type is the dialect's interpretation of the generic type. Two deliberate deviations from the conceptual table above:
- Unbounded
Stringon MySQL maps toVARCHAR(255), notTEXT. MySQL cannot index/PRIMARY KEY/UNIQUE/foreign-key aTEXTcolumn without a prefix length (error 1170), and the schema indexes FK and identifier columns.VARCHAR(255)keeps every generated column indexable.DateTimeusessa.DateTime()(no timezone) on SQLite and MySQL, andJSONBcollapses to the genericsa.JSON()(Postgres keepspostgresql.JSONB()). SQLite enforces foreign keys via aPRAGMA foreign_keys=ONconnection listener (runtime engine + Alembic env).Partial indexes use
postgresql_where(Postgres) /sqlite_where(SQLite); MySQL has no equivalent, so the filter is dropped, a plain index is emitted, and the CLI prints a per-dialect downgrade warning.
3.2 Index generation
Primary keys come from the left side of state relations (the key domain). Additional indexes are inferred from:
-
Query patterns in operations: If
Resolvelooks up bycode, andcodeis already the PK, no additional index is needed. If a list operation filters bycreated_at, a btree index is added. -
Foreign key columns: Always indexed (PostgreSQL does not auto-index FK targets).
-
Unique constraints from invariants:
invariant: unique(email)produces a unique index.
3.3 Migration versioning strategy
We use sequential numbering (001, 002, 003...) rather than timestamps because:
- The compiler controls all migrations; there is no risk of developer-caused ordering conflicts.
- Sequential numbers are easier to read and reason about.
- The format matches Alembic (Python), golang-migrate (Go), and Prisma (TS) defaults.
3.4 Diff migrations for spec changes
When the spec changes, the compiler compares the old IR and new IR:
old_ir.state vs new_ir.state -> diffDiff categories and their DDL
| Change | DDL |
|---|---|
| New state relation | CREATE TABLE |
| Removed state relation | DROP TABLE (with confirmation prompt) |
| New field on existing relation | ALTER TABLE ADD COLUMN |
| Removed field | ALTER TABLE DROP COLUMN (with confirmation prompt) |
| Changed type | ALTER TABLE ALTER COLUMN TYPE |
| New invariant (constraint) | ALTER TABLE ADD CONSTRAINT |
| Removed invariant | ALTER TABLE DROP CONSTRAINT |
| New entity relation | CREATE TABLE (junction table if many-to-many) |
3.5 Complete generated SQL: URL shortener (simple)
-- Migration 001: Initial schema for UrlShortener
-- Generated by spec-to-rest compiler
BEGIN;
CREATE TABLE store (
code VARCHAR(10) NOT NULL,
url TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- Primary key from state relation: store: ShortCode -> lone LongURL
CONSTRAINT pk_store PRIMARY KEY (code),
-- From entity ShortCode invariants
CONSTRAINT ck_code_length CHECK (length(code) >= 6 AND length(code) <= 10),
CONSTRAINT ck_code_pattern CHECK (code ~ '^[a-zA-Z0-9]+$'),
-- From entity LongURL invariants
CONSTRAINT ck_url_valid_uri CHECK (url ~ '^https?://'),
-- From global invariant: all c in store | c in created_at
-- (enforced by NOT NULL + DEFAULT on created_at column constraint)
);
-- Index for temporal queries (list by creation time)
CREATE INDEX ix_store_created_at ON store (created_at);
COMMIT;3.6 Complete generated SQL: E-commerce order service (complex)
For a spec with entities Customer, Product, Order, OrderItem, Address, and state relations
modeling a full order lifecycle:
-- Migration 001: Initial schema for OrderService
-- Generated by spec-to-rest compiler
BEGIN;
-- ============================================================
-- Table: customers
-- From state: customers: CustomerId -> lone CustomerProfile
-- ============================================================
CREATE TABLE customers (
id UUID NOT NULL DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL,
name VARCHAR(200) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT pk_customers PRIMARY KEY (id),
CONSTRAINT uq_customers_email UNIQUE (email),
CONSTRAINT ck_email_format CHECK (email ~ '^[^@]+@[^@]+\.[^@]+$'),
CONSTRAINT ck_name_nonempty CHECK (length(trim(name)) > 0)
);
CREATE INDEX ix_customers_email ON customers (email);
-- ============================================================
-- Table: products
-- From state: catalog: ProductId -> lone ProductInfo
-- ============================================================
CREATE TABLE products (
id UUID NOT NULL DEFAULT gen_random_uuid(),
sku VARCHAR(50) NOT NULL,
name VARCHAR(300) NOT NULL,
description TEXT,
price_cents INTEGER NOT NULL,
stock INTEGER NOT NULL DEFAULT 0,
active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT pk_products PRIMARY KEY (id),
CONSTRAINT uq_products_sku UNIQUE (sku),
CONSTRAINT ck_price_positive CHECK (price_cents > 0),
CONSTRAINT ck_stock_nonneg CHECK (stock >= 0),
CONSTRAINT ck_sku_nonempty CHECK (length(trim(sku)) > 0)
);
CREATE INDEX ix_products_sku ON products (sku);
CREATE INDEX ix_products_active ON products (active) WHERE active = true;
-- ============================================================
-- Table: addresses
-- From state: addresses: AddressId -> lone AddressInfo
-- ============================================================
CREATE TABLE addresses (
id UUID NOT NULL DEFAULT gen_random_uuid(),
customer_id UUID NOT NULL,
line1 VARCHAR(300) NOT NULL,
line2 VARCHAR(300),
city VARCHAR(100) NOT NULL,
state VARCHAR(100),
postal_code VARCHAR(20) NOT NULL,
country VARCHAR(2) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT pk_addresses PRIMARY KEY (id),
CONSTRAINT fk_addresses_customer FOREIGN KEY (customer_id)
REFERENCES customers (id) ON DELETE CASCADE,
CONSTRAINT ck_country_iso CHECK (length(country) = 2)
);
CREATE INDEX ix_addresses_customer ON addresses (customer_id);
-- ============================================================
-- Table: orders
-- From state: orders: OrderId -> lone OrderInfo
-- State machine column from operation lifecycle:
-- PlaceOrder -> FulfillOrder -> ShipOrder -> DeliverOrder
-- CancelOrder can fire from pending/confirmed/shipped
-- ============================================================
CREATE TABLE orders (
id UUID NOT NULL DEFAULT gen_random_uuid(),
customer_id UUID NOT NULL,
shipping_addr_id UUID NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
total_cents INTEGER NOT NULL,
placed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
confirmed_at TIMESTAMPTZ,
shipped_at TIMESTAMPTZ,
delivered_at TIMESTAMPTZ,
cancelled_at TIMESTAMPTZ,
CONSTRAINT pk_orders PRIMARY KEY (id),
CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id)
REFERENCES customers (id) ON DELETE RESTRICT,
CONSTRAINT fk_orders_address FOREIGN KEY (shipping_addr_id)
REFERENCES addresses (id) ON DELETE RESTRICT,
CONSTRAINT ck_order_status CHECK (
status IN ('pending', 'confirmed', 'shipped', 'delivered', 'cancelled')
),
CONSTRAINT ck_total_nonneg CHECK (total_cents >= 0)
);
CREATE INDEX ix_orders_customer ON orders (customer_id);
CREATE INDEX ix_orders_status ON orders (status);
CREATE INDEX ix_orders_placed_at ON orders (placed_at);
-- ============================================================
-- Table: order_items (junction table)
-- From state: order_items: OrderId -> set OrderItemInfo
-- (one-to-many: each order has multiple items)
-- ============================================================
CREATE TABLE order_items (
id UUID NOT NULL DEFAULT gen_random_uuid(),
order_id UUID NOT NULL,
product_id UUID NOT NULL,
quantity INTEGER NOT NULL,
unit_price_cents INTEGER NOT NULL,
CONSTRAINT pk_order_items PRIMARY KEY (id),
CONSTRAINT fk_items_order FOREIGN KEY (order_id)
REFERENCES orders (id) ON DELETE CASCADE,
CONSTRAINT fk_items_product FOREIGN KEY (product_id)
REFERENCES products (id) ON DELETE RESTRICT,
CONSTRAINT ck_quantity_positive CHECK (quantity > 0),
CONSTRAINT ck_unit_price_positive CHECK (unit_price_cents > 0)
);
CREATE INDEX ix_order_items_order ON order_items (order_id);
CREATE INDEX ix_order_items_product ON order_items (product_id);
-- ============================================================
-- Trigger: update orders.updated_at equivalent (total recalc)
-- From invariant: order.total = sum(item.quantity * item.unit_price)
-- ============================================================
CREATE OR REPLACE FUNCTION recalc_order_total()
RETURNS TRIGGER AS $$
BEGIN
UPDATE orders
SET total_cents = (
SELECT COALESCE(SUM(quantity * unit_price_cents), 0)
FROM order_items
WHERE order_id = COALESCE(NEW.order_id, OLD.order_id)
)
WHERE id = COALESCE(NEW.order_id, OLD.order_id);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_recalc_order_total
AFTER INSERT OR UPDATE OR DELETE ON order_items
FOR EACH ROW EXECUTE FUNCTION recalc_order_total();
COMMIT;4. OpenAPI spec generation
The compiler generates a complete OpenAPI 3.1 specification from the IR. This serves dual purposes: documentation and conformance testing (Schemathesis).
4.1 Mapping rules
| IR Element | OpenAPI Element |
|---|---|
| Service name | info.title |
Operation with http_method + http_path | paths.{path}.{method} |
| Operation input fields | requestBody schema (POST/PUT/PATCH) or parameters (GET/DELETE) |
| Operation output fields | responses.{status}.content.schema |
| Entity invariants | Schema pattern, minLength, maxLength, minimum, maximum |
requires clause violations | Error response entries (404, 422) |
| Convention overrides | Header definitions, redirect responses |
4.2 Complete generated OpenAPI for URL shortener
openapi: "3.1.0"
info:
title: UrlShortener
version: 0.1.0
description: >
URL Shortener service. Generated from formal specification by the spec-to-rest compiler. All
endpoints enforce the behavioral contracts defined in the specification.
contact:
name: Generated Service
license:
name: MIT
servers:
- url: http://localhost:8000
description: Local development
paths:
/shorten:
post:
operationId: shorten
summary: Shorten a URL
description: >
Creates a new short code mapping to the provided long URL.
Spec contract:
requires: isValidURI(url.value)
ensures: code not in pre(store), store'[code] = url,
short_url = base_url + "/" + code.value, #store' = #store + 1
tags:
- url_shortener
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ShortenRequest"
examples:
basic:
summary: Shorten a URL
value:
url: "https://example.com/very/long/path?query=1¶m=2"
https:
summary: HTTPS URL
value:
url: "https://docs.example.org/api/v2/reference"
responses:
"201":
description: Short URL created successfully
content:
application/json:
schema:
$ref: "#/components/schemas/ShortenResponse"
examples:
created:
summary: Newly created short URL
value:
code: "aBcDeFgH"
short_url: "http://localhost:8000/aBcDeFgH"
created_at: "2026-04-05T12:00:00Z"
"422":
description: >
Validation error. The requires clause `isValidURI(url.value)` was violated.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
invalid_url:
summary: Invalid URL format
value:
detail: "Must be a valid HTTP or HTTPS URL"
/{code}:
get:
operationId: resolve
summary: Resolve a short code
description: >
Looks up the short code and redirects (302) to the original URL.
Spec contract:
requires: code in store
ensures: url = store[code], store' = store
Convention override: http_status_success = 302, Location header set.
tags:
- url_shortener
parameters:
- name: code
in: path
required: true
description: The short code to resolve
schema:
type: string
minLength: 6
maxLength: 10
pattern: "^[a-zA-Z0-9]+$"
examples:
valid_code:
summary: A valid short code
value: "aBcDeFgH"
responses:
"302":
description: Redirect to the original long URL
headers:
Location:
description: The original long URL
schema:
type: string
format: uri
example: "https://example.com/very/long/path?query=1"
"404":
description: >
Short code not found. The requires clause `code in store` was violated.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
not_found:
summary: Code does not exist
value:
detail: "Short code 'xYzAbCdE' not found"
"422":
description: Short code format invalid
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
bad_format:
summary: Code too short
value:
detail: "Short code must be 6-10 characters, got 3"
delete:
operationId: delete
summary: Delete a short code
description: >
Removes the short code mapping from the store.
Spec contract:
requires: code in store
ensures: code not in store', #store' = #store - 1
tags:
- url_shortener
parameters:
- name: code
in: path
required: true
description: The short code to delete
schema:
type: string
minLength: 6
maxLength: 10
pattern: "^[a-zA-Z0-9]+$"
responses:
"204":
description: Short code deleted successfully (no body)
"404":
description: >
Short code not found. The requires clause `code in store` was violated.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
not_found:
value:
detail: "Short code 'xYzAbCdE' not found"
/health:
get:
operationId: health
summary: Health check
description: Infrastructure endpoint. Returns 200 if the service is running.
tags:
- infrastructure
responses:
"200":
description: Service is healthy
content:
application/json:
schema:
type: object
properties:
status:
type: string
enum: ["ok"]
required:
- status
components:
schemas:
ShortenRequest:
type: object
description: >
Input for the Shorten operation. Derived from: operation Shorten { input: url: LongURL }
required:
- url
properties:
url:
type: string
format: uri
description: The long URL to shorten
pattern: "^https?://\\S+$"
example: "https://example.com/very/long/path"
ShortenResponse:
type: object
description: >
Output for the Shorten operation. Derived from: operation Shorten { output: code: ShortCode,
short_url: String }
required:
- code
- short_url
- created_at
properties:
code:
type: string
minLength: 6
maxLength: 10
pattern: "^[a-zA-Z0-9]+$"
description: The generated short code
short_url:
type: string
format: uri
description: The full short URL
created_at:
type: string
format: date-time
description: When the short URL was created
ErrorResponse:
type: object
description: Standard error response body
required:
- detail
properties:
detail:
type: string
description: Human-readable error description
tags:
- name: url_shortener
description: URL shortening operations
- name: infrastructure
description: Health and metrics endpoints5. Dafny output integration
5.1 The compilation pipeline
Dafny can compile to Python, Go, JavaScript, Java, and C#. The pipeline:
Spec operation Dafny source Compiled output
(pre/postconditions) -> (verified function) -> (target-language module)
| |
Dafny verifier Adapter layer
confirms correctness wraps typesDafny source for generate_short_code
module ShortCodeGen {
// The alphabet for short codes
const ALPHABET: string := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// Predicate: a string contains only alphanumeric characters
predicate IsAlphanumeric(s: string)
{
forall i :: 0 <= i < |s| ==> s[i] in ALPHABET
}
// Generate a short code that is not in the existing set
method GenerateShortCode(existingCodes: set<string>, length: nat)
returns (code: string)
requires 6 <= length <= 10
ensures 6 <= |code| <= 10
ensures IsAlphanumeric(code)
ensures code !in existingCodes
{
var candidate := "";
var found := false;
while !found
decreases * // non-terminating: depends on randomness
{
candidate := RandomAlphanumeric(length);
if candidate !in existingCodes {
found := true;
}
}
code := candidate;
}
// External method: filled in by the target runtime
method {:extern "ShortCodeRuntime", "RandomAlphanumeric"} RandomAlphanumeric(length: nat)
returns (s: string)
requires 6 <= length <= 10
ensures |s| == length
ensures IsAlphanumeric(s)
}5.2 Dafny-compiled output characteristics
When Dafny compiles to Python, the output has specific traits:
- Uses Dafny runtime library:
import _dafnyprovidesSeq,Map,Set, etc. - Non-idiomatic types: Dafny strings become
_dafny.Seqof characters, rather than nativestr. - Class-based structure: Each module becomes a class with static methods.
- No async support: Dafny output is synchronous; async wrapping is needed for FastAPI.
Example Dafny-compiled Python (simplified)
import _dafny
class ShortCodeGen:
@staticmethod
def GenerateShortCode(existingCodes, length):
# Dafny-generated loop
candidate = _dafny.Seq([])
found = False
while not found:
candidate = ShortCodeRuntime.RandomAlphanumeric(length)
if candidate not in existingCodes:
found = True
return candidate5.3 The adapter layer
The adapter converts between Dafny types and native types:
# adapters/short_code_gen.py — generated adapter
import _dafny
from dafny_output.ShortCodeGen import ShortCodeGen as _DafnyShortCodeGen
def generate_short_code(existing_codes: set[str], length: int = 8) -> str:
"""Adapter wrapping Dafny-verified GenerateShortCode.
Converts:
set[str] -> _dafny.Set of _dafny.Seq
int -> int (compatible)
_dafny.Seq -> str (return value)
"""
dafny_existing = _dafny.Set({_dafny.Seq(code) for code in existing_codes})
dafny_result = _DafnyShortCodeGen.GenerateShortCode(dafny_existing, length)
return "".join(dafny_result.Elements)5.4 When to use Dafny vs convention-generated code
The decision happens at the operation level:
| Operation Type | Dafny? | Rationale |
|---|---|---|
| Simple CRUD (create, read, update, delete) | No | Convention engine emits directly; no non-trivial logic |
| Lookup by key | No | SELECT ... WHERE key = $1 needs no verification |
| List with filtering | No | Standard query patterns |
| Computation with postconditions | Yes | E.g., generate a unique code satisfying constraints |
| State machine transitions | Yes | Complex guard conditions need verification |
| Business rules with invariants | Yes | E.g., "order total = sum of item prices" |
The "seam" is in the service layer. Each service method either calls the convention-generated database query directly or delegates to the Dafny adapter for the computed portion:
async def shorten(self, request: ShortenRequest) -> ShortenResponse:
# Convention-generated: validate input
validate_long_url(request.url)
# Convention-generated: gather existing state for Dafny precondition
existing_codes = await self._get_existing_codes()
# DAFNY SEAM: verified code generation
code = generate_short_code(existing_codes)
# Convention-generated: persist to database
entry = StoreEntry(code=code, url=request.url)
self._session.add(entry)
await self._session.flush()
# Convention-generated: construct response
return ShortenResponse(code=code, short_url=f"{settings.base_url}/{code}")5.5 Dafny runtime dependency
Each target requires the Dafny runtime library:
| Target | Dafny Runtime | Size | Install |
|---|---|---|---|
| Python | DafnyRuntimePython | ~50 KB | pip install dafnyruntimepython |
| Go | dafny/dafnyRuntime.go | ~30 KB | Vendored in internal/dafny/ |
| JavaScript | DafnyRuntimeJs | ~80 KB | npm install @aspect/dafny-runtime |
The runtime is included automatically when any operation uses Dafny output. When no operations require Dafny (pure CRUD services), the runtime dependency is omitted entirely.
6. Infrastructure code generation
6.1 Dockerfile generation
All targets use multi-stage builds:
Python pattern
builder stage -> install dependencies from pyproject.toml
runtime stage -> copy installed packages + app code, run as non-rootGo pattern
builder stage -> go build -o /app/server ./cmd/server
runtime stage -> FROM scratch or distroless, copy binary onlyTypeScript pattern
builder stage -> npm ci && npm run build
runtime stage -> copy dist/ + node_modules (production only)Health checks are always included. The health endpoint is generated as a built-in route (/health)
and the Dockerfile HEALTHCHECK instruction points to it.
6.2 Docker-compose.yml generation
The compose file is generated based on the state declarations:
| State Type | Compose Service |
|---|---|
| Any persistent state | PostgreSQL container |
| Cache-like state (TTL annotations) | Redis container (optional) |
| Queue/event state | RabbitMQ container (optional) |
The compose file always includes:
- The application container
- The database container with health check
- A migration runner (runs once, then exits)
- A volume for database persistence
6.3 CI/CD configuration
A GitHub Actions workflow is generated:
# .github/workflows/ci.yml — Generated by spec-to-rest compiler
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready --health-interval 5s --health-timeout 3s --health-retries 10
env:
DATABASE_URL: postgresql+psycopg://test:test@localhost:5432/test
BASE_URL: http://localhost:8000
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: pip install -e ".[dev]"
- name: Lint
run: ruff check app/ tests/
- name: Type check
run: mypy app/
- name: Run migrations
run: alembic upgrade head
- name: Run tests
run: pytest tests/ -v --tb=short
- name: Conformance tests
run: |
uvicorn app.main:app --host 0.0.0.0 --port 8000 &
sleep 3
schemathesis run openapi.yaml --base-url http://localhost:8000 --stateful=links
docker:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t url-shortener .
- name: Smoke test
run: |
docker compose up -d
sleep 5
curl -f http://localhost:8000/health
docker compose down -v6.4 Environment variable templates
The .env.example file lists every environment variable the service uses, with safe default values
and comments explaining each:
# Database connection string
DATABASE_URL=postgresql+psycopg://shortener:shortener@localhost:5432/shortener
# Base URL for constructing short URLs (no trailing slash)
BASE_URL=http://localhost:8000
# Logging level (debug, info, warning, error)
LOG_LEVEL=info6.5 Makefile generation
The Makefile provides a uniform interface across all targets:
# Common targets (every generated project has these)
.PHONY: help run test lint build docker-up docker-down migrate
help: ## Show available commands
run: ## Run the development server
test: ## Run all tests
lint: ## Run linter and formatter check
build: ## Build the production artifact
docker-up: ## Start all services with docker-compose
docker-down: ## Stop all services
migrate: ## Run database migrationsThe implementation varies by target but the interface is identical, so users can switch between Python, Go, and TypeScript projects and use the same commands.
7. Quality assurance of generated code
7.1 Static analysis pipeline
Every generated project passes through a static analysis pipeline before being delivered to the user:
| Target | Linter | Formatter | Type Checker |
|---|---|---|---|
| Python/FastAPI | ruff check | ruff format --check | mypy --strict |
| Go/chi | golangci-lint run | gofmt -l (must be empty) | go vet ./... |
| TypeScript/Express | eslint | prettier --check | tsc --noEmit |
The code generation pipeline runs these checks as a post-generation step. If any check fails, it indicates a template bug, and the pipeline fails with a clear error pointing to the offending template and generated file.
7.2 Test hierarchy
Generated tests form a three-tier pyramid:
Tier 1, API Integration Tests (from spec operations): Each spec operation gets test methods for
success cases and error cases. The requires clause violations map to specific error status codes.
The ensures clause postconditions map to assertion checks.
Tier 2, Property-Based Tests (from spec ensures clauses): Each ensures clause is translated
into a Hypothesis/rapid/fast-check property. The property generator produces arbitrary valid inputs
and checks that the postcondition holds.
Tier 3, Conformance Tests (from OpenAPI spec): Schemathesis fuzzes every endpoint with random valid and invalid inputs, checking that responses conform to the OpenAPI schema. This catches edge cases that hand-written tests miss.
7.3 Benchmark against hand-written code
To validate that generated code is production-quality, the pipeline includes a benchmark suite that compares generated code against hand-written equivalents on three axes:
- Correctness: Do the generated tests pass? Do hand-written tests (if any) pass?
- Performance: Request latency and throughput under load (wrk/hey).
- Code quality metrics: Lines of code, cyclomatic complexity, dependency count.
The benchmark is optional and runs in CI when a benchmark label is applied to a PR. It produces a
markdown report:
| Metric | Generated | Hand-Written | Delta |
|-------------------|-----------|--------------|--------|
| Lines of code | 487 | 523 | -7% |
| Cyclomatic compl. | 1.8 avg | 2.1 avg | -14% |
| p50 latency | 2.3ms | 2.1ms | +10% |
| p99 latency | 8.7ms | 7.9ms | +10% |
| Test pass rate | 100% | 100% | = |The expected outcome is that generated code is slightly more verbose (due to explicit contracts) but equivalent in performance, since the hot path is database I/O regardless.
8. Incremental regeneration
8.1 Diff-based regeneration
When the spec changes, the compiler does not regenerate everything blindly. It computes the diff between the old IR and new IR and regenerates only affected files:
old_ir = load("url_shortener.spec.v1")
new_ir = parse("url_shortener.spec.v2")
diff = compute_ir_diff(old_ir, new_ir)
for change in diff:
affected_files = dependency_map[change.kind]
for file in affected_files:
regenerate(file, new_ir)Dependency map
| Change Kind | Affected Files |
|---|---|
| New entity | models, schemas, validators, migration |
| Changed entity invariant | validators, schemas, migration (CHECK constraint), tests |
| New operation | routers, services, tests, OpenAPI |
| Changed operation pre/post | services, validators, tests, OpenAPI |
| New state relation | models, migration, database config |
| Changed convention override | routers (HTTP mapping), OpenAPI |
| Global invariant change | validators, migration, tests |
8.2 Migration generation for schema changes
When a state relation changes, the compiler generates a new migration file (incrementing the version number) rather than modifying the initial migration:
# Spec change: add 'click_count: ShortCode -> Int' to state
# Generates: alembic/versions/002_add_click_count.py
def upgrade() -> None:
op.add_column("store", sa.Column("click_count", sa.Integer(), server_default="0",
nullable=False))
def downgrade() -> None:
op.drop_column("store", "click_count")8.3 Preserving manual modifications
Users may need to modify generated code. The compiler supports two strategies:
Strategy 1: Protected Regions (default)
Generated files contain marked regions:
# === GENERATED CODE START — do not edit below this line ===
class StoreEntry(Base):
__tablename__ = "store"
code: Mapped[str] = mapped_column(String(10), primary_key=True)
url: Mapped[str] = mapped_column(Text, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
# === GENERATED CODE END ===
# === USER CODE START — your modifications here ===
# Add custom methods, properties, or class attributes below.
# This region is preserved during regeneration.
# === USER CODE END ===During regeneration, the compiler replaces the content between GENERATED CODE START and
GENERATED CODE END while preserving everything between USER CODE START and USER CODE END.
Strategy 2: Extension Files (recommended)
Rather than modifying generated files, users create extension files:
# app/extensions/url_shortener.py — never overwritten by the compiler
from app.services.url_shortener import UrlShortenerService
# Add custom methods to the service
async def get_top_urls(self, limit: int = 10):
"""Custom method not in the spec."""
...
# Monkey-patch or use dependency injection to add the method
UrlShortenerService.get_top_urls = get_top_urlsExtension files live in app/extensions/ (or internal/extensions/, src/extensions/) and are
never touched by the compiler. The generated main.py auto-discovers and loads extensions at
startup.
8.4 Git integration
The compiler can optionally commit generated changes:
spec-to-rest generate --commit url_shortener.specThis runs:
- Generate all files
git addonly generated files (tracked via.spec-to-rest-manifest.json)git commit -m "regen: update from spec change (added click_count state)"- Show the diff summary
The manifest file tracks which files were generated, so the compiler knows what to git add and
what to leave alone.
9. Extensibility
9.1 Hook points
Generated services expose hook points at standard locations:
| Hook | When It Fires | Use Case |
|---|---|---|
on_startup | Application boot | Initialize external connections, warm caches |
on_shutdown | Application shutdown | Close connections, flush buffers |
before_request | Before each request handler | Logging, auth, rate limiting |
after_request | After each request handler | Response logging, metrics |
before_{operation} | Before the service method | Custom validation, audit logging |
after_{operation} | After the service method | Notifications, cache invalidation |
on_error | When an exception occurs | Custom error responses, alerting |
Hooks are registered via a hooks file:
# app/hooks.py — user-defined, never overwritten
from app.hooks_registry import on_startup, before_request, after_shorten
@on_startup
async def init_redis():
"""Connect to Redis cache on startup."""
...
@before_request
async def check_api_key(request):
"""Validate API key header."""
...
@after_shorten
async def notify_analytics(result):
"""Send analytics event after URL shortening."""
...9.2 Plugin system for custom code generators
Users can register custom code generator plugins. The shipped plugin shape is the
DeploymentProfile registry in
modules/profile/src/main/scala/specrest/profile/Registry.scala;
new targets like Go/chi (#33) and TS/Express (#35) plug in as additional registry
entries plus a Handlebars template tree:
// modules/profile/src/main/scala/specrest/profile/KotlinSpring.scala (sketch)
package specrest.profile
object KotlinSpring:
val profile: DeploymentProfile = DeploymentProfile(
name = "kotlin-spring-postgres",
displayName = "Kotlin + Spring Boot + PostgreSQL",
language = "kotlin",
framework = "spring",
// ... typeMap, dependencies, etc.
)
// Then register in Registry.scala:
object Registry:
private val Profiles: Map[String, DeploymentProfile] = Map(
"python-fastapi-postgres" -> PythonFastapi.profile,
"kotlin-spring-postgres" -> KotlinSpring.profile
)Templates live under modules/codegen/src/main/resources/templates/<target-name>/
and codegen/Emit.scala dispatches on the
profile's name field. There is no separate plugin-discovery mechanism today,
adding a target is a registry entry plus a template tree.
9.3 Custom endpoints
Users can add endpoints not in the spec using the extensions mechanism:
# app/extensions/analytics.py
from fastapi import APIRouter
analytics_router = APIRouter(prefix="/analytics", tags=["analytics"])
@analytics_router.get("/top")
async def get_top_urls():
"""Custom endpoint: not in the spec, not overwritten by the compiler."""
...The generated main.py auto-discovers routers in app/extensions/:
# In generated main.py
import importlib
import pkgutil
for _, module_name, _ in pkgutil.iter_modules(["app/extensions"]):
module = importlib.import_module(f"app.extensions.{module_name}")
for attr_name in dir(module):
attr = getattr(module, attr_name)
if isinstance(attr, APIRouter):
app.include_router(attr)9.4 Overriding generated implementations
For any operation, users can replace the generated implementation:
# app/overrides/url_shortener.py — user-provided, never overwritten
async def shorten_override(session, request):
"""Replace the generated Shorten implementation.
The compiler-generated service will call this instead of its own logic
when this override exists.
"""
# Custom implementation using a counter-based scheme instead of random codes
...The service layer checks for overrides at startup:
class UrlShortenerService:
def __init__(self, session):
self._session = session
# Check for user override
try:
from app.overrides.url_shortener import shorten_override
self._shorten_impl = shorten_override
except ImportError:
self._shorten_impl = self._default_shorten
async def shorten(self, request):
return await self._shorten_impl(self._session, request)10. Comparison with existing generators
10.1 OpenAPI generator
What it is. Generates client SDKs and server stubs from OpenAPI specs. Supports 40+ targets.
What it does well
- Breadth: 40+ language targets means wide coverage.
- Community: large contributor base, many edge cases handled.
- Schema-to-model generation is solid for simple types.
What it does poorly
- Generated code often does not compile for less-popular targets. The Java and TypeScript targets are well-maintained; Go and Rust targets frequently have issues. A 2023 study found that 30% of generated Go clients had compilation errors.
- Server stubs are truly stubs, empty method bodies with TODO comments. No business logic, no database integration, no tests.
- No behavioral specification support. The input is purely structural (OpenAPI), so there is no way to express "this operation requires X and ensures Y."
- Template quality varies wildly between targets because different maintainers own different targets with no cross-target quality standard.
- Generated code style is often non-idiomatic. Go code uses Java-style getters/setters. Python code does not follow PEP 8 conventions.
What we should copy
- The template-per-target architecture. Each target has its own template set.
- The mustache template system is simple and well-understood.
- The
--additional-propertiesflag for customization per target.
What we should avoid
- Trying to support 40+ targets. We target 3 (Python, Go, TypeScript) and make them excellent.
- Generating stubs. Our output must be complete and runnable.
- Relying on community maintainers for individual targets with no integration testing.
10.2 JHipster
What it is. Full-stack application generator. Produces Spring Boot + Angular/React/Vue from a domain model (JDL).
What it does well
- Generates COMPLETE, RUNNABLE applications.
docker-compose upworks out of the box. This is the bar we must meet. - JDL (JHipster Domain Language) is simple and well-documented.
- Produces production-quality code: Liquibase migrations, Spring Security config, CI/CD pipelines, Docker files, Kubernetes manifests.
- Extensive entity relationship support (one-to-many, many-to-many, enums, pagination).
- Built-in support for microservice architectures (gateway, service discovery, config server).
What it does poorly
- CRUD-only. JDL defines entities and relationships, rather than behavior. There is no way to say "this operation requires X and ensures Y."
- Java/Spring only on the backend (with experimental .NET support). No Python, Go, or TypeScript backend.
- Opinionated to the point of rigidity. If you want something JHipster does not support (e.g., a custom auth scheme), you fight the generator.
- Overwhelming output: a simple 2-entity JHipster project generates 200+ files, most of which are boilerplate the developer never reads.
- No formal verification of any kind.
What we should copy
- The completeness standard: generated projects include migrations, Docker files, CI config, tests, and documentation. Everything works.
- The JDL syntax is a good model for how a simple DSL should feel.
- The entity relationship mapping (including junction tables for many-to-many).
What we should avoid
- Generating 200+ files for a simple service. Our URL shortener should produce ~25 files.
- Being opinionated about frontend framework. We generate backends only.
- Locking into one backend ecosystem (Spring).
10.3 Smithy code generators
What it is. AWS's API design language. Smithy IDL defines the model; code generators produce clients and servers.
What it does well
- Production-proven: every AWS SDK is generated from Smithy models.
- The trait system is elegant:
@readonly,@paginated,@httpQueryare annotations that modify code generation behavior without changing the model. - smithy-dafny exists: Smithy models can be compiled to Dafny for formal verification, then to target languages. This is the closest prior art to our Dafny integration pipeline.
- Clean separation between model (Smithy IDL), transform (code generators), and output (target code).
What it does poorly
- AWS-centric. Smithy's type system and traits are designed for AWS services. Concepts like "eventually consistent reads" and "pagination tokens" are baked in as first-class constructs.
- Steep learning curve. The Smithy model requires understanding resource lifecycle, service closures, and trait application order.
- No behavioral specification. Smithy is purely structural. You can say "this operation takes X and returns Y" but not "this operation requires that X is in the database and ensures that Y is added."
- Server code generation is still limited (mainly Java/Kotlin via smithy4s).
What we should copy
- The trait system for extensible metadata.
- The smithy-dafny pipeline architecture for verified code generation.
- The clean model-transform-output separation.
What we should avoid
- AWS-specific abstractions. Our DSL is domain-agnostic.
- The complexity of the Smithy gradle build system.
10.4 Grpc-gateway
What it is. Generates a REST reverse proxy in Go from protobuf service definitions with HTTP annotations.
What it does well
- Battle-tested at massive scale (millions of requests per day at many companies).
- Clean mapping from RPC to REST via annotations.
- Generates OpenAPI from the same protobuf source.
- The generated code is idiomatic Go.
What it does poorly
- Go-only server generation.
- Protobuf is verbose for simple REST APIs.
- No database integration. The generated code is a proxy; you still write the backend.
- No behavioral contracts beyond input/output types.
What we should copy
- The annotation-based convention override system (
option (google.api.http) = {...}). - The OpenAPI co-generation approach: produce the API spec alongside the server code.
What we should avoid
- Requiring protobuf as input. Our DSL should be simpler than protobuf.
- Being proxy-only. We generate the complete service, rather than a proxy.
10.5 Prisma
What it is. ORM and schema management tool for TypeScript/JavaScript (with growing Rust and
Python support). Schema-first: define models in schema.prisma, generate type-safe client code.
What it does well
- The
schema.prismaDSL is one of the best-designed data modeling languages. Simple, readable, powerful. - Migration generation is excellent:
prisma migrate devdetects schema changes and generates SQL migrations automatically. - Type-safe client: queries are statically typed, preventing runtime errors.
- Introspection: can reverse-engineer an existing database into a schema.
What it does poorly
- Data modeling only. No HTTP routes, no business logic, no tests.
- TypeScript/JavaScript only (Rust client is new, Python is community-maintained).
- No behavioral contracts. You cannot express preconditions or postconditions.
- The query engine is a Rust binary, adding deployment complexity.
What we should copy
- The migration generation approach: diff the old and new schemas, generate SQL.
- The schema.prisma DSL design: simple, declarative, readable.
- The introspection capability (future feature: import existing DB into spec).
What we should avoid
- Shipping a separate query engine binary. Our generated code uses the target's native ORM/database driver.
10.6 Sqlalchemy alembic
What it is. Database migration tool for Python. Auto-generates migrations by comparing SQLAlchemy model metadata to the current database state.
What it does well
- Auto-detection of schema changes: add a column to a model,
alembic revision --autogenerateproduces the migration. - Supports complex migrations: data migrations, multi-step changes, custom SQL.
- The migration chain (revision graph) handles branching and merging.
- Mature and battle-tested (used by most Python ORMs in production).
What it does poorly
- Python-only.
- Auto-generation misses some changes (renamed columns appear as drop+add).
- No support for behavioral constraints (CHECK constraints must be added manually).
- No cross-language migration generation.
What we should copy
- The auto-generation approach: compare model state to database state, generate diff.
- The revision chain for migration ordering.
- The
upgrade()/downgrade()pattern.
What we should avoid
- Depending on a running database for migration generation. Our compiler generates migrations from the spec diff alone, without needing a database connection.
10.7 Summary comparison matrix
| Capability | OpenAPI Gen | JHipster | Smithy | gRPC-GW | Prisma | Alembic | Ours |
|---|---|---|---|---|---|---|---|
| Behavioral spec input | No | No | No | No | No | No | Yes |
| Complete runnable output | No (stubs) | Yes | Partial | No (proxy) | No (ORM) | No (migrations) | Yes |
| Multi-language targets | 40+ (poor quality) | 1 (Java) | 7+ (clients) | 1 (Go) | 1 (TS) | 1 (Python) | 3 (high quality) |
| Database migrations | No | Yes | No | No | Yes | Yes | Yes |
| Generated tests | No | Partial | No | No | No | No | Yes (3 tiers) |
| Formal verification | No | No | Partial (smithy-dafny) | No | No | No | Yes (Dafny) |
| OpenAPI co-generation | Input, rather than output | Yes | Yes | Yes | No | No | Yes |
| Docker/infra generation | No | Yes | No | No | No | No | Yes |
| Incremental regeneration | No | Partial | No | No | Yes | Yes | Yes |
The key differentiator is that no existing tool combines behavioral specification with complete code generation. JHipster comes closest on the "complete output" axis; Smithy-Dafny comes closest on the "verified output" axis. Our compiler merges both.
Appendix A: Code generation pipeline implementation sketch
The orchestrator that drives the entire pipeline:
class CodeGenerationPipeline:
"""Top-level orchestrator for multi-target code generation."""
def __init__(self, target: str, template_dir: Path):
self.target = target
self.env = jinja2.Environment(
loader=jinja2.FileSystemLoader(template_dir / target),
trim_blocks=True,
lstrip_blocks=True,
keep_trailing_newline=True,
)
self.env.filters["snake_case"] = to_snake_case
self.env.filters["camel_case"] = to_camel_case
self.env.filters["python_type"] = to_python_type
self.env.filters["go_type"] = to_go_type
self.env.filters["ts_type"] = to_typescript_type
def generate(self, ir: ServiceIR, output_dir: Path) -> list[GeneratedFile]:
"""Generate all files for the target from the IR."""
manifest = self._load_manifest()
generated = []
for entry in manifest["files"]:
template = self.env.get_template(entry["template"])
output_path = output_dir / self._resolve_path(entry["output"], ir)
content = template.render(
service=ir,
entities=ir.entities,
state=ir.state,
operations=ir.operations,
invariants=ir.invariants,
conventions=ir.conventions,
target=self.target,
)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(content)
generated.append(GeneratedFile(path=output_path, template=entry["template"]))
# Post-generation quality checks
self._lint(output_dir)
self._typecheck(output_dir)
return generated
def _load_manifest(self) -> dict:
manifest_path = self.env.loader.searchpath[0] / "manifest.json"
return json.loads(manifest_path.read_text())
def _resolve_path(self, path_template: str, ir: ServiceIR) -> str:
return path_template.replace("{{ svc }}", to_snake_case(ir.name))
def _lint(self, output_dir: Path) -> None:
if self.target.startswith("python"):
subprocess.run(["ruff", "check", str(output_dir)], check=True)
subprocess.run(["ruff", "format", "--check", str(output_dir)], check=True)
elif self.target.startswith("go"):
subprocess.run(["golangci-lint", "run", str(output_dir / "...")], check=True)
elif self.target.startswith("typescript"):
subprocess.run(["npx", "eslint", str(output_dir / "src")], check=True)
def _typecheck(self, output_dir: Path) -> None:
if self.target.startswith("python"):
subprocess.run(["mypy", str(output_dir / "app")], check=True)
elif self.target.startswith("go"):
subprocess.run(["go", "vet", "./..."], cwd=output_dir, check=True)
elif self.target.startswith("typescript"):
subprocess.run(["npx", "tsc", "--noEmit"], cwd=output_dir, check=True)Appendix B: Template example, router generation (Jinja2)
A concrete example of a Jinja2 template for FastAPI route generation:
{#- templates/python-fastapi/router.py.j2 -#}
"""FastAPI route handlers for {{ service.name }}.
Generated from spec operations via the convention engine.
{%- for op in operations %}
{{ op.name }} ({{ "mutates" if op.mutates_state else "reads" }} state) -> {{ op.http_method }} {{ op.http_path }}
{%- endfor %}
"""
from fastapi import APIRouter, Depends, HTTPException, Response
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_session
from app.schemas.{{ service.name | snake_case }} import (
{%- for op in operations %}
{%- if op.inputs %}
{{ op.name }}Request,
{%- endif %}
{%- if op.outputs and op.http_success_status != 302 %}
{{ op.name }}Response,
{%- endif %}
{%- endfor %}
ErrorResponse,
)
from app.services.{{ service.name | snake_case }} import {{ service.name }}Service
router = APIRouter(tags=["{{ service.name | snake_case }}"])
{% for op in operations %}
@router.{{ op.http_method | lower }}(
"{{ op.http_path }}",
{%- if op.http_success_status != 302 %}
response_model={{ op.name }}Response,
{%- endif %}
status_code={{ op.http_success_status }},
responses={
{%- for status, desc in op.http_error_responses %}
{{ status }}: {"model": ErrorResponse, "description": "{{ desc }}"},
{%- endfor %}
},
summary="{{ op.name }}",
)
async def {{ op.name | snake_case }}(
{%- if op.inputs %}
body: {{ op.name }}Request,
{%- endif %}
{%- for param in op.path_params %}
{{ param.name }}: {{ param | python_type }},
{%- endfor %}
session: AsyncSession = Depends(get_session),
) -> {% if op.http_success_status == 302 %}Response{% else %}{{ op.name }}Response{% endif %}:
service = {{ service.name }}Service(session)
result = await service.{{ op.name | snake_case }}(
{%- if op.inputs %}body{% endif %}
{%- for param in op.path_params %}{% if op.inputs or not loop.first %}, {% endif %}{{ param.name }}{% endfor %}
)
{%- if op.requires and "in store" in (op.requires | join(" ")) %}
if result is None:
raise HTTPException(status_code=404, detail="Resource not found")
{%- endif %}
{%- if op.http_success_status == 302 %}
return Response(status_code=302, headers={"Location": result.url})
{%- elif op.http_success_status == 204 %}
return Response(status_code=204)
{%- else %}
return result
{%- endif %}
{% endfor %}This template, when rendered with the URL shortener IR, produces the app/routers/url_shortener.py
file shown in Section 1.1.
Appendix C: End-to-end generation sequence
The complete sequence from spec input to running service:
1. User writes url_shortener.spec
|
2. Parser produces ServiceIR
|
3. Convention engine annotates IR with HTTP/DB/validation mappings
|
4. Verification engine model-checks the spec (Alloy/Quint)
|
5. LLM synthesis engine generates Dafny code for non-CRUD operations
|
6. Dafny verifier confirms correctness
|
7. Dafny compiler produces target-language modules
|
8. Code generation pipeline runs:
a. Load target manifest (e.g., python-fastapi-postgres/manifest.json)
b. Render each template with IR + convention outputs + Dafny modules
c. Write generated files to output directory
d. Run linter + type checker on generated code
e. Generate .spec-to-rest-manifest.json listing all generated files
|
9. User runs: docker-compose up
|
10. Migration container runs alembic upgrade head
|
11. App container starts FastAPI on port 8000
|
12. User runs: make test
|
13. pytest runs test_api.py (integration), test_properties.py (property-based)
|
14. schemathesis runs test_conformance.py (schema fuzzing)
|
15. All tests pass -> service is verified end-to-endTotal wall-clock time for steps 2-8 (the compiler): under 30 seconds for a URL-shortener-sized spec. Under 5 minutes for a 20-operation service with Dafny verification in the loop.
11. Observability in generated services
Every generated service includes observability infrastructure out of the box: structured logging, distributed tracing, metrics, and health endpoints. These are not optional add-ons, they are part of the standard file manifest for every target.
11.1 Structured logging
Generated services use structured JSON logging (not plaintext). Every log entry includes:
{
"timestamp": "2026-04-05T12:34:56.789Z",
"level": "info",
"service": "url_shortener",
"operation": "Shorten",
"trace_id": "abc123",
"span_id": "def456",
"duration_ms": 42,
"status_code": 201,
"message": "Operation completed"
}Implementation per target:
- Python/FastAPI:
structlogwith JSON renderer, middleware that injects request context - Go/chi:
slogwith JSON handler, middleware that injects request context - TypeScript/Express:
pinowith JSON transport, middleware that injects request context
Sensitive fields (marked @sensitive in the spec or matching patterns like password, token,
secret) are automatically redacted in log output.
11.2 Opentelemetry integration
Generated services export traces and metrics via OpenTelemetry:
url_shortener/
app/
middleware/
The generated docker-compose.yml includes an optional Jaeger or OTLP collector container for local
development. Each operation is wrapped in a span that records:
- Operation name (from the spec)
- Input parameters (non-sensitive)
- Duration
- Success/failure status
- Database query spans (auto-instrumented via ORM)
Convention override:
conventions {
global.otel_exporter = "otlp" // "otlp", "jaeger", "none"
global.otel_endpoint = "http://collector:4317"
}11.3 Health and readiness endpoints
Every generated service includes two health endpoints (not derived from the spec, these are infrastructure conventions):
| Endpoint | Purpose | Checks |
|---|---|---|
GET /health | Liveness probe (K8s livenessProbe) | Returns {"status": "ok"} if the process is running |
GET /ready | Readiness probe (K8s readinessProbe) | Checks DB connectivity, checks external service reachability |
The readiness endpoint verifies that all dependencies declared via effect blocks (see Appendix F
of the spec language design) are reachable. If a dependency is down, the readiness endpoint returns
HTTP 503 with details about which dependency failed.
11.4 Metrics
Generated services expose a /metrics endpoint (Prometheus format) or push metrics via OTel,
covering:
- Request count by operation, method, and status code
- Request duration histogram by operation
- Active database connections
- Error rate by operation