diff --git a/datamodel/low/reference.go b/datamodel/low/reference.go index 3c882a9..a9cd1a3 100644 --- a/datamodel/low/reference.go +++ b/datamodel/low/reference.go @@ -16,6 +16,6 @@ type NodeReference[T comparable] struct { } type ObjectReference struct { - Value interface{} + Value map[string]interface{} Node *yaml.Node } diff --git a/utils/model_builder.go b/utils/model_builder.go index 7a30010..43a1c17 100644 --- a/utils/model_builder.go +++ b/utils/model_builder.go @@ -2,7 +2,6 @@ package utils import ( "fmt" - "github.com/iancoleman/strcase" "github.com/pb33f/libopenapi/datamodel/low" "gopkg.in/yaml.v3" "reflect" @@ -14,73 +13,31 @@ func BuildModel(node *yaml.Node, model interface{}) error { for i := 0; i < v.NumField(); i++ { fName := v.Type().Field(i).Name - fieldName := strcase.ToLowerCamel(fName) - _, vn := FindKeyNode(fieldName, node.Content) - field := v.FieldByName(fName) - switch field.Kind() { - case reflect.Struct: - switch field.Type() { - case reflect.TypeOf(low.NodeReference[string]{}): - if vn != nil { - if IsNodeStringValue(vn) { - if field.CanSet() { - nr := low.NodeReference[string]{Value: vn.Value, Node: vn} - field.Set(reflect.ValueOf(nr)) - } - } - } - break - case reflect.TypeOf(low.NodeReference[bool]{}): - if vn != nil { - if IsNodeBoolValue(vn) { - if field.CanSet() { - bv, _ := strconv.ParseBool(vn.Value) - nr := low.NodeReference[bool]{Value: bv, Node: vn} - field.Set(reflect.ValueOf(nr)) - } - } - } - case reflect.TypeOf(low.NodeReference[int]{}): - if vn != nil { - if IsNodeIntValue(vn) { - if field.CanSet() { - fv, _ := strconv.Atoi(vn.Value) - nr := low.NodeReference[int]{Value: fv, Node: vn} - field.Set(reflect.ValueOf(nr)) - } - } - } - case reflect.TypeOf(low.NodeReference[int64]{}): - if vn != nil { - if IsNodeIntValue(vn) { - if field.CanSet() { - fv, _ := strconv.Atoi(vn.Value) - nr := low.NodeReference[int64]{Value: int64(fv), Node: vn} - field.Set(reflect.ValueOf(nr)) - } - } - } - case reflect.TypeOf(low.NodeReference[float32]{}): - if vn != nil { - if IsNodeFloatValue(vn) { - if field.CanSet() { - fv, _ := strconv.ParseFloat(vn.Value, 32) - nr := low.NodeReference[float32]{Value: float32(fv), Node: vn} - field.Set(reflect.ValueOf(nr)) - } - } - } - case reflect.TypeOf(low.NodeReference[float64]{}): - if vn != nil { - if IsNodeFloatValue(vn) { - if field.CanSet() { - fv, _ := strconv.ParseFloat(vn.Value, 64) - nr := low.NodeReference[float64]{Value: fv, Node: vn} - field.Set(reflect.ValueOf(nr)) - } - } - } + // we need to find a matching field in the YAML, the cases may be off, so take no chances. + cases := []Case{PascalCase, CamelCase, ScreamingSnakeCase, + SnakeCase, KebabCase, RegularCase} + + var vn, kn *yaml.Node + for _, tryCase := range cases { + kn, vn = FindKeyNode(ConvertCase(fName, tryCase), node.Content) + if vn != nil { + break + } + } + + 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: + err := SetField(field, vn, kn) + if err != nil { + return nil } default: fmt.Printf("Unsupported type: %v", v.Field(i).Kind()) @@ -91,3 +48,211 @@ func BuildModel(node *yaml.Node, model interface{}) error { return nil } + +func SetField(field reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) error { + switch field.Type() { + + case reflect.TypeOf(map[string]low.ObjectReference{}): + if valueNode != nil { + if 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, + Node: sliceItem, + } + } + 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 IsNodeMap(valueNode) { + if field.CanSet() { + or := low.ObjectReference{Value: decoded, Node: valueNode} + field.Set(reflect.ValueOf(or)) + } + } + } + break + case reflect.TypeOf([]low.ObjectReference{}): + if valueNode != nil { + if 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, + Node: sliceItem, + }) + } + field.Set(reflect.ValueOf(items)) + } + } + } + break + case reflect.TypeOf(low.NodeReference[string]{}): + if valueNode != nil { + if IsNodeStringValue(valueNode) { + if field.CanSet() { + nr := low.NodeReference[string]{Value: valueNode.Value, Node: valueNode} + field.Set(reflect.ValueOf(nr)) + } + } + } + break + case reflect.TypeOf(low.NodeReference[bool]{}): + if valueNode != nil { + if IsNodeBoolValue(valueNode) { + if field.CanSet() { + bv, _ := strconv.ParseBool(valueNode.Value) + nr := low.NodeReference[bool]{Value: bv, Node: valueNode} + field.Set(reflect.ValueOf(nr)) + } + } + } + break + case reflect.TypeOf(low.NodeReference[int]{}): + if valueNode != nil { + if IsNodeIntValue(valueNode) { + if field.CanSet() { + fv, _ := strconv.Atoi(valueNode.Value) + nr := low.NodeReference[int]{Value: fv, Node: valueNode} + field.Set(reflect.ValueOf(nr)) + } + } + } + break + case reflect.TypeOf(low.NodeReference[int64]{}): + if valueNode != nil { + if IsNodeIntValue(valueNode) { + if field.CanSet() { + fv, _ := strconv.Atoi(valueNode.Value) + nr := low.NodeReference[int64]{Value: int64(fv), Node: valueNode} + field.Set(reflect.ValueOf(nr)) + } + } + } + break + case reflect.TypeOf(low.NodeReference[float32]{}): + if valueNode != nil { + if IsNodeFloatValue(valueNode) { + if field.CanSet() { + fv, _ := strconv.ParseFloat(valueNode.Value, 32) + nr := low.NodeReference[float32]{Value: float32(fv), Node: valueNode} + field.Set(reflect.ValueOf(nr)) + } + } + } + break + case reflect.TypeOf(low.NodeReference[float64]{}): + if valueNode != nil { + if IsNodeFloatValue(valueNode) { + if field.CanSet() { + fv, _ := strconv.ParseFloat(valueNode.Value, 64) + nr := low.NodeReference[float64]{Value: fv, Node: valueNode} + field.Set(reflect.ValueOf(nr)) + } + } + } + break + case reflect.TypeOf([]low.NodeReference[string]{}): + if valueNode != nil { + if IsNodeArray(valueNode) { + if field.CanSet() { + var items []low.NodeReference[string] + for _, sliceItem := range valueNode.Content { + items = append(items, low.NodeReference[string]{Value: sliceItem.Value, Node: sliceItem}) + } + field.Set(reflect.ValueOf(items)) + } + } + } + break + case reflect.TypeOf([]low.NodeReference[float32]{}): + if valueNode != nil { + if 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), Node: sliceItem}) + } + field.Set(reflect.ValueOf(items)) + } + } + } + break + case reflect.TypeOf([]low.NodeReference[float64]{}): + if valueNode != nil { + if 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, Node: sliceItem}) + } + field.Set(reflect.ValueOf(items)) + } + } + } + break + case reflect.TypeOf([]low.NodeReference[int]{}): + if valueNode != nil { + if 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, Node: sliceItem}) + } + field.Set(reflect.ValueOf(items)) + } + } + } + break + case reflect.TypeOf([]low.NodeReference[bool]{}): + if valueNode != nil { + if 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, Node: sliceItem}) + } + field.Set(reflect.ValueOf(items)) + } + } + } + break + default: + m := field.Type() + fmt.Printf("error, unknown type!!! %v", m) + return fmt.Errorf("unknown type, cannot parse: %v", m) + } + return nil +} diff --git a/utils/model_builder_test.go b/utils/model_builder_test.go index 4ab1aea..8da1c08 100644 --- a/utils/model_builder_test.go +++ b/utils/model_builder_test.go @@ -8,13 +8,20 @@ import ( ) type hotdog struct { - Name low.NodeReference[string] - Beef low.NodeReference[bool] - Fat low.NodeReference[int] - Ketchup low.NodeReference[float32] - Mustard low.NodeReference[float64] - Grilled low.NodeReference[bool] - MaxTemp low.NodeReference[int] + Name low.NodeReference[string] + Fat low.NodeReference[int] + Ketchup low.NodeReference[float32] + Mustard low.NodeReference[float64] + Grilled low.NodeReference[bool] + MaxTemp low.NodeReference[int] + Drinks []low.NodeReference[string] + Sides []low.NodeReference[float32] + BigSides []low.NodeReference[float64] + Temps []low.NodeReference[int] + Buns []low.NodeReference[bool] + UnknownElements low.ObjectReference + LotsOfUnknowns []low.ObjectReference + Where map[string]low.ObjectReference } func (h hotdog) Build(node *yaml.Node) { @@ -28,8 +35,46 @@ beef: true fat: 200 ketchup: 200.45 mustard: 324938249028.98234892374892374923874823974 -grilled: false +grilled: true maxTemp: 250 +drinks: + - nice + - rice + - spice +sides: + - 0.23 + - 22.23 + - 99.45 + - 22311.2234 +bigSides: + - 98237498.9872349872349872349872347982734927342983479234234234234234234 + - 9827347234234.982374982734987234987 + - 234234234.234982374982347982374982374982347 + - 987234987234987234982734.987234987234987234987234987234987234987234982734982734982734987234987234987234987 +temps: + - 1 + - 2 +buns: + - true + - false +unknownElements: + well: + whoKnows: not me? + doYou: + love: beerToo? +lotsOfUnknowns: + - wow: + what: aTrip + - amazing: + french: fries + - amazing: + french: fries +where: + things: + are: + wild: out here + howMany: + bears: 200 ` var rootNode yaml.Node @@ -40,10 +85,18 @@ maxTemp: 250 cErr := BuildModel(&rootNode, &hd) assert.Equal(t, 200, hd.Fat.Value) assert.Equal(t, 3, hd.Fat.Node.Line) - assert.Equal(t, true, hd.Beef.Value) + assert.Equal(t, true, hd.Grilled.Value) assert.Equal(t, "yummy", hd.Name.Value) assert.Equal(t, float32(200.45), hd.Ketchup.Value) + assert.Len(t, hd.Drinks, 3) + assert.Len(t, hd.Sides, 4) + assert.Len(t, hd.BigSides, 4) + assert.Len(t, hd.Temps, 2) + assert.Equal(t, 2, hd.Temps[1].Value) + assert.Equal(t, 24, hd.Temps[1].Node.Line) + assert.Len(t, hd.UnknownElements.Value, 2) + assert.Len(t, hd.LotsOfUnknowns, 3) + assert.Len(t, hd.Where, 2) assert.Equal(t, 324938249028.98234892374892374923874823974, hd.Mustard.Value) assert.NoError(t, cErr) - } diff --git a/utils/utils.go b/utils/utils.go index c95db18..be232a1 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -3,12 +3,16 @@ 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" ) +type Case int8 + const ( // OpenApi3 is used by all OpenAPI 3+ docs OpenApi3 = "openapi" @@ -18,6 +22,14 @@ const ( // AsyncApi is used by akk AsyncAPI docs, all versions. AsyncApi = "asyncapi" + + PascalCase Case = iota + CamelCase + ScreamingSnakeCase + SnakeCase + KebabCase + ScreamingKebabCase + RegularCase ) // FindNodes will find a node based on JSONPath, it accepts raw yaml/json as input. @@ -439,3 +451,56 @@ func RenderCodeSnippet(startNode *yaml.Node, specData []string, before, after in 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 + } +} + +func DetectCase(input string) Case { + 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 +} diff --git a/utils/utils_test.go b/utils/utils_test.go index 466f02d..990df3d 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -419,3 +419,23 @@ func TestConvertComponentIdIntoPath(t *testing.T) { assert.Equal(t, "$.chicken.chips.pizza.cake", path) assert.Equal(t, "cake", segment) } + +func TestDetectCase(t *testing.T) { + assert.Equal(t, PascalCase, DetectCase("PizzaPie")) + assert.Equal(t, CamelCase, DetectCase("anyoneForTennis")) + assert.Equal(t, ScreamingSnakeCase, DetectCase("I_LOVE_BEER")) + assert.Equal(t, ScreamingKebabCase, DetectCase("I-LOVE-BURGERS")) + assert.Equal(t, SnakeCase, DetectCase("snakes_on_a_plane")) + assert.Equal(t, KebabCase, DetectCase("chicken-be-be-beef-or-pork")) + assert.Equal(t, RegularCase, DetectCase("kebab-TimeIn_london-TOWN")) +} + +func TestConvertCase(t *testing.T) { + str1 := "chicken-nuggets-chicken-soup" + assert.Equal(t, "chickenNuggetsChickenSoup", ConvertCase(str1, CamelCase)) + assert.Equal(t, "ChickenNuggetsChickenSoup", ConvertCase(str1, PascalCase)) + assert.Equal(t, "chicken_nuggets_chicken_soup", ConvertCase(str1, SnakeCase)) + assert.Equal(t, str1, ConvertCase(str1, KebabCase)) + assert.Equal(t, "CHICKEN-NUGGETS-CHICKEN-SOUP", ConvertCase(str1, ScreamingKebabCase)) + assert.Equal(t, "CHICKEN_NUGGETS_CHICKEN_SOUP", ConvertCase(str1, ScreamingSnakeCase)) +}