scorpio 0.2.3 → 0.3.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.
@@ -0,0 +1,34 @@
1
+ module Scorpio
2
+ class Response < ::Ur::Response
3
+ def response_schema
4
+ ur.scorpio_request.operation.response_schema(status: status, media_type: media_type)
5
+ end
6
+
7
+ def body_object
8
+ # TODO handle media types like `application/schema-instance+json` or vendor things like github's
9
+ if media_type == 'application/json'
10
+ if body.empty?
11
+ # an empty body isn't valid json, of course, but we'll just return nil for it.
12
+ body_object = nil
13
+ else
14
+ begin
15
+ body_object = ::JSON.parse(body)
16
+ #rescue ::JSON::ParserError
17
+ # TODO
18
+ end
19
+ end
20
+
21
+ if response_schema && (body_object.respond_to?(:to_hash) || body_object.respond_to?(:to_ary))
22
+ body_object = JSI.class_for_schema(response_schema).new(JSI::JSON::Node.new_doc(body_object))
23
+ end
24
+
25
+ body_object
26
+ elsif media_type == 'text/plain'
27
+ body
28
+ else
29
+ # we will return the body if we do not have a supported parsing. for now.
30
+ body
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,33 @@
1
+ module Scorpio
2
+ class Ur < ::Ur
3
+ attr_accessor :scorpio_request
4
+
5
+ def class_for_schema(schema)
6
+ jsi_class_for_schema = super
7
+ if jsi_class_for_schema == ::Ur::Response
8
+ Scorpio::Response
9
+ else
10
+ jsi_class_for_schema
11
+ end
12
+ end
13
+
14
+ def raise_on_http_error
15
+ error_class = Scorpio.error_classes_by_status[response.status]
16
+ error_class ||= if (400..499).include?(response.status)
17
+ ClientError
18
+ elsif (500..599).include?(response.status)
19
+ ServerError
20
+ elsif !response.success?
21
+ HTTPError
22
+ end
23
+ if error_class
24
+ message = "Error calling operation #{scorpio_request.operation.operationId} on #{self}:\n" + response.body
25
+ raise(error_class.new(message).tap do |e|
26
+ e.ur = self
27
+ e.response_object = response.body_object
28
+ end)
29
+ end
30
+ nil
31
+ end
32
+ end
33
+ end
@@ -1,3 +1,3 @@
1
1
  module Scorpio
2
- VERSION = "0.2.3"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -20,11 +20,9 @@ Gem::Specification.new do |spec|
20
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
21
  spec.require_paths = ["lib"]
22
22
 
23
+ spec.add_dependency "jsi", "~> 0.0.3"
24
+ spec.add_dependency "ur", "~> 0.0.2"
23
25
  spec.add_dependency "faraday"
24
- # we are monkey patching json-schema with a fix that has not been merged in a timely fashion.
25
- spec.add_dependency "json-schema", "~> 2.8"
26
- spec.add_dependency "api_hammer"
27
- spec.add_development_dependency "bundler", "~> 1.12"
28
26
  spec.add_development_dependency "rake", "~> 10.0"
29
27
  spec.add_development_dependency "minitest", "~> 5.0"
30
28
  spec.add_development_dependency "minitest-around"
@@ -33,6 +31,7 @@ Gem::Specification.new do |spec|
33
31
  spec.add_development_dependency "rack", "~> 1.0"
34
32
  spec.add_development_dependency "rack-accept"
35
33
  spec.add_development_dependency "rack-test"
34
+ spec.add_development_dependency "api_hammer"
36
35
  spec.add_development_dependency "activerecord"
37
36
  spec.add_development_dependency "sqlite3"
38
37
  spec.add_development_dependency "database_cleaner"
metadata CHANGED
@@ -1,45 +1,45 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scorpio
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ethan
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-08-19 00:00:00.000000000 Z
11
+ date: 2019-01-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: faraday
14
+ name: jsi
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: 0.0.3
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: 0.0.3
27
27
  - !ruby/object:Gem::Dependency
28
- name: json-schema
28
+ name: ur
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '2.8'
33
+ version: 0.0.2
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '2.8'
40
+ version: 0.0.2
41
41
  - !ruby/object:Gem::Dependency
