client-api-builder 0.2.3 → 0.2.7

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: '01795148989ed0a14c42cb886fd3ce76e3692d82c388f49adf09b7b7229b928f'
4
- data.tar.gz: 4ce828ecc5ee2017e6318ab83e8c1c02e3a557f746a7f3f6fc8d7471cdac2750
3
+ metadata.gz: 920aa0f202c1e60cf6e3dc8bf0e44d289f3cc37ec6052b2bca3535493065bb7e
4
+ data.tar.gz: c8f534c244d2c572f36f9b42a64610bde62a7bbc8b8d54ef2ae7049982f6a1c0
5
5
  SHA512:
6
- metadata.gz: 57b713cda946c263a13f6642d8548442f574eaea1f0374b1e02c7d2f071a3826aeb3e49eaa3a785784eeb7caa671584192f0391e6a508b5dbbd4fff32868063e
7
- data.tar.gz: d41bfea574c3399d534455ab7165a9fc6f91c347fd5c6d6634fa67731bb5198ccc843532e06fea853097316f766522daebf875cce855694b87e67bb0ea6771fb
6
+ metadata.gz: 971aea4195329e45bf445ec744bcffaf3fc9a4d1a4eed5b88ec4be973f30daa568e7bca15180bc008b7f36967144fb130df67a11a4b060a96bae93ad1fd8ce32
7
+ data.tar.gz: ded78f47f7b31dfb671fb8bcd8ee383e0e965a16c94090cabba69a2bc1eedd678f99acc9baa2e89c23a391df6d7dd3e7b4ed3634d1722bda18f13cfd505696c2
data/Gemfile.lock CHANGED
@@ -9,7 +9,7 @@ GEM
9
9
  diff-lcs (1.4.4)
10
10
  docile (1.4.0)
11
11
  hashdiff (1.0.1)
12
- inheritance-helper (0.1.5)
12
+ inheritance-helper (0.2.5)
13
13
  parallel (1.20.1)
14
14
  parser (3.0.1.1)
15
15
  ast (~> 2.4.1)
@@ -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.3'
5
+ s.version = '0.2.7'
6
6
  s.licenses = ['MIT']
7
7
  s.summary = 'Develop Client API libraries faster'
8
8
  s.description = 'Utility for constructing API clients'
@@ -10,4 +10,6 @@ Gem::Specification.new do |s|
10
10
  s.email = 'dougyouch@gmail.com'
11
11
  s.homepage = 'https://github.com/dougyouch/client-api-builder'
12
12
  s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
13
+
14
+ s.add_runtime_dependency 'inheritance-helper', '>= 0.2.5'
13
15
  end
@@ -1,4 +1,5 @@
1
1
  require 'base64'
2
+ require 'securerandom'
2
3
 
