Skip to content

Commit

Permalink
add missing support for reading context (#427)
Browse files Browse the repository at this point in the history
  • Loading branch information
edaniszewski committed Mar 30, 2020
1 parent 96bf932 commit a0e8880
Show file tree
Hide file tree
Showing 8 changed files with 289 additions and 30 deletions.
10 changes: 10 additions & 0 deletions sdk/config/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ type DeviceProto struct {
// back to the default value of 30s.
WriteTimeout time.Duration `yaml:"writeTimeout,omitempty"`

// Context defines any context information which should be associated
// with a device instance's reading(s). If specified here, all prototype
// instances will inherit the context, unless inheritance is disabled.
Context map[string]string `yaml:"context,omitempty"`

// Instances contains the data for all configured instances of the
// device prototype.
Instances []*DeviceInstance `yaml:"instances,omitempty"`
Expand All @@ -96,6 +101,11 @@ type DeviceInstance struct {
// tags, so these are supplemental.
Tags []string `yaml:"tags,omitempty"`

// Context defines any context information which should be associated with
// the device instance's reading(s). Any values specified here will be
// applied to the reading context automatically by the SDK.
Context map[string]string `yaml:"context,omitempty"`

// Data contains any protocol/plugin/device-specific configuration that
// is associated with the device instance. It is the responsibility of the
// plugin to handle these values correctly.
Expand Down
14 changes: 13 additions & 1 deletion sdk/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ type Device struct {
// Data contains any plugin-specific configuration data for the device.
Data map[string]interface{}

// Context contains any contextual information which should be associated
// with the device's reading(s).
Context map[string]string

// Handler is the name of the device's handler.
Handler string

Expand Down Expand Up @@ -124,6 +128,7 @@ func NewDeviceFromConfig(proto *config.DeviceProto, instance *config.DeviceInsta
// device prototype configuration.
var (
data map[string]interface{}
context map[string]string
tags []string
handler string
deviceType string
Expand All @@ -133,6 +138,7 @@ func NewDeviceFromConfig(proto *config.DeviceProto, instance *config.DeviceInsta
// If inheritance is enabled, use the prototype defined value as the base.
if !instance.DisableInheritance {
data = proto.Data
context = proto.Context
tags = proto.Tags
handler = proto.Handler
deviceType = proto.Type
Expand All @@ -144,10 +150,15 @@ func NewDeviceFromConfig(proto *config.DeviceProto, instance *config.DeviceInsta

// Merge instance data.
if err := mergo.Map(&data, instance.Data, mergo.WithOverride, mergo.WithAppendSlice); err != nil {
log.WithField("error", err).Error("[device] failed merging device instance config")
log.WithField("error", err).Error("[device] failed merging device instance config: data")
return nil, err
}

// Merge context data.
if err := mergo.Map(&context, instance.Context, mergo.WithOverride); err != nil {
log.WithField("error", err).Error("[device] failed merging device instance config: context")
}

// Merge tags. It is okay if the same tag is defined more than once, (e.g.
// no need to error), but we do ultimately just want the set of tags.
tags = append(tags, instance.Tags...)
Expand Down Expand Up @@ -245,6 +256,7 @@ func NewDeviceFromConfig(proto *config.DeviceProto, instance *config.DeviceInsta
Type: deviceType,
Tags: deviceTags,
Data: data,
Context: context,
Handler: handler,
Metadata: proto.Metadata,
Info: instance.Info,
Expand Down
78 changes: 78 additions & 0 deletions sdk/device_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ func TestNewDeviceFromConfig(t *testing.T) {
Data: map[string]interface{}{
"port": 5000,
},
Context: map[string]string{
"foo": "bar",
},
Tags: []string{"default/foo"},
Handler: "testhandler",
WriteTimeout: 3 * time.Second,
Expand All @@ -67,6 +70,9 @@ func TestNewDeviceFromConfig(t *testing.T) {
Data: map[string]interface{}{
"address": "localhost",
},
Context: map[string]string{
"123": "456",
},
Output: "temperature",
SortIndex: 1,
Handler: "testhandler2",
Expand All @@ -85,6 +91,7 @@ func TestNewDeviceFromConfig(t *testing.T) {
assert.Equal(t, "testdata", device.Info)
assert.Equal(t, 2, len(device.Tags))
assert.Equal(t, map[string]interface{}{"address": "localhost", "port": 5000}, device.Data)
assert.Equal(t, map[string]string{"foo": "bar", "123": "456"}, device.Context)
assert.Equal(t, "testhandler2", device.Handler)
assert.Equal(t, int32(1), device.SortIndex)
assert.Equal(t, "foo", device.Alias)
Expand All @@ -105,6 +112,9 @@ func TestNewDeviceFromConfig2(t *testing.T) {
Data: map[string]interface{}{
"port": 5000,
},
Context: map[string]string{
"foo": "bar",
},
Tags: []string{"default/foo"},
Handler: "testhandler",
WriteTimeout: 3 * time.Second,
Expand Down Expand Up @@ -132,6 +142,7 @@ func TestNewDeviceFromConfig2(t *testing.T) {
assert.Equal(t, "testdata", device.Info)
assert.Equal(t, 2, len(device.Tags))
assert.Equal(t, map[string]interface{}{"address": "localhost", "port": 5000}, device.Data)
assert.Equal(t, map[string]string{"foo": "bar"}, device.Context)
assert.Equal(t, "testhandler", device.Handler)
assert.Equal(t, int32(1), device.SortIndex)
assert.Equal(t, "foo", device.Alias)
Expand Down Expand Up @@ -208,6 +219,7 @@ func TestNewDeviceFromConfig4(t *testing.T) {
assert.Equal(t, "testdata", device.Info)
assert.Equal(t, 2, len(device.Tags))
assert.Equal(t, map[string]interface{}{"address": "localhost", "port": 5000}, device.Data)
assert.Empty(t, device.Context)
assert.Equal(t, "type2", device.Handler)
assert.Equal(t, int32(1), device.SortIndex)
assert.Equal(t, "foo", device.Alias)
Expand All @@ -227,6 +239,9 @@ func TestNewDeviceFromConfig5a(t *testing.T) {
Data: map[string]interface{}{
"port": 5000,
},
Context: map[string]string{
"foo": "bar",
},
Tags: []string{"default/foo"},
Handler: "testhandler",
WriteTimeout: 3 * time.Second,
Expand All @@ -238,6 +253,9 @@ func TestNewDeviceFromConfig5a(t *testing.T) {
Data: map[string]interface{}{
"address": "localhost",
},
Context: map[string]string{
"abc": "def",
},
SortIndex: 1,
Alias: &config.DeviceAlias{
Name: "foo",
Expand All @@ -253,6 +271,7 @@ func TestNewDeviceFromConfig5a(t *testing.T) {
assert.Equal(t, "testdata", device.Info)
assert.Equal(t, 1, len(device.Tags))
assert.Equal(t, map[string]interface{}{"address": "localhost"}, device.Data)
assert.Equal(t, map[string]string{"abc": "def"}, device.Context)
assert.Equal(t, "type2", device.Handler) // inheritance disabled, does not get proto handler
assert.Equal(t, int32(1), device.SortIndex)
assert.Equal(t, "foo", device.Alias)
Expand Down Expand Up @@ -299,6 +318,7 @@ func TestNewDeviceFromConfig5b(t *testing.T) {
assert.Equal(t, "testdata", device.Info)
assert.Equal(t, 2, len(device.Tags))
assert.Equal(t, map[string]interface{}{"address": "localhost", "port": 5000}, device.Data)
assert.Empty(t, device.Context)
assert.Equal(t, "testhandler", device.Handler) // inheritance enabled, gets proto handler
assert.Equal(t, int32(1), device.SortIndex)
assert.Equal(t, "foo", device.Alias)
Expand Down Expand Up @@ -519,6 +539,64 @@ func TestNewDeviceFromConfig11(t *testing.T) {
assert.Nil(t, device)
}

func TestNewDeviceFromConfig12(t *testing.T) {
// Tests creating a device where inheritance is enabled and the instance context
// overrides some values in the prototype context.
proto := &config.DeviceProto{
Type: "type1",
Metadata: map[string]string{
"a": "b",
},
Data: map[string]interface{}{
"port": 5000,
},
Context: map[string]string{
"foo": "bar",
"123": "456",
},
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",
},
Context: map[string]string{
"123": "abc",
"xyz": "456",
},
Output: "temperature",
SortIndex: 1,
Handler: "testhandler2",
Alias: &config.DeviceAlias{
Name: "foo",
},
ScalingFactor: "2",
WriteTimeout: 5 * time.Second,
DisableInheritance: false,
}

device, err := NewDeviceFromConfig(proto, instance)
assert.NoError(t, err)
assert.Equal(t, "type2", device.Type)
assert.Equal(t, map[string]string{"a": "b"}, device.Metadata)
assert.Equal(t, "testdata", device.Info)
assert.Equal(t, 2, len(device.Tags))
assert.Equal(t, map[string]interface{}{"address": "localhost", "port": 5000}, device.Data)
assert.Equal(t, map[string]string{"foo": "bar", "123": "abc", "xyz": "456"}, device.Context)
assert.Equal(t, "testhandler2", device.Handler)
assert.Equal(t, int32(1), device.SortIndex)
assert.Equal(t, "foo", device.Alias)
assert.Equal(t, float64(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 TestDevice_setAlias_noConf(t *testing.T) {
device := Device{}

Expand Down
4 changes: 2 additions & 2 deletions sdk/output/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func TestOutput_MakeReading(t *testing.T) {
assert.Equal(t, o.Type, r.Type)
assert.Equal(t, o.Unit.Symbol, r.Unit.Symbol)
assert.Equal(t, o.Unit.Name, r.Unit.Name)
assert.Empty(t, r.Info)
assert.Empty(t, r.Context)
assert.NotEmpty(t, r.Timestamp)
}

Expand All @@ -127,7 +127,7 @@ func TestOutput_MakeReading_noUnit(t *testing.T) {
assert.Equal(t, 3, r.Value)
assert.Equal(t, o.Type, r.Type)
assert.Nil(t, r.Unit)
assert.Empty(t, r.Info)
assert.Empty(t, r.Context)
assert.NotEmpty(t, r.Timestamp)
}

Expand Down
34 changes: 26 additions & 8 deletions sdk/output/reading.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,18 @@ type Reading struct {
// Type is the type of the reading, as defined by the Reading's output.
Type string

// Info provides additional information about a reading.
Info string

// Unit describes the unit of measure for the reading.
Unit *Unit

// Value is the reading value itself.
Value interface{}

// Context provides an arbitrary key-value mapping which can be used to
// provide contextual information about the reading. This is not required
// but can be useful if a device provides multiple readings from the
// same output, or readings which are meaningless on their own.
Context map[string]string

// output is the Output used to render and format the reading.
output *Output
}
Expand All @@ -51,6 +54,25 @@ func (reading *Reading) GetOutput() *Output {
return reading.output
}

// WithContext adds a context to the reading. This is useful when creating a
// reading from an output and you wish to in-line the setting of the reading
// context, e.g.
//
// SomeOutput.MakeReading(3).WithContext(map[string]string{"source": "foo"})
//
// This will merge the provided context with any existing context. If there
// is a conflict, the context value provided here will override the pre-existing
// value.
func (reading *Reading) WithContext(ctx map[string]string) *Reading {
if reading.Context == nil {
reading.Context = make(map[string]string)
}
for k, v := range ctx {
reading.Context[k] = v
}
return reading
}

// Scale multiplies the given scaling factor to the Reading value and updates
// the Value with the new scaled value.
//
Expand Down Expand Up @@ -96,14 +118,10 @@ func (reading *Reading) Encode() *synse.V3Reading {
r := synse.V3Reading{
Timestamp: reading.Timestamp,
Type: reading.Type,
Context: map[string]string{}, // todo: adding context to reading
Context: reading.Context,
Unit: unit.Encode(),
}

if reading.Info != "" {
r.Context["info"] = reading.Info
}

switch t := reading.Value.(type) {
case string:
r.Value = &synse.V3Reading_StringValue{StringValue: t}
Expand Down
41 changes: 38 additions & 3 deletions sdk/output/reading_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,39 @@ func TestReading_GetOutput_noOutput(t *testing.T) {
assert.Nil(t, r.GetOutput())
}

func TestReading_WithContext_noContext(t *testing.T) {
r := Reading{}
r.WithContext(map[string]string{})

assert.Empty(t, r.Context)
}

func TestReading_WithContext_newContext(t *testing.T) {
r := Reading{}
r.WithContext(map[string]string{"foo": "bar"})

assert.Equal(t, map[string]string{"foo": "bar"}, r.Context)
}

func TestReading_WithContext_noOverride(t *testing.T) {
r := Reading{
Context: map[string]string{"abc": "def"},
}
r.WithContext(map[string]string{"123": "456"})

assert.Equal(t, map[string]string{"abc": "def", "123": "456"}, r.Context)
}

func TestReading_WithContext_withOverride(t *testing.T) {
r := Reading{
Context: map[string]string{"abc": "def"},
}

r.WithContext(map[string]string{"abc": "456"})

assert.Equal(t, map[string]string{"abc": "456"}, r.Context)
}

func TestReading_Scale(t *testing.T) {
cases := []struct {
value interface{}
Expand Down Expand Up @@ -138,7 +171,7 @@ func TestReading_Encode(t *testing.T) {
r := Reading{
Timestamp: "now",
Type: "testtype",
Info: "foo",
Context: map[string]string{"foo": "bar"},
Value: c.value,
}

Expand All @@ -149,6 +182,7 @@ func TestReading_Encode(t *testing.T) {
assert.Equal(t, "testtype", encoded.Type)
assert.Equal(t, "", encoded.Unit.Name)
assert.Equal(t, "", encoded.Unit.Symbol)
assert.Equal(t, map[string]string{"foo": "bar"}, encoded.Context)
}
}

Expand All @@ -157,7 +191,7 @@ func TestReading_Encode2(t *testing.T) {
r := Reading{
Timestamp: "now",
Type: "testtype",
Info: "foo",
Context: map[string]string{"foo": "bar"},
Value: 123,
Unit: &Unit{
Name: "unit",
Expand All @@ -172,6 +206,7 @@ func TestReading_Encode2(t *testing.T) {
assert.Equal(t, "testtype", encoded.Type)
assert.Equal(t, "unit", encoded.Unit.Name)
assert.Equal(t, "u", encoded.Unit.Symbol)
assert.Equal(t, map[string]string{"foo": "bar"}, encoded.Context)
}

func TestReading_Encode_error(t *testing.T) {
Expand All @@ -187,7 +222,7 @@ func TestReading_Encode_error(t *testing.T) {
r := Reading{
Timestamp: "now",
Type: "testtype",
Info: "foo",
Context: map[string]string{"foo": "bar"},
Value: c.value,
}

Expand Down
Loading

0 comments on commit a0e8880

Please sign in to comment.