schemable 0.1.3 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +5 -1
- data/Gemfile +5 -5
- data/Gemfile.lock +82 -50
- data/README.md +324 -100
- data/lib/generators/schemable/install_generator.rb +3 -13
- data/lib/generators/schemable/model_generator.rb +1 -16
- data/lib/schemable/attribute_schema_generator.rb +130 -0
- data/lib/schemable/configuration.rb +114 -0
- data/lib/schemable/definition.rb +322 -0
- data/lib/schemable/included_schema_generator.rb +107 -0
- data/lib/schemable/relationship_schema_generator.rb +119 -0
- data/lib/schemable/request_schema_generator.rb +88 -0
- data/lib/schemable/response_schema_generator.rb +124 -0
- data/lib/schemable/schema_modifier.rb +248 -0
- data/lib/schemable/version.rb +1 -1
- data/lib/schemable.rb +60 -927
- data/lib/templates/schemable.rb +76 -0
- data/schemable.gemspec +2 -5
- data/sig/schemable/attribute_schema_generator.rbs +13 -0
- data/sig/schemable/configuration.rbs +21 -0
- data/sig/schemable/definition.rbs +32 -0
- data/sig/schemable/included_schema_generator.rbs +11 -0
- data/sig/schemable/relationship_schema_generator.rbs +11 -0
- data/sig/schemable/request_schema_generator.rbs +10 -0
- data/sig/schemable/response_schema_generator.rbs +13 -0
- data/sig/schemable/schema_modifier.rbs +9 -0
- data/sig/schemable.rbs +4 -1
- metadata +23 -36
- data/lib/templates/common_definitions.rb +0 -13
- data/lib/templates/serializers_helper.rb +0 -7
@@ -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
|