Skip to content

Commit 51ea1bf

Browse files
committed
Add docgen
1 parent 4355ef0 commit 51ea1bf

File tree

3 files changed

+327
-3
lines changed

3 files changed

+327
-3
lines changed

docgen/docgen.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package docgen
2+
3+
import (
4+
"github.com/antonmedv/expr/internal/conf"
5+
"reflect"
6+
"regexp"
7+
)
8+
9+
// Kind can be any of array, map, struct, func, string, int, float, bool or any.
10+
type Kind string
11+
12+
// Identifier represents variable names and field names.
13+
type Identifier string
14+
15+
// TypeName is a name of type in types map.
16+
type TypeName string
17+
18+
type Context struct {
19+
Variables map[Identifier]*Type `json:"variables"`
20+
Types map[TypeName]*Type `json:"types"`
21+
}
22+
23+
type Type struct {
24+
Name TypeName `json:"name,omitempty"`
25+
Kind Kind `json:"kind,omitempty"`
26+
Type *Type `json:"type,omitempty"`
27+
Key *Type `json:"key_type,omitempty"`
28+
Fields map[Identifier]*Type `json:"fields,omitempty"`
29+
Arguments []*Type `json:"arguments,omitempty"`
30+
Return *Type `json:"return,omitempty"`
31+
}
32+
33+
func CreateDoc(i interface{}) *Context {
34+
c := &Context{
35+
Variables: make(map[Identifier]*Type),
36+
Types: make(map[TypeName]*Type),
37+
}
38+
39+
for name, t := range conf.CreateTypesTable(i) {
40+
c.Variables[Identifier(name)] = c.use(t.Type, fromMethod(t.Method))
41+
}
42+
43+
return c
44+
}
45+
46+
type config struct {
47+
method bool
48+
}
49+
50+
type option func(c *config)
51+
52+
func fromMethod(b bool) option {
53+
return func(c *config) {
54+
c.method = b
55+
}
56+
}
57+
58+
func (c *Context) use(t reflect.Type, ops ...option) *Type {
59+
config := &config{}
60+
for _, op := range ops {
61+
op(config)
62+
}
63+
64+
methods := make([]reflect.Method, 0)
65+
66+
// Methods of struct should be gathered from original struct with pointer,
67+
// as methods maybe declared on pointer receiver. Also this method retrieves
68+
// all embedded structs methods as well, no need to recursion.
69+
for i := 0; i < t.NumMethod(); i++ {
70+
m := t.Method(i)
71+
if isPrivate(m.Name) {
72+
continue
73+
}
74+
methods = append(methods, m)
75+
}
76+
77+
for t.Kind() == reflect.Ptr {
78+
t = t.Elem()
79+
}
80+
81+
// Only named types will have methods defined on them.
82+
// It maybe not even struct, but we gonna call then
83+
// structs in appendix anyway.
84+
if len(methods) > 0 {
85+
goto appendix
86+
}
87+
88+
// This switch only for "simple" types.
89+
switch t.Kind() {
90+
case reflect.Bool:
91+
return &Type{Kind: "bool"}
92+
93+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
94+
fallthrough
95+
96+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
97+
return &Type{Kind: "int"}
98+
99+
case reflect.Float32, reflect.Float64:
100+
return &Type{Kind: "float"}
101+
102+
case reflect.String:
103+
return &Type{Kind: "string"}
104+
105+
case reflect.Interface:
106+
return &Type{Kind: "any"}
107+
108+
case reflect.Array, reflect.Slice:
109+
return &Type{
110+
Kind: "array",
111+
Type: c.use(t.Elem()),
112+
}
113+
114+
case reflect.Map:
115+
return &Type{
116+
Kind: "map",
117+
Key: c.use(t.Key()),
118+
Type: c.use(t.Elem()),
119+
}
120+
121+
case reflect.Struct:
122+
goto appendix
123+
124+
case reflect.Func:
125+
arguments := make([]*Type, 0)
126+
127+
start := 0
128+
if config.method {
129+
start = 1
130+
}
131+
132+
for i := start; i < t.NumIn(); i++ {
133+
arguments = append(arguments, c.use(t.In(i)))
134+
}
135+
return &Type{
136+
Kind: "func",
137+
Arguments: arguments,
138+
Return: c.use(t.Out(0)),
139+
}
140+
}
141+
142+
appendix:
143+
name := TypeName(t.Name())
144+
anonymous := name == ""
145+
146+
a, ok := c.Types[name]
147+
148+
if !ok {
149+
a = &Type{
150+
Kind: "struct",
151+
Fields: make(map[Identifier]*Type),
152+
}
153+
154+
// Type a should be saved before starting recursion, or it will never end.
155+
if !anonymous {
156+
c.Types[name] = a
157+
}
158+
159+
for name, field := range conf.FieldsFromStruct(t) {
160+
if isPrivate(name) {
161+
continue
162+
}
163+
a.Fields[Identifier(name)] = c.use(field.Type)
164+
}
165+
166+
for _, m := range methods {
167+
if isPrivate(m.Name) {
168+
continue
169+
}
170+
a.Fields[Identifier(m.Name)] = c.use(m.Type, fromMethod(true))
171+
}
172+
}
173+
174+
if anonymous {
175+
return a
176+
}
177+
178+
return &Type{
179+
Kind: "struct",
180+
Name: name,
181+
}
182+
}
183+
184+
var isCapital = regexp.MustCompile("^[A-Z]")
185+
186+
func isPrivate(s string) bool {
187+
return !isCapital.Match([]byte(s))
188+
}

