sm_sms_campaign_webhook 0.1.1 → 2.1.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -0
  3. data/README.md +340 -7
  4. data/app/controllers/sm_sms_campaign_webhook/application_controller.rb +22 -0
  5. data/app/controllers/sm_sms_campaign_webhook/webhook_controller.rb +25 -0
  6. data/app/exceptions/sm_sms_campaign_webhook/error.rb +6 -0
  7. data/app/exceptions/sm_sms_campaign_webhook/invalid_payload.rb +6 -0
  8. data/app/exceptions/sm_sms_campaign_webhook/invalid_payload_value.rb +6 -0
  9. data/app/exceptions/sm_sms_campaign_webhook/missing_config_error.rb +6 -0
  10. data/app/exceptions/sm_sms_campaign_webhook/payload_dispatch_error.rb +6 -0
  11. data/app/jobs/sm_sms_campaign_webhook/application_job.rb +7 -0
  12. data/app/jobs/sm_sms_campaign_webhook/dispatch_payload_job.rb +14 -0
  13. data/app/jobs/sm_sms_campaign_webhook/process_campaign_engagement_job.rb +14 -0
  14. data/app/models/sm_sms_campaign_webhook/campaign_engagement/answer.rb +76 -0
  15. data/app/models/sm_sms_campaign_webhook/campaign_engagement.rb +216 -0
  16. data/app/operations/sm_sms_campaign_webhook/campaign_engagement_operation.rb +36 -0
  17. data/app/operations/sm_sms_campaign_webhook/payload_operation.rb +26 -0
  18. data/app/processors/sm_sms_campaign_webhook/default_processor.rb +11 -0
  19. data/app/processors/sm_sms_campaign_webhook/processable.rb +21 -0
  20. data/config/routes.rb +7 -0
  21. data/lib/generators/sm_sms_campaign_webhook/install/USAGE +12 -0
  22. data/lib/generators/sm_sms_campaign_webhook/install/install_generator.rb +35 -0
  23. data/lib/generators/sm_sms_campaign_webhook/install/templates/README +25 -0
  24. data/lib/generators/sm_sms_campaign_webhook/install/templates/sm_sms_campaign_webhook.rb +9 -0
  25. data/lib/generators/sm_sms_campaign_webhook/install/templates/sms_payload_processor.rb.erb +13 -0
  26. data/lib/sm_sms_campaign_webhook/engine.rb +11 -0
  27. data/lib/sm_sms_campaign_webhook/version.rb +1 -1
  28. data/lib/sm_sms_campaign_webhook.rb +25 -2
  29. data/sm_sms_campaign_webhook.gemspec +35 -21
  30. metadata +87 -22
  31. data/.gitignore +0 -16
  32. data/.rspec +0 -3
  33. data/.travis.yml +0 -18
  34. data/Gemfile +0 -4
  35. data/Rakefile +0 -6
  36. data/bin/console +0 -14
  37. data/bin/setup +0 -8
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module SmSmsCampaignWebhook
6
+ # Data model for campaign engagement event from SMS campaign.
7
+ class CampaignEngagement
8
+ attr_reader :payload
9
+
10
+ # @param payload [Hash] Campaign engagement event payload
11
+ def initialize(payload:)
12
+ @payload = payload.deep_dup.freeze
13
+ end
14
+
15
+ # @return [String] Campaign engagement event UUID
16
+ # @raise [InvalidPayload] when uuid missing from payload
17
+ def event_uuid
18
+ @event_uuid ||= String(
19
+ payload.fetch("uuid") {
20
+ raise_invalid_payload_for("uuid")
21
+ }.freeze
22
+ )
23
+ end
24
+
25
+ # @return [String] Campaign engagement event type
26
+ # @raise [InvalidPayload] when type missing from payload
27
+ def event_type
28
+ @event_type ||= String(
29
+ payload.fetch("type") {
30
+ raise_invalid_payload_for("type")
31
+ }.freeze
32
+ )
33
+ end
34
+
35
+ # @return [DateTime] Campaign engagement event timestamp
36
+ # @raise [InvalidPayload] when created_at missing from payload
37
+ # @raise [InvalidPayloadValue] when created_at not datetime
38
+ def event_created_at
39
+ @event_created_at ||= begin
40
+ raw_created_at = payload.fetch("created_at") {
41
+ raise_invalid_payload_for("created_at")
42
+ }
43
+ DateTime.parse(raw_created_at).freeze
44
+ end
45
+ rescue ArgumentError
46
+ raise InvalidPayloadValue,
47
+ "created_at has invalid datetime value #{payload.inspect}"
48
+ end
49
+
50
+ # @return [Integer] ID of the engaged campaign
51
+ # @raise [InvalidPayload] when campaign id missing from payload
52
+ # @raise [InvalidPayloadValue] when campaign id not numeric
53
+ def campaign_id
54
+ @campaign_id ||= Integer(
55
+ campaign_hash.fetch("id") {
56
+ raise_invalid_payload_for("campaign id")
57
+ }
58
+ )
59
+ rescue ArgumentError
60
+ raise InvalidPayloadValue,
61
+ "campaign id has invalid integer value #{payload.inspect}"
62
+ end
63
+
64
+ # @return [String] Keyword of the engaged campaign
65
+ # @raise [InvalidPayload] when campaign keyword missing from payload
66
+ def campaign_keyword
67
+ @campaign_keyword ||= String(
68
+ campaign_hash.fetch("keyword") {
69
+ raise_invalid_payload_for("campaign keyword")
70
+ }.freeze
71
+ )
72
+ end
73
+
74
+ # @return [Integer] ID of the engaging phone
75
+ # @raise [InvalidPayload] when phone id missing from payload
76
+ # @raise [InvalidPayloadValue] when phone id not numeric
77
+ def phone_id
78
+ @phone_id ||= Integer(
79
+ phone_hash.fetch("id") {
80
+ raise_invalid_payload_for("phone id")
81
+ }
82
+ )
83
+ rescue ArgumentError
84
+ raise InvalidPayloadValue,
85
+ "phone id has invalid integer value #{payload.inspect}"
86
+ end
87
+
88
+ # @return [String] Phone number engaging the campaign
89
+ # @raise [InvalidPayload] when phone number missing from payload
90
+ def phone_number
91
+ @phone_number ||= String(
92
+ phone_hash.fetch("number") {
93
+ raise_invalid_payload_for("phone number")
94
+ }.freeze
95
+ )
96
+ end
97
+
98
+ # @return [Integer] ID of campaign engagement state record
99
+ # @raise [InvalidPayload] when phone_campaign_state id missing from payload
100
+ # @raise [InvalidPayloadValue] when phone_campaign_state id not numeric
101
+ def phone_campaign_state_id
102
+ @phone_campaign_state_id ||= Integer(
103
+ phone_campaign_state_hash.fetch("id") {
104
+ raise_invalid_payload_for("phone_campaign_state id")
105
+ }
106
+ )
107
+ rescue ArgumentError
108
+ raise InvalidPayloadValue,
109
+ "phone_campaign_state id has invalid integer value #{payload.inspect}"
110
+ end
111
+
112
+ # @return [TrueClass,FalseClass] Has campaign engagement completed?
113
+ # @raise [InvalidPayload] when phone_campaign_state completed missing from payload
114
+ # @raise [InvalidPayloadValue] when phone_campaign_state completed is not boolean
115
+ def phone_campaign_state_completed?
116
+ # Has the boolean value already been assigned?
117
+ unless @phone_campaign_state_completed.nil?
118
+ return @phone_campaign_state_completed
119
+ end
120
+
121
+ # Extract the value and memoize it.
122
+ @phone_campaign_state_completed = begin
123
+ completed = phone_campaign_state_hash
124
+ .fetch("completed") {
125
+ raise_invalid_payload_for("phone_campaign_state completed")
126
+ }
127
+
128
+ # Is this a boolean value?
129
+ if [true, false].none?(completed)
130
+ raise InvalidPayloadValue,
131
+ "phone_campaign_state completed has invalid boolean value #{payload.inspect}"
132
+ end
133
+
134
+ completed
135
+ end
136
+ end
137
+
138
+ # @return [DateTime,NilClass] Timestamp of campaign engagement completion if completed
139
+ # @raise [InvalidPayload] when phone_campaign_state completed_at missing from payload
140
+ # @raise [InvalidPayloadValue] when phone_campaign_state completed_at not datetime
141
+ def phone_campaign_state_completed_at
142
+ @phone_campaign_state_completed_at ||= begin
143
+ raw_completed_at = phone_campaign_state_hash.fetch("completed_at") {
144
+ raise_invalid_payload_for("phone_campaign_state completed_at")
145
+ }
146
+ DateTime.parse(raw_completed_at).freeze if raw_completed_at
147
+ end
148
+ rescue ArgumentError
149
+ raise InvalidPayloadValue,
150
+ "phone_campaign_state completed_at has invalid datetime value #{payload.inspect}"
151
+ end
152
+
153
+ # @return [Array<Answer>] Modeled campaign engagement answers
154
+ # @raise [InvalidPayload] when phone_campaign_state answers missing from payload
155
+ # @raise [InvalidPayloadValue] when phone_campaign_state answers not hash
156
+ def phone_campaign_state_answers
157
+ @phone_campaign_state_answers ||= begin
158
+ # Extract answers data from payload.
159
+ data = phone_campaign_state_hash.fetch("answers") {
160
+ raise_invalid_payload_for("phone_campaign_state answers")
161
+ }
162
+
163
+ # Is this hash data?
164
+ unless data.is_a?(Hash)
165
+ raise InvalidPayloadValue,
166
+ "phone_campaign_state answers has invalid hash value #{payload.inspect}"
167
+ end
168
+
169
+ # Cast answers data.
170
+ Answer.cast(data: data).freeze
171
+ end
172
+ end
173
+
174
+ # @return [TrueClass,FalseClass] Are any campaign engagement answers present?
175
+ def phone_campaign_state_answers?
176
+ !phone_campaign_state_answers.empty?
177
+ end
178
+
179
+ # @param field [String] Answer data to find
180
+ # @return [Answer,NilClass] Modeled answer for field when found
181
+ def answer_for(field:)
182
+ phone_campaign_state_answers.detect do |answer|
183
+ answer.field == field
184
+ end
185
+ end
186
+
187
+ private
188
+
189
+ # @param attr [String] Expected attribute missing from payload
190
+ def raise_invalid_payload_for(attr)
191
+ raise InvalidPayload, "#{attr} missing from payload #{payload.inspect}"
192
+ end
193
+
194
+ # @return [Hash] Data from campaign engagement payload
195
+ def payload_data
196
+ @payload_data ||= payload.fetch("data", {}).freeze
197
+ end
198
+
199
+ # @return [Hash] Campaign hash from payload data
200
+ def campaign_hash
201
+ @campaign_hash ||= payload_data.fetch("campaign", {}).freeze
202
+ end
203
+
204
+ # @return [Hash] Phone hash from payload data
205
+ def phone_hash
206
+ @phone_hash ||= payload_data.fetch("phone", {}).freeze
207
+ end
208
+
209
+ # @return [Hash] Campaign engagement state hash from payload data
210
+ def phone_campaign_state_hash
211
+ @phone_campaign_state_hash ||= payload_data
212
+ .fetch("phone_campaign_state", {})
213
+ .freeze
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmSmsCampaignWebhook
4
+ # Handles campaign engagement payload data modeling and processing.
5
+ class CampaignEngagementOperation
6
+ # Models the campaign engagement payload and hands of processing
7
+ # of the data to the processor.
8
+ #
9
+ # @param payload [Hash] Deserialized SMS campaign engagement payload
10
+ # @raise [PayloadDispatchError] when not campaign engagement payload
11
+ # @see .processor
12
+ def self.process(payload:)
13
+ logger.debug "#{name} - Processing campaign engagement payload: #{payload.inspect}"
14
+
15
+ if payload.fetch("type") != "campaign.engagement"
16
+ raise PayloadDispatchError,
17
+ "dispatched payload different from campaign.engagement #{payload.inspect}"
18
+ end
19
+
20
+ campaign_engagement = CampaignEngagement.new(payload: payload)
21
+ processor.process_campaign_engagement(campaign_engagement)
22
+ end
23
+
24
+ # @return [Processable] Configured SMS campaign payload processor
25
+ # @see SmSmsCampaignWebhook.processor
26
+ def self.processor
27
+ @processor ||= SmSmsCampaignWebhook.processor
28
+ end
29
+
30
+ # @return [ActiveSupport::Logger] Abstraction of app logger
31
+ def self.logger
32
+ @logger ||= Rails.logger
33
+ end
34
+ private_class_method :logger
35
+ end
36
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmSmsCampaignWebhook
4
+ # Handles payload data modeling and data processing.
5
+ class PayloadOperation
6
+ # @param payload [Hash] Deserialized payload from SMS campaign service
7
+ # @see ProcessCampaignEngagementJob#perform
8
+ def self.dispatch(payload:)
9
+ logger.debug "#{name} - Dispatching payload: #{payload.inspect}"
10
+
11
+ case payload.fetch("type", "unknown")
12
+ when "campaign.engagement"
13
+ ProcessCampaignEngagementJob.perform_later(payload)
14
+ else
15
+ # NOOP - Unsupported event type.
16
+ logger.warn "#{name} - Unsupported event type"
17
+ end
18
+ end
19
+
20
+ # @return [ActiveSupport::Logger] Abstraction of app logger
21
+ def self.logger
22
+ @logger ||= Rails.logger
23
+ end
24
+ private_class_method :logger
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "sm_sms_campaign_webhook/processable"
4
+
5
+ module SmSmsCampaignWebhook
6
+ # Default processor with NOOP implementations.
7
+ # @see Processable
8
+ class DefaultProcessor
9
+ include Processable
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmSmsCampaignWebhook
4
+ # Define behavior that SMS campaign payload processors must implement.
5
+ module Processable
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ # Campaign engagement operation sends modeled campaign engagement payload
10
+ # to this method for applying business logic. Implementors should define
11
+ # the app behavior to properly handle this data.
12
+ #
13
+ # @param campaign_engagement [CampaignEngagement] modeled payload
14
+ # @raise [NotImplementedError] requiring implementing class to define behavior
15
+ def process_campaign_engagement(campaign_engagement)
16
+ raise NotImplementedError,
17
+ "#{self.class} must implement .process_campaign_engagement receiving campaign_engagement param"
18
+ end
19
+ end
20
+ end
21
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ SmSmsCampaignWebhook::Engine.routes.draw do
4
+ scope "/api" do
5
+ resources :webhook, only: [:create]
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ Description:
2
+ Configures the app to handle inbound requests from the SMS campaign service.
3
+
4
+ Example:
5
+ rails generate sm_sms_campaign_webhook:install
6
+
7
+ This will create:
8
+ app/processors/sms_payload_processor.rb
9
+ config/initializers/sm_sms_campaign_webhook.rb
10
+
11
+ This will mount the engine in:
12
+ config/routes.rb
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module SmSmsCampaignWebhook
6
+ # Namespace for generators provided by the gem.
7
+ module Generators
8
+ # Installs files to prep an app for SMS campaign webhook.
9
+ class InstallGenerator < Rails::Generators::Base
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ # Copy initializer template to config/initializers
13
+ def copy_initializer
14
+ template "sm_sms_campaign_webhook.rb", "config/initializers/sm_sms_campaign_webhook.rb"
15
+ end
16
+
17
+ # Copy processor template to app/processors
18
+ def copy_processor
19
+ template "sms_payload_processor.rb.erb", "app/processors/sms_payload_processor.rb"
20
+ end
21
+
22
+ # Mount engine to path in config/routes.rb
23
+ def add_mount_path
24
+ insert_into_file "config/routes.rb",
25
+ " mount SmSmsCampaignWebhook::Engine => \"/sms_campaign\"\n",
26
+ after: "Rails.application.routes.draw do\n"
27
+ end
28
+
29
+ # Dump the README for the app developer
30
+ def show_readme
31
+ readme "README" if behavior == :invoke
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ ================================================================================
2
+
3
+ Some setup you must do manually if you haven't yet:
4
+
5
+ 1. Set the ENV value SM_SMS_CAMPAIGN_WEBHOOK_AUTH_TOKEN with a Rails secret:
6
+
7
+ $ bundle exec rails secret
8
+
9
+ 2. Prep the app for ActiveJob using Sidekiq. In config/application.rb:
10
+
11
+ config.active_job.queue_adapter = :sidekiq
12
+
13
+ In Procfile or appropriate app launch config:
14
+
15
+ worker: bundle exec sidekiq --config config/sidekiq.yml
16
+
17
+ 3. Implement SmsPayloadProcessor behavior. Remember, the generated processor
18
+ provides the skeleton but will raise errors out of the box. Data handling
19
+ is app specific!
20
+
21
+ This will configure the app to be ready to accept SMS campaign service payloads.
22
+
23
+ Happy hacking!
24
+
25
+ ================================================================================
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sm_sms_campaign_webhook"
4
+
5
+ SmSmsCampaignWebhook.config do |config|
6
+ # SMS campaign payload processor implementing SmSmsCampaignWebhook::Processable behavior.
7
+ # default: SmSmsCampaignWebhook::DefaultProcessor (raises errors for processing)
8
+ config.processor = SmsPayloadProcessor
9
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Processes SMS campaign service payloads.
4
+ # @see https://github.com/SouthernMade/sm_sms_campaign_webhook/blob/v<%= SmSmsCampaignWebhook::VERSION %>/app/processors/sm_sms_campaign_webhook/processable.rb SmSmsCampaignWebhook::Processable
5
+ class SmsPayloadProcessor
6
+ include SmSmsCampaignWebhook::Processable
7
+
8
+ # @param campaign_engagement [SmSmsCampaignWebhook::CampaignEngagement]
9
+ # @see https://github.com/SouthernMade/sm_sms_campaign_webhook/blob/v<%= SmSmsCampaignWebhook::VERSION %>/app/models/sm_sms_campaign_webhook/campaign_engagement.rb SmSmsCampaignWebhook::CampaignEngagement
10
+ # @todo Implement business logic to process campaign engagement.
11
+ # def self.process_campaign_engagement(campaign_engagement)
12
+ # end
13
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+
5
+ module SmSmsCampaignWebhook
6
+ # General Rails engine configuration.
7
+ class Engine < ::Rails::Engine
8
+ isolate_namespace SmSmsCampaignWebhook
9
+ config.generators.api_only = true
10
+ end
11
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module SmSmsCampaignWebhook
4
4
  # @return [String] gem version
5
- VERSION = "0.1.1"
5
+ VERSION = "2.1.0"
6
6
  end
@@ -1,9 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "sm_sms_campaign_webhook/engine"
3
4
  require "sm_sms_campaign_webhook/version"
4
5
 
5
6
  # Namespace for SMS campaign webhook.
6
7
  module SmSmsCampaignWebhook
7
- # General base error type for custom errors.
8
- class Error < StandardError; end
8
+ # @return [SmSmsCampaignWebhook] self for configuration purposes
9
+ def self.config(&block)
10
+ yield self if block
11
+ end
12
+
13
+ # @return [String] SMS campaign webhook auth token
14
+ # @raise [MissingConfigError] when ENV does not contain SM_SMS_CAMPAIGN_WEBHOOK_AUTH_TOKEN value
15
+ def self.auth_token
16
+ @auth_token ||= ENV.fetch("SM_SMS_CAMPAIGN_WEBHOOK_AUTH_TOKEN") {
17
+ raise MissingConfigError,
18
+ "ENV does not contain SM_SMS_CAMPAIGN_WEBHOOK_AUTH_TOKEN value"
19
+ }
20
+ end
21
+
22
+ # @return [Processable] SMS campaign payload processor used by operations
23
+ def self.processor
24
+ @processor ||= DefaultProcessor
25
+ end
26
+
27
+ # @param processor [Processable] Custom SMS campaign payload processor
28
+ # @see Processable
29
+ def self.processor=(processor)
30
+ @processor = processor
31
+ end
9
32
  end
@@ -1,34 +1,48 @@
1
+ # frozen_string_literal: true
2
+
1
3
  lib = File.expand_path("lib", __dir__)
2
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
5
  require "sm_sms_campaign_webhook/version"
4
6
 
5
7
  Gem::Specification.new do |spec|
6
- spec.name = "sm_sms_campaign_webhook"
7
- spec.version = SmSmsCampaignWebhook::VERSION
8
- spec.authors = ["Cameron Dykes", "Matt Mueller"]
9
- spec.email = ["cameron@southernmade.com", "matt@southernmade.com"]
10
-
11
- spec.summary = %q{Middleware providing webhook for Southern Made SMS Campaign Engagement.}
12
- spec.homepage = "https://github.com/SouthernMade/sm_sms_campaign_webhook"
13
- spec.license = "MIT"
14
-
15
- spec.metadata["allowed_push_host"] = "https://rubygems.org"
8
+ spec.name = "sm_sms_campaign_webhook"
9
+ spec.version = SmSmsCampaignWebhook::VERSION
10
+ spec.license = "MIT"
11
+ spec.summary = "Middleware providing webhook for Southern Made SMS Campaign Engagement."
12
+ spec.description = "Middleware providing webhook for Southern Made SMS Campaign Engagement."
13
+ spec.homepage = "https://github.com/SouthernMade/sm_sms_campaign_webhook"
14
+ spec.authors = ["Cameron Dykes", "Matt Mueller"]
15
+ spec.email = ["cameron@southernmade.com", "matt@southernmade.com"]
16
16
 
