Skip to content
Merged
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
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -789,3 +789,54 @@ on-the-fly at run time. Example:
-templates my-templates/ \
-generate types,client \
petstore-expanded.yaml

When using the configuration file, it is possible to provide templates directly
as text, as a local path, or as a URL. If the data provided to the
configuration file is more than one line, the local path and URL checks will be
ignored and will be treated as raw template text. If one line, the string
will be used to check for a local file, followed by checking performing a HTTP
GET request. If the file lookup returns any error other than not found, or the
HTTP request returns a non 200 response code, the generator will error.

⚠️ Warning: If using urls that tracks against git repositories such as
`raw.githubusercontent.com`, it is strongly encouraged to use a tag or a hash
instead of a branch like `main`. Tracking a branch can lead to unexpected API
drift, and loss of the ability to reproduce a build.

Examples:
```yaml
output: api.gen.go
package: api
output-options:
user-templates:
# using a local file
client-with-responses.tmpl: /home/username/workspace/templatesProject/my-client-with-responses.tmpl

# The following are referencing a versuion of the default
# client-with-responses.tmpl file, but loaded in through
# github's raw.githubusercontent.com. The general form
# to use raw.githubusercontent.com is as follows
# https://raw.githubusercontent.com/<username>/<project>/<hash|tag|branch>/path/to/template/template.tmpl

# using raw.githubusercontent.com with a hash
client-with-responses.tmpl: https://raw.githubusercontent.com/deepmap/oapi-codegen/7b010099dcf1192b3bfaa3898b5f375bb9590ddf/pkg/codegen/templates/client-with-responses.tmpl
# using raw.githubusercontent.com with a tag
client-with-responses.tmpl: https://raw.githubusercontent.com/deepmap/oapi-codegen/v1.12.4/pkg/codegen/templates/client-with-responses.tmpl
# using raw.githubusercontent.com with a branch
client-with-responses.tmpl: https://raw.githubusercontent.com/deepmap/oapi-codegen/master/pkg/codegen/templates/client-with-responses.tmpl

#This example is directly embedding the template into the config file.
client-with-responses.tmpl: |
// ClientWithResponses builds on ClientInterface to offer response payloads
type ClientWithResponses struct {
ClientInterface
}
...
# template shortened for brevity

```

Using the configuration file to load in templates **will** load in templates
with names other than those defined by the built in templates. These user
templates will not be called unless the user overrides a built in template to
call them however.
60 changes: 53 additions & 7 deletions pkg/codegen/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import (
"bytes"
"embed"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"runtime/debug"
"sort"
"strings"
Expand Down Expand Up @@ -128,13 +131,18 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) {
return "", fmt.Errorf("error parsing oapi-codegen templates: %w", err)
}

// Override built-in templates with user-provided versions
for _, tpl := range t.Templates() {
if _, ok := opts.OutputOptions.UserTemplates[tpl.Name()]; ok {
utpl := t.New(tpl.Name())
if _, err := utpl.Parse(opts.OutputOptions.UserTemplates[tpl.Name()]); err != nil {
return "", fmt.Errorf("error parsing user-provided template %q: %w", tpl.Name(), err)
}
// load user-provided templates. Will Override built-in versions.
for name, template := range opts.OutputOptions.UserTemplates {
utpl := t.New(name)

txt, err := GetUserTemplateText(template)
if err != nil {
return "", fmt.Errorf("error loading user-provided template %q: %w", name, err)
}

_, err = utpl.Parse(txt)
if err != nil {
return "", fmt.Errorf("error parsing user-provided template %q: %w", name, err)
}
}

Expand Down Expand Up @@ -817,6 +825,44 @@ func SanitizeCode(goCode string) string {
return strings.Replace(goCode, "\uFEFF", "", -1)
}

// GetUserTemplateText attempts to retrieve the template text from a passed in URL or file
// path when inputData is more than one line.
// This function will attempt to load a file first, and if it fails, will try to get the
// data from the remote endpoint.
func GetUserTemplateText(inputData string) (template string, err error) {
//if the input data is more than one line, assume its a template and return that data.
if strings.Contains(inputData, "\n") {
return inputData, nil
}

//load data from file
data, err := os.ReadFile(inputData)
//return data if found and loaded
if err == nil {
return string(data), nil
}

//check for non "not found" errors
if !os.IsNotExist(err) {
return "", fmt.Errorf("failed to open file %s: %w", inputData, err)
}

//attempt to get data from url
resp, err := http.Get(inputData)
if err != nil {
return "", fmt.Errorf("failed to execute GET request data from %s: %w", inputData, err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return "", fmt.Errorf("got non %d status code on GET %s", resp.StatusCode, inputData)
}
data, err = io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response body from GET %s: %w", inputData, err)
}

return string(data), nil
}

// LoadTemplates loads all of our template files into a text/template. The
// path of template is relative to the templates directory.
func LoadTemplates(src embed.FS, t *template.Template) error {
Expand Down
89 changes: 88 additions & 1 deletion pkg/codegen/codegen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package codegen
import (
"bytes"
_ "embed"
"fmt"
"go/format"
"io"
"net"
"net/http"
"testing"

Expand Down Expand Up @@ -77,7 +79,92 @@ func TestExamplePetStoreCodeGeneration(t *testing.T) {

func TestExamplePetStoreCodeGenerationWithUserTemplates(t *testing.T) {

userTemplates := map[string]string{"typedef.tmpl": "//blah"}
userTemplates := map[string]string{"typedef.tmpl": "//blah\n//blah"}

// Input vars for code generation:
packageName := "api"
opts := Configuration{
PackageName: packageName,
Generate: GenerateOptions{
Models: true,
},
OutputOptions: OutputOptions{
UserTemplates: userTemplates,
},
}

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

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

// Check that we have valid (formattable) code:
_, err = format.Source([]byte(code))
assert.NoError(t, err)

// Check that we have a package:
assert.Contains(t, code, "package api")

// Check that the built-in template has been overriden
assert.Contains(t, code, "//blah")
}

func TestExamplePetStoreCodeGenerationWithFileUserTemplates(t *testing.T) {

userTemplates := map[string]string{"typedef.tmpl": "./templates/typedef.tmpl"}

// Input vars for code generation:
packageName := "api"
opts := Configuration{
PackageName: packageName,
Generate: GenerateOptions{
Models: true,
},
OutputOptions: OutputOptions{
UserTemplates: userTemplates,
},
}

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

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

// Check that we have valid (formattable) code:
_, err = format.Source([]byte(code))
assert.NoError(t, err)

// Check that we have a package:
assert.Contains(t, code, "package api")

// Check that the built-in template has been overriden
assert.Contains(t, code, "// Package api provides primitives to interact with the openapi")
}

func TestExamplePetStoreCodeGenerationWithHTTPUserTemplates(t *testing.T) {

ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
defer ln.Close()

//nolint:errcheck
//Does not matter if the server returns an error on close etc.
go http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, writeErr := w.Write([]byte("//blah"))
assert.NoError(t, writeErr)
}))

t.Logf("Listening on %s", ln.Addr().String())

userTemplates := map[string]string{"typedef.tmpl": fmt.Sprintf("http://%s", ln.Addr().String())}

// Input vars for code generation:
packageName := "api"
Expand Down