Initial commit

This commit is contained in:
2022-03-22 09:21:55 +01:00
commit ada7f18e36
49 changed files with 2635 additions and 0 deletions

View File

@ -0,0 +1,73 @@
package jsonpointer
import (
"strconv"
"github.com/pkg/errors"
)
func del(doc interface{}, tokens []string) (interface{}, error) {
currentToken := tokens[0]
switch typedDoc := doc.(type) {
case map[string]interface{}:
nestedDoc, exists := typedDoc[currentToken]
if !exists {
return doc, nil
}
if len(tokens) == 1 {
delete(typedDoc, currentToken)
return typedDoc, nil
}
nestedDoc, err := del(nestedDoc, tokens[1:])
if err != nil {
return nil, errors.WithStack(err)
}
typedDoc[currentToken] = nestedDoc
return typedDoc, nil
case []interface{}:
var (
index uint64
nestedDoc interface{}
err error
)
if currentToken == NonExistentMemberToken {
index = uint64(len(typedDoc) - 1)
} else {
index, err = strconv.ParseUint(currentToken, 10, 64)
if err != nil {
return nil, errors.WithStack(err)
}
if len(typedDoc) <= int(index) {
return typedDoc, nil
}
}
if len(tokens) == 1 {
typedDoc = append(typedDoc[:index], typedDoc[index+1:]...)
return typedDoc, nil
}
nestedDoc, err = del(nestedDoc, tokens[1:])
if err != nil {
return nil, errors.WithStack(err)
}
typedDoc[index] = nestedDoc
return typedDoc, nil
default:
return typedDoc, nil
}
}

View File

@ -0,0 +1,95 @@
package jsonpointer
import (
"encoding/json"
"fmt"
"io/ioutil"
"reflect"
"strings"
"testing"
"github.com/pkg/errors"
)
type pointerDeleteTestCase struct {
DocPath string
Pointer string
ExpectedRawDocument string
}
func TestPointerDelete(t *testing.T) {
t.Parallel()
testCases := []pointerDeleteTestCase{
{
DocPath: "./testdata/set/basic.json",
Pointer: "/foo",
ExpectedRawDocument: `{}`,
},
{
DocPath: "./testdata/set/nested.json",
Pointer: "/nestedObject/foo/1",
ExpectedRawDocument: `
{
"nestedObject": {
"foo": [
"bar"
]
}
}`,
},
{
DocPath: "./testdata/set/nested.json",
Pointer: "/nestedObject/foo/-",
ExpectedRawDocument: `
{
"nestedObject": {
"foo": [
"bar"
]
}
}`,
},
}
for i, tc := range testCases {
func(index int, tc pointerDeleteTestCase) {
t.Run(fmt.Sprintf("#%d: '%s'", i, tc.Pointer), func(t *testing.T) {
t.Parallel()
baseRawDocument, err := ioutil.ReadFile(tc.DocPath)
if err != nil {
t.Fatal(errors.WithStack(err))
}
var baseDoc interface{}
if err := json.Unmarshal([]byte(baseRawDocument), &baseDoc); err != nil {
t.Fatal(errors.WithStack(err))
}
pointer := New(tc.Pointer)
updatedDoc, err := pointer.Delete(baseDoc)
if err != nil {
t.Fatal(errors.WithStack(err))
}
rawDoc, err := json.MarshalIndent(updatedDoc, "", " ")
if err != nil {
t.Fatal(errors.WithStack(err))
}
var expectedDoc interface{}
if err := json.Unmarshal([]byte(tc.ExpectedRawDocument), &expectedDoc); err != nil {
t.Fatal(errors.WithStack(err))
}
if !reflect.DeepEqual(expectedDoc, updatedDoc) {
t.Errorf("Delete pointer '%s': expected document \n'%s', got \n'%s'", tc.Pointer, strings.TrimSpace(tc.ExpectedRawDocument), rawDoc)
}
})
}(i, tc)
}
}

View File

@ -0,0 +1,9 @@
package jsonpointer
import "errors"
var (
ErrNotFound = errors.New("not found")
ErrUnexpectedType = errors.New("unexpected type")
ErrOutOfBounds = errors.New("out of bounds")
)

View File

