Skip to content

Commit

Permalink
Merge pull request #3 from dogmatiq/1-filters
Browse files Browse the repository at this point in the history
Add support for filters.
  • Loading branch information
jmalloc authored Jan 20, 2019
2 parents 18088a6 + 8c61ce2 commit a41bb53
Show file tree
Hide file tree
Showing 18 changed files with 521 additions and 111 deletions.
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

0 comments on commit a41bb53

Please sign in to comment.