client-api-builder 0.2.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 201c8daf4abbc1489d26007b97c9b50145cffd98c699c2918d8f929138a1ef70
4
- data.tar.gz: 627baef257a7d3a8037fa38eb5fa2e1c4f99f3ef0d191f083114f187e5690a30
3
+ metadata.gz: c22eb6c8362909af90380e2513875c7a82c4bd67028e5a601694c1d604fc599d
4
+ data.tar.gz: e6ad4f872b67a07563b88f3880bbd1f3375ed2d628a4c15cdedb0734fc9723a3
5
5
  SHA512:
6
- metadata.gz: cc3b8a34104099c0ab3bdcbf66c611286fe07841eac76e7d6ddbfebe5e97d5b374c0f52e9c47b3d9f1e49761ee0fca02565862875f48c932a31bd7d8f07543aa
7
- data.tar.gz: 00216ed9808389eb0f41c9a084527f7311c1e13ad525945a2a920d39aca55e48c395471dd3bcb0ead25db8f6bb08f73f99693cc853e232534d298eff3bfbba31
6
+ metadata.gz: 5bbe4fe12d3ea5ca240b445630360346a0a1628c340c9ce602efbacfb185ad23c7215233dcb84cb245ff5d3961b0695e8d177a15c39f87e9f6f6b4c0d030e60c
7
+ data.tar.gz: 44c82d195b80ad34205af3fa256523fa0a3a1e7249bd857ac7cc0cece431d9d75906f26a5714914075e0b9291d84ad5649704add740c099d960ad2e60ad3e2ee
data/README.md CHANGED
@@ -1,2 +1,33 @@
1
1
  # client-api-builder
2
- Utility for creating methods to make api calls
2
+
3
+ Utility for creating API clients through configuration
4
+
5
+ Example:
6
+
7
+ ```
8
+ class LoremIpsumClient
9
+ include ClientApiBuilder::Router
10
+
11
+ # by default it converts the body data to JSON
12
+ # to convert the body to query params (x=1&y=2) use the following
13
+ # if using active support change this to :to_query
14
+ body_builder :query_params
15
+
16
+ base_url 'https://www.lipsum.com'
17
+
18
+ header 'Content-Type', 'application/x-www-form-urlencoded'
19
+ header 'Accept', 'application/json'
20
+
21
+ # this creates a method called create_lorem_ipsum with 2 named arguments amont and what
22
+ route :create_lorem_ipsum, '/feed/json', body: {amount: :amount, what: :what, start: 'yes', generate: 'Generate Lorem Ipsum'}
23
+ end
24
+ ```
25
+
26
+ How to use:
27
+
28
+ ```
29
+ client = LoremIpsumClient.new
30
+ payload = client.create_lorem_ipsum(amount: 10, what: 'words')
31
+ puts payload.dig('feed', 'lipsum')
32
+ # outputs: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at.
33
+ ```
@@ -2,10 +2,10 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'client-api-builder'
5
- s.version = '0.2.9'
5
+ s.version = '0.4.0'
6
6
  s.licenses = ['MIT']
7
- s.summary = 'Develop Client API libraries faster'
8
- s.description = 'Utility for constructing API clients'
7
+ s.summary = 'Utility for creating API clients through configuration'
8
+ s.description = 'Create API clients through configuration with complete transparency'
9
9
  s.authors = ['Doug Youch']
10
10
  s.email = 'dougyouch@gmail.com'
11
11
  s.homepage = 'https://github.com/dougyouch/client-api-builder'
@@ -11,6 +11,8 @@ class BasicAuthExampleClient < Struct.new(
11
11
 
12
12
  base_url 'https://www.example.com'
13
13
 
14
+ configure_retries(2)
15
+
14
16
  header 'Authorization', :basic_authorization
15
17
  query_param('cache_buster') { (Time.now.to_f * 1000).to_i }
16
18
 
@@ -0,0 +1,16 @@
1
+ class LoremIpsumClient
2
+ include ClientApiBuilder::Router
3
+
4
+ # by default it converts the body data to JSON
5
+ # to convert the body to query params (x=1&y=2) use the following
6
+ # if using active support change this to :to_query
7
+ body_builder :query_params
8
+
9
+ base_url 'https://www.lipsum.com'
10
+
11
+ header 'Content-Type', 'application/x-www-form-urlencoded'
12
+ header 'Accept', 'application/json'
13
+
14
+ # this creates a method called create_lorem_ipsum with 2 named arguments amont and what
15
+ route :create_lorem_ipsum, '/feed/json', body: {amount: :amount, what: :what, start: 'yes', generate: 'Generate Lorem Ipsum'}
16
+ end
@@ -11,6 +11,12 @@ module ClientApiBuilder
11
11
  end
12
12
  end
13
13
 
14
+ class << self
15
+ attr_accessor :logger
16
+ end
17
+
18
+ autoload :ActiveSupportNotifications, 'client_api_builder/active_support_notifications'
19
+ autoload :ActiveSupportLogSubscriber, 'client_api_builder/active_support_log_subscriber'
14
20
  autoload :NestedRouter, 'client_api_builder/nested_router'
15
21
  autoload :QueryParams, 'client_api_builder/query_params'
16
22
  autoload :Router, 'client_api_builder/router'
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support'
3
+
4
+ # Purpose is to log all requests
5
+ module ClientApiBuilder
6
+ class ActiveSupportLogSubscriber
7
+ attr_reader :logger
8
+
9
+ def initialize(logger)
10
+ @logger = logger
11
+ end
12
+
13
+ def subscribe!
14
+ ActiveSupport::Notifications.subscribe('client_api_builder.request') do |event|
15
+ logger.info(generate_log_message(event))
16
+ end
17
+ end
18
+
19
+ def generate_log_message(event)
20
+ client = event.payload[:client]
21
+ method = client.request_options[:method].to_s.upcase
22
+ uri = client.request_options[:uri]
23
+ response = client.response
24
+ response_code = response ? response.code : 'UNKNOWN'
25
+
26
+ "#{method} #{uri.scheme}://#{uri.host}#{uri.path}[#{response_code}] took #{event.duration.to_i}ms"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support'
3
+
4
+ # Purpose is to change the instrument_request to use ActiveSupport::Notifications.instrument
5
+ module ClientApiBuilder
6
+ module ActiveSupportNotifications
7
+ def instrument_request
8
+ start_time = Time.now
9
+ error = nil
10
+ result = nil
11
+ ActiveSupport::Notifications.instrument('client_api_builder.request', client: self) do
12
+ begin
13
+ result = yield
14
+ rescue Exception => e
15
+ error = e
16
+ end
17
+ end
18
+
19
+ raise(error) if error
20
+ result
21
+ ensure
22
+ @total_request_time = Time.now - start_time
23
+ end
24
+ end
25
+ end
@@ -8,10 +8,12 @@ module ClientApiBuilder
8
8
  class NestedRouter
9
9
  include ::ClientApiBuilder::Router
10
10
 
11
- attr_reader :root_router
11
+ attr_reader :root_router,
12
+ :nested_router_options
12
13
 
13
- def initialize(root_router)
14
+ def initialize(root_router, nested_router_options)
14
15
  @root_router = root_router
16
+ @nested_router_options = nested_router_options
15
17
  end
16
18
 
17
19
  def self.get_instance_method(var)
@@ -39,7 +41,7 @@ module ClientApiBuilder
39
41
  end
40
42
 
41
43
  def build_headers(options)
42
- headers = root_router.build_headers(options)
44
+ headers = nested_router_options[:ignore_headers] ? {} : root_router.build_headers(options)
43
45
 
44
46
  add_header_proc = proc do |name, value|
45
47
  headers[name] =
@@ -63,6 +65,7 @@ module ClientApiBuilder
63
65
 
64
66
  def build_query(query, options)
65
67
  return nil if query.nil? && root_router.class.default_query_params.empty? && self.class.default_query_params.empty?
68
+ return nil if nested_router_options[:ignore_query] && query.nil? && self.class.default_query_params.empty?
66
69
 
67
70
  query_params = {}
68
71
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require 'inheritance-helper'
3
+ require 'json'
3
4
 
4
5
  module ClientApiBuilder
5
6
  module Router
@@ -8,7 +9,8 @@ module ClientApiBuilder
8
9
  base.extend ClassMethods
9
10
  base.include ::ClientApiBuilder::Section
10
11
  base.include ::ClientApiBuilder::NetHTTP::Request
11
- base.send(:attr_reader, :response, :request_options)
12
+ base.include(::ClientApiBuilder::ActiveSupportNotifications) if defined?(ActiveSupport)
13
+ base.send(:attr_reader, :response, :request_options, :total_request_time, :request_attempts)
12
14
  end
13
15
 
14
16
  module ClassMethods
@@ -26,7 +28,9 @@ module ClientApiBuilder
26
28
  headers: {},
27
29
  query_builder: Hash.method_defined?(:to_query) ? :to_query : :query_params,
28
30
  query_params: {},
29
- response_procs: {}
31
+ response_procs: {},
32
+ max_retries: 1,
33
+ sleep: 0.05
30
34
  }.freeze
