client-api-builder 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 34ae348684b22669748f3dca2fc9866f2b87247da8452fc5b848e175c1ef74a8
4
+ data.tar.gz: 95e6b09d24e961c0ce20a6cc15d3fcdf858f47b2725151838cd8ea5d01df0965
5
+ SHA512:
6
+ metadata.gz: e6dd7780a28eec364c787d209fbd37618c6c9f8c7262c1abaa2fbf90b419240657a77bd1a7523388f680643837839162c36a427ca3023ccee986d6f0c1ba3b43
7
+ data.tar.gz: 6b407cefa4e351149e881c6f19897f1566b0440684790f26b9df7636a151737c068efd23ced372b8cd0a306179dc6a4c3491de574fdaacdd8b5f90b0b03e5e93
data/.gitignore ADDED
@@ -0,0 +1,51 @@
1
+ # rcov generated
2
+ coverage
3
+ coverage.data
4
+
5
+ # rdoc generated
6
+ rdoc
7
+
8
+ # yard generated
9
+ doc
10
+ .yardoc
11
+
12
+ # bundler
13
+ .bundle
14
+
15
+ # jeweler generated
16
+ pkg
17
+
18
+ # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
19
+ #
20
+ # * Create a file at ~/.gitignore
21
+ # * Include files you want ignored
22
+ # * Run: git config --global core.excludesfile ~/.gitignore
23
+ #
24
+ # After doing this, these files will be ignored in all your git projects,
25
+ # saving you from having to 'pollute' every project you touch with them
26
+ #
27
+ # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
28
+ #
29
+ # For MacOS:
30
+ #
31
+ .DS_Store
32
+
33
+ # For TextMate
34
+ #*.tmproj
35
+ #tmtags
36
+
37
+ # For emacs:
38
+ *~
39
+ \#*
40
+ .\#*
41
+
42
+ # For vim:
43
+ *.swp
44
+
45
+ # For redcar:
46
+ #.redcar
47
+
48
+ # For rubinius:
49
+ #*.rbc
50
+
51
+ *.gem
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ client-api-builder
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.7.1
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'http://rubygems.org'
4
+
5
+ gem 'inheritance-helper'
6
+
7
+ group :development do
8
+ gem 'rake'
9
+ gem 'rubocop'
10
+ end
11
+
12
+ group :spec do
13
+ gem 'rspec'
14
+ gem 'simplecov'
15
+ gem 'webmock'
16
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,68 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ addressable (2.7.0)
5
+ public_suffix (>= 2.0.2, < 5.0)
6
+ ast (2.4.1)
7
+ crack (0.4.5)
8
+ rexml
9
+ diff-lcs (1.4.4)
10
+ docile (1.3.2)
11
+ hashdiff (1.0.1)
12
+ inheritance-helper (0.1.5)
13
+ parallel (1.19.2)
14
+ parser (2.7.1.4)
15
+ ast (~> 2.4.1)
16
+ public_suffix (4.0.6)
17
+ rainbow (3.0.0)
18
+ rake (13.0.1)
19
+ regexp_parser (1.7.1)
20
+ rexml (3.2.4)
21
+ rspec (3.9.0)
22
+ rspec-core (~> 3.9.0)
23
+ rspec-expectations (~> 3.9.0)
24
+ rspec-mocks (~> 3.9.0)
25
+ rspec-core (3.9.2)
26
+ rspec-support (~> 3.9.3)
27
+ rspec-expectations (3.9.2)
28
+ diff-lcs (>= 1.2.0, < 2.0)
29
+ rspec-support (~> 3.9.0)
30
+ rspec-mocks (3.9.1)
31
+ diff-lcs (>= 1.2.0, < 2.0)
32
+ rspec-support (~> 3.9.0)
33
+ rspec-support (3.9.3)
34
+ rubocop (0.87.1)
35
+ parallel (~> 1.10)
36
+ parser (>= 2.7.1.1)
37
+ rainbow (>= 2.2.2, < 4.0)
38
+ regexp_parser (>= 1.7)
39
+ rexml
40
+ rubocop-ast (>= 0.1.0, < 1.0)
41
+ ruby-progressbar (~> 1.7)
42
+ unicode-display_width (>= 1.4.0, < 2.0)
43
+ rubocop-ast (0.1.0)
44
+ parser (>= 2.7.0.1)
45
+ ruby-progressbar (1.10.1)
46
+ simplecov (0.18.5)
47
+ docile (~> 1.1)
48
+ simplecov-html (~> 0.11)
49
+ simplecov-html (0.12.2)
50
+ unicode-display_width (1.7.0)
51
+ webmock (3.11.1)
52
+ addressable (>= 2.3.6)
53
+ crack (>= 0.3.2)
54
+ hashdiff (>= 0.4.0, < 2.0.0)
55
+
56
+ PLATFORMS
57
+ ruby
58
+
59
+ DEPENDENCIES
60
+ inheritance-helper
61
+ rake
62
+ rspec
63
+ rubocop
64
+ simplecov
65
+ webmock
66
+
67
+ BUNDLED WITH
68
+ 2.1.4
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 dougyouch
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # client-api-builder
2
+ Utility for creating methods to make api calls
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'client-api-builder'
5
+ s.version = '0.1.0'
6
+ s.licenses = ['MIT']
7
+ s.summary = 'Develop Client API libraries faster'
8
+ s.description = 'Utility for constructing API clients'
9
+ s.authors = ['Doug Youch']
10
+ s.email = 'dougyouch@gmail.com'
11
+ s.homepage = 'https://github.com/dougyouch/client-api-builder'
12
+ s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
13
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClientApiBuilder
4
+ class Error < StandardError; end
5
+ class UnexpectedResponse < Error
6
+ attr_reader :response
7
+
8
+ def initialize(msg, response)
9
+ super(msg)
10
+ @response = response
11
+ end
12
+ end
13
+
14
+ autoload :Router, 'client_api_builder/router'
15
+ autoload :QueryParams, 'client_api_builder/query_params'
16
+
17
+ module NetHTTP
18
+ autoload :Request, 'client_api_builder/net_http_request'
19
+ end
20
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+ require 'net/http'
3
+
4
+ module ClientApiBuilder
5
+ module NetHTTP
6
+ module Request
7
+ # Copied from https://ruby-doc.org/stdlib-2.7.1/libdoc/net/http/rdoc/Net/HTTP.html
8
+ METHOD_TO_NET_HTTP_CLASS = {
9
+ copy: Net::HTTP::Copy,
10
+ delete: Net::HTTP::Delete,
11
+ get: Net::HTTP::Get,
12
+ head: Net::HTTP::Head,
13
+ lock: Net::HTTP::Lock,
14
+ mkcol: Net::HTTP::Mkcol,
15
+ move: Net::HTTP::Move,
16
+ options: Net::HTTP::Options,
17
+ patch: Net::HTTP::Patch,
18
+ post: Net::HTTP::Post,
19
+ propfind: Net::HTTP::Propfind,
20
+ proppatch: Net::HTTP::Proppatch,
21
+ put: Net::HTTP::Put,
22
+ trace: Net::HTTP::Trace,
23
+ unlock: Net::HTTP::Unlock
24
+ }
25
+
26
+ def request(method:, uri:, body:, headers:, connection_options:)
27
+ request = METHOD_TO_NET_HTTP_CLASS[method].new(uri.request_uri, headers)
28
+ request.body = body if body
29
+
30
+ Net::HTTP.start(uri.hostname, uri.port, connection_options.merge(use_ssl: uri.scheme == 'https')) do |http|
31
+ http.request(request)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+ require 'cgi'
3
+
4
+ module ClientApiBuilder
5
+ module QueryParams
6
+ module_function
7
+
8
+ def to_query(data, namespace = nil, name_value_separator = '=', param_separator = '&')
9
+ case data
10
+ when Hash
11
+ to_query_from_hash(data, (namespace ? CGI.escape(namespace) : nil), name_value_separator).join(param_separator)
12
+ when Array
13
+ to_query_from_array(data, (namespace ? "#{CGI.escape(namespace)}[]" : '[]'), name_value_separator).join(param_separator)
14
+ else
15
+ if namespace
16
+ "#{CGI.escape(namespace)}#{name_value_separator}#{CGI.escape(data.to_s)}"
17
+ else
18
+ CGI.escape(data.to_s)
19
+ end
20
+ end
21
+ end
22
+
23
+ def to_query_from_hash(hsh, namespace, name_value_separator)
24
+ query_params = []
25
+
26
+ hsh.each do |key, value|
27
+ case value
28
+ 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)
31
+ 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)
34
+ 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)}"
37
+ end
38
+ end
39
+
40
+ query_params
41
+ end
42
+
43
+ def to_query_from_array(array, namespace, name_value_separator)
44
+ query_params = []
45
+
46
+ array.each do |value|
47
+ case value
48
+ when Hash
49
+ query_params += to_query_from_hash(value, namespace, name_value_separator)
50
+ when Array
51
+ query_params += to_query_from_array(value, "#{namespace}[]", name_value_separator)
52
+ else
53
+ query_params << "#{namespace}#{name_value_separator}#{CGI.escape(value.to_s)}"
54
+ end
55
+ end
56
+
57
+ query_params
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,268 @@
1
+ # frozen_string_literal: true
2
+ require 'inheritance-helper'
3
+
4
+ module ClientApiBuilder
5
+ module Router
6
+ def self.included(base)
7
+ base.extend InheritanceHelper::Methods
8
+ base.extend ClassMethods
9
+ base.include ::ClientApiBuilder::NetHTTP::Request
10
+ base.attr_reader :response
11
+ end
12
+
13
+ module ClassMethods
14
+ REQUIRED_BODY_HTTP_METHODS = [
15
+ :post,
16
+ :put,
17
+ :patch
18
+ ]
19
+
20
+ def default_options
21
+ {
22
+ base_url: nil,
23
+ headers: {},
24
+ connection_options: {},
25
+ query_builder: Hash.method_defined?(:to_query) ? :to_query : :query_params
26
+ }.freeze
27
+ end
28
+
29
+ def base_url(url = nil)
30
+ return default_options[:base_url] unless url
31
+
32
+ add_value_to_class_method(:default_options, base_url: url)
33
+ end
34
+
35
+ def query_builder(builder = nil)
36
+ return default_options[:query_builder] unless builder
37
+
38
+ add_value_to_class_method(:default_options, query_builder: builder)
39
+ end
40
+
41
+ def header(name, value)
42
+ headers = default_options[:headers].dup
43
+ headers[name] = value
44
+ add_value_to_class_method(:default_options, headers: headers)
45
+ end
46
+
47
+ def connection_option(name, value)
48
+ connection_options = default_options[:connection_options].dup
49
+ connection_options[name] = value
50
+ add_value_to_class_method(:default_options, connection_options: connection_options)
51
+ end
52
+
53
+ def headers
54
+ default_options[:headers]
55
+ end
56
+
57
+ def connection_options
58
+ default_options[:connection_options]
59
+ end
60
+
61
+ def build_query(query)
62
+ case query_builder
63
+ when :to_query
64
+ query.to_query
65
+ when :query_params
66
+ ClientApiBuilder::QueryParams.to_query(query)
67
+ else
68
+ query_builder.call(query)
69
+ end
70
+ end
71
+
72
+ def http_method(method_name)
73
+ case method_name.to_s
74
+ when /^(?:post|create|add|insert)/i
75
+ :post
76
+ when /^(?:put|update|modify|change)/i
77
+ :put
78
+ when /^(?:delete|remove)/i
79
+ :delete
80
+ else
81
+ :get
82
+ end
83
+ end
84
+
85
+ def requires_body?(http_method, options)
86
+ return !options[:no_body] if options.key?(:no_body)
87
+ return options[:has_body] if options.key?(:has_body)
88
+
89
+ REQUIRED_BODY_HTTP_METHODS.include?(http_method)
90
+ end
91
+
92
+ def get_hash_arguments(hsh)
93
+ arguments = []
94
+ hsh.each do |k, v|
95
+ case v
96
+ when Symbol
97
+ hsh[k] = "__||#{v}||__"
98
+ arguments << v
99
+ when Hash
100
+ arguments += get_hash_arguments(v)
101
+ when Array
102
+ arguments += get_array_arguments(v)
103
+ end
104
+ end
105
+ arguments
106
+ end
107
+
108
+ def get_array_arguments(list)
109
+ arguments = []
110
+ list.each_with_index do |v, idx|
111
+ case v
112
+ when Symbol
113
+ list[idx] = "__||#{v}||__"
114
+ arguments << v
115
+ when Hash
116
+ arguments += get_hash_arguments(v)
117
+ when Array
118
+ arguments += get_array_arguments(v)
119
+ end
120
+ end
121
+ arguments
122
+ end
123
+
124
+ def get_arguments(value)
125
+ case value
126
+ when Hash
127
+ get_hash_arguments(value)
128
+ when Array
129
+ get_array_arguments(value)
130
+ else
131
+ []
132
+ end
133
+ end
134
+
135
+ def generate_route_code(method_name, path, options = {})
136
+ http_method = options[:method] || http_method(method_name)
137
+
138
+ path_arguments = []
139
+ path.gsub!(/:([a-z0-9_]+)/i) do |_|
140
+ path_arguments << $1
141
+ "#\{#{$1}\}"
142
+ end
143
+
144
+ has_body_param = options[:body].nil? && requires_body?(http_method, options)
145
+
146
+ query =
147
+ if options[:query]
148
+ query_arguments = get_arguments(options[:query])
149
+ str = options[:query].inspect
150
+ str.gsub!(/"__\|\|(.+?)\|\|__"/) { $1 }
151
+ str
152
+ else
153
+ query_arguments = []
154
+ 'nil'
155
+ end
156
+
157
+ body =
158
+ if options[:body]
159
+ has_body_param = false
160
+ body_arguments = get_arguments(options[:body])
161
+ str = options[:body].inspect
162
+ str.gsub!(/"__\|\|(.+?)\|\|__"/) { $1 }
163
+ str
164
+ else
165
+ body_arguments = []
166
+ has_body_param ? 'body' : 'nil'
167
+ end
168
+
169
+ query_arguments.map!(&:to_s)
170
+ body_arguments.map!(&:to_s)
171
+ named_arguments = path_arguments + query_arguments + body_arguments
172
+ named_arguments.uniq!
173
+
174
+ expected_response_codes =
175
+ if options[:expected_response_codes]
176
+ options[:expected_response_codes]
177
+ elsif options[:expected_response_code]
178
+ [options[:expected_response_code]]
179
+ else
180
+ []
181
+ end
182
+ expected_response_codes.map!(&:to_s)
183
+
184
+ method_args = named_arguments.map { |arg_name| "#{arg_name}:" }
185
+ method_args += ['body:'] if has_body_param
186
+ method_args += ['**__options__', '&block']
187
+
188
+ code = "def #{method_name}(" + method_args.join(', ') + ")\n"
189
+ code += " __path__ = \"#{path}\"\n"
190
+ code += " __query__ = #{query}\n"
191
+ code += " __body__ = #{body}\n"
192
+ code += " __expected_response_codes__ = #{expected_response_codes.inspect}\n"
193
+ code += " __uri__ = build_uri(__path__, __query__, __options__)\n"
194
+ code += " __body__ = build_body(__body__, __options__)\n"
195
+ code += " __headers__ = build_headers(__options__)\n"
196
+ code += " __connection_options__ = build_connection_options(__options__)\n"
197
+ code += " @response = request(method: #{http_method.inspect}, uri: __uri__, body: __body__, headers: __headers__, connection_options: __connection_options__)\n"
198
+ code += " expected_response_code!(@response, __expected_response_codes__, __options__)\n"
199
+ code += " handle_response(@response, __options__, &block)\n"
200
+ code += "end\n"
201
+ code
202
+ end
203
+
204
+ def route(method_name, path, options = {})
205
+ self.class_eval generate_route_code(method_name, path, options), __FILE__, __LINE__
206
+ end
207
+ end
208
+
209
+ def base_url(options)
210
+ self.class.base_url
211
+ end
212
+
213
+ def build_headers(options)
214
+ if options[:headers]
215
+ self.class.headers.merge(options[:headers])
216
+ else
217
+ self.class.headers
218
+ end
219
+ end
220
+
221
+ def build_connection_options(options)
222
+ if options[:connection_options]
223
+ self.class.connection_options.merge(options[:connection_options])
224
+ else
225
+ self.class.connection_options
226
+ end
227
+ end
228
+
229
+ def build_query(query, options)
230
+ query.merge!(options[:query]) if options[:query]
231
+ self.class.build_query(query)
232
+ end
233
+
234
+ def build_body(body, options)
235
+ return unless body
236
+ return body if body.is_a?(String)
237
+
238
+ body.merge!(options[:body]) if options[:body]
239
+ body.to_json
240
+ end
241
+
242
+ def build_uri(path, query, options)
243
+ uri = URI(base_url(options) + path)
244
+ uri.query = build_query(query, options) if query
245
+ uri
246
+ end
247
+
248
+ def expected_response_code!(response, expected_response_codes, options)
249
+ return if expected_response_codes.empty? && response.kind_of?(Net::HTTPSuccess)
250
+ return if expected_response_codes.include?(response.code)
251
+
252
+ raise(::ClientApiBuilder::UnexpectedResponse.new("unexpected response code #{response.code}", response))
253
+ end
254
+
255
+ def parse_response(response, options)
256
+ response.body && JSON.parse(response.body)
257
+ end
258
+
259
+ def handle_response(response, options, &block)
260
+ data = parse_response(response, options)
261
+ if block
262
+ instance_exec(data, &block)
263
+ else
264
+ data
265
+ end
266
+ end
267
+ end
268
+ end
data/script/console ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH << File.expand_path('../lib', __dir__)
5
+ require 'client-api-builder'
6
+ require 'irb'
7
+ IRB.start(__FILE__)
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: client-api-builder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Doug Youch
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-05-05 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Utility for constructing API clients
14
+ email: dougyouch@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - ".gitignore"
20
+ - ".ruby-gemset"
21
+ - ".ruby-version"
22
+ - Gemfile
23
+ - Gemfile.lock
24
+ - LICENSE
25
+ - README.md
26
+ - client-api-builder.gemspec
27
+ - lib/client-api-builder.rb
28
+ - lib/client_api_builder/net_http_request.rb
29
+ - lib/client_api_builder/query_params.rb
30
+ - lib/client_api_builder/router.rb
31
+ - script/console
32
+ homepage: https://github.com/dougyouch/client-api-builder
33
+ licenses:
34
+ - MIT
35
+ metadata: {}
36
+ post_install_message:
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ requirements: []
51
+ rubygems_version: 3.1.2
52
+ signing_key:
53
+ specification_version: 4
54
+ summary: Develop Client API libraries faster
55
+ test_files: []