diff --git a/cmd/generate.go b/cmd/generate.go index 81573b7d6c..694163696c 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -30,6 +30,7 @@ type generateCmd struct { classicMode bool noPrettyPrint bool parametersOnly bool + set []string // derived containerService *api.ContainerService @@ -48,6 +49,15 @@ func newGenerateCmd() *cobra.Command { if err := gc.validate(cmd, args); err != nil { log.Fatalf(fmt.Sprintf("error validating generateCmd: %s", err.Error())) } + + if err := gc.mergeAPIModel(); err != nil { + log.Fatalf(fmt.Sprintf("error merging API model in generateCmd: %s", err.Error())) + } + + if err := gc.loadAPIModel(cmd, args); err != nil { + log.Fatalf(fmt.Sprintf("error loading API model in generateCmd: %s", err.Error())) + } + return gc.run() }, } @@ -57,6 +67,7 @@ func newGenerateCmd() *cobra.Command { f.StringVar(&gc.outputDirectory, "output-directory", "", "output directory (derived from FQDN if absent)") f.StringVar(&gc.caCertificatePath, "ca-certificate-path", "", "path to the CA certificate to use for Kubernetes PKI assets") f.StringVar(&gc.caPrivateKeyPath, "ca-private-key-path", "", "path to the CA private key to use for Kubernetes PKI assets") + f.StringArrayVar(&gc.set, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") f.BoolVar(&gc.classicMode, "classic-mode", false, "enable classic parameters and outputs") f.BoolVar(&gc.noPrettyPrint, "no-pretty-print", false, "skip pretty printing the output") f.BoolVar(&gc.parametersOnly, "parameters-only", false, "only output parameters files") @@ -65,8 +76,6 @@ func newGenerateCmd() *cobra.Command { } func (gc *generateCmd) validate(cmd *cobra.Command, args []string) error { - var caCertificateBytes []byte - var caKeyBytes []byte var err error gc.locale, err = i18n.LoadTranslations() @@ -90,6 +99,34 @@ func (gc *generateCmd) validate(cmd *cobra.Command, args []string) error { return fmt.Errorf(fmt.Sprintf("specified api model does not exist (%s)", gc.apimodelPath)) } + return nil +} + +func (gc *generateCmd) mergeAPIModel() error { + var err error + + // if --set flag has been used + if gc.set != nil && len(gc.set) > 0 { + m := make(map[string]transform.APIModelValue) + transform.MapValues(m, gc.set) + + // overrides the api model and generates a new file + gc.apimodelPath, err = transform.MergeValuesWithAPIModel(gc.apimodelPath, m) + if err != nil { + return fmt.Errorf(fmt.Sprintf("error merging --set values with the api model: %s", err.Error())) + } + + log.Infoln(fmt.Sprintf("new api model file has been generated during merge: %s", gc.apimodelPath)) + } + + return nil +} + +func (gc *generateCmd) loadAPIModel(cmd *cobra.Command, args []string) error { + var caCertificateBytes []byte + var caKeyBytes []byte + var err error + apiloader := &api.Apiloader{ Translator: &i18n.Translator{ Locale: gc.locale, @@ -128,6 +165,7 @@ func (gc *generateCmd) validate(cmd *cobra.Command, args []string) error { prop.CertificateProfile.CaCertificate = string(caCertificateBytes) prop.CertificateProfile.CaPrivateKey = string(caKeyBytes) } + return nil } diff --git a/cmd/generate_test.go b/cmd/generate_test.go index bfac8251d8..24eb7196bc 100644 --- a/cmd/generate_test.go +++ b/cmd/generate_test.go @@ -7,9 +7,7 @@ import ( ) func TestGenerateCmdValidate(t *testing.T) { - g := &generateCmd{} - r := &cobra.Command{} // validate cmd with 1 arg @@ -37,3 +35,51 @@ func TestGenerateCmdValidate(t *testing.T) { } } + +func TestGenerateCmdMergeAPIModel(t *testing.T) { + g := &generateCmd{} + g.apimodelPath = "../pkg/acsengine/testdata/simple/kubernetes.json" + err := g.mergeAPIModel() + if err != nil { + t.Fatalf("unexpected error calling mergeAPIModel with no --set flag defined: %s", err.Error()) + } + + g = &generateCmd{} + g.apimodelPath = "../pkg/acsengine/testdata/simple/kubernetes.json" + g.set = []string{"masterProfile.count=3,linuxProfile.adminUsername=testuser"} + err = g.mergeAPIModel() + if err != nil { + t.Fatalf("unexpected error calling mergeAPIModel with one --set flag: %s", err.Error()) + } + + g = &generateCmd{} + g.apimodelPath = "../pkg/acsengine/testdata/simple/kubernetes.json" + g.set = []string{"masterProfile.count=3", "linuxProfile.adminUsername=testuser"} + err = g.mergeAPIModel() + if err != nil { + t.Fatalf("unexpected error calling mergeAPIModel with multiple --set flags: %s", err.Error()) + } + + g = &generateCmd{} + g.apimodelPath = "../pkg/acsengine/testdata/simple/kubernetes.json" + g.set = []string{"agentPoolProfiles[0].count=1"} + err = g.mergeAPIModel() + if err != nil { + t.Fatalf("unexpected error calling mergeAPIModel with one --set flag to override an array property: %s", err.Error()) + } +} + +func TestGenerateCmdMLoadAPIModel(t *testing.T) { + g := &generateCmd{} + r := &cobra.Command{} + + g.apimodelPath = "../pkg/acsengine/testdata/simple/kubernetes.json" + g.set = []string{"agentPoolProfiles[0].count=1"} + + g.validate(r, []string{"../pkg/acsengine/testdata/simple/kubernetes.json"}) + g.mergeAPIModel() + err := g.loadAPIModel(r, []string{"../pkg/acsengine/testdata/simple/kubernetes.json"}) + if err != nil { + t.Fatalf("unexpected error loading api model: %s", err.Error()) + } +} diff --git a/docs/kubernetes/deploy.md b/docs/kubernetes/deploy.md index ba22aa592a..ff43391f48 100644 --- a/docs/kubernetes/deploy.md +++ b/docs/kubernetes/deploy.md @@ -98,12 +98,26 @@ Edit the [simple Kubernetes cluster definition](/examples/kubernetes.json) and f Optional: attach to an existing virtual network (VNET). Details [here](features.md#feat-custom-vnet) +Note: you can then use the `--set` option of the generate command to override values from the cluster definition file directly in the command line (cf. [Step 4](deploy.md#step-4-generate-the-templates)) + ### Step 4: Generate the Templates The generate command takes a cluster definition and outputs a number of templates which describe your Kubernetes cluster. By default, `generate` will create a new directory named after your cluster nested in the `_output` directory. If my dnsPrefix was `larry` my cluster templates would be found in `_output/larry-`. Run `acs-engine generate examples/kubernetes.json` +The generate command lets you override values from the cluster definition file without having to update the file. You can use the `--set` flag to do that: + +```bash +acs-engine generate --set linuxProfile.adminUsername=myNewUsername,masterProfile.count=3 clusterdefinition.json +``` + +The `--set` flag only supports JSON properties under `properties`. You can also work with array, like the following: + +```bash +acs-engine generate --set agentPoolProfiles[0].count=5,agentPoolProfiles[1].name=myPoolName clusterdefinition.json +``` + ### Step 5: Submit your Templates to Azure Resource Manager (ARM) [Deploy the output azuredeploy.json and azuredeploy.parameters.json](../acsengine.md#deployment-usage) diff --git a/glide.lock b/glide.lock index b70e262412..eef655bd3f 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 6ccf7505e5ada9ca2cbbfc77ca540b6ec59e0bd40d9392c79e29942386446685 -updated: 2018-04-06T09:25:41.745057-07:00 +hash: d3817832375df23ed63ab732dabbb73bd3b59896118b8965a1cfbf09815d4c27 +updated: 2018-04-25T16:03:39.306816-07:00 imports: - name: github.com/alexcesaro/statsd version: 7fea3f0d2fab1ad973e641e51dba45443a311a90 @@ -82,6 +82,8 @@ imports: - client/v2 - models - pkg/escape +- name: github.com/Jeffail/gabs + version: 2a3aa15961d5fee6047b8151b67ac2f08ba2c48c - name: github.com/JiangtianLi/gettext version: a8983c062be4b565d723c478922d7736e04fdba4 - name: github.com/juju/ratelimit @@ -165,6 +167,8 @@ imports: - name: golang.org/x/crypto version: 122d919ec1efcfb58483215da23f815853e24b81 subpackages: + - bcrypt + - blowfish - curve25519 - ed25519 - ed25519/internal/edwards25519 diff --git a/glide.yaml b/glide.yaml index 9d9b59ad28..1668886cb1 100644 --- a/glide.yaml +++ b/glide.yaml @@ -56,6 +56,8 @@ import: version: 366b072768b4e6d93c7de236464c0abe85d0b7c6 - package: k8s.io/client-go version: ~4.0.0 +- package: github.com/Jeffail/gabs + version: 1.0 testImport: # glide isn't able to mutually reconcile pinned versions of these deps - package: github.com/onsi/gomega diff --git a/pkg/acsengine/transform/apimodel_merger.go b/pkg/acsengine/transform/apimodel_merger.go new file mode 100644 index 0000000000..e307f0f64c --- /dev/null +++ b/pkg/acsengine/transform/apimodel_merger.go @@ -0,0 +1,119 @@ +package transform + +import ( + "fmt" + "io/ioutil" + "os" + "regexp" + "strconv" + "strings" + + "github.com/Jeffail/gabs" + log "github.com/sirupsen/logrus" +) + +// APIModelValue represents a value in the APIModel JSON file +type APIModelValue struct { + stringValue string + intValue int64 + arrayValue bool + arrayIndex int + arrayProperty string + arrayName string +} + +// MapValues converts an arraw of rwa ApiModel values (like ["masterProfile.count=4","linuxProfile.adminUsername=admin"]) to a map +func MapValues(m map[string]APIModelValue, values []string) { + if values == nil || len(values) == 0 { + return + } + + for _, value := range values { + splittedValues := strings.Split(value, ",") + if len(splittedValues) > 1 { + MapValues(m, splittedValues) + } else { + keyValueSplitted := strings.Split(value, "=") + key := keyValueSplitted[0] + stringValue := keyValueSplitted[1] + + flagValue := APIModelValue{} + + if asInteger, err := strconv.ParseInt(stringValue, 10, 64); err == nil { + flagValue.intValue = asInteger + } else { + flagValue.stringValue = stringValue + } + + // use regex to find array[index].property pattern in the key + re := regexp.MustCompile(`(.*?)\[(.*?)\]\.(.*?)$`) + match := re.FindStringSubmatch(key) + + // it's an array + if len(match) != 0 { + i, err := strconv.ParseInt(match[2], 10, 32) + if err != nil { + log.Warnln(fmt.Sprintf("array index is not specified for property %s", key)) + } else { + arrayIndex := int(i) + flagValue.arrayValue = true + flagValue.arrayName = match[1] + flagValue.arrayIndex = arrayIndex + flagValue.arrayProperty = match[3] + m[key] = flagValue + } + } else { + m[key] = flagValue + } + } + } +} + +// MergeValuesWithAPIModel takes the path to an ApiModel JSON file, loads it and merges it with the values in the map to another temp file +func MergeValuesWithAPIModel(apiModelPath string, m map[string]APIModelValue) (string, error) { + // load the apiModel file from path + fileContent, err := ioutil.ReadFile(apiModelPath) + if err != nil { + return "", err + } + + // parse the json from file content + jsonObj, err := gabs.ParseJSON(fileContent) + if err != nil { + return "", err + } + + // update api model definition with each value in the map + for key, flagValue := range m { + // working on an array + if flagValue.arrayValue { + log.Infoln(fmt.Sprintf("--set flag array value detected. Name: %s, Index: %b, PropertyName: %s", flagValue.arrayName, flagValue.arrayIndex, flagValue.arrayProperty)) + arrayValue := jsonObj.Path(fmt.Sprint("properties.", flagValue.arrayName)) + if flagValue.stringValue != "" { + arrayValue.Index(flagValue.arrayIndex).SetP(flagValue.stringValue, flagValue.arrayProperty) + } else { + arrayValue.Index(flagValue.arrayIndex).SetP(flagValue.intValue, flagValue.arrayProperty) + } + } else { + if flagValue.stringValue != "" { + jsonObj.SetP(flagValue.stringValue, fmt.Sprint("properties.", key)) + } else { + jsonObj.SetP(flagValue.intValue, fmt.Sprint("properties.", key)) + } + } + } + + // generate a new file + tmpFile, err := ioutil.TempFile("", "mergedApiModel") + if err != nil { + return "", err + } + + tmpFileName := tmpFile.Name() + err = ioutil.WriteFile(tmpFileName, []byte(jsonObj.String()), os.ModeAppend) + if err != nil { + return "", err + } + + return tmpFileName, nil +} diff --git a/pkg/acsengine/transform/apimodel_merger_test.go b/pkg/acsengine/transform/apimodel_merger_test.go new file mode 100644 index 0000000000..ef8c820c51 --- /dev/null +++ b/pkg/acsengine/transform/apimodel_merger_test.go @@ -0,0 +1,50 @@ +package transform + +import ( + "io/ioutil" + "testing" + + "github.com/Jeffail/gabs" + . "github.com/onsi/gomega" +) + +func TestAPIModelMergerMapValues(t *testing.T) { + RegisterTestingT(t) + + m := make(map[string]APIModelValue) + values := []string{"masterProfile.count=5", "agentPoolProfiles[0].name=agentpool1", "linuxProfile.adminUsername=admin"} + + MapValues(m, values) + Expect(m["masterProfile.count"].intValue).To(BeIdenticalTo(int64(5))) + Expect(m["agentPoolProfiles[0].name"].arrayValue).To(BeTrue()) + Expect(m["agentPoolProfiles[0].name"].arrayIndex).To(BeIdenticalTo(0)) + Expect(m["agentPoolProfiles[0].name"].arrayProperty).To(BeIdenticalTo("name")) + Expect(m["agentPoolProfiles[0].name"].arrayName).To(BeIdenticalTo("agentPoolProfiles")) + Expect(m["agentPoolProfiles[0].name"].stringValue).To(BeIdenticalTo("agentpool1")) + Expect(m["linuxProfile.adminUsername"].stringValue).To(BeIdenticalTo("admin")) +} + +func TestMergeValuesWithAPIModel(t *testing.T) { + RegisterTestingT(t) + + m := make(map[string]APIModelValue) + values := []string{"masterProfile.count=5", "agentPoolProfiles[0].name=agentpool1", "linuxProfile.adminUsername=admin"} + + MapValues(m, values) + tmpFile, _ := MergeValuesWithAPIModel("../testdata/simple/kubernetes.json", m) + + jsonFileContent, err := ioutil.ReadFile(tmpFile) + Expect(err).To(BeNil()) + + jsonAPIModel, err := gabs.ParseJSON(jsonFileContent) + Expect(err).To(BeNil()) + + masterProfileCount := jsonAPIModel.Path("properties.masterProfile.count").Data() + Expect(masterProfileCount).To(BeIdenticalTo(float64(5))) + + adminUsername := jsonAPIModel.Path("properties.linuxProfile.adminUsername").Data() + Expect(adminUsername).To(BeIdenticalTo("admin")) + + agentPoolProfileName := jsonAPIModel.Path("properties.agentPoolProfiles").Index(0).Path("name").Data().(string) + Expect(agentPoolProfileName).To(BeIdenticalTo("agentpool1")) +} diff --git a/vendor/github.com/Jeffail/gabs/LICENSE b/vendor/github.com/Jeffail/gabs/LICENSE new file mode 100644 index 0000000000..99a62c6298 --- /dev/null +++ b/vendor/github.com/Jeffail/gabs/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014 Ashley Jeffs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/Jeffail/gabs/README.md b/vendor/github.com/Jeffail/gabs/README.md new file mode 100644 index 0000000000..9ae67f7536 --- /dev/null +++ b/vendor/github.com/Jeffail/gabs/README.md @@ -0,0 +1,284 @@ +![Gabs](gabs_logo.png "Gabs") + +Gabs is a small utility for dealing with dynamic or unknown JSON structures in golang. It's pretty much just a helpful wrapper around the golang json.Marshal/json.Unmarshal behaviour and map[string]interface{} objects. It does nothing spectacular except for being fabulous. + +https://godoc.org/github.com/Jeffail/gabs + +##How to install: + +```bash +go get github.com/Jeffail/gabs +``` + +##How to use + +###Parsing and searching JSON + +```go +... + +import "github.com/Jeffail/gabs" + +jsonParsed, err := gabs.ParseJSON([]byte(`{ + "outter":{ + "inner":{ + "value1":10, + "value2":22 + }, + "alsoInner":{ + "value1":20 + } + } +}`)) + +var value float64 +var ok bool + +value, ok = jsonParsed.Path("outter.inner.value1").Data().(float64) +// value == 10.0, ok == true + +value, ok = jsonParsed.Search("outter", "inner", "value1").Data().(float64) +// value == 10.0, ok == true + +value, ok = jsonParsed.Path("does.not.exist").Data().(float64) +// value == 0.0, ok == false + +exists := jsonParsed.Exists("outter", "inner", "value1") +// exists == true + +exists := jsonParsed.Exists("does", "not", "exist") +// exists == false + +exists := jsonParsed.ExistsP("does.not.exist") +// exists == false + +... +``` + +###Iterating objects + +```go +... + +jsonParsed, _ := gabs.ParseJSON([]byte(`{"object":{ "first": 1, "second": 2, "third": 3 }}`)) + +// S is shorthand for Search +children, _ := jsonParsed.S("object").ChildrenMap() +for key, child := range children { + fmt.Printf("key: %v, value: %v\n", key, child.Data().(string)) +} + +... +``` + +###Iterating arrays + +```go +... + +jsonParsed, _ := gabs.ParseJSON([]byte(`{"array":[ "first", "second", "third" ]}`)) + +// S is shorthand for Search +children, _ := jsonParsed.S("array").Children() +for _, child := range children { + fmt.Println(child.Data().(string)) +} + +... +``` + +Will print: + +``` +first +second +third +``` + +Children() will return all children of an array in order. This also works on objects, however, the children will be returned in a random order. + +###Searching through arrays + +If your JSON structure contains arrays you can still search the fields of the objects within the array, this returns a JSON array containing the results for each element. + +```go +... + +jsonParsed, _ := gabs.ParseJSON([]byte(`{"array":[ {"value":1}, {"value":2}, {"value":3} ]}`)) +fmt.Println(jsonParsed.Path("array.value").String()) + +... +``` + +Will print: + +``` +[1,2,3] +``` + +###Generating JSON + +```go +... + +jsonObj := gabs.New() +// or gabs.Consume(jsonObject) to work on an existing map[string]interface{} + +jsonObj.Set(10, "outter", "inner", "value") +jsonObj.SetP(20, "outter.inner.value2") +jsonObj.Set(30, "outter", "inner2", "value3") + +fmt.Println(jsonObj.String()) + +... +``` + +Will print: + +``` +{"outter":{"inner":{"value":10,"value2":20},"inner2":{"value3":30}}} +``` + +To pretty-print: + +```go +... + +fmt.Println(jsonObj.StringIndent("", " ")) + +... +``` + +Will print: + +``` +{ + "outter": { + "inner": { + "value": 10, + "value2": 20 + }, + "inner2": { + "value3": 30 + } + } +} +``` + +###Generating Arrays + +```go +... + +jsonObj := gabs.New() + +jsonObj.Array("foo", "array") +// Or .ArrayP("foo.array") + +jsonObj.ArrayAppend(10, "foo", "array") +jsonObj.ArrayAppend(20, "foo", "array") +jsonObj.ArrayAppend(30, "foo", "array") + +fmt.Println(jsonObj.String()) + +... +``` + +Will print: + +``` +{"foo":{"array":[10,20,30]}} +``` + +Working with arrays by index: + +```go +... + +jsonObj := gabs.New() + +// Create an array with the length of 3 +jsonObj.ArrayOfSize(3, "foo") + +jsonObj.S("foo").SetIndex("test1", 0) +jsonObj.S("foo").SetIndex("test2", 1) + +// Create an embedded array with the length of 3 +jsonObj.S("foo").ArrayOfSizeI(3, 2) + +jsonObj.S("foo").Index(2).SetIndex(1, 0) +jsonObj.S("foo").Index(2).SetIndex(2, 1) +jsonObj.S("foo").Index(2).SetIndex(3, 2) + +fmt.Println(jsonObj.String()) + +... +``` + +Will print: + +``` +{"foo":["test1","test2",[1,2,3]]} +``` + +###Converting back to JSON + +This is the easiest part: + +```go +... + +jsonParsedObj := gabs.ParseJSON([]byte(`{ + "outter":{ + "values":{ + "first":10, + "second":11 + } + }, + "outter2":"hello world" +}`)) + +jsonOutput := jsonParsedObj.String() +// Becomes `{"outter":{"values":{"first":10,"second":11}},"outter2":"hello world"}` + +... +``` + +And to serialize a specific segment is as simple as: + +```go +... + +jsonParsedObj := gabs.ParseJSON([]byte(`{ + "outter":{ + "values":{ + "first":10, + "second":11 + } + }, + "outter2":"hello world" +}`)) + +jsonOutput := jsonParsedObj.Search("outter").String() +// Becomes `{"values":{"first":10,"second":11}}` + +... +``` + +### Parsing Numbers + +Gabs uses the `json` package under the bonnet, which by default will parse all number values into `float64`. If you need to parse `Int` values then you should use a `json.Decoder` (https://golang.org/pkg/encoding/json/#Decoder): + +```go +sample := []byte(`{"test":{"int":10, "float":6.66}}`) +dec := json.NewDecoder(bytes.NewReader(sample)) +dec.UseNumber() + +val, err := gabs.ParseJSONDecoder(dec) +if err != nil { + t.Errorf("Failed to parse: %v", err) + return +} + +intValue, err := val.Path("test.int").Data().(json.Number).Int64() +``` diff --git a/vendor/github.com/Jeffail/gabs/gabs.go b/vendor/github.com/Jeffail/gabs/gabs.go new file mode 100644 index 0000000000..3fcec06ec2 --- /dev/null +++ b/vendor/github.com/Jeffail/gabs/gabs.go @@ -0,0 +1,475 @@ +/* +Copyright (c) 2014 Ashley Jeffs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +// Package gabs implements a simplified wrapper around creating and parsing JSON. +package gabs + +import ( + "encoding/json" + "errors" + "io" + "io/ioutil" + "strings" +) + +//-------------------------------------------------------------------------------------------------- + +var ( + // ErrOutOfBounds - Index out of bounds. + ErrOutOfBounds = errors.New("out of bounds") + + // ErrNotObjOrArray - The target is not an object or array type. + ErrNotObjOrArray = errors.New("not an object or array") + + // ErrNotObj - The target is not an object type. + ErrNotObj = errors.New("not an object") + + // ErrNotArray - The target is not an array type. + ErrNotArray = errors.New("not an array") + + // ErrPathCollision - Creating a path failed because an element collided with an existing value. + ErrPathCollision = errors.New("encountered value collision whilst building path") + + // ErrInvalidInputObj - The input value was not a map[string]interface{}. + ErrInvalidInputObj = errors.New("invalid input object") + + // ErrInvalidInputText - The input data could not be parsed. + ErrInvalidInputText = errors.New("input text could not be parsed") + + // ErrInvalidPath - The filepath was not valid. + ErrInvalidPath = errors.New("invalid file path") + + // ErrInvalidBuffer - The input buffer contained an invalid JSON string + ErrInvalidBuffer = errors.New("input buffer contained invalid JSON") +) + +//-------------------------------------------------------------------------------------------------- + +// Container - an internal structure that holds a reference to the core interface map of the parsed +// json. Use this container to move context. +type Container struct { + object interface{} +} + +// Data - Return the contained data as an interface{}. +func (g *Container) Data() interface{} { + return g.object +} + +//-------------------------------------------------------------------------------------------------- + +// Path - Search for a value using dot notation. +func (g *Container) Path(path string) *Container { + return g.Search(strings.Split(path, ".")...) +} + +// Search - Attempt to find and return an object within the JSON structure by specifying the +// hierarchy of field names to locate the target. If the search encounters an array and has not +// reached the end target then it will iterate each object of the array for the target and return +// all of the results in a JSON array. +func (g *Container) Search(hierarchy ...string) *Container { + var object interface{} + + object = g.object + for target := 0; target < len(hierarchy); target++ { + if mmap, ok := object.(map[string]interface{}); ok { + object = mmap[hierarchy[target]] + } else if marray, ok := object.([]interface{}); ok { + tmpArray := []interface{}{} + for _, val := range marray { + tmpGabs := &Container{val} + res := tmpGabs.Search(hierarchy[target:]...).Data() + if res != nil { + tmpArray = append(tmpArray, res) + } + } + if len(tmpArray) == 0 { + return &Container{nil} + } + return &Container{tmpArray} + } else { + return &Container{nil} + } + } + return &Container{object} +} + +// S - Shorthand method, does the same thing as Search. +func (g *Container) S(hierarchy ...string) *Container { + return g.Search(hierarchy...) +} + +// Exists - Checks whether a path exists. +func (g *Container) Exists(hierarchy ...string) bool { + return g.Search(hierarchy...).Data() != nil +} + +// ExistsP - Checks whether a dot notation path exists. +func (g *Container) ExistsP(path string) bool { + return g.Exists(strings.Split(path, ".")...) +} + +// Index - Attempt to find and return an object within a JSON array by index. +func (g *Container) Index(index int) *Container { + if array, ok := g.Data().([]interface{}); ok { + if index >= len(array) { + return &Container{nil} + } + return &Container{array[index]} + } + return &Container{nil} +} + +// Children - Return a slice of all the children of the array. This also works for objects, however, +// the children returned for an object will NOT be in order and you lose the names of the returned +// objects this way. +func (g *Container) Children() ([]*Container, error) { + if array, ok := g.Data().([]interface{}); ok { + children := make([]*Container, len(array)) + for i := 0; i < len(array); i++ { + children[i] = &Container{array[i]} + } + return children, nil + } + if mmap, ok := g.Data().(map[string]interface{}); ok { + children := []*Container{} + for _, obj := range mmap { + children = append(children, &Container{obj}) + } + return children, nil + } + return nil, ErrNotObjOrArray +} + +// ChildrenMap - Return a map of all the children of an object. +func (g *Container) ChildrenMap() (map[string]*Container, error) { + if mmap, ok := g.Data().(map[string]interface{}); ok { + children := map[string]*Container{} + for name, obj := range mmap { + children[name] = &Container{obj} + } + return children, nil + } + return nil, ErrNotObj +} + +//-------------------------------------------------------------------------------------------------- + +// Set - Set the value of a field at a JSON path, any parts of the path that do not exist will be +// constructed, and if a collision occurs with a non object type whilst iterating the path an error +// is returned. +func (g *Container) Set(value interface{}, path ...string) (*Container, error) { + if len(path) == 0 { + g.object = value + return g, nil + } + var object interface{} + if g.object == nil { + g.object = map[string]interface{}{} + } + object = g.object + for target := 0; target < len(path); target++ { + if mmap, ok := object.(map[string]interface{}); ok { + if target == len(path)-1 { + mmap[path[target]] = value + } else if mmap[path[target]] == nil { + mmap[path[target]] = map[string]interface{}{} + } + object = mmap[path[target]] + } else { + return &Container{nil}, ErrPathCollision + } + } + return &Container{object}, nil +} + +// SetP - Does the same as Set, but using a dot notation JSON path. +func (g *Container) SetP(value interface{}, path string) (*Container, error) { + return g.Set(value, strings.Split(path, ".")...) +} + +// SetIndex - Set a value of an array element based on the index. +func (g *Container) SetIndex(value interface{}, index int) (*Container, error) { + if array, ok := g.Data().([]interface{}); ok { + if index >= len(array) { + return &Container{nil}, ErrOutOfBounds + } + array[index] = value + return &Container{array[index]}, nil + } + return &Container{nil}, ErrNotArray +} + +// Object - Create a new JSON object at a path. Returns an error if the path contains a collision +// with a non object type. +func (g *Container) Object(path ...string) (*Container, error) { + return g.Set(map[string]interface{}{}, path...) +} + +// ObjectP - Does the same as Object, but using a dot notation JSON path. +func (g *Container) ObjectP(path string) (*Container, error) { + return g.Object(strings.Split(path, ".")...) +} + +// ObjectI - Create a new JSON object at an array index. Returns an error if the object is not an +// array or the index is out of bounds. +func (g *Container) ObjectI(index int) (*Container, error) { + return g.SetIndex(map[string]interface{}{}, index) +} + +// Array - Create a new JSON array at a path. Returns an error if the path contains a collision with +// a non object type. +func (g *Container) Array(path ...string) (*Container, error) { + return g.Set([]interface{}{}, path...) +} + +// ArrayP - Does the same as Array, but using a dot notation JSON path. +func (g *Container) ArrayP(path string) (*Container, error) { + return g.Array(strings.Split(path, ".")...) +} + +// ArrayI - Create a new JSON array at an array index. Returns an error if the object is not an +// array or the index is out of bounds. +func (g *Container) ArrayI(index int) (*Container, error) { + return g.SetIndex([]interface{}{}, index) +} + +// ArrayOfSize - Create a new JSON array of a particular size at a path. Returns an error if the +// path contains a collision with a non object type. +func (g *Container) ArrayOfSize(size int, path ...string) (*Container, error) { + a := make([]interface{}, size) + return g.Set(a, path...) +} + +// ArrayOfSizeP - Does the same as ArrayOfSize, but using a dot notation JSON path. +func (g *Container) ArrayOfSizeP(size int, path string) (*Container, error) { + return g.ArrayOfSize(size, strings.Split(path, ".")...) +} + +// ArrayOfSizeI - Create a new JSON array of a particular size at an array index. Returns an error +// if the object is not an array or the index is out of bounds. +func (g *Container) ArrayOfSizeI(size, index int) (*Container, error) { + a := make([]interface{}, size) + return g.SetIndex(a, index) +} + +// Delete - Delete an element at a JSON path, an error is returned if the element does not exist. +func (g *Container) Delete(path ...string) error { + var object interface{} + + if g.object == nil { + return ErrNotObj + } + object = g.object + for target := 0; target < len(path); target++ { + if mmap, ok := object.(map[string]interface{}); ok { + if target == len(path)-1 { + delete(mmap, path[target]) + } else if mmap[path[target]] == nil { + return ErrNotObj + } + object = mmap[path[target]] + } else { + return ErrNotObj + } + } + return nil +} + +// DeleteP - Does the same as Delete, but using a dot notation JSON path. +func (g *Container) DeleteP(path string) error { + return g.Delete(strings.Split(path, ".")...) +} + +//-------------------------------------------------------------------------------------------------- + +/* +Array modification/search - Keeping these options simple right now, no need for anything more +complicated since you can just cast to []interface{}, modify and then reassign with Set. +*/ + +// ArrayAppend - Append a value onto a JSON array. +func (g *Container) ArrayAppend(value interface{}, path ...string) error { + array, ok := g.Search(path...).Data().([]interface{}) + if !ok { + return ErrNotArray + } + array = append(array, value) + _, err := g.Set(array, path...) + return err +} + +// ArrayAppendP - Append a value onto a JSON array using a dot notation JSON path. +func (g *Container) ArrayAppendP(value interface{}, path string) error { + return g.ArrayAppend(value, strings.Split(path, ".")...) +} + +// ArrayRemove - Remove an element from a JSON array. +func (g *Container) ArrayRemove(index int, path ...string) error { + if index < 0 { + return ErrOutOfBounds + } + array, ok := g.Search(path...).Data().([]interface{}) + if !ok { + return ErrNotArray + } + if index < len(array) { + array = append(array[:index], array[index+1:]...) + } else { + return ErrOutOfBounds + } + _, err := g.Set(array, path...) + return err +} + +// ArrayRemoveP - Remove an element from a JSON array using a dot notation JSON path. +func (g *Container) ArrayRemoveP(index int, path string) error { + return g.ArrayRemove(index, strings.Split(path, ".")...) +} + +// ArrayElement - Access an element from a JSON array. +func (g *Container) ArrayElement(index int, path ...string) (*Container, error) { + if index < 0 { + return &Container{nil}, ErrOutOfBounds + } + array, ok := g.Search(path...).Data().([]interface{}) + if !ok { + return &Container{nil}, ErrNotArray + } + if index < len(array) { + return &Container{array[index]}, nil + } + return &Container{nil}, ErrOutOfBounds +} + +// ArrayElementP - Access an element from a JSON array using a dot notation JSON path. +func (g *Container) ArrayElementP(index int, path string) (*Container, error) { + return g.ArrayElement(index, strings.Split(path, ".")...) +} + +// ArrayCount - Count the number of elements in a JSON array. +func (g *Container) ArrayCount(path ...string) (int, error) { + if array, ok := g.Search(path...).Data().([]interface{}); ok { + return len(array), nil + } + return 0, ErrNotArray +} + +// ArrayCountP - Count the number of elements in a JSON array using a dot notation JSON path. +func (g *Container) ArrayCountP(path string) (int, error) { + return g.ArrayCount(strings.Split(path, ".")...) +} + +//-------------------------------------------------------------------------------------------------- + +// Bytes - Converts the contained object back to a JSON []byte blob. +func (g *Container) Bytes() []byte { + if g.object != nil { + if bytes, err := json.Marshal(g.object); err == nil { + return bytes + } + } + return []byte("{}") +} + +// BytesIndent - Converts the contained object to a JSON []byte blob formatted with prefix, indent. +func (g *Container) BytesIndent(prefix string, indent string) []byte { + if g.object != nil { + if bytes, err := json.MarshalIndent(g.object, prefix, indent); err == nil { + return bytes + } + } + return []byte("{}") +} + +// String - Converts the contained object to a JSON formatted string. +func (g *Container) String() string { + return string(g.Bytes()) +} + +// StringIndent - Converts the contained object back to a JSON formatted string with prefix, indent. +func (g *Container) StringIndent(prefix string, indent string) string { + return string(g.BytesIndent(prefix, indent)) +} + +// New - Create a new gabs JSON object. +func New() *Container { + return &Container{map[string]interface{}{}} +} + +// Consume - Gobble up an already converted JSON object, or a fresh map[string]interface{} object. +func Consume(root interface{}) (*Container, error) { + return &Container{root}, nil +} + +// ParseJSON - Convert a string into a representation of the parsed JSON. +func ParseJSON(sample []byte) (*Container, error) { + var gabs Container + + if err := json.Unmarshal(sample, &gabs.object); err != nil { + return nil, err + } + + return &gabs, nil +} + +// ParseJSONDecoder - Convert a json.Decoder into a representation of the parsed JSON. +func ParseJSONDecoder(decoder *json.Decoder) (*Container, error) { + var gabs Container + + if err := decoder.Decode(&gabs.object); err != nil { + return nil, err + } + + return &gabs, nil +} + +// ParseJSONFile - Read a file and convert into a representation of the parsed JSON. +func ParseJSONFile(path string) (*Container, error) { + if len(path) > 0 { + cBytes, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + container, err := ParseJSON(cBytes) + if err != nil { + return nil, err + } + + return container, nil + } + return nil, ErrInvalidPath +} + +// ParseJSONBuffer - Read the contents of a buffer into a representation of the parsed JSON. +func ParseJSONBuffer(buffer io.Reader) (*Container, error) { + var gabs Container + jsonDecoder := json.NewDecoder(buffer) + if err := jsonDecoder.Decode(&gabs.object); err != nil { + return nil, err + } + + return &gabs, nil +} + +//-------------------------------------------------------------------------------------------------- diff --git a/vendor/github.com/Jeffail/gabs/gabs_logo.png b/vendor/github.com/Jeffail/gabs/gabs_logo.png new file mode 100644 index 0000000000..e8c2832288 Binary files /dev/null and b/vendor/github.com/Jeffail/gabs/gabs_logo.png differ diff --git a/vendor/github.com/Jeffail/gabs/gabs_test.go b/vendor/github.com/Jeffail/gabs/gabs_test.go new file mode 100644 index 0000000000..ba34fb6a46 --- /dev/null +++ b/vendor/github.com/Jeffail/gabs/gabs_test.go @@ -0,0 +1,1056 @@ +package gabs + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + "testing" +) + +func TestBasic(t *testing.T) { + sample := []byte(`{"test":{"value":10},"test2":20}`) + + val, err := ParseJSON(sample) + if err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + if result, ok := val.Search([]string{"test", "value"}...).Data().(float64); ok { + if result != 10 { + t.Errorf("Wrong value of result: %v", result) + } + } else { + t.Errorf("Didn't find test.value") + } + + if _, ok := val.Search("test2", "value").Data().(string); ok { + t.Errorf("Somehow found a field that shouldn't exist") + } + + if result, ok := val.Search("test2").Data().(float64); ok { + if result != 20 { + t.Errorf("Wrong value of result: %v", result) + } + } else { + t.Errorf("Didn't find test2") + } + + if result := val.Bytes(); string(result) != string(sample) { + t.Errorf("Wrong []byte conversion: %s != %s", result, sample) + } +} + +func TestExists(t *testing.T) { + sample := []byte(`{"test":{"value":10},"test2":20}`) + + val, err := ParseJSON(sample) + if err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + paths := []struct { + Path []string + Exists bool + }{ + {[]string{"one", "two", "three"}, false}, + {[]string{"test"}, true}, + {[]string{"test", "value"}, true}, + {[]string{"test2"}, true}, + {[]string{"test2", "value"}, false}, + {[]string{"test", "value2"}, false}, + {[]string{"test", "VALUE"}, false}, + } + + for _, p := range paths { + if exp, actual := p.Exists, val.Exists(p.Path...); exp != actual { + t.Errorf("Wrong result from Exists: %v != %v, for path: %v", exp, actual, p.Path) + } + if exp, actual := p.Exists, val.ExistsP(strings.Join(p.Path, ".")); exp != actual { + t.Errorf("Wrong result from ExistsP: %v != %v, for path: %v", exp, actual, p.Path) + } + } +} + +func TestExistsWithArrays(t *testing.T) { + sample := []byte(`{"foo":{"bar":{"baz":[10, 2, 3]}}}`) + + val, err := ParseJSON(sample) + if err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + if exp, actual := true, val.Exists("foo", "bar", "baz"); exp != actual { + t.Errorf("Wrong result from array based Exists: %v != %v", exp, actual) + } + + sample = []byte(`{"foo":{"bar":[{"baz":10},{"baz":2},{"baz":3}]}}`) + + if val, err = ParseJSON(sample); err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + if exp, actual := true, val.Exists("foo", "bar", "baz"); exp != actual { + t.Errorf("Wrong result from array based Exists: %v != %v", exp, actual) + } + if exp, actual := false, val.Exists("foo", "bar", "baz_NOPE"); exp != actual { + t.Errorf("Wrong result from array based Exists: %v != %v", exp, actual) + } + + sample = []byte(`{"foo":[{"bar":{"baz":10}},{"bar":{"baz":2}},{"bar":{"baz":3}}]}`) + + if val, err = ParseJSON(sample); err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + if exp, actual := true, val.Exists("foo", "bar", "baz"); exp != actual { + t.Errorf("Wrong result from array based Exists: %v != %v", exp, actual) + } + if exp, actual := false, val.Exists("foo", "bar", "baz_NOPE"); exp != actual { + t.Errorf("Wrong result from array based Exists: %v != %v", exp, actual) + } + + sample = + []byte(`[{"foo":{"bar":{"baz":10}}},{"foo":{"bar":{"baz":2}}},{"foo":{"bar":{"baz":3}}}]`) + + if val, err = ParseJSON(sample); err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + if exp, actual := true, val.Exists("foo", "bar", "baz"); exp != actual { + t.Errorf("Wrong result from array based Exists: %v != %v", exp, actual) + } + if exp, actual := false, val.Exists("foo", "bar", "baz_NOPE"); exp != actual { + t.Errorf("Wrong result from array based Exists: %v != %v", exp, actual) + } +} + +func TestBasicWithBuffer(t *testing.T) { + sample := bytes.NewReader([]byte(`{"test":{"value":10},"test2":20}`)) + + _, err := ParseJSONBuffer(sample) + if err != nil { + t.Errorf("Failed to parse: %v", err) + return + } +} + +func TestBasicWithDecoder(t *testing.T) { + sample := []byte(`{"test":{"int":10, "float":6.66}}`) + dec := json.NewDecoder(bytes.NewReader(sample)) + dec.UseNumber() + + val, err := ParseJSONDecoder(dec) + if err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + checkNumber := func(path string, expectedVal json.Number) { + data := val.Path(path).Data() + asNumber, isNumber := data.(json.Number) + if !isNumber { + t.Error("Failed to parse using decoder UseNumber policy") + } + if expectedVal != asNumber { + t.Errorf("Expected[%s] but got [%s]", expectedVal, asNumber) + } + } + + checkNumber("test.int", "10") + checkNumber("test.float", "6.66") +} + +func TestFailureWithDecoder(t *testing.T) { + sample := []byte(`{"test":{" "invalidCrap":.66}}`) + dec := json.NewDecoder(bytes.NewReader(sample)) + + _, err := ParseJSONDecoder(dec) + if err == nil { + t.Fatal("Expected parsing error") + } +} + +func TestFindArray(t *testing.T) { + for i, this := range []struct { + input string + target string + expect string + }{ + { + `{"test":{"array":[{"value":1}, {"value":2}, {"value":3}]}}`, + "test.array.value", + "[1,2,3]", + }, + { + `{ + "test":{ + "array":[ + { + "values":[ + {"more":1}, + {"more":2}, + {"more":3} + ] + }, + { + "values":[ + {"more":4}, + {"more":5}, + {"more":6} + ] + }, + { + "values":[ + {"more":7}, + {"more":8}, + {"more":9} + ] + } + ] + } + }`, + "test.array.values.more", + "[[1,2,3],[4,5,6],[7,8,9]]", + }, + } { + val, err := ParseJSON([]byte(this.input)) + if err != nil { + t.Errorf("[%d] Failed to parse: %s", i, err) + return + } + + target := val.Path(this.target) + result := target.String() + + if this.expect != result { + t.Errorf("[%d] Expected %v, received %v", i, this.expect, result) + } + } +} + +func TestDeletes(t *testing.T) { + jsonParsed, _ := ParseJSON([]byte(`{ + "outter":{ + "inner":{ + "value1":10, + "value2":22, + "value3":32 + }, + "alsoInner":{ + "value1":20, + "value2":42, + "value3":92 + } + } + }`)) + + if err := jsonParsed.Delete("outter", "inner", "value2"); err != nil { + t.Error(err) + } + if err := jsonParsed.DeleteP("outter.alsoInner.value1"); err != nil { + t.Error(err) + } + + expected := `{"outter":{"alsoInner":{"value2":42,"value3":92},"inner":{"value1":10,"value3":32}}}` + if actual := jsonParsed.String(); actual != expected { + t.Errorf("Unexpected result from deletes: %v != %v", actual, expected) + } +} + +func TestExamples(t *testing.T) { + jsonParsed, _ := ParseJSON([]byte(`{ + "outter":{ + "inner":{ + "value1":10, + "value2":22 + }, + "alsoInner":{ + "value1":20 + } + } + }`)) + + var value float64 + var ok bool + + value, ok = jsonParsed.Path("outter.inner.value1").Data().(float64) + if value != 10.0 || !ok { + t.Errorf("wrong value: %v, %v", value, ok) + } + + value, ok = jsonParsed.Search("outter", "inner", "value1").Data().(float64) + if value != 10.0 || !ok { + t.Errorf("wrong value: %v, %v", value, ok) + } + + value, ok = jsonParsed.Path("does.not.exist").Data().(float64) + if value != 0.0 || ok { + t.Errorf("wrong value: %v, %v", value, ok) + } + + jsonParsed, _ = ParseJSON([]byte(`{"array":[ "first", "second", "third" ]}`)) + + expected := []string{"first", "second", "third"} + + children, err := jsonParsed.S("array").Children() + if err != nil { + t.Errorf("Error: %v", err) + return + } + for i, child := range children { + if expected[i] != child.Data().(string) { + t.Errorf("Child unexpected: %v != %v", expected[i], child.Data().(string)) + } + } +} + +func TestExamples2(t *testing.T) { + var err error + + jsonObj := New() + + _, err = jsonObj.Set(10, "outter", "inner", "value") + if err != nil { + t.Errorf("Error: %v", err) + return + } + _, err = jsonObj.SetP(20, "outter.inner.value2") + if err != nil { + t.Errorf("Error: %v", err) + return + } + _, err = jsonObj.Set(30, "outter", "inner2", "value3") + if err != nil { + t.Errorf("Error: %v", err) + return + } + + expected := `{"outter":{"inner":{"value":10,"value2":20},"inner2":{"value3":30}}}` + if jsonObj.String() != expected { + t.Errorf("Non matched output: %v != %v", expected, jsonObj.String()) + } + + jsonObj, _ = Consume(map[string]interface{}{}) + + jsonObj.Array("array") + + jsonObj.ArrayAppend(10, "array") + jsonObj.ArrayAppend(20, "array") + jsonObj.ArrayAppend(30, "array") + + expected = `{ + "array": [ + 10, + 20, + 30 + ] + }` + result := jsonObj.StringIndent(" ", " ") + if result != expected { + t.Errorf("Non matched output: %v != %v", expected, result) + } +} + +func TestExamples3(t *testing.T) { + jsonObj := New() + + jsonObj.ArrayP("foo.array") + + jsonObj.ArrayAppend(10, "foo", "array") + jsonObj.ArrayAppend(20, "foo", "array") + jsonObj.ArrayAppend(30, "foo", "array") + + result := jsonObj.String() + expected := `{"foo":{"array":[10,20,30]}}` + + if result != expected { + t.Errorf("Non matched output: %v != %v", result, expected) + } +} + +func TestDotNotation(t *testing.T) { + sample := []byte(`{"test":{"inner":{"value":10}},"test2":20}`) + + val, err := ParseJSON(sample) + if err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + if result, _ := val.Path("test.inner.value").Data().(float64); result != 10 { + t.Errorf("Expected 10, received: %v", result) + } +} + +func TestModify(t *testing.T) { + sample := []byte(`{"test":{"value":10},"test2":20}`) + + val, err := ParseJSON(sample) + if err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + if _, err := val.S("test").Set(45.0, "value"); err != nil { + t.Errorf("Failed to set field") + } + + if result, ok := val.Search([]string{"test", "value"}...).Data().(float64); ok { + if result != 45 { + t.Errorf("Wrong value of result: %v", result) + } + } else { + t.Errorf("Didn't find test.value") + } + + if out := val.String(); `{"test":{"value":45},"test2":20}` != out { + t.Errorf("Incorrectly serialized: %v", out) + } + + if out := val.Search("test").String(); `{"value":45}` != out { + t.Errorf("Incorrectly serialized: %v", out) + } +} + +func TestChildren(t *testing.T) { + json1, _ := ParseJSON([]byte(`{ + "objectOne":{ + }, + "objectTwo":{ + }, + "objectThree":{ + } + }`)) + + objects, _ := json1.Children() + for _, object := range objects { + object.Set("hello world", "child") + } + + expected := `{"objectOne":{"child":"hello world"},"objectThree":{"child":"hello world"}` + + `,"objectTwo":{"child":"hello world"}}` + received := json1.String() + if expected != received { + t.Errorf("json1: expected %v, received %v", expected, received) + } + + json2, _ := ParseJSON([]byte(`{ + "values":[ + { + "objectOne":{ + } + }, + { + "objectTwo":{ + } + }, + { + "objectThree":{ + } + } + ] + }`)) + + json3, _ := ParseJSON([]byte(`{ + "values":[ + ] + }`)) + + numChildren1, _ := json2.ArrayCount("values") + numChildren2, _ := json3.ArrayCount("values") + if _, err := json3.ArrayCount("valuesNOTREAL"); err == nil { + t.Errorf("expected numChildren3 to fail") + } + + if numChildren1 != 3 || numChildren2 != 0 { + t.Errorf("CountElements, expected 3 and 0, received %v and %v", + numChildren1, numChildren2) + } + + objects, _ = json2.S("values").Children() + for _, object := range objects { + object.Set("hello world", "child") + json3.ArrayAppend(object.Data(), "values") + } + + expected = `{"values":[{"child":"hello world","objectOne":{}},{"child":"hello world",` + + `"objectTwo":{}},{"child":"hello world","objectThree":{}}]}` + received = json2.String() + if expected != received { + t.Errorf("json2: expected %v, received %v", expected, received) + } + + received = json3.String() + if expected != received { + t.Errorf("json3: expected %v, received %v", expected, received) + } +} + +func TestChildrenMap(t *testing.T) { + json1, _ := ParseJSON([]byte(`{ + "objectOne":{"num":1}, + "objectTwo":{"num":2}, + "objectThree":{"num":3} + }`)) + + objectMap, err := json1.ChildrenMap() + if err != nil { + t.Error(err) + return + } + + if len(objectMap) != 3 { + t.Errorf("Wrong num of elements in objectMap: %v != %v", len(objectMap), 3) + return + } + + for key, val := range objectMap { + if "objectOne" == key { + if val := val.S("num").Data().(float64); val != 1 { + t.Errorf("%v != %v", val, 1) + } + } else if "objectTwo" == key { + if val := val.S("num").Data().(float64); val != 2 { + t.Errorf("%v != %v", val, 2) + } + } else if "objectThree" == key { + if val := val.S("num").Data().(float64); val != 3 { + t.Errorf("%v != %v", val, 3) + } + } else { + t.Errorf("Unexpected key: %v", key) + } + } + + objectMap["objectOne"].Set(500, "num") + if val := json1.Path("objectOne.num").Data().(int); val != 500 { + t.Errorf("set objectOne failed: %v != %v", val, 500) + } +} + +func TestNestedAnonymousArrays(t *testing.T) { + json1, _ := ParseJSON([]byte(`{ + "array":[ + [ 1, 2, 3 ], + [ 4, 5, 6 ], + [ 7, 8, 9 ], + [{ "test" : 50 }] + ] + }`)) + + childTest, err := json1.S("array").Index(0).Children() + if err != nil { + t.Error(err) + return + } + + if val := childTest[0].Data().(float64); val != 1 { + t.Errorf("child test: %v != %v", val, 1) + } + if val := childTest[1].Data().(float64); val != 2 { + t.Errorf("child test: %v != %v", val, 2) + } + if val := childTest[2].Data().(float64); val != 3 { + t.Errorf("child test: %v != %v", val, 3) + } + + if val := json1.Path("array").Index(1).Index(1).Data().(float64); val != 5 { + t.Errorf("nested child test: %v != %v", val, 5) + } + + if val := json1.Path("array").Index(3).Index(0).S("test").Data().(float64); val != 50 { + t.Errorf("nested child object test: %v != %v", val, 50) + } + + json1.Path("array").Index(3).Index(0).Set(200, "test") + + if val := json1.Path("array").Index(3).Index(0).S("test").Data().(int); val != 200 { + t.Errorf("set nested child object: %v != %v", val, 200) + } +} + +func TestArrays(t *testing.T) { + json1, _ := ParseJSON([]byte(`{ + "languages":{ + "english":{ + "places":0 + }, + "french": { + "places": [ + "france", + "belgium" + ] + } + } + }`)) + + json2, _ := ParseJSON([]byte(`{ + "places":[ + "great_britain", + "united_states_of_america", + "the_world" + ] + }`)) + + if englishPlaces := json2.Search("places").Data(); englishPlaces != nil { + json1.Path("languages.english").Set(englishPlaces, "places") + } else { + t.Errorf("Didn't find places in json2") + } + + if englishPlaces := json1.Search("languages", "english", "places").Data(); englishPlaces != nil { + + englishArray, ok := englishPlaces.([]interface{}) + if !ok { + t.Errorf("places in json1 (%v) was not an array", englishPlaces) + } + + if len(englishArray) != 3 { + t.Errorf("wrong length of array: %v", len(englishArray)) + } + + } else { + t.Errorf("Didn't find places in json1") + } + + for i := 0; i < 3; i++ { + if err := json2.ArrayRemove(0, "places"); err != nil { + t.Errorf("Error removing element: %v", err) + } + } + + json2.ArrayAppend(map[string]interface{}{}, "places") + json2.ArrayAppend(map[string]interface{}{}, "places") + json2.ArrayAppend(map[string]interface{}{}, "places") + + // Using float64 for this test even though it's completely inappropriate because + // later on the API might do something clever with types, in which case all numbers + // will become float64. + for i := 0; i < 3; i++ { + obj, _ := json2.ArrayElement(i, "places") + obj2, _ := obj.Object(fmt.Sprintf("object%v", i)) + obj2.Set(float64(i), "index") + } + + children, _ := json2.S("places").Children() + for i, obj := range children { + if id, ok := obj.S(fmt.Sprintf("object%v", i)).S("index").Data().(float64); ok { + if id != float64(i) { + t.Errorf("Wrong index somehow, expected %v, received %v", i, id) + } + } else { + t.Errorf("Failed to find element %v from %v", i, obj) + } + } + + if err := json2.ArrayRemove(1, "places"); err != nil { + t.Errorf("Error removing element: %v", err) + } + + expected := `{"places":[{"object0":{"index":0}},{"object2":{"index":2}}]}` + received := json2.String() + + if expected != received { + t.Errorf("Wrong output, expected: %v, received: %v", expected, received) + } +} + +func TestArraysTwo(t *testing.T) { + json1 := New() + + test1, err := json1.ArrayOfSize(4, "test1") + if err != nil { + t.Error(err) + } + + if _, err = test1.ArrayOfSizeI(2, 0); err != nil { + t.Error(err) + } + if _, err = test1.ArrayOfSizeI(2, 1); err != nil { + t.Error(err) + } + if _, err = test1.ArrayOfSizeI(2, 2); err != nil { + t.Error(err) + } + if _, err = test1.ArrayOfSizeI(2, 3); err != nil { + t.Error(err) + } + + if _, err = test1.ArrayOfSizeI(2, 4); err != ErrOutOfBounds { + t.Errorf("Index should have been out of bounds") + } + + if _, err = json1.S("test1").Index(0).SetIndex(10, 0); err != nil { + t.Error(err) + } + if _, err = json1.S("test1").Index(0).SetIndex(11, 1); err != nil { + t.Error(err) + } + + if _, err = json1.S("test1").Index(1).SetIndex(12, 0); err != nil { + t.Error(err) + } + if _, err = json1.S("test1").Index(1).SetIndex(13, 1); err != nil { + t.Error(err) + } + + if _, err = json1.S("test1").Index(2).SetIndex(14, 0); err != nil { + t.Error(err) + } + if _, err = json1.S("test1").Index(2).SetIndex(15, 1); err != nil { + t.Error(err) + } + + if _, err = json1.S("test1").Index(3).SetIndex(16, 0); err != nil { + t.Error(err) + } + if _, err = json1.S("test1").Index(3).SetIndex(17, 1); err != nil { + t.Error(err) + } + + if val := json1.S("test1").Index(0).Index(0).Data().(int); val != 10 { + t.Errorf("create array: %v != %v", val, 10) + } + if val := json1.S("test1").Index(0).Index(1).Data().(int); val != 11 { + t.Errorf("create array: %v != %v", val, 11) + } + + if val := json1.S("test1").Index(1).Index(0).Data().(int); val != 12 { + t.Errorf("create array: %v != %v", val, 12) + } + if val := json1.S("test1").Index(1).Index(1).Data().(int); val != 13 { + t.Errorf("create array: %v != %v", val, 13) + } + + if val := json1.S("test1").Index(2).Index(0).Data().(int); val != 14 { + t.Errorf("create array: %v != %v", val, 14) + } + if val := json1.S("test1").Index(2).Index(1).Data().(int); val != 15 { + t.Errorf("create array: %v != %v", val, 15) + } + + if val := json1.S("test1").Index(3).Index(0).Data().(int); val != 16 { + t.Errorf("create array: %v != %v", val, 16) + } + if val := json1.S("test1").Index(3).Index(1).Data().(int); val != 17 { + t.Errorf("create array: %v != %v", val, 17) + } +} + +func TestArraysThree(t *testing.T) { + json1 := New() + + test, err := json1.ArrayOfSizeP(1, "test1.test2") + if err != nil { + t.Error(err) + } + + test.SetIndex(10, 0) + if val := json1.S("test1", "test2").Index(0).Data().(int); val != 10 { + t.Error(err) + } +} + +func TestArraysRoot(t *testing.T) { + sample := []byte(`["test1"]`) + + val, err := ParseJSON(sample) + if err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + val.ArrayAppend("test2") + val.ArrayAppend("test3") + if obj, err := val.ObjectI(2); err != nil { + t.Error(err) + } else { + obj.Set("bar", "foo") + } + + if expected, actual := `["test1","test2",{"foo":"bar"}]`, val.String(); expected != actual { + t.Errorf("expected %v, received: %v", expected, actual) + } +} + +func TestLargeSample(t *testing.T) { + sample := []byte(`{ + "test":{ + "innerTest":{ + "value":10, + "value2":22, + "value3":{ + "moreValue":45 + } + } + }, + "test2":20 + }`) + + val, err := ParseJSON(sample) + if err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + if result, ok := val.Search("test", "innerTest", "value3", "moreValue").Data().(float64); ok { + if result != 45 { + t.Errorf("Wrong value of result: %v", result) + } + } else { + t.Errorf("Didn't find value") + } +} + +func TestShorthand(t *testing.T) { + json, _ := ParseJSON([]byte(`{ + "outter":{ + "inner":{ + "value":5, + "value2":10, + "value3":11 + }, + "inner2":{ + } + }, + "outter2":{ + "inner":0 + } + }`)) + + missingValue := json.S("outter").S("doesntexist").S("alsodoesntexist").S("inner").S("value").Data() + if missingValue != nil { + t.Errorf("missing value was actually found: %v\n", missingValue) + } + + realValue := json.S("outter").S("inner").S("value2").Data().(float64) + if realValue != 10 { + t.Errorf("real value was incorrect: %v\n", realValue) + } + + _, err := json.S("outter2").Set(json.S("outter").S("inner").Data(), "inner") + if err != nil { + t.Errorf("error setting outter2: %v\n", err) + } + + compare := `{"outter":{"inner":{"value":5,"value2":10,"value3":11},"inner2":{}}` + + `,"outter2":{"inner":{"value":5,"value2":10,"value3":11}}}` + out := json.String() + if out != compare { + t.Errorf("wrong serialized structure: %v\n", out) + } + + compare2 := `{"outter":{"inner":{"value":6,"value2":10,"value3":11},"inner2":{}}` + + `,"outter2":{"inner":{"value":6,"value2":10,"value3":11}}}` + + json.S("outter").S("inner").Set(6, "value") + out = json.String() + if out != compare2 { + t.Errorf("wrong serialized structure: %v\n", out) + } +} + +func TestInvalid(t *testing.T) { + invalidJSONSamples := []string{ + `{dfads"`, + ``, + // `""`, + // `"hello"`, + "{}\n{}", + } + + for _, sample := range invalidJSONSamples { + if _, err := ParseJSON([]byte(sample)); err == nil { + t.Errorf("parsing invalid JSON '%v' did not return error", sample) + } + } + + if _, err := ParseJSON(nil); err == nil { + t.Errorf("parsing nil did not return error") + } + + validObj, err := ParseJSON([]byte(`{}`)) + if err != nil { + t.Errorf("failed to parse '{}'") + } + + invalidStr := validObj.S("Doesn't exist").String() + if "{}" != invalidStr { + t.Errorf("expected '{}', received: %v", invalidStr) + } +} + +func TestCreation(t *testing.T) { + json, _ := ParseJSON([]byte(`{}`)) + inner, err := json.ObjectP("test.inner") + if err != nil { + t.Errorf("Error: %v", err) + return + } + + inner.Set(10, "first") + inner.Set(20, "second") + + inner.Array("array") + inner.ArrayAppend("first element of the array", "array") + inner.ArrayAppend(2, "array") + inner.ArrayAppend("three", "array") + + expected := `{"test":{"inner":{"array":["first element of the array",2,"three"],` + + `"first":10,"second":20}}}` + actual := json.String() + if actual != expected { + t.Errorf("received incorrect output from json object: %v\n", actual) + } +} + +type outterJSON struct { + FirstInner innerJSON + SecondInner innerJSON + ThirdInner innerJSON +} + +type innerJSON struct { + NumberType float64 + StringType string +} + +type jsonStructure struct { + FirstOutter outterJSON + SecondOutter outterJSON +} + +var jsonContent = []byte(`{ + "firstOutter":{ + "firstInner":{ + "numberType":11, + "stringType":"hello world, first first" + }, + "secondInner":{ + "numberType":12, + "stringType":"hello world, first second" + }, + "thirdInner":{ + "numberType":13, + "stringType":"hello world, first third" + } + }, + "secondOutter":{ + "firstInner":{ + "numberType":21, + "stringType":"hello world, second first" + }, + "secondInner":{ + "numberType":22, + "stringType":"hello world, second second" + }, + "thirdInner":{ + "numberType":23, + "stringType":"hello world, second third" + } + } +}`) + +/* +Simple use case, compares unmarshalling declared structs vs dynamically searching for +the equivalent hierarchy. Hopefully we won't see too great a performance drop from the +dynamic approach. +*/ + +func BenchmarkStatic(b *testing.B) { + for i := 0; i < b.N; i++ { + var jsonObj jsonStructure + json.Unmarshal(jsonContent, &jsonObj) + + if val := jsonObj.FirstOutter.SecondInner.NumberType; val != 12 { + b.Errorf("Wrong value of FirstOutter.SecondInner.NumberType: %v\n", val) + } + expected := "hello world, first second" + if val := jsonObj.FirstOutter.SecondInner.StringType; val != expected { + b.Errorf("Wrong value of FirstOutter.SecondInner.StringType: %v\n", val) + } + if val := jsonObj.SecondOutter.ThirdInner.NumberType; val != 23 { + b.Errorf("Wrong value of SecondOutter.ThirdInner.NumberType: %v\n", val) + } + expected = "hello world, second second" + if val := jsonObj.SecondOutter.SecondInner.StringType; val != expected { + b.Errorf("Wrong value of SecondOutter.SecondInner.StringType: %v\n", val) + } + } +} + +func BenchmarkDynamic(b *testing.B) { + for i := 0; i < b.N; i++ { + jsonObj, err := ParseJSON(jsonContent) + if err != nil { + b.Errorf("Error parsing json: %v\n", err) + } + + FOSI := jsonObj.S("firstOutter", "secondInner") + SOSI := jsonObj.S("secondOutter", "secondInner") + SOTI := jsonObj.S("secondOutter", "thirdInner") + + if val := FOSI.S("numberType").Data().(float64); val != 12 { + b.Errorf("Wrong value of FirstOutter.SecondInner.NumberType: %v\n", val) + } + expected := "hello world, first second" + if val := FOSI.S("stringType").Data().(string); val != expected { + b.Errorf("Wrong value of FirstOutter.SecondInner.StringType: %v\n", val) + } + if val := SOTI.S("numberType").Data().(float64); val != 23 { + b.Errorf("Wrong value of SecondOutter.ThirdInner.NumberType: %v\n", val) + } + expected = "hello world, second second" + if val := SOSI.S("stringType").Data().(string); val != expected { + b.Errorf("Wrong value of SecondOutter.SecondInner.StringType: %v\n", val) + } + } +} + +func TestNoTypeChildren(t *testing.T) { + jsonObj, err := ParseJSON([]byte(`{"not_obj_or_array":1}`)) + if err != nil { + t.Error(err) + } + exp := ErrNotObjOrArray + if _, act := jsonObj.S("not_obj_or_array").Children(); act != exp { + t.Errorf("Unexpected value returned: %v != %v", exp, act) + } + exp = ErrNotObj + if _, act := jsonObj.S("not_obj_or_array").ChildrenMap(); act != exp { + t.Errorf("Unexpected value returned: %v != %v", exp, act) + } +} + +func TestBadIndexes(t *testing.T) { + jsonObj, err := ParseJSON([]byte(`{"array":[1,2,3]}`)) + if err != nil { + t.Error(err) + } + if act := jsonObj.Index(0).Data(); nil != act { + t.Errorf("Unexpected value returned: %v != %v", nil, act) + } + if act := jsonObj.S("array").Index(4).Data(); nil != act { + t.Errorf("Unexpected value returned: %v != %v", nil, act) + } +} + +func TestNilSet(t *testing.T) { + obj := Container{nil} + if _, err := obj.Set("bar", "foo"); err != nil { + t.Error(err) + } + if _, err := obj.Set("new", "foo", "bar"); err != ErrPathCollision { + t.Errorf("Expected ErrPathCollision: %v, %s", err, obj.Data()) + } + if _, err := obj.SetIndex("new", 0); err != ErrNotArray { + t.Errorf("Expected ErrNotArray: %v, %s", err, obj.Data()) + } +}