Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,27 @@ 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+.

### Dependencies

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).
Expand Down
176 changes: 147 additions & 29 deletions clients/go/gengo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -85,15 +97,14 @@ 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
}

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

Expand All @@ -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
}
Expand Down Expand Up @@ -154,6 +168,11 @@ func shortHash(s string) string {
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,
Expand All @@ -162,6 +181,7 @@ func generateClient(packageName, basePath, outputPath string, s spec.Swagger) er
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) {
Expand All @@ -180,6 +200,29 @@ func generateClient(packageName, basePath, outputPath string, s spec.Swagger) er
}
}

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
Expand All @@ -192,11 +235,20 @@ func generateClient(packageName, basePath, outputPath string, s spec.Swagger) er
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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -430,11 +547,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 {
Expand Down Expand Up @@ -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),
Expand Down
39 changes: 39 additions & 0 deletions clients/go/subrouters.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading