diff --git a/Makefile b/Makefile index c746377..58b385f 100644 --- a/Makefile +++ b/Makefile @@ -33,11 +33,14 @@ imports: tidy: go mod tidy -sanity: lint trivy +sanity: lint test trivy lint: ./tools/run_linter.sh +test: + go test ./... + docker: docker build -t $(IMAGE) -f tools/Dockerfile . diff --git a/README.md b/README.md index cd19661..729257c 100644 --- a/README.md +++ b/README.md @@ -64,12 +64,7 @@ To find even more sophisticated uses of `tcli`, see [stress test with tcli](/doc #### Areas we need help with -- openapi parsing is not complete and does not address all versions. - - json path processing is not implemented. - - maybe it is better to delegate spec parsing to another library. -`tcli` was built to work with openapi specs from projects it was used to test. -It did not have a reason to expand out. If you are looking to contribute, this is -an area that can use your help. +- testing - `-format` does not handle input beyond simple one level input like `5`, `{"data":5}` etc. If fully supported for multi-level json input, the `-format` feature can be much more versatile. @@ -80,6 +75,8 @@ If fully supported for multi-level json input, the `-format` feature can be much - [How to add a module](/docs/modules.md) - [Explanation of test code](/docs/example_explanation.md) - [Examples](/examples/README.md) +- Parsing openapi spec + - [pb33f/libopenapi](https://github.com/pb33f/libopenapi) - Formatting output - [jq lang](https://github.com/jqlang/jq/wiki/jq-Language-Description) - [gojq use as library](https://github.com/itchyny/gojq?tab=readme-ov-file#usage-as-a-library) diff --git a/examples/_pubsub/pubsub.json b/examples/_pubsub/pubsub.json index 0fe1ccc..50e99c3 100644 --- a/examples/_pubsub/pubsub.json +++ b/examples/_pubsub/pubsub.json @@ -3,9 +3,6 @@ "/sns": { "post": { "description": "", - "extension": { - "class": "sns" - }, "operationId": "send_sns", "parameters": [ { @@ -35,15 +32,15 @@ "type": "string" } ], - "summary": "send sns message" + "summary": "send sns message", + "x-extension": { + "class": "sns" + } } }, "/sqs": { "post": { "description": "", - "extension": { - "class": "sqs" - }, "operationId": "send_sqs", "parameters": [ { @@ -67,8 +64,12 @@ "type": "string" } ], - "summary": "send sqs message" + "summary": "send sqs message", + "x-extension": { + "class": "sqs" + } } } - } + }, + "swagger": "2.0" } diff --git a/go.mod b/go.mod index 69a260f..526d57e 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,17 @@ go 1.26.0 require ( github.com/itchyny/gojq v0.12.17 + github.com/pb33f/libopenapi v0.34.2 gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 ) -require github.com/itchyny/timefmt-go v0.1.6 // indirect +require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/itchyny/timefmt-go v0.1.6 // indirect + github.com/pb33f/jsonpath v0.8.1 // indirect + github.com/pb33f/ordered-map/v2 v2.3.0 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect + golang.org/x/sync v0.19.0 // indirect +) diff --git a/go.sum b/go.sum index f102992..b4353af 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,37 @@ +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg= github.com/itchyny/gojq v0.12.17/go.mod h1:WBrEMkgAfAGO1LUcGOckBl5O726KPp+OlkKug0I/FEY= github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pb33f/jsonpath v0.8.1 h1:84C6QRyx6HcSm6PZnsMpcqYot3IsZ+m0n95+0NbBbvs= +github.com/pb33f/jsonpath v0.8.1/go.mod h1:zBV5LJW4OQOPatmQE2QdKpGQJvhDTlE5IEj6ASaRNTo= +github.com/pb33f/libopenapi v0.34.2 h1:ValgPCDIVSC1IzPY7rY6GPOslCzaAWEml40IuFGZXOc= +github.com/pb33f/libopenapi v0.34.2/go.mod h1:YOP20KzYe3mhE5301aQzJtzQ9MnvhABBGO7RMttA4V4= +github.com/pb33f/ordered-map/v2 v2.3.0 h1:k2OhVEQkhTCQMhAicQ3Z6iInzoZNQ7L9MVomwKBZ5WQ= +github.com/pb33f/ordered-map/v2 v2.3.0/go.mod h1:oe5ue+6ZNhy7QN9cPZvPA23Hx0vMHnNVeMg4fGdCANw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= +go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/parser/doc_mapper.go b/internal/parser/doc_mapper.go new file mode 100644 index 0000000..ba8f094 --- /dev/null +++ b/internal/parser/doc_mapper.go @@ -0,0 +1,417 @@ +package parser + +import ( + "os" + "strings" + + "github.com/hpinc/tcli/internal/common" + "github.com/pb33f/libopenapi" + "github.com/pb33f/libopenapi/datamodel/high/base" + v2 "github.com/pb33f/libopenapi/datamodel/high/v2" + v3 "github.com/pb33f/libopenapi/datamodel/high/v3" + "gopkg.in/yaml.v3" +) + +type extNode struct { + Content []*extNode `yaml:"content"` + Value string `yaml:"value"` +} + +func extractClassFromNodeValue(val any) string { + b, _ := yaml.Marshal(val) + var n extNode + _ = yaml.Unmarshal(b, &n) + if len(n.Content) > 0 { + for i := 0; i < len(n.Content)-1; i += 2 { + if n.Content[i].Value == "class" { + return n.Content[i+1].Value + } + } + } + return "" +} + +func ReadSwagger(f string) (*Root, error) { + bytes, err := os.ReadFile(f) // #nosec G304 + if err != nil { + return nil, err + } + + doc, err := libopenapi.NewDocument(bytes) + if err != nil { + return nil, err + } + + m2, errs2 := doc.BuildV2Model() + if errs2 == nil && m2 != nil { + return buildFromV2(&m2.Model), nil + } + + m3, errs3 := doc.BuildV3Model() + if errs3 == nil && m3 != nil { + return buildFromV3(&m3.Model), nil + } + + // if both failed, try returning the v2 or v3 error + if errs2 != nil { + return nil, errs2 + } + if errs3 != nil { + return nil, errs3 + } + + return nil, common.ErrNotFound +} + +func buildFromV2(doc *v2.Swagger) *Root { + r := &Root{ + Paths: make(map[string]*Path), + Definitions: make(map[string]Definition), + SecurityDefinitions: make(map[string]SecurityDefinition), + } + + if doc.Host != "" { + r.Host = doc.Host + } + if doc.BasePath != "" { + r.BasePath = doc.BasePath + } + r.Schemes = doc.Schemes + + for _, tag := range doc.Tags { + r.Tags = append(r.Tags, Tag{ + Name: tag.Name, + Description: tag.Description, + }) + } + + if doc.SecurityDefinitions != nil && doc.SecurityDefinitions.Definitions != nil { + for pair := doc.SecurityDefinitions.Definitions.First(); pair != nil; pair = pair.Next() { + if pair.Value() != nil { + r.SecurityDefinitions[pair.Key()] = SecurityDefinition{Type: pair.Value().Type} + } + } + } + + if doc.Paths != nil && doc.Paths.PathItems != nil { + for pair := doc.Paths.PathItems.First(); pair != nil; pair = pair.Next() { + p := pair.Value() + pathObj := &Path{ + Get: mapV2Method(p.Get, pair.Key(), "GET"), + Put: mapV2Method(p.Put, pair.Key(), "PUT"), + Post: mapV2Method(p.Post, pair.Key(), "POST"), + Delete: mapV2Method(p.Delete, pair.Key(), "DELETE"), + Options: mapV2Method(p.Options, pair.Key(), "OPTIONS"), + Patch: mapV2Method(p.Patch, pair.Key(), "PATCH"), + } + r.Paths[pair.Key()] = pathObj + } + } + + if doc.Definitions != nil && doc.Definitions.Definitions != nil { + for pair := doc.Definitions.Definitions.First(); pair != nil; pair = pair.Next() { + r.Definitions[pair.Key()] = mapV2Definition(pair.Value()) + } + } + + return r +} + +func mapV2Method(op *v2.Operation, pathStr, methodStr string) *Method { + if op == nil { + return nil + } + + m := &Method{ + Summary: op.Summary, + Description: op.Description, + OperationId: op.OperationId, + Tags: op.Tags, + MethodName: methodStr, + Consumes: op.Consumes, + Path: pathStr, + } + + for _, param := range op.Parameters { + p := Parameter{ + Name: param.Name, + Description: param.Description, + In: param.In, + Type: param.Type, + Format: param.Format, + } + if param.Required != nil { + p.Required = *param.Required + } + if param.Default != nil { + p.Default = param.Default.Value + } + if param.Schema != nil { + p.Schema = &Schema{Ref: param.Schema.GetReference()} + } + m.Parameters = append(m.Parameters, p) + } + + m.Securities = make([]map[string][]string, 0) + for _, req := range op.Security { + secMap := make(map[string][]string) + for reqPair := req.Requirements.First(); reqPair != nil; reqPair = reqPair.Next() { + secMap[reqPair.Key()] = reqPair.Value() + } + m.Securities = append(m.Securities, secMap) + } + + if val, ok := op.Extensions.Get("x-extension"); ok && val != nil { + if cls := extractClassFromNodeValue(val); cls != "" { + m.Extension = &Extension{Class: cls} + } + } + + return m +} + +func mapV2Definition(schemaProxy *base.SchemaProxy) Definition { + def := Definition{ + Properties: make(map[string]*Property), + Type: "object", + } + if schemaProxy == nil { + return def + } + schema := schemaProxy.Schema() + if schema != nil { + if len(schema.Type) > 0 { + def.Type = schema.Type[0] + } + def.Required = schema.Required + if schema.Properties != nil { + for propPair := schema.Properties.First(); propPair != nil; propPair = propPair.Next() { + propSchema := propPair.Value().Schema() + prop := &Property{} + if propSchema != nil { + if len(propSchema.Type) > 0 { + prop.Type = propSchema.Type[0] + } + prop.Format = propSchema.Format + prop.Description = propSchema.Description + if propPair.Value().GetReference() != "" { + prop.Ref = propPair.Value().GetReference() + } + if propSchema.Items != nil && propSchema.Items.A != nil { + itemSchema := propSchema.Items.A.Schema() + if itemSchema != nil { + itemType := "" + if len(itemSchema.Type) > 0 { + itemType = itemSchema.Type[0] + } + prop.Items = &ArrayItem{ + Type: itemType, + Format: itemSchema.Format, + Ref: propSchema.Items.A.GetReference(), + } + } + } + } else if propPair.Value().GetReference() != "" { + prop.Ref = propPair.Value().GetReference() + } + def.Properties[propPair.Key()] = prop + } + } + } + return def +} + +func buildFromV3(doc *v3.Document) *Root { + r := &Root{ + Paths: make(map[string]*Path), + Definitions: make(map[string]Definition), + SecurityDefinitions: make(map[string]SecurityDefinition), + } + + if doc.Servers != nil && len(doc.Servers) > 0 { + srv := doc.Servers[0] + r.Host = strings.TrimPrefix(strings.TrimPrefix(srv.URL, "https://"), "http://") + // rudimentary parsing of host/basePath from URL if absolute + idx := strings.Index(r.Host, "/") + if idx > 0 { + r.BasePath = r.Host[idx:] + r.Host = r.Host[:idx] + } + if strings.HasPrefix(srv.URL, "https") { + r.Schemes = []string{"https"} + } else if strings.HasPrefix(srv.URL, "http") { + r.Schemes = []string{"http"} + } + } + + for _, tag := range doc.Tags { + r.Tags = append(r.Tags, Tag{ + Name: tag.Name, + Description: tag.Description, + }) + } + + if doc.Components != nil && doc.Components.SecuritySchemes != nil { + for pair := doc.Components.SecuritySchemes.First(); pair != nil; pair = pair.Next() { + if pair.Value() != nil { + r.SecurityDefinitions[pair.Key()] = SecurityDefinition{Type: pair.Value().Type} + } + } + } + + if doc.Paths != nil && doc.Paths.PathItems != nil { + for pair := doc.Paths.PathItems.First(); pair != nil; pair = pair.Next() { + p := pair.Value() + pathObj := &Path{ + Get: mapV3Method(p.Get, pair.Key(), "GET"), + Put: mapV3Method(p.Put, pair.Key(), "PUT"), + Post: mapV3Method(p.Post, pair.Key(), "POST"), + Delete: mapV3Method(p.Delete, pair.Key(), "DELETE"), + Options: mapV3Method(p.Options, pair.Key(), "OPTIONS"), + Patch: mapV3Method(p.Patch, pair.Key(), "PATCH"), + } + r.Paths[pair.Key()] = pathObj + } + } + + if doc.Components != nil && doc.Components.Schemas != nil { + for pair := doc.Components.Schemas.First(); pair != nil; pair = pair.Next() { + r.Definitions[pair.Key()] = mapV3Definition(pair.Value()) + } + } + + return r +} + +func mapV3Method(op *v3.Operation, pathStr, methodStr string) *Method { + if op == nil { + return nil + } + + m := &Method{ + Summary: op.Summary, + Description: op.Description, + OperationId: op.OperationId, + Tags: op.Tags, + MethodName: methodStr, + Path: pathStr, + } + + if op.RequestBody != nil && op.RequestBody.Content != nil { + for pair := op.RequestBody.Content.First(); pair != nil; pair = pair.Next() { + m.Consumes = append(m.Consumes, pair.Key()) + // V3 RequestBody becomes a "body" parameter for backwards compatibility with cmd pkg + param := Parameter{ + Name: "body", + Description: op.RequestBody.Description, + In: "body", + } + if op.RequestBody.Required != nil { + param.Required = *op.RequestBody.Required + } + mediaType := pair.Value() + if mediaType.Schema != nil { + param.Schema = &Schema{Ref: mediaType.Schema.GetReference()} + } + m.Parameters = append(m.Parameters, param) + break // just take the first content type mapping + } + } + + for _, paramProxy := range op.Parameters { + param := paramProxy + p := Parameter{ + Name: param.Name, + Description: param.Description, + In: param.In, + } + if param.Required != nil { + p.Required = *param.Required + } + if param.Schema != nil { + schema := param.Schema.Schema() + if schema != nil && len(schema.Type) > 0 { + p.Type = schema.Type[0] + p.Format = schema.Format + if schema.Default != nil { + p.Default = schema.Default.Value + } + } + // if schema is a ref + if param.Schema.GetReference() != "" { + p.Schema = &Schema{Ref: param.Schema.GetReference()} + } + } + m.Parameters = append(m.Parameters, p) + } + + m.Securities = make([]map[string][]string, 0) + for _, req := range op.Security { + secMap := make(map[string][]string) + for reqPair := req.Requirements.First(); reqPair != nil; reqPair = reqPair.Next() { + secMap[reqPair.Key()] = reqPair.Value() + } + m.Securities = append(m.Securities, secMap) + } + + if val, ok := op.Extensions.Get("x-extension"); ok && val != nil { + if cls := extractClassFromNodeValue(val); cls != "" { + m.Extension = &Extension{Class: cls} + } + } + + return m +} + +func mapV3Definition(schemaProxy *base.SchemaProxy) Definition { + def := Definition{ + Properties: make(map[string]*Property), + Type: "object", + } + if schemaProxy == nil { + return def + } + schema := schemaProxy.Schema() + if schema != nil { + if len(schema.Type) > 0 { + def.Type = schema.Type[0] + } + def.Required = schema.Required + if schema.Properties != nil { + for propPair := schema.Properties.First(); propPair != nil; propPair = propPair.Next() { + propSchemaProxy := propPair.Value() + propSchema := propSchemaProxy.Schema() + prop := &Property{} + if propSchema != nil { + if len(propSchema.Type) > 0 { + prop.Type = propSchema.Type[0] + } + prop.Format = propSchema.Format + prop.Description = propSchema.Description + if propSchemaProxy.GetReference() != "" { + prop.Ref = propSchemaProxy.GetReference() + } + if propSchema.Items != nil && propSchema.Items.A != nil { + itemSchemaProxy := propSchema.Items.A + itemSchema := itemSchemaProxy.Schema() + if itemSchema != nil { + itemType := "" + if len(itemSchema.Type) > 0 { + itemType = itemSchema.Type[0] + } + prop.Items = &ArrayItem{ + Type: itemType, + Format: itemSchema.Format, + Ref: itemSchemaProxy.GetReference(), + } + } + } + } else if propSchemaProxy.GetReference() != "" { + prop.Ref = propSchemaProxy.GetReference() + } + def.Properties[propPair.Key()] = prop + } + } + } + return def +} diff --git a/internal/parser/doc_mapper_test.go b/internal/parser/doc_mapper_test.go new file mode 100644 index 0000000..a71eed4 --- /dev/null +++ b/internal/parser/doc_mapper_test.go @@ -0,0 +1,100 @@ +package parser + +import ( + "path/filepath" + "testing" +) + +func TestReadSwagger_UtilsJSON(t *testing.T) { + path := filepath.Join("..", "..", "tools", "data", "utils.json") + root, err := ReadSwagger(path) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if root == nil { + t.Fatal("expected non-nil root") + } + + pathObj, ok := root.Paths["/echo"] + if !ok { + t.Fatalf("expected path /echo") + } + + if pathObj.Post == nil { + t.Fatalf("expected POST method on /echo") + } + + if pathObj.Post.Extension == nil { + t.Fatalf("expected Extension on POST /echo") + } + + if pathObj.Post.Extension.Class != "echo" { + t.Errorf("expected extension class 'echo', got '%s'", pathObj.Post.Extension.Class) + } +} + +func TestReadSwagger_TCPJSON(t *testing.T) { + path := filepath.Join("..", "..", "tools", "data", "tcp.json") + root, err := ReadSwagger(path) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if root == nil { + t.Fatal("expected non-nil root") + } + + pathObj, ok := root.Paths["/wait_for_server"] + if !ok { + t.Fatalf("expected path /wait_for_server") + } + + if pathObj.Post == nil { + t.Fatalf("expected POST method on /wait_for_server") + } + + if pathObj.Post.Extension == nil { + t.Fatalf("expected Extension on POST /wait_for_server") + } + + if pathObj.Post.Extension.Class != "tcp" { + t.Errorf("expected extension class 'tcp', got '%s'", pathObj.Post.Extension.Class) + } + + if root.Schemes == nil || len(root.Schemes) == 0 || root.Schemes[0] != "tcp" { + t.Errorf("expected scheme 'tcp', got %v", root.Schemes) + } +} + +func TestReadSwagger_PetstoreJSON(t *testing.T) { + path := filepath.Join("..", "..", "tools", "data", "petstore.json") + root, err := ReadSwagger(path) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if root == nil { + t.Fatal("expected non-nil root") + } + + if root.Host != "petstore.swagger.io" { + t.Errorf("expected host 'petstore.swagger.io', got '%s'", root.Host) + } + + if root.BasePath != "/v2" { + t.Errorf("expected base path '/v2', got '%s'", root.BasePath) + } + + if len(root.Paths) == 0 { + t.Errorf("expected paths to be parsed, got none") + } + + if _, ok := root.Definitions["Pet"]; !ok { + t.Errorf("expected Pet definition to be parsed") + } +} + +func TestReadSwagger_FileNotFound(t *testing.T) { + _, err := ReadSwagger("nonexistent.json") + if err == nil { + t.Fatal("expected error for nonexistent file") + } +} diff --git a/internal/parser/parse.go b/internal/parser/parse.go index 07ef486..4d0b1ed 100644 --- a/internal/parser/parse.go +++ b/internal/parser/parse.go @@ -4,9 +4,7 @@ package parser import ( - "encoding/json" "fmt" - "os" "strings" "github.com/hpinc/tcli/internal/common" @@ -132,18 +130,6 @@ func (r *Root) NeedJwt(s string) bool { return false } -func ReadSwagger(f string) (*Root, error) { - bytes, err := os.ReadFile(f) // #nosec G304 - if err != nil { - return nil, err - } - var r Root - if err := json.Unmarshal(bytes, &r); err != nil { - return nil, err - } - return &r, nil -} - func (m *Method) hasOperation(tag string) bool { if m == nil { return false diff --git a/internal/parser/structs.go b/internal/parser/structs.go index 9c18420..6535384 100644 --- a/internal/parser/structs.go +++ b/internal/parser/structs.go @@ -13,65 +13,65 @@ const ( // struct defs type Schema struct { - Ref string `json:"$ref"` - Definition *Definition `json:"-"` + Ref string + Definition *Definition } type Parameter struct { - Name string `json:"name"` - Description string `json:"description"` - Required bool `json:"required"` - In string `json:"in"` - Type string `json:"type,omitempty"` - Format string `json:"format,omitempty"` - Schema *Schema `json:"schema,omitempty"` - Default interface{} `json:"default,omitempty"` + Name string + Description string + Required bool + In string + Type string + Format string + Schema *Schema + Default interface{} } type Extension struct { - Class string `json:"class"` + Class string } type Method struct { - Summary string `json:"summary"` - Description string `json:"description"` - OperationId string `json:"operationId"` - Tags []string `json:"tags"` - Parameters []Parameter `json:"parameters"` + Summary string + Description string + OperationId string + Tags []string + Parameters []Parameter MethodName string - Consumes []string `json:"consumes"` + Consumes []string Path string - Extension *Extension `json:"extension,omitempty"` - Securities []map[string][]string `json:"security"` + Extension *Extension + Securities []map[string][]string } type Path struct { - Get *Method `json:"get,omitempty"` - Delete *Method `json:"delete,omitempty"` - Options *Method `json:"options,omitempty"` - Patch *Method `json:"patch,omitempty"` - Post *Method `json:"post,omitempty"` - Put *Method `json:"put,omitempty"` + Get *Method + Delete *Method + Options *Method + Patch *Method + Post *Method + Put *Method } type ArrayItem struct { - Type string `json:"type,omitempty"` - Format string `json:"format,omitempty"` - Ref string `json:"$ref,omitempty"` + Type string + Format string + Ref string } type Property struct { - Type string `json:"type,omitempty"` - Format string `json:"format,omitempty"` - Description string `json:"description,omitempty"` - Ref string `json:"$ref,omitempty"` - Items *ArrayItem `json:"items,omitempty"` + Type string + Format string + Description string + Ref string + Items *ArrayItem } type Definition struct { - Type string `json:"type"` - Required []string `json:"required,omitempty"` - Properties map[string]*Property `json:"properties"` + Type string + Required []string + Properties map[string]*Property } // top level commands / sub commands @@ -83,20 +83,20 @@ type Command struct { } type Tag struct { - Name string `json:"name"` - Description string `json:"description"` + Name string + Description string } type SecurityDefinition struct { - Type string `json:"type"` + Type string } type Root struct { - Host string `json:"host"` - BasePath string `json:"basePath"` - Schemes []string `json:"schemes"` - Paths map[string]*Path `json:"paths,omitempty"` - Tags []Tag `json:"tags"` - Definitions map[string]Definition `json:"definitions"` - SecurityDefinitions map[string]SecurityDefinition `json:"securityDefinitions"` + Host string + BasePath string + Schemes []string + Paths map[string]*Path + Tags []Tag + Definitions map[string]Definition + SecurityDefinitions map[string]SecurityDefinition } diff --git a/tools/data/tcp.json b/tools/data/tcp.json index a4738df..7183706 100644 --- a/tools/data/tcp.json +++ b/tools/data/tcp.json @@ -3,16 +3,17 @@ "/wait_for_server": { "post": { "description": "", - "extension": { - "class": "tcp" - }, "operationId": "wait_for_server", "parameters": [], - "summary": "Wait for tcp server" + "summary": "Wait for tcp server", + "x-extension": { + "class": "tcp" + } } } }, "schemes": [ "tcp" - ] + ], + "swagger": "2.0" } diff --git a/tools/data/utils.json b/tools/data/utils.json index d2b0cc2..6407888 100644 --- a/tools/data/utils.json +++ b/tools/data/utils.json @@ -1,11 +1,9 @@ { + "host": "localhost:8080", "paths": { "/echo": { "post": { "description": "", - "extension": { - "class": "echo" - }, "operationId": "echo", "parameters": [ { @@ -16,8 +14,15 @@ "type": "string" } ], - "summary": "echo" + "summary": "echo", + "x-extension": { + "class": "echo" + } } } - } + }, + "schemes": [ + "http" + ], + "swagger": "2.0" }