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.
@@ -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: []