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

Support array result include sequence action #170

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
33 changes: 26 additions & 7 deletions common/gobuild.py.launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"io"
"log"
"os"
"reflect"
"strings"
)

Expand All @@ -42,10 +43,11 @@ func main() {
log.Printf("ACTION ENV: %v", os.Environ())
}

// assign the main function
type Action func(event map[string]interface{}) map[string]interface{}
var action Action
action = Main
resultKind := reflect.TypeOf(Main).Out(0).Kind()
if resultKind != reflect.Map && resultKind != reflect.Slice && resultKind != reflect.Array {
fmt.Println("Support map and slice and array only")
os.Exit(1)
}

// input
out := os.NewFile(3, "pipe")
Expand Down Expand Up @@ -89,12 +91,29 @@ func main() {
}
}
// get payload if not empty
var payload map[string]interface{}
isJsonObjectParam := true
var payloadForJsonObject map[string]interface{}
var payloadForJsonArray []interface{}
if value, ok := input["value"].(map[string]interface{}); ok {
payload = value
payloadForJsonObject = value
} else {
if value, ok := input["value"].([]interface{}); ok {
payloadForJsonArray = value
isJsonObjectParam = false
}
}
// process the request
result := action(payload)
var result interface{}
funcMain := reflect.ValueOf(Main)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not that familiar with the runtime-go code.
Where this Main comes from?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is the user's action's Main. i debugged the /init(or /run) before, when execute /init, it will inject user action codes to action container's suitable directory, the common/gobuild.py.launcher.go can recognize this Main method.

if isJsonObjectParam {
param := []reflect.Value{reflect.ValueOf(payloadForJsonObject)}
reflectResult := funcMain.Call(param)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If invokes Main directly, compile would be failed.
So i used go reflect to invoke Main

result = reflectResult[0].Interface()
} else {
param := []reflect.Value{reflect.ValueOf(payloadForJsonArray)}
reflectResult := funcMain.Call(param)
result = reflectResult[0].Interface()
}
// encode the answer
output, err := json.Marshal(&result)
if err != nil {
Expand Down
33 changes: 26 additions & 7 deletions golang1.17/lib/launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"io"
"log"
"os"
"reflect"
"strings"
)

Expand All @@ -50,10 +51,11 @@ func main() {
log.Printf("Environment: %v", os.Environ())
}

// assign the main function
type Action func(event map[string]interface{}) map[string]interface{}
var action Action
action = Main
resultKind := reflect.TypeOf(Main).Out(0).Kind()
if resultKind != reflect.Map && resultKind != reflect.Slice && resultKind != reflect.Array {
fmt.Println("Support map and slice and array only")
os.Exit(1)
}

// input
out := os.NewFile(3, "pipe")
Expand Down Expand Up @@ -100,12 +102,29 @@ func main() {
}
}
// get payload if not empty
var payload map[string]interface{}
isJsonObjectParam := true
var payloadForJsonObject map[string]interface{}
var payloadForJsonArray []interface{}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Support json array as input param, this is for sequence action

if value, ok := input["value"].(map[string]interface{}); ok {
payload = value
payloadForJsonObject = value
} else {
if value, ok := input["value"].([]interface{}); ok {
payloadForJsonArray = value
isJsonObjectParam = false
}
}
// process the request
result := action(payload)
var result interface{}
funcMain := reflect.ValueOf(Main)
if isJsonObjectParam {
param := []reflect.Value{reflect.ValueOf(payloadForJsonObject)}
reflectResult := funcMain.Call(param)
result = reflectResult[0].Interface()
} else {
param := []reflect.Value{reflect.ValueOf(payloadForJsonArray)}
reflectResult := funcMain.Call(param)
result = reflectResult[0].Interface()
}
// encode the answer
output, err := json.Marshal(&result)
if err != nil {
Expand Down
33 changes: 26 additions & 7 deletions golang1.18/lib/launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"io"
"log"
"os"
"reflect"
"strings"
)

Expand All @@ -50,10 +51,11 @@ func main() {
log.Printf("Environment: %v", os.Environ())
}

// assign the main function
type Action func(event map[string]interface{}) map[string]interface{}
var action Action
action = Main
resultKind := reflect.TypeOf(Main).Out(0).Kind()
if resultKind != reflect.Map && resultKind != reflect.Slice && resultKind != reflect.Array {
fmt.Println("Support map and slice and array only")
os.Exit(1)
}

