client-api-builder 0.2.8 → 0.3.1

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: b9853c5806a999fe126ad1b1ce5fdeb6ea6bdb985142e36a1cbdcbbbac6434ae
4
- data.tar.gz: 9bfb059e5adcbf18c55882d1fcca7d04d310e04dce915cd3d46e88f770e36650
3
+ metadata.gz: 06500a9c514d0979b01f672ae5bdac70c5dfc7b23b3633f0662a71c4a057a01b
4
+ data.tar.gz: c3a00f73d888ad6a19b98a9a0c172685fd5299b61fa99b41931c427db8d73470
5
5
  SHA512:
6
- metadata.gz: 1896be3318d8f5362c8b367d9836854d8567f9e7a394cac645c126a866afd2eb53507721228ae78f7bc94d5625b5fdfe2abe2273320de04c844e94cbe1b8df5e
7
- data.tar.gz: 90aaaf4e7a73452397c699f095b383397c9a9d715b91b20431f0faf798b00e716d387eef9280f8687e118538c6e9c1e56f894979559b96b3671f04cf75f5ea6e
6
+ metadata.gz: 2263f684643292a86895f23906e2d566043d914704b92db919a1f23e1d47c1f5371ef7ea344257a63b234aac6ce8e5f293d4cf16b20e08248c7859fb8b692448
7
+ data.tar.gz: 0a2508597031c861ea8ba44ac00f9fcd3596b040bde9df1d3724afdb4c5a0d4b4b463d19e75b0360a2987ba1ad7375bff8c25572cb6fced4e194a560bb6f927e
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.0.0
1
+ 3.1.0
data/Gemfile CHANGED
@@ -10,6 +10,7 @@ group :development do
10
10
  end
11
11
 
12
12
  group :spec do
13
+ gem 'activesupport'
13
14
  gem 'rspec'
14
15
  gem 'simplecov'
15
16
  gem 'webmock'
data/Gemfile.lock CHANGED
@@ -1,64 +1,76 @@
1
1
  GEM
2
2
  remote: http://rubygems.org/
3
3
  specs:
4
+ activesupport (7.0.2.3)
5
+ concurrent-ruby (~> 1.0, >= 1.0.2)
6
+ i18n (>= 1.6, < 2)
7
+ minitest (>= 5.1)
8
+ tzinfo (~> 2.0)
4
9
  addressable (2.8.0)
5
10
  public_suffix (>= 2.0.2, < 5.0)
6
11
  ast (2.4.2)
12
+ concurrent-ruby (1.1.9)
7
13
  crack (0.4.5)
8
14
  rexml
9
- diff-lcs (1.4.4)
15
+ diff-lcs (1.5.0)
10
16
  docile (1.4.0)
11
17
  hashdiff (1.0.1)
18
+ i18n (1.10.0)
19
+ concurrent-ruby (~> 1.0)
12
20
  inheritance-helper (0.2.5)
13
- parallel (1.20.1)
14
- parser (3.0.1.1)
21
+ minitest (5.15.0)
22
+ parallel (1.21.0)
23
+ parser (3.1.1.0)
15
24
  ast (~> 2.4.1)
16
25
  public_suffix (4.0.6)
17
- rainbow (3.0.0)
18
- rake (13.0.3)
19
- regexp_parser (2.1.1)
26
+ rainbow (3.1.1)
27
+ rake (13.0.6)
28
+ regexp_parser (2.2.1)
20
29
  rexml (3.2.5)
21
- rspec (3.10.0)
22
- rspec-core (~> 3.10.0)
23
- rspec-expectations (~> 3.10.0)
24
- rspec-mocks (~> 3.10.0)
25
- rspec-core (3.10.1)
26
- rspec-support (~> 3.10.0)
27
- rspec-expectations (3.10.1)
30
+ rspec (3.11.0)
31
+ rspec-core (~> 3.11.0)
32
+ rspec-expectations (~> 3.11.0)
33
+ rspec-mocks (~> 3.11.0)
34
+ rspec-core (3.11.0)
35
+ rspec-support (~> 3.11.0)
36
+ rspec-expectations (3.11.0)
28
37
  diff-lcs (>= 1.2.0, < 2.0)
