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 +4 -4
- data/README.md +32 -1
- data/client-api-builder.gemspec +3 -3
- data/examples/basic_auth_example_client.rb +2 -0
- data/examples/lorem_ipsum_client.rb +16 -0
- data/lib/client-api-builder.rb +6 -0
- data/lib/client_api_builder/active_support_log_subscriber.rb +29 -0
- data/lib/client_api_builder/active_support_notifications.rb +25 -0
- data/lib/client_api_builder/nested_router.rb +6 -3
- data/lib/client_api_builder/router.rb +78 -9
- data/lib/client_api_builder/section.rb +2 -2
- data/script/console +5 -0
- metadata +7 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c22eb6c8362909af90380e2513875c7a82c4bd67028e5a601694c1d604fc599d
|
4
|
+
data.tar.gz: e6ad4f872b67a07563b88f3880bbd1f3375ed2d628a4c15cdedb0734fc9723a3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5bbe4fe12d3ea5ca240b445630360346a0a1628c340c9ce602efbacfb185ad23c7215233dcb84cb245ff5d3961b0695e8d177a15c39f87e9f6f6b4c0d030e60c
|
7
|
+
data.tar.gz: 44c82d195b80ad34205af3fa256523fa0a3a1e7249bd857ac7cc0cece431d9d75906f26a5714914075e0b9291d84ad5649704add740c099d960ad2e60ad3e2ee
|
data/README.md
CHANGED
@@ -1,2 +1,33 @@
|
|
1
1
|
# client-api-builder
|
2
|
-
|
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
|
+
```
|
data/client-api-builder.gemspec
CHANGED
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = 'client-api-builder'
|
5
|
-
s.version = '0.
|
5
|
+
s.version = '0.4.0'
|
6
6
|
s.licenses = ['MIT']
|
7
|
-
s.summary = '
|
8
|
-
s.description = '
|
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'
|
@@ -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
|
data/lib/client-api-builder.rb
CHANGED
@@ -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.
|
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 += "
|
313
|
-
code += "
|
314
|
-
code += "
|
315
|
-
code += "
|
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 += "
|
331
|
+
code += " @response\n"
|
319
332
|
elsif options[:return] == :body
|
320
|
-
code += "
|
333
|
+
code += " @response.body\n"
|
321
334
|
else
|
322
|
-
code += "
|
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.
|
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
|
+
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:
|
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:
|
75
|
+
summary: Utility for creating API clients through configuration
|
73
76
|
test_files: []
|