diff --git a/Makefile b/Makefile index 445b4da4..10383244 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,11 @@ GOFLAGS ?= "-mod=vendor" GO111MODULE ?= "on" GOPRIVATE ?= GOPRIVATE=github.com/untangle/golang-shared +EXTRA_TEST_FLAGS ?= +GOTEST_COVERAGE ?= yes +GO_COVERPROFILE ?= /tmp/packetd_coverage.out +COVERAGE_HTML ?= /tmp/packetd_coverage.html +BROWSER ?= x-www-browser # logging NC := "\033[0m" # no color @@ -39,4 +44,16 @@ lint: modules # IMPORTANT --issues-exit-code 0 will let the build continue without failing lint checks - this should be removed eventually $(shell go env GOPATH)/bin/golangci-lint run --issues-exit-code 0 +test: build + if [ $(GOTEST_COVERAGE) = "yes" ]; \ + then \ + go test -vet=off $(EXTRA_TEST_FLAGS) -coverprofile=$(GO_COVERPROFILE) ./...; \ + else \ + go test -vet=off $(EXTRA_TEST_FLAGS) ./...; \ + fi + +racetest: EXTRA_TEST_FLAGS=-race +racetest: test + + .PHONY: build lint environment diff --git a/build/Dockerfile.build-glibc b/build/Dockerfile.build-glibc index 50ca9fb6..cd7161a2 100644 --- a/build/Dockerfile.build-glibc +++ b/build/Dockerfile.build-glibc @@ -27,4 +27,4 @@ RUN go get google.golang.org/protobuf/cmd/protoc-gen-go RUN mkdir -p /go/untangle-shared VOLUME /go/untangle-shared WORKDIR /go/untangle-shared -CMD make +CMD make all && (if [ -n "${UNIT_TEST}" -a "${UNIT_TEST}" = "yes" ]; then make test; make racetest; fi) diff --git a/build/Dockerfile.build-musl b/build/Dockerfile.build-musl index 8e9a1a2c..e87f9941 100644 --- a/build/Dockerfile.build-musl +++ b/build/Dockerfile.build-musl @@ -27,4 +27,4 @@ RUN go get google.golang.org/protobuf/cmd/protoc-gen-go RUN mkdir -p /go/untangle-shared VOLUME /go/untangle-shared WORKDIR /go/untangle-shared -CMD make +CMD make all && (if [ -n "${UNIT_TEST}" -a "${UNIT_TEST}" = "yes" ]; then make test; fi) diff --git a/build/docker-compose.build.yml b/build/docker-compose.build.yml index bd832960..155445ef 100644 --- a/build/docker-compose.build.yml +++ b/build/docker-compose.build.yml @@ -4,8 +4,9 @@ services: build: context: . dockerfile: Dockerfile.build-musl - environment: + environment: SSH_AUTH_SOCK: /ssh-agent + UNIT_TEST: "yes" volumes: - ..:/go/untangle-shared - ${SSH_AUTH_SOCK}:/ssh-agent @@ -14,8 +15,9 @@ services: build: context: . dockerfile: Dockerfile.build-glibc - environment: + environment: SSH_AUTH_SOCK: /ssh-agent + UNIT_TEST: "yes" volumes: - ..:/go/untangle-shared - ${SSH_AUTH_SOCK}:/ssh-agent diff --git a/go.mod b/go.mod index f943dc17..b7ad6aef 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,10 @@ module github.com/untangle/golang-shared go 1.12 require ( - github.com/golang/protobuf v1.4.3 + github.com/golang/protobuf v1.5.0 github.com/pebbe/zmq4 v1.2.2 github.com/r3labs/diff/v2 v2.14.2 - google.golang.org/protobuf v1.25.0 + github.com/stretchr/testify v1.5.1 + google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect + google.golang.org/protobuf v1.28.0 ) diff --git a/go.sum b/go.sum index 17018962..56e76859 100644 --- a/go.sum +++ b/go.sum @@ -19,12 +19,15 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/pebbe/zmq4 v1.2.2 h1:RZ5Ogp0D5S6u+tSxopnI3afAf0ifWbvQOAw9HxXvZP4= github.com/pebbe/zmq4 v1.2.2/go.mod h1:7N4y5R18zBiu3l0vajMUWQgZyjv464prE8RCyBcmnZM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -53,10 +56,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -71,11 +72,9 @@ google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuh google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -87,6 +86,9 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= diff --git a/services/settings/path_unmarshaller.go b/services/settings/path_unmarshaller.go new file mode 100644 index 00000000..584bd53a --- /dev/null +++ b/services/settings/path_unmarshaller.go @@ -0,0 +1,280 @@ +package settings + +import ( + "encoding/json" + "fmt" + "io" +) + +const notFound = int64(-1) + +// PathUnmarshaller unmarshals objects in a JSON object given a path +// through the JSON input object to that desired target object. +// e.g. given {"x": {"y": "z"}} and the path "x", "y", we can extract +// the JSON for just "z". +type PathUnmarshaller struct { + // decoder object that we use to get tokens from. + decoder *json.Decoder + + // should we call UseNumber when we decode the object we find? + useNumber bool + + // underlying io object. + iostream io.ReadSeeker + + // the path being searched for. + searchedForPath []string +} + +func (unm *PathUnmarshaller) formatError(msg string, params ...interface{}) error { + paramsList := make([]interface{}, len(params), len(params)+1) + copy(paramsList, params) + paramsList = append([]interface{}{fmt.Sprintf("%#v", unm.searchedForPath)}, + params...) + return fmt.Errorf( + "path unmarshaller: encountered error: looking for path %v "+msg, + paramsList...) +} + +func (unm *PathUnmarshaller) getToken() (json.Token, error) { + next, err := unm.decoder.Token() + if err != nil && err == io.EOF { + return nil, unm.formatError("can't find path, EOF") + } else if err != nil { + return nil, unm.formatError("path unmarshaller: json error: %w", err) + } + return next, nil + +} + +// ignore JSON in the token stream until we hit delim. +func (unm *PathUnmarshaller) ignoreUntil(delim json.Delim) error { + for { + next, err := unm.getToken() + if err != nil { + return err + } + switch tok := next.(type) { + case json.Delim: + switch tok { + case delim: + return nil + case '[': + if err := unm.ignoreUntil(']'); err != nil { + return err + } + case '{': + if err := unm.ignoreUntil('}'); err != nil { + return err + } + default: + return unm.formatError("bad JSON, got unexpected token: %T (%s)", + tok, + tok) + } + default: + return unm.formatError("unexpected token type: %T (%s)", tok, tok) + case nil, string, int, int64, float32, + float64, bool, json.Number: + } + } +} + +// ignore the entire next object in the stream. +func (unm *PathUnmarshaller) ignoreNextObject() error { + next, err := unm.getToken() + if err != nil { + return err + } + switch tok := next.(type) { + case json.Delim: + switch tok { + case '[': + if err := unm.ignoreUntil(']'); err != nil { + return err + } + case '{': + if err := unm.ignoreUntil('}'); err != nil { + return err + } + default: + return unm.formatError("bad/unexpected JSON token: %T (%s)", tok, tok) + } + default: + return unm.formatError("unexpected json token: %T (%s)", tok, tok) + case nil, string, int, int64, float32, + float64, bool, json.Number: + return nil + } + return nil +} + +const ( + completeMatch = iota // the path is completely matched, return now. + partialMatch // we so far match but we haven't gone all the way. + noMatch // This is the wrong path, stop going down it. +) + +type matchResult int32 + +// pathMatchStatus returns the status of the current path -- fullPath +// is what we want, partialPath is what we have so far. See the consts +// above. +func pathMatchStatus(fullPath []string, partialPath []string) matchResult { + if len(fullPath) < len(partialPath) { + return noMatch + } + + for i, elem := range partialPath { + if elem != fullPath[i] { + return noMatch + } + } + if len(fullPath) == len(partialPath) { + return completeMatch + } + return partialMatch + +} + +// processObject is called with a 'value' object from a parent +// object. It processes it based on type: +// 1. Array -- ignore (eat it up). +// 2. Object -- recurse into it. +// 3. other -- ignore (eat it up). +func (unm *PathUnmarshaller) processObject( + currentPath []string, + searchPath []string) (int64, error) { + for { + next, err := unm.getToken() + if err != nil { + return notFound, err + } + switch tok := next.(type) { + case json.Delim: + switch tok { + case '{': + if result, err := unm.searchObject(currentPath, + searchPath); result != notFound { + return result, err + } else if err != nil { + return notFound, err + } + case '[': + if err := unm.ignoreUntil(']'); err != nil { + return notFound, err + } + default: + return notFound, unm.formatError( + "unexpected json delim: %s", tok) + } + case string, int64, int, float64, float32: + return notFound, nil + default: + return notFound, unm.formatError("unexpected json token: %s", tok) + } + } +} + +// searchObject is given a dict object and looks through the keys in +// that object to see if they match the desired input path in +// searchPath. It makes a recursive call to itself if it can extend +// the matching path. +func (unm *PathUnmarshaller) searchObject( + currentPath []string, + searchPath []string) (int64, error) { + for { + next, err := unm.getToken() + if err != nil { + return notFound, err + } + switch tok := next.(type) { + case string: + newPath := make([]string, len(currentPath), len(currentPath)+1) + copy(newPath, currentPath) + newPath = append(newPath, tok) + matchResult := pathMatchStatus(searchPath, newPath) + switch matchResult { + case completeMatch: + return unm.decoder.InputOffset(), nil + case partialMatch: + return unm.processObject(newPath, searchPath) + case noMatch: + if err := unm.ignoreNextObject(); err != nil { + return notFound, err + } + } + case json.Delim: + if tok == '}' { + return notFound, nil + } + return notFound, unm.formatError("unexpected JSON delim: %s", tok) + default: + return notFound, unm.formatError("unexpected JSON token: %T (%s)", tok, tok) + } + } +} + +// The JSON tokenizer does not have : tokens. So when we find the key +// we look for, and get the stream for that point, there is still a +// ':' in the stream. So we eat it up. +func (unm *PathUnmarshaller) fastForwardToValue() error { + bytes := []byte{0} + for { + n, err := unm.iostream.Read(bytes) + if n != 1 || err != nil { + return unm.formatError( + "couldn't read expected object from JSON stream: %w", + err) + } else if bytes[0] == ':' { + return nil + } + } +} + +// UseNumber will tell the underlying decoder to UseNumber using the +// json.Decoder.UseNumber method, for the object returned via +// UnmarshalAtPath. +func (unm *PathUnmarshaller) UseNumber() { + unm.useNumber = true +} + +// NewPathUnmarshaller creates a new PathUnmarshaller that will read +// from reader. reader should point to valid JSON. The JSON object +// should be a dict object (curly braces), not any other kind of JSON +// object. +func NewPathUnmarshaller(reader io.ReadSeeker) *PathUnmarshaller { + return &PathUnmarshaller{ + iostream: reader, + decoder: json.NewDecoder(reader), + } +} + +// UnmarshalAtPath unmarshalls the JSON object found at path into +// output. path is a list of keys into the object. For example, in the +// object {"one": {"two": 3}}, the path "one", "two" corresponds to +// the value 3. If the value is not found or some other error occurs, +// we return an error, otherwise we unmarshal into output. Output can +// be anything json.Unmarshal accepts. +func (unm *PathUnmarshaller) UnmarshalAtPath(output interface{}, path ...string) error { + unm.searchedForPath = path + outputPosition, err := unm.processObject([]string{}, path) + if err != nil { + return err + } else if outputPosition == notFound { + return unm.formatError("couldn't find path: %s", path) + } + if _, err := unm.iostream.Seek(outputPosition, io.SeekStart); err != nil { + return fmt.Errorf("error seeking to position of object: %w", err) + } + + err = unm.fastForwardToValue() + if err != nil { + return err + } + newDecoder := json.NewDecoder(unm.iostream) + if unm.useNumber { + newDecoder.UseNumber() + } + return newDecoder.Decode(output) +} diff --git a/services/settings/path_unmarshaller_test.go b/services/settings/path_unmarshaller_test.go new file mode 100644 index 00000000..b38110f3 --- /dev/null +++ b/services/settings/path_unmarshaller_test.go @@ -0,0 +1,165 @@ +package settings + +import ( + "bytes" + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/suite" +) + +// Test suite for testing path unmarshaller. +type TestJSONUnmarshalSuite struct { + suite.Suite +} + +// Test the most basic schenario -- get a value for a key. +func (suite *TestJSONUnmarshalSuite) TestSimpleUnmarshalPath() { + jsonSimple := `{"one": "two"}` + unmarshaller := suite.getUnmarshallerForString(jsonSimple) + var output interface{} + suite.Nil(unmarshaller.UnmarshalAtPath(&output, "one")) + stringOutput := output.(string) + suite.Equal(stringOutput, "two") +} + +// Test a more nested path. +func (suite *TestJSONUnmarshalSuite) TestBiggerPath() { + jsonAdvanced := `{"one": {"two": {"three": {"x": 1, "y": 2}}}}` + var output interface{} = nil + unmarshaller := suite.getUnmarshallerForString(jsonAdvanced) + unmarshaller.UseNumber() + suite.Nil(unmarshaller.UnmarshalAtPath(&output, "one", "two", "three")) + mapOutput := output.(map[string]interface{}) + xValue, err := mapOutput["x"].(json.Number).Int64() + suite.Nil(err) + yValue, err := mapOutput["y"].(json.Number).Int64() + suite.Nil(err) + suite.Equal(int(xValue), 1) + suite.Equal(int(yValue), 2) +} + +// Test that basic structure unmarshalling works. +func (suite *TestJSONUnmarshalSuite) TestStructUnmarshal() { + + type Dummy struct { + Value1 int64 `json:"value1"` + Value2 string `json:"value2"` + } + + jsonDummy := `{"": null, "x": false, "path1": {"path2": {"value1": 222, "value2": "hello"}}}` + output := &Dummy{} + unmarshaller := suite.getUnmarshallerForString(jsonDummy) + suite.Nil(unmarshaller.UnmarshalAtPath(output, "path1", "path2")) + suite.Equal(output.Value1, int64(222)) + suite.Equal(output.Value2, "hello") +} + +// Test that we catch various error cases and do not panic. +func (suite *TestJSONUnmarshalSuite) TestBadJSON() { + for _, i := range []string{ + `}}`, + `{"valid": "json", "invalid": }}}`, + `[]`, + `{"some": {"path"}}`, + `{{{`, + `{{]]`, + `{{[[[]]]}}`, + } { + var output interface{} + unmarshaller := suite.getUnmarshallerForString(i) + err := unmarshaller.UnmarshalAtPath(output, "some", "path") + suite.NotNil(err) + fmt.Printf("[ok]: Caught expected error, string value: '%s'\n", err) + } +} + +// Test that deserializing nested structures with slices work. +func (suite *TestJSONUnmarshalSuite) TestNestedStructs() { + type Inner struct { + Name string `json:"name"` + Status int32 `json:"status"` + IsCool bool `json:"is_cool"` + } + type Outer struct { + Key string `json:"key"` + Inners []Inner `json:"inners"` + } + + output := Outer{} + + json := ` +{"ignore": null, + "large_object": [ +1, +2, +3, +4, +5, +6, +7, +1, +2, +3, +4, +5, +6, +7 +], + "otherIgnore": {"key": false, "key2": false}, + "pathComp1": { + "pathComp2": { + "pathComp3": { + "key": "hello!", + "inners": [ + {"name": "world!", "status": 42, "is_cool": true}, + {"name": "doge", "status": 99, "is_cool": true}, + {"name": "goop", "status": 9000, "is_cool": true}, + {"name": "glop", "status": 0, "is_cool": false}, + {"name": "doge", "status": 998, "is_cool": true}, + {"name": "goop", "status": 9009, "is_cool": true}, + {"name": "glop", "status": 2, "is_cool": false}, + {"name": "doge", "status": 998, "is_cool": true}, + {"name": "goop", "status": 9009, "is_cool": true}, + {"name": "glop", "status": 2, "is_cool": false}, + {"name": "doge", "status": 998, "is_cool": true}, + {"name": "goop", "status": 9009, "is_cool": true}, + {"name": "glop", "status": 2, "is_cool": false} + ] + } + } + } +} +` + unm := suite.getUnmarshallerForString(json) + suite.Nil(unm.UnmarshalAtPath(&output, "pathComp1", "pathComp2", "pathComp3")) + suite.Equal(output.Key, "hello!") + suite.Equal(len(output.Inners), 13) + expected := []Inner{ + {Name: "world!", Status: 42, IsCool: true}, + {Name: "doge", Status: 99, IsCool: true}, + {Name: "goop", Status: 9000, IsCool: true}, + {Name: "glop", Status: 0, IsCool: false}, + {Name: "doge", Status: 998, IsCool: true}, + {Name: "goop", Status: 9009, IsCool: true}, + {Name: "glop", Status: 2, IsCool: false}, + {Name: "doge", Status: 998, IsCool: true}, + {Name: "goop", Status: 9009, IsCool: true}, + {Name: "glop", Status: 2, IsCool: false}, + {Name: "doge", Status: 998, IsCool: true}, + {Name: "goop", Status: 9009, IsCool: true}, + {Name: "glop", Status: 2, IsCool: false}, + } + suite.Equal(expected, output.Inners) +} + +func (suite *TestJSONUnmarshalSuite) getUnmarshallerForString(json string) *PathUnmarshaller { + reader := bytes.NewReader([]byte(json)) + return NewPathUnmarshaller(reader) +} + +func TestUnmarshaller(t *testing.T) { + testSuite := &TestJSONUnmarshalSuite{} + suite.Run(t, testSuite) +} diff --git a/services/settings/settings.go b/services/settings/settings.go index 03e6cbfb..ee6e44eb 100644 --- a/services/settings/settings.go +++ b/services/settings/settings.go @@ -91,6 +91,24 @@ func GetDefaultSettings(segments []string) (interface{}, error) { return GetSettingsFile(segments, defaultsFile) } +// UnmarshalSettingsAtPath is a wrapper function for the +// PathUnmarshaller struct and associated functions. It opens the +// settings file and unmarshalls the object at path into output. it +// has the same behavior PathUnmarshaller.UnmarshalAtPath, with the +// PathUnmarshaller having been constructed with a reader that reads +// from settingsFile. +func UnmarshalSettingsAtPath(output interface{}, path ...string) error { + saveLocker.RLock() + defer saveLocker.RUnlock() + reader, err := os.Open(settingsFile) + if err != nil { + return err + } + unmarshaller := NewPathUnmarshaller(reader) + err = unmarshaller.UnmarshalAtPath(output, path...) + return err +} + // GetSettingsFile returns the settings from the specified path of the specified filename func GetSettingsFile(segments []string, filename string) (interface{}, error) { var err error