scorpio 0.1.0 → 0.2.0

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/scorpio.rb CHANGED
@@ -8,7 +8,11 @@ module Scorpio
8
8
  def self.root
9
9
  @root ||= Pathname.new(__FILE__).dirname.parent.expand_path
10
10
  end
11
+ end
12
+
13
+ require "scorpio/util"
11
14
 
15
+ module Scorpio
12
16
  # generally put in code paths that are not expected to be valid control flow paths.
13
17
  # rather a NotImplementedCorrectlyError. but that's too long.
14
18
  class Bug < NotImplementedError
@@ -17,78 +21,74 @@ module Scorpio
17
21
  proc { |v| define_singleton_method(:error_classes_by_status) { v } }.call({})
18
22
  class Error < StandardError; end
19
23
  class HTTPError < Error
20
- define_singleton_method(:status) { |status| Scorpio.error_classes_by_status[status] = self }
21
- attr_accessor :response
22
- end
23
- class ClientError < HTTPError; end
24
- class ServerError < HTTPError; end
25
-
26
- class BadRequest400Error < ClientError; status(400); end
27
- class Unauthorized401Error < ClientError; status(401); end
28
- class PaymentRequired402Error < ClientError; status(402); end
29
- class Forbidden403Error < ClientError; status(403); end
30
- class NotFound404Error < ClientError; status(404); end
31
- class MethodNotAllowed405Error < ClientError; status(405); end
32
- class NotAcceptable406Error < ClientError; status(406); end
33
- class ProxyAuthenticationRequired407Error < ClientError; status(407); end
34
- class RequestTimeout408Error < ClientError; status(408); end
35
- class Conflict409Error < ClientError; status(409); end
36
- class Gone410Error < ClientError; status(410); end
37
- class LengthRequired411Error < ClientError; status(411); end
38
- class PreconditionFailed412Error < ClientError; status(412); end
39
- class PayloadTooLarge413Error < ClientError; status(413); end
40
- class URITooLong414Error < ClientError; status(414); end
41
- class UnsupportedMediaType415Error < ClientError; status(415); end
42
- class RangeNotSatisfiable416Error < ClientError; status(416); end
43
- class ExpectationFailed417Error < ClientError; status(417); end
44
- class ImaTeapot418Error < ClientError; status(418); end
45
- class MisdirectedRequest421Error < ClientError; status(421); end
46
- class UnprocessableEntity422Error < ClientError; status(422); end
47
- class Locked423Error < ClientError; status(423); end
48
- class FailedDependency424Error < ClientError; status(424); end
49
- class UpgradeRequired426Error < ClientError; status(426); end
50
- class PreconditionRequired428Error < ClientError; status(428); end
51
- class TooManyRequests429Error < ClientError; status(429); end
52
- class RequestHeaderFieldsTooLarge431Error < ClientError; status(431); end
53
- class UnavailableForLegalReasons451Error < ClientError; status(451); end
54
-
55
- class InternalServerError500Error < ServerError; status(500); end
56
- class NotImplemented501Error < ServerError; status(501); end
57
- class BadGateway502Error < ServerError; status(502); end
58
- class ServiceUnavailable503Error < ServerError; status(503); end
59
- class GatewayTimeout504Error < ServerError; status(504); end
60
- class HTTPVersionNotSupported505Error < ServerError; status(505); end
61
- class VariantAlsoNegotiates506Error < ServerError; status(506); end
62
- class InsufficientStorage507Error < ServerError; status(507); end
63
- class LoopDetected508Error < ServerError; status(508); end
64
- class NotExtended510Error < ServerError; status(510); end
65
- class NetworkAuthenticationRequired511Error < ServerError; status(511); end
66
- error_classes_by_status.freeze
67
-
68
- autoload :Model, 'scorpio/model'
69
- autoload :OpenAPI, 'scorpio/openapi'
70
- autoload :Google, 'scorpio/google_api_document'
71
- autoload :JSON, 'scorpio/json'
72
- autoload :Schema, 'scorpio/schema'
73
-
74
- class << self
75
- def stringify_symbol_keys(hash)
76
- unless hash.is_a?(Hash)
77
- raise ArgumentError, "expected argument to be a Hash; got #{hash.class}: #{hash.pretty_inspect}"
24
+ define_singleton_method(:status) do |status = nil|
25
+ if status
26
+ @status = status
27
+ Scorpio.error_classes_by_status[status] = self
28
+ else
29
+ @status
78
30
  end
79
- hash.map { |k,v| {k.is_a?(Symbol) ? k.to_s : k => v} }.inject({}, &:update)
80
31
  end
32
+ attr_accessor :faraday_response, :response_object
81
33
  end
34
+ # HTTP Error classes' canonical names are like Scorpio::HTTPErrors::BadRequest400Error, but can
35
+ # be referred to like Scorpio::BadRequest400Error. this is just to avoid clutter in the Scorpio
36
+ # namespace in yardoc.
37
+ module HTTPErrors
38
+ class ClientError < HTTPError; end
39
+ class ServerError < HTTPError; end
82
40
 
83
- module FingerprintHash
84
- def ==(other)
85
- other.respond_to?(:fingerprint) && other.fingerprint == self.fingerprint
86
- end
87
-
88
- alias eql? ==
41
+ class BadRequest400Error < ClientError; status(400); end
42
+ class Unauthorized401Error < ClientError; status(401); end
43
+ class PaymentRequired402Error < ClientError; status(402); end
44
+ class Forbidden403Error < ClientError; status(403); end
45
+ class NotFound404Error < ClientError; status(404); end
46
+ class MethodNotAllowed405Error < ClientError; status(405); end
47
+ class NotAcceptable406Error < ClientError; status(406); end
48
+ class ProxyAuthenticationRequired407Error < ClientError; status(407); end
49
+ class RequestTimeout408Error < ClientError; status(408); end
50
+ class Conflict409Error < ClientError; status(409); end
51
+ class Gone410Error < ClientError; status(410); end
52
+ class LengthRequired411Error < ClientError; status(411); end
53
+ class PreconditionFailed412Error < ClientError; status(412); end
54
+ class PayloadTooLarge413Error < ClientError; status(413); end
55
+ class URITooLong414Error < ClientError; status(414); end
56
+ class UnsupportedMediaType415Error < ClientError; status(415); end
57
+ class RangeNotSatisfiable416Error < ClientError; status(416); end
58
+ class ExpectationFailed417Error < ClientError; status(417); end
59
+ class ImaTeapot418Error < ClientError; status(418); end
60
+ class MisdirectedRequest421Error < ClientError; status(421); end
61
+ class UnprocessableEntity422Error < ClientError; status(422); end
62
+ class Locked423Error < ClientError; status(423); end
63
+ class FailedDependency424Error < ClientError; status(424); end
64
+ class UpgradeRequired426Error < ClientError; status(426); end
65
+ class PreconditionRequired428Error < ClientError; status(428); end
66
+ class TooManyRequests429Error < ClientError; status(429); end
67
+ class RequestHeaderFieldsTooLarge431Error < ClientError; status(431); end
68
+ class UnavailableForLegalReasons451Error < ClientError; status(451); end
89
69
 
90
- def hash
91
- fingerprint.hash
92
- end
70
+ class InternalServerError500Error < ServerError; status(500); end
71
+ class NotImplemented501Error < ServerError; status(501); end
72
+ class BadGateway502Error < ServerError; status(502); end
73
+ class ServiceUnavailable503Error < ServerError; status(503); end
74
+ class GatewayTimeout504Error < ServerError; status(504); end
75
+ class HTTPVersionNotSupported505Error < ServerError; status(505); end
76
+ class VariantAlsoNegotiates506Error < ServerError; status(506); end
77
+ class InsufficientStorage507Error < ServerError; status(507); end
78
+ class LoopDetected508Error < ServerError; status(508); end
79
+ class NotExtended510Error < ServerError; status(510); end
80
+ class NetworkAuthenticationRequired511Error < ServerError; status(511); end
93
81
  end