17
- spec.metadata["homepage_uri"] = spec.homepage
18
- spec.metadata["source_code_uri"] = "https://github.com/SouthernMade/sm_sms_campaign_webhook"
19
- spec.metadata["changelog_uri"] = "https://github.com/SouthernMade/sm_sms_campaign_webhook/blob/develop/CHANGELOG.md"
17
+ spec.metadata = {
18
+ "allowed_push_host" => "https://rubygems.org",
19
+ "changelog_uri" => "#{spec.homepage}/blob/develop/CHANGELOG.md",
20
+ "documentation_uri" => "https://www.rubydoc.info/gems/sm_sms_campaign_webhook/#{spec.version}",
21
+ "homepage_uri" => spec.homepage,
22
+ "source_code_uri" => spec.homepage
23
+ }
20
24
 
21
25
  # Specify which files should be added to the gem when it is released.
22
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
24
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
- end
26
- spec.bindir = "exe"
27
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
+ spec.files = Dir[
27
+ "{app,config,lib}/**/*",
28
+ "CHANGELOG.md",
29
+ "LICENSE.txt",
30
+ "README.md",
31
+ "sm_sms_campaign_webhook.gemspec"
32
+ ]
28
33
  spec.require_paths = ["lib"]
29
34
 
35
+ # Required version of Ruby guided by Rails.
36
+ spec.required_ruby_version = ">= 2.7.5"
37
+
38
+ # Runtime dependencies.
39
+ spec.add_dependency "rails", [">= 6.0", "< 7.1"]
40
+
41
+ # Development + test dependencies.
30
42
  spec.add_development_dependency "bundler", "~> 2.0"
31
- spec.add_development_dependency "rake", "~> 12.0"
32
- spec.add_development_dependency "rspec", "~> 3.0"
43
+ spec.add_development_dependency "rake", "~> 13.0"
44
+ spec.add_development_dependency "rspec-rails", "~> 5.0"
45
+ spec.add_development_dependency "simplecov", "~> 0.20"
46
+ spec.add_development_dependency "standard", "~> 1.0"
33
47
  spec.add_development_dependency "yard", "~> 0.9"
34
48
  end