docgen/docgen_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package docgen_test
2+
3+
import (
4+
. "github.com/antonmedv/expr/docgen"
5+
"github.com/sanity-io/litter"
6+
"github.com/stretchr/testify/assert"
7+
"testing"
8+
"time"
9+
)
10+
11+
type Tweet struct {
12+
Size int
13+
Message string
14+
}
15+
16+
type Env struct {
17+
Tweets []Tweet
18+
Config struct {
19+
MaxSize int32
20+
}
21+
Env map[string]interface{}
22+
}
23+
24+
func (*Env) Duration(s string) time.Duration {
25+
d, _ := time.ParseDuration(s)
26+
return d
27+
}
28+
29+
func TestCreateDoc(t *testing.T) {
30+
doc := CreateDoc(&Env{})
31+
expected := &Context{
32+
Variables: map[Identifier]*Type{
33+
"Tweets": {
34+
Kind: "array",
35+
Type: &Type{
36+
Kind: "struct",
37+
Name: "Tweet",
38+
},
39+
},
40+
"Config": {
41+
Kind: "struct",
42+
Fields: map[Identifier]*Type{
43+
"MaxSize": {Kind: "int"},
44+
},
45+
},
46+
"Env": {
47+
Kind: "map",
48+
Key: &Type{Kind: "string"},
49+
Type: &Type{Kind: "any"},
50+
},
51+
"Duration": {
52+
Kind: "func",
53+
Arguments: []*Type{
54+
{Kind: "string"},
55+
},
56+
Return: &Type{Kind: "struct", Name: "Duration"},
57+
},
58+
},
59+
Types: map[TypeName]*Type{
60+
"Tweet": {
61+
Kind: "struct",
62+
Fields: map[Identifier]*Type{
63+
"Size": {Kind: "int"},
64+
"Message": {Kind: "string"},
65+
},
66+
},
67+
"Duration": {
68+
Kind: "struct",
69+
Fields: map[Identifier]*Type{
70+
"Hours": {
71+
Kind: "func",
72+
Arguments: []*Type{},
73+
Return: &Type{
74+
Kind: "float",
75+
},
76+
},
77+
"Minutes": {
78+
Kind: "func",
79+
Arguments: []*Type{},
80+
Return: &Type{
81+
Kind: "float",
82+
},
83+
},
84+
"Nanoseconds": {
85+
Kind: "func",
86+
Arguments: []*Type{},
87+
Return: &Type{
88+
Kind: "int",
89+
},
90+
},
91+
"Round": {
92+
Kind: "func",
93+
Arguments: []*Type{
94+
{
95+
Name: "Duration",
96+
Kind: "struct",
97+
},
98+
},
99+
Return: &Type{
100+
Name: "Duration",
101+
Kind: "struct",
102+
},
103+
},
104+
"Seconds": {
105+
Kind: "func",
106+
Arguments: []*Type{},
107+
Return: &Type{
108+
Kind: "float",
109+
},
110+
},
111+
"String": {
112+
Kind: "func",
113+
Arguments: []*Type{},
114+
Return: &Type{
115+
Kind: "string",
116+
},
117+
},
118+
"Truncate": {
119+
Kind: "func",
120+
Arguments: []*Type{
121+
{
122+
Name: "Duration",
123+
Kind: "struct",
124+
},
125+
},
126+
Return: &Type{
127+
Name: "Duration",
128+
Kind: "struct",
129+
},
130+
},
131+
},
132+
},
133+
},
134+
}
135+
assert.Equal(t, litter.Sdump(expected), litter.Sdump(doc))
136+
}

internal/conf/types_table.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func CreateTypesTable(i interface{}) TypesTable {
3131

3232
switch d.Kind() {
3333
case reflect.Struct:
34-
types = fieldsFromStruct(d)
34+
types = FieldsFromStruct(d)
3535

3636
// Methods of struct should be gathered from original struct with pointer,
3737
// as methods maybe declared on pointer receiver. Also this method retrieves
@@ -59,7 +59,7 @@ func CreateTypesTable(i interface{}) TypesTable {
5959
return types
6060
}
6161

62-
func fieldsFromStruct(t reflect.Type) TypesTable {
62+
func FieldsFromStruct(t reflect.Type) TypesTable {
6363
types := make(TypesTable)
6464
t = dereference(t)
6565
if t == nil {
@@ -72,7 +72,7 @@ func fieldsFromStruct(t reflect.Type) TypesTable {
7272
f := t.Field(i)
7373

7474
if f.Anonymous {
75-
for name, typ := range fieldsFromStruct(f.Type) {
75+
for name, typ := range FieldsFromStruct(f.Type) {
7676
types[name] = typ
7777
}
7878
}

0 commit comments

Comments
 (0)