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(ogen): support URL as spec path argument #647

Merged
merged 4 commits into from
Nov 2, 2022
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
109 changes: 97 additions & 12 deletions cmd/ogen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import (
"flag"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"runtime"
Expand Down Expand Up @@ -46,12 +49,17 @@ func cleanDir(targetDir string, files []os.DirEntry) (rerr error) {
return rerr
}

func generate(data []byte, packageName, targetDir string, clean bool, opts gen.Options) error {
func generate(f file, packageName, targetDir string, clean bool, opts gen.Options) error {
data := f.data
log := opts.Logger

spec, err := ogen.Parse(data)
if err != nil {
return errors.Wrap(err, "parse spec")
// For pretty error message, we need to pass location.File.
return &location.Error{
File: f.location(),
Err: errors.Wrap(err, "parse spec"),
}
}

start := time.Now()
Expand Down Expand Up @@ -127,6 +135,84 @@ Also, you can use --ct-alias to map content types to supported ones.
return false
}

type file struct {
data []byte
fileName string
source string
rootURL *url.URL
}

func (f file) location() location.File {
return location.NewFile(f.fileName, f.source, f.data)
}

func parseSpecPath(
p string,
client *http.Client,
readFile func(string) ([]byte, error),
) (f file, opts gen.RemoteOptions, _ error) {
// FIXME(tdakkota): pass context.
if u, _ := url.Parse(p); u != nil {
switch u.Scheme {
case "http", "https":
_, fileName := path.Split(u.Path)

resp, err := client.Get(p)
if err != nil {
return f, opts, err
}
defer func() {
_ = resp.Body.Close()
}()

data, err := io.ReadAll(resp.Body)
if err != nil {
return f, opts, err
}

f = file{
data: data,
fileName: fileName,
source: p,
rootURL: u,
}
opts = gen.RemoteOptions{
ReadFile: func(p string) ([]byte, error) {
return nil, errors.New("local files are not supported in remote mode")
},
HTTPClient: client,
}
return f, opts, nil
case "":
default:
if runtime.GOOS == "windows" && filepath.VolumeName(p) != "" {
break
}
return f, opts, errors.Errorf("unsupported scheme %q", u.Scheme)
}
}
p = filepath.Clean(p)
_, fileName := filepath.Split(p)

data, err := readFile(p)
if err != nil {
return f, opts, err
}

f = file{
data: data,
fileName: fileName,
source: p,
rootURL: &url.URL{Path: filepath.ToSlash(p)},
}
opts = gen.RemoteOptions{
HTTPClient: client,
ReadFile: readFile,
}

return f, opts, nil
}

func run() error {
set := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
set.Usage = func() {
Expand Down Expand Up @@ -202,7 +288,6 @@ func run() error {
set.Usage()
return errors.New("no spec provided")
}
specPath = filepath.Clean(specPath)

logger, err := ogenzap.Create(logOptions)
if err != nil {
Expand Down Expand Up @@ -247,8 +332,11 @@ func run() error {
}()
}

specDir, fileName := filepath.Split(specPath)
data, err := os.ReadFile(specPath)
f, remoteOpts, err := parseSpecPath(
specPath,
&http.Client{Timeout: time.Minute},
os.ReadFile,
)
if err != nil {
return err
}
Expand All @@ -261,18 +349,15 @@ func run() error {
SkipUnimplemented: *skipUnimplemented,
InferSchemaType: *inferTypes,
AllowRemote: *allowRemote,
Remote: gen.RemoteOptions{
ReadFile: func(p string) ([]byte, error) {
return os.ReadFile(filepath.Join(specDir, p))
},
},
RootURL: f.rootURL,
Remote: remoteOpts,
Filters: gen.Filters{
PathRegex: filterPath,
Methods: filterMethods,
},
IgnoreNotImplemented: strings.Split(*debugIgnoreNotImplemented, ","),
ContentTypeAliases: ctAliases,
File: location.NewFile(fileName, specPath, data),
File: f.location(),
Logger: logger,
}
if expr := *skipTestsRegex; expr != "" {
Expand All @@ -291,7 +376,7 @@ func run() error {
}
}

if err := generate(data, *packageName, *targetDir, *clean, opts); err != nil {
if err := generate(f, *packageName, *targetDir, *clean, opts); err != nil {
if handleGenerateError(os.Stderr, logOptions.Color, err) {
return errors.New("generation failed")
}
Expand Down
91 changes: 91 additions & 0 deletions cmd/ogen/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package main

import (
"bytes"
"fmt"
"io"
"net/http"
"net/url"
"runtime"
"testing"

"github.com/go-faster/errors"
"github.com/stretchr/testify/require"
)

type roundTripFunc func(req *http.Request) (*http.Response, error)

func (r roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return r(req)
}

func Test_parseSpecPath(t *testing.T) {
testdata := []byte(`{}`)
urlPath := func(p string) *url.URL {
return &url.URL{Path: p}
}
urlParse := func(s string) *url.URL {
u, err := url.Parse(s)
require.NoError(t, err)
return u
}

type testCase struct {
input string
httpData []byte
fileData []byte
wantFilename string
wantURL *url.URL
}

tests := []testCase{
{"spec.json", nil, testdata, "spec.json", urlPath("spec.json")},
{"./spec.json", nil, testdata, "spec.json", urlPath("spec.json")},
{"_testdata/spec.json", nil, testdata, "spec.json", urlPath("_testdata/spec.json")},

{"http://example.com/spec.json", testdata, nil, "spec.json", urlParse("http://example.com/spec.json")},
}
if runtime.GOOS == "windows" {
tests = append(tests, []testCase{
{`_testdata\spec.json`, nil, testdata, "spec.json", urlPath("_testdata/spec.json")},
{`C:\_testdata\spec.json`, nil, testdata, "spec.json", urlPath("C:/_testdata/spec.json")},
}...)
}
for i, tt := range tests {
tt := tt
t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
a := require.New(t)

var (
data = tt.httpData
readFile = func(filename string) ([]byte, error) {
return nil, errors.Errorf("unexpected read file: %q", filename)
}
httpClient = &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(data)),
}, nil
}),
}
)
if data == nil {
data = tt.fileData
readFile = func(filename string) ([]byte, error) {
return data, nil
}
httpClient = &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
return nil, errors.Errorf("unexpected http request: %q", req.URL)
}),
}
}

f, _, err := parseSpecPath(tt.input, httpClient, readFile)
a.NoError(err)
a.Equal(tt.wantFilename, f.fileName)
a.Equal(tt.wantURL, f.rootURL)
})
}
}
106 changes: 0 additions & 106 deletions gen/external.go

This file was deleted.

3 changes: 2 additions & 1 deletion gen/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,12 @@ func NewGenerator(spec *ogen.Spec, opts Options) (*Generator, error) {

var external jsonschema.ExternalResolver
if opts.AllowRemote {
external = newExternalResolver(opts.Remote)
external = jsonschema.NewExternalResolver(opts.Remote)
}
api, err := parser.Parse(spec, parser.Settings{
External: external,
File: opts.File,
RootURL: opts.RootURL,
InferTypes: opts.InferSchemaType,
})
if err != nil {
Expand Down
Loading