42
- name: api_hammer
42
+ name: faraday
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
@@ -52,20 +52,6 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: bundler
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '1.12'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '1.12'
69
55
  - !ruby/object:Gem::Dependency
70
56
  name: rake
71
57
  requirement: !ruby/object:Gem::Requirement
@@ -178,6 +164,20 @@ dependencies:
178
164
  - - ">="
179
165
  - !ruby/object:Gem::Version
180
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: api_hammer
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
181
  - !ruby/object:Gem::Dependency
182
182
  name: activerecord
183
183
  requirement: !ruby/object:Gem::Requirement
@@ -255,20 +255,16 @@ files:
255
255
  - documents/www.googleapis.com/discovery/v1/apis/discovery/v1/rest.yml
256
256
  - lib/scorpio.rb
257
257
  - lib/scorpio/google_api_document.rb
258
- - lib/scorpio/json-schema-fragments.rb
259
- - lib/scorpio/json.rb
260
- - lib/scorpio/json/node.rb
261
258
  - lib/scorpio/openapi.rb
259
+ - lib/scorpio/openapi/document.rb
260
+ - lib/scorpio/openapi/operation.rb
261
+ - lib/scorpio/openapi/operations_scope.rb
262
+ - lib/scorpio/openapi/v3/server.rb
262
263
  - lib/scorpio/pickle_adapter.rb
264
+ - lib/scorpio/request.rb
263
265
  - lib/scorpio/resource_base.rb
264
- - lib/scorpio/schema.rb
265
- - lib/scorpio/schema_instance_base.rb
266
- - lib/scorpio/schema_instance_base/to_rb.rb
267
- - lib/scorpio/schema_instance_json_coder.rb
268
- - lib/scorpio/struct_json_coder.rb
269
- - lib/scorpio/typelike_modules.rb
270
- - lib/scorpio/util.rb
271
- - lib/scorpio/util/faraday/response_media_type.rb
266
+ - lib/scorpio/response.rb
267
+ - lib/scorpio/ur.rb
272
268
  - lib/scorpio/version.rb
273
269
  - scorpio.gemspec
274
270
  homepage: https://github.com/notEthan/scorpio
