Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: client generation #103

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
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
26 changes: 26 additions & 0 deletions codegen/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
// Most callers to this package will use Generate.
type Options struct {
GenerateServer bool // GenerateChiServer specifies whether to generate chi server boilerplate
GenerateClient bool // GenerateClient specifies whether to generate client boilerplate
GenerateTypes bool // GenerateTypes specifies whether to generate type definitions
EmbedSpec bool // Whether to embed the swagger spec in the generated code
SkipFmt bool // Whether to skip go imports on the generated code
Expand Down Expand Up @@ -207,6 +208,18 @@ func Generate(swagger *openapi3.T, packageName string, opts Options) (string, er
}
}

if opts.GenerateClient {
clientOut, err := GenerateClient(t, ops)
if err != nil {
return "", fmt.Errorf("error writing client: %w", err)
}

_, err = w.WriteString(clientOut)
if err != nil {
return "", fmt.Errorf("error writing client: %w", err)
}
}

if opts.EmbedSpec {
_, err = w.WriteString(inlinedSpec)
if err != nil {
Expand Down Expand Up @@ -610,6 +623,19 @@ func GenerateAdditionalPropertyBoilerplate(t *template.Template, typeDefs []Type
return GenerateTemplates([]string{"additional-properties.tmpl"}, t, context)
}

func GenerateClient(
t *template.Template,
ops []OperationDefinition,
) (string, error) {
context := struct {
Operations []OperationDefinition
}{
Operations: ops,
}

return GenerateTemplates([]string{"client.tmpl"}, t, context)
}

// SanitizeCode runs sanitizers across the generated Go code to ensure the
// generated code will be able to compile.
func SanitizeCode(goCode string) string {
Expand Down
90 changes: 90 additions & 0 deletions codegen/codegen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ package codegen

import (
"go/format"
"go/token"
"go/types"
"net/url"
"os"
"testing"
"text/template"

examplePetstore "github.com/discord-gophers/goapi-gen/examples/petstore-expanded/api"
"golang.org/x/tools/go/packages"

"github.com/discord-gophers/goapi-gen/templates"
"github.com/getkin/kin-openapi/openapi3"
Expand Down Expand Up @@ -439,3 +444,88 @@ func (t *MyType) FromValue(value int64) error {
})
}
}

func TestGenerateClientFormat(t *testing.T) {
// Input vars for code generation:
opts := Options{
GenerateClient: true,
GenerateTypes: true,
}

// Get a spec from the example PetStore definition:
swagger, err := examplePetstore.GetSwagger()
assert.NoError(t, err)

// Run our code generation:
code, err := Generate(swagger, "petstore", opts)
assert.NoError(t, err)

tfs := token.NewFileSet()
tfs.AddFile("petstore.gen.go", tfs.Base(), len(code))
cwd, _ := os.Getwd()
pkgs, err := packages.Load(&packages.Config{
Mode: packages.NeedSyntax | packages.NeedTypes | packages.NeedImports,
Fset: tfs,
Overlay: map[string][]byte{
cwd + "/petstore/petstore.gen.go": []byte(code),
},
}, "./petstore")
assert.NoError(t, err)
assert.Len(t, pkgs, 1)
pkg := pkgs[0]
assert.Empty(t, pkg.Errors)

// check we import the requests package
_, ok := pkg.Imports["github.com/carlmjohnson/requests"]
assert.True(t, ok)

client := pkg.Types.Scope().Lookup("Client")
assert.NotNil(t, client)

assert.True(t, client.Exported())
assert.Equal(t, 4, client.Type().Underlying().(*types.Struct).NumFields())
}

func TestGenerateK8sClient(t *testing.T) {
uri, err := url.Parse("https://raw.githubusercontent.com/kubernetes/kubernetes/master/api/openapi-spec/v3/api__v1_openapi.json")
assert.NoError(t, err)
swagger, err := openapi3.NewLoader().LoadFromURI(uri)
assert.NoError(t, err)

// Run our code generation:
code, err := Generate(swagger, "k8s", Options{
GenerateClient: true,
GenerateTypes: true,
})
if of := os.Getenv("OUT_FILE"); of != "" {
assert.NoError(t, os.WriteFile(of, []byte(code), 0644))
}

assert.NoError(t, err)
assert.NotEmpty(t, code)

tfs := token.NewFileSet()
tfs.AddFile("petstore.gen.go", tfs.Base(), len(code))
cwd, _ := os.Getwd()
pkgs, err := packages.Load(&packages.Config{
Mode: packages.NeedSyntax | packages.NeedTypes | packages.NeedImports,
Fset: tfs,
Overlay: map[string][]byte{
cwd + "/petstore/petstore.gen.go": []byte(code),
},
}, "./petstore")
assert.NoError(t, err)
assert.Len(t, pkgs, 1)
pkg := pkgs[0]
assert.Empty(t, pkg.Errors)

// check we import the requests package
_, ok := pkg.Imports["github.com/carlmjohnson/requests"]
assert.True(t, ok)

client := pkg.Types.Scope().Lookup("Client")
assert.NotNil(t, client)

assert.True(t, client.Exported())
assert.Equal(t, client.Type().Underlying().(*types.Struct).NumFields(), 4)
}
19 changes: 18 additions & 1 deletion codegen/template_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ func responseNameToStatusCode(responseName string) string {
}
}

func responseToStatusRangeString(responseName string) string {
switch strings.ToUpper(responseName) {
case "DEFAULT":
return "resp.StatusCode != 0"
case "1XX", "2XX", "3XX", "4XX", "5XX":
return fmt.Sprintf("resp.StatusCode < %s00 && resp.StatusCode > %s99", responseName[:1], responseName[:1])
}
return fmt.Sprintf("resp.StatusCode != %s", responseName)
}

// TitleWord converts a single worded string to title case.
// This is a replacement to `strings.Title` which we used previously.
// We didn't need strings.Title word boundary rules, and just want to Title the words directly,
Expand All @@ -112,6 +122,11 @@ func TitleWord(s string) string {
return string(r)
}

// ToComment converts a string to a comment.
func ToComment(s string) string {
return "// " + strings.ReplaceAll(s, "\n", "\n// ")
}

// TemplateFunctions generates the list of utlity and helpfer functions used by
// the templates.
var TemplateFunctions = template.FuncMap{
Expand All @@ -120,10 +135,12 @@ var TemplateFunctions = template.FuncMap{
"getResponseTypeDefinitions": getResponseTypeDefinitions,
"genTaggedMiddleware": getTaggedMiddlewares,
"toStringArray": toStringArray,
"toComment": ToComment,

"swaggerURIToChiURI": SwaggerURIToChiURI,

"statusCode": responseNameToStatusCode,
"statusCode": responseNameToStatusCode,
"statusCodeRange": responseToStatusRangeString,

"ucFirst": snaker.ForceCamelIdentifier,
"lower": strings.ToLower,
Expand Down
Loading