@ -0,0 +1,111 @@
package jsonpointer
import (
"strconv"
"github.com/pkg/errors"
)
func force(doc interface{}, tokens []string, value interface{}) (interface{}, error) {
if len(tokens) == 0 {
return value, nil
}
currentToken := tokens[0]
switch typedDoc := doc.(type) {
case map[string]interface{}:
nestedDoc, exists := typedDoc[currentToken]
if !exists {
if len(tokens) == 1 {
typedDoc[currentToken] = value
return typedDoc, nil
}
nextToken := tokens[1]
if isArrayIndexToken(nextToken) {
nestedDoc = make([]interface{}, 0)
} else {
nestedDoc = make(map[string]interface{})
}
}
nestedDoc, err := force(nestedDoc, tokens[1:], value)
if err != nil {
return nil, errors.WithStack(err)
}
typedDoc[currentToken] = nestedDoc
return typedDoc, nil
case []interface{}:
var (
index uint64
nestedDoc interface{}
err error
)
if currentToken == NonExistentMemberToken {
typedDoc = append(typedDoc, value)
index = uint64(len(typedDoc) - 1)
} else {
index, err = strconv.ParseUint(currentToken, 10, 64)
if err != nil {
return nil, errors.WithStack(err)
}
if len(typedDoc) <= int(index) {
for i := len(typedDoc); i <= int(index); i++ {
typedDoc = append(typedDoc, nil)
}
}
nestedDoc = typedDoc[index]
}
nestedDoc, err = force(nestedDoc, tokens[1:], value)
if err != nil {
return nil, errors.WithStack(err)
}
typedDoc[index] = nestedDoc
return typedDoc, nil
default:
overrideDoc := map[string]interface{}{}
overrideDoc[currentToken] = value
var nestedDoc interface{}
if len(tokens) > 1 && isArrayIndexToken(tokens[1]) {
nestedDoc = make([]interface{}, 0)
} else {
nestedDoc = make(map[string]interface{})
}
nestedDoc, err := force(nestedDoc, tokens[1:], value)
if err != nil {
return nil, errors.WithStack(err)
}
overrideDoc[currentToken] = nestedDoc
return overrideDoc, nil
}
}
func isArrayIndexToken(token string) bool {
if token == NonExistentMemberToken {
return true
}
if _, err := strconv.ParseUint(token, 10, 64); err != nil {
return false
}
return true
}

View File

@ -0,0 +1,60 @@
package jsonpointer
import (
"strconv"
"github.com/pkg/errors"
)
func get(doc interface{}, tokens []string) (interface{}, error) {
if len(tokens) == 0 {
return doc, nil
}
currentToken := tokens[0]
if doc == nil {
return nil, errors.Wrapf(ErrNotFound, "pointer '%s' not found on document", tokensToString(tokens))
}
switch typedDoc := doc.(type) {
case map[string]interface{}:
value, exists := typedDoc[currentToken]
if !exists {
return nil, errors.Wrapf(ErrNotFound, "pointer '%s' not found on document", tokensToString(tokens))
}
value, err := get(value, tokens[1:])
if err != nil {
return nil, errors.WithStack(err)
}
return value, nil
case []interface{}:
if currentToken == NonExistentMemberToken {
return nil, errors.WithStack(ErrOutOfBounds)
}
index, err := strconv.ParseUint(currentToken, 10, 64)
if err != nil {
return nil, errors.WithStack(err)
}
if len(typedDoc) <= int(index) {
return nil, errors.WithStack(ErrOutOfBounds)
}
value := typedDoc[index]
value, err = get(value, tokens[1:])
if err != nil {
return nil, errors.WithStack(err)
}
return value, nil
default:
return nil, errors.Wrapf(ErrUnexpectedType, "unexpected type '%T'", typedDoc)
}
}

View File

