-
Notifications
You must be signed in to change notification settings - Fork 2
/
jsengine.go
278 lines (241 loc) · 8.34 KB
/
jsengine.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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
/*
* Copyright © 2018. TIBCO Software Inc.
* This file is subject to the license terms contained
* in the license file that is distributed with this file.
*/
// Package gojsonata provides utility functions to execute JSONata expression for JSON data transformations
package gojsonata
import (
"fmt"
"io/ioutil"
"path"
"strings"
"sync"
"github.com/pkg/errors"
"github.com/robertkrimen/otto"
"github.com/yxuco/gojsonata/jsdata"
)
// Engine is modular vm environment
type Engine struct {
// it is based on otto
*otto.Otto
// sourceCache contains the exported value of loaded source code - sync map[string]otto.Value
sourceCache sync.Map
}
// NewEngine creates a new otto vm instance.
func NewEngine() *Engine {
return &Engine{
Otto: otto.New(),
}
}
var jsengine *Engine
func init() {
jsengine = NewEngine()
jsengine.registerResources()
// Provide global "require" method in the module scope.
jsRequire := func(call otto.FunctionCall) otto.Value {
jsModuleName := call.Argument(0).String()
moduleValue, err := jsengine.Require(jsModuleName)
if err != nil {
jsException(jsengine, "failed to load required module "+jsModuleName+": "+err.Error())
return otto.UndefinedValue()
}
return moduleValue
}
jsengine.Set("require", jsRequire)
// set global var for jsonata module
jm, _ := jsengine.Require("jsonata")
jsengine.Set("jsonata", jm)
// define global js function for jsonata transformation
jsengine.Run(`var transform = function(data, expr) {
var expression = jsonata(expr);
var result = expression.evaluate(JSON.parse(data));
return JSON.stringify(result);
}`)
}
// CachedModules returns names of js modules currently loaded in the engine
func CachedModules() []string {
var keys []string
jsengine.sourceCache.Range(func(k, v interface{}) bool {
keys = append(keys, k.(string))
return true
})
return keys
}
// Transform executes JSONata expression on input JSON data, and returns the transformation result
func Transform(data, expr string) (string, error) {
value, err := jsengine.Call("transform", nil, data, expr)
if err != nil {
return "", errors.Wrapf(err, "failed to transform data")
}
return value.String(), nil
}
// CallScriptFile evaluates content of js or json file, and return value of specified result var
// the file extension must be .js or .json
func CallScriptFile(filename, result string) (otto.Value, error) {
source, err := ioutil.ReadFile(filename)
if err != nil {
return otto.UndefinedValue(), err
}
// call json parser
if path.Ext(filename) == ".json" {
return jsengine.Call("JSON.parse", nil, string(source))
}
return CallScript(string(source), result)
}
// CallScript evaluates a js script, and return value of specified result var
// It checks if all required modules are loaded in cache
func CallScript(source, result string) (otto.Value, error) {
// wrap script in a js function
source = "(function() {\n" + source + "\nreturn " + result + ";\n})"
// Note: support for require(module) added to init()
return jsengine.Call(source, nil)
}
// RunScript executes js script and returns the result
// It checks if all required modules are loaded in cache
func RunScript(source interface{}) (otto.Value, error) {
// Note: support for require(module) added to init()
return jsengine.Run(source)
}
// AddModuleSource loads source code of a js module file and add the result to cache,
// so later script evaluation can use this module
func AddModuleSource(key, source string) error {
return jsengine.addModule(key, source)
}
// AddModuleFile loads content of a js module file and add the result to cache,
// so later script evaluation can use this module
func AddModuleFile(filename string) error {
source, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
return jsengine.addModule(sourceKey(filename), string(source))
}
// LoadModule loads source code of a js module.
// It recursively loads dependent modules required by the source code.
// All dependent modules' source code should be preloaded in jsdata, or loaded before this module.
// Note: this implementation is based on https://github.com/ddliu/motto/blob/master/module.go
func (m *Engine) LoadModule(source string) (otto.Value, error) {
// Wraps the source to create a module environment
source = "(function(module) {var require = module.require;var exports = module.exports;var __dirname = module.__dirname;\n" + source + "\n})"
// Provide the "require" method in the module scope.
jsRequire := func(call otto.FunctionCall) otto.Value {
jsModuleName := call.Argument(0).String()
moduleValue, err := m.Require(jsModuleName)
if err != nil {
jsException(m, "failed to load required module "+jsModuleName+": "+err.Error())
return otto.UndefinedValue()
}
return moduleValue
}
jsModule, _ := m.Object(`({exports: {}})`)
jsModule.Set("require", jsRequire)
jsModule.Set("__dirname", "")
jsExports, _ := jsModule.Get("exports")
// Run the module source, with "jsModule" as the "module" variable, "jsExports" as "this"(Nodejs capable).
moduleReturn, err := m.Call(source, jsExports, jsModule)
if err != nil {
return otto.UndefinedValue(), err
}
if !moduleReturn.IsUndefined() {
jsModule.Set("exports", moduleReturn)
return moduleReturn, nil
}
return jsModule.Get("exports")
}
// Require implements js 'require(name)' for modeules.
// It first check if the module is already in cache, then check if it is a preloaded resource in jsdata.
// It returns error if source code of the specified module is not found.
// Note: input name may contains file path info, e.g., ./js/mycode.js, but
// the cache key will use only the file name without suffix, i.e., mycode => value
func (m *Engine) Require(name string) (otto.Value, error) {
// return cached value if already loaded
key := sourceKey(name)
if cache, ok := m.sourceCache.Load(key); ok {
fmt.Printf("found module %s in cache\n", name)
return cache.(otto.Value), nil
}
// find a known asset in jsdata
asset := assetName(name)
if asset != "" {
// load a known asset and add it to cache
if err := m.registerResource(asset); err != nil {
return otto.UndefinedValue(), err
}
// return the newly added asset in cache
if value, ok := m.sourceCache.Load(key); ok {
return value.(otto.Value), nil
}
}
return otto.UndefinedValue(), errors.Errorf("required module %s is not loaded", name)
}
func (m *Engine) registerResources() error {
// must load traceur-runtime first
traceurAsset := assetName("traceur-runtime")
if traceurAsset != "" {
if err := m.registerResource(traceurAsset); err != nil {
return err
}
} else {
return errors.New("cannot find traceur-runtime in jsdata")
}
// add all other known asset from jsdata
assets := jsdata.AssetNames()
for _, a := range assets {
err := m.registerResource(a)
if err != nil {
return err
}
}
return nil
}
func (m *Engine) registerResource(name string) error {
// check cache first
key := sourceKey(name)
if _, ok := m.sourceCache.Load(key); ok {
fmt.Printf("module %s already loaded, skip register\n", name)
return nil
}
// load a known asset
data := jsdata.MustAsset(name)
return m.addModule(key, string(data))
}
// addModule loads source code of a js module and add the result to cache,
// so later script evaluation can use this module
func (m *Engine) addModule(key, source string) error {
fmt.Printf("load resource %s content %d ...\n", key, len(source))
oValue, err := m.LoadModule(source)
if err != nil {
return errors.Wrapf(err, "failed to load js resource %s", key)
}
fmt.Printf("add resource %s to cache\n", key)
m.sourceCache.Store(key, oValue)
return nil
}
// assetName is the asset ID in jsdata, it may not be the same as the module name in request('module')
// e.g., require('./datetime') refers the asset name 'js/datetime.js'
// here, we find the asset correponding to a module name by comparing keys of all known assets
// return the asset name, or "" if not found
func assetName(module string) string {
key := sourceKey(module)
assets := jsdata.AssetNames()
for _, a := range assets {
if key == sourceKey(a) {
return a
}
}
return ""
}
func sourceKey(name string) string {
key := path.Base(strings.ToLower(strings.TrimSpace(name)))
ext := path.Ext(key)
if ext == ".js" {
key = key[0 : len(key)-3]
}
return key
}
// Throw a javascript error, see https://github.com/robertkrimen/otto/issues/17
func jsException(vm *Engine, msg string) {
value, _ := vm.Call("new Error", nil, msg)
panic(value)
}