f_http_client 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fa009fca4cb5465b7a9c07e71037e15c5dffbbe97804e1b6a7ee2888cbfa771c
4
+ data.tar.gz: '058b8ef192570f144a44c4dac7e7f95aa401923f4176522944421b507b93e7e0'
5
+ SHA512:
6
+ metadata.gz: a80d429fdc6fc7134ffeee8d6d627a87bad579dc4b29c38113e1a927e503cca3174de5d45829a0a39eb535c3c35b1193a2c3971bdad02acbfdc1cf04e305f1bc
7
+ data.tar.gz: e1f5e810e2fb1003fa8d503b7703699c75e2c1dc139062306b6eb1ade158cbdc1fe2273ea0cf76d772858a7cc803d61dfa2f3f4d29622d5103b5a7476f24123b
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,121 @@
1
+ require:
2
+ - rubocop-rspec
3
+ - rubocop-performance
4
+
5
+ AllCops:
6
+ TargetRubyVersion: 3.0
7
+ SuggestExtensions: false
8
+ NewCops: enable
9
+ CacheRootDirectory: tmp
10
+ Exclude:
11
+ - 'vendor/**/*'
12
+ - 'bin/**/*'
13
+ - '.git/**/*'
14
+ - 'examples/**/*'
15
+
16
+ ##### LAYOUT #####
17
+
18
+ Layout/FirstArrayElementIndentation:
19
+ Enabled: true
20
+ EnforcedStyle: consistent
21
+
22
+ Layout/FirstHashElementIndentation:
23
+ Enabled: true
24
+ EnforcedStyle: consistent
25
+
26
+ Layout/LineLength:
27
+ Max: 120
28
+ AllowedPatterns: ['(\A|\s)#']
29
+
30
+ ##### STYLE #####
31
+
32
+ Style/PreferredHashMethods:
33
+ Enabled: true
34
+ Safe: false
35
+ EnforcedStyle: verbose
36
+
37
+ Style/SymbolArray:
38
+ Enabled: true
39
+ EnforcedStyle: percent
40
+ MinSize: 3
41
+
42
+ Style/WordArray:
43
+ Enabled: true
44
+ EnforcedStyle: percent
45
+ MinSize: 3
46
+
47
+ Style/MultilineBlockChain:
48
+ Enabled: false
49
+
50
+ Style/Lambda:
51
+ Enabled: true
52
+ EnforcedStyle: literal
53
+
54
+ Style/LambdaCall:
55
+ Enabled: false
56
+
57
+ Style/Documentation:
58
+ Enabled: false
59
+
60
+ Style/ClassAndModuleChildren:
61
+ Enabled: false
62
+
63
+ ##### LINT #####
64
+
65
+ Lint/MissingSuper:
66
+ Enabled: false
67
+
68
+ ##### NAMING #####
69
+
70
+ Naming/InclusiveLanguage:
71
+ Enabled: false
72
+
73
+ Naming/PredicateName:
74
+ ForbiddenPrefixes:
75
+ - is_
76
+ - have_
77
+ # allows `has_` predicate
78
+
79
+ ###### METRICS #####
80
+
81
+ Metrics/AbcSize:
82
+ Enabled: true
83
+ Max: 22
84
+
85
+ Metrics/BlockLength:
86
+ Enabled: true
87
+ CountComments: false
88
+ Exclude:
89
+ - '**/*.gemspec'
90
+ - 'spec/**/*'
91
+
92
+ Metrics/ClassLength:
93
+ Max: 200
94
+
95
+ Metrics/MethodLength:
96
+ Enabled: true
97
+ CountAsOne: ['array', 'heredoc']
98
+ Max: 20
99
+
100
+ ##### RSPEC #####
101
+
102
+ RSpec/ContextWording:
103
+ Prefixes:
104
+ - and
105
+ - but
106
+ - when
107
+ - with
108
+ - without
109
+
110
+ RSpec/ExampleLength:
111
+ Max: 20
112
+
113
+ RSpec/FilePath:
114
+ CustomTransform:
115
+ FHTTPClient: f_http_client
116
+
117
+ RSpec/MultipleMemoizedHelpers:
118
+ Enabled: false
119
+
120
+ RSpec/NestedGroups:
121
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2023-02-13
4
+
5
+ - Initial release
6
+ - Add Caching strategies;
7
+ - Add Logging strategies;
8
+ - Add Custom parse for JSON responses;
9
+ - Add Response and Exception processors;
10
+ - Add Base client class;
11
+ - Add Basic configuration;
12
+ - Add RSpec helpers to simulate responses
data/Gemfile ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in f_http_client.gemspec
6
+ gemspec
7
+
8
+ group :development do
9
+ gem 'f_service', github: 'Fretadao/f_service', branch: 'master'
10
+ gem 'rubocop'
11
+ gem 'rubocop-performance'
12
+ gem 'rubocop-rspec'
13
+ end
14
+
15
+ group :test do
16
+ gem 'rspec', '~> 3.0'
17
+ gem 'simplecov'
18
+ gem 'webmock'
19
+ end
20
+
21
+ group :development, :test do
22
+ gem 'pry'
23
+ gem 'pry-nav'
24
+ gem 'rake', '~> 13.0'
25
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,132 @@
1
+ GIT
2
+ remote: https://github.com/Fretadao/f_service.git
3
+ revision: 22d3e1c8bd4aa83b14f34b1af19aa4cd22b8ad8b
4
+ branch: master
5
+ specs:
6
+ f_service (0.2.0)
7
+
8
+ PATH
9
+ remote: .
10
+ specs:
11
+ f_http_client (0.1.0)
12
+ activesupport
13
+ addressable
14
+ dry-configurable
15
+ dry-initializer
16
+ httparty
17
+
18
+ GEM
19
+ remote: https://rubygems.org/
20
+ specs:
21
+ activesupport (7.0.4.3)
22
+ concurrent-ruby (~> 1.0, >= 1.0.2)
23
+ i18n (>= 1.6, < 2)
24
+ minitest (>= 5.1)
25
+ tzinfo (~> 2.0)
26
+ addressable (2.8.1)
27
+ public_suffix (>= 2.0.2, < 6.0)
28
+ ast (2.4.2)
29
+ coderay (1.1.3)
30
+ concurrent-ruby (1.2.2)
31
+ crack (0.4.5)
32
+ rexml
33
+ diff-lcs (1.5.0)
34
+ docile (1.4.0)
35
+ dry-configurable (1.0.1)
36
+ dry-core (~> 1.0, < 2)
37
+ zeitwerk (~> 2.6)
38
+ dry-core (1.0.0)
39
+ concurrent-ruby (~> 1.0)
40
+ zeitwerk (~> 2.6)
41
+ dry-initializer (3.1.1)
42
+ hashdiff (1.0.1)
43
+ httparty (0.21.0)
44
+ mini_mime (>= 1.0.0)
45
+ multi_xml (>= 0.5.2)
46
+ i18n (1.12.0)
47
+ concurrent-ruby (~> 1.0)
48
+ json (2.6.3)
49
+ method_source (1.0.0)
50
+ mini_mime (1.1.2)
51
+ minitest (5.18.0)
52
+ multi_xml (0.6.0)
53
+ parallel (1.22.1)
54
+ parser (3.2.1.0)
55
+ ast (~> 2.4.1)
56
+ pry (0.14.2)
57
+ coderay (~> 1.1)
58
+ method_source (~> 1.0)
59
+ pry-nav (1.0.0)
60
+ pry (>= 0.9.10, < 0.15)
61
+ public_suffix (5.0.1)
62
+ rainbow (3.1.1)
63
+ rake (13.0.6)
64
+ regexp_parser (2.7.0)
65
+ rexml (3.2.5)
66
+ rspec (3.12.0)
67
+ rspec-core (~> 3.12.0)
68
+ rspec-expectations (~> 3.12.0)
69
+ rspec-mocks (~> 3.12.0)
70
+ rspec-core (3.12.1)
71
+ rspec-support (~> 3.12.0)
72
+ rspec-expectations (3.12.2)
73
+ diff-lcs (>= 1.2.0, < 2.0)
74
+ rspec-support (~> 3.12.0)
75
+ rspec-mocks (3.12.3)
76
+ diff-lcs (>= 1.2.0, < 2.0)
77
+ rspec-support (~> 3.12.0)
78
+ rspec-support (3.12.0)
79
+ rubocop (1.45.1)
80
+ json (~> 2.3)
81
+ parallel (~> 1.10)
82
+ parser (>= 3.2.0.0)
83
+ rainbow (>= 2.2.2, < 4.0)
84
+ regexp_parser (>= 1.8, < 3.0)
85
+ rexml (>= 3.2.5, < 4.0)
86
+ rubocop-ast (>= 1.24.1, < 2.0)
87
+ ruby-progressbar (~> 1.7)
88
+ unicode-display_width (>= 2.4.0, < 3.0)
89
+ rubocop-ast (1.26.0)
90
+ parser (>= 3.2.1.0)
91
+ rubocop-capybara (2.17.1)
92
+ rubocop (~> 1.41)
93
+ rubocop-performance (1.16.0)
94
+ rubocop (>= 1.7.0, < 2.0)
95
+ rubocop-ast (>= 0.4.0)
96
+ rubocop-rspec (2.18.1)
97
+ rubocop (~> 1.33)
98
+ rubocop-capybara (~> 2.17)
99
+ ruby-progressbar (1.11.0)
100
+ simplecov (0.22.0)
101
+ docile (~> 1.1)
102
+ simplecov-html (~> 0.11)
103
+ simplecov_json_formatter (~> 0.1)
104
+ simplecov-html (0.12.3)
105
+ simplecov_json_formatter (0.1.4)
106
+ tzinfo (2.0.6)
107
+ concurrent-ruby (~> 1.0)
108
+ unicode-display_width (2.4.2)
109
+ webmock (3.18.1)
110
+ addressable (>= 2.8.0)
111
+ crack (>= 0.3.2)
112
+ hashdiff (>= 0.4.0, < 2.0.0)
113
+ zeitwerk (2.6.7)
114
+
115
+ PLATFORMS
116
+ x86_64-linux
117
+
118
+ DEPENDENCIES
119
+ f_http_client!
120
+ f_service!
121
+ pry
122
+ pry-nav
123
+ rake (~> 13.0)
124
+ rspec (~> 3.0)
125
+ rubocop
126
+ rubocop-performance
127
+ rubocop-rspec
128
+ simplecov
129
+ webmock
130
+
131
+ BUNDLED WITH
132
+ 2.4.6
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Fretadão
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # FHTTPClient
2
+
3
+ Provides a basic skeleton for creating an HTTP client using the FService architecture.
4
+
5
+ ## Installation
6
+
7
+ Add to your `.gemspec` client file `spec.add_runtime_dependency 'f_http_client'` and run `bundle install`.
8
+
9
+ ## Configure
10
+
11
+ The gem allow the following configuration
12
+
13
+ - base_uri: a URI to be used for all services for your client;
14
+ - log_strategy: s symbol representing which logger the gem will use;
15
+ - null (default): means that the client will not log anything;
16
+ - rails: means that Rails logger will be used;
17
+ - any other value the gem will use a default logger which loggs at STDOUT.
18
+ - cache
19
+ - strategy: Defines which cache strategy will be used;
20
+ - null (default): the client does not log anything;
21
+ - rails: the Rails.cache will be used to perform caching;
22
+ - expires_in: Deines in seconds how much time the cache will be kept (default 0).
23
+
24
+ ```rb
25
+ module BlogClient
26
+ class Configuration < FHTTPClient::Configuration
27
+ setting :paginate do
28
+ setting :enabled, default: true
29
+ setting :per_page, :20
30
+ end
31
+ end
32
+ end
33
+
34
+ BlogClient::Configuration.configure do |config|
35
+ config.base_uri = 'https://jsonplaceholder.typicode.com'
36
+ confg.log_strategy = :rails
37
+
38
+ config.cache.strategy = :rails
39
+ config.cache.expires_in = 25.minutes
40
+ config.paginate.per_page = 50
41
+ end
42
+
43
+
44
+ class BlogClient::Base < FHTTPClient::Base
45
+ def self.config
46
+ BlogClient::Configuration.config
47
+ end
48
+
49
+ cache_expires_in 10.minutes
50
+ end
51
+ ```
52
+
53
+ ## Usage
54
+
55
+ Could create a class:
56
+
57
+ ```rb
58
+ # frozen_string_literal: true
59
+
60
+ module BlogClient
61
+ module Post
62
+ class Find < BlogClient::Base
63
+ private
64
+
65
+ def make_request
66
+ self.class.get(formatted_path, headers: headers)
67
+ end
68
+
69
+ def path_template
70
+ '/posts/%<id>s'
71
+ end
72
+
73
+ def headers
74
+ @headers.merge(content_type: 'application/json')
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ # BlogClient::Post::Find.(path_params: { id: 1 })
81
+ # .and_then { |response| response.parsed_response }
82
+ #
83
+ # => {
84
+ # userId: 1,
85
+ # id: 1,
86
+ # title: "How to use a FHTTPCLient Gem",
87
+ # body: "A great text here."
88
+ # }
89
+ ```
90
+
91
+ Result examples:
92
+
93
+ ```rb
94
+ Person::Create.(name: 'Joe Vicenzo', birthdate: '2000-01-01')
95
+ .and_then { |user| return redirect_to users_path(user.id) }
96
+ .on_failure(:unprocessable_entity) { |errors| return render_action :new, locals: { errors: errors } }
97
+ .on_failure(:client_error) { |errors| render_action :new, warning: errors }
98
+ .on_failure(:server_error) { |error| render_action :new, warning: ['Try again latter.'] }
99
+ .on_failure(:server_error) { |error| render_action :new, warning: ['Server is busy. Try again latter.'] }
100
+ .on_failure { |_error, type| render_action :new, warning: ["Unexpected error. Contact admin and talk about #{type} error."] }
101
+ ```
102
+
103
+ This gem uses the gem [HTTParty](https://github.com/jnunemaker/httparty) as base to perform requests.
104
+ Then we can use any [example](https://github.com/jnunemaker/httparty/tree/master/examples) to implements the method make_request, for GET, POST, PUT, etc.
105
+ The class *FHTTPClient::Base* provides the following options to help building the request:
106
+ - *headers*: can be used to provide custom headers;
107
+ - *query*: can be used to provide querystring params;
108
+ - *body*: can be used to provide a body when request requires this;
109
+ - *options*: can be used provide any other option for HTTParty;
110
+ - *path_params*, can be used to fills params which is in the request path.
111
+
112
+ ## Development
113
+
114
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
115
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
116
+
117
+ To install this gem onto your local machine, run `bundle exec rake install`.
118
+ To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
119
+
120
+ ## Contributing
121
+
122
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Fretadao/f_http_client.
123
+
124
+ ## License
125
+
126
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Post
4
+ class Find < FHTTPClient::Base
5
+ base_uri 'https://jsonplaceholder.typicode.com'
6
+
7
+ private
8
+
9
+ def make_request
10
+ self.class.get(formatted_path, headers: headers)
11
+ end
12
+
13
+ def path_template
14
+ '/posts/%<id>s'
15
+ end
16
+
17
+ def headers
18
+ @headers.merge(content_type: 'application/json')
19
+ end
20
+ end
21
+ end
22
+
23
+ # Post::Find.(path_params: { id: 1 })
24
+ # .and_then { |response| response.parsed_response }
25
+ #
26
+ # => {
27
+ # userId: 1,
28
+ # id: 1,
29
+ # title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
30
+ # body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
31
+ # }
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FHTTPClient
4
+ # Allows to have an http client to build a service
5
+ # @abstract
6
+ class Base < FHTTPClient::Service
7
+ include HTTParty
8
+
9
+ DEFAULT_SKIP_CACHE_IF = ->(response) { FHTTPClient::Cache::HTTPResponseAnalyzer.not_cacheable?(response) }
10
+
11
+ option :headers, default: -> { {} }
12
+ option :query, default: -> { {} }
13
+ option :body, default: -> { {} }
14
+ option :options, default: -> { {} }
15
+ option :path_params, default: -> { {} }
16
+
17
+ parser FHTTPClient::Parser::Response
18
+
19
+ def run
20
+ configure_base_uri.and_then do
21
+ response = make_cached_request.value!
22
+
23
+ FHTTPClient::Processor::Response.(response: response, log_strategy: log_strategy)
24
+ end
25
+ rescue StandardError => e
26
+ FHTTPClient::Processor::Exception.(error: e, log_strategy: log_strategy)
27
+ .on_failure(:uncaught_error) { raise e }
28
+ end
29
+
30
+ def self.config
31
+ raise NotImplementedError, 'Clients must implement .config'
32
+ end
33
+
34
+ def self.cache_strategy(strategy = nil)
35
+ default_options[:cache_strategy] = config.cache.strategy if default_options[:cache_strategy].nil?
36
+ return default_options[:cache_strategy] unless strategy
37
+
38
+ default_options[:cache_strategy] = strategy
39
+ end
40
+
41
+ def self.cache_expires_in(expires_in = nil)
42
+ default_options[:cache_expires_in] = config.cache.expires_in if default_options[:cache_expires_in].nil?
43
+
44
+ return default_options[:cache_expires_in] unless expires_in
45
+
46
+ default_options[:cache_expires_in] = expires_in
47
+ end
48
+
49
+ private
50
+
51
+ def configure_base_uri
52
+ return Success(:uri_already_configured) if self.class.base_uri.present?
53
+
54
+ return Failure(:no_base_uri_configured) if config.base_uri.blank?
55
+
56
+ self.class.base_uri(config.base_uri)
57
+
58
+ Success(:base_uri_configured)
59
+ end
60
+
61
+ def make_cached_request
62
+ FHTTPClient::Store.(
63
+ key: cache_key,
64
+ strategy: cache_strategy,
65
+ block: -> { make_request },
66
+ expires_in: cache_expires_in,
67
+ skip_if: skip_cache_if
68
+ )
69
+ end
70
+
71
+ def make_request
72
+ raise NotImplementedError, 'Clients must implement #make_request'
73
+ end
74
+
75
+ def path_template
76
+ raise NotImplementedError, 'Clients must implement #path_template'
77
+ end
78
+
79
+ def formatted_path
80
+ @formatted_path ||= format(path_template, path_params)
81
+ end
82
+
83
+ def cache_key
84
+ FHTTPClient::Cache::Key.generate(
85
+ self.class,
86
+ path: formatted_path,
87
+ headers: headers,
88
+ query: query,
89
+ body: body,
90
+ options: options
91
+ )
92
+ end
93
+
94
+ def config
95
+ self.class.config
96
+ end
97
+
98
+ def log_strategy
99
+ config.log_strategy
100
+ end
101
+
102
+ def cache_strategy
103
+ self.class.default_options[:cache_strategy] || config.cache.strategy
104
+ end
105
+
106
+ def cache_expires_in
107
+ self.class.default_options[:cache_expires_in] || config.cache.expires_in
108
+ end
109
+
110
+ def skip_cache_if
111
+ DEFAULT_SKIP_CACHE_IF
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FHTTPClient
4
+ module Cache
5
+ module HTTPResponseAnalyzer
6
+ attr_reader :response
7
+
8
+ NOT_CACHEABLE_RESPONSES = %i[
9
+ server_error?
10
+ gateway_timeout?
11
+ request_timeout?
12
+ unauthorized?
13
+ too_many_requests?
14
+ ].freeze
15
+
16
+ class << self
17
+ def not_cacheable_responses
18
+ NOT_CACHEABLE_RESPONSES
19
+ end
20
+
21
+ def not_cacheable?(response)
22
+ not_cacheable_responses.any? do |not_cacheable_response|
23
+ response.public_send(not_cacheable_response)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FHTTPClient
4
+ module Cache
5
+ class Key
6
+ def self.generate(*args)
7
+ params = args.extract_options!
8
+ klass = args.first.to_s
9
+
10
+ [klass.underscore, params.compact_blank.to_query].filter_map(&:presence).join('?')
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FHTTPClient
4
+ module Cache
5
+ class Null
6
+ class << self
7
+ def fetch(_name, _options = nil)
8
+ yield if block_given?
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FHTTPClient
4
+ module Cache
5
+ RailsNotDefined = Class.new(StandardError)
6
+
7
+ class Rails
8
+ class << self
9
+ def fetch(name, options = {})
10
+ raise RailsNotDefined unless defined?(::Rails)
11
+
12
+ cache_entry = cache.read(name)
13
+
14
+ return cache_entry if cache_entry.present?
15
+ return unless block_given?
16
+
17
+ result = yield
18
+
19
+ cache.write(name, result, options) unless options[:skip_if]&.(result)
20
+
21
+ result
22
+ end
23
+
24
+ private
25
+
26
+ def cache
27
+ ::Rails.cache
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FHTTPClient
4
+ class Configuration
5
+ extend Dry::Configurable
6
+
7
+ setting :base_uri
8
+ setting :log_strategy, default: :null
9
+ setting :cache do
10
+ setting :strategy, default: :null
11
+ setting :expires_in, default: 0
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FHTTPClient
4
+ # Allow to log a message to system log
5
+ #
6
+ # Attributes:
7
+ # - message: object containint the message to be logged;
8
+ # - level (optional): level symbol to log the message (debug, info, warn error, fatal or unknown);
9
+ # - tags (optional): list of tags to be added to log line.
10
+ #
11
+ # Example:
12
+ # FHTTPClient::Log.(message: { name: 'Bruno' }.to_json)
13
+ # FHTTPClient::Log.(message: { name: 'Bruno' }.to_json, stragegy: :rails)
14
+ # FHTTPClient::Log.(message: { name: 'Bruno' }.to_json, tags: ['Response'])
15
+ # FHTTPClient::Log.(message: { response: { name: 'Bruno' } }.to_json, level: :warn)
16
+ class Log < FHTTPClient::Service
17
+ LOGGERS = {
18
+ rails: FHTTPClient::Logger::Rails,
19
+ default: FHTTPClient::Logger::Default
20
+ }.freeze
21
+ private_constant :LOGGERS
22
+
23
+ option :message
24
+ option :strategy, default: -> { :null }
25
+ option :level, default: -> { :info }
26
+ option :tags, default: -> { [] }
27
+
28
+ def run
29
+ logger.tagged(*Array(tags)).public_send(level, message)
30
+
31
+ Success(:logged)
32
+ end
33
+
34
+ def logger
35
+ @logger ||= LOGGERS.fetch(strategy, FHTTPClient::Logger::Null).new
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FHTTPClient
4
+ module Logger
5
+ # A Basic logger to use when no logger is provided
6
+ class Default
7
+ def initialize(tags: [])
8
+ @logger = ::Logger.new($stdout)
9
+ @current_tags = tags
10
+ end
11
+
12
+ def tagged(*tags)
13
+ tagged = self.class.new(tags: current_tags + tags)
14
+ block_given? ? yield(tagged) : tagged
15
+ end
16
+
17
+ def debug(message) = add(::Logger::DEBUG, message)
18
+ def info(message) = add(::Logger::INFO, message)
19
+ def warn(message) = add(::Logger::WARN, message)
20
+ def error(message) = add(::Logger::ERROR, message)
21
+ def fatal(message) = add(::Logger::FATAL, message)
22
+ def unknown(message) = add(::Logger::UNKNOWN, message)
23
+
24
+ private
25
+
26
+ attr_reader :logger, :current_tags
27
+
28
+ def add(severity, message)
29
+ formatted_tags = format_tags
30
+ full_message = formatted_tags.empty? ? message : [format_tags, message].join(' - ')
31
+
32
+ logger.add(severity, full_message)
33
+ end
34
+
35
+ def format_tags
36
+ current_tags.map { |tag| "[#{tag}]" }.join(' ')
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FHTTPClient
4
+ module Logger
5
+ # Logs nothing
6
+ class Null
7
+ def tagged(...) = block_given? ? yield(self) : self
8
+ def debug(...) = nil
9
+ def info(...) = nil
10
+ def warn(...) = nil
11
+ def error(...) = nil
12
+ def fatal(...) = nil
13
+ def unknown(...) = nil
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FHTTPClient
4
+ module Logger
5
+ # Logging with Rails logger
6
+ class Rails
7
+ extend Forwardable
8
+
9
+ delegate %i[tagged debug info warn error fatal unknown] => :@logger
10
+
11
+ def initialize
12
+ @logger = ::Rails.logger
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FHTTPClient
4
+ module Parser
5
+ # Parse a json response letting this as symbol hash
6
+ # Following those examples: https://github.com/jnunemaker/httparty/blob/master/examples/custom_parsers.rb
7
+ # And looking default formats: https://github.com/jnunemaker/httparty/blob/fb7c40303b7e6429196ef748754505768520407c/lib/httparty/parser.rb#L42
8
+ class Response < HTTParty::Parser
9
+ protected
10
+
11
+ def json
12
+ JSON.parse(body, quirks_mode: true, allow_nan: true, symbolize_names: true)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FHTTPClient
4
+ module Processor
5
+ # This Service proccess a HTTP exception generating failure result.
6
+ #
7
+ # Example:
8
+ #
9
+ # def process
10
+ # error = Errno::ECONNREFUSED.new('Failed to open TCP connection to :80')
11
+ # FHTTPClient::Processor::ResponseProcessor.(error: error)
12
+ # .on_failure(:connection_refused) { return 'The server has been refused the connection.' }
13
+ # .on_failure { |error_message| return "Generic #{error_message}" }
14
+ # end
15
+ #
16
+ # proccess
17
+ # # => 'The server has been refused the connection.'
18
+ class Exception < FHTTPClient::Service
19
+ option :error
20
+ option :log_strategy, default: -> { :null }
21
+
22
+ def run
23
+ log_data.and_then { Failure(error_name, :exception, data: error) }
24
+ end
25
+
26
+ private
27
+
28
+ def error_name
29
+ case error.class.to_s
30
+ when 'Errno::ECONNREFUSED'
31
+ :connection_refused
32
+ when /timeout/i
33
+ :timeout
34
+ else
35
+ :uncaught_error
36
+ end
37
+ end
38
+
39
+ def log_data
40
+ FHTTPClient::Log.(
41
+ message: { error: error.class.to_s, message: error.message, backtrace: error.backtrace.join(', ') }.to_json,
42
+ tags: 'EXTERNAL REQUEST',
43
+ level: :error,
44
+ strategy: log_strategy
45
+ )
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FHTTPClient
4
+ module Processor
5
+ # This Service proccess a HTTP response, generating a success or failure result.
6
+ #
7
+ # Example:
8
+ #
9
+ # def process
10
+ # response = OpenStruct.new(success?: false, parsed_response: {}, headers: {}, message: 'Conflict')
11
+ # processor = FHTTPClient::Processor::Response.new(response: response)
12
+ # processor.()
13
+ # .on_success { |value| "Success! #{value.inspect}"}
14
+ # .on_failure(:conflict) { |value| return "A conflict happened! #{value.inspect}"}
15
+ # .on_failure(:client_error) { |value| return "An unexpected client error happened #{value.inspect}"}
16
+ # .on_failure { |value| return "Generic #{value}" }
17
+ # end
18
+ #
19
+ # proccess
20
+ # # => "A conflict happened! {}"
21
+ class Response < FHTTPClient::Service
22
+ extend Forwardable
23
+
24
+ option :response
25
+ option :log_strategy, default: -> { :null }
26
+
27
+ STATUS_FAMILIES = {
28
+ 200..299 => :successful,
29
+ 400..499 => :client_error,
30
+ 500..599 => :server_error,
31
+ 300..399 => :redirection,
32
+ 100..199 => :informational
33
+ }.freeze
34
+
35
+ private_constant :STATUS_FAMILIES
36
+
37
+ def run
38
+ log_data.and_then { success? ? success_response : failure_response }
39
+ end
40
+
41
+ private
42
+
43
+ def_delegators :@response, :headers, :success?, :parsed_response, :code
44
+
45
+ def success_response
46
+ Success(response_type, response_family, data: response)
47
+ end
48
+
49
+ def failure_response
50
+ Failure(response_type, response_family, data: response)
51
+ end
52
+
53
+ # Private:
54
+ # Transform HTTParty response message in to a symbol
55
+ # Ex: When the request returns a 400 (Bad Request)
56
+ #
57
+ # # => :bad_request
58
+ def response_type
59
+ message.empty? ? :unknown_type : message.downcase.tr(' ', '_').to_sym
60
+ end
61
+
62
+ # Private:
63
+ # Transform HTTParty response code in to a symbol
64
+ # Ex: When the request returns a 400 (Bad Request)
65
+ #
66
+ # # => :client_error
67
+ def response_family
68
+ STATUS_FAMILIES
69
+ .find(-> { [code, :unknown_family] }) { |codes, _family| codes.cover?(code.to_i) }
70
+ .last
71
+ end
72
+
73
+ # Private
74
+ # Generates a HTTParty response message from a Net::HTTPRespoonse Class
75
+ # Examples:
76
+ #
77
+ # response_type(Net::HTTPOK)
78
+ # # => "OK"
79
+ #
80
+ # response_type(Net::HTTPUnprocessableEntity)
81
+ # # => "Unprocessable Entity"
82
+ def message
83
+ @message ||= response_class
84
+ .to_s
85
+ .delete_prefix('Net::HTTP')
86
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1 \2')
87
+ .gsub(/([a-z])([A-Z])/, '\1 \2')
88
+ end
89
+
90
+ # Private
91
+ # Get an HTTP status error name for a status code
92
+ #
93
+ # Ex: 400
94
+ # # => "Net:HTTPBadRequest"
95
+ def response_class
96
+ Net::HTTPResponse::CODE_TO_OBJ[code.to_s].to_s
97
+ end
98
+
99
+ def log_data
100
+ FHTTPClient::Log.(
101
+ message: { request: request_log_data, response: response_log_data }.to_json,
102
+ strategy: log_strategy,
103
+ tags: 'EXTERNAL REQUEST'
104
+ )
105
+ end
106
+
107
+ def request_log_data
108
+ request = response.request
109
+ request_options = request.options
110
+
111
+ {
112
+ headers: request_options[:headers],
113
+ method: request.http_method.name.split('::').last.upcase,
114
+ path: request.uri.path,
115
+ querystring: Addressable::URI.parse(request.uri.query)&.query_values,
116
+ body: request_options[:body]
117
+ }.compact_blank
118
+ end
119
+
120
+ def response_log_data
121
+ { code: code, human_code: message, headers: headers, body: parsed_response }.compact_blank
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FHTTPClientFakeResponse
4
+ def build_httparty_response(code: 200, parsed_response: {})
5
+ request_object = HTTParty::Request.new Net::HTTP::Get, '/'
6
+ response_object = Net::HTTPResponse::CODE_TO_OBJ[code.to_s].new('1.1', code, '')
7
+ allow(response_object).to receive(:body)
8
+
9
+ HTTParty::Response.new(request_object, response_object, -> { parsed_response })
10
+ end
11
+ end
12
+
13
+ RSpec.configure do |config|
14
+ config.include FHTTPClientFakeResponse
15
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helpers/fake_response'
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'support/helpers'
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'support/helpers'
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rspec/support'
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FHTTPClient
4
+ # Allow defining keyword args for a class
5
+ #
6
+ # @example
7
+ # class User::Create < FHTTPClient::Service
8
+ # option :name
9
+ # option :age
10
+ # option :email, default: -> { 'guest@user.com' }
11
+ #
12
+ # def run
13
+ # Success(:created, data: "Hello #{name}! Your email is: #{email}")
14
+ # end
15
+ # end
16
+ #
17
+ # User.(name: 'Matheus', age: 22)
18
+ # => #<FService::Result::Success:0x0000557fae615ea8 @handled=false, @matching_types=[], @types=[:created], @value="Hello Bruno! Your email is: guest@user.com">
19
+ class Service < FService::Base
20
+ extend Dry::Initializer
21
+ end
22
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FHTTPClient
4
+ # Allow to log a message to system log
5
+ #
6
+ # Attributes:
7
+ # - strategy: symbol representing cache stragegy to be used (null or rails);
8
+ # - block: the lambda to have its result cached;
9
+ # - key: the cache key to be used to store the result;
10
+ # - expires_in (optional): seconds to keep the result in cache;
11
+ # - skip_if (optional): a block to run over the block result and decide if result caching must be persisted or skiped.
12
+ #
13
+ # Example:
14
+ # FHTTPClient::Cache.(strategy: :rails, key: 'users/list?name=bruno', block: -> { make_request })
15
+ # FHTTPClient::Cache.(strategy: :rails, key: 'users/list?name=bruno', block: -> { make_request }, expires_in: 15.minutes)
16
+ # FHTTPClient::Cache.(strategy: :rails, key: 'users/list?name=bruno', block: -> { make_request }, skip_if: ->(response) { response.status != 200 })
17
+ class Store < FHTTPClient::Service
18
+ option :strategy
19
+ option :key
20
+ option :block
21
+ option :expires_in, default: -> {}
22
+ option :skip_if, default: -> {}
23
+
24
+ def run
25
+ Success(
26
+ :done,
27
+ data: cache.fetch(key, { expires_in: expires_in, skip_if: skip_if }.compact_blank, &block)
28
+ )
29
+ end
30
+
31
+ private
32
+
33
+ def cache
34
+ @cache ||= strategy == :rails ? FHTTPClient::Cache::Rails : FHTTPClient::Cache::Null
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FHTTPClient
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'f_http_client/version'
4
+ require 'active_support/core_ext/array'
5
+ require 'active_support/core_ext/enumerable'
6
+ require 'active_support/core_ext/string'
7
+ require 'addressable'
8
+ require 'dry-configurable'
9
+ require 'dry-initializer'
10
+ require 'forwardable'
11
+ require 'httparty'
12
+ require 'f_service'
13
+
14
+ require 'f_http_client/configuration'
15
+ require 'f_http_client/service'
16
+ require 'f_http_client/store'
17
+ require 'f_http_client/cache/http_response_analizer'
18
+ require 'f_http_client/cache/key'
19
+ require 'f_http_client/cache/null'
20
+ require 'f_http_client/cache/rails'
21
+
22
+ require 'f_http_client/logger/default'
23
+ require 'f_http_client/logger/null'
24
+ require 'f_http_client/logger/rails'
25
+ require 'f_http_client/log'
26
+
27
+ require 'f_http_client/parser/response'
28
+ require 'f_http_client/processor/exception'
29
+ require 'f_http_client/processor/response'
30
+
31
+ require 'f_http_client/base'
32
+
33
+ module FHTTPClient
34
+ end
@@ -0,0 +1,4 @@
1
+ module FHTTPClient
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: f_http_client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Fretadao Tech Team
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-05-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: addressable
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: dry-configurable
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: dry-initializer
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: httparty
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description:
84
+ email:
85
+ - tech@fretadao.com.br
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".rspec"
91
+ - ".rubocop.yml"
92
+ - CHANGELOG.md
93
+ - Gemfile
94
+ - Gemfile.lock
95
+ - LICENSE
96
+ - README.md
97
+ - Rakefile
98
+ - examples/post_find.rb
99
+ - lib/f_http_client.rb
100
+ - lib/f_http_client/base.rb
101
+ - lib/f_http_client/cache/http_response_analizer.rb
102
+ - lib/f_http_client/cache/key.rb
103
+ - lib/f_http_client/cache/null.rb
104
+ - lib/f_http_client/cache/rails.rb
105
+ - lib/f_http_client/configuration.rb
106
+ - lib/f_http_client/log.rb
107
+ - lib/f_http_client/logger/default.rb
108
+ - lib/f_http_client/logger/null.rb
109
+ - lib/f_http_client/logger/rails.rb
110
+ - lib/f_http_client/parser/response.rb
111
+ - lib/f_http_client/processor/exception.rb
112
+ - lib/f_http_client/processor/response.rb
113
+ - lib/f_http_client/rspec.rb
114
+ - lib/f_http_client/rspec/support.rb
115
+ - lib/f_http_client/rspec/support/helpers.rb
116
+ - lib/f_http_client/rspec/support/helpers/fake_response.rb
117
+ - lib/f_http_client/rspec/support/support.rb
118
+ - lib/f_http_client/service.rb
119
+ - lib/f_http_client/store.rb
120
+ - lib/f_http_client/version.rb
121
+ - sig/f_http_client.rbs
122
+ homepage: https://github.com/Fretadao/f_http_client
123
+ licenses:
124
+ - MIT
125
+ metadata:
126
+ homepage_uri: https://github.com/Fretadao/f_http_client
127
+ source_code_uri: https://github.com/Fretadao/f_http_client
128
+ changelog_uri: https://github.com/Fretadao/f_http_client/blob/master/CHANGELOG.md
129
+ rubygems_mfa_required: 'true'
130
+ post_install_message:
131
+ rdoc_options: []
132
+ require_paths:
133
+ - lib
134
+ required_ruby_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: 3.0.0
139
+ required_rubygems_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ requirements: []
145
+ rubygems_version: 3.3.5
146
+ signing_key:
147
+ specification_version: 4
148
+ summary: Gem to provade a base for an HTTP client using FService architecture
149
+ test_files: []