Class: JSI::JSON::Pointer
- Inherits:
-
Object
- Object
- JSI::JSON::Pointer
- 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
-
#reference_tokens ⇒ Object
readonly
Returns the value of attribute reference_tokens.
Class Method Summary collapse
-
.[](*reference_tokens) ⇒ JSI::JSON::Pointer
instantiates a Pointer from any given reference tokens.
-
.from_fragment(fragment) ⇒ JSI::JSON::Pointer
parse a URI-escaped fragment and instantiate as a JSI::JSON::Pointer.
-
.from_pointer(pointer_string, type: 'pointer') ⇒ JSI::JSON::Pointer
parse a pointer string and instantiate as a JSI::JSON::Pointer.
Instance Method Summary collapse
-
#+(ptr) ⇒ JSI::JSON::Pointer
A pointer with the reference tokens of this one plus the given ptr's.
-
#[](token) ⇒ JSI::JSON::Pointer
appends the given token to this Pointer's reference tokens and returns the result.
-
#contains?(other_ptr) ⇒ Boolean
Does this pointer contain the other_ptr - that is, is this pointer an ancestor of other_ptr, a child pointer.
-
#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.
-
#evaluate(document) ⇒ Object
takes a root json document and evaluates this pointer through the document, returning the value pointed to by this pointer.
-
#fragment ⇒ String
The fragment string representation of this Pointer.
-
#initialize(reference_tokens, type: nil) ⇒ Pointer
constructor
initializes a JSI::JSON::Pointer from the given reference_tokens.
-
#inspect ⇒ String
(also: #to_s)
String representation of this Pointer.
-
#jsi_fingerprint ⇒ Object
pointers are equal if the reference_tokens are equal, regardless of @type.
-
#modified_document_copy(document) {|Object| ... } ⇒ Object
takes a document and a block.
-
#parent ⇒ JSI::JSON::Pointer
Pointer to the parent of where this pointer points.
-
#pointer ⇒ String
The pointer string representation of this Pointer.
-
#ptr_relative_to(ancestor_ptr) ⇒ JSI::JSON::Pointer
Returns this pointer relative to the given ancestor_ptr.
-
#root? ⇒ Boolean
Whether this pointer points to the root (has an empty array of reference_tokens).
-
#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.
-
#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.
-
#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.
-
#take(n) ⇒ JSI::JSON::Pointer
A Pointer consisting of the first n of our reference_tokens.
-
#uri ⇒ Addressable::URI
A URI consisting only of a pointer fragment.
Methods included from Util::FingerprintHash
Constructor Details
#initialize(reference_tokens, type: nil) ⇒ Pointer
initializes a JSI::JSON::Pointer from the given reference_tokens.
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_tokens ⇒ Object (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.
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"]
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"]
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.
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
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.
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.
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.
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 |
#fragment ⇒ String
Returns 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 |
#inspect ⇒ String Also known as: to_s
Returns 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_fingerprint ⇒ Object
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.
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 |
#parent ⇒ JSI::JSON::Pointer
Returns pointer to the parent of where this pointer points.
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 |
#pointer ⇒ String
Returns 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
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).
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.
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.
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.
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.
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 |
#uri ⇒ Addressable::URI
Returns 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 |