Files
go-omni/cmd/go-omni/apigen/generate.go

549 lines
23 KiB
Go

package apigen
import (
"encoding/json"
"errors"
"fmt"
"go/format"
"os"
"path/filepath"
"runtime/debug"
"sort"
"strconv"
"strings"
"git.yuntuo.hk/k2safe/go-omni/cmd/go-omni/scaffold"
"golang.org/x/mod/modfile"
"golang.org/x/mod/module"
"golang.org/x/mod/semver"
)
const frameworkModule = "git.yuntuo.hk/k2safe/go-omni"
const minimumVersion = "v0.10.0-preview"
const generatedHeader = "// Code generated by go-omni api. DO NOT EDIT.\n"
type Options struct {
Database, Storage string
API, Dir, Module, FrameworkDir, FrameworkVersion, LogOutput, LogFormat string
Check bool
DryRun bool
}
type FileChange struct {
Path string
Action string
}
type Output struct {
Generated, Created, Preserved, Removed []string
Changes []FileChange
}
// Generate creates or safely updates one contract-driven project. It renders
// and validates every output before applying an ownership-checked transaction.
func Generate(o Options) (Output, error) {
if o.Check && o.DryRun {
return Output{}, errors.New("--check and --dry-run cannot be combined")
}
if o.API == "" || o.Dir == "" {
return Output{}, errors.New("--api and --dir are required")
}
spec, err := Parse(o.API)
if err != nil {
return Output{}, err
}
compiled, err := Compile(spec)
if err != nil {
return Output{}, err
}
return generate(o, compiled, false)
}
const starterAPI = "syntax = \"v1\"\n\ninfo (\n title: \"Greeting API\"\n version: \"1.0.0\"\n)\n\ntype GreetReq {\n Name string `path:\"name\" binding:\"required,max=80\"`\n}\ntype GreetResp {\n Message string `json:\"message\"`\n}\n\n@server (\n group: greet\n prefix: /api\n)\nservice greet-api {\n @doc \"Greet a visitor\"\n @handler greet\n get /greet/:name (GreetReq) returns (GreetResp)\n}\n"
// New creates a runnable greeting project, including its editable API file.
func New(o Options) (Output, error) {
if o.API != "" || o.Check || o.DryRun {
return Output{}, errors.New("api new does not accept --api, --check or --dry-run")
}
if o.Dir == "" {
return Output{}, errors.New("project directory required")
}
if entries, err := os.ReadDir(o.Dir); err == nil && len(entries) > 0 {
return Output{}, errors.New("api new requires an empty directory")
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
return Output{}, err
}
tokens, err := lex("service.api", starterAPI)
if err != nil {
return Output{}, err
}
spec := &Spec{Version: "1.0.0", Entry: "service.api", Sources: map[string][]byte{"service.api": []byte(starterAPI)}}
p := parser{tokens: tokens, spec: spec}
if err = p.parse(); err != nil {
return Output{}, err
}
c, err := Compile(spec)
if err != nil {
return Output{}, err
}
return generate(o, c, true)
}
func generate(o Options, c *Compiled, starter bool) (Output, error) {
directory, err := filepath.Abs(o.Dir)
if err != nil {
return Output{}, err
}
o.Dir = directory
for _, r := range c.Spec.Routes {
if !r.Manual {
path := filepath.Join(directory, "internal/handler", r.Group, lower(r.Handler)+"Manual.go")
if _, err := os.Lstat(path); err == nil {
return Output{}, fmt.Errorf("manual handler %s exists; migrate and remove it explicitly before switching to automatic generation", path)
} else if !errors.Is(err, os.ErrNotExist) {
return Output{}, err
}
}
}
if err := resolveProfile(&o); err != nil {
return Output{}, err
}
seeds := map[string][]byte{}
moduleData, readErr := os.ReadFile(filepath.Join(directory, "go.mod"))
if readErr == nil {
mf, err := modfile.Parse("go.mod", moduleData, nil)
if err != nil || mf.Module == nil {
return Output{}, errors.New("invalid project go.mod")
}
if o.Module != "" && o.Module != mf.Module.Mod.Path {
return Output{}, errors.New("--module differs from existing go.mod")
}
o.Module = mf.Module.Mod.Path
found := false
requiredVersion := ""
for _, r := range mf.Require {
if r.Mod.Path == frameworkModule {
found = true
requiredVersion = r.Mod.Version
if o.FrameworkVersion != "" && o.FrameworkVersion != r.Mod.Version {
return Output{}, errors.New("existing framework version differs; update go.mod explicitly")
}
}
}
if !found {
return Output{}, errors.New("existing go.mod must require go-omni")
}
if semver.Compare(requiredVersion, minimumVersion) < 0 {
localCurrent := false
for _, r := range mf.Replace {
if r.Old.Path == frameworkModule && r.New.Version == "" {
path := r.New.Path
if !filepath.IsAbs(path) {
path = filepath.Join(directory, path)
}
if _, err := os.Stat(filepath.Join(path, "errors", "catalog.go")); err == nil {
localCurrent = true
}
}
}
if !localCurrent {
return Output{}, fmt.Errorf("existing framework dependency predates error catalog support; use %s or a current local replacement", minimumVersion)
}
}
if o.FrameworkDir != "" {
matched := false
for _, r := range mf.Replace {
if r.Old.Path == frameworkModule && r.New.Version == "" {
replacement := r.New.Path
if !filepath.IsAbs(replacement) {
replacement = filepath.Join(directory, replacement)
}
matched = canonical(replacement) == canonical(o.FrameworkDir)
}
}
if !matched {
return Output{}, errors.New("existing framework replacement differs; update go.mod explicitly")
}
}
} else if !errors.Is(readErr, os.ErrNotExist) {
return Output{}, readErr
} else {
if o.Module == "" {
o.Module = filepath.Base(directory)
}
data, err := moduleFile(o)
if err != nil {
return Output{}, err
}
seeds["go.mod"] = data
}
if err = module.CheckImportPath(o.Module); err != nil {
return Output{}, err
}
generated, base, err := renderProject(o, c, starter)
if err != nil {
return Output{}, err
}
for name, data := range base {
seeds[name] = data
}
// The API source remains developer-owned. A relocated project regenerates
// from its own api/ tree, including all imports.
entry := "api/" + c.Spec.Entry
for name, data := range c.Spec.Sources {
seeds["api/"+name] = data
}
if o.API != "" {
source, _ := filepath.Abs(o.API)
sourceReal, _ := filepath.EvalSymlinks(source)
targetReal := canonical(directory)
rel, e := filepath.Rel(targetReal, sourceReal)
if e == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
entry = filepath.ToSlash(rel)
for name := range c.Spec.Sources {
delete(seeds, "api/"+name)
}
}
}
// Source collisions must never leave a copied contract different from the
// generated code. On updates, use the developer-owned API in the project.
for name, data := range seeds {
if strings.HasSuffix(name, ".api") {
current, e := os.ReadFile(filepath.Join(directory, filepath.FromSlash(name)))
if e == nil && string(current) != string(data) {
return Output{}, fmt.Errorf("%s already differs; regenerate from that project API instead", name)
}
}
}
seeds["README.md"] = []byte(projectREADME(o.Module, entry) + infrastructureREADME(o))
for _, files := range []map[string][]byte{generated, seeds} {
for name, data := range files {
if strings.HasSuffix(name, ".go") {
data, err = format.Source(data)
if err != nil {
return Output{}, fmt.Errorf("format %s: %w", name, err)
}
files[name] = data
}
}
}
return apply(directory, o.Module, entry, generated, seeds, o.Check, o.DryRun, o.profile())
}
func moduleFile(o Options) ([]byte, error) {
if err := module.CheckImportPath(o.Module); err != nil {
return nil, err
}
root := o.FrameworkDir
version := o.FrameworkVersion
if root == "" && version == "" {
wd, _ := os.Getwd()
for wd != "" {
data, err := os.ReadFile(filepath.Join(wd, "go.mod"))
if err == nil {
mf, e := modfile.Parse("go.mod", data, nil)
if e == nil && mf.Module != nil && mf.Module.Mod.Path == frameworkModule {
root = wd
}
break
}
parent := filepath.Dir(wd)
if parent == wd {
break
}
wd = parent
}
}
if version == "" {
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Path == frameworkModule && info.Main.Sum != "" {
version = info.Main.Version
} else {
version = minimumVersion
}
}
if !semver.IsValid(version) || semver.Major(version) != "v0" && semver.Major(version) != "v1" {
return nil, errors.New("invalid framework version")
}
if root == "" && semver.Compare(version, minimumVersion) < 0 {
return nil, fmt.Errorf("API generation needs %s or newer; use --framework-dir for unpublished source", minimumVersion)
}
text := "module " + o.Module + "\n\ngo 1.22.0\n\nrequire " + frameworkModule + " " + version + "\n"
if root != "" {
root = canonical(root)
data, err := os.ReadFile(filepath.Join(root, "go.mod"))
if err != nil {
return nil, err
}
mf, err := modfile.Parse("go.mod", data, nil)
if err != nil || mf.Module == nil || mf.Module.Mod.Path != frameworkModule {
return nil, errors.New("--framework-dir must contain go-omni")
}
if _, err = os.Stat(filepath.Join(root, "contract", "contract.go")); err != nil {
return nil, errors.New("local framework lacks API contract support")
}
relative, err := filepath.Rel(canonical(o.Dir), root)
if err != nil {
return nil, err
}
text += "\nreplace " + frameworkModule + " => " + strconv.Quote(filepath.ToSlash(relative)) + "\n"
}
return []byte(text), nil
}
func canonical(path string) string {
path, _ = filepath.Abs(path)
candidate := path
var tail []string
for {
if resolved, err := filepath.EvalSymlinks(candidate); err == nil {
for i := len(tail) - 1; i >= 0; i-- {
resolved = filepath.Join(resolved, tail[i])
}
return resolved
}
parent := filepath.Dir(candidate)
if parent == candidate {
return path
}
tail = append(tail, filepath.Base(candidate))
candidate = parent
}
}
func renderProject(o Options, c *Compiled, starter bool) (map[string][]byte, map[string][]byte, error) {
generated, seeds := map[string][]byte{}, map[string][]byte{}
put := func(files map[string][]byte, name, content string) { files[name] = []byte(content) }
if len(c.Spec.Errors) > 0 {
content := "package apierrors\nimport(\"encoding/json\"; omnierrors " + strconv.Quote(frameworkModule+"/errors") + ")\n"
content += fmt.Sprintf("func Catalog()omnierrors.Catalog{var c omnierrors.Catalog;if err:=json.Unmarshal([]byte(%q),&c);err!=nil{panic(err)};return c}\nvar errorRegistry=omnierrors.MustRegistry(Catalog())\n", errorCatalogJSON(c.Spec, nil))
for _, e := range c.Spec.Errors {
content += fmt.Sprintf("const Code%s=%q\nfunc %s()*omnierrors.APIError{return errorRegistry.Resolve(omnierrors.ByCode(Code%s))}\n", e.Name, e.Code, e.Name, e.Name)
}
put(generated, "internal/apierrors/errorsGen.go", generatedHeader+content)
}
types := "package types\n\n"
if len(c.Spec.Scalars) > 0 {
types += "import("
for i, d := range c.Spec.Scalars {
types += fmt.Sprintf("scalar%d %q;", i, d.GoImport)
}
types += ")\n"
registry := fmt.Sprintf("package types\nimport %q\nfunc ContractScalars()[]contract.Scalar{return []contract.Scalar{", frameworkModule+"/contract")
for i, d := range c.Spec.Scalars {
types += fmt.Sprintf("type %s = scalar%d.%s\n", d.Name, i, d.GoType)
registry += fmt.Sprintf("contract.ScalarOf[%s](%q,%q,%t),", d.Name, d.JSONInput, d.JSONOutput, d.Text)
}
registry += "}}\n"
put(generated, "internal/types/scalarsGen.go", generatedHeader+registry)
}
for _, t := range c.Spec.Types {
types += "type " + t.Name + " struct {\n"
for _, f := range t.Fields {
types += fmt.Sprintf("%s %s `%s`\n", f.Name, f.Type, f.Tag)
}
types += "}\n"
}
put(generated, "internal/types/typesGen.go", generatedHeader+types)
put(generated, "docs/openapi.json", string(c.Document)+"\n")
put(generated, "internal/handler/openapiGen.go", generatedHeader+fmt.Sprintf("package handler\nimport (\"net/http\"; %q)\nconst OpenAPIDocument = %q\nfunc OpenAPI(c *httpx.Context) error { return c.WriteResponse(200,http.Header{\"Content-Type\": {\"application/json; charset=utf-8\"}},[]byte(OpenAPIDocument)) }\n", frameworkModule+"/httpx", string(c.Document)+"\n"))
policySource := fmt.Sprintf("package handler\nimport(\"encoding/json\";\"strings\";%q; omnierrors %q)\nvar responseTemplates=map[string]response.Encoder{\"\":response.Default(),\n", frameworkModule+"/response", frameworkModule+"/errors")
for _, definition := range c.Spec.Responses {
data, err := json.Marshal(definition.Config)
if err != nil {
return nil, nil, err
}
policySource += fmt.Sprintf("%q: newResponseTemplate(%q),\n", definition.Name, string(data))
}
policySource += "}\nfunc newResponseTemplate(raw string)response.Encoder{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)}\n"
policySource += "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)};encoder=response.MustWithErrors(catalog,encoder);if raw{encoder=response.RawJSON(encoder)};return encoder}\n"
put(generated, "internal/handler/responseGen.go", generatedHeader+policySource)
imports := fmt.Sprintf("import (\"fmt\"; %q; %q; %q;", frameworkModule+"/httpx", frameworkModule+"/openapi", o.Module+"/internal/servicecontext")
routes := "func RegisterRoutes(router openapi.Registrar, deps *servicecontext.Context) error {\n if router==nil || deps==nil {return fmt.Errorf(\"router and dependencies required\")}\n"
if len(c.Spec.Scalars) > 0 {
imports += fmt.Sprintf("%q;%q;", frameworkModule+"/contract", o.Module+"/internal/types")
routes += "if err:=contract.ValidateScalars(types.ContractScalars()...);err!=nil{return err}\n"
}
middlewareSet := map[string]bool{}
for _, r := range c.Spec.Routes {
for _, m := range r.Middleware {
middlewareSet[m] = true
}
}
var middlewareNames []string
for m := range middlewareSet {
middlewareNames = append(middlewareNames, m)
}
sort.Strings(middlewareNames)
for _, m := range middlewareNames {
routes += fmt.Sprintf("if deps.Middlewares[%q]==nil {return fmt.Errorf(%q)}\n", m, "missing middleware "+m)
}
for i, group := range c.Groups {
imports += fmt.Sprintf("h%d %q; s%d %q;", i, o.Module+"/internal/handler/"+group, i, o.Module+"/internal/service/"+group)
routes += fmt.Sprintf("h%d := h%d.New(s%d.New(deps),deps)\n", i, i, i)
groupRoutes := []Route{}
for _, r := range c.Spec.Routes {
if r.Group == group {
groupRoutes = append(groupRoutes, r)
}
}
packageName := filepath.Base(group)
text := "package " + packageName + "\nimport(\"context\"; \"fmt\";"
needsTypes, needsBinder, hasAutomatic := false, false, false
for _, r := range groupRoutes {
if r.Manual {
continue
}
hasAutomatic = true
needsTypes = needsTypes || r.Request != "" || r.Response != ""
needsBinder = needsBinder || (r.Request != "" && !r.CustomBinding)
}
text += fmt.Sprintf("%q;%q;", frameworkModule+"/httpx", o.Module+"/internal/servicecontext")
if needsTypes {
text += fmt.Sprintf("%q;", o.Module+"/internal/types")
}
if needsBinder {
text += fmt.Sprintf("%q;", frameworkModule+"/contract")
}
text += ")\n"
text += "type Service interface {\n"
for _, r := range groupRoutes {
if !r.Manual {
text += signature(r) + "\n"
}
}
text += "}\ntype Handler struct {service Service;deps *servicecontext.Context}\nfunc New(s Service,deps ...*servicecontext.Context)*Handler {h:=&Handler{service:s};if len(deps)>0{h.deps=deps[0]};return h}\n"
for _, r := range groupRoutes {
name := upper(r.Handler)
// The response policy and body reader cap are installed before App middleware.
if r.MaxBodyBytes > 0 {
routes += fmt.Sprintf("httpx.HandleWithBodyLimit(router,%q,%q,%d,responseEncoder(%q,%t,%q),h%d.%s", strings.ToUpper(r.Method), r.Path, r.MaxBodyBytes, responseName(r), r.ResponseFormat == "raw", errorCatalogJSON(c.Spec, append([]string{}, r.Errors...)), i, name)
} else {
routes += fmt.Sprintf("httpx.HandleWithResponse(router,%q,%q,responseEncoder(%q,%t,%q),h%d.%s", strings.ToUpper(r.Method), r.Path, responseName(r), r.ResponseFormat == "raw", errorCatalogJSON(c.Spec, append([]string{}, r.Errors...)), i, name)
}
for _, m := range r.Middleware {
routes += fmt.Sprintf(",deps.Middlewares[%q]", m)
}
routes += ")\n"
if r.Manual {
put(seeds, "internal/handler/"+group+"/"+lower(r.Handler)+"Manual.go", fmt.Sprintf("package %s\nimport(%q;apierrors %q)\n// %s owns raw/multipart binding and response output. h.deps holds project dependencies.\n// This file is preserved on regeneration.\nfunc(h *Handler)%s(c *httpx.Context)error{return apierrors.New(501,\"NOT_IMPLEMENTED\",\"manual handler is not implemented\")}\n", packageName, frameworkModule+"/httpx", frameworkModule+"/errors", name, name))
continue
}
if r.Request != "" && !r.CustomBinding {
scalarArgs := ""
if len(c.Spec.Scalars) > 0 {
scalarArgs = "types.ContractScalars()..."
}
text += fmt.Sprintf("var %sRequest = contract.MustNew[types.%s](%s)\n", lower(r.Handler), r.Request, scalarArgs)
}
text += fmt.Sprintf("func(h *Handler) %s(c *httpx.Context)error {\n", name)
call := "c.Context()"
if r.Request != "" {
if r.CustomBinding {
text += fmt.Sprintf("var req types.%s\nif err:=h.bind%s(c,&req);err!=nil{return err}\n", r.Request, name)
put(seeds, "internal/handler/"+group+"/"+lower(r.Handler)+"Bind.go", fmt.Sprintf("package %s\nimport(%q;%q;apierrors %q)\n// bind%s is project-owned; repeated generation preserves it.\nfunc(h *Handler)bind%s(c *httpx.Context,req *types.%s)error{return apierrors.New(501,\"BINDING_NOT_IMPLEMENTED\",\"custom binding is not implemented\")}\n", packageName, frameworkModule+"/httpx", o.Module+"/internal/types", frameworkModule+"/errors", name, name, r.Request))
} else {
limit := ""
if r.MaxBodyBytes > 0 {
limit = fmt.Sprintf(",%d", r.MaxBodyBytes)
}
text += fmt.Sprintf("var req types.%s\nif err:=%sRequest.Bind(c.Request(),httpx.RouteParams(c.Context()),&req%s);err!=nil{return err}\n", r.Request, lower(r.Handler), limit)
}
call += ", &req"
}
if r.Response != "" {
text += fmt.Sprintf("resp,err:=h.service.%s(%s);if err!=nil{return err};if resp==nil{return fmt.Errorf(\"%s returned a nil response\")}\n", name, call, name)
method := map[int]string{200: "OK", 201: "Created", 202: "Accepted"}[r.Status]
text += "return c." + method + "(resp)\n}\n"
} else {
text += fmt.Sprintf("if err:=h.service.%s(%s);err!=nil{return err};return c.NoContent()\n}\n", name, call)
}
service := fmt.Sprintf("package %s\nimport(\"context\"; apierrors %q;", packageName, frameworkModule+"/errors")
if r.Request != "" || r.Response != "" {
service += fmt.Sprintf("%q;", o.Module+"/internal/types")
}
service += ")\n"
service += "// " + name + " implements the business operation. This file is never overwritten.\nfunc(s *Service) " + signature(r) + " {\n"
if starter && r.Handler == "greet" {
service = strings.Replace(service, "apierrors "+strconv.Quote(frameworkModule+"/errors")+";", "\"fmt\";", 1)
service += "s.deps.Logger.InfoContext(ctx,\"greet.called\")\nreturn &types.GreetResp{Message:fmt.Sprintf(\"Hello %s\",req.Name)},nil\n}"
} else {
service += "// TODO: implement business behavior and use s.deps for dependencies.\n"
if r.Response != "" {
service += "return nil, "
} else {
service += "return "
}
service += "apierrors.New(501,\"NOT_IMPLEMENTED\",\"operation is not implemented\")\n}"
}
put(seeds, "internal/service/"+group+"/"+lower(r.Handler)+".go", service)
}
if !hasAutomatic {
text = strings.Replace(text, strconv.Quote(frameworkModule+"/httpx")+";", "", 1)
text = strings.Replace(text, "\"context\";", "", 1)
}
if !hasResponse(groupRoutes) {
text = strings.Replace(text, "\"fmt\";", "", 1)
}
put(generated, "internal/handler/"+group+"/handlersGen.go", generatedHeader+text)
put(seeds, "internal/service/"+group+"/service.go", fmt.Sprintf("package %s\nimport %q\ntype Service struct {deps *servicecontext.Context}\nfunc New(deps *servicecontext.Context)*Service{return &Service{deps:deps}}\n", packageName, o.Module+"/internal/servicecontext"))
put(seeds, "internal/repository/"+group+"/repository.go", "// Package "+packageName+" owns persistence contracts for this business group.\n// Define repository interfaces and implementations here; HTTP DTOs do not\n// determine a database schema. This file is never overwritten.\npackage "+packageName+"\n")
}
put(generated, "internal/handler/routesGen.go", generatedHeader+"package handler\n"+imports+")\n"+routes+"return nil\n}\n")
registry := "package middleware\nimport " + strconv.Quote(frameworkModule+"/httpx") + "\nfunc Defaults()map[string]httpx.Middleware{return map[string]httpx.Middleware{\n"
for _, m := range middlewareNames {
registry += fmt.Sprintf("%q:%s,\n", m, upper(m))
put(seeds, "internal/middleware/"+lower(m)+".go", fmt.Sprintf("package middleware\nimport(%q; apierrors %q)\n// %s is a developer-owned hook. Implement it before enabling this route.\nfunc %s(next httpx.Handler)httpx.Handler{return func(c *httpx.Context)error {return apierrors.New(501,\"MIDDLEWARE_NOT_IMPLEMENTED\",\"middleware is not implemented\")}}\n", frameworkModule+"/httpx", frameworkModule+"/errors", upper(m), upper(m)))
}
put(generated, "internal/middleware/registryGen.go", generatedHeader+registry+"}}\n")
for name, source := range baseTemplates {
content := strings.ReplaceAll(source, "MODULE", o.Module)
content = strings.ReplaceAll(content, "LOGFORMAT", o.LogFormat)
content = strings.ReplaceAll(content, "LOGOUTPUT", o.LogOutput)
if name == "etc/config.yaml" && o.LogOutput == "file" {
content = strings.Replace(content, "output: file", "output: stderr\n file: ./var/log/server.log\n rotation:\n enabled: true\n maxSizeMb: 100\n maxBackups: 10\n maxAgeDays: 7\n compress: true", 1)
}
put(seeds, name, content)
}
infra, err := scaffold.APIInfrastructure(o.Module, o.Database, o.Storage, o.LogOutput, o.LogFormat)
if err != nil {
return nil, nil, err
}
for name, data := range infra {
seeds[name] = data
}
if o.Storage == "local" {
for _, r := range c.Spec.Routes {
if r.Path == "/storage/object" {
return nil, nil, errors.New("/storage/object is reserved by local storage")
}
}
main := string(seeds["cmd/server/main.go"])
main = strings.Replace(main, " if err=handler.RegisterRoutes", ` for _,method:=range []string{"GET","HEAD","PUT"}{app.Handle(method,"/storage/object",func(c *httpx.Context)error{deps.FileHTTP.ServeHTTP(c.Raw(),c.Request());return nil})}
if err=handler.RegisterRoutes`, 1)
main = strings.Replace(main, `"git.yuntuo.hk/k2safe/go-omni/logx"`, `"git.yuntuo.hk/k2safe/go-omni/logx"; "git.yuntuo.hk/k2safe/go-omni/httpx"`, 1)
seeds["cmd/server/main.go"] = []byte(main)
}
return generated, seeds, nil
}
func hasResponse(routes []Route) bool {
for _, r := range routes {
if r.Response != "" && !r.Manual {
return true
}
}
return false
}
func signature(r Route) string {
s := upper(r.Handler) + "(ctx context.Context"
if r.Request != "" {
s += ",req *types." + r.Request
}
s += ") "
if r.Response != "" {
s += "(*types." + r.Response + ",error)"
} else {
s += "error"
}
return s
}