3
4
  class BasicAuthExampleClient < Struct.new(
4
5
  :username,
@@ -6,6 +7,7 @@ class BasicAuthExampleClient < Struct.new(
6
7
  )
7
8
 
8
9
  include ClientApiBuilder::Router
10
+ include ClientApiBuilder::Section
9
11
 
10
12
  base_url 'https://www.example.com'
11
13
 
@@ -15,9 +17,27 @@ class BasicAuthExampleClient < Struct.new(
15
17
  route :get_apps, '/apps'
16
18
  route :get_app, '/apps/:app_id'
17
19
 
20
+ section :users do
21
+ header 'Authorization', :bearer_authorization
22
+
23
+ route :create_user, '/users?z={cache_buster}'
24
+ end
25
+
26
+ def cache_buster
27
+ (Time.now.to_f * 1000).to_i
28
+ end
29
+
18
30
  private
19
31
 
32
+ def auth_token
33
+ @auth_token ||= SecureRandom.uuid
34
+ end
35
+
20
36
  def basic_authorization
21
37
  'basic ' + Base64.strict_encode64(username + ':' + password)
22
38
  end
39
+
40
+ def bearer_authorization
41
+ 'bearer ' + auth_token
42
+ end
23
43
  end
@@ -0,0 +1,31 @@
1
+ class IMDBDatesetsClient
2
+ include ClientApiBuilder::Router
3
+
4
+ base_url 'https://datasets.imdbws.com'
5
+
6
+ route :get_name_basics, '/name.basics.tsv.gz', stream: :file
7
+ route :get_title_akas, '/title.akas.tsv.gz', stream: :io
8
+ route :get_title_basics, '/title.basics.tsv.gz', stream: :block
9
+
10
+ def self.stream_to_file
11
+ new.get_name_basics(file: 'name.basics.tsv.gz')
12
+ end
13
+
14
+ def self.stream_to_io
15
+ File.open('title.akas.tsv.gz', 'wb') do |io|
16
+ new.get_title_akas(io: io)
17
+ end
18
+ end
19
+
20
+ def self.stream_with_block
21
+ File.open('title.basics.tsv.gz', 'wb') do |io|
22
+ total_read = 0.0
23
+ new.get_title_basics do |response, chunk|
24
+ total_read += chunk.bytesize
25
+ percentage_complete = ((total_read / response.content_length) * 100).to_i
26
+ puts "downloading title.basics.tsv.gz completed: #{percentage_complete}%"
27
+ io.write chunk
28
+ end
29
+ end
30
+ end
31
+ end
@@ -11,8 +11,10 @@ module ClientApiBuilder
11
11
  end
12
12
  end
13
13
 
14
- autoload :Router, 'client_api_builder/router'
14
+ autoload :NestedRouter, 'client_api_builder/nested_router'
15
15
  autoload :QueryParams, 'client_api_builder/query_params'
16
+ autoload :Router, 'client_api_builder/router'
17
+ autoload :Section, 'client_api_builder/section'
16
18
 
17
19
  module NetHTTP
18
20
  autoload :Request, 'client_api_builder/net_http_request'
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Purpose: to nest routers, which are sub sections of APIs
4
+ # for example if you had an entire section of your API dedicatd to user management.
5
+ # you may want to nest all calls to those routes under the user section
6
+ # ex: client.users.get_user(id: 1) # where users is a nested router
7
+ module ClientApiBuilder
8
+ class NestedRouter
9
+ include ::ClientApiBuilder::Router
10
+
11
+ attr_reader :root_router
12
+
13
+ def initialize(root_router)
14
+ @root_router = root_router
15
+ end
16
+
17
+ def self.get_instance_method(var)
18
+ "\#{root_router.#{var}\}"
19
+ end
20
+
21
+ def request(**options, &block)
22
+ root_router.request(**options, &block)
23
+ end
24
+
25
+ def stream(**options, &block)
26
+ root_router.stream(**options, &block)
27
+ end
28
+
29
+ def stream_to_io(**options, &block)
30
+ root_router.stream_to_io(**options, &block)
31
+ end
32
+
33
+ def stream_to_file(**options, &block)
34
+ root_router.stream_to_file(**options, &block)
35
+ end
36
+
37
+ def base_url(options)
38
+ self.class.base_url || root_router.base_url(options)
39
+ end
40
+
41
+ def build_headers(options)
42
+ headers = root_router.build_headers(options)
43
+
44
+ add_header_proc = proc do |name, value|
45
+ headers[name] =
46
+ if value.is_a?(Proc)
47
+ root_router.instance_eval(&value)
48
+ elsif value.is_a?(Symbol)
49
+ root_router.send(value)
50
+ else
51
+ value
52
+ end
53
+ end
54
+
55
+ self.class.headers.each(&add_header_proc)
56
+
57
+ headers
58
+ end
59
+
60
+ def build_connection_options(options)
61
+ root_router.build_connection_options(options)
62
+ end
63
+
64
+ def build_query(query, options)
65
+ return nil if query.nil? && root_router.class.query_params.empty? && self.class.query_params.empty?
66
+
67
+ query_params = {}
68
+
69
+ add_query_param_proc = proc do |name, value|
70
+ query_params[name] =
71
+ if value.is_a?(Proc)
72
+ root_router.instance_eval(&value)
73
+ elsif value.is_a?(Symbol)
74
+ root_router.send(value)
75
+ else
76
+ value
77
+ end
78
+ end
79
+
80
+ root_router.class.query_params.each(&add_query_param_proc)
81
+ self.class.query_params.each(&add_query_param_proc)
82
+ query && query.each(&add_query_param_proc)
83
+ options[:query] && options[:query].each(&add_query_param_proc)
84
+
85
+ self.class.build_query(query_params)
86
+ end
87
+
88
+ def build_body(body, options)
89
+ root_router.build_body(body, options)
90
+ end
91
+
92
+ def expected_response_code!(response, expected_response_codes, options)
93
+ root_router.expected_response_code!(response, expected_response_codes, options)
94
+ end
95
+
96
+ def handle_response(response, options, &block)
97
+ root_router.handle_response(response, options, &block)
98
+ end
99
+ end
100
+ end
@@ -37,13 +37,13 @@ module ClientApiBuilder
37
37
  def stream(method:, uri:, body:, headers:, connection_options:)
38
38
  request(method: method, uri: uri, body: body, headers: headers, connection_options: connection_options) do |response|
39
39
  response.read_body do |chunk|
40
- yield chunk
40
+ yield response, chunk
41
41
  end
42
42
  end
43
43
  end
44
44
 
45
45
  def stream_to_io(method:, uri:, body:, headers:, connection_options:, io:)
46
- stream(method: method, uri: uri, body: body, headers: headers, connection_options: connection_options) do |chunk|
46
+ stream(method: method, uri: uri, body: body, headers: headers, connection_options: connection_options) do |_, chunk|
47
47
  io.write chunk
48
48
  end
49
49
  end
@@ -6,6 +6,7 @@ module ClientApiBuilder
6
6
  def self.included(base)
7
7
  base.extend InheritanceHelper::Methods
8
8
  base.extend ClassMethods
9
+ base.include ::ClientApiBuilder::Section
9
10
  base.include ::ClientApiBuilder::NetHTTP::Request
10
11
  base.attr_reader :response, :request_options
11
12
  end
@@ -20,19 +21,39 @@ module ClientApiBuilder
20
21
  def default_options
21
22
  {
22
23
  base_url: nil,
23
- headers: {},
24
+ body_builder: :to_json,
24
25
  connection_options: {},
26
+ headers: {},
27
+ query_builder: Hash.method_defined?(:to_query) ? :to_query : :query_params,
25
28
  query_params: {},
26
- query_builder: Hash.method_defined?(:to_query) ? :to_query : :query_params
29
+ response_procs: {}
27
30
  }.freeze
28
31
  end
29
32
 
33
+
34
+ def add_response_procs(method_name, proc)
35
+ response_procs = default_options[:response_procs].dup
36
+ response_procs[method_name] = proc
37
+ add_value_to_class_method(:default_options, response_procs: response_procs)
38
+ end
39
+
40
+ def response_proc(method_name)
41
+ proc = default_options[:response_procs][method_name]
42
+ proc
43
+ end
44
+
30
45
  def base_url(url = nil)
31
46
  return default_options[:base_url] unless url
32
47
 
33
48
  add_value_to_class_method(:default_options, base_url: url)
34
49
  end
35
50
 
51
+ def body_builder(builder = nil)
52
+ return default_options[:body_builder] unless builder
53
+
54
+ add_value_to_class_method(:default_options, body_builder: builder)
55
+ end
56
+
36
57
  def query_builder(builder = nil)
37
58
  return default_options[:query_builder] unless builder
38
59
 
@@ -69,6 +90,21 @@ module ClientApiBuilder
69
90
  default_options[:query_params]
70
91
  end
71
92
 
93
+ def build_body(router, body, options)
94
+ builder = options[:body_builder] || body_builder
95
+
96
+ case builder
97
+ when :to_json
98
+ body.to_json
99
+ when :to_query
100
+ body.to_query
101
+ when :query_params
102
+ ClientApiBuilder::QueryParams.to_query(body)
103
+ else
104
+ router.instance_exec(body, &builder)
105
+ end
106
+ end
107
+
72
108
  def build_query(query)
73
109
  case query_builder
74
110
  when :to_query
@@ -111,6 +147,8 @@ module ClientApiBuilder
111
147
  arguments += get_hash_arguments(v)
112
148
  when Array
113
149
  arguments += get_array_arguments(v)
150
+ when String
151
+ hsh[k] = "__||#{$1}||__" if v =~ /\{([a-z0-9_]+)\}/i
114
152
  end
115
153
  end
116
154
  arguments
@@ -127,6 +165,8 @@ module ClientApiBuilder
127
165
  arguments += get_hash_arguments(v)
128
166
  when Array
129
167
  arguments += get_array_arguments(v)
168
+ when String
169
+ list[idx] = "__||#{$1}||__" if v =~ /\{([a-z0-9_]+)\}/i
130
170
  end
131
171
  end
132
172
  arguments
@@ -143,9 +183,31 @@ module ClientApiBuilder
143
183
  end
144
184
  end
145
185
 
186
+ def get_instance_method(var)
187
+ "#\{#{var}\}"
188
+ end
189
+
190
+ @@namespaces = []
191
+ def namespaces
192
+ @@namespaces
193
+ end
194
+
195
+ def namespace(name)
196
+ namespaces << name
197
+ yield
198
+ namespaces.pop
199
+ end
200
+
146
201
  def generate_route_code(method_name, path, options = {})
147
202
  http_method = options[:method] || http_method(method_name)
148
203
 
204
+ path = namespaces.join + path
205
+
206
+ # instance method
207
+ path.gsub!(/\{([a-z0-9_]+)\}/i) do |_|
208
+ get_instance_method($1)
209
+ end
210
+
149
211
  path_arguments = []
150
212
  path.gsub!(/:([a-z0-9_]+)/i) do |_|
151
213
  path_arguments << $1
@@ -207,6 +269,7 @@ module ClientApiBuilder
207
269
  method_args += ['**__options__', '&block']
208
270
 
209
271
  code = "def #{method_name}(" + method_args.join(', ') + ")\n"
272
+ code += " block ||= self.class.response_proc(#{method_name.inspect})\n"
210
273
  code += " __path__ = \"#{path}\"\n"
211
274
  code += " __query__ = #{query}\n"
212
275
  code += " __body__ = #{body}\n"
@@ -220,9 +283,10 @@ module ClientApiBuilder
220
283
 
221
284
  case options[:stream]
222
285
  when true,
223
- :file,
224
- :io
286
+ :file
225
287
  code += " @response = stream_to_file(**@request_options)\n"
288
+ when :io
289
+ code += " @response = stream_to_io(**@request_options)\n"
226
290
  when :block
227
291
  code += " @response = stream(**@request_options, &block)\n"
228
292
  else
@@ -243,13 +307,15 @@ module ClientApiBuilder
243
307
  code
244
308
  end
245
309
 
246
- def route(method_name, path, options = {})
310
+ def route(method_name, path, options = {}, &block)
311
+ add_response_procs(method_name, block) if block
312
+
247
313
  self.class_eval generate_route_code(method_name, path, options), __FILE__, __LINE__
248
314
  end
249
315
  end
250
316
 
251
317
  def base_url(options)
252
- self.class.base_url
318
+ options[:base_url] || self.class.base_url
253
319
  end
254
320
 
255
321
  def build_headers(options)
@@ -267,7 +333,7 @@ module ClientApiBuilder
267
333
  end
268
334
 
269
335
  self.class.headers.each(&add_header_proc)
270
- options[:headers]&.each(&add_header_proc)
336
+ options[:headers] && options[:headers].each(&add_header_proc)
271
337
 
272
338
  headers
273
339
  end
@@ -281,6 +347,8 @@ module ClientApiBuilder
281
347
  end
282
348
 
283
349
  def build_query(query, options)
350
+ return nil if query.nil? && self.class.query_params.empty?
351
+
284
352
  query_params = {}
285
353
 
286
354
  add_query_param_proc = proc do |name, value|
@@ -295,22 +363,24 @@ module ClientApiBuilder
295
363
  end
296
364
 
297
365
  self.class.query_params.each(&add_query_param_proc)
298
- query&.each(&add_query_param_proc)
366
+ query && query.each(&add_query_param_proc)
367
+ options[:query] && options[:query].each(&add_query_param_proc)
299
368
 
300
369
  self.class.build_query(query_params)
301
370
  end
302
371
 
303
372
  def build_body(body, options)
304
- return unless body
373
+ body = options[:body] if options.key?(:body)
374
+
375
+ return nil unless body
305
376
  return body if body.is_a?(String)
306
377
 
307
- body.merge!(options[:body]) if options[:body]
308
- body.to_json
378
+ self.class.build_body(self, body, options)
309
379
  end
310
380
 
311
381
  def build_uri(path, query, options)
312
382
  uri = URI(base_url(options) + path)
313
- uri.query = build_query(query, options) if query
383
+ uri.query = build_query(query, options)
314
384
  uri
315
385
  end
316
386
 
@@ -342,5 +412,9 @@ module ClientApiBuilder
342
412
  data
343
413
  end
344
414
  end
415
+
416
+ def root_router
417
+ self
418
+ end
345
419
  end
346
420
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Purpose is to encapsulate adding nested routers
4
+ module ClientApiBuilder
5
+ module Section
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ def section(name, &block)
12
+ kls = InheritanceHelper::ClassBuilder::Utils.create_class(
13
+ self,
14
+ name,
15
+ ::ClientApiBuilder::NestedRouter,
16
+ nil,
17
+ 'NestedRouter',
18
+ &block
19
+ )
20
+
21
+ code = <<CODE
22
+ def self.#{name}_router
23
+ #{kls.name}
24
+ end
25
+
26
+ def #{name}
27
+ @#{name} ||= self.class.#{name}_router.new(self.root_router)
28
+ end
29
+ CODE
30
+ self.class_eval code, __FILE__, __LINE__
31
+ end
32
+ end
33
+ end
34
+ end
data/script/console CHANGED
@@ -2,6 +2,9 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  $LOAD_PATH << File.expand_path('../lib', __dir__)
5
+ $LOAD_PATH << File.expand_path('../examples', __dir__)
5
6
  require 'client-api-builder'
7
+ autoload :BasicAuthExampleClient, 'basic_auth_example_client'
8
+ autoload :IMDBDatesetsClient, 'imdb_datasets_client'
6
9
  require 'irb'
7
10
  IRB.start(__FILE__)
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: client-api-builder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.7
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-07-17 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2021-08-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: inheritance-helper
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.2.5
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.2.5
13
27
  description: Utility for constructing API clients
14
28
  email: dougyouch@gmail.com
15
29
  executables: []
@@ -25,10 +39,13 @@ files:
25
39
  - README.md
26
40
  - client-api-builder.gemspec
27
41
  - examples/basic_auth_example_client.rb
42
+ - examples/imdb_datasets_client.rb
28
43
  - lib/client-api-builder.rb
44
+ - lib/client_api_builder/nested_router.rb
29
45
  - lib/client_api_builder/net_http_request.rb
30
46
  - lib/client_api_builder/query_params.rb
31
47
  - lib/client_api_builder/router.rb
48
+ - lib/client_api_builder/section.rb
32
49
  - script/console
33
50
  homepage: https://github.com/dougyouch/client-api-builder
34
51
  licenses: