feat: release managed service generation and contract diagnostics

This commit is contained in:
2026-09-24 17:34:21 +08:00
parent eb69260586
commit 158b8002b3
85 changed files with 5722 additions and 360 deletions

View File

@@ -3,7 +3,7 @@ package apigen
import "fmt"
var baseTemplates = map[string]string{
"cmd/server/main.go": `package main
"main.go": `package main
import (
"context"
"errors"
@@ -12,35 +12,31 @@ import (
"os"
"os/signal"
"syscall"
omni "git.yuntuo.hk/k2safe/go-omni"
"git.yuntuo.hk/k2safe/go-omni/logx"
"MODULE/internal/config"
"MODULE/internal/handler"
"MODULE/internal/servicecontext"
"MODULE/internal/settings"
"MODULE/internal/svc"
)
func main(){
path:=flag.String("config","etc/config.yaml","configuration file; empty uses defaults and environment")
flag.Parse()
if err:=run(*path);err!=nil{log.Fatal(err)}
}
func run(path string)(runErr error){
ctx,stop:=signal.NotifyContext(context.Background(),os.Interrupt,syscall.SIGTERM)
defer stop()
cfg,err:=settings.Load(path);if err!=nil{return err}
if err:=run(ctx,*path);err!=nil{log.Fatal(err)}
}
func run(ctx context.Context,path string)(runErr error){
cfg,err:=config.Load(path);if err!=nil{return err}
logger,err:=logx.New(cfg.Server.Log);if err!=nil{return err}
defer func(){runErr=errors.Join(runErr,logger.Close())}()
logx.SetDefault(logger)
deps,err:=servicecontext.New(ctx,logger,cfg);if err!=nil{return err}
deps,err:=svc.New(ctx,logger,cfg);if err!=nil{return err}
// RunContext drains HTTP requests before returning. This scope owns dependencies
// and closes them exactly once, before closing the borrowed logger.
defer func(){runErr=errors.Join(runErr,deps.Close())}()
app:=omni.New(omni.WithConfig(cfg.Server))
app.OnShutdown(logger.Close)
app.OnShutdown(deps.Close)
if err=handler.RegisterRoutes(app,deps);err!=nil{return err}
app.GET("/openapi.json",handler.OpenAPI)
app.GET("/health/live",deps.Health.LivenessHandler())
app.GET("/health/ready",deps.Health.ReadinessHandler())
logger.InfoContext(ctx,"service.starting","address",cfg.Server.Address)
return app.RunContext(ctx,cfg.Server.Address)
app,err:=handler.NewApp(deps);if err!=nil{return err}
defer func(){runErr=errors.Join(runErr,app.Close())}()
logger.InfoContext(ctx,"service.starting","address",app.Config().Address)
return app.RunContext(ctx)
}
`,
".gitignore": "/server\n/var/\n.env\n.env.*\n!.env.example\n/.goOmni/api.lock\n",
@@ -51,12 +47,13 @@ func projectREADME(module, entry string) string {
This project is generated from an editable API contract. Business code uses
standard context.Context; generated HTTP handlers use Omni's transport-neutral httpx.
main.go and HTTP composition are continuously generator-owned.
## Run
`+"```sh"+`
go mod tidy
go run ./cmd/server
go run .
`+"```"+`
GET /health/live and /health/ready check the running service. GET /openapi.json
@@ -83,9 +80,9 @@ go test ./...
Commands inside a generated project discover the recorded API entry automatically.
--dry-run previews create/update/remove actions without writing files.
The --check command is read-only and suitable for CI. It fails on stale or missing
generated files and missing business scaffolds. go.mod, configuration, main,
Service, Repository, servicecontext, middleware hook bodies and API files are
created once and then preserved byte for byte. Gen.go files and OpenAPI are owned
generated files and missing business scaffolds. go.mod, configuration, Logic,
Repository, svc, middleware hook bodies and API files are created once and then
preserved byte for byte. main.go, Gen.go files and OpenAPI are owned
by the generator; manual edits cause a conflict instead of being overwritten.
Commit .goOmni/apiManifest.json alongside the generated files. Do not edit it.
Avoid editing files while generation is running; api.lock serializes generators.
@@ -95,18 +92,30 @@ business files remain: remove/adapt obsolete implementations explicitly if an
API change makes their old types or signatures fail to compile. Every regeneration
must be followed by go test ./... . Do not delete service files to change contracts.
Generation compares the last saved contract with the new one and reports affected
Logic files, including nested DTO field/tag changes and streaming/body changes.
REVIEW notices require a business review. ERROR diagnostics identify definite
Logic signature, syntax or duplicate-method problems. Generation preserves
business code and still writes valid generated output; fix those methods next.
--check fails on definite Logic errors even after generated files are current.
Import aliases, parameter names and moved methods within a group are supported.
Project type aliases, embedded methods or build tags may need compiler verification
and produce REVIEW notices. This source scan does not execute project code or
replace go test ./... . Snapshot-based REVIEW notices describe changes since the
last successful generation; they do not persist as an acknowledgement checklist.
## Layout
`+"```text"+`
api/ # editable contract and imported types
cmd/server/main.go # startup, lifecycle and route registration
main.go # GENERATED startup, lifecycle and route registration
etc/config.yaml # lowerCamelCase configuration
internal/settings/ # defaults -> file -> environment
internal/servicecontext/ # dependency initialization and cleanup
internal/config/ # defaults -> file -> environment
internal/svc/ # dependency initialization and cleanup
internal/types/typesGen.go # generated request/response types
internal/apierrors/errorsGen.go # generated fresh error constructors, when declared
internal/handler/<group>/ # generated binding and service adapters
internal/service/<group>/ # developer-owned business operations
internal/handler/<group>/ # generated binding and Logic adapters
internal/logic/<group>/ # developer-owned business operations
internal/repository/<group>/ # developer-owned persistence contracts
internal/middleware/ # named hooks and generated registry
docs/openapi.json # generated OpenAPI 3.0.3
@@ -114,12 +123,25 @@ docs/openapi.json # generated OpenAPI 3.0.3
`+"```"+`
The API response type describes business data. Declare response Name (...) and
select it with @server(response: Name) or @response Name to share a public body;
select the App default with @app(defaultResponse: Name) in the entry API.
@server(response: Name) and @response Name override it for groups/routes;
@response raw returns the business JSON directly. Without a template, the default
envelope includes code, message, data and requestId. Generated routes and OpenAPI
use the API contract, independently of the app's default response configuration.
use the same contract. 404/405 and ordinary App routes use the declared App default.
No template is selected by declaration/import order. Without @app, existing framework
response defaults apply. An explicit API App default takes precedence over response
options in svc, keeping generated routes and OpenAPI consistent.
Handlers reject nil successful responses. Body DTOs use application/json and have a 1 MiB limit by default.
Use contract.Binder.Bind's optional limit in a custom adapter for larger payloads.
Use @body(maxBytes: ...) for route limits. @body(mode: raw|multipart; maxBytes: ...)
adds a *httpx.Body argument to Logic, without handwritten handlers. Raw is the exact
limited request stream; Parts supports sequential multipart reading with no temp files.
@produces "application/octet-stream" (or another concrete media type) generates a
Logic result of *httpx.Stream; the handler closes Content and streams it unwrapped.
Signature verification and storage policy belong to Logic/middleware. A consumed
request stream cannot be reread automatically; use one reader owner or explicitly
bounded buffering. Stream reads respect configured server timeouts. Multipart parts
are not DTO form fields: form tags remain URL query parameters. The optional manual
handler/custom binder hooks are preserved, and are needed only for custom protocols.
Fields require exactly one source tag: json, path, form (URL query only), header.
All are required by presence unless optional, omitempty or default is specified.
@@ -132,20 +154,33 @@ and binding compiler. Responses are expected to honor their declared constraints
they are not validated on every response. Do not return nil slices/maps for a
non-nullable collection (initialize empty collections instead).
Declare public business errors in error (...) and reference them with @errors.
Go-owned errors can be registered once through svc.ServiceContext.AppOptions with
omni.WithErrors(projecterrors.Catalog()). Generated routes inherit them unless they
declare an independent catalog. Do not repeat Go-owned errors in the API contract.
Alternatively, declare public business errors in error (...) and reference them with @errors.
Use internal/apierrors constructors to share status, code and message with OpenAPI.
Auth declarations connect a bearer/header/cookie scheme to a real named middleware.
The selected auth runs first; @auth none removes inherited declarative auth.
Validation policy remains in the middleware implementation. Public messages must
not contain secrets; error causes may be included in server logs.
svc.New receives the loaded Config and borrowed Logger. Add dependencies there,
then immediately register each cleanup with OnClose. A later initialization failure
runs registered cleanups in reverse order. Close is idempotent. GlobalMiddleware
provides global hooks; Middlewares supplies named API hooks. AppOptions is evaluated
once before route registration; logging is configured earlier through Config.
The generated main runs HTTP draining before closing svc resources, then the logger.
Do not close borrowed resources or attach duplicate cleanup hooks to App. The startup
context cancels on signals; long-lived clients/workers that requests depend on need
their own lifetime, canceled by OnClose after HTTP draining.
Named middleware is composed in declaration order (first is outermost). Implement
the generated hooks or replace entries in servicecontext; never bypass an auth
the generated hooks or replace entries in svc; never bypass an auth
hook merely to make the server start. Middleware names do not imply a JWT policy.
This CLI supports a documented go-zero-style subset, not all goctl features.
Database tables and repositories cannot be inferred from an HTTP contract. Add
business repositories through servicecontext. Database and storage options are
business repositories through svc. Database and storage options are
composed at creation; the original profile is retained on regeneration.
`, module, entry)
}