diff --git a/README.md b/README.md index e31915e9..7c28a89d 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,9 @@ Devices currently present on Arduino IoT Cloud can be retrieved by using this co Things can be created starting from a template or by cloning another thing. -Create a thing from a thing template. The name parameter is optional. If it is provided then it overrides the name retrieved from the template: +Create a thing from a thing template. Supported template formats are JSON and YAML. The name parameter is optional. If it is provided then it overrides the name retrieved from the template: -`$ iot-cloud-cli thing create --name --template ` +`$ iot-cloud-cli thing create --name --template ` Create a thing by cloning another thing, here the *name is mandatory*: @@ -72,9 +72,9 @@ Delete a thing with the following command: `$ iot-cloud-cli thing delete --device-id ` -Extract a template from an existing thing: +Extract a template from an existing thing. The template can be saved in two formats: json or yaml. The default format is yaml: -`$ iot-cloud-cli thing extract --id --outfile ` +`$ iot-cloud-cli thing extract --id --outfile --format ` Bind a thing to an existing device: diff --git a/cli/thing/create.go b/cli/thing/create.go index 72f8737a..fd3545da 100644 --- a/cli/thing/create.go +++ b/cli/thing/create.go @@ -20,7 +20,13 @@ func initCreateCommand() *cobra.Command { RunE: runCreateCommand, } createCommand.Flags().StringVarP(&createFlags.name, "name", "n", "", "Thing name") - createCommand.Flags().StringVarP(&createFlags.template, "template", "t", "", "File containing a thing template") + createCommand.Flags().StringVarP( + &createFlags.template, + "template", + "t", + "", + "File containing a thing template, JSON and YAML format are supported", + ) createCommand.MarkFlagRequired("template") return createCommand } diff --git a/cli/thing/extract.go b/cli/thing/extract.go index 093970f2..ba99971a 100644 --- a/cli/thing/extract.go +++ b/cli/thing/extract.go @@ -10,6 +10,7 @@ import ( var extractFlags struct { id string outfile string + format string } func initExtractCommand() *cobra.Command { @@ -21,6 +22,13 @@ func initExtractCommand() *cobra.Command { } extractCommand.Flags().StringVarP(&extractFlags.id, "id", "i", "", "Thing ID") extractCommand.Flags().StringVarP(&extractFlags.outfile, "outfile", "o", "", "Template file destination path") + extractCommand.Flags().StringVar( + &extractFlags.format, + "format", + "yaml", + "Format of template file, can be {json|yaml}. Default is 'yaml'", + ) + extractCommand.MarkFlagRequired("id") return extractCommand } @@ -29,7 +37,8 @@ func runExtractCommand(cmd *cobra.Command, args []string) error { fmt.Printf("Extracting template from thing %s\n", extractFlags.id) params := &thing.ExtractParams{ - ID: extractFlags.id, + ID: extractFlags.id, + Format: extractFlags.format, } if extractFlags.outfile != "" { params.Outfile = &extractFlags.outfile diff --git a/command/thing/create.go b/command/thing/create.go index 79746056..22feb62c 100644 --- a/command/thing/create.go +++ b/command/thing/create.go @@ -10,6 +10,7 @@ import ( iotclient "github.com/arduino/iot-client-go" "github.com/arduino/iot-cloud-cli/internal/config" "github.com/arduino/iot-cloud-cli/internal/iot" + "gopkg.in/yaml.v3" ) // CreateParams contains the parameters needed to create a new thing. @@ -67,9 +68,12 @@ func loadTemplate(file string) (*iotclient.Thing, error) { } template := make(map[string]interface{}) - err = json.Unmarshal([]byte(templateBytes), &template) - if err != nil { - return nil, fmt.Errorf("%s: %w", "reading template file: template not valid: ", err) + + // Extract template trying all the supported formats: json and yaml + if err = json.Unmarshal([]byte(templateBytes), &template); err != nil { + if err = yaml.Unmarshal([]byte(templateBytes), &template); err != nil { + return nil, errors.New("reading template file: template format is not valid") + } } // Adapt thing template to thing structure diff --git a/command/thing/extract.go b/command/thing/extract.go index 596dffe0..a01b1a37 100644 --- a/command/thing/extract.go +++ b/command/thing/extract.go @@ -2,26 +2,36 @@ package thing import ( "encoding/json" + "errors" "fmt" "io/ioutil" "os" + "strings" iotclient "github.com/arduino/iot-client-go" "github.com/arduino/iot-cloud-cli/internal/config" "github.com/arduino/iot-cloud-cli/internal/iot" + "gopkg.in/yaml.v3" ) // ExtractParams contains the parameters needed to // extract a thing from Arduino IoT Cloud and save it on local storage. +// Format determines the file format of the template ("json" or "yaml") // Output indicates the destination path of the extraction. type ExtractParams struct { ID string + Format string Outfile *string } // Extract command is used to extract a thing template // from a thing on Arduino IoT Cloud. func Extract(params *ExtractParams) error { + params.Format = strings.ToLower(params.Format) + if params.Format != "json" && params.Format != "yaml" { + return errors.New("format is not valid: only 'json' and 'yaml' are supported") + } + conf, err := config.Retrieve() if err != nil { return err @@ -42,23 +52,19 @@ func Extract(params *ExtractParams) error { return err } - if params.Outfile == nil { - outfile := thing.Name + "-template.json" - params.Outfile = &outfile - } - err = ioutil.WriteFile(*params.Outfile, template, os.FileMode(0644)) + err = templateToFile(template, params) if err != nil { - err = fmt.Errorf("%s: %w", "cannot write outfile: ", err) return err } return nil } -func templateFromThing(thing *iotclient.ArduinoThing) ([]byte, error) { +func templateFromThing(thing *iotclient.ArduinoThing) (map[string]interface{}, error) { template := make(map[string]interface{}) template["name"] = thing.Name + // Extract template from thing structure var props []map[string]interface{} for _, p := range thing.Properties { prop := make(map[string]interface{}) @@ -72,11 +78,42 @@ func templateFromThing(thing *iotclient.ArduinoThing) ([]byte, error) { } template["variables"] = props - // Extract json template from thing structure - file, err := json.MarshalIndent(template, "", " ") + return template, nil +} + +func templateToFile(template map[string]interface{}, params *ExtractParams) error { + var file []byte + var err error + + if params.Format == "json" { + file, err = json.MarshalIndent(template, "", " ") + if err != nil { + return fmt.Errorf("%s: %w", "thing marshal failure: ", err) + } + + } else if params.Format == "yaml" { + file, err = yaml.Marshal(template) + if err != nil { + return fmt.Errorf("%s: %w", "thing marshal failure: ", err) + } + + } else { + return errors.New("format is not valid: only 'json' and 'yaml' are supported") + } + + if params.Outfile == nil { + name, ok := template["name"].(string) + if name == "" || !ok { + return errors.New("thing template does not have a valid name") + } + outfile := name + "." + params.Format + params.Outfile = &outfile + } + + err = ioutil.WriteFile(*params.Outfile, file, os.FileMode(0644)) if err != nil { - err = fmt.Errorf("%s: %w", "thing marshal failure: ", err) - return nil, err + return fmt.Errorf("%s: %w", "cannot write outfile: ", err) } - return file, nil + + return nil } diff --git a/go.mod b/go.mod index d8864431..18c9a4c6 100644 --- a/go.mod +++ b/go.mod @@ -20,4 +20,5 @@ require ( google.golang.org/genproto v0.0.0-20210504143626-3b2ad6ccc450 // indirect google.golang.org/grpc v1.39.0 google.golang.org/protobuf v1.27.1 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c )