scorpio 0.1.0 → 0.2.0

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