Skip to content

Commit

Permalink
Merge pull request #68 from dogmatiq/annotation
Browse files Browse the repository at this point in the history
Add annotations.
  • Loading branch information
jmalloc authored Aug 20, 2024
2 parents 7d1548c + 20c7603 commit 4ff2475
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 3 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ The format is based on [Keep a Changelog], and this project adheres to
[keep a changelog]: https://keepachangelog.com/en/1.0.0/
[semantic versioning]: https://semver.org/spec/v2.0.0.html

## [Unreleased]

### Added

- Added `Annotator` type and `Config.Annotators` configuration, to add
user-defined annotations to values.

## [0.5.3] - 2024-04-08

### Fixed
Expand Down
7 changes: 7 additions & 0 deletions annotator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dapper

// Annotator is a function that annotates a value with additional information.
//
// If it returns a non-empty string, the string is rendered after the value,
// regardless of whether the value is rendered by a filter.
type Annotator func(Value) string
176 changes: 176 additions & 0 deletions annotator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package dapper_test

import (
"errors"
"reflect"
"testing"

. "github.com/dogmatiq/dapper"
)

func TestPrinter_Annotator(t *testing.T) {
cases := []struct {
Name string
Value any
Annotators []Annotator
Output []string
}{
{
Name: "empty annotation",
Value: 123,
Annotators: []Annotator{
func(Value) string { return "" },
},
Output: []string{
`int(123)`,
},
},
{
Name: "annotated nil value",
Value: nil,
Annotators: []Annotator{
func(Value) string { return "this is nil" },
},
Output: []string{
`any(nil) <<this is nil>>`,
},
},
{
Name: "annotated non-nil value",
Value: 123,
Annotators: []Annotator{
func(Value) string { return "this is not nil" },
},
Output: []string{
"int(123) <<this is not nil>>",
},
},
{
Name: "multiple annotations",
Value: 123,
Annotators: []Annotator{
func(Value) string { return "first" },
func(Value) string { return "" },
func(Value) string { return "second" },
},
Output: []string{
`int(123) <<first, second>>`,
},
},
{
Name: "multiline rendered value",
Value: struct {
Value int
}{},
Annotators: []Annotator{
func(v Value) string {
if v.DynamicType.Kind() == reflect.Struct {
return "an anonymous struct"
}
return ""
},
},
Output: []string{
`{`,
` Value: int(0)`,
`} <<an anonymous struct>>`,
},
},
{
Name: "annotation of nested values",
Value: struct{ Value int }{},
Annotators: []Annotator{
func(v Value) string {
if v.DynamicType.Kind() == reflect.Struct {
return "outer"
}
return "inner"
},
},
Output: []string{
`{`,
` Value: int(0) <<inner>>`,
`} <<outer>>`,
},
},
{
Name: "annotation of value that is rendered by a filter",
Value: errors.New("<error>"),
Annotators: []Annotator{
func(v Value) string {
if v.Value.CanInterface() {
if _, ok := v.Value.Interface().(error); ok {
return "an annotated error"
}
}
return ""
},
},
Output: []string{
`*errors.errorString{`,
` s: "<error>"`,
`} [<error>] <<an annotated error>>`,
},
},
{
Name: "annotation of recursion marker",
Value: func() any {
type T struct {
Self *T
Other int
}

var v T
v.Self = &v

return &v
}(),
Annotators: []Annotator{
func(v Value) string {
if v.DynamicType.String() == "*dapper_test.T" {
return "a recursive value"
}
return ""
},
},
Output: []string{
`*github.com/dogmatiq/dapper_test.T{`,
` Self: <recursion> <<a recursive value>>`,
` Other: 0`,
`} <<a recursive value>>`,
},
},
{
Name: "annotation of zero value marker",
Value: func() any {
type named struct {
Value int
}
return named{}
}(),
Annotators: []Annotator{
func(v Value) string {
return "a zero value"
},
},
Output: []string{
`github.com/dogmatiq/dapper_test.named{<zero>} <<a zero value>>`,
},
},
}

for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
cfg := DefaultPrinter.Config
cfg.Annotators = c.Annotators

testWithConfig(
t,
cfg,
c.Name,
c.Value,
c.Output...,
)
})
}
}
33 changes: 33 additions & 0 deletions printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,30 @@ const (
// DefaultRecursionMarker is the default string to display when recursion
// is detected within a Go value.
DefaultRecursionMarker = "<recursion>"

// DefaultAnnotationPrefix is the default string to display before
// annotations.
DefaultAnnotationPrefix = "<<"

// DefaultAnnotationSuffix is the default string to display after
// annotations.
DefaultAnnotationSuffix = ">>"
)