29
- rspec-support (~> 3.10.0)
30
- rspec-mocks (3.10.2)
38
+ rspec-support (~> 3.11.0)
39
+ rspec-mocks (3.11.0)
31
40
  diff-lcs (>= 1.2.0, < 2.0)
32
- rspec-support (~> 3.10.0)
33
- rspec-support (3.10.2)
34
- rubocop (1.18.2)
41
+ rspec-support (~> 3.11.0)
42
+ rspec-support (3.11.0)
43
+ rubocop (1.26.0)
35
44
  parallel (~> 1.10)
36
- parser (>= 3.0.0.0)
45
+ parser (>= 3.1.0.0)
37
46
  rainbow (>= 2.2.2, < 4.0)
38
47
  regexp_parser (>= 1.8, < 3.0)
39
48
  rexml
40
- rubocop-ast (>= 1.7.0, < 2.0)
49
+ rubocop-ast (>= 1.16.0, < 2.0)
41
50
  ruby-progressbar (~> 1.7)
42
51
  unicode-display_width (>= 1.4.0, < 3.0)
43
- rubocop-ast (1.7.0)
44
- parser (>= 3.0.1.1)
52
+ rubocop-ast (1.16.0)
53
+ parser (>= 3.1.1.0)
45
54
  ruby-progressbar (1.11.0)
46
55
  simplecov (0.21.2)
47
56
  docile (~> 1.1)
48
57
  simplecov-html (~> 0.11)
49
58
  simplecov_json_formatter (~> 0.1)
50
59
  simplecov-html (0.12.3)
51
- simplecov_json_formatter (0.1.3)
52
- unicode-display_width (2.0.0)
53
- webmock (3.13.0)
54
- addressable (>= 2.3.6)
60
+ simplecov_json_formatter (0.1.4)
61
+ tzinfo (2.0.4)
62
+ concurrent-ruby (~> 1.0)
63
+ unicode-display_width (2.1.0)
64
+ webmock (3.14.0)
65
+ addressable (>= 2.8.0)
55
66
  crack (>= 0.3.2)
56
67
  hashdiff (>= 0.4.0, < 2.0.0)
57
68
 
58
69
  PLATFORMS
59
- x86_64-darwin-20
70
+ x86_64-darwin-21
60
71
 
61
72
  DEPENDENCIES
73
+ activesupport
62
74
  inheritance-helper
63
75
  rake
64
76
  rspec
@@ -67,4 +79,4 @@ DEPENDENCIES
67
79
  webmock
68
80
 
69
81
  BUNDLED WITH
70
- 2.2.3
82
+ 2.3.3
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'client-api-builder'
5
- s.version = '0.2.8'
5
+ s.version = '0.3.1'
6
6
  s.licenses = ['MIT']
7
7
  s.summary = 'Develop Client API libraries faster'
8
8
  s.description = 'Utility for constructing API clients'
@@ -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
 
@@ -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)
@@ -34,12 +36,12 @@ module ClientApiBuilder
34
36
  root_router.stream_to_file(**options, &block)
35
37
  end
36
38
 
37
- def base_url(options)
38
- self.class.base_url || root_router.base_url(options)
39
+ def base_url
40
+ self.class.base_url || root_router.base_url
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] =
@@ -52,7 +54,7 @@ module ClientApiBuilder
52
54
  end
53
55
  end
54
56
 
55
- self.class.headers.each(&add_header_proc)
57
+ self.class.default_headers.each(&add_header_proc)
56
58
 
57
59
  headers
58
60
  end
@@ -62,7 +64,8 @@ module ClientApiBuilder
62
64
  end
63
65
 
64
66
  def build_query(query, options)
65
- return nil if query.nil? && root_router.class.query_params.empty? && self.class.query_params.empty?
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
 
@@ -77,12 +80,12 @@ module ClientApiBuilder
77
80
  end
78
81
  end
79
82
 
80
- root_router.class.query_params.each(&add_query_param_proc)
81
- self.class.query_params.each(&add_query_param_proc)
83
+ root_router.class.default_query_params.each(&add_query_param_proc)
84
+ self.class.default_query_params.each(&add_query_param_proc)
82
85
  query && query.each(&add_query_param_proc)
