resteze 0.1.0 → 0.3.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/.rubocop.yml +11 -1
- data/.ruby-version +1 -0
- data/Guardfile +0 -1
- data/README.md +59 -13
- data/assets/resteze.png +0 -0
- data/assets/resteze.svg +3 -0
- data/lib/resteze/api_module.rb +25 -0
- data/lib/resteze/api_resource.rb +70 -0
- data/lib/resteze/client.rb +242 -0
- data/lib/resteze/errors.rb +38 -0
- data/lib/resteze/list.rb +43 -0
- data/lib/resteze/list_object.rb +40 -0
- data/lib/resteze/middleware/raise_error.rb +17 -0
- data/lib/resteze/object.rb +73 -0
- data/lib/resteze/request.rb +14 -0
- data/lib/resteze/response.rb +52 -0
- data/lib/resteze/save.rb +35 -0
- data/lib/resteze/util.rb +54 -0
- data/lib/resteze/version.rb +1 -1
- data/lib/resteze.rb +122 -3
- metadata +20 -5
@@ -0,0 +1,25 @@
|
|
1
|
+
module Resteze
|
2
|
+
module ApiModule
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
delegate :api_module, :logger, :util, to: :class
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def api_module
|
9
|
+
@api_module ||=
|
10
|
+
begin
|
11
|
+
parents = name.scan("::").inject([name]) { |mods, _n| mods << mods.last.deconstantize }
|
12
|
+
parents.map(&:constantize).detect { |mod| mod == Resteze || mod.include?(Resteze) }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def logger
|
17
|
+
api_module.logger
|
18
|
+
end
|
19
|
+
|
20
|
+
def util
|
21
|
+
api_module::Util
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Resteze
|
2
|
+
module ApiResource
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
include Resteze::Request
|
5
|
+
|
6
|
+
def initialize(id = nil, values: {})
|
7
|
+
super(values)
|
8
|
+
id, @retrieve_params = util.normalize_id(id)
|
9
|
+
self.id = id if self.class.property?(:id)
|
10
|
+
end
|
11
|
+
|
12
|
+
def resource_path
|
13
|
+
unless id.present?
|
14
|
+
raise api_module::InvalidRequestError.new("Could not determine which PATH to request: #{self.class} instance has " \
|
15
|
+
"invalid ID: #{id.inspect}", "id")
|
16
|
+
end
|
17
|
+
|
18
|
+
self.class.resource_path(id)
|
19
|
+
end
|
20
|
+
|
21
|
+
def retrieve_method
|
22
|
+
:get
|
23
|
+
end
|
24
|
+
|
25
|
+
def retrieve_params
|
26
|
+
@retrieve_params || {}
|
27
|
+
end
|
28
|
+
|
29
|
+
def retrieve_headers
|
30
|
+
{}
|
31
|
+
end
|
32
|
+
|
33
|
+
def refresh
|
34
|
+
resp = request(
|
35
|
+
retrieve_method,
|
36
|
+
resource_path,
|
37
|
+
params: retrieve_params,
|
38
|
+
headers: retrieve_headers
|
39
|
+
)
|
40
|
+
|
41
|
+
initialize_from(resp.data)
|
42
|
+
end
|
43
|
+
|
44
|
+
module ClassMethods
|
45
|
+
def service_path
|
46
|
+
api_module.default_service_path(self)
|
47
|
+
end
|
48
|
+
|
49
|
+
def api_version
|
50
|
+
api_module.default_api_version(self)
|
51
|
+
end
|
52
|
+
|
53
|
+
def api_path(path)
|
54
|
+
[service_path, api_version, path].join("/".freeze).squeeze("/".freeze)
|
55
|
+
end
|
56
|
+
|
57
|
+
def resource_slug
|
58
|
+
api_module.default_resource_slug(self)
|
59
|
+
end
|
60
|
+
|
61
|
+
def resource_path(id = nil)
|
62
|
+
api_path([resource_slug, id].compact.map { |part| CGI.escape(part.to_s) }.join("/".freeze))
|
63
|
+
end
|
64
|
+
|
65
|
+
def retrieve(id)
|
66
|
+
new(id).refresh
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,242 @@
|
|
1
|
+
module Resteze
|
2
|
+
class Client
|
3
|
+
include Resteze::ApiModule
|
4
|
+
|
5
|
+
def self.user_agent
|
6
|
+
[
|
7
|
+
"Ruby/#{RUBY_VERSION}",
|
8
|
+
"Faraday/#{Faraday::VERSION} (#{faraday_adapter})",
|
9
|
+
"Resteze/#{Resteze::VERSION}",
|
10
|
+
"#{api_module}/#{api_module::VERSION}"
|
11
|
+
].join(" ")
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.proxy_options
|
15
|
+
return if api_module.try(:proxy).blank?
|
16
|
+
|
17
|
+
Faraday::ProxyOptions.from(api_module.proxy)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.client_name
|
21
|
+
@client_name ||= to_s.underscore
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.active_client
|
25
|
+
Thread.current[client_name] || default_client
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.default_client
|
29
|
+
Thread.current["#{client_name}_default_client"] ||= new(default_connection)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.default_connection
|
33
|
+
Thread.current["#{client_name}_default_connection"] ||= Faraday.new do |conn|
|
34
|
+
conn.use Faraday::Request::UrlEncoded
|
35
|
+
conn.use api_module::Middleware::RaiseError
|
36
|
+
conn.adapter Faraday.default_adapter
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.faraday_adapter
|
41
|
+
@faraday_adapter ||= default_connection.builder.adapter.name.demodulize.underscore
|
42
|
+
end
|
43
|
+
|
44
|
+
# This can be overriden to customize
|
45
|
+
def self.api_url(path = "")
|
46
|
+
[api_module.api_base.chomp("/".freeze), path].join
|
47
|
+
end
|
48
|
+
|
49
|
+
attr_accessor :connection
|
50
|
+
|
51
|
+
delegate :logger,
|
52
|
+
:api_url,
|
53
|
+
:client_name,
|
54
|
+
to: :class
|
55
|
+
|
56
|
+
def initialize(connection = self.class.default_connection)
|
57
|
+
self.connection = connection
|
58
|
+
end
|
59
|
+
|
60
|
+
def request
|
61
|
+
@last_response = nil
|
62
|
+
old_client = Thread.current[client_name]
|
63
|
+
Thread.current[client_name] = self
|
64
|
+
begin
|
65
|
+
res = yield
|
66
|
+
[res, @last_response]
|
67
|
+
ensure
|
68
|
+
Thread.current[client_name] = old_client
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# TODO: Look at refactoring this if possible to improve the Abc size
|
73
|
+
# rubocop:disable Metrics/AbcSize
|
74
|
+
def execute_request(method, path, headers: {}, params: {})
|
75
|
+
params = util.objects_to_ids(params)
|
76
|
+
body, query_params = process_params(method, params)
|
77
|
+
headers = request_headers.merge(util.normalize_headers(headers))
|
78
|
+
context = request_log_context(body:, method:, path:, query_params:, headers:)
|
79
|
+
|
80
|
+
http_resp = execute_request_with_rescues(context) do
|
81
|
+
connection.run_request(method, api_url(path), body, headers) do |req|
|
82
|
+
req.options.open_timeout = api_module.open_timeout
|
83
|
+
req.options.timeout = api_module.read_timeout
|
84
|
+
req.options.proxy = self.class.proxy_options
|
85
|
+
req.params = query_params unless query_params.nil?
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
api_module::Response.from_faraday_response(http_resp).tap do |response|
|
90
|
+
@last_response = response
|
91
|
+
end
|
92
|
+
end
|
93
|
+
# rubocop:enable Metrics/AbcSize
|
94
|
+
|
95
|
+
protected
|
96
|
+
|
97
|
+
# Override to customize
|
98
|
+
def request_headers
|
99
|
+
{
|
100
|
+
"User-Agent" => self.class.user_agent,
|
101
|
+
"Accept" => "application/json",
|
102
|
+
"Content-Type" => "application/json"
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def process_params(method, params)
|
109
|
+
body = nil
|
110
|
+
query_params = nil
|
111
|
+
case method.to_s.downcase.to_sym
|
112
|
+
when :get, :head, :delete
|
113
|
+
query_params = params
|
114
|
+
else
|
115
|
+
body = params.to_json
|
116
|
+
end
|
117
|
+
[body, query_params]
|
118
|
+
end
|
119
|
+
|
120
|
+
def params_encoder
|
121
|
+
self.class.default_connection.options.params_encoder || Faraday::Utils.default_params_encoder
|
122
|
+
end
|
123
|
+
|
124
|
+
def request_log_context(method: nil, path: nil, query_params: nil, headers: nil, body: nil)
|
125
|
+
RequestLogContext.new.tap do |context|
|
126
|
+
context.method = method
|
127
|
+
context.path = path
|
128
|
+
context.query_params = params_encoder.encode(query_params)
|
129
|
+
context.headers = headers
|
130
|
+
context.body = body
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def execute_request_with_rescues(context)
|
135
|
+
begin
|
136
|
+
request_start = Time.now
|
137
|
+
log_request(context)
|
138
|
+
resp = yield
|
139
|
+
context = context.dup_from_response(resp)
|
140
|
+
log_response(context, request_start, resp)
|
141
|
+
rescue StandardError => e
|
142
|
+
execute_request_rescue_log(e, context, request_start)
|
143
|
+
raise e
|
144
|
+
end
|
145
|
+
|
146
|
+
resp
|
147
|
+
end
|
148
|
+
|
149
|
+
def execute_request_rescue_log(err, context, request_start)
|
150
|
+
if err.respond_to?(:response) && err.response
|
151
|
+
error_context = context.dup_from_response(err.response)
|
152
|
+
log_response(error_context, request_start, err.response)
|
153
|
+
else
|
154
|
+
log_response_error(context, request_start, err)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def log_request(context)
|
159
|
+
logger.info do
|
160
|
+
payload = {
|
161
|
+
method: context.method,
|
162
|
+
path: context.path
|
163
|
+
}
|
164
|
+
"#{self.class} API Request: #{payload}"
|
165
|
+
end
|
166
|
+
logger.debug { request_details_in_http_syntax(context) }
|
167
|
+
end
|
168
|
+
|
169
|
+
def log_response(context, request_start, response)
|
170
|
+
status = response.respond_to?(:status) ? response.status : response[:status]
|
171
|
+
logger.debug { response_details_in_http_syntax(response) }
|
172
|
+
logger.info do
|
173
|
+
payload = {
|
174
|
+
elapsed: Time.now - request_start,
|
175
|
+
method: context.method,
|
176
|
+
path: context.path,
|
177
|
+
status:
|
178
|
+
}
|
179
|
+
"#{self.class} API Response: #{payload}"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def log_response_error(context, request_start, err)
|
184
|
+
logger.error do
|
185
|
+
payload = {
|
186
|
+
elapsed: Time.now - request_start,
|
187
|
+
error_message: err.message,
|
188
|
+
method: context.method,
|
189
|
+
path: context.path
|
190
|
+
}
|
191
|
+
"#{self.class} Request Error: #{payload}"
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def request_details_in_http_syntax(context)
|
196
|
+
"#{self.class} Full Request:\n\n".tap do |s|
|
197
|
+
s << request_in_http_syntax(context)
|
198
|
+
s << headers_in_http_syntax(context.headers)
|
199
|
+
s << "\n\n#{context.body}\n" if context.body.present?
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# TODO: Look into refactoring to improve Abc Size
|
204
|
+
# rubocop:disable Metrics/AbcSize
|
205
|
+
def response_details_in_http_syntax(response)
|
206
|
+
status = response.respond_to?(:status) ? response.status : response[:status]
|
207
|
+
body = response.respond_to?(:body) ? response.body : response[:body]
|
208
|
+
headers = response.respond_to?(:headers) ? response.headers : response[:headers]
|
209
|
+
"#{self.class} Full Response:\n\n".tap do |s|
|
210
|
+
s << "HTTP/1.1 #{status}\n"
|
211
|
+
s << headers_in_http_syntax(headers)
|
212
|
+
s << "\n\n"
|
213
|
+
s << (body.encoding == Encoding::ASCII_8BIT ? "(Binary Response)" : body)
|
214
|
+
s << "\n"
|
215
|
+
end
|
216
|
+
end
|
217
|
+
# rubocop:enable Metrics/AbcSize
|
218
|
+
|
219
|
+
def request_in_http_syntax(context)
|
220
|
+
method = context.method.to_s.upcase
|
221
|
+
method.tap do |s|
|
222
|
+
s << " "
|
223
|
+
s << [self.class.api_url(context.path), context.query_params].select(&:present?).join("?")
|
224
|
+
s << "\n"
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def headers_in_http_syntax(headers)
|
229
|
+
headers.map { |k, v| [k, v].join(": ") }.join("\n")
|
230
|
+
end
|
231
|
+
|
232
|
+
class RequestLogContext
|
233
|
+
attr_accessor :body, :method, :path, :query_params, :headers
|
234
|
+
|
235
|
+
def dup_from_response(resp)
|
236
|
+
return self if resp.nil?
|
237
|
+
|
238
|
+
dup
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Resteze
|
2
|
+
class Error < StandardError
|
3
|
+
attr_accessor :response
|
4
|
+
attr_reader :message, :code, :http_body, :http_headers, :http_status, :json_body
|
5
|
+
|
6
|
+
# def initialize(message = nil, http_status: nil, http_body: nil, json_body: nil,
|
7
|
+
# http_headers: nil, code: nil)
|
8
|
+
def initialize(message = nil, **kwargs)
|
9
|
+
super(**kwargs)
|
10
|
+
@message = message
|
11
|
+
@http_status = kwargs[:http_status]
|
12
|
+
@http_body = kwargs[:http_body]
|
13
|
+
@http_headers = kwargs.fetch(:http_headers, {})
|
14
|
+
@json_body = kwargs[:json_body]
|
15
|
+
@code = kwargs[:code]
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
status = "HTTP #{@http_status}:" if @http_status.present?
|
20
|
+
[status, message].compact.join(" ")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class InvalidRequestError < Error
|
25
|
+
attr_accessor :param
|
26
|
+
|
27
|
+
def initialize(message, param, **keyword_args)
|
28
|
+
super(message, **keyword_args)
|
29
|
+
@param = param
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class AuthenticationError < Error; end
|
34
|
+
|
35
|
+
class ApiConnectionError < Error; end
|
36
|
+
|
37
|
+
class ApiError < Error; end
|
38
|
+
end
|
data/lib/resteze/list.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
module Resteze
|
2
|
+
module List
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
include ApiModule
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def list(params: {})
|
8
|
+
resp = request(list_method, list_resource_path(params), params: list_params(params))
|
9
|
+
construct_list_from(resp.data)
|
10
|
+
end
|
11
|
+
|
12
|
+
def construct_list_from(payload)
|
13
|
+
values = payload.deep_symbolize_keys
|
14
|
+
api_module::ListObject.construct_from(values, self)
|
15
|
+
end
|
16
|
+
|
17
|
+
def construct_empty_list
|
18
|
+
payload = { list_key => [] }
|
19
|
+
construct_list_from(payload)
|
20
|
+
end
|
21
|
+
|
22
|
+
def list_params(params = {})
|
23
|
+
params
|
24
|
+
end
|
25
|
+
|
26
|
+
def list_headers(_params = {})
|
27
|
+
{}
|
28
|
+
end
|
29
|
+
|
30
|
+
def list_resource_path(_params = {})
|
31
|
+
resource_path
|
32
|
+
end
|
33
|
+
|
34
|
+
def list_key
|
35
|
+
api_module.default_list_key(self)
|
36
|
+
end
|
37
|
+
|
38
|
+
def list_method
|
39
|
+
:get
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Resteze
|
2
|
+
module ListObject
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
def metadata
|
6
|
+
@metadata ||= {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize_from(values, metadata: {})
|
10
|
+
@metadata = metadata
|
11
|
+
values.each { |v| self << v }
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
def as_list_json(list_key)
|
16
|
+
metadata.as_json.merge({ list_key => as_json })
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
def object_key
|
21
|
+
:_list
|
22
|
+
end
|
23
|
+
|
24
|
+
def construct_from(payload, klass)
|
25
|
+
list_key = klass.list_key
|
26
|
+
payload = payload.deep_symbolize_keys
|
27
|
+
values = util.convert_to_object(payload[list_key], klass)
|
28
|
+
metadata = payload.except(list_key)
|
29
|
+
|
30
|
+
new.initialize_from(values, metadata:)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def populate_metadata(values)
|
37
|
+
@metadata = metadata.merge(values.except(self.class.object_key) || {})
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Resteze
|
2
|
+
module Middleware
|
3
|
+
class RaiseError < Faraday::Response::RaiseError
|
4
|
+
include Resteze::ApiModule
|
5
|
+
|
6
|
+
def on_complete(env)
|
7
|
+
super
|
8
|
+
rescue Faraday::ConflictError => e
|
9
|
+
raise api_module::ConflictError, e
|
10
|
+
rescue Faraday::UnprocessableEntityError => e
|
11
|
+
raise api_module::UnprocessableEntityError, e
|
12
|
+
rescue Faraday::ResourceNotFound => e
|
13
|
+
raise api_module::ResourceNotFound, e
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Resteze
|
2
|
+
class Object < Hashie::Trash
|
3
|
+
include Resteze::ApiModule
|
4
|
+
include Hashie::Extensions::DeepMerge
|
5
|
+
|
6
|
+
attr_reader :resteze_metadata, :property_bag
|
7
|
+
|
8
|
+
def initialize(attributes = {}, &)
|
9
|
+
@resteze_metadata = {}
|
10
|
+
@property_bag = {}
|
11
|
+
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
# This allows us to take advantage of the #property features of
|
16
|
+
# the Hashie::Dash, but also to support unexpected hash values
|
17
|
+
# and store them in the metadata property
|
18
|
+
def []=(property, value)
|
19
|
+
super
|
20
|
+
rescue NoMethodError
|
21
|
+
@property_bag = property_bag.merge({ property => value })
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.class_name
|
25
|
+
name.demodulize
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.object_key
|
29
|
+
api_module.default_object_key(self)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.construct_from(payload)
|
33
|
+
new.initialize_from(payload)
|
34
|
+
end
|
35
|
+
|
36
|
+
# This is replaced with the idea of Hashie in Channel Advisor
|
37
|
+
def initialize_from(values)
|
38
|
+
values = values.deep_symbolize_keys
|
39
|
+
if self.class.object_key
|
40
|
+
update_attributes(values[self.class.object_key] || {})
|
41
|
+
else
|
42
|
+
update_attributes(values)
|
43
|
+
end
|
44
|
+
populate_metadata(values)
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
def merge_from(values)
|
49
|
+
values = values.deep_symbolize_keys
|
50
|
+
if self.class.object_key
|
51
|
+
data = values[self.class.object_key] || {}
|
52
|
+
metadata = values.except(self.class.object_key) || {}
|
53
|
+
else
|
54
|
+
data = values
|
55
|
+
metadata = {}
|
56
|
+
end
|
57
|
+
|
58
|
+
deep_merge!(data)
|
59
|
+
@resteze_metadata.deep_merge!(metadata)
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def persisted?
|
64
|
+
respond_to?(:id) && id.present?
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def populate_metadata(values)
|
70
|
+
@resteze_metadata = values.except(self.class.object_key)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Resteze
|
2
|
+
module Request
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
include Resteze::ApiModule
|
5
|
+
|
6
|
+
delegate :request, to: :class
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def request(method, url, params: {}, headers: {})
|
10
|
+
api_module::Client.active_client.execute_request(method, url, params:, headers:)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Resteze
|
2
|
+
class Response
|
3
|
+
OK = 200
|
4
|
+
attr_accessor :data, :http_body, :http_headers, :http_status, :request_id
|
5
|
+
|
6
|
+
def self.from_faraday_response(faraday_response)
|
7
|
+
new.tap do |response|
|
8
|
+
response.http_body = faraday_response.body
|
9
|
+
response.http_headers = faraday_response.headers
|
10
|
+
response.http_status = faraday_response.status
|
11
|
+
response.request_id = faraday_response.headers["Request-Id"]
|
12
|
+
|
13
|
+
response.data = parse_body(
|
14
|
+
faraday_response.body,
|
15
|
+
status: response.http_status,
|
16
|
+
headers: response.http_headers
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.batch_response?(headers)
|
22
|
+
headers["Content-Type".freeze].to_s.include?("boundary=batchresponse".freeze)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.parse_body(body, status: nil, headers: {})
|
26
|
+
return body if unparsable_body?(status:, headers:)
|
27
|
+
|
28
|
+
type = mime_type(headers)&.symbol || :json
|
29
|
+
case type
|
30
|
+
when :json
|
31
|
+
JSON.parse(body, symbolize_names: true)
|
32
|
+
when :xml
|
33
|
+
Hash.from_xml(body).deep_symbolize_keys
|
34
|
+
else
|
35
|
+
body
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.unparsable_body?(status:, headers:)
|
40
|
+
status.to_i == 204 || (300..399).cover?(status.to_i) || batch_response?(headers)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.mime_type(headers = {})
|
44
|
+
mime_type = headers.transform_keys(&:downcase)["content-type"].to_s.split(";").first.to_s.strip
|
45
|
+
Mime::LOOKUP[mime_type] || Mime::Type.lookup_by_extension(:json)
|
46
|
+
end
|
47
|
+
|
48
|
+
def ok?
|
49
|
+
http_status == OK
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/resteze/save.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
module Resteze
|
2
|
+
module Save
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
def save
|
6
|
+
resp = request(
|
7
|
+
save_method,
|
8
|
+
save_resource_path,
|
9
|
+
params: as_save_json,
|
10
|
+
headers: save_headers
|
11
|
+
)
|
12
|
+
process_save_response(resp)
|
13
|
+
end
|
14
|
+
|
15
|
+
def save_method
|
16
|
+
persisted? ? :put : :post
|
17
|
+
end
|
18
|
+
|
19
|
+
def save_resource_path
|
20
|
+
persisted? ? resource_path : self.class.resource_path
|
21
|
+
end
|
22
|
+
|
23
|
+
def as_save_json
|
24
|
+
as_json
|
25
|
+
end
|
26
|
+
|
27
|
+
def save_headers
|
28
|
+
respond_to?(:retrieve_headers) ? retrieve_headers : {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def process_save_response(response)
|
32
|
+
merge_from(response.data)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/resteze/util.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
module Resteze
|
2
|
+
module Util
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def convert_to_object(data, object_class)
|
7
|
+
case data
|
8
|
+
when Array
|
9
|
+
data.map { |i| convert_to_object(i, object_class) }
|
10
|
+
when Hash
|
11
|
+
object_class.construct_from(object_class.object_key.present? ? { object_class.object_key => data } : data)
|
12
|
+
else
|
13
|
+
data
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def normalize_headers(headers)
|
18
|
+
headers.transform_keys { |key| capitalize_parts(key) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def normalize_id(id)
|
22
|
+
if id.is_a?(Hash)
|
23
|
+
params_hash = id.dup
|
24
|
+
id = params_hash.delete(:id)
|
25
|
+
else
|
26
|
+
params_hash = {}
|
27
|
+
end
|
28
|
+
|
29
|
+
[id.to_s.presence, params_hash]
|
30
|
+
end
|
31
|
+
|
32
|
+
def objects_to_ids(obj)
|
33
|
+
case obj
|
34
|
+
when ApiResource
|
35
|
+
obj.id
|
36
|
+
when Hash
|
37
|
+
res = {}
|
38
|
+
obj.each { |k, v| res[k] = objects_to_ids(v) unless v.nil? }
|
39
|
+
res
|
40
|
+
when Array
|
41
|
+
obj.map { |v| objects_to_ids(v) }
|
42
|
+
else
|
43
|
+
obj
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def capitalize_parts(str)
|
48
|
+
str.to_s.dasherize.split("-").reject(&:blank?).map(&:capitalize).join("-")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
extend ClassMethods
|
53
|
+
end
|
54
|
+
end
|