-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.go
More file actions
296 lines (250 loc) · 8.42 KB
/
main.go
File metadata and controls
296 lines (250 loc) · 8.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
package jpatch
import (
"fmt"
"strings"
"github.com/sethjback/jpatch/jpatcherror"
)
const (
// Add action
Add = "add"
// Remove action
Remove = "remove"
// Replace action
Replace = "replace"
// Move action
Move = "move"
// Copy action
Copy = "copy"
// Test action
Test = "test"
)
// Patch defines an operation to perform on a particular path within a JSON object
type Patch struct {
// Op is the operation
Op string `json:"op"`
// Path is the RFC 6901 path within the object to modify.
Path string `json:"path"`
// Value is the new data to set at the path.
Value interface{} `json:"value,omitempty"`
// From is the path to move or copy
From string `json:"from,omitempty"`
}
// PathSegment defines an appropriate object path segment
// PathSegments are a way for an object to tell jpatch what constitutes a valid path within the object.
// Each segment must contain all the possible branches which can be followed from it
//
// Values represent all the possible valid values for the path segement (unless the segment is a wildcard)
//
// Children maps the possible next step segments, indexed by the Value.
// For wildcard segements, use "*" as the index
type PathSegment struct {
// Optional signals whether this segment is required
Optional bool
// Wildcard signals whether any value should be accepted
Wildcard bool
// Values has two purposes:
// 1. it defines acceptable values for this segement
// 2. it can be used to map the json object property name to a different name in the system.
// for example, if the path was userName, but the database field was user_name, you can make that adjustment here:
// map[string]string{"userName":"user_name"}
Values map[string]*PathValue
// Children are the possible paths beneath the current path, based on the Value
// The key is the the json path attribute, and the returned path segment describes the next
// level in the path/hierarchy.
// For Wildcard paths, use "*"
Children map[string]*PathSegment
}
// PathValue defines an appropriate value for the segment, and what operaitons are permitted on that segment
// SupportedOps is only evaluated when the path is wanting to act on the value itself, i.e. if the path extends to
// children then the child's value and supported ops are used
type PathValue struct {
Name string
SupportedOps []string
}
// Patchable is the interface that must be implemented to translate the Patch operations
// to valid operations that can be run against the datastore
// The path is translated into a "." separated document path
type Patchable interface {
// GetJPatchRootSegment returns the root path segment definition
// All potential patch operations are validatd against this definition
GetRootSegment() *PathSegment
// ValidatePatches gives the object a chance to validate and modify the patch values in any way before passing them to the datastore
ValidatePatches([]Patch) ([]Patch, []error)
// ApplyPatches expects the object to actually apply the patches
ApplyPatches([]Patch) []error
}
// ProcessPatches process patch objects
func ProcessPatches(patches []Patch, pable Patchable) ([]Patch, []error) {
ps, errs := validateSortPataches(patches, pable.GetRootSegment())
if errs != nil {
return nil, errs
}
return pable.ValidatePatches(ps)
}
func PatchObject(patches []Patch, pable Patchable) []error {
ps, errs := validateSortPataches(patches, pable.GetRootSegment())
if errs != nil {
return errs
}
return pable.ApplyPatches(ps)
}
func validateSortPataches(patches []Patch, rootSegment *PathSegment) ([]Patch, []error) {
var errs []error
vAdd := make([]Patch, 0)
vRemove := make([]Patch, 0)
vMove := make([]Patch, 0)
vReplace := make([]Patch, 0)
for _, p := range patches {
err := validatePatch(p)
if err != nil {
errs = append(errs, err)
continue
}
finalPath, err := validatePath(p.Path, p.Op, rootSegment)
if err != nil {
errs = append(errs, err)
}
p.Path = finalPath
if p.From != "" {
finalPath, err = validateFrom(p.From, p.Op, rootSegment)
if err != nil {
errs = append(errs, err)
}
p.From = finalPath
}
switch p.Op {
case Add:
vAdd = append(vAdd, p)
case Move:
vMove = append(vMove, p)
case Remove:
vRemove = append(vRemove, p)
case Replace:
vReplace = append(vReplace, p)
}
}
vPatches := vRemove
vPatches = append(vPatches, vReplace...)
vPatches = append(vPatches, vMove...)
vPatches = append(vPatches, vAdd...)
return vPatches, errs
}
func validatePath(path, op string, root *PathSegment) (string, error) {
finalPath, lastVal, err := traceObjectPathString(path, op, root)
if err != nil {
return finalPath, err
}
// Check for valid operations on "-"
if lastVal.Name == "-" && (op == Remove || op == Test || op == Replace) {
return "", jpatcherror.New("Invalid Operation", jpatcherror.ErrorInvalidOperation, "cannot "+op+" array index of '-'", nil)
}
if !allowedOperation(op, lastVal.SupportedOps) {
return "", jpatcherror.New("Invalid operation", jpatcherror.ErrorInvalidOperation, fmt.Sprintf("supported operations are: %v", lastVal.SupportedOps), nil)
}
return finalPath, nil
}
func validateFrom(path, op string, root *PathSegment) (string, error) {
finalPath, _, err := traceObjectPathString(path, op, root)
if err != nil {
return finalPath, err
}
if finalPath == "-" {
return "", jpatcherror.New("Invalid Operation", jpatcherror.ErrorInvalidOperation, "cannot "+op+" array index of '-'", nil)
}
return finalPath, nil
}
func traceObjectPathString(path string, op string, root *PathSegment) (string, *PathValue, error) {
// get rid of the leading ""
split := strings.Split(path, "/")[1:]
splitLength := len(split)
currentSegment := root
finalPath := ""
var lastPath *PathValue
for i, pathSeg := range split {
pathValue, nextSeg, err := processSegment(currentSegment, pathSeg)
if err != nil {
return "", nil, err
}
if pathValue.Name == "-" && i < splitLength-1 {
return "", nil, jpatcherror.New("Invalid Path", jpatcherror.ErrorInvalidPath, `'-' must be final path segment`, nil)
}
if nextSeg != nil && i == splitLength-1 && nextSeg.Optional == false {
return "", nil, jpatcherror.New("Invalid Path", jpatcherror.ErrorInvalidPath, "required path segment missing", nil)
}
if nextSeg == nil && i < splitLength-1 {
return "", nil, jpatcherror.New("Invalid path", jpatcherror.ErrorInvalidPath, "path reaches undefined segment: "+path, nil)
}
currentSegment = nextSeg
lastPath = pathValue
finalPath += "/" + pathValue.Name
}
return finalPath, lastPath, nil
}
func processSegment(seg *PathSegment, path string) (*PathValue, *PathSegment, error) {
var nextSeg *PathSegment
val := &PathValue{}
if seg.Wildcard {
val.Name = path
if seg.Values["*"] != nil {
val.SupportedOps = seg.Values["*"].SupportedOps
}
path = "*"
} else {
pv, ok := seg.Values[path]
if !ok {
return nil, nil, jpatcherror.New("Invalid path", jpatcherror.ErrorInvalidSegment, "unknown segement: "+path, nil)
}
val = pv
}
if seg.Children != nil && seg.Children[path] != nil {
nextSeg = seg.Children[path]
}
return val, nextSeg, nil
}
func requiredFurtherSegements(c map[string]*PathSegment) bool {
for _, seg := range c {
if !seg.Optional {
return true
}
}
return false
}
func validatePatch(p Patch) error {
if !validOperation(p.Op) {
return jpatcherror.New("Invalid operation", jpatcherror.ErrorInvalidOperation, fmt.Sprintf("supported operations are: %v, %v, %v, %v, %v and %v", Add, Remove, Replace, Copy, Move, Test), p)
}
split := strings.Split(p.Path, "/")[1:]
splitLength := len(split)
if splitLength == 0 || (splitLength == 1 && split[0] == "") {
return jpatcherror.New("Empty Paths Not Supported", jpatcherror.ErrorInvalidPath, "paths must begin with /", p)
}
if p.Op == Copy || p.Op == Move {
split = strings.Split(p.From, "/")[1:]
splitLength = len(split)
if splitLength == 0 || (splitLength == 1 && split[0] == "") {
return jpatcherror.New("From path required", jpatcherror.ErrorInvalidPath, "copy and move operations require from", p)
}
}
if p.Op == Add || p.Op == Replace || p.Op == Test {
if p.Value == nil {
return jpatcherror.New("Value required", jpatcherror.ErrorInvalidValue, "value required for "+p.Op, p)
}
}
return nil
}
func validOperation(op string) bool {
for _, o := range []string{Add, Remove, Replace, Copy, Move, Test} {
if op == o {
return true
}
}
return false
}
func allowedOperation(op string, ops []string) bool {
for _, o := range ops {
if op == o {
return true
}
}
return false
}