Skip to content
Closed
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
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ help:
# want to run the linters or generate the SDK.
VERSION_DIR:=$(GOBIN)/versions
VERSION_GOIMPORTS:=v0.26.0
VERSION_GOLANGCILINT:=v1.61.0
VERSION_STATICCHECK:=2024.1.1
VERSION_GOLANGCILINT:=v1.64.8
VERSION_STATICCHECK:=2025.1.1
VERSION_WHATSIT:=1f5eb3ea

tools: $(GOBIN)/golangci-lint $(GOBIN)/goimports $(GOBIN)/staticcheck
Expand Down
34 changes: 34 additions & 0 deletions internal/generate/interfaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

package main

import (
"fmt"
)

// Generate the Interfaces.go file.
func generateInterfaces(file string, methods []methodTemplate) error {
f, err := openGeneratedFile(file)
if err != nil {
return err
}
defer f.Close()

// build the Client interface
fmt.Fprintln(f, "type Client interface {")
for _, method := range methods {
if method.IsListAll {
fmt.Fprintf(f, "\t%s(ctx context.Context, %s) (%s, error)\n", method.FunctionName, method.ParamsString, method.ResponseType)
} else if method.ResponseType == "" {
fmt.Fprintf(f, "\t%s(ctx context.Context, %s) error\n", method.FunctionName, method.ParamsString)
} else {
fmt.Fprintf(f, "\t%s(ctx context.Context, %s) (*%s, error)\n", method.FunctionName, method.ParamsString, method.ResponseType)
}
}
fmt.Fprintln(f, "}")
fmt.Fprintln(f, "")

return nil
}
8 changes: 7 additions & 1 deletion internal/generate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,13 @@ func generateSDK() error {
}

