client-api-builder 0.2.9 → 0.4.0

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