mirror of
https://github.com/LukeHagar/libopenapi.git
synced 2025-12-07 04:20:14 +00:00
I over use this package name, it's going to create all kinds of conflicts in my apps, so this name is going to be slightly different.
213 lines
6.8 KiB
Go
213 lines
6.8 KiB
Go
package datamodel
|
|
|
|
import (
|
|
_ "embed"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/daveshanley/vacuum/utils"
|
|
"gopkg.in/yaml.v3"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
OAS2 = "oas2"
|
|
OAS3 = "oas3"
|
|
OAS31 = "oas3_1"
|
|
)
|
|
|
|
//go:embed schemas/oas3-schema.json
|
|
var OpenAPI3SchemaData string
|
|
|
|
//go:embed schemas/swagger2-schema.json
|
|
var OpenAPI2SchemaData string
|
|
|
|
var OAS3_1Format = []string{OAS31}
|
|
var OAS3Format = []string{OAS3}
|
|
var OAS3AllFormat = []string{OAS3, OAS31}
|
|
var OAS2Format = []string{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 := parseVersionTypeData(openAPI3.Value)
|
|
|
|
// 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 := parseVersionTypeData(openAPI2.Value)
|
|
|
|
// 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 := parseVersionTypeData(asyncAPI.Value)
|
|
|
|
// 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) {
|
|
r := []rune(strings.TrimSpace(fmt.Sprintf("%v", d)))
|
|
return string(r), int(r[0]) - '0'
|
|
}
|
|
|
|
// 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
|
|
}
|