mesg-foundation / engine

Build apps or autonomous workflows with reusable, shareable integrations connecting any service, app, blockchain or decentralized network.
https://mesg.com/
Apache License 2.0
130 stars 13 forks source link

A Go client #278

Closed ilgooz closed 6 years ago

ilgooz commented 6 years ago

We need a decent Go client, maybe with a name mesg-go would be brilliant. :) Any plans on this? I can propose a developer friendly package API within next week.

antho1404 commented 6 years ago

We already have a go client for the application part that can be found in the github.com/mesg-foundation/core/api/client package

and you can find one use case in this application https://github.com/mesg-foundation/application-devcon-update-on-slack/

It's really basic and definitely need to make it better but this could be a nice base.

I would personally prefer to have it as a package in the core (like that we can keep it in sync with the development of the core) but definitely we can have a library that uses this package and that will have its own readme with documentation etc... that would be more user friendly

ilgooz commented 6 years ago

Oh okay, I haven't notice that. Thanks!

ilgooz commented 6 years ago

here is an experimental client with a high level api: https://github.com/ilgooz/mesg-go

ilgooz commented 6 years ago

Can you please check and confirm the package apis for service and application please? https://godoc.org/gopkg.in/ilgooz/mesg-go.v0

antho1404 commented 6 years ago

Initializer

I find it a bit confusing to have the New and the GetApplication or GetService, I understand that the New doesn't use the singleton but maybe in this case we shouldn't export this function or otherwise use the singleton in the new. I think the idea is to have only one function to call either New or Get... but both is a bit confusing

Service API

The tasks always have to return the outputKey and the data associated to this output so why not having a task handler that need to return a string and a interface{} or map[string]interface{} like that the task just output the results and don't have to explicitly call the req.Reply.

Continuing on the same idea, why not delegating the req.Get to the executeTask and like that we can have the task handler with only the inputs needed for this task.

Like that tasks don't need to know anything about the API, not even the executionID

Application API

I'm not really a big fan of the API for the Application. I kind of like the api of the mesg-js lib. I would love to have this simplicity on the go lib too with something like

whenEvent(&Event{serviceID: 'XXX', eventType: 'YYY'})
  .Execute(&Task{
    serviceID: 'XXX',
    taskType: 'YYY', 
    inputs: func (data map[string]interface{}) map[string]interface {
      ...
    }
})

I know it's less "go style" but it's closest to the future workflow file and I feel it's really accessible.

These are just ideas and definitely open to discussion on all this would love to have the pro and con for all this :)

ilgooz commented 6 years ago

Initializer

Totally right. I removed the GetX APIs. I think it's a good idea using New with their options for customization.

Service API

func sendHandler(req *TaskRequest) TaskResponse { var data emailRequest if err := req.Get(&data); err != nil { return emailResponse{ Error: &emailErrorResponse{"Unexpected input data type"}, } }

return emailResponse{
    Success: &emailSuccessResponse{"202", "Accepted"},
}

}

type emailRequest struct { Email string json:"email" SendgridAPIKey string json:"sendgridAPIKey" }

