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
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1731,6 +1731,71 @@ func TestClient_canCall() {
}
```

### With Server URLs

An OpenAPI specification makes it possible to denote Servers that a client can interact with, such as:

```yaml
servers:
- url: https://development.gigantic-server.com/v1
description: Development server
- url: https://{username}.gigantic-server.com:{port}/{basePath}
description: The production API server
variables:
username:
# note! no enum here means it is an open value
default: demo
description: this value is assigned by the service provider, in this example `gigantic-server.com`
port:
enum:
- '8443'
- '443'
default: '8443'
basePath:
# open meaning there is the opportunity to use special base paths as assigned by the provider, default is `v2`
default: v2
```

It is possible to opt-in to the generation of these Server URLs with the following configuration:

```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.json
package: serverurls
output: gen.go
generate:
# NOTE that this uses default settings - if you want to use initialisms to generate i.e. `ServerURLDevelopmentServer`, you should look up the `output-options.name-normalizer` configuration
server-urls: true
```

This will then generate the following boilerplate:

```go
// (the below does not include comments that are generated)

const ServerUrlDevelopmentServer = "https://development.gigantic-server.com/v1"

type ServerUrlTheProductionAPIServerBasePathVariable string
const ServerUrlTheProductionAPIServerBasePathVariableDefault = "v2"

type ServerUrlTheProductionAPIServerPortVariable string
const ServerUrlTheProductionAPIServerPortVariable8443 ServerUrlTheProductionAPIServerPortVariable = "8443"
const ServerUrlTheProductionAPIServerPortVariable443 ServerUrlTheProductionAPIServerPortVariable = "443"
const ServerUrlTheProductionAPIServerPortVariableDefault ServerUrlTheProductionAPIServerPortVariable = ServerUrlTheProductionAPIServerPortVariable8443

type ServerUrlTheProductionAPIServerUsernameVariable string
const ServerUrlTheProductionAPIServerUsernameVariableDefault = "demo"

