Skip to content

Commit 5b174e0

Browse files
committed
Add Options to prioritize some set of options over another
Added API: Default() Option FilterPriority(opts ...Option) Option FilterPriority returns a new Option where an option, opts[i], is only evaluated if no fundamental options remain after applying all filters in all prior options, opts[:i]. In order to prevent further options from being evaluated, the Default option can be used to ensure that some fundamental option remains. Suppose you have a value tree T, where T1 and T2 are sub-trees within T. Prior to the addition of FilterPriority, it was impossible to do certain things. Example 1: You could not make the following compose together nicely. * Have a set of options OT1 to affect only values under T1. * Have a set of options OT2 to affect only values under T2. * Have a set of options OT to affect only T, but not values under T1 and T2. * Have a set of options O to affect all other values, but no those in T (and by extension those in T1 and T2). Solution 1: FilterPriority( // Since T1 and T2 do not overlap, they could be placed within the // same priority level by grouping them in an Options group. FilterTree(T1, OT1), FilterTree(T2, OT2), FilterTree(T, OT), O, ) Example 2: You could not make the following compose together nicely. * Have a set of options O apply on all nodes except those in T1 and T2. * Instead, we want the default behavior of cmp on T1 and T2. Solution 2: FilterPriority( // Here we show how to group T1 and T2 together to be on the same // priority level. Options{ FilterTree(T1, Default()), FilterTree(T2, Default()), }, O, ) Example 3: You have this: type MyStruct struct { *pb.MyMessage; ... } * Generally, you want to use Comparer(proto.Equal) to ensure that all proto.Messages within the struct are properly compared. However, this type has an embedded proto (generally a bad idea), which causes the MyStruct to satisfy the proto.Message interface and unintentionally causes Equal to use proto.Equal on MyStruct, which crashes. * How can you have Comparer(proto.Equal) apply to all other proto.Message without applying just to MyStruct? Solution 3: FilterPriority( // Only for MyStruct, use the default behavior of Equal, // which is to recurse into the structure of MyStruct. FilterPath(func(p Path) bool { return p.Last().Type() == reflect.TypeOf(MyStruct{}) }, Default()), // Use proto.Equal for all other cases of proto.Message. Comparer(proto.Equal), )
1 parent 8099a97 commit 5b174e0

File tree

2 files changed

+89
-27
lines changed

2 files changed

+89
-27
lines changed

cmp/compare.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,12 @@ var nothing = reflect.Value{}
4949
// • If two values are not of the same type, then they are never equal
5050
// and the overall result is false.
5151
//
52-
// • Let S be the set of all Ignore, Transformer, and Comparer options that
53-
// remain after applying all path filters, value filters, and type filters.
52+
// • Let S be the set of all Ignore, Transformer, Comparer, and Default options
53+
// that remain after applying all implicit and explicit filters.
5454
// If at least one Ignore exists in S, then the comparison is ignored.
55-
// If the number of Transformer and Comparer options in S is greater than one,
56-
// then Equal panics because it is ambiguous which option to use.
55+
// If S contains multiple Default options, they are coalesced into one Default.
56+
// If any Transformer, Comparer, or Default options coexist in S,
57+
// then Equal panics because it is ambiguous which to use.
5758
// If S contains a single Transformer, then use that to transform the current
5859
// values and recursively call Equal on the output values.
5960
// If S contains a single Comparer, then use that to compare the current values.

cmp/options.go

Lines changed: 84 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ import (
1414
)
1515

1616
// Option configures for specific behavior of Equal and Diff. In particular,
17-
// the fundamental Option functions (Ignore, Transformer, and Comparer),
17+
// the fundamental options (Ignore, Transformer, Comparer, and Default),
1818
// configure how equality is determined.
1919
//
20-
// The fundamental options may be composed with filters (FilterPath and
21-
// FilterValues) to control the scope over which they are applied.
20+
// The fundamental options may be composed with filters (FilterPath, FilterValues,
21+
// and FilterPriority) to control the scope over which they are applied.
2222
//
2323
// The cmp/cmpopts package provides helper functions for creating options that
2424
// may be used with Equal and Diff.
@@ -33,7 +33,7 @@ type Option interface {
3333
}
3434

3535
// applicableOption represents the following types:
36-
// Fundamental: ignore | invalid | *comparer | *transformer
36+
// Fundamental: noop | ignore | invalid | *comparer | *transformer
3737
// Grouping: Options
3838
type applicableOption interface {
3939
Option
@@ -44,8 +44,8 @@ type applicableOption interface {
4444
}
4545

4646
// coreOption represents the following types:
47-
// Fundamental: ignore | invalid | *comparer | *transformer
48-
// Filters: *pathFilter | *valuesFilter
47+
// Fundamental: noop | ignore | invalid | *comparer | *transformer
48+
// Filters: *pathFilter | *valuesFilter | *priorityFilter
4949
type coreOption interface {
5050
Option
5151
isCore()
@@ -57,28 +57,28 @@ func (core) isCore() {}
5757

5858
// Options is a list of Option values that also satisfies the Option interface.
5959
// Helper comparison packages may return an Options value when packing multiple
60-
// Option values into a single Option. When this package processes an Options,
61-
// it will be implicitly expanded into a flat list.
62-
//
63-
// Applying a filter on an Options is equivalent to applying that same filter
64-
// on all individual options held within.
60+
// Option values into a single Option. When this package processes Options
61+
// or nested Options, it will be implicitly expanded into a flat set.
6562
type Options []Option
6663

6764
func (opts Options) filter(s *state, vx, vy reflect.Value, t reflect.Type) (out applicableOption) {
6865
for _, opt := range opts {
6966
switch opt := opt.filter(s, vx, vy, t); opt.(type) {
7067
case ignore:
71-
return ignore{} // Only ignore can short-circuit evaluation
68+
return ignore{} // Highest precedence; can short-circuit filtering
7269
case invalid:
73-
out = invalid{} // Takes precedence over comparer or transformer
74-
case *comparer, *transformer, Options:
70+
out = invalid{} // Second highest precedence
71+
case noop, *comparer, *transformer, Options:
7572
switch out.(type) {
7673
case nil:
7774
out = opt
7875
case invalid:
7976
// Keep invalid
80-
case *comparer, *transformer, Options:
81-
out = Options{out, opt} // Conflicting comparers or transformers
77+
case noop, *comparer, *transformer, Options:
78+
if opt == (noop{}) && out == (noop{}) {
79+
break // Coelesce redundant Default together
80+
}
81+
out = Options{out, opt} // Conflicting Comparer, Tranformer, or Default
8282
}
8383
}
8484
}
@@ -87,7 +87,7 @@ func (opts Options) filter(s *state, vx, vy reflect.Value, t reflect.Type) (out
8787

8888
func (opts Options) apply(s *state, _, _ reflect.Value) bool {
8989
const warning = "ambiguous set of applicable options"
90-
const help = "consider using filters to ensure at most one Comparer or Transformer may apply"
90+
const help = "consider using filters to ensure at most one Comparer, Transformer, or Default may apply"
9191
var ss []string
9292
for _, opt := range flattenOptions(nil, opts) {
9393
ss = append(ss, fmt.Sprint(opt))
@@ -104,11 +104,12 @@ func (opts Options) String() string {
104104
return fmt.Sprintf("Options{%s}", strings.Join(ss, ", "))
105105
}
106106

107-
// FilterPath returns a new Option where opt is only evaluated if filter f
107+
// FilterPath returns a new Option where opt is only applicable if filter f
108108
// returns true for the current Path in the value tree.
109109
//
110-
// The option passed in may be an Ignore, Transformer, Comparer, Options, or
111-
// a previously filtered Option.
110+
// The Option passed in may be a filtered option (via the Filter functions),
111+
// fundamental option (like Ignore, Transformer, Comparer, or Default), or
112+
// Options group containing elements of the former.
112113
func FilterPath(f func(Path) bool, opt Option) Option {
113114
if f == nil {
114115
panic("invalid path filter function")
@@ -137,7 +138,7 @@ func (f pathFilter) String() string {
137138
return fmt.Sprintf("FilterPath(%s, %v)", fn, f.opt)
138139
}
139140

140-
// FilterValues returns a new Option where opt is only evaluated if filter f,
141+
// FilterValues returns a new Option where opt is only applicable if filter f,
141142
// which is a function of the form "func(T, T) bool", returns true for the
142143
// current pair of values being compared. If the type of the values is not
143144
// assignable to T, then this filter implicitly returns false.
@@ -148,8 +149,9 @@ func (f pathFilter) String() string {
148149
// If T is an interface, it is possible that f is called with two values with
149150
// different concrete types that both implement T.
150151
//
151-
// The option passed in may be an Ignore, Transformer, Comparer, Options, or
152-
// a previously filtered Option.
152+
// The Option passed in may be a filtered option (via the Filter functions),
153+
// fundamental option (like Ignore, Transformer, Comparer, or Default), or
154+
// Options group containing elements of the former.
153155
func FilterValues(f interface{}, opt Option) Option {
154156
v := reflect.ValueOf(f)
155157
if !function.IsType(v.Type(), function.ValueFilter) || v.IsNil() {
@@ -187,6 +189,65 @@ func (f valuesFilter) String() string {
187189
return fmt.Sprintf("FilterValues(%s, %v)", fn, f.opt)
188190
}
189191

192+
// FilterPriority returns a new Option where an option, opts[i],
193+
// is only applicable if no fundamental options remain after applying all filters
194+
// in all prior options, opts[:i].
195+
//
196+
// In order to prevent further options from being applicable, the Default option
197+
// can be used to ensure that some fundamental option remains.
198+
//
199+
// The Option passed in may be a filtered option (via the Filter functions),
200+
// fundamental option (like Ignore, Transformer, Comparer, or Default), or
201+
// Options group containing elements of the former.
202+
func FilterPriority(opts ...Option) Option {
203+
var newOpts []Option
204+
for _, opt := range opts {
205+
if opt := normalizeOption(opt); opt != nil {
206+
newOpts = append(newOpts, opt)
207+
}
208+
}
209+
if len(newOpts) > 0 {
210+
return &priorityFilter{opts: newOpts}
211+
}
212+
return nil
213+
}
214+
215+
type priorityFilter struct {
216+
core
217+
opts []Option
218+
}
219+
220+
func (f priorityFilter) filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption {
221+
for _, opt := range f.opts {
222+
if opt := opt.filter(s, vx, vy, t); opt != nil {
223+
return opt
224+
}
225+
}
226+
return nil
227+
}
228+
229+
func (f priorityFilter) String() string {
230+
var ss []string
231+
for _, opt := range f.opts {
232+
ss = append(ss, fmt.Sprint(opt))
233+
}
234+
return fmt.Sprintf("FilterPriority(%s)", strings.Join(ss, ", "))
235+
}
236+
237+
// Default is an Option that configures Equal to stop processing options and
238+
// to proceed to the next evaluation rule (i.e., checking for the Equal method).
239+
// This value is intended to be combined with FilterPriority to act as a
240+
// sentinel type that prevents other options from being applicable.
241+
// It is an error to pass an unfiltered Default option to Equal.
242+
func Default() Option { return noop{} }
243+
244+
type noop struct{ core }
245+
246+
func (noop) isFiltered() bool { return false }
247+
func (noop) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return noop{} }
248+
func (noop) apply(_ *state, _, _ reflect.Value) bool { return false }
249+
func (noop) String() string { return "Default()" }
250+
190251
// Ignore is an Option that causes all comparisons to be ignored.
191252
// This value is intended to be combined with FilterPath or FilterValues.
192253
// It is an error to pass an unfiltered Ignore option to Equal.

0 commit comments

Comments
 (0)