json-tools 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/jsontools.rb +9 -0
- data/lib/jsontools/jsontools.rb +397 -0
- metadata +46 -0
data/lib/jsontools.rb
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
##############################################
|
|
2
|
+
# Author: James M Snell (jasnell@gmail.com) #
|
|
3
|
+
# License: Apache v2.0 #
|
|
4
|
+
# #
|
|
5
|
+
# A simple JSON Patch+Pointer+Predicates Impl#
|
|
6
|
+
##############################################
|
|
7
|
+
REQUIRED_VERSION = '1.9.3'
|
|
8
|
+
raise "The jsontools gem currently requires Ruby version #{REQUIRED_VERSION} or higher" if RUBY_VERSION < REQUIRED_VERSION
|
|
9
|
+
require 'jsontools/jsontools'
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
####################################
|
|
2
|
+
# JSON Tools #
|
|
3
|
+
# Implementation of JSON Patch, #
|
|
4
|
+
# Pointer and Predicates #
|
|
5
|
+
# #
|
|
6
|
+
# Author: James M Snell #
|
|
7
|
+
# (jasnell@gmail.com) #
|
|
8
|
+
# License: Apache v2.0 #
|
|
9
|
+
####################################
|
|
10
|
+
|
|
11
|
+
require 'json'
|
|
12
|
+
|
|
13
|
+
class Hash
|
|
14
|
+
# A fairly inefficient means of
|
|
15
|
+
# generating a deep copy of the
|
|
16
|
+
# hash; but it ensures that our
|
|
17
|
+
# hash conforms to the JSON spec
|
|
18
|
+
# and does not contain any cycles
|
|
19
|
+
def json_deep_copy
|
|
20
|
+
JSON.parse to_json
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def insert loc,val
|
|
24
|
+
self[loc] = val
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def delete_at loc
|
|
28
|
+
self.delete loc
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
module JsonTools
|
|
34
|
+
|
|
35
|
+
def self.fix_key obj, key
|
|
36
|
+
if Array === obj
|
|
37
|
+
idx = Integer key
|
|
38
|
+
fail if not (0...obj.length).cover? idx
|
|
39
|
+
key = idx
|
|
40
|
+
end
|
|
41
|
+
key
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class Pointer
|
|
45
|
+
|
|
46
|
+
# Raised when an error occurs during the
|
|
47
|
+
# evaluation of the pointer against a
|
|
48
|
+
# given context
|
|
49
|
+
class PointerError < StandardError; end
|
|
50
|
+
|
|
51
|
+
def initialize path
|
|
52
|
+
@parts = path.split('/').drop(1).map { |p|
|
|
53
|
+
p.gsub(/\~1/, '/').gsub(/\~0/, '~')
|
|
54
|
+
}
|
|
55
|
+
@last = @parts.pop
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns the last segment of the JSON Pointer
|
|
59
|
+
def last; @last; end
|
|
60
|
+
|
|
61
|
+
# Evaluates the pointer against the given
|
|
62
|
+
# context hash object and returns the
|
|
63
|
+
# parent. That is, if the Pointer is
|
|
64
|
+
# "/a/b/c", parent will return the object
|
|
65
|
+
# referenced by "/a/b", or nil if that
|
|
66
|
+
# object does not exist.
|
|
67
|
+
def parent context
|
|
68
|
+
@parts.reduce(context) do |o, p|
|
|
69
|
+
o[(o.is_a?(Array) ? p.to_i : p)]
|
|
70
|
+
end
|
|
71
|
+
rescue
|
|
72
|
+
raise PointerError
|
|
73
|
+
end
|
|
74
|
+
alias :[] :parent
|
|
75
|
+
|
|
76
|
+
# Enumerates down the pointer path, yielding
|
|
77
|
+
# to the given block each name, value pair
|
|
78
|
+
# specified in the path, halting at the first
|
|
79
|
+
# nil value encountered. The required block
|
|
80
|
+
# will be passed two parameters. The first is
|
|
81
|
+
# the accessor name, the second is the value.
|
|
82
|
+
# For instance, given the hash {'a'=>{'b'=>{'c'=>123}}},
|
|
83
|
+
# and the pointer "/a/b/c", the block will be
|
|
84
|
+
# called three times, first with ['a',{'b'=>{'c'=>123}}],
|
|
85
|
+
# next with ['b',{'c'=>123}], and finally with
|
|
86
|
+
# ['c',123].
|
|
87
|
+
def walk context
|
|
88
|
+
p = @parts.reduce(context) do |o,p|
|
|
89
|
+
n = o[(o.is_a?(Array) ? p.to_i : p)]
|
|
90
|
+
yield p, n
|
|
91
|
+
return if NilClass === n # exit the loop if the object is nil
|
|
92
|
+
n
|
|
93
|
+
end
|
|
94
|
+
key = JsonTools.fix_key(p,@last)
|
|
95
|
+
yield key, (!p ? nil : p[key])
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Returns the specific value identified by this
|
|
99
|
+
# pointer, if any. Nil is returned if the path
|
|
100
|
+
# does not exist. Note that this does not differentiate
|
|
101
|
+
# between explicitly null values or missing paths.
|
|
102
|
+
def value context
|
|
103
|
+
parent = parent context
|
|
104
|
+
parent[JsonTools.fix_key(parent,@last)] unless !parent
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Alternative to value that raises a PointerError
|
|
108
|
+
# if the referenced path does not exist.
|
|
109
|
+
def value_with_fail context
|
|
110
|
+
parent = parent context
|
|
111
|
+
fail if !parent
|
|
112
|
+
parent.fetch(JsonTools.fix_key(parent,@last))
|
|
113
|
+
rescue
|
|
114
|
+
raise PointerError
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# True if the referenced path exists
|
|
118
|
+
def exists? context
|
|
119
|
+
p = parent context
|
|
120
|
+
if Array === p
|
|
121
|
+
(0...p.length).cover? Integer(@last)
|
|
122
|
+
else
|
|
123
|
+
p.has_key? @last
|
|
124
|
+
end
|
|
125
|
+
rescue
|
|
126
|
+
false
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class Patch
|
|
132
|
+
|
|
133
|
+
PATCH_OPERATIONS = {}
|
|
134
|
+
|
|
135
|
+
class InvalidPatchDocumentError < StandardError; end
|
|
136
|
+
class FailedOperationError < StandardError; end
|
|
137
|
+
|
|
138
|
+
def initialize ops, with_predicates=false
|
|
139
|
+
# Parse JSON if necessary
|
|
140
|
+
if ops.is_a?(String) || ops.respond_to?(:read)
|
|
141
|
+
ops = JSON.load(ops)
|
|
142
|
+
end
|
|
143
|
+
fail unless Array === ops
|
|
144
|
+
@ops = ops
|
|
145
|
+
# Should we include the JSON Predicate operations?
|
|
146
|
+
# Off by default
|
|
147
|
+
extend Predicate if with_predicates
|
|
148
|
+
rescue
|
|
149
|
+
raise InvalidPatchDocumentError
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Initialize a new Patch object with
|
|
153
|
+
# JSON Predicate Operations enabled
|
|
154
|
+
def self.new_with_predicates ops
|
|
155
|
+
new ops, true
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Apply the patch to the given target hash
|
|
159
|
+
# object. Note that the target will be
|
|
160
|
+
# modified in place and changes will not
|
|
161
|
+
# be reversable in the case of failure.
|
|
162
|
+
def apply_to! target
|
|
163
|
+
@ops.each_with_object(target) do |operation, target|
|
|
164
|
+
op = operation['op'].to_sym if operation.key?('op')
|
|
165
|
+
PATCH_OPERATIONS[op][operation, target] rescue raise 'Invalid Operation'
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Apply the patch to a copy of the given
|
|
170
|
+
# target hash. The new, modified hash
|
|
171
|
+
# will be returned.
|
|
172
|
+
def apply_to target
|
|
173
|
+
apply_to! target.json_deep_copy
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
# Define the various core patch operations
|
|
179
|
+
class << Patch
|
|
180
|
+
|
|
181
|
+
def add params, target
|
|
182
|
+
ptr = Pointer.new params['path']
|
|
183
|
+
fail if ptr.exists? target
|
|
184
|
+
obj = ptr[target]
|
|
185
|
+
fail if not (Array === obj || Hash === obj)
|
|
186
|
+
obj.insert JsonTools.fix_key(obj,ptr.last),params['value']
|
|
187
|
+
rescue
|
|
188
|
+
raise FailedOperationError
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def remove params, target
|
|
192
|
+
ptr = Pointer.new params['path']
|
|
193
|
+
return if not ptr.exists? target #it's gone, just ignore.. TODO: might still need to throw an error, but we'll skip it for now
|
|
194
|
+
obj = ptr[target]
|
|
195
|
+
obj.delete_at JsonTools.fix_key(obj,ptr.last)
|
|
196
|
+
rescue
|
|
197
|
+
raise FailedOperationError
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def move params, target
|
|
201
|
+
move_or_copy params, target, true
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def copy params, target
|
|
205
|
+
move_or_copy params, target, false
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def move_or_copy params, target, move=false
|
|
209
|
+
from = Pointer.new params['path']
|
|
210
|
+
to = Pointer.new params['to']
|
|
211
|
+
fail if !from.exists?(target) || to.exists?(target)
|
|
212
|
+
obj = from[target]
|
|
213
|
+
val = obj[JsonTools.fix_key(obj,from.last)]
|
|
214
|
+
remove(({'path'=>params['path']}), target) if move # we only remove it if we're doing a move operation
|
|
215
|
+
add ({'path'=>params['to'],'value'=>val}), target
|
|
216
|
+
rescue
|
|
217
|
+
raise FailedOperationError
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def replace params, target
|
|
221
|
+
ptr = Pointer.new params['path']
|
|
222
|
+
fail if not ptr.exists? target
|
|
223
|
+
obj = ptr[target]
|
|
224
|
+
obj[JsonTools.fix_key(obj,ptr.last)] = params['value']
|
|
225
|
+
rescue
|
|
226
|
+
raise FailedOperationError
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def test params, target
|
|
230
|
+
ptr = Pointer.new(params['path'])
|
|
231
|
+
fail if not ptr.exists? target
|
|
232
|
+
obj = ptr[target]
|
|
233
|
+
val = obj[JsonTools.fix_key(obj,ptr.last)]
|
|
234
|
+
fail unless val == params['value']
|
|
235
|
+
rescue
|
|
236
|
+
raise FailedOperationError
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
end # END EIGENCLASS DEFINITION
|
|
240
|
+
|
|
241
|
+
# Specify the Patch Operations
|
|
242
|
+
[:add,:remove,:replace,:move,:copy,:test].each { |x| PATCH_OPERATIONS[x] = lambda(&method(x)) }
|
|
243
|
+
|
|
244
|
+
public
|
|
245
|
+
|
|
246
|
+
def register_op sym, op
|
|
247
|
+
PATCH_OPERATIONS[sym] = op
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
end # End Patch Class
|
|
251
|
+
|
|
252
|
+
# Define the Predicate methods for use with the Patch object
|
|
253
|
+
module Predicate
|
|
254
|
+
|
|
255
|
+
def self.string_check params, target, &block
|
|
256
|
+
ptr = Pointer.new params['path']
|
|
257
|
+
return false if !ptr.exists?(target)
|
|
258
|
+
parent, key = ptr[target], ptr.last
|
|
259
|
+
key = JsonTools.fix_key(parent, key)
|
|
260
|
+
val = parent[key]
|
|
261
|
+
return false unless String === val
|
|
262
|
+
ignore_case = params['ignore_case']
|
|
263
|
+
test_val = params['value']
|
|
264
|
+
if ignore_case
|
|
265
|
+
test_val.upcase!
|
|
266
|
+
val.upcase!
|
|
267
|
+
end
|
|
268
|
+
yield val, test_val
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def self.number_check params, target, &block
|
|
272
|
+
ptr = Pointer.new params['path']
|
|
273
|
+
return false if !ptr.exists?(target)
|
|
274
|
+
parent, key = ptr[target], ptr.last
|
|
275
|
+
key = JsonTools.fix_key(parent, key)
|
|
276
|
+
val = parent[key]
|
|
277
|
+
test_val = params['value']
|
|
278
|
+
return false unless (Numeric === val && Numeric === test_val)
|
|
279
|
+
yield val, test_val
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def self.contains params, target
|
|
283
|
+
string_check(params,target) {|x,y| x.include? y }
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def self.defined params, target
|
|
287
|
+
ptr = Pointer.new params['path']
|
|
288
|
+
ptr.exists?(target)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def self.ends params, target
|
|
292
|
+
string_check(params,target) {|x,y| x.end_with? y }
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def self.matches params, target
|
|
296
|
+
ptr = Pointer.new params['path']
|
|
297
|
+
return false if !ptr.exists?(target)
|
|
298
|
+
parent, key = ptr[target], ptr.last
|
|
299
|
+
key = JsonTools.fix_key(parent, key)
|
|
300
|
+
val = parent[key]
|
|
301
|
+
return false unless String === val
|
|
302
|
+
ignore_case = params['ignore_case']
|
|
303
|
+
test_val = params['value']
|
|
304
|
+
regex = ignore_case ? Regexp.new(test_val, Regexp::IGNORECASE) : Regexp.new(test_val)
|
|
305
|
+
regex.match val
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def self.less params, target
|
|
309
|
+
number_check(params,target) {|x,y| x < y}
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def self.more params, target
|
|
313
|
+
number_check(params,target) {|x,y| x > y}
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def self.starts params, target
|
|
317
|
+
string_check(params,target) {|x,y| x.start_with? y }
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def self.type params, target
|
|
321
|
+
ptr = Pointer.new params['path']
|
|
322
|
+
test_val = params['value']
|
|
323
|
+
if !ptr.exists? target
|
|
324
|
+
test_val == 'undefined'
|
|
325
|
+
else
|
|
326
|
+
return false if !ptr.exists?(target)
|
|
327
|
+
val = ptr.value target
|
|
328
|
+
case test_val
|
|
329
|
+
when 'number'
|
|
330
|
+
Numeric === val
|
|
331
|
+
when 'string'
|
|
332
|
+
String === val
|
|
333
|
+
when 'boolean'
|
|
334
|
+
TrueClass === val || FalseClass === val
|
|
335
|
+
when 'object'
|
|
336
|
+
Hash === val
|
|
337
|
+
when 'array'
|
|
338
|
+
Array === val
|
|
339
|
+
when 'null'
|
|
340
|
+
NilClass === val
|
|
341
|
+
else
|
|
342
|
+
false
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def self.undefined params, target
|
|
348
|
+
ptr = Pointer.new params['path']
|
|
349
|
+
!ptr.exists?(target)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def self.and params, target
|
|
353
|
+
preds = params['apply']
|
|
354
|
+
return false unless preds.all? {|pred|
|
|
355
|
+
op = pred['op'].to_sym
|
|
356
|
+
PREDICATES[op][pred,target] rescue return false
|
|
357
|
+
}
|
|
358
|
+
true
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def self.not params, target
|
|
362
|
+
preds = params['apply']
|
|
363
|
+
return false unless preds.none? {|pred|
|
|
364
|
+
op = pred['op'].to_sym
|
|
365
|
+
PREDICATES[op][pred,target] rescue return false
|
|
366
|
+
}
|
|
367
|
+
true
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def self.or params, target
|
|
371
|
+
preds = params['apply']
|
|
372
|
+
return false unless preds.any? {|pred|
|
|
373
|
+
op = pred['op'].to_sym
|
|
374
|
+
PREDICATES[op][pred,target] rescue return false
|
|
375
|
+
}
|
|
376
|
+
true
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
PREDICATES = {}
|
|
380
|
+
[:contains, :defined, :ends, :less,
|
|
381
|
+
:matches, :more, :starts, :type,
|
|
382
|
+
:undefined, :and, :not, :or].each {|x|
|
|
383
|
+
PREDICATES[x] = lambda(&method(x))
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
def self.extended other
|
|
387
|
+
|
|
388
|
+
PREDICATES.each_pair {|x,y|
|
|
389
|
+
other.register_op x, ->(params,target) {
|
|
390
|
+
raise Patch::FailedOperationError unless y.call params,target
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
end # End Module
|
metadata
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: json-tools
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- James M Snell
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2012-10-02 00:00:00.000000000 Z
|
|
13
|
+
dependencies: []
|
|
14
|
+
description: A JSON Patch + Pointer + Predicates Implementation
|
|
15
|
+
email: jasnell@gmail.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- lib/jsontools.rb
|
|
21
|
+
- lib/jsontools/jsontools.rb
|
|
22
|
+
homepage: https://github.com/jasnell/json-tools
|
|
23
|
+
licenses: []
|
|
24
|
+
post_install_message:
|
|
25
|
+
rdoc_options: []
|
|
26
|
+
require_paths:
|
|
27
|
+
- lib
|
|
28
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
29
|
+
none: false
|
|
30
|
+
requirements:
|
|
31
|
+
- - ! '>='
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
35
|
+
none: false
|
|
36
|
+
requirements:
|
|
37
|
+
- - ! '>='
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
requirements: []
|
|
41
|
+
rubyforge_project:
|
|
42
|
+
rubygems_version: 1.8.24
|
|
43
|
+
signing_key:
|
|
44
|
+
specification_version: 3
|
|
45
|
+
summary: JSON Patch + Pointer + Predicates
|
|
46
|
+
test_files: []
|