scorpio 0.2.3 → 0.3.0

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