f_http_client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []