Skip to content

Commit

Permalink
feat: add first version
Browse files Browse the repository at this point in the history
  • Loading branch information
denouche committed Jan 17, 2023
1 parent b2a9d5e commit 036f97c
Show file tree
Hide file tree
Showing 10 changed files with 590 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.idea
vendor
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/denouche/go-json-patch-jsonpath

go 1.18

require github.com/ohler55/ojg v1.17.2
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/ohler55/ojg v1.17.1 h1:tctLinqjSLksyXOip6ALetxK4l9CRGH6K5KSYUYUrYI=
github.com/ohler55/ojg v1.17.1/go.mod h1:7Ghirupn8NC8hSSDpI0gcjorPxj+vSVIONDWfliHR1k=
github.com/ohler55/ojg v1.17.2 h1:e8vUJa6l9v9li0T5xqQIDhVFnialk21LA1biXJlu0Rs=
github.com/ohler55/ojg v1.17.2/go.mod h1:7Ghirupn8NC8hSSDpI0gcjorPxj+vSVIONDWfliHR1k=
50 changes: 50 additions & 0 deletions model_patchrequest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package jsonpatch

import (
"encoding/json"
"errors"
"fmt"
)

const (
PatchOperationAdd = "add"
PatchOperationRemove = "remove"
PatchOperationReplace = "replace"
)

type PatchOperation string

type PatchRequest[T any] struct {
Operation PatchOperation `json:"op" validate:"required,oneof=remove replace"` // TODO implements add
Path string `json:"path" validate:"required,jsonpath,ne=$"`
Value any `json:"value"`
}

// Apply TODO
func (pr *PatchRequest[T]) Apply(initialResource *T, emptyResource *T) (*T, error) {
switch pr.Operation {
case PatchOperationReplace:
return pr.replace(initialResource, emptyResource)
case PatchOperationRemove:
return pr.remove(initialResource, emptyResource)
case PatchOperationAdd:
//TODO make the implementation
fallthrough // fallthrough for now
default:
return nil, errors.New("operation not implemented")
}
}

func (pr *PatchRequest[T]) remarshal(resourceAsMap interface{}, emptyResource *T) (*T, error) {
newBytes, err := json.Marshal(resourceAsMap)
if err != nil {
return nil, fmt.Errorf("match fail to marshal input resource %s", err.Error())
}

err = json.Unmarshal(newBytes, &emptyResource)
if err != nil {
return nil, fmt.Errorf("match fail to unmarshal %s", err.Error())
}

return emptyResource, nil
}
18 changes: 18 additions & 0 deletions model_patchrequests.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package jsonpatch

type PatchRequests[T any] struct {
Patches []*PatchRequest[T] `json:"patches" validate:"required,min=1,dive,required"`
}

// Apply TODO
func (prs *PatchRequests[T]) Apply(initialResource *T, newer func() *T) (*T, error) {
var err error
patched := initialResource
for _, pr := range prs.Patches {
patched, err = pr.Apply(initialResource, newer())
if err != nil {
return nil, err
}
}
return patched, nil
}
21 changes: 21 additions & 0 deletions model_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package jsonpatch

type MyStruct struct {
FieldString string `json:"field_string"`
FieldStringPtr *string `json:"field_string_ptr"`
FieldMapStringString map[string]string `json:"field_map_string_string"`
FieldArrayString []string `json:"field_array_string"`
FieldStruct *MySubStruct `json:"field_struct"`
FieldArrayStruct []*MySubStruct `json:"field_array_struct"`
}

type MySubStruct struct {
FieldIntPtr *int `json:"field_int_ptr"`
FieldBool bool `json:"field_bool"`
FieldString1 string `json:"field_string_sub1"`
FieldString2 string `json:"field_string_sub2"`
}

func getPtr[T any](in T) *T {
return &in
}
33 changes: 33 additions & 0 deletions operation_remove.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package jsonpatch

import (
"encoding/json"
"fmt"

"github.com/ohler55/ojg/jp"
)

func (pr *PatchRequest[T]) remove(initialResource *T, emptyResource *T) (*T, error) {
compiledPath, err := jp.ParseString(pr.Path)
if err != nil {
return nil, fmt.Errorf("fail to compile path %s %s", pr.Path, err.Error())
}

b, err := json.Marshal(initialResource)
if err != nil {
return nil, fmt.Errorf("match fail to marshal input resource %s", err.Error())
}

var resourceAsMap interface{}
err = json.Unmarshal(b, &resourceAsMap)
if err != nil {
return nil, fmt.Errorf("match fail to unmarshal %s", err.Error())
}

resourceAsMapModified, err := compiledPath.Remove(resourceAsMap)
if err != nil {
return nil, fmt.Errorf("error while deleting nodes %s", err.Error())
}

return pr.remarshal(resourceAsMapModified, emptyResource)
}
81 changes: 81 additions & 0 deletions operation_remove_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package jsonpatch

import (
"encoding/json"
"reflect"
"testing"
)

type applyRemoveTestCase[T any] struct {
name string
patches []*PatchRequest[MyStruct]
input T
newEmptyInputFunc func() T
expectError bool
expect T
}

func TestPatchRequest_applyRemove(t *testing.T) {
testCases := []applyRemoveTestCase[*MyStruct]{
{
name: "remove_string",
patches: []*PatchRequest[MyStruct]{
{
Operation: "remove",
Path: "$.field_string",
},
},
newEmptyInputFunc: func() *MyStruct {
return &MyStruct{}
},
input: &MyStruct{
FieldString: "foo",
},
expectError: false,
expect: &MyStruct{
FieldString: "",
},
},

{
name: "remove_string_ptr",
patches: []*PatchRequest[MyStruct]{
{
Operation: "remove",
Path: "$.field_string_ptr",
},
},
newEmptyInputFunc: func() *MyStruct {
return &MyStruct{}
},
input: &MyStruct{
FieldStringPtr: getPtr("foo"),
},
expectError: false,
expect: &MyStruct{
FieldStringPtr: nil,
},
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
var err error
var patched *MyStruct
patched = tt.input
for _, pr := range tt.patches {
patched, err = pr.remove(patched, tt.newEmptyInputFunc())
}

if (err != nil) != tt.expectError {
t.Errorf("applyRemove() error = %v, expectError %v", err, tt.expectError)
return
}
if !reflect.DeepEqual(patched, tt.expect) {
bPatched, _ := json.Marshal(patched)
bExpected, _ := json.Marshal(tt.expect)
t.Errorf("applyRemove() got = %s", string(bPatched))
t.Errorf("applyRemove() expect = %s", string(bExpected))
}
})
}
}
33 changes: 33 additions & 0 deletions operation_replace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package jsonpatch

import (
"encoding/json"
"fmt"

"github.com/ohler55/ojg/jp"
)

func (pr *PatchRequest[T]) replace(resource *T, emptyResource *T) (*T, error) {
compiledPath, err := jp.ParseString(pr.Path)
if err != nil {
return nil, fmt.Errorf("fail to compile path %s %s", pr.Path, err.Error())
}

b, err := json.Marshal(resource)
if err != nil {
return nil, fmt.Errorf("match fail to marshal input resource %s", err.Error())
}

var resourceAsMap interface{}
err = json.Unmarshal(b, &resourceAsMap)
if err != nil {
return nil, fmt.Errorf("match fail to unmarshal %s", err.Error())
}

err = compiledPath.Set(resourceAsMap, pr.Value)
if err != nil {
return nil, err
}

return pr.remarshal(resourceAsMap, emptyResource)
}
Loading

0 comments on commit 036f97c

Please sign in to comment.