diff --git a/patch/copy_op.go b/patch/copy_op.go new file mode 100644 index 0000000..ae4a4b3 --- /dev/null +++ b/patch/copy_op.go @@ -0,0 +1,15 @@ +package patch + +type CopyOp struct { + Path Pointer + From Pointer +} + +func (op CopyOp) Apply(doc interface{}) (interface{}, error) { + value, err := FindOp{Path: op.From}.Apply(doc) + if err != nil { + return nil, err + } + + return ReplaceOp{Path: op.Path, Value: value}.Apply(doc) +} diff --git a/patch/copy_op_test.go b/patch/copy_op_test.go new file mode 100644 index 0000000..4d9028f --- /dev/null +++ b/patch/copy_op_test.go @@ -0,0 +1,41 @@ +package patch_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cppforlife/go-patch/patch" +) + +var _ = Describe("CopyOp.Apply", func() { + Describe("array item", func() { + It("replaces array item", func() { + res, err := CopyOp{ + Path: MustNewPointerFromString("/-"), + From: MustNewPointerFromString("/0"), + }.Apply([]interface{}{1, 2, 3}) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal([]interface{}{1, 2, 3, 1})) + }) + }) + + Describe("map key", func() { + It("copies map key", func() { + doc := map[interface{}]interface{}{ + "abc": "abc", + "xyz": "xyz", + } + + res, err := CopyOp{ + From: MustNewPointerFromString("/abc"), + Path: MustNewPointerFromString("/def?"), + }.Apply(doc) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal(map[interface{}]interface{}{ + "abc": "abc", + "def": "abc", + "xyz": "xyz", + })) + }) + }) +}) diff --git a/patch/op_definition.go b/patch/op_definition.go index 6da7ad6..b364a22 100644 --- a/patch/op_definition.go +++ b/patch/op_definition.go @@ -10,6 +10,7 @@ import ( type OpDefinition struct { Type string `json:",omitempty"` Path *string `json:",omitempty"` + From *string `json:",omitempty"` Value *interface{} `json:",omitempty"` Error *string `json:",omitempty"` @@ -40,6 +41,12 @@ func NewOpsFromDefinitions(opDefs []OpDefinition) (Ops, error) { return nil, fmt.Errorf("Remove operation [%d]: %s within\n%s", i, err, opFmt) } + case "copy": + op, err = p.newCopyOp(opDef) + if err != nil { + return nil, fmt.Errorf("Copy operation [%d]: %s within\n%s", i, err, opFmt) + } + default: return nil, fmt.Errorf("Unknown operation [%d] with type '%s' within\n%s", i, opDef.Type, opFmt) } @@ -88,6 +95,32 @@ func (parser) newRemoveOp(opDef OpDefinition) (RemoveOp, error) { return RemoveOp{Path: ptr}, nil } +func (parser) newCopyOp(opDef OpDefinition) (CopyOp, error) { + if opDef.Path == nil { + return CopyOp{}, fmt.Errorf("Missing path") + } + + if opDef.From == nil { + return CopyOp{}, fmt.Errorf("Missing from") + } + + if opDef.Value != nil { + return CopyOp{}, fmt.Errorf("Cannot specify value") + } + + pathPtr, err := NewPointerFromString(*opDef.Path) + if err != nil { + return CopyOp{}, fmt.Errorf("Invalid path: %s", err) + } + + fromPtr, err := NewPointerFromString(*opDef.From) + if err != nil { + return CopyOp{}, fmt.Errorf("Invalid from: %s", err) + } + + return CopyOp{Path: pathPtr, From: fromPtr}, nil +} + func (parser) fmtOpDef(opDef OpDefinition) string { var ( redactedVal interface{} = "" diff --git a/patch/op_definition_test.go b/patch/op_definition_test.go index 17beec7..bb830ca 100644 --- a/patch/op_definition_test.go +++ b/patch/op_definition_test.go @@ -10,16 +10,19 @@ import ( var _ = Describe("NewOpsFromDefinitions", func() { var ( path = "/abc" + from = "/abc" invalidPath = "abc" + invalidFrom = "abc" errorMsg = "error" val interface{} = 123 complexVal interface{} = map[interface{}]interface{}{123: 123} ) - It("supports 'replace' and 'remove' operations", func() { + It("supports 'replace', 'remove', 'copy' operations", func() { opDefs := []OpDefinition{ {Type: "replace", Path: &path, Value: &val}, {Type: "remove", Path: &path}, + {Type: "copy", Path: &path, From: &from}, } ops, err := NewOpsFromDefinitions(opDefs) @@ -28,6 +31,10 @@ var _ = Describe("NewOpsFromDefinitions", func() { Expect(ops).To(Equal(Ops([]Op{ ReplaceOp{Path: MustNewPointerFromString("/abc"), Value: 123}, RemoveOp{Path: MustNewPointerFromString("/abc")}, + CopyOp{ + Path: MustNewPointerFromString("/abc"), + From: MustNewPointerFromString("/abc"), + }, }))) }) @@ -145,6 +152,62 @@ var _ = Describe("NewOpsFromDefinitions", func() { { "Type": "remove", "Path": "abc" +}`)) + }) + }) + + Describe("copy", func() { + It("requires path", func() { + _, err := NewOpsFromDefinitions([]OpDefinition{{Type: "copy", From: &from}}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal(`Copy operation [0]: Missing path within +{ + "Type": "copy", + "From": "/abc" +}`)) + }) + + It("requires from", func() { + _, err := NewOpsFromDefinitions([]OpDefinition{{Type: "copy", Path: &path}}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal(`Copy operation [0]: Missing from within +{ + "Type": "copy", + "Path": "/abc" +}`)) + }) + + It("does not allow value", func() { + _, err := NewOpsFromDefinitions([]OpDefinition{{Type: "copy", Path: &path, From: &from, Value: &val}}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal(`Copy operation [0]: Cannot specify value within +{ + "Type": "copy", + "Path": "/abc", + "From": "/abc", + "Value": "" +}`)) + }) + + It("requires valid path", func() { + _, err := NewOpsFromDefinitions([]OpDefinition{{Type: "copy", Path: &invalidPath, From: &from}}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal(`Copy operation [0]: Invalid path: Expected to start with '/' within +{ + "Type": "copy", + "Path": "abc", + "From": "/abc" +}`)) + }) + + It("requires valid from", func() { + _, err := NewOpsFromDefinitions([]OpDefinition{{Type: "copy", Path: &path, From: &invalidFrom}}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal(`Copy operation [0]: Invalid from: Expected to start with '/' within +{ + "Type": "copy", + "Path": "/abc", + "From": "abc" }`)) }) })