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
|