func ServerUrlTheProductionAPIServer(basePath ServerUrlTheProductionAPIServerBasePathVariable, port ServerUrlTheProductionAPIServerPortVariable, username ServerUrlTheProductionAPIServerUsernameVariable) (string, error) {
// ...
}
```

Notice that for URLs that are not templated, a simple `const` definition is created.

However, for more complex URLs that defined `variables` in them, we generate the types (and any `enum` values or `default` values), and instead use a function to create the URL.

For a complete example see [`examples/generate/serverurls`](examples/generate/serverurls).

## Generating API models

If you're looking to only generate the models for interacting with a remote service, for instance if you need to hand-roll the API client for whatever reason, you can do this as-is.
Expand Down
4 changes: 4 additions & 0 deletions configuration-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@
"embedded-spec": {
"type": "boolean",
"description": "EmbeddedSpec indicates whether to embed the swagger spec in the generated code"
},
"server-urls": {
"type": "boolean",
"description": "Generate types for the `Server` definitions' URLs, instead of needing to provide your own values"
}
}
},
Expand Down
47 changes: 47 additions & 0 deletions examples/generate/serverurls/api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Server URLs can be optionally generated
servers:
# adapted from https://spec.openapis.org/oas/v3.0.3#server-object
- url: https://development.gigantic-server.com/v1
description: Development server
- url: https://staging.gigantic-server.com/v1
description: Staging server
- url: https://api.gigantic-server.com/v1
description: Production server
# adapted from https://spec.openapis.org/oas/v3.0.3#server-object
- url: https://{username}.gigantic-server.com:{port}/{basePath}
description: The production API server
variables:
username:
# note! no enum here means it is an open value
default: demo
description: this value is assigned by the service provider, in this example `gigantic-server.com`
port:
enum:
- '8443'
- '443'
default: '8443'
basePath:
# open meaning there is the opportunity to use special base paths as assigned by the provider, default is `v2`
default: v2
# an example of a type that's defined, but doesn't have a default
noDefault: {}
# # TODO this conflict will cause broken generated code https://github.com/oapi-codegen/oapi-codegen/issues/2003
# conflicting:
# enum:
# - 'default'
# - '443'
# default: 'default'
# clash with the previous definition of `Development server` to trigger a new name
- url: http://localhost:80
description: Development server
# clash with the previous definition of `Development server` to trigger a new name (again)
- url: http://localhost:80
description: Development server
# make sure that the lowercase `description` gets converted to an uppercase
- url: http://localhost:80
description: some lowercase name
# there may be URLs on their own, without a `description`
- url: http://localhost:443
8 changes: 8 additions & 0 deletions examples/generate/serverurls/cfg.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# yaml-language-server: $schema=../../../configuration-schema.json
package: serverurls
output: gen.go
generate:
server-urls: true
output-options:
# to make sure that all types are generated, even if they're unreferenced
skip-prune: true
74 changes: 74 additions & 0 deletions examples/generate/serverurls/gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 48 additions & 0 deletions examples/generate/serverurls/gen_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package serverurls

import (
"net/url"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestServerUrlTheProductionAPIServer(t *testing.T) {
t.Run("when no values are provided, it does not error", func(t *testing.T) {
serverUrl, err := NewServerUrlTheProductionAPIServer("", "", "", "")
require.NoError(t, err)

assert.Equal(t, "https://.gigantic-server.com:/", serverUrl)

// NOTE that ideally this should fail as it doesn't /seem/ to provide a valid URL, but it does seem to be valid
_, err = url.Parse(serverUrl)
require.NoError(t, err)
})

// TODO:when we validate enums, this will need more testing https://github.com/oapi-codegen/oapi-codegen/issues/2006
t.Run("when values that are not part of the enum are provided, it does not error", func(t *testing.T) {
invalidPort := ServerUrlTheProductionAPIServerPortVariable("12345")
serverUrl, err := NewServerUrlTheProductionAPIServer(
ServerUrlTheProductionAPIServerBasePathVariableDefault,
ServerUrlTheProductionAPIServerNoDefaultVariable(""),
invalidPort,
ServerUrlTheProductionAPIServerUsernameVariableDefault,
)
require.NoError(t, err)

assert.Equal(t, "https://demo.gigantic-server.com:12345/v2", serverUrl)
})

t.Run("when default values are provided, it does not error", func(t *testing.T) {
serverUrl, err := NewServerUrlTheProductionAPIServer(
ServerUrlTheProductionAPIServerBasePathVariableDefault,
ServerUrlTheProductionAPIServerNoDefaultVariable(""),
ServerUrlTheProductionAPIServerPortVariableDefault,
ServerUrlTheProductionAPIServerUsernameVariableDefault,
)
require.NoError(t, err)

assert.Equal(t, "https://demo.gigantic-server.com:8443/v2", serverUrl)
})
}
3 changes: 3 additions & 0 deletions examples/generate/serverurls/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package serverurls

//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen -config cfg.yaml api.yaml
13 changes: 13 additions & 0 deletions pkg/codegen/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,14 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) {
MergeImports(xGoTypeImports, imprts)
}

var serverURLsDefinitions string
if opts.Generate.ServerURLs {
serverURLsDefinitions, err = GenerateServerURLs(t, spec)
if err != nil {
return "", fmt.Errorf("error generating Server URLs: %w", err)
}
}

var irisServerOut string
if opts.Generate.IrisServer {
irisServerOut, err = GenerateIrisServer(t, ops)
Expand Down Expand Up @@ -326,6 +334,11 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) {
return "", fmt.Errorf("error writing constants: %w", err)
}

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

_, err = w.WriteString(typeDefinitions)
if err != nil {
return "", fmt.Errorf("error writing type definitions: %w", err)
Expand Down
2 changes: 2 additions & 0 deletions pkg/codegen/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ type GenerateOptions struct {
Models bool `yaml:"models,omitempty"`
// EmbeddedSpec indicates whether to embed the swagger spec in the generated code
EmbeddedSpec bool `yaml:"embedded-spec,omitempty"`
// ServerURLs generates types for the `Server` definitions' URLs, instead of needing to provide your own values
ServerURLs bool `yaml:"server-urls,omitempty"`
}

func (oo GenerateOptions) Validate() map[string]string {
Expand Down
81 changes: 81 additions & 0 deletions pkg/codegen/server_urls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package codegen

import (
"fmt"
"strconv"
"text/template"

"github.com/getkin/kin-openapi/openapi3"
)

const serverURLPrefix = "ServerUrl"
const serverURLSuffixIterations = 10

// ServerObjectDefinition defines the definition of an OpenAPI Server object (https://spec.openapis.org/oas/v3.0.3#server-object) as it is provided to code generation in `oapi-codegen`
type ServerObjectDefinition struct {
// GoName is the name of the variable for this Server URL
GoName string

// OAPISchema is the underlying OpenAPI representation of the Server
OAPISchema *openapi3.Server
}

func GenerateServerURLs(t *template.Template, spec *openapi3.T) (string, error) {
names := make(map[string]*openapi3.Server)

for _, server := range spec.Servers {
suffix := server.Description
if suffix == "" {
suffix = nameNormalizer(server.URL)
}
name := serverURLPrefix + UppercaseFirstCharacter(suffix)
name = nameNormalizer(name)

// if this is the only type with this name, store it
if _, conflict := names[name]; !conflict {
names[name] = server
continue
}

// otherwise, try appending a number to the name
saved := false
// NOTE that we start at 1 on purpose, as
//
// ... ServerURLDevelopmentServer
// ... ServerURLDevelopmentServer1`
//
// reads better than:
//
// ... ServerURLDevelopmentServer
// ... ServerURLDevelopmentServer0
for i := 1; i < 1+serverURLSuffixIterations; i++ {
suffixed := name + strconv.Itoa(i)
// and then store it if there's no conflict
if _, suffixConflict := names[suffixed]; !suffixConflict {
names[suffixed] = server
saved = true
break
}
}

if saved {
continue
}

// otherwise, error
return "", fmt.Errorf("failed to create a unique name for the Server URL (%#v) with description (%#v) after %d iterations", server.URL, server.Description, serverURLSuffixIterations)
}

keys := SortedMapKeys(names)
servers := make([]ServerObjectDefinition, len(keys))
i := 0
for _, k := range keys {
servers[i] = ServerObjectDefinition{
GoName: k,
OAPISchema: names[k],
}
i++
}

return GenerateTemplates([]string{"server-urls.tmpl"}, t, servers)
}
Loading