feat: release managed service generation and contract diagnostics

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

View File

@@ -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: "上传内容不完整"},
},
}
}

View 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
}

View 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
}

View 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)
}

File diff suppressed because one or more lines are too long

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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} }

View 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)
}

View 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
}

View 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
}
}

View 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
}

View File

@@ -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{}
}

View File

@@ -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

View 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)
}

View 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)
}
},
}
}

View 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
}

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"`
}