client-api-builder 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []