aitch 1.0.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +5 -5
  2. data/.github/FUNDING.yml +3 -0
  3. data/.github/workflows/tests.yml +50 -0
  4. data/.rubocop.yml +26 -0
  5. data/Gemfile +2 -2
  6. data/README.md +54 -14
  7. data/Rakefile +5 -1
  8. data/aitch.gemspec +12 -7
  9. data/lib/aitch/configuration.rb +5 -0
  10. data/lib/aitch/dsl.rb +4 -3
  11. data/lib/aitch/engines/json.rb +15 -0
  12. data/lib/aitch/errors.rb +1 -0
  13. data/lib/aitch/ext/to_query.rb +6 -10
  14. data/lib/aitch/location.rb +4 -3
  15. data/lib/aitch/namespace.rb +38 -14
  16. data/lib/aitch/redirect.rb +1 -0
  17. data/lib/aitch/request.rb +40 -28
  18. data/lib/aitch/response/body.rb +1 -0
  19. data/lib/aitch/response/description.rb +3 -2
  20. data/lib/aitch/response/errors.rb +2 -1
  21. data/lib/aitch/response.rb +9 -5
  22. data/lib/aitch/response_parser/default_parser.rb +2 -1
  23. data/lib/aitch/response_parser/html_parser.rb +1 -0
  24. data/lib/aitch/response_parser/json_parser.rb +2 -1
  25. data/lib/aitch/response_parser/xml_parser.rb +1 -0
  26. data/lib/aitch/response_parser.rb +8 -5
  27. data/lib/aitch/uri.rb +5 -2
  28. data/lib/aitch/utils.rb +3 -1
  29. data/lib/aitch/version.rb +2 -1
  30. data/lib/aitch.rb +13 -13
  31. data/test/aitch/aitch_test.rb +1 -0
  32. data/test/aitch/configuration_test.rb +2 -1
  33. data/test/aitch/dsl_test.rb +1 -0
  34. data/test/aitch/execute_test.rb +1 -0
  35. data/test/aitch/namespace_test.rb +2 -1
  36. data/test/aitch/request/client_https_test.rb +1 -0
  37. data/test/aitch/request/follow_redirect_test.rb +4 -3
  38. data/test/aitch/request/json_request_test.rb +1 -0
  39. data/test/aitch/request/request_class_test.rb +3 -2
  40. data/test/aitch/request/status_code_validation_test.rb +3 -2
  41. data/test/aitch/request_test.rb +45 -8
  42. data/test/aitch/response/custom_response_parser_test.rb +1 -4
  43. data/test/aitch/response/default_response_parser_test.rb +14 -0
  44. data/test/aitch/response/errors_test.rb +1 -0
  45. data/test/aitch/response/html_response_test.rb +1 -0
  46. data/test/aitch/response/json_response_test.rb +2 -1
  47. data/test/aitch/response/raw_response_test.rb +1 -0
  48. data/test/aitch/response/status_3xx_test.rb +1 -0
  49. data/test/aitch/response/status_4xx_test.rb +1 -0
  50. data/test/aitch/response/status_5xx_test.rb +1 -0
  51. data/test/aitch/response/xml_response_test.rb +1 -0
  52. data/test/aitch/response_parser/html_parser_test.rb +1 -0
  53. data/test/aitch/response_parser/json_parser_test.rb +2 -1
  54. data/test/aitch/response_parser/xml_parser_test.rb +1 -1
  55. data/test/aitch/response_test.rb +1 -1
  56. data/test/aitch/to_query_test.rb +28 -0
  57. data/test/aitch/uri_test.rb +2 -1
  58. data/test/aitch/utils/symbolize_keys_test.rb +1 -0
  59. data/test/aitch/utils/underscore_test.rb +1 -0
  60. data/test/support/helpers.rb +5 -4
  61. data/test/test_helper.rb +3 -2
  62. metadata +77 -15
  63. data/.travis.yml +0 -23
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 1608bb2a19cd498230af4c13ff13f5470c038b8c
4
- data.tar.gz: 1dd7c3cde77b473060edfccb1865eed448de4cda
2
+ SHA256:
3
+ metadata.gz: a3f4cd3269e5b709f85e34148b2ed6aad4a9080f9903dc625d4840d8ffc2293a
4
+ data.tar.gz: d97a344ec31e5f6ba528f1d359b96aa2860ab51e1cdde507d5cbc0557e9a75a4
5
5
  SHA512:
6
- metadata.gz: 00029220787908d61ed6b1d64a8dcbe9e0dffdc3f450c3ce67d56b836d9967919fc12a1f18298919876478c5ef916146d529992d1008752b33eff1e36622573c
7
- data.tar.gz: ff9e4b489b83f083d172e035a5440bd4db7ef36404c8f6fe728c408124a8a86927069e18183eea312038b7fb581668fe7e729d39a5e90bd4131811f46c0ee618
6
+ metadata.gz: a11acc6e165677b372cd4c9a07b9fd33640611a056fb1c437917ffa1d89ab0520bcaf9943cd5e8f72a3f088942a8f7b9d968a59ec7207dc6a061f54364216aba
7
+ data.tar.gz: a0228f9d4719ee0fddd2932fece69c454062d40f4526612b41044168198f7ec91478267163e343a995ceb5f8bbcdc1d4d228d8edb73a9845b5aa0d08145581c9
@@ -0,0 +1,3 @@
1
+ ---
2
+ github: [fnando]
3
+ custom: ["https://www.paypal.me/nandovieira/🍕"]
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: Tests
3
+
4
+ on:
5
+ pull_request:
6
+ branches:
7
+ - main
8
+ push:
9
+ branches:
10
+ - main
11
+ jobs:
12
+ build:
13
+ name: Tests with Ruby ${{ matrix.ruby }}
14
+ runs-on: "ubuntu-latest"
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ ruby: ["2.7.x", "2.6.x", "2.5.x", "3.0.x", "3.1.x"]
19
+
20
+ steps:
21
+ - uses: actions/checkout@v1
22
+
23
+ - uses: actions/cache@v2
24
+ with:
25
+ path: vendor/bundle
26
+ key: >
27
+ ${{ runner.os }}-${{ matrix.ruby }}-gems-${{
28
+ hashFiles('aitch.gemspec') }}
29
+ restore-keys: >
30
+ ${{ runner.os }}-${{ matrix.ruby }}-gems-${{
31
+ hashFiles('aitch.gemspec') }}
32
+
33
+ - name: Set up Ruby
34
+ uses: actions/setup-ruby@v1
35
+ with:
36
+ ruby-version: ${{ matrix.ruby }}
37
+
38
+ - name: Install gem dependencies
39
+ env:
40
+ RAILS_ENV: test
41
+ run: |
42
+ gem install bundler
43
+ bundle config path vendor/bundle
44
+ bundle update --jobs 4 --retry 3
45
+
46
+ - name: Run Tests
47
+ env:
48
+ RAILS_ENV: test
49
+ run: |
50
+ bundle exec rake
data/.rubocop.yml ADDED
@@ -0,0 +1,26 @@
1
+ ---
2
+ inherit_gem:
3
+ rubocop-fnando: .rubocop.yml
4
+
5
+ AllCops:
6
+ TargetRubyVersion: 2.5
7
+
8
+ Layout/LineLength:
9
+ Exclude:
10
+ - test/**/*.rb
11
+
12
+ Naming/AccessorMethodName:
13
+ Exclude:
14
+ - lib/aitch/request.rb
15
+
16
+ Metrics/ClassLength:
17
+ Enabled: false
18
+
19
+ Metrics/AbcSize:
20
+ Enabled: false
21
+
22
+ Metrics/MethodLength:
23
+ Enabled: false
24
+
25
+ Metrics/ParameterLists:
26
+ Enabled: false
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
  gemspec
3
-
4
- gem "pry-meta", platforms: [:ruby_20, :ruby_21, :ruby_22]
data/README.md CHANGED
@@ -1,24 +1,24 @@
1
1
  # Aitch
2
2
 
3
- [![Build Status](https://travis-ci.org/fnando/aitch.png)](https://travis-ci.org/fnando/aitch)
3
+ [![Tests](https://github.com/fnando/aitch/workflows/Tests/badge.svg)](https://github.com/fnando/aitch)
4
4
  [![Code Climate](https://codeclimate.com/github/fnando/aitch/badges/gpa.svg)](https://codeclimate.com/github/fnando/aitch)
5
- [![Test Coverage](https://codeclimate.com/github/fnando/aitch/badges/coverage.svg)](https://codeclimate.com/github/fnando/aitch)
6
- [![RubyGems](https://badge.fury.io/rb/aitch.png)](https://rubygems.org/gems/aitch)
5
+ [![Gem Version](https://img.shields.io/gem/v/aitch.svg)](https://rubygems.org/gems/aitch)
6
+ [![Gem Downloads](https://img.shields.io/gem/dt/aitch.svg)](https://rubygems.org/gems/aitch)
7
7
 
8
8
  A simple HTTP client.
9
9
 
10
10
  Features:
11
11
 
12
- * Supports Gzip|Deflate response
13
- * Automatically parses JSON, HTML and XML responses
14
- * Automatically follows redirect
12
+ - Supports Gzip|Deflate response
13
+ - Automatically parses JSON, HTML and XML responses
14
+ - Automatically follows redirect
15
15
 
16
16
  ## Installation
17
17
 
18
18
  Add this line to your application's Gemfile:
19
19
 
20
20
  ```ruby
21
- gem 'aitch'
21
+ gem "aitch"
22
22
  ```
23
23
 
24
24
  And then execute:
@@ -54,6 +54,9 @@ Aitch.configure do |config|
54
54
 
55
55
  # Set the logger.
56
56
  config.logger = nil
57
+
58
+ # Set the base url.
59
+ config.base_url = nil
57
60
  end
58
61
  ```
59
62
 
@@ -83,6 +86,17 @@ response = Aitch.get do
83
86
  end
84
87
  ```
85
88
 
89
+ Finally, you can use keyword arguments:
90
+
91
+ ```ruby
92
+ Aitch.get(
93
+ url: "http://example.org",
94
+ params: {a: 1, b: 2},
95
+ headers: {Authorization: "Token token=abc"},
96
+ options: {follow_redirect: false}
97
+ )
98
+ ```
99
+
86
100
  ### Response
87
101
 
88
102
  The response object:
@@ -104,7 +118,8 @@ response.data # Parsed response body
104
118
 
105
119
  #### Parsing JSON, XML and HTML with Nokogiri
106
120
 
107
- If your response is a JSON, XML or a HTML content type, we'll automatically convert the response into the appropriate object.
121
+ If your response is a JSON, XML or a HTML content type, we'll automatically
122
+ convert the response into the appropriate object.
108
123
 
109
124
  ```ruby
110
125
  response = Aitch.get("http://simplesideias.com.br")
@@ -148,7 +163,8 @@ The request:
148
163
  Aitch.get("http://example.org")
149
164
  ```
150
165
 
151
- If the redirect limit is exceeded, then the `Aitch::TooManyRedirectsError` exception is raised.
166
+ If the redirect limit is exceeded, then the `Aitch::TooManyRedirectsError`
167
+ exception is raised.
152
168
 
153
169
  ### Basic auth
154
170
 
@@ -196,7 +212,8 @@ Request.get("http://example.org")
196
212
 
197
213
  ### Validating responses
198
214
 
199
- When you know the kind of response you're expecting, you can validate it by specifying the `expect` option.
215
+ When you know the kind of response you're expecting, you can validate it by
216
+ specifying the `expect` option.
200
217
 
201
218
  ```ruby
202
219
  Aitch.get do
@@ -205,7 +222,8 @@ Aitch.get do
205
222
  end
206
223
  ```
207
224
 
208
- If this request receives anything other than `200`, it will raise a `Aitch::StatusCodeError` exception.
225
+ If this request receives anything other than `200`, it will raise a
226
+ `Aitch::StatusCodeError` exception.
209
227
 
210
228
  ```
211
229
  Expect(200 OK) <=> Actual(404 Not Found)
@@ -215,7 +233,10 @@ You can also provide a list of accepted statuses, like `expect: [200, 201]`.
215
233
 
216
234
  ### Response Parsers
217
235
 
218
- You can register new response parsers by using `Aitch::ResponseParser.register(name, parser)`, where parser must implement the methods `match?(content_type)` and `load(response_body)`. This is how you could load CSV values.
236
+ You can register new response parsers by using
237
+ `Aitch::ResponseParser.register(name, parser)`, where parser must implement the
238
+ methods `match?(content_type)` and `load(response_body)`. This is how you could
239
+ load CSV values.
219
240
 
220
241
  ```ruby
221
242
  require "csv"
@@ -237,7 +258,8 @@ end
237
258
  Aitch::ResponseParser.prepend(:csv, CSVParser)
238
259
  ```
239
260
 
240
- The default behavior is returning the response body. You can replace it as the following:
261
+ The default behavior is returning the response body. You can replace it as the
262
+ following:
241
263
 
242
264
  ```ruby
243
265
  module DefaultParser
@@ -261,13 +283,31 @@ Aitch::ResponseParser.append(:default, DefaultParser)
261
283
 
262
284
  Aitch comes with response parsers for HTML, XML and JSON.
263
285
 
264
- By default, the JSON parser will be `JSON`. To set it to something else, use `Aitch::ResponseParser::JSONParser.engine`.
286
+ By default, the JSON parser will be `JSON`. To set it to something else, use
287
+ `Aitch::ResponseParser::JSONParser.engine`.
265
288
 
266
289
  ```ruby
267
290
  require "oj"
268
291
  Aitch::ResponseParser::JSONParser.engine = Oj
269
292
  ```
270
293
 
294
+ ### Setting the base url
295
+
296
+ When you're creating a wrapper for an API, usually the hostname is the same for
297
+ the whole API. In this case, you can avoid having to pass it around all the time
298
+ by setting `Aitch::Configuration#base_url`. This option is meant to be used when
299
+ you instantiate a new namespace.
300
+
301
+ ```ruby
302
+ Client = Aitch::Namespace.new
303
+
304
+ Client.configure do |config|
305
+ config.base_url = "https://api.example.com"
306
+ end
307
+
308
+ Client.get("/users")
309
+ ```
310
+
271
311
  ## Contributing
272
312
 
273
313
  1. Fork it
data/Rakefile CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "bundler/gem_tasks"
3
4
  require "rake/testtask"
5
+ require "rubocop/rake_task"
4
6
 
5
7
  Rake::TestTask.new(:test) do |t|
6
8
  t.libs << "test"
@@ -8,4 +10,6 @@ Rake::TestTask.new(:test) do |t|
8
10
  t.warning = false
9
11
  end
10
12
 
11
- task default: :test
13
+ RuboCop::RakeTask.new
14
+
15
+ task default: %i[test rubocop]
data/aitch.gemspec CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "./lib/aitch/version"
2
4
 
3
5
  Gem::Specification.new do |spec|
@@ -9,20 +11,23 @@ Gem::Specification.new do |spec|
9
11
  spec.summary = spec.description
10
12
  spec.homepage = "http://rubygems.org/gems/aitch"
11
13
  spec.license = "MIT"
14
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
12
15
 
13
- spec.files = `git ls-files`.split($/)
14
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
+ spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
17
+ spec.executables = spec.files.grep(%r{^bin/}) {|f| File.basename(f) }
15
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
19
  spec.require_paths = ["lib"]
17
20
 
18
- spec.required_ruby_version = ">= 2.0"
19
-
20
- spec.add_dependency "nokogiri", ">= 1.6.0"
21
+ spec.add_dependency "nokogiri"
21
22
 
22
- spec.add_development_dependency "codeclimate-test-reporter"
23
23
  spec.add_development_dependency "bundler"
24
- spec.add_development_dependency "rake"
24
+ spec.add_development_dependency "minitest"
25
25
  spec.add_development_dependency "minitest-utils"
26
26
  spec.add_development_dependency "mocha"
27
+ spec.add_development_dependency "pry-meta"
28
+ spec.add_development_dependency "rake"
29
+ spec.add_development_dependency "rubocop"
30
+ spec.add_development_dependency "rubocop-fnando"
31
+ spec.add_development_dependency "simplecov"
27
32
  spec.add_development_dependency "webmock"
28
33
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Aitch
3
4
  class Configuration
4
5
  # Set proxy.
@@ -22,12 +23,16 @@ module Aitch
22
23
  # Set the logger.
23
24
  attr_accessor :logger
24
25
 
26
+ # Set the base url.
27
+ attr_accessor :base_url
28
+
25
29
  def initialize
26
30
  @timeout = 10
27
31
  @redirect_limit = 5
28
32
  @follow_redirect = true
29
33
  @user_agent = "Aitch/#{Aitch::VERSION} (http://rubygems.org/gems/aitch)"
30
34
  @default_headers = {}
35
+ @base_url = nil
31
36
  end
32
37
 
33
38
  def to_h
data/lib/aitch/dsl.rb CHANGED
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Aitch
3
4
  class DSL
4
5
  %w[url options headers data].each do |name|
5
- class_eval <<-RUBY
6
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
6
7
  attr_writer :#{name}
7
8
 
8
9
  def #{name}(*args)
@@ -12,8 +13,8 @@ module Aitch
12
13
  RUBY
13
14
  end
14
15
 
15
- alias_method :params, :data
16
- alias_method :body, :data
16
+ alias params data
17
+ alias body data
17
18
 
18
19
  def to_h
19
20
  {
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aitch
4
+ module Engines
5
+ module JSON
6
+ def self.load(data)
7
+ data && ::JSON.parse(data)
8
+ end
9
+
10
+ def self.dump(data)
11
+ ::JSON.dump(data)
12
+ end
13
+ end
14
+ end
15
+ end
data/lib/aitch/errors.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Aitch
3
4
  InvalidURIError = Class.new(StandardError)
4
5
  InvalidHTTPMethodError = Class.new(StandardError)
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "cgi"
3
4
 
4
5
  class Object
@@ -36,23 +37,18 @@ class FalseClass
36
37
  end
37
38
 
38
39
  class Array
39
- # Calls <tt>to_param</tt> on all its elements and joins the result with
40
- # slashes. This is used by <tt>url_for</tt> in Action Pack.
41
- def to_param
42
- collect { |e| e.to_param }.join '/'
43
- end
44
-
45
40
  # Converts an array into a string suitable for use as a URL query string,
46
41
  # using the given +key+ as the param name.
47
42
  #
48
- # ['Rails', 'coding'].to_query('hobbies') # => "hobbies%5B%5D=Rails&hobbies%5B%5D=coding"
43
+ # ["Rails", "coding"].to_query("hobbies")
44
+ # # => "hobbies%5B%5D=Rails&hobbies%5B%5D=coding"
49
45
  def to_query(key)
50
46
  prefix = "#{key}[]"
51
47
 
52
48
  if empty?
53
49
  nil.to_query(prefix)
54
50
  else
55
- collect { |value| value.to_query(prefix) }.join '&'
51
+ collect {|value| value.to_query(prefix) }.join "&"
56
52
  end
57
53
  end
58
54
  end
@@ -78,8 +74,8 @@ class Hash
78
74
  unless (value.is_a?(Hash) || value.is_a?(Array)) && value.empty?
79
75
  value.to_query(namespace ? "#{namespace}[#{key}]" : key)
80
76
  end
81
- end.compact.sort! * '&'
77
+ end.compact.sort! * "&"
82
78
  end
83
79
 
84
- alias_method :to_param, :to_query
80
+ alias to_param to_query
85
81
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Aitch
3
4
  class Location
4
5
  attr_reader :redirect_stack, :current_url
@@ -9,7 +10,7 @@ module Aitch
9
10
  end
10
11
 
11
12
  def location
12
- return current_url unless current_url.match(%r[\A/])
13
+ return current_url unless current_url.match?(%r{\A/}) # rubocop:disable Performance/StartWith
13
14
 
14
15
  uri = find_uri_with_host
15
16
  url = ["#{uri.scheme}://#{uri.hostname}"]
@@ -20,8 +21,8 @@ module Aitch
20
21
 
21
22
  def find_uri_with_host
22
23
  redirect_stack.reverse
23
- .map {|url| ::URI.parse(url) }
24
- .find {|uri| uri.scheme }
24
+ .map {|url| ::URI.parse(url) }
25
+ .find(&:scheme)
25
26
  end
26
27
  end
27
28
  end
@@ -1,17 +1,31 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Aitch
3
4
  class Namespace
4
- def configure(&block)
5
+ def configure
5
6
  yield config
6
7
  end
7
8
 
8
9
  def config
9
10
  @config ||= Configuration.new
10
11
  end
11
- alias_method :configuration, :config
12
+ alias configuration config
13
+
14
+ def execute(
15
+ request_method: nil,
16
+ url: nil,
17
+ params: nil,
18
+ data: nil,
19
+ body: nil,
20
+ headers: nil,
21
+ options: nil,
22
+ &block
23
+ )
24
+ data = data || params || body || {}
25
+ headers ||= {}
26
+ options ||= {}
12
27
 
13
- def execute(request_method = nil, url = nil, data = {}, headers = {}, options = {}, &block)
14
- if block_given?
28
+ if block
15
29
  dsl = DSL.new
16
30
  dsl.instance_eval(&block)
17
31
  args = dsl.to_h
@@ -24,17 +38,18 @@ module Aitch
24
38
  }
25
39
  end
26
40
 
27
- args.merge!(
28
- request_method: request_method,
29
- options: config.to_h.merge(Utils.symbolize_keys(args[:options]))
30
- )
41
+ args[:request_method] = request_method
42
+ args[:options] = config.to_h.merge(Utils.symbolize_keys(args[:options]))
31
43
 
32
44
  Request.new(args).perform
33
45
  end
34
46
 
35
47
  def execute!(*args, &block)
36
- response = execute(*args, &block)
48
+ options = extract_args!(args)
49
+ response = execute(**options, &block)
50
+
37
51
  raise response.error if response.error?
52
+
38
53
  response
39
54
  end
40
55
 
@@ -47,14 +62,23 @@ module Aitch
47
62
  options
48
63
  trace
49
64
  head
50
- ].each do |method_name|
51
- define_method(method_name) do |url = nil, data = {}, headers = {}, options = {}, &block|
52
- execute(method_name, url, data, headers, options, &block)
65
+ ].each do |request_method|
66
+ define_method(request_method) do |*args, &block|
67
+ options = extract_args!(args)
68
+ execute(**options.merge(request_method: request_method), &block)
53
69
  end
54
70
 
55
- define_method("#{method_name}!") do |url = nil, data = {}, headers = {}, options = {}, &block|
56
- execute!(method_name, url, data, headers, options, &block)
71
+ define_method("#{request_method}!") do |*args, &block|
72
+ options = extract_args!(args)
73
+
74
+ execute!(**options.merge(request_method: request_method), &block)
57
75
  end
58
76
  end
77
+
78
+ private def extract_args!(args)
79
+ return args.first if args.size == 1 && args.first.is_a?(Hash)
80
+
81
+ %i[url data headers options].zip(args).to_h
82
+ end
59
83
  end
60
84
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Aitch
3
4
  class Redirect
4
5
  attr_reader :tries