Skip to content

Commit

Permalink
refactor: Rework SDK to use Interfaces and factory methods (#741)
Browse files Browse the repository at this point in the history
* refactor: Rework SDK to use Interfaces and factory methods

Added mocks for interfaces for use is custom service unit tests.
Refactored to make better use of the dependency injection container (DIC) to propagate the common dependencies through the layers
General file clean up for items flag by IDE as non-standard code.

closes #573

BREAKING CHANGE: App Services will require refactoring to use new interfaces  and factory methods

Signed-off-by: lenny <[email protected]>
  • Loading branch information
lenny-goodell authored Mar 18, 2021
1 parent 3ee2f0b commit 3a57661
Show file tree
Hide file tree
Showing 90 changed files with 3,766 additions and 2,403 deletions.
22 changes: 20 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
#
# Copyright (c) 2021 Intel Corporation
#
# 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.
#

.PHONY: test

GO=CGO_ENABLED=1 GO111MODULE=on go
Expand All @@ -8,9 +24,11 @@ build:
test-template:
make -C ./app-service-template test

test: build test-template
test-sdk:
$(GO) test ./... -coverprofile=coverage.out ./...
$(GO) vet ./...
gofmt -l .
[ "`gofmt -l .`" = "" ]
./app-service-template/bin/test-go-mod-tidy.sh
./app-service-template/bin/test-go-mod-tidy.sh

test: build test-template test-sdk
5 changes: 4 additions & 1 deletion app-service-template/Attribution.txt
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,7 @@ github.com/mattn/go-isatty (MIT) https://github.com/mattn/go-isatty
https://github.com/mattn/go-isatty/blob/master/LICENSE

golang.org/x/sys (Unspecified) https://github.com/golang/sys
https://github.com/golang/sys/blob/master/LICENSE
https://github.com/golang/sys/blob/master/LICENSE

stretchr/objx (MIT) https://github.com/stretchr/objx
https://github.com/stretchr/objx/blob/master/LICENSE
16 changes: 16 additions & 0 deletions app-service-template/Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
#
# Copyright (c) 2021 Intel Corporation
#
# 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.
#

.PHONY: build test clean docker

GO=CGO_ENABLED=1 go
Expand Down
50 changes: 28 additions & 22 deletions app-service-template/functions/sample.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ package functions

import (
"errors"
"fmt"
"strings"

"github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext"
"github.com/edgexfoundry/go-mod-core-contracts/v2/clients"

"github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces"

"github.com/edgexfoundry/go-mod-core-contracts/v2/v2"
"github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos"
)
Expand All @@ -39,27 +41,28 @@ type Sample struct {

// LogEventDetails is example of processing an Event and passing the original Event to to next function in the pipeline
// For more details on the Context API got here: https://docs.edgexfoundry.org/1.3/microservices/application/ContextAPI/
func (s *Sample) LogEventDetails(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) {
edgexcontext.LoggingClient.Debug("LogEventDetails called")
func (s *Sample) LogEventDetails(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) {
lc := ctx.LoggingClient()
lc.Debug("LogEventDetails called")

if len(params) < 1 {
if data == nil {
// Go here for details on Error Handle: https://docs.edgexfoundry.org/1.3/microservices/application/ErrorHandling/
return false, errors.New("no Event Received")
}

event, ok := params[0].(dtos.Event)
event, ok := data.(dtos.Event)
if !ok {
return false, errors.New("type received is not an Event")
}

edgexcontext.LoggingClient.Infof("Event received: ID=%s, Device=%s, and ReadingCount=%d",
lc.Infof("Event received: ID=%s, Device=%s, and ReadingCount=%d",
event.Id,
event.DeviceName,
len(event.Readings))
for index, reading := range event.Readings {
switch strings.ToLower(reading.ValueType) {
case strings.ToLower(v2.ValueTypeBinary):
edgexcontext.LoggingClient.Infof(
lc.Infof(
"Reading #%d received with ID=%s, Resource=%s, ValueType=%s, MediaType=%s and BinaryValue of size=`%d`",
index+1,
reading.Id,
Expand All @@ -68,7 +71,7 @@ func (s *Sample) LogEventDetails(edgexcontext *appcontext.Context, params ...int
reading.MediaType,
len(reading.BinaryValue))
default:
edgexcontext.LoggingClient.Infof("Reading #%d received with ID=%s, Resource=%s, ValueType=%s, Value=`%s`",
lc.Infof("Reading #%d received with ID=%s, Resource=%s, ValueType=%s, Value=`%s`",
index+1,
reading.Id,
reading.ResourceName,
Expand All @@ -83,14 +86,15 @@ func (s *Sample) LogEventDetails(edgexcontext *appcontext.Context, params ...int
}

// ConvertEventToXML is example of transforming an Event and passing the transformed data to to next function in the pipeline
func (s *Sample) ConvertEventToXML(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) {
edgexcontext.LoggingClient.Debug("ConvertEventToXML called")
func (s *Sample) ConvertEventToXML(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) {
lc := ctx.LoggingClient()
lc.Debug("ConvertEventToXML called")

if len(params) < 1 {
if data == nil {
return false, errors.New("no Event Received")
}

event, ok := params[0].(dtos.Event)
event, ok := data.(dtos.Event)
if !ok {
return false, errors.New("type received is not an Event")
}
Expand All @@ -103,34 +107,36 @@ func (s *Sample) ConvertEventToXML(edgexcontext *appcontext.Context, params ...i
// Example of DEBUG message which by default you don't want to be logged.
// To see debug log messages, Set WRITABLE_LOGLEVEL=DEBUG environment variable or
// change LogLevel in configuration.toml before running app service.
edgexcontext.LoggingClient.Debug("Event converted to XML: " + xml)
lc.Debug("Event converted to XML: " + xml)

// Returning true indicates that the pipeline execution should continue with the next function
// using the event passed as input in this case.
return true, xml
}

// OutputXML is an example of processing transformed data
func (s *Sample) OutputXML(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) {
edgexcontext.LoggingClient.Debug("OutputXML called")
func (s *Sample) OutputXML(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) {
lc := ctx.LoggingClient()
lc.Debug("OutputXML called")

if len(params) < 1 {
if data == nil {
return false, errors.New("no XML Received")
}

xml, ok := params[0].(string)
xml, ok := data.(string)
if !ok {
return false, errors.New("type received is not an string")
}

edgexcontext.LoggingClient.Debug(fmt.Sprintf("Outputting the following XML: %s", xml))
lc.Debugf("Outputting the following XML: %s", xml)

// This sends the XML as a response. i.e. publish for MessageBus/MQTT triggers as configured or
// HTTP response to for the HTTP Trigger
// For more details on the Complete() function go here: https://docs.edgexfoundry.org/1.3/microservices/application/ContextAPI/#complete
edgexcontext.Complete([]byte(xml))
// For more details on the SetResponseData() function go here: https://docs.edgexfoundry.org/1.3/microservices/application/ContextAPI/#complete
ctx.SetResponseData([]byte(xml))
ctx.SetResponseContentType(clients.ContentTypeXML)

// Returning false terminates the pipeline execution, so this should be last function specified in the pipeline,
// which is typical in conjunction with usage of .Complete() function.
// which is typical in conjunction with usage of .SetResponseData() function.
return false, nil
}
42 changes: 27 additions & 15 deletions app-service-template/functions/sample_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,44 @@ package functions
import (
"testing"

"github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext"
"github.com/edgexfoundry/go-mod-core-contracts/v2/clients"
"github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger"
"github.com/edgexfoundry/go-mod-core-contracts/v2/v2"
"github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos"
"github.com/stretchr/testify/require"

"github.com/edgexfoundry/app-functions-sdk-go/v2/pkg"
"github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)

// This file contains example of how to unit test pipeline functions
// TODO: Change these sample unit tests to test your custom type and function(s)

var appContext interfaces.AppFunctionContext

func TestMain(m *testing.M) {
//
// This can be changed to a real logger when needing more debug information output to the console
// lc := logger.NewClient("testing", "DEBUG")
//
lc := logger.NewMockClient()
correlationId := uuid.New().String()

// NewAppFuncContextForTest creates a context with basic dependencies for unit testing with the passed in logger
// If more additional dependencies (such as mock clients) are required, then use
// NewAppFuncContext(correlationID string, dic *di.Container) and pass in an initialized DIC (dependency injection container)
appContext = pkg.NewAppFuncContextForTest(correlationId, lc)
}

func TestSample_LogEventDetails(t *testing.T) {
expectedEvent := createTestEvent(t)
expectedContinuePipeline := true

target := NewSample()
actualContinuePipeline, actualEvent := target.LogEventDetails(createTestAppSdkContext(), expectedEvent)
actualContinuePipeline, actualEvent := target.LogEventDetails(appContext, expectedEvent)

assert.Equal(t, expectedContinuePipeline, actualContinuePipeline)
assert.Equal(t, expectedEvent, actualEvent)
Expand All @@ -49,7 +68,7 @@ func TestSample_ConvertEventToXML(t *testing.T) {
expectedContinuePipeline := true

target := NewSample()
actualContinuePipeline, actualXml := target.ConvertEventToXML(createTestAppSdkContext(), event)
actualContinuePipeline, actualXml := target.ConvertEventToXML(appContext, event)

assert.Equal(t, expectedContinuePipeline, actualContinuePipeline)
assert.Equal(t, expectedXml, actualXml)
Expand All @@ -58,17 +77,17 @@ func TestSample_ConvertEventToXML(t *testing.T) {

func TestSample_OutputXML(t *testing.T) {
testEvent := createTestEvent(t)
expectedXml, _ := testEvent.ToXML()
xml, _ := testEvent.ToXML()
expectedContinuePipeline := false
appContext := createTestAppSdkContext()
expectedContentType := clients.ContentTypeXML

target := NewSample()
actualContinuePipeline, result := target.OutputXML(appContext, expectedXml)
actualXml := string(appContext.OutputData)
actualContinuePipeline, result := target.OutputXML(appContext, xml)
actualContentType := appContext.ResponseContentType()

assert.Equal(t, expectedContinuePipeline, actualContinuePipeline)
assert.Nil(t, result)
assert.Equal(t, expectedXml, actualXml)
assert.Equal(t, expectedContentType, actualContentType)
}

func createTestEvent(t *testing.T) dtos.Event {
Expand All @@ -87,10 +106,3 @@ func createTestEvent(t *testing.T) dtos.Event {

return event
}

func createTestAppSdkContext() *appcontext.Context {
return &appcontext.Context{
CorrelationID: uuid.New().String(),
LoggingClient: logger.NewMockClient(),
}
}
38 changes: 23 additions & 15 deletions app-service-template/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ package main
import (
"os"

"github.com/edgexfoundry/app-functions-sdk-go/v2/appsdk"
"github.com/edgexfoundry/app-functions-sdk-go/v2/pkg"
"github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces"
"github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/transforms"

"new-app-service/functions"
Expand All @@ -34,38 +35,45 @@ func main() {
// TODO: See https://docs.edgexfoundry.org/1.3/microservices/application/ApplicationServices/
// for documentation on application services.

edgexSdk := &appsdk.AppFunctionsSDK{ServiceKey: serviceKey}
if err := edgexSdk.Initialize(); err != nil {
edgexSdk.LoggingClient.Errorf("SDK initialization failed: %s", err.Error())
os.Exit(-1)
code := CreateAndRunService(serviceKey, pkg.NewAppService)
os.Exit(code)
}

// CreateAndRunService wraps what would normally be in main() so that it can be unit tested
func CreateAndRunService(serviceKey string, newServiceFactory func(string) (interfaces.ApplicationService, bool)) int {
service, ok := newServiceFactory(serviceKey)
if !ok {
return -1
}

lc := service.LoggingClient()

// TODO: Replace with retrieving your custom ApplicationSettings from configuration
deviceNames, err := edgexSdk.GetAppSettingStrings("DeviceNames")
deviceNames, err := service.GetAppSettingStrings("DeviceNames")
if err != nil {
edgexSdk.LoggingClient.Errorf("failed to retrieve DeviceNames from configuration: %s", err.Error())
os.Exit(-1)
lc.Errorf("failed to retrieve DeviceNames from configuration: %s", err.Error())
return -1
}

// TODO: Replace below functions with built in and/or your custom functions for your use case.
// See https://docs.edgexfoundry.org/1.3/microservices/application/BuiltIn/ for list of built-in functions
sample := functions.NewSample()
err = edgexSdk.SetFunctionsPipeline(
err = service.SetFunctionsPipeline(
transforms.NewFilterFor(deviceNames).FilterByDeviceName,
sample.LogEventDetails,
sample.ConvertEventToXML,
sample.OutputXML)
if err != nil {
edgexSdk.LoggingClient.Errorf("SetFunctionsPipeline returned error: %s", err.Error())
os.Exit(-1)
lc.Errorf("SetFunctionsPipeline returned error: %s", err.Error())
return -1
}

if err := edgexSdk.MakeItRun(); err != nil {
edgexSdk.LoggingClient.Errorf("MakeItRun returned error: %s", err.Error())
os.Exit(-1)
if err := service.MakeItRun(); err != nil {
lc.Errorf("MakeItRun returned error: %s", err.Error())
return -1
}

// TODO: Do any required cleanup here, if needed

os.Exit(0)
return 0
}
Loading

0 comments on commit 3a57661

Please sign in to comment.