mirror of
https://github.com/LukeHagar/libopenapi.git
synced 2025-12-06 12:37:49 +00:00
slices of maps were not rendered properly. Now corrected, coverage continues to rise, refactoring node generation into utils package.
614 lines
15 KiB
Go
614 lines
15 KiB
Go
package utils
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/vmware-labs/yaml-jsonpath/pkg/yamlpath"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type Case int8
|
|
|
|
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"
|
|
|
|
PascalCase Case = iota
|
|
CamelCase
|
|
ScreamingSnakeCase
|
|
SnakeCase
|
|
KebabCase
|
|
ScreamingKebabCase
|
|
RegularCase
|
|
UnknownCase
|
|
)
|
|
|
|
// FindNodes will find a node based on JSONPath, it accepts raw yaml/json as input.
|
|
func FindNodes(yamlData []byte, jsonPath string) ([]*yaml.Node, error) {
|
|
jsonPath = FixContext(jsonPath)
|
|
|
|
var node yaml.Node
|
|
yaml.Unmarshal(yamlData, &node)
|
|
|
|
path, err := yamlpath.NewPath(jsonPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
results, _ := path.Find(&node)
|
|
return results, nil
|
|
}
|
|
|
|
func FindLastChildNode(node *yaml.Node) *yaml.Node {
|
|
s := len(node.Content) - 1
|
|
if s < 0 {
|
|
s = 0
|
|
}
|
|
if len(node.Content) > 0 && len(node.Content[s].Content) > 0 {
|
|
return FindLastChildNode(node.Content[s])
|
|
} else {
|
|
if len(node.Content) > 0 {
|
|
return node.Content[s]
|
|
}
|
|
return node
|
|
}
|
|
}
|
|
|
|
// BuildPath will construct a JSONPath from a base and an array of strings.
|
|
func BuildPath(basePath string, segs []string) string {
|
|
|
|
path := strings.Join(segs, ".")
|
|
|
|
// trim that last period.
|
|
if len(path) > 0 && path[len(path)-1] == '.' {
|
|
path = path[:len(path)-1]
|
|
}
|
|
return fmt.Sprintf("%s.%s", basePath, path)
|
|
}
|
|
|
|
// FindNodesWithoutDeserializing will find a node based on JSONPath, without deserializing from yaml/json
|
|
func FindNodesWithoutDeserializing(node *yaml.Node, jsonPath string) ([]*yaml.Node, error) {
|
|
jsonPath = FixContext(jsonPath)
|
|
|
|
path, err := yamlpath.NewPath(jsonPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
results, _ := path.Find(node)
|
|
return results, nil
|
|
}
|
|
|
|
// ConvertInterfaceIntoStringMap will convert an unknown input into a string map.
|
|
func ConvertInterfaceIntoStringMap(context interface{}) map[string]string {
|
|
converted := make(map[string]string)
|
|
if context != nil {
|
|
if v, ok := context.(map[string]interface{}); ok {
|
|
for k, n := range v {
|
|
if s, okB := n.(string); okB {
|
|
converted[k] = s
|
|
}
|
|
}
|
|
}
|
|
if v, ok := context.(map[string]string); ok {
|
|
for k, n := range v {
|
|
converted[k] = n
|
|
}
|
|
}
|
|
}
|
|
return converted
|
|
}
|
|
|
|
// ConvertInterfaceToStringArray will convert an unknown input map type into a string array/slice
|
|
func ConvertInterfaceToStringArray(raw interface{}) []string {
|
|
if vals, ok := raw.(map[string]interface{}); ok {
|
|
var s []string
|
|
for _, v := range vals {
|
|
if g, y := v.([]interface{}); y {
|
|
for _, q := range g {
|
|
s = append(s, fmt.Sprint(q))
|
|
}
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
if vals, ok := raw.(map[string][]string); ok {
|
|
var s []string
|
|
for _, v := range vals {
|
|
s = append(s, v...)
|
|
}
|
|
return s
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ConvertInterfaceArrayToStringArray will convert an unknown interface array type, into a string slice
|
|
func ConvertInterfaceArrayToStringArray(raw interface{}) []string {
|
|
if vals, ok := raw.([]interface{}); ok {
|
|
s := make([]string, len(vals))
|
|
for i, v := range vals {
|
|
s[i] = fmt.Sprint(v)
|
|
}
|
|
return s
|
|
}
|
|
if vals, ok := raw.([]string); ok {
|
|
return vals
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ExtractValueFromInterfaceMap pulls out an unknown value from a map using a string key
|
|
func ExtractValueFromInterfaceMap(name string, raw interface{}) interface{} {
|
|
|
|
if propMap, ok := raw.(map[string]interface{}); ok {
|
|
if props, okn := propMap[name].([]interface{}); okn {
|
|
return props
|
|
} else {
|
|
return propMap[name]
|
|
}
|
|
}
|
|
if propMap, ok := raw.(map[string][]string); ok {
|
|
return propMap[name]
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// FindFirstKeyNode will locate the first key and value yaml.Node based on a key.
|
|
func FindFirstKeyNode(key string, nodes []*yaml.Node, depth int) (keyNode *yaml.Node, valueNode *yaml.Node) {
|
|
if depth > 40 {
|
|
return nil, nil
|
|
}
|
|
for i, v := range nodes {
|
|
if key != "" && key == v.Value {
|
|
if i+1 >= len(nodes) {
|
|
return v, nodes[i] // this is the node we need.
|
|
}
|
|
return v, nodes[i+1] // next node is what we need.
|
|
}
|
|
if len(v.Content) > 0 {
|
|
depth++
|
|
x, y := FindFirstKeyNode(key, v.Content, depth)
|
|
if x != nil && y != nil {
|
|
return x, y
|
|
}
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// KeyNodeResult is a result from a KeyNodeSearch performed by the FindAllKeyNodesWithPath
|
|
type KeyNodeResult struct {
|
|
KeyNode *yaml.Node
|
|
ValueNode *yaml.Node
|
|
Parent *yaml.Node
|
|
Path []yaml.Node
|
|
}
|
|
|
|
// KeyNodeSearch keeps a track of everything we have found on our adventure down the trees.
|
|
type KeyNodeSearch struct {
|
|
Key string
|
|
Ignore []string
|
|
Results []*KeyNodeResult
|
|
AllowExtensions bool
|
|
}
|
|
|
|
// FindKeyNodeTop is a non-recursive search of top level nodes for a key, will not look at content.
|
|
// Returns the key and value
|
|
func FindKeyNodeTop(key string, nodes []*yaml.Node) (keyNode *yaml.Node, valueNode *yaml.Node) {
|
|
for i, v := range nodes {
|
|
if i%2 != 0 {
|
|
continue
|
|
}
|
|
if strings.ToLower(key) == strings.ToLower(v.Value) {
|
|
return v, nodes[i+1] // next node is what we need.
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// FindKeyNode is a non-recursive search of a *yaml.Node Content for a child node with a key.
|
|
// Returns the key and value
|
|
func FindKeyNode(key string, nodes []*yaml.Node) (keyNode *yaml.Node, valueNode *yaml.Node) {
|
|
|
|
//numNodes := len(nodes)
|
|
for i, v := range nodes {
|
|
if i%2 == 0 && key == v.Value {
|
|
return v, nodes[i+1] // next node is what we need.
|
|
}
|
|
for x, j := range v.Content {
|
|
if key == j.Value {
|
|
if IsNodeMap(v) {
|
|
if x+1 == len(v.Content) {
|
|
return v, v.Content[x]
|
|
}
|
|
return v, v.Content[x+1] // next node is what we need.
|
|
|
|
}
|
|
if IsNodeArray(v) {
|
|
return v, v.Content[x]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// FindKeyNodeFull is an overloaded version of FindKeyNode. This version however returns keys, labels and values.
|
|
// generally different things are required from different node trees, so depending on what this function is looking at
|
|
// it will return different things.
|
|
func FindKeyNodeFull(key string, nodes []*yaml.Node) (keyNode *yaml.Node, labelNode *yaml.Node, valueNode *yaml.Node) {
|
|
for i := range nodes {
|
|
if i%2 == 0 && key == nodes[i].Value {
|
|
return nodes[i], nodes[i], nodes[i+1] // next node is what we need.
|
|
}
|
|
}
|
|
for _, v := range nodes {
|
|
for x := range v.Content {
|
|
if key == v.Content[x].Value {
|
|
if IsNodeMap(v) {
|
|
if x+1 == len(v.Content) {
|
|
return v, v.Content[x], v.Content[x]
|
|
}
|
|
return v, v.Content[x], v.Content[x+1]
|
|
}
|
|
if IsNodeArray(v) {
|
|
return v, v.Content[x], v.Content[x]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil, nil, nil
|
|
}
|
|
|
|
// FindKeyNodeFullTop is an overloaded version of FindKeyNodeFull. This version only looks at the top
|
|
// level of the node and not the children.
|
|
func FindKeyNodeFullTop(key string, nodes []*yaml.Node) (keyNode *yaml.Node, labelNode *yaml.Node, valueNode *yaml.Node) {
|
|
for i := range nodes {
|
|
if i%2 != 0 {
|
|
continue
|
|
}
|
|
if i%2 == 0 && key == nodes[i].Value {
|
|
return nodes[i], nodes[i], nodes[i+1] // next node is what we need.
|
|
}
|
|
}
|
|
return nil, nil, nil
|
|
}
|
|
|
|
type ExtensionNode struct {
|
|
Key *yaml.Node
|
|
Value *yaml.Node
|
|
}
|
|
|
|
func FindExtensionNodes(nodes []*yaml.Node) []*ExtensionNode {
|
|
var extensions []*ExtensionNode
|
|
for i, v := range nodes {
|
|
if i%2 == 0 && strings.HasPrefix(v.Value, "x-") {
|
|
if i+1 < len(nodes) {
|
|
extensions = append(extensions, &ExtensionNode{
|
|
Key: v,
|
|
Value: nodes[i+1],
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return extensions
|
|
}
|
|
|
|
var ObjectLabel = "object"
|
|
var IntegerLabel = "integer"
|
|
var NumberLabel = "number"
|
|
var StringLabel = "string"
|
|
var BinaryLabel = "binary"
|
|
var ArrayLabel = "array"
|
|
var BooleanLabel = "boolean"
|
|
var SchemaSource = "https://json-schema.org/draft/2020-12/schema"
|
|
var SchemaId = "https://pb33f.io/openapi-changes/schema"
|
|
|
|
func MakeTagReadable(node *yaml.Node) string {
|
|
switch node.Tag {
|
|
case "!!map":
|
|
return ObjectLabel
|
|
case "!!seq":
|
|
return ArrayLabel
|
|
case "!!str":
|
|
return StringLabel
|
|
case "!!int":
|
|
return IntegerLabel
|
|
case "!!float":
|
|
return NumberLabel
|
|
case "!!bool":
|
|
return BooleanLabel
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
// IsNodeMap checks if the node is a map type
|
|
func IsNodeMap(node *yaml.Node) bool {
|
|
if node == nil {
|
|
return false
|
|
}
|
|
return node.Tag == "!!map"
|
|
}
|
|
|
|
// IsNodePolyMorphic will return true if the node contains polymorphic keys.
|
|
func IsNodePolyMorphic(node *yaml.Node) bool {
|
|
for i, v := range node.Content {
|
|
if i%2 == 0 {
|
|
if v.Value == "anyOf" || v.Value == "oneOf" || v.Value == "allOf" {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsNodeArray checks if a node is an array type
|
|
func IsNodeArray(node *yaml.Node) bool {
|
|
if node == nil {
|
|
return false
|
|
}
|
|
return node.Tag == "!!seq"
|
|
}
|
|
|
|
// IsNodeStringValue checks if a node is a string value
|
|
func IsNodeStringValue(node *yaml.Node) bool {
|
|
if node == nil {
|
|
return false
|
|
}
|
|
return node.Tag == "!!str"
|
|
}
|
|
|
|
// IsNodeIntValue will check if a node is an int value
|
|
func IsNodeIntValue(node *yaml.Node) bool {
|
|
if node == nil {
|
|
return false
|
|
}
|
|
return node.Tag == "!!int"
|
|
}
|
|
|
|
// IsNodeFloatValue will check is a node is a float value.
|
|
func IsNodeFloatValue(node *yaml.Node) bool {
|
|
if node == nil {
|
|
return false
|
|
}
|
|
return node.Tag == "!!float"
|
|
}
|
|
|
|
// IsNodeBoolValue will check is a node is a bool
|
|
func IsNodeBoolValue(node *yaml.Node) bool {
|
|
if node == nil {
|
|
return false
|
|
}
|
|
return node.Tag == "!!bool"
|
|
}
|
|
|
|
func IsNodeRefValue(node *yaml.Node) (bool, *yaml.Node, string) {
|
|
for i, r := range node.Content {
|
|
if i%2 == 0 {
|
|
if r.Value == "$ref" {
|
|
return true, r, node.Content[i+1].Value
|
|
}
|
|
}
|
|
}
|
|
return false, nil, ""
|
|
}
|
|
|
|
// FixContext will clean up a JSONpath string to be correctly traversable.
|
|
func FixContext(context string) string {
|
|
tokens := strings.Split(context, ".")
|
|
var cleaned = []string{}
|
|
|
|
for i, t := range tokens {
|
|
if v, err := strconv.Atoi(t); err == nil {
|
|
if v < 200 { // codes start here
|
|
if cleaned[i-1] != "" {
|
|
cleaned[i-1] += fmt.Sprintf("[%v]", t)
|
|
}
|
|
} else {
|
|
cleaned = append(cleaned, t)
|
|
}
|
|
continue
|
|
}
|
|
cleaned = append(cleaned, strings.ReplaceAll(t, "(root)", "$"))
|
|
}
|
|
|
|
return strings.Join(cleaned, ".")
|
|
}
|
|
|
|
// IsJSON will tell you if a string is JSON or not.
|
|
func IsJSON(testString string) bool {
|
|
if testString == "" {
|
|
return false
|
|
}
|
|
runes := []rune(strings.TrimSpace(testString))
|
|
if runes[0] == '{' && runes[len(runes)-1] == '}' {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsYAML will tell you if a string is YAML or not.
|
|
func IsYAML(testString string) bool {
|
|
if testString == "" {
|
|
return false
|
|
}
|
|
if IsJSON(testString) {
|
|
return false
|
|
}
|
|
var n interface{}
|
|
err := yaml.Unmarshal([]byte(testString), &n)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
_, err = yaml.Marshal(n)
|
|
return err == nil
|
|
}
|
|
|
|
// ConvertYAMLtoJSON will do exactly what you think it will. It will deserialize YAML into serialized JSON.
|
|
func ConvertYAMLtoJSON(yamlData []byte) ([]byte, error) {
|
|
var decodedYaml map[string]interface{}
|
|
err := yaml.Unmarshal(yamlData, &decodedYaml)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// if the data can be decoded, it can be encoded (that's my view anyway). no need for an error check.
|
|
jsonData, _ := json.Marshal(decodedYaml)
|
|
return jsonData, nil
|
|
}
|
|
|
|
// IsHttpVerb will check if an operation is valid or not.
|
|
func IsHttpVerb(verb string) bool {
|
|
verbs := []string{"get", "post", "put", "patch", "delete", "options", "trace", "head"}
|
|
for _, v := range verbs {
|
|
if verb == v {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func ConvertComponentIdIntoFriendlyPathSearch(id string) (string, string) {
|
|
segs := strings.Split(id, "/")
|
|
name, _ := url.QueryUnescape(strings.ReplaceAll(segs[len(segs)-1], "~1", "/"))
|
|
var cleaned []string
|
|
|
|
// check for strange spaces, chars and if found, wrap them up, clean them and create a new cleaned path.
|
|
for i := range segs {
|
|
reg, _ := regexp.MatchString("[%=;~.]", segs[i])
|
|
if reg {
|
|
segs[i], _ = url.QueryUnescape(strings.ReplaceAll(segs[i], "~1", "/"))
|
|
segs[i] = fmt.Sprintf("['%s']", segs[i])
|
|
if len(cleaned) > 0 {
|
|
cleaned[len(cleaned)-1] = fmt.Sprintf("%s%s", segs[i-1], segs[i])
|
|
continue
|
|
}
|
|
} else {
|
|
intVal, err := strconv.ParseInt(segs[i], 10, 32)
|
|
if err == nil && intVal <= 99 {
|
|
segs[i] = fmt.Sprintf("[%d]", intVal)
|
|
cleaned[len(cleaned)-1] = fmt.Sprintf("%s%s", cleaned[len(cleaned)-1], segs[i])
|
|
continue
|
|
}
|
|
if err == nil && intVal > 99 {
|
|
segs[i] = fmt.Sprintf("['%d']", intVal)
|
|
cleaned[len(cleaned)-1] = fmt.Sprintf("%s%s", cleaned[len(cleaned)-1], segs[i])
|
|
continue
|
|
}
|
|
cleaned = append(cleaned, segs[i])
|
|
}
|
|
}
|
|
_, err := strconv.ParseInt(name, 10, 32)
|
|
var replaced string
|
|
if err != nil {
|
|
replaced = strings.ReplaceAll(fmt.Sprintf("%s",
|
|
strings.Join(cleaned, ".")), "#", "$")
|
|
} else {
|
|
replaced = strings.ReplaceAll(fmt.Sprintf("%s",
|
|
strings.Join(cleaned, ".")), "#", "$")
|
|
}
|
|
|
|
if len(replaced) > 0 {
|
|
if replaced[0] != '$' {
|
|
replaced = fmt.Sprintf("$%s", replaced)
|
|
}
|
|
}
|
|
return name, replaced
|
|
}
|
|
|
|
func ConvertComponentIdIntoPath(id string) (string, string) {
|
|
segs := strings.Split(id, "/")
|
|
name := segs[len(segs)-1]
|
|
|
|
return name, strings.ReplaceAll(fmt.Sprintf("%s.%s",
|
|
strings.Join(segs[:len(segs)-1], "."), name), "#", "$")
|
|
}
|
|
|
|
func RenderCodeSnippet(startNode *yaml.Node, specData []string, before, after int) string {
|
|
|
|
buf := new(strings.Builder)
|
|
|
|
startLine := startNode.Line - before
|
|
endLine := startNode.Line + after
|
|
|
|
if startLine < 0 {
|
|
startLine = 0
|
|
}
|
|
|
|
if endLine >= len(specData) {
|
|
endLine = len(specData) - 1
|
|
}
|
|
|
|
delta := endLine - startLine
|
|
|
|
for i := 0; i < delta; i++ {
|
|
l := startLine + i
|
|
if l < len(specData) {
|
|
line := specData[l]
|
|
buf.WriteString(fmt.Sprintf("%s\n", line))
|
|
}
|
|
}
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
func DetectCase(input string) Case {
|
|
trim := strings.TrimSpace(input)
|
|
if trim == "" {
|
|
return UnknownCase
|
|
}
|
|
|
|
pascalCase := regexp.MustCompile("^[A-Z][a-z]+(?:[A-Z][a-z]+)*$")
|
|
camelCase := regexp.MustCompile("^[a-z]+(?:[A-Z][a-z]+)*$")
|
|
screamingSnakeCase := regexp.MustCompile("^[A-Z]+(_[A-Z]+)*$")
|
|
snakeCase := regexp.MustCompile("^[a-z]+(_[a-z]+)*$")
|
|
kebabCase := regexp.MustCompile("^[a-z]+(-[a-z]+)*$")
|
|
screamingKebabCase := regexp.MustCompile("^[A-Z]+(-[A-Z]+)*$")
|
|
if pascalCase.MatchString(trim) {
|
|
return PascalCase
|
|
}
|
|
if camelCase.MatchString(trim) {
|
|
return CamelCase
|
|
}
|
|
if screamingSnakeCase.MatchString(trim) {
|
|
return ScreamingSnakeCase
|
|
}
|
|
if snakeCase.MatchString(trim) {
|
|
return SnakeCase
|
|
}
|
|
if kebabCase.MatchString(trim) {
|
|
return KebabCase
|
|
}
|
|
if screamingKebabCase.MatchString(trim) {
|
|
return ScreamingKebabCase
|
|
}
|
|
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
|
|
}
|
|
|
|
|
|
|