client-api-builder 0.2.3 → 0.2.7
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/Gemfile.lock +1 -1
- data/client-api-builder.gemspec +3 -1
- data/examples/basic_auth_example_client.rb +20 -0
- data/examples/imdb_datasets_client.rb +31 -0
- data/lib/client-api-builder.rb +3 -1
- data/lib/client_api_builder/nested_router.rb +100 -0
- data/lib/client_api_builder/net_http_request.rb +2 -2
- data/lib/client_api_builder/router.rb +86 -12
- data/lib/client_api_builder/section.rb +34 -0
- data/script/console +3 -0
- metadata +20 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 920aa0f202c1e60cf6e3dc8bf0e44d289f3cc37ec6052b2bca3535493065bb7e
|
4
|
+
data.tar.gz: c8f534c244d2c572f36f9b42a64610bde62a7bbc8b8d54ef2ae7049982f6a1c0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 971aea4195329e45bf445ec744bcffaf3fc9a4d1a4eed5b88ec4be973f30daa568e7bca15180bc008b7f36967144fb130df67a11a4b060a96bae93ad1fd8ce32
|
7
|
+
data.tar.gz: ded78f47f7b31dfb671fb8bcd8ee383e0e965a16c94090cabba69a2bc1eedd678f99acc9baa2e89c23a391df6d7dd3e7b4ed3634d1722bda18f13cfd505696c2
|
data/Gemfile.lock
CHANGED
data/client-api-builder.gemspec
CHANGED
@@ -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.
|
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
|
data/lib/client-api-builder.rb
CHANGED
@@ -11,8 +11,10 @@ module ClientApiBuilder
|
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
-
autoload :
|
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
|
-
|
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
|
-
|
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]
|
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
|
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
|
-
|
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
|
-
|
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)
|
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.
|
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-
|
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:
|