Skip to content

Commit ff2e476

Browse files
committed
Add Options to prioritize some set of options over another
Added API: Continue() 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 Continue 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, Continue()), FilterTree(T2, Continue()), }, 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{}) }, Continue()), // Use proto.Equal for all other cases of proto.Message. Comparer(proto.Equal), )
1 parent 8099a97 commit ff2e476

File tree

2 files changed

+76
-12
lines changed

2 files changed

+76
-12
lines changed

cmp/compare.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ 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 Continue 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.
5555
// If the number of Transformer and Comparer options in S is greater than one,
5656
// then Equal panics because it is ambiguous which option to use.

cmp/options.go

Lines changed: 74 additions & 10 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 Continue),
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,7 +44,7 @@ type applicableOption interface {
4444
}
4545

4646
// coreOption represents the following types:
47-
// Fundamental: ignore | invalid | *comparer | *transformer
47+
// Fundamental: noop | ignore | invalid | *comparer | *transformer
4848
// Filters: *pathFilter | *valuesFilter
4949
type coreOption interface {
5050
Option
@@ -73,13 +73,17 @@ func (opts Options) filter(s *state, vx, vy reflect.Value, t reflect.Type) (out
7373
out = invalid{} // Takes precedence over comparer or transformer
7474
case *comparer, *transformer, Options:
7575
switch out.(type) {
76-
case nil:
76+
case nil, noop:
7777
out = opt
7878
case invalid:
7979
// Keep invalid
8080
case *comparer, *transformer, Options:
8181
out = Options{out, opt} // Conflicting comparers or transformers
8282
}
83+
case noop:
84+
if out == nil {
85+
out = noop{} // Takes lowest precedence
86+
}
8387
}
8488
}
8589
return out
@@ -107,8 +111,8 @@ func (opts Options) String() string {
107111
// FilterPath returns a new Option where opt is only evaluated if filter f
108112
// returns true for the current Path in the value tree.
109113
//
110-
// The option passed in may be an Ignore, Transformer, Comparer, Options, or
111-
// a previously filtered Option.
114+
// The option passed in may be an Ignore, Transformer, Comparer, Continue,
115+
// Options, or a previously filtered Option.
112116
func FilterPath(f func(Path) bool, opt Option) Option {
113117
if f == nil {
114118
panic("invalid path filter function")
@@ -148,8 +152,8 @@ func (f pathFilter) String() string {
148152
// If T is an interface, it is possible that f is called with two values with
149153
// different concrete types that both implement T.
150154
//
151-
// The option passed in may be an Ignore, Transformer, Comparer, Options, or
152-
// a previously filtered Option.
155+
// The option passed in may be an Ignore, Transformer, Comparer, Continue,
156+
// Options, or a previously filtered Option.
153157
func FilterValues(f interface{}, opt Option) Option {
154158
v := reflect.ValueOf(f)
155159
if !function.IsType(v.Type(), function.ValueFilter) || v.IsNil() {
@@ -187,6 +191,66 @@ func (f valuesFilter) String() string {
187191
return fmt.Sprintf("FilterValues(%s, %v)", fn, f.opt)
188192
}
189193

194+
// FilterPriority returns a new Option where an option, opts[i],
195+
// is only evaluated if no fundamental options remain after applying all filters
196+
// in all prior options, opts[:i].
197+
//
198+
// In order to prevent further options from being evaluated, the Continue option
199+
// can be used to ensure that some fundamental option remains.
200+
//
201+
// The options passed in may be an Ignore, Transformer, Comparer, Continue,
202+
// Options, or a previously filtered Option.
203+
func FilterPriority(opts ...Option) Option {
204+
var newOpts []Option
205+
for _, opt := range opts {
206+
if opt := normalizeOption(opt); opt != nil {
207+
newOpts = append(newOpts, opt)
208+
}
209+
}
210+
if len(newOpts) > 0 {
211+
return &priorityFilter{opts: newOpts}
212+
}
213+
return nil
214+
}
215+
216+
type priorityFilter struct {
217+
core
218+
opts []Option
219+
}
220+
221+
func (f priorityFilter) filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption {
222+
for _, opt := range f.opts {
223+
if opt := opt.filter(s, vx, vy, t); opt != nil {
224+
return opt
225+
}
226+
}
227+
return nil
228+
}
229+
230+
func (f priorityFilter) String() string {
231+
var ss []string
232+
for _, opt := range f.opts {
233+
ss = append(ss, fmt.Sprint(opt))
234+
}
235+
return fmt.Sprintf("FilterPriority(%s)", strings.Join(ss, ", "))
236+
}
237+
238+
// Continue is an Option that is intended to be combined with FilterPriority
239+
// and acts as a sentinel type that if applicable can prevent later options
240+
// from evaluating.
241+
//
242+
// Evaluating a Continue is a noop. Other than affecting the evaluation
243+
// of FilterPriority, its presence has no effect on Equal.
244+
// If any Ignore, Comparer, or Transformer options are also applicable,
245+
// they always take precedence over Continue.
246+
func Continue() Option { return noop{} }
247+
248+
type noop struct{ core }
249+
250+
func (noop) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return noop{} }
251+
func (noop) apply(_ *state, _, _ reflect.Value) bool { return false }
252+
func (noop) String() string { return "Continue()" }
253+
190254
// Ignore is an Option that causes all comparisons to be ignored.
191255
// This value is intended to be combined with FilterPath or FilterValues.
192256
// It is an error to pass an unfiltered Ignore option to Equal.

0 commit comments

Comments
 (0)