83
86
  options[:query] && options[:query].each(&add_query_param_proc)
84
87
 
85
- self.class.build_query(query_params)
88
+ self.class.build_query(self, query_params)
86
89
  end
87
90
 
88
91
  def build_body(body, options)
@@ -96,5 +99,9 @@ module ClientApiBuilder
96
99
  def handle_response(response, options, &block)
97
100
  root_router.handle_response(response, options, &block)
98
101
  end
102
+
103
+ def escape_path(path)
104
+ root_router.escape_path(path)
105
+ end
99
106
  end
100
107
  end
@@ -2,59 +2,72 @@
2
2
  require 'cgi'
3
3
 
4
4
  module ClientApiBuilder
5
- module QueryParams
6
- module_function
5
+ class QueryParams
6
+ attr_reader :name_value_separator,
7
+ :param_separator
7
8
 
8
- def to_query(data, namespace = nil, name_value_separator = '=', param_separator = '&')
9
+ attr_accessor :custom_escape_proc
10
+
11
+ def initialize(name_value_separator: '=', param_separator: '&', custom_escape_proc: nil)
12
+ @name_value_separator = name_value_separator
13
+ @param_separator = param_separator
14
+ @custom_escape_proc = custom_escape_proc
15
+ end
16
+
17
+ def to_query(data, namespace = nil)
9
18
  case data
10
19
  when Hash
11
- to_query_from_hash(data, (namespace ? CGI.escape(namespace) : nil), name_value_separator).join(param_separator)
20
+ to_query_from_hash(data, (namespace ? escape(namespace) : nil)).join(param_separator)
12
21
  when Array
13
- to_query_from_array(data, (namespace ? "#{CGI.escape(namespace)}[]" : '[]'), name_value_separator).join(param_separator)
22
+ to_query_from_array(data, (namespace ? "#{escape(namespace)}[]" : '[]')).join(param_separator)
14
23
  else
15
24
  if namespace
16
- "#{CGI.escape(namespace)}#{name_value_separator}#{CGI.escape(data.to_s)}"
25
+ "#{escape(namespace)}#{name_value_separator}#{escape(data.to_s)}"
17
26
  else
18
- CGI.escape(data.to_s)
27
+ escape(data.to_s)
19
28
  end
20
29
  end
21
30
  end
22
31
 
23
- def to_query_from_hash(hsh, namespace, name_value_separator)
32
+ def to_query_from_hash(hsh, namespace)
24
33
  query_params = []
25
34
 
26
35
  hsh.each do |key, value|
27
36
  case value
28
37
  when Array
29
- array_namespace = namespace ? "#{namespace}[#{CGI.escape(key.to_s)}][]" : "#{CGI.escape(key.to_s)}[]"
30
- query_params += to_query_from_array(value, array_namespace, name_value_separator)
38
+ array_namespace = namespace ? "#{namespace}[#{escape(key.to_s)}][]" : "#{escape(key.to_s)}[]"
39
+ query_params += to_query_from_array(value, array_namespace)
31
40
  when Hash
32
- hash_namespace = namespace ? "#{namespace}[#{CGI.escape(key.to_s)}]" : "#{CGI.escape(key.to_s)}"
33
- query_params += to_query_from_hash(value, hash_namespace, name_value_separator)
41
+ hash_namespace = namespace ? "#{namespace}[#{escape(key.to_s)}]" : "#{escape(key.to_s)}"
42
+ query_params += to_query_from_hash(value, hash_namespace)
34
43
  else
35
- query_name = namespace ? "#{namespace}[#{CGI.escape(key.to_s)}]" : "#{CGI.escape(key.to_s)}"
36
- query_params << "#{query_name}#{name_value_separator}#{CGI.escape(value.to_s)}"
44
+ query_name = namespace ? "#{namespace}[#{escape(key.to_s)}]" : "#{escape(key.to_s)}"
45
+ query_params << "#{query_name}#{name_value_separator}#{escape(value.to_s)}"
37
46
  end
38
47
  end
39
48
 
40
49
  query_params
41
50
  end
42
51
 
43
- def to_query_from_array(array, namespace, name_value_separator)
52
+ def to_query_from_array(array, namespace)
44
53
  query_params = []
