Skip to content

Commit a3279d8

Browse files
authored
feat: make resources tree traverseable (#4)
* Some internal fields are now public * Also a function Tree has been added to boxutil package * Upgrade travis to pass tests until go 1.13
1 parent 646d918 commit a3279d8

File tree

8 files changed

+286
-46
lines changed

8 files changed

+286
-46
lines changed

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ go:
33
- "1.8"
44
- "1.9"
55
- "1.10"
6+
- "1.11"
7+
- "1.12"
8+
- "1.13"
69

710
script:
811
- make setup && make test

action.go

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@ type A struct {
77
Attr
88
HttpMethod string
99

10-
name string
11-
bound bool
12-
resource *R
13-
handler interface{}
14-
interceptors []I
10+
// Name is the name to identify the action AND the invocation to url suffix
11+
Name string
12+
13+
// Bound is true if this action is not an extended action
14+
Bound bool
15+
16+
// Interceptors is the list of actions that will be executed before executing handler
17+
Interceptors []I
18+
19+
resource *R
20+
handler interface{}
1521
}
1622

1723
func Action(handler interface{}) *A {
@@ -22,7 +28,7 @@ func Action(handler interface{}) *A {
2228
HttpMethod: "GET",
2329
Attr: Attr{},
2430
handler: handler,
25-
interceptors: []I{},
31+
Interceptors: []I{},
2632
}
2733
}
2834

@@ -32,13 +38,13 @@ func ActionPost(handler interface{}) *A {
3238
HttpMethod: "POST",
3339
Attr: Attr{},
3440
handler: handler,
35-
interceptors: []I{},
41+
Interceptors: []I{},
3642
}
3743
}
3844

3945
func actionBound(handler interface{}, method string) *A {
4046
a := Action(handler)
41-
a.bound = true
47+
a.Bound = true
4248
a.HttpMethod = method
4349
return a
4450
}
@@ -74,13 +80,13 @@ func Trace(handler interface{}) *A {
7480

7581
// WithName overwrite default action name
7682
func (a *A) WithName(name string) *A {
77-
a.name = name
83+
a.Name = name
7884
return a
7985
}
8086

8187
func (a *A) Bind(method string) *A {
8288
a.HttpMethod = method
83-
a.bound = true
89+
a.Bound = true
8490
return a
8591
}
8692

@@ -92,7 +98,7 @@ func (a *A) WithAttribute(key string, value interface{}) *A {
9298
func (a *A) WithInterceptors(interceptor ...I) *A {
9399

94100
for _, i := range interceptor {
95-
a.interceptors = append(a.interceptors, i)
101+
a.Interceptors = append(a.Interceptors, i)
96102
}
97103

98104
return a

boxutil/tree.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package boxutil
2+
3+
import (
4+
"box"
5+
"fmt"
6+
"reflect"
7+
"runtime"
8+
"sort"
9+
"strings"
10+
)
11+
12+
// Tree return a string representation of resources hierarchy
13+
func Tree(root *box.R) (s string) {
14+
15+
traverse(root, func(r *box.R) {
16+
17+
actions := r.GetActions()
18+
19+
sortActions(actions)
20+
21+
if len(r.Interceptors) == 0 && len(actions) == 0 {
22+
return
23+
}
24+
25+
s += getFullPath(r) + "\n"
26+
27+
for _, i := range r.Interceptors {
28+
s += fmt.Sprintf(" <%s>\n", getFunctionName(i))
29+
}
30+
31+
for _, a := range actions {
32+
s += fmt.Sprintf(" %s", a.HttpMethod)
33+
34+
if !a.Bound {
35+
s += fmt.Sprintf(" :%s", a.Name)
36+
}
37+
38+
s += " "
39+
40+
for _, i := range a.Interceptors {
41+
s += fmt.Sprintf("<%s>", getFunctionName(i))
42+
}
43+
44+
s += "\n"
45+
}
46+
})
47+
48+
return
49+
}
50+
51+
func traverse(r *box.R, f func(r *box.R)) {
52+
f(r)
53+
for _, c := range r.Children {
54+
traverse(c, f)
55+
}
56+
}
57+
58+
func getFullPath(r *box.R) (s string) {
59+
s = r.Path
60+
for {
61+
if r.Parent == nil {
62+
return
63+
}
64+
s = r.Parent.Path + "/" + s
65+
r = r.Parent
66+
}
67+
}
68+
69+
func sortActions(actions []*box.A) {
70+
// Sort actions:
71+
// First bound (by method between them)
72+
// Second unbound (by name between them)
73+
sort.Slice(actions, func(i, j int) bool {
74+
a := actions[i]
75+
b := actions[j]
76+
77+
if a.Bound {
78+
if b.Bound {
79+
return a.HttpMethod < b.HttpMethod
80+
}
81+
return true
82+
} else {
83+
if b.Bound {
84+
return false
85+
}
86+
return a.Name < b.Name
87+
}
88+
})
89+
}
90+
91+
func getFunctionName(i interface{}) string {
92+
name := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
93+
94+
parts := strings.Split(name, ".")
95+
96+
return parts[1]
97+
}

boxutil/tree_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package boxutil
2+
3+
import (
4+
. "box"
5+
"context"
6+
"fmt"
7+
"testing"
8+
)
9+
10+
func TestTree(t *testing.T) {
11+
12+
root := NewResource()
13+
v1 := root.Resource("/api/v1").
14+
WithInterceptors(Auth, AccessLog)
15+
v1.Resource("/users").
16+
WithActions(
17+
Get(ListUsers).WithInterceptors(AdminRequired, NoAudit),
18+
Post(CreateUser).WithInterceptors(AdminRequired),
19+
)
20+
v1.Resource("/users/{userId}").
21+
WithActions(
22+
Get(GetUser),
23+
Delete(DeleteUser),
24+
ActionPost(BanUser).WithInterceptors(AdminRequired),
25+
ActionPost(EnableUser).WithInterceptors(AdminRequired),
26+
ActionPost(DisableUser).WithInterceptors(AdminRequired),
27+
)
28+
v1.Resource("/spots/{spotId}/menus/{menuId}").
29+
WithActions(
30+
Delete(DeleteMenu).WithInterceptors(AdminRequired, NoAudit),
31+
Patch(UpdateMenu),
32+
)
33+
34+
s := Tree(root)
35+
36+
expected := `/api/v1
37+
<Auth>
38+
<AccessLog>
39+
/api/v1/users
40+
GET <AdminRequired><NoAudit>
41+
POST <AdminRequired>
42+
/api/v1/users/{userId}
43+
DELETE
44+
GET
45+
POST :banUser <AdminRequired>
46+
POST :disableUser <AdminRequired>
47+
POST :enableUser <AdminRequired>
48+
/api/v1/spots/{spotId}/menus/{menuId}
49+
DELETE <AdminRequired><NoAudit>
50+
PATCH
51+
`
52+
53+
if s != expected {
54+
t.Errorf("Tree output does not match\nExpected: %s\nObtained:%s\n", expected, s)
55+
}
56+
57+
}
58+
59+
func CreateUser() {}
60+
func GetUser() {}
61+
func BanUser() {}
62+
func DisableUser() {}
63+
func EnableUser() {}
64+
func DeleteUser() {}
65+
func ListUsers() {}
66+
func DeleteMenu() {}
67+
func UpdateMenu() {}
68+
69+
func Auth(next H) H {
70+
return func(ctx context.Context) {
71+
next(ctx)
72+
}
73+
}
74+
func AccessLog(next H) H {
75+
return func(ctx context.Context) {
76+
next(ctx)
77+
}
78+
}
79+
80+
func AdminRequired(next H) H {
81+
return func(ctx context.Context) {
82+
next(ctx)
83+
}
84+
}
85+
86+
func NoAudit(next H) H {
87+
return func(ctx context.Context) {
88+
next(ctx)
89+
}
90+
}
91+
92+
func TestSortActions(t *testing.T) {
93+
94+
actions := []*A{
95+
{Name: "D", Bound: false},
96+
{Name: "C", Bound: false},
97+
{Name: "B", Bound: true, HttpMethod: "B"},
98+
{Name: "A", Bound: true, HttpMethod: "A"},
99+
}
100+
101+
fmt.Println(actions)
102+
sortActions(actions)
103+
fmt.Println(actions)
104+
105+
if actions[0].Name != "A" {
106+
t.Errorf("A should be action[0]")
107+
}
108+
if actions[1].Name != "B" {
109+
t.Errorf("B should be action[1]")
110+
}
111+
if actions[2].Name != "C" {
112+
t.Errorf("C should be action[2]")
113+
}
114+
if actions[3].Name != "D" {
115+
t.Errorf("D should be action[3]")
116+
}
117+
}

http.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ func Box2Http(b *B) http.Handler {
5353
if nil == ri {
5454
break
5555
}
56-
interceptors = append(ri.interceptors, interceptors...)
57-
ri = ri.parent
56+
interceptors = append(ri.Interceptors, interceptors...)
57+
ri = ri.Parent
5858
}
5959
}
60-
interceptors = append(interceptors, c.Action.interceptors...)
60+
interceptors = append(interceptors, c.Action.Interceptors...)
6161

6262
hi := func(ctx context.Context) {
6363

main_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func BenchmarkList(b *testing.B) {
2323
//last = P + strconv.Itoa(rand.Intn(N))
2424

2525
for _, v := range l {
26-
if last == v.name {
26+
if last == v.Name {
2727
//fmt.Println(v)
2828
break
2929
}

0 commit comments

Comments
 (0)