diff --git a/datamodel/low/3.0/document.go b/datamodel/low/3.0/document.go index aec5c67..c9ffc9d 100644 --- a/datamodel/low/3.0/document.go +++ b/datamodel/low/3.0/document.go @@ -1,17 +1,17 @@ package v3 import ( - "github.com/pb33f/libopenapi/datamodel/low" + "github.com/pb33f/libopenapi/datamodel/low" ) type Document struct { - Version low.NodeReference[string] - Info low.NodeReference[*Info] - Servers []low.NodeReference[*Server] - Paths *Paths - Components *Components - Security []*SecurityRequirement - Tags []*Tag - ExternalDocs *ExternalDoc - Extensions map[string]low.ObjectReference + Version low.NodeReference[string] + Info low.NodeReference[*Info] + Servers []low.NodeReference[*Server] + Paths *Paths + Components *Components + Security []*SecurityRequirement + Tags []low.NodeReference[*Tag] + ExternalDocs *ExternalDoc + Extensions map[string]low.ObjectReference } diff --git a/datamodel/low/3.0/external_doc.go b/datamodel/low/3.0/external_doc.go index 687ca20..97222e6 100644 --- a/datamodel/low/3.0/external_doc.go +++ b/datamodel/low/3.0/external_doc.go @@ -1,13 +1,11 @@ package v3 import ( - "github.com/pb33f/libopenapi/datamodel/low" - "gopkg.in/yaml.v3" + "github.com/pb33f/libopenapi/datamodel/low" ) type ExternalDoc struct { - Node *yaml.Node - Description low.NodeReference[string] - URL low.NodeReference[string] - Extensions map[string]low.ObjectReference + Description low.NodeReference[string] + URL low.NodeReference[string] + Extensions map[string]low.ObjectReference } diff --git a/datamodel/low/3.0/server.go b/datamodel/low/3.0/server.go index 7fa7417..5fd6994 100644 --- a/datamodel/low/3.0/server.go +++ b/datamodel/low/3.0/server.go @@ -1,49 +1,53 @@ package v3 import ( - "github.com/pb33f/libopenapi/datamodel" - "github.com/pb33f/libopenapi/datamodel/low" - "github.com/pb33f/libopenapi/utils" - "gopkg.in/yaml.v3" + "github.com/pb33f/libopenapi/datamodel" + "github.com/pb33f/libopenapi/datamodel/low" + "github.com/pb33f/libopenapi/utils" + "gopkg.in/yaml.v3" +) + +const ( + Variables = "variables" ) type Server struct { - URL low.NodeReference[string] - Description low.NodeReference[string] - Variables low.NodeReference[*map[string]low.NodeReference[*ServerVariable]] + URL low.NodeReference[string] + Description low.NodeReference[string] + Variables low.NodeReference[map[string]low.NodeReference[*ServerVariable]] } func (s *Server) Build(root *yaml.Node) error { - kn, vars := utils.FindKeyNode("variables", root.Content) - if vars == nil { - return nil - } - variablesMap := make(map[string]low.NodeReference[*ServerVariable]) - if utils.IsNodeMap(vars) { - var currentNode string - var keyNode *yaml.Node - for i, varNode := range vars.Content { - if i%2 == 0 { - currentNode = varNode.Value - keyNode = varNode - continue - } - variable := ServerVariable{} - err := datamodel.BuildModel(varNode, &variable) - if err != nil { - return err - } - variablesMap[currentNode] = low.NodeReference[*ServerVariable]{ - ValueNode: varNode, - KeyNode: keyNode, - Value: &variable, - } - } - s.Variables = low.NodeReference[*map[string]low.NodeReference[*ServerVariable]]{ - KeyNode: kn, - ValueNode: vars, - Value: &variablesMap, - } - } - return nil + kn, vars := utils.FindKeyNode(Variables, root.Content) + if vars == nil { + return nil + } + variablesMap := make(map[string]low.NodeReference[*ServerVariable]) + if utils.IsNodeMap(vars) { + var currentNode string + var keyNode *yaml.Node + for i, varNode := range vars.Content { + if i%2 == 0 { + currentNode = varNode.Value + keyNode = varNode + continue + } + variable := ServerVariable{} + err := datamodel.BuildModel(varNode, &variable) + if err != nil { + return err + } + variablesMap[currentNode] = low.NodeReference[*ServerVariable]{ + ValueNode: varNode, + KeyNode: keyNode, + Value: &variable, + } + } + s.Variables = low.NodeReference[map[string]low.NodeReference[*ServerVariable]]{ + KeyNode: kn, + ValueNode: vars, + Value: variablesMap, + } + } + return nil } diff --git a/datamodel/low/3.0/tag.go b/datamodel/low/3.0/tag.go index 31d4fa4..1d214f4 100644 --- a/datamodel/low/3.0/tag.go +++ b/datamodel/low/3.0/tag.go @@ -1,14 +1,44 @@ package v3 import ( - "github.com/pb33f/libopenapi/datamodel/low" - "gopkg.in/yaml.v3" + "github.com/pb33f/libopenapi/datamodel" + "github.com/pb33f/libopenapi/datamodel/low" + "github.com/pb33f/libopenapi/utils" + "gopkg.in/yaml.v3" +) + +const ( + Tags = "tags" + ExternalDocs = "externalDocs" ) type Tag struct { - Node *yaml.Node - Name low.NodeReference[string] - Description low.NodeReference[string] - ExternalDocs ExternalDoc - Extensions map[string]low.ObjectReference + Name low.NodeReference[string] + Description low.NodeReference[string] + ExternalDocs low.NodeReference[*ExternalDoc] + Extensions map[low.NodeReference[string]]low.NodeReference[any] +} + +func (t *Tag) Build(root *yaml.Node) error { + _, ln, exDocs := utils.FindKeyNodeFull(ExternalDocs, root.Content) + + // extract extensions + extensionMap, err := datamodel.ExtractExtensions(root) + if err != nil { + return err + } + t.Extensions = extensionMap + + // extract external docs + var externalDoc ExternalDoc + err = datamodel.BuildModel(exDocs, &externalDoc) + if err != nil { + return err + } + t.ExternalDocs = low.NodeReference[*ExternalDoc]{ + Value: &externalDoc, + KeyNode: ln, + ValueNode: exDocs, + } + return nil } diff --git a/datamodel/low/reference.go b/datamodel/low/reference.go index 6f7795a..58f41e6 100644 --- a/datamodel/low/reference.go +++ b/datamodel/low/reference.go @@ -3,21 +3,21 @@ package low import "gopkg.in/yaml.v3" type HasNode interface { - GetNode() *yaml.Node + GetNode() *yaml.Node } type Buildable interface { - Build(node *yaml.Node) error + Build(node *yaml.Node) error } type NodeReference[T any] struct { - Value T - ValueNode *yaml.Node - KeyNode *yaml.Node + Value T + ValueNode *yaml.Node + KeyNode *yaml.Node } type ObjectReference struct { - Value map[string]interface{} - ValueNode *yaml.Node - KeyNode *yaml.Node + Value interface{} + ValueNode *yaml.Node + KeyNode *yaml.Node } diff --git a/datamodel/model_builder.go b/datamodel/model_builder.go index 4f7ef85..3a2d395 100644 --- a/datamodel/model_builder.go +++ b/datamodel/model_builder.go @@ -1,354 +1,411 @@ package datamodel import ( - "fmt" - "github.com/pb33f/libopenapi/datamodel/low" - "github.com/pb33f/libopenapi/utils" - "gopkg.in/yaml.v3" - "reflect" - "strconv" - "sync" + "fmt" + "github.com/pb33f/libopenapi/datamodel/low" + "github.com/pb33f/libopenapi/utils" + "gopkg.in/yaml.v3" + "reflect" + "strconv" + "sync" ) func BuildModel(node *yaml.Node, model interface{}) error { - if reflect.ValueOf(model).Type().Kind() != reflect.Pointer { - return fmt.Errorf("cannot build model on non-pointer: %v", reflect.ValueOf(model).Type().Kind()) - } - v := reflect.ValueOf(model).Elem() - for i := 0; i < v.NumField(); i++ { + if reflect.ValueOf(model).Type().Kind() != reflect.Pointer { + return fmt.Errorf("cannot build model on non-pointer: %v", reflect.ValueOf(model).Type().Kind()) + } + v := reflect.ValueOf(model).Elem() + for i := 0; i < v.NumField(); i++ { - fName := v.Type().Field(i).Name + fName := v.Type().Field(i).Name - // we need to find a matching field in the YAML, the cases may be off, so take no chances. - cases := []utils.Case{utils.PascalCase, utils.CamelCase, utils.ScreamingSnakeCase, - utils.SnakeCase, utils.KebabCase, utils.RegularCase} + // we need to find a matching field in the YAML, the cases may be off, so take no chances. + cases := []utils.Case{utils.PascalCase, utils.CamelCase, utils.ScreamingSnakeCase, + utils.SnakeCase, utils.KebabCase, utils.RegularCase} - var vn, kn *yaml.Node - for _, tryCase := range cases { - kn, vn = utils.FindKeyNode(utils.ConvertCase(fName, tryCase), node.Content) - if vn != nil { - break - } - } + var vn, kn *yaml.Node + for _, tryCase := range cases { + kn, vn = utils.FindKeyNode(utils.ConvertCase(fName, tryCase), node.Content) + if vn != nil { + break + } + } - if vn == nil { - // no point in going on. - continue - } + if vn == nil { + // no point in going on. + continue + } - field := v.FieldByName(fName) - kind := field.Kind() - switch kind { - case reflect.Struct, reflect.Slice, reflect.Map, reflect.Pointer: - err := SetField(field, vn, kn) - if err != nil { - return err - } - default: - return fmt.Errorf("unable to parse unsupported type: %v", kind) - } + field := v.FieldByName(fName) + kind := field.Kind() + switch kind { + case reflect.Struct, reflect.Slice, reflect.Map, reflect.Pointer: + err := SetField(field, vn, kn) + if err != nil { + return err + } + default: + return fmt.Errorf("unable to parse unsupported type: %v", kind) + } - } + } - return nil + return nil } func SetField(field reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) error { - switch field.Type() { + switch field.Type() { - case reflect.TypeOf(map[string]low.ObjectReference{}): - if valueNode != nil { - if utils.IsNodeMap(valueNode) { - if field.CanSet() { - items := make(map[string]low.ObjectReference) - var currentLabel string - for i, sliceItem := range valueNode.Content { - if i%2 == 0 { - currentLabel = sliceItem.Value - continue - } - var decoded map[string]interface{} - err := sliceItem.Decode(&decoded) - if err != nil { - return err - } - items[currentLabel] = low.ObjectReference{ - Value: decoded, - ValueNode: sliceItem, - KeyNode: valueNode, - } - } - field.Set(reflect.ValueOf(items)) - } - } - } - break + case reflect.TypeOf(map[string]low.ObjectReference{}): + if valueNode != nil { + if utils.IsNodeMap(valueNode) { + if field.CanSet() { + items := make(map[string]low.ObjectReference) + var currentLabel string + for i, sliceItem := range valueNode.Content { + if i%2 == 0 { + currentLabel = sliceItem.Value + continue + } + var decoded map[string]interface{} + err := sliceItem.Decode(&decoded) + if err != nil { + return err + } + items[currentLabel] = low.ObjectReference{ + Value: decoded, + ValueNode: sliceItem, + KeyNode: valueNode, + } + } + field.Set(reflect.ValueOf(items)) + } + } + } + break - case reflect.TypeOf(map[string]low.NodeReference[string]{}): - if valueNode != nil { - if utils.IsNodeMap(valueNode) { - if field.CanSet() { - items := make(map[string]low.NodeReference[string]) - var currentLabel string - for i, sliceItem := range valueNode.Content { - if i%2 == 0 { - currentLabel = sliceItem.Value - continue - } - items[currentLabel] = low.NodeReference[string]{ - Value: sliceItem.Value, - ValueNode: sliceItem, - KeyNode: valueNode, - } - } - field.Set(reflect.ValueOf(items)) - } - } - } - break - case reflect.TypeOf(low.ObjectReference{}): - if valueNode != nil { - var decoded map[string]interface{} - err := valueNode.Decode(&decoded) - if err != nil { - return err - } - if utils.IsNodeMap(valueNode) { - if field.CanSet() { - or := low.ObjectReference{Value: decoded, ValueNode: valueNode} - field.Set(reflect.ValueOf(or)) - } - } - } - break - case reflect.TypeOf([]low.ObjectReference{}): - if valueNode != nil { - if utils.IsNodeArray(valueNode) { - if field.CanSet() { - var items []low.ObjectReference - for _, sliceItem := range valueNode.Content { - var decoded map[string]interface{} - err := sliceItem.Decode(&decoded) - if err != nil { - return err - } - items = append(items, low.ObjectReference{ - Value: decoded, - ValueNode: sliceItem, - KeyNode: valueNode, - }) - } - field.Set(reflect.ValueOf(items)) - } - } - } - break - case reflect.TypeOf(low.NodeReference[string]{}): - if valueNode != nil { - if utils.IsNodeStringValue(valueNode) { - if field.CanSet() { - nr := low.NodeReference[string]{ - Value: valueNode.Value, - ValueNode: valueNode, - KeyNode: keyNode, - } - field.Set(reflect.ValueOf(nr)) - } - } - } - break - case reflect.TypeOf(low.NodeReference[bool]{}): - if valueNode != nil { - if utils.IsNodeBoolValue(valueNode) { - if field.CanSet() { - bv, _ := strconv.ParseBool(valueNode.Value) - nr := low.NodeReference[bool]{ - Value: bv, - ValueNode: valueNode, - KeyNode: keyNode, - } - field.Set(reflect.ValueOf(nr)) - } - } - } - break - case reflect.TypeOf(low.NodeReference[int]{}): - if valueNode != nil { - if utils.IsNodeIntValue(valueNode) { - if field.CanSet() { - fv, _ := strconv.Atoi(valueNode.Value) - nr := low.NodeReference[int]{ - Value: fv, - ValueNode: valueNode, - KeyNode: keyNode, - } - field.Set(reflect.ValueOf(nr)) - } - } - } - break - case reflect.TypeOf(low.NodeReference[int64]{}): - if valueNode != nil { - if utils.IsNodeIntValue(valueNode) || utils.IsNodeFloatValue(valueNode) { // - if field.CanSet() { - fv, _ := strconv.ParseInt(valueNode.Value, 10, 64) - nr := low.NodeReference[int64]{ - Value: fv, - ValueNode: valueNode, - KeyNode: keyNode, - } - field.Set(reflect.ValueOf(nr)) - } - } - } - break - case reflect.TypeOf(low.NodeReference[float32]{}): - if valueNode != nil { - if utils.IsNodeFloatValue(valueNode) { - if field.CanSet() { - fv, _ := strconv.ParseFloat(valueNode.Value, 32) - nr := low.NodeReference[float32]{ - Value: float32(fv), - ValueNode: valueNode, - KeyNode: keyNode, - } - field.Set(reflect.ValueOf(nr)) - } - } - } - break - case reflect.TypeOf(low.NodeReference[float64]{}): - if valueNode != nil { - if utils.IsNodeFloatValue(valueNode) { - if field.CanSet() { - fv, _ := strconv.ParseFloat(valueNode.Value, 64) - nr := low.NodeReference[float64]{ - Value: fv, - ValueNode: valueNode, - KeyNode: keyNode, - } - field.Set(reflect.ValueOf(nr)) - } - } - } - break - case reflect.TypeOf([]low.NodeReference[string]{}): - if valueNode != nil { - if utils.IsNodeArray(valueNode) { - if field.CanSet() { - var items []low.NodeReference[string] - for _, sliceItem := range valueNode.Content { - items = append(items, low.NodeReference[string]{ - Value: sliceItem.Value, - ValueNode: sliceItem, - KeyNode: valueNode, - }) - } - field.Set(reflect.ValueOf(items)) - } - } - } - break - case reflect.TypeOf([]low.NodeReference[float32]{}): - if valueNode != nil { - if utils.IsNodeArray(valueNode) { - if field.CanSet() { - var items []low.NodeReference[float32] - for _, sliceItem := range valueNode.Content { - fv, _ := strconv.ParseFloat(sliceItem.Value, 32) - items = append(items, low.NodeReference[float32]{ - Value: float32(fv), - ValueNode: sliceItem, - KeyNode: valueNode, - }) - } - field.Set(reflect.ValueOf(items)) - } - } - } - break - case reflect.TypeOf([]low.NodeReference[float64]{}): - if valueNode != nil { - if utils.IsNodeArray(valueNode) { - if field.CanSet() { - var items []low.NodeReference[float64] - for _, sliceItem := range valueNode.Content { - fv, _ := strconv.ParseFloat(sliceItem.Value, 64) - items = append(items, low.NodeReference[float64]{Value: fv, ValueNode: sliceItem}) - } - field.Set(reflect.ValueOf(items)) - } - } - } - break - case reflect.TypeOf([]low.NodeReference[int]{}): - if valueNode != nil { - if utils.IsNodeArray(valueNode) { - if field.CanSet() { - var items []low.NodeReference[int] - for _, sliceItem := range valueNode.Content { - iv, _ := strconv.Atoi(sliceItem.Value) - items = append(items, low.NodeReference[int]{ - Value: iv, - ValueNode: sliceItem, - KeyNode: valueNode, - }) - } - field.Set(reflect.ValueOf(items)) - } - } - } - break - case reflect.TypeOf([]low.NodeReference[int64]{}): - if valueNode != nil { - if utils.IsNodeArray(valueNode) { - if field.CanSet() { - var items []low.NodeReference[int64] - for _, sliceItem := range valueNode.Content { - iv, _ := strconv.ParseInt(sliceItem.Value, 10, 64) - items = append(items, low.NodeReference[int64]{ - Value: iv, - ValueNode: sliceItem, - KeyNode: valueNode, - }) - } - field.Set(reflect.ValueOf(items)) - } - } - } - break - case reflect.TypeOf([]low.NodeReference[bool]{}): - if valueNode != nil { - if utils.IsNodeArray(valueNode) { - if field.CanSet() { - var items []low.NodeReference[bool] - for _, sliceItem := range valueNode.Content { - bv, _ := strconv.ParseBool(sliceItem.Value) - items = append(items, low.NodeReference[bool]{ - Value: bv, - ValueNode: sliceItem, - KeyNode: valueNode, - }) - } - field.Set(reflect.ValueOf(items)) - } - } - } - break - default: - // we want to ignore everything else. - break - } - return nil + case reflect.TypeOf(map[string]low.NodeReference[string]{}): + if valueNode != nil { + if utils.IsNodeMap(valueNode) { + if field.CanSet() { + items := make(map[string]low.NodeReference[string]) + var currentLabel string + for i, sliceItem := range valueNode.Content { + if i%2 == 0 { + currentLabel = sliceItem.Value + continue + } + items[currentLabel] = low.NodeReference[string]{ + Value: sliceItem.Value, + ValueNode: sliceItem, + KeyNode: valueNode, + } + } + field.Set(reflect.ValueOf(items)) + } + } + } + break + case reflect.TypeOf(low.ObjectReference{}): + if valueNode != nil { + var decoded map[string]interface{} + err := valueNode.Decode(&decoded) + if err != nil { + return err + } + if utils.IsNodeMap(valueNode) { + if field.CanSet() { + or := low.ObjectReference{Value: decoded, ValueNode: valueNode} + field.Set(reflect.ValueOf(or)) + } + } + } + break + case reflect.TypeOf([]low.ObjectReference{}): + if valueNode != nil { + if utils.IsNodeArray(valueNode) { + if field.CanSet() { + var items []low.ObjectReference + for _, sliceItem := range valueNode.Content { + var decoded map[string]interface{} + err := sliceItem.Decode(&decoded) + if err != nil { + return err + } + items = append(items, low.ObjectReference{ + Value: decoded, + ValueNode: sliceItem, + KeyNode: valueNode, + }) + } + field.Set(reflect.ValueOf(items)) + } + } + } + break + case reflect.TypeOf(low.NodeReference[string]{}): + if valueNode != nil { + if utils.IsNodeStringValue(valueNode) { + if field.CanSet() { + nr := low.NodeReference[string]{ + Value: valueNode.Value, + ValueNode: valueNode, + KeyNode: keyNode, + } + field.Set(reflect.ValueOf(nr)) + } + } + } + break + case reflect.TypeOf(low.NodeReference[bool]{}): + if valueNode != nil { + if utils.IsNodeBoolValue(valueNode) { + if field.CanSet() { + bv, _ := strconv.ParseBool(valueNode.Value) + nr := low.NodeReference[bool]{ + Value: bv, + ValueNode: valueNode, + KeyNode: keyNode, + } + field.Set(reflect.ValueOf(nr)) + } + } + } + break + case reflect.TypeOf(low.NodeReference[int]{}): + if valueNode != nil { + if utils.IsNodeIntValue(valueNode) { + if field.CanSet() { + fv, _ := strconv.Atoi(valueNode.Value) + nr := low.NodeReference[int]{ + Value: fv, + ValueNode: valueNode, + KeyNode: keyNode, + } + field.Set(reflect.ValueOf(nr)) + } + } + } + break + case reflect.TypeOf(low.NodeReference[int64]{}): + if valueNode != nil { + if utils.IsNodeIntValue(valueNode) || utils.IsNodeFloatValue(valueNode) { // + if field.CanSet() { + fv, _ := strconv.ParseInt(valueNode.Value, 10, 64) + nr := low.NodeReference[int64]{ + Value: fv, + ValueNode: valueNode, + KeyNode: keyNode, + } + field.Set(reflect.ValueOf(nr)) + } + } + } + break + case reflect.TypeOf(low.NodeReference[float32]{}): + if valueNode != nil { + if utils.IsNodeFloatValue(valueNode) { + if field.CanSet() { + fv, _ := strconv.ParseFloat(valueNode.Value, 32) + nr := low.NodeReference[float32]{ + Value: float32(fv), + ValueNode: valueNode, + KeyNode: keyNode, + } + field.Set(reflect.ValueOf(nr)) + } + } + } + break + case reflect.TypeOf(low.NodeReference[float64]{}): + if valueNode != nil { + if utils.IsNodeFloatValue(valueNode) { + if field.CanSet() { + fv, _ := strconv.ParseFloat(valueNode.Value, 64) + nr := low.NodeReference[float64]{ + Value: fv, + ValueNode: valueNode, + KeyNode: keyNode, + } + field.Set(reflect.ValueOf(nr)) + } + } + } + break + case reflect.TypeOf([]low.NodeReference[string]{}): + if valueNode != nil { + if utils.IsNodeArray(valueNode) { + if field.CanSet() { + var items []low.NodeReference[string] + for _, sliceItem := range valueNode.Content { + items = append(items, low.NodeReference[string]{ + Value: sliceItem.Value, + ValueNode: sliceItem, + KeyNode: valueNode, + }) + } + field.Set(reflect.ValueOf(items)) + } + } + } + break + case reflect.TypeOf([]low.NodeReference[float32]{}): + if valueNode != nil { + if utils.IsNodeArray(valueNode) { + if field.CanSet() { + var items []low.NodeReference[float32] + for _, sliceItem := range valueNode.Content { + fv, _ := strconv.ParseFloat(sliceItem.Value, 32) + items = append(items, low.NodeReference[float32]{ + Value: float32(fv), + ValueNode: sliceItem, + KeyNode: valueNode, + }) + } + field.Set(reflect.ValueOf(items)) + } + } + } + break + case reflect.TypeOf([]low.NodeReference[float64]{}): + if valueNode != nil { + if utils.IsNodeArray(valueNode) { + if field.CanSet() { + var items []low.NodeReference[float64] + for _, sliceItem := range valueNode.Content { + fv, _ := strconv.ParseFloat(sliceItem.Value, 64) + items = append(items, low.NodeReference[float64]{Value: fv, ValueNode: sliceItem}) + } + field.Set(reflect.ValueOf(items)) + } + } + } + break + case reflect.TypeOf([]low.NodeReference[int]{}): + if valueNode != nil { + if utils.IsNodeArray(valueNode) { + if field.CanSet() { + var items []low.NodeReference[int] + for _, sliceItem := range valueNode.Content { + iv, _ := strconv.Atoi(sliceItem.Value) + items = append(items, low.NodeReference[int]{ + Value: iv, + ValueNode: sliceItem, + KeyNode: valueNode, + }) + } + field.Set(reflect.ValueOf(items)) + } + } + } + break + case reflect.TypeOf([]low.NodeReference[int64]{}): + if valueNode != nil { + if utils.IsNodeArray(valueNode) { + if field.CanSet() { + var items []low.NodeReference[int64] + for _, sliceItem := range valueNode.Content { + iv, _ := strconv.ParseInt(sliceItem.Value, 10, 64) + items = append(items, low.NodeReference[int64]{ + Value: iv, + ValueNode: sliceItem, + KeyNode: valueNode, + }) + } + field.Set(reflect.ValueOf(items)) + } + } + } + break + case reflect.TypeOf([]low.NodeReference[bool]{}): + if valueNode != nil { + if utils.IsNodeArray(valueNode) { + if field.CanSet() { + var items []low.NodeReference[bool] + for _, sliceItem := range valueNode.Content { + bv, _ := strconv.ParseBool(sliceItem.Value) + items = append(items, low.NodeReference[bool]{ + Value: bv, + ValueNode: sliceItem, + KeyNode: valueNode, + }) + } + field.Set(reflect.ValueOf(items)) + } + } + } + break + default: + // we want to ignore everything else. + break + } + return nil } func BuildModelAsync(n *yaml.Node, model interface{}, lwg *sync.WaitGroup, errors *[]error) { - if n != nil { - err := BuildModel(n, model) - if err != nil { - *errors = append(*errors, err) - } - } - lwg.Done() + if n != nil { + err := BuildModel(n, model) + if err != nil { + *errors = append(*errors, err) + } + } + lwg.Done() +} + +func ExtractExtensions(root *yaml.Node) (map[low.NodeReference[string]]low.NodeReference[any], error) { + extensions := utils.FindExtensionNodes(root.Content) + extensionMap := make(map[low.NodeReference[string]]low.NodeReference[any]) + for _, ext := range extensions { + if utils.IsNodeMap(ext.Value) { + var v interface{} + err := ext.Value.Decode(&v) + if err != nil { + return nil, err + } + extensionMap[low.NodeReference[string]{ + Value: ext.Key.Value, + KeyNode: ext.Key, + }] = low.NodeReference[any]{Value: v, KeyNode: ext.Key} + } + if utils.IsNodeStringValue(ext.Value) { + extensionMap[low.NodeReference[string]{ + Value: ext.Key.Value, + KeyNode: ext.Key, + }] = low.NodeReference[any]{Value: ext.Value.Value, ValueNode: ext.Value} + } + if utils.IsNodeFloatValue(ext.Value) { + fv, _ := strconv.ParseFloat(ext.Value.Value, 64) + extensionMap[low.NodeReference[string]{ + Value: ext.Key.Value, + KeyNode: ext.Key, + }] = low.NodeReference[any]{Value: fv, ValueNode: ext.Value} + } + if utils.IsNodeIntValue(ext.Value) { + iv, _ := strconv.ParseInt(ext.Value.Value, 10, 64) + extensionMap[low.NodeReference[string]{ + Value: ext.Key.Value, + KeyNode: ext.Key, + }] = low.NodeReference[any]{Value: iv, ValueNode: ext.Value} + } + if utils.IsNodeBoolValue(ext.Value) { + bv, _ := strconv.ParseBool(ext.Value.Value) + extensionMap[low.NodeReference[string]{ + Value: ext.Key.Value, + KeyNode: ext.Key, + }] = low.NodeReference[any]{Value: bv, ValueNode: ext.Value} + } + if utils.IsNodeArray(ext.Value) { + var v []interface{} + err := ext.Value.Decode(&v) + if err != nil { + return nil, err + } + extensionMap[low.NodeReference[string]{ + Value: ext.Key.Value, + KeyNode: ext.Key, + }] = low.NodeReference[any]{Value: v, ValueNode: ext.Value} + } + } + return extensionMap, nil } diff --git a/openapi/create_document.go b/openapi/create_document.go index 4569c68..a9ca3cd 100644 --- a/openapi/create_document.go +++ b/openapi/create_document.go @@ -1,78 +1,182 @@ package openapi import ( - "github.com/pb33f/libopenapi/datamodel" - "github.com/pb33f/libopenapi/datamodel/low" - v3 "github.com/pb33f/libopenapi/datamodel/low/3.0" - "github.com/pb33f/libopenapi/utils" + "github.com/pb33f/libopenapi/datamodel" + "github.com/pb33f/libopenapi/datamodel/low" + v3 "github.com/pb33f/libopenapi/datamodel/low/3.0" + "github.com/pb33f/libopenapi/utils" + "gopkg.in/yaml.v3" + "strconv" + "sync" ) -func CreateDocument(spec []byte) (*v3.Document, error) { +const ( + Info = "info" + Servers = "servers" +) - // extract details from spec - info, err := datamodel.ExtractSpecInfo(spec) - if err != nil { - return nil, err - } +func CreateDocument(info *datamodel.SpecInfo) (*v3.Document, error) { - doc := v3.Document{Version: low.NodeReference[string]{Value: info.Version, ValueNode: info.RootNode}} + doc := v3.Document{Version: low.NodeReference[string]{Value: info.Version, ValueNode: info.RootNode}} - // build an index - //idx := index.NewSpecIndex(info.RootNode) - //datamodel.BuildModel(info.RootNode.Content[0], &doc) + // build an index + //idx := index.NewSpecIndex(info.RootNode) + //datamodel.BuildModel(info.RootNode.Content[0], &doc) + var wg sync.WaitGroup + var errors []error + var runExtraction = func(info *datamodel.SpecInfo, doc *v3.Document, + runFunc func(i *datamodel.SpecInfo, d *v3.Document) error, + ers *[]error, + wg *sync.WaitGroup) { - // extract info - extractErr := extractInfo(info, &doc) - if extractErr != nil { - return nil, extractErr - } + if er := runFunc(info, doc); er != nil { + *ers = append(*ers, er) + } - // extract servers - extractErr = extractServers(info, &doc) - if extractErr != nil { - return nil, extractErr - } + wg.Done() + } - return &doc, nil + wg.Add(3) + go runExtraction(info, &doc, extractInfo, &errors, &wg) + go runExtraction(info, &doc, extractServers, &errors, &wg) + go runExtraction(info, &doc, extractTags, &errors, &wg) + wg.Wait() + + // todo fix this. + if len(errors) > 0 { + return &doc, errors[0] + } + + return &doc, nil } func extractInfo(info *datamodel.SpecInfo, doc *v3.Document) error { - _, ln, vn := utils.FindKeyNodeFull("info", info.RootNode.Content) - if vn != nil { - ir := v3.Info{} - err := datamodel.BuildModel(vn, &ir) - if err != nil { - return err - } - err = ir.Build(vn) - nr := low.NodeReference[*v3.Info]{Value: &ir, ValueNode: vn, KeyNode: ln} - doc.Info = nr - } - return nil + _, ln, vn := utils.FindKeyNodeFull(Info, info.RootNode.Content) + if vn != nil { + ir := v3.Info{} + err := datamodel.BuildModel(vn, &ir) + if err != nil { + return err + } + err = ir.Build(vn) + nr := low.NodeReference[*v3.Info]{Value: &ir, ValueNode: vn, KeyNode: ln} + doc.Info = nr + } + return nil } func extractServers(info *datamodel.SpecInfo, doc *v3.Document) error { - _, ln, vn := utils.FindKeyNodeFull("servers", info.RootNode.Content) - if vn != nil { - if utils.IsNodeArray(vn) { - var servers []low.NodeReference[*v3.Server] - for _, srvN := range vn.Content { - if utils.IsNodeMap(srvN) { - srvr := v3.Server{} - err := datamodel.BuildModel(srvN, &srvr) - if err != nil { - return err - } - srvr.Build(srvN) - servers = append(servers, low.NodeReference[*v3.Server]{ - Value: &srvr, - ValueNode: srvN, - KeyNode: ln, - }) - } - } - doc.Servers = servers - } - } - return nil + _, ln, vn := utils.FindKeyNodeFull(Servers, info.RootNode.Content) + if vn != nil { + if utils.IsNodeArray(vn) { + var servers []low.NodeReference[*v3.Server] + for _, srvN := range vn.Content { + if utils.IsNodeMap(srvN) { + srvr := v3.Server{} + err := datamodel.BuildModel(srvN, &srvr) + if err != nil { + return err + } + srvr.Build(srvN) + servers = append(servers, low.NodeReference[*v3.Server]{ + Value: &srvr, + ValueNode: srvN, + KeyNode: ln, + }) + } + } + doc.Servers = servers + } + } + return nil +} + +func extractTags(info *datamodel.SpecInfo, doc *v3.Document) error { + _, ln, vn := utils.FindKeyNodeFull(v3.Tags, info.RootNode.Content) + if vn != nil { + if utils.IsNodeArray(vn) { + var tags []low.NodeReference[*v3.Tag] + for _, tagN := range vn.Content { + if utils.IsNodeMap(tagN) { + tag := v3.Tag{} + err := datamodel.BuildModel(tagN, &tag) + if err != nil { + return err + } + tag.Build(tagN) + tags = append(tags, low.NodeReference[*v3.Tag]{ + Value: &tag, + ValueNode: tagN, + KeyNode: ln, + }) + } + } + doc.Tags = tags + } + } + return nil +} + +func ExtractExtensions(root *yaml.Node) (map[low.NodeReference[string]]low.NodeReference[any], error) { + extensions := utils.FindExtensionNodes(root.Content) + extensionMap := make(map[low.NodeReference[string]]low.NodeReference[any]) + for _, ext := range extensions { + // this is an object, decode into an unknown map. + if utils.IsNodeMap(ext.Value) { + var v interface{} + err := ext.Value.Decode(&v) + if err != nil { + return nil, err + } + extensionMap[low.NodeReference[string]{ + Value: ext.Key.Value, + KeyNode: ext.Key, + ValueNode: ext.Value, + }] = low.NodeReference[any]{Value: v, KeyNode: ext.Key} + } + if utils.IsNodeStringValue(ext.Value) { + extensionMap[low.NodeReference[string]{ + Value: ext.Key.Value, + KeyNode: ext.Key, + ValueNode: ext.Value, + }] = low.NodeReference[any]{Value: ext.Value.Value, ValueNode: ext.Value} + } + if utils.IsNodeFloatValue(ext.Value) { + fv, _ := strconv.ParseFloat(ext.Value.Value, 64) + extensionMap[low.NodeReference[string]{ + Value: ext.Key.Value, + KeyNode: ext.Key, + ValueNode: ext.Value, + }] = low.NodeReference[any]{Value: fv, ValueNode: ext.Value} + } + if utils.IsNodeIntValue(ext.Value) { + iv, _ := strconv.ParseInt(ext.Value.Value, 10, 64) + extensionMap[low.NodeReference[string]{ + Value: ext.Key.Value, + KeyNode: ext.Key, + ValueNode: ext.Value, + }] = low.NodeReference[any]{Value: iv, ValueNode: ext.Value} + } + if utils.IsNodeBoolValue(ext.Value) { + bv, _ := strconv.ParseBool(ext.Value.Value) + extensionMap[low.NodeReference[string]{ + Value: ext.Key.Value, + KeyNode: ext.Key, + ValueNode: ext.Value, + }] = low.NodeReference[any]{Value: bv, ValueNode: ext.Value} + } + if utils.IsNodeArray(ext.Value) { + var v []interface{} + err := ext.Value.Decode(&v) + if err != nil { + return nil, err + } + extensionMap[low.NodeReference[string]{ + Value: ext.Key.Value, + KeyNode: ext.Key, + ValueNode: ext.Value, + }] = low.NodeReference[any]{Value: v, ValueNode: ext.Value} + } + } + return extensionMap, nil } diff --git a/openapi/create_document_test.go b/openapi/create_document_test.go index a5ecd6f..cdb26b7 100644 --- a/openapi/create_document_test.go +++ b/openapi/create_document_test.go @@ -1,32 +1,109 @@ package openapi import ( + "github.com/pb33f/libopenapi/datamodel" + v3 "github.com/pb33f/libopenapi/datamodel/low/3.0" "github.com/stretchr/testify/assert" "io/ioutil" "testing" ) -func TestCreateDocument_NoData(t *testing.T) { - doc, err := CreateDocument(nil) - assert.Nil(t, doc) - assert.Error(t, err) +var doc *v3.Document + +func init() { + data, _ := ioutil.ReadFile("../test_specs/burgershop.openapi.yaml") + info, _ := datamodel.ExtractSpecInfo(data) + doc, _ = CreateDocument(info) +} + +func BenchmarkCreateDocument(b *testing.B) { + data, _ := ioutil.ReadFile("../test_specs/burgershop.openapi.yaml") + info, _ := datamodel.ExtractSpecInfo(data) + for i := 0; i < b.N; i++ { + doc, _ = CreateDocument(info) + } } func TestCreateDocument(t *testing.T) { - data, aErr := ioutil.ReadFile("../test_specs/burgershop.openapi.yaml") - assert.NoError(t, aErr) - - doc, err := CreateDocument(data) - assert.NotNil(t, doc) - assert.NoError(t, err) - assert.Equal(t, "3.0.1", doc.Version.Value) assert.Equal(t, "Burger Shop", doc.Info.Value.Title.Value) assert.NotEmpty(t, doc.Info.Value.Title.Value) +} + +func TestCreateDocument_Info(t *testing.T) { assert.Equal(t, "https://pb33f.io", doc.Info.Value.TermsOfService.Value) assert.Equal(t, "pb33f", doc.Info.Value.Contact.Value.Name.Value) assert.Equal(t, "buckaroo@pb33f.io", doc.Info.Value.Contact.Value.Email.Value) assert.Equal(t, "https://pb33f.io", doc.Info.Value.Contact.Value.URL.Value) + assert.Equal(t, "pb33f", doc.Info.Value.License.Value.Name.Value) + assert.Equal(t, "https://pb33f.io/made-up", doc.Info.Value.License.Value.URL.Value) +} + +func TestCreateDocument_Servers(t *testing.T) { + assert.Len(t, doc.Servers, 2) + server1 := doc.Servers[0] + server2 := doc.Servers[1] + + // server 1 + assert.Equal(t, "{scheme}://api.pb33f.io", server1.Value.URL.Value) + assert.NotEmpty(t, server1.Value.Description.Value) + assert.Len(t, server1.Value.Variables.Value, 1) + assert.Len(t, server1.Value.Variables.Value["scheme"].Value.Enum, 2) + assert.Equal(t, server1.Value.Variables.Value["scheme"].Value.Default.Value, "https") + assert.NotEmpty(t, server1.Value.Variables.Value["scheme"].Value.Description.Value) + + // server 2 + assert.Equal(t, "https://{domain}.{host}.com", server2.Value.URL.Value) + assert.NotEmpty(t, server2.Value.Description.Value) + assert.Len(t, server2.Value.Variables.Value, 2) + assert.Equal(t, server2.Value.Variables.Value["domain"].Value.Default.Value, "api") + assert.NotEmpty(t, server2.Value.Variables.Value["domain"].Value.Description.Value) + assert.NotEmpty(t, server2.Value.Variables.Value["host"].Value.Description.Value) + assert.Equal(t, server2.Value.Variables.Value["host"].Value.Default.Value, "pb33f.io") assert.Equal(t, "1.2", doc.Info.Value.Version.Value) +} + +func TestCreateDocument_Tags(t *testing.T) { + assert.Len(t, doc.Tags, 2) + + // tag1 + assert.Equal(t, "Burgers", doc.Tags[0].Value.Name.Value) + assert.NotEmpty(t, doc.Tags[0].Value.Description.Value) + assert.NotNil(t, doc.Tags[0].Value.ExternalDocs.Value) + assert.Equal(t, "https://pb33f.io", doc.Tags[0].Value.ExternalDocs.Value.URL.Value) + assert.NotEmpty(t, doc.Tags[0].Value.ExternalDocs.Value.URL.Value) + assert.Len(t, doc.Tags[0].Value.Extensions, 7) + + for key, extension := range doc.Tags[0].Value.Extensions { + switch key.Value { + case "x-internal-ting": + assert.Equal(t, "somethingSpecial", extension.Value) + case "x-internal-tong": + assert.Equal(t, int64(1), extension.Value) + case "x-internal-tang": + assert.Equal(t, 1.2, extension.Value) + case "x-internal-tung": + assert.Equal(t, true, extension.Value) + case "x-internal-arr": + assert.Len(t, extension.Value, 2) + assert.Equal(t, "one", extension.Value.([]interface{})[0].(string)) + case "x-internal-arrmap": + assert.Len(t, extension.Value, 2) + assert.Equal(t, "now", extension.Value.([]interface{})[0].(map[string]interface{})["what"]) + case "x-something-else": + // crazy times in the upside down. this API should be avoided for the higher up use cases. + // this is why we will need a higher level API to this model, this looks cool and all, but dude. + assert.Equal(t, "now?", extension.Value.(map[string]interface{})["ok"].([]interface{})[0].(map[string]interface{})["what"]) + } + + } + + /// tag2 + assert.Equal(t, "Dressing", doc.Tags[1].Value.Name.Value) + assert.NotEmpty(t, doc.Tags[1].Value.Description.Value) + assert.NotNil(t, doc.Tags[1].Value.ExternalDocs.Value) + assert.Equal(t, "https://pb33f.io", doc.Tags[1].Value.ExternalDocs.Value.URL.Value) + assert.NotEmpty(t, doc.Tags[1].Value.ExternalDocs.Value.URL.Value) + assert.Len(t, doc.Tags[1].Value.Extensions, 0) } diff --git a/test_specs/burgershop.openapi.yaml b/test_specs/burgershop.openapi.yaml index b606c41..c76748f 100644 --- a/test_specs/burgershop.openapi.yaml +++ b/test_specs/burgershop.openapi.yaml @@ -10,25 +10,38 @@ info: url: https://pb33f.io license: name: pb33f - url: https://quobix.com/made-up + url: https://pb33f.io/made-up version: "1.2" tags: - name: "Burgers" description: "All kinds of yummy burgers." externalDocs: description: "Find out more" - url: "https://quobix.com/" + url: "https://pb33f.io" + x-internal-ting: somethingSpecial + x-internal-tong: 1 + x-internal-tang: 1.2 + x-internal-tung: true + x-internal-arr: + - one + - two + x-internal-arrmap: + - what: now + - why: that + x-something-else: + ok: + - what: now? - name: "Dressing" description: "Variety of dressings: cheese, veggie, oil and a lot more" externalDocs: description: "Find out more information about our products)" - url: "https://quobix.com/" + url: "https://pb33f.io" servers: - - url: "{scheme}://api.quobix.com" + - url: "{scheme}://api.pb33f.io" description: "this is our main API server, for all fun API things." variables: scheme: - enum: [https] + enum: [https, wss] default: https description: this is a server variable for the scheme - url: "https://{domain}.{host}.com" @@ -38,8 +51,8 @@ servers: default: "api" description: the default API domain is 'api' host: - default: "quobix.com" - description: the default host for this API is 'quobix.com' + default: "pb33f.io" + description: the default host for this API is 'pb33f.com' paths: /burgers: post: diff --git a/utils/utils.go b/utils/utils.go index 3923c3a..a7d5d39 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,265 +1,285 @@ package utils import ( - "encoding/json" - "fmt" - "github.com/iancoleman/strcase" - "github.com/vmware-labs/yaml-jsonpath/pkg/yamlpath" - "gopkg.in/yaml.v3" - "regexp" - "strconv" - "strings" + "encoding/json" + "fmt" + "github.com/iancoleman/strcase" + "github.com/vmware-labs/yaml-jsonpath/pkg/yamlpath" + "gopkg.in/yaml.v3" + "regexp" + "strconv" + "strings" ) type Case int8 const ( - // OpenApi3 is used by all OpenAPI 3+ docs - OpenApi3 = "openapi" + // OpenApi3 is used by all OpenAPI 3+ docs + OpenApi3 = "openapi" - // OpenApi2 is used by all OpenAPI 2 docs, formerly known as swagger. - OpenApi2 = "swagger" + // 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" + // AsyncApi is used by akk AsyncAPI docs, all versions. + AsyncApi = "asyncapi" - PascalCase Case = iota - CamelCase - ScreamingSnakeCase - SnakeCase - KebabCase - ScreamingKebabCase - RegularCase + PascalCase Case = iota + CamelCase + ScreamingSnakeCase + SnakeCase + KebabCase + ScreamingKebabCase + RegularCase ) // 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) + jsonPath = FixContext(jsonPath) - var node yaml.Node - yaml.Unmarshal(yamlData, &node) + 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 + 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 - } + 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, ".") + 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) + // 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) + jsonPath = FixContext(jsonPath) - path, err := yamlpath.NewPath(jsonPath) - if err != nil { - return nil, err - } - results, _ := path.Find(node) - return results, nil + 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 + 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 + 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 + 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] - } + 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 + 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] // next node is what 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 + 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] // next node is what 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 + 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 + 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 key == v.Value { - return v, nodes[i+1] // next node is what we need. - } - } - return nil, nil + for i, v := range nodes { + if key == 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. + //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 + } + if IsNodeArray(v) { + return v, v.Content[x] + } + } + } + } + return nil, nil } func FindKeyNodeFull(key string, nodes []*yaml.Node) (keyNode *yaml.Node, labelNode *yaml.Node, valueNode *yaml.Node) { - for i, v := range nodes { - if i%2 == 0 && key == v.Value { - return v, nodes[i], 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], v.Content[x] - } - return v, v.Content[x], v.Content[x+1] // next node is what we need. + for i, v := range nodes { + if i%2 == 0 && key == v.Value { + return v, nodes[i], 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], v.Content[x] + } + return v, v.Content[x], v.Content[x+1] // next node is what we need. - } - if IsNodeArray(v) { - return v, v.Content[x], v.Content[x] - } - } - } - } - return nil, nil, nil + } + if IsNodeArray(v) { + return v, v.Content[x], v.Content[x] + } + } + } + } + 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" @@ -273,257 +293,257 @@ var SchemaSource = "https://json-schema.org/draft/2020-12/schema" var SchemaId = "https://quobix.com/api/vacuum" 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" + 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" + 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 + 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" + 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" + 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" + 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" + 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" + if node == nil { + return false + } + return node.Tag == "!!bool" } // 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 { + tokens := strings.Split(context, ".") + var cleaned = []string{} + for i, t := range tokens { - if v, err := strconv.Atoi(t); err == nil { + 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)", "$")) + 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, ".") + } + 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 + 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 + 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 + 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 + 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 := segs[len(segs)-1] + segs := strings.Split(id, "/") + name := segs[len(segs)-1] - replaced := strings.ReplaceAll(fmt.Sprintf("%s['%s']", - strings.Join(segs[:len(segs)-1], "."), name), "#", "$") + replaced := strings.ReplaceAll(fmt.Sprintf("%s['%s']", + strings.Join(segs[:len(segs)-1], "."), name), "#", "$") - if replaced[0] != '$' { - replaced = fmt.Sprintf("$%s", replaced) - } - return name, replaced + 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] + segs := strings.Split(id, "/") + name := segs[len(segs)-1] - return name, strings.ReplaceAll(fmt.Sprintf("%s.%s", - strings.Join(segs[:len(segs)-1], "."), name), "#", "$") + 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) + buf := new(strings.Builder) - startLine := startNode.Line - before - endLine := startNode.Line + after + startLine := startNode.Line - before + endLine := startNode.Line + after - if startLine < 0 { - startLine = 0 - } + if startLine < 0 { + startLine = 0 + } - if endLine >= len(specData) { - endLine = len(specData) - 1 - } + if endLine >= len(specData) { + endLine = len(specData) - 1 + } - delta := endLine - startLine + 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)) - } - } + 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() + return buf.String() } func ConvertCase(input string, convert Case) string { - if input == "" { - return "" - } - switch convert { - case PascalCase: - return strcase.ToCamel(input) - case CamelCase: - return strcase.ToLowerCamel(input) - case ScreamingKebabCase: - return strcase.ToScreamingKebab(input) - case ScreamingSnakeCase: - return strcase.ToScreamingSnake(input) - case SnakeCase: - return strcase.ToSnake(input) - default: - return input - } + if input == "" { + return "" + } + switch convert { + case PascalCase: + return strcase.ToCamel(input) + case CamelCase: + return strcase.ToLowerCamel(input) + case ScreamingKebabCase: + return strcase.ToScreamingKebab(input) + case ScreamingSnakeCase: + return strcase.ToScreamingSnake(input) + case SnakeCase: + return strcase.ToSnake(input) + default: + return input + } } func DetectCase(input string) Case { - trim := strings.TrimSpace(input) - if trim == "" { - return -1 - } + trim := strings.TrimSpace(input) + if trim == "" { + return -1 + } - 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 + 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 }