Merge "bpdocs for struct types created using reflection"

This commit is contained in:
Spandan Das 2021-09-21 20:00:35 +00:00 committed by Gerrit Code Review
commit 87fd4b11af
4 changed files with 186 additions and 9 deletions

View file

@ -121,16 +121,12 @@ func assembleModuleTypeInfo(r *Reader, name string, factory reflect.Value,
v := reflect.ValueOf(s).Elem() v := reflect.ValueOf(s).Elem()
t := v.Type() t := v.Type()
// Ignore property structs with unexported or unnamed types
if t.PkgPath() == "" {
continue
}
ps, err := r.PropertyStruct(t.PkgPath(), t.Name(), v) ps, err := r.PropertyStruct(t.PkgPath(), t.Name(), v)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ps.ExcludeByTag("blueprint", "mutated") ps.ExcludeByTag("blueprint", "mutated")
for _, nestedProperty := range nestedPropertyStructs(v) { for _, nestedProperty := range nestedPropertyStructs(v) {
nestedName := nestedProperty.nestPoint nestedName := nestedProperty.nestPoint
nestedValue := nestedProperty.value nestedValue := nestedProperty.value
@ -357,7 +353,6 @@ propertyLoop:
s := &n[i] s := &n[i]
if s.SameSubProperties(child) { if s.SameSubProperties(child) {
s.OtherNames = append(s.OtherNames, child.Name) s.OtherNames = append(s.OtherNames, child.Name)
s.OtherTexts = append(s.OtherTexts, child.Text)
continue propertyLoop continue propertyLoop
} }
} }

View file

@ -143,7 +143,26 @@ func (ps *PropertyStruct) GetByName(name string) *Property {
} }
func (ps *PropertyStruct) Nest(nested *PropertyStruct) { func (ps *PropertyStruct) Nest(nested *PropertyStruct) {
ps.Properties = append(ps.Properties, nested.Properties...) ps.Properties = nestUnique(ps.Properties, nested.Properties)
}
// Adds a target element to src if it does not exist in src
func nestUnique(src []Property, target []Property) []Property {
var ret []Property
ret = append(ret, src...)
for _, elem := range target {
isUnique := true
for _, retElement := range ret {
if elem.Equal(retElement) {
isUnique = false
break
}
}
if isUnique {
ret = append(ret, elem)
}
}
return ret
} }
func getByName(name string, prefix string, props *[]Property) *Property { func getByName(name string, prefix string, props *[]Property) *Property {
@ -158,7 +177,7 @@ func getByName(name string, prefix string, props *[]Property) *Property {
} }
func (p *Property) Nest(nested *PropertyStruct) { func (p *Property) Nest(nested *PropertyStruct) {
p.Properties = append(p.Properties, nested.Properties...) p.Properties = nestUnique(p.Properties, nested.Properties)
} }
func (p *Property) SetAnonymous() { func (p *Property) SetAnonymous() {

View file

@ -16,6 +16,7 @@ package bpdoc
import ( import (
"reflect" "reflect"
"strings"
"testing" "testing"
) )
@ -51,6 +52,131 @@ func TestIncludeByTag(t *testing.T) {
} }
} }
func TestPropertiesOfReflectionStructs(t *testing.T) {
testCases := []struct {
fields map[string]interface{}
expectedProperties map[string]Property
description string
}{
{
fields: map[string]interface{}{
"A": "A is a string",
"B": 0, //B is an int
},
expectedProperties: map[string]Property{
"a": *createProperty("a", "string", ""),
"b": *createProperty("b", "int", ""),
},
description: "struct is composed of primitive types",
},
{
fields: map[string]interface{}{
"A": "A is a string",
"B": 0, //B is an int
"C": props{},
},
expectedProperties: map[string]Property{
"a": *createProperty("a", "string", ""),
"b": *createProperty("b", "int", ""),
"c": *createProperty("c", "props", "props docs."),
},
description: "struct is composed of primitive types and other structs",
},
}
r := NewReader(pkgFiles)
for _, testCase := range testCases {
structType := reflectionStructType(testCase.fields)
ps, err := r.PropertyStruct(structType.PkgPath(), structType.String(), reflect.New(structType).Elem())
if err != nil {
t.Fatal(err)
}
for _, actualProperty := range ps.Properties {
propName := actualProperty.Name
assertProperties(t, testCase.expectedProperties[propName], actualProperty)
}
}
}
func TestNestUnique(t *testing.T) {
testCases := []struct {
src []Property
target []Property
expected []Property
description string
}{
{
src: []Property{},
target: []Property{},
expected: []Property{},
description: "Nest Unique fails for empty slice",
},
{
src: []Property{*createProperty("a", "string", ""), *createProperty("b", "string", "")},
target: []Property{},
expected: []Property{*createProperty("a", "string", ""), *createProperty("b", "string", "")},
description: "Nest Unique fails when all elements are unique",
},
{
src: []Property{*createProperty("a", "string", ""), *createProperty("b", "string", "")},
target: []Property{*createProperty("c", "string", "")},
expected: []Property{*createProperty("a", "string", ""), *createProperty("b", "string", ""), *createProperty("c", "string", "")},
description: "Nest Unique fails when all elements are unique",
},
{
src: []Property{*createProperty("a", "string", ""), *createProperty("b", "string", "")},
target: []Property{*createProperty("a", "string", "")},
expected: []Property{*createProperty("a", "string", ""), *createProperty("b", "string", "")},
description: "Nest Unique fails when nested elements are duplicate",
},
}
errMsgTemplate := "%s. Expected: %q, Actual: %q"
for _, testCase := range testCases {
actual := nestUnique(testCase.src, testCase.target)
if len(actual) != len(testCase.expected) {
t.Errorf(errMsgTemplate, testCase.description, testCase.expected, actual)
}
for i := 0; i < len(actual); i++ {
if !actual[i].Equal(testCase.expected[i]) {
t.Errorf(errMsgTemplate, testCase.description, testCase.expected[i], actual[i])
}
}
}
}
// Creates a struct using reflection and return its type
func reflectionStructType(fields map[string]interface{}) reflect.Type {
var structFields []reflect.StructField
for fieldname, obj := range fields {
structField := reflect.StructField{
Name: fieldname,
Type: reflect.TypeOf(obj),
}
structFields = append(structFields, structField)
}
return reflect.StructOf(structFields)
}
// Creates a Property object with a subset of its props populated
func createProperty(propName string, propType string, propDocs string) *Property {
return &Property{Name: propName, Type: propType, Text: formatText(propDocs)}
}
// Asserts that two Property objects are "similar"
// Name, Type and Text properties are checked for similarity
func assertProperties(t *testing.T, expected Property, actual Property) {
assertStrings(t, expected.Name, actual.Name)
assertStrings(t, expected.Type, actual.Type)
assertStrings(t, strings.TrimSpace(string(expected.Text)), strings.TrimSpace(string(actual.Text)))
}
func assertStrings(t *testing.T, expected string, actual string) {
if expected != actual {
t.Errorf("expected: %s, actual: %s", expected, actual)
}
}
func actualProperties(t *testing.T, props []Property) []string { func actualProperties(t *testing.T, props []Property) []string {
t.Helper() t.Helper()

View file

@ -83,7 +83,7 @@ func (r *Reader) ModuleType(name string, factory reflect.Value) (*ModuleType, er
// Return the PropertyStruct associated with a property struct type. The type should be in the // Return the PropertyStruct associated with a property struct type. The type should be in the
// format <package path>.<type name> // format <package path>.<type name>
func (r *Reader) PropertyStruct(pkgPath, name string, defaults reflect.Value) (*PropertyStruct, error) { func (r *Reader) propertyStruct(pkgPath, name string, defaults reflect.Value) (*PropertyStruct, error) {
ps := r.getPropertyStruct(pkgPath, name) ps := r.getPropertyStruct(pkgPath, name)
if ps == nil { if ps == nil {
@ -113,6 +113,43 @@ func (r *Reader) PropertyStruct(pkgPath, name string, defaults reflect.Value) (*
return ps, nil return ps, nil
} }
// Return the PropertyStruct associated with a struct type using recursion
// This method is useful since golang structs created using reflection have an empty PkgPath()
func (r *Reader) PropertyStruct(pkgPath, name string, defaults reflect.Value) (*PropertyStruct, error) {
var props []Property
// Base case: primitive type
if defaults.Kind() != reflect.Struct {
props = append(props, Property{Name: name,
Type: defaults.Type().String()})
return &PropertyStruct{Properties: props}, nil
}
// Base case: use r.propertyStruct if struct has a non empty pkgpath
if pkgPath != "" {
return r.propertyStruct(pkgPath, name, defaults)
}
numFields := defaults.NumField()
for i := 0; i < numFields; i++ {
field := defaults.Type().Field(i)
// Recurse
ps, err := r.PropertyStruct(field.Type.PkgPath(), field.Type.Name(), reflect.New(field.Type).Elem())
if err != nil {
return nil, err
}
prop := Property{
Name: strings.ToLower(field.Name),
Text: formatText(ps.Text),
Type: field.Type.Name(),
Properties: ps.Properties,
}
props = append(props, prop)
}
return &PropertyStruct{Properties: props}, nil
}
func (r *Reader) getModuleTypeDoc(pkgPath, factoryFuncName string) (string, error) { func (r *Reader) getModuleTypeDoc(pkgPath, factoryFuncName string) (string, error) {
goPkg, err := r.goPkg(pkgPath) goPkg, err := r.goPkg(pkgPath)
if err != nil { if err != nil {