Skip to content

Commit 62046b3

Browse files
author
Frank Martinez
authored
Merge pull request #30 from fm-tibco/jsexec
add jsexec to contrib
2 parents 97da16e + f7d9c57 commit 62046b3

File tree

14 files changed

+930
-0
lines changed

14 files changed

+930
-0
lines changed

activity/jsexec/README.md

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
2+
# Javascript Execution Activity
3+
4+
The `jsexec` activity evaluates a javascript `script` along with provided `parameters` and returns the result in the `outputs`.
5+
6+
### Flogo CLI
7+
```bash
8+
flogo install github.com/project-flogo/contrib/activity/jsexec
9+
```
10+
11+
## Configuration
12+
13+
### Settings:
14+
15+
| Name | Type | Description
16+
|:--- | :--- | :---
17+
| script | string | The javascript code to evaluate |
18+
19+
### Input:
20+
21+
| Name | Type | Description
22+
|:--- | :--- | :---
23+
| parameters | object | Key/value pairs representing parameters to evaluate within the context of the script |
24+
25+
### Output:
26+
27+
| Name | Type | Description
28+
|:--- | :--- | :---
29+
| error | bool | Flag indicating if there was an error executing the script |
30+
| errorMessage | string | The error message |
31+
| result | object | The result object from the javascript code |
32+
33+
## Microgateway Usage
34+
35+
A sample `service` definition is:
36+
37+
```json
38+
{
39+
"name": "JSCalc",
40+
"description": "Make calls to a JS calculator",
41+
"ref": "github.com/project-flogo/jsexec",
42+
"settings": {
43+
"script": "result.total = parameters.num * 2;"
44+
}
45+
}
46+
```
47+
48+
An example `step` that invokes the above `JSCalc` service using `parameters` is:
49+
50+
```json
51+
{
52+
"if": "$.PetStorePets.outputs.result.status == 'available'",
53+
"service": "JSCalc",
54+
"input": {
55+
"parameters.num": "=$.PetStorePets.outputs.result.available"
56+
}
57+
}
58+
```
59+
60+
Utilizing the response values can be seen in a response handler:
61+
62+
```json
63+
{
64+
"if": "$.PetStorePets.outputs.result.status == 'available'",
65+
"error": false,
66+
"output": {
67+
"code": 200,
68+
"data": {
69+
"body.pet": "=$.PetStorePets.outputs.result",
70+
"body.inventory": "=$.PetStoreInventory.outputs.result",
71+
"body.availableTimesTwo": "=$.JSCalc.outputs.result.total"
72+
}
73+
}
74+
}
75+
```
76+
Additional microgateway examples that utilize the `jsexec` activity
77+
78+
* [api](examples/microgateway/api/README.md) - A simple API example
79+
* [json](examples/microgateway/api/README.md) - An example that using a _flogo.json_
80+
81+
## Development
82+
83+
### Testing
84+
85+
To run tests issue the following command in the root of the project:
86+
87+
```bash
88+
go test -p 1 ./...
89+
```
90+
91+
The `-p 1` is needed to prevent tests from being run in parallel. To re-run the tests first run the following:
92+
93+
```bash
94+
go clean -testcache
95+
```
96+
97+
To skip the integration tests use the `-short` flag:
98+
99+
```bash
100+
go test -p 1 -short ./...
101+
```

activity/jsexec/activity.go

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package jsexec
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
7+
"github.com/dop251/goja"
8+
"github.com/project-flogo/core/activity"
9+
"github.com/project-flogo/core/data/metadata"
10+
)
11+
12+
var activityMetadata = activity.ToMetadata(&Settings{}, &Input{}, &Output{})
13+
14+
func init() {
15+
_ = activity.Register(&Activity{}, New)
16+
}
17+
18+
// Activity is a javascript activity
19+
type Activity struct {
20+
script string
21+
}
22+
23+
// New creates a new javascript activity
24+
func New(ctx activity.InitContext) (activity.Activity, error) {
25+
settings := Settings{}
26+
err := metadata.MapToStruct(ctx.Settings(), &settings, true)
27+
if err != nil {
28+
return nil, err
29+
}
30+
31+
logger := ctx.Logger()
32+
logger.Debugf("Setting: %b", settings)
33+
34+
act := Activity{
35+
script: settings.Script,
36+
}
37+
38+
return &act, nil
39+
}
40+
41+
// Metadata return the metadata for the activity
42+
func (a *Activity) Metadata() *activity.Metadata {
43+
return activityMetadata
44+
}
45+
46+
// Eval executes the activity
47+
func (a *Activity) Eval(ctx activity.Context) (done bool, err error) {
48+
input := Input{}
49+
err = ctx.GetInputObject(&input)
50+
if err != nil {
51+
return false, err
52+
}
53+
54+
output := Output{}
55+
result := make(map[string]interface{})
56+
vm, err := NewVM(nil)
57+
if err != nil {
58+
output.Error = true
59+
output.ErrorMessage = err.Error()
60+
return false, err
61+
}
62+
//todo is ok to ignore the errors for the SetInVM calls?
63+
_ = vm.SetInVM("parameters", input.Parameters)
64+
_ = vm.SetInVM("result", result)
65+
66+
_, err = vm.vm.RunScript("JSServiceScript", a.script)
67+
if err != nil {
68+
output.Error = true
69+
output.ErrorMessage = err.Error()
70+
return false, err
71+
}
72+
err = vm.GetFromVM("result", &result)
73+
if err != nil {
74+
output.Error = true
75+
output.ErrorMessage = err.Error()
76+
return false, err
77+
}
78+
output.Result = result
79+
80+
err = ctx.SetOutputObject(&output)
81+
if err != nil {
82+
return false, err
83+
}
84+
85+
return true, nil
86+
}
87+
88+
// VM represents a VM object.
89+
type VM struct {
90+
vm *goja.Runtime
91+
}
92+
93+
// NewVM initializes a new VM with defaults.
94+
func NewVM(defaults map[string]interface{}) (vm *VM, err error) {
95+
vm = &VM{}
96+
vm.vm = goja.New()
97+
for k, v := range defaults {
98+
if v != nil {
99+
vm.vm.Set(k, v)
100+
}
101+
}
102+
return vm, err
103+
}
104+
105+
// EvaluateToBool evaluates a string condition within the context of the VM.
106+
func (vm *VM) EvaluateToBool(condition string) (truthy bool, err error) {
107+
if condition == "" {
108+
return true, nil
109+
}
110+
var res goja.Value
111+
res, err = vm.vm.RunString(condition)
112+
if err != nil {
113+
return false, err
114+
}
115+
truthy, ok := res.Export().(bool)
116+
if !ok {
117+
err = errors.New("condition does not evaluate to bool")
118+
return false, err
119+
}
120+
return truthy, err
121+
}
122+
123+
// SetInVM sets the object name and value in the VM.
124+
func (vm *VM) SetInVM(name string, object interface{}) (err error) {
125+
var valueJSON json.RawMessage
126+
var vmObject map[string]interface{}
127+
valueJSON, err = json.Marshal(object)
128+
if err != nil {
129+
return err
130+
}
131+
err = json.Unmarshal(valueJSON, &vmObject)
132+
if err != nil {
133+
return err
134+
}
135+
vm.vm.Set(name, vmObject)
136+
return err
137+
}
138+
139+
// GetFromVM extracts the current object value from the VM.
140+
func (vm *VM) GetFromVM(name string, object interface{}) (err error) {
141+
var valueJSON json.RawMessage
142+
var vmObject map[string]interface{}
143+
_ = vm.vm.ExportTo(vm.vm.Get(name), &vmObject) //todo is ok to ignore the error?
144+
145+
valueJSON, err = json.Marshal(vmObject)
146+
if err != nil {
147+
return err
148+
}
149+
err = json.Unmarshal(valueJSON, object)
150+
if err != nil {
151+
return err
152+
}
153+
return err
154+
}
155+
156+
// SetPrimitiveInVM sets primitive value in VM.
157+
func (vm *VM) SetPrimitiveInVM(name string, primitive interface{}) {
158+
vm.vm.Set(name, primitive)
159+
}

activity/jsexec/activity_test.go

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package jsexec
2+
3+
import (
4+
"testing"
5+
6+
"github.com/project-flogo/core/activity"
7+
"github.com/project-flogo/core/data"
8+
"github.com/project-flogo/core/data/mapper"
9+
"github.com/project-flogo/core/data/metadata"
10+
logger "github.com/project-flogo/core/support/log"
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
type initContext struct {
15+
settings map[string]interface{}
16+
}
17+
18+
func newInitContext(values map[string]interface{}) *initContext {
19+
if values == nil {
20+
values = make(map[string]interface{})
21+
}
22+
return &initContext{
23+
settings: values,
24+
}
25+
}
26+
27+
func (i *initContext) Settings() map[string]interface{} {
28+
return i.settings
29+
}
30+
31+
func (i *initContext) MapperFactory() mapper.Factory {
32+
return nil
33+
}
34+
35+
func (i *initContext) Logger() logger.Logger {
36+
return logger.RootLogger()
37+
}
38+
39+
type activityContext struct {
40+
input map[string]interface{}
41+
output map[string]interface{}
42+
}
43+
44+
func newActivityContext(values map[string]interface{}) *activityContext {
45+
if values == nil {
46+
values = make(map[string]interface{})
47+
}
48+
return &activityContext{
49+
input: values,
50+
output: make(map[string]interface{}),
51+
}
52+
}
53+
54+
func (a *activityContext) ActivityHost() activity.Host {
55+
return a
56+
}
57+
58+
func (a *activityContext) Name() string {
59+
return "test"
60+
}
61+
62+
func (a *activityContext) GetInput(name string) interface{} {
63+
return a.input[name]
64+
}
65+
66+
func (a *activityContext) SetOutput(name string, value interface{}) error {
67+
a.output[name] = value
68+
return nil
69+
}
70+
71+
func (a *activityContext) GetInputObject(input data.StructValue) error {
72+
return input.FromMap(a.input)
73+
}
74+
75+
func (a *activityContext) SetOutputObject(output data.StructValue) error {
76+
a.output = output.ToMap()
77+
return nil
78+
}
79+
80+
func (a *activityContext) GetSharedTempData() map[string]interface{} {
81+
return nil
82+
}
83+
84+
func (a *activityContext) ID() string {
85+
return "test"
86+
}
87+
88+
func (a *activityContext) IOMetadata() *metadata.IOMetadata {
89+
return nil
90+
}
91+
92+
func (a *activityContext) Reply(replyData map[string]interface{}, err error) {
93+
94+
}
95+
96+
func (a *activityContext) Return(returnData map[string]interface{}, err error) {
97+
98+
}
99+
100+
func (a *activityContext) Scope() data.Scope {
101+
return nil
102+
}
103+
104+
func (a *activityContext) Logger() logger.Logger {
105+
return logger.RootLogger()
106+
}
107+
108+
func TestJS(t *testing.T) {
109+
act, err := New(newInitContext(map[string]interface{}{
110+
"script": "result.sum = parameters.a + parameters.b",
111+
}))
112+
assert.Nil(t, err)
113+
114+
ctx := newActivityContext(map[string]interface{}{
115+
"parameters": map[string]interface{}{"a": 1.0, "b": 2.0},
116+
})
117+
_, err = act.Eval(ctx)
118+
assert.Nil(t, err)
119+
assert.Equal(t, 3.0, ctx.output["result"].(map[string]interface{})["sum"].(float64))
120+
}

0 commit comments

Comments
 (0)