cloudenvoy 0.1.0.dev → 0.4.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.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +41 -0
  3. data/.gitignore +3 -0
  4. data/.rubocop.yml +1 -0
  5. data/Appraisals +25 -0
  6. data/CHANGELOG.md +32 -0
  7. data/Gemfile.lock +215 -1
  8. data/README.md +581 -7
  9. data/app/controllers/cloudenvoy/application_controller.rb +8 -0
  10. data/app/controllers/cloudenvoy/subscriber_controller.rb +59 -0
  11. data/cloudenvoy.gemspec +15 -2
  12. data/config/routes.rb +5 -0
  13. data/examples/rails/.ruby-version +1 -0
  14. data/examples/rails/Gemfile +15 -0
  15. data/examples/rails/Gemfile.lock +207 -0
  16. data/examples/rails/Procfile +1 -0
  17. data/examples/rails/README.md +31 -0
  18. data/examples/rails/Rakefile +8 -0
  19. data/examples/rails/app/assets/config/manifest.js +2 -0
  20. data/examples/rails/app/assets/images/.keep +0 -0
  21. data/examples/rails/app/assets/stylesheets/application.css +15 -0
  22. data/examples/rails/app/channels/application_cable/channel.rb +6 -0
  23. data/examples/rails/app/channels/application_cable/connection.rb +6 -0
  24. data/examples/rails/app/controllers/application_controller.rb +4 -0
  25. data/examples/rails/app/controllers/concerns/.keep +0 -0
  26. data/examples/rails/app/helpers/application_helper.rb +4 -0
  27. data/examples/rails/app/javascript/packs/application.js +15 -0
  28. data/examples/rails/app/jobs/application_job.rb +9 -0
  29. data/examples/rails/app/mailers/application_mailer.rb +6 -0
  30. data/examples/rails/app/models/application_record.rb +5 -0
  31. data/examples/rails/app/models/concerns/.keep +0 -0
  32. data/examples/rails/app/publishers/hello_publisher.rb +34 -0
  33. data/examples/rails/app/subscribers/hello_subscriber.rb +16 -0
  34. data/examples/rails/app/views/layouts/application.html.erb +14 -0
  35. data/examples/rails/app/views/layouts/mailer.html.erb +13 -0
  36. data/examples/rails/app/views/layouts/mailer.text.erb +1 -0
  37. data/examples/rails/bin/rails +6 -0
  38. data/examples/rails/bin/rake +6 -0
  39. data/examples/rails/bin/setup +35 -0
  40. data/examples/rails/config.ru +7 -0
  41. data/examples/rails/config/application.rb +19 -0
  42. data/examples/rails/config/boot.rb +7 -0
  43. data/examples/rails/config/cable.yml +10 -0
  44. data/examples/rails/config/credentials.yml.enc +1 -0
  45. data/examples/rails/config/database.yml +25 -0
  46. data/examples/rails/config/environment.rb +7 -0
  47. data/examples/rails/config/environments/development.rb +65 -0
  48. data/examples/rails/config/environments/production.rb +114 -0
  49. data/examples/rails/config/environments/test.rb +50 -0
  50. data/examples/rails/config/initializers/application_controller_renderer.rb +9 -0
  51. data/examples/rails/config/initializers/assets.rb +14 -0
  52. data/examples/rails/config/initializers/backtrace_silencers.rb +8 -0
  53. data/examples/rails/config/initializers/cloudenvoy.rb +22 -0
  54. data/examples/rails/config/initializers/content_security_policy.rb +29 -0
  55. data/examples/rails/config/initializers/cookies_serializer.rb +7 -0
  56. data/examples/rails/config/initializers/filter_parameter_logging.rb +6 -0
  57. data/examples/rails/config/initializers/inflections.rb +17 -0
  58. data/examples/rails/config/initializers/mime_types.rb +5 -0
  59. data/examples/rails/config/initializers/wrap_parameters.rb +16 -0
  60. data/examples/rails/config/locales/en.yml +33 -0
  61. data/examples/rails/config/master.key +1 -0
  62. data/examples/rails/config/puma.rb +37 -0
  63. data/examples/rails/config/routes.rb +4 -0
  64. data/examples/rails/config/spring.rb +8 -0
  65. data/examples/rails/config/storage.yml +34 -0
  66. data/examples/rails/db/development.sqlite3 +0 -0
  67. data/examples/rails/db/test.sqlite3 +0 -0
  68. data/examples/rails/lib/assets/.keep +0 -0
  69. data/examples/rails/log/.keep +0 -0
  70. data/examples/rails/public/404.html +67 -0
  71. data/examples/rails/public/422.html +67 -0
  72. data/examples/rails/public/500.html +66 -0
  73. data/examples/rails/public/apple-touch-icon-precomposed.png +0 -0
  74. data/examples/rails/public/apple-touch-icon.png +0 -0
  75. data/examples/rails/public/favicon.ico +0 -0
  76. data/examples/rails/storage/.keep +0 -0
  77. data/gemfiles/rails_5.2.gemfile +7 -0
  78. data/gemfiles/rails_5.2.gemfile.lock +251 -0
  79. data/gemfiles/rails_6.0.gemfile +7 -0
  80. data/gemfiles/rails_6.0.gemfile.lock +267 -0
  81. data/gemfiles/semantic_logger_3.4.gemfile +7 -0
  82. data/gemfiles/semantic_logger_3.4.gemfile.lock +265 -0
  83. data/gemfiles/semantic_logger_4.6.gemfile +7 -0
  84. data/gemfiles/semantic_logger_4.6.gemfile.lock +265 -0
  85. data/gemfiles/semantic_logger_4.7.0.gemfile +7 -0
  86. data/gemfiles/semantic_logger_4.7.0.gemfile.lock +265 -0
  87. data/gemfiles/semantic_logger_4.7.2.gemfile +7 -0
  88. data/gemfiles/semantic_logger_4.7.2.gemfile.lock +265 -0
  89. data/lib/cloudenvoy.rb +96 -2
  90. data/lib/cloudenvoy/authentication_error.rb +6 -0
  91. data/lib/cloudenvoy/authenticator.rb +57 -0
  92. data/lib/cloudenvoy/backend/google_pub_sub.rb +146 -0
  93. data/lib/cloudenvoy/backend/memory_pub_sub.rb +89 -0
  94. data/lib/cloudenvoy/config.rb +165 -0
  95. data/lib/cloudenvoy/engine.rb +20 -0
  96. data/lib/cloudenvoy/invalid_subscriber_error.rb +6 -0
  97. data/lib/cloudenvoy/logger_wrapper.rb +167 -0
  98. data/lib/cloudenvoy/message.rb +96 -0
  99. data/lib/cloudenvoy/middleware/chain.rb +250 -0
  100. data/lib/cloudenvoy/pub_sub_client.rb +76 -0
  101. data/lib/cloudenvoy/publisher.rb +211 -0
  102. data/lib/cloudenvoy/publisher_logger.rb +32 -0
  103. data/lib/cloudenvoy/subscriber.rb +222 -0
  104. data/lib/cloudenvoy/subscriber_logger.rb +26 -0
  105. data/lib/cloudenvoy/subscription.rb +19 -0
  106. data/lib/cloudenvoy/testing.rb +106 -0
  107. data/lib/cloudenvoy/topic.rb +19 -0
  108. data/lib/cloudenvoy/version.rb +1 -1
  109. data/lib/tasks/cloudenvoy.rake +61 -0
  110. metadata +263 -6
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudenvoy
4
+ # CLoudenvoy Rails engine
5
+ class Engine < ::Rails::Engine
6
+ isolate_namespace Cloudenvoy
7
+
8
+ initializer 'cloudenvoy', before: :load_config_initializers do
9
+ Rails.application.routes.append do
10
+ mount Cloudenvoy::Engine, at: '/cloudenvoy'
11
+ end
12
+ end
13
+
14
+ config.generators do |g|
15
+ g.test_framework :rspec, fixture: false
16
+ g.assets false
17
+ g.helper false
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudenvoy
4
+ class InvalidSubscriberError < StandardError
5
+ end
6
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudenvoy
4
+ # Add contextual information to logs generated
5
+ # by subscribers/publishers.
6
+ #
7
+ # This class is a base class which aims at being inherited by
8
+ # object-specific logger wrappers. See Cloudenvoy::SubscriberLogger
9
+ # and Cloudenvoy::PublisherLogger.
10
+ class LoggerWrapper
11
+ attr_accessor :loggable
12
+
13
+ class << self
14
+ attr_accessor :log_context_processor
15
+ end
16
+
17
+ #
18
+ # The default context processor. Aims at being overriden by
19
+ # child classes.
20
+ #
21
+ # @return [Proc] The context processor proc.
22
+ #
23
+ def self.default_context_processor
24
+ @default_context_processor ||= ->(_) { {} }
25
+ end
26
+
27
+ #
28
+ # Build a new instance of the class.
29
+ #
30
+ # @param [Any] loggable The loggable to wrap for logging.
31
+ #
32
+ def initialize(loggable)
33
+ @loggable = loggable
34
+ end
35
+
36
+ #
37
+ # Return the Proc responsible for formatting the log payload.
38
+ #
39
+ # @return [Proc] The context processor.
40
+ #
41
+ def context_processor
42
+ @context_processor ||= loggable.class.cloudenvoy_options_hash[:log_context_processor] ||
43
+ self.class.log_context_processor ||
44
+ self.class.default_context_processor
45
+ end
46
+
47
+ #
48
+ # The block to pass to log messages.
49
+ #
50
+ # @return [Proc] The log block.
51
+ #
52
+ def log_block
53
+ @log_block ||= proc { context_processor.call(loggable) }
54
+ end
55
+
56
+ #
57
+ # Return the Cloudenvoy logger.
58
+ #
59
+ # @return [Logger, any] The cloudenvoy logger.
60
+ #
61
+ def logger
62
+ Cloudenvoy.logger
63
+ end
64
+
65
+ #
66
+ # Format main log message.
67
+ #
68
+ # @param [String] msg The message to log.
69
+ #
70
+ # @return [String] The formatted log message
71
+ #
72
+ def formatted_message(msg)
73
+ "[Cloudenvoy][#{loggable.class}] #{msg}"
74
+ end
75
+
76
+ #
77
+ # Log an info message.
78
+ #
79
+ # @param [String] msg The message to log.
80
+ # @param [Proc] &block Optional context block.
81
+ #
82
+ def info(msg, &block)
83
+ log_message(:info, msg, &block)
84
+ end
85
+
86
+ #
87
+ # Log an error message.
88
+ #
89
+ # @param [String] msg The message to log.
90
+ # @param [Proc] &block Optional context block.
91
+ #
92
+ def error(msg, &block)
93
+ log_message(:error, msg, &block)
94
+ end
95
+
96
+ #
97
+ # Log an fatal message.
98
+ #
99
+ # @param [String] msg The message to log.
100
+ # @param [Proc] &block Optional context block.
101
+ #
102
+ def fatal(msg, &block)
103
+ log_message(:fatal, msg, &block)
104
+ end
105
+
106
+ #
107
+ # Log an debut message.
108
+ #
109
+ # @param [String] msg The message to log.
110
+ # @param [Proc] &block Optional context block.
111
+ #
112
+ def debug(msg, &block)
113
+ log_message(:debug, msg, &block)
114
+ end
115
+
116
+ #
117
+ # Delegate all methods to the underlying logger.
118
+ #
119
+ # @param [String, Symbol] name The method to delegate.
120
+ # @param [Array<any>] *args The list of method arguments.
121
+ # @param [Proc] &block Block passed to the method.
122
+ #
123
+ # @return [Any] The method return value
124
+ #
125
+ def method_missing(name, *args, &block)
126
+ if logger.respond_to?(name)
127
+ logger.send(name, *args, &block)
128
+ else
129
+ super
130
+ end
131
+ end
132
+
133
+ #
134
+ # Check if the class respond to a certain method.
135
+ #
136
+ # @param [String, Symbol] name The name of the method.
137
+ # @param [Boolean] include_private Whether to check private methods or not. Default to false.
138
+ #
139
+ # @return [Boolean] Return true if the class respond to this method.
140
+ #
141
+ def respond_to_missing?(name, include_private = false)
142
+ logger.respond_to?(name) || super
143
+ end
144
+
145
+ private
146
+
147
+ #
148
+ # Log a message for the provided log level.
149
+ #
150
+ # @param [String, Symbol] level The log level
151
+ # @param [String] msg The message to log.
152
+ # @param [Proc] &block Optional context block.
153
+ #
154
+ def log_message(level, msg, &block)
155
+ # Merge log-specific context into object-specific context
156
+ payload_block = ->(*_args) { log_block.call.merge(block&.call || {}) }
157
+
158
+ # ActiveSupport::Logger does not support passing a payload through a block on top
159
+ # of a message.
160
+ if defined?(ActiveSupport::Logger) && logger.is_a?(ActiveSupport::Logger)
161
+ logger.send(level) { "#{formatted_message(msg)} -- #{payload_block.call}" }
162
+ else
163
+ logger.send(level, formatted_message(msg), &payload_block)
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudenvoy
4
+ # Represents a Pub/Sub message
5
+ class Message
6
+ attr_writer :topic
7
+ attr_accessor :id, :payload, :metadata, :sub_uri
8
+
9
+ #
10
+ # Return an instantiated message from a Pub/Sub webhook
11
+ # payload.
12
+ #
13
+ # @param [Hash] input_payload The Pub/Sub webhook hash describing
14
+ # the message to process.
15
+ #
16
+ # @return [Cloudenvoy::Message] The instantiated message.
17
+ #
18
+ def self.from_descriptor(input_payload)
19
+ # Build new message
20
+ new(
21
+ id: input_payload.dig('message', 'message_id'),
22
+ payload: JSON.parse(Base64.decode64(input_payload.dig('message', 'data'))),
23
+ metadata: input_payload.dig('message', 'attributes'),
24
+ sub_uri: input_payload['subscription']
25
+ )
26
+ end
27
+
28
+ #
29
+ # Constructor
30
+ #
31
+ # @param [String] id The message ID
32
+ # @param [Hash, String] payload The message payload
33
+ # @param [Hash] metadata The message attributes
34
+ # @param [String] topic The topic - will be inferred from sub_uri if left blank
35
+ # @param [String] sub_uri The sub_uri this message was sent for
36
+ #
37
+ def initialize(id: nil, payload: nil, metadata: nil, topic: nil, sub_uri: nil)
38
+ @id = id
39
+ @payload = payload
40
+ @topic = topic
41
+ @metadata = metadata || {}
42
+ @sub_uri = sub_uri
43
+ end
44
+
45
+ #
46
+ # Return the message topic.
47
+ #
48
+ # @return [String] The message topic.
49
+ #
50
+ def topic
51
+ return @topic if @topic
52
+ return nil unless sub_uri
53
+
54
+ Subscriber.parse_sub_uri(sub_uri)[1]
55
+ end
56
+
57
+ #
58
+ # Return the instantiated Subscriber designated to process this message.
59
+ #
60
+ # @return [Subscriber] The instantiated subscriber.
61
+ #
62
+ def subscriber
63
+ @subscriber ||= begin
64
+ return nil unless sub_uri && (klass = Subscriber.from_sub_uri(sub_uri))
65
+
66
+ klass.new(message: self)
67
+ end
68
+ end
69
+
70
+ #
71
+ # Return a hash description of the message.
72
+ #
73
+ # @return [Hash] The message description
74
+ #
75
+ def to_h
76
+ {
77
+ id: id,
78
+ payload: payload,
79
+ metadata: metadata,
80
+ topic: topic,
81
+ sub_uri: sub_uri
82
+ }.compact
83
+ end
84
+
85
+ #
86
+ # Equality operator.
87
+ #
88
+ # @param [Any] other The object to compare.
89
+ #
90
+ # @return [Boolean] True if the object is equal.
91
+ #
92
+ def ==(other)
93
+ other.is_a?(self.class) && other.id == id
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudenvoy
4
+ module Middleware
5
+ # The class below was originally taken from Sidekiq.
6
+ # See: https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/middleware/chain.rb
7
+ #
8
+ # Middleware are callables configured to run before/after a message is processed.
9
+ # Middlewares can be configured to run on the client side (when jobs are pushed
10
+ # to Cloud Tasks) as well as on the server side (when jobs are processed by
11
+ # your application)
12
+ #
13
+ # To add a middleware on publishers:
14
+ #
15
+ # Cloudenvoy.configure do |config|
16
+ # config.publisher_middleware do |chain|
17
+ # chain.add MyPublisherHook
18
+ # end
19
+ # end
20
+ #
21
+ # To modify middlewares on subscribers, just call
22
+ # with another block:
23
+ #
24
+ # Cloudenvoy.configure do |config|
25
+ # config.subscriber_middleware do |chain|
26
+ # chain.add MySubscriberHook
27
+ # chain.remove ActiveRecord
28
+ # end
29
+ # end
30
+ #
31
+ # To insert immediately preceding another entry:
32
+ #
33
+ # Cloudenvoy.configure do |config|
34
+ # config.publisher_middleware do |chain|
35
+ # chain.insert_before ActiveRecord, MyPublisherHook
36
+ # end
37
+ # end
38
+ #
39
+ # To insert immediately after another entry:
40
+ #
41
+ # Cloudenvoy.configure do |config|
42
+ # config.publisher_middleware do |chain|
43
+ # chain.insert_after ActiveRecord, MyPublisherHook
44
+ # end
45
+ # end
46
+ #
47
+ # This is an example of a minimal server middleware:
48
+ #
49
+ # class MySubscriberHook
50
+ # def call(subscriber, msg, queue)
51
+ # puts "Before work"
52
+ # yield
53
+ # puts "After work"
54
+ # end
55
+ # end
56
+ #
57
+ # This is an example of a minimal client middleware, note
58
+ # the method must return the result or the job will not push
59
+ # to Redis:
60
+ #
61
+ # class MyPublisherHook
62
+ # def call(publisher, msg, queue, redis_pool)
63
+ # puts "Before push"
64
+ # result = yield
65
+ # puts "After push"
66
+ # result
67
+ # end
68
+ # end
69
+ #
70
+ class Chain
71
+ include Enumerable
72
+
73
+ #
74
+ # Build a new middleware chain.
75
+ #
76
+ def initialize
77
+ @entries = nil
78
+ yield self if block_given?
79
+ end
80
+
81
+ #
82
+ # Iterate over the list middlewares and execute the block on each item.
83
+ #
84
+ # @param [Proc] &block The block to execute on each item.
85
+ #
86
+ def each(&block)
87
+ entries.each(&block)
88
+ end
89
+
90
+ #
91
+ # Return the list of middlewares.
92
+ #
93
+ # @return [Array<Cloudenvoy::Middleware::Chain::Entry>] The list of middlewares
94
+ #
95
+ def entries
96
+ @entries ||= []
97
+ end
98
+
99
+ #
100
+ # Remove a middleware from the list.
101
+ #
102
+ # @param [Class] klass The middleware class to remove.
103
+ #
104
+ def remove(klass)
105
+ entries.delete_if { |entry| entry.klass == klass }
106
+ end
107
+
108
+ #
109
+ # Add a middleware at the end of the list.
110
+ #
111
+ # @param [Class] klass The middleware class to add.
112
+ # @param [Arry<any>] *args The list of arguments to the middleware.
113
+ #
114
+ # @return [Array<Cloudenvoy::Middleware::Chain::Entry>] The updated list of middlewares
115
+ #
116
+ def add(klass, *args)
117
+ remove(klass) if exists?(klass)
118
+ entries << Entry.new(klass, *args)
119
+ end
120
+
121
+ #
122
+ # Add a middleware at the beginning of the list.
123
+ #
124
+ # @param [Class] klass The middleware class to add.
125
+ # @param [Arry<any>] *args The list of arguments to the middleware.
126
+ #
127
+ # @return [Array<Cloudenvoy::Middleware::Chain::Entry>] The updated list of middlewares
128
+ #
129
+ def prepend(klass, *args)
130
+ remove(klass) if exists?(klass)
131
+ entries.insert(0, Entry.new(klass, *args))
132
+ end
133
+
134
+ #
135
+ # Add a middleware before another middleware.
136
+ #
137
+ # @param [Class] oldklass The middleware class before which the new middleware should be inserted.
138
+ # @param [Class] newklass The middleware class to insert.
139
+ # @param [Arry<any>] *args The list of arguments for the inserted middleware.
140
+ #
141
+ # @return [Array<Cloudenvoy::Middleware::Chain::Entry>] The updated list of middlewares
142
+ #
143
+ def insert_before(oldklass, newklass, *args)
144
+ i = entries.index { |entry| entry.klass == newklass }
145
+ new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
146
+ i = entries.index { |entry| entry.klass == oldklass } || 0
147
+ entries.insert(i, new_entry)
148
+ end
149
+
150
+ #
151
+ # Add a middleware after another middleware.
152
+ #
153
+ # @param [Class] oldklass The middleware class after which the new middleware should be inserted.
154
+ # @param [Class] newklass The middleware class to insert.
155
+ # @param [Arry<any>] *args The list of arguments for the inserted middleware.
156
+ #
157
+ # @return [Array<Cloudenvoy::Middleware::Chain::Entry>] The updated list of middlewares
158
+ #
159
+ def insert_after(oldklass, newklass, *args)
160
+ i = entries.index { |entry| entry.klass == newklass }
161
+ new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
162
+ i = entries.index { |entry| entry.klass == oldklass } || entries.count - 1
163
+ entries.insert(i + 1, new_entry)
164
+ end
165
+
166
+ #
167
+ # Checks if middleware has been added to the list.
168
+ #
169
+ # @param [Class] klass The middleware class to check.
170
+ #
171
+ # @return [Boolean] Return true if the middleware is in the list.
172
+ #
173
+ def exists?(klass)
174
+ any? { |entry| entry.klass == klass }
175
+ end
176
+
177
+ #
178
+ # Checks if the middlware list is empty
179
+ #
180
+ # @return [Boolean] Return true if the middleware list is empty.
181
+ #
182
+ def empty?
183
+ @entries.nil? || @entries.empty?
184
+ end
185
+
186
+ #
187
+ # Return a list of instantiated middlewares. Each middleware gets
188
+ # initialize with the args originally passed to `add`, `insert_before` etc.
189
+ #
190
+ # @return [Array<any>] The list of instantiated middlewares.
191
+ #
192
+ def retrieve
193
+ map(&:make_new)
194
+ end
195
+
196
+ #
197
+ # Empty the list of middlewares.
198
+ #
199
+ # @return [Array<Cloudenvoy::Middleware::Chain::Entry>] The updated list of middlewares
200
+ #
201
+ def clear
202
+ entries.clear
203
+ end
204
+
205
+ #
206
+ # Invoke the chain of middlewares.
207
+ #
208
+ # @param [Array<any>] *args The args to pass to each middleware.
209
+ #
210
+ def invoke(*args)
211
+ return yield if empty?
212
+
213
+ chain = retrieve.dup
214
+ traverse_chain = lambda do
215
+ if chain.empty?
216
+ yield
217
+ else
218
+ chain.shift.call(*args, &traverse_chain)
219
+ end
220
+ end
221
+ traverse_chain.call
222
+ end
223
+ end
224
+
225
+ # Middleware list item.
226
+ class Entry
227
+ attr_reader :klass, :args
228
+
229
+ #
230
+ # Build a new entry.
231
+ #
232
+ # @param [Class] klass The middleware class.
233
+ # @param [Array<any>] *args The list of arguments for the middleware.
234
+ #
235
+ def initialize(klass, *args)
236
+ @klass = klass
237
+ @args = args
238
+ end
239
+
240
+ #
241
+ # Return an instantiated middleware.
242
+ #
243
+ # @return [Any] The instantiated middleware.
244
+ #
245
+ def make_new
246
+ @klass.new(*@args)
247
+ end
248
+ end
249
+ end
250
+ end