// Config holds the configuration for a printer.
type Config struct {
// Filters is the set of filters to apply when formatting values.
//
// Filters are applied in the order they are provided. If any filter renders
// output all subsequent filters and the default rendering logic are
// skipped. Any annotations are still applied.
Filters []Filter

// Annotators is a set of functions that can annotate values with additional
// information, regardless of whether the value is rendered by a filter or
// the default rendering logic.
Annotators []Annotator

// Indent is the string used to indent nested values.
// If it is empty, [DefaultIndent] is used.
Indent string
Expand All @@ -44,6 +61,13 @@ type Config struct {
// If it is empty, [DefaultRecursionMarker] is used instead.
RecursionMarker string

// AnnotationPrefix and AnnotationSuffix are the strings that are displayed
// before and after annotations, respectively.
//
// If they are empty, [DefaultAnnotationOpen] and [DefaultAnnotationClose]
// are used instead.
AnnotationPrefix, AnnotationSuffix string

// OmitPackagePaths, when true, causes the printer to omit the
// fully-qualified package path from the rendered type names.
OmitPackagePaths bool
Expand Down Expand Up @@ -97,6 +121,14 @@ func (p *Printer) Write(w io.Writer, v any) (_ int, err error) {
cfg.RecursionMarker = DefaultRecursionMarker
}

if cfg.AnnotationPrefix == "" {
cfg.AnnotationPrefix = DefaultAnnotationPrefix
}

if cfg.AnnotationSuffix == "" {
cfg.AnnotationSuffix = DefaultAnnotationSuffix
}

counter := &stream.Counter{
Target: w,
}
Expand Down Expand Up @@ -173,6 +205,7 @@ func Format(v any) string {

var (
stdoutM sync.Mutex
space = []byte(" ")
newLine = []byte("\n")
)

Expand Down
24 changes: 22 additions & 2 deletions renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,33 @@ func (r *renderer) FormatValue(v Value) string {
}

func (r *renderer) WriteValue(v Value) {
isFilterValue := r.FilterValue != nil && r.FilterValue.Value == v.Value

if !isFilterValue {
var annotations []string
for _, annotate := range r.Configuration.Annotators {
if a := annotate(v); a != "" {
annotations = append(annotations, a)
}
}

if len(annotations) > 0 {
defer func() {
r.Print(
" %s%s%s",
r.Configuration.AnnotationPrefix,
strings.Join(annotations, ", "),
r.Configuration.AnnotationSuffix,
)
}()
}
}

if v.Value.Kind() == reflect.Invalid {
r.Print("any(nil)")
return
}

isFilterValue := r.FilterValue != nil && r.FilterValue.Value == v.Value

if !isFilterValue {
if recursive := r.enter(v); recursive {
if v.IsAmbiguousType() {
Expand Down
20 changes: 19 additions & 1 deletion renderer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,34 @@ func test(
lines ...string,
) {
t.Helper()
testWithConfig(
t,
DefaultPrinter.Config,
n,
v,
lines...,
)
}

func testWithConfig(
t *testing.T,
cfg Config,
n string,
v any,
lines ...string,
) {
t.Helper()

x := strings.Join(lines, "\n")
p := &Printer{cfg}

t.Run(
n,
func(t *testing.T) {
t.Helper()

var w strings.Builder
n, err := Write(&w, v)
n, err := p.Write(&w, v)

if err != nil {
t.Fatal(err)
Expand Down

0 comments on commit 4ff2475

Please sign in to comment.