Skip to content

Commit

Permalink
Add functional options.
Browse files Browse the repository at this point in the history
  • Loading branch information
jmalloc committed Aug 20, 2024
1 parent b39ac38 commit 6835469
Show file tree
Hide file tree
Showing 11 changed files with 212 additions and 115 deletions.
32 changes: 30 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,38 @@ The format is based on [Keep a Changelog], and this project adheres to

## [Unreleased]

This release is largely centered around a refactoring of the `Config` type and
the way that a `Printer` is configured. It should not affect most users, but
does introduce some breaking changes for filter authors.

### Added

- Added `Annotator` type and `Config.Annotators` configuration, to add
user-defined annotations to values.
- Added `Annotator`, `Config.Annotators` and `WithAnnotator`, to add
user-defined text annotations to rendered values.
- Added `NewPrinter()` function, that accepts the following functional options:
- `WithFilter()`
- `WithDefaultFilters()`
- `WithAnnotator()`
- `WithUnexportedStructFields()`
- `WithPackagePaths()`

### Changed

- **[BC]** Replaced `Config.OmitUnexportedFields` with `RenderUnexportedStructFields`, note the logic is inverted.
- **[BC]** Replaced `Config.OmitPackagePaths` with `RenderPackagePaths`, note the logic is inverted.
- Bumped the minimum supported Go version to 1.21.

### Removed

- **[BC]** Removed `DefaultPrinter`, use `NewPrinter()` instead.
- **[BC]** Removed `Config.Indent` and `DefaultIndent` constant.
- **[BC]** Removed `Config.RecursionMarker` and `DefaultRecursionMarker` constant.
- **[BC]** Removed `Config.ZeroValueMarker` and `DefaultZeroValueMarker` constant.

### Fixed

- `Renderer.Config()` and `Renderer.WithModifiedConfig()` now properly clone the
slices within `Config`.

## [0.5.3] - 2024-04-08