@ -0,0 +1,140 @@
package jsonpointer
import (
"encoding/json"
"fmt"
"io/ioutil"
"reflect"
"testing"
"github.com/pkg/errors"
)
type pointerGetTestCase struct {
Document interface{}
Pointer string
ExpectedRawValue string
}
func TestPointerGet(t *testing.T) {
t.Parallel()
ietfRawDocument, err := ioutil.ReadFile("./testdata/ietf.json")
if err != nil {
t.Fatal(errors.WithStack(err))
}
var ietfDoc interface{}
if err := json.Unmarshal([]byte(ietfRawDocument), &ietfDoc); err != nil {
t.Fatal(errors.WithStack(err))
}
// IETF tests cases
// From https://datatracker.ietf.org/doc/html/rfc6901
//
// "" // the whole document
// "/foo" ["bar", "baz"]
// "/foo/0" "bar"
// "/" 0
// "/a~1b" 1
// "/c%d" 2
// "/e^f" 3
// "/g|h" 4
// "/i\\j" 5
// "/k\"l" 6
// "/ " 7
// "/m~0n" 8
testCases := []pointerGetTestCase{
{
Document: ietfDoc,
Pointer: "",
ExpectedRawValue: string(ietfRawDocument),
},
{
Document: ietfDoc,
Pointer: "/foo",
ExpectedRawValue: "[\"bar\", \"baz\"]",
},
{
Document: ietfDoc,
Pointer: "/foo/0",
ExpectedRawValue: "\"bar\"",
},
{
Document: ietfDoc,
Pointer: `/`,
ExpectedRawValue: `0`,
},
{
Document: ietfDoc,
Pointer: "/a~1b",
ExpectedRawValue: "1",
},
{
Document: ietfDoc,
Pointer: "/c%d",
ExpectedRawValue: "2",
},
{
Document: ietfDoc,
Pointer: "/e^f",
ExpectedRawValue: "3",
},
{
Document: ietfDoc,
Pointer: "/g|h",
ExpectedRawValue: "4",
},
{
Document: ietfDoc,
Pointer: "/i\\j",
ExpectedRawValue: "5",
},
{
Document: ietfDoc,
Pointer: "/k\"l",
ExpectedRawValue: "6",
},
{
Document: ietfDoc,
Pointer: "/ ",
ExpectedRawValue: "7",
},
{
Document: ietfDoc,
Pointer: "/m~0n",
ExpectedRawValue: "8",
},
}
for i, tc := range testCases {
func(index int, tc pointerGetTestCase) {
t.Run(fmt.Sprintf("#%d: '%s'", i, tc.Pointer), func(t *testing.T) {
t.Parallel()
pointer := New(tc.Pointer)
value, err := pointer.Get(tc.Document)
if err != nil {
t.Fatal(errors.WithStack(err))
}
rawValue, err := json.Marshal(value)
if err != nil {
t.Fatal(errors.WithStack(err))
}
var expectedValue interface{}
if err := json.Unmarshal([]byte(tc.ExpectedRawValue), &expectedValue); err != nil {
t.Fatal(errors.WithStack(err))
}
if !reflect.DeepEqual(expectedValue, value) {
t.Errorf("Pointer '%s': expected value '%s', got '%s'", tc.Pointer, tc.ExpectedRawValue, rawValue)
}
})
}(i, tc)
}
}

View File

@ -0,0 +1,92 @@
package jsonpointer
import (
"strings"
"github.com/pkg/errors"
)
const (
TokenSeparator = "/"
NonExistentMemberToken = "-"
)
type Pointer struct {
tokens []string
}
func (p *Pointer) Get(doc interface{}) (interface{}, error) {
value, err := get(doc, p.tokens)
if err != nil {
return nil, errors.WithStack(err)
}
return value, nil
}
func (p *Pointer) Set(doc interface{}, value interface{}) (interface{}, error) {
doc, err := set(doc, p.tokens, value)
if err != nil {
return nil, errors.WithStack(err)
}
return doc, nil
}
func (p *Pointer) Force(doc interface{}, value interface{}) (interface{}, error) {
doc, err := force(doc, p.tokens, value)
if err != nil {
return nil, errors.WithStack(err)
}
return doc, nil
}
func (p *Pointer) Delete(doc interface{}) (interface{}, error) {
doc, err := del(doc, p.tokens)
if err != nil {
return nil, errors.WithStack(err)
}
return doc, nil
}
func New(raw string) *Pointer {
tokens := decodeTokens(raw)
return &Pointer{tokens}
}
func tokensToString(tokens []string) string {
escapedTokens := make([]string, 0)
for _, t := range tokens {
escapedTokens = append(escapedTokens, escapeToken(t))
}
return TokenSeparator + strings.Join(escapedTokens, TokenSeparator)
}
func escapeToken(token string) string {
token = strings.ReplaceAll(token, "/", "~1")
token = strings.ReplaceAll(token, "~", "~0")
return token
}
func unescapeToken(token string) string {
token = strings.ReplaceAll(token, "~1", "/")
token = strings.ReplaceAll(token, "~0", "~")
return token
}
func decodeTokens(raw string) []string {
tokens := strings.Split(raw, TokenSeparator)
for i, t := range tokens {
tokens[i] = unescapeToken(t)
}
return tokens[1:]
}