45
54
 
46
55
  array.each do |value|
47
56
  case value
48
57
  when Hash
49
- query_params += to_query_from_hash(value, namespace, name_value_separator)
58
+ query_params += to_query_from_hash(value, namespace)
50
59
  when Array
51
- query_params += to_query_from_array(value, "#{namespace}[]", name_value_separator)
60
+ query_params += to_query_from_array(value, "#{namespace}[]")
52
61
  else
53
- query_params << "#{namespace}#{name_value_separator}#{CGI.escape(value.to_s)}"
62
+ query_params << "#{namespace}#{name_value_separator}#{escape(value.to_s)}"
54
63
  end
55
64
  end
56
65
 
57
66
  query_params
58
67
  end
68
+
69
+ def escape(str)
70
+ custom_escape_proc ? custom_escape_proc.call(str) : CGI.escape(str)
71
+ end
59
72
  end
60
73
  end
@@ -8,7 +8,8 @@ module ClientApiBuilder
8
8
  base.extend ClassMethods
9
9
  base.include ::ClientApiBuilder::Section
10
10
  base.include ::ClientApiBuilder::NetHTTP::Request
11
- base.send(:attr_reader, :response, :request_options)
11
+ base.include(::ClientApiBuilder::ActiveSupportNotifications) if defined?(ActiveSupport)
12
+ base.send(:attr_reader, :response, :request_options, :total_request_time, :retry_attempts)
12
13
  end
13
14
 
14
15
  module ClassMethods
@@ -26,97 +27,120 @@ module ClientApiBuilder
26
27
  headers: {},
27
28
  query_builder: Hash.method_defined?(:to_query) ? :to_query : :query_params,
28
29
  query_params: {},
29
- response_procs: {}
30
+ response_procs: {},
31
+ max_retries: 1,
32
+ sleep: 0.05
30
33
  }.freeze
31
34
  end
32
35
 
33
-
34
- def add_response_procs(method_name, proc)
36
+ # tracks the proc used to handle responses
37
+ def add_response_proc(method_name, proc)
35
38
  response_procs = default_options[:response_procs].dup
36
39
  response_procs[method_name] = proc
37
40
  add_value_to_class_method(:default_options, response_procs: response_procs)
38
41
  end
39
42
 
40
- def response_proc(method_name)
41
- proc = default_options[:response_procs][method_name]
42
- proc
43
+ # retrieves the proc used to handle the response
44
+ def get_response_proc(method_name)
45
+ default_options[:response_procs][method_name]
43
46
  end
44
47
 
48
+ # set/get base url
45
49
  def base_url(url = nil)
46
50
  return default_options[:base_url] unless url
47
51
 
48
52
  add_value_to_class_method(:default_options, base_url: url)
49
53
  end
50
54
 
51
- def body_builder(builder = nil)
52
- return default_options[:body_builder] unless builder
55
+ # set the builder to :to_json, :to_query, :query_params or specify a proc to handle building the request body payload
56
+ # or get the body builder
57
+ def body_builder(builder = nil, &block)
58
+ return default_options[:body_builder] if builder.nil? && block.nil?
53
59
 
54
- add_value_to_class_method(:default_options, body_builder: builder)
60
+ add_value_to_class_method(:default_options, body_builder: builder || block)
55
61
  end
56
62
 
57
- def query_builder(builder = nil)
58
- return default_options[:query_builder] unless builder
63
+ # set the builder to :to_query, :query_params or specify a proc to handle building the request query params
64
+ # or get the query builder
65
+ def query_builder(builder = nil, &block)
66
+ return default_options[:query_builder] if builder.nil? && block.nil?
59
67
 
60
- add_value_to_class_method(:default_options, query_builder: builder)
68
+ add_value_to_class_method(:default_options, query_builder: builder || block)
61
69
  end
62
70
 
71
+ # add a request header
63
72
  def header(name, value = nil, &block)
64
73
  headers = default_options[:headers].dup
65
74
  headers[name] = value || block
66
75
  add_value_to_class_method(:default_options, headers: headers)
67
76
  end
68
77
 
78
+ # set a connection_option, specific to Net::HTTP
69
79
  def connection_option(name, value)
70
80
  connection_options = default_options[:connection_options].dup
71
81
  connection_options[name] = value
72
82
  add_value_to_class_method(:default_options, connection_options: connection_options)
73
83
  end
74
84
 
85
+ def configure_retries(max_retries, sleep_time_between_retries_in_seconds = 0.05)
86
+ add_value_to_class_method(
87
+ :default_options,
88
+ max_retries: max_retries,
89
+ sleep: sleep_time_between_retries_in_seconds
90
+ )
91
+ end
92
+
93
+ # add a query param to all requests
75
94
  def query_param(name, value = nil, &block)
76
95
  query_params = default_options[:query_params].dup
77
96
  query_params[name] = value || block
78
97
  add_value_to_class_method(:default_options, query_params: query_params)
79
98
  end
80
99
 
81
- def headers
100
+ # get default headers
101
+ def default_headers
82
102
  default_options[:headers]
83
103
  end
84
104
 
85
- def connection_options
105
+ # get configured connection_options
106
+ def default_connection_options
86
107
  default_options[:connection_options]
87
108
  end
88
109
 
89
- def query_params
110
+ # get default query_params to add to all requests
111
+ def default_query_params
90
112
  default_options[:query_params]
91
113
  end
92
114
 
93
- def build_body(router, body, options)
94
- builder = options[:body_builder] || body_builder
95
-
96
- case builder
115
+ def build_body(router, body)
116
+ case body_builder
97
117
  when :to_json
98
118
  body.to_json
99
119
  when :to_query
100
120
  body.to_query
101
121
  when :query_params
102
- ClientApiBuilder::QueryParams.to_query(body)
122
+ ClientApiBuilder::QueryParams.new.to_query(body)
123
+ when Symbol
124
+ router.send(body_builder, body)
103
125
  else
104
- router.instance_exec(body, &builder)
126
+ router.instance_exec(body, &body_builder)
105
127
  end
106
128
  end
107
129
 
108
- def build_query(query)
130
+ def build_query(router, query)
109
131
  case query_builder
110
132
  when :to_query
111
133
  query.to_query
112
134
  when :query_params
113
- ClientApiBuilder::QueryParams.to_query(query)
135
+ ClientApiBuilder::QueryParams.new.to_query(query)
136
+ when Symbol
137
+ router.send(query_builder, query)
114
138
  else
115
- query_builder.call(query)
139
+ router.instance_exec(query, &query_builder)
116
140
  end
117
141
  end
118
142
 
119
- def http_method(method_name)
143
+ def auto_detect_http_method(method_name)
120
144
  case method_name.to_s
121
145
  when /^(?:post|create|add|insert)/i
122
146
  :post
@@ -172,6 +196,7 @@ module ClientApiBuilder
172
196
  arguments
173
197
  end
174
198
 
199
+ # returns a list of arguments to add to the route method
175
200
  def get_arguments(value)
176
201
  case value
177
202
  when Hash
@@ -184,7 +209,7 @@ module ClientApiBuilder
184
209
  end
185
210
 
186
211
  def get_instance_method(var)
187
- "#\{#{var}\}"
212
+ "#\{escape_path(#{var})\}"
188
213
  end
189
214
 
190
215
  @@namespaces = []
@@ -192,6 +217,7 @@ module ClientApiBuilder
192
217
  @@namespaces
193
218
  end
194
219
 
220
+ # a namespace is a top level path to apply to all routes within the namespace block
195
221
  def namespace(name)
196
222
  namespaces << name
197
223
  yield
@@ -199,7 +225,7 @@ module ClientApiBuilder
199
225
  end
200
226
 
201
227
  def generate_route_code(method_name, path, options = {})
202
- http_method = options[:method] || http_method(method_name)
228
+ http_method = options[:method] || auto_detect_http_method(method_name)
203
229
 
204
230
  path = namespaces.join + path
205
231
 
@@ -211,7 +237,7 @@ module ClientApiBuilder
211
237
  path_arguments = []
212
238
  path.gsub!(/:([a-z0-9_]+)/i) do |_|
213
239
  path_arguments << $1
214
- "#\{#{$1}\}"
240
+ "#\{escape_path(#{$1})\}"
215
241
  end
