action_webhook 0.1.0 → 0.1.1
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.
- checksums.yaml +4 -4
- data/lib/action_webhook/base.rb +132 -125
- data/lib/action_webhook/delivery_job.rb +6 -4
- data/lib/action_webhook/version.rb +1 -1
- data/lib/action_webhook.rb +3 -1
- metadata +8 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 54d0eddc17626872f1d795b8c25c4fda06719a1bfed4b185093c8803d442d2ad
|
4
|
+
data.tar.gz: ec2b67113640e9b1d7e4ddb397858a4d38724d01024f781b492906ed629dfc62
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5635f2e1541ecf586c52a37df1fb03b6e11e71585255cdbef8a01be30323e1d7a53f7f0ab053ff98dd7a8f3bc6e4e5d95dfbd1b0c1b86b3238577aa13bde0459
|
7
|
+
data.tar.gz: e97e0940c7bf16f20d4c6457d115a2054494af9c17fd9957c3f5537774a45374ee1c395a228de33689edac80ba9f3e3b367e82baa2a972b178b3ac9df5ac719e
|
data/lib/action_webhook/base.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActionWebhook
|
2
4
|
# Base class for defining and delivering webhooks
|
3
5
|
#
|
@@ -31,31 +33,31 @@ module ActionWebhook
|
|
31
33
|
include ActiveJob::SerializationAdapter::ObjectSerializer if defined?(ActiveJob::SerializationAdapter)
|
32
34
|
|
33
35
|
# Delivery configuration
|
34
|
-
|
36
|
+
class_attribute :delivery_job, instance_writer: false, default: -> { "ActionWebhook::DeliveryJob".constantize }
|
35
37
|
class_attribute :deliver_later_queue_name, instance_writer: false
|
36
38
|
class_attribute :default_headers, instance_writer: false, default: {}
|
37
39
|
class_attribute :delivery_method, instance_writer: false, default: :deliver_now
|
38
40
|
class_attribute :perform_deliveries, instance_writer: false, default: true
|
39
|
-
|
41
|
+
|
40
42
|
# Retry configuration
|
41
43
|
class_attribute :max_retries, instance_writer: false, default: 3
|
42
44
|
class_attribute :retry_delay, instance_writer: false, default: 30.seconds
|
43
45
|
class_attribute :retry_backoff, instance_writer: false, default: :exponential # :linear or :exponential
|
44
46
|
class_attribute :retry_jitter, instance_writer: false, default: 5.seconds
|
45
|
-
|
47
|
+
|
46
48
|
# Callbacks
|
47
49
|
class_attribute :after_deliver_callback, instance_writer: false
|
48
50
|
class_attribute :after_retries_exhausted_callback, instance_writer: false
|
49
|
-
|
51
|
+
|
50
52
|
# The webhook action that will be performed
|
51
53
|
attr_accessor :action_name
|
52
|
-
|
54
|
+
|
53
55
|
# The webhook details (URLs and headers) that will be used
|
54
56
|
attr_accessor :webhook_details
|
55
57
|
|
56
58
|
# Stores the execution params for later delivery
|
57
59
|
attr_accessor :params
|
58
|
-
|
60
|
+
|
59
61
|
# Current attempt number for retries
|
60
62
|
attr_accessor :attempts
|
61
63
|
|
@@ -70,7 +72,7 @@ module ActionWebhook
|
|
70
72
|
def deliver_now
|
71
73
|
@attempts += 1
|
72
74
|
response = process_webhook
|
73
|
-
|
75
|
+
|
74
76
|
# Call success callback if defined
|
75
77
|
if response.all? { |r| r[:success] }
|
76
78
|
invoke_callback(self.class.after_deliver_callback, response)
|
@@ -81,14 +83,14 @@ module ActionWebhook
|
|
81
83
|
# We've exhausted all retries
|
82
84
|
invoke_callback(self.class.after_retries_exhausted_callback, response)
|
83
85
|
end
|
84
|
-
|
86
|
+
|
85
87
|
response
|
86
88
|
end
|
87
89
|
|
88
90
|
# Helper method to invoke a callback that might be a symbol or a proc
|
89
91
|
def invoke_callback(callback, response)
|
90
92
|
return unless callback
|
91
|
-
|
93
|
+
|
92
94
|
case callback
|
93
95
|
when Symbol
|
94
96
|
send(callback, response)
|
@@ -113,7 +115,7 @@ module ActionWebhook
|
|
113
115
|
@action_name = caller_locations(1, 1)[0].label.to_sym
|
114
116
|
@webhook_details = webhook_details
|
115
117
|
@params = params
|
116
|
-
|
118
|
+
|
117
119
|
# Return self for chaining with delivery methods
|
118
120
|
DeliveryMessenger.new(self)
|
119
121
|
end
|
@@ -125,20 +127,20 @@ module ActionWebhook
|
|
125
127
|
def build_payload(variables = {})
|
126
128
|
# Combine instance variables and passed variables
|
127
129
|
assigns = {}
|
128
|
-
|
130
|
+
|
129
131
|
# Extract instance variables
|
130
132
|
instance_variables.each do |ivar|
|
131
133
|
# Skip internal instance variables
|
132
134
|
next if ivar.to_s.start_with?("@_")
|
133
|
-
next if [
|
134
|
-
|
135
|
+
next if %i[@action_name @webhook_details @params @attempts].include?(ivar)
|
136
|
+
|
135
137
|
# Add to assigns with symbol key (without @)
|
136
|
-
assigns[ivar.to_s[1
|
138
|
+
assigns[ivar.to_s[1..].to_sym] = instance_variable_get(ivar)
|
137
139
|
end
|
138
|
-
|
140
|
+
|
139
141
|
# Add passed variables
|
140
142
|
assigns.merge!(variables)
|
141
|
-
|
143
|
+
|
142
144
|
# Render the template
|
143
145
|
generate_json_from_template(@action_name, assigns)
|
144
146
|
end
|
@@ -150,46 +152,44 @@ module ActionWebhook
|
|
150
152
|
# @return [Array<Hash>] Array of response objects with status and body
|
151
153
|
def post_webhook(webhook_details, payloads)
|
152
154
|
responses = []
|
153
|
-
|
155
|
+
|
154
156
|
webhook_details.each_with_index do |detail, idx|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
logger.error("Webhook delivery failed: #{e.message} for URL: #{detail[:url]} (Attempt #{@attempts})")
|
190
|
-
end
|
157
|
+
# Ensure headers exists
|
158
|
+
detail[:headers] ||= {}
|
159
|
+
|
160
|
+
# Merge default headers
|
161
|
+
headers = default_headers.merge(detail[:headers])
|
162
|
+
|
163
|
+
# Add content type if not present
|
164
|
+
headers["Content-Type"] = "application/json" unless headers.key?("Content-Type")
|
165
|
+
|
166
|
+
# Add attempt tracking in headers
|
167
|
+
headers["X-Webhook-Attempt"] = @attempts.to_s if @attempts.positive?
|
168
|
+
|
169
|
+
response = HTTParty.post(
|
170
|
+
detail[:url],
|
171
|
+
body: payloads[idx].to_json,
|
172
|
+
headers: headers,
|
173
|
+
timeout: 10 # Add reasonable timeout
|
174
|
+
)
|
175
|
+
|
176
|
+
responses << {
|
177
|
+
success: response.success?,
|
178
|
+
status: response.code,
|
179
|
+
body: response.body,
|
180
|
+
url: detail[:url],
|
181
|
+
attempt: @attempts
|
182
|
+
}
|
183
|
+
rescue StandardError => e
|
184
|
+
responses << {
|
185
|
+
success: false,
|
186
|
+
error: e.message,
|
187
|
+
url: detail[:url],
|
188
|
+
attempt: @attempts
|
189
|
+
}
|
190
|
+
logger.error("Webhook delivery failed: #{e.message} for URL: #{detail[:url]} (Attempt #{@attempts})")
|
191
191
|
end
|
192
|
-
|
192
|
+
|
193
193
|
responses
|
194
194
|
end
|
195
195
|
|
@@ -198,158 +198,165 @@ module ActionWebhook
|
|
198
198
|
# @param event_name [Symbol] the name of the webhook method (e.g. `:created`)
|
199
199
|
# @param assigns [Hash] local variables to pass into the template
|
200
200
|
# @return [Hash] the parsed JSON payload
|
201
|
-
def generate_json_from_template(
|
201
|
+
def generate_json_from_template(input_event_name, assigns = {})
|
202
|
+
event_name = extract_method_name(input_event_name.to_s)
|
202
203
|
webhook_class_name = self.class.name.underscore
|
203
|
-
|
204
|
+
|
204
205
|
# Possible template locations
|
205
206
|
possible_paths = [
|
206
207
|
# Main app templates
|
207
208
|
File.join(Rails.root.to_s, "app/webhooks/#{webhook_class_name}/#{event_name}.json.erb"),
|
208
|
-
|
209
|
+
|
209
210
|
# Engine templates
|
210
211
|
engine_root && File.join(engine_root, "app/webhooks/#{webhook_class_name}/#{event_name}.json.erb"),
|
211
|
-
|
212
|
+
|
212
213
|
# Namespaced templates in engine
|
213
214
|
engine_root && self.class.module_parent != Object &&
|
214
|
-
File.join(engine_root,
|
215
|
+
File.join(engine_root,
|
216
|
+
"app/webhooks/#{self.class.module_parent.name.underscore}/#{webhook_class_name.split("/").last}/#{event_name}.json.erb")
|
215
217
|
].compact
|
216
|
-
|
218
|
+
|
217
219
|
# Find the first template that exists
|
218
220
|
template_path = possible_paths.find { |path| File.exist?(path) }
|
219
|
-
|
221
|
+
|
220
222
|
unless template_path
|
221
223
|
raise ArgumentError, "Template not found for #{event_name} in paths:\n#{possible_paths.join("\n")}"
|
222
224
|
end
|
223
|
-
|
225
|
+
|
224
226
|
template = ERB.new(File.read(template_path))
|
225
227
|
json = template.result_with_hash(assigns)
|
226
228
|
JSON.parse(json)
|
227
229
|
rescue JSON::ParserError => e
|
228
230
|
raise "Invalid JSON in template #{event_name}: #{e.message}"
|
229
231
|
end
|
230
|
-
|
232
|
+
|
233
|
+
def extract_method_name(path)
|
234
|
+
path.include?("#") ? path.split("#").last : path
|
235
|
+
end
|
236
|
+
|
231
237
|
# Process the webhook to generate and send the payload
|
232
238
|
def process_webhook
|
233
239
|
# Render the message
|
234
240
|
payloads = [build_payload]
|
235
|
-
|
241
|
+
|
236
242
|
# Post the webhook
|
237
243
|
post_webhook(webhook_details, payloads)
|
238
244
|
end
|
239
|
-
|
245
|
+
|
240
246
|
# Schedule a retry with appropriate backoff delay
|
241
247
|
# Modify the retry_with_backoff method:
|
242
248
|
def retry_with_backoff
|
243
249
|
delay = calculate_backoff_delay
|
244
|
-
|
250
|
+
|
245
251
|
logger.info("Scheduling webhook retry #{@attempts + 1}/#{self.class.max_retries} in #{delay} seconds")
|
246
|
-
|
252
|
+
|
247
253
|
# Get the actual job class by evaluating the proc
|
248
254
|
job_class = self.class.delivery_job.is_a?(Proc) ? self.class.delivery_job.call : self.class.delivery_job
|
249
|
-
|
255
|
+
|
250
256
|
# Serialize the webhook and pass the serialized data instead of the object
|
251
257
|
serialized_webhook = serialize
|
252
|
-
|
258
|
+
|
253
259
|
# Re-enqueue with the calculated delay
|
254
260
|
if deliver_later_queue_name
|
255
|
-
job_class.set(queue: deliver_later_queue_name, wait: delay).perform_later(
|
261
|
+
job_class.set(queue: deliver_later_queue_name, wait: delay).perform_later("deliver_now", serialized_webhook)
|
256
262
|
else
|
257
|
-
job_class.set(wait: delay).perform_later(
|
263
|
+
job_class.set(wait: delay).perform_later("deliver_now", serialized_webhook)
|
258
264
|
end
|
259
265
|
end
|
260
|
-
|
266
|
+
|
261
267
|
# Calculate delay based on retry strategy
|
262
268
|
def calculate_backoff_delay
|
263
269
|
base_delay = self.class.retry_delay
|
264
|
-
|
265
|
-
case self.class.retry_backoff
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
270
|
+
|
271
|
+
delay = case self.class.retry_backoff
|
272
|
+
when :exponential
|
273
|
+
# 30s, 60s, 120s, etc.
|
274
|
+
base_delay * (2**(@attempts - 1))
|
275
|
+
when :linear
|
276
|
+
# 30s, 60s, 90s, etc.
|
277
|
+
base_delay * @attempts
|
278
|
+
else
|
279
|
+
base_delay
|
280
|
+
end
|
281
|
+
|
276
282
|
# Add jitter to prevent thundering herd problem
|
277
283
|
jitter = rand(self.class.retry_jitter)
|
278
284
|
delay + jitter
|
279
285
|
end
|
280
|
-
|
286
|
+
|
281
287
|
# For ActiveJob serialization
|
282
288
|
def serialize
|
283
289
|
{
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
+
"action_name" => @action_name.to_s,
|
291
|
+
"webhook_details" => @webhook_details,
|
292
|
+
"params" => @params,
|
293
|
+
"attempts" => @attempts,
|
294
|
+
"instance_variables" => collect_instance_variables,
|
295
|
+
"webhook_class" => self.class.name
|
290
296
|
}
|
291
297
|
end
|
292
|
-
|
298
|
+
|
293
299
|
# Restores state from serialized data
|
294
300
|
def deserialize(data)
|
295
|
-
@action_name = data[
|
296
|
-
@webhook_details = data[
|
297
|
-
@params = data[
|
298
|
-
@attempts = data[
|
299
|
-
|
301
|
+
@action_name = data["action_name"].to_sym
|
302
|
+
@webhook_details = data["webhook_details"]
|
303
|
+
@params = data["params"]
|
304
|
+
@attempts = data["attempts"] || 0
|
305
|
+
|
300
306
|
# Restore instance variables
|
301
|
-
data[
|
307
|
+
data["instance_variables"].each do |name, value|
|
302
308
|
instance_variable_set("@#{name}", value)
|
303
309
|
end
|
304
310
|
end
|
305
|
-
|
311
|
+
|
306
312
|
private
|
307
|
-
|
313
|
+
|
308
314
|
# Collects all non-system instance variables for serialization
|
309
315
|
def collect_instance_variables
|
310
316
|
result = {}
|
311
317
|
instance_variables.each do |ivar|
|
312
318
|
# Skip internal instance variables
|
313
319
|
next if ivar.to_s.start_with?("@_")
|
314
|
-
next if [
|
315
|
-
|
320
|
+
next if %i[@action_name @webhook_details @params @attempts].include?(ivar)
|
321
|
+
|
316
322
|
# Add to result without @
|
317
|
-
result[ivar.to_s[1
|
323
|
+
result[ivar.to_s[1..]] = instance_variable_get(ivar)
|
318
324
|
end
|
319
325
|
result
|
320
326
|
end
|
321
|
-
|
327
|
+
|
322
328
|
# Find the engine root path
|
323
329
|
def engine_root
|
324
330
|
return nil unless defined?(Rails::Engine)
|
325
|
-
|
331
|
+
|
326
332
|
mod = self.class.module_parent
|
327
333
|
while mod != Object
|
328
|
-
constants = mod.constants.select
|
334
|
+
constants = mod.constants.select do |c|
|
329
335
|
const = mod.const_get(c)
|
330
336
|
const.is_a?(Class) && const < Rails::Engine
|
331
|
-
|
332
|
-
|
337
|
+
end
|
338
|
+
|
333
339
|
return mod.const_get(constants.first).root.to_s if constants.any?
|
340
|
+
|
334
341
|
mod = mod.module_parent
|
335
342
|
end
|
336
|
-
|
343
|
+
|
337
344
|
nil
|
338
345
|
end
|
339
|
-
|
346
|
+
|
340
347
|
# Similarly update enqueue_delivery:
|
341
348
|
def enqueue_delivery(delivery_method, options = {})
|
342
349
|
options = options.dup
|
343
350
|
queue = options.delete(:queue) || self.class.deliver_later_queue_name
|
344
|
-
|
351
|
+
|
345
352
|
args = [delivery_method.to_s]
|
346
|
-
|
353
|
+
|
347
354
|
# Get the actual job class by evaluating the proc
|
348
355
|
job_class = self.class.delivery_job.is_a?(Proc) ? self.class.delivery_job.call : self.class.delivery_job
|
349
|
-
|
356
|
+
|
350
357
|
# Serialize the webhook
|
351
358
|
serialized_webhook = serialize
|
352
|
-
|
359
|
+
|
353
360
|
# Use the delivery job to perform the delivery with serialized data
|
354
361
|
if queue
|
355
362
|
job_class.set(queue: queue).perform_later(*args, serialized_webhook)
|
@@ -358,12 +365,12 @@ module ActionWebhook
|
|
358
365
|
job_class.perform_later(*args, serialized_webhook) unless options[:wait]
|
359
366
|
end
|
360
367
|
end
|
361
|
-
|
368
|
+
|
362
369
|
# Logger for webhook errors
|
363
370
|
def logger
|
364
371
|
Rails.logger
|
365
372
|
end
|
366
|
-
|
373
|
+
|
367
374
|
# Class methods
|
368
375
|
class << self
|
369
376
|
# Handle method calls on the class
|
@@ -371,7 +378,7 @@ module ActionWebhook
|
|
371
378
|
if public_instance_methods(false).include?(method_name)
|
372
379
|
# Create a new instance
|
373
380
|
webhook = new
|
374
|
-
|
381
|
+
|
375
382
|
# Call the instance method
|
376
383
|
webhook.send(method_name, *args, &block)
|
377
384
|
else
|
@@ -382,35 +389,35 @@ module ActionWebhook
|
|
382
389
|
def respond_to_missing?(method_name, include_private = false)
|
383
390
|
public_instance_methods(false).include?(method_name) || super
|
384
391
|
end
|
385
|
-
|
392
|
+
|
386
393
|
# Test delivery collection
|
387
394
|
def deliveries
|
388
395
|
@deliveries ||= []
|
389
396
|
end
|
390
|
-
|
397
|
+
|
391
398
|
# Reset the test delivery collection
|
392
399
|
def clear_deliveries
|
393
400
|
@deliveries = []
|
394
401
|
end
|
395
|
-
|
402
|
+
|
396
403
|
# Register callback for successful delivery
|
397
404
|
def after_deliver(method_name = nil, &block)
|
398
405
|
self.after_deliver_callback = block_given? ? block : method_name
|
399
406
|
end
|
400
|
-
|
407
|
+
|
401
408
|
# Register callback for when retries are exhausted
|
402
409
|
def after_retries_exhausted(method_name = nil, &block)
|
403
410
|
self.after_retries_exhausted_callback = block_given? ? block : method_name
|
404
411
|
end
|
405
412
|
end
|
406
413
|
end
|
407
|
-
|
414
|
+
|
408
415
|
# Delivery messenger for ActionMailer-like API
|
409
416
|
class DeliveryMessenger
|
410
417
|
def initialize(webhook)
|
411
418
|
@webhook = webhook
|
412
419
|
end
|
413
|
-
|
420
|
+
|
414
421
|
def deliver_now
|
415
422
|
if @webhook.respond_to?(:perform_deliveries) && !@webhook.perform_deliveries
|
416
423
|
# Skip delivery
|
@@ -423,9 +430,9 @@ module ActionWebhook
|
|
423
430
|
@webhook.deliver_now
|
424
431
|
end
|
425
432
|
end
|
426
|
-
|
433
|
+
|
427
434
|
def deliver_later(options = {})
|
428
435
|
@webhook.deliver_later(options)
|
429
436
|
end
|
430
437
|
end
|
431
|
-
end
|
438
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActionWebhook
|
2
4
|
# Job responsible for delivering webhooks in the background
|
3
5
|
class DeliveryJob < ActiveJob::Base
|
@@ -9,10 +11,10 @@ module ActionWebhook
|
|
9
11
|
# @param serialized_webhook [Hash] The serialized webhook data
|
10
12
|
def perform(delivery_method, serialized_webhook)
|
11
13
|
# Reconstruct the webhook from serialized data
|
12
|
-
webhook_class = serialized_webhook[
|
14
|
+
webhook_class = serialized_webhook["webhook_class"].constantize
|
13
15
|
webhook = webhook_class.new
|
14
16
|
webhook.deserialize(serialized_webhook)
|
15
|
-
|
17
|
+
|
16
18
|
# Invoke the specified delivery method
|
17
19
|
webhook.send(delivery_method)
|
18
20
|
end
|
@@ -22,9 +24,9 @@ module ActionWebhook
|
|
22
24
|
# Log the error
|
23
25
|
Rails.logger.error("ActionWebhook delivery failed: #{exception.message}")
|
24
26
|
Rails.logger.error(exception.backtrace.join("\n"))
|
25
|
-
|
27
|
+
|
26
28
|
# Re-raise the exception if in development or test
|
27
29
|
raise exception if Rails.env.development? || Rails.env.test?
|
28
30
|
end
|
29
31
|
end
|
30
|
-
end
|
32
|
+
end
|
data/lib/action_webhook.rb
CHANGED
metadata
CHANGED
@@ -1,42 +1,42 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: action_webhook
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vinay Uttam Vemparala
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-05-27 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
|
-
name:
|
13
|
+
name: httparty
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
15
15
|
requirements:
|
16
16
|
- - "~>"
|
17
17
|
- !ruby/object:Gem::Version
|
18
|
-
version:
|
18
|
+
version: 0.18.1
|
19
19
|
type: :runtime
|
20
20
|
prerelease: false
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
22
22
|
requirements:
|
23
23
|
- - "~>"
|
24
24
|
- !ruby/object:Gem::Version
|
25
|
-
version:
|
25
|
+
version: 0.18.1
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
|
-
name:
|
27
|
+
name: rails
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
29
29
|
requirements:
|
30
30
|
- - "~>"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version:
|
32
|
+
version: '7'
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
36
36
|
requirements:
|
37
37
|
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version:
|
39
|
+
version: '7'
|
40
40
|
- !ruby/object:Gem::Dependency
|
41
41
|
name: yard
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|