sms_traffic_sdk 0.0.7 → 0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6eb747b3922af353ca77e265506afa922d1cf736d1b73e744ddaaee3fe0a2d4d
4
- data.tar.gz: 261aa9a1507a0f72c438b0175f506eb2c12725dc22d27c246f5278418b609216
3
+ metadata.gz: c877028062a36207636853033e3e623612a7ae44daf41d91686d86840f3b75c8
4
+ data.tar.gz: bd57b25436c7ea9e76d0e68b213730e7f77cf271f28c170d70234a2080e38a97
5
5
  SHA512:
6
- metadata.gz: 640f165d9e8ed5e9a778e6f35b2e6a735b666e50510c401c75dc28ddfb642a471cbb64fcf1ca3eacfb64fba2e8b7e3e6185a383d2c46b2957041bba73059da09
7
- data.tar.gz: 27e9bad83adcce3984164a924e37cce3ae6d6e9339e465f3eb6ce394abd905b97e2708b2574233ee2c51e0cace159dcf3a19e51beb57da3f883c308fb21a8c73
6
+ metadata.gz: 47f4123facc0dcb03f1fbf072cc266199df3fcf7483d6189fdfcee257988d188bec2607aae674575c9efb3153b36bdf69c9a6ac8c9af972bfdc878c0ca4d8577
7
+ data.tar.gz: 96bf4d2e9ea69cc8d6aad0aa87165f57e88b5163b00fffd034af98ee269d87f257cffdce2731e5793bdb6c539f213f06420db805978591da118b3bcf5cf6cb6c
data/CHANGELOG.md ADDED
@@ -0,0 +1,64 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-11-03
4
+
5
+ ### Breaking changes
6
+
7
+ - Ruby 3.1 or newer is required.
8
+ - Zeitwerk is no longer a dependency; files are loaded with `require_relative`.
9
+ - `SmsTraffic::Configuration` is a plain class, not a `Singleton`. Defaults changed: `logger` is `nil` (was `$stdout`),
10
+ new `open_timeout` (10) and `read_timeout` (30). `originator` is no longer required by `configure`.
11
+ `Configuration#values_at` is removed.
12
+ - `SmsTraffic::Client::Response` is removed. `Client.deliver` returns a `DeliverReply` and `Client.status` a
13
+ `StatusReply` directly, and raise `SmsTraffic::ConnectionError`, `SmsTraffic::HttpError` or `SmsTraffic::ApiError`
14
+ (all `SmsTraffic::Error`) instead of returning a failed response.
15
+ - `Reply#hash` no longer returns the parsed reply (it is `Object#hash` again); use `Reply#to_h`.
16
+ - `DeliverReply#ok?` is strictly boolean and requires a queued message. `#sms_ids` is an Array of every sms id (it was
17
+ a Hash keyed by phone). `#message_infos` is replaced by `#messages`. `#error_description` reads
18
+ `"Sms has not been enqueued: <description>"`.
19
+ - `StatusReply#sms_array` is replaced by `#entries` (`StatusReply::Entry`); `#entry`, `#error?`, `#not_found?`
20
+ and `#send_date` are new.
21
+ - `Sms#response` is replaced by `Sms#reply`. `Sms#update_status` always returns the status String, and `Sms#deliver`
22
+ records `SmsTraffic::Error`s in `errors` instead of the HTTP code.
23
+ - The `konsierge-sms.rb` executable is removed.
24
+ - The default XML parser no longer typecasts (`Nori.new(advanced_typecasting: false)`): dates and `true` stay
25
+ provider strings.
26
+ - `nori` 2.8 or newer is required.
27
+
28
+ ### Fixed
29
+
30
+ - `Sms` phone validation no longer accepts a newline-separated value (`\A`/`\z` anchors).
31
+ - `Client.status` with several ids sends a flat comma-separated `sms_id` list.
32
+ - The client builds its URI from the current `host` on every request instead of memoizing the first one.
33
+ - `open_timeout` and `read_timeout` are applied to every request.
34
+ - `Reply` no longer mutates (or fails on) frozen input.
35
+ - A multi-part SMS keeps every `sms_id`; a single `<message_info>` with a phone is parsed.
36
+ - `require 'sms_traffic_sdk'` works with `Bundler.require`.
37
+ - Loading the gem without a preloaded `StringIO` no longer fails with `NameError` (nori 2.7.0 missed
38
+ `require 'stringio'`; fixed in nori 2.8).
39
+
40
+ ### Changed
41
+
42
+ - CI runs lint and RSpec on Ruby 3.1-3.4, smoke-tests the built gem in a clean Ruby process, uploads Cobertura
43
+ coverage to Codecov, and publishes to RubyGems through trusted publishing (OIDC) from a published release whose tag
44
+ matches the gem version. The gem package contains only `lib`, `README.md`, `LICENSE` and `CHANGELOG.md`.
45
+
46
+ ### Security
47
+
48
+ - `Net::HTTP#set_debug_output` is no longer used, so the login, password and SMS text are never written to the logger.
49
+ Debug logging emits one request line with `login`, `password` and `message` filtered, and one response line with
50
+ the HTTP status and body size only.
51
+
52
+ ### Migration
53
+
54
+ - Replace `Client.deliver(...).reply` / `.success?` with the returned reply and `rescue SmsTraffic::Error`.
55
+ - Replace `reply.hash` with `reply.to_h`, and `sms.response` with `sms.reply`.
56
+ - Treat `reply.sms_ids` as an Array; use `reply.messages` for per-phone data.
57
+ - Set `config.logger` explicitly if you relied on the `$stdout` default with `debug = true`.
58
+ - `Client.deliver` and `Client.status` no longer raise `SocketError`, `SystemCallError` (`Errno::*`), `IOError`,
59
+ `Net::OpenTimeout`, `Net::ReadTimeout` or `OpenSSL::SSL::SSLError`; rescue `SmsTraffic::ConnectionError` (the
60
+ original exception is in `cause`). `Sms#deliver` and `Sms#update_status` already rescue `SmsTraffic::Error` into
61
+ `errors`.
62
+ - Replace `response.as_json` / `sms.response.as_json` with `reply.to_h` / `sms.reply&.to_h`.
63
+ - Replace `response.success?` checks with `rescue SmsTraffic::Error`.
64
+ - Replace `reply.hash["send_date"]` / `reply.hash["status"]` with `reply.send_date` / `reply.status`.
data/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # SmsTraffic
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/sms_traffic_sdk.svg)](https://badge.fury.io/rb/sms_traffic_sdk)
4
- [![Maintainability](https://api.codeclimate.com/v1/badges/bb4795be5024dd81d927/maintainability)](https://codeclimate.com/github/golifox/sms_traffic/maintainability)
5
- [![Coverage](https://codecov.io/github/golifox/sms_traffic/graph/badge.svg?token=74C0YBJP3F)](https://codecov.io/github/golifox/sms_traffic)
6
- [![CI](https://github.com/golifox/sms_traffic/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/golifox/sms_traffic/actions/workflows/ci.yml)
4
+ [![Maintainability](https://api.codeclimate.com/v1/badges/bb4795be5024dd81d927/maintainability)](https://codeclimate.com/github/golifox/sms_traffic_sdk/maintainability)
5
+ [![Coverage](https://codecov.io/github/golifox/sms_traffic_sdk/graph/badge.svg?token=74C0YBJP3F)](https://codecov.io/github/golifox/sms_traffic_sdk)
6
+ [![CI](https://github.com/golifox/sms_traffic_sdk/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/golifox/sms_traffic_sdk/actions/workflows/ci.yml)
7
7
 
8
8
  Ruby Gem as a software development kit (SDK) that facilitates interaction with the SMS Traffic HTTP API (smstraffic.ru/api).
9
9
  This gem provides a convenient wrapper to integrate SMS Traffic services within Ruby applications, allowing for easy
@@ -37,63 +37,89 @@ Or install it yourself as:
37
37
  Define settings:
38
38
 
39
39
  ```ruby
40
- # initializers/sms_traffic.rb
41
- require 'sms_traffic_sdk'
40
+ # config/initializers/sms_traffic.rb
41
+ require 'sms_traffic'
42
42
 
43
43
  SmsTraffic.configure do |config|
44
- config.login = 'login'
45
- config.password = 'password'
46
- config.originator = 'default_originator'
47
- config.server = 'https://api.smstraffic.ru'
48
- config.debug = true
49
- config.debugger = Rails.logger
44
+ config.login = 'login'
45
+ config.password = 'password'
46
+ config.host = 'https://api.smstraffic.ru'
47
+ config.originator = 'default_originator' # optional, can be given per SMS
48
+ config.open_timeout = 10 # seconds, default 10
49
+ config.read_timeout = 30 # seconds, default 30
50
+ config.validate_phone = true # default true: 11+ digits
51
+ config.debug = true # default false
52
+ config.logger = Rails.logger # required when debug is true
50
53
  end
51
54
  ```
52
55
 
53
- Initialize sms:
56
+ `login`, `password` and `host` are required; `logger` is required when `debug` is enabled.
57
+ With `debug` on, one request line and one response line are written through `logger.debug`;
58
+ `login`, `password` and `message` are replaced with `[FILTERED]`.
59
+
60
+ Send an SMS:
61
+
54
62
  ```ruby
55
- sms = SmsTraffic::Sms.new('phone', 'text', 'originator') # initialize sms, originator by default from settings
56
- # => #<SmsTraffic::Sms:0x0000 @errors=[], @message="text", @originator="default_originator", @phone="phone", @status="not-sent">
63
+ sms = SmsTraffic::Sms.new('79991112233', 'text', originator: 'my_brand') # originator defaults to the configured one
57
64
 
65
+ sms.deliver # => true when the provider enqueued it, false otherwise
66
+ sms.status # => 'sent' ('not-sent' until delivered)
67
+ sms.id # => '123456789' (the first sms id of the reply)
68
+ sms.reply # => #<SmsTraffic::Client::DeliverReply>
69
+ sms.errors # => [] (messages of failed attempts)
58
70
  ```
59
71
 
60
- Send it and get sent sms id and dispatch code:
72
+ `deliver` and `update_status` never raise on provider or network failures; they append the message to `errors`
73
+ and keep the current `status`.
74
+
75
+ Refresh the status:
76
+
61
77
  ```ruby
62
- sms.deliver # send sms. returns sms id or dispatch code if something went wrong
63
- # => true
64
- sms.id # get sms id
65
- # => '123456789'
78
+ sms.update_status # => 'delivered' (downcased provider status, or the unchanged status on failure)
66
79
  ```
67
80
 
68
- Get current sms status and update it:
81
+ Use the client directly. It raises typed errors:
82
+
69
83
  ```ruby
70
- sms.status # get sms delivery status
71
- # => 'sent'
72
- sms.update_status # updates sms delivery status. returns status or status check response code on error
73
- # => 'delivered'
84
+ reply = SmsTraffic::Client.deliver('79991112233,79261112233', 'text', 'my_brand')
85
+ reply.ok? # => true
86
+ reply.sms_ids # => ['8287366071', '8287366073']
87
+ reply.messages # => [{ phone: '79991112233', sms_id: '8287366071', push_id: 'a' }, ...]
88
+ reply.sms_id('79261112233') # first sms id for the phone
89
+
90
+ reply = SmsTraffic::Client.status('8287713301', '8287713303')
91
+ reply.entries # => [#<struct SmsTraffic::Client::StatusReply::Entry sms_id="8287713301", status="Delivered", ...>]
92
+ reply.statuses # => { '8287713301' => 'Delivered' }
93
+ reply.errors # => { '8287713303' => 'no such message or this message does not belong to you' }
94
+ reply.entry('8287713303').not_found? # => true
95
+ reply.status('8287713301') # => 'Delivered'; the id may be omitted when there is one entry
74
96
  ```
75
97
 
76
- Get any sms status:
77
- ```ruby
78
- response = SmsTraffic.status(sms_id) # status - sms delivery status unless error or return error
79
- reply = response.reply
80
- # => #<SmsTraffic::Client::StatusReply:0x0000>
81
- status = reply.status
82
- # => "Delivered"
83
- reply.hash
84
- # =>
85
- # {"error"=>nil,
86
- # "submition_date"=>"2024-07-15 11:36:25",
87
- # "send_date"=>"2024-07-15 11:36:25",
88
- # "last_status_change_date"=>"2024-07-15 11:36:25",
89
- # "sms_id"=>"865149797164801235",
90
- # "status"=>"Delivered"}
98
+ Each entry has `sms_id`, `status`, `error`, `submission_date`, `send_date`, `last_status_change_date`
99
+ (strings as sent by the provider), `error?` and `not_found?`.
91
100
 
101
+ ### Errors
102
+
103
+ All errors inherit from `SmsTraffic::Error`:
104
+
105
+ - `SmsTraffic::ConnectionError` - network, timeout or TLS failure (the original exception is `cause`).
106
+ - `SmsTraffic::HttpError` - non-2xx response; has `status` and `body`.
107
+ - `SmsTraffic::ApiError` - the provider replied `ERROR`; has `code` and `description`.
108
+
109
+ ```ruby
110
+ begin
111
+ SmsTraffic::Client.deliver('79991112233', 'text')
112
+ rescue SmsTraffic::ApiError => e
113
+ e.code # => '401'
114
+ e.description # => 'login param is missing'
115
+ rescue SmsTraffic::Error => e
116
+ e.message
117
+ end
92
118
  ```
93
119
 
94
120
  # Contributing
95
121
 
96
- Bug reports and pull requests are welcome on GitHub at https://github.com/golifox/sms_traffic_sdk.
122
+ Bug reports and pull requests are welcome on GitHub at https://github.com/golifox/sms_traffic_sdk.
97
123
 
98
124
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
99
125
 
@@ -1,62 +1,45 @@
1
1
  module SmsTraffic
2
2
  class Client
3
- class DeliverReply < Client::Reply
4
- OK_REPLY = 'OK'.freeze unless const_defined?(:OK_REPLY)
3
+ class DeliverReply < Reply
4
+ OK_RESULT = 'OK'.freeze
5
5
 
6
+ # @return [Boolean] result is OK and at least one message was queued
6
7
  def ok?
7
- result == OK_REPLY && message_infos
8
+ result == OK_RESULT && !messages.empty?
8
9
  end
9
10
 
10
11
  def error?
11
12
  !ok?
12
13
  end
13
14
 
14
- def result
15
- fetch_value(hash, :result)
16
- end
17
-
18
- def code
19
- fetch_value(hash, :code)
20
- end
21
-
22
- def description
23
- fetch_value(hash, :description)
24
- end
25
-
26
- def message_infos
27
- fetch_value(hash, :message_infos)
28
- end
29
-
30
- def sms_id(phone = nil)
31
- message_info = fetch_value(message_infos, :message_info)
32
- return fetch_value(message_info, :sms_id) if message_info.is_a?(Hash) || message_info.nil?
33
-
34
- if phone
35
- fetch_value(find_message_info(phone), :sms_id)
36
- elsif message_info.is_a?(Array)
37
- fetch_value(message_info.first, :sms_id)
15
+ # @return [Array<Hash>] queued messages as { phone:, sms_id:, push_id: }
16
+ def messages
17
+ @messages ||= message_infos.map do |info|
18
+ { phone: fetch_value(info, :phone), sms_id: fetch_value(info, :sms_id), push_id: fetch_value(info, :push_id) }
38
19
  end
39
20
  end
40
21
 
22
+ # @return [Array<String>] every sms id in reply order
41
23
  def sms_ids
42
- message_info = fetch_value(message_infos, :message_info)
43
-
44
- return [fetch_value(message_info, :sms_id)] if message_info&.size == 1
45
- return [] if message_info.nil?
24
+ messages.filter_map { |message| message[:sms_id] }
25
+ end
46
26
 
47
- message_info.each_with_object({}) do |info, hash|
48
- hash[fetch_value(info, :phone)] = fetch_value(info, :sms_id)
49
- end
27
+ # @param phone [String, nil]
28
+ # @return [String, nil] the first sms id, or the first one for the phone
29
+ def sms_id(phone = nil)
30
+ message = phone ? messages.find { |item| item[:phone].to_s == phone.to_s } : messages.first
31
+ message&.fetch(:sms_id)
50
32
  end
51
33
 
52
34
  def error_description
53
- "#{result}: code: #{code}, description: #{description}" if error?
35
+ "Sms has not been enqueued: #{description}" if error?
54
36
  end
55
37
 
56
38
  private
57
39
 
58
- def find_message_info(phone)
59
- fetch_value(message_infos, :message_info).find { |info| fetch_value(info, :phone) == phone }
40
+ def message_infos
41
+ info = fetch_value(fetch_value(to_h, :message_infos), :message_info)
42
+ info.is_a?(Array) ? info : [info].compact
60
43
  end
61
44
  end
62
45
  end
@@ -1,19 +1,38 @@
1
1
  module SmsTraffic
2
2
  class Client
3
3
  class Reply
4
- attr_reader :xml, :hash
4
+ ERROR_RESULT = 'ERROR'.freeze
5
+
6
+ attr_reader :xml, :to_h
5
7
 
6
8
  def initialize(xml, xml_parser: SmsTraffic.configuration.xml_parser)
7
- @xml = xml.gsub!(/\s+/, ' ')
8
- @hash = fetch_value(xml_parser.parse(xml), :reply)
9
+ @xml = xml.gsub(/\s+/, ' ')
10
+ parsed = fetch_value(xml_parser.parse(@xml), :reply)
11
+ @to_h = parsed.is_a?(Hash) ? parsed : {}
12
+ end
13
+
14
+ def result
15
+ fetch_value(to_h, :result)
16
+ end
17
+
18
+ def code
19
+ fetch_value(to_h, :code)
20
+ end
21
+
22
+ def description
23
+ fetch_value(to_h, :description)
24
+ end
25
+
26
+ def api_error?
27
+ result == ERROR_RESULT
9
28
  end
10
29
 
11
30
  private
12
31
 
13
- def fetch_value(hash, key)
14
- hash.fetch(key.to_s, nil) || hash.fetch(key.to_sym, nil)
15
- rescue NoMethodError
16
- nil
32
+ def fetch_value(source, key)
33
+ return unless source.is_a?(Hash)
34
+
35
+ source.fetch(key.to_s) { source[key.to_sym] }
17
36
  end
18
37
  end
19
38
  end
@@ -1,43 +1,77 @@
1
1
  module SmsTraffic
2
2
  class Client
3
3
  class StatusReply < Reply
4
- def sms_array
5
- fetch_value(hash, :sms).is_a?(Array) ? fetch_value(hash, :sms) : [hash]
4
+ NOT_FOUND = /no such message/i
5
+
6
+ Entry = Struct.new(
7
+ :sms_id, :status, :error, :submission_date, :send_date, :last_status_change_date, keyword_init: true
8
+ ) do
9
+ def error?
10
+ !error.nil?
11
+ end
12
+
13
+ def not_found?
14
+ error? && error.match?(NOT_FOUND)
15
+ end
16
+ end
17
+
18
+ # @return [Array<Entry>]
19
+ def entries
20
+ @entries ||= entry_sources.map { |source| build_entry(source) }
6
21
  end
7
22
 
8
- def status(sms_id = nil)
9
- return fetch_value(sms_array.first, :status) if sms_array.size == 1
23
+ # @param sms_id [String, nil] required unless the reply has exactly one entry
24
+ # @return [Entry, nil]
25
+ def entry(sms_id = nil)
26
+ return entries.find { |item| item.sms_id.to_s == sms_id.to_s } if sms_id
27
+ return entries.first if entries.size == 1
10
28
 
11
- raise ArgumentError, 'sms_id is required' if sms_id.nil?
29
+ raise ArgumentError, 'sms_id is required'
30
+ end
31
+
32
+ %i[status error send_date].each do |name|
33
+ define_method(name) { |sms_id = nil| entry(sms_id)&.public_send(name) }
34
+ end
12
35
 
13
- statuses[sms_id.to_s]
36
+ %i[error? not_found?].each do |name|
37
+ define_method(name) { |sms_id = nil| entry(sms_id)&.public_send(name) || false }
14
38
  end
15
39
 
16
- # @return [Hash] statuses hash (key: sms_id, value: status)
17
- # @example
18
- #
19
- # {
20
- # '1234567890': 'DELIVERED',
21
- # '0987654321': 'FAILED'
22
- # }
40
+ # @return [Hash{String => String}] sms_id => status, without nil values
23
41
  def statuses
24
- sms_array.each_with_object({}) do |sms, hash|
25
- hash[fetch_value(sms, :sms_id)] = fetch_value(sms, :status)
26
- end.compact
42
+ entries.to_h { |item| [item.sms_id, item.status] }.compact
27
43
  end
28
44
 
29
- def error(sms_id = nil)
30
- return fetch_value(sms_array.first, :error) if sms_array.size == 1
45
+ # @return [Hash{String => String}] sms_id => error, without nil values
46
+ def errors
47
+ entries.to_h { |item| [item.sms_id, item.error] }.compact
48
+ end
49
+
50
+ private
31
51
 
32
- raise ArgumentError, 'sms_id is required' if sms_id.nil?
52
+ def entry_sources
53
+ sms = fetch_value(to_h, :sms)
54
+ return sms if sms.is_a?(Array)
55
+ return [sms] if sms.is_a?(Hash)
33
56
 
34
- errors[sms_id.to_s]
57
+ to_h.empty? ? [] : [to_h]
35
58
  end
36
59
 
37
- def errors
38
- sms_array.each_with_object({}) do |sms, hash|
39
- hash[fetch_value(sms, :sms_id)] = fetch_value(sms, :error)&.strip
40
- end.compact
60
+ def blank_to_nil(value)
61
+ stripped = value&.strip
62
+ stripped unless stripped.nil? || stripped.empty?
63
+ end
64
+
65
+ def build_entry(source)
66
+ Entry.new(
67
+ sms_id: fetch_value(source, :sms_id),
68
+ status: fetch_value(source, :status),
69
+ error: blank_to_nil(fetch_value(source, :error)),
70
+ # The provider spells this field "submition_date".
71
+ submission_date: fetch_value(source, :submition_date),
72
+ send_date: fetch_value(source, :send_date),
73
+ last_status_change_date: fetch_value(source, :last_status_change_date)
74
+ )
41
75
  end
42
76
  end
43
77
  end
@@ -1,77 +1,104 @@
1
1
  require 'net/http'
2
+ require 'openssl'
3
+ require 'uri'
4
+ require_relative 'client/reply'
5
+ require_relative 'client/deliver_reply'
6
+ require_relative 'client/status_reply'
2
7
 
3
8
  module SmsTraffic
4
9
  class Client
5
- class << self
6
- attr_reader :uri
10
+ ENDPOINT = 'multi.php'.freeze
11
+ FILTERED = '[FILTERED]'.freeze
12
+ FILTERED_PARAMS = %i[login password message].freeze
13
+ CONNECTION_ERRORS = [
14
+ SocketError, SystemCallError, IOError, Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError
15
+ ].freeze
7
16
 
8
- # @param phone [String] phone number
17
+ class << self
18
+ # @param phone [String] phone number or phone numbers separated by comma
9
19
  # @param message [String] message
10
- # @param originator [String] originator (subject)
20
+ # @param originator [String] originator (subject), defaults to the configured one
11
21
  # @param options [Hash] additional options
22
+ # @return [DeliverReply]
23
+ # @raise [ConnectionError, HttpError, ApiError]
12
24
  def deliver(phone, message, originator = nil, **options)
13
25
  originator ||= SmsTraffic.configuration.originator
14
- form_data = deliver_params(phone, message, originator, **options)
15
- response = perform_request(form_data)
16
- Response.new(response, type: :deliver)
26
+ build_reply(DeliverReply, post(deliver_params(phone, message, originator, **options)))
17
27
  end
18
28
 
19
- # @param message_id [String] message id
20
- def status(*message_ids)
21
- form_data = status_params(message_ids)
22
- response = perform_request(form_data)
23
- Response.new(response, type: :status)
29
+ # @param sms_ids [Array<String>] sms ids
30
+ # @return [StatusReply]
31
+ # @raise [ConnectionError, HttpError, ApiError]
32
+ def status(*sms_ids)
33
+ build_reply(StatusReply, post(status_params(sms_ids)))
24
34
  end
25
35
 
26
36
  private
27
37
 
28
- # @return [Net::HTTP]
29
- def connection
30
- host, logger = SmsTraffic.configuration.values_at(:host, :logger)
38
+ def build_reply(reply_class, body)
39
+ reply = reply_class.new(body)
40
+ raise ApiError.new(reply.code, reply.description) if reply.api_error?
31
41
 
32
- @uri ||= URI.join(host, 'multi.php')
33
- http = Net::HTTP.new(uri.host, uri.port)
34
- http.read_timeout = 30
35
- http.use_ssl = (uri.scheme == 'https')
36
- http.set_debug_output(logger)
37
- http
42
+ reply
38
43
  end
39
44
 
40
45
  # @param form_data [Hash<Symbol, String|Integer>]
41
- # @return [Net::HTTPResponse]
42
- def perform_request(form_data)
43
- connection.start do |http|
46
+ # @return [String] response body
47
+ def post(form_data)
48
+ config = SmsTraffic.configuration
49
+ uri = URI.join(config.host, ENDPOINT)
50
+ log("request POST #{uri} #{filter(form_data)}")
51
+
52
+ response = perform(uri, form_data, config)
53
+ log("response #{response.code} (#{response.body.to_s.bytesize} bytes)")
54
+ successful_body(response)
55
+ rescue *CONNECTION_ERRORS => e
56
+ raise ConnectionError, "#{e.class}: #{e.message}"
57
+ end
58
+
59
+ def successful_body(response)
60
+ raise HttpError.new(response.code.to_i, response.body) unless response.is_a?(Net::HTTPSuccess)
61
+
62
+ response.body
63
+ end
64
+
65
+ def perform(uri, form_data, config)
66
+ options = { use_ssl: uri.scheme == 'https', open_timeout: config.open_timeout,
67
+ read_timeout: config.read_timeout }
68
+ Net::HTTP.start(uri.host, uri.port, **options) do |http|
44
69
  request = Net::HTTP::Post.new(uri.request_uri)
45
70
  request.set_form_data(form_data)
46
71
  http.request(request)
47
72
  end
48
73
  end
49
74
 
50
- # @param phone [String] phone number or phone numbers separated by comma
51
- # @param message [String] message
52
- # @param originator [String] originator (subject)
53
- # @param options [Hash] additional options
54
- # @return [Hash<Symbol, String|Integer>]
75
+ def log(line)
76
+ config = SmsTraffic.configuration
77
+ config.logger.debug("[SmsTraffic] #{line}") if config.debug
78
+ end
79
+
80
+ def filter(form_data)
81
+ form_data.to_h { |key, value| [key, FILTERED_PARAMS.include?(key) ? FILTERED : value] }
82
+ end
83
+
55
84
  def deliver_params(phone, message, originator, **options)
56
85
  {
57
86
  login: SmsTraffic.configuration.login,
58
87
  password: SmsTraffic.configuration.password,
59
88
  phones: phone,
60
- message: message,
89
+ message:,
61
90
  want_sms_ids: 1,
62
91
  rus: 5,
63
- originator: originator
92
+ originator:
64
93
  }.merge(options)
65
94
  end
66
95
 
67
- # @param message_ids [Array<String>] message ids
68
- # @return [Hash<Symbol, String|Integer>]
69
- def status_params(*message_ids)
96
+ def status_params(sms_ids)
70
97
  {
71
98
  login: SmsTraffic.configuration.login,
72
99
  password: SmsTraffic.configuration.password,
73
100
  operation: 'status',
74
- sms_id: message_ids.join(',')
101
+ sms_id: sms_ids.join(',')
75
102
  }
76
103
  end
77
104
  end
@@ -1,75 +1,39 @@
1
- require 'singleton'
2
1
  require 'nori'
3
2
 
4
3
  module SmsTraffic
5
4
  class Configuration
6
- include Singleton
5
+ ATTRIBUTES = %i[
6
+ login password host originator debug logger open_timeout read_timeout validate_phone xml_parser
7
+ ].freeze
7
8
 
8
- attr_accessor :login, :password, :host, :originator, :debug, :logger, :xml_parser, :validate_phone
9
+ attr_accessor(*ATTRIBUTES)
9
10
 
10
11
  def initialize
11
- self.debug = default_configuration[:debug]
12
- self.logger = default_configuration[:logger]
13
- self.xml_parser = default_configuration[:xml_parser]
14
- self.validate_phone = default_configuration[:validate_phone]
15
- super
12
+ reset!
16
13
  end
17
14
 
18
15
  def reset!
19
- default_configuration.each do |key, value|
20
- send("#{key}=", value)
21
- end
16
+ self.login = nil
17
+ self.password = nil
18
+ self.host = nil
19
+ self.originator = nil
20
+ self.debug = false
21
+ self.logger = nil
22
+ self.open_timeout = 10
23
+ self.read_timeout = 30
24
+ self.validate_phone = true
25
+ self.xml_parser = Nori.new(advanced_typecasting: false)
22
26
  end
23
27
 
24
28
  def validate!
25
- validation_rules.each do |key, value|
26
- raise ArgumentError, "#{key} should be defined for #{self}." if value.nil?
27
- end
29
+ raise ArgumentError, 'login should be defined' if login.nil?
30
+ raise ArgumentError, 'password should be defined' if password.nil?
31
+ raise ArgumentError, 'host should be defined' if host.nil?
32
+ raise ArgumentError, 'logger should be defined when debug is enabled' if debug && logger.nil?
28
33
  end
29
34
 
30
35
  def to_h
31
- {
32
- login: login,
33
- password: password,
34
- host: host,
35
- originator: originator,
36
- debug: debug,
37
- logger: logger,
38
- xml_parser: xml_parser
39
- }
40
- end
41
-
42
- def values_at(*keys)
43
- to_h.values_at(*keys)
44
- end
45
-
46
- private
47
-
48
- def default_configuration
49
- {
50
- login: nil,
51
- password: nil,
52
- host: nil,
53
- originator: nil,
54
- debug: false,
55
- logger: $stdout,
56
- xml_parser: default_xml_parser,
57
- validate_phone: true
58
- }
59
- end
60
-
61
- def validation_rules
62
- {
63
- 'Login' => login,
64
- 'Password' => password,
65
- 'Server' => host,
66
- 'Originator' => originator,
67
- 'Debug logger' => debug && !logger ? nil : true
68
- }
69
- end
70
-
71
- def default_xml_parser
72
- @default_xml_parser ||= Nori.new(advanced_typecasting: true).freeze
36
+ ATTRIBUTES.to_h { |attribute| [attribute, public_send(attribute)] }
73
37
  end
74
38
  end
75
39
  end
@@ -0,0 +1,28 @@
1
+ module SmsTraffic
2
+ class Error < StandardError; end
3
+
4
+ # Network or TLS failure; the original exception is available as +cause+.
5
+ class ConnectionError < Error; end
6
+
7
+ # Non-2xx HTTP status.
8
+ class HttpError < Error
9
+ attr_reader :status, :body
10
+
11
+ def initialize(status, body)
12
+ @status = status
13
+ @body = body
14
+ super("HTTP #{status}")
15
+ end
16
+ end
17
+
18
+ # HTTP 200 reply whose result is ERROR.
19
+ class ApiError < Error
20
+ attr_reader :code, :description
21
+
22
+ def initialize(code, description)
23
+ @code = code
24
+ @description = description
25
+ super("#{code}: #{description}")
26
+ end
27
+ end
28
+ end
@@ -1,54 +1,67 @@
1
1
  module SmsTraffic
2
2
  class Sms
3
+ PHONE_FORMAT = /\A\d{11,}\z/
4
+
3
5
  attr_accessor :phone, :message, :originator
4
- attr_reader :id, :status, :errors
6
+ attr_reader :id, :status, :errors, :reply
5
7
 
6
8
  def initialize(phone, message, originator: nil)
7
9
  @phone = phone
8
10
  @message = message
9
11
  @originator = originator || SmsTraffic.configuration.originator
12
+ @reply = nil
10
13
  @status = 'not-sent'
11
14
  @errors = []
12
15
  validate!
13
16
  end
14
17
 
15
- def deliver # rubocop:disable Metrics/MethodLength
16
- # @type [Client::Response]
17
- response = Client.deliver(phone, message, originator)
18
-
19
- unless response.success?
20
- @errors << response.error_description
21
- return false
22
- end
23
-
24
- # @type [Client::Response::Reply]
25
- reply = response.reply
18
+ # @return [Boolean] true when the provider enqueued the sms
19
+ def deliver
20
+ @reply = Client.deliver(phone, message, originator)
26
21
 
27
- if reply.ok?
28
- @status = 'sent'
29
- @id = reply.sms_id || reply.sms_ids.values.first
30
- true
31
- else
32
- @errors << (reply.error_description || 'Sms has been not enqueued')
33
- false
34
- end
22
+ reply.ok? ? mark_sent : add_error(reply.error_description)
23
+ reply.ok?
24
+ rescue SmsTraffic::Error => e
25
+ add_error(e.message)
26
+ false
35
27
  end
36
28
 
29
+ # @return [String] the current status; unchanged when the check fails
37
30
  def update_status
38
31
  return status if id.nil?
39
32
 
40
- response = Client.status(id)
41
- return response.code unless response.success?
42
-
43
- @status = response.reply.status.downcase
33
+ apply_entry(Client.status(id).entry(id))
34
+ status
35
+ rescue SmsTraffic::Error => e
36
+ add_error(e.message)
37
+ status
44
38
  end
45
39
 
46
40
  private
47
41
 
48
- def validate! # rubocop:disable Metrics/ AbcSize
42
+ def mark_sent
43
+ @status = 'sent'
44
+ @id = reply.sms_id
45
+ end
46
+
47
+ def add_error(error)
48
+ @errors << error
49
+ end
50
+
51
+ def apply_entry(entry)
52
+ if entry&.error?
53
+ add_error(entry.error)
54
+ elsif entry&.status
55
+ @status = entry.status.downcase
56
+ else
57
+ add_error("No status returned for sms #{id}")
58
+ end
59
+ end
60
+
61
+ def validate! # rubocop:disable Metrics/AbcSize
49
62
  raise ArgumentError, "Phone should be assigned to #{self.class}." if phone.nil?
50
63
 
51
- if SmsTraffic.configuration.validate_phone && phone.to_s !~ /^[0-9]{11,}$/
64
+ if SmsTraffic.configuration.validate_phone && !PHONE_FORMAT.match?(phone.to_s)
52
65
  raise ArgumentError, 'Phone number should contain only numbers. Minimum length is 11.'
53
66
  end
54
67
 
@@ -1,3 +1,3 @@
1
1
  module SmsTraffic
2
- VERSION = '0.0.7'.freeze
2
+ VERSION = '0.1.0'.freeze
3
3
  end
data/lib/sms_traffic.rb CHANGED
@@ -1,19 +1,16 @@
1
- require 'zeitwerk'
2
-
3
- loader = Zeitwerk::Loader.for_gem
4
- loader.ignore("#{__dir__}/sms_traffic/version.rb")
5
- loader.enable_reloading
6
- loader.setup
7
-
8
1
  require_relative 'sms_traffic/version'
2
+ require_relative 'sms_traffic/errors'
3
+ require_relative 'sms_traffic/configuration'
4
+ require_relative 'sms_traffic/client'
5
+ require_relative 'sms_traffic/sms'
9
6
 
10
7
  module SmsTraffic
11
8
  class << self
12
9
  def configuration
13
- @configuration ||= Configuration.instance
10
+ @configuration ||= Configuration.new
14
11
  end
15
12
 
16
- def configure(&_block)
13
+ def configure
17
14
  yield(configuration) if block_given?
18
15
  configuration.validate!
19
16
  end
@@ -0,0 +1 @@
1
+ require_relative 'sms_traffic'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sms_traffic_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Rybolovlev
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2024-10-18 00:00:00.000000000 Z
11
+ date: 2026-11-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -30,68 +30,44 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '2.6'
33
+ version: '2.8'
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.6'
41
- - !ruby/object:Gem::Dependency
42
- name: zeitwerk
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '2.5'
48
- type: :runtime
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '2.5'
40
+ version: '2.8'
55
41
  description: |-
56
42
  Ruby Gem as a SDK for interacting with the SMS Traffic HTTP API (smstraffic.ru/api).
57
43
  This gem provides a convenient way to integrate SMS Traffic services into Ruby applications,
58
44
  enabling tasks such as sending SMS messages, checking delivery status, and others.
59
45
  email:
60
46
  - i@golifox.ru
61
- executables:
62
- - konsierge-sms.rb
47
+ executables: []
63
48
  extensions: []
64
49
  extra_rdoc_files: []
65
50
  files:
66
- - ".env.template"
67
- - ".fasterer.yml"
68
- - ".github/workflows/ci.yml"
69
- - ".gitignore"
70
- - ".rspec"
71
- - ".rubocop.yml"
72
- - ".tool-versions"
73
- - Gemfile
51
+ - CHANGELOG.md
74
52
  - LICENSE
75
53
  - README.md
76
- - Rakefile
77
- - bin/console
78
- - bin/setup
79
- - exe/konsierge-sms.rb
80
54
  - lib/sms_traffic.rb
81
55
  - lib/sms_traffic/client.rb
82
56
  - lib/sms_traffic/client/deliver_reply.rb
83
57
  - lib/sms_traffic/client/reply.rb
84
- - lib/sms_traffic/client/response.rb
85
58
  - lib/sms_traffic/client/status_reply.rb
86
59
  - lib/sms_traffic/configuration.rb
60
+ - lib/sms_traffic/errors.rb
87
61
  - lib/sms_traffic/sms.rb
88
62
  - lib/sms_traffic/version.rb
89
- - sms_traffic_sdk.gemspec
90
- homepage: https://github.com/golifox/sms_traffic
63
+ - lib/sms_traffic_sdk.rb
64
+ homepage: https://github.com/golifox/sms_traffic_sdk
91
65
  licenses:
92
66
  - MIT
93
67
  metadata:
94
68
  rubygems_mfa_required: 'true'
69
+ source_code_uri: https://github.com/golifox/sms_traffic_sdk
70
+ changelog_uri: https://github.com/golifox/sms_traffic_sdk/blob/main/CHANGELOG.md
95
71
  post_install_message:
96
72
  rdoc_options: []
97
73
  require_paths:
@@ -100,14 +76,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
100
76
  requirements:
101
77
  - - ">="
102
78
  - !ruby/object:Gem::Version
103
- version: 2.7.0
79
+ version: '3.1'
104
80
  required_rubygems_version: !ruby/object:Gem::Requirement
105
81
  requirements:
106
82
  - - ">="
107
83
  - !ruby/object:Gem::Version
108
84
  version: '0'
109
85
  requirements: []
110
- rubygems_version: 3.5.11
86
+ rubygems_version: 3.5.22
111
87
  signing_key:
112
88
  specification_version: 4
113
89
  summary: Send sms via SmsTraffic service.
data/.env.template DELETED
@@ -1,4 +0,0 @@
1
- SMS_TRAFFIC_HOST=SMS_TRAFFIC_HOST
2
- SMS_TRAFFIC_LOGIN=SMS_TRAFFIC_LOGIN
3
- SMS_TRAFFIC_PASSWORD=SMS_TRAFFIC_PASSWORD
4
- SMS_TRAFFIC_ORIGINATOR=SMS_TRAFFIC_ORIGINATOR
data/.fasterer.yml DELETED
@@ -1,2 +0,0 @@
1
- speedups:
2
- fetch_with_argument_vs_block: false
@@ -1,72 +0,0 @@
1
- name: Ruby Gem CI/CD
2
-
3
- on:
4
- push:
5
- branches: [ main ]
6
- pull_request:
7
- branches: [ main ]
8
- release:
9
- types: [created]
10
-
11
- jobs:
12
- build:
13
- runs-on: ubuntu-latest
14
- strategy:
15
- matrix:
16
- ruby-version: ['2.7', '3.0', '3.1', '3.2', '3.3']
17
-
18
- steps:
19
- - uses: actions/checkout@v2
20
-
21
- - name: Set up Ruby
22
- uses: ruby/setup-ruby@v1
23
- with:
24
- ruby-version: ${{ matrix.ruby-version }}
25
-
26
- - name: Install Bundler for Ruby < 3.0
27
- if: ${{ matrix.ruby-version < '3.0' }}
28
- run: gem install bundler -v 2.4.22 --no-document
29
-
30
- - name: Install Bundler for Ruby >= 3.0
31
- if: ${{ matrix.ruby-version >= '3.0' }}
32
- run: gem install bundler -v '~> 2.5' --no-document
33
-
34
- - name: Install dependencies
35
- run: bundle install --jobs 4 --retry 3
36
-
37
- - name: Run tests and linting
38
- run: bundle exec rake
39
-
40
- - name: Upload coverage reports to Codecov
41
- uses: codecov/codecov-action@v4.0.1
42
- with:
43
- token: ${{ secrets.CODECOV_TOKEN }}
44
- file: ./coverage/.resultset.json
45
- fail_ci_if_error: false
46
-
47
- publish:
48
- needs: build
49
- runs-on: ubuntu-latest
50
- if: startsWith(github.ref, 'refs/tags/v')
51
- steps:
52
- - uses: actions/checkout@v2
53
-
54
- - name: Set up Ruby for publish
55
- uses: ruby/setup-ruby@v1
56
- with:
57
- ruby-version: '3.3'
58
-
59
- - name: Install dependencies
60
- run: |
61
- gem install bundler --no-document
62
- bundle install --jobs 4 --retry 3
63
-
64
- - name: Build gem
65
- run: |
66
- gem build *.gemspec
67
-
68
- - name: Push gem to RubyGems
69
- run: |
70
- gem push *.gem
71
- env:
72
- GEM_HOST_API_KEY: ${{secrets.GEM_HOST_API_KEY}}
data/.gitignore DELETED
@@ -1,12 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /Gemfile.lock
4
- /_yardoc/
5
- /coverage/
6
- /doc/
7
- /pkg/
8
- /spec/reports/
9
- /tmp/
10
- .idea/
11
- .rspec_status
12
- .env
data/.rspec DELETED
@@ -1,2 +0,0 @@
1
- --require spec_helper
2
- --color
data/.rubocop.yml DELETED
@@ -1,20 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 2.7.4
3
- NewCops: enable
4
- SuggestExtensions: false
5
- Exclude:
6
- - 'bin/**/*'
7
-
8
- Style/Documentation:
9
- Enabled: false
10
-
11
- Style/FrozenStringLiteralComment:
12
- EnforcedStyle: 'never'
13
-
14
- Layout/HashAlignment:
15
- EnforcedHashRocketStyle: table
16
- EnforcedColonStyle: table
17
-
18
- Metrics/BlockLength:
19
- Exclude:
20
- - 'spec/**/*'
data/.tool-versions DELETED
@@ -1 +0,0 @@
1
- ruby 3.3.3
data/Gemfile DELETED
@@ -1,17 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- gemspec
4
-
5
- group :development, :test do
6
- gem 'dotenv', '~> 2.7'
7
- gem 'fasterer', '~> 0.9'
8
- gem 'rack', '~> 3.1.7'
9
- gem 'rake', '~> 13.2'
10
- gem 'rspec', '~> 3.10'
11
- gem 'rspec-mocks', '~> 3.10'
12
- gem 'rubocop', '~> 1.18'
13
- gem 'rubocop-performance', '~> 1.11'
14
- gem 'rubocop-rspec', '~> 2.11'
15
- gem 'simplecov', '~> 0.22'
16
- gem 'webmock', '~> 3.14'
17
- end
data/Rakefile DELETED
@@ -1,10 +0,0 @@
1
- require 'bundler/gem_tasks'
2
- require 'rspec/core/rake_task'
3
-
4
- RSpec::Core::RakeTask.new(:spec)
5
-
6
- require 'rubocop/rake_task'
7
-
8
- RuboCop::RakeTask.new
9
-
10
- task default: %i[spec rubocop]
data/bin/console DELETED
@@ -1,18 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'bundler/setup'
4
- require 'dotenv'
5
- require 'sms_traffic'
6
-
7
- Dotenv.load('.env')
8
-
9
- SmsTraffic.configure do |config|
10
- config.host = ENV['SMS_TRAFFIC_HOST']
11
- config.login = ENV['SMS_TRAFFIC_LOGIN']
12
- config.password = ENV['SMS_TRAFFIC_PASSWORD']
13
- config.originator = ENV['SMS_TRAFFIC_ORIGINATOR']
14
- end
15
-
16
-
17
- require 'irb'
18
- IRB.start
data/bin/setup DELETED
@@ -1,7 +0,0 @@
1
- #!/bin/bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
-
5
- bundle install
6
-
7
- # Do any other automated setup that you need to do here
data/exe/konsierge-sms.rb DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env ruby
2
- require 'konsierge/sms_traffic'
@@ -1,39 +0,0 @@
1
- require_relative 'deliver_reply'
2
- require_relative 'status_reply'
3
-
4
- module SmsTraffic
5
- class Client
6
- class Response
7
- attr_reader :origin, :reply, :code, :body, :message
8
-
9
- REPLY_HASH_MAP = {
10
- :status => StatusReply,
11
- :deliver => DeliverReply,
12
- nil => Reply
13
- }.freeze
14
-
15
- def initialize(response, type: nil)
16
- @origin = response
17
- @code = response.code.to_i
18
- @body = response.body
19
- @message = response.message
20
-
21
- return unless success?
22
-
23
- @reply = REPLY_HASH_MAP[type].new(body)
24
- end
25
-
26
- def success?
27
- code == 200
28
- end
29
-
30
- def failure?
31
- !success?
32
- end
33
-
34
- def error_description
35
- "#{code}: #{message}" if failure?
36
- end
37
- end
38
- end
39
- end
@@ -1,31 +0,0 @@
1
- lib = File.expand_path('lib', __dir__)
2
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
-
4
- require_relative 'lib/sms_traffic/version'
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = 'sms_traffic_sdk'
8
- spec.version = SmsTraffic::VERSION
9
- spec.authors = ['David Rybolovlev']
10
- spec.email = ['i@golifox.ru']
11
-
12
- spec.summary = 'Send sms via SmsTraffic service.'
13
- spec.description = 'Ruby Gem as a SDK for interacting with the SMS Traffic HTTP API (smstraffic.ru/api).
14
- This gem provides a convenient way to integrate SMS Traffic services into Ruby applications,
15
- enabling tasks such as sending SMS messages, checking delivery status, and others.'
16
- spec.homepage = 'https://github.com/golifox/sms_traffic'
17
- spec.license = 'MIT'
18
-
19
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
- spec.bindir = 'exe'
21
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
- spec.require_paths = ['lib']
23
-
24
- spec.required_ruby_version = '>= 2.7.0'
25
-
26
- spec.add_dependency 'nokogiri', '~> 1.12'
27
- spec.add_dependency 'nori', '~> 2.6'
28
- spec.add_dependency 'zeitwerk', '~> 2.5'
29
-
30
- spec.metadata['rubygems_mfa_required'] = 'true'
31
- end