Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for filters. #3

Merged
merged 12 commits into from
Jan 20, 2019
Merged
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ 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 support for filters, allowing custom rendering on a per-value basis
- Added a built-in filter for `reflect.Type`

### Fixed

- Fixed a bug whereby IO errors were not propagated to the caller

## [0.2.0] - 2019-01-19

### Added
Expand Down
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@
[![GoDoc](https://godoc.org/github.com/dogmatiq/dapper?status.svg)](https://godoc.org/github.com/dogmatiq/dapper)
[![Go Report Card](https://goreportcard.com/badge/github.com/dogmatiq/dapper)](https://goreportcard.com/report/github.com/dogmatiq/dapper)

Dapper is a pretty-printer for Go values that aims to produce the shortest
possible output without ambiguity. Additionally, the output is deterministic,
which allows for the generation of human-readable diffs using standard tools.
Dapper is a pretty-printer for Go values.

It is not intended to be used directly as a debugging tool, but as a library
for applications that need to describe Go values to humans, such as testing
frameworks.

Some features include:

- Concise formatting, without type ambiguity
- Deterministic output, useful for generating diffs using standard tools
- A filtering system for producing customized output on a per-value basis

## Example

Expand Down
30 changes: 25 additions & 5 deletions array.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (
)

// visitArray formats values with a kind of reflect.Array or Slice.
func (vis *visitor) visitArray(w io.Writer, v value) {
if v.IsAmbiguousType {
func (vis *visitor) visitArray(w io.Writer, v Value) {
if v.IsAmbiguousType() {
vis.write(w, v.TypeName())
}

Expand All @@ -23,11 +23,31 @@ func (vis *visitor) visitArray(w io.Writer, v value) {
vis.write(w, "}")
}

func (vis *visitor) visitArrayValues(w io.Writer, v value) {
ambiguous := v.Type.Elem().Kind() == reflect.Interface
func (vis *visitor) visitArrayValues(w io.Writer, v Value) {
staticType := v.DynamicType.Elem()
isInterface := staticType.Kind() == reflect.Interface

for i := 0; i < v.Value.Len(); i++ {
vis.visit(w, v.Value.Index(i), ambiguous)
elem := v.Value.Index(i)

// unwrap interface values so that elem has it's actual type/kind, and not
// that of reflect.Interface.
if isInterface && !elem.IsNil() {
elem = elem.Elem()
}

vis.visit(
w,
Value{
Value: elem,
DynamicType: elem.Type(),
StaticType: staticType,
IsAmbiguousDynamicType: isInterface,
IsAmbiguousStaticType: false,
IsUnexported: v.IsUnexported,
},
)

vis.write(w, "\n")
}
}
82 changes: 82 additions & 0 deletions filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package dapper

import (
"io"
"reflect"

"github.com/dogmatiq/iago"
)

// Filter is a function that provides custom formatting logic for specific
// values.
//
// It writes a formatted representation of v to w, and returns the number of
// bytes written.
//
// If the number of bytes written is non-zero, the default formatting logic is
// skipped.
//
// Particular attention should be paid to the v.IsUnexported field. If this flag
// is true, many operations on v.Value are unavailable.
type Filter func(w io.Writer, v Value) (int, error)

// reflectTypeType is the reflect.Type for reflect.Type itself.
var reflectTypeType = reflect.TypeOf((*reflect.Type)(nil)).Elem()

// ReflectTypeFilter is a filter that formats reflect.Type values.
func ReflectTypeFilter(w io.Writer, v Value) (n int, err error) {
defer iago.Recover(&err)

if !v.DynamicType.Implements(reflectTypeType) {
return 0, nil
}

if v.DynamicType.Kind() == reflect.Interface && v.Value.IsNil() {
return 0, nil
}

ambiguous := false

if v.IsAmbiguousStaticType {
// always render the type if the static type is ambiguous
ambiguous = true
} else if v.IsAmbiguousDynamicType {
// only consider the dynamic type to be ambiguous if the static type isn't reflect.Type
// we're not really concerned with rendering the underlying implementation's type.
ambiguous = v.StaticType != reflectTypeType
}

if ambiguous {
n += iago.MustWriteString(w, "reflect.Type(")
}

if v.IsUnexported {
n += iago.MustWriteString(w, "<unknown>")
} else {
t := v.Value.Interface().(reflect.Type)

if s := t.PkgPath(); s != "" {
n += iago.MustWriteString(w, s)
n += iago.MustWriteString(w, ".")
}

if s := t.Name(); s != "" {
n += iago.MustWriteString(w, s)
} else {
n += iago.MustWriteString(w, t.String())
}

}

// always render the pointer value for the type, this way when the field is
// unexported we still get something we can compare to known types instead of a
// rendering of the reflect.rtype struct.
n += iago.MustWriteString(w, " ")
n += iago.MustWriteString(w, formatPointerHex(v.Value.Pointer(), false))

if ambiguous {
n += iago.MustWriteString(w, ")")
}

return
}
101 changes: 101 additions & 0 deletions filter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package dapper_test

import (
"fmt"
"reflect"
"testing"
)

type reflectType struct {
Exported reflect.Type
unexported reflect.Type
}

var (
intType = reflect.TypeOf(0)
intTypePointer = formatReflectTypePointer(intType)
mapType = reflect.TypeOf(map[string]string{})
mapTypePointer = formatReflectTypePointer(mapType)
namedType = reflect.TypeOf(named{})
namedTypePointer = formatReflectTypePointer(namedType)
)

func formatReflectTypePointer(t reflect.Type) string {
return fmt.Sprintf("0x%x", reflect.ValueOf(t).Pointer())
}

func TestPrinter_ReflectTypeFilter(t *testing.T) {
test(
t,
"built-in type",
intType,
"reflect.Type(int "+intTypePointer+")",
)

test(
t,
"built-in parameterized type",
mapType,
"reflect.Type(map[string]string "+mapTypePointer+")",
)

test(
t,
"named type",
reflect.TypeOf(named{}),
"reflect.Type(github.com/dogmatiq/dapper_test.named "+namedTypePointer+")",
)

typ := reflect.TypeOf(struct{ Int int }{})
test(
t,
"anonymous struct",
typ,
"reflect.Type(struct { Int int } "+formatReflectTypePointer(typ)+")",
)

typ = reflect.TypeOf((*interface{ Int() int })(nil)).Elem()
test(
t,
"anonymous interface",
typ,
"reflect.Type(interface { Int() int } "+formatReflectTypePointer(typ)+")",
)

test(
t,
"includes type when in an anonymous struct",
struct {
Type reflect.Type
}{
reflect.TypeOf(0),
},
"{",
" Type: reflect.Type(int "+intTypePointer+")",
"}",
)

test(
t,
"does not include type if static type is also reflect.Type",
reflectType{
Exported: reflect.TypeOf(0),
},
"dapper_test.reflectType{",
" Exported: int "+intTypePointer,
" unexported: nil",
"}",
)

test(
t,
"still renders the pointer address when the value is unexported",
reflectType{
unexported: reflect.TypeOf(0),
},
"dapper_test.reflectType{",
" Exported: nil",
" unexported: <unknown> "+intTypePointer,
"}",
)
}
25 changes: 14 additions & 11 deletions interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ import (
)

// visitInterface formats values with a kind of reflect.Interface.
func (vis *visitor) visitInterface(w io.Writer, v value) {
if v.Value.IsNil() {
if v.IsAmbiguousType {
vis.write(w, v.TypeName())
vis.write(w, "(nil)")
} else {
vis.write(w, "nil")
}

return
func (vis *visitor) visitInterface(w io.Writer, v Value) {
if !v.Value.IsNil() {
// this should never happen, a more appropraite visit method should have been
// chosen based on the value's dynamic type.
panic("unexpectedly called visitInterface() with non-nil interface")
}

vis.visit(w, v.Value.Elem(), v.IsAmbiguousType)
// for a nil interface, we only want to render the type if the STATIC type is
// ambigious, since the only information we have available is the interface
// type itself, not the actual implementation's type.
if v.IsAmbiguousStaticType {
vis.write(w, v.TypeName())
vis.write(w, "(nil)")
} else {
vis.write(w, "nil")
}
}
3 changes: 3 additions & 0 deletions interface_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ type interfaces struct {
Iface interface{}
}

type iface interface{}

func TestPrinter_Interface(t *testing.T) {
// note that capturing a reflect.Value of a nil interface does NOT produces a
// value with a "kind" of reflect.Invalid, NOT reflect.Interface.
test(t, "nil interface", interface{}(nil), "interface{}(nil)")
test(t, "nil named interface", iface(nil), "interface{}(nil)") // interface information is shed when passed to Printer.Write().

test(
t,
Expand Down
Loading