Expand Down
10 changes: 6 additions & 4 deletions annotator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,14 @@ func TestPrinter_Annotator(t *testing.T) {

for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
cfg := DefaultPrinter.Config
cfg.Annotators = c.Annotators
var options []Option
for _, a := range c.Annotators {
options = append(options, WithAnnotator(a))
}

testWithConfig(
testWithPrinter(
t,
cfg,
NewPrinter(options...),
c.Name,
c.Value,
c.Output...,
Expand Down
9 changes: 9 additions & 0 deletions filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,12 @@ package dapper
// The filter uses r to render v. If r is unused v is rendered using the default
// formatting logic.
type Filter func(r Renderer, v Value)

var defaultFilters = []Filter{
StringerFilter, // always first
ErrorFilter,
ProtoFilter,
ReflectFilter,
SyncFilter,
TimeFilter,
}
2 changes: 1 addition & 1 deletion filter_proto.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ func ProtoFilter(r Renderer, v Value) {
r.
WithModifiedConfig(
func(c *Config) {
c.OmitUnexportedFields = true
c.RenderUnexportedStructFields = false
},
).
WriteValue(v)
Expand Down
54 changes: 26 additions & 28 deletions filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
. "github.com/dogmatiq/dapper"
)

func TestPrinter_Filter(t *testing.T) {
func TestPrinter_WithFilter(t *testing.T) {
t.Run("it is passed a valid format function", func(t *testing.T) {
type testType struct {
i int
Expand All @@ -18,34 +18,32 @@ func TestPrinter_Filter(t *testing.T) {
i: 100,
}

p := &Printer{
Config: Config{
Filters: []Filter{
func(r Renderer, v Value) {
if v.DynamicType != reflect.TypeOf(testType{}) {
return
}

r.Print("github.com/dogmatiq/dapper_test.testType<")

fv := v.Value.FieldByName("i")

r.WriteValue(
Value{
Value: fv,
DynamicType: fv.Type(),
StaticType: fv.Type(),
IsAmbiguousDynamicType: false,
IsAmbiguousStaticType: false,
IsUnexported: true,
},
)

r.Print(">")
},
p := NewPrinter(
WithFilter(
func(r Renderer, v Value) {
if v.DynamicType != reflect.TypeOf(testType{}) {
return
}

r.Print("github.com/dogmatiq/dapper_test.testType<")

fv := v.Value.FieldByName("i")

r.WriteValue(
Value{
Value: fv,
DynamicType: fv.Type(),
StaticType: fv.Type(),
IsAmbiguousDynamicType: false,
IsAmbiguousStaticType: false,
IsUnexported: true,
},
)

r.Print(">")
},
},
}
),
)

expected := fmt.Sprintf("github.com/dogmatiq/dapper_test.testType<%d>", tt.i)
t.Log("expected:\n\n" + expected + "\n")
Expand Down
10 changes: 5 additions & 5 deletions kind_struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ func renderStructKind(r Renderer, v Value) {
}

func renderStructFields(r Renderer, v Value) error {
omitUnexported := r.Config().OmitUnexportedFields
alignment := longestFieldName(v.DynamicType, omitUnexported)
renderUnexported := r.Config().RenderUnexportedStructFields
alignment := longestFieldName(v.DynamicType, renderUnexported)

for i := 0; i < v.DynamicType.NumField(); i++ {
f := v.DynamicType.Field(i)
if omitUnexported && isUnexportedField(f) {
if !renderUnexported && isUnexportedField(f) {
continue
}

Expand Down Expand Up @@ -79,12 +79,12 @@ func isUnexportedField(f reflect.StructField) bool {
}

// longestFieldName returns the length of the longest field name in a struct.
func longestFieldName(rt reflect.Type, exportedOnly bool) int {
func longestFieldName(rt reflect.Type, includeUnexported bool) int {
width := 0

for i := 0; i < rt.NumField(); i++ {
f := rt.Field(i)
if !exportedOnly || !isUnexportedField(f) {
if includeUnexported || !isUnexportedField(f) {
n := len(f.Name)

if n > width {
Expand Down
6 changes: 2 additions & 4 deletions kind_struct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,8 @@ func TestPrinter_StructFieldTypes(t *testing.T) {
}

// Verifies not exported fields in a struct are omitted when configured to do so
func TestPrinter_StructUnexportedFieldsWithOmitUnexpoted(t *testing.T) {
config := DefaultPrinter.Config
config.OmitUnexportedFields = true
printer := &Printer{Config: config}
func TestPrinter_WithUnexportedStructFields(t *testing.T) {
printer := NewPrinter(WithUnexportedStructFields(false))
writer := &strings.Builder{}

_, err := printer.Write(writer, struct {
Expand Down
140 changes: 103 additions & 37 deletions printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"io"
"os"
"reflect"
"slices"
"strings"
"sync"

Expand All @@ -26,36 +27,114 @@ const (
annotationSuffix = ">>"
)

// Config holds the configuration for a printer.
// Printer generates human-readable representations of Go values.
//
// The output format is intended to be as minimal as possible, without being
// ambiguous. To that end, type information is only included where it can not be
// reliably inferred from the structure of the value.
type Printer struct {
cfg Config
}

// Option controls the behavior of a printer.
type Option func(*Config)

// Config is the configuration for a printer.
type Config struct {
// 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

// 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
Filters []Filter
applyDefaultFilters bool

// 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
// RenderPackagePaths, when true, causes the printer to render the
// fully-qualified package path when rendering type names.
RenderPackagePaths bool

// OmitPackagePaths, when true, causes the printer to omit the
// fully-qualified package path from the rendered type names.
OmitPackagePaths bool
// RenderUnexportedStructFields, when true, causes the printer to render
// unexported struct fields.
RenderUnexportedStructFields bool
}

// OmitUnexportedFields omits unexported struct fields when set to true
OmitUnexportedFields bool
func (c Config) clone() Config {
c.Annotators = slices.Clone(c.Annotators)
c.Filters = slices.Clone(c.Filters)
return c
}

// Printer generates human-readable representations of Go values.
// WithAnnotator adds an [Annotator] to the printer.
//
// The output format is intended to be as minimal as possible, without being
// ambiguous. To that end, type information is only included where it can not be
// reliably inferred from the structure of the value.
type Printer struct {
// Config is the configuration for the printer.
Config Config
// Annotators are used to add supplementary textual information to the output of
// the printer. They are applied after the value has been rendered by any
// filters or the default rendering logic.
func WithAnnotator(a Annotator) Option {
return func(cfg *Config) {
cfg.Annotators = append(cfg.Annotators, a)
}
}

// WithFilter adds a [Filter] to be applied when formatting values.
//
// Filters allow overriding the default rendering logic for specific types or
// values. They 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 added by [WithFilter] take precedence over the default filters, which
// can be disabled using [WithDefaultFilters].
func WithFilter(f Filter) Option {
return func(cfg *Config) {
cfg.Filters = append(cfg.Filters, f)
}
}

// WithDefaultFilters enables or disables the default [Filter] set.
func WithDefaultFilters(enabled bool) Option {
return func(cfg *Config) {
cfg.applyDefaultFilters = !enabled
}
}

// WithPackagePaths controls whether the printer renders the fully-qualified
// package path in type names. This option is enabled by default.
func WithPackagePaths(show bool) Option {
return func(cfg *Config) {
cfg.RenderPackagePaths = show
}
}

// WithUnexportedStructFields controls whether the printer renders unexported
// struct fields. This option is enabled by default.
func WithUnexportedStructFields(show bool) Option {
return func(opts *Config) {
opts.RenderUnexportedStructFields = show
}
}

// NewPrinter returns a new [Printer] with the given options applied.
func NewPrinter(options ...Option) *Printer {
cfg := Config{
applyDefaultFilters: true,
RenderPackagePaths: true,
RenderUnexportedStructFields: true,
}

for _, opt := range options {
opt(&cfg)
}

if cfg.applyDefaultFilters {
cfg.Filters = append(cfg.Filters, defaultFilters...)
}

return &Printer{cfg}
}

// panicSentinel is a panic value that wraps an error that must be returned from
Expand All @@ -79,18 +158,16 @@ func (p *Printer) Write(w io.Writer, v any) (_ int, err error) {
}
}()

cfg := p.Config

counter := &stream.Counter{
Target: w,
}

r := &renderer{
cfg: p.cfg,
Indenter: stream.Indenter{
Target: counter,
},
Configuration: cfg,
RecursionSet: map[uintptr]struct{}{},
RecursionSet: map[uintptr]struct{}{},
}

rv := reflect.ValueOf(v)
Expand Down Expand Up @@ -127,31 +204,20 @@ func (p *Printer) Format(v any) string {
return b.String()
}

// DefaultPrinter is the printer used by [Write], [Format] and [Print].
var DefaultPrinter = Printer{
Config: Config{
Filters: []Filter{
StringerFilter, // always first
ErrorFilter,
ProtoFilter,
ReflectFilter,
SyncFilter,
TimeFilter,
},
},
}
// defaultPrinter is the printer used by [Write], [Format] and [Print].
var defaultPrinter = NewPrinter()

// Write writes a pretty-printed representation of v to w using
// [DefaultPrinter].
//
// It returns the number of bytes written.
func Write(w io.Writer, v any) (int, error) {
return DefaultPrinter.Write(w, v)
return defaultPrinter.Write(w, v)
}

// Format returns a pretty-printed representation of v using [DefaultPrinter].
func Format(v any) string {
return DefaultPrinter.Format(v)
return defaultPrinter.Format(v)
}

var (
Expand Down
Loading

0 comments on commit 6835469

Please sign in to comment.