82
+ include HTTPErrors
83
+ error_classes_by_status.freeze
84
+
85
+ autoload :JSON, 'scorpio/json'
86
+ autoload :Google, 'scorpio/google_api_document'
87
+ autoload :OpenAPI, 'scorpio/openapi'
88
+ autoload :Typelike, 'scorpio/typelike_modules'
89
+ autoload :Hashlike, 'scorpio/typelike_modules'
90
+ autoload :Arraylike, 'scorpio/typelike_modules'
91
+ autoload :ResourceBase, 'scorpio/resource_base'
92
+ autoload :Schema, 'scorpio/schema'
93
+ autoload :SchemaInstanceBase, 'scorpio/schema_instance_base'
94
94
  end
@@ -1,29 +1,27 @@
1
1
  require 'api_hammer/ycomb'
2
- require 'scorpio/schema_object_base'
2
+ require 'scorpio/schema_instance_base'
3
3
 
4
4
  module Scorpio
5
5
  module Google
6
- apidoc_schema_doc = ::JSON.parse(Scorpio.root.join('documents/www.googleapis.com/discovery/v1/apis/discovery/v1/rest').read)
7
- api_document_class = proc do |*key|
8
- Scorpio.class_for_schema(Scorpio::JSON::Node.new_by_type(apidoc_schema_doc, ['schemas', *key]))
9
- end
6
+ discovery_rest_description_doc = Scorpio::JSON::Node.new_by_type(::JSON.parse(Scorpio.root.join('documents/www.googleapis.com/discovery/v1/apis/discovery/v1/rest').read), [])
10
7
 
11
- # naming these is not strictly necessary, but is nice to have.
12
- # generated: puts Scorpio::Google::ApiDocument.document['schemas'].select { |k,v| v['type'] == 'object' }.keys.map { |k| "#{k[0].upcase}#{k[1..-1]} = api_document_class.call('#{k}')" }
13
- DirectoryList = api_document_class.call('DirectoryList')
14
- JsonSchema = api_document_class.call('JsonSchema')
15
- RestDescription = api_document_class.call('RestDescription')
16
- RestMethod = api_document_class.call('RestMethod')
17
- RestResource = api_document_class.call('RestResource')
8
+ discovery_metaschema = discovery_rest_description_doc['schemas']['JsonSchema']
9
+ rest_description_schema = Scorpio.class_for_schema(discovery_metaschema).new(discovery_rest_description_doc['schemas']['RestDescription'])
10
+ discovery_rest_description = Scorpio.class_for_schema(rest_description_schema).new(discovery_rest_description_doc)
18
11
 
19
- # not generated
20
- RestMethodRequest = api_document_class.call('RestMethod', 'properties', 'request')
21
- RestMethodResponse = api_document_class.call('RestMethod', 'properties', 'response')
12
+ # naming these is not strictly necessary, but is nice to have.
13
+ DirectoryList = Scorpio.class_for_schema(discovery_rest_description['schemas']['DirectoryList'])
14
+ JsonSchema = Scorpio.class_for_schema(discovery_rest_description['schemas']['JsonSchema'])
15
+ RestDescription = Scorpio.class_for_schema(discovery_rest_description['schemas']['RestDescription'])
16
+ RestMethod = Scorpio.class_for_schema(discovery_rest_description['schemas']['RestMethod'])
17
+ RestResource = Scorpio.class_for_schema(discovery_rest_description['schemas']['RestResource'])
18
+ RestMethodRequest = Scorpio.class_for_schema(discovery_rest_description['schemas']['RestMethod']['properties']['request'])
19
+ RestMethodResponse = Scorpio.class_for_schema(discovery_rest_description['schemas']['RestMethod']['properties']['response'])
22
20
 
