-
Notifications
You must be signed in to change notification settings - Fork 29
/
starlight.go
170 lines (153 loc) · 4.89 KB
/
starlight.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
// Package starlight provides a convenience wrapper around github.com/google/starlark-go.
package starlight
import (
"fmt"
"io/ioutil"
"path/filepath"
"sync"
"github.com/starlight-go/starlight/convert"
"go.starlark.net/resolve"
"go.starlark.net/starlark"
)
func init() {
resolve.AllowNestedDef = true // allow def statements within function bodies
resolve.AllowLambda = true // allow lambda expressions
resolve.AllowFloat = true // allow floating point literals, the 'float' built-in, and x / y
resolve.AllowSet = true // allow the 'set' built-in
resolve.AllowBitwise = true // allow bitwise operations
}
// LoadFunc is a function that tells starlark how to find and load other scripts
// using the load() function. If you don't use load() in your scripts, you can pass in nil.
type LoadFunc func(thread *starlark.Thread, module string) (starlark.StringDict, error)
// Eval evaluates the starlark source with the given global variables. The type
// of the argument for the src parameter must be string (filename), []byte, or io.Reader.
func Eval(src interface{}, globals map[string]interface{}, load LoadFunc) (map[string]interface{}, error) {
dict, err := convert.MakeStringDict(globals)
if err != nil {
return nil, err
}
thread := &starlark.Thread{
Load: load,
}
filename, ok := src.(string)
if ok {
dict, err = starlark.ExecFile(thread, filename, nil, dict)
} else {
dict, err = starlark.ExecFile(thread, "eval.sky", src, dict)
}
if err != nil {
return nil, err
}
return convert.FromStringDict(dict), nil
}
// Cache is a cache of scripts to avoid re-reading files and reparsing them.
type Cache struct {
dirs []string
cache *cache
mu sync.Mutex
scripts map[string]*starlark.Program
}
func run(p *starlark.Program, globals map[string]interface{}, load LoadFunc) (map[string]interface{}, error) {
g, err := convert.MakeStringDict(globals)
if err != nil {
return nil, err
}
ret, err := p.Init(&starlark.Thread{Load: load}, g)
if err != nil {
return nil, err
}
return convert.FromStringDict(ret), nil
}
// New returns a Starlight Cache that looks in the given directories for plugin
// files to run. The directories are searched in order for files when Run is
// called. Calls to the script function load() will also look in these
// directories. This function will panic if you give it no directories.
func New(dirs ...string) *Cache {
if len(dirs) == 0 {
panic(fmt.Errorf("no directories given"))
}
return newCache(dirs, nil)
}
// WithGlobals returns a new Starlight cache that passes the listed global
// values to scripts loaded with the load() script function. Note that these
// globals will *not* be passed to individual scripts you run unless you
// explicitly pass them in the Run call.
func WithGlobals(globals map[string]interface{}, dirs ...string) (*Cache, error) {
if len(dirs) == 0 {
return nil, fmt.Errorf("no directories given")
}
g, err := convert.MakeStringDict(globals)
if err != nil {
return nil, err
}
return newCache(dirs, g), nil
}
func newCache(dirs []string, globals starlark.StringDict) *Cache {
c := &Cache{
dirs: dirs,
scripts: map[string]*starlark.Program{},
}
c.cache = &cache{
cache: make(map[string]*entry),
readFile: c.readFile,
globals: globals,
}
return c
}
// Run looks for a file with the given filename, and runs it with the given globals
// passed to the script's global namespace. The return value is all convertible
// global variables from the script, which may include the passed-in globals.
func (c *Cache) Run(filename string, globals map[string]interface{}) (map[string]interface{}, error) {
dict, err := convert.MakeStringDict(globals)
if err != nil {
return nil, err
}
c.mu.Lock()
if p, ok := c.scripts[filename]; ok {
c.mu.Unlock()
return run(p, globals, c.load)
}
c.mu.Unlock()
b, err := c.readFile(filename)
if err != nil {
return nil, err
}
_, p, err := starlark.SourceProgram(filename, b, dict.Has)
if err != nil {
return nil, err
}
c.mu.Lock()
c.scripts[filename] = p
c.mu.Unlock()
return run(p, globals, c.load)
}
func (c *Cache) load(_ *starlark.Thread, module string) (starlark.StringDict, error) {
return c.cache.Load(module)
}
func (c *Cache) readFile(filename string) ([]byte, error) {
var err error
var b []byte
for _, d := range c.dirs {
b, err = ioutil.ReadFile(filepath.Join(d, filename))
if err == nil {
return b, nil
}
}
// guaranteed to have at least one directory, so there should be at least
// not found error here.
return nil, fmt.Errorf("cannot find file %q in any of the configured directories %q", filename, c.dirs)
}
// Reset clears all cached scripts.
func (c *Cache) Reset() {
c.mu.Lock()
c.scripts = map[string]*starlark.Program{}
c.cache.reset()
c.mu.Unlock()
}
// Forget clears the cached script for the given filename.
func (c *Cache) Forget(filename string) {
c.mu.Lock()
c.cache.remove(filename)
delete(c.scripts, filename)
c.mu.Unlock()
}