Files
expr/docgen/docgen.go
T

261 lines
8.2 KiB
Go

package docgen
import (
"reflect"
"regexp"
"strings"
"github.com/expr-lang/expr/checker/nature"
"github.com/expr-lang/expr/conf"
"github.com/expr-lang/expr/internal/deref"
)
// Kind can be any of array, map, struct, func, string, int, float, bool or any.
type Kind string
// Identifier represents variable names and field names.
type Identifier string
// TypeName is a name of type in types map.
type TypeName string
type Context struct {
Variables map[Identifier]*Type `json:"variables"`
Types map[TypeName]*Type `json:"types"`
PkgPath string
}
type Type struct {
Name TypeName `json:"name,omitempty"`
Kind Kind `json:"kind,omitempty"`
Type *Type `json:"type,omitempty"`
Key *Type `json:"key_type,omitempty"`
Fields map[Identifier]*Type `json:"fields,omitempty"`
Arguments []*Type `json:"arguments,omitempty"`
Return *Type `json:"return,omitempty"`
}
var (
Operators = []string{"matches", "contains", "startsWith", "endsWith"}
Builtins = map[Identifier]*Type{
"true": {Kind: "bool"},
"false": {Kind: "bool"},
"len": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}}, Return: &Type{Kind: "int"}},
"all": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "bool"}},
"none": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "bool"}},
"any": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "bool"}},
"one": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "bool"}},
"filter": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "array", Type: &Type{Kind: "any"}}},
"map": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "array", Type: &Type{Kind: "any"}}},
"count": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "int"}},
"trim": {Kind: "func", Arguments: []*Type{{Name: "string", Kind: "string"}, {Name: "cutstr", Kind: "string"}}, Return: &Type{Name: "string", Kind: "string"}},
"trimPrefix": {Kind: "func", Arguments: []*Type{{Name: "string", Kind: "string"}, {Name: "cutstr", Kind: "string"}}, Return: &Type{Name: "string", Kind: "string"}},
"trimSuffix": {Kind: "func", Arguments: []*Type{{Name: "string", Kind: "string"}, {Name: "cutstr", Kind: "string"}}, Return: &Type{Name: "string", Kind: "string"}},
"upper": {Kind: "func", Arguments: []*Type{{Name: "string", Kind: "string"}}, Return: &Type{Name: "string", Kind: "string"}},
"lower": {Kind: "func", Arguments: []*Type{{Name: "string", Kind: "string"}}, Return: &Type{Name: "string", Kind: "string"}},
"repeat": {Kind: "func", Arguments: []*Type{{Name: "n", Kind: "int"}}, Return: &Type{Name: "string", Kind: "string"}},
"join": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Name: "glue", Kind: "string"}}, Return: &Type{Name: "string", Kind: "string"}},
"indexOf": {Kind: "func", Arguments: []*Type{{Name: "string", Kind: "string"}, {Name: "substr", Kind: "string"}}, Return: &Type{Name: "index", Kind: "int"}},
"lastIndexOf": {Kind: "func", Arguments: []*Type{{Name: "string", Kind: "string"}, {Name: "substr", Kind: "string"}}, Return: &Type{Name: "index", Kind: "int"}},
"hasPrefix": {Kind: "func", Arguments: []*Type{{Name: "string", Kind: "string"}, {Name: "prefix", Kind: "string"}}, Return: &Type{Kind: "bool"}},
"hasSuffix": {Kind: "func", Arguments: []*Type{{Name: "string", Kind: "string"}, {Name: "prefix", Kind: "string"}}, Return: &Type{Kind: "bool"}},
"toJSON": {Kind: "func", Arguments: []*Type{{Kind: "any"}}, Return: &Type{Kind: "string"}},
"fromJSON": {Kind: "func", Arguments: []*Type{{Kind: "string"}}, Return: &Type{Kind: "any"}},
"toBase64": {Kind: "func", Arguments: []*Type{{Kind: "string"}}, Return: &Type{Kind: "string"}},
"fromBase64": {Kind: "func", Arguments: []*Type{{Kind: "string"}}, Return: &Type{Kind: "string"}},
"first": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}}, Return: &Type{Kind: "any"}},
"last": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}}, Return: &Type{Kind: "any"}},
"now": {Kind: "func", Return: &Type{Name: "time.Time", Kind: "struct"}},
"duration": {Kind: "func", Arguments: []*Type{{Kind: "string"}}, Return: &Type{Kind: "time.Duration"}},
}
)
func CreateDoc(i any) *Context {
c := &Context{
Variables: make(map[Identifier]*Type),
Types: make(map[TypeName]*Type),
PkgPath: deref.Type(reflect.TypeOf(i)).PkgPath(),
}
for name, t := range conf.Env(i).All() {
if _, ok := c.Variables[Identifier(name)]; ok {
continue
}
c.Variables[Identifier(name)] = c.use(t.Type, fromMethod(t.Method))
}
for _, op := range Operators {
c.Variables[Identifier(op)] = &Type{
Kind: "operator",
}
}
for builtin, t := range Builtins {
c.Variables[builtin] = t
}
return c
}
type config struct {
method bool
}
type option func(c *config)
func fromMethod(b bool) option {
return func(c *config) {
c.method = b
}
}
func (c *Context) use(t reflect.Type, ops ...option) *Type {
config := &config{}
for _, op := range ops {
op(config)
}
methods := make([]reflect.Method, 0)
// Methods of struct should be gathered from original struct with pointer,
// as methods maybe declared on pointer receiver. Also this method retrieves
// all embedded structs methods as well, no need to recursion.
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
if isPrivate(m.Name) || isProtobuf(m.Name) {
continue
}
methods = append(methods, m)
}
t = deref.Type(t)
// Only named types will have methods defined on them.
// It maybe not even struct, but we gonna call then
// structs in appendix anyway.
if len(methods) > 0 {
goto appendix
}
// This switch only for "simple" types.
switch t.Kind() {
case reflect.Bool:
return &Type{Kind: "bool"}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
fallthrough
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return &Type{Kind: "int"}
case reflect.Float32, reflect.Float64:
return &Type{Kind: "float"}
case reflect.String:
return &Type{Kind: "string"}
case reflect.Interface:
return &Type{Kind: "any"}
case reflect.Array, reflect.Slice:
return &Type{
Kind: "array",
Type: c.use(t.Elem()),
}
case reflect.Map:
return &Type{
Kind: "map",
Key: c.use(t.Key()),
Type: c.use(t.Elem()),
}
case reflect.Struct:
goto appendix
case reflect.Func:
arguments := make([]*Type, 0)
start := 0
if config.method {
start = 1
}
for i := start; i < t.NumIn(); i++ {
arguments = append(arguments, c.use(t.In(i)))
}
f := &Type{
Kind: "func",
Arguments: arguments,
}
if t.NumOut() > 0 {
f.Return = c.use(t.Out(0))
}
return f
}
appendix:
name := TypeName(t.String())
if c.PkgPath == t.PkgPath() {
name = TypeName(t.Name())
}
anonymous := t.Name() == ""
a, ok := c.Types[name]
if !ok {
a = &Type{
Kind: "struct",
Fields: make(map[Identifier]*Type),
}
// baseNode a should be saved before starting recursion, or it will never end.
if !anonymous {
c.Types[name] = a
}
for name, field := range nature.StructFields(t) {
if isPrivate(name) || isProtobuf(name) {
continue
}
if _, ok := a.Fields[Identifier(name)]; ok {
continue
}
a.Fields[Identifier(name)] = c.use(field.Type)
}
for _, m := range methods {
if isPrivate(m.Name) || isProtobuf(m.Name) {
continue
}
a.Fields[Identifier(m.Name)] = c.use(m.Type, fromMethod(true))
}
}
if anonymous {
return a
}
return &Type{
Kind: "struct",
Name: name,
}
}
var isCapital = regexp.MustCompile("^[A-Z]")
func isPrivate(s string) bool {
return !isCapital.Match([]byte(s))
}
func isProtobuf(s string) bool {
return strings.HasPrefix(s, "XXX_")
}