Class: JSI::JSON::Pointer

Inherits:
Object
  • Object
show all
Includes:
Util::FingerprintHash
Defined in:
lib/jsi/json/pointer.rb

Overview

a JSON Pointer, as described by RFC 6901 https://tools.ietf.org/html/rfc6901

Defined Under Namespace

Classes: Error, PointerSyntaxError, ReferenceError

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Util::FingerprintHash

#==, #hash

Constructor Details

#initialize(reference_tokens, type: nil) ⇒ Pointer

initializes a JSI::JSON::Pointer from the given reference_tokens.

Parameters:

  • reference_tokens (Array<Object>)
  • type (String, Symbol) (defaults to: nil)

    one of 'pointer' or 'fragment'



86
87
88
89
90
91
92
# File 'lib/jsi/json/pointer.rb', line 86

def initialize(reference_tokens, type: nil)
  unless reference_tokens.respond_to?(:to_ary)
    raise(TypeError, "reference_tokens must be an array. got: #{reference_tokens.inspect}")
  end
  @reference_tokens = reference_tokens.to_ary.map(&:freeze).freeze
  @type = type.is_a?(Symbol) ? type.to_s : type
end

Instance Attribute Details

#reference_tokensObject (readonly)

Returns the value of attribute reference_tokens.



94
95
96
# File 'lib/jsi/json/pointer.rb', line 94

def reference_tokens
  @reference_tokens
end

Class Method Details

.[](*reference_tokens) ⇒ JSI::JSON::Pointer

instantiates a Pointer from any given reference tokens.

>> JSI::JSON::Pointer[]
=> #<JSI::JSON::Pointer reference_tokens: []>
>> JSI::JSON::Pointer['a', 'b']
=> #<JSI::JSON::Pointer reference_tokens: ["a", "b"]>
>> JSI::JSON::Pointer['a']['b']
=> #<JSI::JSON::Pointer reference_tokens: ["a", "b"]>

note in the last example that you can conveniently chain the class .[] method with the instance #[] method.

Parameters:

  • *reference_tokens

    any number of reference tokens

Returns:



28
29
30
# File 'lib/jsi/json/pointer.rb', line 28

def self.[](*reference_tokens)
  new(reference_tokens)
end

.from_fragment(fragment) ⇒ JSI::JSON::Pointer

parse a URI-escaped fragment and instantiate as a JSI::JSON::Pointer

ptr = JSI::JSON::Pointer.from_fragment('/foo/bar')
=> #<JSI::JSON::Pointer fragment: /foo/bar>
ptr.reference_tokens
=> ["foo", "bar"]

with URI escaping:

ptr = JSI::JSON::Pointer.from_fragment('/foo%20bar')
=> #<JSI::JSON::Pointer fragment: /foo%20bar>
ptr.reference_tokens
=> ["foo bar"]

