Repository: koron/go-dproxy Branch: main Commit: 81dfa780e5ac Files: 24 Total size: 40.1 KB Directory structure: gitextract_caioeg4v/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ └── go.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── doc.go ├── drain.go ├── drain_test.go ├── error.go ├── frame.go ├── go.mod ├── go.sum ├── package_test.go ├── pointer.go ├── pointer_test.go ├── proxy.go ├── proxy_test.go ├── set.go ├── set_test.go ├── staticcheck.conf ├── type.go ├── value.go └── value_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "github-actions" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" # $Hash:c5d3212bc9191230f44684f4d30f030b6c70d515e68cbc9c3467c4d9$ ================================================ FILE: .github/workflows/go.yml ================================================ name: Go on: [push] permissions: contents: write env: GO_VERSION: 'oldstable' jobs: check: name: Check main packages outputs: targets: ${{ steps.list.outputs.targets }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} - name: List "main" packages to be released id: list shell: bash run: | found=0 echo "targets<<__END__" >> "$GITHUB_OUTPUT" main_packages=$(go list -f '{{if (eq .Name "main")}}{{.ImportPath}} .{{slice .ImportPath (len .Module.Path)}}{{end}}' ./...) while read -r path dir; do [ -z "$path" ] && continue name=$(basename "$path") if [ -f "$dir/.norelease" ] ; then printf "Skipped %s\t(%s), due to %s/.norelease found\n" "$name" "$dir" "$dir" else found=1 echo "$name $dir $path" >> "$GITHUB_OUTPUT" printf "Added %s\t(%s) to the release\n" "$name" "$dir" fi done <> "$GITHUB_OUTPUT" if [ $found -eq 0 ] ; then echo "⛔ No packages found to release" fi build: name: Build needs: check env: RELEASE_TARGETS: ${{needs.check.outputs.targets}} strategy: fail-fast: false matrix: include: - { os: darwin, arch: amd64, runner: macos-latest, } - { os: darwin, arch: arm64, runner: macos-latest, } - { os: freebsd, arch: amd64, runner: ubuntu-latest, } - { os: linux, arch: arm64, runner: ubuntu-24.04-arm, } - { os: linux, arch: amd64, runner: ubuntu-latest, } - { os: windows, arch: amd64, runner: windows-latest, } defaults: run: shell: ${{ matrix.os == 'freebsd' && 'freebsd {0}' || 'bash' }} runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} - name: Setup env id: setup shell: bash run: | NAME="${GITHUB_REPOSITORY#${GITHUB_REPOSITORY_OWNER}/}" GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} if [[ ${GITHUB_REF} =~ ^refs/tags/v[0-9]+\.[0-9]+ ]] ; then VERSION=${GITHUB_REF_NAME} else VERSION=SNAPSHOT fi case ${{ matrix.os }} in linux|freebsd) PKGEXT=.tar.gz ;; darwin|windows) PKGEXT=.zip ;; esac case ${{ matrix.os }} in windows) choco install zip ;; esac echo "VERSION=${VERSION}" >> $GITHUB_ENV echo "GOOS=${GOOS}" >> $GITHUB_ENV echo "GOARCH=${GOARCH}" >> $GITHUB_ENV echo "CGO_ENABLED=1" >> $GITHUB_ENV echo "PKGNAME=${NAME}_${VERSION}_${GOOS}_${GOARCH}" >> $GITHUB_ENV echo "PKGEXT=${PKGEXT}" >> $GITHUB_ENV - name: Start FreeBSD VM if: matrix.os == 'freebsd' uses: vmactions/freebsd-vm@d1e65811565151536c0c894fff74f06351ed26e6 # v1.4.5 with: usesh: true copyback: false disable-cache: true arch: ${{ matrix.arch }} envs: 'RELEASE_TARGETS PKGNAME PKGEXT' prepare: | echo "::group::Install Go" pkg install -y go - name: Test all run: | go test ./... - name: Build all "main" packages if: needs.check.outputs.targets != '' run: | while read -r name dir path ; do printf "building %s\t(%s)\n" "$name" "$dir" ( cd "$dir" && go build ) done < 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. ================================================ FILE: Makefile ================================================ # Get relative paths of all "main" packages MAIN_PACKAGE ?= $(shell go list -f '{{if (eq .Name "main")}}.{{slice .ImportPath (len .Module.Path)}}{{end}}' ./...) TEST_PACKAGE ?= ./... .PHONY: build build: go build -gcflags '-e' ./... .PHONY: test test: go test $(TEST_PACKAGE) .PHONY: race race: go test -race $(TEST_PACKAGE) .PHONY: bench bench: go test -bench $(TEST_PACKAGE) .PHONY: tags tags: gotags -f tags -R . .PHONY: cover cover: mkdir -p tmp go test -coverprofile tmp/_cover.out $(TEST_PACKAGE) go tool cover -html tmp/_cover.out -o tmp/cover.html .PHONY: checkall checkall: vet staticcheck .PHONY: vet vet: go vet $(TEST_PACKAGE) .PHONY: staticcheck staticcheck: staticcheck $(TEST_PACKAGE) .PHONY: clean clean: go clean rm -f tags rm -f tmp/_cover.out tmp/cover.html .PHONY: upgradable upgradable: @go list -m -mod=readonly -u -f='{{if and (not .Indirect) (not .Main)}}{{if .Update}}{{.Path}}@{{.Update.Version}} [{{.Version}}]{{else if .Replace}}{{if .Replace.Update}}{{.Path}}@{{.Replace.Update.Version}} [replaced:{{.Replace.Version}} {{.Version}}]{{end}}{{end}}{{end}}' all .PHONY: upgradable-all upgradable-all: @go list -m -u -f '{{if .Update}}{{.Path}} {{.Version}} [{{.Update.Version}}]{{end}}' all # Build all "main" packages .PHONY: main-build main-build: @for d in $(MAIN_PACKAGE) ; do \ echo "cd $$d && go build -gcflags '-e'" ; \ ( cd $$d && go build -gcflags '-e' ) ; \ done # Clean all "main" packages .PHONY: main-clean main-clean: @for d in $(MAIN_PACKAGE) ; do \ echo "cd $$d && go clean" ; \ ( cd $$d && go clean ) ; \ done # based on: github.com/koron-go/_skeleton/Makefile # $Hash:93a5966a0297543bcdd82a4dd9c2d60232a1b02c49cfa0b4341fdb71$ ================================================ FILE: README.md ================================================ # dProxy - document proxy [![GoDoc](https://godoc.org/github.com/koron/go-dproxy?status.svg)](https://godoc.org/github.com/koron/go-dproxy) [![Actions/Go](https://github.com/koron/go-dproxy/workflows/Go/badge.svg)](https://github.com/koron/go-dproxy/actions?query=workflow%3AGo) [![Go Report Card](https://goreportcard.com/badge/github.com/koron/go-dproxy)](https://goreportcard.com/report/github.com/koron/go-dproxy) dProxy is a proxy to access `interface{}` (document) by simple query. It is intented to be used with `json.Unmarshal()` or `json.NewDecorder()`. See codes for overview. ```go import ( "encoding/json" "github.com/koron/go-dproxy" ) var v interface{} json.Unmarshal([]byte(`{ "cities": [ "tokyo", 100, "osaka", 200, "hakata", 300 ], "data": { "custom": [ "male", 23, "female", 24 ] } }`), &v) // s == "tokyo", got a string. s, _ := dproxy.New(v).M("cities").A(0).String() // err != nil, type not matched. _, err := dproxy.New(v).M("cities").A(0).Float64() // n == 200, got a float64 n, _ := dproxy.New(v).M("cities").A(3).Float64() // can be chained. dproxy.New(v).M("data").M("custom").A(0).String() // err.Error() == "not found: data.kustom", wrong query can be verified. _, err = dproxy.New(v).M("data").M("kustom").String() ``` ## Getting started ### Proxy 1. Wrap a value (`interface{}`) with `dproxy.New()` get `dproxy.Proxy`. ```go p := dproxy.New(v) // v should be a value of interface{} ``` 2. Query as a map (`map[string]interface{}`)by `M()`, returns `dproxy.Proxy`. ```go p.M("cities") ``` 3. Query as an array (`[]interface{}`) with `A()`, returns `dproxy.Proxy`. ```go p.A(3) ``` 4. Therefore, can be chained queries. ```go p.M("cities").A(3) ``` 5. Get a value finally. ```go n, _ := p.M("cities").A(3).Int64() ``` 6. You'll get an error when getting a value, if there were some mistakes. ```go // OOPS! "kustom" is typo, must be "custom" _, err := p.M("data").M("kustom").A(3).Int64() // "not found: data.kustom" fmt.Println(err) ``` 7. If you tried to get a value as different type, get an error. ```go // OOPS! "cities[3]" (=200) should be float64 or int64. _, err := p.M("cities").A(3).String() // "not matched types: expected=string actual=float64: cities[3]" fmt.Println(err) ``` 8. You can verify queries easily. ### Drain Getting value and error from Proxy/ProxySet multiple times, is very awful. It must check error when every getting values. ```go p := dproxy.New(v) v1, err := p.M("cities").A(3).Int64() if err != nil { return err } v2, err := p.M("data").M("kustom").A(3).Int64() if err != nil { return err } v3, err := p.M("cities").A(3).String() if err != nil { return err } ``` It can be rewrite as simple like below with `dproxy.Drain` ```go var d Drain p := dproxy.New(v) v1 := d.Int64(p.M("cities").A(3)) v2 := d.Int64(p.M("data").M("kustom").A(3)) v3 := d.String(p.M("cities").A(3)) if err := d.CombineErrors(); err != nil { return err } ``` ### JSON Pointer JSON Pointer can be used to query `interface{}` ```go v1, err := dproxy.New(v).P("/cities/0").Int64() ``` or ```go v1, err := dproxy.Pointer(v, "/cities/0").Int64() ``` See [RFC6901][1] for details of JSON Pointer. ## LICENSE MIT license. See LICENSE. [1]: https://tools.ietf.org/html/rfc6901 ================================================ FILE: doc.go ================================================ /* Package dproxy provides a proxy to adccess `interface{}` (document) by simple query. It is intended to be used with `"encoding/json".Unmarshal()` or so. */ package dproxy ================================================ FILE: drain.go ================================================ package dproxy import "bytes" // Drain stores errors from Proxy or ProxySet. type Drain struct { errors []error } // Has returns true if the drain stored some of errors. func (d *Drain) Has() bool { return d != nil && len(d.errors) > 0 } // First returns a stored error. Returns nil if there are no errors. func (d *Drain) First() error { if d == nil || len(d.errors) == 0 { return nil } return d.errors[0] } // All returns all errors which stored. Return nil if no errors stored. func (d *Drain) All() []error { if d == nil || len(d.errors) == 0 { return nil } a := make([]error, 0, len(d.errors)) return append(a, d.errors...) } // CombineErrors returns an error which combined all stored errors. Return nil // if not erros stored. func (d *Drain) CombineErrors() error { if d == nil || len(d.errors) == 0 { return nil } return drainError(d.errors) } func (d *Drain) put(err error) { if err == nil { return } d.errors = append(d.errors, err) } // Bool returns bool value and stores an error. func (d *Drain) Bool(p Proxy) bool { v, err := p.Bool() d.put(err) return v } // Int64 returns int64 value and stores an error. func (d *Drain) Int64(p Proxy) int64 { v, err := p.Int64() d.put(err) return v } // Float64 returns float64 value and stores an error. func (d *Drain) Float64(p Proxy) float64 { v, err := p.Float64() d.put(err) return v } // String returns string value and stores an error. func (d *Drain) String(p Proxy) string { v, err := p.String() d.put(err) return v } // Array returns []interface{} value and stores an error. func (d *Drain) Array(p Proxy) []interface{} { v, err := p.Array() d.put(err) return v } // Map returns map[string]interface{} value and stores an error. func (d *Drain) Map(p Proxy) map[string]interface{} { v, err := p.Map() d.put(err) return v } // BoolArray returns []bool value and stores an error. func (d *Drain) BoolArray(ps ProxySet) []bool { v, err := ps.BoolArray() d.put(err) return v } // Int64Array returns []int64 value and stores an error. func (d *Drain) Int64Array(ps ProxySet) []int64 { v, err := ps.Int64Array() d.put(err) return v } // Float64Array returns []float64 value and stores an error. func (d *Drain) Float64Array(ps ProxySet) []float64 { v, err := ps.Float64Array() d.put(err) return v } // StringArray returns []string value and stores an error. func (d *Drain) StringArray(ps ProxySet) []string { v, err := ps.StringArray() d.put(err) return v } // ArrayArray returns [][]interface{} value and stores an error. func (d *Drain) ArrayArray(ps ProxySet) [][]interface{} { v, err := ps.ArrayArray() d.put(err) return v } // MapArray returns []map[string]interface{} value and stores an error. func (d *Drain) MapArray(ps ProxySet) []map[string]interface{} { v, err := ps.MapArray() d.put(err) return v } // ProxyArray returns []Proxy value and stores an error. func (d *Drain) ProxyArray(ps ProxySet) []Proxy { v, err := ps.ProxyArray() d.put(err) return v } type drainError []error func (derr drainError) Error() string { b := bytes.Buffer{} for i, err := range derr { if i > 0 { _, _ = b.WriteString("; ") } _, _ = b.WriteString(err.Error()) } return b.String() } ================================================ FILE: drain_test.go ================================================ package dproxy import ( "testing" ) func TestDrainBool(t *testing.T) { v := parseJSON(`{ "foo": true, "bar": false }`) d := new(Drain) foo := d.Bool(New(v).M("foo")) if d.Has() { t.Error(d.First()) } else if foo != true { t.Errorf("foo must be true") } bar := d.Bool(New(v).M("bar")) if d.Has() { t.Error(d.First()) } else if bar != false { t.Errorf("bar must be false") } baz := d.Bool(New(v).M("baz")) if !d.Has() { t.Error("baz must not exist") } else if err := d.First(); err == nil || err.Error() != "not found: baz" { t.Errorf("unexpected error: %s", err) } _ = baz } ================================================ FILE: error.go ================================================ package dproxy import ( "fmt" "strconv" ) // ErrorType is type of errors type ErrorType int const ( // Etype means expected type is not matched with actual. Etype ErrorType = iota + 1 // Enotfound means key or index doesn't exist. Enotfound // EmapNorArray means target is not a map nor an array (for JSON Pointer) EmapNorArray // EconvertFailure means value conversion is failed. EconvertFailure // EinvalidIndex means token is invalid as index (for JSON Pointer) EinvalidIndex // EinvalidQuery means query is invalid as JSON Pointer. EinvalidQuery // ErequiredType means the type mismatch against user required one. // For example M() requires map, A() requires array. ErequiredType ) func (et ErrorType) String() string { switch et { case Etype: return "Etype" case Enotfound: return "Enotfound" case EmapNorArray: return "EmapNorArray" case EconvertFailure: return "EconvertFailure" case EinvalidIndex: return "EinvalidIndex" case EinvalidQuery: return "EinvalidQuery" case ErequiredType: return "ErequiredType" default: return "Eunknown" } } // Error get detail information of the errror. type Error interface { // ErrorType returns type of error. ErrorType() ErrorType // FullAddress returns query string where cause first error. FullAddress() string } type errorProxy struct { errorType ErrorType parent frame label string expected Type actual Type infoStr string } // errorProxy implements error, Proxy and ProxySet. var ( _ error = (*errorProxy)(nil) _ Proxy = (*errorProxy)(nil) _ ProxySet = (*errorProxy)(nil) ) func (p *errorProxy) Nil() bool { return false } func (p *errorProxy) Value() (interface{}, error) { return nil, p } func (p *errorProxy) Bool() (bool, error) { return false, p } func (p *errorProxy) Int64() (int64, error) { return 0, p } func (p *errorProxy) Float64() (float64, error) { return 0, p } func (p *errorProxy) String() (string, error) { return "", p } func (p *errorProxy) Array() ([]interface{}, error) { return nil, p } func (p *errorProxy) Map() (map[string]interface{}, error) { return nil, p } func (p *errorProxy) A(n int) Proxy { return p } func (p *errorProxy) M(k string) Proxy { return p } func (p *errorProxy) P(q string) Proxy { return p } func (p *errorProxy) Empty() bool { return true } func (p *errorProxy) Len() int { return 0 } func (p *errorProxy) BoolArray() ([]bool, error) { return nil, p } func (p *errorProxy) Int64Array() ([]int64, error) { return nil, p } func (p *errorProxy) Float64Array() ([]float64, error) { return nil, p } func (p *errorProxy) StringArray() ([]string, error) { return nil, p } func (p *errorProxy) ArrayArray() ([][]interface{}, error) { return nil, p } func (p *errorProxy) MapArray() ([]map[string]interface{}, error) { return nil, p } func (p *errorProxy) ProxyArray() ([]Proxy, error) { return nil, p } func (p *errorProxy) ProxySet() ProxySet { return p } func (p *errorProxy) Q(k string) ProxySet { return p } func (p *errorProxy) Qc(k string) ProxySet { return p } func (p *errorProxy) findJPT(t string) Proxy { return p } func (p *errorProxy) parentFrame() frame { return p.parent } func (p *errorProxy) frameLabel() string { return p.label } func (p *errorProxy) Error() string { switch p.errorType { case Etype: return fmt.Sprintf("not matched types: expected=%s actual=%s: %s", p.expected.String(), p.actual.String(), p.FullAddress()) case Enotfound: return fmt.Sprintf("not found: %s", p.FullAddress()) case EmapNorArray: // FIXME: better error message. return fmt.Sprintf("not map nor array: actual=%s: %s", p.actual.String(), p.FullAddress()) case EconvertFailure: return fmt.Sprintf("convert error: %s: %s", p.infoStr, p.FullAddress()) case EinvalidIndex: // FIXME: better error message. return fmt.Sprintf("invalid index: %s: %s", p.infoStr, p.FullAddress()) case EinvalidQuery: // FIXME: better error message. return fmt.Sprintf("invalid query: %s: %s", p.infoStr, p.FullAddress()) case ErequiredType: return fmt.Sprintf("not required types: required=%s actual=%s: %s", p.expected.String(), p.actual.String(), p.FullAddress()) default: return fmt.Sprintf("unexpected: %s", p.FullAddress()) } } func (p *errorProxy) ErrorType() ErrorType { return p.errorType } func (p *errorProxy) FullAddress() string { return fullAddress(p) } func typeError(p frame, expected Type, actual interface{}) *errorProxy { return &errorProxy{ errorType: Etype, parent: p, expected: expected, actual: detectType(actual), } } func requiredTypeError(p frame, expected Type, actual interface{}) *errorProxy { return &errorProxy{ errorType: ErequiredType, parent: p, expected: expected, actual: detectType(actual), } } func elementTypeError(p frame, index int, expected Type, actual interface{}) *errorProxy { q := &simpleFrame{ parent: p, label: "[" + strconv.Itoa(index) + "]", } return typeError(q, expected, actual) } func notfoundError(p frame, address string) *errorProxy { return &errorProxy{ errorType: Enotfound, parent: p, label: address, } } ================================================ FILE: frame.go ================================================ package dproxy type frame interface { // parentFrame returns parent frame. parentFrame() frame // frameLabel return label of frame. frameLabel() string } func fullAddress(f frame) string { x := 0 for g := f; g != nil; g = g.parentFrame() { x += len(g.frameLabel()) } if x == 0 { return "(root)" } b := make([]byte, x) for g := f; g != nil; g = g.parentFrame() { x -= len(g.frameLabel()) copy(b[x:], g.frameLabel()) } if b[0] == '.' { return string(b[1:]) } return string(b) } type simpleFrame struct { parent frame label string } var _ frame = (*simpleFrame)(nil) func (f *simpleFrame) parentFrame() frame { return f.parent } func (f *simpleFrame) frameLabel() string { return f.label } ================================================ FILE: go.mod ================================================ module github.com/koron/go-dproxy go 1.13 ================================================ FILE: go.sum ================================================ ================================================ FILE: package_test.go ================================================ package dproxy import ( "fmt" "reflect" "testing" ) func assertEquals(t *testing.T, actual, expected interface{}, format string, a ...interface{}) { t.Helper() if !reflect.DeepEqual(actual, expected) { msg := fmt.Sprintf(format, a...) t.Errorf("not equal: %s\nexpect=%+v\nactual=%+v", msg, expected, actual) } } func assertError(t *testing.T, err error, exp string) { t.Helper() if err == nil { t.Fatalf("should fail with: %s", exp) } if act := err.Error(); act != exp { t.Fatalf("unexpected error:\nexpect=%s\nactual=%s\n", exp, act) } } ================================================ FILE: pointer.go ================================================ package dproxy import "strings" var jptR = strings.NewReplacer("~1", "/", "~0", "~") func unescapeJPT(t string) string { return jptR.Replace(t) } func pointer(p Proxy, q string) Proxy { if len(q) == 0 { return p } if q[0] != '/' { return &errorProxy{ errorType: EinvalidQuery, parent: p, infoStr: "not start with '/'", } } for _, t := range strings.Split(q[1:], "/") { p = p.findJPT(unescapeJPT(t)) } return p } // Pointer returns a Proxy which pointed by JSON Pointer's query q func Pointer(v interface{}, q string) Proxy { return pointer(New(v), q) } ================================================ FILE: pointer_test.go ================================================ package dproxy import "testing" func TestUnescapeJPT(t *testing.T) { f := func(d, expect string) { s := unescapeJPT(d) if s != expect { t.Errorf("unescapeJPT(%q) should be %q but actually %q", d, expect, s) } } f("foo", "foo") f("bar", "bar") f("~0", "~") f("foo~0bar", "foo~bar") f("~1", "/") f("foo~1bar", "foo/bar") f("~01", "~1") f("foo~01bar", "foo~1bar") f("~10", "/0") } func TestPointerInvalidQuery(t *testing.T) { p := Pointer(nil, "invalid") err, ok := p.(*errorProxy) if !ok { t.Fatalf("it should be *errorProxy but: %+v", p) } if err.errorType != EinvalidQuery { t.Fatalf("errorType should be EinvalidQuery but: %s", err.errorType) } } func TestPointer(t *testing.T) { f := func(q string, d, expected interface{}) { p := Pointer(d, q) v, err := p.Value() if err != nil { t.Errorf("Pointer:%q for %+v failed: %s", q, d, err) return } assertEquals(t, v, expected, "Pointer:%q for %+v", q, d) } v := parseJSON(`{ "cities": [ "tokyo", 100, "osaka", 200, "hakata", 300 ], "data": { "custom": [ "male", 21, "female", 22 ] } }`) f("", v, v) f("/cities", v, parseJSON(`["tokyo",100,"osaka",200,"hakata",300]`)) f("/cities/0", v, "tokyo") f("/cities/1", v, float64(100)) f("/cities/2", v, "osaka") f("/cities/3", v, float64(200)) f("/cities/4", v, "hakata") f("/cities/5", v, float64(300)) f("/data/custom", v, parseJSON(`["male",21,"female",22]`)) // Example from RFC6901 https://tools.ietf.org/html/rfc6901 w := parseJSON(`{ "foo": ["bar", "baz"], "": 0, "a/b": 1, "c%d": 2, "e^f": 3, "g|h": 4, "i\\j": 5, "k\"l": 6, " ": 7, "m~n": 8 }`) f("", w, w) f("/foo", w, parseJSON(`["bar","baz"]`)) f("/foo/0", w, "bar") f("/", w, float64(0)) f("/a~1b", w, float64(1)) f("/c%d", w, float64(2)) f("/e^f", w, float64(3)) f("/g|h", w, float64(4)) f("/i\\j", w, float64(5)) f("/k\"l", w, float64(6)) f("/ ", w, float64(7)) f("/m~0n", w, float64(8)) } ================================================ FILE: proxy.go ================================================ package dproxy // Proxy is a proxy to access a document (interface{}). type Proxy interface { // Nil returns true, if target value is nil. Nil() bool // Value returns a proxied value. If there are no values, it returns // error. Value() (interface{}, error) // Bool returns its value. If value isn't the type, it returns error. Bool() (bool, error) // Int64 returns its value. If value isn't the type, it returns error. Int64() (int64, error) // Float64 returns its value. If value isn't the type, it returns error. Float64() (float64, error) // String returns its value. If value isn't the type, it returns error. String() (string, error) // Array returns its value. If value isn't the type, it returns error. Array() ([]interface{}, error) // Map returns its value. If value isn't the type, it returns error. Map() (map[string]interface{}, error) // A returns an item from value treated as the array. A(n int) Proxy // M returns an item from value treated as the map. M(k string) Proxy // P returns which pointed by JSON Pointer's query q. P(q string) Proxy // ProxySet returns a set which converted from its array value. ProxySet() ProxySet // Q returns set of all items which property matchs with k. Q(k string) ProxySet // findJPT returns a match of JSON Pointer's Token t. findJPT(t string) Proxy // Proxy implements frame. frame } // ProxySet proxies to access to set. type ProxySet interface { // Empty returns true when the set is empty. Empty() bool // Len returns count of items in the set. Len() int // BoolArray returns []bool which converterd from the set. BoolArray() ([]bool, error) // Int64Array returns []int64 which converterd from the set. Int64Array() ([]int64, error) // Float64Array returns []float64 which converterd from the set. Float64Array() ([]float64, error) // StringArray returns []string which converterd from the set. StringArray() ([]string, error) // ArrayArray returns [][]interface{} which converterd from the set. ArrayArray() ([][]interface{}, error) // MapArray returns []map[string]interface{} which converterd from the set. MapArray() ([]map[string]interface{}, error) // ProxyArray returns []Proxy which wrap each items. ProxyArray() ([]Proxy, error) // A returns an proxy for index in the set. A(n int) Proxy // Q returns set of all items which property matchs with k. Q(k string) ProxySet // Qc returns set of property of all items. Qc(k string) ProxySet // Proxy implements frame. frame } // New creates a new Proxy instance for v. func New(v interface{}) Proxy { return &valueProxy{value: v} } // NewSet create a new ProxySet instance for v. func NewSet(v []interface{}) ProxySet { return &setProxy{values: v} } ================================================ FILE: proxy_test.go ================================================ package dproxy import ( "encoding/json" "testing" ) func parseJSON(s string) interface{} { var v interface{} if err := json.Unmarshal([]byte(s), &v); err != nil { panic(err) } return v } func equalStrings(a, b []string) bool { if len(a) != len(b) { return false } for i, s := range a { if s != b[i] { return false } } return true } func equalInts(a, b []int64) bool { if len(a) != len(b) { return false } for i, s := range a { if s != b[i] { return false } } return true } func TestReadme(t *testing.T) { v := parseJSON(`{ "cities": [ "tokyo", 100, "osaka", 200, "hakata", 300 ], "data": { "custom": [ "male", 21, "female", 22 ] } }`) s, err := New(v).M("cities").A(0).String() if s != "tokyo" { t.Error("cities[0] must be \"tokyo\":", err) } _, err = New(v).M("cities").A(0).Float64() if err == nil { t.Error("cities[0] (float64) must be failed:", err) } n, err := New(v).M("cities").A(1).Float64() if n != 100 { t.Error("cities[1] must be 100:", err) } s2, err := New(v).M("data").M("custom").A(2).String() if s2 != "female" { t.Error("data.custom[2] must be \"female\":", err) } _, err = New(v).M("data").M("kustom").String() if err == nil || err.Error() != "not found: data.kustom" { t.Error("err is not \"not found: data.kustom\":", err) } } func TestMapBool(t *testing.T) { v := parseJSON(`{ "foo": true, "bar": false }`) // check "foo" foo, err := New(v).M("foo").Bool() if err != nil { t.Error(err) } else if foo != true { t.Errorf("foo must be true") } // check "bar" bar, err := New(v).M("bar").Bool() if err != nil { t.Error(err) } else if bar != false { t.Errorf("bar must be false") } } type wrappedMap map[string]interface{} func TestWrappedMap(t *testing.T) { v := wrappedMap{ "foo": 123, } n, err := New(v).M("foo").Int64() if err != nil { t.Fatalf("failed: %s", err) } if n != 123 { t.Fatalf("unexpected value: %d", n) } } ================================================ FILE: set.go ================================================ package dproxy import "strconv" type setProxy struct { values []interface{} parent frame label string } // setProxy implements ProxySet var _ ProxySet = (*setProxy)(nil) func (p *setProxy) Empty() bool { return p.Len() == 0 } func (p *setProxy) Len() int { return len(p.values) } func (p *setProxy) BoolArray() ([]bool, error) { r := make([]bool, len(p.values)) for i, v := range p.values { switch v := v.(type) { case bool: r[i] = v default: return nil, elementTypeError(p, i, Tbool, v) } } return r, nil } func (p *setProxy) Int64Array() ([]int64, error) { r := make([]int64, len(p.values)) for i, v := range p.values { switch v := v.(type) { case int: r[i] = int64(v) case int32: r[i] = int64(v) case int64: r[i] = v case float32: r[i] = int64(v) case float64: r[i] = int64(v) default: return nil, elementTypeError(p, i, Tint64, v) } } return r, nil } func (p *setProxy) Float64Array() ([]float64, error) { r := make([]float64, len(p.values)) for i, v := range p.values { switch v := v.(type) { case int: r[i] = float64(v) case int32: r[i] = float64(v) case int64: r[i] = float64(v) case float32: r[i] = float64(v) case float64: r[i] = v default: return nil, elementTypeError(p, i, Tfloat64, v) } } return r, nil } func (p *setProxy) StringArray() ([]string, error) { r := make([]string, len(p.values)) for i, v := range p.values { switch v := v.(type) { case string: r[i] = v default: return nil, elementTypeError(p, i, Tstring, v) } } return r, nil } func (p *setProxy) ArrayArray() ([][]interface{}, error) { r := make([][]interface{}, len(p.values)) for i, v := range p.values { switch v := v.(type) { case []interface{}: r[i] = v default: return nil, elementTypeError(p, i, Tarray, v) } } return r, nil } func (p *setProxy) MapArray() ([]map[string]interface{}, error) { r := make([]map[string]interface{}, len(p.values)) for i, v := range p.values { switch v := v.(type) { case map[string]interface{}: r[i] = v default: return nil, elementTypeError(p, i, Tmap, v) } } return r, nil } func (p *setProxy) ProxyArray() ([]Proxy, error) { r := make([]Proxy, 0, len(p.values)) for i, v := range p.values { r = append(r, &valueProxy{ value: v, parent: p, label: "[" + strconv.Itoa(i) + "]", }) } return r, nil } func (p *setProxy) A(n int) Proxy { a := "[" + strconv.Itoa(n) + "]" if n < 0 || n >= len(p.values) { return notfoundError(p, a) } return &valueProxy{ value: p.values[n], parent: p, label: a, } } func (p *setProxy) Q(k string) ProxySet { w := findAll(p.values, k) return &setProxy{ values: w, parent: p, label: ".." + k, } } func (p *setProxy) Qc(k string) ProxySet { r := make([]interface{}, 0, len(p.values)) for _, v := range p.values { switch v := v.(type) { case map[string]interface{}: if w, ok := v[k]; ok { r = append(r, w) } } } return &setProxy{ values: r, parent: p, label: ".." + k, } } func (p *setProxy) parentFrame() frame { return p.parent } func (p *setProxy) frameLabel() string { return p.label } func findAll(v interface{}, k string) []interface{} { return findAllImpl(v, k, make([]interface{}, 0, 10)) } func findAllImpl(v interface{}, k string, r []interface{}) []interface{} { switch v := v.(type) { case map[string]interface{}: for n, w := range v { if n == k { r = append(r, w) } r = findAllImpl(w, k, r) } case []interface{}: for _, w := range v { r = findAllImpl(w, k, r) } } return r } ================================================ FILE: set_test.go ================================================ package dproxy import "testing" func TestSet(t *testing.T) { v := parseJSON(`{ "items" : [ { "name": "Bob", "age": 20 }, { "name": "Mike", "age": 23 }, { "name": "John", "age": 22 } ] }`) names, err := New(v).Q("name").StringArray() if err != nil { t.Fatal(err) } else if !equalStrings(names, []string{"Bob", "Mike", "John"}) { t.Error("unexpected names:", names) } ages, err := New(v).Q("age").Int64Array() if err != nil { t.Fatal(err) } else if !equalInts(ages, []int64{20, 23, 22}) { t.Error("unexpected ages:", ages) } _ = ages } func TestSetTypeError(t *testing.T) { v := parseJSON(`[true, false, 0, true, false]`) _, err := New(v).ProxySet().BoolArray() if err == nil { t.Fatal("should fail") } err2, ok := err.(Error) if !ok { t.Fatal("err is not Error:", err) } if et := err2.ErrorType(); et != Etype { t.Fatal("unexpected ErrorType:", et) } if ea := err2.FullAddress(); ea != "[2]" { t.Fatal("unexpected FullAddress:", ea) } } ================================================ FILE: staticcheck.conf ================================================ # vim:set ft=toml: checks = ["all"] # based on: github.com/koron-go/_skeleton/staticcheck.conf # $Hash:cd6871e83e780f6a2ce05386c0551034badf78b2ad40a76a8f6f5510$ ================================================ FILE: type.go ================================================ package dproxy // Type is type of value. type Type int const ( // Tunknown shows value is not supported. Tunknown Type = iota // Tnil shows value is nil. Tnil // Tbool shows value is bool. Tbool // Tint64 shows value is int64. Tint64 // Tfloat64 shows value is float64. Tfloat64 // Tstring shows value is string. Tstring // Tarray shows value is an array ([]interface{}) Tarray // Tmap shows value is a map (map[string]interface{}) Tmap ) // detectType returns type of a value. func detectType(v interface{}) Type { if v == nil { return Tnil } switch v.(type) { case bool: return Tbool case int, int32, int64: return Tint64 case float32, float64: return Tfloat64 case string: return Tstring case []interface{}: return Tarray case map[string]interface{}: return Tmap default: return Tunknown } } func (t Type) String() string { switch t { case Tunknown: return "unknown" case Tnil: return "nil" case Tbool: return "bool" case Tint64: return "int64" case Tfloat64: return "float64" case Tstring: return "string" case Tarray: return "array" case Tmap: return "map" default: return "unknown" } } ================================================ FILE: value.go ================================================ package dproxy import ( "reflect" "strconv" ) type valueProxy struct { value interface{} parent frame label string } // valueProxy implements Proxy. var _ Proxy = (*valueProxy)(nil) func (p *valueProxy) Nil() bool { return p.value == nil } func (p *valueProxy) Value() (interface{}, error) { return p.value, nil } func (p *valueProxy) Bool() (bool, error) { switch v := p.value.(type) { case bool: return v, nil default: return false, typeError(p, Tbool, v) } } type int64er interface { Int64() (int64, error) } func (p *valueProxy) Int64() (int64, error) { switch v := p.value.(type) { case int: return int64(v), nil case int32: return int64(v), nil case int64: return v, nil case float32: return int64(v), nil case float64: return int64(v), nil case int64er: w, err := v.Int64() if err != nil { return 0, &errorProxy{ errorType: EconvertFailure, parent: p, infoStr: err.Error(), } } return w, nil default: return 0, typeError(p, Tint64, v) } } type float64er interface { Float64() (float64, error) } func (p *valueProxy) Float64() (float64, error) { switch v := p.value.(type) { case int: return float64(v), nil case int32: return float64(v), nil case int64: return float64(v), nil case float32: return float64(v), nil case float64: return v, nil case float64er: w, err := v.Float64() if err != nil { return 0, &errorProxy{ errorType: EconvertFailure, parent: p, infoStr: err.Error(), } } return w, nil default: return 0, typeError(p, Tfloat64, v) } } func (p *valueProxy) String() (string, error) { switch v := p.value.(type) { case string: return v, nil default: return "", typeError(p, Tstring, v) } } func (p *valueProxy) Array() ([]interface{}, error) { switch v := p.value.(type) { case []interface{}: return v, nil default: return nil, typeError(p, Tarray, v) } } func (p *valueProxy) Map() (map[string]interface{}, error) { switch v := p.value.(type) { case map[string]interface{}: return v, nil default: return nil, typeError(p, Tmap, v) } } func (p *valueProxy) A(n int) Proxy { switch v := p.value.(type) { case []interface{}: a := "[" + strconv.Itoa(n) + "]" if n < 0 || n >= len(v) { return notfoundError(p, a) } return &valueProxy{ value: v[n], parent: p, label: a, } default: return requiredTypeError(p, Tarray, v) } } var mapType = reflect.TypeOf(map[string]interface{}(nil)) func (p *valueProxy) m(v map[string]interface{}, k string) Proxy { a := "." + k w, ok := v[k] if !ok { return notfoundError(p, a) } return &valueProxy{ value: w, parent: p, label: a, } } func (p *valueProxy) M(k string) Proxy { if v, ok := p.value.(map[string]interface{}); ok { return p.m(v, k) } if rv := reflect.ValueOf(p.value); rv.IsValid() && rv.Type().ConvertibleTo(mapType) { v, _ := rv.Convert(mapType).Interface().(map[string]interface{}) return p.m(v, k) } return requiredTypeError(p, Tmap, p.value) } func (p *valueProxy) P(q string) Proxy { return pointer(p, q) } func (p *valueProxy) ProxySet() ProxySet { switch v := p.value.(type) { case []interface{}: return &setProxy{ values: v, parent: p, } default: return typeError(p, Tarray, v) } } func (p *valueProxy) Q(k string) ProxySet { w := findAll(p.value, k) return &setProxy{ values: w, parent: p, label: ".." + k, } } func (p *valueProxy) findJPT(t string) Proxy { switch v := p.value.(type) { case map[string]interface{}: return p.M(t) case []interface{}: n, err := strconv.ParseUint(t, 10, 0) if err != nil { return &errorProxy{ errorType: EinvalidIndex, parent: p, infoStr: err.Error(), } } return p.A(int(n)) default: return &errorProxy{ errorType: EmapNorArray, parent: p, actual: detectType(v), } } } func (p *valueProxy) parentFrame() frame { return p.parent } func (p *valueProxy) frameLabel() string { return p.label } ================================================ FILE: value_test.go ================================================ package dproxy import "testing" func TestTypeError(t *testing.T) { t.Run("map at root", func(t *testing.T) { v := &valueProxy{} _, err := v.M("foo").Int64() assertError(t, err, "not required types: required=map actual=nil: (root)") }) t.Run("map at child", func(t *testing.T) { v := &valueProxy{ parent: &valueProxy{}, label: "foo", } _, err := v.M("bar").Int64() assertError(t, err, "not required types: required=map actual=nil: foo") }) t.Run("array at root", func(t *testing.T) { v := &valueProxy{} _, err := v.A(0).Int64() assertError(t, err, "not required types: required=array actual=nil: (root)") }) t.Run("array at child", func(t *testing.T) { v := &valueProxy{ parent: &valueProxy{}, label: "foo", } _, err := v.A(0).Int64() assertError(t, err, "not required types: required=array actual=nil: foo") }) }