Skip to content

Commit

Permalink
add transform fns to allow generalized plugins to manipulate reading …
Browse files Browse the repository at this point in the history
…values (#399)
  • Loading branch information
edaniszewski committed Mar 30, 2020
1 parent 73e9dc0 commit ac64e26
Show file tree
Hide file tree
Showing 10 changed files with 548 additions and 5 deletions.
7 changes: 7 additions & 0 deletions sdk/config/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,13 @@ type DeviceInstance struct {
// e.g. "1e-2".
ScalingFactor string `yaml:"scalingFactor,omitempty"`

// Apply defines a list of functions which are to be applied to the device
// reading values, in the order in which they are defined.
//
// There are some built-in functions that the SDK provides. A plugin can also
// register their own functions.
Apply []string `yaml:"apply,omitempty"`

// WriteTimeout defines a custom write timeout for the device instance. This
// is the time within which the write transaction will remain valid. If left
// unspecified, it will fall back to the default value of 30s.
Expand Down
16 changes: 16 additions & 0 deletions sdk/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/vapor-ware/synse-sdk/sdk/config"
"github.com/vapor-ware/synse-sdk/sdk/errors"
"github.com/vapor-ware/synse-sdk/sdk/funcs"
"github.com/vapor-ware/synse-sdk/sdk/output"
"github.com/vapor-ware/synse-sdk/sdk/utils"
synse "github.com/vapor-ware/synse-server-grpc/go"
Expand Down Expand Up @@ -99,6 +100,10 @@ type Device struct {
// populated via the SDK on device loading and parsing and uses the Handler
// field to match the name of the handler to the actual instance.
handler *DeviceHandler

// fns defines a list of functions which should be applied to the reading value(s)
// for the device. This is called internally, if any fns are defined.
fns []*funcs.Func
}

// NewDeviceFromConfig creates a new instance of a Device from its device prototype
Expand Down Expand Up @@ -179,6 +184,16 @@ func NewDeviceFromConfig(proto *config.DeviceProto, instance *config.DeviceInsta
}
}

var fns []*funcs.Func
for _, fn := range instance.Apply {
f := funcs.Get(fn)
if f == nil {
// fixme: err message
return nil, fmt.Errorf("device specified unknown transform function")
}
fns = append(fns, f)
}

// Override write timeout, if set.
if instance.WriteTimeout != 0 {
writeTimeout = instance.WriteTimeout
Expand All @@ -200,6 +215,7 @@ func NewDeviceFromConfig(proto *config.DeviceProto, instance *config.DeviceInsta
ScalingFactor: instance.ScalingFactor,
WriteTimeout: writeTimeout,
Output: instance.Output,
fns: fns,
}

if err := d.setAlias(instance.Alias); err != nil {
Expand Down
39 changes: 39 additions & 0 deletions sdk/device_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ func TestNewDeviceFromConfig(t *testing.T) {
assert.Equal(t, "2", device.ScalingFactor)
assert.Equal(t, 5*time.Second, device.WriteTimeout)
assert.Equal(t, "temperature", device.Output)
assert.Equal(t, 0, len(device.fns))
}

func TestNewDeviceFromConfig2(t *testing.T) {
Expand All @@ -115,6 +116,7 @@ func TestNewDeviceFromConfig2(t *testing.T) {
"address": "localhost",
},
Output: "temperature",
Apply: []string{"FtoC"},
SortIndex: 1,
Alias: &config.DeviceAlias{
Name: "foo",
Expand All @@ -136,6 +138,7 @@ func TestNewDeviceFromConfig2(t *testing.T) {
assert.Equal(t, "2", device.ScalingFactor)
assert.Equal(t, 3*time.Second, device.WriteTimeout)
assert.Equal(t, "temperature", device.Output)
assert.Equal(t, 1, len(device.fns))
}

func TestNewDeviceFromConfig3(t *testing.T) {
Expand Down Expand Up @@ -211,6 +214,7 @@ func TestNewDeviceFromConfig4(t *testing.T) {
assert.Equal(t, "2", device.ScalingFactor)
assert.Equal(t, 30*time.Second, device.WriteTimeout) // takes the default value
assert.Equal(t, "", device.Output)
assert.Equal(t, 0, len(device.fns))
}

func TestNewDeviceFromConfig5(t *testing.T) {
Expand Down Expand Up @@ -255,6 +259,7 @@ func TestNewDeviceFromConfig5(t *testing.T) {
assert.Equal(t, "2", device.ScalingFactor)
assert.Equal(t, 30*time.Second, device.WriteTimeout) // takes the default value
assert.Equal(t, "", device.Output)
assert.Equal(t, 0, len(device.fns))
}

func TestNewDeviceFromConfig6(t *testing.T) {
Expand Down Expand Up @@ -401,6 +406,40 @@ func TestNewDeviceFromConfig9(t *testing.T) {
assert.Nil(t, device)
}

func TestNewDeviceFromConfig10(t *testing.T) {
// Unknown transformation function specified
proto := &config.DeviceProto{
Type: "type1",
Metadata: map[string]string{
"a": "b",
},
Data: map[string]interface{}{
"port": 5000,
},
Tags: []string{"default/foo"},
Handler: "testhandler",
WriteTimeout: 3 * time.Second,
}
instance := &config.DeviceInstance{
Type: "type2",
Info: "testdata",
Tags: []string{"vapor/io"},
Data: map[string]interface{}{
"address": "localhost",
},
SortIndex: 1,
Handler: "testhandler2",
Apply: []string{"unknown-fn"},
ScalingFactor: "2",
WriteTimeout: 5 * time.Second,
DisableInheritance: false,
}

device, err := NewDeviceFromConfig(proto, instance)
assert.Error(t, err)
assert.Nil(t, device)
}

func TestDevice_setAlias_noConf(t *testing.T) {
device := Device{}

Expand Down
40 changes: 40 additions & 0 deletions sdk/funcs/builtins.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Synse SDK
// Copyright (c) 2019 Vapor IO
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package funcs

import "github.com/vapor-ware/synse-sdk/sdk/utils"

// GetBuiltins returns all of the built-in Funcs supplied by the SDK.
func GetBuiltins() []*Func {
return []*Func{
&FtoC,
}
}

// FtoC is a Func which converts a value from degrees Fahrenheit to
// degrees Celsius.
var FtoC = Func{
Name: "FtoC",
Fn: func(value interface{}) (interface{}, error) {
f, err := utils.ConvertToFloat64(value)
if err != nil {
return nil, err
}
c := float64((f - 32.0) * 5.0 / 9.0)
return c, nil
},
}
79 changes: 79 additions & 0 deletions sdk/funcs/builtins_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Synse SDK
// Copyright (c) 2019 Vapor IO
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package funcs

import (
"testing"

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

func TestGetBuiltins(t *testing.T) {
fns := GetBuiltins()
assert.NotEmpty(t, fns)
}

func TestFtoC_Fn(t *testing.T) {
cases := []struct {
f float64
c float64
}{
{f: -459.67, c: -273.15},
{f: -50, c: -45.56},
{f: -40, c: -40.00},
{f: -30, c: -34.44},
{f: -20, c: -28.89},
{f: -10, c: -23.33},
{f: 0, c: -17.78},
{f: 10, c: -12.22},
{f: 20, c: -6.67},
{f: 30, c: -1.11},
{f: 32, c: 0},
{f: 40, c: 4.44},
{f: 50, c: 10.00},
{f: 60, c: 15.56},
{f: 70, c: 21.11},
{f: 80, c: 26.67},
{f: 90, c: 32.22},
{f: 100, c: 37.78},
{f: 110, c: 43.33},
{f: 120, c: 48.89},
{f: 130, c: 54.44},
{f: 140, c: 60.00},
{f: 150, c: 65.56},
{f: 160, c: 71.11},
{f: 170, c: 76.67},
{f: 180, c: 82.22},
{f: 190, c: 87.78},
{f: 200, c: 93.33},
{f: 212, c: 100},
{f: 300, c: 148.89},
{f: 400, c: 204.44},
{f: 500, c: 260.00},
{f: 600, c: 315.56},
{f: 700, c: 371.11},
{f: 800, c: 426.67},
{f: 900, c: 482.22},
{f: 1000, c: 537.78},
}

for _, c := range cases {
val, err := FtoC.Fn(c.f)
assert.NoError(t, err)
assert.InDelta(t, c.c, val, 0.01)
}
}
67 changes: 67 additions & 0 deletions sdk/funcs/functions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Synse SDK
// Copyright (c) 2019 Vapor IO
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package funcs

import (
"fmt"

"github.com/vapor-ware/synse-sdk/sdk/errors"
)

var registeredFuncs map[string]*Func

func init() {
registeredFuncs = make(map[string]*Func)
for _, f := range GetBuiltins() {
registeredFuncs[f.Name] = f
}
}

// Get gets a Func by its name. If a func with the specified name
// is not found, nil is returned.
func Get(name string) *Func {
return registeredFuncs[name]
}

// Register registers new funcs to the tracked funcs.
func Register(funcs ...*Func) error {
multiErr := errors.NewMultiError("func registration")

for _, f := range funcs {
if _, exists := registeredFuncs[f.Name]; exists {
multiErr.Add(fmt.Errorf("conflict: Func with name '%s' already exists", f.Name))
continue
}
registeredFuncs[f.Name] = f
}
return multiErr.Err()
}

// Func is a function that can be applied to a device reading.
type Func struct {
// Name is the name of the function. This is how it is identified
// and referenced.
Name string

// Fn is the function which will be called on the reading value.
Fn func(value interface{}) (interface{}, error)
}

// Call calls the function defined for the Func.
func (fn *Func) Call(value interface{}) (interface{}, error) {
return fn.Fn(value)
}
Loading

0 comments on commit ac64e26

Please sign in to comment.