Skip to content

Commit

Permalink
feat(sdk): Enable Custom Trigger Registration
Browse files Browse the repository at this point in the history
accept registration of factory functions for making custom trigger types
available to the SDK

Signed-off-by: Alex Ullrich <alexullrich@technotects.com>
  • Loading branch information
Alex Ullrich committed Jan 4, 2021
1 parent 64c59fd commit c18f6a6
Show file tree
Hide file tree
Showing 4 changed files with 328 additions and 31 deletions.
31 changes: 1 addition & 30 deletions appsdk/sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,6 @@ import (
"github.com/edgexfoundry/app-functions-sdk-go/internal/common"
"github.com/edgexfoundry/app-functions-sdk-go/internal/runtime"
"github.com/edgexfoundry/app-functions-sdk-go/internal/store/db/interfaces"
"github.com/edgexfoundry/app-functions-sdk-go/internal/trigger"
"github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/http"
"github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/messagebus"
"github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/mqtt"
"github.com/edgexfoundry/app-functions-sdk-go/internal/webserver"
"github.com/edgexfoundry/app-functions-sdk-go/pkg/util"

Expand Down Expand Up @@ -129,6 +125,7 @@ type AppFunctionsSDK struct {
deferredFunctions []bootstrap.Deferred
serviceKeyOverride string
backgroundChannel <-chan types.MessageEnvelope
customTriggerBuilders map[string]func(sdk *AppFunctionsSDK) (Trigger, error)
}

// AddRoute allows you to leverage the existing webserver to add routes.
Expand Down Expand Up @@ -458,32 +455,6 @@ func (sdk *AppFunctionsSDK) StoreSecrets(path string, secrets map[string]string)
return sdk.secretProvider.StoreSecrets(path, secrets)
}

// setupTrigger configures the appropriate trigger as specified by configuration.
func (sdk *AppFunctionsSDK) setupTrigger(configuration *common.ConfigurationStruct, runtime *runtime.GolangRuntime) trigger.Trigger {
var t trigger.Trigger
// Need to make dynamic, search for the binding that is input

switch strings.ToUpper(configuration.Binding.Type) {
case bindingTypeHTTP:
sdk.LoggingClient.Info("HTTP trigger selected")
t = &http.Trigger{Configuration: configuration, Runtime: runtime, Webserver: sdk.webserver, EdgeXClients: sdk.EdgexClients}

case bindingTypeMessageBus,
bindingTypeEdgeXMessageBus: // Allows for more explicit name now that we have plain MQTT option also
sdk.LoggingClient.Info("EdgeX MessageBus trigger selected")
t = &messagebus.Trigger{Configuration: configuration, Runtime: runtime, EdgeXClients: sdk.EdgexClients}

case bindingTypeMQTT:
sdk.LoggingClient.Info("External MQTT trigger selected")
t = mqtt.NewTrigger(configuration, runtime, sdk.EdgexClients, sdk.secretProvider)

default:
sdk.LoggingClient.Error(fmt.Sprintf("Invalid Trigger type of '%s' specified", configuration.Binding.Type))
}

return t
}

func (sdk *AppFunctionsSDK) addContext(next func(nethttp.ResponseWriter, *nethttp.Request)) func(nethttp.ResponseWriter, *nethttp.Request) {
return func(w nethttp.ResponseWriter, r *nethttp.Request) {
ctx := context.WithValue(r.Context(), SDKKey, sdk)
Expand Down
19 changes: 18 additions & 1 deletion internal/trigger/trigger.go → appsdk/trigger.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@
// limitations under the License.
//

package trigger
package appsdk

import (
"context"
"github.com/edgexfoundry/app-functions-sdk-go/appcontext"
"github.com/edgexfoundry/app-functions-sdk-go/internal/common"
"github.com/edgexfoundry/go-mod-core-contracts/clients/logger"
"github.com/edgexfoundry/go-mod-messaging/pkg/types"
"sync"

Expand All @@ -29,3 +32,17 @@ type Trigger interface {
// Initialize performs post creation initializations
Initialize(wg *sync.WaitGroup, ctx context.Context, background <-chan types.MessageEnvelope) (bootstrap.Deferred, error)
}

// TriggerMessageProcessor provides an interface that can be used by custom triggers to invoke the runtime
type TriggerMessageProcessor func(ctx *appcontext.Context, envelope types.MessageEnvelope) error

// TriggerContextBuilder provides an interface to construct an appcontext.Context for message
type TriggerContextBuilder func(env types.MessageEnvelope) *appcontext.Context

// TriggerConfig provides a container to pass context needed fo
type TriggerConfig struct {
Config *common.ConfigurationStruct
Logger logger.LoggingClient
ContextBuilder TriggerContextBuilder
MessageProcessor TriggerMessageProcessor
}
117 changes: 117 additions & 0 deletions appsdk/triggerfactory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//
// Copyright (c) 2020 Technotects
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

package appsdk

import (
"errors"
"fmt"
"github.com/edgexfoundry/app-functions-sdk-go/appcontext"
"github.com/edgexfoundry/app-functions-sdk-go/internal/common"
"github.com/edgexfoundry/app-functions-sdk-go/internal/runtime"
"github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/http"
"github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/messagebus"
"github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/mqtt"
"github.com/edgexfoundry/go-mod-messaging/pkg/types"
"strings"
)

func (sdk *AppFunctionsSDK) defaultTriggerMessageProcessor(edgexcontext *appcontext.Context, envelope types.MessageEnvelope) error {
messageError := sdk.runtime.ProcessMessage(edgexcontext, envelope)

if messageError != nil {
// ProcessMessage logs the error, so no need to log it here.
return messageError.Err
}

return nil
}

func (sdk *AppFunctionsSDK) defaultTriggerContextBuilder(env types.MessageEnvelope) *appcontext.Context {
return &appcontext.Context{
CorrelationID: env.CorrelationID,
Configuration: sdk.config,
LoggingClient: sdk.LoggingClient,
EventClient: sdk.EdgexClients.EventClient,
ValueDescriptorClient: sdk.EdgexClients.ValueDescriptorClient,
CommandClient: sdk.EdgexClients.CommandClient,
NotificationsClient: sdk.EdgexClients.NotificationsClient,
SecretProvider: sdk.secretProvider,
}
}

// RegisterCustomTrigger allows users to register builders for custom trigger types
func (s *AppFunctionsSDK) RegisterCustomTrigger(name string,
builder func(TriggerConfig) (Trigger, error)) error {
nu := strings.ToUpper(name)

if nu == bindingTypeEdgeXMessageBus ||
nu == bindingTypeMessageBus ||
nu == bindingTypeHTTP ||
nu == bindingTypeMQTT {
return errors.New(fmt.Sprintf("cannot register custom trigger for builtin type (%s)", name))
}

if s.customTriggerBuilders == nil {
s.customTriggerBuilders = make(map[string]func(sdk *AppFunctionsSDK) (Trigger, error), 1)
}

s.customTriggerBuilders[nu] = func(sdk *AppFunctionsSDK) (Trigger, error) {
return builder(TriggerConfig{
Config: s.config,
Logger: s.LoggingClient,
ContextBuilder: sdk.defaultTriggerContextBuilder,
MessageProcessor: sdk.defaultTriggerMessageProcessor,
})
}

return nil
}

// setupTrigger configures the appropriate trigger as specified by configuration.
func (sdk *AppFunctionsSDK) setupTrigger(configuration *common.ConfigurationStruct, runtime *runtime.GolangRuntime) Trigger {
var t Trigger
// Need to make dynamic, search for the binding that is input

switch triggerType := strings.ToUpper(configuration.Binding.Type); triggerType {
case bindingTypeHTTP:
sdk.LoggingClient.Info("HTTP trigger selected")
t = &http.Trigger{Configuration: configuration, Runtime: runtime, Webserver: sdk.webserver, EdgeXClients: sdk.EdgexClients}

case bindingTypeMessageBus,
bindingTypeEdgeXMessageBus: // Allows for more explicit name now that we have plain MQTT option also
sdk.LoggingClient.Info("EdgeX MessageBus trigger selected")
t = &messagebus.Trigger{Configuration: configuration, Runtime: runtime, EdgeXClients: sdk.EdgexClients}

case bindingTypeMQTT:
sdk.LoggingClient.Info("External MQTT trigger selected")
t = mqtt.NewTrigger(configuration, runtime, sdk.EdgexClients, sdk.secretProvider)

default:
if builder, found := sdk.customTriggerBuilders[triggerType]; found {
var err error
t, err = builder(sdk)
if err != nil {
sdk.LoggingClient.Error(fmt.Sprintf("failed to initialize custom trigger [%s]: %s", triggerType, err.Error()))
panic(err)
}
} else {
sdk.LoggingClient.Error(fmt.Sprintf("Invalid Trigger type of '%s' specified", configuration.Binding.Type))
}
}

return t
}
192 changes: 192 additions & 0 deletions appsdk/triggerfactory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
//
// Copyright (c) 2020 Technotects
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

package appsdk

import (
"context"
"fmt"
"github.com/edgexfoundry/app-functions-sdk-go/internal/common"
"github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/http"
"github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/messagebus"
"github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/mqtt"
"github.com/edgexfoundry/go-mod-bootstrap/bootstrap"
"github.com/edgexfoundry/go-mod-core-contracts/clients/logger"
"github.com/edgexfoundry/go-mod-messaging/pkg/types"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"strings"
"sync"
"testing"
)

func TestRegisterCustomTrigger_HTTP(t *testing.T) {
name := strings.ToTitle(bindingTypeHTTP)

sdk := AppFunctionsSDK{}
err := sdk.RegisterCustomTrigger(name, nil)

require.NotNil(t, err, "should throw error")
require.NotNil(t, err.Error(), fmt.Sprintf("cannot register custom trigger for builtin type (%s)", name))
require.Zero(t, len(sdk.customTriggerBuilders), "nothing should be registered")
}

func TestRegisterCustomTrigger_MessageBus(t *testing.T) {
name := strings.ToTitle(bindingTypeMessageBus)

sdk := AppFunctionsSDK{}
err := sdk.RegisterCustomTrigger(name, nil)

require.NotNil(t, err, "should throw error")
require.NotNil(t, err.Error(), fmt.Sprintf("cannot register custom trigger for builtin type (%s)", name))
require.Zero(t, len(sdk.customTriggerBuilders), "nothing should be registered")
}

func TestRegisterCustomTrigger_EdgeXMessageBus(t *testing.T) {
name := strings.ToTitle(bindingTypeEdgeXMessageBus)

sdk := AppFunctionsSDK{}
err := sdk.RegisterCustomTrigger(name, nil)

require.NotNil(t, err, "should throw error")
require.NotNil(t, err.Error(), fmt.Sprintf("cannot register custom trigger for builtin type (%s)", name))
require.Zero(t, len(sdk.customTriggerBuilders), "nothing should be registered")
}

func TestRegisterCustomTrigger_MQTT(t *testing.T) {
name := strings.ToTitle(bindingTypeMQTT)

sdk := AppFunctionsSDK{}
err := sdk.RegisterCustomTrigger(name, nil)

require.NotNil(t, err, "should throw error")
require.NotNil(t, err.Error(), fmt.Sprintf("cannot register custom trigger for builtin type (%s)", name))
require.Zero(t, len(sdk.customTriggerBuilders), "nothing should be registered")
}

func TestRegisterCustomTrigger(t *testing.T) {
name := "cUsToM tRiGgEr"
trig := mockCustomTrigger{}

builder := func(c TriggerConfig) (Trigger, error) {
return &trig, nil
}
sdk := AppFunctionsSDK{}
err := sdk.RegisterCustomTrigger(name, builder)

require.Nil(t, err, "should not throw error")
require.Equal(t, len(sdk.customTriggerBuilders), 1, "provided function should be registered")

registeredBuilder := sdk.customTriggerBuilders[strings.ToUpper(name)]
require.NotNil(t, registeredBuilder, "provided function should be registered with uppercase name")

res, err := registeredBuilder(&sdk)
require.NoError(t, err)
require.Equal(t, res, &trig, "will be wrapped but should ultimately return result of builder")
}

func TestSetupTrigger_HTTP(t *testing.T) {
sdk := AppFunctionsSDK{
config: &common.ConfigurationStruct{
Binding: common.BindingInfo{
Type: "http",
},
},
LoggingClient: logger.MockLogger{},
}

trigger := sdk.setupTrigger(sdk.config, sdk.runtime)

require.NotNil(t, trigger, "should be defined")
require.IsType(t, &http.Trigger{}, trigger, "should be an http trigger")
}

func TestSetupTrigger_MessageBus(t *testing.T) {
sdk := AppFunctionsSDK{
config: &common.ConfigurationStruct{
Binding: common.BindingInfo{
Type: "messagebus",
},
},
LoggingClient: logger.MockLogger{},
}

trigger := sdk.setupTrigger(sdk.config, sdk.runtime)

require.NotNil(t, trigger, "should be defined")
require.IsType(t, &messagebus.Trigger{}, trigger, "should be a messagebus trigger")
}

func TestSetupTrigger_EdgeXMessageBus(t *testing.T) {
sdk := AppFunctionsSDK{
config: &common.ConfigurationStruct{
Binding: common.BindingInfo{
Type: "edgex-messagebus",
},
},
LoggingClient: logger.MockLogger{},
}

trigger := sdk.setupTrigger(sdk.config, sdk.runtime)

require.NotNil(t, trigger, "should be defined")
require.IsType(t, &messagebus.Trigger{}, trigger, "should be a messagebus trigger")
}

func TestSetupTrigger_MQTT(t *testing.T) {
sdk := AppFunctionsSDK{
config: &common.ConfigurationStruct{
Binding: common.BindingInfo{
Type: "external-mqtt",
},
},
LoggingClient: logger.MockLogger{},
}

trigger := sdk.setupTrigger(sdk.config, sdk.runtime)

require.NotNil(t, trigger, "should be defined")
require.IsType(t, &mqtt.Trigger{}, trigger, "should be an MQTT trigger")
}

type mockCustomTrigger struct {
}

func (*mockCustomTrigger) Initialize(wg *sync.WaitGroup, ctx context.Context, background <-chan types.MessageEnvelope) (bootstrap.Deferred, error) {
return nil, nil
}

func TestSetupTrigger_CustomType(t *testing.T) {
triggerName := uuid.New().String()

sdk := AppFunctionsSDK{
config: &common.ConfigurationStruct{
Binding: common.BindingInfo{
Type: triggerName,
},
},
LoggingClient: logger.MockLogger{},
}

sdk.RegisterCustomTrigger(triggerName, func(c TriggerConfig) (Trigger, error) {
return &mockCustomTrigger{}, nil
})

trigger := sdk.setupTrigger(sdk.config, sdk.runtime)

require.NotNil(t, trigger, "should be defined")
require.IsType(t, &mockCustomTrigger{}, trigger, "should be a custom trigger")
}

0 comments on commit c18f6a6

Please sign in to comment.