Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 93 additions & 51 deletions reference.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,22 @@ func (r *Reference) ToJsonPointer() jsonpointer.Pointer {

var referenceType = reflect.TypeOf((*Reference)(nil)).Elem()

// pointerIfPossible returns a pointer to the specified value if it is addressable.
// Otherwise, it returns the value itself.
func pointerIfPossible(v reflect.Value) reflect.Value {
if v.CanAddr() {
return v.Addr()
}
return v
}

// findRelativeJsonPointer finds a JSON pointer from base to pointedField. The
// base must be pointer. The pointed field should be a pointer to a field
// within the base; if the pointed field is not found within the base, nil is
// returned.
func findRelativeJsonPointer(base reflect.Value, pointedField reflect.Value) jsonpointer.Pointer {
// If possible, use a pointer to the base; maybe JsonReferenceResolver is implemented as a pointer method
base = pointerIfPossible(base)
for {
if base.Equal(pointedField) {
return jsonpointer.Pointer{}
Expand Down Expand Up @@ -86,7 +97,7 @@ func findRelativeJsonPointer(base reflect.Value, pointedField reflect.Value) jso
if !typefield.IsExported() {
continue
}
pointer := findRelativeJsonPointer(field.Addr(), pointedField)
pointer := findRelativeJsonPointer(field, pointedField)
if pointer == nil {
continue
}
Expand All @@ -99,13 +110,26 @@ func findRelativeJsonPointer(base reflect.Value, pointedField reflect.Value) jso
return nil
case reflect.Slice, reflect.Array:
for i := 0; i < base.Len(); i++ {
pointer := findRelativeJsonPointer(base.Index(i).Addr(), pointedField)
elem := base.Index(i)
pointer := findRelativeJsonPointer(elem, pointedField)
if pointer == nil {
continue
}
return jsonpointer.New(strconv.Itoa(i)).Append(pointer...)
}
return nil
case reflect.Map:
if base.Type().Key().Kind() != reflect.String {
return nil
}
for _, key := range base.MapKeys() {
pointer := findRelativeJsonPointer(base.MapIndex(key), pointedField)
if pointer == nil {
continue
}
return jsonpointer.New(key.String()).Append(pointer...)
}
return nil
default:
return nil
}
Expand Down Expand Up @@ -150,63 +174,81 @@ type TextReferenceResolver interface {
}

func findTextLabel(base reflect.Value, pointedField reflect.Value) (string, bool) {
if base.Addr().Equal(pointedField) {
return "", true
if base.CanAddr() {
if base.Addr().Equal(pointedField) {
return "", true
}
}
if base.Kind() == reflect.Pointer || base.Kind() == reflect.Interface {
base = base.Elem()
}
if base.Kind() != reflect.Struct {
return "", false
}
for i := 0; i < base.NumField(); i++ {
field := base.Field(i)
typefield := base.Type().Field(i)
if !typefield.IsExported() {
continue
}
var fieldPointer = field
if field.Kind() != reflect.Pointer {
fieldPointer = field.Addr()
}
var label string
var labelFound bool
var labelVirtual bool
resolver, isResolver := fieldPointer.Interface().(TextReferenceResolver)
if fieldPointer.Equal(pointedField) {
label, labelFound = "", true
} else if isResolver {
label, labelVirtual, labelFound = resolver.RelativeTextPointer(pointedField.Interface())
} else {
label, labelFound = findTextLabel(field, pointedField)
}
if !labelFound {
continue
}
textlogTag := typefield.Tag.Get("textlog")
tagModifiers := strings.Split(textlogTag, ",")
fieldlabel := strings.ToUpper(tagModifiers[0])
tagModifiers = tagModifiers[1:]
var fullLabel string
if typefield.Anonymous || slices.Contains(tagModifiers, TextlogModifierExpand) || isResolver {
if labelVirtual {
// Virtual fields are actually not present in the text log (hence the
// name), so it does not make sense to combine the existent field label
// with the non-existent subfield label which would confuse the reader.
fullLabel = label
switch base.Kind() {
case reflect.Struct:
for i := 0; i < base.NumField(); i++ {
field := base.Field(i)
typefield := base.Type().Field(i)
if !typefield.IsExported() {
continue
}
var fieldPointer = field
if field.Kind() != reflect.Pointer && field.CanAddr() {
fieldPointer = field.Addr()
}
var label string
var labelFound bool
var labelVirtual bool
resolver, isResolver := fieldPointer.Interface().(TextReferenceResolver)
if fieldPointer.Equal(pointedField) {
label, labelFound = "", true
} else if isResolver {
label, labelVirtual, labelFound = resolver.RelativeTextPointer(pointedField.Interface())
} else {
fullLabel = ConcatTextLabels(fieldlabel, label)
label, labelFound = findTextLabel(field, pointedField)
}
} else if label == "" {
fullLabel = fieldlabel
} else {
// Unexpanded fields should not use labels of any subfield because it
// cannot be resolved unambiguously.
return "", false
if !labelFound {
continue
}
textlogTag := typefield.Tag.Get("textlog")
tagModifiers := strings.Split(textlogTag, ",")
fieldlabel := strings.ToUpper(tagModifiers[0])
tagModifiers = tagModifiers[1:]
var fullLabel string
if typefield.Anonymous || slices.Contains(tagModifiers, TextlogModifierExpand) || isResolver {
if labelVirtual {
// Virtual fields are actually not present in the text log (hence the
// name), so it does not make sense to combine the existent field label
// with the non-existent subfield label which would confuse the reader.
fullLabel = label
} else {
fullLabel = ConcatTextLabels(fieldlabel, label)
}
} else if label == "" {
fullLabel = fieldlabel
} else {
// Unexpanded fields should not use labels of any subfield because it
// cannot be resolved unambiguously.
return "", false
}
return fullLabel, true
}
return fullLabel, true
return "", false
case reflect.Slice, reflect.Array:
for i := 0; i < base.Len(); i++ {
element := base.Index(i)
label, labelFound := findTextLabel(element, pointedField)
if !labelFound {
continue
}
return ConcatTextLabels(label, strconv.Itoa(i+1)), true
}
return "", false
case reflect.Map:
// Map values aren't addressable and thus the pointedField can't point to them;
// and they aren't expanded, therefore any references to subfields of map values also have no text label.
return "", false
default:
return "", false
}
return "", false
}

func ConcatTextLabels(prefix string, label string) string {
Expand Down
43 changes: 32 additions & 11 deletions reference_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ type testObject struct {
Resolver TestResolver `json:"resolver" textlog:"resolver"`

SubObject *SubObject `json:"subobject" textlog:"subobject,expand"`

Slice []SliceSubstruct `json:"slice" textlog:"slice,expand"`

Map map[string]*MapSubstruct `json:"map" textlog:"map,expand"`
}

type AnonymousSubstruct struct {
Expand Down Expand Up @@ -91,7 +95,19 @@ type SubObject struct {
Subfield8 string `json:"subfield8" textlog:"subfield8"`
}

func TestReference_ToJsonPointer(t *testing.T) {
type SliceSubstruct struct {
Subfield9 string `json:"subfield9" textlog:"subfield9"`
}

type MapSubstruct struct {
Subfield10 string `json:"subfield10" textlog:"subfield10"`
}

func (m MapSubstruct) String() string {
return m.Subfield10
}

func makeTestObject() testObject {
var test testObject
test.Substruct.SubField1 = "subfield1"
test.SubField2 = "subfield2"
Expand All @@ -102,6 +118,15 @@ func TestReference_ToJsonPointer(t *testing.T) {
test.Resolver.Subfield7 = "subfield7"
test.SubObject = &SubObject{Subfield8: "subfield8"}
test.Recursive = NewReference(&test, &test.Substruct)
test.Slice = []SliceSubstruct{{Subfield9: "slice"}}
test.Map = map[string]*MapSubstruct{mapKey: {Subfield10: "map"}}
return test
}

const mapKey = "key"

func TestReference_ToJsonPointer(t *testing.T) {
test := makeTestObject()

var tests = []struct {
PointedField any
Expand All @@ -121,6 +146,8 @@ func TestReference_ToJsonPointer(t *testing.T) {
{&test.Resolver.Subfield7, "/resolver/subfield7"},
{&test.SubObject, "/subobject"},
{&test.SubObject.Subfield8, "/subobject/subfield8"},
{&test.Slice[0].Subfield9, "/slice/0/subfield9"},
{&test.Map[mapKey].Subfield10, "/map/key/subfield10"},
// test.Recursive not required here: if there is a flaw in the pointer
// search logic, it will panic when encountering this cycle.
}
Expand All @@ -133,16 +160,7 @@ func TestReference_ToJsonPointer(t *testing.T) {
}

func TestReference_ToTextPointer(t *testing.T) {
var test testObject
test.Substruct.SubField1 = "subfield1"
test.SubField2 = "subfield2"
test.Nested.Substruct.SubField3 = "subfield3"
test.Unexpanded.SubField4 = "subfield4"
test.Subfield5 = "subfield5"
test.Resolver.Subfield6 = "subfield6"
test.Resolver.Subfield7 = "subfield7"
test.SubObject = &SubObject{Subfield8: "subfield8"}
test.Recursive = NewReference(&test, &test.Substruct)
test := makeTestObject()

var tests = []struct {
PointedField any
Expand All @@ -162,6 +180,9 @@ func TestReference_ToTextPointer(t *testing.T) {
{&test.Resolver.Subfield7, "subfield7"},
{&test.SubObject, "SUBOBJECT"},
{&test.SubObject.Subfield8, "SUBOBJECT_SUBFIELD8"},
{&test.Slice[0].Subfield9, "SLICE_SUBFIELD9_1"},
// Map values aren't expanded and their subfields therefore aren't reachable in textlog
{&test.Map[mapKey].Subfield10, ""},
}
for _, tt := range tests {
t.Run(tt.want, func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion textlog.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ func (t TextlogFormatter) toEntry(object reflect.Value) TextlogEntry {
var details TextlogEntry
for _, key := range object.MapKeys() {
details = append(details, TextlogValuePair{
Key: key.String(),
Key: strings.ToUpper(key.String()),
Value: t.format(object.MapIndex(key).Interface(), nil),
})
}
Expand Down
13 changes: 3 additions & 10 deletions textlog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,7 @@ func TestToDetails(t *testing.T) {
}

func TestTextlogFormatting(t *testing.T) {
var test testObject
test.Substruct.SubField1 = "subfield1"
test.SubField2 = "subfield2"
test.Nested.Substruct.SubField3 = "subfield3"
test.Unexpanded.SubField4 = "subfield4"
test.Subfield5 = "subfield5"
test.Resolver.Subfield6 = "subfield6"
test.Resolver.Subfield7 = "subfield7"
test.SubObject = &SubObject{Subfield8: "subfield8"}
test.Recursive = NewReference(&test, &test.Substruct)
test := makeTestObject()

formatter := TextlogFormatter{
FormatValue: func(data any, modifiers []string) string {
Expand All @@ -93,5 +84,7 @@ func TestTextlogFormatting(t *testing.T) {
{"SUBFIELD5", "subfield5"},
{"RESOLVER", "subfield6, subfield7"},
{"SUBOBJECT_SUBFIELD8", "subfield8"},
{"SLICE_SUBFIELD9_1", "slice"},
{"MAP_KEY", "map"},
}, details)
}
Loading