549 lines
23 KiB
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
|
|
}
|