Files
go-omni/examples/managedService/README.md

10 KiB

example.com/managed

This checked-in example uses a relative local framework replacement (../..). Run go mod tidy && go run . here. No external database or cloud account is required. The Go-only error catalog is internal/apierrors/catalog.go, registered by svc. The generated main and handlers are unmodified. Built-in endpoint switches and paths are configured in etc/config.yaml under server.endpoints. Health replies use the project envelope by default; responseMode: raw opts out. Readiness failures stay HTTP 503, independently of business error status policies.

Dependencies can call svc.RequestShutdown(err); the generated entry drains requests, closes resources and retains the failure for a nonzero exit. Try:

curl -i 'http://127.0.0.1:8080/check?kind=fixed'
curl -i 'http://127.0.0.1:8080/check?kind=params'
curl -i 'http://127.0.0.1:8080/missing'
curl -i -H 'X-Signature: valid' --data-binary ' {"hello":1} ' http://127.0.0.1:8080/callback
curl -i -F 'file=hello' http://127.0.0.1:8080/upload
curl -OJ http://127.0.0.1:8080/download

The upload demonstration caps requests at 2048 bytes, so use a small test file; large input is intentionally rejected with HTTP 413. Replace the demonstration X-Signature check with the actual provider's signature algorithm and secret. From the framework root, check generated files with: go run ./cmd/go-omni api generate --dir examples/managedService --check.

Contract changes produce REVIEW notices for affected Logic methods and DTO fields. Definite Logic signature problems produce ERROR diagnostics with a file/line and the required signature. Regeneration preserves business code; --check continues to fail on unresolved signature errors after generation. Run go test ./... too, especially for project type aliases, build tags and changes inside method bodies.

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

go mod tidy
go run .

GET /health/live and /health/ready check the running service. GET /openapi.json serves the same document as docs/openapi.json. Bind address defaults to localhost. Set GO_OMNI_TRANSPORT=native to select the native transport without changing services. The greeting starter works immediately; other new operation stubs return 501 until their business logic is implemented. Authentication hooks also fail closed with 501.

Develop

  1. Edit api/managed.api and its relative imports.
  2. Run the same go-omni CLI version as the framework dependency:
go-omni api validate
go-omni api inspect
go-omni api generate --dry-run
go-omni api generate
go-omni api generate --check
go-omni api export > openapi.json
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, 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.

Routes and types that disappear are removed from generated output. Handwritten 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.

Layout

api/                         # editable contract and imported types
main.go                      # GENERATED startup, lifecycle and route registration
etc/config.yaml              # lowerCamelCase configuration
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 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
.goOmni/apiManifest.json      # ownership and content hashes

The API response type describes business data. Declare response Name (...) and 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 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 @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. Zero and false are valid present values; binding:"required" rejects scalar zero. Use *T with optional when business logic needs to distinguish missing from zero. Pointers may carry null; non-pointer null is rejected. Unknown/duplicate JSON fields and duplicate scalar query/header values are rejected. Arrays in the query use repeated keys. Enum/range/default and nested fields share the documentation 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).

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 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 svc. Database and storage options are composed at creation; the original profile is retained on regeneration.

Infrastructure

Selected database: none; storage: none; logging: stderr/json. MySQL uses database/sql through deps.DB, including InTx and a readiness probe. Set GO_OMNI_DB_DSN; pool settings live in etc/config.yaml and GO_OMNI_DB_*. No tables are invented or migrated automatically from request/response DTOs. The none/memory selection opens no database; implement repositories per business.

With storage enabled, services receive deps.Files, Uploads, Downloads and UploadLinks capability interfaces. Local signed GET/HEAD/PUT uses /storage/object; set GO_OMNI_STORAGE_SIGNING_KEY (32+ bytes) and GO_OMNI_STORAGE_PUBLIC_URL. Cloud credentials use GO_OMNI_STORAGE_ACCESS_KEY/SECRET_KEY; set bucket and endpoint or R2 accountId as appropriate. Signed URLs grant temporary access: authenticate and authorize before issuing them. No public upload-link route is added. Storage failures during startup close an already-open database; Close is idempotent.

Database/storage/logging selection is recorded in .goOmni/apiManifest.json. Changing selection through generation flags is rejected on an existing project; edit your owned configuration and initialization explicitly, or generate a fresh project for comparison. New routes still regenerate without repeating these flags.