Parameters:

  • fragment (String)

    a fragment containing a pointer (starting with #)

Returns:

Raises:



49
50
51
# File 'lib/jsi/json/pointer.rb', line 49

def self.from_fragment(fragment)
  from_pointer(Addressable::URI.unescape(fragment), type: 'fragment')
end

.from_pointer(pointer_string, type: 'pointer') ⇒ JSI::JSON::Pointer

parse a pointer string and instantiate as a JSI::JSON::Pointer

ptr1 = JSI::JSON::Pointer.from_pointer('/foo')
=> #<JSI::JSON::Pointer pointer: /foo>
ptr1.reference_tokens
=> ["foo"]

ptr2 = JSI::JSON::Pointer.from_pointer('/foo~0bar/baz~1qux')
=> #<JSI::JSON::Pointer pointer: /foo~0bar/baz~1qux>
ptr2.reference_tokens
=> ["foo~bar", "baz/qux"]

Parameters:

  • pointer_string (String)

    a pointer string

  • type (for internal use) (defaults to: 'pointer')

    indicates the original representation of the pointer

Returns:

Raises:



69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/jsi/json/pointer.rb', line 69

def self.from_pointer(pointer_string, type: 'pointer')
  tokens = pointer_string.split('/', -1).map! do |piece|
    piece.gsub('~1', '/').gsub('~0', '~')
  end
  if tokens[0] == ''
    new(tokens[1..-1], type: type)
  elsif tokens.empty?
    new(tokens, type: type)
  else
    raise(PointerSyntaxError, "Invalid pointer syntax in #{pointer_string.inspect}: pointer must begin with /")
  end
end

Instance Method Details

#+(ptr) ⇒ JSI::JSON::Pointer

Returns a pointer with the reference tokens of this one plus the given ptr's.

Parameters:

Returns:

  • (JSI::JSON::Pointer)

    a pointer with the reference tokens of this one plus the given ptr's.



174
175
176
177
178
179
# File 'lib/jsi/json/pointer.rb', line 174

def +(ptr)
  unless ptr.is_a?(JSI::JSON::Pointer)
    raise(TypeError, "ptr must be a JSI::JSON::Pointer; got: #{ptr.inspect}")
  end
  Pointer.new(reference_tokens + ptr.reference_tokens, type: @type)
end

#[](token) ⇒ JSI::JSON::Pointer

appends the given token to this Pointer's reference tokens and returns the result

Parameters:

  • token (Object)

Returns:



195
196
197
# File 'lib/jsi/json/pointer.rb', line 195

def [](token)
  Pointer.new(reference_tokens + [token], type: @type)
end

#contains?(other_ptr) ⇒ Boolean

Returns does this pointer contain the other_ptr - that is, is this pointer an ancestor of other_ptr, a child pointer. contains? is inclusive; a pointer does contain itself.

Returns:

  • (Boolean)

    does this pointer contain the other_ptr - that is, is this pointer an ancestor of other_ptr, a child pointer. contains? is inclusive; a pointer does contain itself.



159
160
161
# File 'lib/jsi/json/pointer.rb', line 159

def contains?(other_ptr)
  self.reference_tokens == other_ptr.reference_tokens[0...self.reference_tokens.size]
end

#deref(document) {|Pointer| ... } ⇒ Pointer

if this Pointer points at a $ref node within the given document, #deref attempts to follow that $ref and return a Pointer to the referenced location. otherwise, this Pointer is returned.

if the content this Pointer points to in the document is not hash-like, does not have a $ref property, its $ref cannot be found, or its $ref points outside the document, this pointer is returned.

Parameters:

  • document (Object)

    the document this pointer applies to

Yields:

  • (Pointer)

    if a block is given (optional), this will yield a deref'd pointer. if this pointer does not point to a $ref object in the given document, the block is not called. if we point to a $ref which cannot be followed (e.g. a $ref to an external document, which is not yet supported), the block is not called.

Returns:

  • (Pointer)

    dereferenced pointer, or this pointer



376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# File 'lib/jsi/json/pointer.rb', line 376

def deref(document, &block)
  block ||= Util::NOOP
  content = evaluate(document)

  if content.respond_to?(:to_hash)
    ref = (content.respond_to?(:[]) ? content : content.to_hash)['$ref']
  end
  return self unless ref.is_a?(String)

  if ref[/\A#/]
    return Pointer.from_fragment(Addressable::URI.parse(ref).fragment).tap(&block)
  end

  # HAX for how google does refs and ids
  if document['schemas'].respond_to?(:to_hash)
    if document['schemas'][ref]
      return Pointer.new(['schemas', ref], type: 'hax').tap(&block)
    end
    document['schemas'].each do |k, schema|
      if schema['id'] == ref
        return Pointer.new(['schemas', k], type: 'hax').tap(&block)
      end
    end
  end

  #raise(NotImplementedError, "cannot dereference #{ref}") # TODO
  return self
end

#evaluate(document) ⇒ Object

takes a root json document and evaluates this pointer through the document, returning the value pointed to by this pointer.

Parameters:

  • document (#to_ary, #to_hash)

    the document against which we will evaluate this pointer

Returns:

  • (Object)

    the content of the document pointed to by this pointer

Raises:



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/jsi/json/pointer.rb', line 102

def evaluate(document)
  res = reference_tokens.inject(document) do |value, token|
    if value.respond_to?(:to_ary)
      if token.is_a?(String) && token =~ /\A\d|[1-9]\d+\z/
        token = token.to_i
      end
      unless token.is_a?(Integer)
        raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not an integer and cannot be resolved in array #{value.inspect}")
      end
      unless (0...(value.respond_to?(:size) ? value : value.to_ary).size).include?(token)
        raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid index of #{value.inspect}")
      end
      (value.respond_to?(:[]) ? value : value.to_ary)[token]
    elsif value.respond_to?(:to_hash)
      unless (value.respond_to?(:key?) ? value : value.to_hash).key?(token)
        raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid key of #{value.inspect}")
      end
      (value.respond_to?(:[]) ? value : value.to_hash)[token]
    else
      raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} cannot be resolved in #{value.inspect}")
    end
  end
  res
end

#fragmentString

Returns the fragment string representation of this Pointer.

Returns:

  • (String)

    the fragment string representation of this Pointer



133
134
135
# File 'lib/jsi/json/pointer.rb', line 133

def fragment
  Addressable::URI.escape(pointer)
end

#inspectString Also known as: to_s

Returns string representation of this Pointer.

Returns:

  • (String)

    string representation of this Pointer



406
407
408
# File 'lib/jsi/json/pointer.rb', line 406

def inspect
  "#{self.class.name}[#{reference_tokens.map(&:inspect).join(", ")}]"
end

#jsi_fingerprintObject

pointers are equal if the reference_tokens are equal, regardless of @type



413
414
415
# File 'lib/jsi/json/pointer.rb', line 413

def jsi_fingerprint
  {class: JSI::JSON::Pointer, reference_tokens: reference_tokens}
end

#modified_document_copy(document) {|Object| ... } ⇒ Object

takes a document and a block. the block is yielded the content of the given document at this pointer's location. the block must result a modified copy of that content (and MUST NOT modify the object it is given). this modified copy of that content is incorporated into a modified copy of the given document, which is then returned. the structure and contents of the document outside the path pointed to by this pointer is not modified.

Parameters:

  • document (Object)

    the document to apply this pointer to

Yields:

  • (Object)

    the content this pointer applies to in the given document the block must result in the new content which will be placed in the modified document copy.

Returns:

  • (Object)

    a copy of the given document, with the content this pointer applies to replaced by the result of the block



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/jsi/json/pointer.rb', line 314

def modified_document_copy(document, &block)
  # we need to preserve the rest of the document, but modify the content at our path.
  #
  # this is actually a bit tricky. we can't modify the original document, obviously.
  # we could do a deep copy, but that's expensive. instead, we make a copy of each array
  # or hash in the path above this node. this node's content is modified by the caller, and
  # that is recursively merged up to the document root. the recursion is done with a
  # y combinator, for no other reason than that was a fun way to implement it.
  modified_document = JSI::Util.ycomb do |rec|
    proc do |subdocument, subpath|
      if subpath == []
        Typelike.modified_copy(subdocument, &block)
      else
        car = subpath[0]
        cdr = subpath[1..-1]
        if subdocument.respond_to?(:to_hash)
          subdocument_car = (subdocument.respond_to?(:[]) ? subdocument : subdocument.to_hash)[car]
          car_object = rec.call(subdocument_car, cdr)
          if car_object.object_id == subdocument_car.object_id
            subdocument
          else
            (subdocument.respond_to?(:merge) ? subdocument : subdocument.to_hash).merge({car => car_object})
          end
        elsif subdocument.respond_to?(:to_ary)
          if car.is_a?(String) && car =~ /\A\d+\z/
            car = car.to_i
          end
          unless car.is_a?(Integer)
            raise(TypeError, "bad subscript #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for array: #{subdocument.pretty_inspect.chomp}")
          end
          subdocument_car = (subdocument.respond_to?(:[]) ? subdocument : subdocument.to_ary)[car]
          car_object = rec.call(subdocument_car, cdr)
          if car_object.object_id == subdocument_car.object_id
            subdocument
          else
            (subdocument.respond_to?(:[]=) ? subdocument : subdocument.to_ary).dup.tap do |arr|
              arr[car] = car_object
            end
          end
        else
          raise(TypeError, "bad subscript: #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for content: #{subdocument.pretty_inspect.chomp}")
        end
      end
    end
  end.call(document, reference_tokens)
  modified_document
end

#parentJSI::JSON::Pointer

Returns pointer to the parent of where this pointer points.

Returns:

Raises:



149
150
151
152
153
154
155
# File 'lib/jsi/json/pointer.rb', line 149

def parent
  if root?
    raise(ReferenceError, "cannot access parent of root pointer: #{pretty_inspect.chomp}")
  else
    Pointer.new(reference_tokens[0...-1], type: @type)
  end
end

#pointerString

Returns the pointer string representation of this Pointer.

Returns:

  • (String)

    the pointer string representation of this Pointer



128
129
130
# File 'lib/jsi/json/pointer.rb', line 128

def pointer
  reference_tokens.map { |t| '/' + t.to_s.gsub('~', '~0').gsub('/', '~1') }.join('')
end

#ptr_relative_to(ancestor_ptr) ⇒ JSI::JSON::Pointer

Returns this pointer relative to the given ancestor_ptr

Returns:

Raises:



165
166
167
168
169
170
# File 'lib/jsi/json/pointer.rb', line 165

def ptr_relative_to(ancestor_ptr)
  unless ancestor_ptr.contains?(self)
    raise(ReferenceError, "ancestor_ptr #{ancestor_ptr.inspect} is not ancestor of #{inspect}")
  end
  Pointer.new(reference_tokens[ancestor_ptr.reference_tokens.size..-1], type: @type)
end

#root?Boolean

Returns whether this pointer points to the root (has an empty array of reference_tokens).

Returns:

  • (Boolean)

    whether this pointer points to the root (has an empty array of reference_tokens)



143
144
145
# File 'lib/jsi/json/pointer.rb', line 143

def root?
  reference_tokens.empty?
end

#schema_match_ptrs_to_instance(document, instance) ⇒ JSI::JSON::Pointer

given this Pointer points to a schema in the given document, this matches any applicators of the schema (oneOf, anyOf, allOf, $ref) which should be applied and returns them as a set of pointers.

Parameters:

  • document (#to_hash, #to_ary, Object)

    document containing the schema this pointer points to

  • instance (Object)

    the instance to check any applicators against

Returns:

  • (JSI::JSON::Pointer)

    either a pointer to a *Of subschema in the document, or self if no other subschema was matched



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
297
298
299
300
301
# File 'lib/jsi/json/pointer.rb', line 262

def schema_match_ptrs_to_instance(document, instance)
  ptr = self
  schema = ptr.evaluate(document)

  Set.new.tap do |ptrs|
    if schema.respond_to?(:to_hash)
      if schema['$ref'].respond_to?(:to_str)
        ptr.deref(document) do |deref_ptr|
          ptrs.merge(deref_ptr.schema_match_ptrs_to_instance(document, instance))
        end
      else
        ptrs << ptr
      end
      if schema['allOf'].respond_to?(:to_ary)
        schema['allOf'].each_index do |i|
          ptrs.merge(ptr['allOf'][i].schema_match_ptrs_to_instance(document, instance))
        end
      end
      if schema['anyOf'].respond_to?(:to_ary)
        schema['anyOf'].each_index do |i|
          valid = ::JSON::Validator.validate(JSI::Typelike.as_json(document), JSI::Typelike.as_json(instance), fragment: ptr['anyOf'][i].fragment)
          if valid
            ptrs.merge(ptr['anyOf'][i].schema_match_ptrs_to_instance(document, instance))
          end
        end
      end
      if schema['oneOf'].respond_to?(:to_ary)
        one_i = schema['oneOf'].each_index.detect do |i|
          ::JSON::Validator.validate(JSI::Typelike.as_json(document), JSI::Typelike.as_json(instance), fragment: ptr['oneOf'][i].fragment)
        end
        if one_i
          ptrs.merge(ptr['oneOf'][one_i].schema_match_ptrs_to_instance(document, instance))
        end
      end
      # TODO dependencies
    else
      ptrs << ptr
    end
  end
end

#schema_subschema_ptrs_for_index(document, idx) ⇒ Set<JSI::JSON::Pointer>

given this Pointer points to a schema in the given document, returns a set of pointers to subschemas of that schema for the given array index.

Parameters:

  • document (#to_hash, #to_ary, Object)

    document containing the schema this pointer points to

  • idx (Object)

    the array index for which to find subschemas

Returns:



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/jsi/json/pointer.rb', line 236

def schema_subschema_ptrs_for_index(document, idx)
  ptr = self
  schema = ptr.evaluate(document)
  Set.new.tap do |ptrs|
    if schema.respond_to?(:to_hash)
      if schema['items'].respond_to?(:to_ary)
        if schema['items'].each_index.to_a.include?(idx)
          ptrs << ptr['items'][idx]
        elsif schema.key?('additionalItems')
          ptrs << ptr['additionalItems']
        end
      elsif schema.key?('items')
        ptrs << ptr['items']
      end
    end
  end
end

#schema_subschema_ptrs_for_property_name(document, property_name) ⇒ Set<JSI::JSON::Pointer>

given this Pointer points to a schema in the given document, returns a set of pointers to subschemas of that schema for the given property name.

Parameters:

  • document (#to_hash, #to_ary, Object)

    document containing the schema this pointer points to

  • property_name (Object)

    the property name for which to find a subschema

Returns:



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/jsi/json/pointer.rb', line 205

def schema_subschema_ptrs_for_property_name(document, property_name)
  ptr = self
  schema = ptr.evaluate(document)
  Set.new.tap do |ptrs|
    if schema.respond_to?(:to_hash)
      apply_additional = true
      if schema.key?('properties') && schema['properties'].respond_to?(:to_hash) && schema['properties'].key?(property_name)
        apply_additional = false
        ptrs << ptr['properties'][property_name]
      end
      if schema['patternProperties'].respond_to?(:to_hash)
        schema['patternProperties'].each_key do |pattern|
          if property_name.to_s =~ Regexp.new(pattern) # TODO map pattern to ruby syntax
            apply_additional = false
            ptrs << ptr['patternProperties'][pattern]
          end
        end
      end
      if apply_additional && schema.key?('additionalProperties')
        ptrs << ptr['additionalProperties']
      end
    end
  end
end

#take(n) ⇒ JSI::JSON::Pointer

Returns a Pointer consisting of the first n of our reference_tokens.

Parameters:

  • n (Integer)

Returns:

Raises:

  • (ArgumentError)

    if n is not between 0 and the size of our reference_tokens



184
185
186
187
188
189
# File 'lib/jsi/json/pointer.rb', line 184

def take(n)
  unless (0..reference_tokens.size).include?(n)
    raise(ArgumentError, "n not in range (0..#{reference_tokens.size}): #{n.inspect}")
  end
  Pointer.new(reference_tokens.take(n), type: @type)
end

#uriAddressable::URI

Returns a URI consisting only of a pointer fragment.

Returns:

  • (Addressable::URI)

    a URI consisting only of a pointer fragment



138
139
140
# File 'lib/jsi/json/pointer.rb', line 138

def uri
  Addressable::URI.new(fragment: fragment)
end