216
242
 
217
243
  has_body_param = options[:body].nil? && requires_body?(http_method, options)
@@ -268,12 +294,10 @@ module ClientApiBuilder
268
294
  method_args += ["#{stream_param}:"] if stream_param
269
295
  method_args += ['**__options__', '&block']
270
296
 
271
- code = "def #{method_name}(" + method_args.join(', ') + ")\n"
272
- code += " block ||= self.class.response_proc(#{method_name.inspect})\n"
297
+ code = "def #{method_name}_raw_response(" + method_args.join(', ') + ")\n"
273
298
  code += " __path__ = \"#{path}\"\n"
274
299
  code += " __query__ = #{query}\n"
275
300
  code += " __body__ = #{body}\n"
276
- code += " __expected_response_codes__ = #{expected_response_codes.inspect}\n"
277
301
  code += " __uri__ = build_uri(__path__, __query__, __options__)\n"
278
302
  code += " __body__ = build_body(__body__, __options__)\n"
279
303
  code += " __headers__ = build_headers(__options__)\n"
@@ -292,30 +316,38 @@ module ClientApiBuilder
292
316
  else
293
317
  code += " @response = request(**@request_options)\n"
294
318
  end
319
+ code += "end\n"
320
+ code += "\n"
295
321
 
296
- code += " expected_response_code!(@response, __expected_response_codes__, __options__)\n"
322
+ code += "def #{method_name}(" + method_args.join(', ') + ")\n"
323
+ code += " request_wrapper(__options__) do\n"
324
+ code += " block ||= self.class.get_response_proc(#{method_name.inspect})\n"
325
+ code += " __expected_response_codes__ = #{expected_response_codes.inspect}\n"
326
+ code += " #{method_name}_raw_response(" + method_args.map { |a| a =~ /:$/ ? "#{a} #{a.sub(':', '')}" : a }.join(', ') + ")\n"
327
+ code += " expected_response_code!(@response, __expected_response_codes__, __options__)\n"
297
328
 
298
329
  if options[:stream] || options[:return] == :response
299
- code += " @response\n"
330
+ code += " @response\n"
300
331
  elsif options[:return] == :body
301
- code += " @response.body\n"
332
+ code += " @response.body\n"
302
333
  else
303
- code += " handle_response(@response, __options__, &block)\n"
334
+ code += " handle_response(@response, __options__, &block)\n"
304
335
  end
305
336
 
337
+ code += " end\n"
306
338
  code += "end\n"
307
339
  code
308
340
  end
309
341
 
310
342
  def route(method_name, path, options = {}, &block)
311
- add_response_procs(method_name, block) if block
343
+ add_response_proc(method_name, block) if block
312
344
 
313
345
  self.class_eval generate_route_code(method_name, path, options), __FILE__, __LINE__
314
346
  end
315
347
  end
316
348
 
317
- def base_url(options)
318
- options[:base_url] || self.class.base_url
349
+ def base_url
350
+ self.class.base_url
319
351
  end
320
352
 
321
353
  def build_headers(options)
@@ -332,7 +364,7 @@ module ClientApiBuilder
332
364
  end
333
365
  end
334
366
 
335
- self.class.headers.each(&add_header_proc)
367
+ self.class.default_headers.each(&add_header_proc)
336
368
  options[:headers] && options[:headers].each(&add_header_proc)
337
369
 
338
370
  headers
@@ -340,14 +372,14 @@ module ClientApiBuilder
340
372
 
341
373
  def build_connection_options(options)
342
374
  if options[:connection_options]
343
- self.class.connection_options.merge(options[:connection_options])
375
+ self.class.default_connection_options.merge(options[:connection_options])
344
376
  else
345
- self.class.connection_options
377
+ self.class.default_connection_options
346
378
  end
347
379
  end
348
380
 
349
381
  def build_query(query, options)
350
- return nil if query.nil? && self.class.query_params.empty?
382
+ return nil if query.nil? && self.class.default_query_params.empty?
351
383
 
352
384
  query_params = {}
353
385
 
@@ -362,11 +394,11 @@ module ClientApiBuilder
362
394
  end
363
395
  end
364
396
 
365
- self.class.query_params.each(&add_query_param_proc)
397
+ self.class.default_query_params.each(&add_query_param_proc)
366
398
  query && query.each(&add_query_param_proc)
367
399
  options[:query] && options[:query].each(&add_query_param_proc)
368
400
 
369
- self.class.build_query(query_params)
401
+ self.class.build_query(self, query_params)
370
402
  end
371
403
 
372
404
  def build_body(body, options)
@@ -375,11 +407,11 @@ module ClientApiBuilder
375
407
  return nil unless body
376
408
  return body if body.is_a?(String)
377
409
 
378
- self.class.build_body(self, body, options)
410
+ self.class.build_body(self, body)
379
411
  end
380
412
 
381
413
  def build_uri(path, query, options)
382
- uri = URI(base_url(options) + path)
414
+ uri = URI(base_url + path)
383
415
  uri.query = build_query(query, options)
384
416
  uri
385
417
  end
@@ -416,5 +448,64 @@ module ClientApiBuilder
416
448
  def root_router
417
449
  self
418
450
  end
451
+
452
+ def escape_path(path)
453
+ path
454
+ end
455
+
456
+ def instrument_request
457
+ start_time = Time.now
458
+ yield
459
+ ensure
460
+ @total_request_time = Time.now - start_time
461
+ end
462
+
463
+ def retry_request(options)
464
+ @request_attempts = 0
465
+ max_attempts = get_retry_request_max_retries(options)
466
+ begin
467
+ @request_attempts += 1
468
+ yield
469
+ rescue Exception => e
470
+ log_request_exception(e)
471
+ raise(e) if @request_attempts >= max_attempts || !retry_request?(e, options)
472
+ sleep_time = get_retry_request_sleep_time(e, options)
473
+ sleep(sleep_time) if sleep_time && sleep_time > 0
474
+ retry
475
+ end
476
+ end
477
+
478
+ def get_retry_request_sleep_time(e, options)
479
+ options[:sleep] || self.class.default_options[:sleep] || 0.05
480
+ end
481
+
482
+ def get_retry_request_max_retries(options)
483
+ options[:retries] || self.class.default_options[:max_retries] || 1
484
+ end
485
+
486
+ def request_wrapper(options)
487
+ retry_request(options) do
488
+ instrument_request do
489
+ yield
490
+ end
491
+ end
492
+ end
493
+
494
+ def retry_request?(exception, options)
495
+ true
496
+ end
497
+
498
+ def log_request_exception(exception)
499
+ ::ClientApiBuilder.logger && ::ClientApiBuilder.logger.error(exception)
500
+ end
501
+
502
+ def request_log_message
503
+ method = request_options[:method].to_s.upcase
504
+ uri = request_options[:uri]
505
+ response_code = response ? response.code : 'UNKNOWN'
506
+
507
+ duration = (total_request_time * 1000).to_i
508
+ "#{method} #{uri.scheme}://#{uri.host}#{uri.path}[#{response_code}] took #{duration}ms"
509
+ end
419
510
  end
420
511
  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,9 @@ $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
+ require 'logger'
10
+ LOG = Logger.new(STDOUT)
11
+ ClientApiBuilder.logger = LOG
12
+ ClientApiBuilder::ActiveSupportLogSubscriber.new(LOG).subscribe!
9
13
  require 'irb'
10
14
  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.8
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Doug Youch
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-08-16 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
@@ -41,6 +41,8 @@ files:
41
41
  - examples/basic_auth_example_client.rb
42
42
  - examples/imdb_datasets_client.rb
43
43
  - lib/client-api-builder.rb
44
+ - lib/client_api_builder/active_support_log_subscriber.rb
45
+ - lib/client_api_builder/active_support_notifications.rb
44
46
  - lib/client_api_builder/nested_router.rb
45
47
  - lib/client_api_builder/net_http_request.rb
46
48
  - lib/client_api_builder/query_params.rb
@@ -66,7 +68,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
66
68
  - !ruby/object:Gem::Version
67
69
  version: '0'
68
70
  requirements: []
69
- rubygems_version: 3.2.3
71
+ rubygems_version: 3.3.3
70
72
  signing_key:
71
73
  specification_version: 4
72
74
  summary: Develop Client API libraries faster