feat: release managed service generation and contract diagnostics
This commit is contained in:
8
examples/managedService/.gitignore
vendored
Normal file
8
examples/managedService/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/server
|
||||
/var/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
/.goOmni/api.lock
|
||||
|
||||
/managed
|
||||
161
examples/managedService/.goOmni/apiManifest.json
Normal file
161
examples/managedService/.goOmni/apiManifest.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"profile": {
|
||||
"database": "none",
|
||||
"storage": "none",
|
||||
"logOutput": "stderr",
|
||||
"logFormat": "json"
|
||||
},
|
||||
"version": 2,
|
||||
"module": "example.com/managed",
|
||||
"entry": "api/managed.api",
|
||||
"generated": {
|
||||
"docs/openapi.json": "9eb707aefeefd3f4881c774534e2d143949c0edb1d8a4aabd762f978f6203df1",
|
||||
"internal/handler/appGen.go": "dbadfb73165e0e6ef10e8277dd1b8a0edc2992dac3690d634808ac92829c2654",
|
||||
"internal/handler/demo/handlersGen.go": "7a6f9d5e105e44b07196cc540cee3f0562d5f3d1c5f0bc094434db1064d0339c",
|
||||
"internal/handler/openapiGen.go": "612920ed25e5dc7a4f673be56041d47b70aa3216707ff282e52dfe503c2d705a",
|
||||
"internal/handler/responseGen.go": "84397a502bba4777ebfa6fae3d3bc418297acab4f26b04b42e3c31b36c735399",
|
||||
"internal/handler/routesGen.go": "76c1a5b2644e75e56553dc6e9e862a63128d902dd94807ce7c8aee5491cd1e3e",
|
||||
"internal/middleware/registryGen.go": "e5f47efa57a1432765ef3419bae59dbf3b4d55b3aac0a6c935ce7a52ba6b47f4",
|
||||
"internal/types/typesGen.go": "7586e86fa5d67051283178cc578c890f6c02fd2d5c383170a71a39c2e77ce344",
|
||||
"main.go": "09ae81857b806f3f7dca608399b1881d94bc9c4b2258e774e77d694fcadbab65"
|
||||
},
|
||||
"contract": {
|
||||
"routes": {
|
||||
"demo/callback": {
|
||||
"method": "POST",
|
||||
"path": "/callback",
|
||||
"group": "demo",
|
||||
"handler": "callback",
|
||||
"signature": "Callback(context.Context, *httpx.Body) (*httpx.Stream, error)",
|
||||
"bodyMode": "raw",
|
||||
"produces": "text/plain",
|
||||
"maxBodyBytes": 64
|
||||
},
|
||||
"demo/check": {
|
||||
"method": "GET",
|
||||
"path": "/check",
|
||||
"group": "demo",
|
||||
"handler": "check",
|
||||
"signature": "Check(context.Context, *types.CheckReq) (*types.Receipt, error)",
|
||||
"request": "CheckReq",
|
||||
"response": "Receipt",
|
||||
"types": {
|
||||
"CheckReq": {
|
||||
"fields": [
|
||||
{
|
||||
"name": "Kind",
|
||||
"type": "string",
|
||||
"tag": "form:\"kind,optional\""
|
||||
}
|
||||
]
|
||||
},
|
||||
"Receipt": {
|
||||
"fields": [
|
||||
{
|
||||
"name": "Bytes",
|
||||
"type": "int64",
|
||||
"tag": "json:\"bytes\""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"demo/download": {
|
||||
"method": "GET",
|
||||
"path": "/download",
|
||||
"group": "demo",
|
||||
"handler": "download",
|
||||
"signature": "Download(context.Context) (*httpx.Stream, error)",
|
||||
"produces": "application/octet-stream"
|
||||
},
|
||||
"demo/other": {
|
||||
"method": "GET",
|
||||
"path": "/other",
|
||||
"group": "demo",
|
||||
"handler": "other",
|
||||
"signature": "Other(context.Context, *types.CheckReq) (*types.Receipt, error)",
|
||||
"request": "CheckReq",
|
||||
"response": "Receipt",
|
||||
"types": {
|
||||
"CheckReq": {
|
||||
"fields": [
|
||||
{
|
||||
"name": "Kind",
|
||||
"type": "string",
|
||||
"tag": "form:\"kind,optional\""
|
||||
}
|
||||
]
|
||||
},
|
||||
"Receipt": {
|
||||
"fields": [
|
||||
{
|
||||
"name": "Bytes",
|
||||
"type": "int64",
|
||||
"tag": "json:\"bytes\""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"demo/rawJSON": {
|
||||
"method": "GET",
|
||||
"path": "/raw",
|
||||
"group": "demo",
|
||||
"handler": "rawJSON",
|
||||
"signature": "RawJSON(context.Context) (*types.Receipt, error)",
|
||||
"response": "Receipt",
|
||||
"types": {
|
||||
"Receipt": {
|
||||
"fields": [
|
||||
{
|
||||
"name": "Bytes",
|
||||
"type": "int64",
|
||||
"tag": "json:\"bytes\""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"demo/slow": {
|
||||
"method": "GET",
|
||||
"path": "/slow",
|
||||
"group": "demo",
|
||||
"handler": "slow",
|
||||
"signature": "Slow(context.Context) (*types.Receipt, error)",
|
||||
"response": "Receipt",
|
||||
"types": {
|
||||
"Receipt": {
|
||||
"fields": [
|
||||
{
|
||||
"name": "Bytes",
|
||||
"type": "int64",
|
||||
"tag": "json:\"bytes\""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"demo/upload": {
|
||||
"method": "POST",
|
||||
"path": "/upload",
|
||||
"group": "demo",
|
||||
"handler": "upload",
|
||||
"signature": "Upload(context.Context, *httpx.Body) (*types.Receipt, error)",
|
||||
"response": "Receipt",
|
||||
"bodyMode": "multipart",
|
||||
"maxBodyBytes": 2048,
|
||||
"types": {
|
||||
"Receipt": {
|
||||
"fields": [
|
||||
{
|
||||
"name": "Bytes",
|
||||
"type": "int64",
|
||||
"tag": "json:\"bytes\""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
175
examples/managedService/README.md
Normal file
175
examples/managedService/README.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# 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. Try:
|
||||
|
||||
```sh
|
||||
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
|
||||
|
||||
```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.
|
||||
57
examples/managedService/api/managed.api
Normal file
57
examples/managedService/api/managed.api
Normal file
@@ -0,0 +1,57 @@
|
||||
syntax = "v1"
|
||||
|
||||
// Error definitions are intentionally owned by Go, not duplicated here.
|
||||
@app (
|
||||
defaultResponse: CommonResponse
|
||||
)
|
||||
response CommonResponse (
|
||||
codeType: int32
|
||||
successCode: 0
|
||||
errorCode: 9999
|
||||
messageField: msg
|
||||
dataMode: always
|
||||
requestIdField: "-"
|
||||
)
|
||||
response AlternateResponse (
|
||||
codeType: int32
|
||||
successCode: 0
|
||||
errorCode: 9999
|
||||
messageField: detail
|
||||
dataMode: always
|
||||
requestIdField: "-"
|
||||
)
|
||||
type CheckReq {
|
||||
Kind string `form:"kind,optional"`
|
||||
}
|
||||
type Receipt {
|
||||
Bytes int64 `json:"bytes"`
|
||||
}
|
||||
@server (group: demo)
|
||||
service managed-api {
|
||||
@handler check
|
||||
get /check (CheckReq) returns (Receipt)
|
||||
|
||||
@handler other
|
||||
@response AlternateResponse
|
||||
get /other (CheckReq) returns (Receipt)
|
||||
|
||||
@handler rawJSON
|
||||
@response raw
|
||||
get /raw returns (Receipt)
|
||||
|
||||
@handler upload
|
||||
@body (mode: multipart; maxBytes: 2048)
|
||||
post /upload returns (Receipt)
|
||||
|
||||
@handler callback
|
||||
@body (mode: raw; maxBytes: 64)
|
||||
@produces "text/plain"
|
||||
post /callback
|
||||
|
||||
@handler download
|
||||
@produces "application/octet-stream"
|
||||
get /download
|
||||
|
||||
@handler slow
|
||||
get /slow returns (Receipt)
|
||||
}
|
||||
1422
examples/managedService/docs/openapi.json
Normal file
1422
examples/managedService/docs/openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
12
examples/managedService/etc/config.yaml
Normal file
12
examples/managedService/etc/config.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
# Non-secret settings only. Environment variables override these values.
|
||||
server:
|
||||
address: '127.0.0.1:8080'
|
||||
enableLogger: true
|
||||
slowRequestThreshold: 500ms
|
||||
shutdownTimeout: 10s
|
||||
log:
|
||||
service: "example.com/managed"
|
||||
level: info
|
||||
format: json
|
||||
output: stderr
|
||||
redactKeys: []
|
||||
22
examples/managedService/go.mod
Normal file
22
examples/managedService/go.mod
Normal file
@@ -0,0 +1,22 @@
|
||||
module example.com/managed
|
||||
|
||||
go 1.22.0
|
||||
|
||||
require git.yuntuo.hk/k2safe/go-omni v0.12.0-preview
|
||||
|
||||
require (
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace git.yuntuo.hk/k2safe/go-omni => ../..
|
||||
34
examples/managedService/go.sum
Normal file
34
examples/managedService/go.sum
Normal file
@@ -0,0 +1,34 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
18
examples/managedService/internal/apierrors/catalog.go
Normal file
18
examples/managedService/internal/apierrors/catalog.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package apierrors
|
||||
|
||||
import omnierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||
|
||||
func Catalog() omnierrors.Catalog {
|
||||
return omnierrors.Catalog{
|
||||
DefaultStatus: 400,
|
||||
Definitions: map[string]omnierrors.Definition{
|
||||
"INVALID": {Code: 400002, Message: "用户名格式不正确"},
|
||||
"TOO_LONG": {Code: 400003, Message: "{label}不能超过{max}个字符", Params: []omnierrors.Parameter{{Name: "label", Type: "string"}, {Name: "max", Type: "int"}}},
|
||||
"INTERNAL_ERROR": {Status: 500, Code: 9999, Message: "系统错误"},
|
||||
"NOT_FOUND": {Status: 404, Code: 404001, Message: "接口不存在"},
|
||||
"METHOD_NOT_ALLOWED": {Status: 405, Code: 405001, Message: "请求方法不支持"},
|
||||
"BODY_TOO_LARGE": {Status: 413, Code: 413001, Message: "请求体太大"},
|
||||
"INVALID_MULTIPART": {Status: 400, Code: 400004, Message: "上传内容不完整"},
|
||||
},
|
||||
}
|
||||
}
|
||||
69
examples/managedService/internal/config/config.go
Normal file
69
examples/managedService/internal/config/config.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Package settings owns this project's configuration; it adds no business identity model.
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.yuntuo.hk/k2safe/go-omni/config"
|
||||
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server config.Config `json:"server"`
|
||||
}
|
||||
|
||||
func Default() Config {
|
||||
server := config.Default()
|
||||
server.Address = "127.0.0.1:8080"
|
||||
server.EnableLogger = true
|
||||
server.SlowRequestThreshold = 500 * time.Millisecond
|
||||
server.Log.Service = "example.com/managed"
|
||||
server.Log.Format = "json"
|
||||
server.Log.Output = "stderr"
|
||||
return Config{Server: server}
|
||||
}
|
||||
|
||||
// Load uses defaults -> strict YAML/JSON -> nonempty environment variables.
|
||||
// Pass an empty path to use only defaults and environment.
|
||||
func Load(path string) (Config, error) {
|
||||
cfg := Default()
|
||||
var err error
|
||||
if path != "" {
|
||||
cfg, err = config.DecodeFile(path, cfg)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
36
examples/managedService/internal/handler/appGen.go
Normal file
36
examples/managedService/internal/handler/appGen.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Code generated by go-omni api. DO NOT EDIT.
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
omni "git.yuntuo.hk/k2safe/go-omni"
|
||||
|
||||
"example.com/managed/internal/svc"
|
||||
)
|
||||
|
||||
// NewApp is generated HTTP composition. The caller owns deps and its logger.
|
||||
func NewApp(deps *svc.ServiceContext) (*omni.App, error) {
|
||||
if deps == nil || deps.Health == nil {
|
||||
return nil, fmt.Errorf("service context and health checker required")
|
||||
}
|
||||
cfg := deps.Config.Server
|
||||
for _, option := range deps.AppOptions() {
|
||||
if option != nil {
|
||||
option(&cfg)
|
||||
}
|
||||
}
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
app := omni.New(omni.WithConfig(cfg))
|
||||
app.Use(deps.GlobalMiddleware()...)
|
||||
if err := RegisterRoutes(app, deps); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
app.GET("/openapi.json", OpenAPI)
|
||||
app.GET("/health/live", deps.Health.LivenessHandler())
|
||||
app.GET("/health/ready", deps.Health.ReadinessHandler())
|
||||
return app, nil
|
||||
}
|
||||
134
examples/managedService/internal/handler/demo/handlersGen.go
Normal file
134
examples/managedService/internal/handler/demo/handlersGen.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// Code generated by go-omni api. DO NOT EDIT.
|
||||
package demo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"example.com/managed/internal/svc"
|
||||
"example.com/managed/internal/types"
|
||||
"fmt"
|
||||
"git.yuntuo.hk/k2safe/go-omni/contract"
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
)
|
||||
|
||||
type Logic interface {
|
||||
Check(ctx context.Context, req *types.CheckReq) (*types.Receipt, error)
|
||||
Other(ctx context.Context, req *types.CheckReq) (*types.Receipt, error)
|
||||
RawJSON(ctx context.Context) (*types.Receipt, error)
|
||||
Upload(ctx context.Context, body *httpx.Body) (*types.Receipt, error)
|
||||
Callback(ctx context.Context, body *httpx.Body) (*httpx.Stream, error)
|
||||
Download(ctx context.Context) (*httpx.Stream, error)
|
||||
Slow(ctx context.Context) (*types.Receipt, error)
|
||||
}
|
||||
type Handler struct {
|
||||
logic Logic
|
||||
deps *svc.ServiceContext
|
||||
}
|
||||
|
||||
func New(s Logic, deps ...*svc.ServiceContext) *Handler {
|
||||
h := &Handler{logic: s}
|
||||
if len(deps) > 0 {
|
||||
h.deps = deps[0]
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
resp, err := h.logic.Check(c.Context(), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp == nil {
|
||||
return fmt.Errorf("Check returned a nil response")
|
||||
}
|
||||
return c.OK(resp)
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
resp, err := h.logic.Other(c.Context(), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp == nil {
|
||||
return fmt.Errorf("Other returned a nil response")
|
||||
}
|
||||
return c.OK(resp)
|
||||
}
|
||||
func (h *Handler) RawJSON(c *httpx.Context) error {
|
||||
resp, err := h.logic.RawJSON(c.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp == nil {
|
||||
return fmt.Errorf("RawJSON returned a nil response")
|
||||
}
|
||||
return c.OK(resp)
|
||||
}
|
||||
func (h *Handler) Upload(c *httpx.Context) error {
|
||||
body, err := httpx.NewBody(c.Request(), "multipart")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := h.logic.Upload(c.Context(), body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp == nil {
|
||||
return fmt.Errorf("Upload returned a nil response")
|
||||
}
|
||||
if err := body.Complete(); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.OK(resp)
|
||||
}
|
||||
func (h *Handler) Callback(c *httpx.Context) error {
|
||||
body, err := httpx.NewBody(c.Request(), "raw")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := h.logic.Callback(c.Context(), body)
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
_ = resp.Close()
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := body.Complete(); err != nil {
|
||||
if resp != nil {
|
||||
_ = resp.Close()
|
||||
}
|
||||
return err
|
||||
}
|
||||
return c.WriteStream(200, "text/plain", resp)
|
||||
}
|
||||
func (h *Handler) Download(c *httpx.Context) error {
|
||||
resp, err := h.logic.Download(c.Context())
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
_ = resp.Close()
|
||||
}
|
||||
return err
|
||||
}
|
||||
return c.WriteStream(200, "application/octet-stream", resp)
|
||||
}
|
||||
func (h *Handler) Slow(c *httpx.Context) error {
|
||||
resp, err := h.logic.Slow(c.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp == nil {
|
||||
return fmt.Errorf("Slow returned a nil response")
|
||||
}
|
||||
return c.OK(resp)
|
||||
}
|
||||
13
examples/managedService/internal/handler/openapiGen.go
Normal file
13
examples/managedService/internal/handler/openapiGen.go
Normal file
File diff suppressed because one or more lines are too long
41
examples/managedService/internal/handler/responseGen.go
Normal file
41
examples/managedService/internal/handler/responseGen.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Code generated by go-omni api. DO NOT EDIT.
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
omnierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||
"git.yuntuo.hk/k2safe/go-omni/response"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var responseTemplates = map[string]response.Encoder{"": response.Default(),
|
||||
"CommonResponse": newResponseTemplate("{\"body\":null,\"errorBody\":null,\"messageField\":\"msg\",\"requestIdField\":\"-\",\"codeType\":\"int32\",\"successCode\":0,\"errorCode\":9999,\"dataMode\":\"always\"}"),
|
||||
"AlternateResponse": newResponseTemplate("{\"body\":null,\"errorBody\":null,\"messageField\":\"detail\",\"requestIdField\":\"-\",\"codeType\":\"int32\",\"successCode\":0,\"errorCode\":9999,\"dataMode\":\"always\"}"),
|
||||
}
|
||||
|
||||
func newResponseTemplate(raw string) *response.Formatter {
|
||||
var cfg response.Config
|
||||
decoder := json.NewDecoder(strings.NewReader(raw))
|
||||
decoder.UseNumber()
|
||||
if err := decoder.Decode(&cfg); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return response.MustNew(cfg)
|
||||
}
|
||||
func responseEncoder(name string, raw bool, catalogJSON string) response.Encoder {
|
||||
encoder := responseTemplates[name]
|
||||
if encoder == nil {
|
||||
panic("unknown response template")
|
||||
}
|
||||
var catalog omnierrors.Catalog
|
||||
if err := json.Unmarshal([]byte(catalogJSON), &catalog); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(catalog.Definitions) > 0 {
|
||||
encoder = response.MustWithErrors(catalog, encoder)
|
||||
}
|
||||
if raw {
|
||||
encoder = response.RawJSON(encoder)
|
||||
}
|
||||
return encoder
|
||||
}
|
||||
26
examples/managedService/internal/handler/routesGen.go
Normal file
26
examples/managedService/internal/handler/routesGen.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Code generated by go-omni api. DO NOT EDIT.
|
||||
package handler
|
||||
|
||||
import (
|
||||
h0 "example.com/managed/internal/handler/demo"
|
||||
s0 "example.com/managed/internal/logic/demo"
|
||||
"example.com/managed/internal/svc"
|
||||
"fmt"
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
"git.yuntuo.hk/k2safe/go-omni/openapi"
|
||||
)
|
||||
|
||||
func RegisterRoutes(router openapi.Registrar, deps *svc.ServiceContext) error {
|
||||
if router == nil || deps == nil {
|
||||
return fmt.Errorf("router and dependencies required")
|
||||
}
|
||||
h0 := h0.New(s0.New(deps), deps)
|
||||
httpx.HandleWithResponse(router, "GET", "/check", responseEncoder("CommonResponse", false, "{\"definitions\":{}}"), h0.Check)
|
||||
httpx.HandleWithResponse(router, "GET", "/other", responseEncoder("AlternateResponse", false, "{\"definitions\":{}}"), h0.Other)
|
||||
httpx.HandleWithResponse(router, "GET", "/raw", responseEncoder("CommonResponse", true, "{\"definitions\":{}}"), h0.RawJSON)
|
||||
httpx.HandleWithBodyLimit(router, "POST", "/upload", 2048, responseEncoder("CommonResponse", false, "{\"definitions\":{}}"), h0.Upload)
|
||||
httpx.HandleWithBodyLimit(router, "POST", "/callback", 64, responseEncoder("CommonResponse", false, "{\"definitions\":{}}"), h0.Callback)
|
||||
httpx.HandleWithResponse(router, "GET", "/download", responseEncoder("CommonResponse", false, "{\"definitions\":{}}"), h0.Download)
|
||||
httpx.HandleWithResponse(router, "GET", "/slow", responseEncoder("CommonResponse", false, "{\"definitions\":{}}"), h0.Slow)
|
||||
return nil
|
||||
}
|
||||
22
examples/managedService/internal/logic/demo/callback.go
Normal file
22
examples/managedService/internal/logic/demo/callback.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package demo
|
||||
|
||||
import (
|
||||
"context"
|
||||
omnierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (l *Logic) Callback(ctx context.Context, body *httpx.Body) (*httpx.Stream, error) {
|
||||
// The route's 64-byte limit bounds this example's memory use. A real signature
|
||||
// implementation can hash body.Raw incrementally and authenticate its headers.
|
||||
data, err := io.ReadAll(body.Raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if body.Header.Get("X-Signature") != "valid" {
|
||||
return nil, omnierrors.ByCode("INVALID")
|
||||
}
|
||||
return &httpx.Stream{Content: io.NopCloser(strings.NewReader("ACK:" + string(data)))}, nil
|
||||
}
|
||||
20
examples/managedService/internal/logic/demo/check.go
Normal file
20
examples/managedService/internal/logic/demo/check.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package demo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"example.com/managed/internal/types"
|
||||
omnierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||
)
|
||||
|
||||
func (l *Logic) Check(ctx context.Context, req *types.CheckReq) (*types.Receipt, error) {
|
||||
switch req.Kind {
|
||||
case "fixed":
|
||||
return nil, omnierrors.ByCode("INVALID")
|
||||
case "params":
|
||||
return nil, omnierrors.ByCode("TOO_LONG").WithParams(map[string]any{"label": "企业名称", "max": 100})
|
||||
case "unknown":
|
||||
return nil, omnierrors.ByCode("UNKNOWN").WithCause(errors.New("private cause"))
|
||||
}
|
||||
return &types.Receipt{Bytes: 42}, nil
|
||||
}
|
||||
12
examples/managedService/internal/logic/demo/download.go
Normal file
12
examples/managedService/internal/logic/demo/download.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package demo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (l *Logic) Download(context.Context) (*httpx.Stream, error) {
|
||||
return &httpx.Stream{Content: io.NopCloser(strings.NewReader("file-content")), Name: "example.txt"}, nil
|
||||
}
|
||||
7
examples/managedService/internal/logic/demo/logic.go
Normal file
7
examples/managedService/internal/logic/demo/logic.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package demo
|
||||
|
||||
import "example.com/managed/internal/svc"
|
||||
|
||||
type Logic struct{ deps *svc.ServiceContext }
|
||||
|
||||
func New(deps *svc.ServiceContext) *Logic { return &Logic{deps: deps} }
|
||||
10
examples/managedService/internal/logic/demo/other.go
Normal file
10
examples/managedService/internal/logic/demo/other.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package demo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"example.com/managed/internal/types"
|
||||
)
|
||||
|
||||
func (l *Logic) Other(ctx context.Context, req *types.CheckReq) (*types.Receipt, error) {
|
||||
return l.Check(ctx, req)
|
||||
}
|
||||
10
examples/managedService/internal/logic/demo/rawJSON.go
Normal file
10
examples/managedService/internal/logic/demo/rawJSON.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package demo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"example.com/managed/internal/types"
|
||||
)
|
||||
|
||||
func (l *Logic) RawJSON(context.Context) (*types.Receipt, error) {
|
||||
return &types.Receipt{Bytes: 3}, nil
|
||||
}
|
||||
16
examples/managedService/internal/logic/demo/slow.go
Normal file
16
examples/managedService/internal/logic/demo/slow.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package demo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"example.com/managed/internal/types"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (l *Logic) Slow(ctx context.Context) (*types.Receipt, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
return &types.Receipt{Bytes: 1}, nil
|
||||
}
|
||||
}
|
||||
31
examples/managedService/internal/logic/demo/upload.go
Normal file
31
examples/managedService/internal/logic/demo/upload.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package demo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"example.com/managed/internal/types"
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
"io"
|
||||
)
|
||||
|
||||
func (l *Logic) Upload(ctx context.Context, body *httpx.Body) (*types.Receipt, error) {
|
||||
var total int64
|
||||
for {
|
||||
part, err := body.Parts.NextPart()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n, err := io.Copy(io.Discard, part)
|
||||
closeErr := part.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if closeErr != nil {
|
||||
return nil, closeErr
|
||||
}
|
||||
total += n
|
||||
}
|
||||
return &types.Receipt{Bytes: total}, nil
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Code generated by go-omni api. DO NOT EDIT.
|
||||
package middleware
|
||||
|
||||
import "git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
|
||||
func Defaults() map[string]httpx.Middleware {
|
||||
return map[string]httpx.Middleware{}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Package demo owns persistence contracts for this business group.
|
||||
// Define repository interfaces and implementations here; HTTP DTOs do not
|
||||
// determine a database schema. This file is never overwritten.
|
||||
package demo
|
||||
36
examples/managedService/internal/svc/close.go
Normal file
36
examples/managedService/internal/svc/close.go
Normal file
@@ -0,0 +1,36 @@
|
||||
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() {
|
||||
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)
|
||||
}
|
||||
24
examples/managedService/internal/svc/options.go
Normal file
24
examples/managedService/internal/svc/options.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"example.com/managed/internal/apierrors"
|
||||
omni "git.yuntuo.hk/k2safe/go-omni"
|
||||
omnierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
)
|
||||
|
||||
func (c *ServiceContext) AppOptions() []omni.Option {
|
||||
return []omni.Option{omni.WithErrors(apierrors.Catalog())}
|
||||
}
|
||||
func (c *ServiceContext) GlobalMiddleware() []httpx.Middleware {
|
||||
return []httpx.Middleware{
|
||||
func(next httpx.Handler) httpx.Handler {
|
||||
return func(c *httpx.Context) error {
|
||||
if c.Header("X-Fail") != "" {
|
||||
return omnierrors.ByCode("INVALID")
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
59
examples/managedService/internal/svc/serviceContext.go
Normal file
59
examples/managedService/internal/svc/serviceContext.go
Normal file
@@ -0,0 +1,59 @@
|
||||
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/logx"
|
||||
)
|
||||
|
||||
// Context owns infrastructure, while Logger is borrowed from main.
|
||||
// Add business repositories here; HTTP contracts do not define database tables.
|
||||
type ServiceContext struct {
|
||||
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, err
|
||||
}
|
||||
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()}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = errors.Join(err, c.Close())
|
||||
}
|
||||
}()
|
||||
|
||||
return c, nil
|
||||
}
|
||||
9
examples/managedService/internal/types/typesGen.go
Normal file
9
examples/managedService/internal/types/typesGen.go
Normal 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"`
|
||||
}
|
||||
52
examples/managedService/main.go
Normal file
52
examples/managedService/main.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Code generated by go-omni api. DO NOT EDIT.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"example.com/managed/internal/config"
|
||||
"example.com/managed/internal/handler"
|
||||
"example.com/managed/internal/svc"
|
||||
"flag"
|
||||
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func main() {
|
||||
path := flag.String("config", "etc/config.yaml", "configuration file; empty uses defaults and environment")
|
||||
flag.Parse()
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
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 := 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, 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)
|
||||
}
|
||||
Reference in New Issue
Block a user