feat: complete contract-driven business development and MySQL integration
This commit is contained in:
@@ -19,10 +19,11 @@ import (
|
||||
)
|
||||
|
||||
const frameworkModule = "git.yuntuo.hk/k2safe/go-omni"
|
||||
const minimumVersion = "v0.13.0-preview"
|
||||
const minimumVersion = "v0.14.0-preview"
|
||||
const generatedHeader = "// Code generated by go-omni api. DO NOT EDIT.\n"
|
||||
|
||||
type Options struct {
|
||||
lockHeld bool
|
||||
Database, Storage string
|
||||
API, Dir, Module, FrameworkDir, FrameworkVersion, LogOutput, LogFormat string
|
||||
Check bool
|
||||
@@ -95,6 +96,9 @@ func generate(o Options, c *Compiled, starter bool) (Output, error) {
|
||||
return Output{}, err
|
||||
}
|
||||
o.Dir = directory
|
||||
if err = checkRuntimeExtension(directory); err != nil {
|
||||
return Output{}, err
|
||||
}
|
||||
for _, r := range c.Spec.Routes {
|
||||
if !r.Manual {
|
||||
path := filepath.Join(directory, "internal/handler", r.Group, lower(r.Handler)+"Manual.go")
|
||||
@@ -109,6 +113,7 @@ func generate(o Options, c *Compiled, starter bool) (Output, error) {
|
||||
return Output{}, err
|
||||
}
|
||||
seeds := map[string][]byte{}
|
||||
requiredVersion := ""
|
||||
moduleData, readErr := os.ReadFile(filepath.Join(directory, "go.mod"))
|
||||
if readErr == nil {
|
||||
mf, err := modfile.Parse("go.mod", moduleData, nil)
|
||||
@@ -120,7 +125,6 @@ func generate(o Options, c *Compiled, starter bool) (Output, error) {
|
||||
}
|
||||
o.Module = mf.Module.Mod.Path
|
||||
found := false
|
||||
requiredVersion := ""
|
||||
for _, r := range mf.Require {
|
||||
if r.Mod.Path == frameworkModule {
|
||||
found = true
|
||||
@@ -141,13 +145,13 @@ func generate(o Options, c *Compiled, starter bool) (Output, error) {
|
||||
if !filepath.IsAbs(path) {
|
||||
path = filepath.Join(directory, path)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(path, "lifecycle", "controller.go")); err == nil {
|
||||
if _, err := os.Stat(filepath.Join(path, "lifecycle", "runtime.go")); err == nil {
|
||||
localCurrent = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !localCurrent {
|
||||
return Output{}, fmt.Errorf("existing framework dependency predates dependency shutdown and configurable endpoint support; use %s or a current local replacement", minimumVersion)
|
||||
return Output{}, fmt.Errorf("existing framework dependency predates the generated Runtime, request policy and structured MySQL support; use %s or a current local replacement", minimumVersion)
|
||||
}
|
||||
}
|
||||
if o.FrameworkDir != "" {
|
||||
@@ -191,7 +195,16 @@ func generate(o Options, c *Compiled, starter bool) (Output, error) {
|
||||
// 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 !strings.HasPrefix(name, "@") {
|
||||
seeds["api/"+name] = data
|
||||
}
|
||||
}
|
||||
if len(c.Spec.SharedRoots) > 0 {
|
||||
raw, e := sharedSeeds(c.Spec, directory)
|
||||
if e != nil {
|
||||
return Output{}, e
|
||||
}
|
||||
seeds["api/"+importsFile] = raw
|
||||
}
|
||||
if o.API != "" {
|
||||
source, _ := filepath.Abs(o.API)
|
||||
@@ -200,6 +213,7 @@ func generate(o Options, c *Compiled, starter bool) (Output, error) {
|
||||
rel, e := filepath.Rel(targetReal, sourceReal)
|
||||
if e == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
||||
entry = filepath.ToSlash(rel)
|
||||
delete(seeds, "api/"+importsFile)
|
||||
for name := range c.Spec.Sources {
|
||||
delete(seeds, "api/"+name)
|
||||
}
|
||||
@@ -208,7 +222,7 @@ func generate(o Options, c *Compiled, starter bool) (Output, error) {
|
||||
// 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") {
|
||||
if strings.HasSuffix(name, ".api") || filepath.Base(name) == catalogDocumentFile || filepath.Base(name) == importsFile {
|
||||
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)
|
||||
@@ -227,7 +241,12 @@ func generate(o Options, c *Compiled, starter bool) (Output, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return apply(directory, o.Module, entry, generated, seeds, o.Check, o.DryRun, o.profile(), snapshotContract(c.Spec))
|
||||
out, err := apply(directory, o.Module, entry, generated, seeds, o.Check, o.DryRun, o.profile(), snapshotContract(c.Spec), o.lockHeld)
|
||||
if requiredVersion != "" && requiredVersion != minimumVersion {
|
||||
out.Diagnostics = append(out.Diagnostics, Diagnostic{Severity: "review", Code: "frameworkVersion", Path: "go.mod", Before: requiredVersion, After: minimumVersion, Message: "project dependency differs from this CLI template version; use matching CLI/framework versions, or verify the explicit local replacement"})
|
||||
sortDiagnostics(out.Diagnostics)
|
||||
}
|
||||
return out, err
|
||||
}
|
||||
|
||||
func moduleFile(o Options) ([]byte, error) {
|
||||
@@ -336,6 +355,13 @@ func renderProject(o Options, c *Compiled, starter bool) (map[string][]byte, map
|
||||
}
|
||||
|
||||
types := "package types\n\n"
|
||||
needsUpload := false
|
||||
for _, b := range c.Bindings {
|
||||
needsUpload = needsUpload || b.HasMultipart()
|
||||
}
|
||||
if needsUpload {
|
||||
types += fmt.Sprintf("import %q\n", frameworkModule+"/contract")
|
||||
}
|
||||
if len(c.Spec.Scalars) > 0 {
|
||||
types += "import("
|
||||
for i, d := range c.Spec.Scalars {
|
||||
@@ -350,12 +376,28 @@ func renderProject(o Options, c *Compiled, starter bool) (map[string][]byte, map
|
||||
registry += "}}\n"
|
||||
put(generated, "internal/types/scalarsGen.go", generatedHeader+registry)
|
||||
}
|
||||
if needsUpload {
|
||||
types += "type File = contract.File\n"
|
||||
}
|
||||
bySource := map[string]string{}
|
||||
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)
|
||||
name := dtoFile(t.Position)
|
||||
if bySource[name] == "" {
|
||||
bySource[name] = "package types\n\n"
|
||||
}
|
||||
types += "}\n"
|
||||
text := "type " + t.Name + " struct {\n"
|
||||
for _, f := range t.Fields {
|
||||
if f.Description != "" {
|
||||
for _, line := range strings.Split(f.Description, "\n") {
|
||||
text += "// " + line + "\n"
|
||||
}
|
||||
}
|
||||
text += fmt.Sprintf("%s %s `%s`\n", f.Name, f.Type, fieldTag(f))
|
||||
}
|
||||
bySource[name] += text + "}\n"
|
||||
}
|
||||
for name, text := range bySource {
|
||||
put(generated, "internal/types/"+name, generatedHeader+text)
|
||||
}
|
||||
put(generated, "internal/types/typesGen.go", generatedHeader+types)
|
||||
put(generated, "docs/openapi.json", string(c.Document)+"\n")
|
||||
@@ -408,8 +450,8 @@ func renderProject(o Options, c *Compiled, starter bool) (map[string][]byte, map
|
||||
continue
|
||||
}
|
||||
hasAutomatic = true
|
||||
needsTypes = needsTypes || r.Request != "" || r.Response != ""
|
||||
needsBinder = needsBinder || (r.Request != "" && !r.CustomBinding)
|
||||
needsTypes = needsTypes || r.Request != "" || responseGoType(r.Response) != ""
|
||||
needsBinder = needsBinder || !r.CustomBinding
|
||||
}
|
||||
text += fmt.Sprintf("%q;%q;", frameworkModule+"/httpx", o.Module+"/internal/svc")
|
||||
if needsTypes {
|
||||
@@ -442,12 +484,18 @@ func renderProject(o Options, c *Compiled, starter bool) (map[string][]byte, map
|
||||
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 {
|
||||
if !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)
|
||||
requestType := "struct{}"
|
||||
if r.Request != "" {
|
||||
requestType = "types." + r.Request
|
||||
} else {
|
||||
scalarArgs = ""
|
||||
}
|
||||
text += fmt.Sprintf("var %sRequest = contract.MustNew[%s](%s)\n", lower(r.Handler), requestType, scalarArgs)
|
||||
}
|
||||
text += fmt.Sprintf("func(h *Handler) %s(c *httpx.Context)error {\n", name)
|
||||
call := "c.Context()"
|
||||
@@ -455,16 +503,32 @@ func renderProject(o Options, c *Compiled, starter bool) (map[string][]byte, map
|
||||
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 if r.BoundMultipart {
|
||||
text += fmt.Sprintf("var req types.%s\ncleanup,err:=%sRequest.BindMultipart(c.Request(),httpx.RouteParams(c.Context()),&req,contract.MultipartOptions{MaxBodyBytes:%d,MaxParts:%d,MaxFieldBytes:%d,MaxFileBytes:%d,Policy:%s});if err!=nil{return err};defer cleanup()\n", r.Request, lower(r.Handler), r.MaxBodyBytes, r.MaxParts, r.MaxFieldBytes, r.MaxFileBytes, policyLiteral(r.RequestPolicy))
|
||||
} 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)
|
||||
policy := r.RequestPolicy
|
||||
if r.BodyMode == "raw" || r.BodyMode == "multipart" {
|
||||
policy.Body = "auto"
|
||||
}
|
||||
text += fmt.Sprintf("var req types.%s\nif err:=%sRequest.BindWithPolicy(c.Request(),httpx.RouteParams(c.Context()),&req,%s%s);err!=nil{return err}\n", r.Request, lower(r.Handler), policyLiteral(policy), limit)
|
||||
}
|
||||
call += ", &req"
|
||||
} else {
|
||||
limit := ""
|
||||
if r.MaxBodyBytes > 0 {
|
||||
limit = fmt.Sprintf(",%d", r.MaxBodyBytes)
|
||||
}
|
||||
policy := r.RequestPolicy
|
||||
if r.BodyMode == "raw" || r.BodyMode == "multipart" {
|
||||
policy.Body = "auto"
|
||||
}
|
||||
text += fmt.Sprintf("if err:=%sRequest.BindWithPolicy(c.Request(),httpx.RouteParams(c.Context()),&struct{}{},%s%s);err!=nil{return err}\n", lower(r.Handler), policyLiteral(policy), limit)
|
||||
}
|
||||
streamingBody := r.BodyMode == "raw" || r.BodyMode == "multipart"
|
||||
streamingBody := streamsBody(r)
|
||||
if streamingBody {
|
||||
text += fmt.Sprintf("body,err:=httpx.NewBody(c.Request(),%q);if err!=nil{return err}\n", r.BodyMode)
|
||||
call += ",body"
|
||||
@@ -475,8 +539,15 @@ func renderProject(o Options, c *Compiled, starter bool) (map[string][]byte, map
|
||||
text += "if err:=body.Complete();err!=nil{if resp!=nil{_ =resp.Close()};return err}\n"
|
||||
}
|
||||
text += fmt.Sprintf("return c.WriteStream(%d,%q,resp)\n}\n", r.Status, r.Produces)
|
||||
} else if r.Response != "" {
|
||||
text += fmt.Sprintf("resp,err:=h.logic.%s(%s);if err!=nil{return err};if resp==nil{return fmt.Errorf(\"%s returned a nil response\")}\n", name, call, name)
|
||||
} else if responseGoType(r.Response) != "" {
|
||||
text += fmt.Sprintf("resp,err:=h.logic.%s(%s);if err!=nil{return err};\n", name, call)
|
||||
kind, _ := responseShape(r.Response)
|
||||
if kind == "object" {
|
||||
text += fmt.Sprintf("if resp==nil{return fmt.Errorf(%q)}\n", name+" returned a nil response")
|
||||
}
|
||||
if kind == "array" {
|
||||
text += "if resp==nil{resp=make(" + responseGoType(r.Response) + ",0)}\n"
|
||||
}
|
||||
if streamingBody {
|
||||
text += "if err:=body.Complete();err!=nil{return err}\n"
|
||||
}
|
||||
@@ -487,10 +558,15 @@ func renderProject(o Options, c *Compiled, starter bool) (map[string][]byte, map
|
||||
if streamingBody {
|
||||
text += "if err:=body.Complete();err!=nil{return err};"
|
||||
}
|
||||
text += "return c.NoContent()\n}\n"
|
||||
if r.Response == "null" {
|
||||
method := map[int]string{200: "OK", 201: "Created", 202: "Accepted"}[r.Status]
|
||||
text += "return c." + method + "(nil)\n}\n"
|
||||
} else {
|
||||
text += "return c.NoContent()\n}\n"
|
||||
}
|
||||
}
|
||||
service := fmt.Sprintf("package %s\nimport(\"context\"; apierrors %q;", packageName, frameworkModule+"/errors")
|
||||
if r.Request != "" || r.Response != "" {
|
||||
if r.Request != "" || responseGoType(r.Response) != "" {
|
||||
service += fmt.Sprintf("%q;", o.Module+"/internal/types")
|
||||
}
|
||||
if streamingBody || r.Produces != "" {
|
||||
@@ -503,7 +579,7 @@ func renderProject(o Options, c *Compiled, starter bool) (map[string][]byte, map
|
||||
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 != "" || r.Produces != "" {
|
||||
if responseGoType(r.Response) != "" || r.Produces != "" {
|
||||
service += "return nil, "
|
||||
} else {
|
||||
service += "return "
|
||||
@@ -549,7 +625,11 @@ func renderProject(o Options, c *Compiled, starter bool) (map[string][]byte, map
|
||||
return nil, nil, err
|
||||
}
|
||||
for name, data := range infra {
|
||||
seeds[name] = data
|
||||
if strings.HasSuffix(name, "Gen.go") {
|
||||
generated[name] = append([]byte(generatedHeader), data...)
|
||||
} else {
|
||||
seeds[name] = data
|
||||
}
|
||||
}
|
||||
if o.Storage == "local" {
|
||||
for _, r := range c.Spec.Routes {
|
||||
@@ -564,7 +644,7 @@ func renderProject(o Options, c *Compiled, starter bool) (map[string][]byte, map
|
||||
}
|
||||
func hasResponse(routes []Route) bool {
|
||||
for _, r := range routes {
|
||||
if r.Response != "" && !r.Manual && r.Produces == "" {
|
||||
if kind, _ := responseShape(r.Response); r.Response != "" && kind == "object" && !r.Manual && r.Produces == "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -575,17 +655,41 @@ func signature(r Route) string {
|
||||
if r.Request != "" {
|
||||
s += ",req *types." + r.Request
|
||||
}
|
||||
if r.BodyMode == "raw" || r.BodyMode == "multipart" {
|
||||
if streamsBody(r) {
|
||||
s += ",body *httpx.Body"
|
||||
}
|
||||
s += ") "
|
||||
if r.Produces != "" {
|
||||
return s + "(*httpx.Stream,error)"
|
||||
}
|
||||
if r.Response != "" {
|
||||
s += "(*types." + r.Response + ",error)"
|
||||
if result := responseGoType(r.Response); result != "" {
|
||||
s += "(" + result + ",error)"
|
||||
} else {
|
||||
s += "error"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func streamsBody(r Route) bool {
|
||||
return r.BodyMode == "raw" || (r.BodyMode == "multipart" && !r.BoundMultipart)
|
||||
}
|
||||
|
||||
func dtoFile(position string) string {
|
||||
source := position
|
||||
for i := 0; i < 2; i++ {
|
||||
if idx := strings.LastIndex(source, ":"); idx >= 0 {
|
||||
source = source[:idx]
|
||||
}
|
||||
}
|
||||
stem := strings.TrimSuffix(filepath.Base(source), ".api")
|
||||
var b strings.Builder
|
||||
for _, r := range stem {
|
||||
if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
if b.Len() == 0 {
|
||||
b.WriteString("contract")
|
||||
}
|
||||
return "dto" + upper(b.String()) + digest([]byte(source))[:12] + "Gen.go"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user