From 26c62e14c7217914aeebbe8951a95ad3aac00181 Mon Sep 17 00:00:00 2001 From: Madeline Lumetta Date: Mon, 16 Jun 2025 20:15:18 -0400 Subject: [PATCH 01/12] Expose a Register function that can be used to install routes in subrouter Takes the existing logic for generating the routes in gen-go/server and applies them to an injected router. This allows a server to compose routers by calling ``` Register(parentRouter.PathPrefix("/subrouter-prefix").Subrouter(), controller) ``` Testing this hypothesis out in app-district-service. --- server/router.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/server/router.go b/server/router.go index e0000787..eccc340c 100644 --- a/server/router.go +++ b/server/router.go @@ -122,6 +122,19 @@ func NewRouter(c Controller) *mux.Router { func newRouter(c Controller) *mux.Router { router := mux.NewRouter() router.Use(servertracing.MuxServerMiddleware("{{.Title}}")) + Register(router, c) + + return router +} + +// Register registers a controller's behavior at the appropriate routes within a given router. +// +// Making this function public supports using a wag-defined router as a subrouter. This +// functionality allows for routers to be used in two ways: +// +// 1. Register the routes in newRouter in order to construct a Server (the traditional usage) +// 2. Compose this router as a subrouter within a parent router +func Register(router *mux.Router, c Controller) { h := handler{Controller: c} {{range $index, $val := .Functions}} @@ -130,7 +143,6 @@ func newRouter(c Controller) *mux.Router { h.{{$val.HandlerName}}Handler(r.Context(), w, r) }) {{end}} - return router } // NewWithMiddleware returns a Server that implemenets the Controller interface. It runs the From 84adc372b56983ee654a6df38314b4ec128a2256 Mon Sep 17 00:00:00 2001 From: Madeline Lumetta Date: Tue, 17 Jun 2025 20:25:24 -0400 Subject: [PATCH 02/12] Add -subrouter argument and cut out unnecessary server code when set --- main.go | 29 +++++++++++++++++------ server/genserver.go | 57 ++++++++++++++++++++++++++++----------------- server/router.go | 10 ++++++-- 3 files changed, 65 insertions(+), 31 deletions(-) diff --git a/main.go b/main.go index 15085eb0..1cb7447c 100644 --- a/main.go +++ b/main.go @@ -37,6 +37,7 @@ type config struct { relativeDynamoPath *string jsModulePath *string goPackageName *string + subrouter *bool dynamoPath string goAbsolutePackagePath string @@ -64,6 +65,11 @@ func main() { dynamoOnly: flag.Bool("dynamo-only", false, "only generate dynamo code"), relativeDynamoPath: flag.String("dynamo-path", "", "path to generate dynamo code relative to go package path"), withTests: flag.Bool("with-tests", false, "generate tests for the generated db code"), + subrouter: flag.Bool( + "subrouter", + false, + "generate a router.go that registers routes as a subrouter, not a standalone own server", + ), } flag.Parse() if *conf.versionFlag { @@ -99,7 +105,13 @@ func main() { } if conf.generateServer { - if err := generateServer(*conf.goPackageName, conf.goAbsolutePackagePath, *conf.outputPath, swaggerSpec); err != nil { + if err := generateServer( + *conf.goPackageName, + conf.goAbsolutePackagePath, + *conf.outputPath, + swaggerSpec, + *conf.subrouter, + ); err != nil { log.Fatal(err.Error()) } } @@ -139,17 +151,20 @@ func generateGoModels(packageName, basePath, outputPath string, swaggerSpec spec return nil } -func generateServer(goPackageName, basePath, outputPath string, swaggerSpec spec.Swagger) error { +func generateServer(goPackageName, basePath, outputPath string, swaggerSpec spec.Swagger, subrouter bool) error { if err := prepareDir(filepath.Join(basePath, "server")); err != nil { return err } - if err := server.Generate(goPackageName, basePath, outputPath, swaggerSpec); err != nil { + if err := server.Generate(goPackageName, basePath, outputPath, swaggerSpec, subrouter); err != nil { return fmt.Errorf("Failed to generate server: %s", err) } - middlewareGenerator := swagger.Generator{BasePath: basePath} - middlewareGenerator.Write(hardcoded.MustAsset("../_hardcoded/middleware.go")) - if err := middlewareGenerator.WriteFile("server/middleware.go"); err != nil { - return fmt.Errorf("Failed to copy middleware.go: %s", err) + + if !subrouter { + middlewareGenerator := swagger.Generator{BasePath: basePath} + middlewareGenerator.Write(hardcoded.MustAsset("../_hardcoded/middleware.go")) + if err := middlewareGenerator.WriteFile("server/middleware.go"); err != nil { + return fmt.Errorf("Failed to copy middleware.go: %s", err) + } } return nil diff --git a/server/genserver.go b/server/genserver.go index e27d3af2..16a1ec74 100644 --- a/server/genserver.go +++ b/server/genserver.go @@ -14,8 +14,8 @@ import ( ) // Generate server package for a swagger spec. -func Generate(packageName, basePath, outputPath string, s spec.Swagger) error { - if err := generateRouter(packageName, basePath, outputPath, s, s.Paths); err != nil { +func Generate(packageName, basePath, outputPath string, s spec.Swagger, subrouter bool) error { + if err := generateRouter(packageName, basePath, outputPath, s, s.Paths, subrouter); err != nil { return err } if err := generateInterface(packageName, basePath, outputPath, &s, s.Info.InfoProps.Title, s.Paths); err != nil { @@ -38,11 +38,14 @@ type routerTemplate struct { ImportStatements string Title string Functions []routerFunction + Subrouter bool } -func generateRouter(packageName, basePath, outputPath string, s spec.Swagger, paths *spec.Paths) error { +func generateRouter(packageName, basePath, outputPath string, s spec.Swagger, paths *spec.Paths, subrouter bool) error { var template routerTemplate template.Title = s.Info.Title + template.Subrouter = subrouter + for _, path := range swagger.SortedPathItemKeys(paths.Paths) { pathItem := paths.Paths[path] pathItemOps := swagger.PathItemOperations(pathItem) @@ -57,25 +60,35 @@ func generateRouter(packageName, basePath, outputPath string, s spec.Swagger, pa }) } } - template.ImportStatements = swagger.ImportStatements([]string{ - "compress/gzip", - "context", - "log", - "net/http", - `_ "net/http/pprof"`, - "os", - "os/signal", - "path", - "syscall", - "time", - "github.com/Clever/go-process-metrics/metrics", - packageName + "/servertracing", - "github.com/gorilla/handlers", - "github.com/gorilla/mux", - "github.com/kardianos/osext", - "github.com/Clever/kayvee-go/v7/logger", - `kvMiddleware "github.com/Clever/kayvee-go/v7/middleware"`, - }) + + if subrouter { + template.ImportStatements = swagger.ImportStatements([]string{ + "net/http", + "github.com/gorilla/mux", + "github.com/Clever/kayvee-go/v7/logger", + }) + } else { + template.ImportStatements = swagger.ImportStatements([]string{ + "compress/gzip", + "context", + "log", + "net/http", + `_ "net/http/pprof"`, + "os", + "os/signal", + "path", + "syscall", + "time", + "github.com/Clever/go-process-metrics/metrics", + packageName + "/servertracing", + "github.com/gorilla/handlers", + "github.com/gorilla/mux", + "github.com/kardianos/osext", + "github.com/Clever/kayvee-go/v7/logger", + `kvMiddleware "github.com/Clever/kayvee-go/v7/middleware"`, + }) + } + routerCode, err := templates.WriteTemplate(routerTemplateStr, template) if err != nil { return err diff --git a/server/router.go b/server/router.go index eccc340c..1fc9e0ce 100644 --- a/server/router.go +++ b/server/router.go @@ -7,6 +7,7 @@ package server {{.ImportStatements}} +{{if not .Subrouter}} // Server defines a HTTP server that implements the Controller interface. type Server struct { // Handler should generally not be changed. It exposed to make testing easier. @@ -80,10 +81,12 @@ func (s *Server) Serve() error { return nil } +{{end}} type handler struct { Controller } +{{if not .Subrouter}} func startLoggingProcessMetrics() { metrics.Log("{{.Title}}", 1*time.Minute) } @@ -107,7 +110,6 @@ func withMiddleware(serviceName string, router http.Handler, m []func(http.Handl return handler } - // New returns a Server that implements the Controller interface. It will start when "Serve" is called. func New(c Controller, addr string, options ...func(*serverConfig)) *Server { return NewWithMiddleware(c, addr, []func(http.Handler) http.Handler{}, options...) @@ -127,6 +129,7 @@ func newRouter(c Controller) *mux.Router { return router } +{{end}} // Register registers a controller's behavior at the appropriate routes within a given router. // // Making this function public supports using a wag-defined router as a subrouter. This @@ -145,6 +148,7 @@ func Register(router *mux.Router, c Controller) { {{end}} } +{{if not .Subrouter}} // NewWithMiddleware returns a Server that implemenets the Controller interface. It runs the // middleware after the built-in middleware (e.g. logging), but before the controller methods. // The middleware is executed in the order specified. The server will start when "Serve" is called. @@ -174,4 +178,6 @@ func AttachMiddleware(router *mux.Router, addr string, m []func(http.Handler) ht handler := withMiddleware("{{.Title}}", router, m, config) return &Server{Handler: handler, addr: addr, l: l, config: config} -}` +} + +{{end}}` From 771b01271d4598613aed3e19afcbb53708463178 Mon Sep 17 00:00:00 2001 From: Madeline Lumetta Date: Wed, 18 Jun 2025 19:50:59 -0400 Subject: [PATCH 03/12] Enable configuring subrouters in parent swagger.yml with x-routers The extension schema is of the form ```yaml x-routers: - key: subrouter1 path: /v1/abc - key: subrouter2 path: /v1/xyz ``` This pattern assumes that the repo has a routers/ directory with subdirectories routers/subrouter1 and routers/subrouter2, with both being subdirectories having wag-generated route handlers. --- server/genserver.go | 90 ++++++++++++++++++++++++++++++++++++------ server/router.go | 78 ++++++++++++++++++++++++++++++------ templates/templates.go | 5 ++- 3 files changed, 148 insertions(+), 25 deletions(-) diff --git a/server/genserver.go b/server/genserver.go index 16a1ec74..8f3a8f94 100644 --- a/server/genserver.go +++ b/server/genserver.go @@ -2,7 +2,10 @@ package server import ( "bytes" + "encoding/json" "fmt" + "log" + "reflect" "strings" "github.com/go-openapi/spec" @@ -34,17 +37,30 @@ type routerFunction struct { OpID string } +const SubrouterKey string = "x-routers" + +type subrouter struct { + Key string `json:"key"` + Path string `json:"path"` +} + type routerTemplate struct { ImportStatements string Title string Functions []routerFunction - Subrouter bool + IsSubrouter bool + Subrouters []subrouter } -func generateRouter(packageName, basePath, outputPath string, s spec.Swagger, paths *spec.Paths, subrouter bool) error { +func generateRouter( + packageName, basePath, outputPath string, + s spec.Swagger, + paths *spec.Paths, + isSubrouter bool, +) error { var template routerTemplate template.Title = s.Info.Title - template.Subrouter = subrouter + template.IsSubrouter = isSubrouter for _, path := range swagger.SortedPathItemKeys(paths.Paths) { pathItem := paths.Paths[path] @@ -61,14 +77,24 @@ func generateRouter(packageName, basePath, outputPath string, s spec.Swagger, pa } } - if subrouter { - template.ImportStatements = swagger.ImportStatements([]string{ + var subrouterImports []string + var err error + template.Subrouters, subrouterImports, err = buildSubrouters(packageName, s) + if err != nil { + return err + } + + var imports []string + if isSubrouter { + imports = []string{ "net/http", "github.com/gorilla/mux", "github.com/Clever/kayvee-go/v7/logger", - }) + } + + imports = append(imports, subrouterImports...) } else { - template.ImportStatements = swagger.ImportStatements([]string{ + imports = []string{ "compress/gzip", "context", "log", @@ -80,15 +106,22 @@ func generateRouter(packageName, basePath, outputPath string, s spec.Swagger, pa "syscall", "time", "github.com/Clever/go-process-metrics/metrics", - packageName + "/servertracing", + "github.com/Clever/kayvee-go/v7/logger", + `kvMiddleware "github.com/Clever/kayvee-go/v7/middleware"`, + } + + imports = append(imports, subrouterImports...) + imports = append( + imports, + packageName+"/servertracing", "github.com/gorilla/handlers", "github.com/gorilla/mux", "github.com/kardianos/osext", - "github.com/Clever/kayvee-go/v7/logger", - `kvMiddleware "github.com/Clever/kayvee-go/v7/middleware"`, - }) + ) } + template.ImportStatements = swagger.ImportStatements(imports) + routerCode, err := templates.WriteTemplate(routerTemplateStr, template) if err != nil { return err @@ -98,6 +131,41 @@ func generateRouter(packageName, basePath, outputPath string, s spec.Swagger, pa return g.WriteFile("server/router.go") } +func buildSubrouters(packageName string, s spec.Swagger) ([]subrouter, []string, error) { + var subrouterConfig []subrouter + if routers, ok := s.Extensions[SubrouterKey]; ok { + fmt.Println("ROUTERS", routers, reflect.TypeOf(routers)) + if subroutersM, ok := routers.([]interface{}); ok { + subroutersB, err := json.Marshal(subroutersM) + if err != nil { + return nil, nil, err + } + + err = json.Unmarshal(subroutersB, &subrouterConfig) + if err != nil { + return nil, nil, err + } + } else { + log.Println("WARNING: x-routers subrouter config was not an array") + } + } + + var imports []string + for _, r := range subrouterConfig { + imports = append( + imports, + fmt.Sprintf( + "%srouter \"%s/routers/%s/gen-go/server\"", + r.Key, + strings.TrimSuffix(packageName, "/gen-go"), + r.Key, + ), + ) + } + + return subrouterConfig, imports, nil +} + type interfaceTemplate struct { Comment string Definition string diff --git a/server/router.go b/server/router.go index 1fc9e0ce..8dd73364 100644 --- a/server/router.go +++ b/server/router.go @@ -7,7 +7,7 @@ package server {{.ImportStatements}} -{{if not .Subrouter}} +{{if not .IsSubrouter}} // Server defines a HTTP server that implements the Controller interface. type Server struct { // Handler should generally not be changed. It exposed to make testing easier. @@ -86,7 +86,7 @@ type handler struct { Controller } -{{if not .Subrouter}} +{{if not .IsSubrouter}} func startLoggingProcessMetrics() { metrics.Log("{{.Title}}", 1*time.Minute) } @@ -111,20 +111,49 @@ func withMiddleware(serviceName string, router http.Handler, m []func(http.Handl } // New returns a Server that implements the Controller interface. It will start when "Serve" is called. +{{- if .Subrouters}} +func New( + c Controller, + {{- range $i, $val := .Subrouters}} + sc{{index1 $i}} {{$val.Key}}router.Controller, + {{- end}} + addr string, + options ...func(*serverConfig), +) *Server { +{{else}} func New(c Controller, addr string, options ...func(*serverConfig)) *Server { - return NewWithMiddleware(c, addr, []func(http.Handler) http.Handler{}, options...) +{{end -}} + return NewWithMiddleware(c{{range $i, $val := .Subrouters}}, sc{{index1 $i}}{{end}}, addr, []func(http.Handler) http.Handler{}, options...) } // NewRouter returns a mux.Router with no middleware. This is so we can attach additional routes to the // router if necessary +{{- if .Subrouters}} +func NewRouter( + c Controller, + {{- range $i, $val := .Subrouters}} + sc{{index1 $i}} {{$val.Key}}router.Controller, + {{- end}} +) *mux.Router { +{{else}} func NewRouter(c Controller) *mux.Router { - return newRouter(c) +{{end -}} + return newRouter(c{{range $i, $val := .Subrouters}}, sc{{index1 $i}}{{end}}) } +{{if .Subrouters}} +func newRouter( + c Controller, + {{- range $i, $val := .Subrouters}} + sc{{index1 $i}} {{$val.Key}}router.Controller, + {{- end}} +) *mux.Router { +{{else}} func newRouter(c Controller) *mux.Router { +{{end -}} router := mux.NewRouter() router.Use(servertracing.MuxServerMiddleware("{{.Title}}")) - Register(router, c) + Register(router, c{{range $i, $val := .Subrouters}}, sc{{index1 $i}}{{end}}) return router } @@ -137,23 +166,42 @@ func newRouter(c Controller) *mux.Router { // // 1. Register the routes in newRouter in order to construct a Server (the traditional usage) // 2. Compose this router as a subrouter within a parent router +{{- if .Subrouters}} +func Register( + router *mux.Router, + c Controller, + {{- range $i, $val := .Subrouters}} + sc{{index1 $i}} {{$val.Key}}router.Controller, + {{- end}} +) { +{{else}} func Register(router *mux.Router, c Controller) { +{{end -}} h := handler{Controller: c} - + {{range $index, $val := .Subrouters -}} + {{$val.Key}}router.Register(router.PathPrefix("{{$val.Path}}").Subrouter(), sc{{index1 $index}}) + {{end}} {{range $index, $val := .Functions}} router.Methods("{{$val.Method}}").Path("{{$val.Path}}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { logger.FromContext(r.Context()).AddContext("op", "{{$val.OpID}}") h.{{$val.HandlerName}}Handler(r.Context(), w, r) }) - {{end}} -} +{{end}}} -{{if not .Subrouter}} +{{if not .IsSubrouter}} // NewWithMiddleware returns a Server that implemenets the Controller interface. It runs the // middleware after the built-in middleware (e.g. logging), but before the controller methods. // The middleware is executed in the order specified. The server will start when "Serve" is called. -func NewWithMiddleware(c Controller, addr string, m []func(http.Handler) http.Handler, options ...func(*serverConfig)) *Server { - router := newRouter(c) +func NewWithMiddleware( + c Controller, + {{- if .Subrouters -}}{{range $i, $val := .Subrouters}} + sc{{index1 $i}} {{$val.Key}}router.Controller, + {{- end}}{{end}} + addr string, + m []func(http.Handler) http.Handler, + options ...func(*serverConfig), +) *Server { + router := newRouter(c{{range $i, $val := .Subrouters}}, sc{{index1 $i}}{{end}}) return AttachMiddleware(router, addr, m, options...) } @@ -162,7 +210,12 @@ func NewWithMiddleware(c Controller, addr string, m []func(http.Handler) http.Ha // NewServer. It attaches custom middleware passed as arguments as well as the built-in middleware for // logging, tracing, and handling panics. It should be noted that the built-in middleware executes first // followed by the passed in middleware (in the order specified). -func AttachMiddleware(router *mux.Router, addr string, m []func(http.Handler) http.Handler, options ...func(*serverConfig)) *Server { +func AttachMiddleware( + router *mux.Router, + addr string, + m []func(http.Handler) http.Handler, + options ...func(*serverConfig), +) *Server { // Set sane defaults, to be overriden by the varargs functions. // This would probably be better done in NewWithMiddleware, but there are services that call // AttachMiddleWare directly instead. @@ -173,7 +226,6 @@ func AttachMiddleware(router *mux.Router, addr string, m []func(http.Handler) ht option(&config) } - l := logger.New("{{.Title}}") handler := withMiddleware("{{.Title}}", router, m, config) diff --git a/templates/templates.go b/templates/templates.go index 14689cf5..008428ec 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -9,7 +9,10 @@ import ( // and returns a filled-out template. func WriteTemplate(templateStr string, templateStruct interface{}) (string, error) { - tmpl, err := template.New("test").Parse(templateStr) + tmpl, err := template. + New("test"). + Funcs(template.FuncMap{"index1": func(i int) int { return i + 1 }}). + Parse(templateStr) if err != nil { return "", err } From 0a43e5bb9163dc8efeae4d7415d526ca77f74e0f Mon Sep 17 00:00:00 2001 From: Madeline Lumetta Date: Mon, 23 Jun 2025 21:03:47 -0400 Subject: [PATCH 04/12] Export NoOpLogger from logging/wagclientlogger package for tests --- logging/wagclientlogger/nooplogger.go | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 logging/wagclientlogger/nooplogger.go diff --git a/logging/wagclientlogger/nooplogger.go b/logging/wagclientlogger/nooplogger.go new file mode 100644 index 00000000..70f3c4db --- /dev/null +++ b/logging/wagclientlogger/nooplogger.go @@ -0,0 +1,7 @@ +package wagclientlogger + +type NoOpLogger struct{} + +// NoOpLogger.Log does not have a function body, since it's intended as a mock logger solely to +// satisfy the interface in tests where we are not asserting on logging behavior. +func (l NoOpLogger) Log(level LogLevel, message string, pairs map[string]interface{}) {} From b77b3e11d88e8129cade40e6293c1441f7e9b919 Mon Sep 17 00:00:00 2001 From: Madeline Lumetta Date: Mon, 23 Jun 2025 22:02:53 -0400 Subject: [PATCH 05/12] Update router generation not to include `basePath` value if generating subrouter This change allows us to use the existing notion of `basePath` to generate the correct client paths for a subrouter, assuming that we do not want to support path parameters for a subrouter, which keeps us spec compliant for OAS 2. --- clients/go/gengo.go | 14 +++++++------- main.go | 16 +++++++++++++--- server/genserver.go | 8 +++++++- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/clients/go/gengo.go b/clients/go/gengo.go index da8c642b..3abf8921 100644 --- a/clients/go/gengo.go +++ b/clients/go/gengo.go @@ -15,8 +15,8 @@ import ( ) // Generate generates a client -func Generate(packageName, basePath, outputPath string, s spec.Swagger) error { - if err := generateClient(packageName, basePath, outputPath, s); err != nil { +func Generate(packageName, basePath, outputPath string, s spec.Swagger, isSubrouter bool) error { + if err := generateClient(packageName, basePath, outputPath, s, isSubrouter); err != nil { return err } return generateInterface(packageName, basePath, outputPath, &s, s.Info.InfoProps.Title, s.Paths) @@ -53,7 +53,7 @@ import ( discovery "github.com/Clever/discovery-go" wcl "github.com/Clever/wag/logging/wagclientlogger" - + ) var _ = json.Marshal @@ -93,7 +93,7 @@ func New(basePath string, logger wcl.WagClientLogger, transport *http.RoundTripp basePath = strings.TrimSuffix(basePath, "/") base := baseDoer{} - + // Don't use the default retry policy since its 5 retries can 5X the traffic retry := retryDoer{d: base, retryPolicy: SingleRetryPolicy{}} @@ -151,7 +151,7 @@ func shortHash(s string) string { } ` -func generateClient(packageName, basePath, outputPath string, s spec.Swagger) error { +func generateClient(packageName, basePath, outputPath string, s spec.Swagger, isSubrouter bool) error { outputPath = strings.TrimPrefix(outputPath, ".") moduleName, versionSuffix := utils.ExtractModuleNameAndVersionSuffix(packageName, outputPath) codeTemplate := clientCodeTemplate{ @@ -430,11 +430,11 @@ func (c *WagClient) do%sRequest(ctx context.Context, req *http.Request, headers "status_code": retCode, } if err == nil && retCode > 399 && retCode < 500{ - logData["message"] = resp.Status + logData["message"] = resp.Status c.logger.Log(wcl.Warning, "client-request-finished", logData) } if err == nil && retCode > 499{ - logData["message"] = resp.Status + logData["message"] = resp.Status c.logger.Log(wcl.Error, "client-request-finished", logData) } if err != nil { diff --git a/main.go b/main.go index 1cb7447c..30b4cbc6 100644 --- a/main.go +++ b/main.go @@ -129,7 +129,13 @@ func main() { } if conf.generateGoClient { - if err := generateGoClient(*conf.goPackageName, conf.goAbsolutePackagePath, *conf.outputPath, swaggerSpec); err != nil { + if err := generateGoClient( + *conf.goPackageName, + conf.goAbsolutePackagePath, + *conf.outputPath, + swaggerSpec, + *conf.subrouter, + ); err != nil { log.Fatal(err.Error()) } } @@ -194,11 +200,15 @@ func generateDynamo(dynamoPath, goPackageName, basePath, relativeDynamoPath, Out return nil } -func generateGoClient(goPackageName, basePath, outputPath string, swaggerSpec spec.Swagger) error { +func generateGoClient( + goPackageName, basePath, outputPath string, + swaggerSpec spec.Swagger, + subrouter bool, +) error { if err := prepareDir(filepath.Join(basePath, "client")); err != nil { return err } - if err := goclient.Generate(goPackageName, basePath, outputPath, swaggerSpec); err != nil { + if err := goclient.Generate(goPackageName, basePath, outputPath, swaggerSpec, subrouter); err != nil { return fmt.Errorf("Failed generating go client %s", err) } doerGenerator := swagger.Generator{BasePath: basePath} diff --git a/server/genserver.go b/server/genserver.go index 8f3a8f94..1197ef7a 100644 --- a/server/genserver.go +++ b/server/genserver.go @@ -65,12 +65,18 @@ func generateRouter( for _, path := range swagger.SortedPathItemKeys(paths.Paths) { pathItem := paths.Paths[path] pathItemOps := swagger.PathItemOperations(pathItem) + + // If this router is a subrouter, then the basePath routing is applied + if !isSubrouter { + path = s.BasePath + path + } + for _, method := range swagger.SortedOperationsKeys(pathItemOps) { op := pathItemOps[method] template.Functions = append(template.Functions, routerFunction{ Method: method, - Path: s.BasePath + path, + Path: path, HandlerName: swagger.Capitalize(op.ID), OpID: op.ID, }) From 348c6f4ffc47f6d87e377a5ea65d07c9d60f3a8d Mon Sep 17 00:00:00 2001 From: Madeline Lumetta Date: Tue, 24 Jun 2025 11:12:26 -0400 Subject: [PATCH 06/12] Validate that parent x-routers[KEY].path matches subrouter basePath --- main.go | 15 +++++++++++++-- server/genserver.go | 33 +++++---------------------------- swagger/ext_subrouters.go | 36 ++++++++++++++++++++++++++++++++++++ validation/validation.go | 29 ++++++++++++++++++++++++++--- 4 files changed, 80 insertions(+), 33 deletions(-) create mode 100644 swagger/ext_subrouters.go diff --git a/main.go b/main.go index 30b4cbc6..c7b936f1 100644 --- a/main.go +++ b/main.go @@ -86,11 +86,22 @@ func main() { if err != nil { log.Fatalf("Error loading swagger file: %s", err) } - swaggerSpec := *doc.Spec() + var parentDoc *loads.Document + if *conf.subrouter { + parentDoc, err = loads.Spec( + filepath.Join(filepath.Dir(*conf.swaggerFile), "..", "..", "swagger.yml"), + ) + + if err != nil { + log.Fatalf("Error loading parent swagger file: %s", err) + } + } + + swaggerSpec := *doc.Spec() injectDefaultDefinitions(&swaggerSpec) - if err := validation.Validate(*doc, conf.generateJSClient); err != nil { + if err := validation.Validate(doc, parentDoc, conf.generateJSClient); err != nil { log.Fatalf("Swagger file not valid: %s", err) } diff --git a/server/genserver.go b/server/genserver.go index 1197ef7a..73d07589 100644 --- a/server/genserver.go +++ b/server/genserver.go @@ -2,10 +2,7 @@ package server import ( "bytes" - "encoding/json" "fmt" - "log" - "reflect" "strings" "github.com/go-openapi/spec" @@ -37,19 +34,12 @@ type routerFunction struct { OpID string } -const SubrouterKey string = "x-routers" - -type subrouter struct { - Key string `json:"key"` - Path string `json:"path"` -} - type routerTemplate struct { ImportStatements string Title string Functions []routerFunction IsSubrouter bool - Subrouters []subrouter + Subrouters []swagger.Subrouter } func generateRouter( @@ -137,23 +127,10 @@ func generateRouter( return g.WriteFile("server/router.go") } -func buildSubrouters(packageName string, s spec.Swagger) ([]subrouter, []string, error) { - var subrouterConfig []subrouter - if routers, ok := s.Extensions[SubrouterKey]; ok { - fmt.Println("ROUTERS", routers, reflect.TypeOf(routers)) - if subroutersM, ok := routers.([]interface{}); ok { - subroutersB, err := json.Marshal(subroutersM) - if err != nil { - return nil, nil, err - } - - err = json.Unmarshal(subroutersB, &subrouterConfig) - if err != nil { - return nil, nil, err - } - } else { - log.Println("WARNING: x-routers subrouter config was not an array") - } +func buildSubrouters(packageName string, s spec.Swagger) ([]swagger.Subrouter, []string, error) { + subrouterConfig, err := swagger.ParseSubrouters(s) + if err != nil { + return nil, nil, err } var imports []string diff --git a/swagger/ext_subrouters.go b/swagger/ext_subrouters.go new file mode 100644 index 00000000..09c51060 --- /dev/null +++ b/swagger/ext_subrouters.go @@ -0,0 +1,36 @@ +package swagger + +import ( + "encoding/json" + "log" + + "github.com/go-openapi/spec" +) + +const SubrouterKey string = "x-routers" + +type Subrouter struct { + Key string `json:"key"` + Path string `json:"path"` +} + +func ParseSubrouters(s spec.Swagger) ([]Subrouter, error) { + var subrouterConfig []Subrouter + if routers, ok := s.Extensions[SubrouterKey]; ok { + if subroutersM, ok := routers.([]interface{}); ok { + subroutersB, err := json.Marshal(subroutersM) + if err != nil { + return nil, err + } + + err = json.Unmarshal(subroutersB, &subrouterConfig) + if err != nil { + return nil, err + } + } else { + log.Printf("WARNING: %s subrouter config was not an array\n", SubrouterKey) + } + } + + return subrouterConfig, nil +} diff --git a/validation/validation.go b/validation/validation.go index 235c55fd..de9580db 100644 --- a/validation/validation.go +++ b/validation/validation.go @@ -210,10 +210,10 @@ func validateDefinitions(definitions map[string]spec.Schema) error { // we don't support. Note that this isn't a comprehensive check for all things // we don't support, so this may not return an error, but the Swagger file might // have values we don't support -func Validate(d loads.Document, generateJSClient bool) error { - s := d.Spec() +func Validate(doc, parentDoc *loads.Document, generateJSClient bool) error { + s := doc.Spec() - goSwaggerError := validate.Spec(&d, strfmt.Default) + goSwaggerError := validate.Spec(doc, strfmt.Default) if goSwaggerError != nil { str := "" for _, desc := range goSwaggerError.(*swaggererrors.CompositeError).Errors { @@ -259,6 +259,29 @@ func Validate(d loads.Document, generateJSClient bool) error { return fmt.Errorf("must provide 'x-npm-package' in the 'info' section of the swagger.yml") } + if s.BasePath != "" && parentDoc != nil { + parentSpec := parentDoc.Spec() + subrouters, err := swagger.ParseSubrouters(*parentSpec) + if err != nil { + return err + } + + hasConfiguredPath := false + for _, subrouter := range subrouters { + if subrouter.Path == s.BasePath { + hasConfiguredPath = true + break + } + } + + if !hasConfiguredPath { + return fmt.Errorf( + "subrouter base path %s is not configured in parent spec x-routers config", + s.BasePath, + ) + } + } + for path, pathItem := range s.Paths.Paths { if pathItem.Ref.String() != "" { return fmt.Errorf("wag does not support paths with $ref fields. Define the references on " + From c1fb61bad6dc7f1f0986d76e92d8d5f4625d3d13 Mon Sep 17 00:00:00 2001 From: Madeline Lumetta Date: Tue, 1 Jul 2025 18:37:39 -0400 Subject: [PATCH 07/12] Generate root client that uses implementation of subrouter clients --- clients/go/gengo.go | 176 +++++++++++++++++++++++++++++++------- clients/go/subrouters.go | 39 +++++++++ go.mod | 1 + go.sum | 2 + main.go | 4 +- swagger/ext_subrouters.go | 11 +++ swagger/operation.go | 16 ++-- templates/templates.go | 9 +- 8 files changed, 218 insertions(+), 40 deletions(-) create mode 100644 clients/go/subrouters.go diff --git a/clients/go/gengo.go b/clients/go/gengo.go index 3abf8921..4ea60278 100644 --- a/clients/go/gengo.go +++ b/clients/go/gengo.go @@ -15,8 +15,8 @@ import ( ) // Generate generates a client -func Generate(packageName, basePath, outputPath string, s spec.Swagger, isSubrouter bool) error { - if err := generateClient(packageName, basePath, outputPath, s, isSubrouter); err != nil { +func Generate(packageName, basePath, outputPath string, s spec.Swagger) error { + if err := generateClient(packageName, basePath, outputPath, s); err != nil { return err } return generateInterface(packageName, basePath, outputPath, &s, s.Info.InfoProps.Title, s.Paths) @@ -31,29 +31,34 @@ type clientCodeTemplate struct { Operations []string Version string VersionSuffix string + Subrouters []swagger.Subrouter } var clientCodeTemplateStr = ` package client import ( - "context" - "strings" - "bytes" - "net/http" - "strconv" - "encoding/json" - "strconv" - "time" - "fmt" - "io/ioutil" - "crypto/md5" - - "{{.ModuleName}}{{.OutputPath}}/models{{.VersionSuffix}}" - - discovery "github.com/Clever/discovery-go" - wcl "github.com/Clever/wag/logging/wagclientlogger" - + "context" + "strings" + "bytes" + "net/http" + "strconv" + "encoding/json" + "strconv" + "time" + "fmt" + "io/ioutil" + "crypto/md5" + + "{{.ModuleName}}{{.OutputPath}}/models{{.VersionSuffix}}" +{{- if .Subrouters }} +{{ range $i, $val := .Subrouters -}} + {{$val.Key}}client "{{$.ModuleName}}/routers/{{$val.Key}}/gen-go/client{{$.VersionSuffix}}" + {{$val.Key}}models "{{$.ModuleName}}/routers/{{$val.Key}}/gen-go/models{{$.VersionSuffix}}" +{{ end }} +{{ end }} + discovery "github.com/Clever/discovery-go" + wcl "github.com/Clever/wag/logging/wagclientlogger" ) var _ = json.Marshal @@ -76,6 +81,13 @@ type WagClient struct { retryDoer *retryDoer defaultTimeout time.Duration logger wcl.WagClientLogger +{{- if .Subrouters }} + + // Subrouters +{{ range $i, $val := .Subrouters -}} + {{camelcase $val.Key}}Client {{$val.Key}}client.Client +{{ end }} +{{ end -}} } var _ Client = (*WagClient)(nil) @@ -85,7 +97,6 @@ var _ Client = (*WagClient)(nil) // provide an instrumented transport using the wag clientconfig module. If no tracing is required, pass nil to use // the default transport. func New(basePath string, logger wcl.WagClientLogger, transport *http.RoundTripper) *WagClient { - t := http.DefaultTransport if transport != nil { t = *transport @@ -106,6 +117,9 @@ func New(basePath string, logger wcl.WagClientLogger, transport *http.RoundTripp retryDoer: &retry, defaultTimeout: 5 * time.Second, logger: logger, +{{ range $i, $val := .Subrouters -}} + {{camelcase $val.Key}}Client: {{$val.Key}}client.New(basePath, logger, transport), +{{ end }} } return client } @@ -151,9 +165,14 @@ func shortHash(s string) string { } ` -func generateClient(packageName, basePath, outputPath string, s spec.Swagger, isSubrouter bool) error { +func generateClient(packageName, basePath, outputPath string, s spec.Swagger) error { outputPath = strings.TrimPrefix(outputPath, ".") moduleName, versionSuffix := utils.ExtractModuleNameAndVersionSuffix(packageName, outputPath) + subrouters, err := swagger.ParseSubrouters(s) + if err != nil { + return err + } + codeTemplate := clientCodeTemplate{ PackageName: packageName, OutputPath: outputPath, @@ -162,6 +181,7 @@ func generateClient(packageName, basePath, outputPath string, s spec.Swagger, is FormattedServiceName: strings.ToUpper(strings.Replace(s.Info.InfoProps.Title, "-", "_", -1)), Version: s.Info.InfoProps.Version, VersionSuffix: versionSuffix, + Subrouters: subrouters, } for _, path := range swagger.SortedPathItemKeys(s.Paths.Paths) { @@ -180,6 +200,29 @@ func generateClient(packageName, basePath, outputPath string, s spec.Swagger, is } } + for _, router := range subrouters { + routerSpec, err := swagger.LoadSubrouterSpec(router) + if err != nil { + return err + } + + for _, path := range swagger.SortedPathItemKeys(routerSpec.Paths.Paths) { + pathItem := routerSpec.Paths.Paths[path] + pathItemOps := swagger.PathItemOperations(pathItem) + for _, method := range swagger.SortedOperationsKeys(pathItemOps) { + op := pathItemOps[method] + if op.Deprecated { + continue + } + code, err := subrouterOperationCode(routerSpec, op, router) + if err != nil { + return err + } + codeTemplate.Operations = append(codeTemplate.Operations, code) + } + } + } + clientCode, err := templates.WriteTemplate(clientCodeTemplateStr, codeTemplate) if err != nil { return err @@ -192,11 +235,20 @@ func generateClient(packageName, basePath, outputPath string, s spec.Swagger, is return err } - return CreateModFile("client/go.mod", basePath, codeTemplate) + return CreateModFile("client/go.mod", basePath, codeTemplate, s) } // CreateModFile creates a go.mod file for the client module. -func CreateModFile(path string, basePath string, codeTemplate clientCodeTemplate) error { +func CreateModFile( + path, basePath string, + codeTemplate clientCodeTemplate, + s spec.Swagger, +) error { + subrouters, err := swagger.ParseSubrouters(s) + if err != nil { + return err + } + absPath := basePath + "/" + path f, err := os.Create(absPath) if err != nil { @@ -214,8 +266,35 @@ require ( github.com/Clever/wag/logging/wagclientlogger v0.0.0-20221024182247-2bf828ef51be github.com/donovanhide/eventsource v0.0.0-20171031113327-3ed64d21fb0b ) + //Replace directives will work locally but mess up imports. -replace ` + codeTemplate.ModuleName + codeTemplate.OutputPath + `/models` + codeTemplate.VersionSuffix + ` => ../models ` +` + + if subrouters != nil { + replaceString := fmt.Sprintf( + `replace ( + %s%s/models%s => ../models +`, + codeTemplate.ModuleName, + codeTemplate.OutputPath, + codeTemplate.VersionSuffix, + ) + + for _, router := range subrouters { + replaceString += fmt.Sprintf( + "\t%s/routers/%s/gen-go/client => ../../routers/%s/gen-go/client\n", + codeTemplate.ModuleName, + router.Key, + router.Key, + ) + } + + replaceString += ")\n" + modFileString += replaceString + } else { + modFileString += `replace ` + codeTemplate.ModuleName + codeTemplate.OutputPath + `/models` + codeTemplate.VersionSuffix + ` => ../models +` + } _, err = f.WriteString(modFileString) if err != nil { @@ -241,12 +320,36 @@ func IsBinaryParam(param spec.Parameter, definitions map[string]spec.Schema) boo return definitions[definitionName].Format == "binary" } -func generateInterface(packageName, basePath, outputPath string, s *spec.Swagger, serviceName string, paths *spec.Paths) error { +func generateInterface( + packageName, basePath, outputPath string, + s *spec.Swagger, + serviceName string, + paths *spec.Paths, +) error { outputPath = strings.TrimPrefix(outputPath, ".") g := swagger.Generator{BasePath: basePath} - g.Print("package client\n\n") + subrouters, err := swagger.ParseSubrouters(*s) + if err != nil { + return err + } + moduleName, versionSuffix := utils.ExtractModuleNameAndVersionSuffix(packageName, outputPath) - g.Print(swagger.ImportStatements([]string{"context", moduleName + outputPath + "/models" + versionSuffix})) + imports := []string{"context", moduleName + outputPath + "/models" + versionSuffix} + for _, router := range subrouters { + imports = append( + imports, + fmt.Sprintf( + "%sclient \"%s/routers/%s/gen-go/client%s\"", + router.Key, + moduleName, + router.Key, + versionSuffix, + ), + ) + } + + g.Print("package client\n\n") + g.Print(swagger.ImportStatements(imports)) g.Print("//go:generate mockgen -source=$GOFILE -destination=mock_client.go -package client --build_flags=--mod=mod -imports=models=" + moduleName + outputPath + "/models" + versionSuffix + "\n\n") if err := generateClientInterface(s, &g, serviceName, paths); err != nil { @@ -259,10 +362,24 @@ func generateInterface(packageName, basePath, outputPath string, s *spec.Swagger return g.WriteFile("client/interface.go") } -func generateClientInterface(s *spec.Swagger, g *swagger.Generator, serviceName string, paths *spec.Paths) error { +func generateClientInterface( + s *spec.Swagger, + g *swagger.Generator, + serviceName string, + paths *spec.Paths, +) error { g.Printf("// Client defines the methods available to clients of the %s service.\n", serviceName) g.Print("type Client interface {\n\n") + subrouters, err := swagger.ParseSubrouters(*s) + if err != nil { + return err + } + + for _, router := range subrouters { + g.Printf("\t%sclient.Client\n", router.Key) + } + for _, pathKey := range swagger.SortedPathItemKeys(paths.Paths) { path := paths.Paths[pathKey] pathItemOps := swagger.PathItemOperations(path) @@ -736,12 +853,13 @@ func iterCode(s *spec.Swagger, op *spec.Operation, basePath, methodPath, method resourceAccessString = resourceAccessString + "." + utils.CamelCase(pathComponent, true) } + operationInput, _ := swagger.OperationInput(op) return templates.WriteTemplate( iterTmplStr, iterTmpl{ OpID: op.ID, CapOpID: capOpID, - Input: swagger.OperationInput(op), + Input: operationInput, BuildPathCode: buildPathCode(s, op, basePath, methodPath), BuildHeadersCode: buildHeadersCode(s, op), BuildBodyCode: buildBodyCode(s, op, method), diff --git a/clients/go/subrouters.go b/clients/go/subrouters.go new file mode 100644 index 00000000..6a12655b --- /dev/null +++ b/clients/go/subrouters.go @@ -0,0 +1,39 @@ +package goclient + +import ( + "strings" + + "github.com/Clever/wag/v9/swagger" + "github.com/Clever/wag/v9/templates" + "github.com/go-openapi/spec" + "github.com/iancoleman/strcase" +) + +func subrouterOperationCode( + s *spec.Swagger, + op *spec.Operation, + subrouter swagger.Subrouter, +) (string, error) { + _, param := swagger.OperationInput(op) + templateArgs := struct { + ClientOperation string + InputParamName string + OperationID string + SubrouterClient string + }{ + ClientOperation: strings.ReplaceAll( + swagger.ClientInterface(s, op), + "models", + subrouter.Key+"models", + ), + InputParamName: param, + OperationID: op.ID, + SubrouterClient: strcase.ToLowerCamel(subrouter.Key) + "Client", + } + + templateStr := `func (c *WagClient) {{.ClientOperation}} { + return c.{{.SubrouterClient}}.{{pascalcase .OperationID}}(ctx, {{.InputParamName}}) +}` + + return templates.WriteTemplate(templateStr, templateArgs) +} diff --git a/go.mod b/go.mod index cce2721b..36448fa8 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/go-openapi/swag v0.22.3 github.com/go-openapi/validate v0.19.15 github.com/go-swagger/go-swagger v0.23.0 + github.com/iancoleman/strcase v0.3.0 github.com/stretchr/testify v1.11.1 ) diff --git a/go.sum b/go.sum index 74698103..5724e206 100644 --- a/go.sum +++ b/go.sum @@ -144,6 +144,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= diff --git a/main.go b/main.go index c7b936f1..6c18d63d 100644 --- a/main.go +++ b/main.go @@ -145,7 +145,6 @@ func main() { conf.goAbsolutePackagePath, *conf.outputPath, swaggerSpec, - *conf.subrouter, ); err != nil { log.Fatal(err.Error()) } @@ -214,12 +213,11 @@ func generateDynamo(dynamoPath, goPackageName, basePath, relativeDynamoPath, Out func generateGoClient( goPackageName, basePath, outputPath string, swaggerSpec spec.Swagger, - subrouter bool, ) error { if err := prepareDir(filepath.Join(basePath, "client")); err != nil { return err } - if err := goclient.Generate(goPackageName, basePath, outputPath, swaggerSpec, subrouter); err != nil { + if err := goclient.Generate(goPackageName, basePath, outputPath, swaggerSpec); err != nil { return fmt.Errorf("Failed generating go client %s", err) } doerGenerator := swagger.Generator{BasePath: basePath} diff --git a/swagger/ext_subrouters.go b/swagger/ext_subrouters.go index 09c51060..fe0df533 100644 --- a/swagger/ext_subrouters.go +++ b/swagger/ext_subrouters.go @@ -3,7 +3,9 @@ package swagger import ( "encoding/json" "log" + "path/filepath" + "github.com/go-openapi/loads" "github.com/go-openapi/spec" ) @@ -34,3 +36,12 @@ func ParseSubrouters(s spec.Swagger) ([]Subrouter, error) { return subrouterConfig, nil } + +func LoadSubrouterSpec(router Subrouter) (*spec.Swagger, error) { + doc, err := loads.Spec(filepath.Join("routers", router.Key, "swagger.yml")) + if err != nil { + return nil, err + } + + return doc.Spec(), nil +} diff --git a/swagger/operation.go b/swagger/operation.go index 84838ec0..5703fbc9 100644 --- a/swagger/operation.go +++ b/swagger/operation.go @@ -23,7 +23,7 @@ func ClientInterface(s *spec.Swagger, op *spec.Operation) string { // builder of an operation func ClientIterInterface(s *spec.Swagger, op *spec.Operation) string { capOpID := Capitalize(op.ID) - input := OperationInput(op) + input, _ := OperationInput(op) return fmt.Sprintf( "New%sIter(ctx context.Context, %s) (%sIter, error)", capOpID, @@ -32,8 +32,8 @@ func ClientIterInterface(s *spec.Swagger, op *spec.Operation) string { ) } -// OperationInput returns the input to an operation -func OperationInput(op *spec.Operation) string { +// OperationInput returns the input to an operation and its variable name +func OperationInput(op *spec.Operation) (string, string) { // Don't add the input parameter argument unless there are some arguments. // If a method has a single input parameter, and it's a schema, make the // generated type for that schema the input of the method. @@ -41,22 +41,26 @@ func OperationInput(op *spec.Operation) string { // make that the input of the method. // If a method has multiple input parameters, wrap them in a struct. capOpID := Capitalize(op.ID) - input := "" + var input, variable string if singleSchemaedBodyParameter, opModel := SingleSchemaedBodyParameter(op); singleSchemaedBodyParameter { input = fmt.Sprintf("i *models.%s", opModel) + variable = "i" } else if singleStringPathParameter, inputName := SingleStringPathParameter(op); singleStringPathParameter { input = fmt.Sprintf("%s string", inputName) + variable = inputName } else if len(op.Parameters) > 0 { input = fmt.Sprintf("i *models.%sInput", capOpID) + variable = "i" } - return input + + return input, variable } // generateInterface returns the interface for an operation func opInterface(s *spec.Swagger, op *spec.Operation, includePaging bool) string { capOpID := Capitalize(op.ID) - input := OperationInput(op) + input, _ := OperationInput(op) returnTypes := []string{} if successType := SuccessType(s, op); successType != nil { diff --git a/templates/templates.go b/templates/templates.go index 008428ec..1b622b8b 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -3,15 +3,20 @@ package templates import ( "bytes" "text/template" + + "github.com/iancoleman/strcase" ) // WriteTemplate takes in the template and the definition of its variables // and returns a filled-out template. func WriteTemplate(templateStr string, templateStruct interface{}) (string, error) { - tmpl, err := template. New("test"). - Funcs(template.FuncMap{"index1": func(i int) int { return i + 1 }}). + Funcs(template.FuncMap{ + "index1": func(i int) int { return i + 1 }, + "camelcase": func(v string) string { return strcase.ToLowerCamel(v) }, + "pascalcase": func(v string) string { return strcase.ToCamel(v) }, + }). Parse(templateStr) if err != nil { return "", err From caf0d123b2dcbcf8e606154c8ba502ec01d25fdf Mon Sep 17 00:00:00 2001 From: Madeline Lumetta Date: Wed, 9 Jul 2025 11:22:07 -0400 Subject: [PATCH 08/12] Document subrouters experiment in README and docs/subrouters.md --- README.md | 15 +++- docs/subrouters.md | 208 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 docs/subrouters.md diff --git a/README.md b/README.md index e65dc67b..7a4d6527 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,20 @@ A custom Web API Generator written by Clever. Despite the presence of a `swagger.yml` file, WAG does not support all of the Swagger standard. WAG is a custom re-implementation of a subset of the Swagger version `2.0` standard. +## **SUBROUTERS EXPERIMENT** + +This branch implements an experiment to provide support for Gorilla Mux subrouters, which are one mechanism available in the `gorilla/mux` API framework for [matching routes](https://github.com/gorilla/mux?tab=readme-ov-file#matching-routes), in WAG. + +Using the subrouters experimental feature requires the following pieces: + +1. Running `wag` against both the root `swagger.yml` and any subrouter `routers/*/swagger.yml` files +2. Using the new `-subrouter` argument in `wag` invocations against the subrouter `routers/*/swagger.yml` files +3. Using the new extension `x-routers` key in the root `swagger.yml` file +4. Using the `basePath` key in the subrouter `routers/*/swagger.yml` files +5. Implementing the subrouter controllers in `routers/*/controllers` + +For more details, see the [Subrouters](./docs/subrouters.md) documentation page. + ## Usage Wag requires Go 1.24+ to build, and the generated code also requires Go 1.24+. @@ -12,7 +26,6 @@ Wag requires Go 1.24+ to build, and the generated code also requires Go 1.24+. The code generated by `wag` imposes dependencies that you should include in your `go.mod`. The `go.mod` file under `samples/` provides a list of versions that definitely work; pay special attention to the versions of `go.opentelemetry.io/*`, `github.com/go-swagger/*`, and `github.com/go-openapi/*`. - ### Generating Code Create a swagger.yml file with your [service definition](http://editor.swagger.io/#/). Wag supports a [subset](https://github.com/Clever/wag#swagger-spec) of the Swagger spec. Copy the latest `wag.mk` from the [dev-handbook](https://github.com/Clever/dev-handbook/blob/master/make/wag.mk). diff --git a/docs/subrouters.md b/docs/subrouters.md new file mode 100644 index 00000000..72953710 --- /dev/null +++ b/docs/subrouters.md @@ -0,0 +1,208 @@ +# Subrouters + +This document describes the experimental implementation of Gorilla Mux subrouters on this branch. + +1. [**Gorilla Mux Subrouters**](#introduction-to-subrouters) +2. [**Configuration**](#configuration) +3. [**Implementation: Root Level**](#root-level) +4. [**Implementation: Subrouter Level**](#subrouter-level) +5. [**Evaluation**](#evaluation) + +## Introduction to Subrouters + +Most API frameworks across languages have some concept of subrouters that can group route handlers by path prefix and apply common behavior across those grouped routes. This subrouting behavior can be applied for the following benefits, with widely varying degrees of functional improvement: + +- Simple grouping of routes for readability and code organization +- Package-level separation of routes such that one route implementation can be installed in multiple places or in different packages +- Separating code ownership of routes among different teams within one server or repo (which may also be facilitated by or complemented by package-level separation) +- Applying middleware to a path prefix rather than to the whole server or to each route individually +- Marginally more efficient route matching for each request in the server at runtime since the API router is essentially matching routes against a tree of paths rather than a list + +`gorilla/mux` provides the [`Route.PathPrefix()`](https://pkg.go.dev/github.com/gorilla/mux#Route.PathPrefix) and [`Router.PathPrefix()`](https://pkg.go.dev/github.com/gorilla/mux#Router.PathPrefix) functions for this purpose. The [Matching Routes](https://github.com/gorilla/mux#matching-routes) documentation shows how to use `PathPrefix()` in practice. + +## Configuration + +To install a subrouter in your wag service, follow the steps in this section. + +First, create a `routers/` directory with a subdirectory for your router and a `swagger.yml` file in that subdirectory (replace `KEY` with a meaningful path segment for your router): + +```sh +mkdir -p routers/KEY +touch routers/KEY/swagger.yml +``` + +Fill out your swagger.yml with a meaningful config. The main thing to call out is that `basePath` is required. + +```yaml +swagger: '2.0' +info: + # title is used in the same way as root swagger.yml, but usage may be changed in the future + title: app-district-service + description: A router that serves routes related to managing the off-Clever districts themselves, including composing calls to other foundational services like district-config-service and dac-service. + + # version and x-npm-package are functionally ignored in this case, but we have + # not yet removed them from the validation or generation. + version: 0.1.0 + x-npm-package: '@clever/app-district-service' +schemes: + - http +produces: + - application/json +responses: + # same as root swagger.yml + +# basePath is technically the same as in a traditional swagger.yml file, but it +# is REQUIRED in this case to generate the client. The value must match the path +# configured in the x-routers key in the root swagger.yml. +basePath: /v0/apps +paths: + # same as root swagger.yml +definitions: + # same as root swagger.yml +``` + +Next, add the `x-routers` extension key to the top level of the root `swagger.yml` file. This config enables the root router to discover its subrouters for usage in the server and client code. + +```yaml +x-routers: + - key: districts # matches the subdirectory name under routers/ + path: /v0/apps # matches basePath in routers/districts/swagger.yml + # ... more subrouters +``` + +With config for both the root router and subrouter installed, generate both the root router and subrouters in a `go:generate` directive or `make` target, using the `-subrouter` argument for the subrouter invocations. + +```go +//go:generate wag -output-path ./gen-go -js-path ./gen-js -file swagger.yml +//go:generate wag -subrouter -output-path ./routers/districts/gen-go -js-path ./routers/districts/gen-js -file ./routers/districts/swagger.yml +//go:generate wag -subrouter [... other subrouter args] +``` + +Finally, implement the subrouter controllers in the `routers/KEY/controller` packages and wire it up in your server's `main.go`. + +```go + s := server.New( + myController, + districtscontroller.Controller{}, + sessionscontroller.Controller{}, + *addr, + ) +``` + +And that's it! + +## Implementation: Subrouter Level + +### main.go + +- `wag` now accepts [a boolean `-subrouter` flag](https://github.com/Clever/wag/blob/subrouters/main.go#L68-L72) to indicate that this target spec is for a subrouter. +- It [loads the parent `swagger.yml` spec](https://github.com/Clever/wag/blob/subrouters/main.go#L90-L99) if it is a subrouter. +- It [uses the parent spec as part of validation](https://github.com/Clever/wag/blob/subrouters/main.go#L104-L106) and [validates that the subrouter `basePath` matches some configured `path` in `x-routers` of the parent spec](https://github.com/Clever/wag/blob/subrouters/validation/validation.go#L262-L283). +- It [passes the value of the `-subrouter` flag to `generateServer`](https://github.com/Clever/wag/blob/subrouters/main.go#L124), which [passes it to `server.Generate()`](https://github.com/Clever/wag/blob/subrouters/main.go#L174) and [skips middleware generation for subrouters](https://github.com/Clever/wag/blob/subrouters/main.go#L178-L184). + +The `generateClient` call is unchanged because it uses the pre-existing notion of `basePath`. + +### Server + +- `server.Generate()` [passes its new `subrouter` boolean arg to `generateRouter()`](https://github.com/Clever/wag/blob/subrouters/server/genserver.go#L18). +- `generateRouter` [sets the new `routerTemplate.IsSubrouter` struct field to the value of the `subrouter` arg](https://github.com/Clever/wag/blob/subrouters/server/genserver.go#L53). Note that [`routerTemplate.IsSubrouter`](https://github.com/Clever/wag/blob/subrouters/server/genserver.go#L41) is different from `routerTemplate.Subrouters`, which is defined for routers that _have_ subrouters rather than a subrouter itself; see [Server](#server-1) under [Implementation: Root Level](#implementation-root-level). +- _DOES NOT_ [prepend the `basePath` from the spec to the path](https://github.com/Clever/wag/blob/subrouters/server/genserver.go#L59-L62) in generating operations for the router to handle. +- It [creates a limited slice of imports for the subrouter `router.go`](https://github.com/Clever/wag/blob/subrouters/server/genserver.go#L84-L92). +- It cuts out sections irrelevant to subrouters in the router template with an `{{if not .IsSubrouter}}` so that only the `handler` struct and `Register()` function remain. + - [`Server` struct, `serverConfig` struct, `CompressionLevel` function, `Server.Serve` function](https://github.com/Clever/wag/blob/subrouters/server/router.go#L10-L84) + - [`startLoggingProcessMetrics` function, `withMiddleware` function, `New` function, `NewRouter` creator function, `newRouter` function](https://github.com/Clever/wag/blob/subrouters/server/router.go#L89-L161) + - [`NewWithMiddleware` function, `AttachMiddleware` function](https://github.com/Clever/wag/blob/subrouters/server/router.go#L191-L235) + +The `buildSubrouters()` call in `generateRouter()` only applies to parent routers, not subrouters. + +### Client + +The client generation for subrouters is essentially unchanged from the default behavior. Since we've already validated the `basePath` against the matching `x-routers[*].path` value in the parent spec, we just rely on the existing behavior to prepend the `basePath` in the client. On the server side, we don't do that, because the `PathPrefix()` call handles the `basePath` from the side of `x-routers[*].path`. (Alternatively, we could maybe have the parent generation discover its subrouter paths from the subrouter specs and omit the `path` entirely; this is a future consideration.) + +## Implementation: Root Level + +### `main.go` + +### Server + +- Gets [`template.Subrouters` and a slice of subrouter `gen-go/server` package imports](https://github.com/Clever/wag/blob/subrouters/server/genserver.go#L78) with `buildSubrouters()`. `buildSubrouters()` [calls `swagger.ParseSubrouters()`](https://github.com/Clever/wag/blob/subrouters/server/genserver.go#L131) and [maps the subrouter keys to their server package imports](https://github.com/Clever/wag/blob/subrouters/server/genserver.go#L136-L147). This setup allows the `server` package to discover subrouters from the root `swagger.yml` file. +- It [uses the full list of imports to run the whole server](https://github.com/Clever/wag/blob/subrouters/server/genserver.go#L92-L117), including [importing `gen-go/server` packages for its subrouters](https://github.com/Clever/wag/blob/subrouters/server/genserver.go#L109). Mostly these functions are wrapping each other, which is why this pattern repeats for so many functions. +- Accepts additional controllers for subrouters in the `New`, `NewWithMiddleware`, `NewRouter`, `newRouter`, and `Register` functions in the `router.go` template. + - [`New` with subrouter controllers](https://github.com/Clever/wag/blob/subrouters/server/router.go#L113-L127) + - [`NewWithMiddleware` with subrouter controllers](https://github.com/Clever/wag/blob/subrouters/server/router.go#L192-L207) + - [`NewRouter` with subrouter controllers](https://github.com/Clever/wag/blob/subrouters/server/router.go#L129-L142) + - [`newRouter` with subrouter controllers](https://github.com/Clever/wag/blob/subrouters/server/router.go#L144-L159) + - [`Register` with subrouter controllers](https://github.com/Clever/wag/blob/subrouters/server/router.go#L162-L189) +- Most importantly, the parent `Register()` [includes calls to subrouter `Register()` functions](https://github.com/Clever/wag/blob/subrouters/server/router.go#L182) with the actual Gorilla Mux subrouter created by `PathPrefix()`: + +```go +KEYrouter.Register(router.PathPrefix("PATH").Subrouter(), subcontroller) +``` + +### Client + +- All of `generateClient`, `generateInterface`, `generateClientInterface`, and `CreateModFile` use `swagger.ParseSubrouters()` to extract the `x-routers` extension config from the root `swagger.yml` file. + - [`generateClient` call to `swagger.ParseSubrouters()`](https://github.com/Clever/wag/blob/subrouters/clients/go/gengo.go#L171) + - [`generateInterface` call to `swagger.ParseSubrouters()`](https://github.com/Clever/wag/blob/subrouters/clients/go/gengo.go#L331) + - [`generateClientInterface` call to `swagger.ParseSubrouters()`](https://github.com/Clever/wag/blob/subrouters/clients/go/gengo.go#L374) + - [`CreateModFile` call to `swagger.ParseSubrouters()`](https://github.com/Clever/wag/blob/subrouters/clients/go/gengo.go#L247) +- `CreateModFile` [adds `replace` directives for subrouter `gen-go/client` and `gen-go/models` packages](https://github.com/Clever/wag/blob/subrouters/clients/go/gengo.go#L273-L294). It _should_, in the future, add this for the subrouter `gen-go/server` package as well. +- `generateInterface` [adds imports for the subrouter to `interface.go`](https://github.com/Clever/wag/blob/subrouters/clients/go/gengo.go#L337-L349). +- `generateClientInterface`, which is called by `generateInterface`, embeds the subrouter `Client` interfaces in the parent `Client` interface. +- `generateClient` [creates handler code for subrouter operations that wraps the subrouter client methods](https://github.com/Clever/wag/blob/subrouters/clients/go/gengo.go#L203-L224) and [instantiates subrouter clients with their `New()` functions within the parent `New()` function](https://github.com/Clever/wag/blob/subrouters/clients/go/gengo.go#L120-L122). + +## Evaluation + +Given that we (Maddy and the API team) have run this experiment to this point, what is its status? What is the value in this potential feature? How reliable and easy to work with are the design and implementation? + +At this point, the feature is not and will not be supported by Infra. As such, this branch can be treated like a fork of an open source, where the API team and any other teams that use it have to pay [the maintenance costs](#maintenance) of updating it against the main branch. + +### User Stories and Potential Benefits + +First off, it's important to consider why product teams would actually want to consider adopting this subrouter fork, what potential value it provides. + +Let's start by separating the benefits that can be achieved by other methods from the benefits which are less likely to be achieved without subrouters. + +Examples of user stories that can be achieved without a subrouter implementation: + +- When working on a service that is very large, with potentially tens of routes, I want to group routes within one service into multiple specs and controller packages so that the service can be split into more manageable, readable pieces. +- When working on a service that has routes with very divergent dependencies, I want to split up those route implementations across multiple controllers so that each route handler has access only to the dependencies it needs, or at least eliminates access to dependencies it doesn't need, to the extent that it's possible to enforce this grouping by path prefix. +- When working on a service that has multiple teams working in it (possibly because it is very large or has routes with divergent dependencies), I want to split up route specs and controller implementations by directory path so that I can use Github `CODEOWNERS` or other tools to define team ownership by directory path. +- When working with a service that has routes versioned by path prefix, I want to separate the versions into separate packages so I can more easily comprehend which version has which behavior and not generate models with `V2`, `V3`, etc suffixes. + +Examples of user stories that can only be achieved with a subrouter implementation or are less likely to be achieved without one: + +- When working on a service that is very large, with potentially tens of routes, I want to optimize route matching so that the Gorilla Mux router matches paths by walking a tree of prefixes until it reaches the leaves/terminal segments of the path rather than matching against a list of literal routes. +- When working on a service where routes have shared behavior by path prefix — for example, ensuring that a path segment of the type `/collection/{itemID}` has a valid object reference of `itemID` within `collection`, and perhaps that it meets other domain-specific constraints — I want to implement that behavior as middleware rather than handler by handler. This functionality is not supported by the current implementation, but it could easily be; however, the specific example given would require additional work to use spec extensions or support a non-compliant `basePath`, because path parameters are not supported in OpenAPI Spec's `basePath` regardless of the OAS version. + +Broadly speaking, the balance of the benefits could be achieved through some other means. Those means could include + +- Concatenation of multiple OpenAPI specs +- Usage of OAS v3 which allows referencing other specs (if I'm not mistaken) +- Embedding subcontrollers from subpackages in a root controller manually + +In my opinion (Maddy), it would be ideal for the platform to provide explicit support for any such features as required. From the list above, only concatenation of multiple OpenAPI specs would require platform support on its own, as OpenAPI Spec v3 support is a feature request that is going to be prioritized in its own right and embedding subcontrollers does not require any platform support. The platform could also choose to support mapping middleware to specific routes in order to apply behavior across multiple routes but not the whole server through some means other than subrouters, but that isn't a trivial feature either, and I think it's likely preferable to use subrouters in order to get the route matching optimization as well. + +All in all: you decide! If you happen to try this out, please register your feedback wherever appropriate (backend guild, the API team, the Infra team). + +### Installation + +To install the subrouters fork of `wag`, run the following command in your Go module root. + +```sh +go get -u github.com/Clever/wag/v9@subrouters +``` + +You'll need to rerun this command any time that the `subrouters` branch is rebased off the `wag` main branch to get the latest changes in the platform + +I recommend also configuring `wag` as a tool and using `go install tool` to update the version of `wag` in your path. + +### Maintenance + +In order to get upstream changes, we'll need to regularly rebase off the main branch for `wag`. Consumers of the subrouters experiment should reinstall via `go get -u`. We should schedule that maintenance to the degree possible. + +### Implementation + +It's worth considering: How much complexity does the subrouter implementation add to the `wag` implementation, and would that complexity be hard to maintain going forward? + +My assessment is that yes, it's a little unnecessarily complex, but that that complexity is also a function of `wag`'s heavily procedural implementation. If we were to actually adopt this feature, I would suggest potentially suggest refactoring `wag` to be more object-oriented and interface-driven so that it can support multiple types of targets and implementations can be resolved and injected based on `wag` args and `swagger.yml` config. That kind of refactor could also support other types of features, like OAS v2 and v3 support simultaneously (although that's a little different in that it's probably mostly about mapping different inputs to the same output, the principle applies). From 302645ef36e847b53cba02724512925e78b3eb53 Mon Sep 17 00:00:00 2001 From: Madeline Lumetta Date: Mon, 14 Jul 2025 20:49:40 -0400 Subject: [PATCH 09/12] Generate JavaScript client methods for subrouter operations --- clients/js/genjs.go | 74 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/clients/js/genjs.go b/clients/js/genjs.go index 180efb1b..d64062f4 100644 --- a/clients/js/genjs.go +++ b/clients/js/genjs.go @@ -25,6 +25,11 @@ func Generate(modulePath string, s spec.Swagger) error { return errors.New("must provide 'x-npm-package' in the 'info' section of the swagger.yml") } + subrouters, err := swagger.ParseSubrouters(s) + if err != nil { + return err + } + tmplInfo := clientCodeTemplate{ ClassName: utils.CamelCase(s.Info.InfoProps.Title, true), PackageName: pkgName, @@ -41,11 +46,34 @@ func Generate(modulePath string, s spec.Swagger) error { if op.Deprecated { continue } - methodCode, err := methodCode(s, op, method, path) + code, err := methodCode(s, op, method, path) if err != nil { return err } - tmplInfo.Methods = append(tmplInfo.Methods, methodCode) + tmplInfo.Methods = append(tmplInfo.Methods, code) + } + } + + for _, router := range subrouters { + routerSpec, err := swagger.LoadSubrouterSpec(router) + if err != nil { + return err + } + + for _, path := range swagger.SortedPathItemKeys(routerSpec.Paths.Paths) { + pathItem := routerSpec.Paths.Paths[path] + pathItemOps := swagger.PathItemOperations(pathItem) + for _, method := range swagger.SortedOperationsKeys(pathItemOps) { + op := pathItemOps[method] + if op.Deprecated { + continue + } + code, err := methodCode(s, op, method, routerSpec.BasePath+path) + if err != nil { + return err + } + tmplInfo.Methods = append(tmplInfo.Methods, code) + } } } @@ -193,7 +221,7 @@ function responseLog(logger, req, res, err) { "message": err || (res.statusMessage || ""), "status_code": res.statusCode || 0, }; - + if (err) { if (logData.status_code <= 499){ logger.warnD("client-request-finished", logData); @@ -273,7 +301,7 @@ class {{.ClassName}} { * @param {number} [options.circuit.errorPercentThreshold] - the threshold to place on the rolling error * rate. Once the error rate exceeds this percentage, the circuit opens. * Default: 90. - * @param {object} [options.asynclocalstore] a request scoped async store + * @param {object} [options.asynclocalstore] a request scoped async store */ constructor(options) { options = options || {}; @@ -314,7 +342,7 @@ class {{.ClassName}} { const circuitOptions = Object.assign({}, defaultCircuitOptions, options.circuit); // hystrix implements a caching mechanism, we don't want this or we can't trust that clients - // are initialized with the values passed in. + // are initialized with the values passed in. commandFactory.resetCache(); circuitFactory.resetCache(); metricsFactory.resetCache(); @@ -433,7 +461,7 @@ const methodTmplStr = ` if (!options) { options = {}; } - + const optionsBaggage = options.baggage || new Map(); const storeContext = this.asynclocalstore?.get("context") || new Map(); @@ -1022,6 +1050,40 @@ func generateTypescriptTypes(s spec.Swagger) (string, error) { } } + subrouters, err := swagger.ParseSubrouters(s) + if err != nil { + return "", err + } + + for _, router := range subrouters { + routerSpec, err := swagger.LoadSubrouterSpec(router) + if err != nil { + return "", err + } + + for _, path := range swagger.SortedPathItemKeys(routerSpec.Paths.Paths) { + pathItem := routerSpec.Paths.Paths[path] + pathItemOps := swagger.PathItemOperations(pathItem) + for _, method := range swagger.SortedOperationsKeys(pathItemOps) { + op := pathItemOps[method] + if op.Deprecated { + continue + } + + code, err := methodDecl(s, op, method, routerSpec.BasePath+path) + if err != nil { + return "", err + } + tt.MethodDecls = append(tt.MethodDecls, code) + + err = addInputType(&includedTypeMap, op) + if err != nil { + return "", err + } + } + } + } + for name, schema := range s.Definitions { if !isDefaultIncludedType[name] { theType, err := asJSType(&schema, "") From a41f56a3a1c1bb341788cab4e70280e26ca236ce Mon Sep 17 00:00:00 2001 From: Madeline Lumetta Date: Fri, 1 Aug 2025 16:14:04 -0400 Subject: [PATCH 10/12] fix: Generate TypeScript models from subrouter specs in addition to root spec --- clients/js/genjs.go | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/clients/js/genjs.go b/clients/js/genjs.go index d64062f4..f6f21b54 100644 --- a/clients/js/genjs.go +++ b/clients/js/genjs.go @@ -1050,6 +1050,16 @@ func generateTypescriptTypes(s spec.Swagger) (string, error) { } } + for name, schema := range s.Definitions { + if !isDefaultIncludedType[name] { + theType, err := asJSType(&schema, "") + if err != nil { + return "", err + } + includedTypeMap[name] = theType + } + } + subrouters, err := swagger.ParseSubrouters(s) if err != nil { return "", err @@ -1082,15 +1092,15 @@ func generateTypescriptTypes(s spec.Swagger) (string, error) { } } } - } - for name, schema := range s.Definitions { - if !isDefaultIncludedType[name] { - theType, err := asJSType(&schema, "") - if err != nil { - return "", err + for name, schema := range routerSpec.Definitions { + if !isDefaultIncludedType[name] { + theType, err := asJSType(&schema, "") + if err != nil { + return "", err + } + includedTypeMap[name] = theType } - includedTypeMap[name] = theType } } @@ -1508,13 +1518,13 @@ declare namespace {{.ServiceName}} { {{range .ErrorTypes}} {{.}} - {{end}} +{{end}} } namespace Models { {{range .IncludedTypes}} {{.}} - {{end}} +{{end}} } } From d374d4a145616f2a0ea16cff1b1e668a0f8fd9c6 Mon Sep 17 00:00:00 2001 From: Madeline Lumetta Date: Fri, 1 Aug 2025 16:31:32 -0400 Subject: [PATCH 11/12] chore: Don't generate JavaScript clients for subrouters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since the strategy for subrouter JavaScript clients (at least, so far), has been to include the subrouters in the root client generation — which is the opposite of how the Go clients are generated — we don't need to generate the subrouter clients. This change autosets conf.generateJSClient to false if conf.subrouter is true. --- main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main.go b/main.go index 6c18d63d..4fbf8afd 100644 --- a/main.go +++ b/main.go @@ -96,6 +96,8 @@ func main() { if err != nil { log.Fatalf("Error loading parent swagger file: %s", err) } + + conf.generateJSClient = false } swaggerSpec := *doc.Spec() From 7edc879b9aa254c2d88c18c173d30cb5353f44bc Mon Sep 17 00:00:00 2001 From: Madeline Lumetta Date: Mon, 4 Aug 2025 18:30:35 -0400 Subject: [PATCH 12/12] fix: Generate JS error classes from subrouter specs --- clients/js/genjs.go | 99 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/clients/js/genjs.go b/clients/js/genjs.go index f6f21b54..ae2166fa 100644 --- a/clients/js/genjs.go +++ b/clients/js/genjs.go @@ -993,6 +993,60 @@ func generateErrorsFile(s spec.Swagger) (string, error) { } } + subrouters, err := swagger.ParseSubrouters(s) + if err != nil { + return "", err + } + + for _, router := range subrouters { + subspec, err := swagger.LoadSubrouterSpec(router) + if err != nil { + return "", err + } + + for _, pathKey := range swagger.SortedPathItemKeys(subspec.Paths.Paths) { + path := subspec.Paths.Paths[pathKey] + pathItemOps := swagger.PathItemOperations(path) + for _, opKey := range swagger.SortedOperationsKeys(pathItemOps) { + op := pathItemOps[opKey] + for _, statusCode := range swagger.SortedStatusCodeKeys(op.Responses.StatusCodeResponses) { + if statusCode < 400 { + continue + } + typeName, _ := swagger.OutputType(&s, op, statusCode) + if strings.HasPrefix(typeName, "models.") { + typeName = typeName[7:] + } + if typeNames.Contains(typeName) { + log.Printf( + "Duplicate type name %s declared in %s subrouter spec\n", + typeName, + router.Key, + ) + continue + } + typeNames.Add(typeName) + + etype := errorType{ + StatusCode: statusCode, + Name: typeName, + } + + if schema, ok := subspec.Definitions[typeName]; !ok { + log.Printf("TODO: could not find schema for %s, JS documentation will be incomplete", typeName) + } else if len(schema.Properties) > 0 { + for _, name := range swagger.SortedSchemaProperties(schema) { + propertySchema := schema.Properties[name] + etype.JSDocProperties = append(etype.JSDocProperties, jsDocPropertyFromSchema(name, &propertySchema)) + } + } + + typesTmpl.ErrorTypes = append(typesTmpl.ErrorTypes, etype) + } + } + } + } + return templates.WriteTemplate(typeTmplString, typesTmpl) } @@ -1165,6 +1219,51 @@ func getErrorTypes(s spec.Swagger) ([]string, error) { } } } + + subrouters, err := swagger.ParseSubrouters(s) + if err != nil { + return nil, err + } + + for _, router := range subrouters { + routerSpec, err := swagger.LoadSubrouterSpec(router) + if err != nil { + return nil, err + } + + for _, pathKey := range swagger.SortedPathItemKeys(routerSpec.Paths.Paths) { + path := routerSpec.Paths.Paths[pathKey] + pathItemOps := swagger.PathItemOperations(path) + for _, opKey := range swagger.SortedOperationsKeys(pathItemOps) { + op := pathItemOps[opKey] + for _, statusCode := range swagger.SortedStatusCodeKeys(op.Responses.StatusCodeResponses) { + if statusCode < 400 { + continue + } + typeName, _ := swagger.OutputType(routerSpec, op, statusCode) + if strings.HasPrefix(typeName, "models.") { + typeName = typeName[7:] + } + + if _, exists := typeNames[typeName]; exists { + continue + } + typeNames[typeName] = struct{}{} + + if schema, ok := routerSpec.Definitions[typeName]; !ok { + errorTypes = append(errorTypes, fmt.Sprintf("class %s {}", typeName)) + } else if len(schema.Properties) > 0 { + declaration, err := generateErrorDeclaration(&schema, typeName, "models.") + if err != nil { + return errorTypes, err + } + errorTypes = append(errorTypes, declaration) + } + } + } + } + } + return errorTypes, nil }