mirror of
https://github.com/LukeHagar/libopenapi.git
synced 2025-12-06 12:37:49 +00:00
Added in high document API
filling out documentation, looking at DX and how to consume things, re-shuffling and cleaning house.
This commit is contained in:
16
datamodel/datamodel.go
Normal file
16
datamodel/datamodel.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
// Package datamodel contains two sets of models, high and low.
|
||||||
|
//
|
||||||
|
// The low level (or plumbing) models are designed to capture every single detail about specification, including
|
||||||
|
// all lines, columns, positions, tags, comments and essentially everything you would ever want to know.
|
||||||
|
// Positions of every key, value and meta-data that is lost when blindly un-marshaling JSON/YAML into a struct.
|
||||||
|
//
|
||||||
|
// The high model (porcelain) is a much simpler representation of the low model, keys are simple strings and indices
|
||||||
|
// are numbers. When developing consumers of the model, the high model is really what you want to use instead of the
|
||||||
|
// low model, it's much easier to navigate and is designed for easy consumption.
|
||||||
|
//
|
||||||
|
// The high model requires the low model to be built. Every high model has a 'GoLow' method that allows the consumer
|
||||||
|
// to 'drop down' from the porcelain API to the plumbing API, which gives instant access to everything low.
|
||||||
|
package datamodel
|
||||||
@@ -1,224 +1,38 @@
|
|||||||
|
// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package datamodel
|
package datamodel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/pb33f/libopenapi/utils"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Constants used by utilities to determine the version of OpenAPI that we're referring to.
|
||||||
const (
|
const (
|
||||||
OAS2 = "oas2"
|
OAS2 = "oas2"
|
||||||
OAS3 = "oas3"
|
OAS3 = "oas3"
|
||||||
OAS31 = "oas3_1"
|
OAS31 = "oas3_1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// OpenAPI3SchemaData is an embedded version of the OpenAPI 3 Schema
|
||||||
//go:embed schemas/oas3-schema.json
|
//go:embed schemas/oas3-schema.json
|
||||||
var OpenAPI3SchemaData string
|
var OpenAPI3SchemaData string // embedded OAS3 schema
|
||||||
|
|
||||||
|
// OpenAPI2SchemaData is an embedded version of the OpenAPI 2 (Swagger) Schema
|
||||||
//go:embed schemas/swagger2-schema.json
|
//go:embed schemas/swagger2-schema.json
|
||||||
var OpenAPI2SchemaData string
|
var OpenAPI2SchemaData string // embedded OAS3 schema
|
||||||
|
|
||||||
|
// OAS3_1Format defines documents that can only be version 3.1
|
||||||
var OAS3_1Format = []string{OAS31}
|
var OAS3_1Format = []string{OAS31}
|
||||||
|
|
||||||
|
// OAS3Format defines documents that can only be version 3.0
|
||||||
var OAS3Format = []string{OAS3}
|
var OAS3Format = []string{OAS3}
|
||||||
|
|
||||||
|
// OAS3AllFormat defines documents that compose all 3+ versions
|
||||||
var OAS3AllFormat = []string{OAS3, OAS31}
|
var OAS3AllFormat = []string{OAS3, OAS31}
|
||||||
|
|
||||||
|
// OAS2Format defines documents that compose swagger documnets (version 2.0)
|
||||||
var OAS2Format = []string{OAS2}
|
var OAS2Format = []string{OAS2}
|
||||||
|
|
||||||
|
// AllFormats defines all versions of OpenAPI
|
||||||
var AllFormats = []string{OAS3, OAS31, OAS2}
|
var AllFormats = []string{OAS3, OAS31, OAS2}
|
||||||
|
|
||||||
// ExtractSpecInfo will look at a supplied OpenAPI specification, and return a *SpecInfo pointer, or an error
|
|
||||||
// if the spec cannot be parsed correctly.
|
|
||||||
func ExtractSpecInfo(spec []byte) (*SpecInfo, error) {
|
|
||||||
|
|
||||||
var parsedSpec yaml.Node
|
|
||||||
|
|
||||||
specVersion := &SpecInfo{}
|
|
||||||
specVersion.JsonParsingChannel = make(chan bool)
|
|
||||||
|
|
||||||
// set original bytes
|
|
||||||
specVersion.SpecBytes = &spec
|
|
||||||
|
|
||||||
runes := []rune(strings.TrimSpace(string(spec)))
|
|
||||||
if len(runes) <= 0 {
|
|
||||||
return specVersion, errors.New("there are no runes in the spec")
|
|
||||||
}
|
|
||||||
|
|
||||||
if runes[0] == '{' && runes[len(runes)-1] == '}' {
|
|
||||||
specVersion.SpecFileType = "json"
|
|
||||||
} else {
|
|
||||||
specVersion.SpecFileType = "yaml"
|
|
||||||
}
|
|
||||||
|
|
||||||
err := yaml.Unmarshal(spec, &parsedSpec)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to parse specification: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
specVersion.RootNode = &parsedSpec
|
|
||||||
|
|
||||||
_, openAPI3 := utils.FindKeyNode(utils.OpenApi3, parsedSpec.Content)
|
|
||||||
_, openAPI2 := utils.FindKeyNode(utils.OpenApi2, parsedSpec.Content)
|
|
||||||
_, asyncAPI := utils.FindKeyNode(utils.AsyncApi, parsedSpec.Content)
|
|
||||||
|
|
||||||
parseJSON := func(bytes []byte, spec *SpecInfo) {
|
|
||||||
var jsonSpec map[string]interface{}
|
|
||||||
|
|
||||||
// no point in worrying about errors here, extract JSON friendly format.
|
|
||||||
// run in a separate thread, don't block.
|
|
||||||
|
|
||||||
if spec.SpecType == utils.OpenApi3 {
|
|
||||||
spec.APISchema = OpenAPI3SchemaData
|
|
||||||
}
|
|
||||||
if spec.SpecType == utils.OpenApi2 {
|
|
||||||
spec.APISchema = OpenAPI2SchemaData
|
|
||||||
}
|
|
||||||
|
|
||||||
if utils.IsYAML(string(bytes)) {
|
|
||||||
yaml.Unmarshal(bytes, &jsonSpec)
|
|
||||||
jsonData, _ := json.Marshal(jsonSpec)
|
|
||||||
spec.SpecJSONBytes = &jsonData
|
|
||||||
spec.SpecJSON = &jsonSpec
|
|
||||||
} else {
|
|
||||||
json.Unmarshal(bytes, &jsonSpec)
|
|
||||||
spec.SpecJSONBytes = &bytes
|
|
||||||
spec.SpecJSON = &jsonSpec
|
|
||||||
}
|
|
||||||
spec.JsonParsingChannel <- true
|
|
||||||
close(spec.JsonParsingChannel)
|
|
||||||
}
|
|
||||||
// check for specific keys
|
|
||||||
if openAPI3 != nil {
|
|
||||||
specVersion.SpecType = utils.OpenApi3
|
|
||||||
version, majorVersion, versionError := parseVersionTypeData(openAPI3.Value)
|
|
||||||
if versionError != nil {
|
|
||||||
return nil, versionError
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse JSON
|
|
||||||
go parseJSON(spec, specVersion)
|
|
||||||
|
|
||||||
// double check for the right version, people mix this up.
|
|
||||||
if majorVersion < 3 {
|
|
||||||
specVersion.Error = errors.New("spec is defined as an openapi spec, but is using a swagger (2.0), or unknown version")
|
|
||||||
return specVersion, specVersion.Error
|
|
||||||
}
|
|
||||||
specVersion.Version = version
|
|
||||||
specVersion.SpecFormat = OAS3
|
|
||||||
}
|
|
||||||
if openAPI2 != nil {
|
|
||||||
specVersion.SpecType = utils.OpenApi2
|
|
||||||
version, majorVersion, versionError := parseVersionTypeData(openAPI2.Value)
|
|
||||||
if versionError != nil {
|
|
||||||
return nil, versionError
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse JSON
|
|
||||||
go parseJSON(spec, specVersion)
|
|
||||||
|
|
||||||
// I am not certain this edge-case is very frequent, but let's make sure we handle it anyway.
|
|
||||||
if majorVersion > 2 {
|
|
||||||
specVersion.Error = errors.New("spec is defined as a swagger (openapi 2.0) spec, but is an openapi 3 or unknown version")
|
|
||||||
return specVersion, specVersion.Error
|
|
||||||
}
|
|
||||||
specVersion.Version = version
|
|
||||||
specVersion.SpecFormat = OAS2
|
|
||||||
}
|
|
||||||
if asyncAPI != nil {
|
|
||||||
specVersion.SpecType = utils.AsyncApi
|
|
||||||
version, majorVersion, versionErr := parseVersionTypeData(asyncAPI.Value)
|
|
||||||
if versionErr != nil {
|
|
||||||
return nil, versionErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse JSON
|
|
||||||
go parseJSON(spec, specVersion)
|
|
||||||
|
|
||||||
// so far there is only 2 as a major release of AsyncAPI
|
|
||||||
if majorVersion > 2 {
|
|
||||||
specVersion.Error = errors.New("spec is defined as asyncapi, but has a major version that is invalid")
|
|
||||||
return specVersion, specVersion.Error
|
|
||||||
}
|
|
||||||
specVersion.Version = version
|
|
||||||
// TODO: format for AsyncAPI.
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if specVersion.SpecType == "" {
|
|
||||||
|
|
||||||
// parse JSON
|
|
||||||
go parseJSON(spec, specVersion)
|
|
||||||
|
|
||||||
specVersion.Error = errors.New("spec type not supported by vacuum, sorry")
|
|
||||||
return specVersion, specVersion.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
return specVersion, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseVersionTypeData(d interface{}) (string, int, error) {
|
|
||||||
r := []rune(strings.TrimSpace(fmt.Sprintf("%v", d)))
|
|
||||||
if len(r) <= 0 {
|
|
||||||
return "", 0, fmt.Errorf("unable to extract version from: %v", d)
|
|
||||||
}
|
|
||||||
return string(r), int(r[0]) - '0', nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AreValuesCorrectlyTyped will look through an array of unknown values and check they match
|
|
||||||
// against the supplied type as a string. The return value is empty if everything is OK, or it
|
|
||||||
// contains failures in the form of a value as a key and a message as to why it's not valid
|
|
||||||
func AreValuesCorrectlyTyped(valType string, values interface{}) map[string]string {
|
|
||||||
var arr []interface{}
|
|
||||||
if _, ok := values.([]interface{}); !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
arr = values.([]interface{})
|
|
||||||
|
|
||||||
results := make(map[string]string)
|
|
||||||
for _, v := range arr {
|
|
||||||
switch v.(type) {
|
|
||||||
case string:
|
|
||||||
if valType != "string" {
|
|
||||||
results[v.(string)] = fmt.Sprintf("enum value '%v' is a "+
|
|
||||||
"string, but it's defined as a '%v'", v, valType)
|
|
||||||
}
|
|
||||||
case int64:
|
|
||||||
if valType != "integer" && valType != "number" {
|
|
||||||
results[fmt.Sprintf("%v", v)] = fmt.Sprintf("enum value '%v' is a "+
|
|
||||||
"integer, but it's defined as a '%v'", v, valType)
|
|
||||||
}
|
|
||||||
case int:
|
|
||||||
if valType != "integer" && valType != "number" {
|
|
||||||
results[fmt.Sprintf("%v", v)] = fmt.Sprintf("enum value '%v' is a "+
|
|
||||||
"integer, but it's defined as a '%v'", v, valType)
|
|
||||||
}
|
|
||||||
case float64:
|
|
||||||
if valType != "number" {
|
|
||||||
results[fmt.Sprintf("%v", v)] = fmt.Sprintf("enum value '%v' is a "+
|
|
||||||
"number, but it's defined as a '%v'", v, valType)
|
|
||||||
}
|
|
||||||
case bool:
|
|
||||||
if valType != "boolean" {
|
|
||||||
results[fmt.Sprintf("%v", v)] = fmt.Sprintf("enum value '%v' is a "+
|
|
||||||
"boolean, but it's defined as a '%v'", v, valType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckEnumForDuplicates will check an array of nodes to check if there are any duplicates.
|
|
||||||
func CheckEnumForDuplicates(seq []*yaml.Node) []*yaml.Node {
|
|
||||||
var res []*yaml.Node
|
|
||||||
seen := make(map[string]*yaml.Node)
|
|
||||||
|
|
||||||
for _, enum := range seq {
|
|
||||||
if seen[enum.Value] != nil {
|
|
||||||
res = append(res, enum)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[enum.Value] = enum
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,274 +1 @@
|
|||||||
package datamodel
|
package datamodel
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/pb33f/libopenapi/utils"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// OpenApi3 is used by all OpenAPI 3+ docs
|
|
||||||
OpenApi3 = "openapi"
|
|
||||||
|
|
||||||
// OpenApi2 is used by all OpenAPI 2 docs, formerly known as swagger.
|
|
||||||
OpenApi2 = "swagger"
|
|
||||||
|
|
||||||
// AsyncApi is used by akk AsyncAPI docs, all versions.
|
|
||||||
AsyncApi = "asyncapi"
|
|
||||||
)
|
|
||||||
|
|
||||||
var goodJSON = `{"name":"kitty", "noises":["meow","purrrr","gggrrraaaaaooooww"]}`
|
|
||||||
var badJSON = `{"name":"kitty, "noises":[{"meow","purrrr","gggrrraaaaaooooww"]}}`
|
|
||||||
var goodYAML = `name: kitty
|
|
||||||
noises:
|
|
||||||
- meow
|
|
||||||
- purrr
|
|
||||||
- gggggrrraaaaaaaaaooooooowwwwwww
|
|
||||||
`
|
|
||||||
|
|
||||||
var badYAML = `name: kitty
|
|
||||||
noises:
|
|
||||||
- meow
|
|
||||||
- purrr
|
|
||||||
- gggggrrraaaaaaaaaooooooowwwwwww
|
|
||||||
`
|
|
||||||
|
|
||||||
var OpenApiWat = `openapi: 3.2
|
|
||||||
info:
|
|
||||||
title: Test API, valid, but not quite valid
|
|
||||||
servers:
|
|
||||||
- url: https://quobix.com/api`
|
|
||||||
|
|
||||||
var OpenApiFalse = `openapi: false
|
|
||||||
info:
|
|
||||||
title: Test API version is a bool?
|
|
||||||
servers:
|
|
||||||
- url: https://quobix.com/api`
|
|
||||||
|
|
||||||
var OpenApiOne = `openapi: 1.0.1
|
|
||||||
info:
|
|
||||||
title: Test API version is what version?
|
|
||||||
servers:
|
|
||||||
- url: https://quobix.com/api`
|
|
||||||
|
|
||||||
var OpenApi3Spec = `openapi: 3.0.1
|
|
||||||
info:
|
|
||||||
title: Test API
|
|
||||||
tags:
|
|
||||||
- name: "Test"
|
|
||||||
- name: "Test 2"
|
|
||||||
servers:
|
|
||||||
- url: https://quobix.com/api`
|
|
||||||
|
|
||||||
var OpenApi2Spec = `swagger: 2.0.1
|
|
||||||
info:
|
|
||||||
title: Test API
|
|
||||||
tags:
|
|
||||||
- name: "Test"
|
|
||||||
servers:
|
|
||||||
- url: https://quobix.com/api`
|
|
||||||
|
|
||||||
var OpenApi2SpecOdd = `swagger: 3.0.1
|
|
||||||
info:
|
|
||||||
title: Test API
|
|
||||||
tags:
|
|
||||||
- name: "Test"
|
|
||||||
servers:
|
|
||||||
- url: https://quobix.com/api`
|
|
||||||
|
|
||||||
var AsyncAPISpec = `asyncapi: 2.0.0
|
|
||||||
info:
|
|
||||||
title: Hello world application
|
|
||||||
version: '0.1.0'
|
|
||||||
channels:
|
|
||||||
hello:
|
|
||||||
publish:
|
|
||||||
message:
|
|
||||||
payload:
|
|
||||||
type: string
|
|
||||||
pattern: '^hello .+$'`
|
|
||||||
|
|
||||||
var AsyncAPISpecOdd = `asyncapi: 3.0.0
|
|
||||||
info:
|
|
||||||
title: Hello world application
|
|
||||||
version: '0.1.0'`
|
|
||||||
|
|
||||||
func TestExtractSpecInfo_ValidJSON(t *testing.T) {
|
|
||||||
r, e := ExtractSpecInfo([]byte(goodJSON))
|
|
||||||
<-r.JsonParsingChannel
|
|
||||||
assert.Greater(t, len(*r.SpecJSONBytes), 0)
|
|
||||||
assert.Error(t, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractSpecInfo_InvalidJSON(t *testing.T) {
|
|
||||||
_, e := ExtractSpecInfo([]byte(badJSON))
|
|
||||||
assert.Error(t, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractSpecInfo_Nothing(t *testing.T) {
|
|
||||||
_, e := ExtractSpecInfo([]byte(""))
|
|
||||||
assert.Error(t, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractSpecInfo_ValidYAML(t *testing.T) {
|
|
||||||
r, e := ExtractSpecInfo([]byte(goodYAML))
|
|
||||||
<-r.JsonParsingChannel
|
|
||||||
assert.Greater(t, len(*r.SpecJSONBytes), 0)
|
|
||||||
assert.Error(t, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractSpecInfo_InvalidYAML(t *testing.T) {
|
|
||||||
_, e := ExtractSpecInfo([]byte(badYAML))
|
|
||||||
assert.Error(t, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractSpecInfo_InvalidOpenAPIVersion(t *testing.T) {
|
|
||||||
_, e := ExtractSpecInfo([]byte(OpenApiOne))
|
|
||||||
assert.Error(t, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractSpecInfo_OpenAPI3(t *testing.T) {
|
|
||||||
|
|
||||||
r, e := ExtractSpecInfo([]byte(OpenApi3Spec))
|
|
||||||
assert.Nil(t, e)
|
|
||||||
assert.Equal(t, utils.OpenApi3, r.SpecType)
|
|
||||||
assert.Equal(t, "3.0.1", r.Version)
|
|
||||||
|
|
||||||
<-r.JsonParsingChannel
|
|
||||||
assert.Greater(t, len(*r.SpecJSONBytes), 0)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractSpecInfo_OpenAPIWat(t *testing.T) {
|
|
||||||
|
|
||||||
r, e := ExtractSpecInfo([]byte(OpenApiWat))
|
|
||||||
assert.Nil(t, e)
|
|
||||||
assert.Equal(t, OpenApi3, r.SpecType)
|
|
||||||
assert.Equal(t, "3.2", r.Version)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractSpecInfo_OpenAPIFalse(t *testing.T) {
|
|
||||||
|
|
||||||
spec, e := ExtractSpecInfo([]byte(OpenApiFalse))
|
|
||||||
assert.NoError(t, e)
|
|
||||||
assert.Equal(t, "false", spec.Version)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractSpecInfo_OpenAPI2(t *testing.T) {
|
|
||||||
|
|
||||||
r, e := ExtractSpecInfo([]byte(OpenApi2Spec))
|
|
||||||
assert.Nil(t, e)
|
|
||||||
assert.Equal(t, OpenApi2, r.SpecType)
|
|
||||||
assert.Equal(t, "2.0.1", r.Version)
|
|
||||||
|
|
||||||
<-r.JsonParsingChannel
|
|
||||||
assert.Greater(t, len(*r.SpecJSONBytes), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractSpecInfo_OpenAPI2_OddVersion(t *testing.T) {
|
|
||||||
|
|
||||||
_, e := ExtractSpecInfo([]byte(OpenApi2SpecOdd))
|
|
||||||
assert.NotNil(t, e)
|
|
||||||
assert.Equal(t,
|
|
||||||
"spec is defined as a swagger (openapi 2.0) spec, but is an openapi 3 or unknown version", e.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractSpecInfo_AsyncAPI(t *testing.T) {
|
|
||||||
|
|
||||||
r, e := ExtractSpecInfo([]byte(AsyncAPISpec))
|
|
||||||
assert.Nil(t, e)
|
|
||||||
assert.Equal(t, AsyncApi, r.SpecType)
|
|
||||||
assert.Equal(t, "2.0.0", r.Version)
|
|
||||||
<-r.JsonParsingChannel
|
|
||||||
assert.Greater(t, len(*r.SpecJSONBytes), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractSpecInfo_AsyncAPI_OddVersion(t *testing.T) {
|
|
||||||
|
|
||||||
_, e := ExtractSpecInfo([]byte(AsyncAPISpecOdd))
|
|
||||||
assert.NotNil(t, e)
|
|
||||||
assert.Equal(t,
|
|
||||||
"spec is defined as asyncapi, but has a major version that is invalid", e.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAreValuesCorrectlyTyped(t *testing.T) {
|
|
||||||
|
|
||||||
assert.Len(t, AreValuesCorrectlyTyped("string", []interface{}{"hi"}), 0)
|
|
||||||
assert.Len(t, AreValuesCorrectlyTyped("string", []interface{}{1}), 1)
|
|
||||||
assert.Len(t, AreValuesCorrectlyTyped("string", []interface{}{"nice", 123, int64(12345)}), 2)
|
|
||||||
assert.Len(t, AreValuesCorrectlyTyped("string", []interface{}{1.2, "burgers"}), 1)
|
|
||||||
assert.Len(t, AreValuesCorrectlyTyped("string", []interface{}{true, false, "what"}), 2)
|
|
||||||
|
|
||||||
assert.Len(t, AreValuesCorrectlyTyped("integer", []interface{}{1, 2, 3, 4}), 0)
|
|
||||||
assert.Len(t, AreValuesCorrectlyTyped("integer", []interface{}{"no way!"}), 1)
|
|
||||||
assert.Len(t, AreValuesCorrectlyTyped("integer", []interface{}{"nice", 123, int64(12345)}), 1)
|
|
||||||
assert.Len(t, AreValuesCorrectlyTyped("integer", []interface{}{999, 1.2, "burgers"}), 2)
|
|
||||||
assert.Len(t, AreValuesCorrectlyTyped("integer", []interface{}{true, false, "what"}), 3)
|
|
||||||
|
|
||||||
assert.Len(t, AreValuesCorrectlyTyped("number", []interface{}{1.2345}), 0)
|
|
||||||
assert.Len(t, AreValuesCorrectlyTyped("number", []interface{}{"no way!"}), 1)
|
|
||||||
assert.Len(t, AreValuesCorrectlyTyped("number", []interface{}{"nice", 123, 2.353}), 1)
|
|
||||||
assert.Len(t, AreValuesCorrectlyTyped("number", []interface{}{999, 1.2, "burgers"}), 1)
|
|
||||||
assert.Len(t, AreValuesCorrectlyTyped("number", []interface{}{true, false, "what"}), 3)
|
|
||||||
|
|
||||||
assert.Len(t, AreValuesCorrectlyTyped("boolean", []interface{}{true, false, true}), 0)
|
|
||||||
assert.Len(t, AreValuesCorrectlyTyped("boolean", []interface{}{"no way!"}), 1)
|
|
||||||
assert.Len(t, AreValuesCorrectlyTyped("boolean", []interface{}{"nice", 123, 2.353, true}), 3)
|
|
||||||
assert.Len(t, AreValuesCorrectlyTyped("boolean", []interface{}{true, true, "burgers"}), 1)
|
|
||||||
assert.Len(t, AreValuesCorrectlyTyped("boolean", []interface{}{true, false, "what", 1.2, 4}), 3)
|
|
||||||
|
|
||||||
assert.Nil(t, AreValuesCorrectlyTyped("boolean", []string{"hi"}))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckEnumForDuplicates_Success(t *testing.T) {
|
|
||||||
yml := "- yes\n- no\n- crisps"
|
|
||||||
var rootNode yaml.Node
|
|
||||||
yaml.Unmarshal([]byte(yml), &rootNode)
|
|
||||||
assert.Len(t, CheckEnumForDuplicates(rootNode.Content[0].Content), 0)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckEnumForDuplicates_Fail(t *testing.T) {
|
|
||||||
yml := "- yes\n- no\n- crisps\n- no"
|
|
||||||
var rootNode yaml.Node
|
|
||||||
yaml.Unmarshal([]byte(yml), &rootNode)
|
|
||||||
assert.Len(t, CheckEnumForDuplicates(rootNode.Content[0].Content), 1)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckEnumForDuplicates_FailMultiple(t *testing.T) {
|
|
||||||
yml := "- yes\n- no\n- crisps\n- no\n- rice\n- yes\n- no"
|
|
||||||
|
|
||||||
var rootNode yaml.Node
|
|
||||||
yaml.Unmarshal([]byte(yml), &rootNode)
|
|
||||||
assert.Len(t, CheckEnumForDuplicates(rootNode.Content[0].Content), 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractSpecInfo_BadVersion_OpenAPI3(t *testing.T) {
|
|
||||||
|
|
||||||
yml := `openapi:
|
|
||||||
should: fail`
|
|
||||||
|
|
||||||
_, err := ExtractSpecInfo([]byte(yml))
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractSpecInfo_BadVersion_Swagger(t *testing.T) {
|
|
||||||
|
|
||||||
yml := `swagger:
|
|
||||||
should: fail`
|
|
||||||
|
|
||||||
_, err := ExtractSpecInfo([]byte(yml))
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractSpecInfo_BadVersion_AsyncAPI(t *testing.T) {
|
|
||||||
|
|
||||||
yml := `asyncapi:
|
|
||||||
should: fail`
|
|
||||||
|
|
||||||
_, err := ExtractSpecInfo([]byte(yml))
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,18 +4,23 @@
|
|||||||
package datamodel
|
package datamodel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/pb33f/libopenapi/utils"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SpecInfo represents information about a supplied specification.
|
// SpecInfo represents a 'ready-to-process' OpenAPI Document.
|
||||||
type SpecInfo struct {
|
type SpecInfo struct {
|
||||||
SpecType string `json:"type"`
|
SpecType string `json:"type"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
SpecFormat string `json:"format"`
|
SpecFormat string `json:"format"`
|
||||||
SpecFileType string `json:"fileType"`
|
SpecFileType string `json:"fileType"`
|
||||||
|
SpecBytes *[]byte `json:"bytes"` // the original byte array
|
||||||
RootNode *yaml.Node `json:"-"` // reference to the root node of the spec.
|
RootNode *yaml.Node `json:"-"` // reference to the root node of the spec.
|
||||||
SpecBytes *[]byte `json:"bytes"` // the original bytes
|
|
||||||
SpecJSONBytes *[]byte `json:"-"` // original bytes converted to JSON
|
SpecJSONBytes *[]byte `json:"-"` // original bytes converted to JSON
|
||||||
SpecJSON *map[string]interface{} `json:"-"` // standard JSON map of original bytes
|
SpecJSON *map[string]interface{} `json:"-"` // standard JSON map of original bytes
|
||||||
Error error `json:"-"` // something go wrong?
|
Error error `json:"-"` // something go wrong?
|
||||||
@@ -25,7 +30,149 @@ type SpecInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetJSONParsingChannel returns a channel that will close once async JSON parsing is completed.
|
// GetJSONParsingChannel returns a channel that will close once async JSON parsing is completed.
|
||||||
// This is required as rules may start executing before we're even done reading in the spec to JSON.
|
// This is really useful if your application wants to analyze the JSON via SpecJSON. the library will
|
||||||
|
// return *SpecInfo BEFORE the JSON is done parsing, so things are as fast as possible.
|
||||||
|
//
|
||||||
|
// If you want to know when parsing is done, listen on the channel for a bool.
|
||||||
func (si SpecInfo) GetJSONParsingChannel() chan bool {
|
func (si SpecInfo) GetJSONParsingChannel() chan bool {
|
||||||
return si.JsonParsingChannel
|
return si.JsonParsingChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExtractSpecInfo accepts an OpenAPI/Swagger specification that has been read into a byte array
|
||||||
|
// and will return a *SpecInfo pointer, which contains details on the version and an un-marshaled
|
||||||
|
// *yaml.Node root node tree. The root node tree is what's used by the library when building out models.
|
||||||
|
//
|
||||||
|
// If the spec cannot be parsed correctly then an error will be returned, otherwise the error is nil.
|
||||||
|
func ExtractSpecInfo(spec []byte) (*SpecInfo, error) {
|
||||||
|
|
||||||
|
var parsedSpec yaml.Node
|
||||||
|
|
||||||
|
specVersion := &SpecInfo{}
|
||||||
|
specVersion.JsonParsingChannel = make(chan bool)
|
||||||
|
|
||||||
|
// set original bytes
|
||||||
|
specVersion.SpecBytes = &spec
|
||||||
|
|
||||||
|
runes := []rune(strings.TrimSpace(string(spec)))
|
||||||
|
if len(runes) <= 0 {
|
||||||
|
return specVersion, errors.New("there is nothing in the spec, it's empty - so there is nothing to be done")
|
||||||
|
}
|
||||||
|
|
||||||
|
if runes[0] == '{' && runes[len(runes)-1] == '}' {
|
||||||
|
specVersion.SpecFileType = "json"
|
||||||
|
} else {
|
||||||
|
specVersion.SpecFileType = "yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
err := yaml.Unmarshal(spec, &parsedSpec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to parse specification: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
specVersion.RootNode = &parsedSpec
|
||||||
|
|
||||||
|
_, openAPI3 := utils.FindKeyNode(utils.OpenApi3, parsedSpec.Content)
|
||||||
|
_, openAPI2 := utils.FindKeyNode(utils.OpenApi2, parsedSpec.Content)
|
||||||
|
_, asyncAPI := utils.FindKeyNode(utils.AsyncApi, parsedSpec.Content)
|
||||||
|
|
||||||
|
parseJSON := func(bytes []byte, spec *SpecInfo) {
|
||||||
|
var jsonSpec map[string]interface{}
|
||||||
|
|
||||||
|
// no point in worrying about errors here, extract JSON friendly format.
|
||||||
|
// run in a separate thread, don't block.
|
||||||
|
|
||||||
|
if spec.SpecType == utils.OpenApi3 {
|
||||||
|
spec.APISchema = OpenAPI3SchemaData
|
||||||
|
}
|
||||||
|
if spec.SpecType == utils.OpenApi2 {
|
||||||
|
spec.APISchema = OpenAPI2SchemaData
|
||||||
|
}
|
||||||
|
|
||||||
|
if utils.IsYAML(string(bytes)) {
|
||||||
|
_ = yaml.Unmarshal(bytes, &jsonSpec)
|
||||||
|
jsonData, _ := json.Marshal(jsonSpec)
|
||||||
|
spec.SpecJSONBytes = &jsonData
|
||||||
|
spec.SpecJSON = &jsonSpec
|
||||||
|
} else {
|
||||||
|
_ = json.Unmarshal(bytes, &jsonSpec)
|
||||||
|
spec.SpecJSONBytes = &bytes
|
||||||
|
spec.SpecJSON = &jsonSpec
|
||||||
|
}
|
||||||
|
spec.JsonParsingChannel <- true
|
||||||
|
close(spec.JsonParsingChannel)
|
||||||
|
}
|
||||||
|
// check for specific keys
|
||||||
|
if openAPI3 != nil {
|
||||||
|
specVersion.SpecType = utils.OpenApi3
|
||||||
|
version, majorVersion, versionError := parseVersionTypeData(openAPI3.Value)
|
||||||
|
if versionError != nil {
|
||||||
|
return nil, versionError
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse JSON
|
||||||
|
go parseJSON(spec, specVersion)
|
||||||
|
|
||||||
|
// double check for the right version, people mix this up.
|
||||||
|
if majorVersion < 3 {
|
||||||
|
specVersion.Error = errors.New("spec is defined as an openapi spec, but is using a swagger (2.0), or unknown version")
|
||||||
|
return specVersion, specVersion.Error
|
||||||
|
}
|
||||||
|
specVersion.Version = version
|
||||||
|
specVersion.SpecFormat = OAS3
|
||||||
|
}
|
||||||
|
if openAPI2 != nil {
|
||||||
|
specVersion.SpecType = utils.OpenApi2
|
||||||
|
version, majorVersion, versionError := parseVersionTypeData(openAPI2.Value)
|
||||||
|
if versionError != nil {
|
||||||
|
return nil, versionError
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse JSON
|
||||||
|
go parseJSON(spec, specVersion)
|
||||||
|
|
||||||
|
// I am not certain this edge-case is very frequent, but let's make sure we handle it anyway.
|
||||||
|
if majorVersion > 2 {
|
||||||
|
specVersion.Error = errors.New("spec is defined as a swagger (openapi 2.0) spec, but is an openapi 3 or unknown version")
|
||||||
|
return specVersion, specVersion.Error
|
||||||
|
}
|
||||||
|
specVersion.Version = version
|
||||||
|
specVersion.SpecFormat = OAS2
|
||||||
|
}
|
||||||
|
if asyncAPI != nil {
|
||||||
|
specVersion.SpecType = utils.AsyncApi
|
||||||
|
version, majorVersion, versionErr := parseVersionTypeData(asyncAPI.Value)
|
||||||
|
if versionErr != nil {
|
||||||
|
return nil, versionErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse JSON
|
||||||
|
go parseJSON(spec, specVersion)
|
||||||
|
|
||||||
|
// so far there is only 2 as a major release of AsyncAPI
|
||||||
|
if majorVersion > 2 {
|
||||||
|
specVersion.Error = errors.New("spec is defined as asyncapi, but has a major version that is invalid")
|
||||||
|
return specVersion, specVersion.Error
|
||||||
|
}
|
||||||
|
specVersion.Version = version
|
||||||
|
// TODO: format for AsyncAPI.
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if specVersion.SpecType == "" {
|
||||||
|
// parse JSON
|
||||||
|
go parseJSON(spec, specVersion)
|
||||||
|
|
||||||
|
specVersion.Error = errors.New("spec type not supported by vacuum, sorry")
|
||||||
|
return specVersion, specVersion.Error
|
||||||
|
}
|
||||||
|
return specVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract version number from specification
|
||||||
|
func parseVersionTypeData(d interface{}) (string, int, error) {
|
||||||
|
r := []rune(strings.TrimSpace(fmt.Sprintf("%v", d)))
|
||||||
|
if len(r) <= 0 {
|
||||||
|
return "", 0, fmt.Errorf("unable to extract version from: %v", d)
|
||||||
|
}
|
||||||
|
return string(r), int(r[0]) - '0', nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,15 +4,228 @@
|
|||||||
package datamodel
|
package datamodel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/pb33f/libopenapi/utils"
|
||||||
"testing"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// OpenApi3 is used by all OpenAPI 3+ docs
|
||||||
|
OpenApi3 = "openapi"
|
||||||
|
|
||||||
|
// OpenApi2 is used by all OpenAPI 2 docs, formerly known as swagger.
|
||||||
|
OpenApi2 = "swagger"
|
||||||
|
|
||||||
|
// AsyncApi is used by akk AsyncAPI docs, all versions.
|
||||||
|
AsyncApi = "asyncapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSpecInfo_GetJSONParsingChannel(t *testing.T) {
|
func TestSpecInfo_GetJSONParsingChannel(t *testing.T) {
|
||||||
|
|
||||||
// dumb, but we need to ensure coverage is as high as we can make it.
|
// dumb, but we need to ensure coverage is as high as we can make it.
|
||||||
bchan := make(chan bool)
|
bchan := make(chan bool)
|
||||||
si := &SpecInfo{JsonParsingChannel: bchan}
|
si := &SpecInfo{JsonParsingChannel: bchan}
|
||||||
assert.Equal(t, si.GetJSONParsingChannel(), bchan)
|
assert.Equal(t, si.GetJSONParsingChannel(), bchan)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var goodJSON = `{"name":"kitty", "noises":["meow","purrrr","gggrrraaaaaooooww"]}`
|
||||||
|
var badJSON = `{"name":"kitty, "noises":[{"meow","purrrr","gggrrraaaaaooooww"]}}`
|
||||||
|
var goodYAML = `name: kitty
|
||||||
|
noises:
|
||||||
|
- meow
|
||||||
|
- purrr
|
||||||
|
- gggggrrraaaaaaaaaooooooowwwwwww
|
||||||
|
`
|
||||||
|
|
||||||
|
var badYAML = `name: kitty
|
||||||
|
noises:
|
||||||
|
- meow
|
||||||
|
- purrr
|
||||||
|
- gggggrrraaaaaaaaaooooooowwwwwww
|
||||||
|
`
|
||||||
|
|
||||||
|
var OpenApiWat = `openapi: 3.2
|
||||||
|
info:
|
||||||
|
title: Test API, valid, but not quite valid
|
||||||
|
servers:
|
||||||
|
- url: https://quobix.com/api`
|
||||||
|
|
||||||
|
var OpenApiFalse = `openapi: false
|
||||||
|
info:
|
||||||
|
title: Test API version is a bool?
|
||||||
|
servers:
|
||||||
|
- url: https://quobix.com/api`
|
||||||
|
|
||||||
|
var OpenApiOne = `openapi: 1.0.1
|
||||||
|
info:
|
||||||
|
title: Test API version is what version?
|
||||||
|
servers:
|
||||||
|
- url: https://quobix.com/api`
|
||||||
|
|
||||||
|
var OpenApi3Spec = `openapi: 3.0.1
|
||||||
|
info:
|
||||||
|
title: Test API
|
||||||
|
tags:
|
||||||
|
- name: "Test"
|
||||||
|
- name: "Test 2"
|
||||||
|
servers:
|
||||||
|
- url: https://quobix.com/api`
|
||||||
|
|
||||||
|
var OpenApi2Spec = `swagger: 2.0.1
|
||||||
|
info:
|
||||||
|
title: Test API
|
||||||
|
tags:
|
||||||
|
- name: "Test"
|
||||||
|
servers:
|
||||||
|
- url: https://quobix.com/api`
|
||||||
|
|
||||||
|
var OpenApi2SpecOdd = `swagger: 3.0.1
|
||||||
|
info:
|
||||||
|
title: Test API
|
||||||
|
tags:
|
||||||
|
- name: "Test"
|
||||||
|
servers:
|
||||||
|
- url: https://quobix.com/api`
|
||||||
|
|
||||||
|
var AsyncAPISpec = `asyncapi: 2.0.0
|
||||||
|
info:
|
||||||
|
title: Hello world application
|
||||||
|
version: '0.1.0'
|
||||||
|
channels:
|
||||||
|
hello:
|
||||||
|
publish:
|
||||||
|
message:
|
||||||
|
payload:
|
||||||
|
type: string
|
||||||
|
pattern: '^hello .+$'`
|
||||||
|
|
||||||
|
var AsyncAPISpecOdd = `asyncapi: 3.0.0
|
||||||
|
info:
|
||||||
|
title: Hello world application
|
||||||
|
version: '0.1.0'`
|
||||||
|
|
||||||
|
func TestExtractSpecInfo_ValidJSON(t *testing.T) {
|
||||||
|
r, e := ExtractSpecInfo([]byte(goodJSON))
|
||||||
|
<-r.JsonParsingChannel
|
||||||
|
assert.Greater(t, len(*r.SpecJSONBytes), 0)
|
||||||
|
assert.Error(t, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractSpecInfo_InvalidJSON(t *testing.T) {
|
||||||
|
_, e := ExtractSpecInfo([]byte(badJSON))
|
||||||
|
assert.Error(t, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractSpecInfo_Nothing(t *testing.T) {
|
||||||
|
_, e := ExtractSpecInfo([]byte(""))
|
||||||
|
assert.Error(t, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractSpecInfo_ValidYAML(t *testing.T) {
|
||||||
|
r, e := ExtractSpecInfo([]byte(goodYAML))
|
||||||
|
<-r.JsonParsingChannel
|
||||||
|
assert.Greater(t, len(*r.SpecJSONBytes), 0)
|
||||||
|
assert.Error(t, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractSpecInfo_InvalidYAML(t *testing.T) {
|
||||||
|
_, e := ExtractSpecInfo([]byte(badYAML))
|
||||||
|
assert.Error(t, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractSpecInfo_InvalidOpenAPIVersion(t *testing.T) {
|
||||||
|
_, e := ExtractSpecInfo([]byte(OpenApiOne))
|
||||||
|
assert.Error(t, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractSpecInfo_OpenAPI3(t *testing.T) {
|
||||||
|
|
||||||
|
r, e := ExtractSpecInfo([]byte(OpenApi3Spec))
|
||||||
|
assert.Nil(t, e)
|
||||||
|
assert.Equal(t, utils.OpenApi3, r.SpecType)
|
||||||
|
assert.Equal(t, "3.0.1", r.Version)
|
||||||
|
|
||||||
|
<-r.JsonParsingChannel
|
||||||
|
assert.Greater(t, len(*r.SpecJSONBytes), 0)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractSpecInfo_OpenAPIWat(t *testing.T) {
|
||||||
|
|
||||||
|
r, e := ExtractSpecInfo([]byte(OpenApiWat))
|
||||||
|
assert.Nil(t, e)
|
||||||
|
assert.Equal(t, OpenApi3, r.SpecType)
|
||||||
|
assert.Equal(t, "3.2", r.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractSpecInfo_OpenAPIFalse(t *testing.T) {
|
||||||
|
|
||||||
|
spec, e := ExtractSpecInfo([]byte(OpenApiFalse))
|
||||||
|
assert.NoError(t, e)
|
||||||
|
assert.Equal(t, "false", spec.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractSpecInfo_OpenAPI2(t *testing.T) {
|
||||||
|
|
||||||
|
r, e := ExtractSpecInfo([]byte(OpenApi2Spec))
|
||||||
|
assert.Nil(t, e)
|
||||||
|
assert.Equal(t, OpenApi2, r.SpecType)
|
||||||
|
assert.Equal(t, "2.0.1", r.Version)
|
||||||
|
|
||||||
|
<-r.JsonParsingChannel
|
||||||
|
assert.Greater(t, len(*r.SpecJSONBytes), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractSpecInfo_OpenAPI2_OddVersion(t *testing.T) {
|
||||||
|
|
||||||
|
_, e := ExtractSpecInfo([]byte(OpenApi2SpecOdd))
|
||||||
|
assert.NotNil(t, e)
|
||||||
|
assert.Equal(t,
|
||||||
|
"spec is defined as a swagger (openapi 2.0) spec, but is an openapi 3 or unknown version", e.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractSpecInfo_AsyncAPI(t *testing.T) {
|
||||||
|
|
||||||
|
r, e := ExtractSpecInfo([]byte(AsyncAPISpec))
|
||||||
|
assert.Nil(t, e)
|
||||||
|
assert.Equal(t, AsyncApi, r.SpecType)
|
||||||
|
assert.Equal(t, "2.0.0", r.Version)
|
||||||
|
<-r.JsonParsingChannel
|
||||||
|
assert.Greater(t, len(*r.SpecJSONBytes), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractSpecInfo_AsyncAPI_OddVersion(t *testing.T) {
|
||||||
|
|
||||||
|
_, e := ExtractSpecInfo([]byte(AsyncAPISpecOdd))
|
||||||
|
assert.NotNil(t, e)
|
||||||
|
assert.Equal(t,
|
||||||
|
"spec is defined as asyncapi, but has a major version that is invalid", e.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractSpecInfo_BadVersion_OpenAPI3(t *testing.T) {
|
||||||
|
|
||||||
|
yml := `openapi:
|
||||||
|
should: fail`
|
||||||
|
|
||||||
|
_, err := ExtractSpecInfo([]byte(yml))
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractSpecInfo_BadVersion_Swagger(t *testing.T) {
|
||||||
|
|
||||||
|
yml := `swagger:
|
||||||
|
should: fail`
|
||||||
|
|
||||||
|
_, err := ExtractSpecInfo([]byte(yml))
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractSpecInfo_BadVersion_AsyncAPI(t *testing.T) {
|
||||||
|
|
||||||
|
yml := `asyncapi:
|
||||||
|
should: fail`
|
||||||
|
|
||||||
|
_, err := ExtractSpecInfo([]byte(yml))
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|||||||
69
document.go
69
document.go
@@ -4,25 +4,80 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/pb33f/libopenapi/datamodel"
|
"github.com/pb33f/libopenapi/datamodel"
|
||||||
v2high "github.com/pb33f/libopenapi/datamodel/high/2.0"
|
v2high "github.com/pb33f/libopenapi/datamodel/high/2.0"
|
||||||
v3high "github.com/pb33f/libopenapi/datamodel/high/3.0"
|
v3high "github.com/pb33f/libopenapi/datamodel/high/3.0"
|
||||||
|
v2low "github.com/pb33f/libopenapi/datamodel/low/2.0"
|
||||||
|
v3low "github.com/pb33f/libopenapi/datamodel/low/3.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Document[T any] struct {
|
type Document struct {
|
||||||
version string
|
version string
|
||||||
info *datamodel.SpecInfo
|
info *datamodel.SpecInfo
|
||||||
Model T
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Document[T]) GetVersion() string {
|
type DocumentModel[T v2high.Swagger | v3high.Document] struct {
|
||||||
|
Model T
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDocument(specByteArray []byte) (*Document, error) {
|
||||||
|
info, err := datamodel.ExtractSpecInfo(specByteArray)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
d := new(Document)
|
||||||
|
d.version = info.Version
|
||||||
|
d.info = info
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) GetVersion() string {
|
||||||
return d.version
|
return d.version
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Document[T]) BuildV2Document() (*v2high.Swagger, error) {
|
func (d *Document) GetSpecInfo() *datamodel.SpecInfo {
|
||||||
return nil, nil
|
return d.info
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Document[T]) BuildV3Document() (*v3high.Document, error) {
|
func (d *Document) BuildV2Document() (*DocumentModel[v2high.Swagger], []error) {
|
||||||
return nil, nil
|
var errors []error
|
||||||
|
if d.info == nil {
|
||||||
|
errors = append(errors, fmt.Errorf("unable to build swagger document, no specification has been loaded"))
|
||||||
|
return nil, errors
|
||||||
|
}
|
||||||
|
if d.info.SpecFormat != datamodel.OAS2 {
|
||||||
|
errors = append(errors, fmt.Errorf("unable to build swagger document, "+
|
||||||
|
"supplied spec is a different version (%v). Try 'BuildV3Document()'", d.info.SpecFormat))
|
||||||
|
return nil, errors
|
||||||
|
}
|
||||||
|
lowDoc, err := v2low.CreateDocument(d.info)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
highDoc := v2high.NewSwaggerDocument(lowDoc)
|
||||||
|
return &DocumentModel[v2high.Swagger]{
|
||||||
|
Model: *highDoc,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) BuildV3Document() (*DocumentModel[v3high.Document], []error) {
|
||||||
|
var errors []error
|
||||||
|
if d.info == nil {
|
||||||
|
errors = append(errors, fmt.Errorf("unable to build document, no specification has been loaded"))
|
||||||
|
return nil, errors
|
||||||
|
}
|
||||||
|
if d.info.SpecFormat != datamodel.OAS3 {
|
||||||
|
errors = append(errors, fmt.Errorf("unable to build openapi document, "+
|
||||||
|
"supplied spec is a different version (%v). Try 'BuildV2Document()'", d.info.SpecFormat))
|
||||||
|
return nil, errors
|
||||||
|
}
|
||||||
|
lowDoc, err := v3low.CreateDocument(d.info)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
highDoc := v3high.NewDocument(lowDoc)
|
||||||
|
return &DocumentModel[v3high.Document]{
|
||||||
|
Model: *highDoc,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
104
document_test.go
104
document_test.go
@@ -4,23 +4,105 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLoadDocument_Simple(t *testing.T) {
|
func TestLoadDocument_Simple_V2(t *testing.T) {
|
||||||
//
|
|
||||||
//yml := `openapi: 3.0.1`
|
yml := `swagger: 2.0.1`
|
||||||
//doc, err := LoadDocument([]byte(yml))
|
doc, err := NewDocument([]byte(yml))
|
||||||
//assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
//assert.Equal(t, "3.0.1", doc.GetVersion())
|
assert.Equal(t, "2.0.1", doc.GetVersion())
|
||||||
|
|
||||||
|
v2Doc, docErr := doc.BuildV2Document()
|
||||||
|
assert.Len(t, docErr, 0)
|
||||||
|
assert.NotNil(t, v2Doc)
|
||||||
|
assert.NotNil(t, doc.GetSpecInfo())
|
||||||
|
|
||||||
|
fmt.Print()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadDocument_WithInfo(t *testing.T) {
|
func TestLoadDocument_Simple_V2_Error(t *testing.T) {
|
||||||
|
|
||||||
//yml := `openapi: 3.0.1`
|
yml := `swagger: 2.0`
|
||||||
//doc, err := LoadDocument([]byte(yml))
|
doc, err := NewDocument([]byte(yml))
|
||||||
//assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
//assert.Equal(t, "3.0.1", doc.GetVersion())
|
|
||||||
|
|
||||||
|
v2Doc, docErr := doc.BuildV3Document()
|
||||||
|
assert.Len(t, docErr, 1)
|
||||||
|
assert.Nil(t, v2Doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadDocument_Simple_V2_Error_BadSpec(t *testing.T) {
|
||||||
|
|
||||||
|
yml := `swagger: 2.0
|
||||||
|
definitions:
|
||||||
|
thing:
|
||||||
|
$ref: bork`
|
||||||
|
doc, err := NewDocument([]byte(yml))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
v2Doc, docErr := doc.BuildV2Document()
|
||||||
|
assert.Len(t, docErr, 1)
|
||||||
|
assert.Nil(t, v2Doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadDocument_Simple_V3_Error(t *testing.T) {
|
||||||
|
|
||||||
|
yml := `openapi: 3.0.1`
|
||||||
|
doc, err := NewDocument([]byte(yml))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
v2Doc, docErr := doc.BuildV2Document()
|
||||||
|
assert.Len(t, docErr, 1)
|
||||||
|
assert.Nil(t, v2Doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadDocument_Error_V2NoSpec(t *testing.T) {
|
||||||
|
|
||||||
|
doc := new(Document) // not how this should be instantiated.
|
||||||
|
_, err := doc.BuildV2Document()
|
||||||
|
assert.Len(t, err, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadDocument_Error_V3NoSpec(t *testing.T) {
|
||||||
|
|
||||||
|
doc := new(Document) // not how this should be instantiated.
|
||||||
|
_, err := doc.BuildV3Document()
|
||||||
|
assert.Len(t, err, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadDocument_Empty(t *testing.T) {
|
||||||
|
yml := ``
|
||||||
|
_, err := NewDocument([]byte(yml))
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadDocument_Simple_V3(t *testing.T) {
|
||||||
|
|
||||||
|
yml := `openapi: 3.0.1`
|
||||||
|
doc, err := NewDocument([]byte(yml))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "3.0.1", doc.GetVersion())
|
||||||
|
|
||||||
|
v3Doc, docErr := doc.BuildV3Document()
|
||||||
|
assert.Len(t, docErr, 0)
|
||||||
|
assert.NotNil(t, v3Doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadDocument_Simple_V3_Error_BadSpec(t *testing.T) {
|
||||||
|
|
||||||
|
yml := `openapi: 3.0
|
||||||
|
paths:
|
||||||
|
"/some":
|
||||||
|
$ref: bork`
|
||||||
|
doc, err := NewDocument([]byte(yml))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
v3Doc, docErr := doc.BuildV3Document()
|
||||||
|
assert.Len(t, docErr, 1)
|
||||||
|
assert.Nil(t, v3Doc)
|
||||||
}
|
}
|
||||||
|
|||||||
49
utils/type_check.go
Normal file
49
utils/type_check.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// AreValuesCorrectlyTyped will look through an array of unknown values and check they match
|
||||||
|
// against the supplied type as a string. The return value is empty if everything is OK, or it
|
||||||
|
// contains failures in the form of a value as a key and a message as to why it's not valid
|
||||||
|
func AreValuesCorrectlyTyped(valType string, values interface{}) map[string]string {
|
||||||
|
var arr []interface{}
|
||||||
|
if _, ok := values.([]interface{}); !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
arr = values.([]interface{})
|
||||||
|
|
||||||
|
results := make(map[string]string)
|
||||||
|
for _, v := range arr {
|
||||||
|
switch v.(type) {
|
||||||
|
case string:
|
||||||
|
if valType != "string" {
|
||||||
|
results[v.(string)] = fmt.Sprintf("enum value '%v' is a "+
|
||||||
|
"string, but it's defined as a '%v'", v, valType)
|
||||||
|
}
|
||||||
|
case int64:
|
||||||
|
if valType != "integer" && valType != "number" {
|
||||||
|
results[fmt.Sprintf("%v", v)] = fmt.Sprintf("enum value '%v' is a "+
|
||||||
|
"integer, but it's defined as a '%v'", v, valType)
|
||||||
|
}
|
||||||
|
case int:
|
||||||
|
if valType != "integer" && valType != "number" {
|
||||||
|
results[fmt.Sprintf("%v", v)] = fmt.Sprintf("enum value '%v' is a "+
|
||||||
|
"integer, but it's defined as a '%v'", v, valType)
|
||||||
|
}
|
||||||
|
case float64:
|
||||||
|
if valType != "number" {
|
||||||
|
results[fmt.Sprintf("%v", v)] = fmt.Sprintf("enum value '%v' is a "+
|
||||||
|
"number, but it's defined as a '%v'", v, valType)
|
||||||
|
}
|
||||||
|
case bool:
|
||||||
|
if valType != "boolean" {
|
||||||
|
results[fmt.Sprintf("%v", v)] = fmt.Sprintf("enum value '%v' is a "+
|
||||||
|
"boolean, but it's defined as a '%v'", v, valType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
37
utils/type_check_test.go
Normal file
37
utils/type_check_test.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAreValuesCorrectlyTyped(t *testing.T) {
|
||||||
|
assert.Len(t, AreValuesCorrectlyTyped("string", []interface{}{"hi"}), 0)
|
||||||
|
assert.Len(t, AreValuesCorrectlyTyped("string", []interface{}{1}), 1)
|
||||||
|
assert.Len(t, AreValuesCorrectlyTyped("string", []interface{}{"nice", 123, int64(12345)}), 2)
|
||||||
|
assert.Len(t, AreValuesCorrectlyTyped("string", []interface{}{1.2, "burgers"}), 1)
|
||||||
|
assert.Len(t, AreValuesCorrectlyTyped("string", []interface{}{true, false, "what"}), 2)
|
||||||
|
|
||||||
|
assert.Len(t, AreValuesCorrectlyTyped("integer", []interface{}{1, 2, 3, 4}), 0)
|
||||||
|
assert.Len(t, AreValuesCorrectlyTyped("integer", []interface{}{"no way!"}), 1)
|
||||||
|
assert.Len(t, AreValuesCorrectlyTyped("integer", []interface{}{"nice", 123, int64(12345)}), 1)
|
||||||
|
assert.Len(t, AreValuesCorrectlyTyped("integer", []interface{}{999, 1.2, "burgers"}), 2)
|
||||||
|
assert.Len(t, AreValuesCorrectlyTyped("integer", []interface{}{true, false, "what"}), 3)
|
||||||
|
|
||||||
|
assert.Len(t, AreValuesCorrectlyTyped("number", []interface{}{1.2345}), 0)
|
||||||
|
assert.Len(t, AreValuesCorrectlyTyped("number", []interface{}{"no way!"}), 1)
|
||||||
|
assert.Len(t, AreValuesCorrectlyTyped("number", []interface{}{"nice", 123, 2.353}), 1)
|
||||||
|
assert.Len(t, AreValuesCorrectlyTyped("number", []interface{}{999, 1.2, "burgers"}), 1)
|
||||||
|
assert.Len(t, AreValuesCorrectlyTyped("number", []interface{}{true, false, "what"}), 3)
|
||||||
|
|
||||||
|
assert.Len(t, AreValuesCorrectlyTyped("boolean", []interface{}{true, false, true}), 0)
|
||||||
|
assert.Len(t, AreValuesCorrectlyTyped("boolean", []interface{}{"no way!"}), 1)
|
||||||
|
assert.Len(t, AreValuesCorrectlyTyped("boolean", []interface{}{"nice", 123, 2.353, true}), 3)
|
||||||
|
assert.Len(t, AreValuesCorrectlyTyped("boolean", []interface{}{true, true, "burgers"}), 1)
|
||||||
|
assert.Len(t, AreValuesCorrectlyTyped("boolean", []interface{}{true, false, "what", 1.2, 4}), 3)
|
||||||
|
assert.Nil(t, AreValuesCorrectlyTyped("boolean", []string{"hi"}))
|
||||||
|
|
||||||
|
}
|
||||||
@@ -563,3 +563,18 @@ func DetectCase(input string) Case {
|
|||||||
}
|
}
|
||||||
return RegularCase
|
return RegularCase
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckEnumForDuplicates will check an array of nodes to check if there are any duplicate values.
|
||||||
|
func CheckEnumForDuplicates(seq []*yaml.Node) []*yaml.Node {
|
||||||
|
var res []*yaml.Node
|
||||||
|
seen := make(map[string]*yaml.Node)
|
||||||
|
|
||||||
|
for _, enum := range seq {
|
||||||
|
if seen[enum.Value] != nil {
|
||||||
|
res = append(res, enum)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[enum.Value] = enum
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|||||||
@@ -659,3 +659,27 @@ func TestIsNodeRefValue_False(t *testing.T) {
|
|||||||
assert.Nil(t, node)
|
assert.Nil(t, node)
|
||||||
assert.Empty(t, val)
|
assert.Empty(t, val)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCheckEnumForDuplicates_Success(t *testing.T) {
|
||||||
|
yml := "- yes\n- no\n- crisps"
|
||||||
|
var rootNode yaml.Node
|
||||||
|
yaml.Unmarshal([]byte(yml), &rootNode)
|
||||||
|
assert.Len(t, CheckEnumForDuplicates(rootNode.Content[0].Content), 0)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckEnumForDuplicates_Fail(t *testing.T) {
|
||||||
|
yml := "- yes\n- no\n- crisps\n- no"
|
||||||
|
var rootNode yaml.Node
|
||||||
|
yaml.Unmarshal([]byte(yml), &rootNode)
|
||||||
|
assert.Len(t, CheckEnumForDuplicates(rootNode.Content[0].Content), 1)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckEnumForDuplicates_FailMultiple(t *testing.T) {
|
||||||
|
yml := "- yes\n- no\n- crisps\n- no\n- rice\n- yes\n- no"
|
||||||
|
|
||||||
|
var rootNode yaml.Node
|
||||||
|
yaml.Unmarshal([]byte(yml), &rootNode)
|
||||||
|
assert.Len(t, CheckEnumForDuplicates(rootNode.Content[0].Content), 3)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user