View File

@ -0,0 +1,67 @@
package jsonpointer
import (
"strconv"
"github.com/pkg/errors"
)
func set(doc interface{}, tokens []string, value interface{}) (interface{}, error) {
if len(tokens) == 0 {
return value, nil
}
currentToken := tokens[0]
switch typedDoc := doc.(type) {
case map[string]interface{}:
nestedDoc, exists := typedDoc[currentToken]
if !exists {
return nil, errors.Wrapf(ErrNotFound, "pointer '%s' not found on document", tokensToString(tokens))
}
nestedDoc, err := set(nestedDoc, tokens[1:], value)
if err != nil {
return nil, errors.WithStack(err)
}
typedDoc[currentToken] = nestedDoc
return typedDoc, nil
case []interface{}:
var (
index uint64
nestedDoc interface{}
err error
)
if currentToken == NonExistentMemberToken {
typedDoc = append(typedDoc, value)
index = uint64(len(typedDoc) - 1)
} else {
index, err = strconv.ParseUint(currentToken, 10, 64)
if err != nil {
return nil, errors.WithStack(err)
}
if len(typedDoc) <= int(index) {
return nil, errors.WithStack(ErrOutOfBounds)
}
nestedDoc = typedDoc[index]
}
nestedDoc, err = set(nestedDoc, tokens[1:], value)
if err != nil {
return nil, errors.WithStack(err)
}
typedDoc[index] = nestedDoc
return typedDoc, nil
default:
return nil, errors.Wrapf(ErrUnexpectedType, "unexpected type '%T'", typedDoc)
}
}

View File

@ -0,0 +1,102 @@
package jsonpointer
import (
"encoding/json"
"fmt"
"io/ioutil"
"reflect"
"strings"
"testing"
"github.com/pkg/errors"
)
type pointerSetTestCase struct {
DocPath string
Pointer string
Value interface{}
ExpectedRawDocument string
}
func TestPointerSet(t *testing.T) {
t.Parallel()
testCases := []pointerSetTestCase{
{
DocPath: "./testdata/set/basic.json",
Pointer: "/foo",
Value: "bar",
ExpectedRawDocument: `{"foo":"bar"}`,
},
{
DocPath: "./testdata/set/nested.json",
Pointer: "/nestedObject/foo/1",
Value: "test",
ExpectedRawDocument: `
{
"nestedObject": {
"foo": [
"bar",
"test"
]
}
}`,
},
{
DocPath: "./testdata/set/nested.json",
Pointer: "/nestedObject/foo/-",
Value: "baz",
ExpectedRawDocument: `
{
"nestedObject": {
"foo": [
"bar",
0,
"baz"
]
}
}`,
},
}
for i, tc := range testCases {
func(index int, tc pointerSetTestCase) {
t.Run(fmt.Sprintf("#%d: '%s'", i, tc.Pointer), func(t *testing.T) {
t.Parallel()
baseRawDocument, err := ioutil.ReadFile(tc.DocPath)
if err != nil {
t.Fatal(errors.WithStack(err))
}
var baseDoc interface{}
if err := json.Unmarshal([]byte(baseRawDocument), &baseDoc); err != nil {
t.Fatal(errors.WithStack(err))
}
pointer := New(tc.Pointer)
updatedDoc, err := pointer.Set(baseDoc, tc.Value)
if err != nil {
t.Fatal(errors.WithStack(err))
}
rawDoc, err := json.MarshalIndent(updatedDoc, "", " ")
if err != nil {
t.Fatal(errors.WithStack(err))
}
var expectedDoc interface{}
if err := json.Unmarshal([]byte(tc.ExpectedRawDocument), &expectedDoc); err != nil {
t.Fatal(errors.WithStack(err))
}
if !reflect.DeepEqual(expectedDoc, updatedDoc) {
t.Errorf("Set pointer '%s' -> '%v': expected document '%s', got '%s'", tc.Pointer, tc.Value, strings.TrimSpace(tc.ExpectedRawDocument), rawDoc)
}
})
}(i, tc)
}
}

12
internal/jsonpointer/testdata/ietf.json vendored Normal file
View File

@ -0,0 +1,12 @@
{
"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
}

View File

@ -0,0 +1 @@
{"foo":null}

View File

@ -0,0 +1,8 @@
{
"nestedObject": {
"foo": [
"bar",
0
]
}
}