type emailResponse struct { // Output keys holds a pointer for their output data structs // Only one outputKey with it's data will be sent as a response by // using a pointer and omitempty tag. Success emailSuccessResponse json:"success,omitempty" Error emailErrorResponse json:"error,omitempty" }

type emailSuccessResponse struct { Code string json:"code" Message string json:"message" }

type emailErrorResponse struct { Message string json:"message" }


### Application API
I'm a big fan of incoming Workflow API(pipelining data from events or tasks to multiple tasks). For now we don't a have decent way of providing a complete workflow API in Go. _- I don't know situation with conf file yet. -_ 

The only drawback I can see with mesg-js style event based task executions is we cannot execute tasks based on such conditions calculated by the data we gather from event output. I may execute one or many tasks depending on the data I received from an event but that syntax require me to always run a task without a cond. I think either we need to provide a more flexible API or stick with the current one like below:

```go
// Package main is an application that uses functionalities from following services:
// https://github.com/mesg-foundation/service-webhook
// https://github.com/mesg-foundation/service-discord-invitation
package main

import (
    "fmt"
    "log"

    "github.com/ilgooz/mesg-go/application"
)

var webhookServiceID = "v1_4bbafb91f94cd437dc21b5770e986d09"
var discordInvServiceID = "v1_5cd4c617ef5334cfddfb28171de53a9e"
var sendgridKey = "SG.XXX"
var email = "ilkergoktugozturk@gmail.com"

func main() {
    app, err := application.New()
    if err != nil {
        log.Fatal(err)
    }

    events, err := app.WhenEvent(webhookServiceID).Event("request").Listen()
    if err != nil {
        log.Fatal(err)
    }

    for range events {
        req := sendgridRequest{
            Email:          email,
            SendgridAPIKey: sendgridKey,
        }
        var res sendgridResponse
        err = app.Execute(discordInvServiceID, "send", req, &res)

        fmt.Println(err, res)
        // outputs:
        // <nil> {{202 Accepted} {}}
    }
}

type sendgridRequest struct {
    Email          string `json:"email"`
    SendgridAPIKey string `json:"sendgridAPIKey"`
}

type sendgridResponse struct {
    Success *struct {
        Code    int    `json:"code"`
        Message string `json:"message"`
    } `json:"success"`

    Error *struct {
        Message string `json:"message"`
    } `json:"error"`
}
NicolasMahe commented 6 years ago

After a long discussion, we propose this API for application:

Application API

package main

import (
    "fmt"

    mesg "mesg-fondation/go-application"
)

func main() {

    mesgClient, err := mesg.New(mesg.EndpointOption("localhost:3737"), mesg.LogOutputOption(stdout))

    // WhenEvent returns a type that implements Filter, Map and Execute
    observable1 := mesgClient.WhenEvent("SERVICE_ID_1",
        mesg.KeyEventCondition("EVENT_KEY"),
        mesg.DataEventCondition(Data: map[string]interface{}{
            "KEY_1": 42,
            "KEY_2": "3433",
        })
    )
    // Filter returns a type that implements Filter, Map and Execute
    observable2 := observable1.Filter(func(event *mesg.Event) bool {
        var eventData serviceID1EventData
        fmt.Println(event.Key)
        event.Data(&eventData)
        return eventData.Name == "hello"
    })
    // Map returns a type that implements Execute
    observable3 := observable2.Map(func(event *mesg.Event) interface{} {
        var eventData serviceID1EventData
        fmt.Println(event.Key)
        event.Data(&eventData)
        return &serviceID2TaskInputs{
            Method:  "POST",
            Code:    eventData.Code,
            Message: eventData.Message,
        }
    })
    // Only the execution function actually do the listening and connect to Core
    serviceID2TaskKeyExecution, err := observable3.Execute("SERVICE_ID_2", "TASK_KEY")
    defer serviceID2TaskKeyExecution.Close()
    for taskExecution := range serviceID2TaskKeyExecution.TaskExecutions {
        // taskExecution.executionID
    }
}

type serviceID1EventData struct {
    Name    string `json:"name"`
    Code    int    `json:"code"`
    Message string `json:"message"`
}

type serviceID2TaskInputs struct {
    Method  string `json:"method"`
    Code    int    `json:"code"`
    Message string `json:"message"`
}

// THE FOLLOWING CODE SHOULD BE IN GO MESG PACKAGE

type Execution struct {
    TaskExecutions chan(*TaskExecution)
    Errors chan(*error)
    // TaskExecutionErrors chan(*error) Should we have 2 distinct errors chan? Or merge them into 1 err chan?
    // EventListenerErrors chan(*error)
    eventListener *Stream 
}
func (e *Execution) Close() error {
    return e.eventListener.Close()
}

With chaining:

package main

import (
    "fmt"

    mesg "mesg-fondation/go-application"
)

func main() {

    mesgClient, err := mesg.New(mesg.EndpointOption("localhost:3737"), mesg.LogOutputOption(stdout))

    // WhenEvent returns a type that implements Filter, Map and Execute
    serviceID2TaskKeyExecution, err := mesgClient.WhenEvent("SERVICE_ID_1",
        mesg.KeyEventCondition("EVENT_KEY"),
        mesg.DataEventCondition(Data: map[string]interface{}{
            "KEY_1": 42,
            "KEY_2": "3433",
        })
    )
    .Filter(func(event *mesg.Event) bool {
        var eventData serviceID1EventData
        fmt.Println(event.Key)
        event.Data(&eventData)
        return eventData.Name == "hello"
    })
    .Map(func(event *mesg.Event) interface{} {
        var eventData serviceID1EventData
        fmt.Println(event.Key)
        event.Data(&eventData)
        return &serviceID2TaskInputs{
            Method:  "POST",
            Code:    eventData.Code,
            Message: eventData.Message,
        }
    })
        .Execute("SERVICE_ID_2", "TASK_KEY")
    defer serviceID2TaskKeyExecution.Close()
    for taskExecution := range serviceID2TaskKeyExecution.TaskExecutions {
        // taskExecution.executionID
    }
}
ilgooz commented 6 years ago

Thank you for the feedbacks. After some internal discussions too, I've finally came up with 4 new packages for this go client. Please check them out:

Please do following if you want to try out examples/application-quickstart and examples/service-logger.

We have good amount of test coverage in service, servicetest and application. I'll add tests for applicationtest too.

Do you have an agreement on where to host this package? Under mesg-core, independently or as a two independent packages/repos as service and application?

NicolasMahe commented 6 years ago

@ilgooz Nice jobs 👍

We agree to host the packages in 2 repos: mesg-foundation/go-application and mesg-foundation/go-service. This way, the version and issues will be managed separately. Both package should also be named "mesg". So we don't use the common names "application" and "services", and from a outsider developer point of view, it will be simpler to use the lib if its name is "mesg" (type eg: mesg.Result, mesg.Event, mesg.Application)

We will do the same for the "mesg-js" libs.

NicolasMahe commented 6 years ago

Closing this issue. Let's continue the conversation on https://github.com/mesg-foundation/go-service and https://github.com/mesg-foundation/go-application