// input
out := os.NewFile(3, "pipe")
Expand Down Expand Up @@ -100,12 +102,29 @@ func main() {
}
}
// get payload if not empty
var payload map[string]interface{}
isJsonObjectParam := true
var payloadForJsonObject map[string]interface{}
var payloadForJsonArray []interface{}
Copy link
Contributor Author

@ningyougang ningyougang Aug 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Support jsonArray as input param as well. this is for sequence action

if value, ok := input["value"].(map[string]interface{}); ok {
payload = value
payloadForJsonObject = value
} else {
if value, ok := input["value"].([]interface{}); ok {
payloadForJsonArray = value
isJsonObjectParam = false
}
}
// process the request
result := action(payload)
var result interface{}
funcMain := reflect.ValueOf(Main)
if isJsonObjectParam {
param := []reflect.Value{reflect.ValueOf(payloadForJsonObject)}
reflectResult := funcMain.Call(param)
result = reflectResult[0].Interface()
} else {
param := []reflect.Value{reflect.ValueOf(payloadForJsonArray)}
reflectResult := funcMain.Call(param)
result = reflectResult[0].Interface()
}
// encode the answer
output, err := json.Marshal(&result)
if err != nil {
Expand Down
8 changes: 6 additions & 2 deletions openwhisk/runHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,14 @@ func (ap *ActionProxy) runHandler(w http.ResponseWriter, r *http.Request) {

// check if the answer is an object map
var objmap map[string]*json.RawMessage
var objarray []interface{}
err = json.Unmarshal(response, &objmap)
if err != nil {
sendError(w, http.StatusBadGateway, "The action did not return a dictionary or array.")
return
err = json.Unmarshal(response, &objarray)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First, parse the result as json, if failed, parse the result as array.

if err != nil {
sendError(w, http.StatusBadGateway, "The action did not return a dictionary or array.")
return
}
}

w.Header().Set("Content-Type", "application/json")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import actionContainers.{ActionContainer, ActionProxyContainerTestUtils}
import actionContainers.ActionContainer.withContainer
import common.WskActorSystem

import spray.json.{JsObject, JsString}
import spray.json.{JsArray, JsObject, JsString}

abstract class ActionLoopGoContainerTests
extends ActionProxyContainerTestUtils
Expand Down Expand Up @@ -135,4 +135,51 @@ abstract class ActionLoopGoContainerTests
c.run(helloMsg()) should be(okMsg("hello-Hello", "Hello, Demo!"))
}
}

it should "support return array result" in {
val helloArrayGo = {
s"""
|package main
|
|func Main(obj map[string]interface{}) []interface{} {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if we want to use the JSON array params, do we have to use this signature for the main function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for JSON array params, can use like blow

// Main is the function implementing the action
func Main(event []interface{}) []interface{} {
	input := make([]string, len(event))
	for i, v := range event {
		input[i] = fmt.Sprint(v)
	}
	sort.Strings(input)
	output := make([]interface{}, len(event))
	for i, v := range input {
		output[i] = v
	}
	return output
}

hm..it is better to add a test case for json array params as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok now I get it.

So originally the signature of the Main method is

func Main(obj map[string]interface{}) map[string]interface{} {
...
}

And now we add support for JSON array params.

| result := []interface{}{"a", "b"}
| return result
|}
|
""".stripMargin
}
val src = ExeBuilder.mkBase64SrcZip(
Seq(
Seq(s"main.go") -> helloArrayGo
))
withActionLoopContainer { c =>
c.init(initPayload(src))._1 shouldBe (200)
val result = c.runForJsArray(JsObject())
result._1 shouldBe (200)
result._2 shouldBe Some(JsArray(JsString("a"), JsString("b")))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compare the result

}
}

it should "support array as input param" in {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add another test case for support array as input param

val helloArrayGo = {
s"""
|package main
|
|func Main(obj []interface{}) []interface{} {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder what happens when the argument of the main function is an array and an object parameter is passed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@upgle
Use reflect to invoke the Main, e.g.

Can't invoke the Main directly due to compile error, should use reflect to invoke.

Btw, why can invoke Main direclty before? because the input param's data type is explicit.
But in this pr, the data type supports 2 data types, so should use reflect to invoke it.

| return obj
|}
|
""".stripMargin
}
val src = ExeBuilder.mkBase64SrcZip(
Seq(
Seq(s"main.go") -> helloArrayGo
))
withActionLoopContainer { c =>
c.init(initPayload(src))._1 shouldBe (200)
val result = c.runForJsArray(runPayload(JsArray(JsString("a"), JsString("b"))))
result._1 shouldBe (200)
result._2 shouldBe Some(JsArray(JsString("a"), JsString("b")))
}
}
}