diff --git a/reference.go b/reference.go index 0577b91..00ffd67 100644 --- a/reference.go +++ b/reference.go @@ -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{} @@ -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 } @@ -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 } @@ -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 { diff --git a/reference_test.go b/reference_test.go index e775b92..e3ced5e 100644 --- a/reference_test.go +++ b/reference_test.go @@ -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 { @@ -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" @@ -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 @@ -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. } @@ -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 @@ -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) { diff --git a/textlog.go b/textlog.go index 775ff2b..4c59f12 100644 --- a/textlog.go +++ b/textlog.go @@ -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), }) } diff --git a/textlog_test.go b/textlog_test.go index 8ffaa0f..5be914c 100644 --- a/textlog_test.go +++ b/textlog_test.go @@ -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 { @@ -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) }