23
21
  # google does a weird thing where it defines a schema with a $ref property where a json-schema is to be used in the document (method request and response fields), instead of just setting the schema to be the json-schema schema. we'll share a module across those schema classes that really represent schemas. is this confusingly meta enough?
24
22
  module SchemaLike
25
23
  def to_openapi
26
- dup_doc = ::JSON.parse(::JSON.generate(object.content))
24
+ dup_doc = ::JSON.parse(::JSON.generate(instance.content))
27
25
  # openapi does not want an id field on schemas
28
26
  dup_doc.delete('id')
29
27
  if dup_doc['properties'].is_a?(Hash)
@@ -45,12 +43,12 @@ module Scorpio
45
43
 
46
44
  class RestDescription
47
45
  def to_openapi_document(options = {})
48
- Scorpio::OpenAPI::Document.new(to_openapi_hash(options))
46
+ Scorpio::OpenAPI::V2::Document.new(to_openapi_hash(options))
49
47
  end
50
48
 
51
49
  def to_openapi_hash(options = {})
52
50
  # we will be modifying the api document (RestDescription). clone self and modify that one.
53
- ad = self.class.new(::JSON.parse(::JSON.generate(object.document)))
51
+ ad = self.class.new(::JSON.parse(::JSON.generate(instance.document)))
54
52
  ad_methods = []
55
53
  if ad['methods']
56
54
  ad_methods += ad['methods'].map do |mn, m|
@@ -80,13 +78,9 @@ module Scorpio
80
78
  method = http_method_methods.first
81
79
  unused_path_params = Addressable::Template.new(path).variables
