Merge "bpdocs for struct types created using reflection"
This commit is contained in:
commit
87fd4b11af
4 changed files with 186 additions and 9 deletions
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue