sms_traffic_sdk 0.2.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7073f3fdf9537cf98008d5c6fac2bbc28457fbe4ef05852ccfd4e3b3a4606fbc
4
- data.tar.gz: 7ad131b8ca90beebf7d4e227fa4031655bdc49d7e5db4f12c7ee11763cde8e0a
3
+ metadata.gz: 4c5c633712d477a8978bb88a14dded77e522195705ecc68e2bae2bf18078943c
4
+ data.tar.gz: 64b2ea39745e547e707898b6ec4778a937eff0c9c54ee2df5783b37af04d01d6
5
5
  SHA512:
6
- metadata.gz: f22009b2e84816ea0ad7294fd80ec8810dfc99c4ad46a7c66e9f9604f837c31fe5d6ae996639e776cb79501523b50257d20390069412993c704b2456ed3b5fb7
7
- data.tar.gz: 4dc45cb30d25dd521d716f16bcb7a4ae530ee9fa3b8d14f0b8ec0cfde6a479c81d41268b1015a7518bdd16d79a11fc4f8c5663d2e8266dc7f34c1d0415202843
6
+ metadata.gz: 4f02913510ac31a7b1b7f8f61cb503ca10332ec04a6bc2281c90da2598444ad9a940b329948b6759a79be48842a918299936b74e5b0b7c2b986fe5cf6b5febd8
7
+ data.tar.gz: 7a708811f729349e37db2c75c68c636390f147ef3facff7cfcaf7b200ef57d6538a20b64c0dde0a93af71066f7d632d747127f10ebabf50eab874ac51b3f8b3c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 - 2026-11-03
4
+
5
+ ### Breaking changes
6
+
7
+ - An HTTP 200 body that is not an XML `<reply>` element (an HTML error page, plain text, a truncated or empty body)
8
+ raises `SmsTraffic::MalformedReplyError` instead of returning an empty reply. Its message is
9
+ `"Malformed reply (N bytes)"`; `#body` keeps the body. `Sms#deliver` returns `false` and records that message
10
+ instead of `"Sms has not been enqueued: "`. An empty `<reply/>` is still an empty reply.
11
+ - The debug request line shows only `originator`, `want_sms_ids`, `rus`, `operation` and `sms_id`; `phones` and any
12
+ extra `deliver` option are now `[FILTERED]` too.
13
+
14
+ ### Changed
15
+
16
+ - `Client.status` and `Client.balance` repeat on the fallback host after `MalformedReplyError`; `Client.deliver`
17
+ does not, since the provider may already have queued the SMS.
18
+
19
+ ## 0.3.0 - 2026-11-03
20
+
21
+ ### Breaking changes
22
+
23
+ - `nokogiri` is no longer a dependency. Applications that relied on Nokogiri being installed through this gem must
24
+ depend on it themselves.
25
+
26
+ ### Changed
27
+
28
+ - The default XML parser is `Nori.new(advanced_typecasting: false, parser: :rexml)`. Every fixture parses to the
29
+ same hash as with Nokogiri. `rexml` (`>= 3.3.9`, after the 2024 DoS fixes) is a new runtime dependency; a custom
30
+ `xml_parser` is still possible.
31
+ - A body that is not well-formed XML (an HTML error page, plain text, a truncated reply) keeps the 0.2.0 behavior:
32
+ the reply is empty, so `Sms#deliver` returns `false` and `Client.*` return a reply with an empty hash.
33
+ - Runtime dependency lower bounds are tested: `gemfiles/minimum.gemfile` pins `nori` 2.8.0, `rexml` 3.3.9 and
34
+ `bigdecimal` 3.1.0, and the suite passes with them. CI runs it as `test-minimum` on Ruby 3.1 and 3.4; `publish`
35
+ waits for it.
36
+
3
37
  ## 0.2.0 - 2026-11-03
4
38
 
5
39
  ### Added
data/README.md CHANGED
@@ -58,7 +58,8 @@ end
58
58
 
59
59
  `login`, `password` and `host` are required; `logger` is required when `debug` is enabled.
60
60
  With `debug` on, one request line and one response line are written through `logger.debug`;
61
- `login`, `password` and `message` are replaced with `[FILTERED]`.
61
+ only `originator`, `want_sms_ids`, `rus`, `operation` and `sms_id` are shown, every other param (credentials,
62
+ `phones`, `message`, extra options) is replaced with `[FILTERED]`.
62
63
 
63
64
  Send an SMS:
64
65
 
@@ -132,16 +133,24 @@ The value is returned as the provider sends it; its units are not documented and
132
133
  Set `config.fallback_host` (the provider documents `https://api2.smstraffic.ru`) to repeat a failed request once
