diff --git a/context.go b/context.go index e911fb9..b67747a 100644 --- a/context.go +++ b/context.go @@ -5,4 +5,5 @@ type contextKey int const ( ckNamespace contextKey = iota ckResolveNestedDirectives + ckDirectiveRunOrder ) diff --git a/option.go b/option.go index 03d28db..8df2475 100644 --- a/option.go +++ b/option.go @@ -41,3 +41,14 @@ func WithValue(key, value interface{}) Option { func WithNestedDirectivesEnabled(resolve bool) Option { return WithValue(ckResolveNestedDirectives, resolve) } + +type DirectiveRunOrder func(*Directive, *Directive) bool + +// WithDirectiveRunOrder sets the order of execution for directives. +// +// When used in New, the directives will be sorted at the tree building stage. +// +// When Used in Resolve or Scan, a copy of the directives will be sorted and used. +func WithDirectiveRunOrder(runOrder DirectiveRunOrder) Option { + return WithValue(ckDirectiveRunOrder, runOrder) +} diff --git a/resolver.go b/resolver.go index 52aa9db..171d767 100644 --- a/resolver.go +++ b/resolver.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "reflect" + "sort" "strconv" "strings" "sync" @@ -55,6 +56,7 @@ func New(structValue interface{}, opts ...Option) (*Resolver, error) { // Apply the context to each resolver. tree.Iterate(func(r *Resolver) error { r.Context = ctx + r.Directives = getSortedDirectives(ctx, r.Directives) return nil }) @@ -317,7 +319,7 @@ func (r *Resolver) runDirectives(ctx context.Context, rv reflect.Value) error { ns = nsOverriden.(*Namespace) } - for _, directive := range r.Directives { + for _, directive := range getSortedDirectives(ctx, r.Directives) { dirRuntime := &DirectiveRuntime{ Directive: directive, Resolver: r, @@ -345,6 +347,18 @@ func (r *Resolver) runDirectives(ctx context.Context, rv reflect.Value) error { return nil } +func getSortedDirectives(ctx context.Context, directives []*Directive) []*Directive { + if directiveRunOrder := ctx.Value(ckDirectiveRunOrder); directiveRunOrder != nil { + var directivesCopy []*Directive + directivesCopy = append(directivesCopy, directives...) + sort.SliceStable(directivesCopy, func(i, j int) bool { + return directiveRunOrder.(DirectiveRunOrder)(directivesCopy[i], directivesCopy[j]) + }) + return directivesCopy + } + return directives // the original one +} + func (r *Resolver) DebugLayoutText(depth int) string { var sb strings.Builder sb.WriteString(r.String()) diff --git a/resolver_test.go b/resolver_test.go index 85c2e27..5d2ad0e 100644 --- a/resolver_test.go +++ b/resolver_test.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "reflect" + "strings" "testing" "github.com/ggicci/owl" @@ -840,6 +841,76 @@ func TestWithNestedDirectivesEnabled_definitionOfNestedDirectives(t *testing.T) }, tracker.Executed.ExecutedDirectives(), "tell the difference between nested and non-nested directives") } +func TestWithDirectiveRunOrder_buildtime(t *testing.T) { + type Record struct { + R1 string `owl:"DOTA=2;csgo=1"` + R2 string `owl:"apple=green;pear;Grape=purple"` + } + + tree, err := owl.New(Record{}, owl.WithDirectiveRunOrder(func(d1, d2 *owl.Directive) bool { + return strings.ToLower(d1.Name) < strings.ToLower(d2.Name) // sort directives by name (alphabetical order) + })) + + assert.NoError(t, err) + assert.NotNil(t, tree) + suite.Run(t, NewBuildResolverTreeTestSuite( + tree, + []*expectedResolver{ + { + Index: []int{0}, + LookupPath: "R1", + NumFields: 0, + Directives: []*owl.Directive{ + owl.NewDirective("csgo", "1"), + owl.NewDirective("DOTA", "2"), + }, + Leaf: true, + }, + { + Index: []int{1}, + LookupPath: "R2", + NumFields: 0, + Directives: []*owl.Directive{ + owl.NewDirective("apple", "green"), + owl.NewDirective("Grape", "purple"), + owl.NewDirective("pear"), + }, + Leaf: true, + }, + }, + )) +} + +func TestWithDirectiveRunOrder_runtime(t *testing.T) { + ns, tracker := createNsForTracking() + resolver, err := owl.New(User{}, owl.WithNamespace(ns)) + assert.NoError(t, err) + + form := &User{ + Name: "Ggicci", + Gender: "male", + Birthday: "1991-11-10", + } + + err = resolver.Scan(form, owl.WithDirectiveRunOrder(func(d1, d2 *owl.Directive) bool { + return d1.Name == "default" // makes default directive run first + })) + assert.NoError(t, err) + + expected := ExecutedDataList{ + {owl.NewDirective("form", "name"), "Ggicci"}, + + // The order of directives below is different from the order in the struct. + // Because we set the directive run order with WithDirectiveRunOrder when calling Scan. + // Now the default directive will run first, then the form directive. + {owl.NewDirective("default", "unknown"), "male"}, + {owl.NewDirective("form", "gender"), "male"}, + + {owl.NewDirective("form", "birthday"), "1991-11-10"}, + } + assert.Equal(t, expected, tracker.Executed) +} + func TestTreeDebugLayout(t *testing.T) { var ( tree *owl.Resolver