├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── README.asciidoc ├── cmd ├── cmd.go ├── generate.go └── generate_test.go ├── generate ├── spec.go └── spec_test.go ├── go.mod ├── go.sum ├── interpret ├── _test_files │ ├── func_with_parameter.go │ ├── func_with_path.go │ ├── main_with_info.go │ └── structs_with_models.go ├── interpret.go └── interpret_test.go ├── main.go └── models ├── models.go └── models_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | test: 4 | docker: 5 | - image: circleci/golang:1 6 | 7 | working_directory: /go/src/github.com/VanMoof/gopenapi 8 | 9 | environment: # environment variables for the build itself 10 | GO111MODULE: "on" 11 | 12 | steps: 13 | - checkout 14 | - restore_cache: # restores saved cache if no changes are detected since last run 15 | keys: 16 | - v1-pkg-cache 17 | - run: 18 | name: Downloading project dependecies 19 | command: | 20 | go mod download 21 | go get -u github.com/jstemmer/go-junit-report 22 | - run: 23 | name: Run unit tests 24 | command: | 25 | mkdir -p test-results/go-test 26 | go test -v -cover ./... 2>&1 | tee testreport; cat testreport | /go/bin/go-junit-report > test-results/go-test/results.xml 27 | - save_cache: 28 | key: v1-pkg-cache 29 | paths: 30 | - /go/pkg 31 | 32 | workflows: 33 | version: 2 34 | test: 35 | jobs: 36 | - test 37 | 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 VanMoof 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.asciidoc: -------------------------------------------------------------------------------- 1 | = GOpenAPI 2 | 3 | image:https://circleci.com/gh/VanMoof/gopenapi.svg?style=svg&circle-token=3af6268b2c8da20c22632261882d358e3027c045["CircleCI", link="https://circleci.com/gh/VanMoof/gopenapi"] 4 | 5 | An OpenAPI utility for Go. 6 | This project aims to bring support of OpenAPI v3. 7 | 8 | == Usage 9 | 10 | ```bash 11 | $ gopenapi [command] [arg] 12 | ``` 13 | 14 | === Generating Specifications From Code 15 | 16 | ```bash 17 | gopenapi generate spec [optional path] [flags] 18 | ``` 19 | 20 | ==== Args 21 | 22 | ```bash 23 | [optional path] Optionally specify the directory in which to search. Accepts absolute paths. Relative paths are relative to the current directory. (default ".") 24 | ``` 25 | 26 | ==== Flags 27 | 28 | ```bash 29 | -f, --format string The format of the output. May be json or yaml (default "json") 30 | -o, --output string Where the output should be directed. May be '-' (stdout) or a path to a file (default "-") 31 | ``` 32 | 33 | ==== Format 34 | 35 | Code is annotated with different types of comments that help generate the spec. 36 | 37 | The comment contains a keyword that specifies the type of the OpenAPI element. 38 | 39 | The content of the comment should be a valid YAML OpenAPI element 40 | 41 | ===== Info 42 | 43 | Begin a comment with `gopenapi:info` and follow up with a YAML representation of the OpenAPI Info element. 44 | 45 | This element is then set to the `info` property of the specification. 46 | 47 | ```go 48 | package main 49 | 50 | /* 51 | gopenapi:info 52 | title: The App Name 53 | version: 1.0 54 | description: |- 55 | The app description 56 | contact: 57 | name: Jimbob Jones 58 | url: https://jones.com 59 | email: jimbob@jones.com 60 | license: 61 | name: Apache 2.0 62 | url: https://www.apache.org/licenses/LICENSE-2.0.html 63 | */ 64 | func main() { 65 | } 66 | ``` 67 | 68 | ===== Path 69 | 70 | Begin a comment with `gopenapi:path` and follow up with a YAML representation of the OpenAPI PathItem element. 71 | 72 | This element is then appended to the `paths` property of the specification. 73 | 74 | ```go 75 | package main 76 | 77 | /* 78 | gopenapi:path 79 | /ping: 80 | get: 81 | responses: 82 | 200: 83 | description: |- 84 | The default response of "ping" 85 | content: 86 | text/plain: 87 | example: pong 88 | */ 89 | func ControllerFunc() { 90 | } 91 | ``` 92 | 93 | ===== Object Schema 94 | 95 | Annotate a struct with a `gopenapi:objectSchema`. 96 | 97 | The generated ObjectSchema element will be appended to the `components.schemas` property of the specification. 98 | 99 | ```go 100 | //gopenapi:objectSchema 101 | type RootModel struct { 102 | IntField int64 `json:"intField"` 103 | StringField string `json:"stringField"` 104 | } 105 | 106 | // This struct will be ignored 107 | type IgnoredModel struct { 108 | } 109 | 110 | //gopenapi:objectSchema 111 | type AliasedModels []*AliasedModel // This alias will appear as a schema too 112 | 113 | //gopenapi:objectSchema 114 | type AliasedModel struct { 115 | IgnoredField string `json:"-"` // This field will be ignored 116 | TimeField time.Time 117 | } 118 | 119 | ``` 120 | 121 | ===== Parameter 122 | 123 | Annotate a `const` or a `var` with a `gopenapi:parameter`. 124 | 125 | The annotated field will be appended to the `components.parameters` property of the specification. 126 | 127 | ```go 128 | /* 129 | gopenapi:parameter 130 | in: path 131 | required: true 132 | content: 133 | text/plain: 134 | example: 30 135 | */ 136 | const Limit = "limit" 137 | ``` 138 | 139 | The name of the field (`Limit`) will be the parameter identifier and the value of the field (`limit`) will be the name of the parameter. -------------------------------------------------------------------------------- /cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "os" 6 | ) 7 | 8 | func Execute() error { 9 | var rootCmd = &cobra.Command{ 10 | Use: "gopenapi", 11 | Short: "An OpenAPI utility for Go", 12 | } 13 | var generateCmd = &cobra.Command{ 14 | Use: "generate", 15 | Short: "The generator utility", 16 | } 17 | 18 | var format string 19 | var output string 20 | var generateSpecCmd = &cobra.Command{ 21 | Use: "spec [optional path]", 22 | Short: "The spec generator utility", 23 | Long: "The spec generator utility can GenerateSpec specifications from source code", 24 | 25 | Run: func(cmd *cobra.Command, args []string) { 26 | if err := GenerateSpec(format, output, args); err != nil { 27 | println(err) 28 | os.Exit(1) 29 | } 30 | }, 31 | } 32 | generateSpecCmd.Flags().StringVarP(&format, "format", "f", "json", "The format of the output. May be json or yaml") 33 | generateSpecCmd.Flags().StringVarP(&output, "output", "o", "-", "Where the output should be directed. May be '-' (stdout) or a path to a file") 34 | 35 | generateCmd.AddCommand(generateSpecCmd) 36 | rootCmd.AddCommand(generateCmd) 37 | 38 | return rootCmd.Execute() 39 | } 40 | -------------------------------------------------------------------------------- /cmd/generate.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/VanMoof/gopenapi/generate" 6 | "github.com/VanMoof/gopenapi/interpret" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | func GenerateSpec(format string, output string, args []string) error { 13 | givenPath := "" 14 | if len(args) != 0 { 15 | givenPath = args[0] 16 | } 17 | normalizedPath, err := NormalizeInputPath(givenPath) 18 | if err != nil { 19 | return fmt.Errorf("failed to normalize working directory: %w", err) 20 | } 21 | 22 | out, err := ResolveOutputWriter(output) 23 | if err != nil { 24 | return err 25 | } 26 | s := ResolveOutputSink(format, out) 27 | return generate.Generate(generate.GoFileVisitor{BasePath: normalizedPath}, &interpret.ASTInterpreter{}, s) 28 | } 29 | 30 | func ResolveOutputSink(format string, out io.WriteCloser) generate.Sink { 31 | var s generate.Sink 32 | if format == "json" { 33 | s = &generate.JSONSink{W: out} 34 | } else if format == "yaml" { 35 | s = &generate.YAMLSink{W: out} 36 | } 37 | return s 38 | } 39 | 40 | func ResolveOutputWriter(output string) (io.WriteCloser, error) { 41 | if output == "-" { 42 | return os.Stdout, nil 43 | } 44 | out, outFileError := os.Create(output) 45 | if outFileError != nil { 46 | return nil, outFileError 47 | } 48 | return out, nil 49 | } 50 | 51 | func NormalizeInputPath(inputPath string) (string, error) { 52 | if filepath.IsAbs(inputPath) { 53 | return inputPath, nil 54 | } 55 | currentDir, err := os.Getwd() 56 | if err != nil { 57 | return "", err 58 | } 59 | return filepath.Join(currentDir, inputPath), nil 60 | } 61 | -------------------------------------------------------------------------------- /cmd/generate_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "github.com/VanMoof/gopenapi/cmd" 7 | "github.com/stretchr/testify/assert" 8 | "gopkg.in/yaml.v3" 9 | "io" 10 | "io/ioutil" 11 | "os" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | func TestGenerateSpec_YAMLFile(t *testing.T) { 17 | a := assert.New(t) 18 | 19 | tempFile, tempFileError := ioutil.TempFile("", "*.yaml") 20 | a.NoError(tempFileError) 21 | a.NoError(cmd.GenerateSpec("yaml", tempFile.Name(), []string{"../interpret/_test_files"})) 22 | 23 | decoded := map[string]interface{}{} 24 | a.NoError(yaml.NewDecoder(tempFile).Decode(&decoded)) 25 | a.Equal("3.0.2", decoded["openapi"]) 26 | } 27 | 28 | func TestGenerateSpec_JSONFile(t *testing.T) { 29 | a := assert.New(t) 30 | 31 | tempFile, tempFileError := ioutil.TempFile("", "*.yaml") 32 | a.NoError(tempFileError) 33 | a.NoError(cmd.GenerateSpec("json", tempFile.Name(), []string{"../interpret/_test_files"})) 34 | 35 | decoded := map[string]interface{}{} 36 | a.NoError(json.NewDecoder(tempFile).Decode(&decoded)) 37 | a.Equal("3.0.2", decoded["openapi"]) 38 | } 39 | 40 | func TestGenerateSpec_JSONStdout(t *testing.T) { 41 | a := assert.New(t) 42 | 43 | writeFunc := func() { 44 | a.NoError(cmd.GenerateSpec("json", "-", []string{"../interpret/_test_files"})) 45 | } 46 | assertFunc := func(out string) { 47 | decoded := map[string]interface{}{} 48 | a.NoError(json.NewDecoder(strings.NewReader(out)).Decode(&decoded)) 49 | a.Equal("3.0.2", decoded["openapi"]) 50 | } 51 | withPipedStdOut(writeFunc, assertFunc) 52 | } 53 | 54 | func TestResolveOutputWriter_Stdout(t *testing.T) { 55 | a := assert.New(t) 56 | 57 | writeFunc := func() { 58 | writer, writerError := cmd.ResolveOutputWriter("-") 59 | a.NoError(writerError) 60 | writer.Write([]byte{'a'}) 61 | } 62 | assertFunc := func(out string) { 63 | a.Equal("a", out) 64 | } 65 | withPipedStdOut(writeFunc, assertFunc) 66 | } 67 | 68 | func TestResolveOutputWriter_File(t *testing.T) { 69 | a := assert.New(t) 70 | 71 | tempFile, tempFileError := ioutil.TempFile("", "*.txt") 72 | a.NoError(tempFileError) 73 | writer, writerError := cmd.ResolveOutputWriter(tempFile.Name()) 74 | a.NoError(writerError) 75 | writer.Write([]byte{'a'}) 76 | 77 | content, readError := ioutil.ReadFile(tempFile.Name()) 78 | a.NoError(readError) 79 | a.Equal("a", string(content)) 80 | } 81 | 82 | func TestResolveOutputSink_YAML(t *testing.T) { 83 | a := assert.New(t) 84 | 85 | var buff bytes.Buffer 86 | closableBuff := &ClosableBuff{b: &buff} 87 | sink := cmd.ResolveOutputSink("yaml", closableBuff) 88 | a.NoError(sink.Write(map[string]string{"key": "value"})) 89 | a.Equal("key: value\n", buff.String()) 90 | } 91 | 92 | func TestResolveOutputSink_JSON(t *testing.T) { 93 | a := assert.New(t) 94 | 95 | var buff bytes.Buffer 96 | closableBuff := &ClosableBuff{b: &buff} 97 | sink := cmd.ResolveOutputSink("json", closableBuff) 98 | a.NoError(sink.Write(map[string]string{"key": "value"})) 99 | a.Equal("{\n \"key\": \"value\"\n}\n", buff.String()) 100 | } 101 | 102 | func TestNormalizeInputPath_Absolute(t *testing.T) { 103 | a := assert.New(t) 104 | 105 | normalized, err := cmd.NormalizeInputPath("/a/b/c") 106 | a.NoError(err) 107 | a.Equal("/a/b/c", normalized) 108 | } 109 | 110 | func TestNormalizeInputPath_Relative(t *testing.T) { 111 | a := assert.New(t) 112 | 113 | normalized, err := cmd.NormalizeInputPath("jim/bob") 114 | a.NoError(err) 115 | a.Contains(normalized, "jim/bob") 116 | } 117 | 118 | //Piping stdout taken from https://stackoverflow.com/questions/10473800 119 | func withPipedStdOut(writeFunc func(), assertFunc func(out string)) { 120 | old := os.Stdout // keep backup of the real stdout 121 | r, w, _ := os.Pipe() 122 | os.Stdout = w 123 | 124 | writeFunc() 125 | 126 | outC := make(chan string) 127 | // copy the output in a separate goroutine so printing can't block indefinitely 128 | go func() { 129 | var buf bytes.Buffer 130 | io.Copy(&buf, r) 131 | outC <- buf.String() 132 | }() 133 | 134 | // back to normal state 135 | w.Close() 136 | os.Stdout = old // restoring the real stdout 137 | out := <-outC 138 | 139 | assertFunc(out) 140 | } 141 | 142 | type ClosableBuff struct { 143 | b *bytes.Buffer 144 | } 145 | 146 | func (c *ClosableBuff) Write(p []byte) (n int, err error) { 147 | return c.b.Write(p) 148 | } 149 | 150 | func (c *ClosableBuff) Close() error { 151 | return nil 152 | } 153 | -------------------------------------------------------------------------------- /generate/spec.go: -------------------------------------------------------------------------------- 1 | package generate 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/VanMoof/gopenapi/interpret" 7 | "github.com/VanMoof/gopenapi/models" 8 | "gopkg.in/yaml.v3" 9 | "io" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | ) 14 | 15 | type Sink interface { 16 | Write(interface{}) error 17 | } 18 | 19 | type JSONSink struct { 20 | W io.WriteCloser 21 | } 22 | 23 | func (j *JSONSink) Write(i interface{}) error { 24 | defer j.W.Close() 25 | encoder := json.NewEncoder(j.W) 26 | encoder.SetIndent("", " ") 27 | return encoder.Encode(i) 28 | } 29 | 30 | type YAMLSink struct { 31 | W io.WriteCloser 32 | } 33 | 34 | func (y *YAMLSink) Write(i interface{}) error { 35 | defer y.W.Close() 36 | encoder := yaml.NewEncoder(y.W) 37 | encoder.SetIndent(2) 38 | return encoder.Encode(i) 39 | } 40 | 41 | type FileVisitor interface { 42 | VisitFiles(func(filePath string, info os.FileInfo, err error) error) error 43 | } 44 | 45 | type GoFileVisitor struct { 46 | BasePath string 47 | } 48 | 49 | func (g GoFileVisitor) VisitFiles(f func(filePath string, info os.FileInfo, err error) error) error { 50 | return filepath.Walk(g.BasePath, func(filePath string, info os.FileInfo, err error) error { 51 | if info.IsDir() { 52 | return nil 53 | } 54 | if strings.HasSuffix(filePath, ".go") { 55 | return f(filePath, info, err) 56 | } 57 | return nil 58 | }) 59 | } 60 | 61 | func Generate(f FileVisitor, i interpret.Interpreter, s Sink) error { 62 | root := models.Root{OpenAPI: "3.0.2", Components: &models.Components{}} 63 | 64 | err := f.VisitFiles(func(filePath string, info os.FileInfo, err error) error { 65 | file, _ := os.Open(filePath) 66 | defer file.Close() 67 | return i.InterpretFile(file, &root) 68 | }) 69 | 70 | if err != nil { 71 | return fmt.Errorf("failed to read files: %w", err) 72 | } 73 | 74 | err = s.Write(&root) 75 | if err != nil { 76 | return err 77 | } 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /generate/spec_test.go: -------------------------------------------------------------------------------- 1 | package generate_test 2 | 3 | import ( 4 | "errors" 5 | "github.com/VanMoof/gopenapi/generate" 6 | "github.com/VanMoof/gopenapi/models" 7 | "github.com/stretchr/testify/assert" 8 | "io/ioutil" 9 | "os" 10 | "testing" 11 | ) 12 | 13 | func TestJSONSink(t *testing.T) { 14 | a := assert.New(t) 15 | tempJSON, tempFileError := ioutil.TempFile("", "*.json") 16 | a.NoError(tempFileError) 17 | sink := generate.JSONSink{W: tempJSON} 18 | 19 | writeError := sink.Write(map[string]string{"key": "value"}) 20 | a.NoError(writeError) 21 | 22 | writtenContent, readError := ioutil.ReadFile(tempJSON.Name()) 23 | a.NoError(readError) 24 | a.Equal("{\n \"key\": \"value\"\n}\n", string(writtenContent)) 25 | } 26 | 27 | func TestYAMLSink(t *testing.T) { 28 | a := assert.New(t) 29 | tempYAML, tempFileError := ioutil.TempFile("", "*.yaml") 30 | a.NoError(tempFileError) 31 | sink := generate.YAMLSink{W: tempYAML} 32 | 33 | writeError := sink.Write(map[string]string{"key": "value"}) 34 | a.NoError(writeError) 35 | 36 | writtenContent, readError := ioutil.ReadFile(tempYAML.Name()) 37 | a.NoError(readError) 38 | a.Equal("key: value\n", string(writtenContent)) 39 | } 40 | 41 | func TestGoFileVisitor(t *testing.T) { 42 | a := assert.New(t) 43 | tempDir, tempDirError := ioutil.TempDir("", "some-dir") 44 | a.NoError(tempDirError) 45 | 46 | tempGoFile, tempGoFileError := ioutil.TempFile(tempDir, "*.go") 47 | a.NoError(tempGoFileError) 48 | _, tempTxtFileError := ioutil.TempFile(tempDir, "*.txt") 49 | a.NoError(tempTxtFileError) 50 | 51 | var visitedFiles []string 52 | visitor := &generate.GoFileVisitor{BasePath: tempDir} 53 | visitError := visitor.VisitFiles(func(filePath string, info os.FileInfo, err error) error { 54 | visitedFiles = append(visitedFiles, filePath) 55 | return nil 56 | }) 57 | a.NoError(visitError) 58 | 59 | a.Len(visitedFiles, 1) 60 | a.Equal(tempGoFile.Name(), visitedFiles[0]) 61 | } 62 | 63 | func TestGenerate_FailOnVisitor(t *testing.T) { 64 | a := assert.New(t) 65 | 66 | visitor := &testFileVisitor{fail: true} 67 | interpreter := &testInterpreter{} 68 | sink := &testSink{} 69 | 70 | a.Error(generate.Generate(visitor, interpreter, sink)) 71 | a.True(visitor.called) 72 | a.False(interpreter.called) 73 | a.False(sink.called) 74 | } 75 | 76 | func TestGenerate_FailOnInterpreter(t *testing.T) { 77 | a := assert.New(t) 78 | 79 | visitor := &testFileVisitor{} 80 | interpreter := &testInterpreter{fail: true} 81 | sink := &testSink{} 82 | 83 | a.Error(generate.Generate(visitor, interpreter, sink)) 84 | a.True(visitor.called) 85 | a.True(interpreter.called) 86 | a.False(sink.called) 87 | } 88 | 89 | func TestGenerate_FailOnSink(t *testing.T) { 90 | a := assert.New(t) 91 | 92 | visitor := &testFileVisitor{} 93 | interpreter := &testInterpreter{} 94 | sink := &testSink{fail: true} 95 | 96 | a.Error(generate.Generate(visitor, interpreter, sink)) 97 | a.True(visitor.called) 98 | a.True(interpreter.called) 99 | a.True(sink.called) 100 | } 101 | 102 | func TestGenerate(t *testing.T) { 103 | a := assert.New(t) 104 | 105 | visitor := &testFileVisitor{} 106 | interpreter := &testInterpreter{} 107 | sink := &testSink{} 108 | 109 | a.NoError(generate.Generate(visitor, interpreter, sink)) 110 | a.True(visitor.called) 111 | a.True(interpreter.called) 112 | a.True(sink.called) 113 | } 114 | 115 | type testFileVisitor struct { 116 | called bool 117 | fail bool 118 | } 119 | 120 | func (t *testFileVisitor) VisitFiles(f func(filePath string, info os.FileInfo, err error) error) error { 121 | t.called = true 122 | if t.fail { 123 | return errors.New("something happened") 124 | } 125 | return f("", nil, nil) 126 | } 127 | 128 | type testInterpreter struct { 129 | called bool 130 | fail bool 131 | } 132 | 133 | func (t *testInterpreter) InterpretFile(file *os.File, root *models.Root) error { 134 | t.called = true 135 | if t.fail { 136 | return errors.New("something happened") 137 | } 138 | return nil 139 | } 140 | 141 | type testSink struct { 142 | called bool 143 | fail bool 144 | } 145 | 146 | func (t *testSink) Write(interface{}) error { 147 | t.called = true 148 | if t.fail { 149 | return errors.New("something happened") 150 | } 151 | return nil 152 | } 153 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/VanMoof/gopenapi 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/spf13/cobra v0.0.5 7 | github.com/stretchr/testify v1.4.0 8 | gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 3 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 4 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 5 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 6 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 7 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 12 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 13 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 14 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 15 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 16 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 17 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 18 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 22 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 23 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 24 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= 25 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 26 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 27 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 28 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 29 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 30 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 31 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 32 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 33 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 34 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 35 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 36 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 37 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 38 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 39 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 40 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 41 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 42 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 43 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 44 | gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966 h1:B0J02caTR6tpSJozBJyiAzT6CtBzjclw4pgm9gg8Ys0= 45 | gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 46 | -------------------------------------------------------------------------------- /interpret/_test_files/func_with_parameter.go: -------------------------------------------------------------------------------- 1 | // +build testResource 2 | 3 | package _test_files 4 | 5 | /* 6 | gopenapi:parameter 7 | in: path 8 | required: true 9 | content: 10 | text/plain: 11 | example: some text 12 | */ 13 | const ConstParamName = "constParamName" 14 | 15 | /* 16 | gopenapi:parameter 17 | in: query 18 | required: true 19 | content: 20 | text/plain: 21 | example: some text 22 | */ 23 | var VarParamName = "varParamName" 24 | -------------------------------------------------------------------------------- /interpret/_test_files/func_with_path.go: -------------------------------------------------------------------------------- 1 | // +build testResource 2 | 3 | package _test_files 4 | 5 | /* 6 | gopenapi:path 7 | /ping: 8 | get: 9 | responses: 10 | 200: 11 | description: |- 12 | The default response of "ping" 13 | content: 14 | text/plain: 15 | example: pong 16 | */ 17 | func anotherFunc() { 18 | } 19 | -------------------------------------------------------------------------------- /interpret/_test_files/main_with_info.go: -------------------------------------------------------------------------------- 1 | // +build testResource 2 | 3 | package _test_files 4 | 5 | /* 6 | gopenapi:info 7 | title: The App Name 8 | version: 1.0 9 | description: |- 10 | The app description 11 | contact: 12 | name: Jimbob Jones 13 | url: https://jones.com 14 | email: jimbob@jones.com 15 | license: 16 | name: Apache 2.0 17 | url: https://www.apache.org/licenses/LICENSE-2.0.html 18 | */ 19 | func main() { 20 | } 21 | -------------------------------------------------------------------------------- /interpret/_test_files/structs_with_models.go: -------------------------------------------------------------------------------- 1 | // +build testResource 2 | 3 | package _test_files 4 | 5 | import "time" 6 | 7 | //gopenapi:objectSchema 8 | type RootModel struct { 9 | IntField int64 `json:"intField"` 10 | StringField string `json:"stringField"` 11 | SubModels []*SubModel `json:"subModels"` 12 | } 13 | 14 | //gopenapi:objectSchema 15 | type SubModel struct { 16 | FloatField float64 `json:"floatField"` 17 | SubSubModel map[string]*SubSubModel `json:"subSubModel"` 18 | } 19 | 20 | //gopenapi:objectSchema 21 | type SubSubModel struct { 22 | BoolField bool `json:"boolField"` 23 | Aliased AliasedSubs `json:"aliased"` 24 | } 25 | 26 | type IgnoredModel struct { 27 | } 28 | 29 | //gopenapi:objectSchema 30 | type AliasedSubs []*AliasedSub 31 | 32 | //gopenapi:objectSchema 33 | type AliasedSub struct { 34 | IgnoredField string `json:"-"` 35 | TimeField time.Time 36 | } 37 | -------------------------------------------------------------------------------- /interpret/interpret.go: -------------------------------------------------------------------------------- 1 | package interpret 2 | 3 | import ( 4 | "fmt" 5 | "github.com/VanMoof/gopenapi/models" 6 | "go/ast" 7 | "go/parser" 8 | "go/token" 9 | "gopkg.in/yaml.v3" 10 | "os" 11 | "reflect" 12 | "strconv" 13 | "strings" 14 | "unicode" 15 | ) 16 | 17 | type Interpreter interface { 18 | InterpretFile(file *os.File, root *models.Root) error 19 | } 20 | 21 | type ASTInterpreter struct { 22 | } 23 | 24 | func (a *ASTInterpreter) InterpretFile(file *os.File, root *models.Root) error { 25 | fileSet := token.NewFileSet() 26 | 27 | parsedFile, parseError := parser.ParseFile(fileSet, file.Name(), file, parser.ParseComments) 28 | if parseError != nil { 29 | return fmt.Errorf("failed to interpret file %s: %w", file.Name(), parseError) 30 | } 31 | return interpretFile(parsedFile, root) 32 | } 33 | 34 | func interpretFile(parsedFile *ast.File, root *models.Root) error { 35 | declarations := parsedFile.Decls 36 | for _, declaration := range declarations { 37 | switch declaration.(type) { 38 | case *ast.FuncDecl: 39 | err := openAPIBlockFromFunctionDeclaration(declaration.(*ast.FuncDecl), root) 40 | if err != nil { 41 | return err 42 | } 43 | case *ast.GenDecl: 44 | openAPIBlockFromGenDeclaration(declaration.(*ast.GenDecl), root) 45 | } 46 | } 47 | return nil 48 | } 49 | 50 | func openAPIBlockFromFunctionDeclaration(funcDecl *ast.FuncDecl, root *models.Root) error { 51 | commentGroup := funcDecl.Doc 52 | cleanedComment := cleanComment(commentGroup.Text()) 53 | err := commentAsOpenAPIBlock(root, cleanedComment) 54 | if err != nil { 55 | return fmt.Errorf("failed to resolve comment as OpenAPI element: %w", err) 56 | } 57 | return nil 58 | } 59 | 60 | func openAPIBlockFromGenDeclaration(genDecl *ast.GenDecl, root *models.Root) { 61 | switch genDecl.Tok { 62 | case token.TYPE: 63 | openAPIBlockFromTypeDeclaration(genDecl, root) 64 | case token.CONST, token.VAR: 65 | openAPIBlockFromConstAndVarDeclaration(genDecl, root) 66 | } 67 | } 68 | 69 | func openAPIBlockFromTypeDeclaration(decl *ast.GenDecl, root *models.Root) { 70 | commentGroup := decl.Doc 71 | if !strings.Contains(commentGroup.Text(), "gopenapi:objectSchema") { 72 | return 73 | } 74 | for _, spec := range decl.Specs { 75 | switch spec.(type) { 76 | case *ast.TypeSpec: 77 | openAPIBlockFromTypeSpec(spec.(*ast.TypeSpec), root) 78 | } 79 | } 80 | } 81 | 82 | func openAPIBlockFromConstAndVarDeclaration(decl *ast.GenDecl, root *models.Root) error { 83 | cleanedComment := cleanComment(decl.Doc.Text()) 84 | if !strings.HasPrefix(cleanedComment, "gopenapi:parameter") { 85 | return nil 86 | } 87 | if root.Components == nil { 88 | root.Components = &models.Components{} 89 | } 90 | if root.Components.Parameters == nil { 91 | root.Components.Parameters = map[string]*models.Parameter{} 92 | } 93 | cleanedComment = strings.TrimPrefix(cleanedComment, "gopenapi:parameter") 94 | spec := decl.Specs[0] 95 | valueSpec := spec.(*ast.ValueSpec) 96 | parameter := models.Parameter{} 97 | root.Components.Parameters[valueSpec.Names[0].Name] = ¶meter 98 | basicLit := valueSpec.Values[0].(*ast.BasicLit) 99 | unquoted, unquoteError := strconv.Unquote(basicLit.Value) 100 | if unquoteError != nil { 101 | return unquoteError 102 | } 103 | parameter.Name = unquoted 104 | 105 | err := yaml.NewDecoder(strings.NewReader(cleanedComment)).Decode(¶meter) 106 | if err != nil { 107 | return fmt.Errorf("failed to decode comment:\n%s\nError: %w", cleanedComment, err) 108 | } 109 | return nil 110 | } 111 | 112 | func openAPIBlockFromTypeSpec(typeSpec *ast.TypeSpec, root *models.Root) { 113 | if root.Components == nil { 114 | root.Components = &models.Components{} 115 | } 116 | if root.Components.Schemas == nil { 117 | root.Components.Schemas = map[string]*models.Schema{} 118 | } 119 | 120 | newSchema := &models.Schema{ 121 | Type: "object", 122 | Properties: map[string]*models.Schema{}, 123 | } 124 | newSchemaName := lower(typeSpec.Name.Name) 125 | 126 | root.Components.Schemas[newSchemaName] = newSchema 127 | switch typeSpec.Type.(type) { 128 | case *ast.StructType: 129 | structType := typeSpec.Type.(*ast.StructType) 130 | schemaFieldsFromStructType(structType, newSchema) 131 | case *ast.ArrayType: 132 | arrayType := typeSpec.Type.(*ast.ArrayType) 133 | setSchemaType(newSchema, "array") 134 | 135 | starExpr := arrayType.Elt.(*ast.StarExpr) 136 | 137 | newSchema.Items = &models.Schema{} 138 | setSchemaType(newSchema.Items, starExpr.X.(*ast.Ident).Name) 139 | } 140 | } 141 | 142 | func structFieldName(structField *ast.Field) string { 143 | if structField.Tag == nil { 144 | return lower(structField.Names[0].Name) 145 | } 146 | structTag := reflect.StructTag(strings.ReplaceAll(structField.Tag.Value, "`", "")) 147 | fieldName := structTag.Get("json") 148 | if fieldName == "-" { 149 | return "" 150 | } 151 | return fieldName 152 | } 153 | 154 | func schemaFieldsFromStructType(structType *ast.StructType, newSchema *models.Schema) { 155 | structFields := structType.Fields 156 | for _, structField := range structFields.List { 157 | fieldName := structFieldName(structField) 158 | if fieldName == "" { 159 | continue 160 | } 161 | newSchema.Properties[fieldName] = &models.Schema{} 162 | structFieldType := structField.Type 163 | switch structFieldType.(type) { 164 | case *ast.SelectorExpr: 165 | selectorExpr := structFieldType.(*ast.SelectorExpr) 166 | name := fmt.Sprintf("%s.%s", selectorExpr.X.(*ast.Ident).Name, selectorExpr.Sel.Name) 167 | setSchemaType(newSchema.Properties[fieldName], name) 168 | case *ast.Ident: 169 | setSchemaType(newSchema.Properties[fieldName], structFieldType.(*ast.Ident).Name) 170 | case *ast.ArrayType: 171 | setSchemaType(newSchema.Properties[fieldName], "array") 172 | 173 | arrayType := structFieldType.(*ast.ArrayType) 174 | starExpr := arrayType.Elt.(*ast.StarExpr) 175 | 176 | newSchema.Properties[fieldName].Items = &models.Schema{} 177 | setSchemaType(newSchema.Properties[fieldName].Items, starExpr.X.(*ast.Ident).Name) 178 | case *ast.MapType: 179 | setSchemaType(newSchema.Properties[fieldName], "object") 180 | mapSchema := &models.Schema{} 181 | 182 | mapType := structFieldType.(*ast.MapType) 183 | setSchemaType(mapSchema, mapType.Value.(*ast.StarExpr).X.(*ast.Ident).Name) 184 | newSchema.Properties[fieldName].AdditionalProperties = mapSchema 185 | } 186 | } 187 | } 188 | 189 | func lower(s string) string { 190 | a := []rune(s) 191 | a[0] = unicode.ToLower(a[0]) 192 | return string(a) 193 | } 194 | 195 | func commentAsOpenAPIBlock(r *models.Root, comment string) error { 196 | types := map[string]func(*models.Root) interface{}{ 197 | "gopenapi:info": func(r *models.Root) interface{} { 198 | r.Info = &models.Info{} 199 | return r.Info 200 | }, 201 | "gopenapi:path": func(r *models.Root) interface{} { 202 | if r.Paths == nil { 203 | r.Paths = map[string]*models.PathItem{} 204 | } 205 | return &r.Paths 206 | }, 207 | } 208 | for blockType, modelPointerRetriever := range types { 209 | if strings.HasPrefix(comment, blockType) { 210 | comment = strings.TrimPrefix(comment, blockType) 211 | modelPointer := modelPointerRetriever(r) 212 | err := yaml.NewDecoder(strings.NewReader(comment)).Decode(modelPointer) 213 | if err != nil { 214 | return fmt.Errorf("failed to decode comment:\n%s\nError: %w", comment, err) 215 | } 216 | return nil 217 | } 218 | } 219 | return nil 220 | } 221 | 222 | func cleanComment(c string) string { 223 | return strings.ReplaceAll(strings.TrimSpace(c), "\t", " ") 224 | } 225 | 226 | func setSchemaType(schema *models.Schema, typeName string) { 227 | switch typeName { 228 | case "bool": 229 | schema.Type = "boolean" 230 | case "int64", "int": 231 | schema.Type = "integer" 232 | schema.Format = "int64" 233 | case "int32", "time.Month": 234 | schema.Type = "integer" 235 | schema.Format = "int32" 236 | case "float64", "float": 237 | schema.Type = "number" 238 | schema.Format = "double" 239 | case "float32": 240 | schema.Type = "number" 241 | schema.Format = "float" 242 | case "string": 243 | schema.Type = "string" 244 | case "array": 245 | schema.Type = "array" 246 | case "object": 247 | schema.Type = "object" 248 | case "time.Time": 249 | schema.Type = "string" 250 | schema.Format = "date-time" 251 | default: 252 | schema.Ref = "#/components/schemas/" + lower(typeName) 253 | } 254 | return 255 | } 256 | -------------------------------------------------------------------------------- /interpret/interpret_test.go: -------------------------------------------------------------------------------- 1 | package interpret_test 2 | 3 | import ( 4 | "github.com/VanMoof/gopenapi/interpret" 5 | "github.com/VanMoof/gopenapi/models" 6 | "github.com/stretchr/testify/assert" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | func TestASTInterpreter_Info(t *testing.T) { 12 | a := assert.New(t) 13 | 14 | file, openError := os.Open("./_test_files/main_with_info.go") 15 | a.NoError(openError) 16 | 17 | root := models.Root{} 18 | interpreter := &interpret.ASTInterpreter{} 19 | a.NoError(interpreter.InterpretFile(file, &root)) 20 | 21 | a.Equal("1.0", root.Info.Version) 22 | a.Equal("The App Name", root.Info.Title) 23 | a.Equal("The app description", root.Info.Description) 24 | a.Equal("Jimbob Jones", root.Info.Contact.Name) 25 | a.Equal("https://jones.com", root.Info.Contact.URL) 26 | a.Equal("jimbob@jones.com", root.Info.Contact.Email) 27 | a.Equal("Apache 2.0", root.Info.License.Name) 28 | a.Equal("https://www.apache.org/licenses/LICENSE-2.0.html", root.Info.License.URL) 29 | } 30 | 31 | func TestASTInterpreter_Path(t *testing.T) { 32 | a := assert.New(t) 33 | 34 | file, openError := os.Open("./_test_files/func_with_path.go") 35 | a.NoError(openError) 36 | 37 | root := models.Root{} 38 | interpreter := &interpret.ASTInterpreter{} 39 | a.NoError(interpreter.InterpretFile(file, &root)) 40 | 41 | a.Equal("The default response of \"ping\"", root.Paths["/ping"].Get.Responses["200"].Description) 42 | a.Equal("pong", root.Paths["/ping"].Get.Responses["200"].Content["text/plain"].Example) 43 | } 44 | 45 | func TestASTInterpreter_Models(t *testing.T) { 46 | a := assert.New(t) 47 | 48 | file, openError := os.Open("./_test_files/structs_with_models.go") 49 | a.NoError(openError) 50 | 51 | root := models.Root{} 52 | interpreter := &interpret.ASTInterpreter{} 53 | a.NoError(interpreter.InterpretFile(file, &root)) 54 | schemas := root.Components.Schemas 55 | 56 | rootModel := schemas["rootModel"] 57 | a.Equal("object", rootModel.Type) 58 | a.Equal("integer", rootModel.Properties["intField"].Type) 59 | a.Equal("int64", rootModel.Properties["intField"].Format) 60 | a.Equal("string", rootModel.Properties["stringField"].Type) 61 | a.Equal("array", rootModel.Properties["subModels"].Type) 62 | a.Equal("#/components/schemas/subModel", rootModel.Properties["subModels"].Items.Ref) 63 | 64 | subModel := schemas["subModel"] 65 | a.Equal("object", subModel.Type) 66 | a.Equal("number", subModel.Properties["floatField"].Type) 67 | a.Equal("double", subModel.Properties["floatField"].Format) 68 | a.Equal("object", subModel.Properties["subSubModel"].Type) 69 | a.Equal("#/components/schemas/subSubModel", subModel.Properties["subSubModel"].AdditionalProperties.(*models.Schema).Ref) 70 | 71 | subSubModel := schemas["subSubModel"] 72 | a.Equal("object", subSubModel.Type) 73 | a.Equal("boolean", subSubModel.Properties["boolField"].Type) 74 | a.Equal("#/components/schemas/aliasedSubs", subSubModel.Properties["aliased"].Ref) 75 | 76 | aliasedSubs := schemas["aliasedSubs"] 77 | a.Equal("array", aliasedSubs.Type) 78 | a.Equal("#/components/schemas/aliasedSub", aliasedSubs.Items.Ref) 79 | 80 | aliasedSub := schemas["aliasedSub"] 81 | a.Equal("object", aliasedSub.Type) 82 | a.Equal("string", aliasedSub.Properties["timeField"].Type) 83 | a.Equal("date-time", aliasedSub.Properties["timeField"].Format) 84 | } 85 | 86 | func TestASTInterpreter_Parameter(t *testing.T) { 87 | a := assert.New(t) 88 | 89 | file, openError := os.Open("./_test_files/func_with_parameter.go") 90 | a.NoError(openError) 91 | 92 | root := models.Root{} 93 | interpreter := &interpret.ASTInterpreter{} 94 | a.NoError(interpreter.InterpretFile(file, &root)) 95 | parameter := root.Components.Parameters["ConstParamName"] 96 | a.Equal("constParamName", parameter.Name) 97 | a.Equal("path", parameter.In) 98 | a.Equal("some text", parameter.Content["text/plain"].Example) 99 | 100 | parameter = root.Components.Parameters["VarParamName"] 101 | a.Equal("varParamName", parameter.Name) 102 | a.Equal("query", parameter.In) 103 | a.Equal("some text", parameter.Content["text/plain"].Example) 104 | } 105 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/VanMoof/gopenapi/cmd" 5 | "os" 6 | ) 7 | 8 | func main() { 9 | if err := cmd.Execute(); err != nil { 10 | println(err) 11 | os.Exit(1) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "gopkg.in/yaml.v3" 4 | 5 | type Root struct { 6 | OpenAPI string `json:"openapi" yaml:"openapi"` 7 | Info *Info `json:"info" yaml:"info"` 8 | Servers []*Server `json:"servers,omitempty" yaml:"servers,omitempty"` 9 | Paths PathItems `json:"paths" yaml:"paths"` 10 | Components *Components `json:"components,omitempty" yaml:"components,omitempty"` 11 | Security []*SecurityRequirement `json:"security,omitempty" yaml:"security,omitempty"` 12 | Tags []*Tag `json:"tag,omitempty" yaml:"tag,omitempty"` 13 | ExternalDocumentation *ExternalDocumentation `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` 14 | } 15 | 16 | type PathItems map[string]*PathItem 17 | 18 | func (n *PathItems) UnmarshalYAML(value *yaml.Node) error { 19 | if *n == nil { 20 | *n = map[string]*PathItem{} 21 | } 22 | 23 | decodedItems := map[string]*PathItem{} 24 | if err := value.Decode(&decodedItems); err != nil { 25 | return err 26 | } 27 | 28 | for decodedPath, decodedPathItem := range decodedItems { 29 | if existingPathItem, ok := (*n)[decodedPath]; ok { 30 | existingPathItem.merge(decodedPathItem) 31 | } else { 32 | (*n)[decodedPath] = decodedPathItem 33 | } 34 | } 35 | return nil 36 | } 37 | 38 | type Info struct { 39 | Title string `json:"title" yaml:"title"` 40 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 41 | TermsOfService string `json:"termsOfService,omitempty" yaml:"termsOfService,omitempty"` 42 | Contact *Contact `json:"contact,omitempty" yaml:"contact,omitempty"` 43 | License *License `json:"license,omitempty" yaml:"license,omitempty"` 44 | Version string `json:"version" yaml:"version"` 45 | } 46 | 47 | type Contact struct { 48 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 49 | URL string `json:"url,omitempty" yaml:"url,omitempty"` 50 | Email string `json:"email,omitempty" yaml:"email,omitempty"` 51 | } 52 | 53 | type License struct { 54 | Name string `json:"name" yaml:"name"` 55 | URL string `json:"url,omitempty" yaml:"url,omitempty"` 56 | } 57 | 58 | type ServerVariable struct { 59 | Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"` 60 | Default string `json:"default" yaml:"default"` 61 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 62 | } 63 | 64 | type Server struct { 65 | URL string `json:"url" yaml:"url"` 66 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 67 | Variables map[string]*ServerVariable `json:"variables,omitempty" yaml:"variables,omitempty"` 68 | } 69 | 70 | type PathItem struct { 71 | Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` 72 | Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` 73 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 74 | Get *Operation `json:"get,omitempty" yaml:"get,omitempty"` 75 | Put *Operation `json:"put,omitempty" yaml:"put,omitempty"` 76 | Post *Operation `json:"post,omitempty" yaml:"post,omitempty"` 77 | Delete *Operation `json:"delete,omitempty" yaml:"delete,omitempty"` 78 | Options *Operation `json:"options,omitempty" yaml:"options,omitempty"` 79 | Head *Operation `json:"head,omitempty" yaml:"head,omitempty"` 80 | Patch *Operation `json:"patch,omitempty" yaml:"patch,omitempty"` 81 | Trace *Operation `json:"trace,omitempty" yaml:"trace,omitempty"` 82 | Servers []*Server `json:"servers,omitempty" yaml:"servers,omitempty"` 83 | Parameters []*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` 84 | } 85 | 86 | func (p *PathItem) merge(other *PathItem) { 87 | if other.Ref != "" { 88 | p.Ref = other.Ref 89 | } 90 | if other.Summary != "" { 91 | p.Summary = other.Summary 92 | } 93 | if other.Description != "" { 94 | p.Description = other.Description 95 | } 96 | if other.Get != nil { 97 | p.Get = other.Get 98 | } 99 | if other.Put != nil { 100 | p.Put = other.Put 101 | } 102 | if other.Post != nil { 103 | p.Post = other.Post 104 | } 105 | if other.Delete != nil { 106 | p.Delete = other.Delete 107 | } 108 | if other.Options != nil { 109 | p.Options = other.Options 110 | } 111 | if other.Head != nil { 112 | p.Head = other.Head 113 | } 114 | if other.Patch != nil { 115 | p.Patch = other.Patch 116 | } 117 | if other.Trace != nil { 118 | p.Trace = other.Trace 119 | } 120 | if len(other.Servers) > 0 { 121 | p.Servers = append(p.Servers, other.Servers...) 122 | } 123 | if len(other.Parameters) > 0 { 124 | existingParams := map[string]interface{}{} 125 | for _, param := range p.Parameters { 126 | existingParams[param.Name+param.In] = true 127 | } 128 | 129 | for _, otherParam := range other.Parameters { 130 | if _, ok := existingParams[otherParam.Name+otherParam.In]; !ok { 131 | p.Parameters = append(p.Parameters, otherParam) 132 | } 133 | } 134 | } 135 | } 136 | 137 | type Operation struct { 138 | Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` 139 | Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` 140 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 141 | ExternalDocumentation *ExternalDocumentation `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` 142 | OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` 143 | Parameters []*Parameter `json:"parameter,omitempty" yaml:"parameter,omitempty"` 144 | RequestBody *RequestBody `json:"requestBody,omitempty" yaml:"requestBody,omitempty"` 145 | Responses map[string]*Response `json:"responses" yaml:"responses"` 146 | Callbacks map[string]*Callback `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` 147 | Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` 148 | Security []*SecurityRequirement `json:"security,omitempty" yaml:"security,omitempty"` 149 | Servers []*Server `json:"servers,omitempty" yaml:"servers,omitempty"` 150 | } 151 | 152 | type Parameter struct { 153 | Name string `json:"name" yaml:"name"` 154 | In string `json:"in" yaml:"in"` 155 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 156 | Required bool `json:"required" yaml:"required"` 157 | Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` 158 | AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` 159 | Style string `json:"style,omitempty" yaml:"style,omitempty"` 160 | Explode bool `json:"explode,omitempty" yaml:"explode,omitempty"` 161 | AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` 162 | Schema *Schema `json:"schema,omitempty" yaml:"schema,omitempty"` 163 | Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` 164 | Examples map[string]*Example `json:"examples,omitempty" yaml:"examples,omitempty"` 165 | Content map[string]*MediaType `json:"content" yaml:"content"` 166 | } 167 | 168 | type RequestBody struct { 169 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 170 | Content map[string]*MediaType `json:"content" yaml:"content"` 171 | Required bool `json:"required,omitempty" yaml:"required,omitempty"` 172 | } 173 | 174 | type MediaType struct { 175 | Schema *Schema `json:"schema,omitempty" yaml:"schema,omitempty"` 176 | Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` 177 | Examples map[string]*Example `json:"examples,omitempty" yaml:"examples,omitempty"` 178 | Encoding map[string]*Encoding `json:"encoding,omitempty" yaml:"encoding,omitempty"` 179 | } 180 | 181 | type Header struct { 182 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 183 | Required bool `json:"required" yaml:"required"` 184 | Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` 185 | AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` 186 | Style string `json:"style,omitempty" yaml:"style,omitempty"` 187 | Explode bool `json:"explode,omitempty" yaml:"explode,omitempty"` 188 | AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` 189 | Schema *Schema `json:"schema,omitempty" yaml:"schema,omitempty"` 190 | Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` 191 | Examples map[string]*Example `json:"examples,omitempty" yaml:"examples,omitempty"` 192 | Content map[string]*MediaType `json:"content" yaml:"content"` 193 | } 194 | 195 | type Encoding struct { 196 | ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` 197 | Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` 198 | Style string `json:"style,omitempty" yaml:"style,omitempty"` 199 | Explode bool `json:"explode,omitempty" yaml:"explode,omitempty"` 200 | AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` 201 | } 202 | 203 | type Response struct { 204 | Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` 205 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 206 | Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` 207 | Content map[string]*MediaType `json:"content,omitempty" yaml:"content,omitempty"` 208 | Links map[string]*Link `json:"links,omitempty" yaml:"links,omitempty"` 209 | } 210 | 211 | type Link struct { 212 | OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"` 213 | OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` 214 | Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"` 215 | RequestBody interface{} `json:"requestBody,omitempty" yaml:"requestBody,omitempty"` 216 | Description string `json:"description" yaml:"description"` 217 | Server *Server `json:"server,omitempty" yaml:"server,omitempty"` 218 | } 219 | 220 | type Example struct { 221 | Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` 222 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 223 | Value interface{} `json:"value,omitempty" yaml:"value,omitempty"` 224 | ExternalValue string `json:"externalValue,omitempty" yaml:"externalValue,omitempty"` 225 | } 226 | 227 | type Components struct { 228 | Schemas map[string]*Schema `json:"schemas,omitempty" yaml:"schemas,omitempty"` 229 | Responses map[string]*Response `json:"responses,omitempty" yaml:"responses,omitempty"` 230 | Parameters map[string]*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` 231 | Examples map[string]*Example `json:"examples,omitempty" yaml:"examples,omitempty"` 232 | RequestBodies map[string]*RequestBody `json:"requestBodies,omitempty" yaml:"requestBodies,omitempty"` 233 | Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` 234 | SecuritySchemes map[string]*SecurityScheme `json:"securitySchemes,omitempty" yaml:"securitySchemes,omitempty"` 235 | Links map[string]*Link `json:"links,omitempty" yaml:"links,omitempty"` 236 | Callbacks map[string]*Callback `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` 237 | } 238 | 239 | type Tag struct { 240 | Name string `json:"name" yaml:"name"` 241 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 242 | ExternalDocumentation *ExternalDocumentation `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` 243 | } 244 | 245 | type SecurityRequirement struct { 246 | Name []string `json:"name,omitempty" yaml:"name,omitempty"` 247 | } 248 | 249 | type ExternalDocumentation struct { 250 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 251 | URL string `json:"url" yaml:"url"` 252 | } 253 | 254 | type Callback map[string]*PathItem 255 | 256 | type Schema struct { 257 | Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` 258 | Nullable bool `json:"nullable,omitempty" yaml:"nullable,omitempty"` 259 | Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` 260 | ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"` 261 | WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"` 262 | XML *XML `json:"xml,omitempty" yaml:"xml,omitempty"` 263 | ExternalDocumentation *ExternalDocumentation `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` 264 | Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` 265 | Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` 266 | 267 | Type string `json:"type,omitempty" yaml:"type,omitempty"` 268 | AllOf []*Schema `json:"allOf,omitempty" yaml:"allOf,omitempty"` 269 | OneOf []*Schema `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` 270 | AnyOf []*Schema `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` 271 | Not []*Schema `json:"not,omitempty" yaml:"not,omitempty"` 272 | Items *Schema `json:"items,omitempty" yaml:"items,omitempty"` 273 | Properties map[string]*Schema `json:"properties,omitempty" yaml:"properties,omitempty"` 274 | AdditionalProperties interface{} `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"` 275 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 276 | Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` 277 | Format string `json:"format,omitempty" yaml:"format,omitempty"` 278 | } 279 | 280 | type XML struct { 281 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 282 | Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` 283 | Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"` 284 | Attribute bool `json:"attribute,omitempty" yaml:"attribute,omitempty"` 285 | Wrapped bool `json:"wrapped,omitempty" yaml:"wrapped,omitempty"` 286 | } 287 | 288 | type Discriminator struct { 289 | PropertyName string `json:"propertyName" yaml:"propertyName"` 290 | Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"` 291 | } 292 | 293 | type SecurityScheme struct { 294 | Type string `json:"type" yaml:"type"` 295 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 296 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 297 | In string `json:"in,omitempty" yaml:"in,omitempty"` 298 | Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` 299 | BearerFormat string `json:"bearerFormat,omitempty" yaml:"bearerFormat,omitempty"` 300 | Flows *OAuthFlows `json:"flows,omitempty" yaml:"flows,omitempty"` 301 | OpenIdConnectUrl string `json:"openIdConnectUrl,omitempty" yaml:"openIdConnectUrl,omitempty"` 302 | } 303 | 304 | type OAuthFlows struct { 305 | Implicit *OathFlowObject `json:"implicit,omitempty" yaml:"implicit,omitempty"` 306 | Password *OathFlowObject `json:"password,omitempty" yaml:"password,omitempty"` 307 | ClientCredentials *OathFlowObject `json:"clientCredentials,omitempty" yaml:"clientCredentials,omitempty"` 308 | AuthorizationCode *OathFlowObject `json:"authorizationCode,omitempty" yaml:"authorizationCode,omitempty"` 309 | } 310 | 311 | type OathFlowObject struct { 312 | AuthorizationURL string `json:"authorizationUrl" yaml:"authorizationUrl"` 313 | TokenURL string `json:"tokenUrl" yaml:"tokenUrl"` 314 | RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` 315 | Scopes map[string]string `json:"scopes" yaml:"scopes"` 316 | } 317 | -------------------------------------------------------------------------------- /models/models_test.go: -------------------------------------------------------------------------------- 1 | package models_test 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/VanMoof/gopenapi/models" 6 | "github.com/stretchr/testify/assert" 7 | "gopkg.in/yaml.v3" 8 | "testing" 9 | ) 10 | 11 | func TestEncodeRoot_YAML(t *testing.T) { 12 | r := root() 13 | encoded, err := yaml.Marshal(r) 14 | 15 | a := assert.New(t) 16 | a.NoError(err) 17 | 18 | r2 := &models.Root{} 19 | a.NoError(yaml.Unmarshal(encoded, r2)) 20 | validateRoot(a, r2) 21 | } 22 | 23 | func TestEncodeRoot_JSON(t *testing.T) { 24 | r := root() 25 | encoded, err := json.Marshal(r) 26 | 27 | a := assert.New(t) 28 | a.NoError(err) 29 | 30 | r2 := &models.Root{} 31 | a.NoError(json.Unmarshal(encoded, r2)) 32 | validateRoot(a, r2) 33 | } 34 | 35 | func TestEncodeInfo_YAML(t *testing.T) { 36 | i := info() 37 | 38 | encoded, err := yaml.Marshal(i) 39 | 40 | a := assert.New(t) 41 | a.NoError(err) 42 | 43 | i2 := &models.Info{} 44 | a.NoError(yaml.Unmarshal(encoded, i2)) 45 | 46 | validateInfo(a, i2) 47 | } 48 | 49 | func TestEncodeInfo_JSON(t *testing.T) { 50 | i := info() 51 | 52 | encoded, err := json.Marshal(i) 53 | 54 | a := assert.New(t) 55 | a.NoError(err) 56 | 57 | i2 := &models.Info{} 58 | a.NoError(json.Unmarshal(encoded, i2)) 59 | 60 | validateInfo(a, i2) 61 | } 62 | 63 | func TestEncodeServer_YAML(t *testing.T) { 64 | s := server() 65 | 66 | encoded, err := yaml.Marshal(s) 67 | 68 | a := assert.New(t) 69 | a.NoError(err) 70 | 71 | s2 := &models.Server{} 72 | a.NoError(yaml.Unmarshal(encoded, s2)) 73 | 74 | validateServer(a, s2) 75 | } 76 | 77 | func TestEncodeServer_JSON(t *testing.T) { 78 | s := server() 79 | 80 | encoded, err := json.Marshal(s) 81 | 82 | a := assert.New(t) 83 | a.NoError(err) 84 | 85 | s2 := &models.Server{} 86 | a.NoError(json.Unmarshal(encoded, s2)) 87 | 88 | validateServer(a, s2) 89 | } 90 | 91 | func TestComponents_YAML(t *testing.T) { 92 | c := components() 93 | 94 | encoded, err := yaml.Marshal(c) 95 | 96 | a := assert.New(t) 97 | a.NoError(err) 98 | 99 | c2 := &models.Components{} 100 | a.NoError(yaml.Unmarshal(encoded, c2)) 101 | 102 | validateComponents(a, c2) 103 | } 104 | 105 | func TestComponents_JSON(t *testing.T) { 106 | c := components() 107 | 108 | encoded, err := json.Marshal(c) 109 | 110 | a := assert.New(t) 111 | a.NoError(err) 112 | 113 | c2 := &models.Components{} 114 | a.NoError(json.Unmarshal(encoded, c2)) 115 | 116 | validateComponents(a, c2) 117 | } 118 | 119 | func TestMergePaths_PartialDuplicates(t *testing.T) { 120 | item1 := ` 121 | /the/path: 122 | $ref: ref 123 | summary: summary 124 | description: description 125 | parameters: 126 | - name: param1 127 | in: path 128 | - name: param2 129 | in: path 130 | get: 131 | summary: the get summary 132 | description: the get description 133 | post: 134 | summary: the post summary 135 | description: the post description 136 | servers: 137 | - url: the url 138 | description: the description 139 | ` 140 | 141 | item2 := ` 142 | /the/path: 143 | summary: summary2 144 | parameters: 145 | - name: param1 146 | in: path 147 | - name: param3 148 | in: query 149 | delete: 150 | summary: the delete summary 151 | description: the delete description 152 | post: 153 | summary: the post summary 2 154 | description: the post description 2 155 | servers: 156 | - url: the url 2 157 | description: the description 2 158 | ` 159 | 160 | var pathItems models.PathItems 161 | 162 | a := assert.New(t) 163 | a.NoError(yaml.Unmarshal([]byte(item1), &pathItems)) 164 | a.NoError(yaml.Unmarshal([]byte(item2), &pathItems)) 165 | 166 | a.Equal("ref", pathItems["/the/path"].Ref) 167 | a.Equal("summary2", pathItems["/the/path"].Summary) 168 | a.Equal("description", pathItems["/the/path"].Description) 169 | 170 | a.Equal("the get description", pathItems["/the/path"].Get.Description) 171 | a.Equal("the get summary", pathItems["/the/path"].Get.Summary) 172 | 173 | a.Equal("the post description 2", pathItems["/the/path"].Post.Description) 174 | a.Equal("the post summary 2", pathItems["/the/path"].Post.Summary) 175 | 176 | a.Equal("the delete description", pathItems["/the/path"].Delete.Description) 177 | a.Equal("the delete summary", pathItems["/the/path"].Delete.Summary) 178 | 179 | servers := pathItems["/the/path"].Servers 180 | a.Equal("the description", servers[0].Description) 181 | a.Equal("the url", servers[0].URL) 182 | a.Equal("the description 2", servers[1].Description) 183 | a.Equal("the url 2", servers[1].URL) 184 | 185 | parameters := pathItems["/the/path"].Parameters 186 | a.Equal("param1", parameters[0].Name) 187 | a.Equal("path", parameters[0].In) 188 | a.Equal("param2", parameters[1].Name) 189 | a.Equal("path", parameters[1].In) 190 | a.Equal("param3", parameters[2].Name) 191 | a.Equal("query", parameters[2].In) 192 | } 193 | 194 | func root() *models.Root { 195 | return &models.Root{ 196 | OpenAPI: "3.0.2", 197 | Info: &models.Info{ 198 | Title: "something", 199 | Description: "something", 200 | TermsOfService: "something", 201 | Version: "something", 202 | }, 203 | Servers: []*models.Server{ 204 | { 205 | URL: "something", 206 | Description: "something", 207 | Variables: map[string]*models.ServerVariable{}, 208 | }, 209 | }, 210 | Paths: map[string]*models.PathItem{ 211 | "something": { 212 | Ref: "something", 213 | Summary: "something", 214 | Description: "something", 215 | Servers: []*models.Server{}, 216 | Parameters: []*models.Parameter{}, 217 | }, 218 | }, 219 | Components: &models.Components{ 220 | Schemas: map[string]*models.Schema{}, 221 | Responses: map[string]*models.Response{}, 222 | Parameters: map[string]*models.Parameter{}, 223 | Examples: map[string]*models.Example{}, 224 | RequestBodies: map[string]*models.RequestBody{}, 225 | Headers: map[string]*models.Header{}, 226 | SecuritySchemes: map[string]*models.SecurityScheme{}, 227 | Links: map[string]*models.Link{}, 228 | Callbacks: map[string]*models.Callback{}, 229 | }, 230 | Security: []*models.SecurityRequirement{ 231 | {Name: []string{"something"}}, 232 | }, 233 | Tags: []*models.Tag{ 234 | { 235 | Name: "something", 236 | Description: "something", 237 | ExternalDocumentation: nil, 238 | }, 239 | }, 240 | ExternalDocumentation: &models.ExternalDocumentation{ 241 | Description: "something", 242 | URL: "something", 243 | }, 244 | } 245 | } 246 | 247 | func validateRoot(a *assert.Assertions, r *models.Root) { 248 | a.Equal("3.0.2", r.OpenAPI) 249 | a.Equal("something", r.Info.Title) 250 | a.Equal("something", r.Servers[0].URL) 251 | a.Equal("something", r.Paths["something"].Description) 252 | a.NotNil(r.Components) 253 | a.Equal("something", r.Security[0].Name[0]) 254 | a.Equal("something", r.Tags[0].Name) 255 | a.Equal("something", r.ExternalDocumentation.URL) 256 | } 257 | 258 | func info() *models.Info { 259 | return &models.Info{ 260 | Title: "title", 261 | Description: "description", 262 | TermsOfService: "tos", 263 | Contact: &models.Contact{ 264 | Name: "name", 265 | URL: "url", 266 | Email: "email", 267 | }, 268 | License: &models.License{ 269 | Name: "name", 270 | URL: "url", 271 | }, 272 | Version: "version", 273 | } 274 | } 275 | 276 | func validateInfo(a *assert.Assertions, i *models.Info) { 277 | a.Equal("title", i.Title) 278 | a.Equal("description", i.Description) 279 | a.Equal("tos", i.TermsOfService) 280 | a.Equal("name", i.Contact.Name) 281 | a.Equal("url", i.Contact.URL) 282 | a.Equal("email", i.Contact.Email) 283 | a.Equal("name", i.License.Name) 284 | a.Equal("url", i.License.URL) 285 | a.Equal("version", i.Version) 286 | } 287 | 288 | func server() *models.Server { 289 | return &models.Server{ 290 | URL: "url", 291 | Description: "description", 292 | Variables: map[string]*models.ServerVariable{ 293 | "key": { 294 | Enum: []string{"enum"}, 295 | Default: "default", 296 | Description: "description", 297 | }, 298 | }, 299 | } 300 | } 301 | 302 | func validateServer(a *assert.Assertions, s *models.Server) { 303 | a.Equal("url", s.URL) 304 | a.Equal("description", s.Description) 305 | 306 | variable := s.Variables["key"] 307 | a.Equal("description", variable.Description) 308 | a.Equal("default", variable.Default) 309 | a.Equal("enum", variable.Enum[0]) 310 | } 311 | 312 | func components() *models.Components { 313 | return &models.Components{ 314 | Schemas: map[string]*models.Schema{ 315 | "schema": { 316 | Nullable: true, 317 | ReadOnly: true, 318 | WriteOnly: true, 319 | Deprecated: true, 320 | }, 321 | }, 322 | Responses: map[string]*models.Response{ 323 | "response": { 324 | Description: "description", 325 | }, 326 | }, 327 | Parameters: map[string]*models.Parameter{ 328 | "parameter": { 329 | Name: "name", 330 | In: "in", 331 | Description: "description", 332 | Required: true, 333 | Deprecated: true, 334 | AllowEmptyValue: true, 335 | Style: "style", 336 | Explode: true, 337 | AllowReserved: true, 338 | Example: "example", 339 | }, 340 | }, 341 | Examples: map[string]*models.Example{ 342 | "example": { 343 | Summary: "summary", 344 | Description: "description", 345 | Value: "value", 346 | ExternalValue: "externalValue", 347 | }, 348 | }, 349 | RequestBodies: map[string]*models.RequestBody{ 350 | "requestBody": { 351 | Description: "description", 352 | Required: true, 353 | }, 354 | }, 355 | Headers: map[string]*models.Header{ 356 | "header": { 357 | Description: "description", 358 | Required: true, 359 | Deprecated: true, 360 | AllowEmptyValue: true, 361 | Style: "style", 362 | Explode: true, 363 | AllowReserved: true, 364 | Example: "example", 365 | }, 366 | }, 367 | SecuritySchemes: map[string]*models.SecurityScheme{ 368 | "securitySchema": { 369 | Type: "type", 370 | Description: "description", 371 | Name: "name", 372 | In: "in", 373 | Scheme: "scheme", 374 | BearerFormat: "bearer", 375 | OpenIdConnectUrl: "url", 376 | }, 377 | }, 378 | Links: map[string]*models.Link{ 379 | "link": { 380 | OperationRef: "ref", 381 | OperationID: "id", 382 | Description: "description", 383 | }, 384 | }, 385 | Callbacks: map[string]*models.Callback{ 386 | "callback": { 387 | "pathItem": { 388 | Ref: "ref", 389 | Summary: "summary", 390 | Description: "description", 391 | }, 392 | }, 393 | }, 394 | } 395 | } 396 | 397 | func validateComponents(a *assert.Assertions, c *models.Components) { 398 | a.Equal("description", c.Links["link"].Description) 399 | a.Equal("ref", c.Links["link"].OperationRef) 400 | a.Equal("id", c.Links["link"].OperationID) 401 | 402 | a.True(c.Schemas["schema"].Deprecated) 403 | a.True(c.Schemas["schema"].Nullable) 404 | a.True(c.Schemas["schema"].ReadOnly) 405 | a.True(c.Schemas["schema"].WriteOnly) 406 | 407 | a.Equal("ref", (*c.Callbacks["callback"])["pathItem"].Ref) 408 | a.Equal("description", (*c.Callbacks["callback"])["pathItem"].Description) 409 | 410 | a.Equal("description", c.Examples["example"].Description) 411 | a.Equal("summary", c.Examples["example"].Summary) 412 | a.Equal("externalValue", c.Examples["example"].ExternalValue) 413 | a.Equal("value", c.Examples["example"].Value) 414 | 415 | a.Equal("description", c.Headers["header"].Description) 416 | a.Equal("example", c.Headers["header"].Example) 417 | a.Equal("style", c.Headers["header"].Style) 418 | a.True(c.Headers["header"].Deprecated) 419 | a.True(c.Headers["header"].AllowEmptyValue) 420 | a.True(c.Headers["header"].AllowReserved) 421 | a.True(c.Headers["header"].Explode) 422 | a.True(c.Headers["header"].Required) 423 | 424 | a.Equal("description", c.Parameters["parameter"].Description) 425 | a.Equal("example", c.Parameters["parameter"].Example) 426 | a.Equal("style", c.Parameters["parameter"].Style) 427 | a.True(c.Parameters["parameter"].Deprecated) 428 | a.True(c.Parameters["parameter"].AllowEmptyValue) 429 | a.True(c.Parameters["parameter"].AllowReserved) 430 | a.True(c.Parameters["parameter"].Explode) 431 | a.True(c.Parameters["parameter"].Required) 432 | 433 | a.True(c.RequestBodies["requestBody"].Required) 434 | a.Equal("description", c.RequestBodies["requestBody"].Description) 435 | 436 | a.Equal("description", c.Responses["response"].Description) 437 | 438 | a.Equal("description", c.SecuritySchemes["securitySchema"].Description) 439 | a.Equal("name", c.SecuritySchemes["securitySchema"].Name) 440 | a.Equal("in", c.SecuritySchemes["securitySchema"].In) 441 | a.Equal("bearer", c.SecuritySchemes["securitySchema"].BearerFormat) 442 | a.Equal("url", c.SecuritySchemes["securitySchema"].OpenIdConnectUrl) 443 | a.Equal("scheme", c.SecuritySchemes["securitySchema"].Scheme) 444 | a.Equal("type", c.SecuritySchemes["securitySchema"].Type) 445 | } 446 | --------------------------------------------------------------------------------