model-api 0.8.3
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 +7 -0
- data/README.md +2 -0
- data/lib/model-api.rb +38 -0
- data/lib/model-api/base_controller.rb +1352 -0
- data/lib/model-api/bypass_parse_middleware.rb +32 -0
- data/lib/model-api/hash_metadata.rb +48 -0
- data/lib/model-api/model.rb +49 -0
- data/lib/model-api/not_found_exception.rb +10 -0
- data/lib/model-api/open_api_extensions.rb +287 -0
- data/lib/model-api/renderer.rb +504 -0
- data/lib/model-api/simple_metadata.rb +33 -0
- data/lib/model-api/suppress_login_redirect_middleware.rb +38 -0
- data/lib/model-api/unauthorized_exception.rb +4 -0
- data/lib/model-api/utils.rb +392 -0
- data/model-api.gemspec +24 -0
- metadata +114 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
module ModelApi
|
2
|
+
class BypassParseMiddleware
|
3
|
+
def initialize(app)
|
4
|
+
@app = app
|
5
|
+
@api_roots = nil
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
request = ActionDispatch::Request.new(env)
|
10
|
+
unless @api_roots.present?
|
11
|
+
options = Rails.application.config.class.class_variable_get(:@@options)
|
12
|
+
options ||= {}
|
13
|
+
@api_roots = options[:api_middleware_root_paths] || ['api']
|
14
|
+
@api_roots = [@api_roots] unless @api_roots.is_a?(Array)
|
15
|
+
@api_roots = @api_roots.map { |path| path.starts_with?('/') ? path : "/#{path}" }
|
16
|
+
end
|
17
|
+
@api_roots.each do |path|
|
18
|
+
next unless env['REQUEST_PATH'].to_s.starts_with?(path)
|
19
|
+
api_format = nil
|
20
|
+
if request.content_type.to_s.downcase.ends_with?('json')
|
21
|
+
api_format = :json
|
22
|
+
elsif request.content_type.to_s.downcase.ends_with?('xml')
|
23
|
+
api_format = :xml
|
24
|
+
end
|
25
|
+
env['action_dispatch.request.content_type'] = 'application/x-api'
|
26
|
+
env['API_CONTENT_TYPE'] = api_format
|
27
|
+
break
|
28
|
+
end
|
29
|
+
@app.call(env)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module ModelApi
|
2
|
+
class HashMetadata
|
3
|
+
class << self
|
4
|
+
def process_metadata(type, obj, args)
|
5
|
+
type = type.to_sym
|
6
|
+
instance_var = :"@api_#{type}_metadata"
|
7
|
+
metadata = obj.instance_variable_get(instance_var) || {}
|
8
|
+
if args.present?
|
9
|
+
if args.size == 1 && args[0].is_a?(Hash)
|
10
|
+
new_metadata = args[0].symbolize_keys
|
11
|
+
elsif args.size == 1 && args[0].is_a?(Array)
|
12
|
+
new_metadata = Hash[args[0].map { |key| [key.to_sym, {}] }]
|
13
|
+
else
|
14
|
+
new_metadata = Hash[args.map { |key| [key.to_sym, {}] }]
|
15
|
+
end
|
16
|
+
new_metadata.symbolize_keys.each do |key, item_metadata|
|
17
|
+
if (existing_item_metadata = metadata[key]).is_a?(Hash)
|
18
|
+
existing_item_metadata.merge!(item_metadata)
|
19
|
+
else
|
20
|
+
item_metadata[:key] = key
|
21
|
+
metadata[key] = item_metadata
|
22
|
+
end
|
23
|
+
end
|
24
|
+
obj.instance_variable_set(instance_var, metadata)
|
25
|
+
end
|
26
|
+
metadata.dup
|
27
|
+
end
|
28
|
+
|
29
|
+
def merge_superclass_metadata(type, sc, metadata)
|
30
|
+
metadata_method = :"api_#{type}"
|
31
|
+
return metadata if sc == ActiveRecord::Base || !sc.respond_to?(metadata_method)
|
32
|
+
superclass_metadata = sc.send(metadata_method)
|
33
|
+
merged_metadata = {}
|
34
|
+
superclass_metadata.each do |item, item_metadata|
|
35
|
+
merged_metadata[item] = item_metadata.dup
|
36
|
+
end
|
37
|
+
metadata.each do |key, item_metadata|
|
38
|
+
if (existing_item_metadata = merged_metadata[key]).is_a?(Hash)
|
39
|
+
existing_item_metadata.merge!(item_metadata)
|
40
|
+
else
|
41
|
+
merged_metadata[key] = item_metadata
|
42
|
+
end
|
43
|
+
end
|
44
|
+
merged_metadata
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module ModelApi
|
2
|
+
module Model
|
3
|
+
module ClassMethods
|
4
|
+
def api_model(*args)
|
5
|
+
metadata = ModelApi::SimpleMetadata.process_metadata(:model, self, args)
|
6
|
+
ModelApi::SimpleMetadata.merge_superclass_metadata(:model, superclass, metadata,
|
7
|
+
exclude_keys: [:alias])
|
8
|
+
end
|
9
|
+
|
10
|
+
def api_attributes(*args)
|
11
|
+
metadata = ModelApi::HashMetadata.process_metadata(:attributes, self, args)
|
12
|
+
metadata = ModelApi::HashMetadata.merge_superclass_metadata(:attributes, superclass, metadata)
|
13
|
+
if args.present?
|
14
|
+
id_attrs = []
|
15
|
+
metadata.each { |attr, attr_metadata| id_attrs << attr if attr_metadata[:id] }
|
16
|
+
if id_attrs.present?
|
17
|
+
id_attrs = id_attrs.map(&:to_sym)
|
18
|
+
existing_id_attrs = (api_model[:id_attributes] || []).map(&:to_sym)
|
19
|
+
if (id_attrs - existing_id_attrs).present?
|
20
|
+
api_model id_attributes: (id_attrs - existing_id_attrs).uniq
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
if self < ActiveRecord::Base && (args.present? ||
|
25
|
+
!self.instance_variable_get(:@api_attrs_characterized))
|
26
|
+
metadata.each do |attr, attr_metadata|
|
27
|
+
if (assoc = self.reflect_on_association(attr)).present?
|
28
|
+
attr_metadata[:type] = :association
|
29
|
+
attr_metadata[:association] = assoc
|
30
|
+
else
|
31
|
+
attr_metadata[:type] = :attribute
|
32
|
+
end
|
33
|
+
end
|
34
|
+
self.instance_variable_set(:@api_attrs_characterized, true)
|
35
|
+
end
|
36
|
+
metadata
|
37
|
+
end
|
38
|
+
|
39
|
+
def api_links(*args)
|
40
|
+
metadata = ModelApi::HashMetadata.process_metadata(:links, self, args)
|
41
|
+
ModelApi::HashMetadata.merge_superclass_metadata(:links, superclass, metadata)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.included(base)
|
46
|
+
base.extend(ClassMethods)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,287 @@
|
|
1
|
+
module ModelApi
|
2
|
+
module OpenApiExtensions
|
3
|
+
module ClassMethods
|
4
|
+
def add_open_api_action(action, operation, opts = {})
|
5
|
+
return unless respond_to?(:open_api_action) # Must have open_api gem installed
|
6
|
+
opts = opts.merge(action: action, operation: operation)
|
7
|
+
if ENV['ADMIN'].present? && ENV['ADMIN'].to_s != '0'
|
8
|
+
opts[:admin_content] = true
|
9
|
+
elsif opts[:admin_only]
|
10
|
+
open_api_action action, hidden: :true
|
11
|
+
return
|
12
|
+
end
|
13
|
+
open_api_spec = {}
|
14
|
+
open_api_spec[:description] = opts[:description] if opts.include?(:description)
|
15
|
+
response_class = opts[:response] || model_class
|
16
|
+
if operation == :index || opts[:collection]
|
17
|
+
response = Utils.define_api_collection_response(self, response_class, opts)
|
18
|
+
open_api_spec[:query_string] = Utils.filter_and_sort_params(self, response_class, opts)
|
19
|
+
else
|
20
|
+
response = Utils.define_api_response(self, response_class,
|
21
|
+
opts.merge(operation: :show))
|
22
|
+
end
|
23
|
+
open_api_spec[:responses] = { 200 => { schema: response } } if response.present?
|
24
|
+
if [:create, :update, :patch].include?(operation)
|
25
|
+
payload = opts[:payload] || model_class
|
26
|
+
if payload.present?
|
27
|
+
open_api_spec[:body] = { description: 'Payload', schema: Utils.define_open_api_object(
|
28
|
+
self, payload, opts.merge(object_context: :payload)) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
open_api_action action, open_api_spec
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.included(base)
|
36
|
+
base.extend(ClassMethods)
|
37
|
+
end
|
38
|
+
|
39
|
+
class Utils
|
40
|
+
class << self
|
41
|
+
def define_api_response(controller_class, model_class, opts = {})
|
42
|
+
object_name = define_open_api_object(controller_class, model_class, opts)
|
43
|
+
return nil unless object_name.present?
|
44
|
+
wrapper_object_name = "#{object_name}:response"
|
45
|
+
response_object_metadata = { type: :object, required: true }
|
46
|
+
response_object_metadata[:'$ref'] = object_name if object_name.present?
|
47
|
+
controller_class.send(:open_api_object, wrapper_object_name,
|
48
|
+
successful: { type: :boolean, required: true,
|
49
|
+
description: 'Returns true if successfully processed; otherwise false' },
|
50
|
+
status: { type: :string, required: true, description: 'HTTP status' },
|
51
|
+
statusCode: { type: :integer, required: true,
|
52
|
+
description: 'Numeric HTTP status code' },
|
53
|
+
ModelApi::Utils.ext_attr(opts[:root] || ModelApi::Utils.model_name(model_class).singular) =>
|
54
|
+
response_object_metadata)
|
55
|
+
wrapper_object_name
|
56
|
+
end
|
57
|
+
|
58
|
+
def filter_and_sort_params(controller_class, model_class, opts = {})
|
59
|
+
params = opts[:parameters] || {}
|
60
|
+
opts = opts.merge(attr_types: attr_types_from_columns(model_class))
|
61
|
+
attr_prefix = opts[:attr_prefix]
|
62
|
+
sort_attrs = []
|
63
|
+
|
64
|
+
filter_metadata = ModelApi::Utils.filtered_ext_attrs(model_class, :filter, opts)
|
65
|
+
filter_metadata.each do |attr, attr_metadata|
|
66
|
+
if attr_metadata[:type] == :association
|
67
|
+
next unless (assoc = attr_metadata[:association]).present? &&
|
68
|
+
assoc.respond_to?(:klass)
|
69
|
+
assoc_params = filter_and_sort_params(controller_class, assoc.klass,
|
70
|
+
opts.merge(attr_prefix: "#{attr}."))
|
71
|
+
sort_attrs += assoc_params.delete(:sort_by) || []
|
72
|
+
assoc_params.each { |assoc_attr, property_hash| params[assoc_attr] ||= property_hash }
|
73
|
+
else
|
74
|
+
property_hash = open_api_attr_hash(controller_class, model_class, attr, attr_metadata,
|
75
|
+
opts)
|
76
|
+
property_hash.merge!(
|
77
|
+
description: filter_description(attr_prefix, attr, property_hash, attr_metadata),
|
78
|
+
required: false)
|
79
|
+
next if property_hash.include?(:'$ref')
|
80
|
+
params[:"#{attr_prefix}#{attr}"] ||= property_hash
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
add_sort_params(params, model_class, sort_attrs, opts)
|
85
|
+
end
|
86
|
+
|
87
|
+
def add_sort_params(params, model_class, addl_sort_attrs, opts)
|
88
|
+
attr_prefix = opts[:attr_prefix]
|
89
|
+
if (sort_metadata = ModelApi::Utils.filtered_ext_attrs(model_class, :sort, opts)).present?
|
90
|
+
if attr_prefix.present?
|
91
|
+
params[:sort_by] = sort_metadata.keys.sort.map { |k| :"#{attr_prefix}#{k}" }
|
92
|
+
else
|
93
|
+
addl_sort_attrs = (sort_metadata.keys + addl_sort_attrs).compact.sort
|
94
|
+
params[:sort_by] = { type: :string,
|
95
|
+
description: "Sortable fields: #{addl_sort_attrs.join(', ')} " \
|
96
|
+
'(optionally append " asc" or " desc" on field(s) to indicate sort order)' }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
params
|
100
|
+
end
|
101
|
+
|
102
|
+
def filter_description(attr_prefix, attr, property_hash, attr_metadata)
|
103
|
+
return attr_metadata[:filter_description] if attr_metadata[:filter_description].present?
|
104
|
+
desc = "Filter by #{attr_prefix}#{attr}"
|
105
|
+
case property_hash[:type]
|
106
|
+
when :string
|
107
|
+
case property_hash[:format]
|
108
|
+
when :date, :'date-time'
|
109
|
+
"#{desc} (supports <, <=, !=, >= > operator prefixes, comma-delimited criteria)"
|
110
|
+
else
|
111
|
+
"#{desc} (supports comma-delimited values)"
|
112
|
+
end
|
113
|
+
when :boolean
|
114
|
+
"#{desc} (must be true or false)"
|
115
|
+
when :integer, :number
|
116
|
+
"#{desc} (supports comma-delimited values, <, <=, !=, >= > operator prefixes)"
|
117
|
+
else
|
118
|
+
desc
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def define_api_collection_response(controller_class, model_class, opts = {})
|
123
|
+
object_name = define_open_api_object(controller_class, model_class, opts)
|
124
|
+
return nil unless object_name.present?
|
125
|
+
wrapper_object_name = "#{object_name}:response"
|
126
|
+
array_metadata = { type: :array, required: true }
|
127
|
+
array_metadata[:'$ref'] = object_name if object_name.present?
|
128
|
+
controller_class.send(:open_api_object, wrapper_object_name,
|
129
|
+
successful: { type: :boolean, required: true,
|
130
|
+
description: 'Returns true if successfully processed; otherwise false' },
|
131
|
+
status: { type: :string, description: 'HTTP status', required: true },
|
132
|
+
statusCode: { type: :integer, description: 'Numeric HTTP status code',
|
133
|
+
required: true },
|
134
|
+
ModelApi::Utils.model_name(model_class).plural => array_metadata,
|
135
|
+
count: { type: :integer, description: 'Total items available', required: true },
|
136
|
+
page: { type: :integer, description: 'Index (1-based) of page returned',
|
137
|
+
required: true },
|
138
|
+
pageCount: { type: :integer, description: 'Total number of pages available',
|
139
|
+
required: true },
|
140
|
+
pageSize: { type: :integer, description: 'Maximum item count per page returned',
|
141
|
+
required: true })
|
142
|
+
wrapper_object_name
|
143
|
+
end
|
144
|
+
|
145
|
+
def define_open_api_object(controller_class, define_open_api_object, opts = {})
|
146
|
+
define_open_api_objects(controller_class, define_open_api_object, opts).first
|
147
|
+
end
|
148
|
+
|
149
|
+
def define_open_api_objects(controller_class, *define_open_api_objects)
|
150
|
+
if define_open_api_objects.size > 1 && (opts = define_open_api_objects.last).is_a?(Hash)
|
151
|
+
define_open_api_objects = define_open_api_objects[0..-2]
|
152
|
+
else
|
153
|
+
opts = {}
|
154
|
+
end
|
155
|
+
object_names = []
|
156
|
+
define_open_api_objects.compact.uniq.each do |model_class|
|
157
|
+
object_names << define_open_api_object_from_model(controller_class, model_class, opts)
|
158
|
+
end
|
159
|
+
object_names.compact
|
160
|
+
end
|
161
|
+
|
162
|
+
def define_open_api_object_from_model(controller_class, model_class, opts = {})
|
163
|
+
operation = opts[:operation] || :show
|
164
|
+
metadata_opts = opts.merge(ModelApi::Utils.contextual_metadata_opts(opts))
|
165
|
+
metadata = ModelApi::Utils.filtered_attrs(model_class, operation, metadata_opts)
|
166
|
+
return nil unless metadata.present?
|
167
|
+
|
168
|
+
action = (opts = opts.dup).delete(:action) # Prevent inheritance
|
169
|
+
|
170
|
+
object_name = ModelApi::Utils.model_name(model_class).name
|
171
|
+
if (parent_object_context = opts[:object_context]).present?
|
172
|
+
object_name = "#{parent_object_context}|#{object_name}"
|
173
|
+
end
|
174
|
+
object_name = "#{object_name}|#{action}" if action.present?
|
175
|
+
|
176
|
+
@open_api_views_processed ||= {}
|
177
|
+
return object_name if @open_api_views_processed[object_name]
|
178
|
+
@open_api_views_processed[object_name] = model_class
|
179
|
+
|
180
|
+
class_model_base(metadata, controller_class, model_class,
|
181
|
+
opts.merge(object_context: object_name))
|
182
|
+
object_name
|
183
|
+
end
|
184
|
+
|
185
|
+
def class_model_base(metadata, controller_class, model_class, opts)
|
186
|
+
properties = {}
|
187
|
+
opts = opts.merge(attr_types: attr_types_from_columns(model_class))
|
188
|
+
metadata.each do |attr, attr_metadata|
|
189
|
+
properties[ModelApi::Utils.ext_attr(attr, attr_metadata)] = open_api_attr_hash(
|
190
|
+
controller_class, model_class, attr, attr_metadata, opts)
|
191
|
+
end
|
192
|
+
controller_class.send(:open_api_object, opts[:object_context].to_sym, properties)
|
193
|
+
end
|
194
|
+
|
195
|
+
# rubocop:disable Metrics/ParameterLists
|
196
|
+
def open_api_attr_hash(controller_class, model_class, attr, attr_metadata, opts)
|
197
|
+
property_hash = class_model_base_property(model_class, attr, attr_metadata)
|
198
|
+
if (attr_type = attr_metadata[:type]).is_a?(Symbol) &&
|
199
|
+
![:attribute, :association].include?(attr_type)
|
200
|
+
ModelApi::Utils.set_open_api_type_and_format(property_hash, attr_type)
|
201
|
+
else
|
202
|
+
attr_types = opts[:attr_types] || attr_types_from_columns(model_class)
|
203
|
+
if (attr_type = attr_types[attr_metadata[:key]]).present?
|
204
|
+
ModelApi::Utils.set_open_api_type_and_format(property_hash, attr_type)
|
205
|
+
elsif (assoc = model_class.reflect_on_association(attr)).present?
|
206
|
+
property_hash = class_model_assoc_property(property_hash, controller_class,
|
207
|
+
attr_metadata, opts.merge(association: assoc))
|
208
|
+
end
|
209
|
+
end
|
210
|
+
property_hash[:type] ||= :string unless property_hash.include?(:'$ref')
|
211
|
+
property_hash
|
212
|
+
end
|
213
|
+
|
214
|
+
# rubocop:enable Metrics/ParameterLists
|
215
|
+
|
216
|
+
def class_model_base_property(model_class, attr, attr_metadata)
|
217
|
+
if (required = attr_metadata[:required]).nil?
|
218
|
+
required_attrs ||= required_attrs_from_validators(model_class)
|
219
|
+
required = required_attrs.include?(attr)
|
220
|
+
end
|
221
|
+
property_hash = { required: required ? true : false }
|
222
|
+
if (description = attr_metadata[:description]).present?
|
223
|
+
property_hash[:description] = description
|
224
|
+
end
|
225
|
+
property_hash
|
226
|
+
end
|
227
|
+
|
228
|
+
def class_model_assoc_property(property_hash, controller_class, attr_metadata, opts)
|
229
|
+
assoc = opts[:association]
|
230
|
+
return property_hash unless assoc.present?
|
231
|
+
assoc_class = assoc.class_name.constantize
|
232
|
+
assoc_opts = ModelApi::Utils.assoc_opts(assoc, attr_metadata, opts)
|
233
|
+
assoc_opts = assoc_opts.reject do |k, _v|
|
234
|
+
[:result, :collection_result].include?(k)
|
235
|
+
end
|
236
|
+
assoc_model = define_open_api_object(controller_class, assoc_class, assoc_opts)
|
237
|
+
if assoc.collection?
|
238
|
+
property_hash[:type] = :array
|
239
|
+
property_hash[:items] = { :'$ref' => assoc_model } if assoc_model.present?
|
240
|
+
else
|
241
|
+
property_hash[:'$ref'] = assoc_model if assoc_model.present?
|
242
|
+
end
|
243
|
+
property_hash
|
244
|
+
end
|
245
|
+
|
246
|
+
def attr_types_from_columns(model_class)
|
247
|
+
Hash[model_class.columns.map { |col| [col.name.to_sym, col.type] }]
|
248
|
+
end
|
249
|
+
|
250
|
+
def required_attrs_from_validators(model_class)
|
251
|
+
model_class.validators
|
252
|
+
.select { |v| v.is_a?(ActiveRecord::Validations::PresenceValidator) }
|
253
|
+
.map(&:attributes)
|
254
|
+
.flatten
|
255
|
+
end
|
256
|
+
|
257
|
+
# def add_open_api_view_params(parameters, operation, _opts = {})
|
258
|
+
# parameters = {}
|
259
|
+
# if operation == :index
|
260
|
+
# api.param :query, :page, :integer, :optional,
|
261
|
+
# 'Index (1-based) of the result set page to return'
|
262
|
+
# api.param :query, :page_size, :integer, :optional,
|
263
|
+
# 'Number of records to return per page (cannot exceed 1000)'
|
264
|
+
# end
|
265
|
+
# if [:index, :show].include?(operation)
|
266
|
+
# api.param :query, :fields, :array, :optional, 'Field(s) to include ' \
|
267
|
+
# 'in the response', 'items' => { 'type' => 'string' }
|
268
|
+
# end
|
269
|
+
# end
|
270
|
+
#
|
271
|
+
# def add_open_api_common_errors(api, operation, _opts = {})
|
272
|
+
# if [:show, :update, :delete].include?(operation)
|
273
|
+
# api.response :not_found, 'No entity found matching the ID provided'
|
274
|
+
# end
|
275
|
+
# api.response :unauthorized,
|
276
|
+
# 'Missing a valid access token (access_token parameter or X-Access-Token header)'
|
277
|
+
# api.response :bad_request, 'One or more malformed / invalid ' \
|
278
|
+
# 'parameters identified for the request'
|
279
|
+
# api.param :query, :access_token, :string, :optional, 'Access token ' \
|
280
|
+
# 'used to authenticate client'
|
281
|
+
# api.param :header, :'X-Access-Token', :string, :optional,
|
282
|
+
# 'Access token used to authenticate client'
|
283
|
+
# end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
@@ -0,0 +1,504 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
|
3
|
+
module ModelApi
|
4
|
+
module Renderer
|
5
|
+
class << self
|
6
|
+
def render(controller, response_obj, opts = {})
|
7
|
+
opts = opts.symbolize_keys
|
8
|
+
format = (opts[:format] ||= get_format(controller))
|
9
|
+
opts[:action] ||= controller.action_name.to_sym
|
10
|
+
if format == :xml
|
11
|
+
render_xml_response(response_obj, controller, opts)
|
12
|
+
else
|
13
|
+
render_json_response(response_obj, controller, opts)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def serializable_object(obj, opts = {})
|
20
|
+
obj = obj.first if obj.is_a?(ActiveRecord::Relation)
|
21
|
+
opts = opts.symbolize_keys
|
22
|
+
return nil if obj.nil?
|
23
|
+
operation = opts[:operation] || :show
|
24
|
+
metadata_opts = opts.merge(ModelApi::Utils.contextual_metadata_opts(opts))
|
25
|
+
metadata = ModelApi::Utils.filtered_attrs(obj, operation, metadata_opts)
|
26
|
+
render_values, render_assoc = attrs_by_type(obj, metadata)
|
27
|
+
hash = {}
|
28
|
+
serialize_values(hash, obj, render_values, opts)
|
29
|
+
serialize_associations(hash, obj, render_assoc, opts)
|
30
|
+
process_serializable_hash(metadata, hash, opts)
|
31
|
+
end
|
32
|
+
|
33
|
+
def attrs_by_type(obj, metadata)
|
34
|
+
render_values = []
|
35
|
+
render_assoc = []
|
36
|
+
metadata.each do |attr, attr_metadata|
|
37
|
+
if (value = attr_metadata[:value]).present?
|
38
|
+
render_values << [attr, value, attr_metadata]
|
39
|
+
elsif obj.respond_to?(attr.to_s)
|
40
|
+
render_values << [attr, obj.send(attr.to_sym), attr_metadata]
|
41
|
+
elsif (assoc = obj.class.reflect_on_association(attr)).present?
|
42
|
+
render_assoc << [assoc, attr_metadata]
|
43
|
+
elsif obj.is_a?(ActiveRecord::Base)
|
44
|
+
fail "Invalid API attribute for #{obj.class.model_name.human} instance: #{attr}"
|
45
|
+
else
|
46
|
+
fail "Invalid API attribute for #{obj.class.name} instance: #{attr}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
[render_values, render_assoc]
|
50
|
+
end
|
51
|
+
|
52
|
+
def serialize_values(hash, obj, value_procs, opts = {})
|
53
|
+
value_procs.each do |attr, value, attr_metadata|
|
54
|
+
if value.respond_to?(:call) && value.respond_to?(:parameters)
|
55
|
+
proc_opts = opts.merge(attr: attr, attr_metadata: attr_metadata)
|
56
|
+
value = value.send(*([:call, obj, proc_opts][0..value.parameters.size]))
|
57
|
+
end
|
58
|
+
hash[attr] = serialize_value(value, attr_metadata, opts)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def serialize_associations(hash, obj, associations, opts = {})
|
63
|
+
associations.each do |assoc, attr_metadata|
|
64
|
+
return nil if assoc.nil?
|
65
|
+
assoc_opts = ModelApi::Utils.assoc_opts(assoc, attr_metadata, opts)
|
66
|
+
next if assoc_opts.nil?
|
67
|
+
attr = assoc.name
|
68
|
+
assoc = assoc_opts[:association]
|
69
|
+
render_proc ||= ->(o) { serialize_value(o, attr_metadata, assoc_opts) }
|
70
|
+
if !assoc.nil? && assoc.collection?
|
71
|
+
value = obj.send(attr).map { |o| render_proc.call(o) }
|
72
|
+
else
|
73
|
+
value = render_proc.call(obj.send(attr))
|
74
|
+
end
|
75
|
+
hash[attr] = value
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def serialize_value(value, attr_metadata, opts)
|
80
|
+
if (render_method = attr_metadata[:render_method]).present?
|
81
|
+
if render_method.respond_to?(:call)
|
82
|
+
value = serialize_value_proc(render_method, value)
|
83
|
+
else
|
84
|
+
value = serialize_value_obj_attr(render_method, value)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
opts = ModelApi::Utils.contextual_metadata_opts(attr_metadata, opts)
|
88
|
+
opts[:operation] = :show
|
89
|
+
if value.respond_to?(:map)
|
90
|
+
return value.map do |elem|
|
91
|
+
elem.is_a?(ActiveRecord::Base) ? serializable_object(elem, opts) : elem
|
92
|
+
end
|
93
|
+
elsif value.is_a?(ActiveRecord::Base)
|
94
|
+
return serializable_object(value, opts)
|
95
|
+
end
|
96
|
+
value
|
97
|
+
end
|
98
|
+
|
99
|
+
def serialize_value_proc(render_method, value)
|
100
|
+
if render_method.parameters.count > 1
|
101
|
+
render_method.call(value, opts)
|
102
|
+
else
|
103
|
+
render_method.call(value)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def serialize_value_obj_attr(render_method, value)
|
108
|
+
render_method = render_method.to_s.to_sym
|
109
|
+
if value.is_a?(ActiveRecord::Associations::CollectionProxy) || value.is_a?(Array)
|
110
|
+
(value.map do |obj|
|
111
|
+
obj.respond_to?(render_method) ? obj.send(render_method) : nil
|
112
|
+
end).compact
|
113
|
+
elsif value.respond_to?(render_method)
|
114
|
+
value.send(render_method)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def process_serializable_hash(api_attrs_metadata, hash, opts)
|
119
|
+
updated_hash_array = (hash.map do |key, value|
|
120
|
+
attr_metadata = api_attrs_metadata[key.to_sym] || {}
|
121
|
+
value = ModelApi::Utils.format_value(value, attr_metadata, opts)
|
122
|
+
next nil if value.nil? && attr_metadata[:hide_when_nil]
|
123
|
+
[ModelApi::Utils.ext_attr(key, attr_metadata).to_sym, value]
|
124
|
+
end).compact
|
125
|
+
api_attrs = api_attrs_metadata.map { |k, m| ModelApi::Utils.ext_attr(k, m).to_sym }
|
126
|
+
updated_hash_array.sort_by! do |key, _value|
|
127
|
+
api_attrs.find_index(key.to_s.to_sym) || api_attrs.size
|
128
|
+
end
|
129
|
+
Hash[updated_hash_array]
|
130
|
+
end
|
131
|
+
|
132
|
+
def get_format(controller)
|
133
|
+
format = controller.request.format.symbol rescue :json
|
134
|
+
format == :xml ? :xml : :json
|
135
|
+
end
|
136
|
+
|
137
|
+
def get_object_root_elem(obj, opts)
|
138
|
+
if (root_elem = opts[:root]).present?
|
139
|
+
return root_elem
|
140
|
+
end
|
141
|
+
if obj.respond_to?(:klass)
|
142
|
+
item_class = obj.klass
|
143
|
+
elsif obj.nil?
|
144
|
+
item_class = Object
|
145
|
+
else
|
146
|
+
item_class = obj.class
|
147
|
+
end
|
148
|
+
return 'response' if item_class == Hash
|
149
|
+
ModelApi::Utils.model_name(item_class).singular
|
150
|
+
end
|
151
|
+
|
152
|
+
def get_collection_root_elem(collection, opts)
|
153
|
+
if (root_elem = opts[:root]).present?
|
154
|
+
return root_elem
|
155
|
+
end
|
156
|
+
if collection.respond_to?(:klass)
|
157
|
+
item_class = collection.klass
|
158
|
+
elsif collection.respond_to?(:first) && (first_obj = collection.first).present?
|
159
|
+
item_class = first_obj.class
|
160
|
+
else
|
161
|
+
item_class = Object
|
162
|
+
end
|
163
|
+
ModelApi::Utils.model_name(item_class).plural
|
164
|
+
end
|
165
|
+
|
166
|
+
def hateoas_link_xml(links, _opts = {})
|
167
|
+
'<_links>' +
|
168
|
+
links.map do |link|
|
169
|
+
'<link' + link.map { |k, v| " #{k}=\"#{CGI.escapeHTML(v)}\"" }.join + ' />'
|
170
|
+
end.join +
|
171
|
+
'</_links>'
|
172
|
+
end
|
173
|
+
|
174
|
+
def hateoas_pagination_values_json(count, page, page_count, page_size)
|
175
|
+
json = []
|
176
|
+
if count.present?
|
177
|
+
json << ",\"#{ModelApi::Utils.ext_attr(:count)}\":#{[count.to_i, 0].max}"
|
178
|
+
end
|
179
|
+
json << ",\"#{ModelApi::Utils.ext_attr(:page)}\":#{[page.to_i, 0].max}" if page.present?
|
180
|
+
if page_count.present?
|
181
|
+
json << ",\"#{ModelApi::Utils.ext_attr(:page_count)}\":#{[page_count.to_i, 0].max}"
|
182
|
+
end
|
183
|
+
if page_size.present?
|
184
|
+
json << ",\"#{ModelApi::Utils.ext_attr(:page_size)}\":#{[page_size.to_i, 0].max}"
|
185
|
+
end
|
186
|
+
json.join
|
187
|
+
end
|
188
|
+
|
189
|
+
def hateoas_pagination_values_xml(count, page, page_count, page_size)
|
190
|
+
xml = []
|
191
|
+
count_attr = ModelApi::Utils.ext_attr(:count)
|
192
|
+
page_attr = ModelApi::Utils.ext_attr(:page)
|
193
|
+
page_count_attr = ModelApi::Utils.ext_attr(:page_count)
|
194
|
+
page_size_attr = ModelApi::Utils.ext_attr(:page_size)
|
195
|
+
xml << "<#{count_attr}>#{[count.to_i, 0].max}</#{count_attr}>" if count.present?
|
196
|
+
xml << "<#{page_attr}>#{[page.to_i, 0].max}</#{page_attr}>" if page.present?
|
197
|
+
if page_count.present?
|
198
|
+
xml << "<#{page_count_attr}>#{[page_count.to_i, 0].max}</#{page_count_attr}>"
|
199
|
+
end
|
200
|
+
if page_size.present?
|
201
|
+
xml << "<#{page_size_attr}>#{[page_size.to_i, 0].max}</#{page_size_attr}>"
|
202
|
+
end
|
203
|
+
xml.join
|
204
|
+
end
|
205
|
+
|
206
|
+
def object_hateoas_links(object_links, obj, controller, opts = {})
|
207
|
+
return {} if obj.blank?
|
208
|
+
custom_links = ModelApi::Utils.filtered_links(obj, opts[:operation], opts)
|
209
|
+
links = (object_links || {}).merge(custom_links).map do |rel, route|
|
210
|
+
next { rel: rel.to_s, href: route.to_s } if route.is_a?(URI)
|
211
|
+
if route.is_a?(Hash)
|
212
|
+
link_opts = opts.merge(route)
|
213
|
+
route = link_opts.delete(:route)
|
214
|
+
next nil if route.blank?
|
215
|
+
else
|
216
|
+
link_opts = opts
|
217
|
+
end
|
218
|
+
next { rel: rel.to_s, href: route.to_s } if route.is_a?(URI)
|
219
|
+
route_args = build_object_hateoas_route_args(obj, controller, route, link_opts)
|
220
|
+
if route_args[0].respond_to?(:url_for)
|
221
|
+
next { rel: rel.to_s, href: route_args[0].url_for(*route_args.slice(1..-1)) }
|
222
|
+
elsif !controller.respond_to?(route_args[0].to_s.to_sym)
|
223
|
+
route_args[0] = :"#{route_args[0]}_url"
|
224
|
+
next nil unless controller.respond_to?(route_args[0])
|
225
|
+
end
|
226
|
+
begin
|
227
|
+
param_count = controller.method(route_args[0]).parameters.size
|
228
|
+
{ rel: rel.to_s, href: controller.send(*(route_args[0..param_count])) }
|
229
|
+
rescue Exception => e
|
230
|
+
Rails.logger.warn "Error encountered generating \"#{rel}\" " \
|
231
|
+
"link (\"#{e.message}\") for #{obj.class.name}: " \
|
232
|
+
"#{(obj.respond_to?(:guid) ? obj.guid : obj.id)}"
|
233
|
+
nil
|
234
|
+
end
|
235
|
+
end
|
236
|
+
links.compact
|
237
|
+
end
|
238
|
+
|
239
|
+
def build_object_hateoas_route_args(obj, controller, route, opts = {})
|
240
|
+
id_attr = opts[:id_attribute] || :id
|
241
|
+
link_opts = (opts[:object_link_options] || {}).dup
|
242
|
+
format = controller.request.format.symbol rescue nil
|
243
|
+
link_opts[:format] ||= format if format.present?
|
244
|
+
if route.is_a?(Array)
|
245
|
+
if route.size > 1 && (route_opts = route.last).is_a?(Hash)
|
246
|
+
route.first(route.size - 1).append(link_opts.merge(route_opts))
|
247
|
+
else
|
248
|
+
route + [object.send(id_attr), link_opts]
|
249
|
+
end
|
250
|
+
else
|
251
|
+
id = obj.is_a?(Hash) ? obj[id_attr] : obj.send(id_attr)
|
252
|
+
path_params = Rails.application.routes.routes.named_routes[route.to_s].parts rescue []
|
253
|
+
path_params.each do |param|
|
254
|
+
param = param.to_sym
|
255
|
+
next if link_opts.include?(param)
|
256
|
+
value = controller.params[param]
|
257
|
+
link_opts[param] = value if value.present?
|
258
|
+
end
|
259
|
+
[route, { (opts[:id_param] || :id) => id }.merge(link_opts)]
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def hateoas_links(links, common_link_opts, controller, opts = {})
|
264
|
+
links = (links || {}).map do |rel, route|
|
265
|
+
if route.is_a?(Hash)
|
266
|
+
link_opts = route.merge(common_link_opts)
|
267
|
+
route = link_opts.delete(:route)
|
268
|
+
else
|
269
|
+
link_opts = common_link_opts
|
270
|
+
end
|
271
|
+
next { rel: rel.to_s, href: route.to_s } if route.is_a?(URI)
|
272
|
+
route_args = build_hateoas_route_args(controller, route, link_opts, opts)
|
273
|
+
if route_args[0].respond_to?(:url_for)
|
274
|
+
next { rel: rel.to_s, href: route_args[0].url_for(*route_args.slice(1..-1)) }
|
275
|
+
elsif !controller.respond_to?(route_args[0])
|
276
|
+
route_args[0] = :"#{route_args[0]}_url"
|
277
|
+
next nil unless controller.respond_to?(route_args[0])
|
278
|
+
end
|
279
|
+
begin
|
280
|
+
{ rel: rel.to_s, href: controller.send(*route_args) }
|
281
|
+
rescue Exception => e
|
282
|
+
Rails.logger.warn "Error encountered generating \"#{rel}\" link " \
|
283
|
+
"(\"#{e.message}\") for collection."
|
284
|
+
nil
|
285
|
+
end
|
286
|
+
end
|
287
|
+
links.compact
|
288
|
+
end
|
289
|
+
|
290
|
+
def build_hateoas_route_args(controller, route, link_opts, _opts = {})
|
291
|
+
link_opts ||= {}
|
292
|
+
format = controller.request.format.symbol rescue nil
|
293
|
+
link_opts[:format] ||= format if format.present?
|
294
|
+
if route.is_a?(Array)
|
295
|
+
if route.size > 1 && (route_opts = route.last).is_a?(Hash)
|
296
|
+
route.first(route.size - 1).append(link_opts.merge(route_opts))
|
297
|
+
else
|
298
|
+
route.append(link_opts)
|
299
|
+
end
|
300
|
+
else
|
301
|
+
[route, link_opts]
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def hateoas_object(obj, controller, format, opts = {})
|
306
|
+
object_links = object_hateoas_links(opts[:object_links], obj, controller, opts)
|
307
|
+
hash = serializable_object(obj, opts)
|
308
|
+
if format == :xml
|
309
|
+
root_tag = ModelApi::Utils.ext_attr(opts[:root] || get_object_root_elem(obj, opts) || :obj)
|
310
|
+
end_tag = "</#{root_tag}>"
|
311
|
+
if object_links.present?
|
312
|
+
pretty_xml(hash
|
313
|
+
.to_xml(opts.merge(root: root_tag, skip_instruct: true))
|
314
|
+
.sub(Regexp.new('(' + Regexp.escape(end_tag) + ')\\w*\\Z'),
|
315
|
+
hateoas_link_xml(object_links, opts) + end_tag))
|
316
|
+
else
|
317
|
+
pretty_xml(hash.to_xml(opts.merge(root: root_tag, skip_instruct: true)))
|
318
|
+
end
|
319
|
+
else
|
320
|
+
hash[:_links] = object_links
|
321
|
+
hash.to_json(opts)
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def hateoas_collection(collection, controller, format, opts = {})
|
326
|
+
opts = (opts || {}).symbolize_keys
|
327
|
+
count = opts[:count]
|
328
|
+
page = opts[:page]
|
329
|
+
page_count = opts[:page_count]
|
330
|
+
page_size = opts[:page_size]
|
331
|
+
root_tag = ModelApi::Utils.ext_attr(opts[:root] || get_collection_root_elem(collection, opts) ||
|
332
|
+
:objects)
|
333
|
+
if format == :xml
|
334
|
+
children_tag = opts.delete(:children) || root_tag.to_s.singularize
|
335
|
+
response_xml = []
|
336
|
+
response_xml << "<#{root_tag}>"
|
337
|
+
collection.each do |obj|
|
338
|
+
response_xml << hateoas_object(obj, controller, format, opts.merge(root: children_tag))
|
339
|
+
end
|
340
|
+
response_xml << "</#{root_tag}>"
|
341
|
+
response_xml << hateoas_pagination_values_xml(count, page, page_count, page_size)
|
342
|
+
pretty_xml(response_xml.join)
|
343
|
+
else
|
344
|
+
"\"#{root_tag}\":[" + collection.map do |obj|
|
345
|
+
hateoas_object(obj, controller, format, opts)
|
346
|
+
end.join(',') + ']' +
|
347
|
+
hateoas_pagination_values_json(count, page, page_count, page_size)
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
def render_xml_response(response_obj, controller, opts = {})
|
352
|
+
http_status, http_status_code = http_status_and_status_code(controller, opts)
|
353
|
+
successful = ModelApi::Utils.response_successful?(http_status_code)
|
354
|
+
response_xml = render_xml_response_heading(http_status, opts)
|
355
|
+
render_xml_response_body(response_xml, response_obj, controller, opts)
|
356
|
+
response_xml << "</#{ModelApi::Utils.ext_attr(:response)}>"
|
357
|
+
return pretty_xml(response_xml.join) if opts[:generate_body_only]
|
358
|
+
set_location_header(response_obj, controller, successful, opts)
|
359
|
+
controller.render status: http_status, xml: pretty_xml(response_xml.join)
|
360
|
+
successful
|
361
|
+
end
|
362
|
+
|
363
|
+
def render_xml_response_heading(status, opts = {})
|
364
|
+
http_status_code = ModelApi::Utils.http_status_code(status)
|
365
|
+
successful = ModelApi::Utils.response_successful?(http_status_code)
|
366
|
+
successful_tag = ModelApi::Utils.ext_attr(:successful)
|
367
|
+
status_tag = ModelApi::Utils.ext_attr(:status)
|
368
|
+
status_code_tag = ModelApi::Utils.ext_attr(:status_code)
|
369
|
+
response_xml = []
|
370
|
+
response_xml << "<#{ModelApi::Utils.ext_attr(:response)}>"
|
371
|
+
response_xml << "<#{successful_tag}>#{successful ? 'true' : 'false'}</#{successful_tag}>"
|
372
|
+
response_xml << "<#{status_tag}>#{status}</#{status_tag}>"
|
373
|
+
response_xml << "<#{status_code_tag}>#{http_status_code}</#{status_code_tag}>"
|
374
|
+
if opts[:messages].present?
|
375
|
+
response_xml << xml_collection_elem_tags_with_attrs(
|
376
|
+
ModelApi::Utils.ext_attr(successful ? :messages : :errors), opts[:messages])
|
377
|
+
end
|
378
|
+
response_xml
|
379
|
+
end
|
380
|
+
|
381
|
+
def render_xml_response_body(response_xml, response_obj, controller,
|
382
|
+
opts = {})
|
383
|
+
collection = false
|
384
|
+
if response_obj.is_a?(ActiveRecord::Base)
|
385
|
+
response_xml << hateoas_object(response_obj, controller, opts[:format] || :xml, opts)
|
386
|
+
elsif !response_obj.is_a?(Hash) && response_obj.respond_to?(:map)
|
387
|
+
response_xml << hateoas_collection(response_obj, controller, opts[:format] || :xml, opts)
|
388
|
+
collection = true
|
389
|
+
elsif response_obj.present?
|
390
|
+
root = ModelApi::Utils.ext_attr(opts[:root] || get_object_root_elem(response_obj, opts) ||
|
391
|
+
:response)
|
392
|
+
response_xml << response_obj.to_xml(opts.merge(skip_instruct: true, root: root)).rstrip
|
393
|
+
end
|
394
|
+
if opts[:ignored_fields].present?
|
395
|
+
response_xml << xml_collection_elem_tags_with_attrs(
|
396
|
+
ModelApi::Utils.ext_attr(:ignored_fields), opts[:ignored_fields])
|
397
|
+
end
|
398
|
+
if collection
|
399
|
+
if (links = hateoas_links(opts[:collection_links],
|
400
|
+
opts[:collection_link_options], controller, opts)).present?
|
401
|
+
response_xml << hateoas_link_xml(links, opts)
|
402
|
+
end
|
403
|
+
elsif (links = hateoas_links(opts[:links], opts[:link_opts], controller, opts)).present?
|
404
|
+
response_xml << hateoas_link_xml(links, opts)
|
405
|
+
end
|
406
|
+
response_xml
|
407
|
+
end
|
408
|
+
|
409
|
+
def http_status_and_status_code(controller, opts = {})
|
410
|
+
if opts[:status].present?
|
411
|
+
return [opts[:status].to_sym, ModelApi::Utils.http_status_code(opts[:status].to_sym)]
|
412
|
+
elsif opts[:status_code].present?
|
413
|
+
return [ModelApi::Utils.http_status(opts[:status_code].to_i), opts[:status_code].to_i]
|
414
|
+
elsif controller.response.status.present? && controller.response.status > 0
|
415
|
+
return [ModelApi::Utils.http_status(controller.response.status), controller.response.status]
|
416
|
+
else
|
417
|
+
return [:ok, ModelApi::Utils.http_status_code(:ok)]
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
def render_json_response(response_obj, controller, opts = {})
|
422
|
+
http_status, http_status_code = http_status_and_status_code(controller, opts)
|
423
|
+
successful = ModelApi::Utils.response_successful?(http_status_code)
|
424
|
+
response_json = "\"#{ModelApi::Utils.ext_attr(:successful)}\":#{successful ? 'true' : 'false'},"
|
425
|
+
response_json += "\"#{ModelApi::Utils.ext_attr(:status)}\":\"#{http_status}\","
|
426
|
+
response_json += "\"#{ModelApi::Utils.ext_attr(:status_code)}\":#{http_status_code}"
|
427
|
+
if opts[:messages].present?
|
428
|
+
response_json += ",\"#{ModelApi::Utils.ext_attr(successful ? :messages : :errors)}\":" +
|
429
|
+
opts[:messages].to_json(opts)
|
430
|
+
end
|
431
|
+
response_json += build_response_obj_json(response_obj, controller, opts)
|
432
|
+
if opts[:ignored_fields].present?
|
433
|
+
response_json += ",\"#{ModelApi::Utils.ext_attr(:ignored_fields)}\":" +
|
434
|
+
opts[:ignored_fields].to_json(opts)
|
435
|
+
end
|
436
|
+
return "{#{response_json}}" if opts[:generate_body_only]
|
437
|
+
set_location_header(response_obj, controller, successful, opts)
|
438
|
+
controller.render status: http_status, json: "{#{response_json}}"
|
439
|
+
successful
|
440
|
+
end
|
441
|
+
|
442
|
+
def build_response_obj_json(response_obj, controller, opts = {})
|
443
|
+
if !response_obj.nil?
|
444
|
+
if response_obj.is_a?(ActiveRecord::Base)
|
445
|
+
root_elem_json = ModelApi::Utils.ext_attr(get_object_root_elem(response_obj, opts)).to_json
|
446
|
+
response_json = ",#{root_elem_json}:" +
|
447
|
+
hateoas_object(response_obj, controller, opts[:format] || :json, opts)
|
448
|
+
links = hateoas_links(opts[:links], opts[:link_opts], controller, opts)
|
449
|
+
elsif !response_obj.is_a?(Hash) && response_obj.respond_to?(:map)
|
450
|
+
response_json = ',' + hateoas_collection(response_obj, controller,
|
451
|
+
opts[:format] || :json, opts)
|
452
|
+
links = hateoas_links(opts[:collection_links],
|
453
|
+
opts[:collection_link_options], controller, opts)
|
454
|
+
else
|
455
|
+
root_elem_json = ModelApi::Utils.ext_attr(get_object_root_elem(response_obj, opts)).to_json
|
456
|
+
response_json = ",#{root_elem_json}:" + response_obj.to_json(opts)
|
457
|
+
links = hateoas_links(opts[:links], opts[:link_opts], controller, opts)
|
458
|
+
end
|
459
|
+
if links.present?
|
460
|
+
response_json += ",\"_links\":" + links.to_json(opts)
|
461
|
+
end
|
462
|
+
response_json
|
463
|
+
else
|
464
|
+
''
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
def xml_elem_tags_with_attrs(element, hash)
|
469
|
+
tags = hash.map do |attr, value|
|
470
|
+
" #{attr}=\"#{CGI.escapeHTML(value.to_s)}\""
|
471
|
+
end
|
472
|
+
"<#{element}#{tags.join} />"
|
473
|
+
end
|
474
|
+
|
475
|
+
def xml_collection_elem_tags_with_attrs(element, array, opts = {})
|
476
|
+
child_element = opts[:children] || element.to_s.singularize
|
477
|
+
tags = array.map do |hash|
|
478
|
+
xml_elem_tags_with_attrs(child_element, hash)
|
479
|
+
end
|
480
|
+
"<#{element}>#{tags.join}</#{element}>"
|
481
|
+
end
|
482
|
+
|
483
|
+
def pretty_xml(xml, _indent = 2)
|
484
|
+
xml_doc = REXML::Document.new(xml) rescue nil
|
485
|
+
return xml unless xml_doc.present?
|
486
|
+
formatter = REXML::Formatters::Pretty.new
|
487
|
+
formatter.compact = true
|
488
|
+
out = ''
|
489
|
+
formatter.write(xml_doc, out)
|
490
|
+
out || xml
|
491
|
+
end
|
492
|
+
|
493
|
+
def set_location_header(response_obj, controller, successful, opts = {})
|
494
|
+
return unless successful && opts[:location_header] &&
|
495
|
+
response_obj.is_a?(ActiveRecord::Base)
|
496
|
+
links = opts[:object_links]
|
497
|
+
return unless links.is_a?(Hash) && links.include?(:self)
|
498
|
+
link = object_hateoas_links({ self: links[:self] }, response_obj, controller,
|
499
|
+
opts.merge(exclude_api_links: true)).find { |l| l[:rel].to_s == 'self' }
|
500
|
+
controller.response.header['Location'] = link[:href] if link.include?(:href)
|
501
|
+
end
|
502
|
+
end
|
503
|
+
end
|
504
|
+
end
|