schemable 0.1.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,130 @@
1
+ module Schemable
2
+ # The AttributeSchemaGenerator class is responsible for generating JSON schemas for model attributes.
3
+ # It includes methods for generating the overall schema and individual attribute schemas.
4
+ #
5
+ # @see Schemable
6
+ class AttributeSchemaGenerator
7
+ attr_reader :model_definition, :configuration, :model, :schema_modifier, :response
8
+
9
+ # Initializes a new AttributeSchemaGenerator instance.
10
+ #
11
+ # @param model_definition [ModelDefinition] The model definition to generate the schema for.
12
+ # @example
13
+ # generator = AttributeSchemaGenerator.new(model_definition)
14
+ def initialize(model_definition)
15
+ @model_definition = model_definition
16
+ @model = model_definition.model
17
+ @configuration = Schemable.configuration
18
+ @schema_modifier = SchemaModifier.new
19
+ @response = nil
20
+ end
21
+
22
+ # Generates the JSON schema for the model attributes.
23
+ #
24
+ # @return [Hash] The generated schema.
25
+ # @example
26
+ # schema = generator.generate
27
+ def generate
28
+ schema = {
29
+ type: :object,
30
+ properties: @model_definition.attributes.index_with do |attr|
31
+ generate_attribute_schema(attr)
32
+ end
33
+ }
34
+
35
+ # Rename enum attributes to remove the suffix or prefix if mongoid is used
36
+ if @configuration.orm == :mongoid
37
+ schema[:properties].transform_keys! do |key|
38
+ key.to_s.gsub(@configuration.enum_prefix_for_simple_enum || @configuration.enum_suffix_for_simple_enum, '')
39
+ end
40
+ end
41
+
42
+ # modify the schema to include additional response relations
43
+ schema = @schema_modifier.add_properties(schema, @model_definition.additional_response_attributes, 'properties')
44
+
45
+ # modify the schema to exclude response relations
46
+ @model_definition.excluded_response_attributes.each do |key|
47
+ schema = @schema_modifier.delete_properties(schema, "properties.#{key}")
48
+ end
49
+
50
+ schema
51
+ end
52
+
53
+ # Generates the JSON schema for a specific attribute.
54
+ #
55
+ # @param attribute [Symbol, String] The attribute to generate the schema for.
56
+ # @return [Hash] The generated schema for the attribute.
57
+ # @example
58
+ # attribute_schema = generator.generate_attribute_schema(:attribute_name)
59
+ def generate_attribute_schema(attribute)
60
+ if @configuration.orm == :mongoid
61
+ # Get the column hash for the attribute
62
+ attribute_hash = @model.fields[attribute.to_s]
63
+
64
+ # Check if this attribute has a custom JSON Schema definition
65
+ return @model_definition.array_types[attribute] if @model_definition.array_types.keys.include?(attribute.to_sym)
66
+ return @model_definition.additional_response_attributes[attribute] if @model_definition.additional_response_attributes.keys.include?(attribute)
67
+
68
+ # Check if this is an array attribute
69
+ return @configuration.type_mapper(:array) if attribute_hash.try(:[], 'options').try(:[], 'type') == 'Array'
70
+
71
+ # Check if this is an enum attribute
72
+ @response = if attribute_hash.name.end_with?('_cd')
73
+ @configuration.type_mapper(:string)
74
+ else
75
+ # Map the column type to a JSON Schema type if none of the above conditions are met
76
+ @configuration.type_mapper(attribute_hash.try(:type).to_s.downcase.to_sym)
77
+ end
78
+
79
+ elsif @configuration.orm == :active_record
80
+ # Get the column hash for the attribute
81
+ attribute_hash = @model.columns_hash[attribute.to_s]
82
+
83
+ # Check if this attribute has a custom JSON Schema definition
84
+ return @model_definition.array_types[attribute] if @model_definition.array_types.keys.include?(attribute.to_sym)
85
+ return @model_definition.additional_response_attributes[attribute] if @model_definition.additional_response_attributes.keys.include?(attribute)
86
+
87
+ # Check if this is an array attribute
88
+ return @configuration.type_mapper(:array) if attribute_hash.as_json.try(:[], 'sql_type_metadata').try(:[], 'sql_type').include?('[]')
89
+
90
+ # Map the column type to a JSON Schema type if none of the above conditions are met
91
+ @response = @configuration.type_mapper(attribute_hash.try(:type))
92
+
93
+ else
94
+ raise 'ORM not supported'
95
+ end
96
+
97
+ # If the attribute is nullable, modify the schema accordingly
98
+ return @schema_modifier.add_properties(@response, { nullable: true }, '.') if @response && @model_definition.nullable_attributes.include?(attribute)
99
+
100
+ # If attribute is an enum, modify the schema accordingly
101
+ if @configuration.custom_defined_enum_method && @model.respond_to?(@configuration.custom_defined_enum_method)
102
+ defined_enums = @model.send(@configuration.custom_defined_enum_method)
103
+ enum_attribute = attribute.to_s.gsub(@configuration.enum_prefix_for_simple_enum || @configuration.enum_suffix_for_simple_enum, '').to_s
104
+ return @schema_modifier.add_properties(@response, { enum: defined_enums[enum_attribute].keys }, '.') if @response && defined_enums[enum_attribute].present?
105
+ elsif @model.respond_to?(:defined_enums)
106
+ return @schema_modifier.add_properties(@response, { enum: @model.defined_enums[attribute.to_s].keys }, '.') if @response && @model.defined_enums.key?(attribute.to_s)
107
+ end
108
+
109
+ return @response unless @response.nil?
110
+
111
+ # 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
112
+ if @configuration.use_serialized_instance
113
+ serialized_instance = @model_definition.serialized_instance
114
+
115
+ type_from_instance = serialized_instance.as_json['data']['attributes'][attribute.to_s.camelize(:lower)]&.class&.name&.downcase
116
+
117
+ @response = @configuration.type_mapper(type_from_instance) if type_from_instance.present?
118
+
119
+ return @response unless @response.nil?
120
+ end
121
+
122
+ # If we still haven't found a schema type, default to object
123
+ @configuration.type_mapper(:object)
124
+ rescue NoMethodError
125
+ # Log a warning if the attribute does not exist on the @model
126
+ Rails.logger.warn("\e[33mWARNING: #{@model} does not have an attribute named \e[31m#{attribute}\e[0m")
127
+ {}
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,114 @@
1
+ module Schemable
2
+ # The Configuration class provides a set of configuration options for the Schemable module.
3
+ # It includes options for setting the ORM, handling enums, custom type mappers, and more.
4
+ # It is worth noting that the configuration options are global, and will affect all Definitions.
5
+ #
6
+ # @see Schemable
7
+ class Configuration
8
+ attr_accessor(
9
+ :orm,
10
+ :float_as_string,
11
+ :decimal_as_string,
12
+ :pagination_enabled,
13
+ :custom_type_mappers,
14
+ :use_serialized_instance,
15
+ :custom_defined_enum_method,
16
+ :enum_prefix_for_simple_enum,
17
+ :enum_suffix_for_simple_enum,
18
+ :custom_meta_response_schema,
19
+ :infer_attributes_from_custom_method,
20
+ :infer_attributes_from_jsonapi_serializable
21
+ )
22
+
23
+ # Initializes a new Configuration instance with default values.
24
+ def initialize
25
+ @orm = :active_record # orm options are :active_record, :mongoid
26
+ @float_as_string = false
27
+ @custom_type_mappers = {}
28
+ @pagination_enabled = true
29
+ @decimal_as_string = false
30
+ @use_serialized_instance = false
31
+ @custom_defined_enum_method = nil
32
+ @custom_meta_response_schema = nil
33
+ @enum_prefix_for_simple_enum = nil
34
+ @enum_suffix_for_simple_enum = nil
35
+ @infer_attributes_from_custom_method = nil
36
+ @infer_attributes_from_jsonapi_serializable = false
37
+ end
38
+
39
+ # Returns a type mapper for a given type name.
40
+ #
41
+ # @note If a custom type mapper is defined for the given type name, it will be returned.
42
+ #
43
+ # @example
44
+ # type_mapper(:string) #=> { type: :string }
45
+ #
46
+ # @param type_name [Symbol, String] The name of the type.
47
+ # @return [Hash] The type mapper for the given type name.
48
+ def type_mapper(type_name)
49
+ return @custom_type_mappers[type_name] if @custom_type_mappers.key?(type_name.to_sym)
50
+
51
+ {
52
+ text: { type: :string },
53
+ string: { type: :string },
54
+ symbol: { type: :string },
55
+ integer: { type: :integer },
56
+ boolean: { type: :boolean },
57
+ date: { type: :string, format: :date },
58
+ time: { type: :string, format: :time },
59
+ json: { type: :object, properties: {} },
60
+ hash: { type: :object, properties: {} },
61
+ jsonb: { type: :object, properties: {} },
62
+ object: { type: :object, properties: {} },
63
+ binary: { type: :string, format: :binary },
64
+ trueclass: { type: :boolean, default: true },
65
+ falseclass: { type: :boolean, default: false },
66
+ datetime: { type: :string, format: :'date-time' },
67
+ big_decimal: { type: (@decimal_as_string ? :string : :number).to_s.to_sym, format: :double },
68
+ 'bson/objectid': { type: :string, format: :object_id },
69
+ 'mongoid/boolean': { type: :boolean },
70
+ 'mongoid/stringified_symbol': { type: :string },
71
+ 'active_support/time_with_zone': { type: :string, format: :date_time },
72
+ float: {
73
+ type: (@float_as_string ? :string : :number).to_s.to_sym,
74
+ format: :float
75
+ },
76
+ decimal: {
77
+ type: (@decimal_as_string ? :string : :number).to_s.to_sym,
78
+ format: :double
79
+ },
80
+ array: {
81
+ type: :array,
82
+ items: {
83
+ anyOf: [
84
+ { type: :string },
85
+ { type: :integer },
86
+ { type: :boolean },
87
+ { type: :number, format: :float },
88
+ { type: :object, properties: {} },
89
+ { type: :number, format: :double }
90
+ ]
91
+ }
92
+ }
93
+ }[type_name.to_s.underscore.try(:to_sym)]
94
+ end
95
+
96
+ # Adds a custom type mapper for a given type name.
97
+ #
98
+ # @example
99
+ # add_custom_type_mapper(:custom_type, { type: :custom })
100
+ # type_mapper(:custom_type) #=> { type: :custom }
101
+ #
102
+ # # It preferable to invoke this method in the config/initializers/schemable.rb file.
103
+ # # This way, the custom type mapper will be available for all Definitions.
104
+ # Schemable.configure do |config|
105
+ # config.add_custom_type_mapper(:custom_type, { type: :custom })
106
+ # end
107
+ #
108
+ # @param type_name [Symbol, String] The name of the type.
109
+ # @param mapping [Hash] The mapping to add.
110
+ def add_custom_type_mapper(type_name, mapping)
111
+ custom_type_mappers[type_name.to_sym] = mapping
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,322 @@
1
+ module Schemable
2
+ # The Definition class provides a blueprint for generating and modifying schemas.
3
+ # It includes methods for handling attributes, relationships, and various request and response attributes.
4
+ # The definition class is meant to be inherited by a class that represents a model.
5
+ # This class should be configured to match the model's attributes and relationships.
6
+ # The default configuration is set in this class, but can be overridden in the model's definition class.
7
+ #
8
+ # @see Schemable
9
+ class Definition
10
+ attr_reader :configuration
11
+ attr_writer :relationships, :additional_create_request_attributes, :additional_update_request_attributes
12
+
13
+ def initialize
14
+ @configuration = Schemable.configuration
15
+ end
16
+
17
+ # Returns the serializer of the model for the definition.
18
+ # @example
19
+ # UsersSerializer
20
+ # @return [JSONAPI::Serializable::Resource, nil] The model's serializer.
21
+ def serializer
22
+ raise NotImplementedError, 'You must implement the serializer method in the definition class in order to use the infer_serializer_from_jsonapi_serializable configuration option.' if configuration.infer_attributes_from_jsonapi_serializable
23
+
24
+ nil
25
+ end
26
+
27
+ # Returns the attributes for the definition based on the configuration.
28
+ # The attributes are inferred from the model's attribute names by default.
29
+ # If the infer_attributes_from_custom_method configuration option is set, the attributes are inferred from the method specified.
30
+ # If the infer_attributes_from_jsonapi_serializable configuration option is set, the attributes are inferred from the serializer's attribute blocks.
31
+ #
32
+ # @example
33
+ # attributes = definition.attributes # => [:id, :name, :email]
34
+ #
35
+ # @return [Array<Symbol>] The attributes used for generating the schemas.
36
+ def attributes
37
+ return (serializer&.attribute_blocks&.transform_keys { |key| key.to_s.underscore.to_sym }&.keys || nil) if configuration.infer_attributes_from_jsonapi_serializable
38
+
39
+ return model.send(configuration.infer_attributes_from_custom_method).map(&:to_sym) if configuration.infer_attributes_from_custom_method
40
+
41
+ model.attribute_names.map(&:to_sym)
42
+ end
43
+
44
+ # Returns the relationships defined in the serializer.
45
+ #
46
+ # @note Note that the format of the relationships is as follows:
47
+ # {
48
+ # belongs_to: { relationship_name: relationship_definition },
49
+ # has_many: { relationship_name: relationship_definition },
50
+ # addition_to_included: { relationship_name: relationship_definition }
51
+ # }
52
+ #
53
+ # @note The addition_to_included is used to define the extra nested relationships that are not defined in the belongs_to or has_many for included.
54
+ #
55
+ # @example
56
+ # {
57
+ # belongs_to: {
58
+ # district: Swagger::Definitions::District,
59
+ # user: Swagger::Definitions::User
60
+ # },
61
+ # has_many: {
62
+ # applicants: Swagger::Definitions::Applicant,
63
+ # },
64
+ # addition_to_included: {
65
+ # applicants: Swagger::Definitions::Applicant
66
+ # }
67
+ # }
68
+ #
69
+ # @return [Hash] The relationships defined in the serializer.
70
+ def relationships
71
+ { belongs_to: {}, has_many: {} }
72
+ end
73
+
74
+ # Returns a hash of all the arrays defined for the model.
75
+ # The schema for each array is defined in the definition class manually.
76
+ # This method must be implemented in the definition class if there are any arrays.
77
+ #
78
+ # @return [Hash] The arrays of the model and their schemas.
79
+ #
80
+ # @example
81
+ # {
82
+ # metadata: {
83
+ # type: :array,
84
+ # items: {
85
+ # type: :object, nullable: true,
86
+ # properties: { name: { type: :string, nullable: true } }
87
+ # }
88
+ # }
89
+ # }
90
+ def array_types
91
+ {}
92
+ end
93
+
94
+ # Attributes that are not required in the create request.
95
+ #
96
+ # @example
97
+ # optional_create_request_attributes = definition.optional_create_request_attributes
98
+ # # => [:email]
99
+ #
100
+ # @return [Array<Symbol>] The attributes that are not required in the create request.
101
+ def optional_create_request_attributes
102
+ %i[]
103
+ end
104
+
105
+ # Attributes that are not required in the update request.
106
+ #
107
+ # @example
108
+ # optional_update_request_attributes = definition.optional_update_request_attributes
109
+ # # => [:email]
110
+ #
111
+ # @return [Array<Symbol>] The attributes that are not required in the update request.
112
+ def optional_update_request_attributes
113
+ %i[]
114
+ end
115
+
116
+ # Returns the attributes that are nullable in the request/response body.
117
+ # This means that they can be present in the request/response body but they can be null.
118
+ # They are not required to be present in the request body.
119
+ #
120
+ # @example
121
+ # [:name, :email]
122
+ #
123
+ # @return [Array<Symbol>] The attributes that are nullable in the request/response body.
124
+ def nullable_attributes
125
+ %i[]
126
+ end
127
+
128
+ # Returns the additional create request attributes that are not automatically generated.
129
+ # These attributes are appended to the create request schema.
130
+ #
131
+ # @example
132
+ # { name: { type: :string } }
133
+ #
134
+ # @return [Hash] The additional create request attributes that are not automatically generated (if any).
135
+ def additional_create_request_attributes
136
+ {}
137
+ end
138
+
139
+ # Returns the additional update request attributes that are not automatically generated.
140
+ # These attributes are appended to the update request schema.
141
+ #
142
+ # @example
143
+ # { name: { type: :string } }
144
+ #
145
+ # @return [Hash] The additional update request attributes that are not automatically generated (if any).
146
+ def additional_update_request_attributes
147
+ {}
148
+ end
149
+
150
+ # Returns the additional response attributes that are not automatically generated. These attributes are appended to the response schema.
151
+ #
152
+ # @example
153
+ # { name: { type: :string } }
154
+ #
155
+ # @return [Hash] The additional response attributes that are not automatically generated (if any).
156
+ def additional_response_attributes
157
+ {}
158
+ end
159
+
160
+ # Returns the additional response relations that are not automatically generated.
161
+ # These relations are appended to the response schema's relationships.
162
+ #
163
+ # @example
164
+ # {
165
+ # users: {
166
+ # type: :object,
167
+ # properties: {
168
+ # data: {
169
+ # type: :array,
170
+ # items: {
171
+ # type: :object,
172
+ # properties: {
173
+ # id: { type: :string },
174
+ # type: { type: :string }
175
+ # }
176
+ # }
177
+ # }
178
+ # }
179
+ # }
180
+ # }
181
+ #
182
+ # @return [Hash] The additional response relations that are not automatically generated (if any).
183
+ def additional_response_relations
184
+ {}
185
+ end
186
+
187
+ # Returns the additional response included that are not automatically generated.
188
+ # These included additions are appended to the response schema's included.
189
+ #
190
+ # @example
191
+ # {
192
+ # type: :object,
193
+ # properties: {
194
+ # id: { type: :string },
195
+ # type: { type: :string },
196
+ # attributes: {
197
+ # type: :object,
198
+ # properties: {
199
+ # name: { type: :string }
200
+ # }
201
+ # }
202
+ # }
203
+ # }
204
+ #
205
+ # @return [Hash] The additional response included that are not automatically generated (if any).
206
+ def additional_response_included
207
+ {}
208
+ end
209
+
210
+ # Returns the attributes that are excluded from the create request schema.
211
+ # These attributes are not required or not needed to be present in the create request body.
212
+ #
213
+ # @example
214
+ # [:id, :updated_at, :created_at]
215
+ #
216
+ # @return [Array<Symbol>] The attributes that are excluded from the create request schema.
217
+ def excluded_create_request_attributes
218
+ %i[]
219
+ end
220
+
221
+ # Returns the attributes that are excluded from the response schema.
222
+ # These attributes are not needed to be present in the response body.
223
+ #
224
+ # @example
225
+ # [:id, :updated_at, :created_at]
226
+ #
227
+ # @return [Array<Symbol>] The attributes that are excluded from the response schema.
228
+ def excluded_update_request_attributes
229
+ %i[]
230
+ end
231
+
232
+ # Returns the attributes that are excluded from the update request schema.
233
+ # These attributes are not required or not needed to be present in the update request body.
234
+ #
235
+ # @example
236
+ # [:id, :updated_at, :created_at]
237
+ #
238
+ # @return [Array<Symbol>] The attributes that are excluded from the update request schema.
239
+ def excluded_response_attributes
240
+ %i[]
241
+ end
242
+
243
+ # Returns the relationships that are excluded from the response schema.
244
+ # These relationships are not needed to be present in the response body.
245
+ #
246
+ # @example
247
+ # [:users, :applicants]
248
+ #
249
+ # @return [Array<Symbol>] The relationships that are excluded from the response schema.
250
+ def excluded_response_relations
251
+ %i[]
252
+ end
253
+
254
+ # Returns the included that are excluded from the response schema.
255
+ # These included are not needed to be present in the response body.
256
+ #
257
+ # @example
258
+ # [:users, :applicants]
259
+ #
260
+ # @todo
261
+ # This method is not used anywhere yet.
262
+ #
263
+ # @return [Array<Symbol>] The included that are excluded from the response schema.
264
+ def excluded_response_included
265
+ %i[]
266
+ end
267
+
268
+ # Returns an instance of the model class that is already serialized into jsonapi format.
269
+ #
270
+ # @return [Hash] The serialized instance of the model class.
271
+ def serialized_instance
272
+ {}
273
+ end
274
+
275
+ # Returns the model class (Constantized from the definition class name)
276
+ #
277
+ # @example
278
+ # User
279
+ #
280
+ # @return [Class] The model class (Constantized from the definition class name)
281
+ def model
282
+ self.class.name.gsub('Swagger::Definitions::', '').constantize
283
+ end
284
+
285
+ # Returns the model name. Used for schema type naming.
286
+ #
287
+ # @return [String] The model name.
288
+ #
289
+ # @example
290
+ # 'users' for the User model
291
+ # 'citizen_applications' for the CitizenApplication model
292
+ def model_name
293
+ self.class.name.gsub('Swagger::Definitions::', '').pluralize.underscore.downcase
294
+ end
295
+
296
+ # Given a hash, it returns a new hash with all the keys camelized.
297
+ #
298
+ # @param hash [Hash] The hash with all the keys camelized.
299
+ #
300
+ # @return [Hash, Array] The hash with all the keys camelized.
301
+ #
302
+ # @example
303
+ # { first_name: 'John', last_name: 'Doe' } => { firstName: 'John', lastName: 'Doe' }
304
+ def camelize_keys(hash)
305
+ hash.deep_transform_keys { |key| key.to_s.camelize(:lower).to_sym }
306
+ end
307
+
308
+ # Returns the schema for the create request body, update request body, and response body.
309
+ #
310
+ # @return [Array<Hash>] The schema for the create request body, update request body, and response body.
311
+ def self.generate
312
+ instance = new
313
+
314
+ [
315
+ "#{instance.model}CreateRequest": instance.camelize_keys(RequestSchemaGenerator.new(instance).generate_for_create),
316
+ "#{instance.model}UpdateRequest": instance.camelize_keys(RequestSchemaGenerator.new(instance).generate_for_update),
317
+ "#{instance.model}Response": instance.camelize_keys(ResponseSchemaGenerator.new(instance).generate(collection: true)),
318
+ "#{instance.model}ResponseExpanded": instance.camelize_keys(ResponseSchemaGenerator.new(instance).generate(expand: true))
319
+ ]
320
+ end
321
+ end
322
+ end
@@ -0,0 +1,107 @@
1
+ module Schemable
2
+ # The IncludedSchemaGenerator class is responsible for generating the 'included' part of a JSON:API compliant response.
3
+ # This class generates schemas for related resources that should be included in the response.
4
+ #
5
+ # @see Schemable
6
+ class IncludedSchemaGenerator
7
+ attr_reader :model_definition, :schema_modifier, :relationships
8
+
9
+ # Initializes a new IncludedSchemaGenerator instance.
10
+ #
11
+ # @param model_definition [ModelDefinition] The model definition to generate the schema for.
12
+ #
13
+ # @example
14
+ # generator = IncludedSchemaGenerator.new(model_definition)
15
+ def initialize(model_definition)
16
+ @model_definition = model_definition
17
+ @schema_modifier = SchemaModifier.new
18
+ @relationships = @model_definition.relationships
19
+ end
20
+
21
+ # Generates the 'included' part of the JSON:API response.
22
+ # It iterates over each relationship type (belongs_to, has_many) and for each relationship,
23
+ # it prepares a schema. If the 'expand' option is true, it also includes the relationships of the related resource in the schema.
24
+ # In that case, the 'addition_to_included' relationships are also included in the schema unless they are excluded from expansion.
25
+ #
26
+ # @param expand [Boolean] Whether to include the relationships of the related resource in the schema.
27
+ # @param relationships_to_exclude_from_expansion [Array] The relationships to exclude from the schema.
28
+ #
29
+ # @note Make sure to provide the names correctly in string format and pluralize them if necessary.
30
+ # For example, if you have a relationship named 'applicant', and an applicant has association
31
+ # named 'identity', you should provide 'identities' as the names of the relationship to exclude from expansion.
32
+ # In this case, the included schema of the applicant will not include the identity relationship.
33
+ #
34
+ # @example
35
+ # schema = generator.generate(expand: true, relationships_to_exclude_from_expansion: ['some_relationship'])
36
+ #
37
+ # @return [Hash] The generated schema.
38
+ def generate(expand: false, relationships_to_exclude_from_expansion: [])
39
+ return {} if @relationships.blank?
40
+ return {} if @relationships == { belongs_to: {}, has_many: {} }
41
+
42
+ definitions = []
43
+
44
+ %i[belongs_to has_many addition_to_included].each do |relation_type|
45
+ next if @relationships[relation_type].blank?
46
+
47
+ definitions << @relationships[relation_type].values
48
+ end
49
+
50
+ definitions.flatten!
51
+
52
+ included_schemas = definitions.map do |definition|
53
+ next if relationships_to_exclude_from_expansion.include?(definition.model_name)
54
+
55
+ if expand
56
+ definition_relations = definition.relationships[:belongs_to].values.map(&:model_name) + definition.relationships[:has_many].values.map(&:model_name)
57
+ relations_to_exclude = []
58
+ definition_relations.each do |relation|
59
+ relations_to_exclude << relation if relationships_to_exclude_from_expansion.include?(relation)
60
+ end
61
+
62
+ prepare_schema_for_included(definition, expand:, relationships_to_exclude_from_expansion: relations_to_exclude)
63
+ else
64
+ prepare_schema_for_included(definition)
65
+ end
66
+ end
67
+
68
+ schema = {
69
+ included: {
70
+ type: :array,
71
+ items: {
72
+ anyOf: included_schemas.compact_blank
73
+ }
74
+ }
75
+ }
76
+
77
+ @schema_modifier.add_properties(schema, @model_definition.additional_response_included, 'included.items') if @model_definition.additional_response_included.present?
78
+
79
+ schema
80
+ end
81
+
82
+ # Prepares the schema for a related resource to be included in the response.
83
+ # It generates the attribute and relationship schemas for the related resource and combines them into a single schema.
84
+ #
85
+ # @param model_definition [ModelDefinition] The model definition of the related resource.
86
+ # @param expand [Boolean] Whether to include the relationships of the related resource in the schema.
87
+ # @param relationships_to_exclude_from_expansion [Array] The relationships to exclude from the schema.
88
+ #
89
+ # @example
90
+ # included_schema = generator.prepare_schema_for_included(related_model_definition, expand: true, relationships_to_exclude_from_expansion: ['some_relationship'])
91
+ #
92
+ # @return [Hash] The generated schema for the related resource.
93
+ def prepare_schema_for_included(model_definition, expand: false, relationships_to_exclude_from_expansion: [])
94
+ attributes_schema = AttributeSchemaGenerator.new(model_definition).generate
95
+ relationships_schema = RelationshipSchemaGenerator.new(model_definition).generate(relationships_to_exclude_from_expansion:, expand:)
96
+
97
+ {
98
+ type: :object,
99
+ properties: {
100
+ type: { type: :string, default: model_definition.model_name },
101
+ id: { type: :string },
102
+ attributes: attributes_schema
103
+ }.merge!(relationships_schema.blank? ? {} : { relationships: relationships_schema })
104
+ }.compact_blank
105
+ end
106
+ end
107
+ end