feat: complete contract-driven business development and MySQL integration

This commit is contained in:
2026-09-24 21:56:04 +08:00
parent f9afb8f8dd
commit f5478cdc76
108 changed files with 6557 additions and 550 deletions

View File

@@ -1,4 +1,5 @@
{
"templateVersion": "v0.14.0-preview",
"profile": {
"database": "none",
"storage": "none",
@@ -9,15 +10,17 @@
"module": "example.com/managed",
"entry": "api/managed.api",
"generated": {
"docs/openapi.json": "9eb707aefeefd3f4881c774534e2d143949c0edb1d8a4aabd762f978f6203df1",
"internal/handler/appGen.go": "c6f078932e1a598a46faebba397f4e753db9d8f4f29a02f9b6077a0557d6c14e",
"internal/handler/demo/handlersGen.go": "7a6f9d5e105e44b07196cc540cee3f0562d5f3d1c5f0bc094434db1064d0339c",
"internal/handler/openapiGen.go": "612920ed25e5dc7a4f673be56041d47b70aa3216707ff282e52dfe503c2d705a",
"docs/openapi.json": "68c7e9e0294bf62937aa953a44732288c51571108c922a36a72faa72c4360b5f",
"internal/handler/appGen.go": "f5251b4dc33e2312b0bdb71ead70d2171977d851265e124f640458d705a2cc9d",
"internal/handler/demo/handlersGen.go": "3c76d85dbe89eb5127d9d134dc625203ecf9719dd11d174c1f335691b99e1375",
"internal/handler/openapiGen.go": "ae9b794c63e0f3c8f9d200a5fc6977350139d4a6f00b42a26102c8630d907d03",
"internal/handler/responseGen.go": "84397a502bba4777ebfa6fae3d3bc418297acab4f26b04b42e3c31b36c735399",
"internal/handler/routesGen.go": "76c1a5b2644e75e56553dc6e9e862a63128d902dd94807ce7c8aee5491cd1e3e",
"internal/middleware/registryGen.go": "e5f47efa57a1432765ef3419bae59dbf3b4d55b3aac0a6c935ce7a52ba6b47f4",
"internal/types/typesGen.go": "7586e86fa5d67051283178cc578c890f6c02fd2d5c383170a71a39c2e77ce344",
"main.go": "a12db5b7df0f2fdca4939a7382daee04c85ddb63ef0bccd1a2ce26b718f42579"
"internal/svc/runtimeGen.go": "78ffd0b2a196fbc29e29473919d1341e8a2f508f6594ad148c0b399fbcc40018",
"internal/types/dtoManagedd28645a2af1cGen.go": "7586e86fa5d67051283178cc578c890f6c02fd2d5c383170a71a39c2e77ce344",
"internal/types/typesGen.go": "372d67f23a313b3e77d2a3b7a236315db9721bc63350bf1f71db1293e57570d1",
"main.go": "8a4051c0d51fc14ddf22c36d09de706f02499817accc937f0785e3d0fb5e2aa5"
},
"contract": {
"routes": {

View File

@@ -34,149 +34,18 @@ 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.
## Framework-managed runtime
## Run
`main.go`, `internal/svc/runtimeGen.go`, generated handlers, `internal/types/*Gen.go`
and OpenAPI are generator-owned. Add project dependencies in `svc.Init(ctx)` and
register each successful initialization with `OnClose`. Runtime owns reverse
cleanup and failure rollback; do not copy locks, constructors or Close methods
into this project. The logger is scoped to this App.
```sh
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:
```sh
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
```text
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.
Configuration, API contracts, svc fields/Init/AppOptions, Logic, Repository and
middleware implementations remain developer-owned. Regeneration preserves them.
Use `go-omni api format` to format local contracts, then `api generate` and
`api generate --check`. See the [framework development guide](../../DEVELOPMENT.md)
for array/null responses, typed multipart, full configuration environment overrides,
shared contracts and v0.13 upgrade steps. This example keeps its raw callback,
streaming upload and download to exercise those existing extension points.

View File

@@ -195,6 +195,12 @@
"demo"
],
"x-omni-max-body-bytes": 64,
"x-omni-request-policy": {
"unknownQuery": "",
"utf8": "",
"charset": "",
"body": "auto"
},
"x-omni-response": "CommonResponse"
}
},
@@ -410,6 +416,12 @@
"tags": [
"demo"
],
"x-omni-request-policy": {
"unknownQuery": "",
"utf8": "",
"charset": "",
"body": ""
},
"x-omni-response": "CommonResponse"
}
},
@@ -589,6 +601,12 @@
"tags": [
"demo"
],
"x-omni-request-policy": {
"unknownQuery": "",
"utf8": "",
"charset": "",
"body": ""
},
"x-omni-response": "CommonResponse"
}
},
@@ -804,6 +822,12 @@
"tags": [
"demo"
],
"x-omni-request-policy": {
"unknownQuery": "",
"utf8": "",
"charset": "",
"body": ""
},
"x-omni-response": "AlternateResponse"
}
},
@@ -992,6 +1016,12 @@
"tags": [
"demo"
],
"x-omni-request-policy": {
"unknownQuery": "",
"utf8": "",
"charset": "",
"body": ""
},
"x-omni-response": "raw"
}
},
@@ -1197,6 +1227,12 @@
"tags": [
"demo"
],
"x-omni-request-policy": {
"unknownQuery": "",
"utf8": "",
"charset": "",
"body": ""
},
"x-omni-response": "CommonResponse"
}
},
@@ -1414,9 +1450,27 @@
"demo"
],
"x-omni-max-body-bytes": 2048,
"x-omni-multipart-limits": {
"maxBytes": 2048,
"maxFieldBytes": 65536,
"maxFileBytes": 2048,
"maxParts": 128
},
"x-omni-request-policy": {
"unknownQuery": "",
"utf8": "",
"charset": "",
"body": "auto"
},
"x-omni-response": "CommonResponse"
}
}
},
"x-omni-contract-sources": {
"files": {
"managed.api": "cec14fe0da47a83804bf461be047df3e004ee470cbc3ea330dabc1537d596672"
},
"shared": {}
},
"x-omni-default-response": "CommonResponse"
}

View File

@@ -2,7 +2,7 @@ module example.com/managed
go 1.22.0
require git.yuntuo.hk/k2safe/go-omni v0.13.0-preview
require git.yuntuo.hk/k2safe/go-omni v0.14.0-preview
require (
github.com/gabriel-vasile/mimetype v1.4.3 // indirect

View File

@@ -1,14 +1,11 @@
// Package settings owns this project's configuration; it adds no business identity model.
package config
import (
"errors"
"git.yuntuo.hk/k2safe/go-omni/config"
"git.yuntuo.hk/k2safe/go-omni/logx"
"os"
"strings"
"time"
"git.yuntuo.hk/k2safe/go-omni/config"
"git.yuntuo.hk/k2safe/go-omni/logx"
)
type Config struct {
@@ -26,44 +23,40 @@ func Default() Config {
return Config{Server: server}
}
// Load uses defaults -> strict YAML/JSON -> nonempty environment variables.
// Pass an empty path to use only defaults and environment.
// Load applies defaults -> file -> typed environment -> validation.
// New fields automatically use GO_OMNI_<JSON_PATH>, or an explicit env tag.
func Load(path string) (Config, error) {
cfg := Default()
var err error
cfg, err = config.ApplyDefaults(cfg)
if err != nil {
return Config{}, err
}
if path != "" {
cfg, err = config.DecodeFile(path, cfg)
if err != nil {
return Config{}, err
}
}
cfg, err = config.OverlayEnv(cfg, config.EnvOptions{Prefix: "GO_OMNI_"})
if err != nil {
return Config{}, err
}
// Preserve the documented framework aliases (HTTP_ADDR, LOG_LEVEL, etc).
cfg.Server, err = config.ApplyEnv(cfg.Server, "")
if err != nil {
return Config{}, err
}
// A stream explicitly selected by environment overrides a file profile.
if output := strings.TrimSpace(os.Getenv("GO_OMNI_LOG_OUTPUT")); output != "" && strings.TrimSpace(os.Getenv("GO_OMNI_LOG_FILE")) == "" {
cfg.Server.Log.File = ""
cfg.Server.Log.Rotation = logx.RotationConfig{}
}
if err = cfg.Validate(); err != nil {
return Config{}, err
}
return cfg, nil
return cfg, cfg.Validate()
}
func (c Config) Validate() error {
if err := c.Server.Validate(); err != nil {
return err
}
if c.Server.Log.Format != "json" && c.Server.Log.Format != "text" {
return errors.New("log.format must be json or text")
}
switch strings.ToLower(c.Server.Log.Level) {
case "debug", "info", "warn", "warning", "error":
default:
return errors.New("invalid log.level")
}
return nil
}

View File

@@ -4,6 +4,7 @@ package handler
import (
"fmt"
omni "git.yuntuo.hk/k2safe/go-omni"
"git.yuntuo.hk/k2safe/go-omni/contract"
"example.com/managed/internal/svc"
"git.yuntuo.hk/k2safe/go-omni/config"
@@ -16,11 +17,13 @@ func NewApp(deps *svc.ServiceContext) (*omni.App, error) {
return nil, fmt.Errorf("service context and lifecycle controller required; see PROJECT.md upgrade instructions")
}
cfg := deps.Config.Server
cfg.LogInstance = deps.Logger
for _, option := range deps.AppOptions() {
if option != nil {
option(&cfg)
}
}
cfg.Request = contract.MergePolicy(cfg.Request, contract.RequestPolicy{UnknownQuery: "", UTF8: "", Charset: "", Body: ""})
cfg.Response = newResponseTemplate("{\"body\":null,\"errorBody\":null,\"messageField\":\"msg\",\"requestIdField\":\"-\",\"codeType\":\"int32\",\"successCode\":0,\"errorCode\":9999,\"dataMode\":\"always\"}").Config()
cfg.ResponseEncoder = nil
if err := cfg.Validate(); err != nil {

View File

@@ -36,7 +36,7 @@ var checkRequest = contract.MustNew[types.CheckReq]()
func (h *Handler) Check(c *httpx.Context) error {
var req types.CheckReq
if err := checkRequest.Bind(c.Request(), httpx.RouteParams(c.Context()), &req); err != nil {
if err := checkRequest.BindWithPolicy(c.Request(), httpx.RouteParams(c.Context()), &req, contract.RequestPolicy{UnknownQuery: "", UTF8: "", Charset: "", Body: ""}); err != nil {
return err
}
resp, err := h.logic.Check(c.Context(), &req)
@@ -53,7 +53,7 @@ var otherRequest = contract.MustNew[types.CheckReq]()
func (h *Handler) Other(c *httpx.Context) error {
var req types.CheckReq
if err := otherRequest.Bind(c.Request(), httpx.RouteParams(c.Context()), &req); err != nil {
if err := otherRequest.BindWithPolicy(c.Request(), httpx.RouteParams(c.Context()), &req, contract.RequestPolicy{UnknownQuery: "", UTF8: "", Charset: "", Body: ""}); err != nil {
return err
}
resp, err := h.logic.Other(c.Context(), &req)
@@ -65,7 +65,13 @@ func (h *Handler) Other(c *httpx.Context) error {
}
return c.OK(resp)
}
var rawJSONRequest = contract.MustNew[struct{}]()
func (h *Handler) RawJSON(c *httpx.Context) error {
if err := rawJSONRequest.BindWithPolicy(c.Request(), httpx.RouteParams(c.Context()), &struct{}{}, contract.RequestPolicy{UnknownQuery: "", UTF8: "", Charset: "", Body: ""}); err != nil {
return err
}
resp, err := h.logic.RawJSON(c.Context())
if err != nil {
return err
@@ -75,7 +81,13 @@ func (h *Handler) RawJSON(c *httpx.Context) error {
}
return c.OK(resp)
}
var uploadRequest = contract.MustNew[struct{}]()
func (h *Handler) Upload(c *httpx.Context) error {
if err := uploadRequest.BindWithPolicy(c.Request(), httpx.RouteParams(c.Context()), &struct{}{}, contract.RequestPolicy{UnknownQuery: "", UTF8: "", Charset: "", Body: "auto"}, 2048); err != nil {
return err
}
body, err := httpx.NewBody(c.Request(), "multipart")
if err != nil {
return err
@@ -92,7 +104,13 @@ func (h *Handler) Upload(c *httpx.Context) error {
}
return c.OK(resp)
}
var callbackRequest = contract.MustNew[struct{}]()
func (h *Handler) Callback(c *httpx.Context) error {
if err := callbackRequest.BindWithPolicy(c.Request(), httpx.RouteParams(c.Context()), &struct{}{}, contract.RequestPolicy{UnknownQuery: "", UTF8: "", Charset: "", Body: "auto"}, 64); err != nil {
return err
}
body, err := httpx.NewBody(c.Request(), "raw")
if err != nil {
return err
@@ -112,7 +130,13 @@ func (h *Handler) Callback(c *httpx.Context) error {
}
return c.WriteStream(200, "text/plain", resp)
}
var downloadRequest = contract.MustNew[struct{}]()
func (h *Handler) Download(c *httpx.Context) error {
if err := downloadRequest.BindWithPolicy(c.Request(), httpx.RouteParams(c.Context()), &struct{}{}, contract.RequestPolicy{UnknownQuery: "", UTF8: "", Charset: "", Body: ""}); err != nil {
return err
}
resp, err := h.logic.Download(c.Context())
if err != nil {
if resp != nil {
@@ -122,7 +146,13 @@ func (h *Handler) Download(c *httpx.Context) error {
}
return c.WriteStream(200, "application/octet-stream", resp)
}
var slowRequest = contract.MustNew[struct{}]()
func (h *Handler) Slow(c *httpx.Context) error {
if err := slowRequest.BindWithPolicy(c.Request(), httpx.RouteParams(c.Context()), &struct{}{}, contract.RequestPolicy{UnknownQuery: "", UTF8: "", Charset: "", Body: ""}); err != nil {
return err
}
resp, err := h.logic.Slow(c.Context())
if err != nil {
return err

File diff suppressed because one or more lines are too long

View File

@@ -1,39 +0,0 @@
package svc
import "errors"
// Close is idempotent, including startup failure and graceful shutdown.
func (c *ServiceContext) Close() error {
if c == nil {
return nil
}
c.closeOnce.Do(func() {
if c.Lifecycle != nil {
c.Lifecycle.RequestShutdown(nil)
}
c.closeMu.Lock()
c.closed = true
cleanups := c.cleanups
c.cleanups = nil
c.closeMu.Unlock()
for i := len(cleanups) - 1; i >= 0; i-- {
c.closeErr = errors.Join(c.closeErr, cleanups[i]())
}
})
return c.closeErr
}
// OnClose transfers a resource cleanup to this context immediately after creation.
// Hooks run once in reverse order, also when later initialization fails.
func (c *ServiceContext) OnClose(cleanup func() error) {
if cleanup == nil {
return
}
c.closeMu.Lock()
defer c.closeMu.Unlock()
if c.closed {
panic("svc: register cleanup after Close")
}
c.cleanups = append(c.cleanups, cleanup)
}

View File

@@ -0,0 +1,56 @@
// Code generated by go-omni api. DO NOT EDIT.
package svc
import (
"context"
"errors"
"git.yuntuo.hk/k2safe/go-omni/health"
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
"git.yuntuo.hk/k2safe/go-omni/logx"
"example.com/managed/internal/config"
"example.com/managed/internal/middleware"
)
// New is generator-owned. Customize dependencies in ServiceContext.Init.
func New(ctx context.Context, logger *logx.Logger, configs ...config.Config) (result *ServiceContext, err error) {
if ctx == nil {
ctx = context.Background()
}
if err = ctx.Err(); err != nil {
return nil, errors.Join(err, lifecycle.Cause(ctx))
}
if logger == nil {
return nil, errors.New("service context requires a logger")
}
if len(configs) > 1 {
return nil, errors.New("expected at most one configuration")
}
cfg := config.Default()
if len(configs) == 1 {
cfg = configs[0]
}
if err = cfg.Validate(); err != nil {
return nil, err
}
checker, err := health.New()
if err != nil {
return nil, err
}
c := &ServiceContext{Config: cfg, Logger: logger, Health: checker, Middlewares: middleware.Defaults(), Runtime: lifecycle.NewRuntime(ctx)}
ctx = logx.WithLogger(c.Context(), logger)
defer func() {
if p := recover(); p != nil {
_ = c.Close()
panic(p)
}
if err != nil {
err = c.Finish(err)
}
}()
if err = c.Init(ctx); err != nil {
return nil, err
}
return c, nil
}

View File

@@ -2,66 +2,25 @@ package svc
import (
"context"
"errors"
"sync"
"example.com/managed/internal/config"
"example.com/managed/internal/middleware"
"git.yuntuo.hk/k2safe/go-omni/health"
"git.yuntuo.hk/k2safe/go-omni/httpx"
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
"git.yuntuo.hk/k2safe/go-omni/logx"
)
// Context owns infrastructure, while Logger is borrowed from main.
// Add business repositories here; HTTP contracts do not define database tables.
// Context holds project dependencies. Runtime owns cleanup and shutdown.
// Logger is borrowed; the generated entry closes it after dependencies.
type ServiceContext struct {
Lifecycle *lifecycle.Controller
*lifecycle.Runtime
Config config.Config
cleanups []func() error
Logger *logx.Logger
Health *health.Checker
Middlewares map[string]httpx.Middleware
closeMu sync.Mutex
closed bool
closeOnce sync.Once
closeErr error
}
// Pass the settings loaded by main. The optional form supports lightweight
// tests without infrastructure; selected adapters still require valid config.
func New(ctx context.Context, logger *logx.Logger, configs ...config.Config) (result *ServiceContext, err error) {
if ctx == nil {
ctx = context.Background()
}
if err = ctx.Err(); err != nil {
return nil, errors.Join(err, lifecycle.Cause(ctx))
}
if len(configs) > 1 {
return nil, errors.New("expected at most one configuration")
}
cfg := config.Default()
if len(configs) == 1 {
cfg = configs[0]
}
_ = cfg
checker, err := health.New()
if err != nil {
return nil, err
}
c := &ServiceContext{Config: cfg, Logger: logger, Health: checker, Middlewares: middleware.Defaults(), Lifecycle: lifecycle.New(ctx)}
ctx = c.Lifecycle.Context()
defer func() {
if err != nil {
err = errors.Join(err, c.Close())
if cause := c.Lifecycle.Err(); cause != nil && !errors.Is(err, cause) {
err = errors.Join(err, cause)
}
}
}()
return c, nil
}
func (c *ServiceContext) RequestShutdown(err error) { c.Lifecycle.RequestShutdown(err) }
// Init is project-owned. Add dependencies here and immediately register each
// cleanup with c.OnClose. Returning an error automatically rolls everything back.
// Generated infrastructure is ready here. Never create another App or logger.
func (c *ServiceContext) Init(ctx context.Context) error { return nil }

View File

@@ -0,0 +1,9 @@
// Code generated by go-omni api. DO NOT EDIT.
package types
type CheckReq struct {
Kind string `form:"kind,optional"`
}
type Receipt struct {
Bytes int64 `json:"bytes"`
}

View File

@@ -1,9 +1,2 @@
// Code generated by go-omni api. DO NOT EDIT.
package types
type CheckReq struct {
Kind string `form:"kind,optional"`
}
type Receipt struct {
Bytes int64 `json:"bytes"`
}

View File

@@ -34,19 +34,13 @@ func run(ctx context.Context, path string) (runErr error) {
return err
}
defer func() { runErr = errors.Join(runErr, logger.Close()) }()
logx.SetDefault(logger)
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())
if cause := deps.Lifecycle.Err(); cause != nil && !errors.Is(runErr, cause) {
runErr = errors.Join(runErr, cause)
}
}()
defer func() { runErr = deps.Finish(runErr) }()
app, err := handler.NewApp(deps)
if err != nil {
return err