Repository: seqsense/aws-iot-device-sdk-go
Pull request #221 (2021-02-24 14:02:13)

Add contribution guide

codecov[bot]: # [Codecov](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/221?src=pr&el=h1) Report
> Merging [#221](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/221?src=pr&el=desc) (aa81549) into [master](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/commit/a5d1acad17c5a1ef85e9781589ec6c522a6fda86?el=desc) (a5d1aca) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/221/graphs/tree.svg?width=650&height=150&src=pr&token=St5U6jzMqv)](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/221?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master     #221   +/-   ##
=======================================
  Coverage   58.44%   58.44%           
=======================================
  Files          28       28           
  Lines        1290     1290           
=======================================
  Hits          754      754           
  Misses        470      470           
  Partials       66       66           
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/221?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/221?src=pr&el=footer). Last update [a5d1aca...aa81549](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/221?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/README.md b/README.md
index 3fcb40d..be3f987 100644
--- a/README.md
+++ b/README.md
@@ -2,4 +2,22 @@
 
 [![Go Reference](https://pkg.go.dev/badge/godoc.org/github.com/seqsense/aws-iot-device-sdk-go.svg)](https://pkg.go.dev/godoc.org/github.com/seqsense/aws-iot-device-sdk-go) ![ci](https://github.com/seqsense/aws-iot-device-sdk-go/workflows/ci/badge.svg) [![codecov](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/branch/master/graph/badge.svg)](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/seqsense/aws-iot-device-sdk-go/blob/master/LICENSE)
 
-Package awsiotdev implements AWS IoT presigned WebSockets dialer for [github.com/at-wat/mqtt-go](https://github.com/at-wat/mqtt-go), and AWS IoT functionalities.
+Package awsiotdev implements AWS IoT presigned WebSockets dialer for [github.com/at-wat/mqtt-go](https://github.com/at-wat/mqtt-go), and AWS IoT features.
+
+Implemented features:
+- Device Shadow
+- Jobs
+- Secure tunneling
+
+## Contributing
+
+Your contributions like bug reports, bug fixes, feature requests and implementing new features are welcome.
+Feel free to open a new issue and/or Pull Request.
+
+*seqsense/aws-iot-device-sdk-go* is developed under [GitHub flow](https://guides.github.com/introduction/flow/).
+Please open an issue and ask first before actually working on any significant PR (e.g. implementing features).
+Please refer to the [interface design guide](doc/interface_design_guide.md).
+
+## License
+
+This package is licensed under [Apache License Version 2.0](./LICENSE).
diff --git a/doc/interface_design_guide.md b/doc/interface_design_guide.md
new file mode 100644
index 0000000..c6e602c
--- /dev/null
+++ b/doc/interface_design_guide.md
@@ -0,0 +1,40 @@
+# Interface design guide
+
+Thank you for contributing to *seqsense/aws-iot-device-sdk-go* package!
+
+Please check out the following design guide when you implement a new feature.
+If this guide can't cover it, please discuss it on a relevant issue.
+
+## Package structure
+
+A feature is implemented as a subpackage.
+For example, AWS IoT Device Shadow is implemented as `shadow` subpackage.
+
+## Feature interface
+
+```go
+// Feature is defined as a Go interface to make it mockable.
+type Feature interface {
+  // AWS IoT feature usually subscribes to one or more MQTT topic(s).
+  // Feature interface implements `mqtt.Handler` to process MQTT messages.
+  mqtt.Handler
+
+  // If a method is not immediately processed locally and return immediately,
+  // it should be controlled by Go context.
+  Method(ctx context.Context, ...) ...
+
+  // If a method is processed locally and immediately returned,
+  // it can be without Go context.
+  ImmediateMethod(...) ...
+
+  // If an event is triggered by AWS IoT, an event handler (callback) should be
+  // set by callback setter.
+  OnEvent(func(...) ...)
+}
+
+// New creates and returns Feature interface.
+// First and second arguments should be Go context and awsiotdev.Device.
+// Last argument should be functional options.
+// Mandatory parameters which don't have default values should be passed between them.
+func New(ctx context.Context, cli awsiotdev.Device, ..., opt ...Option) (Feature, error)
+```
diff --git a/shadow/diff.go b/shadow/diff.go
new file mode 100644
index 0000000..bf47517
--- /dev/null
+++ b/shadow/diff.go
@@ -0,0 +1,179 @@
+// Copyright 2020-2021 SEQSENSE, Inc.
+//
+// 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 shadow
+
+import (
+	"errors"
+	"reflect"
+)
+
+var errInvalidAttribute = errors.New("invalid attribute key")
+
+// ErrUnsupportedMapKeyType is returned if map key type is not string.
+var ErrUnsupportedMapKeyType = errors.New("unsupported map key type")
+
+func stateDiff(base, in interface{}) (interface{}, bool, error) {
+	keys, hasChild, err := attributeKeys(base)
+	if err != nil {
+		return nil, false, err
+	}
+	if !hasChild {
+		baseVal := reflect.ValueOf(base)
+		inVal := reflect.ValueOf(in)
+		if !baseVal.IsValid() {
+			if !inVal.IsValid() {
+				// Both nil
+				return nil, false, nil
+			}
+			// One is nil
+			return in, true, nil
+		} else if !inVal.IsValid() {
+			// One is nil
+			return in, true, nil
+		}
+		switch baseVal.Type().Kind() {
+		case reflect.Array, reflect.Slice:
+			// Compare slice/array
+			switch inVal.Type().Kind() {
+			case reflect.Array, reflect.Slice:
+				if baseVal.Len() != inVal.Len() {
+					// Size differs
+					return in, true, nil
+				}
+				for i := 0; i < baseVal.Len(); i++ {
+					_, hasDiff, err := stateDiff(
+						baseVal.Index(i).Interface(),
+						inVal.Index(i).Interface(),
+					)
+					if err != nil {
+						return nil, false, err
+					}
+					if hasDiff {
+						return in, true, nil
+					}
+				}
+			default:
+				// Type differs
+				return in, true, nil
+			}
+			return nil, false, nil
+		default:
+			// Compare primitive value
+			if reflect.DeepEqual(base, in) {
+				return nil, false, nil
+			}
+		}
+		return in, true, nil
+	}
+
+	keysIn, hasChildIn, err := attributeKeys(in)
+	if err != nil {
+		return nil, false, err
+	}
+	if !hasChildIn {
+		return in, true, nil
+	}
+
+	keysInMap := make(map[string]struct{})
+	for _, k := range keysIn {
+		keysInMap[k] = struct{}{}
+	}
+	out := make(map[string]interface{})
+	for _, k := range keys {
+		if _, ok := keysInMap[k]; !ok {
+			continue
+		}
+		delete(keysInMap, k)
+		a, _ := attributeByKey(base, k)
+		b, err := attributeByKey(in, k)
+		if err != nil {
+			return nil, false, err
+		}
+		d, difer, err := stateDiff(a, b)
+		if err != nil {
+			return nil, false, err
+		}
+		if difer {
+			out[k] = d
+		}
+	}
+	for k := range keysInMap {
+		b, _ := attributeByKey(in, k)
+		out[k] = b
+	}
+	if len(out) == 0 {
+		return nil, false, nil
+	}
+	return out, true, nil
+}
+
+func attributeKeys(a interface{}) ([]string, bool, error) {
+	v := reflect.ValueOf(a)
+	if !v.IsValid() {
+		return nil, false, nil
+	}
+	t := v.Type()
+	switch t.Kind() {
+	case reflect.Map:
+		out := make([]string, v.Len())
+		keys := v.MapKeys()
+		for i := range out {
+			if keys[i].Kind() != reflect.String {
+				return nil, false, ErrUnsupportedMapKeyType
+			}
+			out[i] = keys[i].String()
+		}
+		return out, true, nil
+	case reflect.Struct:
+		out := make([]string, v.NumField())
+		for i := range out {
+			out[i] = t.Field(i).Name
+		}
+		return out, true, nil
+	case reflect.Ptr:
+		return attributeKeys(v.Elem().Interface())
+	}
+	return nil, false, nil
+}
+
+func attributeByKey(a interface{}, k string) (interface{}, error) {
+	ret, err := attributeByKeyImpl(reflect.ValueOf(a), k)
+	if err != nil {
+		return nil, err
+	}
+	return ret.Interface(), nil
+}
+
+func attributeByKeyImpl(v reflect.Value, k string) (reflect.Value, error) {
+	if !v.IsValid() {
+		return reflect.Value{}, errInvalidAttribute
+	}
+	t := v.Type()
+	switch t.Kind() {
+	case reflect.Struct:
+		return v.FieldByName(k), nil
+	case reflect.Map:
+		val := v.MapIndex(reflect.ValueOf(k))
+		if !val.IsValid() {
+			return reflect.Value{}, nil
+		}
+		return val, nil
+	case reflect.Ptr:
+		return attributeByKeyImpl(v.Elem(), k)
+	case reflect.Interface:
+		return attributeByKeyImpl(v.Elem(), k)
+	}
+	return reflect.Value{}, errInvalidAttribute
+}
diff --git a/shadow/option.go b/shadow/option.go
index 342c4db..ce54d51 100644
--- a/shadow/option.go
+++ b/shadow/option.go
@@ -16,7 +16,16 @@ package shadow
 
 // Options stores Device Shadow options.
 type Options struct {
-	Name string
+	Name              string
+	IncrementalUpdate bool
+}
+
+// DefaultOptions is a default Device Shadow options.
+var DefaultOptions = Options{
+	// Report() and Desire() take diff between local Thing Document and given state to
+	// reduce data size to be sent.
+	// If you want to send full state, use WithIncrementalUpdate(false).
+	IncrementalUpdate: true,
 }
 
 // Option is a functional option of UpdateJob.
@@ -29,3 +38,12 @@ func WithName(name string) Option {
 		return nil
 	}
 }
+
+// WithIncrementalUpdate enables increamental update of state document.
+// Enabled by default.
+func WithIncrementalUpdate(enable bool) Option {
+	return func(o *Options) error {
+		o.IncrementalUpdate = enable
+		return nil
+	}
+}
diff --git a/shadow/shadow.go b/shadow/shadow.go
index fb896fb..cb5f000 100644
--- a/shadow/shadow.go
+++ b/shadow/shadow.go
@@ -57,7 +57,7 @@ type shadow struct {
 	mqtt.ServeMux
 	cli       mqtt.Client
 	thingName string
-	name      string
+	opts      *Options
 	doc       *ThingDocument
 	mu        sync.Mutex
 	onDelta   func(delta map[string]interface{})
@@ -75,8 +75,8 @@ func (s *shadow) token() string {
 
 func (s *shadow) topic(operation string) string {
 	prefix := "$aws/things/" + s.thingName + "/shadow"
-	if s.name != "" {
-		prefix += "/name/" + s.name
+	if s.opts.Name != "" {
+		prefix += "/name/" + s.opts.Name
 	}
 	return prefix + "/" + operation
 }
@@ -168,16 +168,16 @@ func (s *shadow) deleteAccepted(msg *mqtt.Message) {
 
 // New creates Thing Shadow interface.
 func New(ctx context.Context, cli awsiotdev.Device, opt ...Option) (Shadow, error) {
-	opts := &Options{}
+	opts := DefaultOptions
 	for _, o := range opt {
-		if err := o(opts); err != nil {
+		if err := o(&opts); err != nil {
 			return nil, ioterr.New(err, "applying option")
 		}
 	}
 	s := &shadow{
 		cli:       cli,
 		thingName: cli.ThingName(),
-		name:      opts.Name,
+		opts:      &opts,
 		doc: &ThingDocument{
 			State: ThingState{
 				Desired:  map[string]interface{}{},
@@ -226,6 +226,20 @@ func New(ctx context.Context, cli awsiotdev.Device, opt ...Option) (Shadow, erro
 }
 
 func (s *shadow) Report(ctx context.Context, state interface{}) (*ThingDocument, error) {
+	if s.opts.IncrementalUpdate {
+		var hasDiff bool
+		var err error
+		state, hasDiff, err = stateDiff(s.doc.State.Reported, state)
+		if err != nil {
+			return nil, err
+		}
+		if !hasDiff {
+			s.mu.Lock()
+			doc := s.doc.clone()
+			s.mu.Unlock()
+			return doc, nil
+		}
+	}
 	rawState, err := json.Marshal(state)
 	if err != nil {
 		return nil, ioterr.New(err, "marshaling state")
@@ -277,6 +291,20 @@ func (s *shadow) Report(ctx context.Context, state interface{}) (*ThingDocument,
 }
 
 func (s *shadow) Desire(ctx context.Context, state interface{}) (*ThingDocument, error) {
+	if s.opts.IncrementalUpdate {
+		var hasDiff bool
+		var err error
+		state, hasDiff, err = stateDiff(s.doc.State.Desired, state)
+		if err != nil {
+			return nil, err
+		}
+		if !hasDiff {
+			s.mu.Lock()
+			doc := s.doc.clone()
+			s.mu.Unlock()
+			return doc, nil
+		}
+	}
 	rawState, err := json.Marshal(state)
 	if err != nil {
 		return nil, ioterr.New(err, "marshaling state")


Test patch:
diff --git a/shadow/diff_test.go b/shadow/diff_test.go
new file mode 100644
index 0000000..ba7857d
--- /dev/null
+++ b/shadow/diff_test.go
@@ -0,0 +1,348 @@
+// Copyright 2020-2021 SEQSENSE, Inc.
+//
+// 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 shadow
+
+import (
+	"errors"
+	"fmt"
+	"reflect"
+	"sort"
+	"testing"
+)
+
+type testStruct struct {
+	A, B, C int
+	S       string
+}
+type testSubStruct struct {
+	S1, S2 int
+}
+type testStructNested struct {
+	A, B, C int
+	S       testSubStruct
+}
+
+func TestStateDiff(t *testing.T) {
+	testCases := map[string]struct {
+		base    interface{}
+		input   interface{}
+		diff    interface{}
+		hasDiff bool
+		err     error
+	}{
+		"Map2Map": {
+			base:    map[string]int{"a": 1, "b": 2, "c": 3},
+			input:   map[string]int{"a": 1, "b": 3, "d": 4},
+			diff:    map[string]int{"b": 3, "d": 4},
+			hasDiff: true,
+		},
+		"Map2Map_Equal": {
+			base:    map[string]int{"a": 1, "b": 2, "c": 3},
+			input:   map[string]int{"a": 1, "c": 3, "b": 2},
+			hasDiff: false,
+		},
+		"Map2Map_Remove": {
+			base:    map[string]interface{}{"A": 1, "B": 2, "S": "test"},
+			input:   map[string]interface{}{"B": 2, "A": 1, "S": nil},
+			diff:    map[string]interface{}{"S": nil},
+			hasDiff: true,
+		},
+		"Map2Nil": {
+			base:    map[string]int{"a": 1, "b": 2, "c": 3},
+			input:   nil,
+			diff:    nil,
+			hasDiff: true,
+		},
+		"Nil2Nil": {
+			base:    nil,
+			input:   nil,
+			diff:    nil,
+			hasDiff: false,
+		},
+		"Nil2Struct": {
+			base:    nil,
+			input:   testStruct{A: 1, B: 3, S: "test"},
+			diff:    testStruct{A: 1, B: 3, S: "test"},
+			hasDiff: true,
+		},
+		"Map2Struct": {
+			base:    map[string]interface{}{"A": 1, "B": 2, "S": "test"},
+			input:   testStruct{A: 1, B: 3, S: "test"},
+			diff:    map[string]interface{}{"B": 3, "C": 0},
+			hasDiff: true,
+		},
+		"Map2StructPtr": {
+			base:    map[string]interface{}{"A": 1, "B": 2, "S": "test"},
+			input:   &testStruct{A: 1, B: 3, S: "test"},
+			diff:    map[string]interface{}{"B": 3, "C": 0},
+			hasDiff: true,
+		},
+		"Map2NestedStruct_TypeChange": {
+			base:    map[string]interface{}{"A": 1, "B": 2, "S": 3},
+			input:   testStructNested{A: 1, B: 3, S: testSubStruct{S1: 2}},
+			diff:    map[string]interface{}{"B": 3, "C": 0, "S": testSubStruct{S1: 2}},
+			hasDiff: true,
+		},
+		"Map2Struct_Equal": {
+			base:    map[string]interface{}{"B": 2, "A": 1, "C": 0, "S": "test"},
+			input:   testStruct{A: 1, B: 2, S: "test"},
+			hasDiff: false,
+		},
+		"Map2StructPtr_Equal": {
+			base:    map[string]interface{}{"B": 2, "A": 1, "C": 0, "S": "test"},
+			input:   &testStruct{A: 1, B: 2, S: "test"},
+			hasDiff: false,
+		},
+		"Nil2Map": {
+			base:    nil,
+			input:   map[string]interface{}{"A": 1, "B": 3, "S": "test"},
+			diff:    map[string]interface{}{"A": 1, "B": 3, "S": "test"},
+			hasDiff: true,
+		},
+		"WrongType": {
+			base:  map[string]interface{}{"A": 1, "B": 3, "S": "test"},
+			input: map[int]interface{}{1: 1, 3: 3, 4: "test"},
+			err:   ErrUnsupportedMapKeyType,
+		},
+		"WrongType2": {
+			base:  map[int]interface{}{1: 1, 3: 3, 4: "test"},
+			input: map[string]interface{}{"A": 1, "B": 3, "S": "test"},
+			err:   ErrUnsupportedMapKeyType,
+		},
+		"InterfaceSliceSize": {
+			base: map[string]interface{}{"A": []interface{}{1, 2, 4, 5}},
+			input: struct{ A []int }{
+				A: []int{1, 2, 3, 4, 5},
+			},
+			diff:    map[string]interface{}{"A": []interface{}{1, 2, 3, 4, 5}},
+			hasDiff: true,
+		},
+		"InterfaceSliceOrdering": {
+			base: map[string]interface{}{"A": []interface{}{1, 3, 2, 4, 5}},
+			input: struct{ A []int }{
+				A: []int{1, 2, 3, 4, 5},
+			},
+			diff:    map[string]interface{}{"A": []interface{}{1, 2, 3, 4, 5}},
+			hasDiff: true,
+		},
+		"InterfaceSlice": {
+			base: map[string]interface{}{"A": []interface{}{1, 2, 10, 4, 5}},
+			input: struct{ A []int }{
+				A: []int{1, 2, 3, 4, 5},
+			},
+			diff:    map[string]interface{}{"A": []interface{}{1, 2, 3, 4, 5}},
+			hasDiff: true,
+		},
+		"InterfaceSliceEqual": {
+			base: map[string]interface{}{"A": []interface{}{1, 2, 3, 4, 5}},
+			input: struct{ A []int }{
+				A: []int{1, 2, 3, 4, 5},
+			},
+			hasDiff: false,
+		},
+		"InterfaceArraySize": {
+			base: map[string]interface{}{"A": []interface{}{1, 2, 4, 5}},
+			input: struct{ A [5]int }{
+				A: [5]int{1, 2, 3, 4, 5},
+			},
+			diff:    map[string]interface{}{"A": []interface{}{1, 2, 3, 4, 5}},
+			hasDiff: true,
+		},
+		"InterfaceArrayEqual": {
+			base: map[string]interface{}{"A": []interface{}{1, 2, 3, 4, 5}},
+			input: struct{ A [5]int }{
+				A: [5]int{1, 2, 3, 4, 5},
+			},
+			hasDiff: false,
+		},
+	}
+	for name, tt := range testCases {
+		tt := tt
+		t.Run(name, func(t *testing.T) {
+			diff, hasDiff, err := stateDiff(tt.base, tt.input)
+			if tt.err != nil {
+				if !errors.Is(tt.err, err) {
+					t.Errorf("Expected error: '%v', got: '%v'", tt.err, err)
+				}
+				return
+			}
+			if hasDiff != tt.hasDiff {
+				t.Errorf("Expected: hasDiff=%v, got: hasDiff=%v", tt.hasDiff, hasDiff)
+			}
+			if a, b := fmt.Sprint(tt.diff), fmt.Sprint(diff); a != b {
+				t.Errorf("Expected: %+v, got: %+v", a, b)
+			}
+		})
+	}
+}
+
+func TestAttributeKeys(t *testing.T) {
+	testCases := map[string]struct {
+		input    interface{}
+		keys     []string
+		hasChild bool
+		err      error
+	}{
+		"Slice": {
+			input: []int{1, 2, 3},
+		},
+		"Map": {
+			input:    map[string]int{"a": 1, "b": 2},
+			keys:     []string{"a", "b"},
+			hasChild: true,
+		},
+		"Struct": {
+			input:    testStruct{},
+			keys:     []string{"A", "B", "C", "S"},
+			hasChild: true,
+		},
+		"StructPtr": {
+			input:    &testStruct{},
+			keys:     []string{"A", "B", "C", "S"},
+			hasChild: true,
+		},
+		"NestedStruct": {
+			input:    testStructNested{},
+			keys:     []string{"A", "B", "C", "S"},
+			hasChild: true,
+		},
+		"Int": {
+			input: 1,
+		},
+	}
+	for name, tt := range testCases {
+		tt := tt
+		t.Run(name, func(t *testing.T) {
+			keys, hasChild, err := attributeKeys(tt.input)
+			if tt.err != nil {
+				if !errors.Is(tt.err, err) {
+					t.Errorf("Expected error: '%v', got: '%v'", tt.err, err)
+				}
+				return
+			}
+
+			if hasChild != tt.hasChild {
+				t.Errorf("Expected: hasChild=%v, got: hasChild=%v", tt.hasChild, hasChild)
+			}
+			sort.Strings(keys)
+			if !reflect.DeepEqual(tt.keys, keys) {
+				t.Errorf("Expected: %+v, got: %+v", tt.keys, keys)
+			}
+		})
+	}
+}
+
+func TestAttributeByKey(t *testing.T) {
+	testCases := map[string]struct {
+		input    interface{}
+		keyValue map[string]interface{}
+		err      error
+	}{
+		"Map": {
+			input:    map[string]int{"a": 1, "b": 2},
+			keyValue: map[string]interface{}{"a": 1, "b": 2},
+		},
+		"Struct": {
+			input:    testStruct{A: 2, S: "test"},
+			keyValue: map[string]interface{}{"A": 2, "B": 0, "C": 0, "S": "test"},
+		},
+		"StructPtr": {
+			input:    &testStruct{A: 2, S: "test"},
+			keyValue: map[string]interface{}{"A": 2, "B": 0, "C": 0, "S": "test"},
+		},
+		"NestedStruct": {
+			input:    testStructNested{A: 3, S: testSubStruct{S2: 4}},
+			keyValue: map[string]interface{}{"A": 3, "S": testSubStruct{S2: 4}},
+		},
+	}
+	for name, tt := range testCases {
+		tt := tt
+		t.Run(name, func(t *testing.T) {
+			for k, v := range tt.keyValue {
+				a, err := attributeByKey(tt.input, k)
+				if tt.err != nil {
+					if !errors.Is(tt.err, err) {
+						t.Errorf("Expected error: '%v', got: '%v'", tt.err, err)
+					}
+					return
+				}
+				if !reflect.DeepEqual(v, a) {
+					t.Errorf("Expected: %+v, got: %+v", v, a)
+				}
+			}
+		})
+	}
+}
+
+func BenchmarkStateDiff(b *testing.B) {
+	testCases := map[string]struct {
+		base, input interface{}
+	}{
+		"Map2Map": {
+			base: map[string]interface{}{
+				"String":  "test data",
+				"Integer": 1234,
+				"Array":   []string{"value1", "value2", "value3", "value4"},
+			},
+			input: map[string]interface{}{
+				"String":  "test data2",
+				"Integer": 1235,
+				"Array":   []string{"value1", "value2", "value3", "value4"},
+			},
+		},
+		"Map2Struct": {
+			base: map[string]interface{}{
+				"String":  "test data",
+				"Integer": 1234,
+				"Array":   []string{"value1", "value2", "value3", "value4"},
+			},
+			input: struct {
+				String  string
+				Integer int
+				Array   []string
+			}{
+				String:  "test data2",
+				Integer: 1235,
+				Array:   []string{"value1", "value2", "value3", "value4"},
+			},
+		},
+		"Map2StructPtr": {
+			base: map[string]interface{}{
+				"String":  "test data",
+				"Integer": 1234,
+				"Array":   []string{"value1", "value2", "value3", "value4"},
+			},
+			input: &struct {
+				String  string
+				Integer int
+				Array   []string
+			}{
+				String:  "test data2",
+				Integer: 1235,
+				Array:   []string{"value1", "value2", "value3", "value4"},
+			},
+		},
+	}
+	for name, tt := range testCases {
+		b.Run(name, func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				_, _, err := stateDiff(tt.base, tt.input)
+				if err != nil {
+					b.Fatal(err)
+				}
+			}
+		})
+	}
+}
diff --git a/shadow/shadow_test.go b/shadow/shadow_test.go
index 6b28bcf..cb8a8a8 100644
--- a/shadow/shadow_test.go
+++ b/shadow/shadow_test.go
@@ -579,204 +579,214 @@ func TestGet(t *testing.T) {
 }
 
 func TestDesire(t *testing.T) {
-	testCases := map[string]struct {
-		input         map[string]interface{}
-		response      interface{}
-		responseTopic string
-		expected      interface{}
-		err           error
-	}{
-		"Success": {
-			input: map[string]interface{}{"key": "value"},
-			response: &ThingDocument{
-				Version: 5,
-				State: ThingState{
-					Desired: map[string]interface{}{"key": "value"},
-				},
-				Metadata: ThingStateMetadata{
-					Desired: map[string]interface{}{"key": Metadata{Timestamp: 1234}},
-				},
-			},
-			responseTopic: "update/accepted",
-			expected: &ThingDocument{
-				Version: 5,
-				State: ThingState{
-					Desired:  map[string]interface{}{"key": "value"},
-					Reported: map[string]interface{}{},
-					Delta:    map[string]interface{}{},
+	for name, inc := range map[string]bool{"Incremental": true, "NoIncremental": false} {
+		inc := inc
+		t.Run(name, func(t *testing.T) {
+			testCases := map[string]struct {
+				input         map[string]interface{}
+				response      interface{}
+				responseTopic string
+				expected      interface{}
+				err           error
+			}{
+				"Success": {
+					input: map[string]interface{}{"key": "value"},
+					response: &ThingDocument{
+						Version: 5,
+						State: ThingState{
+							Desired: map[string]interface{}{"key": "value"},
+						},
+						Metadata: ThingStateMetadata{
+							Desired: map[string]interface{}{"key": Metadata{Timestamp: 1234}},
+						},
+					},
+					responseTopic: "update/accepted",
+					expected: &ThingDocument{
+						Version: 5,
+						State: ThingState{
+							Desired:  map[string]interface{}{"key": "value"},
+							Reported: map[string]interface{}{},
+							Delta:    map[string]interface{}{},
+						},
+						Metadata: ThingStateMetadata{
+							Desired:  NestedMetadata{"key": Metadata{Timestamp: 1234}},
+							Reported: NestedMetadata{},
+							Delta:    NestedMetadata{},
+						},
+					},
 				},
-				Metadata: ThingStateMetadata{
-					Desired:  NestedMetadata{"key": Metadata{Timestamp: 1234}},
-					Reported: NestedMetadata{},
-					Delta:    NestedMetadata{},
+				"Error": {
+					input: map[string]interface{}{"key": "value"},
+					response: &ErrorResponse{
+						Code:    400,
+						Message: "Reason",
+					},
+					responseTopic: "update/rejected",
+					err: &ErrorResponse{
+						Code:    400,
+						Message: "Reason",
+					},
 				},
-			},
-		},
-		"Error": {
-			input: map[string]interface{}{"key": "value"},
-			response: &ErrorResponse{
-				Code:    400,
-				Message: "Reason",
-			},
-			responseTopic: "update/rejected",
-			err: &ErrorResponse{
-				Code:    400,
-				Message: "Reason",
-			},
-		},
-	}
+			}
 
-	for name, testCase := range testCases {
-		testCase := testCase
-		t.Run(name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+			for name, testCase := range testCases {
+				testCase := testCase
+				t.Run(name, func(t *testing.T) {
+					ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+
+					var s Shadow
+					var cli *mockDevice
+					cli = &mockDevice{
+						Client: &mockmqtt.Client{
+							PublishFn: func(ctx context.Context, msg *mqtt.Message) error {
+								req := &simpleRequest{}
+								if err := json.Unmarshal(msg.Payload, req); err != nil {
+									t.Error(err)
+									cancel()
+									return err
+								}
+								res := testCase.response
+								setClientToken(res, req.ClientToken)
+								bres, err := json.Marshal(res)
+								if err != nil {
+									t.Error(err)
+									cancel()
+									return err
+								}
+								cli.Serve(&mqtt.Message{
+									Topic:   s.(*shadow).topic(testCase.responseTopic),
+									Payload: bres,
+								})
+								return nil
+							},
+						},
+					}
+					var err error
+					s, err = New(ctx, cli, WithIncrementalUpdate(inc))
+					if err != nil {
+						t.Fatal(err)
+					}
+					cli.Handle(s)
 
-			var s Shadow
-			var cli *mockDevice
-			cli = &mockDevice{
-				Client: &mockmqtt.Client{
-					PublishFn: func(ctx context.Context, msg *mqtt.Message) error {
-						req := &simpleRequest{}
-						if err := json.Unmarshal(msg.Payload, req); err != nil {
-							t.Error(err)
-							cancel()
-							return err
+					doc, err := s.Desire(ctx, testCase.input)
+					if err != nil {
+						setClientToken(err, "")
+						if !reflect.DeepEqual(testCase.err, err) {
+							t.Fatalf("Expected error: %v, got: %v", testCase.err, err)
 						}
-						res := testCase.response
-						setClientToken(res, req.ClientToken)
-						bres, err := json.Marshal(res)
-						if err != nil {
-							t.Error(err)
-							cancel()
-							return err
+					} else {
+						if !reflect.DeepEqual(testCase.expected, doc) {
+							t.Errorf("Expected document: %v, got: %v", testCase.expected, doc)
 						}
-						cli.Serve(&mqtt.Message{
-							Topic:   s.(*shadow).topic(testCase.responseTopic),
-							Payload: bres,
-						})
-						return nil
-					},
-				},
-			}
-			var err error
-			s, err = New(ctx, cli)
-			if err != nil {
-				t.Fatal(err)
-			}
-			cli.Handle(s)
-
-			doc, err := s.Desire(ctx, testCase.input)
-			if err != nil {
-				setClientToken(err, "")
-				if !reflect.DeepEqual(testCase.err, err) {
-					t.Fatalf("Expected error:\n%+v\ngot:\n%+v", testCase.err, err)
-				}
-			} else {
-				if !reflect.DeepEqual(testCase.expected, doc) {
-					t.Errorf("Expected document:\n%+v\ngot:\n%+v", testCase.expected, doc)
-				}
+					}
+				})
 			}
 		})
 	}
 }
 
 func TestReport(t *testing.T) {
-	testCases := map[string]struct {
-		input         map[string]interface{}
-		response      interface{}
-		responseTopic string
-		expected      interface{}
-		err           error
-	}{
-		"Success": {
-			input: map[string]interface{}{"key": "value"},
-			response: &ThingDocument{
-				Version: 5,
-				State: ThingState{
-					Reported: map[string]interface{}{"key": "value"},
-				},
-				Metadata: ThingStateMetadata{
-					Reported: map[string]interface{}{"key": Metadata{Timestamp: 1234}},
-				},
-			},
-			responseTopic: "update/accepted",
-			expected: &ThingDocument{
-				Version: 5,
-				State: ThingState{
-					Reported: map[string]interface{}{"key": "value"},
-					Desired:  map[string]interface{}{},
-					Delta:    map[string]interface{}{},
+	for name, inc := range map[string]bool{"Incremental": true, "NoIncremental": false} {
+		inc := inc
+		t.Run(name, func(t *testing.T) {
+			testCases := map[string]struct {
+				input         map[string]interface{}
+				response      interface{}
+				responseTopic string
+				expected      interface{}
+				err           error
+			}{
+				"Success": {
+					input: map[string]interface{}{"key": "value"},
+					response: &ThingDocument{
+						Version: 5,
+						State: ThingState{
+							Reported: map[string]interface{}{"key": "value"},
+						},
+						Metadata: ThingStateMetadata{
+							Reported: map[string]interface{}{"key": Metadata{Timestamp: 1234}},
+						},
+					},
+					responseTopic: "update/accepted",
+					expected: &ThingDocument{
+						Version: 5,
+						State: ThingState{
+							Reported: map[string]interface{}{"key": "value"},
+							Desired:  map[string]interface{}{},
+							Delta:    map[string]interface{}{},
+						},
+						Metadata: ThingStateMetadata{
+							Reported: NestedMetadata{"key": Metadata{Timestamp: 1234}},
+							Desired:  NestedMetadata{},
+							Delta:    NestedMetadata{},
+						},
+					},
 				},
-				Metadata: ThingStateMetadata{
-					Reported: NestedMetadata{"key": Metadata{Timestamp: 1234}},
-					Desired:  NestedMetadata{},
-					Delta:    NestedMetadata{},
+				"Error": {
+					input: map[string]interface{}{"key": "value"},
+					response: &ErrorResponse{
+						Code:    400,
+						Message: "Reason",
+					},
+					responseTopic: "update/rejected",
+					err: &ErrorResponse{
+						Code:    400,
+						Message: "Reason",
+					},
 				},
-			},
-		},
-		"Error": {
-			input: map[string]interface{}{"key": "value"},
-			response: &ErrorResponse{
-				Code:    400,
-				Message: "Reason",
-			},
-			responseTopic: "update/rejected",
-			err: &ErrorResponse{
-				Code:    400,
-				Message: "Reason",
-			},
-		},
-	}
+			}
 
-	for name, testCase := range testCases {
-		testCase := testCase
-		t.Run(name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+			for name, testCase := range testCases {
+				testCase := testCase
+				t.Run(name, func(t *testing.T) {
+					ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+
+					var s Shadow
+					var cli *mockDevice
+					cli = &mockDevice{
+						Client: &mockmqtt.Client{
+							PublishFn: func(ctx context.Context, msg *mqtt.Message) error {
+								req := &simpleRequest{}
+								if err := json.Unmarshal(msg.Payload, req); err != nil {
+									t.Error(err)
+									cancel()
+									return err
+								}
+								res := testCase.response
+								setClientToken(res, req.ClientToken)
+								bres, err := json.Marshal(res)
+								if err != nil {
+									t.Error(err)
+									cancel()
+									return err
+								}
+								cli.Serve(&mqtt.Message{
+									Topic:   s.(*shadow).topic(testCase.responseTopic),
+									Payload: bres,
+								})
+								return nil
+							},
+						},
+					}
+					var err error
+					s, err = New(ctx, cli, WithIncrementalUpdate(inc))
+					if err != nil {
+						t.Fatal(err)
+					}
+					cli.Handle(s)
 
-			var s Shadow
-			var cli *mockDevice
-			cli = &mockDevice{
-				Client: &mockmqtt.Client{
-					PublishFn: func(ctx context.Context, msg *mqtt.Message) error {
-						req := &simpleRequest{}
-						if err := json.Unmarshal(msg.Payload, req); err != nil {
-							t.Error(err)
-							cancel()
-							return err
+					doc, err := s.Report(ctx, testCase.input)
+					if err != nil {
+						setClientToken(err, "")
+						if !reflect.DeepEqual(testCase.err, err) {
+							t.Fatalf("Expected error:\n%+v\ngot:\n%+v", testCase.err, err)
 						}
-						res := testCase.response
-						setClientToken(res, req.ClientToken)
-						bres, err := json.Marshal(res)
-						if err != nil {
-							t.Error(err)
-							cancel()
-							return err
+					} else {
+						if !reflect.DeepEqual(testCase.expected, doc) {
+							t.Errorf("Expected document:\n%+v\ngot:\n%+v", testCase.expected, doc)
 						}
-						cli.Serve(&mqtt.Message{
-							Topic:   s.(*shadow).topic(testCase.responseTopic),
-							Payload: bres,
-						})
-						return nil
-					},
-				},
-			}
-			var err error
-			s, err = New(ctx, cli)
-			if err != nil {
-				t.Fatal(err)
-			}
-			cli.Handle(s)
-
-			doc, err := s.Report(ctx, testCase.input)
-			if err != nil {
-				setClientToken(err, "")
-				if !reflect.DeepEqual(testCase.err, err) {
-					t.Fatalf("Expected error:\n%+v\ngot:\n%+v", testCase.err, err)
-				}
-			} else {
-				if !reflect.DeepEqual(testCase.expected, doc) {
-					t.Errorf("Expected document:\n%+v\ngot:\n%+v", testCase.expected, doc)
-				}
+					}
+				})
 			}
 		})
 	}