feat: complete contract-driven business development and MySQL integration
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
56
examples/managedService/internal/svc/runtimeGen.go
Normal file
56
examples/managedService/internal/svc/runtimeGen.go
Normal 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
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user