feat: complete API-driven projects with infrastructure and business acceptance

This commit is contained in:
2026-09-23 17:33:50 +08:00
parent 04a1f7999d
commit 2c74dc0859
137 changed files with 11926 additions and 105 deletions

233
scripts/verifyAPI.py Normal file
View File

@@ -0,0 +1,233 @@
#!/usr/bin/env python3
"""Bounded local acceptance of API-generated projects, on both Omni transports.
No remote hosts, cloud credentials or database are used. Child servers and
temporary modules are always cleaned up. Run from any directory with Python 3.
"""
import hashlib
import http.client
import json
import os
from pathlib import Path
import signal
import socket
import subprocess
import tempfile
import time
ROOT = Path(__file__).resolve().parents[1]
REPORT = ROOT / "performance/apiGenerationLocal.json"
ENV = {k: v for k, v in os.environ.items() if not k.startswith("GO_OMNI_")}
ENV.update(GOWORK="off", GOFLAGS="")
def run(args, cwd=ROOT, ok=True):
result = subprocess.run([str(x) for x in args], cwd=cwd, env=ENV,
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
timeout=90)
if ok and result.returncode:
raise AssertionError(f"{args}:\n{result.stdout}")
if not ok and not result.returncode:
raise AssertionError(f"command unexpectedly succeeded: {args}")
return result.stdout
def request(port, method, path, body=None, headers=None):
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=3)
try:
if body is not None and len(body) > (1 << 20):
# Probe early Content-Length rejection without racing a 1 MiB
# client send against the server's intentional connection close.
conn.putrequest(method, path)
for key, value in (headers or {}).items():
conn.putheader(key, value)
conn.putheader("Content-Length", str(len(body)))
conn.endheaders()
else:
conn.request(method, path, body=body, headers=headers or {})
response = conn.getresponse()
return response.status, dict(response.getheaders()), response.read()
finally:
conn.close()
def write(path, text):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text)
def exercise(binary, project, transport, starter):
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
environment = dict(ENV, GO_OMNI_HTTP_ADDR=f"127.0.0.1:{port}",
GO_OMNI_TRANSPORT=transport, GO_OMNI_LOG_OUTPUT="stderr",
GO_OMNI_LOG_FORMAT="json")
checks = []
log_path = project / f"{transport}.log"
with log_path.open("w+") as log:
process = subprocess.Popen([str(binary)], cwd=project, env=environment,
stdout=log, stderr=log)
try:
deadline = time.monotonic() + 10
while True:
if process.poll() is not None:
raise AssertionError(log_path.read_text())
try:
if request(port, "GET", "/health/ready")[0] == 200:
break
except OSError:
pass
if time.monotonic() > deadline:
raise AssertionError("server startup timed out")
time.sleep(0.05)
checks.append("readiness")
status, headers, document = request(port, "GET", "/openapi.json")
assert status == 200 and document == (project / "docs/openapi.json").read_bytes()
checks.append("servedOpenApiMatchesFile")
if starter:
status, headers, data = request(port, "GET", "/api/greet/Alice")
assert status == 200 and json.loads(data)["data"]["message"] == "Hello Alice"
assert headers.get("X-Request-Id") or headers.get("X-Request-ID")
checks.extend(["greeting", "requestId"])
else:
body = json.dumps({"name": "Alice", "active": False, "age": 0})
base = {"Content-Type": "application/json", "X-Request-ID": "api-acceptance-42"}
for method, path, payload, expected in [
("PUT", "/api/v1/users/7?notify=true", body, 200),
("PUT", "/api/v1/users/7", '{"name":"Alice"}', 400),
("PUT", "/api/v1/users/7?notify=bad", body, 400),
("PUT", "/api/v1/users/7?notify=true&notify=false", body, 400),
("PUT", "/api/v1/users/7", body[:-1] + ',"extra":1}', 400),
("PUT", "/api/v1/users/7", body + "{}", 400),
("PUT", "/api/v1/users/7", '{"name":"A","active":false,"age":151}', 400),
("PUT", "/api/v1/users/0", body, 400),
("PUT", "/api/v1/users/7", " " * ((1 << 20) + 1), 413),
("GET", "/api/v1/users?page=2&pageSize=10&tag=a&tag=b", None, 200),
("GET", "/api/v1/users?page=0", None, 400),
("DELETE", "/admin/v1/users/7", None, 401),
("POST", "/api/v1/newUsers", body, 201),
("POST", "/api/v1/acceptedUsers", body, 202),
]:
status, headers, data = request(port, method, path, payload, base)
assert status == expected, (method, path, status, data)
if path.startswith("/api/v1/users/7?") and status == 200:
assert json.loads(data)["data"] == {"userId": 7, "name": "Alice", "active": False}
assert json.loads(data)["requestId"] == "api-acceptance-42"
if path.startswith("/api/v1/users?page=2"):
assert json.loads(data)["data"]["page"] == 2
checks.extend(["mixedSources", "zeroAndFalse", "pathRange", "defaultAndQuery",
"requiredFields", "unknownFields", "duplicateScalar", "trailingJson",
"rangeValidation", "bodyLimit413", "auth401", "status201", "status202"])
assert request(port, "PUT", "/api/v1/users/7", body,
{"Content-Type": "text/plain"})[0] == 415
status, headers, data = request(port, "DELETE", "/admin/v1/users/7", headers={"Authorization": "Bearer test-only"})
assert status == 204 and data == b"" and headers.get("X-Audit") == "yes"
checks.extend(["contentType415", "middlewareComposition", "noContent204"])
process.send_signal(signal.SIGTERM)
assert process.wait(timeout=12) == 0
checks.append("gracefulShutdown")
try:
request(port, "GET", "/health/live")
raise AssertionError("listener survived shutdown")
except OSError:
checks.append("listenerReleased")
finally:
if process.poll() is None:
process.kill()
process.wait(timeout=5)
if not starter:
logs = [json.loads(line) for line in log_path.read_text().splitlines() if line.startswith("{")]
assert any(row.get("msg") == "user.updated" and row.get("requestId") == "api-acceptance-42" for row in logs)
checks.append("businessLogCorrelation")
return {"project": "starter" if starter else "contract", "transport": transport,
"checks": checks, "passed": True}
def main():
started = time.monotonic()
results = []
with tempfile.TemporaryDirectory(prefix="goOmniApiAcceptance") as temp:
temp = Path(temp)
cli = temp / "go-omni"
run(["go", "build", "-o", cli, "./cmd/go-omni"])
for starter in (True, False):
project = temp / ("starter" if starter else "contract")
common = ["--module", "example.com/apiAcceptance", "--framework-dir", ROOT]
if starter:
run([cli, "api", "new", *common, project])
entry = project / "api/service.api"
else:
run([cli, "new", "--api", ROOT / "examples/contracts/user.api", *common, project])
entry = project / "api/user.api"
business = '''package user
import("context";"example.com/apiAcceptance/internal/types")
func(s *Service) UpdateUser(ctx context.Context,req *types.UpdateUserReq)(*types.UserResp,error){
s.deps.Logger.InfoContext(ctx,"user.updated","userId",req.UserID)
return &types.UserResp{UserID:req.UserID,Name:req.Name,Active:req.Active},nil
}
'''
write(project / "internal/service/user/updateUser.go", business)
write(project / "internal/service/user/listUsers.go", '''package user
import("context";"example.com/apiAcceptance/internal/types")
func(s *Service) ListUsers(ctx context.Context,req *types.ListUsersReq)(*types.ListUsersResp,error){return &types.ListUsersResp{Items:[]types.UserResp{},Page:req.Page,Total:0},nil}
''')
write(project / "internal/service/admin/user/deleteUser.go", '''package user
import("context";"example.com/apiAcceptance/internal/types")
func(s *Service) DeleteUser(ctx context.Context,req *types.DeleteUserReq)error{return nil}
''')
write(project / "internal/middleware/auth.go", '''package middleware
import("git.yuntuo.hk/k2safe/go-omni/httpx";apierrors "git.yuntuo.hk/k2safe/go-omni/errors")
func Auth(next httpx.Handler)httpx.Handler{return func(c *httpx.Context)error{if c.Header("Authorization")!="Bearer test-only"{return apierrors.Unauthorized("UNAUTHORIZED","authentication required")};return next(c)}}
''')
write(project / "internal/middleware/audit.go", '''package middleware
import("git.yuntuo.hk/k2safe/go-omni/httpx")
func Audit(next httpx.Handler)httpx.Handler{return func(c *httpx.Context)error{c.SetResponseHeader("X-Audit","yes");return next(c)}}
''')
source = entry.read_text().replace(" UpdateUserReq {", ' CreateUserReq {\n Name string `json:"name"`\n Active bool `json:"active"`\n Age *int `json:"age,optional"`\n }\n UpdateUserReq {')
source += '''
@server (
group: user
prefix: /api/v1
)
service user-api {
@handler createUser
@status 201
post /newUsers (CreateUserReq) returns (UserResp)
@handler acceptUser
@status 202
post /acceptedUsers (CreateUserReq) returns (UserResp)
}
'''
entry.write_text(source)
run([cli, "api", "generate", "--api", entry, "--dir", project, "--check"], ok=False)
run([cli, "api", "generate", "--api", entry, "--dir", project])
assert (project / "internal/service/user/updateUser.go").read_text() == business
for operation in ("CreateUser", "AcceptUser"):
name = operation[0].lower() + operation[1:]
write(project / f"internal/service/user/{name}.go", f'''package user
import("context";"example.com/apiAcceptance/internal/types")
func(s *Service) {operation}(ctx context.Context,req *types.CreateUserReq)(*types.UserResp,error){{return &types.UserResp{{UserID:8,Name:req.Name,Active:req.Active}},nil}}
''')
run([cli, "api", "validate", "--api", entry])
run([cli, "api", "generate", "--api", entry, "--dir", project, "--check"])
binary = project / "server"
run(["go", "build", "-mod=mod", "-o", binary, "./cmd/server"], cwd=project)
for transport in ("nethttp", "native"):
result = exercise(binary, project, transport, starter)
results.append(result)
print(f"PASS {result['project']}/{transport}: {len(result['checks'])} checks", flush=True)
files = []
for folder in ("cmd/go-omni/apigen", "contract", "openapi", "examples/contracts"):
files.extend(p for p in (ROOT / folder).rglob("*") if p.is_file())
files.extend(ROOT / p for p in ("cmd/go-omni/api.go", "cmd/go-omni/main.go", "scripts/verifyAPI.py",
"transport/native/server.go", "transport/native/earlyReject_test.go"))
report = {"passed": True, "durationSeconds": round(time.monotonic() - started, 2),
"cases": results,
"sourceSha256": {str(p.relative_to(ROOT)): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(files)}}
REPORT.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n")
print(f"Saved {REPORT}", flush=True)
if __name__ == "__main__":
main()