@@ -1,191 +0,0 @@
1
- require "json-schema"
2
-
3
- # apply the changes from https://github.com/ruby-json-schema/json-schema/pull/382
4
-
5
- # json-schema/pointer.rb
6
- require 'addressable/uri'
7
-
8
- module JSON
9
- class Schema
10
- # a JSON Pointer, as described by RFC 6901 https://tools.ietf.org/html/rfc6901
11
- class Pointer
12
- class Error < JSON::Schema::SchemaError
13
- end
14
- class PointerSyntaxError < Error
15
- end
16
- class ReferenceError < Error
17
- end
18
-
19
- # parse a fragment to an array of reference tokens
20
- #
21
- # #/foo/bar
22
- #
23
- # => ['foo', 'bar']
24
- #
25
- # #/foo%20bar
26
- #
27
- # => ['foo bar']
28
- def self.parse_fragment(fragment)
29
- fragment = Addressable::URI.unescape(fragment)
30
- match = fragment.match(/\A#/)
31
- if match
32
- parse_pointer(match.post_match)
33
- else
34
- raise(PointerSyntaxError, "Invalid fragment syntax in #{fragment.inspect}: fragment must begin with #")
35
- end
36
- end
37
-
38
- # parse a pointer to an array of reference tokens
39
- #
40
- # /foo
41
- #
42
- # => ['foo']
43
- #
44
- # /foo~0bar/baz~1qux
45
- #
46
- # => ['foo~bar', 'baz/qux']
47
- def self.parse_pointer(pointer_string)
48
- tokens = pointer_string.split('/', -1).map! do |piece|
49
- piece.gsub('~1', '/').gsub('~0', '~')
50
- end
51
- if tokens[0] == ''
52
- tokens[1..-1]
53
- elsif tokens.empty?
54
- tokens
55
- else
56
- raise(PointerSyntaxError, "Invalid pointer syntax in #{pointer_string.inspect}: pointer must begin with /")
57
- end
58
- end
59
-
60
- # initializes a JSON::Schema::Pointer from the given representation.
61
- #
62
- # type may be one of:
63
- #
64
- # - :fragment - the representation is a fragment containing a pointer (starting with #)
65
- # - :pointer - the representation is a pointer (starting with /)
66
- # - :reference_tokens - the representation is an array of tokens referencing a path in a document
67
- def initialize(type, representation)
68
- @type = type
69
- if type == :reference_tokens
70
- reference_tokens = representation
71
- elsif type == :fragment
72
- reference_tokens = self.class.parse_fragment(representation)
73
- elsif type == :pointer
74
- reference_tokens = self.class.parse_pointer(representation)
75
- else
76
- raise ArgumentError, "invalid initialization type: #{type.inspect} with representation #{representation.inspect}"
77
- end
78
- @reference_tokens = reference_tokens.map(&:freeze).freeze
79
- end
80
-
81
- attr_reader :reference_tokens
82
-
83
- # takes a root json document and evaluates this pointer through the document, returning the value
84
- # pointed to by this pointer.
85
- def evaluate(document)
86
- reference_tokens.inject(document) do |value, token|
87
- if value.is_a?(Array)
88
- if token.is_a?(String) && token =~ /\A\d|[1-9]\d+\z/
89
- token = token.to_i
90
- end
91
- unless token.is_a?(Integer)
92
- raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not an integer and cannot be resolved in array #{value.inspect}")
93
- end
94
- unless (0...value.size).include?(token)
95
- raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid index of #{value.inspect}")
96
- end
97
- elsif value.is_a?(Hash)
98
- unless value.key?(token)
99
- raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid key of #{value.inspect}")
100
- end
101
- else
102
- raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} cannot be resolved in #{value.inspect}")
103
- end
104
- value[token]
105
- end
106
- end
107
-
108
- # the pointer string representation of this Pointer
109
- def pointer
110
- reference_tokens.map { |t| '/' + t.to_s.gsub('~', '~0').gsub('/', '~1') }.join('')
111
- end
112
-
113
- # the fragment string representation of this Pointer
114
- def fragment
115
- '#' + Addressable::URI.escape(pointer)
116
- end
117
-
118
- def to_s
119
- "#<#{self.class.inspect} #{@type} = #{representation_s}>"
120
- end
121
-
122
- private
123
-
124
- def representation_s
125
- if @type == :fragment
126
- fragment
127
- elsif @type == :pointer
128
- pointer
129
- else
130
- reference_tokens.inspect
131
- end
132
- end
133
- end
134
- end
135
- end
136
-
137
- # json-schema/validator.rb
138
-
139
- module JSON
140
- class Validator
141
- def initialize(schema_data, data, opts={})
142
- @options = @@default_opts.clone.merge(opts)
143
- @errors = []
144
-
145
- validator = self.class.validator_for_name(@options[:version])
146
- @options[:version] = validator
147
- @options[:schema_reader] ||= self.class.schema_reader
148
-
149
- @validation_options = @options[:record_errors] ? {:record_errors => true} : {}
150
- @validation_options[:insert_defaults] = true if @options[:insert_defaults]
151
- @validation_options[:strict] = true if @options[:strict] == true
152
- @validation_options[:clear_cache] = true if !@@cache_schemas || @options[:clear_cache]
153
-
154
- @@mutex.synchronize { @base_schema = initialize_schema(schema_data) }
155
- @original_data = data
156
- @data = initialize_data(data)
157
- @@mutex.synchronize { build_schemas(@base_schema) }
158
-
159
- # If the :fragment option is set, try and validate against the fragment
160
- if opts[:fragment]
161
- @base_schema = schema_from_fragment(@base_schema, opts[:fragment])
162
- end
163
-
164
- # validate the schema, if requested
165
- if @options[:validate_schema]
166
- if @base_schema.schema["$schema"]
167
- base_validator = self.class.validator_for_name(@base_schema.schema["$schema"])
168
- end
169
- metaschema = base_validator ? base_validator.metaschema : validator.metaschema
170
- # Don't clear the cache during metaschema validation!
171
- self.class.validate!(metaschema, @base_schema.schema, {:clear_cache => false})
172
- end
173
- end
174
-
175
- def schema_from_fragment(base_schema, fragment)
176
- schema_uri = base_schema.uri
177
-
178
- pointer = JSON::Schema::Pointer.new(:fragment, fragment)
179
-
180
- base_schema = JSON::Schema.new(pointer.evaluate(base_schema.schema), schema_uri, @options[:version])
181
-
182
- if @options[:list]
183
- base_schema.to_array_schema
184
- elsif base_schema.is_a?(Hash)
185
- JSON::Schema.new(base_schema, schema_uri, @options[:version])
186
- else
187
- base_schema
188
- end
189
- end
190
- end
191
- end
@@ -1,5 +0,0 @@
1
- module Scorpio
2
- module JSON
3
- autoload :Node, 'scorpio/json/node'
4
- end
5
- end
@@ -1,256 +0,0 @@
1
- module Scorpio
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.
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.
18
- def self.new_by_type(document, path)
19
- node = Node.new(document, path)
20
- content = node.content
21
- if content.is_a?(Hash)
22
- HashNode.new(document, path)
23
- elsif content.is_a?(Array)
24
- ArrayNode.new(document, path)
25
- else
26
- node
27
- end
28
- end
29
-
30
- # a Node represents the content of a document at a given path.
31
- def initialize(document, path)
32
- raise(ArgumentError, "path must be an array. got: #{path.pretty_inspect.chomp} (#{path.class})") unless path.is_a?(Array)
33
- @document = document
34
- @path = path.dup.freeze
35
- @pointer = ::JSON::Schema::Pointer.new(:reference_tokens, path)
36
- end
37
-
38
- # the path of this Node within its document
39
- attr_reader :path
40
- # the document containing this Node at is path
41
- attr_reader :document
42
- # ::JSON::Schema::Pointer representing the path to this node within its document
43
- attr_reader :pointer
44
-
45
- # the raw content of this Node from the underlying document at this Node's path.
46
- def content
47
- pointer.evaluate(document)
48
- end
49
-
50
- def [](k)
51
- node = self
52
- content = node.content
53
- if content.is_a?(Hash) && !content.key?(k)
54
- node = node.deref
55
- content = node.content
56
- end
57
- begin
58
- el = content[k]
59
- rescue TypeError => e
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)
61
- end
62
- if el.is_a?(Hash) || el.is_a?(Array)
63
- self.class.new_by_type(node.document, node.path + [k])
64
- else
65
- el
66
- end
67
- end
68
-
69
- def []=(k, v)
70
- if v.is_a?(Node)
71
- content[k] = v.content
72
- else
73
- content[k] = v
74
- end
75
- end
76
-
77
- def deref
78
- content = self.content
79
-
80
- return self unless content.is_a?(Hash) && content['$ref'].is_a?(String)
81
-
82
- if content['$ref'][/\A#/]
83
- return self.class.new_by_type(document, ::JSON::Schema::Pointer.parse_fragment(content['$ref'])).deref
84
- end
85
-
86
- # HAX for how google does refs and ids
87
- if document_node['schemas'].respond_to?(:to_hash)
88
- if document_node['schemas'][content['$ref']]
89
- return document_node['schemas'][content['$ref']]
90
- end
91
- _, deref_by_id = document_node['schemas'].detect { |_k, schema| schema['id'] == content['$ref'] }
92
- if deref_by_id
93
- return deref_by_id
94
- end
95
- end
96
-
97
- #raise(NotImplementedError, "cannot dereference #{content['$ref']}") # TODO
98
- return self
99
- end
100
-
101
- # a Node at the root of the document
102
- def document_node
103
- Node.new_by_type(document, [])
104
- end
105
-
106
- # the parent of this node. if this node is the document root (its path is empty), raises
107
- # ::JSON::Schema::Pointer::ReferenceError.
108
- def parent_node
109
- if path.empty?
110
- raise(::JSON::Schema::Pointer::ReferenceError, "cannot access parent of root node: #{pretty_inspect.chomp}")
111
- else
112
- Node.new_by_type(document, path[0...-1])
113
- end
114
- end
115
-
116
- # the pointer path to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901
117
- def pointer_path
118
- pointer.pointer
119
- end
120
- # the pointer fragment to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901
121
- def fragment
122
- pointer.fragment
123
- end
124
-
125
- def as_json(*opt)
126
- Typelike.as_json(content, *opt)
127
- end
128
-
129
- # takes a block. the block is yielded the content of this node. the block MUST return a modified
130
- # copy of that content (and NOT modify the object it is given).
131
- def modified_copy
132
- # we need to preserve the rest of the document, but modify the content at our path.
133
- #
134
- # this is actually a bit tricky. we can't modify the original document, obviously.
135
- # we could do a deep copy, but that's expensive. instead, we make a copy of each array
136
- # or hash in the path above this node. this node's content is modified by the caller, and
137
- # that is recursively merged up to the document root. the recursion is done with a
138
- # y combinator, for no other reason than that was a fun way to implement it.
139
- modified_document = ycomb do |rec|
140
- proc do |subdocument, subpath|
141
- if subpath == []
142
- yield(subdocument)
143
- else
144
- car = subpath[0]
145
- cdr = subpath[1..-1]
146
- if subdocument.respond_to?(:to_hash)
147
- car_object = rec.call(subdocument[car], cdr)
148
- if car_object.object_id == subdocument[car].object_id
149
- subdocument
150
- else
151
- subdocument.merge({car => car_object})
152
- end
153
- elsif subdocument.respond_to?(:to_ary)
154
- if car.is_a?(String) && car =~ /\A\d+\z/
155
- car = car.to_i
156
- end
157
- unless car.is_a?(Integer)
158
- raise(TypeError, "bad subscript #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for array: #{subdocument.pretty_inspect.chomp}")
159
- end
160
- car_object = rec.call(subdocument[car], cdr)
161
- if car_object.object_id == subdocument[car].object_id
162
- subdocument
163
- else
164
- subdocument.dup.tap do |arr|
165
- arr[car] = car_object
166
- end
167
- end
168
- else
169
- raise(TypeError, "bad subscript: #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for content: #{subdocument.pretty_inspect.chomp}")
170
- end
171
- end
172
- end
173
- end.call(document, path)
174
- Node.new_by_type(modified_document, path)
175
- end
176
-
177
- def object_group_text
178
- "fragment=#{fragment.inspect}"
179
- end
180
- def inspect
181
- "\#<#{self.class.inspect} #{object_group_text} #{content.inspect}>"
182
- end
183
- def pretty_print(q)
184
- q.instance_exec(self) do |obj|
185
- text "\#<#{obj.class.inspect} #{obj.object_group_text}"
186
- group_sub {
187
- nest(2) {
188
- breakable ' '
189
- pp obj.content
190
- }
191
- }
192
- breakable ''
193
- text '>'
194
- end
195
- end
196
-
197
- def fingerprint
198
- {is_node: self.is_a?(Scorpio::JSON::Node), document: document, path: path}
199
- end
200
- include FingerprintHash
201
- end
202
-
203
- class ArrayNode < Node
204
- def each
205
- return to_enum(__method__) { content.size } unless block_given?
206
- content.each_index { |i| yield self[i] }
207
- self
208
- end
209
-
210
- def to_ary
211
- to_a
212
- end
213
-
214
- include Enumerable
215
- include Arraylike
216
-
217
- def as_json(*opt) # needs redefined after including Enumerable
218
- Typelike.as_json(content, *opt)
219
- end
220
-
221
- # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a).
222
- # we override these methods from Arraylike
223
- SAFE_INDEX_ONLY_METHODS.each do |method_name|
224
- define_method(method_name) { |*a, &b| content.public_send(method_name, *a, &b) }
225
- end
226
- end
227
-
228
- class HashNode < Node
229
- def each(&block)
230
- return to_enum(__method__) { content.size } unless block_given?
231
- if block.arity > 1
232
- content.each_key { |k| yield k, self[k] }
233
- else
234
- content.each_key { |k| yield [k, self[k]] }
235
- end
236
- self
237
- end
238
-
239
- def to_hash
240
- inject({}) { |h, (k, v)| h[k] = v; h }
241
- end
242
-
243
- include Enumerable
244
- include Hashlike
245
-
246
- def as_json(*opt) # needs redefined after including Enumerable
247
- Typelike.as_json(content, *opt)
248
- end
249
-
250
- # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash)
251
- SAFE_KEY_ONLY_METHODS.each do |method_name|
252
- define_method(method_name) { |*a, &b| content.public_send(method_name, *a, &b) }
253
- end
254
- end
255
- end
256
- end