133
134
  on the backup host. The request is repeated after `SocketError`, `Errno::ECONNREFUSED`, `Errno::EHOSTUNREACH`,
134
135
  `Errno::ENETUNREACH`, `Net::OpenTimeout`, or API error 1000 (temporary server problem). `Client.status` and
135
- `Client.balance` also retry after any `ConnectionError` (read timeout, reset connection, TLS failure) and HTTP 5xx.
136
+ `Client.balance` also retry after any `ConnectionError` (read timeout, reset connection, TLS failure), HTTP 5xx and
137
+ `MalformedReplyError`.
136
138
  `Client.deliver` does not, since the provider may already have queued the SMS. The error of the last attempt is raised. With `debug`, the retry is
137
139
  logged in one line without credentials.
138
140
 
141
+ ### XML parser
142
+
143
+ Replies are parsed with Nori and its pure-Ruby REXML backend, so no native extension is needed. To use another
144
+ parser, set `config.xml_parser` to any object that responds to `parse(xml)` and returns a Hash.
145
+
139
146
  ### Errors
140
147
 
141
148
  All errors inherit from `SmsTraffic::Error`:
142
149
 
143
150
  - `SmsTraffic::ConnectionError` - network, timeout or TLS failure (the original exception is `cause`).
144
151
  - `SmsTraffic::HttpError` - non-2xx response; has `status` and `body`.
152
+ - `SmsTraffic::MalformedReplyError` - HTTP 200 whose body is not an XML `<reply>` element (an HTML error page, plain
153
+ text, a truncated reply); has `body`, the message gives only its size.
145
154
  - `SmsTraffic::ApiError` - the provider replied `ERROR` (or `ERRO`); has `code` and `description`, and the
146
155
  predicates `retryable?` (1000), `access_denied?` (411, 412), `insufficient_funds?` (415) and `blocked_phone?` (432).
147
156
 
@@ -1,3 +1,6 @@
1
+ # Nori loads REXML lazily on the first parse; Reply#parse rescues its exception even with a custom parser.
2
+ require 'rexml/parsers/baseparser'
3
+
1
4
  module SmsTraffic
2
5
  class Client
3
6
  class Reply
@@ -6,10 +9,11 @@ module SmsTraffic
6
9
 
7
10
  attr_reader :xml, :to_h
8
11
 
12
+ # @raise [MalformedReplyError] the body is not an XML `<reply>` element
9
13
  def initialize(xml, xml_parser: SmsTraffic.configuration.xml_parser)
10
- @xml = xml.gsub(/\s+/, ' ')
11
- parsed = fetch_value(xml_parser.parse(@xml), :reply)
12
- @to_h = parsed.is_a?(Hash) ? parsed : {}
14
+ @xml = xml.to_s.gsub(/\s+/, ' ')
15
+ @to_h = reply_hash(parse(xml_parser))
16
+ raise MalformedReplyError, xml if @to_h.nil?
13
17
  end
14
18
 
15
19
  def result
@@ -31,6 +35,20 @@ module SmsTraffic
31
35
 
32
36
  private
33
37
 
38
+ def parse(xml_parser)
39
+ xml_parser.parse(@xml)
40
+ rescue REXML::ParseException
41
+ nil
42
+ end
43
+
44
+ # An empty `<reply/>` carries no data; a missing `<reply>` element or one holding only text is malformed.
45
+ def reply_hash(parsed)
46
+ return unless parsed.is_a?(Hash) && (parsed.key?('reply') || parsed.key?(:reply))
47
+
48
+ reply = fetch_value(parsed, :reply) || {}
49
+ reply if reply.is_a?(Hash)
50
+ end
51
+
34
52
  def fetch_value(source, key)
35
53
  return unless source.is_a?(Hash)
36
54
 
@@ -10,7 +10,8 @@ module SmsTraffic
10
10
  class Client
11
11
  ENDPOINT = 'multi.php'.freeze
12
12
  FILTERED = '[FILTERED]'.freeze
13
- FILTERED_PARAMS = %i[login password message].freeze
13
+ # The only request params the debug log shows; every other value, phones and options included, is FILTERED.
14
+ LOGGED_PARAMS = %i[originator want_sms_ids rus operation sms_id].freeze
14
15
  CONNECTION_ERRORS = [
15
16
  SocketError, SystemCallError, IOError, Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError
16
17
  ].freeze
@@ -25,7 +26,7 @@ module SmsTraffic
25
26
  # @param originator [String] originator (subject), defaults to the configured one
26
27
  # @param options [Hash] additional options
27
28
  # @return [DeliverReply]
28
- # @raise [ConnectionError, HttpError, ApiError]
29
+ # @raise [ConnectionError, HttpError, MalformedReplyError, ApiError]
29
30
  def deliver(phone, message, originator = nil, **options)
30
31
  originator ||= SmsTraffic.configuration.originator
31
32
  request(DeliverReply, deliver_params(phone, message, originator, **options), read_only: false)
@@ -33,13 +34,13 @@ module SmsTraffic
33
34
 
34
35
  # @param sms_ids [Array<String>] sms ids
35
36
  # @return [StatusReply]
36
- # @raise [ConnectionError, HttpError, ApiError]
37
+ # @raise [ConnectionError, HttpError, MalformedReplyError, ApiError]
37
38
  def status(*sms_ids)
38
39
  request(StatusReply, status_params(sms_ids), read_only: true)
39
40
  end
40
41
 
41
42
  # @return [AccountReply]
42
- # @raise [ConnectionError, HttpError, ApiError]
43
+ # @raise [ConnectionError, HttpError, MalformedReplyError, ApiError]
43
44
  def balance
44
45
  request(AccountReply, account_params, read_only: true)
45
46
  end
@@ -76,12 +77,15 @@ module SmsTraffic
76
77
  def retry_on_fallback?(error, read_only:)
77
78
  case error
78
79
  when ApiError then error.retryable?
79
- when ConnectionError then read_only || UNREACHABLE_ERRORS.any? { |klass| error.cause.is_a?(klass) }
80
+ when MalformedReplyError then read_only
81
+ when ConnectionError then read_only || unreachable?(error)
80
82
  when HttpError then read_only && error.status >= 500
81
83
  else false
82
84
  end
83
85
  end
84
86
 
87
+ def unreachable?(error) = UNREACHABLE_ERRORS.any? { |klass| error.cause.is_a?(klass) }
88
+
85
89
  # @param form_data [Hash<Symbol, String|Integer>]
86
90
  # @return [String] response body
87
91
  def post(form_data, host)
@@ -118,7 +122,7 @@ module SmsTraffic
118
122
  end
119
123
 
120
124
  def filter(form_data)
121
- form_data.to_h { |key, value| [key, FILTERED_PARAMS.include?(key) ? FILTERED : value] }
125
+ form_data.to_h { |key, value| [key, LOGGED_PARAMS.include?(key.to_sym) ? value : FILTERED] }
122
126
  end
123
127
 
124
128
  def deliver_params(phone, message, originator, **options)
@@ -17,7 +17,7 @@ module SmsTraffic
17
17
 
18
18
  def reset!
19
19
  ATTRIBUTES.each { |attribute| public_send("#{attribute}=", DEFAULTS[attribute]) }
20
- self.xml_parser = Nori.new(advanced_typecasting: false)
20
+ self.xml_parser = Nori.new(advanced_typecasting: false, parser: :rexml)
21
21
  end
22
22
 
23
23
  def validate!
@@ -15,6 +15,16 @@ module SmsTraffic
15
15
  end
16
16
  end
17
17
 
18
+ # HTTP 200 body that is not an XML `<reply>` element: an HTML error page, plain text, a truncated reply.
19
+ class MalformedReplyError < Error
20
+ attr_reader :body
21
+
22
+ def initialize(body)
23
+ @body = body
24
+ super("Malformed reply (#{body.to_s.bytesize} bytes)")
25
+ end
26
+ end
27
+
18
28
  # HTTP 200 reply whose `result` is not `OK` (`ERROR`, `ERRO`).
19
29
  class ApiError < Error
20
30
  attr_reader :code, :description
@@ -1,3 +1,3 @@
1
1
  module SmsTraffic
2
- VERSION = '0.2.0'.freeze
2
+ VERSION = '0.4.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sms_traffic_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Rybolovlev
@@ -25,33 +25,33 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '3.1'
27
27
  - !ruby/object:Gem::Dependency
28
- name: nokogiri
28
+ name: nori
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.12'
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: '1.12'
40
+ version: '2.8'
41
41
  - !ruby/object:Gem::Dependency
42
- name: nori
42
+ name: rexml
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '2.8'
47
+ version: 3.3.9
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '2.8'
54
+ version: 3.3.9
55
55
  description: |-
56
56
  Ruby Gem as a SDK for interacting with the SMS Traffic HTTP API (smstraffic.ru/api).
57
57
  This gem provides a convenient way to integrate SMS Traffic services into Ruby applications,