pathsFile := "../../oxide/paths.go"
if err := generatePaths(pathsFile, spec); err != nil {
methods, err := generatePaths(pathsFile, spec)
if err != nil {
return err
}

interfacesFile := "../../oxide/interfaces.go"
if err := generateInterfaces(interfacesFile, methods); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion internal/generate/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func Test_loadAPI(t *testing.T) {
wantErr: "no such file or directory",
},
{
name: "file does not exist",
name: "invalid version",
args: args{"generate/test_utils/INVALID_VERSION"},
wantErr: "error loading openAPI spec from \"https://raw.githubusercontent.com/oxidecomputer/omicron//openapi/nexus.json\"",
},
Expand Down
77 changes: 46 additions & 31 deletions internal/generate/paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ type paramsInfo struct {
}

// Generate the paths.go file.
func generatePaths(file string, spec *openapi3.T) error {
func generatePaths(file string, spec *openapi3.T) ([]methodTemplate, error) {
f, err := openGeneratedFile(file)
if err != nil {
return err
return nil, err
}
defer f.Close()

methods := make([]methodTemplate, 0)

// Iterate over all the paths in the spec and write the methods.
// We want to ensure we keep the order.
keys := make([]string, 0)
Expand All @@ -66,78 +68,87 @@ func generatePaths(file string, spec *openapi3.T) error {
continue
}

err := buildPath(f, spec, path, p)
tmpMethods, err := buildPath(f, spec, path, p)
if err != nil {
return err
return nil, err
}
methods = append(methods, tmpMethods...)
}

return nil
return methods, nil
}

// buildPath builds the given path as an http request to the given file.
func buildPath(f *os.File, spec *openapi3.T, path string, p *openapi3.PathItem) error {
func buildPath(f *os.File, spec *openapi3.T, path string, p *openapi3.PathItem) ([]methodTemplate, error) {
methods := make([]methodTemplate, 0)
if p.Get != nil {
err := buildMethod(f, spec, http.MethodGet, path, p.Get, false)
tmpMethods, err := buildMethod(f, spec, http.MethodGet, path, p.Get, false)
if err != nil {
return err
return nil, err
}
methods = append(methods, tmpMethods...)
}

if p.Post != nil {
err := buildMethod(f, spec, http.MethodPost, path, p.Post, false)
tmpMethods, err := buildMethod(f, spec, http.MethodPost, path, p.Post, false)
if err != nil {
return err
return nil, err
}
methods = append(methods, tmpMethods...)
}

if p.Put != nil {
err := buildMethod(f, spec, http.MethodPut, path, p.Put, false)
tmpMethods, err := buildMethod(f, spec, http.MethodPut, path, p.Put, false)
if err != nil {
return err
return nil, err
}
methods = append(methods, tmpMethods...)
}

if p.Delete != nil {
err := buildMethod(f, spec, http.MethodDelete, path, p.Delete, false)
tmpMethods, err := buildMethod(f, spec, http.MethodDelete, path, p.Delete, false)
if err != nil {
return err
return nil, err
}
methods = append(methods, tmpMethods...)
}

if p.Patch != nil {
err := buildMethod(f, spec, http.MethodPatch, path, p.Patch, false)
tmpMethods, err := buildMethod(f, spec, http.MethodPatch, path, p.Patch, false)
if err != nil {
return err
return nil, err
}
methods = append(methods, tmpMethods...)
}

if p.Head != nil {
err := buildMethod(f, spec, http.MethodHead, path, p.Head, false)
tmpMethods, err := buildMethod(f, spec, http.MethodHead, path, p.Head, false)
if err != nil {
return err
return nil, err
}
methods = append(methods, tmpMethods...)
}

if p.Options != nil {
err := buildMethod(f, spec, http.MethodOptions, path, p.Options, false)
tmpMethods, err := buildMethod(f, spec, http.MethodOptions, path, p.Options, false)
if err != nil {
return err
return nil, err
}
methods = append(methods, tmpMethods...)
}

return nil
return methods, nil
}

func buildMethod(f *os.File, spec *openapi3.T, method string, path string, o *openapi3.Operation, isGetAllPages bool) error {
func buildMethod(f *os.File, spec *openapi3.T, method string, path string, o *openapi3.Operation, isGetAllPages bool) ([]methodTemplate, error) {
respType, pagedRespType, err := getSuccessResponseType(o, isGetAllPages)
if err != nil {
return err
return nil, err
}

if len(o.Tags) == 0 || o.Tags[0] == "hidden" {
fmt.Printf("[WARN] TODO: skipping operation %q, since it has no tag or is hidden\n", o.OperationID)
return nil
return nil, nil
}

methodName := strcase.ToCamel(o.OperationID)
Expand All @@ -147,7 +158,7 @@ func buildMethod(f *os.File, spec *openapi3.T, method string, path string, o *op

if isGetAllPages {
if pagedRespType == "" {
return nil
return nil, nil
}
}

Expand All @@ -165,11 +176,11 @@ func buildMethod(f *os.File, spec *openapi3.T, method string, path string, o *op

pathParams, err := buildPathOrQueryParams("path", pInfo.parameters)
if err != nil {
return err
return nil, err
}
queryParams, err := buildPathOrQueryParams("query", pInfo.parameters)
if err != nil {
return err
return nil, err
}

sanitisedDescription := strings.ReplaceAll(o.Description, "\n", "\n// ")
Expand Down Expand Up @@ -211,18 +222,22 @@ func buildMethod(f *os.File, spec *openapi3.T, method string, path string, o *op
}

if err := writeTpl(f, config); err != nil {
return err
return nil, err
}

methods := make([]methodTemplate, 0)
methods = append(methods, config)

if pInfo.isPageResult && !isGetAllPages {
// Run the method again with get all pages for ListAll methods.
err := buildMethod(f, spec, method, path, o, true)
allPagesMethods, err := buildMethod(f, spec, method, path, o, true)
if err != nil {
return err
return nil, err
}
methods = append(methods, allPagesMethods...)
}

return nil
return methods, nil
}

func getSuccessResponseType(o *openapi3.Operation, isGetAllPages bool) (string, string, error) {
Expand Down
44 changes: 36 additions & 8 deletions internal/generate/paths_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,41 +14,69 @@ import (

func Test_generatePaths(t *testing.T) {
file := "./test_utils/paths.json"
pathsSpec, err := openapi3.NewLoader().LoadFromFile(file)
spec, err := openapi3.NewLoader().LoadFromFile(file)
if err != nil {
t.Error(fmt.Errorf("error loading openAPI spec from %q: %v", file, err))
}

type args struct {
file string
spec *openapi3.T
pathsFile string
interfacesFile string
spec *openapi3.T
}
tests := []struct {
name string
args args
wantErr string
}{
{
name: "fail on non-existent file",
args: args{"sdf/gdsf", pathsSpec},
name: "fail on non-existent paths file",
args: args{
pathsFile: "sdf/gdsf",
interfacesFile: "sdf/gdsf",
spec: spec,
},
wantErr: "no such file or directory",
},
{
name: "fail on non-existent interfaces file",
args: args{
pathsFile: "test_utils/paths_output",
interfacesFile: "sdf/gdsf",
spec: spec,
},
wantErr: "no such file or directory",
},
{
name: "success",
args: args{"test_utils/paths_output", pathsSpec},
args: args{
pathsFile: "test_utils/paths_output",
interfacesFile: "test_utils/interfaces_output",
spec: spec,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// TODO: For now the test is not properly generating the "ListAll" methods
// This is because there is a separate check to the response type. The way this works
// should be changed
if err := generatePaths(tt.args.file, tt.args.spec); err != nil {
methods, err := generatePaths(tt.args.pathsFile, tt.args.spec)
if err != nil {
assert.ErrorContains(t, err, tt.wantErr)
return
}

if err := generateInterfaces(tt.args.interfacesFile, methods); err != nil {
assert.ErrorContains(t, err, tt.wantErr)
return
}

if err := compareFiles("test_utils/paths_output_expected", tt.args.file); err != nil {
if err := compareFiles("test_utils/paths_output_expected", tt.args.pathsFile); err != nil {
t.Error(err)
}

if err := compareFiles("test_utils/interfaces_output_expected", tt.args.interfacesFile); err != nil {
t.Error(err)
}
})
Expand Down
2 changes: 1 addition & 1 deletion internal/generate/templates/listall_method.tpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{template "description" .}}func (c *Client) {{.FunctionName}}(ctx context.Context, {{.ParamsString}}) ({{.ResponseType}}, error) { {{if .HasParams}}
{{template "description" .}}func (c *client) {{.FunctionName}}(ctx context.Context, {{.ParamsString}}) ({{.ResponseType}}, error) { {{if .HasParams}}
if err := params.Validate(); err != nil {
return nil, err
}{{end}}
Expand Down
2 changes: 1 addition & 1 deletion internal/generate/templates/no_resptype_body_method.tpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{template "description" .}}func (c *Client) {{.FunctionName}}(ctx context.Context, {{.ParamsString}}) error { {{if .HasParams}}
{{template "description" .}}func (c *client) {{.FunctionName}}(ctx context.Context, {{.ParamsString}}) error { {{if .HasParams}}
if err := params.Validate(); err != nil {
return err
}{{end}}{{if .IsAppJSON}}
Expand Down
2 changes: 1 addition & 1 deletion internal/generate/templates/no_resptype_method.tpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{template "description" .}}func (c *Client) {{.FunctionName}}(ctx context.Context, {{.ParamsString}}) error { {{if .HasParams}}
{{template "description" .}}func (c *client) {{.FunctionName}}(ctx context.Context, {{.ParamsString}}) error { {{if .HasParams}}
if err := params.Validate(); err != nil {
return err
}{{end}}
Expand Down
2 changes: 1 addition & 1 deletion internal/generate/templates/resptype_body_method.tpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{template "description" .}}func (c *Client) {{.FunctionName}}(ctx context.Context, {{.ParamsString}}) (*{{.ResponseType}}, error) { {{if .HasParams}}
{{template "description" .}}func (c *client) {{.FunctionName}}(ctx context.Context, {{.ParamsString}}) (*{{.ResponseType}}, error) { {{if .HasParams}}
if err := params.Validate(); err != nil {
return nil, err
}{{end}}{{if .IsAppJSON}}
Expand Down
2 changes: 1 addition & 1 deletion internal/generate/templates/resptype_method.tpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{template "description" .}}func (c *Client) {{.FunctionName}}(ctx context.Context, {{.ParamsString}}) (*{{.ResponseType}}, error) { {{if .HasParams}}
{{template "description" .}}func (c *client) {{.FunctionName}}(ctx context.Context, {{.ParamsString}}) (*{{.ResponseType}}, error) { {{if .HasParams}}
if err := params.Validate(); err != nil {
return nil, err
}{{end}}
Expand Down
17 changes: 17 additions & 0 deletions internal/generate/test_utils/interfaces_output
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Code generated by `generate.test`. DO NOT EDIT.

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

package oxide

type Client interface {
IpPoolList(ctx context.Context, params IpPoolListParams, ) (*IpPoolResultsPage, error)
IpPoolListAllPages(ctx context.Context, params IpPoolListParams, ) ([]IpPool, error)
IpPoolCreate(ctx context.Context, params IpPoolCreateParams, ) (*IpPool, error)
IpPoolView(ctx context.Context, params IpPoolViewParams, ) (*IpPool, error)
IpPoolUpdate(ctx context.Context, params IpPoolUpdateParams, ) (*IpPool, error)
IpPoolDelete(ctx context.Context, params IpPoolDeleteParams, ) error
}

17 changes: 17 additions & 0 deletions internal/generate/test_utils/interfaces_output_expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Code generated by `generate.test`. DO NOT EDIT.

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

package oxide

type Client interface {
IpPoolList(ctx context.Context, params IpPoolListParams, ) (*IpPoolResultsPage, error)
IpPoolListAllPages(ctx context.Context, params IpPoolListParams, ) ([]IpPool, error)
IpPoolCreate(ctx context.Context, params IpPoolCreateParams, ) (*IpPool, error)
IpPoolView(ctx context.Context, params IpPoolViewParams, ) (*IpPool, error)
IpPoolUpdate(ctx context.Context, params IpPoolUpdateParams, ) (*IpPool, error)
IpPoolDelete(ctx context.Context, params IpPoolDeleteParams, ) error
}

Loading