diff --git a/appsdk/configurable.go b/appsdk/configurable.go index 5cd5589e2..e7ee329b7 100644 --- a/appsdk/configurable.go +++ b/appsdk/configurable.go @@ -34,6 +34,9 @@ const ( EncryptionKey = "key" InitVector = "initvector" Url = "url" + ExportMethod = "method" + ExportMethodPost = "post" + ExportMethodPut = "put" MimeType = "mimetype" PersistOnError = "persistonerror" SkipVerify = "skipverify" @@ -51,12 +54,24 @@ const ( BrokerAddress = "brokeraddress" ClientID = "clientid" Topic = "topic" + TransformType = "type" + TransformXml = "xml" + TransformJson = "json" AuthMode = "authmode" Tags = "tags" ResponseContentType = "responsecontenttype" + Algorithm = "algorithm" + CompressGZIP = "gzip" + CompressZLIB = "zlib" + EncryptAES = "aes" + Mode = "mode" + BatchByCount = "bycount" + BatchByTime = "bytime" + BatchByTimeAndCount = "bytimecount" ) type postPutParameters struct { + method string url string mimeType string persistOnError bool @@ -80,30 +95,11 @@ type AppFunctionsSDKConfigurable struct { // For example, data generated by a motor does not get passed to functions only interested in data from a thermostat. // This function is a configuration function and returns a function pointer. func (dynamic AppFunctionsSDKConfigurable) FilterByProfileName(parameters map[string]string) appcontext.AppFunction { - profileNames, ok := parameters[ProfileNames] + transform, ok := dynamic.processFilterParameters("FilterByProfileName", parameters, ProfileNames) if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + ProfileNames) return nil } - filterOutBool := false - filterOut, ok := parameters[FilterOut] - if ok { - var err error - filterOutBool, err = strconv.ParseBool(filterOut) - if err != nil { - dynamic.Sdk.LoggingClient.Error("Could not convert filterOut value to bool " + filterOut) - return nil - } - } - - profileNamesCleaned := util.DeleteEmptyAndTrim(strings.FieldsFunc(profileNames, util.SplitComma)) - transform := transforms.Filter{ - FilterValues: profileNamesCleaned, - FilterOut: filterOutBool, - } - dynamic.Sdk.LoggingClient.Debugf("Profile Name Filters (filterOut=%v) are: '%s'", filterOutBool, strings.Join(profileNamesCleaned, ",")) - return transform.FilterByProfileName } @@ -116,30 +112,11 @@ func (dynamic AppFunctionsSDKConfigurable) FilterByProfileName(parameters map[st // For example, data generated by a motor does not get passed to functions only interested in data from a thermostat. // This function is a configuration function and returns a function pointer. func (dynamic AppFunctionsSDKConfigurable) FilterByDeviceName(parameters map[string]string) appcontext.AppFunction { - deviceNames, ok := parameters[DeviceNames] + transform, ok := dynamic.processFilterParameters("FilterByDeviceName", parameters, DeviceNames) if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + DeviceNames) return nil } - filterOutBool := false - filterOut, ok := parameters[FilterOut] - if ok { - var err error - filterOutBool, err = strconv.ParseBool(filterOut) - if err != nil { - dynamic.Sdk.LoggingClient.Error("Could not convert filterOut value to bool " + filterOut) - return nil - } - } - - deviceNamesCleaned := util.DeleteEmptyAndTrim(strings.FieldsFunc(deviceNames, util.SplitComma)) - transform := transforms.Filter{ - FilterValues: deviceNamesCleaned, - FilterOut: filterOutBool, - } - dynamic.Sdk.LoggingClient.Debugf("Device Name Filters (filterOut=%v) are: '%s'", filterOutBool, strings.Join(deviceNamesCleaned, ",")) - return transform.FilterByDeviceName } @@ -152,49 +129,39 @@ func (dynamic AppFunctionsSDKConfigurable) FilterByDeviceName(parameters map[str // For example, pressure reading data does not go to functions only interested in motion data. // This function is a configuration function and returns a function pointer. func (dynamic AppFunctionsSDKConfigurable) FilterByResourceName(parameters map[string]string) appcontext.AppFunction { - resourceNames, ok := parameters[ResourceNames] + transform, ok := dynamic.processFilterParameters("FilterByResourceName", parameters, ResourceNames) if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + ResourceNames) return nil } - filterOutBool := false - filterOut, ok := parameters[FilterOut] - if ok { - var err error - filterOutBool, err = strconv.ParseBool(filterOut) - if err != nil { - dynamic.Sdk.LoggingClient.Error("Could not convert filterOut value to bool " + filterOut) - return nil - } - } - - resourceNamesCleaned := util.DeleteEmptyAndTrim(strings.FieldsFunc(resourceNames, util.SplitComma)) - transform := transforms.Filter{ - FilterValues: resourceNamesCleaned, - FilterOut: filterOutBool, - } - dynamic.Sdk.LoggingClient.Debugf("Resource Name Filters (filterOut=%v) are `%s`", filterOutBool, strings.Join(resourceNamesCleaned, ",")) - return transform.FilterByResourceName } -// TransformToXML transforms an EdgeX event to XML. -// It will return an error and stop the pipeline if a non-edgex -// event is received or if no data is received. +// Transform transforms an EdgeX event to XML or JSON based on specified transform type. +// It will return an error and stop the pipeline if a non-edgex event is received or if no data is received. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) TransformToXML() appcontext.AppFunction { - transform := transforms.Conversion{} - return transform.TransformToXML -} +func (dynamic AppFunctionsSDKConfigurable) Transform(parameters map[string]string) appcontext.AppFunction { + transformType, ok := parameters[TransformType] + if !ok { + dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' parameter for Transform", TransformType) + return nil + } -// TransformToJSON transforms an EdgeX event to JSON. -// It will return an error and stop the pipeline if a non-edgex -// event is received or if no data is received. -// This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) TransformToJSON() appcontext.AppFunction { transform := transforms.Conversion{} - return transform.TransformToJSON + + switch strings.ToLower(transformType) { + case TransformXml: + return transform.TransformToXML + case TransformJson: + return transform.TransformToJSON + default: + dynamic.Sdk.LoggingClient.Errorf( + "Invalid transform type '%s'. Must be '%s' or '%s'", + transformType, + TransformXml, + TransformJson) + return nil + } } // PushToCore pushes the provided value as an event to CoreData using the device name and reading name that have been set. If validation is turned on in @@ -213,7 +180,6 @@ func (dynamic AppFunctionsSDKConfigurable) PushToCore(parameters map[string]stri } deviceName = strings.TrimSpace(deviceName) readingName = strings.TrimSpace(readingName) - dynamic.Sdk.LoggingClient.Debug("PushToCore Parameters", DeviceName, deviceName, ReadingName, readingName) transform := transforms.CoreData{ DeviceName: deviceName, ReadingName: readingName, @@ -221,24 +187,43 @@ func (dynamic AppFunctionsSDKConfigurable) PushToCore(parameters map[string]stri return transform.PushToCoreData } -// CompressWithGZIP compresses data received as either a string,[]byte, or json.Marshaller using gzip algorithm and returns a base64 encoded string as a []byte. +// Compress compresses data received as either a string,[]byte, or json.Marshaller using the specified algorithm (GZIP or ZLIB) +// and returns a base64 encoded string as a []byte. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) CompressWithGZIP() appcontext.AppFunction { - transform := transforms.Compression{} - return transform.CompressWithGZIP -} +func (dynamic AppFunctionsSDKConfigurable) Compress(parameters map[string]string) appcontext.AppFunction { + algorithm, ok := parameters[Algorithm] + if !ok { + dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' parameter for Compress", Algorithm) + return nil + } -// CompressWithZLIB compresses data received as either a string,[]byte, or json.Marshaller using zlib algorithm and returns a base64 encoded string as a []byte. -// This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) CompressWithZLIB() appcontext.AppFunction { transform := transforms.Compression{} - return transform.CompressWithZLIB + + switch strings.ToLower(algorithm) { + case CompressGZIP: + return transform.CompressWithGZIP + case CompressZLIB: + return transform.CompressWithZLIB + default: + dynamic.Sdk.LoggingClient.Errorf( + "Invalid compression algorithm '%s'. Must be '%s' or '%s'", + algorithm, + CompressGZIP, + CompressZLIB) + return nil + } } -// EncryptWithAES encrypts either a string, []byte, or json.Marshaller type using AES encryption. -// It will return a byte[] of the encrypted data. +// Encrypt encrypts either a string, []byte, or json.Marshaller type using specified encryption +// algorithm (AES only at this time). It will return a byte[] of the encrypted data. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) EncryptWithAES(parameters map[string]string) appcontext.AppFunction { +func (dynamic AppFunctionsSDKConfigurable) Encrypt(parameters map[string]string) appcontext.AppFunction { + algorithm, ok := parameters[Algorithm] + if !ok { + dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' parameter for Encrypt", Algorithm) + return nil + } + secretPath := parameters[SecretPath] secretName := parameters[SecretName] encryptionKey := parameters[EncryptionKey] @@ -271,59 +256,24 @@ func (dynamic AppFunctionsSDKConfigurable) EncryptWithAES(parameters map[string] SecretName: secretName, } - return transform.EncryptWithAES -} - -// HTTPPost will send data from the previous function to the specified Endpoint via http POST. If no previous function exists, -// then the event that triggered the pipeline will be used. Passing an empty string to the mimetype -// method will default to application/json. -// This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) HTTPPost(parameters map[string]string) appcontext.AppFunction { - params, err := dynamic.processPostPutParameters(parameters) - if err != nil { - dynamic.Sdk.LoggingClient.Error(err.Error()) + switch strings.ToLower(algorithm) { + case EncryptAES: + return transform.EncryptWithAES + default: + dynamic.Sdk.LoggingClient.Errorf( + "Invalid encryption algorithm '%s'. Must be '%s'", + algorithm, + EncryptAES) return nil } - - var transform transforms.HTTPSender - if len(params.secretPath) != 0 { - transform = transforms.NewHTTPSenderWithSecretHeader( - params.url, - params.mimeType, - params.persistOnError, - params.headerName, - params.secretPath, - params.secretName) - } else { - transform = transforms.NewHTTPSender(params.url, params.mimeType, params.persistOnError) - } - - dynamic.Sdk.LoggingClient.Debugf("HTTPPost Parameters: %v", parameters) - return transform.HTTPPost -} - -// HTTPPostJSON sends data from the previous function to the specified Endpoint via http POST with a mime type of application/json. -// If no previous function exists, then the event that triggered the pipeline will be used. -// This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) HTTPPostJSON(parameters map[string]string) appcontext.AppFunction { - parameters[MimeType] = "application/json" - return dynamic.HTTPPost(parameters) } -// HTTPPostXML sends data from the previous function to the specified Endpoint via http POST with a mime type of application/xml. -// If no previous function exists, then the event that triggered the pipeline will be used. -// This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) HTTPPostXML(parameters map[string]string) appcontext.AppFunction { - parameters[MimeType] = "application/xml" - return dynamic.HTTPPost(parameters) -} - -// HTTPPut will send data from the previous function to the specified Endpoint via http PUT. If no previous function exists, +// HTTPExport will send data from the previous function to the specified Endpoint via http POST or PUT. If no previous function exists, // then the event that triggered the pipeline will be used. Passing an empty string to the mimetype // method will default to application/json. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) HTTPPut(parameters map[string]string) appcontext.AppFunction { - params, err := dynamic.processPostPutParameters(parameters) +func (dynamic AppFunctionsSDKConfigurable) HTTPExport(parameters map[string]string) appcontext.AppFunction { + params, err := dynamic.processHttpExportParameters(parameters) if err != nil { dynamic.Sdk.LoggingClient.Error(err.Error()) return nil @@ -342,113 +292,26 @@ func (dynamic AppFunctionsSDKConfigurable) HTTPPut(parameters map[string]string) transform = transforms.NewHTTPSender(params.url, params.mimeType, params.persistOnError) } - dynamic.Sdk.LoggingClient.Debug("HTTPPut Parameters", Url, transform.URL, MimeType, transform.MimeType) - return transform.HTTPPut -} - -// HTTPPutJSON sends data from the previous function to the specified Endpoint via http PUT with a mime type of application/json. -// If no previous function exists, then the event that triggered the pipeline will be used. -// This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) HTTPPutJSON(parameters map[string]string) appcontext.AppFunction { - parameters[MimeType] = "application/json" - return dynamic.HTTPPut(parameters) -} - -// HTTPPutXML sends data from the previous function to the specified Endpoint via http PUT with a mime type of application/xml. -// If no previous function exists, then the event that triggered the pipeline will be used. -// This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) HTTPPutXML(parameters map[string]string) appcontext.AppFunction { - parameters[MimeType] = "application/xml" - return dynamic.HTTPPut(parameters) -} - -// SetOutputData sets the output data to that passed in from the previous function. -// It will return an error and stop the pipeline if data passed in is not of type []byte, string or json.Marshaller -// This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) SetOutputData(parameters map[string]string) appcontext.AppFunction { - transform := transforms.OutputData{} - - value, ok := parameters[ResponseContentType] - if ok && len(value) > 0 { - transform.ResponseContentType = value - } - - return transform.SetOutputData -} - -// BatchByCount ... -func (dynamic AppFunctionsSDKConfigurable) BatchByCount(parameters map[string]string) appcontext.AppFunction { - batchThreshold, ok := parameters[BatchThreshold] - if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + BatchThreshold) - return nil - } - - thresholdValue, err := strconv.Atoi(batchThreshold) - if err != nil { - dynamic.Sdk.LoggingClient.Error(fmt.Sprintf("Could not parse '%s' to an int for '%s' parameter", batchThreshold, BatchThreshold), "error", err) - return nil - } - transform, err := transforms.NewBatchByCount(thresholdValue) - if err != nil { - dynamic.Sdk.LoggingClient.Error(err.Error()) - } - dynamic.Sdk.LoggingClient.Debug("Batch by count Parameters", BatchThreshold, batchThreshold) - return transform.Batch -} - -// BatchByTime ... -func (dynamic AppFunctionsSDKConfigurable) BatchByTime(parameters map[string]string) appcontext.AppFunction { - timeInterval, ok := parameters[TimeInterval] - if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + TimeInterval) - return nil - } - transform, err := transforms.NewBatchByTime(timeInterval) - if err != nil { - dynamic.Sdk.LoggingClient.Error(err.Error()) - } - dynamic.Sdk.LoggingClient.Debug("Batch by time Parameters", TimeInterval, timeInterval) - return transform.Batch -} - -// BatchByTimeAndCount ... -func (dynamic AppFunctionsSDKConfigurable) BatchByTimeAndCount(parameters map[string]string) appcontext.AppFunction { - timeInterval, ok := parameters[TimeInterval] - if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + TimeInterval) - return nil - } - batchThreshold, ok := parameters[BatchThreshold] - if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + BatchThreshold) - return nil - } - thresholdValue, err := strconv.Atoi(batchThreshold) - if err != nil { - dynamic.Sdk.LoggingClient.Error(fmt.Sprintf("Could not parse '%s' to an int for '%s' parameter", batchThreshold, BatchThreshold), "error", err) - } - transform, err := transforms.NewBatchByTimeAndCount(timeInterval, thresholdValue) - if err != nil { - dynamic.Sdk.LoggingClient.Error(err.Error()) - } - dynamic.Sdk.LoggingClient.Debug("Batch by time and count Parameters", BatchThreshold, batchThreshold, TimeInterval, timeInterval) - return transform.Batch -} - -// JSONLogic ... -func (dynamic AppFunctionsSDKConfigurable) JSONLogic(parameters map[string]string) appcontext.AppFunction { - rule, ok := parameters[Rule] - if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + Rule) + switch strings.ToLower(params.method) { + case ExportMethodPost: + return transform.HTTPPost + case ExportMethodPut: + return transform.HTTPPut + default: + dynamic.Sdk.LoggingClient.Errorf( + "Invalid HTTPExport method of '%s'. Must be '%s' or '%s'", + params.method, + ExportMethodPost, + ExportMethodPut) return nil } - transform := transforms.NewJSONLogic(rule) - return transform.Evaluate } -// MQTTSecretSend -func (dynamic AppFunctionsSDKConfigurable) MQTTSecretSend(parameters map[string]string) appcontext.AppFunction { +// +// MQTTExport will send data from the previous function to the specified Endpoint via MQTT publish. If no previous function exists, +// then the event that triggered the pipeline will be used. +// This function is a configuration function and returns a function pointer. +func (dynamic AppFunctionsSDKConfigurable) MQTTExport(parameters map[string]string) appcontext.AppFunction { var err error qos := 0 retain := false @@ -538,6 +401,109 @@ func (dynamic AppFunctionsSDKConfigurable) MQTTSecretSend(parameters map[string] return transform.MQTTSend } +// SetOutputData sets the output data to that passed in from the previous function. +// It will return an error and stop the pipeline if data passed in is not of type []byte, string or json.Marshaller +// This function is a configuration function and returns a function pointer. +func (dynamic AppFunctionsSDKConfigurable) SetOutputData(parameters map[string]string) appcontext.AppFunction { + transform := transforms.OutputData{} + + value, ok := parameters[ResponseContentType] + if ok && len(value) > 0 { + transform.ResponseContentType = value + } + + return transform.SetOutputData +} + +// Batch sets up Batching of events based on the specified mode parameter (BatchByCount, BatchByTime or BatchByTimeAndCount) +// and mode specific parameters. +// This function is a configuration function and returns a function pointer. +func (dynamic AppFunctionsSDKConfigurable) Batch(parameters map[string]string) appcontext.AppFunction { + mode, ok := parameters[Mode] + if !ok { + dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' parameter for Batch", Mode) + return nil + } + + switch strings.ToLower(mode) { + case BatchByCount: + batchThreshold, ok := parameters[BatchThreshold] + if !ok { + dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' parameter for BatchByCount", BatchThreshold) + return nil + } + + thresholdValue, err := strconv.Atoi(batchThreshold) + if err != nil { + dynamic.Sdk.LoggingClient.Errorf( + "Could not parse '%s' to an int for '%s' parameter for BatchByCount: %s", + batchThreshold, BatchThreshold, err.Error()) + return nil + } + + transform, err := transforms.NewBatchByCount(thresholdValue) + if err != nil { + dynamic.Sdk.LoggingClient.Error(err.Error()) + } + return transform.Batch + + case BatchByTime: + timeInterval, ok := parameters[TimeInterval] + if !ok { + dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' parameter for BatchByTime", TimeInterval) + return nil + } + + transform, err := transforms.NewBatchByTime(timeInterval) + if err != nil { + dynamic.Sdk.LoggingClient.Error(err.Error()) + } + return transform.Batch + + case BatchByTimeAndCount: + timeInterval, ok := parameters[TimeInterval] + if !ok { + dynamic.Sdk.LoggingClient.Error("Could not find " + TimeInterval) + return nil + } + batchThreshold, ok := parameters[BatchThreshold] + if !ok { + dynamic.Sdk.LoggingClient.Error("Could not find " + BatchThreshold) + return nil + } + thresholdValue, err := strconv.Atoi(batchThreshold) + if err != nil { + dynamic.Sdk.LoggingClient.Error(fmt.Sprintf("Could not parse '%s' to an int for '%s' parameter", batchThreshold, BatchThreshold), "error", err) + } + transform, err := transforms.NewBatchByTimeAndCount(timeInterval, thresholdValue) + if err != nil { + dynamic.Sdk.LoggingClient.Error(err.Error()) + } + return transform.Batch + + default: + dynamic.Sdk.LoggingClient.Errorf( + "Invalid batch mode '%s'. Must be '%s', '%s' or '%s'", + mode, + BatchByCount, + BatchByTime, + BatchByTimeAndCount) + return nil + } +} + +// JSONLogic ... +func (dynamic AppFunctionsSDKConfigurable) JSONLogic(parameters map[string]string) appcontext.AppFunction { + rule, ok := parameters[Rule] + if !ok { + dynamic.Sdk.LoggingClient.Error("Could not find " + Rule) + return nil + } + + transform := transforms.NewJSONLogic(rule) + return transform.Evaluate +} + // AddTags adds the configured list of tags to Events passed to the transform. // This function is a configuration function and returns a function pointer. func (dynamic AppFunctionsSDKConfigurable) AddTags(parameters map[string]string) appcontext.AppFunction { @@ -570,24 +536,57 @@ func (dynamic AppFunctionsSDKConfigurable) AddTags(parameters map[string]string) } transform := transforms.NewTags(tags) - dynamic.Sdk.LoggingClient.Debug("Add Tags", Tags, fmt.Sprintf("%v", tags)) - return transform.AddTags } -func (dynamic AppFunctionsSDKConfigurable) processPostPutParameters( +func (dynamic AppFunctionsSDKConfigurable) processFilterParameters( + funcName string, + parameters map[string]string, + paramName string) (*transforms.Filter, bool) { + names, ok := parameters[paramName] + if !ok { + dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' parameter for %s", paramName, funcName) + return nil, false + } + + filterOutBool := false + filterOut, ok := parameters[FilterOut] + if ok { + var err error + filterOutBool, err = strconv.ParseBool(filterOut) + if err != nil { + dynamic.Sdk.LoggingClient.Errorf("Could not convert filterOut value `%s` to bool for $s", filterOut, funcName) + return nil, false + } + } + + namesCleaned := util.DeleteEmptyAndTrim(strings.FieldsFunc(names, util.SplitComma)) + transform := transforms.Filter{ + FilterValues: namesCleaned, + FilterOut: filterOutBool, + } + + return &transform, true +} + +func (dynamic AppFunctionsSDKConfigurable) processHttpExportParameters( parameters map[string]string) (*postPutParameters, error) { result := postPutParameters{} var ok bool + result.method, ok = parameters[ExportMethod] + if !ok { + return nil, fmt.Errorf("HTTPExport Could not find %s", ExportMethod) + } + result.url, ok = parameters[Url] if !ok { - return nil, fmt.Errorf("HTTPPut Could not find %s", Url) + return nil, fmt.Errorf("HTTPExport Could not find %s", Url) } result.mimeType, ok = parameters[MimeType] if !ok { - return nil, fmt.Errorf("HTTPPut Could not find %s", MimeType) + return nil, fmt.Errorf("HTTPExport Could not find %s", MimeType) } // PersistOnError is optional and is false by default. @@ -599,7 +598,7 @@ func (dynamic AppFunctionsSDKConfigurable) processPostPutParameters( result.persistOnError, err = strconv.ParseBool(value) if err != nil { return nil, - fmt.Errorf("HTTPPut Could not parse '%s' to a bool for '%s' parameter: %s", + fmt.Errorf("HTTPExport Could not parse '%s' to a bool for '%s' parameter: %s", value, PersistOnError, err.Error()) @@ -614,15 +613,15 @@ func (dynamic AppFunctionsSDKConfigurable) processPostPutParameters( if len(result.headerName) == 0 && len(result.secretPath) != 0 && len(result.secretName) != 0 { return nil, - fmt.Errorf("HTTPPost missing %s since %s & %s are specified", HeaderName, SecretPath, SecretName) + fmt.Errorf("HTTPExport missing %s since %s & %s are specified", HeaderName, SecretPath, SecretName) } if len(result.secretPath) == 0 && len(result.headerName) != 0 && len(result.secretName) != 0 { return nil, - fmt.Errorf("HTTPPost missing %s since %s & %s are specified", SecretPath, HeaderName, SecretName) + fmt.Errorf("HTTPExport missing %s since %s & %s are specified", SecretPath, HeaderName, SecretName) } if len(result.secretName) == 0 && len(result.secretPath) != 0 && len(result.headerName) != 0 { return nil, - fmt.Errorf("HTTPPost missing %s since %s & %s are specified", SecretName, SecretPath, HeaderName) + fmt.Errorf("HTTPExport missing %s since %s & %s are specified", SecretName, SecretPath, HeaderName) } return &result, nil diff --git a/appsdk/configurable_test.go b/appsdk/configurable_test.go index d968af493..bb49d20af 100644 --- a/appsdk/configurable_test.go +++ b/appsdk/configurable_test.go @@ -21,11 +21,9 @@ import ( "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" "github.com/stretchr/testify/assert" - - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" ) -func TestConfigurableFilterByProfileName(t *testing.T) { +func TestFilterByProfileName(t *testing.T) { configurable := AppFunctionsSDKConfigurable{ Sdk: &AppFunctionsSDK{ LoggingClient: lc, @@ -55,7 +53,7 @@ func TestConfigurableFilterByProfileName(t *testing.T) { } } -func TestConfigurableFilterByDeviceName(t *testing.T) { +func TestFilterByDeviceName(t *testing.T) { configurable := AppFunctionsSDKConfigurable{ Sdk: &AppFunctionsSDK{ LoggingClient: lc, @@ -85,7 +83,7 @@ func TestConfigurableFilterByDeviceName(t *testing.T) { } } -func TestConfigurableFilterByResourceName(t *testing.T) { +func TestFilterByResourceName(t *testing.T) { configurable := AppFunctionsSDKConfigurable{ Sdk: &AppFunctionsSDK{ LoggingClient: lc, @@ -115,21 +113,34 @@ func TestConfigurableFilterByResourceName(t *testing.T) { } } -func TestConfigurableTransformToXML(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{} - - trx := configurable.TransformToXML() - assert.NotNil(t, trx, "return result from TransformToXML should not be nil") -} +func TestTransform(t *testing.T) { + configurable := AppFunctionsSDKConfigurable{ + Sdk: &AppFunctionsSDK{ + LoggingClient: lc, + }, + } -func TestConfigurableTransformToJSON(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{} + tests := []struct { + Name string + TransformType string + ExpectValid bool + }{ + {"Good - XML", "xMl", true}, + {"Good - JSON", "JsOn", true}, + {"Bad Type", "baDType", false}, + } - trx := configurable.TransformToJSON() - assert.NotNil(t, trx, "return result from TransformToJSON should not be nil") + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + params := make(map[string]string) + params[TransformType] = test.TransformType + transform := configurable.Transform(params) + assert.Equal(t, test.ExpectValid, transform != nil) + }) + } } -func TestConfigurableHTTPPostAndPut(t *testing.T) { +func TestHTTPExport(t *testing.T) { configurable := AppFunctionsSDKConfigurable{ Sdk: &AppFunctionsSDK{ LoggingClient: lc, @@ -155,31 +166,33 @@ func TestConfigurableHTTPPostAndPut(t *testing.T) { SecretName *string ExpectValid bool }{ - {"Valid Post - ony required params", http.MethodPost, &testUrl, &testMimeType, nil, nil, nil, nil, true}, + {"Valid Post - ony required params", ExportMethodPost, &testUrl, &testMimeType, nil, nil, nil, nil, true}, {"Valid Post - w/o secrets", http.MethodPost, &testUrl, &testMimeType, &testPersistOnError, nil, nil, nil, true}, - {"Valid Post - with secrets", http.MethodPost, &testUrl, &testMimeType, nil, &testHeaderName, &testSecretPath, &testSecretName, true}, - {"Valid Post - with all params", http.MethodPost, &testUrl, &testMimeType, &testPersistOnError, &testHeaderName, &testSecretPath, &testSecretName, true}, - {"Invalid Post - no url", http.MethodPost, nil, &testMimeType, nil, nil, nil, nil, false}, - {"Invalid Post - no mimeType", http.MethodPost, &testUrl, nil, nil, nil, nil, nil, false}, - {"Invalid Post - bad persistOnError", http.MethodPost, &testUrl, &testMimeType, &testBadPersistOnError, nil, nil, nil, false}, - {"Invalid Post - missing headerName", http.MethodPost, &testUrl, &testMimeType, &testPersistOnError, nil, &testSecretPath, &testSecretName, false}, - {"Invalid Post - missing secretPath", http.MethodPost, &testUrl, &testMimeType, &testPersistOnError, &testHeaderName, nil, &testSecretName, false}, - {"Invalid Post - missing secretName", http.MethodPost, &testUrl, &testMimeType, &testPersistOnError, &testHeaderName, &testSecretPath, nil, false}, - {"Valid Put - ony required params", http.MethodPut, &testUrl, &testMimeType, nil, nil, nil, nil, true}, - {"Valid Put - w/o secrets", http.MethodPut, &testUrl, &testMimeType, &testPersistOnError, nil, nil, nil, true}, + {"Valid Post - with secrets", ExportMethodPost, &testUrl, &testMimeType, nil, &testHeaderName, &testSecretPath, &testSecretName, true}, + {"Valid Post - with all params", ExportMethodPost, &testUrl, &testMimeType, &testPersistOnError, &testHeaderName, &testSecretPath, &testSecretName, true}, + {"Invalid Post - no url", ExportMethodPost, nil, &testMimeType, nil, nil, nil, nil, false}, + {"Invalid Post - no mimeType", ExportMethodPost, &testUrl, nil, nil, nil, nil, nil, false}, + {"Invalid Post - bad persistOnError", ExportMethodPost, &testUrl, &testMimeType, &testBadPersistOnError, nil, nil, nil, false}, + {"Invalid Post - missing headerName", ExportMethodPost, &testUrl, &testMimeType, &testPersistOnError, nil, &testSecretPath, &testSecretName, false}, + {"Invalid Post - missing secretPath", ExportMethodPost, &testUrl, &testMimeType, &testPersistOnError, &testHeaderName, nil, &testSecretName, false}, + {"Invalid Post - missing secretName", ExportMethodPost, &testUrl, &testMimeType, &testPersistOnError, &testHeaderName, &testSecretPath, nil, false}, + {"Valid Put - ony required params", ExportMethodPut, &testUrl, &testMimeType, nil, nil, nil, nil, true}, + {"Valid Put - w/o secrets", ExportMethodPut, &testUrl, &testMimeType, &testPersistOnError, nil, nil, nil, true}, {"Valid Put - with secrets", http.MethodPut, &testUrl, &testMimeType, nil, &testHeaderName, &testSecretPath, &testSecretName, true}, - {"Valid Put - with all params", http.MethodPut, &testUrl, &testMimeType, &testPersistOnError, &testHeaderName, &testSecretPath, &testSecretName, true}, - {"Invalid Put - no url", http.MethodPut, nil, &testMimeType, nil, nil, nil, nil, false}, - {"Invalid Put - no mimeType", http.MethodPut, &testUrl, nil, nil, nil, nil, nil, false}, - {"Invalid Put - bad persistOnError", http.MethodPut, &testUrl, &testMimeType, &testBadPersistOnError, nil, nil, nil, false}, - {"Invalid Put - missing headerName", http.MethodPut, &testUrl, &testMimeType, &testPersistOnError, nil, &testSecretPath, &testSecretName, false}, - {"Invalid Put - missing secretPath", http.MethodPut, &testUrl, &testMimeType, &testPersistOnError, &testHeaderName, nil, &testSecretName, false}, - {"Invalid Put - missing secretName", http.MethodPut, &testUrl, &testMimeType, &testPersistOnError, &testHeaderName, &testSecretPath, nil, false}, + {"Valid Put - with all params", ExportMethodPut, &testUrl, &testMimeType, &testPersistOnError, &testHeaderName, &testSecretPath, &testSecretName, true}, + {"Invalid Put - no url", ExportMethodPut, nil, &testMimeType, nil, nil, nil, nil, false}, + {"Invalid Put - no mimeType", ExportMethodPut, &testUrl, nil, nil, nil, nil, nil, false}, + {"Invalid Put - bad persistOnError", ExportMethodPut, &testUrl, &testMimeType, &testBadPersistOnError, nil, nil, nil, false}, + {"Invalid Put - missing headerName", ExportMethodPut, &testUrl, &testMimeType, &testPersistOnError, nil, &testSecretPath, &testSecretName, false}, + {"Invalid Put - missing secretPath", ExportMethodPut, &testUrl, &testMimeType, &testPersistOnError, &testHeaderName, nil, &testSecretName, false}, + {"Invalid Put - missing secretName", ExportMethodPut, &testUrl, &testMimeType, &testPersistOnError, &testHeaderName, &testSecretPath, nil, false}, } for _, test := range tests { t.Run(test.Name, func(t *testing.T) { params := make(map[string]string) + params[ExportMethod] = test.Method + if test.Url != nil { params[Url] = *test.Url } @@ -204,58 +217,13 @@ func TestConfigurableHTTPPostAndPut(t *testing.T) { params[SecretName] = *test.SecretName } - var transform appcontext.AppFunction - if test.Method == http.MethodPost { - transform = configurable.HTTPPost(params) - } else { - transform = configurable.HTTPPut(params) - } + transform := configurable.HTTPExport(params) assert.Equal(t, test.ExpectValid, transform != nil) }) } } -func TestConfigurableHTTPPostJSON(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } - - params := make(map[string]string) - - // no url in params - params[""] = "" - trx := configurable.HTTPPostJSON(params) - assert.Nil(t, trx, "return result from HTTPPostJSON should be nil") - - params[Url] = "http://url" - params[PersistOnError] = "true" - trx = configurable.HTTPPostJSON(params) - assert.NotNil(t, trx, "return result from HTTPPostJSON should not be nil") -} - -func TestConfigurableHTTPPostXML(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } - - params := make(map[string]string) - - // no url in params - params[""] = "" - trx := configurable.HTTPPostXML(params) - assert.Nil(t, trx, "return result from HTTPPostXML should be nil") - - params[Url] = "http://url" - params[PersistOnError] = "true" - trx = configurable.HTTPPostXML(params) - assert.NotNil(t, trx, "return result from HTTPPostXML should not be nil") -} - -func TestConfigurableSetOutputData(t *testing.T) { +func TestSetOutputData(t *testing.T) { configurable := AppFunctionsSDKConfigurable{ Sdk: &AppFunctionsSDK{ LoggingClient: lc, @@ -292,9 +260,10 @@ func TestBatchByCount(t *testing.T) { } params := make(map[string]string) + params[Mode] = BatchByCount params[BatchThreshold] = "30" - trx := configurable.BatchByCount(params) - assert.NotNil(t, trx, "return result from BatchByCount should not be nil") + transform := configurable.Batch(params) + assert.NotNil(t, transform, "return result for BatchByCount should not be nil") } func TestBatchByTime(t *testing.T) { @@ -305,9 +274,10 @@ func TestBatchByTime(t *testing.T) { } params := make(map[string]string) + params[Mode] = BatchByTime params[TimeInterval] = "10" - trx := configurable.BatchByTime(params) - assert.NotNil(t, trx, "return result from BatchByTime should not be nil") + transform := configurable.Batch(params) + assert.NotNil(t, transform, "return result for BatchByTime should not be nil") } func TestBatchByTimeAndCount(t *testing.T) { @@ -318,11 +288,12 @@ func TestBatchByTimeAndCount(t *testing.T) { } params := make(map[string]string) + params[Mode] = BatchByTimeAndCount params[BatchThreshold] = "30" params[TimeInterval] = "10" - trx := configurable.BatchByTimeAndCount(params) - assert.NotNil(t, trx, "return result from BatchByTimeAndCount should not be nil") + trx := configurable.Batch(params) + assert.NotNil(t, trx, "return result for BatchByTimeAndCount should not be nil") } func TestJSONLogic(t *testing.T) { @@ -339,7 +310,7 @@ func TestJSONLogic(t *testing.T) { } -func TestConfigurableMQTTSecretSend(t *testing.T) { +func TestMQTTExport(t *testing.T) { configurable := AppFunctionsSDKConfigurable{ Sdk: &AppFunctionsSDK{ LoggingClient: lc, @@ -358,11 +329,11 @@ func TestConfigurableMQTTSecretSend(t *testing.T) { params[PersistOnError] = "false" params[AuthMode] = "none" - trx := configurable.MQTTSecretSend(params) + trx := configurable.MQTTExport(params) assert.NotNil(t, trx, "return result from MQTTSecretSend should not be nil") } -func TestAppFunctionsSDKConfigurable_AddTags(t *testing.T) { +func TestAddTags(t *testing.T) { configurable := AppFunctionsSDKConfigurable{ Sdk: &AppFunctionsSDK{ LoggingClient: lc, @@ -395,7 +366,7 @@ func TestAppFunctionsSDKConfigurable_AddTags(t *testing.T) { } } -func TestAppFunctionsSDKConfigurable_EncryptWithAES(t *testing.T) { +func TestEncrypt(t *testing.T) { configurable := AppFunctionsSDKConfigurable{ Sdk: &AppFunctionsSDK{ LoggingClient: lc, @@ -409,23 +380,28 @@ func TestAppFunctionsSDKConfigurable_EncryptWithAES(t *testing.T) { tests := []struct { Name string + Algorithm string EncryptionKey string InitVector string SecretPath string SecretName string ExpectNil bool }{ - {"Good - Key & vector ", key, vector, "", "", false}, - {"Good - Secrets & vector", "", vector, secretsPath, secretName, false}, - {"Bad - No vector ", key, "", "", "", true}, - {"Bad - No Key or secrets ", "", vector, "", "", true}, - {"Bad - Missing secretPath", "", vector, "", secretName, true}, - {"Bad - Missing secretName", "", vector, secretsPath, "", true}, + {"Good - Key & vector ", EncryptAES, key, vector, "", "", false}, + {"Good - Secrets & vector", "aEs", "", vector, secretsPath, secretName, false}, + {"Bad - No algorithm ", "", key, "", "", "", true}, + {"Bad - No vector ", EncryptAES, key, "", "", "", true}, + {"Bad - No Key or secrets ", EncryptAES, "", vector, "", "", true}, + {"Bad - Missing secretPath", EncryptAES, "", vector, "", secretName, true}, + {"Bad - Missing secretName", EncryptAES, "", vector, secretsPath, "", true}, } for _, testCase := range tests { t.Run(testCase.Name, func(t *testing.T) { params := make(map[string]string) + if len(testCase.Algorithm) > 0 { + params[Algorithm] = testCase.Algorithm + } if len(testCase.EncryptionKey) > 0 { params[EncryptionKey] = testCase.EncryptionKey } @@ -439,7 +415,7 @@ func TestAppFunctionsSDKConfigurable_EncryptWithAES(t *testing.T) { params[SecretName] = testCase.SecretName } - transform := configurable.EncryptWithAES(params) + transform := configurable.Encrypt(params) assert.Equal(t, testCase.ExpectNil, transform == nil) }) } diff --git a/appsdk/sdk.go b/appsdk/sdk.go index bdb5d26f8..7db7e4172 100644 --- a/appsdk/sdk.go +++ b/appsdk/sdk.go @@ -251,13 +251,13 @@ func (sdk *AppFunctionsSDK) LoadConfigurablePipeline() ([]appcontext.AppFunction return nil, errors.New( "execution Order has 0 functions specified. You must have a least one function in the pipeline") } - sdk.LoggingClient.Debug("Execution Order", "Functions", strings.Join(executionOrder, ",")) + sdk.LoggingClient.Debugf("Function Pipeline Execution Order: [%s]", pipelineConfig.ExecutionOrder) for _, functionName := range executionOrder { functionName = strings.TrimSpace(functionName) configuration, ok := pipelineConfig.Functions[functionName] if !ok { - return nil, fmt.Errorf("function %s configuration not found in Pipeline.Functions section", functionName) + return nil, fmt.Errorf("function '%s' configuration not found in Pipeline.Functions section", functionName) } result := valueOfType.MethodByName(functionName) @@ -271,7 +271,9 @@ func (sdk *AppFunctionsSDK) LoadConfigurablePipeline() ([]appcontext.AppFunction inputParameters := make([]reflect.Value, result.Type().NumIn()) // set keys to be all lowercase to avoid casing issues from configuration for key := range configuration.Parameters { - configuration.Parameters[strings.ToLower(key)] = configuration.Parameters[key] + value := configuration.Parameters[key] + delete(configuration.Parameters, key) // Make sure the old key has been removed so don't have multiples + configuration.Parameters[strings.ToLower(key)] = value } for index := range inputParameters { parameter := result.Type().In(index) @@ -299,12 +301,31 @@ func (sdk *AppFunctionsSDK) LoadConfigurablePipeline() ([]appcontext.AppFunction } pipeline = append(pipeline, function) - configurable.Sdk.LoggingClient.Debug(fmt.Sprintf("%s function added to configurable pipeline", functionName)) + configurable.Sdk.LoggingClient.Debugf( + "%s function added to configurable pipeline with parameters: [%s]", + functionName, + listParameters(configuration.Parameters)) } return pipeline, nil } +func listParameters(parameters map[string]string) string { + result := "" + first := true + for key, value := range parameters { + if first { + result = fmt.Sprintf("%s='%s'", key, value) + first = false + continue + } + + result += fmt.Sprintf(", %s='%s'", key, value) + } + + return result +} + // SetFunctionsPipeline allows you to define each function to execute and the order in which each function // will be called as each event comes in. func (sdk *AppFunctionsSDK) SetFunctionsPipeline(transforms ...appcontext.AppFunction) error { diff --git a/appsdk/sdk_test.go b/appsdk/sdk_test.go index 7fffc72d4..b5ea8c07d 100644 --- a/appsdk/sdk_test.go +++ b/appsdk/sdk_test.go @@ -272,7 +272,9 @@ func TestLoadConfigurablePipelineNumFunctions(t *testing.T) { functions["FilterByDeviceName"] = common.PipelineFunction{ Parameters: map[string]string{"DeviceNames": "Random-Float-Device, Random-Integer-Device"}, } - functions["TransformToXML"] = common.PipelineFunction{} + functions["Transform"] = common.PipelineFunction{ + Parameters: map[string]string{TransformType: TransformXml}, + } functions["SetOutputData"] = common.PipelineFunction{} sdk := AppFunctionsSDK{ @@ -280,7 +282,7 @@ func TestLoadConfigurablePipelineNumFunctions(t *testing.T) { config: &common.ConfigurationStruct{ Writable: common.WritableInfo{ Pipeline: common.PipelineInfo{ - ExecutionOrder: "FilterByDeviceName, TransformToXML, SetOutputData", + ExecutionOrder: "FilterByDeviceName, Transform, SetOutputData", Functions: functions, }, }, @@ -295,7 +297,9 @@ func TestLoadConfigurablePipelineNumFunctions(t *testing.T) { func TestUseTargetTypeOfByteArrayTrue(t *testing.T) { functions := make(map[string]common.PipelineFunction) - functions["CompressWithGZIP"] = common.PipelineFunction{} + functions["Compress"] = common.PipelineFunction{ + Parameters: map[string]string{Algorithm: CompressGZIP}, + } functions["SetOutputData"] = common.PipelineFunction{} sdk := AppFunctionsSDK{ @@ -303,7 +307,7 @@ func TestUseTargetTypeOfByteArrayTrue(t *testing.T) { config: &common.ConfigurationStruct{ Writable: common.WritableInfo{ Pipeline: common.PipelineInfo{ - ExecutionOrder: "CompressWithGZIP, SetOutputData", + ExecutionOrder: "Compress, SetOutputData", UseTargetTypeOfByteArray: true, Functions: functions, }, @@ -320,7 +324,9 @@ func TestUseTargetTypeOfByteArrayTrue(t *testing.T) { func TestUseTargetTypeOfByteArrayFalse(t *testing.T) { functions := make(map[string]common.PipelineFunction) - functions["CompressWithGZIP"] = common.PipelineFunction{} + functions["Compress"] = common.PipelineFunction{ + Parameters: map[string]string{Algorithm: CompressGZIP}, + } functions["SetOutputData"] = common.PipelineFunction{} sdk := AppFunctionsSDK{ @@ -328,7 +334,7 @@ func TestUseTargetTypeOfByteArrayFalse(t *testing.T) { config: &common.ConfigurationStruct{ Writable: common.WritableInfo{ Pipeline: common.PipelineInfo{ - ExecutionOrder: "CompressWithGZIP, SetOutputData", + ExecutionOrder: "Compress, SetOutputData", UseTargetTypeOfByteArray: false, Functions: functions, },