Repository: seqsense/aws-iot-device-sdk-go (go)

Issue:
Add support for named shadow
### Objective
AWS IoT now supports named shadow, so I believe better to support it in the SDK.
https://aws.amazon.com/about-aws/whats-new/2020/07/aws-iot-core-now-supports-multiple-shadows-for-a-single-iot-device/

### Deliverables
- [ ] Add an option to specify shadow name 

### Approach (optional)


Pull request:
shadow: Support named shadow
Closes #206 

Added an option `shadow.WithName` to specify shadow name. If not specified, use unnamed (classic) shadow.

ref: https://docs.aws.amazon.com/iot/latest/developerguide/device-shadow-mqtt.html

Patch:
diff --git a/examples/shadow/main.go b/examples/shadow/main.go
index ed8a7e0..2b24abb 100644
--- a/examples/shadow/main.go
+++ b/examples/shadow/main.go
@@ -21,7 +21,7 @@ import (
 	"time"
 
 	"github.com/at-wat/mqtt-go"
-	"github.com/seqsense/aws-iot-device-sdk-go/v4"
+	awsiotdev "github.com/seqsense/aws-iot-device-sdk-go/v4"
 	"github.com/seqsense/aws-iot-device-sdk-go/v4/shadow"
 )
 
@@ -29,8 +29,8 @@ func main() {
 	ctx, cancel := context.WithCancel(context.Background())
 	defer cancel()
 
-	if len(os.Args) != 3 {
-		println("usage: shadow AWS_IOT_ENDPOINT THING_NAME")
+	if len(os.Args) != 4 {
+		println("usage: shadow AWS_IOT_ENDPOINT THING_NAME SHADOW_NAME")
 		println("")
 		println("This example updates and deletes AWS IoT Thing Shadow.")
 		println("THING_NAME must be registered to your account of AWS IoT beforehand.")
@@ -43,6 +43,7 @@ func main() {
 	}
 	host := os.Args[1]
 	thingName := os.Args[2]
+	shadowName := os.Args[3]
 
 	for _, file := range []string{
 		"root-CA.crt",
@@ -88,7 +89,7 @@ func main() {
 		panic(err)
 	}
 
-	s, err := shadow.New(ctx, cli)
+	s, err := shadow.New(ctx, cli, shadow.WithName(shadowName))
 	if err != nil {
 		panic(err)
 	}
diff --git a/shadow/option.go b/shadow/option.go
new file mode 100644
index 0000000..342c4db
--- /dev/null
+++ b/shadow/option.go
@@ -0,0 +1,31 @@
+// 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
+
+// Options stores Device Shadow options.
+type Options struct {
+	Name string
+}
+
+// Option is a functional option of UpdateJob.
+type Option func(options *Options) error
+
+// WithName sets shadow name.
+func WithName(name string) Option {
+	return func(o *Options) error {
+		o.Name = name
+		return nil
+	}
+}
diff --git a/shadow/shadow.go b/shadow/shadow.go
index a535502..2004c6b 100644
--- a/shadow/shadow.go
+++ b/shadow/shadow.go
@@ -24,7 +24,7 @@ import (
 
 	"github.com/at-wat/mqtt-go"
 
-	"github.com/seqsense/aws-iot-device-sdk-go/v4"
+	awsiotdev "github.com/seqsense/aws-iot-device-sdk-go/v4"
 	"github.com/seqsense/aws-iot-device-sdk-go/v4/internal/ioterr"
 )
 
@@ -57,6 +57,7 @@ type shadow struct {
 	mqtt.ServeMux
 	cli       mqtt.Client
 	thingName string
+	name      string
 	doc       *ThingDocument
 	mu        sync.Mutex
 	onDelta   func(delta map[string]interface{})
@@ -73,7 +74,11 @@ func (s *shadow) token() string {
 }
 
 func (s *shadow) topic(operation string) string {
-	return "$aws/things/" + s.thingName + "/shadow/" + operation
+	prefix := "$aws/things/" + s.thingName + "/shadow"
+	if s.name != "" {
+		prefix += "/name/" + s.name
+	}
+	return prefix + "/" + operation
 }
 
 func (s *shadow) handleResponse(r interface{}) {
@@ -160,10 +165,17 @@ func (s *shadow) deleteAccepted(msg *mqtt.Message) {
 }
 
 // New creates Thing Shadow interface.
-func New(ctx context.Context, cli awsiotdev.Device) (Shadow, error) {
+func New(ctx context.Context, cli awsiotdev.Device, opt ...Option) (Shadow, error) {
+	opts := &Options{}
+	for _, o := range opt {
+		if err := o(opts); err != nil {
+			return nil, ioterr.New(err, "applying option")
+		}
+	}
 	s := &shadow{
 		cli:       cli,
 		thingName: cli.ThingName(),
+		name:      opts.Name,
 		doc: &ThingDocument{
 			State: ThingState{
 				Desired:  map[string]interface{}{},


Test patch:
diff --git a/shadow/shadow_test.go b/shadow/shadow_test.go
index 933bbff..c8a9a41 100644
--- a/shadow/shadow_test.go
+++ b/shadow/shadow_test.go
@@ -57,6 +57,44 @@ func TestNew(t *testing.T) {
 			t.Errorf("Expected error: %v, got: %v", errDummy, err)
 		}
 	})
+
+	t.Run("Options", func(t *testing.T) {
+		t.Run("WithName", func(t *testing.T) {
+			ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+			defer cancel()
+
+			cli := &mockDevice{&mockmqtt.Client{}}
+			operation := "foo/bar"
+
+			testCases := map[string]struct {
+				input    string
+				expected string
+			}{
+				"ClassicShadow": {
+					input:    "",
+					expected: "$aws/things/" + cli.ThingName() + "/shadow/" + operation,
+				},
+				"NamedShadow": {
+					input:    "testShadow",
+					expected: "$aws/things/" + cli.ThingName() + "/shadow/name/testShadow/" + operation,
+				},
+			}
+
+			for name, testCase := range testCases {
+				testCase := testCase
+				t.Run(name, func(t *testing.T) {
+					s, err := New(ctx, cli, WithName(testCase.input))
+					if err != nil {
+						t.Fatal(err)
+					}
+					topic := s.(*shadow).topic(operation)
+					if topic != testCase.expected {
+						t.Errorf("Expected topic name: %v, got: %v", testCase.expected, topic)
+					}
+				})
+			}
+		})
+	})
 }
 
 func TestHandlers(t *testing.T) {