82
80
  {http_method.downcase => {}.tap do |operation|
83
- #operation['tags'] = []
81
+ operation['tags'] = method.resource_name ? [method.resource_name] : []
84
82
  #operation['summary'] =
85
83
  operation['description'] = method['description'] if method['description']
86
- if method.resource_name && options[:x]
87
- operation['x-resource'] = method.resource_name
88
- operation['x-resource-method'] = method.method_name
89
- end
90
84
  #operation['externalDocs'] =
91
85
  operation['operationId'] = method['id'] || (method.resource_name ? "#{method.resource_name}.#{method.method_name}" : method.method_name)
92
86
  #operation['produces'] =
@@ -186,7 +180,7 @@ module Scorpio
186
180
  proc do |toopenapiobject|
187
181
  toopenapiobject = toopenapiobject.to_openapi if toopenapiobject.respond_to?(:to_openapi)
188
182
  if toopenapiobject.respond_to?(:to_hash)
189
- toopenapiobject.map { |k, v| {toopenapirec.call(k) => toopenapirec.call(v)} }.inject({}, &:update)
183
+ toopenapiobject.map { |k2, v2| {toopenapirec.call(k2) => toopenapirec.call(v2)} }.inject({}, &:update)
190
184
  elsif toopenapiobject.respond_to?(:to_ary)
191
185
  toopenapiobject.map(&toopenapirec)
192
186
  elsif toopenapiobject.is_a?(Symbol)
@@ -116,7 +116,7 @@ module JSON
116
116
  end
117
117
 
118
118
  def to_s
119
- "#<#{self.class.name} #{@type} = #{representation_s}>"
119
+ "#<#{self.class.inspect} #{@type} = #{representation_s}>"
120
120
  end
121
121
 
122
122
  private
@@ -1,8 +1,20 @@
1
- require 'scorpio/typelike_modules'
2
-
3
1
  module Scorpio
4
2
  module JSON
3
+ # Scorpio::JSON::Node is an abstraction of a node within a JSON document.
4
+ # it aims to act like the underlying data type of the node's content
5
+ # (Hash or Array, generally) in most cases, defining methods of Hash
6
+ # and Array which delegate to the content. However, destructive methods
7
+ # are not defined, as modifying the content of a node would change it
8
+ # for any other nodes in the document that contain or refer to it.
9
+ #
10
+ # methods that return a modified copy such as #merge are defined, and
11
+ # return a copy of the document with the content of the node modified.
12
+ # the original node's document and content are untouched.
5
13
  class Node
14
+ # if the content of the document at the given path is a Hash, returns
15
+ # a HashNode; if an Array, returns ArrayNode. otherwise returns a
16
+ # regular Node, though, for the most part this will be called with Hash
17
+ # or Array content.
6
18
  def self.new_by_type(document, path)
7
19
  node = Node.new(document, path)
8
20
  content = node.content
@@ -15,17 +27,22 @@ module Scorpio
15
27
  end
16
28
  end
17
29
 
30
+ # a Node represents the content of a document at a given path.
18
31
  def initialize(document, path)
19
- raise(ArgumentError, "path must be an array. got: #{path.pretty_inspect} (#{path.class})") unless path.is_a?(Array)
20
- define_singleton_method(:document) { document }
32
+ raise(ArgumentError, "path must be an array. got: #{path.pretty_inspect.chomp} (#{path.class})") unless path.is_a?(Array)
33
+ @document = document
21
34
  @path = path.dup.freeze
22
35
  @pointer = ::JSON::Schema::Pointer.new(:reference_tokens, path)
23
36
  end
24
37
 
38
+ # the path of this Node within its document
25
39
  attr_reader :path
40
+ # the document containing this Node at is path
26
41
  attr_reader :document
42
+ # ::JSON::Schema::Pointer representing the path to this node within its document
27
43
  attr_reader :pointer
28
44
 
45
+ # the raw content of this Node from the underlying document at this Node's path.
29
46
  def content
30
47
  pointer.evaluate(document)
31
48
  end
@@ -40,7 +57,7 @@ module Scorpio
40
57
  begin
41
58
  el = content[k]
42
59
  rescue TypeError => e
43
- raise(e.class, e.message + "\nsubscripting from #{content.pretty_inspect} (#{content.class}): #{k.pretty_inspect} (#{k.class})", e.backtrace)
60
+ raise(e.class, e.message + "\nsubscripting with #{k.pretty_inspect.chomp} (#{k.class}) from #{content.class.inspect}. self is: #{pretty_inspect.chomp}", e.backtrace)
44
61
  end
45
62
  if el.is_a?(Hash) || el.is_a?(Array)
46
63
  self.class.new_by_type(node.document, node.path + [k])
@@ -73,26 +90,91 @@ module Scorpio
73
90
  return self
74
91
  end
75
92
 
93
+ # a Node at the root of the document
76
94
  def document_node
77
95
  Node.new_by_type(document, [])
78
96
  end
79
97
 
98
+ # the parent of this node. if this node is the document root (its path is empty), raises
99
+ # ::JSON::Schema::Pointer::ReferenceError.
100
+ def parent_node
101
+ if path.empty?
102
+ raise(::JSON::Schema::Pointer::ReferenceError, "cannot access parent of root node: #{pretty_inspect.chomp}")
103
+ else
104
+ Node.new_by_type(document, path[0...-1])
105
+ end
106
+ end
107
+
108
+ # the pointer path to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901
80
109
  def pointer_path
81
110
  pointer.pointer
82
111
  end
112
+ # the pointer fragment to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901
83
113
  def fragment
84
114
  pointer.fragment
85
115
  end
86
116
 
117
+ def as_json
118
+ Typelike.as_json(content)
119
+ end
120
+
121
+ # takes a block. the block is yielded the content of this node. the block MUST return a modified
122
+ # copy of that content (and NOT modify the object it is given).
123
+ def modified_copy
124
+ # we need to preserve the rest of the document, but modify the content at our path.
125
+ #
126
+ # this is actually a bit tricky. we can't modify the original document, obviously.
127
+ # we could do a deep copy, but that's expensive. instead, we make a copy of each array
128
+ # or hash in the path above this node. this node's content is modified by the caller, and
129
+ # that is recursively merged up to the document root. the recursion is done with a
130
+ # y combinator, for no other reason than that was a fun way to implement it.
131
+ modified_document = ycomb do |rec|
132
+ proc do |subdocument, subpath|
133
+ if subpath == []
134
+ yield(subdocument)
135
+ else
136
+ car = subpath[0]
137
+ cdr = subpath[1..-1]
138
+ if subdocument.respond_to?(:to_hash)
139
+ car_object = rec.call(subdocument[car], cdr)
140
+ if car_object.object_id == subdocument[car].object_id
141
+ subdocument
142
+ else
143
+ subdocument.merge({car => car_object})
144
+ end
145
+ elsif subdocument.respond_to?(:to_ary)
146
+ if car.is_a?(String) && car =~ /\A\d+\z/
147
+ car = car.to_i
148
+ end
149
+ unless car.is_a?(Integer)
150
+ raise(TypeError, "bad subscript #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for array: #{subdocument.pretty_inspect.chomp}")
151
+ end
152
+ car_object = rec.call(subdocument[car], cdr)
153
+ if car_object.object_id == subdocument[car].object_id
154
+ subdocument
155
+ else
156
+ subdocument.dup.tap do |arr|
157
+ arr[car] = car_object
158
+ end
159
+ end
160
+ else
161
+ raise(TypeError, "bad subscript: #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for content: #{subdocument.pretty_inspect.chomp}")
162
+ end
163
+ end
164
+ end
165
+ end.call(document, path)
166
+ Node.new_by_type(modified_document, path)
167
+ end
168
+
87
169
  def object_group_text
88
170
  "fragment=#{fragment.inspect}"
89
171
  end
90
172
  def inspect
91
- "\#<#{self.class.name} #{object_group_text} #{content.inspect}>"
173
+ "\#<#{self.class.inspect} #{object_group_text} #{content.inspect}>"
92
174
  end
93
175
  def pretty_print(q)
94
176
  q.instance_exec(self) do |obj|
95
- text "\#<#{obj.class.name} #{object_group_text}"
177
+ text "\#<#{obj.class.inspect} #{obj.object_group_text}"
96
178
  group_sub {
97
179
  nest(2) {
98
180
  breakable ' '
@@ -105,7 +187,7 @@ module Scorpio
105
187
  end
106
188
 
107
189
  def fingerprint
108
- {class: self.class, document: document, path: path}
190
+ {is_node: self.is_a?(Scorpio::JSON::Node), document: document, path: path}
109
191
  end
110
192
  include FingerprintHash
111
193
  end
@@ -116,7 +198,6 @@ module Scorpio
116
198
  content.each_index { |i| yield self[i] }
117
199
  self
118
200
  end
119
- include Enumerable
120
201
 
121
202
  def to_ary
122
203
  to_a
@@ -124,29 +205,27 @@ module Scorpio
124
205
 
125
206
  include Arraylike
126
207
 
127
- # array methods - define only those which do not modify the array.
128
-
129
- # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a)
130
- index_methods = %w(each_index empty? length size)
131
- index_methods.each do |method_name|
132
- define_method(method_name) { |*a, &b| content.public_send(method_name, *a, &b) }
208
+ def as_json # needs redefined after including Enumerable
209
+ Typelike.as_json(content)
133
210
  end
134
211
 
135
- # methods which use index and value.
136
- # flatten is omitted. flatten should not exist.
137
- array_methods = %w(& | * + - <=> abbrev assoc at bsearch bsearch_index combination compact count cycle dig fetch index first include? join last pack permutation rassoc repeated_combination reject reverse reverse_each rindex rotate sample select shelljoin shuffle slice sort take take_while transpose uniq values_at zip)
138
- array_methods.each do |method_name|
139
- define_method(method_name) { |*a, &b| to_a.public_send(method_name, *a, &b) }
212
+ # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a).
213
+ # we override these methods from Arraylike
214
+ SAFE_INDEX_ONLY_METHODS.each do |method_name|
215
+ define_method(method_name) { |*a, &b| content.public_send(method_name, *a, &b) }
140
216
  end
141
217
  end
142
218
 
143
219
  class HashNode < Node
144
- def each
220
+ def each(&block)
145
221
  return to_enum(__method__) { content.size } unless block_given?
146
- content.each_key { |k| yield k, self[k] }
222
+ if block.arity > 1
223
+ content.each_key { |k| yield k, self[k] }
224
+ else
225
+ content.each_key { |k| yield [k, self[k]] }
226
+ end
147
227
  self
148
228
  end
149
- include Enumerable
150
229
 
151
230
  def to_hash
152
231
  inject({}) { |h, (k, v)| h[k] = v; h }
@@ -154,61 +233,14 @@ module Scorpio
154
233
 
155
234
  include Hashlike
156
235
 
157
- # hash methods - define only those which do not modify the hash.
236
+ def as_json # needs redefined after including Enumerable
237
+ Typelike.as_json(content)
238
+ end
158
239
 
159
240
  # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash)
160
- key_methods = %w(each_key empty? include? has_key? key key? keys length member? size)
161
- key_methods.each do |method_name|
241
+ SAFE_KEY_ONLY_METHODS.each do |method_name|
162
242
  define_method(method_name) { |*a, &b| content.public_send(method_name, *a, &b) }
163
243
  end
164
-
165
- # methods which use key and value
166
- hash_methods = %w(any? compact dig each_pair each_value fetch fetch_values has_value? invert rassoc reject select to_h transform_values value? values values_at)
167
- hash_methods.each do |method_name|
168
- define_method(method_name) { |*a, &b| to_hash.public_send(method_name, *a, &b) }
169
- end
170
-
171
- # methods that return a modified copy
172
- def merge(other)
173
- # we need to preserve the rest of the document, but modify the content at our path.
174
- #
175
- # this is actually a bit tricky. we can't modify the original document, obviously.
176
- # we could do a deep copy, but that's expensive. instead, we make a copy of each array
177
- # or hash in the path above this node. this node's content is merged with `other`, and
178
- # that is recursively merged up to the document root. the recursion is done with a
179
- # y combinator, for no other reason than that was a fun way to implement it.
180
- merged_document = ycomb do |rec|
181
- proc do |subdocument, subpath|
182
- if subpath == []
183
- subdocument.merge(other.is_a?(JSON::Node) ? other.content : other)
184
- else
185
- car = subpath[0]
186
- cdr = subpath[1..-1]
187
- if subdocument.is_a?(Array)
188
- if car.is_a?(String) && car =~ /\A\d+\z/
189
- car = car.to_i
190
- end
191
- unless car.is_a?(Integer)
192
- raise(TypeError, "bad subscript #{car.pretty_inspect} with remaining subpath: #{cdr.inspect} for array: #{subdocument.pretty_inspect}")
193
- end
194
- end
195
- car_object = rec.call(subdocument[car], cdr)
196
- if car_object == subdocument[car]
197
- subdocument
198
- elsif subdocument.is_a?(Hash)
199
- subdocument.merge({car => car_object})
200
- elsif subdocument.is_a?(Array)
201
- subdocument.dup.tap do |arr|
202
- arr[car] = car_object
203
- end
204
- else
205
- raise(TypeError, "bad subscript: #{car.pretty_inspect} with remaining subpath: #{cdr.inspect} for content: #{subdocument.pretty_inspect}")
206
- end
207
- end
208
- end
209
- end.call(document, path)
210
- self.class.new(merged_document, path)
211
- end
212
244
  end
213
245
  end
214
246
  end