schemable 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +117 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +128 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/Rakefile +12 -0
- data/lib/generators/schemable/install_generator.rb +29 -0
- data/lib/generators/schemable/model_generator.rb +53 -0
- data/lib/schemable/version.rb +5 -0
- data/lib/schemable.rb +869 -0
- data/lib/templates/common_definitions.rb +13 -0
- data/lib/templates/serializers_helper.rb +7 -0
- data/schemable.gemspec +37 -0
- data/sig/schemable.rbs +4 -0
- metadata +96 -0
data/lib/schemable.rb
ADDED
@@ -0,0 +1,869 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "schemable/version"
|
4
|
+
require 'active_support/concern'
|
5
|
+
|
6
|
+
module Schemable
|
7
|
+
class Error < StandardError; end
|
8
|
+
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
included do
|
12
|
+
|
13
|
+
# Maps a given type name to a corresponding JSON schema object that represents that type.
|
14
|
+
#
|
15
|
+
# @param type_name [String, Symbol] A String or Symbol representing the type of the property to be mapped.
|
16
|
+
#
|
17
|
+
# @return [Hash, nil] A Hash that represents a JSON schema object for the given type, or nil if the type is not recognized.
|
18
|
+
def type_mapper(type_name)
|
19
|
+
{
|
20
|
+
text: { type: :string },
|
21
|
+
string: { type: :string },
|
22
|
+
integer: { type: :integer },
|
23
|
+
float: { type: :number, format: :float },
|
24
|
+
decimal: { type: :number, format: :double },
|
25
|
+
datetime: { type: :string, format: :"date-time" },
|
26
|
+
date: { type: :string, format: :date },
|
27
|
+
time: { type: :string, format: :time },
|
28
|
+
boolean: { type: :boolean },
|
29
|
+
trueclass: { type: :boolean, default: true },
|
30
|
+
falseclass: { type: :boolean, default: false },
|
31
|
+
binary: { type: :string, format: :binary },
|
32
|
+
json: { type: :object, properties: {} },
|
33
|
+
jsonb: { type: :object, properties: {} },
|
34
|
+
array: { type: :array, items: { anyOf: [
|
35
|
+
{ type: :string }, { type: :integer }, { type: :number, format: :float }, { type: :number, format: :double }, { type: :boolean }, { type: :object, properties: {} }
|
36
|
+
] } },
|
37
|
+
hash: { type: :object, properties: {} },
|
38
|
+
object: { type: :object, properties: {} }
|
39
|
+
}[type_name.try(:to_sym)]
|
40
|
+
end
|
41
|
+
|
42
|
+
# Modify a JSON schema object by merging new properties into it or deleting a specified path.
|
43
|
+
#
|
44
|
+
# @param original_schema [Hash] The original schema object to modify.
|
45
|
+
# @param new_props [Hash] The new properties to merge into the schema.
|
46
|
+
# @param given_path [String, nil] The path to the property to modify or delete, if any.
|
47
|
+
# Use dot notation to specify nested properties (e.g. "person.address.city").
|
48
|
+
# @param delete [Boolean] Whether to delete the property at the given path, if it exists.
|
49
|
+
# @raise [ArgumentError] If `delete` is true but `given_path` is nil, or if `given_path` does not exist in the original schema.
|
50
|
+
#
|
51
|
+
# @return [Hash] A new schema object with the specified modifications.
|
52
|
+
#
|
53
|
+
# @example Merge new properties into the schema
|
54
|
+
# original_schema = { type: 'object', properties: { name: { type: 'string' } } }
|
55
|
+
# new_props = { properties: { age: { type: 'integer' } } }
|
56
|
+
# modify_schema(original_schema, new_props)
|
57
|
+
# # => { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } } }
|
58
|
+
#
|
59
|
+
# @example Delete a property from the schema
|
60
|
+
# original_schema = { type: 'object', properties: { name: { type: 'string' } } }
|
61
|
+
# modify_schema(original_schema, {}, 'properties.name', delete: true)
|
62
|
+
# # => { type: 'object', properties: {} }
|
63
|
+
def modify_schema(original_schema, new_props, given_path = nil, delete: false)
|
64
|
+
return new_props if original_schema.nil?
|
65
|
+
|
66
|
+
if given_path.nil? && delete
|
67
|
+
raise ArgumentError, "Cannot delete without a given path"
|
68
|
+
end
|
69
|
+
|
70
|
+
if given_path.present?
|
71
|
+
path_segments = given_path.split('.').map(&:to_sym)
|
72
|
+
|
73
|
+
if path_segments.size == 1
|
74
|
+
unless original_schema.key?(path_segments.first)
|
75
|
+
raise ArgumentError, "Given path does not exist in the original schema"
|
76
|
+
end
|
77
|
+
else
|
78
|
+
unless original_schema.dig(*path_segments[0..-2]).is_a?(Hash) && original_schema.dig(*path_segments)
|
79
|
+
raise ArgumentError, "Given path does not exist in the original schema"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
path_hash = path_segments.reverse.reduce(new_props) { |a, n| { n => a } }
|
84
|
+
|
85
|
+
if delete
|
86
|
+
new_schema = original_schema.deep_dup
|
87
|
+
parent_hash = path_segments.size > 1 ? new_schema.dig(*path_segments[0..-2]) : new_schema
|
88
|
+
parent_hash.delete(path_segments.last)
|
89
|
+
new_schema
|
90
|
+
else
|
91
|
+
original_schema.deep_merge(path_hash)
|
92
|
+
end
|
93
|
+
else
|
94
|
+
original_schema.deep_merge(new_props)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns a JSON Schema attribute definition for a given attribute on the model.
|
99
|
+
#
|
100
|
+
# @param attribute [Symbol] The name of the attribute.
|
101
|
+
#
|
102
|
+
# @raise [NoMethodError] If the `model` object does not respond to `columns_hash`.
|
103
|
+
#
|
104
|
+
# @return [Hash] The JSON Schema attribute definition as a Hash or an empty Hash if the attribute does not exist on the model.
|
105
|
+
#
|
106
|
+
# @example
|
107
|
+
# attribute_schema(:title)
|
108
|
+
# # => { "type": "string" }
|
109
|
+
def attribute_schema(attribute)
|
110
|
+
# Get the column hash for the attribute
|
111
|
+
column_hash = model.columns_hash[attribute.to_s]
|
112
|
+
|
113
|
+
# Check if this attribute has a custom JSON Schema definition
|
114
|
+
if array_types.keys.include?(attribute)
|
115
|
+
return array_types[attribute]
|
116
|
+
end
|
117
|
+
|
118
|
+
if additional_response_attributes.keys.include?(attribute)
|
119
|
+
return additional_response_attributes[attribute]
|
120
|
+
end
|
121
|
+
|
122
|
+
# Check if this is an array attribute
|
123
|
+
if column_hash.as_json.try(:[], 'sql_type_metadata').try(:[], 'sql_type').include?('[]')
|
124
|
+
return type_mapper(:array)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Map the column type to a JSON Schema type if none of the above conditions are met
|
128
|
+
response = type_mapper(column_hash.try(:type))
|
129
|
+
|
130
|
+
# If the attribute is nullable, modify the schema accordingly
|
131
|
+
if response && nullable_attributes.include?(attribute)
|
132
|
+
return modify_schema(response, { nullable: true })
|
133
|
+
end
|
134
|
+
|
135
|
+
# If attribute is an enum, modify the schema accordingly
|
136
|
+
if response && model.defined_enums.key?(attribute.to_s)
|
137
|
+
return modify_schema(response, { type: :string, enum: model.defined_enums[attribute.to_s].keys })
|
138
|
+
end
|
139
|
+
|
140
|
+
return response unless response.nil?
|
141
|
+
|
142
|
+
# If we haven't found a schema type yet, try to infer it from the type of the attribute's value in the instance data
|
143
|
+
type_from_factory = @instance.as_json['data']['attributes'][attribute.to_s.camelize(:lower)].class.name.downcase
|
144
|
+
response = type_mapper(type_from_factory) if type_from_factory.present?
|
145
|
+
|
146
|
+
return response unless response.nil?
|
147
|
+
|
148
|
+
# If we still haven't found a schema type, default to object
|
149
|
+
type_mapper(:object)
|
150
|
+
|
151
|
+
rescue NoMethodError
|
152
|
+
# Log a warning if the attribute does not exist on the model
|
153
|
+
Rails.logger.warn("\e[33mWARNING: #{model} does not have an attribute named \e[31m#{attribute}\e[0m")
|
154
|
+
{}
|
155
|
+
end
|
156
|
+
|
157
|
+
# Returns a JSON Schema for the model's attributes.
|
158
|
+
# This method is used to generate the schema for the `attributes` that are automatically generated by using the `attribute_schema` method on each attribute.
|
159
|
+
#
|
160
|
+
# @note The `additional_response_attributes` and `excluded_response_attributes` are applied to the schema returned by this method.
|
161
|
+
#
|
162
|
+
# @example
|
163
|
+
# {
|
164
|
+
# type: :object,
|
165
|
+
# properties: {
|
166
|
+
# id: { type: :string },
|
167
|
+
# title: { type: :string }
|
168
|
+
# }
|
169
|
+
# }
|
170
|
+
#
|
171
|
+
# @return [Hash] The JSON Schema for the model's attributes.
|
172
|
+
def attributes_schema
|
173
|
+
schema = {
|
174
|
+
type: :object,
|
175
|
+
properties: attributes.reduce({}) do |props, attr|
|
176
|
+
props[attr] = attribute_schema(attr)
|
177
|
+
props
|
178
|
+
end
|
179
|
+
}
|
180
|
+
|
181
|
+
# modify the schema to include additional response relations
|
182
|
+
schema = modify_schema(schema, additional_response_attributes, given_path = "properties")
|
183
|
+
|
184
|
+
# modify the schema to exclude response relations
|
185
|
+
excluded_response_attributes.each do |key|
|
186
|
+
schema = modify_schema(schema, {}, "properties.#{key}", delete: true)
|
187
|
+
end
|
188
|
+
|
189
|
+
schema
|
190
|
+
end
|
191
|
+
|
192
|
+
# Generates the schema for the relationships of a resource.
|
193
|
+
#
|
194
|
+
# @param relations [Hash] A hash representing the relationships of the resource in the form of { belongs_to: {}, has_many: {} }.
|
195
|
+
# If not provided, the relationships will be inferred from the model's associations.
|
196
|
+
#
|
197
|
+
# @note The `additional_response_relations` and `excluded_response_relations` are applied to the schema returned by this method.
|
198
|
+
#
|
199
|
+
# @param expand [Boolean] A boolean indicating whether to expand the relationships in the schema.
|
200
|
+
# @param exclude_from_expansion [Array] An array of relationship names to exclude from expansion.
|
201
|
+
#
|
202
|
+
# @example
|
203
|
+
# {
|
204
|
+
# type: :object,
|
205
|
+
# properties: {
|
206
|
+
# province: {
|
207
|
+
# type: :object,
|
208
|
+
# properties: {
|
209
|
+
# meta: {
|
210
|
+
# type: :object,
|
211
|
+
# properties: {
|
212
|
+
# included: {
|
213
|
+
# type: :boolean, default: false
|
214
|
+
# }
|
215
|
+
# }
|
216
|
+
# }
|
217
|
+
# }
|
218
|
+
# }
|
219
|
+
# }
|
220
|
+
# }
|
221
|
+
#
|
222
|
+
# @return [Hash] A hash representing the schema for the relationships.
|
223
|
+
def relationships_schema(relations = try(:relationships), expand: false, exclude_from_expansion: [])
|
224
|
+
return {} if relations.blank?
|
225
|
+
return {} if relations == { belongs_to: {}, has_many: {} }
|
226
|
+
|
227
|
+
schema = {
|
228
|
+
type: :object,
|
229
|
+
properties: relations.reduce({}) do |props, (relation_type, relation_definitions)|
|
230
|
+
non_expanded_data_properties = {
|
231
|
+
type: :object,
|
232
|
+
properties: {
|
233
|
+
meta: {
|
234
|
+
type: :object,
|
235
|
+
properties: {
|
236
|
+
included: { type: :boolean, default: false }
|
237
|
+
}
|
238
|
+
}
|
239
|
+
}
|
240
|
+
}
|
241
|
+
|
242
|
+
if relation_type == :has_many
|
243
|
+
props.merge!(
|
244
|
+
relation_definitions.keys.index_with do |relationship|
|
245
|
+
|
246
|
+
result = {
|
247
|
+
type: :object,
|
248
|
+
properties: {
|
249
|
+
data: {
|
250
|
+
type: :array,
|
251
|
+
items: {
|
252
|
+
type: :object,
|
253
|
+
properties: {
|
254
|
+
id: { type: :string },
|
255
|
+
type: { type: :string, default: relation_definitions[relationship].model_name }
|
256
|
+
}
|
257
|
+
}
|
258
|
+
}
|
259
|
+
}
|
260
|
+
}
|
261
|
+
|
262
|
+
result = non_expanded_data_properties if !expand || exclude_from_expansion.include?(relationship)
|
263
|
+
|
264
|
+
result
|
265
|
+
end
|
266
|
+
)
|
267
|
+
else
|
268
|
+
props.merge!(
|
269
|
+
relation_definitions.keys.index_with do |relationship|
|
270
|
+
|
271
|
+
result = {
|
272
|
+
type: :object,
|
273
|
+
properties: {
|
274
|
+
data: {
|
275
|
+
type: :object,
|
276
|
+
properties: {
|
277
|
+
id: { type: :string },
|
278
|
+
type: { type: :string, default: relation_definitions[relationship].model_name }
|
279
|
+
|
280
|
+
}
|
281
|
+
}
|
282
|
+
}
|
283
|
+
}
|
284
|
+
|
285
|
+
result = non_expanded_data_properties if !expand || exclude_from_expansion.include?(relationship)
|
286
|
+
|
287
|
+
result
|
288
|
+
end
|
289
|
+
)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
}
|
293
|
+
|
294
|
+
# modify the schema to include additional response relations
|
295
|
+
schema = modify_schema(schema, additional_response_relations, "properties")
|
296
|
+
|
297
|
+
# modify the schema to exclude response relations
|
298
|
+
excluded_response_relations.each do |key|
|
299
|
+
schema = modify_schema(schema, {}, "properties.#{key}", delete: true)
|
300
|
+
end
|
301
|
+
|
302
|
+
schema
|
303
|
+
end
|
304
|
+
|
305
|
+
# Generates the schema for the included resources in a response.
|
306
|
+
#
|
307
|
+
# @note The `additional_response_includes` and `excluded_response_includes` (yet to be implemented) are applied to the schema returned by this method.
|
308
|
+
#
|
309
|
+
# @param relations [Hash] A hash representing the relationships of the resource in the form of { belongs_to: {}, has_many: {} }.
|
310
|
+
# If not provided, the relationships will be inferred from the model's associations.
|
311
|
+
# @param expand [Boolean] A boolean indicating whether to expand the relationships of the relationships in the schema.
|
312
|
+
# @param exclude_from_expansion [Array] An array of relationship names to exclude from expansion.
|
313
|
+
# @param metadata [Hash] Additional metadata to include in the schema, usually received from the nested_relationships method sent by the response_schema method.
|
314
|
+
#
|
315
|
+
# @example
|
316
|
+
# {
|
317
|
+
# included: {
|
318
|
+
# type: :array,
|
319
|
+
# items: {
|
320
|
+
# anyOf:
|
321
|
+
# [
|
322
|
+
# {
|
323
|
+
# type: :object,
|
324
|
+
# properties: {
|
325
|
+
# type: { type: :string, default: "provinces" },
|
326
|
+
# id: { type: :string },
|
327
|
+
# attributes: {
|
328
|
+
# type: :object,
|
329
|
+
# properties: {
|
330
|
+
# id: { type: :string },
|
331
|
+
# name: { type: :string }
|
332
|
+
# }
|
333
|
+
# }
|
334
|
+
# }
|
335
|
+
# }
|
336
|
+
# ]
|
337
|
+
# }
|
338
|
+
# }
|
339
|
+
# }
|
340
|
+
#
|
341
|
+
# @return [Hash] A hash representing the schema for the included resources.
|
342
|
+
def included_schema(relations = try(:relationships), expand: false, exclude_from_expansion: [], metadata: {})
|
343
|
+
return {} if relations.blank?
|
344
|
+
return {} if relations == { belongs_to: {}, has_many: {} }
|
345
|
+
|
346
|
+
schema = {
|
347
|
+
included: {
|
348
|
+
type: :array,
|
349
|
+
items: {
|
350
|
+
anyOf:
|
351
|
+
relations.reduce([]) do |props, (relation_type, relation_definitions)|
|
352
|
+
props + relation_definitions.keys.reduce([]) do |props, relationship|
|
353
|
+
props + [
|
354
|
+
unless exclude_from_expansion.include?(relationship)
|
355
|
+
{
|
356
|
+
type: :object,
|
357
|
+
properties: {
|
358
|
+
type: { type: :string, default: relation_definitions[relationship].model_name },
|
359
|
+
id: { type: :string },
|
360
|
+
attributes: begin
|
361
|
+
relation_definitions[relationship].new.attributes_schema || {}
|
362
|
+
rescue NoMethodError
|
363
|
+
{}
|
364
|
+
end
|
365
|
+
}.merge(
|
366
|
+
if relation_definitions[relationship].new.relationships != { belongs_to: {}, has_many: {} } || relation_definitions[relationship].new.relationships.blank?
|
367
|
+
if !expand || metadata.blank?
|
368
|
+
{ relationships: relation_definitions[relationship].new.relationships_schema(expand: false) }
|
369
|
+
else
|
370
|
+
{ relationships: relation_definitions[relationship].new.relationships_schema(relations = metadata[:nested_relationships][relationship], expand: true, exclude_from_expansion: exclude_from_expansion) }
|
371
|
+
end
|
372
|
+
else
|
373
|
+
{}
|
374
|
+
end
|
375
|
+
)
|
376
|
+
}
|
377
|
+
end
|
378
|
+
].concat(
|
379
|
+
[
|
380
|
+
if expand && metadata.present? && !exclude_from_expansion.include?(relationship)
|
381
|
+
extra_relations = []
|
382
|
+
metadata[:nested_relationships].keys.reduce({}) do |props, nested_relationship|
|
383
|
+
if metadata[:nested_relationships][relationship].present?
|
384
|
+
props.merge!(metadata[:nested_relationships][nested_relationship].keys.each_with_object({}) do |relationship_type, inner_props|
|
385
|
+
props.merge!(metadata[:nested_relationships][nested_relationship][relationship_type].keys.each_with_object({}) do |relationship, inner_inner_props|
|
386
|
+
|
387
|
+
extra_relation_schema = {
|
388
|
+
type: :object,
|
389
|
+
properties: {
|
390
|
+
type: { type: :string, default: metadata[:nested_relationships][nested_relationship][relationship_type][relationship].model_name },
|
391
|
+
id: { type: :string },
|
392
|
+
attributes: metadata[:nested_relationships][nested_relationship][relationship_type][relationship].new.attributes_schema
|
393
|
+
}.merge(
|
394
|
+
if metadata[:nested_relationships][nested_relationship][relationship_type][relationship].new.relationships == { belongs_to: {}, has_many: {} } || metadata[:nested_relationships][nested_relationship][relationship_type][relationship].new.relationships.blank?
|
395
|
+
{}
|
396
|
+
else
|
397
|
+
result = { relationships: metadata[:nested_relationships][nested_relationship][relationship_type][relationship].new.relationships_schema(expand: false) }
|
398
|
+
return {} if result == { relationships: {} }
|
399
|
+
result
|
400
|
+
end
|
401
|
+
)
|
402
|
+
}
|
403
|
+
|
404
|
+
extra_relations << extra_relation_schema
|
405
|
+
end
|
406
|
+
)
|
407
|
+
end
|
408
|
+
)
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
extra_relations
|
413
|
+
end
|
414
|
+
].flatten
|
415
|
+
).compact_blank
|
416
|
+
end
|
417
|
+
end
|
418
|
+
}
|
419
|
+
}
|
420
|
+
}
|
421
|
+
|
422
|
+
schema = modify_schema(schema, additional_response_included, "included.items")
|
423
|
+
|
424
|
+
schema
|
425
|
+
end
|
426
|
+
|
427
|
+
# Generates the schema for the response of a resource or collection of resources in JSON API format.
|
428
|
+
#
|
429
|
+
# @param relations [Hash] A hash representing the relationships of the resource in the form of { belongs_to: {}, has_many: {} }.
|
430
|
+
# If not provided, the relationships will be inferred from the model's associations.
|
431
|
+
# @param expand [Boolean] A boolean indicating whether to expand the relationships of the relationships in the schema.
|
432
|
+
# @param exclude_from_expansion [Array] An array of relationship names to exclude from expansion.
|
433
|
+
# @param multi [Boolean] A boolean indicating whether the response contains multiple resources.
|
434
|
+
# @param nested [Boolean] A boolean indicating whether the response is to be expanded further than the first level of relationships. (expand relationships of relationships)
|
435
|
+
# @param metadata [Hash] Additional metadata to include in the schema, usually received from the nested_relationships method sent by the response_schema method.
|
436
|
+
#
|
437
|
+
# @example
|
438
|
+
# The returned schema will have a JSON API format, including the data (included attributes and relationships), included and meta keys.
|
439
|
+
#
|
440
|
+
# @return [Hash] A hash representing the schema for the response.
|
441
|
+
def response_schema(relations = try(:relationships), expand: false, exclude_from_expansion: [], multi: false, nested: false, metadata: { nested_relationships: try(:nested_relationships) })
|
442
|
+
|
443
|
+
data = {
|
444
|
+
type: :object,
|
445
|
+
properties: {
|
446
|
+
type: { type: :string, default: itself.class.model_name },
|
447
|
+
id: { type: :string },
|
448
|
+
attributes: attributes_schema,
|
449
|
+
}.merge(
|
450
|
+
if relations.blank? || relations == { belongs_to: {}, has_many: {} }
|
451
|
+
{}
|
452
|
+
else
|
453
|
+
{ relationships: relationships_schema(relations, expand: expand, exclude_from_expansion: exclude_from_expansion) }
|
454
|
+
end
|
455
|
+
)
|
456
|
+
}
|
457
|
+
|
458
|
+
schema = if multi
|
459
|
+
{
|
460
|
+
data: {
|
461
|
+
type: :array,
|
462
|
+
items: data
|
463
|
+
}
|
464
|
+
}
|
465
|
+
else
|
466
|
+
{
|
467
|
+
data: data
|
468
|
+
}
|
469
|
+
end
|
470
|
+
|
471
|
+
schema.merge!(
|
472
|
+
if nested && expand
|
473
|
+
included_schema(relations, expand: nested, exclude_from_expansion: exclude_from_expansion, metadata: metadata)
|
474
|
+
elsif !nested && expand
|
475
|
+
included_schema(relations, expand: nested, exclude_from_expansion: exclude_from_expansion)
|
476
|
+
else
|
477
|
+
{}
|
478
|
+
end
|
479
|
+
).merge!(
|
480
|
+
if !expand
|
481
|
+
{ meta: meta }
|
482
|
+
else
|
483
|
+
{}
|
484
|
+
end
|
485
|
+
).merge!(
|
486
|
+
jsonapi: jsonapi
|
487
|
+
)
|
488
|
+
|
489
|
+
{
|
490
|
+
type: :object,
|
491
|
+
properties: schema
|
492
|
+
}
|
493
|
+
end
|
494
|
+
|
495
|
+
# Generates the schema for the request payload of a resource.
|
496
|
+
#
|
497
|
+
# @note The `additional_request_attributes` and `excluded_request_attributes` applied to the returned schema by this method.
|
498
|
+
# @note The `required_attributes` are applied to the returned schema by this method.
|
499
|
+
# @note The `nullable_attributes` are applied to the returned schema by this method.
|
500
|
+
#
|
501
|
+
# @example
|
502
|
+
# {
|
503
|
+
# type: :object,
|
504
|
+
# properties: {
|
505
|
+
# data: {
|
506
|
+
# type: :object,
|
507
|
+
# properties: {
|
508
|
+
# firstName: { type: :string },
|
509
|
+
# lastName: { type: :string }
|
510
|
+
# },
|
511
|
+
# required: [:firstName, :lastName]
|
512
|
+
# }
|
513
|
+
# }
|
514
|
+
# }
|
515
|
+
#
|
516
|
+
# @return [Hash] A hash representing the schema for the request payload.
|
517
|
+
def request_schema
|
518
|
+
schema = {
|
519
|
+
type: :object,
|
520
|
+
properties: {
|
521
|
+
data: attributes_schema
|
522
|
+
}
|
523
|
+
}
|
524
|
+
|
525
|
+
schema = modify_schema(schema, additional_request_attributes, "properties.data.properties")
|
526
|
+
|
527
|
+
excluded_request_attributes.each do |key|
|
528
|
+
schema = modify_schema(schema, {}, "properties.data.properties.#{key}", delete: true)
|
529
|
+
end
|
530
|
+
|
531
|
+
required_attributes = {
|
532
|
+
required: schema.as_json['properties']['data']['properties'].keys - optional_request_attributes.map(&:to_s) - nullable_attributes.map(&:to_s)
|
533
|
+
}
|
534
|
+
|
535
|
+
schema = modify_schema(schema, required_attributes, "properties.data")
|
536
|
+
|
537
|
+
schema
|
538
|
+
end
|
539
|
+
|
540
|
+
# Returns the schema for the meta data of the response body.
|
541
|
+
#
|
542
|
+
# This is used to provide pagination information usually (in the case of a collection).
|
543
|
+
#
|
544
|
+
# Note that this is an opinionated schema and may not be suitable for all use cases.
|
545
|
+
# If you need to override this schema, you can do so by overriding the `meta` method in your definition.
|
546
|
+
#
|
547
|
+
# @return [Hash] The schema for the meta data of the response body.
|
548
|
+
def meta
|
549
|
+
{
|
550
|
+
type: :object,
|
551
|
+
properties: {
|
552
|
+
page: {
|
553
|
+
type: :object,
|
554
|
+
properties: {
|
555
|
+
totalPages: {
|
556
|
+
type: :integer,
|
557
|
+
default: 1
|
558
|
+
},
|
559
|
+
count: {
|
560
|
+
type: :integer,
|
561
|
+
default: 1
|
562
|
+
},
|
563
|
+
limitValue: {
|
564
|
+
type: :integer,
|
565
|
+
default: 1
|
566
|
+
},
|
567
|
+
currentPage: {
|
568
|
+
type: :integer,
|
569
|
+
default: 1
|
570
|
+
}
|
571
|
+
}
|
572
|
+
}
|
573
|
+
}
|
574
|
+
}
|
575
|
+
end
|
576
|
+
|
577
|
+
# Returns the schema for the JSONAPI version.
|
578
|
+
#
|
579
|
+
# @return [Hash] The schema for the JSONAPI version.
|
580
|
+
def jsonapi
|
581
|
+
{
|
582
|
+
type: :object,
|
583
|
+
properties: {
|
584
|
+
version: {
|
585
|
+
type: :string,
|
586
|
+
default: "1.0"
|
587
|
+
}
|
588
|
+
}
|
589
|
+
}
|
590
|
+
end
|
591
|
+
|
592
|
+
# Returns the resource serializer to be used for serialization. This method must be implemented in the definition class.
|
593
|
+
#
|
594
|
+
# @raise [NotImplementedError] If the method is not implemented in the definition class.
|
595
|
+
#
|
596
|
+
# @example V1::UserSerializer
|
597
|
+
#
|
598
|
+
# @abstract This method must be implemented in the definition class.
|
599
|
+
#
|
600
|
+
# @return [Class] The resource serializer class.
|
601
|
+
def serializer
|
602
|
+
raise NotImplementedError, 'serializer method must be implemented in the definition class'
|
603
|
+
end
|
604
|
+
|
605
|
+
# Returns the attributes defined in the serializer (Auto generated from the serializer).
|
606
|
+
#
|
607
|
+
# @example
|
608
|
+
# [:id, :name, :email, :created_at, :updated_at]
|
609
|
+
#
|
610
|
+
# @return [Array<Symbol>, nil] The attributes defined in the serializer or nil if there are none.
|
611
|
+
def attributes
|
612
|
+
serializer.attribute_blocks.transform_keys { |key| key.to_s.underscore.to_sym }.keys || nil
|
613
|
+
end
|
614
|
+
|
615
|
+
# Returns the relationships defined in the serializer.
|
616
|
+
#
|
617
|
+
# Note that the format of the relationships is as follows: { belongs_to: { relationship_name: relationship_definition }, has_many: { relationship_name: relationship_definition }
|
618
|
+
#
|
619
|
+
# @example
|
620
|
+
# {
|
621
|
+
# belongs_to: {
|
622
|
+
# district: Swagger::Definitions::District,
|
623
|
+
# user: Swagger::Definitions::User
|
624
|
+
# },
|
625
|
+
# has_many: {
|
626
|
+
# applicants: Swagger::Definitions::Applicant,
|
627
|
+
# }
|
628
|
+
# }
|
629
|
+
#
|
630
|
+
# @return [Hash] The relationships defined in the serializer.
|
631
|
+
def relationships
|
632
|
+
{ belongs_to: {}, has_many: {} }
|
633
|
+
end
|
634
|
+
|
635
|
+
# Returns a hash of all the arrays defined for the model. The schema for each array is defined in the definition class manually.
|
636
|
+
#
|
637
|
+
# This method must be implemented in the definition class if there are any arrays.
|
638
|
+
#
|
639
|
+
# @example
|
640
|
+
# {
|
641
|
+
# metadata: {
|
642
|
+
# type: :array,
|
643
|
+
# items: {
|
644
|
+
# type: :object, nullable: true,
|
645
|
+
# properties: { name: { type: :string, nullable: true } }
|
646
|
+
# }
|
647
|
+
# }
|
648
|
+
# }
|
649
|
+
#
|
650
|
+
# @return [Hash] The arrays of the model and their schemas.
|
651
|
+
def array_types
|
652
|
+
{}
|
653
|
+
end
|
654
|
+
|
655
|
+
# Returns the attributes that are optional in the request body. This means that they are not required to be present in the request body thus they are taken out of the required array.
|
656
|
+
#
|
657
|
+
# @example
|
658
|
+
# [:name, :email]
|
659
|
+
#
|
660
|
+
# @return [Array<Symbol>] The attributes that are optional in the request body.
|
661
|
+
def optional_request_attributes
|
662
|
+
%i[]
|
663
|
+
end
|
664
|
+
|
665
|
+
# Returns the attributes that are nullable in the request/response body. This means that they can be present in the request/response body but they can be null.
|
666
|
+
#
|
667
|
+
# They are not required to be present in the request body.
|
668
|
+
#
|
669
|
+
# @example
|
670
|
+
# [:name, :email]
|
671
|
+
#
|
672
|
+
# @return [Array<Symbol>] The attributes that are nullable in the request/response body.
|
673
|
+
def nullable_attributes
|
674
|
+
%i[]
|
675
|
+
end
|
676
|
+
|
677
|
+
# Returns the additional request attributes that are not automatically generated. These attributes are appended to the request schema.
|
678
|
+
#
|
679
|
+
# @example
|
680
|
+
# {
|
681
|
+
# name: { type: :string }
|
682
|
+
# }
|
683
|
+
#
|
684
|
+
# @return [Hash] The additional request attributes that are not automatically generated (if any).
|
685
|
+
def additional_request_attributes
|
686
|
+
{}
|
687
|
+
end
|
688
|
+
|
689
|
+
# Returns the additional response attributes that are not automatically generated. These attributes are appended to the response schema.
|
690
|
+
#
|
691
|
+
# @example
|
692
|
+
# {
|
693
|
+
# name: { type: :string }
|
694
|
+
# }
|
695
|
+
#
|
696
|
+
# @return [Hash] The additional response attributes that are not automatically generated (if any).
|
697
|
+
def additional_response_attributes
|
698
|
+
{}
|
699
|
+
end
|
700
|
+
|
701
|
+
# Returns the additional response relations that are not automatically generated. These relations are appended to the response schema's relationships.
|
702
|
+
#
|
703
|
+
# @example
|
704
|
+
# {
|
705
|
+
# users: {
|
706
|
+
# type: :object,
|
707
|
+
# properties: {
|
708
|
+
# data: {
|
709
|
+
# type: :array,
|
710
|
+
# items: {
|
711
|
+
# type: :object,
|
712
|
+
# properties: {
|
713
|
+
# id: { type: :string },
|
714
|
+
# type: { type: :string }
|
715
|
+
# }
|
716
|
+
# }
|
717
|
+
# }
|
718
|
+
# }
|
719
|
+
# }
|
720
|
+
# }
|
721
|
+
#
|
722
|
+
# @return [Hash] The additional response relations that are not automatically generated (if any).
|
723
|
+
def additional_response_relations
|
724
|
+
{}
|
725
|
+
end
|
726
|
+
|
727
|
+
# Returns the additional response included that are not automatically generated. These included are appended to the response schema's included.
|
728
|
+
#
|
729
|
+
# @example
|
730
|
+
# {
|
731
|
+
# type: :object,
|
732
|
+
# properties: {
|
733
|
+
# id: { type: :string },
|
734
|
+
# type: { type: :string },
|
735
|
+
# attributes: {
|
736
|
+
# type: :object,
|
737
|
+
# properties: {
|
738
|
+
# name: { type: :string }
|
739
|
+
# }
|
740
|
+
# }
|
741
|
+
# }
|
742
|
+
# }
|
743
|
+
#
|
744
|
+
# @return [Hash] The additional response included that are not automatically generated (if any).
|
745
|
+
def additional_response_included
|
746
|
+
{}
|
747
|
+
end
|
748
|
+
|
749
|
+
# Returns the attributes that are excluded from the request schema.
|
750
|
+
# These attributes are not required or not needed to be present in the request body.
|
751
|
+
#
|
752
|
+
# @example
|
753
|
+
# [:id, :updated_at, :created_at]
|
754
|
+
#
|
755
|
+
# @return [Array<Symbol>] The attributes that are excluded from the request schema.
|
756
|
+
def excluded_request_attributes
|
757
|
+
%i[]
|
758
|
+
end
|
759
|
+
|
760
|
+
# Returns the attributes that are excluded from the response schema.
|
761
|
+
# These attributes are not needed to be present in the response body.
|
762
|
+
#
|
763
|
+
# @example
|
764
|
+
# [:id, :updated_at, :created_at]
|
765
|
+
#
|
766
|
+
# @return [Array<Symbol>] The attributes that are excluded from the response schema.
|
767
|
+
def excluded_response_attributes
|
768
|
+
%i[]
|
769
|
+
end
|
770
|
+
|
771
|
+
# Returns the relationships that are excluded from the response schema.
|
772
|
+
# These relationships are not needed to be present in the response body.
|
773
|
+
#
|
774
|
+
# @example
|
775
|
+
# [:users, :applicants]
|
776
|
+
#
|
777
|
+
# @return [Array<Symbol>] The relationships that are excluded from the response schema.
|
778
|
+
def excluded_response_relations
|
779
|
+
%i[]
|
780
|
+
end
|
781
|
+
|
782
|
+
# Returns the included that are excluded from the response schema.
|
783
|
+
# These included are not needed to be present in the response body.
|
784
|
+
#
|
785
|
+
# @todo This method is not used anywhere yet.
|
786
|
+
#
|
787
|
+
# @example
|
788
|
+
# [:users, :applicants]
|
789
|
+
#
|
790
|
+
# @return [Array<Symbol>] The included that are excluded from the response schema.
|
791
|
+
def excluded_response_included
|
792
|
+
%i[]
|
793
|
+
end
|
794
|
+
|
795
|
+
# Returns the relationships to be further expanded in the response schema.
|
796
|
+
#
|
797
|
+
# @example
|
798
|
+
# {
|
799
|
+
# applicants: {
|
800
|
+
# belongs_to: {
|
801
|
+
# district: Swagger::Definitions::District,
|
802
|
+
# province: Swagger::Definitions::Province,
|
803
|
+
# },
|
804
|
+
# has_many: {
|
805
|
+
# attachments: Swagger::Definitions::Upload,
|
806
|
+
# }
|
807
|
+
# }
|
808
|
+
# }
|
809
|
+
#
|
810
|
+
# @return [Hash] The relationships to be further expanded in the response schema.
|
811
|
+
def nested_relationships
|
812
|
+
{}
|
813
|
+
end
|
814
|
+
|
815
|
+
# Returns the model class (Constantized from the definition class name)
|
816
|
+
#
|
817
|
+
# @example
|
818
|
+
# User
|
819
|
+
#
|
820
|
+
# @return [Class] The model class (Constantized from the definition class name)
|
821
|
+
def model
|
822
|
+
self.class.name.gsub("Swagger::Definitions::", '').constantize
|
823
|
+
end
|
824
|
+
|
825
|
+
# Returns the model name. Used for schema type naming.
|
826
|
+
#
|
827
|
+
# @example
|
828
|
+
# 'users' for the User model
|
829
|
+
# 'citizen_applications' for the CitizenApplication model
|
830
|
+
#
|
831
|
+
# @return [String] The model name.
|
832
|
+
def self.model_name
|
833
|
+
name.gsub("Swagger::Definitions::", '').pluralize.underscore.downcase
|
834
|
+
end
|
835
|
+
|
836
|
+
# Returns the generated schemas in JSONAPI format that are used in the swagger documentation.
|
837
|
+
#
|
838
|
+
# @note This method is used for generating schema in 3 different formats: request, response and response expanded.
|
839
|
+
# request: The schema for the request body.
|
840
|
+
# response: The schema for the response body (without any relationships expanded), used for collection responses.
|
841
|
+
# response expanded: The schema for the response body with all the relationships expanded, used for single resource responses.
|
842
|
+
#
|
843
|
+
# @note The returned schemas are in JSONAPI format are usually appended to the rswag component's 'schemas' in swagger_helper.
|
844
|
+
#
|
845
|
+
# @note The method can be overridden in the definition class if there are any additional customizations needed.
|
846
|
+
#
|
847
|
+
# @return [Array<Hash>] The generated schemas in JSONAPI format that are used in the swagger documentation.
|
848
|
+
def self.definitions
|
849
|
+
schema_instance = self.new
|
850
|
+
[
|
851
|
+
"#{schema_instance.model}Request": schema_instance.camelize_keys(schema_instance.request_schema),
|
852
|
+
"#{schema_instance.model}Response": schema_instance.camelize_keys(schema_instance.response_schema(multi: true)),
|
853
|
+
"#{schema_instance.model}ResponseExpanded": schema_instance.camelize_keys(schema_instance.response_schema(expand: true))
|
854
|
+
]
|
855
|
+
end
|
856
|
+
|
857
|
+
# Given a hash, it returns a new hash with all the keys camelized.
|
858
|
+
#
|
859
|
+
# @param hash [Array | Hash] The hash with all the keys camelized.
|
860
|
+
#
|
861
|
+
# @example
|
862
|
+
# { first_name: 'John', last_name: 'Doe' } => { firstName: 'John', lastName: 'Doe' }
|
863
|
+
#
|
864
|
+
# @return [Array | Hash] The hash with all the keys camelized.
|
865
|
+
def camelize_keys(hash)
|
866
|
+
hash.deep_transform_keys { |key| key.to_s.camelize(:lower).to_sym }
|
867
|
+
end
|
868
|
+
end
|
869
|
+
end
|