json-tools 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []