schemable 0.1.0

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