Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add data to the service execute with no file #344

Merged
merged 7 commits into from
Aug 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- (#303) Add command `service dev` that build and run the service with the logs
- (#303) Add command `service execute` that execute a task on a service
- (#316) Delete service when stoping the `service dev` command to avoid to keep all the versions of the services.
- (#344) Add `service execute --data` flag to pass arguments as key=value.

#### Removed
- (#303) Deprecate command `service test` in favor of `service dev` and `service execute`
Expand Down
54 changes: 38 additions & 16 deletions cmd/service/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package service

import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
Expand All @@ -11,32 +13,44 @@ import (
"github.com/mesg-foundation/core/api/core"
"github.com/mesg-foundation/core/cmd/utils"
"github.com/mesg-foundation/core/service"
"github.com/mesg-foundation/core/utils/xpflag"
"github.com/spf13/cobra"
survey "gopkg.in/AlecAivazis/survey.v1"
)

var executions []*core.ResultData
var executeData map[string]string

// Execute a task from a service
var Execute = &cobra.Command{
Use: "execute",
Short: "Execute a task of a service",
Example: `mesg-core service execute SERVICE_ID`,
Args: cobra.MinimumNArgs(1),
Args: cobra.ExactArgs(1),
PreRun: executePreRun,
Run: executeHandler,
DisableAutoGenTag: true,
}

func init() {
Execute.Flags().StringP("task", "t", "", "Run the given task")
Execute.Flags().VarP(xpflag.NewStringToStringValue(&executeData, nil), "data", "d", "data required to run the task")
Execute.Flags().StringP("json", "j", "", "Path to a JSON file containing the data required to run the task")
}

func executePreRun(cmd *cobra.Command, args []string) {
if cmd.Flag("data").Changed && cmd.Flag("json").Changed {
utils.HandleError(errors.New("You can specify only one of '--data' or '--json' options"))
}
}

func executeHandler(cmd *cobra.Command, args []string) {
serviceID := args[0]
taskKey := getTaskKey(cmd, serviceID)
json := getJSON(cmd)
taskData, err := readJSONFile(json)
serviceReply, err := cli().GetService(context.Background(), &core.GetServiceRequest{
ServiceID: serviceID,
})
utils.HandleError(err)
taskKey := getTaskKey(cmd, serviceReply.Service)
taskData, err := getData(cmd, taskKey, serviceReply.Service)
utils.HandleError(err)

stream, err := cli().ListenResult(context.Background(), &core.ListenResultRequest{
Expand Down Expand Up @@ -79,31 +93,39 @@ func taskKeysFromService(s *service.Service) []string {
return taskKeys
}

func getTaskKey(cmd *cobra.Command, serviceID string) string {
func getTaskKey(cmd *cobra.Command, s *service.Service) string {
taskKey := cmd.Flag("task").Value.String()
if taskKey == "" {
serviceReply, err := cli().GetService(context.Background(), &core.GetServiceRequest{
ServiceID: serviceID,
})
utils.HandleError(err)
if survey.AskOne(&survey.Select{
Message: "Select the task to execute",
Options: taskKeysFromService(serviceReply.Service),
Options: taskKeysFromService(s),
}, &taskKey, nil) != nil {
os.Exit(0)
}
}
return taskKey
}

func getJSON(cmd *cobra.Command) string {
json := cmd.Flag("json").Value.String()
if json == "" {
if survey.AskOne(&survey.Input{Message: "Enter the filepath to the inputs"}, &json, nil) != nil {
func getData(cmd *cobra.Command, taskKey string, s *service.Service) (string, error) {
data := cmd.Flag("data").Value.String()
jsonFile := cmd.Flag("json").Value.String()

if data != "" {
castData, err := s.Cast(taskKey, executeData)
if err != nil {
return "", err
}

b, err := json.Marshal(castData)
return string(b), err
}

if jsonFile == "" {
if survey.AskOne(&survey.Input{Message: "Enter the filepath to the inputs"}, &jsonFile, nil) != nil {
os.Exit(0)
}
}
return json
return readJSONFile(jsonFile)
}

func readJSONFile(path string) (string, error) {
Expand Down
2 changes: 1 addition & 1 deletion cmd/utils/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const (
func HandleError(err error) {
if err != nil {
fmt.Println(errorMessage(err))
os.Exit(0)
os.Exit(1)
}
}

Expand Down
7 changes: 4 additions & 3 deletions docs/cli/mesg-core_service_execute.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ mesg-core service execute SERVICE_ID
### Options

```
-h, --help help for execute
-j, --json string Path to a JSON file containing the data required to run the task
-t, --task string Run the given task
-d, --data key=value data required to run the task
-h, --help help for execute
-j, --json string Path to a JSON file containing the data required to run the task
-t, --task string Run the given task
```

### SEE ALSO
Expand Down
79 changes: 79 additions & 0 deletions service/cast.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package service

import (
"encoding/json"
"fmt"
"strconv"
)

type caster func(value string) (interface{}, error)

func castString(value string) (interface{}, error) {
return value, nil
}
func castNumber(value string) (interface{}, error) {
i, err := strconv.ParseInt(value, 10, 64)
if err == nil {
return i, nil
}
f, err := strconv.ParseFloat(value, 64)
if err != nil {
return nil, fmt.Errorf("input %q is not a Number type", value)
}
return f, nil
}
func castBoolean(value string) (interface{}, error) {
b, err := strconv.ParseBool(value)
if err != nil {
return nil, fmt.Errorf("input %q is not a Boolean type", value)
}
return b, nil
}
func castObject(value string) (interface{}, error) {
var v interface{}
if err := json.Unmarshal([]byte(value), &v); err != nil {
return nil, fmt.Errorf("input %q is not a Object type", value)
}
return v, nil
}

var casters = map[string]caster{
"String": castString,
"Number": castNumber,
"Boolean": castBoolean,
"Object": castObject,
}

// Cast converts map[string]string to map[string]interface{} based on defined types in the service tasks map.
func (s *Service) Cast(taskKey string, taskData map[string]string) (map[string]interface{}, error) {
task, ok := s.Tasks[taskKey]
if !ok {
return nil, &TaskNotFoundError{Service: s, TaskKey: taskKey}
}

m := make(map[string]interface{}, len(taskData))
for key, value := range taskData {
inputType, ok := task.Inputs[key]
if !ok {
return nil, &InputNotFoundError{Service: s, InputKey: key}
}

newValue, err := s.cast(value, inputType.Type)
if err != nil {
return nil, fmt.Errorf("Task %q - %s", taskKey, err)
}
if newValue != nil {
m[key] = newValue
}
}
return m, nil
}

// cast converts single value based on its type.
func (s *Service) cast(value, inputType string) (interface{}, error) {
c, ok := casters[inputType]
if !ok {
return nil, fmt.Errorf("input %q - invalid type", value)
}
return c(value)
}
98 changes: 98 additions & 0 deletions service/cast_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package service

import (
"testing"

"github.com/stvp/assert"
)

func TestServiceCast(t *testing.T) {
var tests = []struct {
service *Service
data map[string]string
expected map[string]interface{}
expectErr bool
}{
{
createTestServcieWithInputs(nil),
map[string]string{},
map[string]interface{}{},
false,
},
{
createTestServcieWithInputs(map[string]string{
"a": "String",
"b": "Number",
"c": "Number",
"d": "Boolean",
}),
map[string]string{
"a": "_",
"b": "1",
"c": "1.1",
"d": "true",
},
map[string]interface{}{
"a": "_",
"b": int64(1),
"c": 1.1,
"d": true,
},
false,
},
{
createTestServcieWithInputs(map[string]string{"a": "String"}),
map[string]string{"b": "_"},
map[string]interface{}{},
true,
},
{
createTestServcieWithInputs(map[string]string{"a": "Number"}),
map[string]string{"a": "_"},
map[string]interface{}{},
true,
},
{
createTestServcieWithInputs(map[string]string{"a": "Boolean"}),
map[string]string{"a": "_"},
map[string]interface{}{},
true,
},
{
createTestServcieWithInputs(map[string]string{"a": "Object"}),
map[string]string{"a": `{"b":1}`},
map[string]interface{}{"a": map[string]interface{}{"b": float64(1)}},
false,
},
}

for _, tt := range tests {
got, err := tt.service.Cast("test", tt.data)
if tt.expectErr {
assert.NotNil(t, err)
} else {
assert.Equal(t, len(tt.expected), len(got), "maps len are not equal")
assert.Equal(t, tt.expected, got, "maps are not equal")
}
}

// test if non-existing key returns error
_, err := tests[0].service.Cast("_", nil)
assert.NotNil(t, err)
}

// creates test service with given inputs name and type under "test" task key.
func createTestServcieWithInputs(inputs map[string]string) *Service {
s := &Service{
Tasks: map[string]*Task{
"test": {
Inputs: make(map[string]*Parameter),
},
},
}

for name, itype := range inputs {
s.Tasks["test"].Inputs[name] = &Parameter{Type: itype}
}
return s
}
10 changes: 10 additions & 0 deletions service/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ func (e *InvalidTaskInputError) Error() string {
return errorString
}

// InputNotFoundError is an error when a service doesn't contains a specific input.
type InputNotFoundError struct {
Service *Service
InputKey string
}

func (e *InputNotFoundError) Error() string {
return "Input '" + e.InputKey + "' not found in service '" + e.Service.Name + "'"
}

// OutputNotFoundError is an error when a service doesn't contain a specific output.
type OutputNotFoundError struct {
Service *Service
Expand Down
Loading