31
35
  end
32
36
 
@@ -79,6 +83,14 @@ module ClientApiBuilder
79
83
  add_value_to_class_method(:default_options, connection_options: connection_options)
80
84
  end
81
85
 
86
+ def configure_retries(max_retries, sleep_time_between_retries_in_seconds = 0.05)
87
+ add_value_to_class_method(
88
+ :default_options,
89
+ max_retries: max_retries,
90
+ sleep: sleep_time_between_retries_in_seconds
91
+ )
92
+ end
93
+
82
94
  # add a query param to all requests
83
95
  def query_param(name, value = nil, &block)
84
96
  query_params = default_options[:query_params].dup
@@ -309,19 +321,21 @@ module ClientApiBuilder
309
321
  code += "\n"
310
322
 
311
323
  code += "def #{method_name}(" + method_args.join(', ') + ")\n"
312
- code += " block ||= self.class.get_response_proc(#{method_name.inspect})\n"
313
- code += " __expected_response_codes__ = #{expected_response_codes.inspect}\n"
314
- code += " #{method_name}_raw_response(" + method_args.map { |a| a =~ /:$/ ? "#{a} #{a.sub(':', '')}" : a }.join(', ') + ")\n"
315
- code += " expected_response_code!(@response, __expected_response_codes__, __options__)\n"
324
+ code += " request_wrapper(__options__) do\n"
325
+ code += " block ||= self.class.get_response_proc(#{method_name.inspect})\n"
326
+ code += " __expected_response_codes__ = #{expected_response_codes.inspect}\n"
327
+ code += " #{method_name}_raw_response(" + method_args.map { |a| a =~ /:$/ ? "#{a} #{a.sub(':', '')}" : a }.join(', ') + ")\n"
328
+ code += " expected_response_code!(@response, __expected_response_codes__, __options__)\n"
316
329
 
317
330
  if options[:stream] || options[:return] == :response
318
- code += " @response\n"
331
+ code += " @response\n"
319
332
  elsif options[:return] == :body
320
- code += " @response.body\n"
333
+ code += " @response.body\n"
321
334
  else
322
- code += " handle_response(@response, __options__, &block)\n"
335
+ code += " handle_response(@response, __options__, &block)\n"
323
336
  end
324
337
 
338
+ code += " end\n"
325
339
  code += "end\n"
326
340
  code
327
341
  end
@@ -439,5 +453,60 @@ module ClientApiBuilder
439
453
  def escape_path(path)
440
454
  path
441
455
  end
456
+
457
+ def instrument_request
458
+ start_time = Time.now
459
+ yield
460
+ ensure
461
+ @total_request_time = Time.now - start_time
462
+ end
463
+
464
+ def retry_request(options)
465
+ @request_attempts = 0
466
+ max_attempts = get_retry_request_max_retries(options)
467
+ begin
468
+ @request_attempts += 1
469
+ yield
470
+ rescue Exception => e
471
+ log_request_exception(e)
472
+ raise(e) if @request_attempts >= max_attempts || !retry_request?(e, options)
473
+ sleep_time = get_retry_request_sleep_time(e, options)
474
+ sleep(sleep_time) if sleep_time && sleep_time > 0
475
+ retry
476
+ end
477
+ end
478
+
479
+ def get_retry_request_sleep_time(e, options)
480
+ options[:sleep] || self.class.default_options[:sleep] || 0.05
481
+ end
482
+
483
+ def get_retry_request_max_retries(options)
484
+ options[:retries] || self.class.default_options[:max_retries] || 1
485
+ end
486
+
487
+ def request_wrapper(options)
488
+ retry_request(options) do
489
+ instrument_request do
490
+ yield
491
+ end
492
+ end
493
+ end
494
+
495
+ def retry_request?(exception, options)
496
+ true
497
+ end
498
+
499
+ def log_request_exception(exception)
500
+ ::ClientApiBuilder.logger && ::ClientApiBuilder.logger.error(exception)
501
+ end
502
+
503
+ def request_log_message
504
+ method = request_options[:method].to_s.upcase
505
+ uri = request_options[:uri]
506
+ response_code = response ? response.code : 'UNKNOWN'
507
+
508
+ duration = (total_request_time * 1000).to_i
509
+ "#{method} #{uri.scheme}://#{uri.host}#{uri.path}[#{response_code}] took #{duration}ms"
510
+ end
442
511
  end
443
512
  end
@@ -8,7 +8,7 @@ module ClientApiBuilder
8
8
  end
9
9
 
10
10
  module ClassMethods
11
- def section(name, &block)
11
+ def section(name, nested_router_options={}, &block)
12
12
  kls = InheritanceHelper::ClassBuilder::Utils.create_class(
13
13
  self,
14
14
  name,
@@ -24,7 +24,7 @@ def self.#{name}_router
24
24
  end
25
25
 
26
26
  def #{name}
27
- @#{name} ||= self.class.#{name}_router.new(self.root_router)
27
+ @#{name} ||= self.class.#{name}_router.new(self.root_router, #{nested_router_options.inspect})
28
28
  end
29
29
  CODE
30
30
  self.class_eval code, __FILE__, __LINE__
data/script/console CHANGED
@@ -6,5 +6,10 @@ $LOAD_PATH << File.expand_path('../examples', __dir__)
6
6
  require 'client-api-builder'
7
7
  autoload :BasicAuthExampleClient, 'basic_auth_example_client'
8
8
  autoload :IMDBDatesetsClient, 'imdb_datasets_client'
9
+ autoload :LoremIpsumClient, 'lorem_ipsum_client'
10
+ require 'logger'
11
+ LOG = Logger.new(STDOUT)
12
+ ClientApiBuilder.logger = LOG
13
+ ClientApiBuilder::ActiveSupportLogSubscriber.new(LOG).subscribe!
9
14
  require 'irb'
10
15
  IRB.start(__FILE__)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: client-api-builder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.9
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Doug Youch
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-08-11 00:00:00.000000000 Z
11
+ date: 2022-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: inheritance-helper
@@ -24,7 +24,7 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: 0.2.5
27
- description: Utility for constructing API clients
27
+ description: Create API clients through configuration with complete transparency
28
28
  email: dougyouch@gmail.com
29
29
  executables: []
30
30
  extensions: []
@@ -40,7 +40,10 @@ files:
40
40
  - client-api-builder.gemspec
41
41
  - examples/basic_auth_example_client.rb
42
42
  - examples/imdb_datasets_client.rb
43
+ - examples/lorem_ipsum_client.rb
43
44
  - lib/client-api-builder.rb
45
+ - lib/client_api_builder/active_support_log_subscriber.rb
46
+ - lib/client_api_builder/active_support_notifications.rb
44
47
  - lib/client_api_builder/nested_router.rb
45
48
  - lib/client_api_builder/net_http_request.rb
46
49
  - lib/client_api_builder/query_params.rb
@@ -69,5 +72,5 @@ requirements: []
69
72
  rubygems_version: 3.3.3
70
73
  signing_key:
71
74
  specification_version: 4
72
- summary: Develop Client API libraries faster
75
+ summary: Utility for creating API clients through configuration
73
76
  test_files: []