From 4321390dee0f81c3aefd5dc46d3649fc8b7c535b Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Sat, 29 Jul 2023 00:10:45 +0300 Subject: [PATCH 01/87] Updates .gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index af00795..15391f1 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ # rspec failure tracking .rspec_status -*.gem \ No newline at end of file +*.gem + +.idea \ No newline at end of file From c12bdc807e533e694b71921afd60b8d4776ac356 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Sat, 29 Jul 2023 00:14:08 +0300 Subject: [PATCH 02/87] Updates attributes in excluded_request_attributes generated method --- lib/generators/schemable/model_generator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/generators/schemable/model_generator.rb b/lib/generators/schemable/model_generator.rb index 3c4b05f..467b13e 100644 --- a/lib/generators/schemable/model_generator.rb +++ b/lib/generators/schemable/model_generator.rb @@ -37,7 +37,7 @@ def serializer end def excluded_request_attributes - %i[id updatedAt createdAt] + %i[updated_at created_at] end end end From 33d42c8c8f61c3a0d27e3fab90dc5799a6dd6fad Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Sat, 29 Jul 2023 14:22:33 +0300 Subject: [PATCH 03/87] Fixes bug where required attributes were not caught properly due case difference --- lib/schemable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/schemable.rb b/lib/schemable.rb index 8d5c628..a191639 100644 --- a/lib/schemable.rb +++ b/lib/schemable.rb @@ -529,7 +529,7 @@ def request_schema end required_attributes = { - required: schema.as_json['properties']['data']['properties'].keys - optional_request_attributes.map(&:to_s) - nullable_attributes.map(&:to_s) + required: (schema.as_json['properties']['data']['properties'].keys - optional_request_attributes.map(&:to_s) - nullable_attributes.map(&:to_s)).map { |key| key.to_s.camelize(:lower).to_sym } } schema = modify_schema(schema, required_attributes, "properties.data") From 55c7ec77cb689f087c3c42015abc28929d583dbe Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Sat, 29 Jul 2023 16:27:44 +0300 Subject: [PATCH 04/87] Separates create and update request schema and prettifies documentations --- lib/schemable.rb | 551 ++++++++++++++++++++++++++++------------------- 1 file changed, 325 insertions(+), 226 deletions(-) diff --git a/lib/schemable.rb b/lib/schemable.rb index a191639..9ecdcea 100644 --- a/lib/schemable.rb +++ b/lib/schemable.rb @@ -42,24 +42,31 @@ def type_mapper(type_name) # Modify a JSON schema object by merging new properties into it or deleting a specified path. # # @param original_schema [Hash] The original schema object to modify. + # # @param new_props [Hash] The new properties to merge into the schema. - # @param given_path [String, nil] The path to the property to modify or delete, if any. - # Use dot notation to specify nested properties (e.g. "person.address.city"). + # + # @param given_path [String, nil] The path to the property to modify or delete, if any. Use dot notation to specify nested properties (e.g. "person.address.city"). + # # @param delete [Boolean] Whether to delete the property at the given path, if it exists. + # # @raise [ArgumentError] If `delete` is true but `given_path` is nil, or if `given_path` does not exist in the original schema. # # @return [Hash] A new schema object with the specified modifications. # - # @example Merge new properties into the schema - # original_schema = { type: 'object', properties: { name: { type: 'string' } } } - # new_props = { properties: { age: { type: 'integer' } } } - # modify_schema(original_schema, new_props) - # # => { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } } } + # @example + # `Merge new properties into the schema` + # + # original_schema = { type: 'object', properties: { name: { type: 'string' } } } + # new_props = { properties: { age: { type: 'integer' } } } + # modify_schema(original_schema, new_props) + # => {type: 'object', properties: {name: {type: 'string'}, age: {type: 'integer'}}} + # + # @example + # `Delete a property from the schema` # - # @example Delete a property from the schema - # original_schema = { type: 'object', properties: { name: { type: 'string' } } } - # modify_schema(original_schema, {}, 'properties.name', delete: true) - # # => { type: 'object', properties: {} } + # original_schema = { type: 'object', properties: { name: { type: 'string' } } } + # modify_schema(original_schema, {}, 'properties.name', delete: true) + # => {type: 'object', properties: {}} def modify_schema(original_schema, new_props, given_path = nil, delete: false) return new_props if original_schema.nil? @@ -104,8 +111,7 @@ def modify_schema(original_schema, new_props, given_path = nil, delete: false) # @return [Hash] The JSON Schema attribute definition as a Hash or an empty Hash if the attribute does not exist on the model. # # @example - # attribute_schema(:title) - # # => { "type": "string" } + # attribute_schema(:title) => { "type": "string" } def attribute_schema(attribute) # Get the column hash for the attribute column_hash = model.columns_hash[attribute.to_s] @@ -159,16 +165,16 @@ def attribute_schema(attribute) # # @note The `additional_response_attributes` and `excluded_response_attributes` are applied to the schema returned by this method. # - # @example - # { - # type: :object, - # properties: { - # id: { type: :string }, - # title: { type: :string } - # } - # } - # # @return [Hash] The JSON Schema for the model's attributes. + # + # @example + # { + # type: :object, + # properties: { + # id: { type: :string }, + # title: { type: :string } + # } + # } def attributes_schema schema = { type: :object, @@ -191,35 +197,35 @@ def attributes_schema # Generates the schema for the relationships of a resource. # - # @param relations [Hash] A hash representing the relationships of the resource in the form of { belongs_to: {}, has_many: {} }. - # If not provided, the relationships will be inferred from the model's associations. - # # @note The `additional_response_relations` and `excluded_response_relations` are applied to the schema returned by this method. # + # @param relations [Hash] A hash representing the relationships of the resource in the form of { belongs_to: {}, has_many: {} }. If not provided, the relationships will be inferred from the model's associations. + # # @param expand [Boolean] A boolean indicating whether to expand the relationships in the schema. - # @param exclude_from_expansion [Array] An array of relationship names to exclude from expansion. # - # @example - # { - # type: :object, - # properties: { - # province: { - # type: :object, - # properties: { - # meta: { - # type: :object, - # properties: { - # included: { - # type: :boolean, default: false - # } - # } - # } - # } - # } - # } - # } + # @param exclude_from_expansion [Array] An array of relationship names to exclude from expansion. # # @return [Hash] A hash representing the schema for the relationships. + # + # @example + # { + # type: :object, + # properties: { + # province: { + # type: :object, + # properties: { + # meta: { + # type: :object, + # properties: { + # included: { + # type: :boolean, default: false + # } + # } + # } + # } + # } + # } + # } def relationships_schema(relations = try(:relationships), expand: false, exclude_from_expansion: []) return {} if relations.blank? return {} if relations == { belongs_to: {}, has_many: {} } @@ -306,39 +312,41 @@ def relationships_schema(relations = try(:relationships), expand: false, exclude # # @note The `additional_response_includes` and `excluded_response_includes` (yet to be implemented) are applied to the schema returned by this method. # - # @param relations [Hash] A hash representing the relationships of the resource in the form of { belongs_to: {}, has_many: {} }. - # If not provided, the relationships will be inferred from the model's associations. + # @param relations [Hash] A hash representing the relationships of the resource in the form of { belongs_to: {}, has_many: {} }. If not provided, the relationships will be inferred from the model's associations. + # # @param expand [Boolean] A boolean indicating whether to expand the relationships of the relationships in the schema. + # # @param exclude_from_expansion [Array] An array of relationship names to exclude from expansion. - # @param metadata [Hash] Additional metadata to include in the schema, usually received from the nested_relationships method sent by the response_schema method. # - # @example - # { - # included: { - # type: :array, - # items: { - # anyOf: - # [ - # { - # type: :object, - # properties: { - # type: { type: :string, default: "provinces" }, - # id: { type: :string }, - # attributes: { - # type: :object, - # properties: { - # id: { type: :string }, - # name: { type: :string } - # } - # } - # } - # } - # ] - # } - # } - # } + # @param metadata [Hash] Additional metadata to include in the schema, usually received from the nested_relationships method sent by the response_schema method. # # @return [Hash] A hash representing the schema for the included resources. + # + # @example + # { + # included: { + # type: :array, + # items: { + # anyOf: + # [ + # { + # type: :object, + # properties: { + # type: { type: :string, default: "provinces" }, + # id: { type: :string }, + # attributes: { + # type: :object, + # properties: { + # id: { type: :string }, + # name: { type: :string } + # } + # } + # } + # } + # ] + # } + # } + # } def included_schema(relations = try(:relationships), expand: false, exclude_from_expansion: [], metadata: {}) return {} if relations.blank? return {} if relations == { belongs_to: {}, has_many: {} } @@ -426,18 +434,22 @@ def included_schema(relations = try(:relationships), expand: false, exclude_from # Generates the schema for the response of a resource or collection of resources in JSON API format. # - # @param relations [Hash] A hash representing the relationships of the resource in the form of { belongs_to: {}, has_many: {} }. - # If not provided, the relationships will be inferred from the model's associations. + # @param relations [Hash] A hash representing the relationships of the resource in the form of { belongs_to: {}, has_many: {} }. If not provided, the relationships will be inferred from the model's associations. + # # @param expand [Boolean] A boolean indicating whether to expand the relationships of the relationships in the schema. + # # @param exclude_from_expansion [Array] An array of relationship names to exclude from expansion. + # # @param multi [Boolean] A boolean indicating whether the response contains multiple resources. + # # @param nested [Boolean] A boolean indicating whether the response is to be expanded further than the first level of relationships. (expand relationships of relationships) - # @param metadata [Hash] Additional metadata to include in the schema, usually received from the nested_relationships method sent by the response_schema method. # - # @example - # The returned schema will have a JSON API format, including the data (included attributes and relationships), included and meta keys. + # @param metadata [Hash] Additional metadata to include in the schema, usually received from the nested_relationships method sent by the response_schema method. # # @return [Hash] A hash representing the schema for the response. + # + # @example + # "The returned schema will have a JSON API format, including the data (included attributes and relationships), included and meta keys." def response_schema(relations = try(:relationships), expand: false, exclude_from_expansion: [], multi: false, nested: false, metadata: { nested_relationships: try(:nested_relationships) }) data = { @@ -492,29 +504,76 @@ def response_schema(relations = try(:relationships), expand: false, exclude_from } end - # Generates the schema for the request payload of a resource. + + # Generates the schema for the creation request payload of a resource. # - # @note The `additional_request_attributes` and `excluded_request_attributes` applied to the returned schema by this method. + # @note The `additional_create_request_attributes` and `excluded_create_request_attributes` applied to the returned schema by this method. # @note The `required_attributes` are applied to the returned schema by this method. # @note The `nullable_attributes` are applied to the returned schema by this method. # + # @return [Hash] A hash representing the schema for the request payload. + # # @example - # { - # type: :object, - # properties: { - # data: { - # type: :object, - # properties: { - # firstName: { type: :string }, - # lastName: { type: :string } - # }, - # required: [:firstName, :lastName] - # } - # } - # } + # { + # type: :object, + # properties: { + # data: { + # type: :object, + # properties: { + # firstName: { type: :string }, + # lastName: { type: :string } + # }, + # required: [:firstName, :lastName] + # } + # } + # } + def create_request_schema + schema = { + type: :object, + properties: { + data: attributes_schema + } + } + + schema = modify_schema(schema, additional_create_request_attributes, "properties.data.properties") + + excluded_create_request_attributes.each do |key| + schema = modify_schema(schema, {}, "properties.data.properties.#{key}", delete: true) + end + + required_attributes = { + required: (schema.as_json['properties']['data']['properties'].keys - optional_create_request_attributes.map(&:to_s) - nullable_attributes.map(&:to_s)).map { |key| key.to_s.camelize(:lower).to_sym } + } + + schema = modify_schema(schema, required_attributes, "properties.data") + + schema + end + + + # Generates the schema for the update request payload of a resource. + # + # @note The `additional_update_request_attributes` and `excluded_update_request_attributes` applied to the returned schema by this method. + # @note The `required_attributes` are applied to the returned schema by this method. + # @note The `nullable_attributes` are applied to the returned schema by this method. # # @return [Hash] A hash representing the schema for the request payload. - def request_schema + # + # @example + # { + # type: :object, + # properties: { + # data: { + # type: :object, + # properties: { + # firstName: { type: :string }, + # lastName: { type: :string } + # }, + # required: [:firstName, :lastName] + # } + # } + # } + def update_request_schema schema = { type: :object, properties: { @@ -522,14 +581,14 @@ def request_schema } } - schema = modify_schema(schema, additional_request_attributes, "properties.data.properties") + schema = modify_schema(schema, additional_update_request_attributes, "properties.data.properties") - excluded_request_attributes.each do |key| + excluded_update_request_attributes.each do |key| schema = modify_schema(schema, {}, "properties.data.properties.#{key}", delete: true) end required_attributes = { - required: (schema.as_json['properties']['data']['properties'].keys - optional_request_attributes.map(&:to_s) - nullable_attributes.map(&:to_s)).map { |key| key.to_s.camelize(:lower).to_sym } + required: (schema.as_json['properties']['data']['properties'].keys - optional_update_request_attributes.map(&:to_s) - nullable_attributes.map(&:to_s)).map { |key| key.to_s.camelize(:lower).to_sym } } schema = modify_schema(schema, required_attributes, "properties.data") @@ -538,11 +597,9 @@ def request_schema end # Returns the schema for the meta data of the response body. - # # This is used to provide pagination information usually (in the case of a collection). # - # Note that this is an opinionated schema and may not be suitable for all use cases. - # If you need to override this schema, you can do so by overriding the `meta` method in your definition. + # @note Note that this is an opinionated schema and may not be suitable for all use cases. If you need to override this schema, you can do so by overriding the `meta` method in your definition. # # @return [Hash] The schema for the meta data of the response body. def meta @@ -591,179 +648,213 @@ def jsonapi # Returns the resource serializer to be used for serialization. This method must be implemented in the definition class. # + # @abstract This method must be implemented in the definition class. + # # @raise [NotImplementedError] If the method is not implemented in the definition class. # - # @example V1::UserSerializer + # @return [Class] The resource serializer class. # - # @abstract This method must be implemented in the definition class. + # @example + # V1::UserSerializer # - # @return [Class] The resource serializer class. def serializer raise NotImplementedError, 'serializer method must be implemented in the definition class' end # Returns the attributes defined in the serializer (Auto generated from the serializer). # - # @example - # [:id, :name, :email, :created_at, :updated_at] - # # @return [Array, nil] The attributes defined in the serializer or nil if there are none. + # + # @example + # [:id, :name, :email, :created_at, :updated_at] def attributes serializer.attribute_blocks.transform_keys { |key| key.to_s.underscore.to_sym }.keys || nil end # Returns the relationships defined in the serializer. # - # Note that the format of the relationships is as follows: { belongs_to: { relationship_name: relationship_definition }, has_many: { relationship_name: relationship_definition } + # @return [Hash] The relationships defined in the serializer. # - # @example - # { - # belongs_to: { - # district: Swagger::Definitions::District, - # user: Swagger::Definitions::User - # }, - # has_many: { - # applicants: Swagger::Definitions::Applicant, - # } - # } + # @note Note that the format of the relationships is as follows: + # { belongs_to: { relationship_name: relationship_definition }, has_many: { relationship_name: relationship_definition } # - # @return [Hash] The relationships defined in the serializer. + # @example + # { + # belongs_to: { + # district: Swagger::Definitions::District, + # user: Swagger::Definitions::User + # }, + # has_many: { + # applicants: Swagger::Definitions::Applicant, + # } + # } def relationships { belongs_to: {}, has_many: {} } end # Returns a hash of all the arrays defined for the model. The schema for each array is defined in the definition class manually. - # # This method must be implemented in the definition class if there are any arrays. # - # @example - # { - # metadata: { - # type: :array, - # items: { - # type: :object, nullable: true, - # properties: { name: { type: :string, nullable: true } } - # } - # } - # } - # # @return [Hash] The arrays of the model and their schemas. + # + # @example + # { + # metadata: { + # type: :array, + # items: { + # type: :object, nullable: true, + # properties: { name: { type: :string, nullable: true } } + # } + # } + # } def array_types {} end - # 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. + # Returns the attributes that are optional in the create request body. This means that they are not required to be present in the create request body thus they are taken out of the required array. # - # @example - # [:name, :email] + # @return [Array] The attributes that are optional in the create request body. # - # @return [Array] The attributes that are optional in the request body. - def optional_request_attributes + # @example + # [:name, :email] + def optional_create_request_attributes %i[] end - # 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. + # Returns the attributes that are optional in the update request body. This means that they are not required to be present in the update request body thus they are taken out of the required array. # - # They are not required to be present in the request body. + # @return [Array] The attributes that are optional in the update request body. # # @example - # [:name, :email] + # [:name, :email] + def optional_update_request_attributes + %i[] + end + + # 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. + # They are not required to be present in the request body. # # @return [Array] The attributes that are nullable in the request/response body. + # + # @example + # [:name, :email] def nullable_attributes %i[] end - # Returns the additional request attributes that are not automatically generated. These attributes are appended to the request schema. + # Returns the additional create request attributes that are not automatically generated. These attributes are appended to the create request schema. # - # @example - # { - # name: { type: :string } - # } + # @return [Hash] The additional create request attributes that are not automatically generated (if any). # - # @return [Hash] The additional request attributes that are not automatically generated (if any). - def additional_request_attributes + # @example + # { + # name: { type: :string } + # } + def additional_create_request_attributes {} end - # Returns the additional response attributes that are not automatically generated. These attributes are appended to the response schema. + # Returns the additional update request attributes that are not automatically generated. These attributes are appended to the update request schema. + # + # @return [Hash] The additional update request attributes that are not automatically generated (if any). # # @example - # { - # name: { type: :string } - # } + # { + # name: { type: :string } + # } + def additional_update_request_attributes + {} + end + + # Returns the additional response attributes that are not automatically generated. These attributes are appended to the response schema. # # @return [Hash] The additional response attributes that are not automatically generated (if any). + # + # @example + # { + # name: { type: :string } + # } def additional_response_attributes {} end # Returns the additional response relations that are not automatically generated. These relations are appended to the response schema's relationships. # - # @example - # { - # users: { - # type: :object, - # properties: { - # data: { - # type: :array, - # items: { - # type: :object, - # properties: { - # id: { type: :string }, - # type: { type: :string } - # } - # } - # } - # } - # } - # } - # # @return [Hash] The additional response relations that are not automatically generated (if any). + # + # @example + # { + # users: { + # type: :object, + # properties: { + # data: { + # type: :array, + # items: { + # type: :object, + # properties: { + # id: { type: :string }, + # type: { type: :string } + # } + # } + # } + # } + # } + # } def additional_response_relations {} end # Returns the additional response included that are not automatically generated. These included are appended to the response schema's included. # - # @example - # { - # type: :object, - # properties: { - # id: { type: :string }, - # type: { type: :string }, - # attributes: { - # type: :object, - # properties: { - # name: { type: :string } - # } - # } - # } - # } - # # @return [Hash] The additional response included that are not automatically generated (if any). + # + # @example + # { + # type: :object, + # properties: { + # id: { type: :string }, + # type: { type: :string }, + # attributes: { + # type: :object, + # properties: { + # name: { type: :string } + # } + # } + # } + # } def additional_response_included {} end - # Returns the attributes that are excluded from the request schema. - # These attributes are not required or not needed to be present in the request body. + # Returns the attributes that are excluded from the create request schema. + # These attributes are not required or not needed to be present in the create request body. + # + # @return [Array] The attributes that are excluded from the create request schema. # # @example - # [:id, :updated_at, :created_at] + # [:id, :updated_at, :created_at] + def excluded_create_request_attributes + %i[] + end + + # Returns the attributes that are excluded from the update request schema. + # These attributes are not required or not needed to be present in the update request body. # - # @return [Array] The attributes that are excluded from the request schema. - def excluded_request_attributes + # @return [Array] The attributes that are excluded from the update request schema. + # + # @example + # [:id, :updated_at, :created_at] + def excluded_update_request_attributes %i[] end # Returns the attributes that are excluded from the response schema. # These attributes are not needed to be present in the response body. # - # @example - # [:id, :updated_at, :created_at] - # # @return [Array] The attributes that are excluded from the response schema. + # + # @example + # [:id, :updated_at, :created_at] def excluded_response_attributes %i[] end @@ -771,10 +862,10 @@ def excluded_response_attributes # Returns the relationships that are excluded from the response schema. # These relationships are not needed to be present in the response body. # - # @example - # [:users, :applicants] - # # @return [Array] The relationships that are excluded from the response schema. + # + # @example + # [:users, :applicants] def excluded_response_relations %i[] end @@ -782,73 +873,81 @@ def excluded_response_relations # Returns the included that are excluded from the response schema. # These included are not needed to be present in the response body. # - # @todo This method is not used anywhere yet. + # @return [Array] The included that are excluded from the response schema. # # @example - # [:users, :applicants] + # [:users, :applicants] # - # @return [Array] The included that are excluded from the response schema. + # @todo + # This method is not used anywhere yet. def excluded_response_included %i[] end # Returns the relationships to be further expanded in the response schema. # - # @example - # { - # applicants: { - # belongs_to: { - # district: Swagger::Definitions::District, - # province: Swagger::Definitions::Province, - # }, - # has_many: { - # attachments: Swagger::Definitions::Upload, - # } - # } - # } - # # @return [Hash] The relationships to be further expanded in the response schema. + # + # @example + # { + # applicants: { + # belongs_to: { + # district: Swagger::Definitions::District, + # province: Swagger::Definitions::Province, + # }, + # has_many: { + # attachments: Swagger::Definitions::Upload, + # } + # } + # } def nested_relationships {} end # Returns the model class (Constantized from the definition class name) # - # @example - # User - # # @return [Class] The model class (Constantized from the definition class name) + # + # @example + # User def model self.class.name.gsub("Swagger::Definitions::", '').constantize end # Returns the model name. Used for schema type naming. # - # @example - # 'users' for the User model - # 'citizen_applications' for the CitizenApplication model - # # @return [String] The model name. + # + # @example + # 'users' for the User model + # 'citizen_applications' for the CitizenApplication model def self.model_name name.gsub("Swagger::Definitions::", '').pluralize.underscore.downcase end # Returns the generated schemas in JSONAPI format that are used in the swagger documentation. # - # @note This method is used for generating schema in 3 different formats: request, response and response expanded. - # request: The schema for the request body. - # response: The schema for the response body (without any relationships expanded), used for collection responses. - # response expanded: The schema for the response body with all the relationships expanded, used for single resource responses. + # @return [Array] The generated schemas in JSONAPI format that are used in the swagger documentation. + # + # @note This method is used for generating schema in 4 different formats: request (both create and update), response and response expanded. + # + # @option CreateRequest + # is the schema for the creation request body. + # @option UpdateRequest + # is the schema for the updating request body. + # @option Response + # is the schema for the response body (without any relationships expanded), used for collection responses. + # @option ResponseExpanded: The schema for the response body with all the relationships expanded, used for single resource responses. # # @note The returned schemas are in JSONAPI format are usually appended to the rswag component's 'schemas' in swagger_helper. # # @note The method can be overridden in the definition class if there are any additional customizations needed. # - # @return [Array] The generated schemas in JSONAPI format that are used in the swagger documentation. def self.definitions schema_instance = self.new [ - "#{schema_instance.model}Request": schema_instance.camelize_keys(schema_instance.request_schema), + "#{schema_instance.model}CreateRequest": schema_instance.camelize_keys(schema_instance.create_request_schema), + "#{schema_instance.model}UpdateRequest": schema_instance.camelize_keys(schema_instance.update_request_schema), "#{schema_instance.model}Response": schema_instance.camelize_keys(schema_instance.response_schema(multi: true)), "#{schema_instance.model}ResponseExpanded": schema_instance.camelize_keys(schema_instance.response_schema(expand: true)) ] @@ -858,10 +957,10 @@ def self.definitions # # @param hash [Array | Hash] The hash with all the keys camelized. # - # @example - # { first_name: 'John', last_name: 'Doe' } => { firstName: 'John', lastName: 'Doe' } - # # @return [Array | Hash] The hash with all the keys camelized. + # + # @example + # { first_name: 'John', last_name: 'Doe' } => { firstName: 'John', lastName: 'Doe' } def camelize_keys(hash) hash.deep_transform_keys { |key| key.to_s.camelize(:lower).to_sym } end From ec3a318f53733ed7709cba462b0aaeda44a17a1d Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Sat, 29 Jul 2023 16:29:46 +0300 Subject: [PATCH 05/87] Separates create and update excluded request attributes on file generation --- lib/generators/schemable/model_generator.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/generators/schemable/model_generator.rb b/lib/generators/schemable/model_generator.rb index 467b13e..d0eba91 100644 --- a/lib/generators/schemable/model_generator.rb +++ b/lib/generators/schemable/model_generator.rb @@ -36,7 +36,11 @@ def serializer V1::#{@model_name.classify}Serializer end - def excluded_request_attributes + def excluded_create_request_attributes + %i[updated_at created_at] + end + + def excluded_update_request_attributes %i[updated_at created_at] end end From 64f81a387b9b5ce142526542f1b8540081701dfc Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Sat, 29 Jul 2023 16:35:56 +0300 Subject: [PATCH 06/87] Updates version --- Gemfile.lock | 2 +- lib/schemable/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a582af0..814ee68 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - schemable (0.1.2) + schemable (0.1.3) factory_bot_rails (~> 6.2.0) jsonapi-rails (~> 0.4.1) diff --git a/lib/schemable/version.rb b/lib/schemable/version.rb index daf5918..25da6dc 100644 --- a/lib/schemable/version.rb +++ b/lib/schemable/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Schemable - VERSION = "0.1.2" + VERSION = "0.1.3" end From 9f6ecb935d8d1aa5a9da5c169eed455645520add Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Sat, 29 Jul 2023 16:55:20 +0300 Subject: [PATCH 07/87] Fixes rubocop offenses --- .rubocop.yml | 18 +- Gemfile | 13 +- Gemfile.lock | 28 +-- Rakefile | 6 +- lib/generators/schemable/install_generator.rb | 4 +- lib/generators/schemable/model_generator.rb | 4 +- lib/schemable.rb | 160 +++++++----------- lib/schemable/version.rb | 2 +- schemable.gemspec | 33 ++-- spec/schemable_spec.rb | 4 +- spec/spec_helper.rb | 4 +- 11 files changed, 133 insertions(+), 143 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index fbbb4a2..4863774 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -108,10 +108,24 @@ Metrics/AbcSize: Metrics/MethodLength: Enabled: false Metrics/CyclomaticComplexity: - Max: 15 + Enabled: false Metrics/PerceivedComplexity: - Max: 15 + Enabled: false Lint/DuplicateMethods: # Disables duplicate methods warning Enabled: false Gemspec/RequiredRubyVersion: # Disables required ruby version warning Enabled: false +Metrics/ParameterLists: # Disables parameter lists warning + Enabled: false +Lint/NextWithoutAccumulator: # Disables next without accumulator warning + Enabled: false +Lint/ShadowingOuterLocalVariable: # Disables shadowing outer local variable warning + Enabled: false +Metrics/ModuleLength: # Disables module length warning + Enabled: false +Layout/EmptyLinesAroundClassBody: # Disables empty lines around class body warning + Enabled: false +Layout/HeredocIndentation: # Disables heredoc indentation warning + Enabled: false +Layout/ClosingHeredocIndentation: # Disables closing heredoc indentation warning + Enabled: false \ No newline at end of file diff --git a/Gemfile b/Gemfile index 0014dd4..2df8685 100644 --- a/Gemfile +++ b/Gemfile @@ -1,14 +1,15 @@ # frozen_string_literal: true -source "https://rubygems.org" +source 'https://rubygems.org' gemspec -gem "rake", "~> 13.0" -gem "rspec", "~> 3.0" -gem "rubocop", "~> 1.21" +gem 'rake', '~> 13.0.6' +gem 'rspec', '~> 3.12' +gem 'rubocop', '~> 1.55' +gem 'rubocop-rails', '~> 2.20.2' group :development, :test do - gem 'jsonapi-rails', '~> 0.4.1' gem 'factory_bot_rails', '~> 6.2' -end \ No newline at end of file + gem 'jsonapi-rails', '~> 0.4.1' +end diff --git a/Gemfile.lock b/Gemfile.lock index 814ee68..d234ff8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -51,6 +51,7 @@ GEM jsonapi-renderer (0.2.2) jsonapi-serializable (0.3.1) jsonapi-renderer (~> 0.2.0) + language_server-protocol (3.17.0.3) loofah (2.21.2) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -59,8 +60,9 @@ GEM nokogiri (1.14.4-x86_64-linux) racc (~> 1.4) parallel (1.23.0) - parser (3.2.2.1) + parser (3.2.2.3) ast (~> 2.4.1) + racc racc (1.6.2) rack (2.2.7) rack-test (2.1.0) @@ -79,8 +81,8 @@ GEM zeitwerk (~> 2.5) rainbow (3.1.1) rake (13.0.6) - regexp_parser (2.8.0) - rexml (3.2.5) + regexp_parser (2.8.1) + rexml (3.2.6) rspec (3.12.0) rspec-core (~> 3.12.0) rspec-expectations (~> 3.12.0) @@ -94,18 +96,23 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-support (3.12.0) - rubocop (1.51.0) + rubocop (1.55.0) json (~> 2.3) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.0.0) + parser (>= 3.2.2.3) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.0, < 2.0) + rubocop-ast (>= 1.28.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.28.1) + rubocop-ast (1.29.0) parser (>= 3.2.1.0) + rubocop-rails (2.20.2) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.33.0, < 2.0) ruby-progressbar (1.13.0) thor (1.2.2) tzinfo (2.0.6) @@ -119,9 +126,10 @@ PLATFORMS DEPENDENCIES factory_bot_rails (~> 6.2) jsonapi-rails (~> 0.4.1) - rake (~> 13.0) - rspec (~> 3.0) - rubocop (~> 1.21) + rake (~> 13.0.6) + rspec (~> 3.12) + rubocop (~> 1.55) + rubocop-rails (~> 2.20.2) schemable! BUNDLED WITH diff --git a/Rakefile b/Rakefile index cca7175..4964751 100644 --- a/Rakefile +++ b/Rakefile @@ -1,11 +1,11 @@ # frozen_string_literal: true -require "bundler/gem_tasks" -require "rspec/core/rake_task" +require 'bundler/gem_tasks' +require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) -require "rubocop/rake_task" +require 'rubocop/rake_task' RuboCop::RakeTask.new diff --git a/lib/generators/schemable/install_generator.rb b/lib/generators/schemable/install_generator.rb index 101de4a..dcfcb14 100644 --- a/lib/generators/schemable/install_generator.rb +++ b/lib/generators/schemable/install_generator.rb @@ -4,8 +4,8 @@ class InstallGenerator < Rails::Generators::Base source_root File.expand_path('../../templates', __dir__) class_option :model_name, type: :string, default: 'Model', desc: 'Name of the model' - def initialize(*) - super(*) + def initialize(*args) + super(*args) end def copy_initializer diff --git a/lib/generators/schemable/model_generator.rb b/lib/generators/schemable/model_generator.rb index d0eba91..7fbdf03 100644 --- a/lib/generators/schemable/model_generator.rb +++ b/lib/generators/schemable/model_generator.rb @@ -4,8 +4,8 @@ class ModelGenerator < Rails::Generators::Base source_root File.expand_path('../../templates', __dir__) class_option :model_name, type: :string, default: 'Model', desc: 'Name of the model' - def initialize(*) - super(*) + def initialize(*args) + super(*args) @model_name = options[:model_name] @model_name != 'Model' || raise('Model name is required') diff --git a/lib/schemable.rb b/lib/schemable.rb index 9ecdcea..6a17329 100644 --- a/lib/schemable.rb +++ b/lib/schemable.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "schemable/version" +require_relative 'schemable/version' require 'active_support/concern' module Schemable @@ -9,7 +9,6 @@ class Error < StandardError; end extend ActiveSupport::Concern included do - # Maps a given type name to a corresponding JSON schema object that represents that type. # # @param type_name [String, Symbol] A String or Symbol representing the type of the property to be mapped. @@ -22,7 +21,7 @@ def type_mapper(type_name) integer: { type: :integer }, float: { type: :number, format: :float }, decimal: { type: :number, format: :double }, - datetime: { type: :string, format: :"date-time" }, + datetime: { type: :string, format: :'date-time' }, date: { type: :string, format: :date }, time: { type: :string, format: :time }, boolean: { type: :boolean }, @@ -70,21 +69,15 @@ def type_mapper(type_name) def modify_schema(original_schema, new_props, given_path = nil, delete: false) return new_props if original_schema.nil? - if given_path.nil? && delete - raise ArgumentError, "Cannot delete without a given path" - end + raise ArgumentError, 'Cannot delete without a given path' if given_path.nil? && delete if given_path.present? path_segments = given_path.split('.').map(&:to_sym) if path_segments.size == 1 - unless original_schema.key?(path_segments.first) - raise ArgumentError, "Given path does not exist in the original schema" - end + raise ArgumentError, 'Given path does not exist in the original schema' unless original_schema.key?(path_segments.first) else - unless original_schema.dig(*path_segments[0..-2]).is_a?(Hash) && original_schema.dig(*path_segments) - raise ArgumentError, "Given path does not exist in the original schema" - end + raise ArgumentError, 'Given path does not exist in the original schema' unless original_schema.dig(*path_segments[0..-2]).is_a?(Hash) && original_schema.dig(*path_segments) end path_hash = path_segments.reverse.reduce(new_props) { |a, n| { n => a } } @@ -117,31 +110,21 @@ def attribute_schema(attribute) column_hash = model.columns_hash[attribute.to_s] # Check if this attribute has a custom JSON Schema definition - if array_types.keys.include?(attribute) - return array_types[attribute] - end + return array_types[attribute] if array_types.keys.include?(attribute) - if additional_response_attributes.keys.include?(attribute) - return additional_response_attributes[attribute] - end + return additional_response_attributes[attribute] if additional_response_attributes.keys.include?(attribute) # Check if this is an array attribute - if column_hash.as_json.try(:[], 'sql_type_metadata').try(:[], 'sql_type').include?('[]') - return type_mapper(:array) - end + return type_mapper(:array) if column_hash.as_json.try(:[], 'sql_type_metadata').try(:[], 'sql_type').include?('[]') # Map the column type to a JSON Schema type if none of the above conditions are met response = type_mapper(column_hash.try(:type)) # If the attribute is nullable, modify the schema accordingly - if response && nullable_attributes.include?(attribute) - return modify_schema(response, { nullable: true }) - end + return modify_schema(response, { nullable: true }) if response && nullable_attributes.include?(attribute) # If attribute is an enum, modify the schema accordingly - if response && model.defined_enums.key?(attribute.to_s) - return modify_schema(response, { type: :string, enum: model.defined_enums[attribute.to_s].keys }) - end + return modify_schema(response, { type: :string, enum: model.defined_enums[attribute.to_s].keys }) if response && model.defined_enums.key?(attribute.to_s) return response unless response.nil? @@ -153,7 +136,6 @@ def attribute_schema(attribute) # If we still haven't found a schema type, default to object type_mapper(:object) - rescue NoMethodError # Log a warning if the attribute does not exist on the model Rails.logger.warn("\e[33mWARNING: #{model} does not have an attribute named \e[31m#{attribute}\e[0m") @@ -178,14 +160,13 @@ def attribute_schema(attribute) def attributes_schema schema = { type: :object, - properties: attributes.reduce({}) do |props, attr| - props[attr] = attribute_schema(attr) - props + properties: attributes.index_with do |attr| + attribute_schema(attr) end } # modify the schema to include additional response relations - schema = modify_schema(schema, additional_response_attributes, given_path = "properties") + schema = modify_schema(schema, additional_response_attributes, 'properties') # modify the schema to exclude response relations excluded_response_attributes.each do |key| @@ -248,7 +229,6 @@ def relationships_schema(relations = try(:relationships), expand: false, exclude if relation_type == :has_many props.merge!( relation_definitions.keys.index_with do |relationship| - result = { type: :object, properties: { @@ -273,7 +253,6 @@ def relationships_schema(relations = try(:relationships), expand: false, exclude else props.merge!( relation_definitions.keys.index_with do |relationship| - result = { type: :object, properties: { @@ -298,7 +277,7 @@ def relationships_schema(relations = try(:relationships), expand: false, exclude } # modify the schema to include additional response relations - schema = modify_schema(schema, additional_response_relations, "properties") + schema = modify_schema(schema, additional_response_relations, 'properties') # modify the schema to exclude response relations excluded_response_relations.each do |key| @@ -356,7 +335,7 @@ def included_schema(relations = try(:relationships), expand: false, exclude_from type: :array, items: { anyOf: - relations.reduce([]) do |props, (relation_type, relation_definitions)| + relations.reduce([]) do |props, (_relation_type, relation_definitions)| props + relation_definitions.keys.reduce([]) do |props, relationship| props + [ unless exclude_from_expansion.include?(relationship) @@ -366,16 +345,16 @@ def included_schema(relations = try(:relationships), expand: false, exclude_from type: { type: :string, default: relation_definitions[relationship].model_name }, id: { type: :string }, attributes: begin - relation_definitions[relationship].new.attributes_schema || {} - rescue NoMethodError - {} - end + relation_definitions[relationship].new.attributes_schema || {} + rescue NoMethodError + {} + end }.merge( if relation_definitions[relationship].new.relationships != { belongs_to: {}, has_many: {} } || relation_definitions[relationship].new.relationships.blank? if !expand || metadata.blank? { relationships: relation_definitions[relationship].new.relationships_schema(expand: false) } else - { relationships: relation_definitions[relationship].new.relationships_schema(relations = metadata[:nested_relationships][relationship], expand: true, exclude_from_expansion: exclude_from_expansion) } + { relationships: relation_definitions[relationship].new.relationships_schema(relations = metadata[:nested_relationships][relationship], expand: true, exclude_from_expansion:) } end else {} @@ -385,36 +364,34 @@ def included_schema(relations = try(:relationships), expand: false, exclude_from end ].concat( [ - if expand && metadata.present? && !exclude_from_expansion.include?(relationship) + if expand && metadata.present? && exclude_from_expansion.exclude?(relationship) extra_relations = [] metadata[:nested_relationships].keys.reduce({}) do |props, nested_relationship| - if metadata[:nested_relationships][relationship].present? - props.merge!(metadata[:nested_relationships][nested_relationship].keys.each_with_object({}) do |relationship_type, inner_props| - props.merge!(metadata[:nested_relationships][nested_relationship][relationship_type].keys.each_with_object({}) do |relationship, inner_inner_props| - - extra_relation_schema = { - type: :object, - properties: { - type: { type: :string, default: metadata[:nested_relationships][nested_relationship][relationship_type][relationship].model_name }, - id: { type: :string }, - attributes: metadata[:nested_relationships][nested_relationship][relationship_type][relationship].new.attributes_schema - }.merge( - 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? - {} - else - result = { relationships: metadata[:nested_relationships][nested_relationship][relationship_type][relationship].new.relationships_schema(expand: false) } - return {} if result == { relationships: {} } - result - end - ) - } - - extra_relations << extra_relation_schema - end - ) - end - ) - end + next if metadata[:nested_relationships][relationship].blank? + + props.merge!(metadata[:nested_relationships][nested_relationship].keys.each_with_object({}) do |relationship_type, _inner_props| + props.merge!(metadata[:nested_relationships][nested_relationship][relationship_type].keys.each_with_object({}) do |relationship, _inner_inner_props| + extra_relation_schema = { + type: :object, + properties: { + type: { type: :string, default: metadata[:nested_relationships][nested_relationship][relationship_type][relationship].model_name }, + id: { type: :string }, + attributes: metadata[:nested_relationships][nested_relationship][relationship_type][relationship].new.attributes_schema + }.merge( + 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? + {} + else + result = { relationships: metadata[:nested_relationships][nested_relationship][relationship_type][relationship].new.relationships_schema(expand: false) } + return {} if result == { relationships: {} } + + result + end + ) + } + + extra_relations << extra_relation_schema + end) + end) end extra_relations @@ -427,9 +404,7 @@ def included_schema(relations = try(:relationships), expand: false, exclude_from } } - schema = modify_schema(schema, additional_response_included, "included.items") - - schema + modify_schema(schema, additional_response_included, 'included.items') end # Generates the schema for the response of a resource or collection of resources in JSON API format. @@ -451,18 +426,17 @@ def included_schema(relations = try(:relationships), expand: false, exclude_from # @example # "The returned schema will have a JSON API format, including the data (included attributes and relationships), included and meta keys." def response_schema(relations = try(:relationships), expand: false, exclude_from_expansion: [], multi: false, nested: false, metadata: { nested_relationships: try(:nested_relationships) }) - data = { type: :object, properties: { type: { type: :string, default: itself.class.model_name }, id: { type: :string }, - attributes: attributes_schema, + attributes: attributes_schema }.merge( if relations.blank? || relations == { belongs_to: {}, has_many: {} } {} else - { relationships: relationships_schema(relations, expand: expand, exclude_from_expansion: exclude_from_expansion) } + { relationships: relationships_schema(relations, expand:, exclude_from_expansion:) } end ) } @@ -476,26 +450,26 @@ def response_schema(relations = try(:relationships), expand: false, exclude_from } else { - data: data + data: } end schema.merge!( if nested && expand - included_schema(relations, expand: nested, exclude_from_expansion: exclude_from_expansion, metadata: metadata) + included_schema(relations, expand: nested, exclude_from_expansion:, metadata:) elsif !nested && expand - included_schema(relations, expand: nested, exclude_from_expansion: exclude_from_expansion) + included_schema(relations, expand: nested, exclude_from_expansion:) else {} end ).merge!( - if !expand - { meta: meta } - else + if expand {} + else + { meta: } end ).merge!( - jsonapi: jsonapi + jsonapi: ) { @@ -504,7 +478,6 @@ def response_schema(relations = try(:relationships), expand: false, exclude_from } end - # Generates the schema for the creation request payload of a resource. # # @note The `additional_create_request_attributes` and `excluded_create_request_attributes` applied to the returned schema by this method. @@ -535,7 +508,7 @@ def create_request_schema } } - schema = modify_schema(schema, additional_create_request_attributes, "properties.data.properties") + schema = modify_schema(schema, additional_create_request_attributes, 'properties.data.properties') excluded_create_request_attributes.each do |key| schema = modify_schema(schema, {}, "properties.data.properties.#{key}", delete: true) @@ -545,12 +518,9 @@ def create_request_schema required: (schema.as_json['properties']['data']['properties'].keys - optional_create_request_attributes.map(&:to_s) - nullable_attributes.map(&:to_s)).map { |key| key.to_s.camelize(:lower).to_sym } } - schema = modify_schema(schema, required_attributes, "properties.data") - - schema + modify_schema(schema, required_attributes, 'properties.data') end - # Generates the schema for the update request payload of a resource. # # @note The `additional_update_request_attributes` and `excluded_update_request_attributes` applied to the returned schema by this method. @@ -581,7 +551,7 @@ def update_request_schema } } - schema = modify_schema(schema, additional_update_request_attributes, "properties.data.properties") + schema = modify_schema(schema, additional_update_request_attributes, 'properties.data.properties') excluded_update_request_attributes.each do |key| schema = modify_schema(schema, {}, "properties.data.properties.#{key}", delete: true) @@ -591,9 +561,7 @@ def update_request_schema required: (schema.as_json['properties']['data']['properties'].keys - optional_update_request_attributes.map(&:to_s) - nullable_attributes.map(&:to_s)).map { |key| key.to_s.camelize(:lower).to_sym } } - schema = modify_schema(schema, required_attributes, "properties.data") - - schema + modify_schema(schema, required_attributes, 'properties.data') end # Returns the schema for the meta data of the response body. @@ -640,7 +608,7 @@ def jsonapi properties: { version: { type: :string, - default: "1.0" + default: '1.0' } } } @@ -911,7 +879,7 @@ def nested_relationships # @example # User def model - self.class.name.gsub("Swagger::Definitions::", '').constantize + self.class.name.gsub('Swagger::Definitions::', '').constantize end # Returns the model name. Used for schema type naming. @@ -922,7 +890,7 @@ def model # 'users' for the User model # 'citizen_applications' for the CitizenApplication model def self.model_name - name.gsub("Swagger::Definitions::", '').pluralize.underscore.downcase + name.gsub('Swagger::Definitions::', '').pluralize.underscore.downcase end # Returns the generated schemas in JSONAPI format that are used in the swagger documentation. @@ -944,7 +912,7 @@ def self.model_name # @note The method can be overridden in the definition class if there are any additional customizations needed. # def self.definitions - schema_instance = self.new + schema_instance = new [ "#{schema_instance.model}CreateRequest": schema_instance.camelize_keys(schema_instance.create_request_schema), "#{schema_instance.model}UpdateRequest": schema_instance.camelize_keys(schema_instance.update_request_schema), diff --git a/lib/schemable/version.rb b/lib/schemable/version.rb index 25da6dc..3ffe440 100644 --- a/lib/schemable/version.rb +++ b/lib/schemable/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Schemable - VERSION = "0.1.3" + VERSION = '0.1.3' end diff --git a/schemable.gemspec b/schemable.gemspec index d8fd544..2048249 100644 --- a/schemable.gemspec +++ b/schemable.gemspec @@ -1,37 +1,36 @@ # frozen_string_literal: true -require_relative "lib/schemable/version" +require_relative 'lib/schemable/version' Gem::Specification.new do |spec| - spec.name = "schemable" + spec.name = 'schemable' spec.version = Schemable::VERSION - spec.authors = ["Muhammad Nawzad"] - spec.email = ["hama127n@gmail.com"] + spec.authors = ['Muhammad Nawzad'] + spec.email = ['hama127n@gmail.com'] - spec.summary = "An opiniated Gem for Rails applications to auto generate schema in JSONAPI format." + spec.summary = 'An opiniated Gem for Rails applications to auto generate schema in JSONAPI format.' spec.description = "The schemable gem is an opiniated Gem for Rails applications to auto generate schema for models in JSONAPI format. It is designed to work with rswag's swagger documentation since it can generate the schemas for it." - spec.homepage = "https://github.com/muhammadnawzad/schemable" - spec.license = "MIT" - spec.required_ruby_version = ">= 3.1.2" + spec.homepage = 'https://github.com/muhammadnawzad/schemable' + spec.license = 'MIT' + spec.required_ruby_version = '>= 3.1.2' - spec.metadata["allowed_push_host"] = 'https://rubygems.org' - - spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = 'https://github.com/muhammadnawzad/schemable' - spec.metadata["changelog_uri"] = 'https://github.com/muhammadnawzad/schemable/blob/main/CHANGELOG.md' + spec.metadata['allowed_push_host'] = 'https://rubygems.org' + spec.metadata['homepage_uri'] = spec.homepage + spec.metadata['source_code_uri'] = 'https://github.com/muhammadnawzad/schemable' + spec.metadata['changelog_uri'] = 'https://github.com/muhammadnawzad/schemable/blob/main/CHANGELOG.md' spec.files = Dir.chdir(__dir__) do `git ls-files -z`.split("\x0").reject do |f| (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor]) end end - spec.bindir = "exe" + spec.bindir = 'exe' spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } - spec.require_paths = ["lib"] + spec.require_paths = ['lib'] - spec.add_dependency "jsonapi-rails", "~> 0.4.1" - spec.add_dependency "factory_bot_rails", "~> 6.2.0" + spec.add_dependency 'factory_bot_rails', '~> 6.2.0' + spec.add_dependency 'jsonapi-rails', '~> 0.4.1' spec.metadata['rubygems_mfa_required'] = 'true' end diff --git a/spec/schemable_spec.rb b/spec/schemable_spec.rb index 9f994c3..1b7808f 100644 --- a/spec/schemable_spec.rb +++ b/spec/schemable_spec.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true RSpec.describe Schemable do - it "has a version number" do + it 'has a version number' do expect(Schemable::VERSION).not_to be nil end - it "does something useful" do + it 'does something useful' do expect(true).to eq(true) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 433f41d..abd54ea 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require "schemable" +require 'schemable' RSpec.configure do |config| # Enable flags like --only-failures and --next-failure - config.example_status_persistence_file_path = ".rspec_status" + config.example_status_persistence_file_path = '.rspec_status' # Disable RSpec exposing methods globally on `Module` and `main` config.disable_monkey_patching! From cddc117b473d8d612563bddc947afa85be0df40d Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Tue, 26 Sep 2023 14:15:16 +0300 Subject: [PATCH 08/87] Uses configs to check whether to use string or number for decimal and float types --- lib/schemable.rb | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/schemable.rb b/lib/schemable.rb index 6a17329..41aedfb 100644 --- a/lib/schemable.rb +++ b/lib/schemable.rb @@ -19,8 +19,8 @@ def type_mapper(type_name) text: { type: :string }, string: { type: :string }, integer: { type: :integer }, - float: { type: :number, format: :float }, - decimal: { type: :number, format: :double }, + float: { type: (configs[:float_as_string] ? :string : :number).to_s.to_sym, format: :float }, + decimal: { type: (configs[:decimal_as_string] ? :string : :number).to_s.to_sym, format: :double }, datetime: { type: :string, format: :'date-time' }, date: { type: :string, format: :date }, time: { type: :string, format: :time }, @@ -932,5 +932,15 @@ def self.definitions def camelize_keys(hash) hash.deep_transform_keys { |key| key.to_s.camelize(:lower).to_sym } end + + # Returns a json of config options for the definition class. + # + # @return [Hash] The config options for the definition class. + # + # @example + # { decimal_as_string: true } + def configs + {} + end end end From 671571282d472362b038f6de12a2ec0b112b0e7b Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Tue, 26 Sep 2023 14:17:50 +0300 Subject: [PATCH 09/87] Updates gem version --- Gemfile.lock | 2 +- lib/schemable/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index d234ff8..d4521ff 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - schemable (0.1.3) + schemable (0.1.4) factory_bot_rails (~> 6.2.0) jsonapi-rails (~> 0.4.1) diff --git a/lib/schemable/version.rb b/lib/schemable/version.rb index 3ffe440..a8e0b6f 100644 --- a/lib/schemable/version.rb +++ b/lib/schemable/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Schemable - VERSION = '0.1.3' + VERSION = '0.1.4' end From 7396ddebbfc88642485a81fdd2f32d22c53fff10 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Wed, 8 Nov 2023 22:04:33 +0300 Subject: [PATCH 10/87] Updates Rubocop --- .rubocop.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index 4863774..ca6fa7c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -128,4 +128,8 @@ Layout/EmptyLinesAroundClassBody: # Disables empty lines around class body warni Layout/HeredocIndentation: # Disables heredoc indentation warning Enabled: false Layout/ClosingHeredocIndentation: # Disables closing heredoc indentation warning - Enabled: false \ No newline at end of file + Enabled: false +Rails/Output: # Disables rails output warning + Enabled: false +Metrics/ClassLength: # Disables class length warning + Max: 150 From 923f3ebe12fad5ac56eb18c2d8a00a84d3c3b89e Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Thu, 9 Nov 2023 09:54:41 +0300 Subject: [PATCH 11/87] Adds SchemaModifier Class --- lib/schemable/schema_modifier.rb | 142 ++++++++++++++++++++++++++++++ sig/schemable/schema_modifier.rbs | 42 +++++++++ 2 files changed, 184 insertions(+) create mode 100644 lib/schemable/schema_modifier.rb create mode 100644 sig/schemable/schema_modifier.rbs diff --git a/lib/schemable/schema_modifier.rb b/lib/schemable/schema_modifier.rb new file mode 100644 index 0000000..7234801 --- /dev/null +++ b/lib/schemable/schema_modifier.rb @@ -0,0 +1,142 @@ +module Schemable + class SchemaModifier + def parse_path(path) + path.split('.').map(&:to_sym) + end + + def path_exists?(schema, path) + path_segments = parse_path(path) + + path_segments.reduce(schema) do |current_segment, next_segment| + if current_segment.is_a?(Array) + # The regex pattern '/\[(\d+)\]|\d+/' matches square brackets containing one or more digits, + # or standalone digits. Used for parsing array indices in a path. + index = next_segment.to_s.match(/\[(\d+)\]|\d+/)[1] + # The regex pattern '/\A\d+\z/' matches a sequence of one or more digits from the start ('\A') + # to the end ('\z') of a string. It checks if a string consists of only digits. + return false if index.nil? || !index.match?(/\A\d+\z/) || index.to_i >= current_segment.length + + current_segment[index.to_i] + else + return false unless current_segment.is_a?(Hash) && current_segment.key?(next_segment) + + current_segment[next_segment] + end + end + + true + end + + def deep_merge_hashes(destination, new_data) + if destination.is_a?(Array) && new_data.is_a?(Array) + destination.concat(new_data) + elsif destination.is_a?(Array) && new_data.is_a?(Hash) + destination.push(new_data) + elsif destination.is_a?(Hash) && new_data.is_a?(Hash) + new_data.each do |key, value| + if destination[key].is_a?(Hash) && value.is_a?(Hash) + destination[key] = deep_merge_hashes(destination[key], value) + elsif destination[key].is_a?(Array) && value.is_a?(Array) + destination[key].concat(value) + elsif destination[key].is_a?(Array) && value.is_a?(Hash) + destination[key].push(value) + else + destination[key] = value + end + end + end + + destination + end + + def add_properties(original_schema, new_schema, path) + return deep_merge_hashes(original_schema, new_schema) if path == '.' + + unless path_exists?(original_schema, path) + puts "Error: Path '#{path}' does not exist in the original schema" + return original_schema + end + + path_segments = parse_path(path) + current_segment = original_schema + last_segment = path_segments.pop + + # Navigate to the specified location in the schema + path_segments.each do |segment| + if current_segment.is_a?(Array) + index = segment.to_s.match(/\[(\d+)\]|\d+/)[1] + if index&.match?(/\A\d+\z/) && index.to_i < current_segment.length + current_segment = current_segment[index.to_i] + else + puts "Error: Invalid index in path '#{path}'" + return original_schema + end + elsif current_segment.is_a?(Hash) && current_segment.key?(segment) + current_segment = current_segment[segment] + else + puts "Error: Expected a Hash but found #{current_segment.class} in path '#{path}'" + return original_schema + end + end + + # Merge the new schema into the specified location + if current_segment.is_a?(Array) + index = last_segment.to_s.match(/\[(\d+)\]|\d+/)[1] + if index&.match?(/\A\d+\z/) && index.to_i < current_segment.length + current_segment[index.to_i] = deep_merge_hashes(current_segment[index.to_i], new_schema) + else + puts "Error: Invalid index in path '#{path}'" + end + else + current_segment[last_segment] = deep_merge_hashes(current_segment[last_segment], new_schema) + end + + original_schema + end + + def delete_properties(original_schema, path) + return original_schema if path == '.' + + unless path_exists?(original_schema, path) + puts "Error: Path '#{path}' does not exist in the original schema" + return original_schema + end + + path_segments = parse_path(path) + current_segment = original_schema + last_segment = path_segments.pop + + # Navigate to the parent of the last segment in the path + path_segments.each do |segment| + if current_segment.is_a?(Array) + index = segment.to_s.match(/\[(\d+)\]|\d+/)[1] + if index&.match?(/\A\d+\z/) && index.to_i < current_segment.length + current_segment = current_segment[index.to_i] + else + puts "Error: Invalid index in path '#{path}'" + return original_schema + end + elsif current_segment.is_a?(Hash) && current_segment.key?(segment) + current_segment = current_segment[segment] + else + puts "Error: Expected a Hash but found #{current_segment.class} in path '#{path}'" + return original_schema + end + end + + # Delete the last segment in the path + if current_segment.is_a?(Array) + index = last_segment.to_s.match(/\[(\d+)\]|\d+/)[1] + if index&.match?(/\A\d+\z/) && index.to_i < current_segment.length + current_segment.delete_at(index.to_i) + else + puts "Error: Invalid index in path '#{path}'" + end + else + current_segment.delete(last_segment) + end + + original_schema + end + end +end diff --git a/sig/schemable/schema_modifier.rbs b/sig/schemable/schema_modifier.rbs new file mode 100644 index 0000000..76fa6f8 --- /dev/null +++ b/sig/schemable/schema_modifier.rbs @@ -0,0 +1,42 @@ +# == SchemaModifier +# +# This module provides methods for working with Hash/JSON-like schemas. +# It includes methods to parse paths, check if a path exists in a schema, +# deep merge two hashes or an array and a hash, add properties to a specific +# location in a schema, and delete properties at a specified path. +# +# === Examples +# +# schema_modifier = Schemable::SchemaModifier.new +# +# path = "properties.name.items.[0].properties.age" +# parsed_path = schema_modifier.parse_path(path) +# # => [:properties, :name, :items, :'[0]', :properties, :age] +# +# schema = { properties: { name: "John" } } +# exists = schema_modifier.path_exists?(schema, "properties.name") +# # => true +# +# new_data = { age: 25 } +# merged_data = schema_modifier.deep_merge_hashes({ name: "John" }, new_data) +# # => { name: "John", age: 25 } +# +# original_schema = { properties: { name: "John" } } +# new_schema = { age: 25 } +# updated_schema = schema_modifier.add_properties(original_schema, new_schema, "properties") +# # => { properties: { name: "John", age: 25 } } +# +# schema = { properties: { name: "John", age: 25 } } +# path_to_delete = "properties.name.age" +# updated_schema = schema_modifier.delete_properties(schema, path_to_delete) +# # => { properties: { name: "John" } } +# +module Schemable + class SchemaModifier + def parse_path: (path: String) -> Array[Symbol] + def path_exists?: (schema: Hash[Symbol, any], path: String) -> bool + def deep_merge_hashes: (destination: Hash[Symbol, any], new_data: Hash[Symbol, any]) -> (Hash[Symbol, any] | Array[any]) + def add_properties: (original_schema: (Hash[Symbol, any] | Array[any]), new_schema: Hash[Symbol, any], path: String) -> (Hash[Symbol, any] | Array[any]) + def delete_properties: (original_schema: (Hash[Symbol, any] | Array[any]), path: String) -> (Hash[Symbol, any] | Array[any]) + end +end From 01049c47283bc46d905a1557bced71cc7d98517f Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Thu, 9 Nov 2023 10:00:47 +0300 Subject: [PATCH 12/87] Adds Configuration class for global configurations --- lib/schemable/configuration.rb | 32 ++++++++++++++++++++++++++++++++ sig/schemable/configuration.rbs | 17 +++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 lib/schemable/configuration.rb create mode 100644 sig/schemable/configuration.rbs diff --git a/lib/schemable/configuration.rb b/lib/schemable/configuration.rb new file mode 100644 index 0000000..97e75b9 --- /dev/null +++ b/lib/schemable/configuration.rb @@ -0,0 +1,32 @@ +# configuration.rb +module Schemable + class Configuration + attr_accessor( + :orm, + :timestamps, + :float_as_string, + :decimal_as_string, + :custom_type_mappers, + :disable_factory_bot + ) + + def initialize + @timestamps = true + @orm = :active_record # orm options are :active_record, :mongoid + @float_as_string = false + @custom_type_mappers = {} + @decimal_as_string = false + @disable_factory_bot = true + end + + def type_mapper(type_name) + return @custom_type_mappers[type_name] if @custom_type_mappers.key?(type_name) + + TYPES_MAP[type_name.try(:to_sym)] + end + + def add_custom_type_mapper(type_name, mapping) + custom_type_mappers[type_name.to_sym] = mapping + end + end +end diff --git a/sig/schemable/configuration.rbs b/sig/schemable/configuration.rbs new file mode 100644 index 0000000..af84948 --- /dev/null +++ b/sig/schemable/configuration.rbs @@ -0,0 +1,17 @@ +module Schemable + class Configuration + attr_accessor disable_factory_bot: untyped + attr_accessor orm: Symbol + attr_accessor timestamps: bool + attr_accessor float_as_string: bool + attr_accessor decimal_as_string: bool + attr_accessor disable_factory_bot: bool + attr_accessor custom_type_mappers: Hash[Symbol, Symbol | Object] + + def initialize: -> void + + def add_custom_type_mapper: (Symbol, Hash[Symbol, Symbol | Object]) -> void + + def type_mapper: (Symbol) -> Hash[Symbol, Symbol | Object] + end +end From bfdf6526eff2f31150e36c3e645b8b3439666696 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Thu, 9 Nov 2023 10:01:39 +0300 Subject: [PATCH 13/87] Adds Constants module to store constants --- lib/schemable/constants.rb | 41 +++++++++++++++++++++++++++++++++++++ sig/schemable/constants.rbs | 5 +++++ 2 files changed, 46 insertions(+) create mode 100644 lib/schemable/constants.rb create mode 100644 sig/schemable/constants.rbs diff --git a/lib/schemable/constants.rb b/lib/schemable/constants.rb new file mode 100644 index 0000000..92bfca4 --- /dev/null +++ b/lib/schemable/constants.rb @@ -0,0 +1,41 @@ +module Schemable + module Constants + TYPES_MAP = { + text: { type: :string }, + string: { type: :string }, + integer: { type: :integer }, + boolean: { type: :boolean }, + date: { type: :string, format: :date }, + time: { type: :string, format: :time }, + json: { type: :object, properties: {} }, + hash: { type: :object, properties: {} }, + jsonb: { type: :object, properties: {} }, + object: { type: :object, properties: {} }, + binary: { type: :string, format: :binary }, + trueclass: { type: :boolean, default: true }, + falseclass: { type: :boolean, default: false }, + datetime: { type: :string, format: :'date-time' }, + float: { + type: (configs[:float_as_string] ? :string : :number).to_s.to_sym, + format: :float + }, + decimal: { + type: (configs[:decimal_as_string] ? :string : :number).to_s.to_sym, + format: :double + }, + array: { + type: :array, + items: { + anyOf: [ + { type: :string }, + { type: :integer }, + { type: :boolean }, + { type: :number, format: :float }, + { type: :object, properties: {} }, + { type: :number, format: :double } + ] + } + } + }.freeze + end +end diff --git a/sig/schemable/constants.rbs b/sig/schemable/constants.rbs new file mode 100644 index 0000000..cf93edf --- /dev/null +++ b/sig/schemable/constants.rbs @@ -0,0 +1,5 @@ +module Schemable + module Constants + TYPES_MAP: Hash[Symbol, Symbol | Object] + end +end From 5a0efd1b60a9794e268c21e59a332c0a8a013b78 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Thu, 9 Nov 2023 10:18:44 +0300 Subject: [PATCH 14/87] Uses any for values of a Hash in .rbs files --- sig/schemable/configuration.rbs | 7 +++---- sig/schemable/constants.rbs | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/sig/schemable/configuration.rbs b/sig/schemable/configuration.rbs index af84948..74a47ec 100644 --- a/sig/schemable/configuration.rbs +++ b/sig/schemable/configuration.rbs @@ -1,17 +1,16 @@ module Schemable class Configuration - attr_accessor disable_factory_bot: untyped attr_accessor orm: Symbol attr_accessor timestamps: bool attr_accessor float_as_string: bool attr_accessor decimal_as_string: bool attr_accessor disable_factory_bot: bool - attr_accessor custom_type_mappers: Hash[Symbol, Symbol | Object] + attr_accessor custom_type_mappers: Hash[Symbol, any] def initialize: -> void - def add_custom_type_mapper: (Symbol, Hash[Symbol, Symbol | Object]) -> void + def add_custom_type_mapper: (Symbol, Hash[Symbol, any]) -> void - def type_mapper: (Symbol) -> Hash[Symbol, Symbol | Object] + def type_mapper: (Symbol) -> Hash[Symbol, any] end end diff --git a/sig/schemable/constants.rbs b/sig/schemable/constants.rbs index cf93edf..aab1390 100644 --- a/sig/schemable/constants.rbs +++ b/sig/schemable/constants.rbs @@ -1,5 +1,5 @@ module Schemable module Constants - TYPES_MAP: Hash[Symbol, Symbol | Object] + TYPES_MAP: Hash[Symbol, any] end end From bda6e081de1db69482cd580ac6d38821540e65cd Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Thu, 9 Nov 2023 10:19:29 +0300 Subject: [PATCH 15/87] Adds custom_defined_enum_method config to global configs --- lib/schemable/configuration.rb | 4 +++- sig/schemable/configuration.rbs | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/schemable/configuration.rb b/lib/schemable/configuration.rb index 97e75b9..c302e68 100644 --- a/lib/schemable/configuration.rb +++ b/lib/schemable/configuration.rb @@ -7,7 +7,8 @@ class Configuration :float_as_string, :decimal_as_string, :custom_type_mappers, - :disable_factory_bot + :disable_factory_bot, + :custom_defined_enum_method ) def initialize @@ -17,6 +18,7 @@ def initialize @custom_type_mappers = {} @decimal_as_string = false @disable_factory_bot = true + @custom_defined_enum_method = nil end def type_mapper(type_name) diff --git a/sig/schemable/configuration.rbs b/sig/schemable/configuration.rbs index 74a47ec..46a73e7 100644 --- a/sig/schemable/configuration.rbs +++ b/sig/schemable/configuration.rbs @@ -5,6 +5,7 @@ module Schemable attr_accessor float_as_string: bool attr_accessor decimal_as_string: bool attr_accessor disable_factory_bot: bool + attr_accessor custom_defined_enum_method: Symbol? attr_accessor custom_type_mappers: Hash[Symbol, any] def initialize: -> void From 33c2ef4bec71d5982ec5bd9395ae14b80680410a Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Thu, 9 Nov 2023 10:40:30 +0300 Subject: [PATCH 16/87] Adds use_serialized_instance config --- lib/schemable/configuration.rb | 2 ++ sig/schemable/configuration.rbs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/lib/schemable/configuration.rb b/lib/schemable/configuration.rb index c302e68..6a9a3e4 100644 --- a/lib/schemable/configuration.rb +++ b/lib/schemable/configuration.rb @@ -8,6 +8,7 @@ class Configuration :decimal_as_string, :custom_type_mappers, :disable_factory_bot, + :use_serialized_instance, :custom_defined_enum_method ) @@ -19,6 +20,7 @@ def initialize @decimal_as_string = false @disable_factory_bot = true @custom_defined_enum_method = nil + @use_serialized_instance = false end def type_mapper(type_name) diff --git a/sig/schemable/configuration.rbs b/sig/schemable/configuration.rbs index 46a73e7..bef6c84 100644 --- a/sig/schemable/configuration.rbs +++ b/sig/schemable/configuration.rbs @@ -5,9 +5,11 @@ module Schemable attr_accessor float_as_string: bool attr_accessor decimal_as_string: bool attr_accessor disable_factory_bot: bool + attr_accessor use_serialized_instance: bool attr_accessor custom_defined_enum_method: Symbol? attr_accessor custom_type_mappers: Hash[Symbol, any] + def initialize: -> void def add_custom_type_mapper: (Symbol, Hash[Symbol, any]) -> void From 213fecefd2b5ebbd0b5b18d2da2fe1840c70b1ad Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Thu, 9 Nov 2023 10:45:25 +0300 Subject: [PATCH 17/87] Adds AttributeSchemaGenerator class --- lib/schemable/attribute_schema_generator.rb | 92 ++++++++++++++++++++ sig/schemable/attribute_schema_generator.rbs | 14 +++ 2 files changed, 106 insertions(+) create mode 100644 lib/schemable/attribute_schema_generator.rb create mode 100644 sig/schemable/attribute_schema_generator.rbs diff --git a/lib/schemable/attribute_schema_generator.rb b/lib/schemable/attribute_schema_generator.rb new file mode 100644 index 0000000..4e34447 --- /dev/null +++ b/lib/schemable/attribute_schema_generator.rb @@ -0,0 +1,92 @@ +module Schemable + class AttributeSchemaGenerator + attr_accessor :model_definition, :configuration, :model, :schema_modifier, :response + + def initialize(model_definition, configuration) + @model_definition = model_definition + @model = model_definition.model + @configuration = configuration + @schema_modifier = SchemaModifier.new + @response = nil + end + + # Generate the JSON schema for attributes + def generate_attribute_schema + schema = { + type: :object, + properties: {} + } + + @model.attribute_names.each do |attribute| + schema[:properties][attribute.to_sym] = generate_property_schema(attribute) + end + + schema + end + + # Generate the JSON schema for a specific attribute + def generate_property_schema(attribute) + if @configuration.orm == :mongoid + # Get the column hash for the attribute + attribute_hash = @model.fields[attribute.to_s] + + # Check if this attribute has a custom JSON Schema definition + return @model_definition.array_types[attribute] if @model_definition.array_types.keys.include?(attribute) + return @model_definition.additional_response_attributes[attribute] if @model_definition.additional_response_attributes.keys.include?(attribute) + + # Check if this is an array attribute + return @configuration.type_mapper(:array) if attribute_hash.try(:[], 'options').try(:[], 'type') == 'Array' + + # Map the column type to a JSON Schema type if none of the above conditions are met + @response = @configuration.type_mapper(attribute_hash.try(:type).to_s.downcase.to_sym) + + elsif @configuration.orm == :active_record + # Get the column hash for the attribute + attribute_hash = @model.columns_hash[attribute.to_s] + + # Check if this attribute has a custom JSON Schema definition + return @model_definition.array_types[attribute] if @model_definition.array_types.keys.include?(attribute) + return @model_definition.additional_response_attributes[attribute] if @model_definition.additional_response_attributes.keys.include?(attribute) + + # Check if this is an array attribute + return @configuration.type_mapper(:array) if attribute_hash.as_json.try(:[], 'sql_type_metadata').try(:[], 'sql_type').include?('[]') + + # Map the column type to a JSON Schema type if none of the above conditions are met + @response = @configuration.type_mapper(attribute_hash.try(:type)) + + else + raise 'ORM not supported' + end + + # If the attribute is nullable, modify the schema accordingly + return @schema_modifier.add_properties(@response, { nullable: true }, '.') if @response && @model_definition.nullable_attributes.include?(attribute) + + # If attribute is an enum, modify the schema accordingly + if @configuration.custom_defined_enum_method + return @schema_modifier.add_properties(@response, { enum: @model.send(@configuration.custom_defined_enum_method, attribute.to_s) }, '.') if @response && @model.respond_to?(@configuration.custom_defined_enum_method) + elsif @model.respond_to?(:defined_enums) + return @schema_modifier.add_properties(@response, { enum: @model.defined_enums[attribute.to_s].keys }, '.') if @response && @model.defined_enums.key?(attribute.to_s) + end + + return @response unless @response.nil? + + # 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 + if @configuration.use_serialized_instance + serialized_instance = @model_definition.serialized_instance + + type_from_instance = serialized_instance.as_json['data']['attributes'][attribute.to_s.camelize(:lower)]&.class&.name&.downcase + + @response = @configuration.type_mapper(type_from_instance) if type_from_instance.present? + + return @response unless @response.nil? + end + + # If we still haven't found a schema type, default to object + @configuration.type_mapper(:object) + rescue NoMethodError + # Log a warning if the attribute does not exist on the @model + Rails.logger.warn("\e[33mWARNING: #{@model} does not have an attribute named \e[31m#{attribute}\e[0m") + {} + end + end +end diff --git a/sig/schemable/attribute_schema_generator.rbs b/sig/schemable/attribute_schema_generator.rbs new file mode 100644 index 0000000..a1248b4 --- /dev/null +++ b/sig/schemable/attribute_schema_generator.rbs @@ -0,0 +1,14 @@ +module Schemable + class AttributeSchemaGenerator + attr_accessor model: Class + attr_accessor model_definition: Definition + attr_accessor configuration: Configuration + attr_accessor response: Hash[Symbol, any]? + attr_accessor schema_modifier: SchemaModifier + + def initialize: (Definition, Configuration) -> void + def generate_attribute_schema: -> Hash[Symbol, any] + + def generate_property_schema: (Symbol) -> Hash[Symbol, any] + end +end From 15fae701b58f8230b93941fa300525e8c23b260a Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Thu, 9 Nov 2023 10:53:58 +0300 Subject: [PATCH 18/87] Updates method names and generate_attributes_schema's logic --- lib/schemable/attribute_schema_generator.rb | 16 +++++++++++----- sig/schemable/attribute_schema_generator.rbs | 4 ++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/schemable/attribute_schema_generator.rb b/lib/schemable/attribute_schema_generator.rb index 4e34447..2773073 100644 --- a/lib/schemable/attribute_schema_generator.rb +++ b/lib/schemable/attribute_schema_generator.rb @@ -11,21 +11,27 @@ def initialize(model_definition, configuration) end # Generate the JSON schema for attributes - def generate_attribute_schema + def generate_attributes_schema schema = { type: :object, - properties: {} + properties: attributes.index_with do |attr| + generate_attribute_schema(attr) + end } - @model.attribute_names.each do |attribute| - schema[:properties][attribute.to_sym] = generate_property_schema(attribute) + # modify the schema to include additional response relations + schema = @schema_modifier.add_properties(schema, @model_definition.additional_response_attributes, 'properties') + + # modify the schema to exclude response relations + @model_definition.excluded_response_attributes.each do |key| + schema = @schema_modifier.delete_properties(schema, "properties.#{key}") end schema end # Generate the JSON schema for a specific attribute - def generate_property_schema(attribute) + def generate_attribute_schema(attribute) if @configuration.orm == :mongoid # Get the column hash for the attribute attribute_hash = @model.fields[attribute.to_s] diff --git a/sig/schemable/attribute_schema_generator.rbs b/sig/schemable/attribute_schema_generator.rbs index a1248b4..417fb6c 100644 --- a/sig/schemable/attribute_schema_generator.rbs +++ b/sig/schemable/attribute_schema_generator.rbs @@ -7,8 +7,8 @@ module Schemable attr_accessor schema_modifier: SchemaModifier def initialize: (Definition, Configuration) -> void - def generate_attribute_schema: -> Hash[Symbol, any] + def generate_attributes_schema: -> (Hash[Symbol, any] | Array[any]) - def generate_property_schema: (Symbol) -> Hash[Symbol, any] + def generate_attribute_schema: (Symbol) -> Hash[Symbol, any] end end From 00c4b7fc7e9f0bd8c3891d02e7b4eb99a5605b5a Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Thu, 9 Nov 2023 11:38:05 +0300 Subject: [PATCH 19/87] Invokes attributes from @model_definition --- lib/schemable/attribute_schema_generator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/schemable/attribute_schema_generator.rb b/lib/schemable/attribute_schema_generator.rb index 2773073..57f3589 100644 --- a/lib/schemable/attribute_schema_generator.rb +++ b/lib/schemable/attribute_schema_generator.rb @@ -14,7 +14,7 @@ def initialize(model_definition, configuration) def generate_attributes_schema schema = { type: :object, - properties: attributes.index_with do |attr| + properties: @model_definition.attributes&.index_with do |attr| generate_attribute_schema(attr) end } From cab957709d4b77b1fe60f01b1bdc605cfef9571c Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Thu, 9 Nov 2023 15:13:24 +0300 Subject: [PATCH 20/87] Moves types map into configuration --- lib/schemable/configuration.rb | 39 ++++++++++++++++++++++++++++++-- lib/schemable/constants.rb | 41 ---------------------------------- sig/schemable/constants.rbs | 5 ----- 3 files changed, 37 insertions(+), 48 deletions(-) delete mode 100644 lib/schemable/constants.rb delete mode 100644 sig/schemable/constants.rbs diff --git a/lib/schemable/configuration.rb b/lib/schemable/configuration.rb index 6a9a3e4..85d3a00 100644 --- a/lib/schemable/configuration.rb +++ b/lib/schemable/configuration.rb @@ -1,4 +1,3 @@ -# configuration.rb module Schemable class Configuration attr_accessor( @@ -26,7 +25,43 @@ def initialize def type_mapper(type_name) return @custom_type_mappers[type_name] if @custom_type_mappers.key?(type_name) - TYPES_MAP[type_name.try(:to_sym)] + { + text: { type: :string }, + string: { type: :string }, + integer: { type: :integer }, + boolean: { type: :boolean }, + date: { type: :string, format: :date }, + time: { type: :string, format: :time }, + json: { type: :object, properties: {} }, + hash: { type: :object, properties: {} }, + jsonb: { type: :object, properties: {} }, + object: { type: :object, properties: {} }, + binary: { type: :string, format: :binary }, + trueclass: { type: :boolean, default: true }, + falseclass: { type: :boolean, default: false }, + datetime: { type: :string, format: :'date-time' }, + float: { + type: (@float_as_string ? :string : :number).to_s.to_sym, + format: :float + }, + decimal: { + type: (@decimal_as_string ? :string : :number).to_s.to_sym, + format: :double + }, + array: { + type: :array, + items: { + anyOf: [ + { type: :string }, + { type: :integer }, + { type: :boolean }, + { type: :number, format: :float }, + { type: :object, properties: {} }, + { type: :number, format: :double } + ] + } + } + }[type_name.try(:to_sym)] end def add_custom_type_mapper(type_name, mapping) diff --git a/lib/schemable/constants.rb b/lib/schemable/constants.rb deleted file mode 100644 index 92bfca4..0000000 --- a/lib/schemable/constants.rb +++ /dev/null @@ -1,41 +0,0 @@ -module Schemable - module Constants - TYPES_MAP = { - text: { type: :string }, - string: { type: :string }, - integer: { type: :integer }, - boolean: { type: :boolean }, - date: { type: :string, format: :date }, - time: { type: :string, format: :time }, - json: { type: :object, properties: {} }, - hash: { type: :object, properties: {} }, - jsonb: { type: :object, properties: {} }, - object: { type: :object, properties: {} }, - binary: { type: :string, format: :binary }, - trueclass: { type: :boolean, default: true }, - falseclass: { type: :boolean, default: false }, - datetime: { type: :string, format: :'date-time' }, - float: { - type: (configs[:float_as_string] ? :string : :number).to_s.to_sym, - format: :float - }, - decimal: { - type: (configs[:decimal_as_string] ? :string : :number).to_s.to_sym, - format: :double - }, - array: { - type: :array, - items: { - anyOf: [ - { type: :string }, - { type: :integer }, - { type: :boolean }, - { type: :number, format: :float }, - { type: :object, properties: {} }, - { type: :number, format: :double } - ] - } - } - }.freeze - end -end diff --git a/sig/schemable/constants.rbs b/sig/schemable/constants.rbs deleted file mode 100644 index aab1390..0000000 --- a/sig/schemable/constants.rbs +++ /dev/null @@ -1,5 +0,0 @@ -module Schemable - module Constants - TYPES_MAP: Hash[Symbol, any] - end -end From b4e995a250431aa17c53e6da8e554535206e0ec6 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Thu, 9 Nov 2023 15:25:04 +0300 Subject: [PATCH 21/87] Temporarily Updates Schemable Module --- lib/schemable.rb | 947 +--------------------------------------------- sig/schemable.rbs | 4 +- 2 files changed, 20 insertions(+), 931 deletions(-) diff --git a/lib/schemable.rb b/lib/schemable.rb index 41aedfb..032b2d8 100644 --- a/lib/schemable.rb +++ b/lib/schemable.rb @@ -1,946 +1,33 @@ # frozen_string_literal: true require_relative 'schemable/version' -require 'active_support/concern' +require_relative 'schemable/definition' +require_relative 'schemable/configuration' +require_relative 'schemable/schema_modifier' +require_relative 'schemable/attribute_schema_generator' module Schemable class Error < StandardError; end - extend ActiveSupport::Concern + class << self + attr_accessor :configuration - included do - # Maps a given type name to a corresponding JSON schema object that represents that type. - # - # @param type_name [String, Symbol] A String or Symbol representing the type of the property to be mapped. - # - # @return [Hash, nil] A Hash that represents a JSON schema object for the given type, or nil if the type is not recognized. - def type_mapper(type_name) - { - text: { type: :string }, - string: { type: :string }, - integer: { type: :integer }, - float: { type: (configs[:float_as_string] ? :string : :number).to_s.to_sym, format: :float }, - decimal: { type: (configs[:decimal_as_string] ? :string : :number).to_s.to_sym, format: :double }, - datetime: { type: :string, format: :'date-time' }, - date: { type: :string, format: :date }, - time: { type: :string, format: :time }, - boolean: { type: :boolean }, - trueclass: { type: :boolean, default: true }, - falseclass: { type: :boolean, default: false }, - binary: { type: :string, format: :binary }, - json: { type: :object, properties: {} }, - jsonb: { type: :object, properties: {} }, - array: { type: :array, items: { anyOf: [ - { type: :string }, { type: :integer }, { type: :number, format: :float }, { type: :number, format: :double }, { type: :boolean }, { type: :object, properties: {} } - ] } }, - hash: { type: :object, properties: {} }, - object: { type: :object, properties: {} } - }[type_name.try(:to_sym)] + def configure + @configuration ||= Configuration.new + yield(@configuration) if block_given? end - # Modify a JSON schema object by merging new properties into it or deleting a specified path. - # - # @param original_schema [Hash] The original schema object to modify. - # - # @param new_props [Hash] The new properties to merge into the schema. - # - # @param given_path [String, nil] The path to the property to modify or delete, if any. Use dot notation to specify nested properties (e.g. "person.address.city"). - # - # @param delete [Boolean] Whether to delete the property at the given path, if it exists. - # - # @raise [ArgumentError] If `delete` is true but `given_path` is nil, or if `given_path` does not exist in the original schema. - # - # @return [Hash] A new schema object with the specified modifications. - # - # @example - # `Merge new properties into the schema` - # - # original_schema = { type: 'object', properties: { name: { type: 'string' } } } - # new_props = { properties: { age: { type: 'integer' } } } - # modify_schema(original_schema, new_props) - # => {type: 'object', properties: {name: {type: 'string'}, age: {type: 'integer'}}} - # - # @example - # `Delete a property from the schema` - # - # original_schema = { type: 'object', properties: { name: { type: 'string' } } } - # modify_schema(original_schema, {}, 'properties.name', delete: true) - # => {type: 'object', properties: {}} - def modify_schema(original_schema, new_props, given_path = nil, delete: false) - return new_props if original_schema.nil? + def generate_schemas + klasses = Schemable::Definition.descendants + generated_schemas = [] - raise ArgumentError, 'Cannot delete without a given path' if given_path.nil? && delete - - if given_path.present? - path_segments = given_path.split('.').map(&:to_sym) - - if path_segments.size == 1 - raise ArgumentError, 'Given path does not exist in the original schema' unless original_schema.key?(path_segments.first) - else - raise ArgumentError, 'Given path does not exist in the original schema' unless original_schema.dig(*path_segments[0..-2]).is_a?(Hash) && original_schema.dig(*path_segments) - end - - path_hash = path_segments.reverse.reduce(new_props) { |a, n| { n => a } } - - if delete - new_schema = original_schema.deep_dup - parent_hash = path_segments.size > 1 ? new_schema.dig(*path_segments[0..-2]) : new_schema - parent_hash.delete(path_segments.last) - new_schema - else - original_schema.deep_merge(path_hash) - end - else - original_schema.deep_merge(new_props) - end - end - - # Returns a JSON Schema attribute definition for a given attribute on the model. - # - # @param attribute [Symbol] The name of the attribute. - # - # @raise [NoMethodError] If the `model` object does not respond to `columns_hash`. - # - # @return [Hash] The JSON Schema attribute definition as a Hash or an empty Hash if the attribute does not exist on the model. - # - # @example - # attribute_schema(:title) => { "type": "string" } - def attribute_schema(attribute) - # Get the column hash for the attribute - column_hash = model.columns_hash[attribute.to_s] - - # Check if this attribute has a custom JSON Schema definition - return array_types[attribute] if array_types.keys.include?(attribute) - - return additional_response_attributes[attribute] if additional_response_attributes.keys.include?(attribute) - - # Check if this is an array attribute - return type_mapper(:array) if column_hash.as_json.try(:[], 'sql_type_metadata').try(:[], 'sql_type').include?('[]') - - # Map the column type to a JSON Schema type if none of the above conditions are met - response = type_mapper(column_hash.try(:type)) - - # If the attribute is nullable, modify the schema accordingly - return modify_schema(response, { nullable: true }) if response && nullable_attributes.include?(attribute) - - # If attribute is an enum, modify the schema accordingly - return modify_schema(response, { type: :string, enum: model.defined_enums[attribute.to_s].keys }) if response && model.defined_enums.key?(attribute.to_s) - - return response unless response.nil? - - # 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 - type_from_factory = @instance.as_json['data']['attributes'][attribute.to_s.camelize(:lower)].class.name.downcase - response = type_mapper(type_from_factory) if type_from_factory.present? - - return response unless response.nil? - - # If we still haven't found a schema type, default to object - type_mapper(:object) - rescue NoMethodError - # Log a warning if the attribute does not exist on the model - Rails.logger.warn("\e[33mWARNING: #{model} does not have an attribute named \e[31m#{attribute}\e[0m") - {} - end - - # Returns a JSON Schema for the model's attributes. - # This method is used to generate the schema for the `attributes` that are automatically generated by using the `attribute_schema` method on each attribute. - # - # @note The `additional_response_attributes` and `excluded_response_attributes` are applied to the schema returned by this method. - # - # @return [Hash] The JSON Schema for the model's attributes. - # - # @example - # { - # type: :object, - # properties: { - # id: { type: :string }, - # title: { type: :string } - # } - # } - def attributes_schema - schema = { - type: :object, - properties: attributes.index_with do |attr| - attribute_schema(attr) - end - } - - # modify the schema to include additional response relations - schema = modify_schema(schema, additional_response_attributes, 'properties') - - # modify the schema to exclude response relations - excluded_response_attributes.each do |key| - schema = modify_schema(schema, {}, "properties.#{key}", delete: true) - end - - schema - end - - # Generates the schema for the relationships of a resource. - # - # @note The `additional_response_relations` and `excluded_response_relations` are applied to the schema returned by this method. - # - # @param relations [Hash] A hash representing the relationships of the resource in the form of { belongs_to: {}, has_many: {} }. If not provided, the relationships will be inferred from the model's associations. - # - # @param expand [Boolean] A boolean indicating whether to expand the relationships in the schema. - # - # @param exclude_from_expansion [Array] An array of relationship names to exclude from expansion. - # - # @return [Hash] A hash representing the schema for the relationships. - # - # @example - # { - # type: :object, - # properties: { - # province: { - # type: :object, - # properties: { - # meta: { - # type: :object, - # properties: { - # included: { - # type: :boolean, default: false - # } - # } - # } - # } - # } - # } - # } - def relationships_schema(relations = try(:relationships), expand: false, exclude_from_expansion: []) - return {} if relations.blank? - return {} if relations == { belongs_to: {}, has_many: {} } - - schema = { - type: :object, - properties: relations.reduce({}) do |props, (relation_type, relation_definitions)| - non_expanded_data_properties = { - type: :object, - properties: { - meta: { - type: :object, - properties: { - included: { type: :boolean, default: false } - } - } - } - } - - if relation_type == :has_many - props.merge!( - relation_definitions.keys.index_with do |relationship| - result = { - type: :object, - properties: { - data: { - type: :array, - items: { - type: :object, - properties: { - id: { type: :string }, - type: { type: :string, default: relation_definitions[relationship].model_name } - } - } - } - } - } - - result = non_expanded_data_properties if !expand || exclude_from_expansion.include?(relationship) - - result - end - ) - else - props.merge!( - relation_definitions.keys.index_with do |relationship| - result = { - type: :object, - properties: { - data: { - type: :object, - properties: { - id: { type: :string }, - type: { type: :string, default: relation_definitions[relationship].model_name } - - } - } - } - } - - result = non_expanded_data_properties if !expand || exclude_from_expansion.include?(relationship) - - result - end - ) - end - end - } - - # modify the schema to include additional response relations - schema = modify_schema(schema, additional_response_relations, 'properties') - - # modify the schema to exclude response relations - excluded_response_relations.each do |key| - schema = modify_schema(schema, {}, "properties.#{key}", delete: true) - end - - schema - end - - # Generates the schema for the included resources in a response. - # - # @note The `additional_response_includes` and `excluded_response_includes` (yet to be implemented) are applied to the schema returned by this method. - # - # @param relations [Hash] A hash representing the relationships of the resource in the form of { belongs_to: {}, has_many: {} }. If not provided, the relationships will be inferred from the model's associations. - # - # @param expand [Boolean] A boolean indicating whether to expand the relationships of the relationships in the schema. - # - # @param exclude_from_expansion [Array] An array of relationship names to exclude from expansion. - # - # @param metadata [Hash] Additional metadata to include in the schema, usually received from the nested_relationships method sent by the response_schema method. - # - # @return [Hash] A hash representing the schema for the included resources. - # - # @example - # { - # included: { - # type: :array, - # items: { - # anyOf: - # [ - # { - # type: :object, - # properties: { - # type: { type: :string, default: "provinces" }, - # id: { type: :string }, - # attributes: { - # type: :object, - # properties: { - # id: { type: :string }, - # name: { type: :string } - # } - # } - # } - # } - # ] - # } - # } - # } - def included_schema(relations = try(:relationships), expand: false, exclude_from_expansion: [], metadata: {}) - return {} if relations.blank? - return {} if relations == { belongs_to: {}, has_many: {} } - - schema = { - included: { - type: :array, - items: { - anyOf: - relations.reduce([]) do |props, (_relation_type, relation_definitions)| - props + relation_definitions.keys.reduce([]) do |props, relationship| - props + [ - unless exclude_from_expansion.include?(relationship) - { - type: :object, - properties: { - type: { type: :string, default: relation_definitions[relationship].model_name }, - id: { type: :string }, - attributes: begin - relation_definitions[relationship].new.attributes_schema || {} - rescue NoMethodError - {} - end - }.merge( - if relation_definitions[relationship].new.relationships != { belongs_to: {}, has_many: {} } || relation_definitions[relationship].new.relationships.blank? - if !expand || metadata.blank? - { relationships: relation_definitions[relationship].new.relationships_schema(expand: false) } - else - { relationships: relation_definitions[relationship].new.relationships_schema(relations = metadata[:nested_relationships][relationship], expand: true, exclude_from_expansion:) } - end - else - {} - end - ) - } - end - ].concat( - [ - if expand && metadata.present? && exclude_from_expansion.exclude?(relationship) - extra_relations = [] - metadata[:nested_relationships].keys.reduce({}) do |props, nested_relationship| - next if metadata[:nested_relationships][relationship].blank? - - props.merge!(metadata[:nested_relationships][nested_relationship].keys.each_with_object({}) do |relationship_type, _inner_props| - props.merge!(metadata[:nested_relationships][nested_relationship][relationship_type].keys.each_with_object({}) do |relationship, _inner_inner_props| - extra_relation_schema = { - type: :object, - properties: { - type: { type: :string, default: metadata[:nested_relationships][nested_relationship][relationship_type][relationship].model_name }, - id: { type: :string }, - attributes: metadata[:nested_relationships][nested_relationship][relationship_type][relationship].new.attributes_schema - }.merge( - 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? - {} - else - result = { relationships: metadata[:nested_relationships][nested_relationship][relationship_type][relationship].new.relationships_schema(expand: false) } - return {} if result == { relationships: {} } - - result - end - ) - } - - extra_relations << extra_relation_schema - end) - end) - end - - extra_relations - end - ].flatten - ).compact_blank - end - end - } - } - } - - modify_schema(schema, additional_response_included, 'included.items') - end - - # Generates the schema for the response of a resource or collection of resources in JSON API format. - # - # @param relations [Hash] A hash representing the relationships of the resource in the form of { belongs_to: {}, has_many: {} }. If not provided, the relationships will be inferred from the model's associations. - # - # @param expand [Boolean] A boolean indicating whether to expand the relationships of the relationships in the schema. - # - # @param exclude_from_expansion [Array] An array of relationship names to exclude from expansion. - # - # @param multi [Boolean] A boolean indicating whether the response contains multiple resources. - # - # @param nested [Boolean] A boolean indicating whether the response is to be expanded further than the first level of relationships. (expand relationships of relationships) - # - # @param metadata [Hash] Additional metadata to include in the schema, usually received from the nested_relationships method sent by the response_schema method. - # - # @return [Hash] A hash representing the schema for the response. - # - # @example - # "The returned schema will have a JSON API format, including the data (included attributes and relationships), included and meta keys." - def response_schema(relations = try(:relationships), expand: false, exclude_from_expansion: [], multi: false, nested: false, metadata: { nested_relationships: try(:nested_relationships) }) - data = { - type: :object, - properties: { - type: { type: :string, default: itself.class.model_name }, - id: { type: :string }, - attributes: attributes_schema - }.merge( - if relations.blank? || relations == { belongs_to: {}, has_many: {} } - {} - else - { relationships: relationships_schema(relations, expand:, exclude_from_expansion:) } - end - ) - } - - schema = if multi - { - data: { - type: :array, - items: data - } - } - else - { - data: - } - end - - schema.merge!( - if nested && expand - included_schema(relations, expand: nested, exclude_from_expansion:, metadata:) - elsif !nested && expand - included_schema(relations, expand: nested, exclude_from_expansion:) - else - {} - end - ).merge!( - if expand - {} - else - { meta: } - end - ).merge!( - jsonapi: - ) - - { - type: :object, - properties: schema - } - end - - # Generates the schema for the creation request payload of a resource. - # - # @note The `additional_create_request_attributes` and `excluded_create_request_attributes` applied to the returned schema by this method. - # @note The `required_attributes` are applied to the returned schema by this method. - # @note The `nullable_attributes` are applied to the returned schema by this method. - # - # @return [Hash] A hash representing the schema for the request payload. - # - # @example - # { - # type: :object, - # properties: { - # data: { - # type: :object, - # properties: { - # firstName: { type: :string }, - # lastName: { type: :string } - # }, - # required: [:firstName, :lastName] - # } - # } - # } - def create_request_schema - schema = { - type: :object, - properties: { - data: attributes_schema - } - } - - schema = modify_schema(schema, additional_create_request_attributes, 'properties.data.properties') - - excluded_create_request_attributes.each do |key| - schema = modify_schema(schema, {}, "properties.data.properties.#{key}", delete: true) + klasses.each do |klass| + model_definition = klass.new(configuration) + schema = AttributeSchemaGenerator.new(model_definition, configuration).generate_attributes_schema + generated_schemas << schema end - required_attributes = { - required: (schema.as_json['properties']['data']['properties'].keys - optional_create_request_attributes.map(&:to_s) - nullable_attributes.map(&:to_s)).map { |key| key.to_s.camelize(:lower).to_sym } - } - - modify_schema(schema, required_attributes, 'properties.data') - end - - # Generates the schema for the update request payload of a resource. - # - # @note The `additional_update_request_attributes` and `excluded_update_request_attributes` applied to the returned schema by this method. - # @note The `required_attributes` are applied to the returned schema by this method. - # @note The `nullable_attributes` are applied to the returned schema by this method. - # - # @return [Hash] A hash representing the schema for the request payload. - # - # @example - # { - # type: :object, - # properties: { - # data: { - # type: :object, - # properties: { - # firstName: { type: :string }, - # lastName: { type: :string } - # }, - # required: [:firstName, :lastName] - # } - # } - # } - def update_request_schema - schema = { - type: :object, - properties: { - data: attributes_schema - } - } - - schema = modify_schema(schema, additional_update_request_attributes, 'properties.data.properties') - - excluded_update_request_attributes.each do |key| - schema = modify_schema(schema, {}, "properties.data.properties.#{key}", delete: true) - end - - required_attributes = { - required: (schema.as_json['properties']['data']['properties'].keys - optional_update_request_attributes.map(&:to_s) - nullable_attributes.map(&:to_s)).map { |key| key.to_s.camelize(:lower).to_sym } - } - - modify_schema(schema, required_attributes, 'properties.data') - end - - # Returns the schema for the meta data of the response body. - # This is used to provide pagination information usually (in the case of a collection). - # - # @note Note that this is an opinionated schema and may not be suitable for all use cases. If you need to override this schema, you can do so by overriding the `meta` method in your definition. - # - # @return [Hash] The schema for the meta data of the response body. - def meta - { - type: :object, - properties: { - page: { - type: :object, - properties: { - totalPages: { - type: :integer, - default: 1 - }, - count: { - type: :integer, - default: 1 - }, - limitValue: { - type: :integer, - default: 1 - }, - currentPage: { - type: :integer, - default: 1 - } - } - } - } - } - end - - # Returns the schema for the JSONAPI version. - # - # @return [Hash] The schema for the JSONAPI version. - def jsonapi - { - type: :object, - properties: { - version: { - type: :string, - default: '1.0' - } - } - } - end - - # Returns the resource serializer to be used for serialization. This method must be implemented in the definition class. - # - # @abstract This method must be implemented in the definition class. - # - # @raise [NotImplementedError] If the method is not implemented in the definition class. - # - # @return [Class] The resource serializer class. - # - # @example - # V1::UserSerializer - # - def serializer - raise NotImplementedError, 'serializer method must be implemented in the definition class' - end - - # Returns the attributes defined in the serializer (Auto generated from the serializer). - # - # @return [Array, nil] The attributes defined in the serializer or nil if there are none. - # - # @example - # [:id, :name, :email, :created_at, :updated_at] - def attributes - serializer.attribute_blocks.transform_keys { |key| key.to_s.underscore.to_sym }.keys || nil - end - - # Returns the relationships defined in the serializer. - # - # @return [Hash] The relationships defined in the serializer. - # - # @note Note that the format of the relationships is as follows: - # { belongs_to: { relationship_name: relationship_definition }, has_many: { relationship_name: relationship_definition } - # - # @example - # { - # belongs_to: { - # district: Swagger::Definitions::District, - # user: Swagger::Definitions::User - # }, - # has_many: { - # applicants: Swagger::Definitions::Applicant, - # } - # } - def relationships - { belongs_to: {}, has_many: {} } - end - - # Returns a hash of all the arrays defined for the model. The schema for each array is defined in the definition class manually. - # This method must be implemented in the definition class if there are any arrays. - # - # @return [Hash] The arrays of the model and their schemas. - # - # @example - # { - # metadata: { - # type: :array, - # items: { - # type: :object, nullable: true, - # properties: { name: { type: :string, nullable: true } } - # } - # } - # } - def array_types - {} - end - - # Returns the attributes that are optional in the create request body. This means that they are not required to be present in the create request body thus they are taken out of the required array. - # - # @return [Array] The attributes that are optional in the create request body. - # - # @example - # [:name, :email] - def optional_create_request_attributes - %i[] - end - - # Returns the attributes that are optional in the update request body. This means that they are not required to be present in the update request body thus they are taken out of the required array. - # - # @return [Array] The attributes that are optional in the update request body. - # - # @example - # [:name, :email] - def optional_update_request_attributes - %i[] - end - - # 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. - # They are not required to be present in the request body. - # - # @return [Array] The attributes that are nullable in the request/response body. - # - # @example - # [:name, :email] - def nullable_attributes - %i[] - end - - # Returns the additional create request attributes that are not automatically generated. These attributes are appended to the create request schema. - # - # @return [Hash] The additional create request attributes that are not automatically generated (if any). - # - # @example - # { - # name: { type: :string } - # } - def additional_create_request_attributes - {} - end - - # Returns the additional update request attributes that are not automatically generated. These attributes are appended to the update request schema. - # - # @return [Hash] The additional update request attributes that are not automatically generated (if any). - # - # @example - # { - # name: { type: :string } - # } - def additional_update_request_attributes - {} - end - - # Returns the additional response attributes that are not automatically generated. These attributes are appended to the response schema. - # - # @return [Hash] The additional response attributes that are not automatically generated (if any). - # - # @example - # { - # name: { type: :string } - # } - def additional_response_attributes - {} - end - - # Returns the additional response relations that are not automatically generated. These relations are appended to the response schema's relationships. - # - # @return [Hash] The additional response relations that are not automatically generated (if any). - # - # @example - # { - # users: { - # type: :object, - # properties: { - # data: { - # type: :array, - # items: { - # type: :object, - # properties: { - # id: { type: :string }, - # type: { type: :string } - # } - # } - # } - # } - # } - # } - def additional_response_relations - {} - end - - # Returns the additional response included that are not automatically generated. These included are appended to the response schema's included. - # - # @return [Hash] The additional response included that are not automatically generated (if any). - # - # @example - # { - # type: :object, - # properties: { - # id: { type: :string }, - # type: { type: :string }, - # attributes: { - # type: :object, - # properties: { - # name: { type: :string } - # } - # } - # } - # } - def additional_response_included - {} - end - - # Returns the attributes that are excluded from the create request schema. - # These attributes are not required or not needed to be present in the create request body. - # - # @return [Array] The attributes that are excluded from the create request schema. - # - # @example - # [:id, :updated_at, :created_at] - def excluded_create_request_attributes - %i[] - end - - # Returns the attributes that are excluded from the update request schema. - # These attributes are not required or not needed to be present in the update request body. - # - # @return [Array] The attributes that are excluded from the update request schema. - # - # @example - # [:id, :updated_at, :created_at] - def excluded_update_request_attributes - %i[] - end - - # Returns the attributes that are excluded from the response schema. - # These attributes are not needed to be present in the response body. - # - # @return [Array] The attributes that are excluded from the response schema. - # - # @example - # [:id, :updated_at, :created_at] - def excluded_response_attributes - %i[] - end - - # Returns the relationships that are excluded from the response schema. - # These relationships are not needed to be present in the response body. - # - # @return [Array] The relationships that are excluded from the response schema. - # - # @example - # [:users, :applicants] - def excluded_response_relations - %i[] - end - - # Returns the included that are excluded from the response schema. - # These included are not needed to be present in the response body. - # - # @return [Array] The included that are excluded from the response schema. - # - # @example - # [:users, :applicants] - # - # @todo - # This method is not used anywhere yet. - def excluded_response_included - %i[] - end - - # Returns the relationships to be further expanded in the response schema. - # - # @return [Hash] The relationships to be further expanded in the response schema. - # - # @example - # { - # applicants: { - # belongs_to: { - # district: Swagger::Definitions::District, - # province: Swagger::Definitions::Province, - # }, - # has_many: { - # attachments: Swagger::Definitions::Upload, - # } - # } - # } - def nested_relationships - {} - end - - # Returns the model class (Constantized from the definition class name) - # - # @return [Class] The model class (Constantized from the definition class name) - # - # @example - # User - def model - self.class.name.gsub('Swagger::Definitions::', '').constantize - end - - # Returns the model name. Used for schema type naming. - # - # @return [String] The model name. - # - # @example - # 'users' for the User model - # 'citizen_applications' for the CitizenApplication model - def self.model_name - name.gsub('Swagger::Definitions::', '').pluralize.underscore.downcase - end - - # Returns the generated schemas in JSONAPI format that are used in the swagger documentation. - # - # @return [Array] The generated schemas in JSONAPI format that are used in the swagger documentation. - # - # @note This method is used for generating schema in 4 different formats: request (both create and update), response and response expanded. - # - # @option CreateRequest - # is the schema for the creation request body. - # @option UpdateRequest - # is the schema for the updating request body. - # @option Response - # is the schema for the response body (without any relationships expanded), used for collection responses. - # @option ResponseExpanded: The schema for the response body with all the relationships expanded, used for single resource responses. - # - # @note The returned schemas are in JSONAPI format are usually appended to the rswag component's 'schemas' in swagger_helper. - # - # @note The method can be overridden in the definition class if there are any additional customizations needed. - # - def self.definitions - schema_instance = new - [ - "#{schema_instance.model}CreateRequest": schema_instance.camelize_keys(schema_instance.create_request_schema), - "#{schema_instance.model}UpdateRequest": schema_instance.camelize_keys(schema_instance.update_request_schema), - "#{schema_instance.model}Response": schema_instance.camelize_keys(schema_instance.response_schema(multi: true)), - "#{schema_instance.model}ResponseExpanded": schema_instance.camelize_keys(schema_instance.response_schema(expand: true)) - ] - end - - # Given a hash, it returns a new hash with all the keys camelized. - # - # @param hash [Array | Hash] The hash with all the keys camelized. - # - # @return [Array | Hash] The hash with all the keys camelized. - # - # @example - # { first_name: 'John', last_name: 'Doe' } => { firstName: 'John', lastName: 'Doe' } - def camelize_keys(hash) - hash.deep_transform_keys { |key| key.to_s.camelize(:lower).to_sym } - end - - # Returns a json of config options for the definition class. - # - # @return [Hash] The config options for the definition class. - # - # @example - # { decimal_as_string: true } - def configs - {} + generated_schemas end end end diff --git a/sig/schemable.rbs b/sig/schemable.rbs index ec2c40f..46781e6 100644 --- a/sig/schemable.rbs +++ b/sig/schemable.rbs @@ -1,4 +1,6 @@ module Schemable VERSION: String - # See the writing guide of rbs: https://github.com/ruby/rbs#guides + def generate_schemas: () -> Array[Hash[Symbol, Hash[Symbol, any]]] + def configure: () { (Configuration) -> Configuration } -> Configuration + attr_accessor configuration: Configuration end From 8e3cd6e033e856e825fcd462eab388af9605c366 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Thu, 9 Nov 2023 15:25:31 +0300 Subject: [PATCH 22/87] Adds incomplete Definitions class --- lib/schemable/definition.rb | 292 +++++++++++++++++++++++++++++++++++ sig/schemable/definition.rbs | 5 + 2 files changed, 297 insertions(+) create mode 100644 lib/schemable/definition.rb create mode 100644 sig/schemable/definition.rbs diff --git a/lib/schemable/definition.rb b/lib/schemable/definition.rb new file mode 100644 index 0000000..065f00e --- /dev/null +++ b/lib/schemable/definition.rb @@ -0,0 +1,292 @@ +module Schemable + class Definition + attr_reader :configuration + + def initialize(configuration) + @configuration = configuration + end + + # Returns the resource serializer to be used for serialization. This method must be implemented in the definition class. + # + # @abstract This method must be implemented in the definition class. + # + # @raise [NotImplementedError] If the method is not implemented in the definition class. + # + # @return [Class] The resource serializer class. + # + # @example + # V1::UserSerializer + # + def serializer + raise NotImplementedError, 'serializer method must be implemented in the definition class' + end + + # Returns the attributes defined in the serializer (Auto generated from the serializer). + # + # @return [Array, nil] The attributes defined in the serializer or nil if there are none. + # + # @example + # [:id, :name, :email, :created_at, :updated_at] + def attributes + serializer.attribute_blocks.transform_keys { |key| key.to_s.underscore.to_sym }.keys || nil + end + + # Returns the relationships defined in the serializer. + # + # @return [Hash] The relationships defined in the serializer. + # + # @note Note that the format of the relationships is as follows: + # { belongs_to: { relationship_name: relationship_definition }, has_many: { relationship_name: relationship_definition } + # + # @example + # { + # belongs_to: { + # district: Swagger::Definitions::District, + # user: Swagger::Definitions::User + # }, + # has_many: { + # applicants: Swagger::Definitions::Applicant, + # } + # } + def relationships + { belongs_to: {}, has_many: {} } + end + + # Returns a hash of all the arrays defined for the model. The schema for each array is defined in the definition class manually. + # This method must be implemented in the definition class if there are any arrays. + # + # @return [Hash] The arrays of the model and their schemas. + # + # @example + # { + # metadata: { + # type: :array, + # items: { + # type: :object, nullable: true, + # properties: { name: { type: :string, nullable: true } } + # } + # } + # } + def array_types + {} + end + + # Returns the attributes that are optional in the create request body. This means that they are not required to be present in the create request body thus they are taken out of the required array. + # + # @return [Array] The attributes that are optional in the create request body. + # + # @example + # [:name, :email] + def optional_create_request_attributes + %i[] + end + + # Returns the attributes that are optional in the update request body. This means that they are not required to be present in the update request body thus they are taken out of the required array. + # + # @return [Array] The attributes that are optional in the update request body. + # + # @example + # [:name, :email] + def optional_update_request_attributes + %i[] + end + + # 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. + # They are not required to be present in the request body. + # + # @return [Array] The attributes that are nullable in the request/response body. + # + # @example + # [:name, :email] + def nullable_attributes + %i[] + end + + # Returns the additional create request attributes that are not automatically generated. These attributes are appended to the create request schema. + # + # @return [Hash] The additional create request attributes that are not automatically generated (if any). + # + # @example + # { + # name: { type: :string } + # } + def additional_create_request_attributes + {} + end + + # Returns the additional update request attributes that are not automatically generated. These attributes are appended to the update request schema. + # + # @return [Hash] The additional update request attributes that are not automatically generated (if any). + # + # @example + # { + # name: { type: :string } + # } + def additional_update_request_attributes + {} + end + + # Returns the additional response attributes that are not automatically generated. These attributes are appended to the response schema. + # + # @return [Hash] The additional response attributes that are not automatically generated (if any). + # + # @example + # { + # name: { type: :string } + # } + def additional_response_attributes + {} + end + + # Returns the additional response relations that are not automatically generated. These relations are appended to the response schema's relationships. + # + # @return [Hash] The additional response relations that are not automatically generated (if any). + # + # @example + # { + # users: { + # type: :object, + # properties: { + # data: { + # type: :array, + # items: { + # type: :object, + # properties: { + # id: { type: :string }, + # type: { type: :string } + # } + # } + # } + # } + # } + # } + def additional_response_relations + {} + end + + # Returns the additional response included that are not automatically generated. These included are appended to the response schema's included. + # + # @return [Hash] The additional response included that are not automatically generated (if any). + # + # @example + # { + # type: :object, + # properties: { + # id: { type: :string }, + # type: { type: :string }, + # attributes: { + # type: :object, + # properties: { + # name: { type: :string } + # } + # } + # } + # } + def additional_response_included + {} + end + + # Returns the attributes that are excluded from the create request schema. + # These attributes are not required or not needed to be present in the create request body. + # + # @return [Array] The attributes that are excluded from the create request schema. + # + # @example + # [:id, :updated_at, :created_at] + def excluded_create_request_attributes + %i[] + end + + # Returns the attributes that are excluded from the update request schema. + # These attributes are not required or not needed to be present in the update request body. + # + # @return [Array] The attributes that are excluded from the update request schema. + # + # @example + # [:id, :updated_at, :created_at] + def excluded_update_request_attributes + %i[] + end + + # Returns the attributes that are excluded from the response schema. + # These attributes are not needed to be present in the response body. + # + # @return [Array] The attributes that are excluded from the response schema. + # + # @example + # [:id, :updated_at, :created_at] + def excluded_response_attributes + %i[] + end + + # Returns the relationships that are excluded from the response schema. + # These relationships are not needed to be present in the response body. + # + # @return [Array] The relationships that are excluded from the response schema. + # + # @example + # [:users, :applicants] + def excluded_response_relations + %i[] + end + + # Returns the included that are excluded from the response schema. + # These included are not needed to be present in the response body. + # + # @return [Array] The included that are excluded from the response schema. + # + # @example + # [:users, :applicants] + # + # @todo + # This method is not used anywhere yet. + def excluded_response_included + %i[] + end + + # Returns the relationships to be further expanded in the response schema. + # + # @return [Hash] The relationships to be further expanded in the response schema. + # + # @example + # { + # applicants: { + # belongs_to: { + # district: Swagger::Definitions::District, + # province: Swagger::Definitions::Province, + # }, + # has_many: { + # attachments: Swagger::Definitions::Upload, + # } + # } + # } + def nested_relationships + {} + end + + # Returns the model class (Constantized from the definition class name) + # + # @return [Class] The model class (Constantized from the definition class name) + # + # @example + # User + def model + self.class.name.gsub('Swagger::Definitions::', '').gsub(':Class', '').constantize + end + + def serialized_instance + {} + end + + # Returns the model name. Used for schema type naming. + # + # @return [String] The model name. + # + # @example + # 'users' for the User model + # 'citizen_applications' for the CitizenApplication model + def self.model_name + name.gsub('Swagger::Definitions::', '').pluralize.underscore.downcase + end + end +end diff --git a/sig/schemable/definition.rbs b/sig/schemable/definition.rbs new file mode 100644 index 0000000..9a7e6b4 --- /dev/null +++ b/sig/schemable/definition.rbs @@ -0,0 +1,5 @@ +module Schemable + class Definition + def serializer: -> untyped + end +end From dd7598ba562886023291fd56e3e1212cf5204c06 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Thu, 9 Nov 2023 15:52:18 +0300 Subject: [PATCH 23/87] Adds configurations to infer attributes from --- lib/schemable/configuration.rb | 8 ++++++-- lib/schemable/definition.rb | 12 +++++++++--- sig/schemable/configuration.rbs | 3 +++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/schemable/configuration.rb b/lib/schemable/configuration.rb index 85d3a00..3c2dbbe 100644 --- a/lib/schemable/configuration.rb +++ b/lib/schemable/configuration.rb @@ -8,7 +8,9 @@ class Configuration :custom_type_mappers, :disable_factory_bot, :use_serialized_instance, - :custom_defined_enum_method + :custom_defined_enum_method, + :infer_attributes_from_custom_method, + :infer_attributes_from_jsonapi_serializable ) def initialize @@ -18,8 +20,10 @@ def initialize @custom_type_mappers = {} @decimal_as_string = false @disable_factory_bot = true - @custom_defined_enum_method = nil @use_serialized_instance = false + @custom_defined_enum_method = nil + @infer_attributes_from_custom_method = nil + @infer_attributes_from_jsonapi_serializable = false end def type_mapper(type_name) diff --git a/lib/schemable/definition.rb b/lib/schemable/definition.rb index 065f00e..122c36c 100644 --- a/lib/schemable/definition.rb +++ b/lib/schemable/definition.rb @@ -18,7 +18,9 @@ def initialize(configuration) # V1::UserSerializer # def serializer - raise NotImplementedError, 'serializer method must be implemented in the definition class' + 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 + + nil end # Returns the attributes defined in the serializer (Auto generated from the serializer). @@ -28,7 +30,11 @@ def serializer # @example # [:id, :name, :email, :created_at, :updated_at] def attributes - serializer.attribute_blocks.transform_keys { |key| key.to_s.underscore.to_sym }.keys || nil + return (serializer&.attribute_blocks&.transform_keys { |key| key.to_s.underscore.to_sym }&.keys || nil) if configuration.infer_attributes_from_jsonapi_serializable + + return model.send(configuration.infer_attributes_from_custom_method).map(&:to_sym) if configuration.infer_attributes_from_custom_method + + model.attribute_names end # Returns the relationships defined in the serializer. @@ -271,7 +277,7 @@ def nested_relationships # @example # User def model - self.class.name.gsub('Swagger::Definitions::', '').gsub(':Class', '').constantize + self.class.name.gsub('Swagger::Definitions::', '').constantize end def serialized_instance diff --git a/sig/schemable/configuration.rbs b/sig/schemable/configuration.rbs index bef6c84..9e776ff 100644 --- a/sig/schemable/configuration.rbs +++ b/sig/schemable/configuration.rbs @@ -1,5 +1,6 @@ module Schemable class Configuration + attr_accessor orm: Symbol attr_accessor timestamps: bool attr_accessor float_as_string: bool @@ -8,6 +9,8 @@ module Schemable attr_accessor use_serialized_instance: bool attr_accessor custom_defined_enum_method: Symbol? attr_accessor custom_type_mappers: Hash[Symbol, any] + attr_accessor infer_attributes_from_jsonapi_serializable: bool + attr_accessor infer_attributes_from_custom_method: Symbol? def initialize: -> void From 01d459bf5c9ce9721a918f80b730e9ef9f33d4b2 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Thu, 9 Nov 2023 16:55:49 +0300 Subject: [PATCH 24/87] Updates Definition Class --- lib/schemable/definition.rb | 207 +---------------------------- sig/schemable/definition.rbs | 248 ++++++++++++++++++++++++++++++++++- 2 files changed, 251 insertions(+), 204 deletions(-) diff --git a/lib/schemable/definition.rb b/lib/schemable/definition.rb index 122c36c..7a202f7 100644 --- a/lib/schemable/definition.rb +++ b/lib/schemable/definition.rb @@ -6,29 +6,12 @@ def initialize(configuration) @configuration = configuration end - # Returns the resource serializer to be used for serialization. This method must be implemented in the definition class. - # - # @abstract This method must be implemented in the definition class. - # - # @raise [NotImplementedError] If the method is not implemented in the definition class. - # - # @return [Class] The resource serializer class. - # - # @example - # V1::UserSerializer - # def serializer 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 nil end - # Returns the attributes defined in the serializer (Auto generated from the serializer). - # - # @return [Array, nil] The attributes defined in the serializer or nil if there are none. - # - # @example - # [:id, :name, :email, :created_at, :updated_at] def attributes return (serializer&.attribute_blocks&.transform_keys { |key| key.to_s.underscore.to_sym }&.keys || nil) if configuration.infer_attributes_from_jsonapi_serializable @@ -37,260 +20,78 @@ def attributes model.attribute_names end - # Returns the relationships defined in the serializer. - # - # @return [Hash] The relationships defined in the serializer. - # - # @note Note that the format of the relationships is as follows: - # { belongs_to: { relationship_name: relationship_definition }, has_many: { relationship_name: relationship_definition } - # - # @example - # { - # belongs_to: { - # district: Swagger::Definitions::District, - # user: Swagger::Definitions::User - # }, - # has_many: { - # applicants: Swagger::Definitions::Applicant, - # } - # } def relationships { belongs_to: {}, has_many: {} } end - # Returns a hash of all the arrays defined for the model. The schema for each array is defined in the definition class manually. - # This method must be implemented in the definition class if there are any arrays. - # - # @return [Hash] The arrays of the model and their schemas. - # - # @example - # { - # metadata: { - # type: :array, - # items: { - # type: :object, nullable: true, - # properties: { name: { type: :string, nullable: true } } - # } - # } - # } def array_types {} end - # Returns the attributes that are optional in the create request body. This means that they are not required to be present in the create request body thus they are taken out of the required array. - # - # @return [Array] The attributes that are optional in the create request body. - # - # @example - # [:name, :email] def optional_create_request_attributes %i[] end - # Returns the attributes that are optional in the update request body. This means that they are not required to be present in the update request body thus they are taken out of the required array. - # - # @return [Array] The attributes that are optional in the update request body. - # - # @example - # [:name, :email] def optional_update_request_attributes %i[] end - # 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. - # They are not required to be present in the request body. - # - # @return [Array] The attributes that are nullable in the request/response body. - # - # @example - # [:name, :email] def nullable_attributes %i[] end - # Returns the additional create request attributes that are not automatically generated. These attributes are appended to the create request schema. - # - # @return [Hash] The additional create request attributes that are not automatically generated (if any). - # - # @example - # { - # name: { type: :string } - # } def additional_create_request_attributes {} end - # Returns the additional update request attributes that are not automatically generated. These attributes are appended to the update request schema. - # - # @return [Hash] The additional update request attributes that are not automatically generated (if any). - # - # @example - # { - # name: { type: :string } - # } def additional_update_request_attributes {} end - # Returns the additional response attributes that are not automatically generated. These attributes are appended to the response schema. - # - # @return [Hash] The additional response attributes that are not automatically generated (if any). - # - # @example - # { - # name: { type: :string } - # } def additional_response_attributes {} end - # Returns the additional response relations that are not automatically generated. These relations are appended to the response schema's relationships. - # - # @return [Hash] The additional response relations that are not automatically generated (if any). - # - # @example - # { - # users: { - # type: :object, - # properties: { - # data: { - # type: :array, - # items: { - # type: :object, - # properties: { - # id: { type: :string }, - # type: { type: :string } - # } - # } - # } - # } - # } - # } def additional_response_relations {} end - # Returns the additional response included that are not automatically generated. These included are appended to the response schema's included. - # - # @return [Hash] The additional response included that are not automatically generated (if any). - # - # @example - # { - # type: :object, - # properties: { - # id: { type: :string }, - # type: { type: :string }, - # attributes: { - # type: :object, - # properties: { - # name: { type: :string } - # } - # } - # } - # } def additional_response_included {} end - # Returns the attributes that are excluded from the create request schema. - # These attributes are not required or not needed to be present in the create request body. - # - # @return [Array] The attributes that are excluded from the create request schema. - # - # @example - # [:id, :updated_at, :created_at] def excluded_create_request_attributes %i[] end - # Returns the attributes that are excluded from the update request schema. - # These attributes are not required or not needed to be present in the update request body. - # - # @return [Array] The attributes that are excluded from the update request schema. - # - # @example - # [:id, :updated_at, :created_at] def excluded_update_request_attributes %i[] end - # Returns the attributes that are excluded from the response schema. - # These attributes are not needed to be present in the response body. - # - # @return [Array] The attributes that are excluded from the response schema. - # - # @example - # [:id, :updated_at, :created_at] def excluded_response_attributes %i[] end - # Returns the relationships that are excluded from the response schema. - # These relationships are not needed to be present in the response body. - # - # @return [Array] The relationships that are excluded from the response schema. - # - # @example - # [:users, :applicants] def excluded_response_relations %i[] end - # Returns the included that are excluded from the response schema. - # These included are not needed to be present in the response body. - # - # @return [Array] The included that are excluded from the response schema. - # - # @example - # [:users, :applicants] - # - # @todo - # This method is not used anywhere yet. def excluded_response_included %i[] end - # Returns the relationships to be further expanded in the response schema. - # - # @return [Hash] The relationships to be further expanded in the response schema. - # - # @example - # { - # applicants: { - # belongs_to: { - # district: Swagger::Definitions::District, - # province: Swagger::Definitions::Province, - # }, - # has_many: { - # attachments: Swagger::Definitions::Upload, - # } - # } - # } def nested_relationships {} end - # Returns the model class (Constantized from the definition class name) - # - # @return [Class] The model class (Constantized from the definition class name) - # - # @example - # User - def model - self.class.name.gsub('Swagger::Definitions::', '').constantize - end - def serialized_instance {} end - # Returns the model name. Used for schema type naming. - # - # @return [String] The model name. - # - # @example - # 'users' for the User model - # 'citizen_applications' for the CitizenApplication model + def model + self.class.name.gsub('Swagger::Definitions::', '').constantize + end + def self.model_name name.gsub('Swagger::Definitions::', '').pluralize.underscore.downcase end diff --git a/sig/schemable/definition.rbs b/sig/schemable/definition.rbs index 9a7e6b4..cb29332 100644 --- a/sig/schemable/definition.rbs +++ b/sig/schemable/definition.rbs @@ -1,5 +1,251 @@ module Schemable class Definition - def serializer: -> untyped + attr_accessor configuration: Configuration + + # Initializes the definition with the configuration. + def initialize: (Configuration) -> void + + # 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. + # They are not required to be present in the request body. + # + # @return [Array] The attributes that are nullable in the request/response body. + # + # @example + # [:name, :email] + def nullable_attributes: -> Array[Symbol] + + # Returns the additional create request attributes that are not automatically generated. These attributes are appended to the create request schema. + # + # @return [Hash] The additional create request attributes that are not automatically generated (if any). + # + # @example + # { + # name: { type: :string } + # } + def additional_create_request_attributes: -> Hash[Symbol, any] + + # Returns the additional update request attributes that are not automatically generated. These attributes are appended to the update request schema. + # + # @return [Hash] The additional update request attributes that are not automatically generated (if any). + # + # @example + # { + # name: { type: :string } + # } + def additional_update_request_attributes: -> Hash[Symbol, any] + + # Returns the additional response attributes that are not automatically generated. These attributes are appended to the response schema. + # + # @return [Hash] The additional response attributes that are not automatically generated (if any). + # + # @example + # { + # name: { type: :string } + # } + def additional_response_attributes: -> Hash[Symbol, any] + + # Returns the additional response relations that are not automatically generated. These relations are appended to the response schema's relationships. + # + # @return [Hash] The additional response relations that are not automatically generated (if any). + # + # @example + # { + # users: { + # type: :object, + # properties: { + # data: { + # type: :array, + # items: { + # type: :object, + # properties: { + # id: { type: :string }, + # type: { type: :string } + # } + # } + # } + # } + # } + # } + def additional_response_relations: -> Hash[Symbol, any] + + # Returns the additional response included that are not automatically generated. These included are appended to the response schema's included. + # + # @return [Hash] The additional response included that are not automatically generated (if any). + # + # @example + # { + # type: :object, + # properties: { + # id: { type: :string }, + # type: { type: :string }, + # attributes: { + # type: :object, + # properties: { + # name: { type: :string } + # } + # } + # } + # } + def additional_response_included: -> Hash[Symbol, any] + + # Returns the attributes that are excluded from the create request schema. + # These attributes are not required or not needed to be present in the create request body. + # + # @return [Array] The attributes that are excluded from the create request schema. + # + # @example + # [:id, :updated_at, :created_at] + def excluded_create_request_attributes: -> Array[Symbol] + + # Returns the attributes that are excluded from the response schema. + # These attributes are not needed to be present in the response body. + # + # @return [Array] The attributes that are excluded from the response schema. + # + # @example + # [:id, :updated_at, :created_at] + def excluded_response_attributes: -> Array[Symbol] + + # Returns the attributes that are excluded from the update request schema. + # These attributes are not required or not needed to be present in the update request body. + # + # @return [Array] The attributes that are excluded from the update request schema. + # + # @example + # [:id, :updated_at, :created_at] + def excluded_update_request_attributes: -> Array[Symbol] + + # Returns the relationships that are excluded from the response schema. + # These relationships are not needed to be present in the response body. + # + # @return [Array] The relationships that are excluded from the response schema. + # + # @example + # [:users, :applicants] + def excluded_response_relations: -> Array[Symbol] + + # Returns the included that are excluded from the response schema. + # These included are not needed to be present in the response body. + # + # @return [Array] The included that are excluded from the response schema. + # + # @example + # [:users, :applicants] + # + # @todo + # This method is not used anywhere yet. + def excluded_response_included: -> Array[Symbol] + + # Returns the relationships to be further expanded in the response schema. + # + # @return [Hash] The relationships to be further expanded in the response schema. + # + # @example + # { + # applicants: { + # belongs_to: { + # district: Swagger::Definitions::District, + # province: Swagger::Definitions::Province, + # }, + # has_many: { + # attachments: Swagger::Definitions::Upload, + # } + # } + # } + def nested_relationships: -> Hash[Symbol, any] + + # Returns the resource serializer to be used for serialization. This method must be implemented in the definition class. + # + # @abstract This method must be implemented in the definition class. + # + # @raise [NotImplementedError] If the method is not implemented in the definition class. + # + # @return [Class] The resource serializer class. + # + # @example + # V1::UserSerializer + def serializer: -> Class? + + # Returns the attributes defined in the serializer (Auto generated from the serializer), or from a custom method, or from attributes_names method. + # + # @return [Array] The attributes to be generated. + # + # @example + # [:id, :name, :email, :created_at, :updated_at] + def attributes: -> Array[Symbol] + + # Returns the relationships defined in the serializer. + # + # @return [Hash] The relationships defined in the serializer. + # + # @note Note that the format of the relationships is as follows: + # { belongs_to: { relationship_name: relationship_definition }, has_many: { relationship_name: relationship_definition } + # + # @example + # { + # belongs_to: { + # district: Swagger::Definitions::District, + # user: Swagger::Definitions::User + # }, + # has_many: { + # applicants: Swagger::Definitions::Applicant, + # } + # } + def relationships: -> Hash[Symbol, any] + + # Returns a hash of all the arrays defined for the model. The schema for each array is defined in the definition class manually. + # This method must be implemented in the definition class if there are any arrays. + # + # @return [Hash] The arrays of the model and their schemas. + # + # @example + # { + # metadata: { + # type: :array, + # items: { + # type: :object, nullable: true, + # properties: { name: { type: :string, nullable: true } } + # } + # } + # } + def array_types: -> Hash[Symbol, any] + + # Returns the attributes that are optional in the create request body. This means that they are not required to be present in the create request body thus they are taken out of the required array. + # + # @return [Array] The attributes that are optional in the create request body. + # + # @example + # [:name, :email] + def optional_create_request_attributes: -> Array[Symbol] + + # Returns the attributes that are optional in the update request body. This means that they are not required to be present in the update request body thus they are taken out of the required array. + # + # @return [Array] The attributes that are optional in the update request body. + # + # @example + # [:name, :email] + def optional_update_request_attributes: -> Array[Symbol] + + # Returns an instance of the model class that is already serialized into jsonapi format. + # + # @return [Hash] The serialized instance of the model class. + def serialized_instance: -> Hash[Symbol, any] + + # Returns the model class (Constantized from the definition class name) + # + # @return [Class] The model class (Constantized from the definition class name) + # + # @example + # User + def model: -> Class + + # Returns the model name. Used for schema type naming. + # + # @return [String] The model name. + # + # @example + # 'users' for the User model + # 'citizen_applications' for the CitizenApplication model + def self.model_name: -> String end end From 8b00983c57b2bb43a19d92a44e77413459fb7318 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Thu, 9 Nov 2023 16:57:20 +0300 Subject: [PATCH 25/87] Deletes Blanks --- sig/schemable/attribute_schema_generator.rbs | 1 - sig/schemable/configuration.rbs | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/sig/schemable/attribute_schema_generator.rbs b/sig/schemable/attribute_schema_generator.rbs index 417fb6c..4a05c64 100644 --- a/sig/schemable/attribute_schema_generator.rbs +++ b/sig/schemable/attribute_schema_generator.rbs @@ -8,7 +8,6 @@ module Schemable def initialize: (Definition, Configuration) -> void def generate_attributes_schema: -> (Hash[Symbol, any] | Array[any]) - def generate_attribute_schema: (Symbol) -> Hash[Symbol, any] end end diff --git a/sig/schemable/configuration.rbs b/sig/schemable/configuration.rbs index 9e776ff..538d8cb 100644 --- a/sig/schemable/configuration.rbs +++ b/sig/schemable/configuration.rbs @@ -1,6 +1,5 @@ module Schemable class Configuration - attr_accessor orm: Symbol attr_accessor timestamps: bool attr_accessor float_as_string: bool @@ -14,9 +13,7 @@ module Schemable def initialize: -> void - - def add_custom_type_mapper: (Symbol, Hash[Symbol, any]) -> void - def type_mapper: (Symbol) -> Hash[Symbol, any] + def add_custom_type_mapper: (Symbol, Hash[Symbol, any]) -> void end end From b480c823611365fc3c4ffda769090d15a4fd5340 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Fri, 10 Nov 2023 13:00:53 +0300 Subject: [PATCH 26/87] Sets configurations by default --- lib/schemable/definition.rb | 4 ++-- sig/schemable/definition.rbs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/schemable/definition.rb b/lib/schemable/definition.rb index 7a202f7..e90a9f6 100644 --- a/lib/schemable/definition.rb +++ b/lib/schemable/definition.rb @@ -2,8 +2,8 @@ module Schemable class Definition attr_reader :configuration - def initialize(configuration) - @configuration = configuration + def initialize + @configuration = Schemable.configuration end def serializer diff --git a/sig/schemable/definition.rbs b/sig/schemable/definition.rbs index cb29332..bc06f3c 100644 --- a/sig/schemable/definition.rbs +++ b/sig/schemable/definition.rbs @@ -3,7 +3,7 @@ module Schemable attr_accessor configuration: Configuration # Initializes the definition with the configuration. - def initialize: (Configuration) -> void + def initialize: -> void # 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. # They are not required to be present in the request body. From 0118d57cb7651cc0cf721ab6e870d30aa0e46319 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Fri, 10 Nov 2023 19:50:26 +0300 Subject: [PATCH 27/87] Sets configuration by default --- lib/schemable/attribute_schema_generator.rb | 6 +++--- sig/schemable/attribute_schema_generator.rbs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/schemable/attribute_schema_generator.rb b/lib/schemable/attribute_schema_generator.rb index 57f3589..cdc4a5a 100644 --- a/lib/schemable/attribute_schema_generator.rb +++ b/lib/schemable/attribute_schema_generator.rb @@ -2,10 +2,10 @@ module Schemable class AttributeSchemaGenerator attr_accessor :model_definition, :configuration, :model, :schema_modifier, :response - def initialize(model_definition, configuration) + def initialize(model_definition) @model_definition = model_definition @model = model_definition.model - @configuration = configuration + @configuration = Schemable.configuration @schema_modifier = SchemaModifier.new @response = nil end @@ -14,7 +14,7 @@ def initialize(model_definition, configuration) def generate_attributes_schema schema = { type: :object, - properties: @model_definition.attributes&.index_with do |attr| + properties: @model_definition.attributes.index_with do |attr| generate_attribute_schema(attr) end } diff --git a/sig/schemable/attribute_schema_generator.rbs b/sig/schemable/attribute_schema_generator.rbs index 4a05c64..e3013e0 100644 --- a/sig/schemable/attribute_schema_generator.rbs +++ b/sig/schemable/attribute_schema_generator.rbs @@ -6,7 +6,7 @@ module Schemable attr_accessor response: Hash[Symbol, any]? attr_accessor schema_modifier: SchemaModifier - def initialize: (Definition, Configuration) -> void + def initialize: (Definition) -> void def generate_attributes_schema: -> (Hash[Symbol, any] | Array[any]) def generate_attribute_schema: (Symbol) -> Hash[Symbol, any] end From 0da8634c7610ed1ebfca28fa11d23637cd6dd5e1 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Fri, 10 Nov 2023 23:41:21 +0300 Subject: [PATCH 28/87] Adds Relationship Generator Class --- .../relationship_schema_generator.rb | 66 +++++++++++++++++++ .../relationship_schema_generator.rbs | 14 ++++ 2 files changed, 80 insertions(+) create mode 100644 lib/schemable/relationship_schema_generator.rb create mode 100644 sig/schemable/relationship_schema_generator.rbs diff --git a/lib/schemable/relationship_schema_generator.rb b/lib/schemable/relationship_schema_generator.rb new file mode 100644 index 0000000..f8b7e6e --- /dev/null +++ b/lib/schemable/relationship_schema_generator.rb @@ -0,0 +1,66 @@ +module Schemable + class RelationshipSchemaGenerator + attr_accessor :model_definition, :schema_modifier, :configuration, :relationships, :expand, :relationships_to_exclude_from_expansion + + def initialize(model_definition, relationships_to_exclude_from_expansion: [], expand: false) + @expand = expand + @model_definition = model_definition + @schema_modifier = SchemaModifier.new + @configuration = Schemable.configuration + @relationships = model_definition.relationships + @relationships_to_exclude_from_expansion = relationships_to_exclude_from_expansion + end + + def generate + return {} if @relationships.blank? || @relationships == { belongs_to: {}, has_many: {} } + + schema = { + type: :object, + properties: {} + } + + %i[belongs_to has_many].each do |relation_type| + @relationships[relation_type]&.each do |relation, definition| + non_expanded_data_properties = { + type: :object, + properties: { + meta: { + type: :object, + properties: { + included: { type: :boolean, default: false } + } + } + } + } + + result = { + type: :object, + properties: { + data: { + type: :object, + properties: { + id: { type: :string }, + type: { type: :string, default: definition[:definition].model_name } + } + } + } + } + + result = non_expanded_data_properties if !expand || @relationships_to_exclude_from_expansion.include?(definition[:definition].model_name) + + schema[:properties].merge!(relation => result) + end + end + + # Modify the schema to include additional response relations + schema = @schema_modifier.add_properties(schema, @model_definition.additional_response_relations, 'properties') + + # Modify the schema to exclude response relations + @model_definition.excluded_response_relations.each do |key| + schema = @schema_modifier.delete_properties(schema, "properties.#{key}") + end + + schema + end + end +end diff --git a/sig/schemable/relationship_schema_generator.rbs b/sig/schemable/relationship_schema_generator.rbs new file mode 100644 index 0000000..9c1d7e5 --- /dev/null +++ b/sig/schemable/relationship_schema_generator.rbs @@ -0,0 +1,14 @@ +module Schemable + class RelationshipSchemaGenerator + attr_accessor expand: bool + attr_accessor model_definition: Definition + attr_accessor configuration: Configuration + attr_accessor schema_modifier: SchemaModifier + attr_accessor relationships: Hash[Symbol, any] + attr_accessor relationships_to_exclude_from_expansion: Array[String] + + def initialize: (Definition, ?relationships_to_exclude_from_expansion: Array[String], ?expand: bool) -> void + + def generate: -> (Hash[Symbol, any] | Array[Hash[Symbol, any]]) + end +end From b03fcc60cace0697065a2b98b0a4a19ec5adde80 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Fri, 10 Nov 2023 23:44:11 +0300 Subject: [PATCH 29/87] Uses model_name as an instance method --- lib/schemable/definition.rb | 4 ++-- sig/schemable/definition.rbs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/schemable/definition.rb b/lib/schemable/definition.rb index e90a9f6..ef601ee 100644 --- a/lib/schemable/definition.rb +++ b/lib/schemable/definition.rb @@ -92,8 +92,8 @@ def model self.class.name.gsub('Swagger::Definitions::', '').constantize end - def self.model_name - name.gsub('Swagger::Definitions::', '').pluralize.underscore.downcase + def model_name + self.class.name.gsub('Swagger::Definitions::', '').pluralize.underscore.downcase end end end diff --git a/sig/schemable/definition.rbs b/sig/schemable/definition.rbs index bc06f3c..54a0002 100644 --- a/sig/schemable/definition.rbs +++ b/sig/schemable/definition.rbs @@ -246,6 +246,6 @@ module Schemable # @example # 'users' for the User model # 'citizen_applications' for the CitizenApplication model - def self.model_name: -> String + def model_name: -> String end end From 21d601ee9f74aa2fe731aa302125b18323549302 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Fri, 10 Nov 2023 23:45:11 +0300 Subject: [PATCH 30/87] No need to pass in configuration instance on definition descendant instantiation --- lib/schemable.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/schemable.rb b/lib/schemable.rb index 032b2d8..034d2a7 100644 --- a/lib/schemable.rb +++ b/lib/schemable.rb @@ -5,6 +5,8 @@ require_relative 'schemable/configuration' require_relative 'schemable/schema_modifier' require_relative 'schemable/attribute_schema_generator' +require_relative 'schemable/response_schema_generator' +require_relative 'schemable/relationship_schema_generator' module Schemable class Error < StandardError; end @@ -22,8 +24,8 @@ def generate_schemas generated_schemas = [] klasses.each do |klass| - model_definition = klass.new(configuration) - schema = AttributeSchemaGenerator.new(model_definition, configuration).generate_attributes_schema + model_definition = klass.new + schema = AttributeSchemaGenerator.new(model_definition).generate_attributes_schema generated_schemas << schema end From e046194f375c517fd7e8a22c5cddcfcd509926a7 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Sun, 12 Nov 2023 10:07:05 +0300 Subject: [PATCH 31/87] Adds included schema generator --- lib/schemable/included_schema_generator.rb | 72 +++++++++++++++++++++ sig/schemable/included_schema_generator.rbs | 16 +++++ 2 files changed, 88 insertions(+) create mode 100644 lib/schemable/included_schema_generator.rb create mode 100644 sig/schemable/included_schema_generator.rbs diff --git a/lib/schemable/included_schema_generator.rb b/lib/schemable/included_schema_generator.rb new file mode 100644 index 0000000..8bc6997 --- /dev/null +++ b/lib/schemable/included_schema_generator.rb @@ -0,0 +1,72 @@ +module Schemable + class IncludedSchemaGenerator + attr_accessor :model_definition, :schema_modifier, :configuration, :relationships, :expand, :relationships_to_exclude_from_expansion + + def initialize(model_definition, relationships_to_exclude_from_expansion: [], expand: false) + @expand = expand + @model_definition = model_definition + @schema_modifier = SchemaModifier.new + @configuration = Schemable.configuration + @relationships = @model_definition.relationships + @relationships_to_exclude_from_expansion = relationships_to_exclude_from_expansion + end + + def prepare_schema_for_included(model_definition, expand: false, relationships_to_exclude_from_expansion: []) + attributes_schema = AttributeSchemaGenerator.new(model_definition).generate_attributes_schema + relationships_schema = RelationshipSchemaGenerator.new(model_definition, relationships_to_exclude_from_expansion:, expand:) + + { + type: :object, + properties: { + type: { type: :string, default: model_definition.model_name }, + id: { type: :string }, + attributes: attributes_schema, + relationships: relationships_schema ? {} : relationships_schema + } + }.compact_blank + end + + def generate(expand: false, relationships_to_exclude_from_expansion: []) + return {} if @relationships.blank? + return {} if @relationships == { belongs_to: {}, has_many: {} } + + definitions = [] + + %i[belongs_to has_many addition_to_included].each do |relation_type| + next if @relationships[relation_type].blank? + + definitions << @relationships[relation_type].values + end + + definitions.flatten! + definition_names = definitions.map(&:model_name) + + included_schemas = definitions.map do |definition| + if expand && relationships_to_exclude_from_expansion.exclude?(definition.model_name) + definition_relations = definition.relationships[:belongs_to].keys.map(&:to_s) + definition.relationships[:has_many].keys.map(&:to_s) + relations_to_exclude = [] + definition_relations.each do |relation| + relations_to_exclude << relation if definition_names.exclude?(relation) + end + + prepare_schema_for_included(definition, expand:, relationships_to_exclude_from_expansion: relations_to_exclude) + else + prepare_schema_for_included(definition) + end + end + + schema = { + included: { + type: :array, + items: { + anyOf: included_schemas + } + } + } + + @schema_modifier.add_properties(schema, @model_definition.additional_response_included, 'included.items') if @model_definition.additional_response_included.present? + + schema + end + end +end diff --git a/sig/schemable/included_schema_generator.rbs b/sig/schemable/included_schema_generator.rbs new file mode 100644 index 0000000..796b6ea --- /dev/null +++ b/sig/schemable/included_schema_generator.rbs @@ -0,0 +1,16 @@ +module Schemable + class IncludedSchemaGenerator + attr_accessor expand: bool + attr_accessor model_definition: Definition + attr_accessor configuration: Configuration + attr_accessor schema_modifier: SchemaModifier + attr_accessor relationships: Hash[Symbol, any] + attr_accessor relationships_to_exclude_from_expansion: Array[String] + + def initialize: (Definition, ?relationships_to_exclude_from_expansion: Array[String], ?expand: bool) -> void + + def prepare_schema_for_included: (Definition, ?relationships_to_exclude_from_expansion: Array[String], ?expand: bool) -> Hash[Symbol, any] + + def generate: (?relationships_to_exclude_from_expansion: Array[String], ?expand: bool) -> (Hash[Symbol, any] | Array[Hash[Symbol, any]]) + end +end From e2ba1a7b8c228ab409f0488760aa05ac9717c89c Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Sun, 12 Nov 2023 10:29:19 +0300 Subject: [PATCH 32/87] Updates RelationshipSchemaGenerator to correctly generate schemas --- .../relationship_schema_generator.rb | 48 ++++++++++++++----- .../relationship_schema_generator.rbs | 2 + 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/lib/schemable/relationship_schema_generator.rb b/lib/schemable/relationship_schema_generator.rb index f8b7e6e..609d75c 100644 --- a/lib/schemable/relationship_schema_generator.rb +++ b/lib/schemable/relationship_schema_generator.rb @@ -33,20 +33,9 @@ def generate } } - result = { - type: :object, - properties: { - data: { - type: :object, - properties: { - id: { type: :string }, - type: { type: :string, default: definition[:definition].model_name } - } - } - } - } + result = relation_type == :belongs_to ? generate_schema(definition.model_name) : generate_schema(definition.model_name, collection: true) - result = non_expanded_data_properties if !expand || @relationships_to_exclude_from_expansion.include?(definition[:definition].model_name) + result = non_expanded_data_properties if !expand || @relationships_to_exclude_from_expansion.include?(definition.model_name) schema[:properties].merge!(relation => result) end @@ -62,5 +51,38 @@ def generate schema end + + def generate_schema(type_name, collection: false) + if collection + { + type: :object, + properties: { + data: { + type: :array, + items: { + type: :object, + properties: { + id: { type: :string }, + type: { type: :string, default: type_name } + } + } + } + } + } + else + { + type: :object, + properties: { + data: { + type: :object, + properties: { + id: { type: :string }, + type: { type: :string, default: type_name } + } + } + } + } + end + end end end diff --git a/sig/schemable/relationship_schema_generator.rbs b/sig/schemable/relationship_schema_generator.rbs index 9c1d7e5..a1c50e0 100644 --- a/sig/schemable/relationship_schema_generator.rbs +++ b/sig/schemable/relationship_schema_generator.rbs @@ -10,5 +10,7 @@ module Schemable def initialize: (Definition, ?relationships_to_exclude_from_expansion: Array[String], ?expand: bool) -> void def generate: -> (Hash[Symbol, any] | Array[Hash[Symbol, any]]) + + def generate_schema: (String, ?collection: bool) -> Hash[Symbol, any] end end From 861a740f61084c7d8ad3164cad44ed298db72dd8 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Sun, 12 Nov 2023 10:32:28 +0300 Subject: [PATCH 33/87] Renames method --- lib/schemable.rb | 2 +- lib/schemable/attribute_schema_generator.rb | 2 +- lib/schemable/included_schema_generator.rb | 30 ++++++++++---------- sig/schemable/attribute_schema_generator.rbs | 2 +- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/schemable.rb b/lib/schemable.rb index 034d2a7..5792227 100644 --- a/lib/schemable.rb +++ b/lib/schemable.rb @@ -25,7 +25,7 @@ def generate_schemas klasses.each do |klass| model_definition = klass.new - schema = AttributeSchemaGenerator.new(model_definition).generate_attributes_schema + schema = AttributeSchemaGenerator.new(model_definition).generate generated_schemas << schema end diff --git a/lib/schemable/attribute_schema_generator.rb b/lib/schemable/attribute_schema_generator.rb index cdc4a5a..c5803f2 100644 --- a/lib/schemable/attribute_schema_generator.rb +++ b/lib/schemable/attribute_schema_generator.rb @@ -11,7 +11,7 @@ def initialize(model_definition) end # Generate the JSON schema for attributes - def generate_attributes_schema + def generate schema = { type: :object, properties: @model_definition.attributes.index_with do |attr| diff --git a/lib/schemable/included_schema_generator.rb b/lib/schemable/included_schema_generator.rb index 8bc6997..4786c01 100644 --- a/lib/schemable/included_schema_generator.rb +++ b/lib/schemable/included_schema_generator.rb @@ -11,21 +11,6 @@ def initialize(model_definition, relationships_to_exclude_from_expansion: [], ex @relationships_to_exclude_from_expansion = relationships_to_exclude_from_expansion end - def prepare_schema_for_included(model_definition, expand: false, relationships_to_exclude_from_expansion: []) - attributes_schema = AttributeSchemaGenerator.new(model_definition).generate_attributes_schema - relationships_schema = RelationshipSchemaGenerator.new(model_definition, relationships_to_exclude_from_expansion:, expand:) - - { - type: :object, - properties: { - type: { type: :string, default: model_definition.model_name }, - id: { type: :string }, - attributes: attributes_schema, - relationships: relationships_schema ? {} : relationships_schema - } - }.compact_blank - end - def generate(expand: false, relationships_to_exclude_from_expansion: []) return {} if @relationships.blank? return {} if @relationships == { belongs_to: {}, has_many: {} } @@ -68,5 +53,20 @@ def generate(expand: false, relationships_to_exclude_from_expansion: []) schema end + + def prepare_schema_for_included(model_definition, expand: false, relationships_to_exclude_from_expansion: []) + attributes_schema = AttributeSchemaGenerator.new(model_definition).generate + relationships_schema = RelationshipSchemaGenerator.new(model_definition, relationships_to_exclude_from_expansion:, expand:) + + { + type: :object, + properties: { + type: { type: :string, default: model_definition.model_name }, + id: { type: :string }, + attributes: attributes_schema, + relationships: relationships_schema ? {} : relationships_schema + } + }.compact_blank + end end end diff --git a/sig/schemable/attribute_schema_generator.rbs b/sig/schemable/attribute_schema_generator.rbs index e3013e0..b321b5e 100644 --- a/sig/schemable/attribute_schema_generator.rbs +++ b/sig/schemable/attribute_schema_generator.rbs @@ -7,7 +7,7 @@ module Schemable attr_accessor schema_modifier: SchemaModifier def initialize: (Definition) -> void - def generate_attributes_schema: -> (Hash[Symbol, any] | Array[any]) + def generate: -> (Hash[Symbol, any] | Array[any]) def generate_attribute_schema: (Symbol) -> Hash[Symbol, any] end end From 15fb52eef1877710fa2d5827413ee1add71a450b Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Sun, 12 Nov 2023 11:00:40 +0300 Subject: [PATCH 34/87] Passes parameters to the function on invoke rather than on class initialization --- lib/schemable/included_schema_generator.rb | 9 +++------ lib/schemable/relationship_schema_generator.rb | 11 ++++------- sig/schemable/included_schema_generator.rbs | 2 +- sig/schemable/relationship_schema_generator.rbs | 7 ++----- 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/lib/schemable/included_schema_generator.rb b/lib/schemable/included_schema_generator.rb index 4786c01..7c3e02a 100644 --- a/lib/schemable/included_schema_generator.rb +++ b/lib/schemable/included_schema_generator.rb @@ -1,14 +1,11 @@ module Schemable class IncludedSchemaGenerator - attr_accessor :model_definition, :schema_modifier, :configuration, :relationships, :expand, :relationships_to_exclude_from_expansion + attr_accessor :model_definition, :schema_modifier, :relationships - def initialize(model_definition, relationships_to_exclude_from_expansion: [], expand: false) - @expand = expand + def initialize(model_definition) @model_definition = model_definition @schema_modifier = SchemaModifier.new - @configuration = Schemable.configuration @relationships = @model_definition.relationships - @relationships_to_exclude_from_expansion = relationships_to_exclude_from_expansion end def generate(expand: false, relationships_to_exclude_from_expansion: []) @@ -56,7 +53,7 @@ def generate(expand: false, relationships_to_exclude_from_expansion: []) def prepare_schema_for_included(model_definition, expand: false, relationships_to_exclude_from_expansion: []) attributes_schema = AttributeSchemaGenerator.new(model_definition).generate - relationships_schema = RelationshipSchemaGenerator.new(model_definition, relationships_to_exclude_from_expansion:, expand:) + relationships_schema = RelationshipSchemaGenerator.new(model_definition).generate(relationships_to_exclude_from_expansion:, expand:) { type: :object, diff --git a/lib/schemable/relationship_schema_generator.rb b/lib/schemable/relationship_schema_generator.rb index 609d75c..383e5af 100644 --- a/lib/schemable/relationship_schema_generator.rb +++ b/lib/schemable/relationship_schema_generator.rb @@ -1,17 +1,14 @@ module Schemable class RelationshipSchemaGenerator - attr_accessor :model_definition, :schema_modifier, :configuration, :relationships, :expand, :relationships_to_exclude_from_expansion + attr_accessor :model_definition, :schema_modifier, :relationships - def initialize(model_definition, relationships_to_exclude_from_expansion: [], expand: false) - @expand = expand + def initialize(model_definition) @model_definition = model_definition @schema_modifier = SchemaModifier.new - @configuration = Schemable.configuration @relationships = model_definition.relationships - @relationships_to_exclude_from_expansion = relationships_to_exclude_from_expansion end - def generate + def generate(relationships_to_exclude_from_expansion: [], expand: false) return {} if @relationships.blank? || @relationships == { belongs_to: {}, has_many: {} } schema = { @@ -35,7 +32,7 @@ def generate result = relation_type == :belongs_to ? generate_schema(definition.model_name) : generate_schema(definition.model_name, collection: true) - result = non_expanded_data_properties if !expand || @relationships_to_exclude_from_expansion.include?(definition.model_name) + result = non_expanded_data_properties if !expand || relationships_to_exclude_from_expansion.include?(definition.model_name) schema[:properties].merge!(relation => result) end diff --git a/sig/schemable/included_schema_generator.rbs b/sig/schemable/included_schema_generator.rbs index 796b6ea..a090746 100644 --- a/sig/schemable/included_schema_generator.rbs +++ b/sig/schemable/included_schema_generator.rbs @@ -7,7 +7,7 @@ module Schemable attr_accessor relationships: Hash[Symbol, any] attr_accessor relationships_to_exclude_from_expansion: Array[String] - def initialize: (Definition, ?relationships_to_exclude_from_expansion: Array[String], ?expand: bool) -> void + def initialize: (Definition) -> void def prepare_schema_for_included: (Definition, ?relationships_to_exclude_from_expansion: Array[String], ?expand: bool) -> Hash[Symbol, any] diff --git a/sig/schemable/relationship_schema_generator.rbs b/sig/schemable/relationship_schema_generator.rbs index a1c50e0..2e6a874 100644 --- a/sig/schemable/relationship_schema_generator.rbs +++ b/sig/schemable/relationship_schema_generator.rbs @@ -1,15 +1,12 @@ module Schemable class RelationshipSchemaGenerator - attr_accessor expand: bool attr_accessor model_definition: Definition - attr_accessor configuration: Configuration attr_accessor schema_modifier: SchemaModifier attr_accessor relationships: Hash[Symbol, any] - attr_accessor relationships_to_exclude_from_expansion: Array[String] - def initialize: (Definition, ?relationships_to_exclude_from_expansion: Array[String], ?expand: bool) -> void + def initialize: (Definition) -> void - def generate: -> (Hash[Symbol, any] | Array[Hash[Symbol, any]]) + def generate: (?relationships_to_exclude_from_expansion: Array[String], ?expand: bool) -> (Hash[Symbol, any] | Array[Hash[Symbol, any]]) def generate_schema: (String, ?collection: bool) -> Hash[Symbol, any] end From 2d7dd77f6e0cdb7ec18a6dc4a3c9a6f7ccda89c6 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Sun, 12 Nov 2023 11:01:51 +0300 Subject: [PATCH 35/87] Adds ResponseSchemaGenerator Class --- lib/schemable/response_schema_generator.rb | 81 +++++++++++++++++++++ sig/schemable/response_schema_generator.rbs | 12 +++ 2 files changed, 93 insertions(+) create mode 100644 lib/schemable/response_schema_generator.rb create mode 100644 sig/schemable/response_schema_generator.rbs diff --git a/lib/schemable/response_schema_generator.rb b/lib/schemable/response_schema_generator.rb new file mode 100644 index 0000000..312206f --- /dev/null +++ b/lib/schemable/response_schema_generator.rb @@ -0,0 +1,81 @@ +module Schemable + class ResponseSchemaGenerator + attr_accessor :model_definition, :model + + def initialize(model_definition) + @model_definition = model_definition + @model = model_definition.model + @schema_modifier = SchemaModifier.new + end + + def generate(expand: false, relationships_to_exclude_from_expansion: [], collection: false) + data = { + type: :object, + properties: { + type: { type: :string, default: @model_definition.model_name }, + id: { type: :string }, + attributes: AttributeSchemaGenerator.new(@model_definition).generate + }.merge( + if @model_definition.relationships.blank? || @model_definition.relationships == { belongs_to: {}, has_many: {} } + {} + else + { relationships: RelationshipSchemaGenerator.new(@model_definition).generate(expand:, relationships_to_exclude_from_expansion:) } + end + ) + } + + schema = collection ? { data: { type: :array, items: data } } : { data: } + + if expand + included_schema = IncludedSchemaGenerator.new(@model_definition).generate(expand:, relationships_to_exclude_from_expansion:) + @schema_modifier.add_properties(schema, included_schema, '.') + end + + @schema_modifier.add_properties(schema, meta, '.') if collection + @schema_modifier.add_properties(schema, jsonapi, '.') + + { type: :object, properties: schema } + end + + def meta + { + type: :object, + properties: { + page: { + type: :object, + properties: { + totalPages: { + type: :integer, + default: 1 + }, + count: { + type: :integer, + default: 1 + }, + rowsPerPage: { + type: :integer, + default: 1 + }, + currentPage: { + type: :integer, + default: 1 + } + } + } + } + } + end + + def jsonapi + { + type: :object, + properties: { + version: { + type: :string, + default: '1.0' + } + } + } + end + end +end diff --git a/sig/schemable/response_schema_generator.rbs b/sig/schemable/response_schema_generator.rbs new file mode 100644 index 0000000..f09bee5 --- /dev/null +++ b/sig/schemable/response_schema_generator.rbs @@ -0,0 +1,12 @@ +module Schemable + class ResponseSchemaGenerator + attr_accessor model: Class + attr_accessor model_definition: Definition + attr_accessor schema_modifier: SchemaModifier + + def initialize: (Definition) -> void + def meta: -> Hash[Symbol, any] + def jsonapi: -> Hash[Symbol, any] + def generate: (expand: bool, relationships_to_exclude_from_expansion: Array[Symbol], collection: bool) -> Hash[Symbol, any] + end +end From 215c101c98c76090b86a639e366f094f25c9cbac Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Sun, 12 Nov 2023 11:28:53 +0300 Subject: [PATCH 36/87] Properly adds meta and jsonapi --- lib/schemable/response_schema_generator.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/schemable/response_schema_generator.rb b/lib/schemable/response_schema_generator.rb index 312206f..b29c5c0 100644 --- a/lib/schemable/response_schema_generator.rb +++ b/lib/schemable/response_schema_generator.rb @@ -31,10 +31,10 @@ def generate(expand: false, relationships_to_exclude_from_expansion: [], collect @schema_modifier.add_properties(schema, included_schema, '.') end - @schema_modifier.add_properties(schema, meta, '.') if collection - @schema_modifier.add_properties(schema, jsonapi, '.') + @schema_modifier.add_properties(schema, { meta: }, '.') if collection + @schema_modifier.add_properties(schema, { jsonapi: }, '.') - { type: :object, properties: schema } + { type: :object, properties: schema }.compact_blank end def meta From 159dfc5bc92796ca8071e312a0832194bd7033cb Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Sun, 12 Nov 2023 11:29:14 +0300 Subject: [PATCH 37/87] Properly adds relationships --- lib/schemable/included_schema_generator.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/schemable/included_schema_generator.rb b/lib/schemable/included_schema_generator.rb index 7c3e02a..1f2ee4c 100644 --- a/lib/schemable/included_schema_generator.rb +++ b/lib/schemable/included_schema_generator.rb @@ -60,9 +60,8 @@ def prepare_schema_for_included(model_definition, expand: false, relationships_t properties: { type: { type: :string, default: model_definition.model_name }, id: { type: :string }, - attributes: attributes_schema, - relationships: relationships_schema ? {} : relationships_schema - } + attributes: attributes_schema + }.merge!(relationships_schema.blank? ? {} : { relationships: relationships_schema }) }.compact_blank end end From 5d80d669eb89d0dc3867de6c328ffc8cc8319a63 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Sun, 12 Nov 2023 11:30:01 +0300 Subject: [PATCH 38/87] Includes IncludedSchemaGenerator in Schemable --- lib/schemable.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/schemable.rb b/lib/schemable.rb index 5792227..1fa7fbd 100644 --- a/lib/schemable.rb +++ b/lib/schemable.rb @@ -7,6 +7,7 @@ require_relative 'schemable/attribute_schema_generator' require_relative 'schemable/response_schema_generator' require_relative 'schemable/relationship_schema_generator' +require_relative 'schemable/included_schema_generator' module Schemable class Error < StandardError; end From ac70f1e7dd2cb0cc06aaa0a18faf006fb1fdfaec Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Sun, 12 Nov 2023 11:45:13 +0300 Subject: [PATCH 39/87] Adds RequestSchemaGenerator class --- lib/schemable.rb | 1 + lib/schemable/request_schema_generator.rb | 60 ++++++++++++++++++++++ sig/schemable/request_schema_generator.rbs | 10 ++++ 3 files changed, 71 insertions(+) create mode 100644 lib/schemable/request_schema_generator.rb create mode 100644 sig/schemable/request_schema_generator.rbs diff --git a/lib/schemable.rb b/lib/schemable.rb index 1fa7fbd..e2c1285 100644 --- a/lib/schemable.rb +++ b/lib/schemable.rb @@ -8,6 +8,7 @@ require_relative 'schemable/response_schema_generator' require_relative 'schemable/relationship_schema_generator' require_relative 'schemable/included_schema_generator' +require_relative 'schemable/request_schema_generator' module Schemable class Error < StandardError; end diff --git a/lib/schemable/request_schema_generator.rb b/lib/schemable/request_schema_generator.rb new file mode 100644 index 0000000..5847530 --- /dev/null +++ b/lib/schemable/request_schema_generator.rb @@ -0,0 +1,60 @@ +module Schemable + class RequestSchemaGenerator + attr_accessor :model_definition, :schema_modifier + + def initialize(model_definition) + @model_definition = model_definition + @schema_modifier = SchemaModifier.new + end + + def generate_for_create + schema = { + type: :object, + properties: { + data: AttributeSchemaGenerator.new(@model_definition).generate + } + } + + @schema_modifier.add_properties(schema, @model_definition.additional_create_request_attributes, 'properties.data.properties') + + @model_definition.excluded_create_request_attributes.each do |key| + @schema_modifier.delete_properties(schema, "properties.data.properties.#{key}") + end + + required_attributes = { + required: ( + schema.as_json['properties']['data']['properties'].keys - + @model_definition.optional_create_request_attributes.map(&:to_s) - + @model_definition.nullable_attributes.map(&:to_s) + ).map { |key| key.to_s.camelize(:lower).to_sym } + } + + @schema_modifier.add_properties(schema, required_attributes, 'properties.data') + end + + def generate_for_update + schema = { + type: :object, + properties: { + data: AttributeSchemaGenerator.new(@model_definition).generate + } + } + + @schema_modifier.add_properties(schema, @model_definition.additional_update_request_attributes, 'properties.data.properties') + + @model_definition.excluded_update_request_attributes.each do |key| + @schema_modifier.delete_properties(schema, "properties.data.properties.#{key}") + end + + required_attributes = { + required: ( + schema.as_json['properties']['data']['properties'].keys - + @model_definition.optional_update_request_attributes.map(&:to_s) - + @model_definition.nullable_attributes.map(&:to_s) + ).map { |key| key.to_s.camelize(:lower).to_sym } + } + + @schema_modifier.add_properties(schema, required_attributes, 'properties.data') + end + end +end diff --git a/sig/schemable/request_schema_generator.rbs b/sig/schemable/request_schema_generator.rbs new file mode 100644 index 0000000..29f3996 --- /dev/null +++ b/sig/schemable/request_schema_generator.rbs @@ -0,0 +1,10 @@ +module Schemable + class RequestSchemaGenerator + attr_accessor model_definition: Definition + attr_accessor schema_modifier: SchemaModifier + + def initialize: (Definition) -> void + def generate_for_create: () -> (Hash[Symbol, any] | Array[Hash[Symbol, any]]) + def generate_for_update: () -> (Hash[Symbol, any] | Array[Hash[Symbol, any]]) + end +end From 92d9f33d203c0daea4fe03349b58ac6af43c67ee Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Sun, 12 Nov 2023 14:47:47 +0300 Subject: [PATCH 40/87] Changes Exclude Logic --- lib/schemable/included_schema_generator.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/schemable/included_schema_generator.rb b/lib/schemable/included_schema_generator.rb index 1f2ee4c..6826e24 100644 --- a/lib/schemable/included_schema_generator.rb +++ b/lib/schemable/included_schema_generator.rb @@ -21,14 +21,15 @@ def generate(expand: false, relationships_to_exclude_from_expansion: []) end definitions.flatten! - definition_names = definitions.map(&:model_name) included_schemas = definitions.map do |definition| - if expand && relationships_to_exclude_from_expansion.exclude?(definition.model_name) - definition_relations = definition.relationships[:belongs_to].keys.map(&:to_s) + definition.relationships[:has_many].keys.map(&:to_s) + next if relationships_to_exclude_from_expansion.include?(definition.model_name) + + if expand + definition_relations = definition.relationships[:belongs_to].values.map(&:model_name) + definition.relationships[:has_many].values.map(&:model_name) relations_to_exclude = [] definition_relations.each do |relation| - relations_to_exclude << relation if definition_names.exclude?(relation) + relations_to_exclude << relation if relationships_to_exclude_from_expansion.include?(relation) end prepare_schema_for_included(definition, expand:, relationships_to_exclude_from_expansion: relations_to_exclude) @@ -41,7 +42,7 @@ def generate(expand: false, relationships_to_exclude_from_expansion: []) included: { type: :array, items: { - anyOf: included_schemas + anyOf: included_schemas.compact_blank } } } From e25dfff4ec4febd68e41b2288e73299777e66a25 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Sun, 12 Nov 2023 14:48:46 +0300 Subject: [PATCH 41/87] Adds Camelize Keys Method to Definitions --- lib/schemable/definition.rb | 4 ++++ sig/schemable/definition.rbs | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/lib/schemable/definition.rb b/lib/schemable/definition.rb index ef601ee..8d69f86 100644 --- a/lib/schemable/definition.rb +++ b/lib/schemable/definition.rb @@ -95,5 +95,9 @@ def model def model_name self.class.name.gsub('Swagger::Definitions::', '').pluralize.underscore.downcase end + + def camelize_keys(hash) + hash.deep_transform_keys { |key| key.to_s.camelize(:lower).to_sym } + end end end diff --git a/sig/schemable/definition.rbs b/sig/schemable/definition.rbs index 54a0002..0780946 100644 --- a/sig/schemable/definition.rbs +++ b/sig/schemable/definition.rbs @@ -247,5 +247,15 @@ module Schemable # 'users' for the User model # 'citizen_applications' for the CitizenApplication model def model_name: -> String + + # Given a hash, it returns a new hash with all the keys camelized. + # + # @param hash [Array | Hash] The hash with all the keys camelized. + # + # @return [Array | Hash] The hash with all the keys camelized. + # + # @example + # { first_name: 'John', last_name: 'Doe' } => { firstName: 'John', lastName: 'Doe' } + def camelize_keys: (Hash[Symbol, any]) -> (Hash[Symbol, any] | Array[Hash[Symbol, any]]) end end From 06aafcf8831e9615e20d83e6e214127010b38dce Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Mon, 13 Nov 2023 14:57:32 +0300 Subject: [PATCH 42/87] Speeds up the definition's retrieval --- lib/schemable/definition.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/schemable/definition.rb b/lib/schemable/definition.rb index 8d69f86..aca4bbb 100644 --- a/lib/schemable/definition.rb +++ b/lib/schemable/definition.rb @@ -1,6 +1,7 @@ module Schemable class Definition attr_reader :configuration + attr_writer :relationships, :additional_create_request_attributes, :additional_update_request_attributes def initialize @configuration = Schemable.configuration @@ -17,7 +18,7 @@ def attributes return model.send(configuration.infer_attributes_from_custom_method).map(&:to_sym) if configuration.infer_attributes_from_custom_method - model.attribute_names + model.attribute_names.map(&:to_sym) end def relationships From 2b57b1ed3c10ed09e9523377faa52e1644cebf4c Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Mon, 13 Nov 2023 14:58:20 +0300 Subject: [PATCH 43/87] Adds mongoid specific configurations --- lib/schemable/configuration.rb | 14 ++++++++++++-- sig/schemable/configuration.rbs | 2 ++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/schemable/configuration.rb b/lib/schemable/configuration.rb index 3c2dbbe..0808a91 100644 --- a/lib/schemable/configuration.rb +++ b/lib/schemable/configuration.rb @@ -9,8 +9,10 @@ class Configuration :disable_factory_bot, :use_serialized_instance, :custom_defined_enum_method, + :enum_prefix_for_simple_enum, + :enum_suffix_for_simple_enum, :infer_attributes_from_custom_method, - :infer_attributes_from_jsonapi_serializable + :infer_attributes_from_jsonapi_serializable, ) def initialize @@ -22,6 +24,8 @@ def initialize @disable_factory_bot = true @use_serialized_instance = false @custom_defined_enum_method = nil + @enum_prefix_for_simple_enum = nil + @enum_suffix_for_simple_enum = nil @infer_attributes_from_custom_method = nil @infer_attributes_from_jsonapi_serializable = false end @@ -32,6 +36,7 @@ def type_mapper(type_name) { text: { type: :string }, string: { type: :string }, + symbol: { type: :string }, integer: { type: :integer }, boolean: { type: :boolean }, date: { type: :string, format: :date }, @@ -44,6 +49,11 @@ def type_mapper(type_name) trueclass: { type: :boolean, default: true }, falseclass: { type: :boolean, default: false }, datetime: { type: :string, format: :'date-time' }, + big_decimal: { type: (@decimal_as_string ? :string : :number).to_s.to_sym, format: :double }, + 'bson/objectid': { type: :string, format: :object_id }, + 'mongoid/boolean': { type: :boolean }, + 'mongoid/stringified_symbol': { type: :string }, + 'active_support/time_with_zone': { type: :string, format: :date_time }, float: { type: (@float_as_string ? :string : :number).to_s.to_sym, format: :float @@ -65,7 +75,7 @@ def type_mapper(type_name) ] } } - }[type_name.try(:to_sym)] + }[type_name.to_s.underscore.try(:to_sym)] end def add_custom_type_mapper(type_name, mapping) diff --git a/sig/schemable/configuration.rbs b/sig/schemable/configuration.rbs index 538d8cb..6a874ad 100644 --- a/sig/schemable/configuration.rbs +++ b/sig/schemable/configuration.rbs @@ -6,6 +6,8 @@ module Schemable attr_accessor decimal_as_string: bool attr_accessor disable_factory_bot: bool attr_accessor use_serialized_instance: bool + attr_accessor enum_prefix_for_simple_enum: String? + attr_accessor enum_suffix_for_simple_enum: String? attr_accessor custom_defined_enum_method: Symbol? attr_accessor custom_type_mappers: Hash[Symbol, any] attr_accessor infer_attributes_from_jsonapi_serializable: bool From 5a630a73a3a7abb188526e00c82f07a2587c637a Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Mon, 13 Nov 2023 14:59:02 +0300 Subject: [PATCH 44/87] Adds mongoid specific conditions --- lib/schemable/attribute_schema_generator.rb | 22 +++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/schemable/attribute_schema_generator.rb b/lib/schemable/attribute_schema_generator.rb index c5803f2..b0aa60b 100644 --- a/lib/schemable/attribute_schema_generator.rb +++ b/lib/schemable/attribute_schema_generator.rb @@ -19,6 +19,13 @@ def generate end } + # Rename enum attributes to remove the suffix or prefix if mongoid is used + if @configuration.orm == :mongoid + schema[:properties].transform_keys! do |key| + key.to_s.gsub(@configuration.enum_prefix_for_simple_enum || @configuration.enum_suffix_for_simple_enum, '') + end + end + # modify the schema to include additional response relations schema = @schema_modifier.add_properties(schema, @model_definition.additional_response_attributes, 'properties') @@ -43,8 +50,13 @@ def generate_attribute_schema(attribute) # Check if this is an array attribute return @configuration.type_mapper(:array) if attribute_hash.try(:[], 'options').try(:[], 'type') == 'Array' - # Map the column type to a JSON Schema type if none of the above conditions are met - @response = @configuration.type_mapper(attribute_hash.try(:type).to_s.downcase.to_sym) + # Check if this is an enum attribute + @response = if attribute_hash.name.end_with?('_cd') + @configuration.type_mapper(:string) + else + # Map the column type to a JSON Schema type if none of the above conditions are met + @configuration.type_mapper(attribute_hash.try(:type).to_s.downcase.to_sym) + end elsif @configuration.orm == :active_record # Get the column hash for the attribute @@ -68,8 +80,10 @@ def generate_attribute_schema(attribute) return @schema_modifier.add_properties(@response, { nullable: true }, '.') if @response && @model_definition.nullable_attributes.include?(attribute) # If attribute is an enum, modify the schema accordingly - if @configuration.custom_defined_enum_method - return @schema_modifier.add_properties(@response, { enum: @model.send(@configuration.custom_defined_enum_method, attribute.to_s) }, '.') if @response && @model.respond_to?(@configuration.custom_defined_enum_method) + if @configuration.custom_defined_enum_method && @model.respond_to?(@configuration.custom_defined_enum_method) + defined_enums = @model.send(@configuration.custom_defined_enum_method) + enum_attribute = attribute.to_s.gsub(@configuration.enum_prefix_for_simple_enum || @configuration.enum_suffix_for_simple_enum, '').to_s + return @schema_modifier.add_properties(@response, { enum: defined_enums[enum_attribute].keys }, '.') if @response && defined_enums[enum_attribute].present? elsif @model.respond_to?(:defined_enums) return @schema_modifier.add_properties(@response, { enum: @model.defined_enums[attribute.to_s].keys }, '.') if @response && @model.defined_enums.key?(attribute.to_s) end From 1d9e06a1d35c6ee8a19925b8172bd44c4d300769 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Mon, 13 Nov 2023 15:27:45 +0300 Subject: [PATCH 45/87] Adds missing signature --- sig/schemable/definition.rbs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sig/schemable/definition.rbs b/sig/schemable/definition.rbs index 0780946..8d17ded 100644 --- a/sig/schemable/definition.rbs +++ b/sig/schemable/definition.rbs @@ -1,6 +1,9 @@ module Schemable class Definition - attr_accessor configuration: Configuration + attr_reader configuration: Configuration + attr_writer relationships: Hash[Symbol, any] + attr_writer additional_create_request_attributes: Hash[Symbol, any] + attr_writer additional_update_request_attributes: Hash[Symbol, any] # Initializes the definition with the configuration. def initialize: -> void From a9fbe3e17ed3e8fc809c253768e1c415bf89211a Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Mon, 13 Nov 2023 15:30:06 +0300 Subject: [PATCH 46/87] Updates Schemable Module --- lib/schemable.rb | 17 +---------------- sig/schemable.rbs | 5 +++-- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/lib/schemable.rb b/lib/schemable.rb index e2c1285..aa1033f 100644 --- a/lib/schemable.rb +++ b/lib/schemable.rb @@ -1,13 +1,11 @@ -# frozen_string_literal: true - require_relative 'schemable/version' require_relative 'schemable/definition' require_relative 'schemable/configuration' require_relative 'schemable/schema_modifier' require_relative 'schemable/attribute_schema_generator' -require_relative 'schemable/response_schema_generator' require_relative 'schemable/relationship_schema_generator' require_relative 'schemable/included_schema_generator' +require_relative 'schemable/response_schema_generator' require_relative 'schemable/request_schema_generator' module Schemable @@ -20,18 +18,5 @@ def configure @configuration ||= Configuration.new yield(@configuration) if block_given? end - - def generate_schemas - klasses = Schemable::Definition.descendants - generated_schemas = [] - - klasses.each do |klass| - model_definition = klass.new - schema = AttributeSchemaGenerator.new(model_definition).generate - generated_schemas << schema - end - - generated_schemas - end end end diff --git a/sig/schemable.rbs b/sig/schemable.rbs index 46781e6..1267727 100644 --- a/sig/schemable.rbs +++ b/sig/schemable.rbs @@ -1,6 +1,7 @@ module Schemable VERSION: String - def generate_schemas: () -> Array[Hash[Symbol, Hash[Symbol, any]]] - def configure: () { (Configuration) -> Configuration } -> Configuration + attr_accessor configuration: Configuration + + def configure: () { (Configuration) -> Configuration } -> Configuration end From dcfc80d9ad06e692aaaad7d3f737aed2bca651cf Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Mon, 13 Nov 2023 15:34:48 +0300 Subject: [PATCH 47/87] Adds generate method --- lib/schemable/definition.rb | 11 +++++++++++ sig/schemable/definition.rbs | 2 ++ 2 files changed, 13 insertions(+) diff --git a/lib/schemable/definition.rb b/lib/schemable/definition.rb index aca4bbb..509fa14 100644 --- a/lib/schemable/definition.rb +++ b/lib/schemable/definition.rb @@ -100,5 +100,16 @@ def model_name def camelize_keys(hash) hash.deep_transform_keys { |key| key.to_s.camelize(:lower).to_sym } end + + def self.generate + instance = new + + [ + "#{instance.model}CreateRequest": instance.camelize_keys(RequestSchemaGenerator.new(instance).generate_for_create), + "#{instance.model}UpdateRequest": instance.camelize_keys(RequestSchemaGenerator.new(instance).generate_for_update), + "#{instance.model}Response": instance.camelize_keys(ResponseSchemaGenerator.new(instance).generate(collection: true)), + "#{instance.model}ResponseExpanded": instance.camelize_keys(ResponseSchemaGenerator.new(instance).generate(expand: true)) + ] + end end end diff --git a/sig/schemable/definition.rbs b/sig/schemable/definition.rbs index 8d17ded..eb79900 100644 --- a/sig/schemable/definition.rbs +++ b/sig/schemable/definition.rbs @@ -260,5 +260,7 @@ module Schemable # @example # { first_name: 'John', last_name: 'Doe' } => { firstName: 'John', lastName: 'Doe' } def camelize_keys: (Hash[Symbol, any]) -> (Hash[Symbol, any] | Array[Hash[Symbol, any]]) + + def self.generate: -> Array[Hash[Symbol, any]] end end From b8928ee1ea0d9b7ae530d2d6d705cfd9f6e80b24 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Mon, 13 Nov 2023 15:50:28 +0300 Subject: [PATCH 48/87] Uses attribute reader instead of accessor --- lib/schemable/attribute_schema_generator.rb | 2 +- lib/schemable/included_schema_generator.rb | 2 +- lib/schemable/relationship_schema_generator.rb | 2 +- lib/schemable/request_schema_generator.rb | 2 +- lib/schemable/response_schema_generator.rb | 2 +- sig/schemable/attribute_schema_generator.rbs | 10 +++++----- sig/schemable/included_schema_generator.rbs | 9 +++------ sig/schemable/relationship_schema_generator.rbs | 6 +++--- sig/schemable/request_schema_generator.rbs | 4 ++-- sig/schemable/response_schema_generator.rbs | 6 +++--- 10 files changed, 21 insertions(+), 24 deletions(-) diff --git a/lib/schemable/attribute_schema_generator.rb b/lib/schemable/attribute_schema_generator.rb index b0aa60b..bcf5b08 100644 --- a/lib/schemable/attribute_schema_generator.rb +++ b/lib/schemable/attribute_schema_generator.rb @@ -1,6 +1,6 @@ module Schemable class AttributeSchemaGenerator - attr_accessor :model_definition, :configuration, :model, :schema_modifier, :response + attr_reader :model_definition, :configuration, :model, :schema_modifier, :response def initialize(model_definition) @model_definition = model_definition diff --git a/lib/schemable/included_schema_generator.rb b/lib/schemable/included_schema_generator.rb index 6826e24..721a2d0 100644 --- a/lib/schemable/included_schema_generator.rb +++ b/lib/schemable/included_schema_generator.rb @@ -1,6 +1,6 @@ module Schemable class IncludedSchemaGenerator - attr_accessor :model_definition, :schema_modifier, :relationships + attr_reader :model_definition, :schema_modifier, :relationships def initialize(model_definition) @model_definition = model_definition diff --git a/lib/schemable/relationship_schema_generator.rb b/lib/schemable/relationship_schema_generator.rb index 383e5af..a39ffe3 100644 --- a/lib/schemable/relationship_schema_generator.rb +++ b/lib/schemable/relationship_schema_generator.rb @@ -1,6 +1,6 @@ module Schemable class RelationshipSchemaGenerator - attr_accessor :model_definition, :schema_modifier, :relationships + attr_reader :model_definition, :schema_modifier, :relationships def initialize(model_definition) @model_definition = model_definition diff --git a/lib/schemable/request_schema_generator.rb b/lib/schemable/request_schema_generator.rb index 5847530..ceb8cf8 100644 --- a/lib/schemable/request_schema_generator.rb +++ b/lib/schemable/request_schema_generator.rb @@ -1,6 +1,6 @@ module Schemable class RequestSchemaGenerator - attr_accessor :model_definition, :schema_modifier + attr_reader :model_definition, :schema_modifier def initialize(model_definition) @model_definition = model_definition diff --git a/lib/schemable/response_schema_generator.rb b/lib/schemable/response_schema_generator.rb index b29c5c0..c59464d 100644 --- a/lib/schemable/response_schema_generator.rb +++ b/lib/schemable/response_schema_generator.rb @@ -1,6 +1,6 @@ module Schemable class ResponseSchemaGenerator - attr_accessor :model_definition, :model + attr_reader :model_definition, :model, :schema_modifier def initialize(model_definition) @model_definition = model_definition diff --git a/sig/schemable/attribute_schema_generator.rbs b/sig/schemable/attribute_schema_generator.rbs index b321b5e..4b950b1 100644 --- a/sig/schemable/attribute_schema_generator.rbs +++ b/sig/schemable/attribute_schema_generator.rbs @@ -1,10 +1,10 @@ module Schemable class AttributeSchemaGenerator - attr_accessor model: Class - attr_accessor model_definition: Definition - attr_accessor configuration: Configuration - attr_accessor response: Hash[Symbol, any]? - attr_accessor schema_modifier: SchemaModifier + attr_reader model: Class + attr_reader model_definition: Definition + attr_reader configuration: Configuration + attr_reader response: Hash[Symbol, any]? + attr_reader schema_modifier: SchemaModifier def initialize: (Definition) -> void def generate: -> (Hash[Symbol, any] | Array[any]) diff --git a/sig/schemable/included_schema_generator.rbs b/sig/schemable/included_schema_generator.rbs index a090746..6677d99 100644 --- a/sig/schemable/included_schema_generator.rbs +++ b/sig/schemable/included_schema_generator.rbs @@ -1,11 +1,8 @@ module Schemable class IncludedSchemaGenerator - attr_accessor expand: bool - attr_accessor model_definition: Definition - attr_accessor configuration: Configuration - attr_accessor schema_modifier: SchemaModifier - attr_accessor relationships: Hash[Symbol, any] - attr_accessor relationships_to_exclude_from_expansion: Array[String] + attr_reader model_definition: Definition + attr_reader schema_modifier: SchemaModifier + attr_reader relationships: Hash[Symbol, any] def initialize: (Definition) -> void diff --git a/sig/schemable/relationship_schema_generator.rbs b/sig/schemable/relationship_schema_generator.rbs index 2e6a874..b82bcb1 100644 --- a/sig/schemable/relationship_schema_generator.rbs +++ b/sig/schemable/relationship_schema_generator.rbs @@ -1,8 +1,8 @@ module Schemable class RelationshipSchemaGenerator - attr_accessor model_definition: Definition - attr_accessor schema_modifier: SchemaModifier - attr_accessor relationships: Hash[Symbol, any] + attr_reader model_definition: Definition + attr_reader schema_modifier: SchemaModifier + attr_reader relationships: Hash[Symbol, any] def initialize: (Definition) -> void diff --git a/sig/schemable/request_schema_generator.rbs b/sig/schemable/request_schema_generator.rbs index 29f3996..c6a99b0 100644 --- a/sig/schemable/request_schema_generator.rbs +++ b/sig/schemable/request_schema_generator.rbs @@ -1,7 +1,7 @@ module Schemable class RequestSchemaGenerator - attr_accessor model_definition: Definition - attr_accessor schema_modifier: SchemaModifier + attr_reader model_definition: Definition + attr_reader schema_modifier: SchemaModifier def initialize: (Definition) -> void def generate_for_create: () -> (Hash[Symbol, any] | Array[Hash[Symbol, any]]) diff --git a/sig/schemable/response_schema_generator.rbs b/sig/schemable/response_schema_generator.rbs index f09bee5..22d45a2 100644 --- a/sig/schemable/response_schema_generator.rbs +++ b/sig/schemable/response_schema_generator.rbs @@ -1,8 +1,8 @@ module Schemable class ResponseSchemaGenerator - attr_accessor model: Class - attr_accessor model_definition: Definition - attr_accessor schema_modifier: SchemaModifier + attr_reader model: Class + attr_reader model_definition: Definition + attr_reader schema_modifier: SchemaModifier def initialize: (Definition) -> void def meta: -> Hash[Symbol, any] From d7d260f4ff608be5dad89f67c9cfa8bc9d30201b Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Tue, 14 Nov 2023 22:23:38 +0300 Subject: [PATCH 49/87] Updates Gem Version --- Gemfile.lock | 2 +- lib/schemable/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index d4521ff..b8afb76 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - schemable (0.1.4) + schemable (1.0.0) factory_bot_rails (~> 6.2.0) jsonapi-rails (~> 0.4.1) diff --git a/lib/schemable/version.rb b/lib/schemable/version.rb index a8e0b6f..104ff67 100644 --- a/lib/schemable/version.rb +++ b/lib/schemable/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Schemable - VERSION = '0.1.4' + VERSION = '1.0.0' end From a832924ee5a030a08ec751caebf36fd3981af67b Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Tue, 14 Nov 2023 22:37:59 +0300 Subject: [PATCH 50/87] Updates some of the old documentations --- lib/schemable/definition.rb | 4 ---- sig/schemable/definition.rbs | 34 +++++++++------------------------- 2 files changed, 9 insertions(+), 29 deletions(-) diff --git a/lib/schemable/definition.rb b/lib/schemable/definition.rb index 509fa14..fe01916 100644 --- a/lib/schemable/definition.rb +++ b/lib/schemable/definition.rb @@ -81,10 +81,6 @@ def excluded_response_included %i[] end - def nested_relationships - {} - end - def serialized_instance {} end diff --git a/sig/schemable/definition.rbs b/sig/schemable/definition.rbs index eb79900..929f54f 100644 --- a/sig/schemable/definition.rbs +++ b/sig/schemable/definition.rbs @@ -139,29 +139,7 @@ module Schemable # This method is not used anywhere yet. def excluded_response_included: -> Array[Symbol] - # Returns the relationships to be further expanded in the response schema. - # - # @return [Hash] The relationships to be further expanded in the response schema. - # - # @example - # { - # applicants: { - # belongs_to: { - # district: Swagger::Definitions::District, - # province: Swagger::Definitions::Province, - # }, - # has_many: { - # attachments: Swagger::Definitions::Upload, - # } - # } - # } - def nested_relationships: -> Hash[Symbol, any] - - # Returns the resource serializer to be used for serialization. This method must be implemented in the definition class. - # - # @abstract This method must be implemented in the definition class. - # - # @raise [NotImplementedError] If the method is not implemented in the definition class. + # Returns the resource serializer to be used for serialization. # # @return [Class] The resource serializer class. # @@ -182,7 +160,9 @@ module Schemable # @return [Hash] The relationships defined in the serializer. # # @note Note that the format of the relationships is as follows: - # { belongs_to: { relationship_name: relationship_definition }, has_many: { relationship_name: relationship_definition } + # { belongs_to: { relationship_name: relationship_definition }, has_many: { relationship_name: relationship_definition, addition_to_included: { relationship_name: relationship_definition } } } + # + # @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. # # @example # { @@ -192,7 +172,10 @@ module Schemable # }, # has_many: { # applicants: Swagger::Definitions::Applicant, - # } + # }, + # addition_to_included: { + # applicants: Swagger::Definitions::Applicant + # } # } def relationships: -> Hash[Symbol, any] @@ -261,6 +244,7 @@ module Schemable # { first_name: 'John', last_name: 'Doe' } => { firstName: 'John', lastName: 'Doe' } def camelize_keys: (Hash[Symbol, any]) -> (Hash[Symbol, any] | Array[Hash[Symbol, any]]) + # Returns the schema for the create request body, update request body, and response body. def self.generate: -> Array[Hash[Symbol, any]] end end From ac5a10e1135058ac4a0f518d124b473934a55008 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Tue, 14 Nov 2023 23:04:36 +0300 Subject: [PATCH 51/87] Slightly Updates Readme --- README.md | 51 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e7ae401..2c97f97 100644 --- a/README.md +++ b/README.md @@ -20,17 +20,19 @@ Or install it yourself as: ## Usage -The installation command above will install the Schemable gem and its dependencies. However, in order for Schemable to work, you must also add the following files to your Rails application: +The installation command above will install the Schemable gem and its dependencies. However, in order for Schemable to work, you must also implement your own logic to use the generated schemas to feed it to RSwag. + + - -- `app/helpers/serializers_helper.rb` - This file contains the `serializers_map` helper method, which is used to map a model to its serializer. -- `spec/swagger/common_definitions.rb` - This file contains the `aggregate` method, which is used to aggregate the schemas of all the models in your application into a single place. This file is recommeded, but not required. If you do not use this file, you will need to manually aggregate the schemas of all the models in your application into a single place. - -To generate these files, run the following command: +The below are some command to generate some files to get you started: ```ruby rails g schemable:install ``` +This will generate `schemable.rb` in your `config/initializers` directory. This file will contain the configuration for the Schemable gem. You can modify the configuration to your liking. For more information on the configuration options, see the [Configuration](#configuration) section below. + + ### Generating Definition Files The Schemable gem provides a generator that can be used to generate definition files for your models. To generate a definition file for a model, run the following command: @@ -41,7 +43,28 @@ rails g schemable:model --model_name This will generate a definition file for the specified model in the `lib/swagger/definitions` directory. The definition file will be named `.rb`. This file will have the bare minimum code required to generate a schema for the model. You can then modify the definition file to your liking by overriding the default methods. For example, you can add or remove attributes from the schema, or you can add or remove relationships from the schema. You can also add custom attributes to the schema. For more information on how to customize the schema, see the [Customizing the Schema](#customizing-the-schema) section below. -## Customizing the Schema + +### Configuration +The Schemable gem provides a number of configuration options that can be used to customize the behavior of the gem. The following is a list of the configuration options that are available: + +| Option Name | Description | Default Value | +| ----------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------- | +| `orm` | The ORM that is used in the application. The options are `:active_record` and `:mongoid`. | `:active_record` | +| `timestamps` | Whether or not to include the `created_at` and `updated_at` attributes in the response schema. | `true` | +| `float_as_string` | Whether or not to convert the `float` type to a `string` type in the schema. | `false` | +| `decimal_as_string` | Whether or not to convert the `decimal` type to a `string` type in the schema. | `false` | +| `custom_type_mappers` | A hash of custom type mappers that can be used to override the default type mappers. A specific method should be used, see annex 1.0 for more information. | `{}` | +| `disable_factory_bot` | Whether or not to disable the use of FactoryBot in the gem. To automatically generate serialized instance. See annex 1.1 for an example. | `true` | +| `use_serialized_instance` | Whether or not to use the serialized instance in the process of schema geenration as type fallback for virtual attributes. | `false` | +| `custom_defined_enum_method` | The name of the method that is used to get the enum keys and values. This allows applications with the orm `mongoid` define a method that mimicks what `defined_enums` does in `activerecord. Please see annex 1.2 for an example. | `nil` | +| `enum_prefix_for_simple_enum` | The prefix to be used for the enum values when `mongoid` is used. | `nil` | +| `enum_suffix_for_simple_enum` | The suffix to be used for the enum values when `mongoid` is used. | `nil` | +| `infer_attributes_from_custom_method` | The name of the custom method that is used to get the attributes to be generated in the schema. | `nil` | +| `infer_attributes_from_jsonapi_serializable` | Whether or not to infer the attributes from the JSONAPI::Serializable::Resource class. | `false` | + + + +### Customizing the Schema The Schemable gem provides a number of methods that can be used to customize the schema. These methods are defined in the `Schemable` module of the gem. To customize the schema, simply override the default methods in the definition file for the model. The following is a list of the methods that can be overridden: @@ -51,10 +74,10 @@ The Schemable gem provides a number of methods that can be used to customize the The list of methods that can be overridden are as follows: | Method Name | Description | -| -------------------------------- | ---------------------------------------------------------------------------------------------------------- | -| `serializer` | Returns the serializer class. | -| `attributes` | Returns the attributes that are auto generated from the model. | -| `relationships` | Returns the relationships in the format of { belongs_to: {}, has_many: {} }. | +|----------------------------------|------------------------------------------------------------------------------------------------------------| +| `serializer` | Returns the serializer class. Useful when `infer_attributes_from_jsonapi_serializable` is used | +| `attributes` | Returns the attributes that are auto generated from the model's fields/columns. | +| `relationships` | Returns the relationships in the format of { belongs_to: {}, has_many: {}, addition_to_included: {} }. | | `array_type` | Returns the type of arrays in the model that needs to be manually defined. | | `optional_request_attributes` | Returns the attributes that are optional in the request schema. | | `nullable_attributes` | Returns the attributes that are nullable in the request/response schema. | @@ -66,7 +89,7 @@ The list of methods that can be overridden are as follows: | `excluded_response_attributes` | Returns the attributes that are excluded from the response schema. | | `excluded_response_relations` | Returns the relationships that are excluded from the response schema. | | `excluded_response_included` | (not implemented yet) Returns the included that are excluded from the response schema. | -| `nested_relationships` | Returns the relationships to be further expanded in the response schema. | +| `serialized_instance` | Returns a serialized instance of the model, used for type generating as a fallback. | | `model` | Returns the model class (Constantized from the definition class name). | | `model_name` | Returns the model name. Used for schema type naming. | | `definitions` | Returns the generated schemas in JSONAPI format (It is recommended to override this method to your liking) | @@ -184,12 +207,8 @@ To install this gem onto your local machine, run `bundle exec rake install`. To ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/schemable. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/schemable/blob/master/CODE_OF_CONDUCT.md). +Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/schemable. This project is intended to be a safe, welcoming space for collaboration, and contributors. Please go to issues page to report any bugs or feature requests. Open issues are tagged with the `help wanted` label. If you would like to contribute, please fork the repository and submit a pull request. ## License The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). - -## Code of Conduct - -Everyone interacting in the Schemable project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/schemable/blob/master/CODE_OF_CONDUCT.md). From 2e8cb4b61091fcea36bb35be8ab5f6dba4566d4a Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Thu, 16 Nov 2023 11:34:02 +0300 Subject: [PATCH 52/87] Add documentation for Schemable module --- lib/schemable.rb | 50 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/lib/schemable.rb b/lib/schemable.rb index aa1033f..011b8db 100644 --- a/lib/schemable.rb +++ b/lib/schemable.rb @@ -8,15 +8,63 @@ require_relative 'schemable/response_schema_generator' require_relative 'schemable/request_schema_generator' +# The Schemable module provides a set of classes and methods for generating and modifying schemas in JSONAPI format. +# It includes classes for generating attribute, relationship, included, response, and request schemas. +# It also provides a configuration class for setting up the module's behavior. +# +# @example: +# The following example shows how to use the Schemable module to generate a schema for a Comment model. +# +# # config/initializers/schemable.rb +# Schemable.configure do |config| +# #... chosen configuration options ... +# end +# +# # lib/swagger/definitions/comment.rb +# class Swagger::Definitions::Comment < Schemable::Definition; end +# +# # whenever you need to generate the schema for a Comment model. +# # i.e. in RSwag's swagger_helper.rb +# +# # spec/swagger_helper.rb +# # ... +# RSpec.configure do |config| +# # ... +# +# config.swagger_docs = { +# # ... +# components: { +# # ... +# schemas: Swagger::Definitions::Comment.generate.flatten.reduce({}, :merge) +# # ... +# } +# # ... +# } +# # ... +# end +# +# @see Schemable::Definition +# @see Schemable::Configuration +# @see Schemable::SchemaModifier +# @see Schemable::AttributeSchemaGenerator +# @see Schemable::RelationshipSchemaGenerator +# @see Schemable::IncludedSchemaGenerator +# @see Schemable::ResponseSchemaGenerator +# @see Schemable::RequestSchemaGenerator module Schemable + # Error class for handling exceptions specific to the Schemable module. class Error < StandardError; end class << self + # Accessor for the module's configuration. attr_accessor :configuration + # Configures the module. If a block is given, it yields the current configuration. + # + # @yield [Configuration] The current configuration. def configure @configuration ||= Configuration.new yield(@configuration) if block_given? end end -end +end \ No newline at end of file From 86e208d7ad99d3ed9274085a9a926b8cedc881d0 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Thu, 16 Nov 2023 12:33:47 +0300 Subject: [PATCH 53/87] Add documentation for SchemaModifier class --- lib/schemable/schema_modifier.rb | 110 +++++++++++++++++++++++++++++- sig/schemable/schema_modifier.rbs | 39 +---------- 2 files changed, 111 insertions(+), 38 deletions(-) diff --git a/lib/schemable/schema_modifier.rb b/lib/schemable/schema_modifier.rb index 7234801..77cae40 100644 --- a/lib/schemable/schema_modifier.rb +++ b/lib/schemable/schema_modifier.rb @@ -1,9 +1,54 @@ module Schemable + # The SchemaModifier class provides methods for modifying a given schema. + # It includes methods for parsing paths, checking if a path exists in a schema, + # deeply merging hashes, adding properties to a schema, and deleting properties from a schema. + # + # @see Schemable class SchemaModifier + + # Parses a given path into an array of symbols. + # + # @note This method accepts paths in the following formats: + # - 'path.to.property' + # - 'path.to.array.[0].property' + # + # @example + # parse_path('path.to.property') #=> [:path, :to, :property] + # parse_path('path.to.array.[0].property') #=> [:path, :to, :array, :[0], :property] + # + # @param path [String] The path to parse. + # @return [Array] The parsed path. def parse_path(path) path.split('.').map(&:to_sym) end + # Checks if a given path exists in a schema. + # + # @example + # schema = { + # path: { + # type: :object, + # properties: { + # to: { + # type: :object, + # properties: { + # property: { + # type: :string + # } + # } + # } + # } + # } + # } + # + # path = 'path.properties.to.properties.property' + # incorrect_path = 'path.properties.to.properties.invalid' + # path_exists?(schema, path) #=> true + # path_exists?(schema, incorrect_path) #=> false + # + # @param schema [Hash, Array] The schema to check. + # @param path [String] The path to check for. + # @return [Boolean] True if the path exists in the schema, false otherwise. def path_exists?(schema, path) path_segments = parse_path(path) @@ -27,9 +72,30 @@ def path_exists?(schema, path) true end + # Deeply merges two hashes. + # + # @example + # destination = { level1: { level2: { level3: 'value' } } } + # new_data = { level1_again: 'value' } + # deep_merge_hashes(destination, new_data) + # #=> { level1: { level2: { level3: 'value' } }, level1_again: 'value' } + # + # new_destination = [{ object1: 'value' }, { object2: 'value' }] + # new_new_data = { object3: 'value' } + # deep_merge_hashes(new_destination, new_new_data) + # #=> [{ object1: 'value' }, { object2: 'value' }, { object3: 'value' }] + # + # new_destination = { object1: 'value' } + # new_new_data = [{ object2: 'value' }, { object3: 'value' }] + # deep_merge_hashes(new_destination, new_new_data) + # #=> { object1: 'value', object2: 'value', object3: 'value' } + # + # @param destination [Hash] The hash to merge into. + # @param new_data [Hash] The hash to merge from. + # @return [Hash] The merged hashes. def deep_merge_hashes(destination, new_data) - if destination.is_a?(Array) && new_data.is_a?(Array) - destination.concat(new_data) + if destination.is_a?(Hash) && new_data.is_a?(Array) + destination.merge(new_data) elsif destination.is_a?(Array) && new_data.is_a?(Hash) destination.push(new_data) elsif destination.is_a?(Hash) && new_data.is_a?(Hash) @@ -49,6 +115,30 @@ def deep_merge_hashes(destination, new_data) destination end + # Adds properties to a schema at a given path. + # + # @example + # original_schema = { level1: { level2: { level3: 'value' } } } + # new_data = { L3: 'value' } + # path = 'level1.level2' + # add_properties(original_schema, new_schema, path) + # #=> { level1: { level2: { level3: 'value', L3: 'value' } } } + # + # new_original_schema = { test: [{ object1: 'value' }, { object2: 'value' }] } + # new_new_schema = { object2_again: 'value' } + # path = 'test.[1]' + # add_properties(new_original_schema, new_new_schema, path) + # #=> { test: [{ object1: 'value' }, { object2: 'value', object2_again: 'value' }] } + # + # @param original_schema [Hash] The original schema. + # @param new_schema [Hash] The new schema to add. + # @param path [String] The path at which to add the new schema. + # @note This method accepts paths in the following formats: + # - 'path.to.property' + # - 'path.to.array.[0].property' + # - '.' + # + # @return [Hash] The modified schema. def add_properties(original_schema, new_schema, path) return deep_merge_hashes(original_schema, new_schema) if path == '.' @@ -94,6 +184,22 @@ def add_properties(original_schema, new_schema, path) original_schema end + # Deletes properties from a schema at a given path. + # + # @example + # original_schema = { level1: { level2: { level3: 'value' } } } + # path = 'level1.level2' + # delete_properties(original_schema, path) + # #=> { level1: {} } + # + # new_original_schema = { test: [{ object1: 'value' }, { object2: 'value' }] } + # path = 'test.[1]' + # delete_properties(new_original_schema, path) + # #=> { test: [{ object1: 'value' }] } + # + # @param original_schema [Hash] The original schema. + # @param path [String] The path at which to delete properties. + # @return [Hash] The modified schema. def delete_properties(original_schema, path) return original_schema if path == '.' diff --git a/sig/schemable/schema_modifier.rbs b/sig/schemable/schema_modifier.rbs index 76fa6f8..9dfe2ca 100644 --- a/sig/schemable/schema_modifier.rbs +++ b/sig/schemable/schema_modifier.rbs @@ -1,42 +1,9 @@ -# == SchemaModifier -# -# This module provides methods for working with Hash/JSON-like schemas. -# It includes methods to parse paths, check if a path exists in a schema, -# deep merge two hashes or an array and a hash, add properties to a specific -# location in a schema, and delete properties at a specified path. -# -# === Examples -# -# schema_modifier = Schemable::SchemaModifier.new -# -# path = "properties.name.items.[0].properties.age" -# parsed_path = schema_modifier.parse_path(path) -# # => [:properties, :name, :items, :'[0]', :properties, :age] -# -# schema = { properties: { name: "John" } } -# exists = schema_modifier.path_exists?(schema, "properties.name") -# # => true -# -# new_data = { age: 25 } -# merged_data = schema_modifier.deep_merge_hashes({ name: "John" }, new_data) -# # => { name: "John", age: 25 } -# -# original_schema = { properties: { name: "John" } } -# new_schema = { age: 25 } -# updated_schema = schema_modifier.add_properties(original_schema, new_schema, "properties") -# # => { properties: { name: "John", age: 25 } } -# -# schema = { properties: { name: "John", age: 25 } } -# path_to_delete = "properties.name.age" -# updated_schema = schema_modifier.delete_properties(schema, path_to_delete) -# # => { properties: { name: "John" } } -# module Schemable class SchemaModifier def parse_path: (path: String) -> Array[Symbol] def path_exists?: (schema: Hash[Symbol, any], path: String) -> bool - def deep_merge_hashes: (destination: Hash[Symbol, any], new_data: Hash[Symbol, any]) -> (Hash[Symbol, any] | Array[any]) - def add_properties: (original_schema: (Hash[Symbol, any] | Array[any]), new_schema: Hash[Symbol, any], path: String) -> (Hash[Symbol, any] | Array[any]) - def delete_properties: (original_schema: (Hash[Symbol, any] | Array[any]), path: String) -> (Hash[Symbol, any] | Array[any]) + def deep_merge_hashes: (destination: Hash[Symbol, any], new_data: Hash[Symbol, any]) -> (Hash[Symbol, any]) + def add_properties: (original_schema: (Hash[Symbol, any]), new_schema: Hash[Symbol, any], path: String) -> (Hash[Symbol, any]) + def delete_properties: (original_schema: (Hash[Symbol, any]), path: String) -> (Hash[Symbol, any]) end end From e75e18c43b933a486a1831acf6cd35e7c4fc024e Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Thu, 16 Nov 2023 13:47:16 +0300 Subject: [PATCH 54/87] Add documentation for Configuration class --- lib/schemable/configuration.rb | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lib/schemable/configuration.rb b/lib/schemable/configuration.rb index 0808a91..f90be88 100644 --- a/lib/schemable/configuration.rb +++ b/lib/schemable/configuration.rb @@ -1,4 +1,9 @@ module Schemable + # The Configuration class provides a set of configuration options for the Schemable module. + # It includes options for setting the ORM, handling timestamps, custom type mappers, and more. + # It is worth noting that the configuration options are global, and will affect all Definitions. + # + # @see Schemable class Configuration attr_accessor( :orm, @@ -15,6 +20,7 @@ class Configuration :infer_attributes_from_jsonapi_serializable, ) + # Initializes a new Configuration instance with default values. def initialize @timestamps = true @orm = :active_record # orm options are :active_record, :mongoid @@ -30,6 +36,15 @@ def initialize @infer_attributes_from_jsonapi_serializable = false end + # Returns a type mapper for a given type name. + # + # @note If a custom type mapper is defined for the given type name, it will be returned. + # + # @example + # type_mapper(:string) #=> { type: :string } + # + # @param type_name [Symbol, String] The name of the type. + # @return [Hash] The type mapper for the given type name. def type_mapper(type_name) return @custom_type_mappers[type_name] if @custom_type_mappers.key?(type_name) @@ -78,6 +93,20 @@ def type_mapper(type_name) }[type_name.to_s.underscore.try(:to_sym)] end + # Adds a custom type mapper for a given type name. + # + # @example + # add_custom_type_mapper(:custom_type, { type: :custom }) + # type_mapper(:custom_type) #=> { type: :custom } + # + # # It preferable to invoke this method in the config/initializers/schemable.rb file. + # # This way, the custom type mapper will be available for all Definitions. + # Schemable.configure do |config| + # config.add_custom_type_mapper(:custom_type, { type: :custom }) + # end + # + # @param type_name [Symbol, String] The name of the type. + # @param mapping [Hash] The mapping to add. def add_custom_type_mapper(type_name, mapping) custom_type_mappers[type_name.to_sym] = mapping end From c4a2af54e7674c742feb23e8ed1c5acbc642bb82 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Thu, 16 Nov 2023 13:58:39 +0300 Subject: [PATCH 55/87] Add documentation for AttributeSchemaGenerator class --- lib/schemable/attribute_schema_generator.rb | 22 +++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/schemable/attribute_schema_generator.rb b/lib/schemable/attribute_schema_generator.rb index bcf5b08..33eca63 100644 --- a/lib/schemable/attribute_schema_generator.rb +++ b/lib/schemable/attribute_schema_generator.rb @@ -1,7 +1,16 @@ module Schemable + # The AttributeSchemaGenerator class is responsible for generating JSON schemas for model attributes. + # It includes methods for generating the overall schema and individual attribute schemas. + # + # @see Schemable class AttributeSchemaGenerator attr_reader :model_definition, :configuration, :model, :schema_modifier, :response + # Initializes a new AttributeSchemaGenerator instance. + # + # @param model_definition [ModelDefinition] The model definition to generate the schema for. + # @example + # generator = AttributeSchemaGenerator.new(model_definition) def initialize(model_definition) @model_definition = model_definition @model = model_definition.model @@ -10,7 +19,11 @@ def initialize(model_definition) @response = nil end - # Generate the JSON schema for attributes + # Generates the JSON schema for the model attributes. + # + # @return [Hash] The generated schema. + # @example + # schema = generator.generate def generate schema = { type: :object, @@ -37,7 +50,12 @@ def generate schema end - # Generate the JSON schema for a specific attribute + # Generates the JSON schema for a specific attribute. + # + # @param attribute [Symbol, String] The attribute to generate the schema for. + # @return [Hash] The generated schema for the attribute. + # @example + # attribute_schema = generator.generate_attribute_schema(:attribute_name) def generate_attribute_schema(attribute) if @configuration.orm == :mongoid # Get the column hash for the attribute From f95b047efe7c370d766dab3f2f12a19bfc0c65b0 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Thu, 16 Nov 2023 14:36:12 +0300 Subject: [PATCH 56/87] Fixes Indentation --- lib/schemable.rb | 47 +++++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/lib/schemable.rb b/lib/schemable.rb index 011b8db..ea02382 100644 --- a/lib/schemable.rb +++ b/lib/schemable.rb @@ -12,36 +12,35 @@ # It includes classes for generating attribute, relationship, included, response, and request schemas. # It also provides a configuration class for setting up the module's behavior. # -# @example: +# @example # The following example shows how to use the Schemable module to generate a schema for a Comment model. # -# # config/initializers/schemable.rb -# Schemable.configure do |config| -# #... chosen configuration options ... -# end +# # config/initializers/schemable.rb +# Schemable.configure do |config| +# #... chosen configuration options ... +# end # -# # lib/swagger/definitions/comment.rb -# class Swagger::Definitions::Comment < Schemable::Definition; end +# # lib/swagger/definitions/comment.rb +# class Swagger::Definitions::Comment < Schemable::Definition; end # -# # whenever you need to generate the schema for a Comment model. -# # i.e. in RSwag's swagger_helper.rb +# # whenever you need to generate the schema for a Comment model. +# # i.e. in RSwag's swagger_helper.rb # -# # spec/swagger_helper.rb -# # ... -# RSpec.configure do |config| +# # spec/swagger_helper.rb # # ... +# RSpec.configure do |config| # -# config.swagger_docs = { -# # ... -# components: { -# # ... -# schemas: Swagger::Definitions::Comment.generate.flatten.reduce({}, :merge) -# # ... -# } -# # ... -# } -# # ... -# end +# config.swagger_docs = { +# # ... +# components: { +# # ... +# schemas: Swagger::Definitions::Comment.generate.flatten.reduce({}, :merge) +# # ... +# } +# # ... +# } +# # ... +# end # # @see Schemable::Definition # @see Schemable::Configuration @@ -67,4 +66,4 @@ def configure yield(@configuration) if block_given? end end -end \ No newline at end of file +end From 26ec4d9214691a0a1abef6f6e9b155cfe7b03640 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Fri, 17 Nov 2023 15:54:06 +0300 Subject: [PATCH 57/87] Adds documentations for definition class --- lib/schemable/definition.rb | 211 +++++++++++++++++++++++++++++ sig/schemable/definition.rbs | 250 +++-------------------------------- 2 files changed, 227 insertions(+), 234 deletions(-) diff --git a/lib/schemable/definition.rb b/lib/schemable/definition.rb index fe01916..41c8844 100644 --- a/lib/schemable/definition.rb +++ b/lib/schemable/definition.rb @@ -1,4 +1,11 @@ module Schemable + # The Definition class provides a blueprint for generating and modifying schemas. + # It includes methods for handling attributes, relationships, and various request and response attributes. + # The definition class is meant to be inherited by a class that represents a model. + # This class should be configured to match the model's attributes and relationships. + # The defaullt configuration is set in this class, but can be overridden in the model's definition class. + # + # @see Schemable class Definition attr_reader :configuration attr_writer :relationships, :additional_create_request_attributes, :additional_update_request_attributes @@ -7,12 +14,25 @@ def initialize @configuration = Schemable.configuration end + # Returns the serializer of the model for the definition. + # @example + # UsersSerializer + # @return [JSONAPI::Serializable::Resource, nil] The model's serializer. def serializer 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 nil end + # Returns the attributes for the definition based on the configuration. + # The attributes are inferred from the model's attribute names by default. + # If the infer_attributes_from_custom_method configuration option is set, the attributes are inferred from the method specified. + # If the infer_attributes_from_jsonapi_serializable configuration option is set, the attributes are inferred from the serializer's attribute blocks. + # + # @example + # attributes = definition.attributes # => [:id, :name, :email] + # + # @return [Array] The attributes used for generating the schemas. def attributes return (serializer&.attribute_blocks&.transform_keys { |key| key.to_s.underscore.to_sym }&.keys || nil) if configuration.infer_attributes_from_jsonapi_serializable @@ -21,82 +41,273 @@ def attributes model.attribute_names.map(&:to_sym) end + # Returns the relationships defined in the serializer. + # + # @note Note that the format of the relationships is as follows: + # { + # belongs_to: { relationship_name: relationship_definition }, + # has_many: { relationship_name: relationship_definition }, + # addition_to_included: { relationship_name: relationship_definition } + # } + # + # @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. + # + # @example + # { + # belongs_to: { + # district: Swagger::Definitions::District, + # user: Swagger::Definitions::User + # }, + # has_many: { + # applicants: Swagger::Definitions::Applicant, + # }, + # addition_to_included: { + # applicants: Swagger::Definitions::Applicant + # } + # } + # + # @return [Hash] The relationships defined in the serializer. def relationships { belongs_to: {}, has_many: {} } end + # Returns a hash of all the arrays defined for the model. + # The schema for each array is defined in the definition class manually. + # This method must be implemented in the definition class if there are any arrays. + # + # @return [Hash] The arrays of the model and their schemas. + # + # @example + # { + # metadata: { + # type: :array, + # items: { + # type: :object, nullable: true, + # properties: { name: { type: :string, nullable: true } } + # } + # } + # } def array_types {} end + # Attributes that are not required in the create request. + # + # @example + # optional_create_request_attributes = definition.optional_create_request_attributes + # # => [:email] + # + # @return [Array] The attributes that are not required in the create request. def optional_create_request_attributes %i[] end + # Attributes that are not required in the update request. + # + # @example + # optional_update_request_attributes = definition.optional_update_request_attributes + # # => [:email] + # + # @return [Array] The attributes that are not required in the update request. def optional_update_request_attributes %i[] end + # 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. + # They are not required to be present in the request body. + # + # @example + # [:name, :email] + # + # @return [Array] The attributes that are nullable in the request/response body. def nullable_attributes %i[] end + # Returns the additional create request attributes that are not automatically generated. + # These attributes are appended to the create request schema. + # + # @example + # { name: { type: :string } } + # + # @return [Hash] The additional create request attributes that are not automatically generated (if any). def additional_create_request_attributes {} end + # Returns the additional update request attributes that are not automatically generated. + # These attributes are appended to the update request schema. + # + # @example + # { name: { type: :string } } + # + # @return [Hash] The additional update request attributes that are not automatically generated (if any). def additional_update_request_attributes {} end + # Returns the additional response attributes that are not automatically generated. These attributes are appended to the response schema. + # + # @example + # { name: { type: :string } } + # + # @return [Hash] The additional response attributes that are not automatically generated (if any). def additional_response_attributes {} end + # Returns the additional response relations that are not automatically generated. + # These relations are appended to the response schema's relationships. + # + # @example + # { + # users: { + # type: :object, + # properties: { + # data: { + # type: :array, + # items: { + # type: :object, + # properties: { + # id: { type: :string }, + # type: { type: :string } + # } + # } + # } + # } + # } + # } + # + # @return [Hash] The additional response relations that are not automatically generated (if any). def additional_response_relations {} end + # Returns the additional response included that are not automatically generated. + # These included additions are appended to the response schema's included. + # + # @example + # { + # type: :object, + # properties: { + # id: { type: :string }, + # type: { type: :string }, + # attributes: { + # type: :object, + # properties: { + # name: { type: :string } + # } + # } + # } + # } + # + # @return [Hash] The additional response included that are not automatically generated (if any). def additional_response_included {} end + # Returns the attributes that are excluded from the create request schema. + # These attributes are not required or not needed to be present in the create request body. + # + # @example + # [:id, :updated_at, :created_at] + # + # @return [Array] The attributes that are excluded from the create request schema. def excluded_create_request_attributes %i[] end + # Returns the attributes that are excluded from the response schema. + # These attributes are not needed to be present in the response body. + # + # @example + # [:id, :updated_at, :created_at] + # + # @return [Array] The attributes that are excluded from the response schema. def excluded_update_request_attributes %i[] end + # Returns the attributes that are excluded from the update request schema. + # These attributes are not required or not needed to be present in the update request body. + # + # @example + # [:id, :updated_at, :created_at] + # + # @return [Array] The attributes that are excluded from the update request schema. def excluded_response_attributes %i[] end + # Returns the relationships that are excluded from the response schema. + # These relationships are not needed to be present in the response body. + # + # @example + # [:users, :applicants] + # + # @return [Array] The relationships that are excluded from the response schema. def excluded_response_relations %i[] end + # Returns the included that are excluded from the response schema. + # These included are not needed to be present in the response body. + # + # @example + # [:users, :applicants] + # + # @todo + # This method is not used anywhere yet. + # + # @return [Array] The included that are excluded from the response schema. def excluded_response_included %i[] end + # Returns an instance of the model class that is already serialized into jsonapi format. + # + # @return [Hash] The serialized instance of the model class. def serialized_instance {} end + # Returns the model class (Constantized from the definition class name) + # + # @example + # User + # + # @return [Class] The model class (Constantized from the definition class name) def model self.class.name.gsub('Swagger::Definitions::', '').constantize end + # Returns the model name. Used for schema type naming. + # + # @return [String] The model name. + # + # @example + # 'users' for the User model + # 'citizen_applications' for the CitizenApplication model def model_name self.class.name.gsub('Swagger::Definitions::', '').pluralize.underscore.downcase end + # Given a hash, it returns a new hash with all the keys camelized. + # + # @param hash [Hash] The hash with all the keys camelized. + # + # @return [Hash] The hash with all the keys camelized. + # + # @example + # { first_name: 'John', last_name: 'Doe' } => { firstName: 'John', lastName: 'Doe' } def camelize_keys(hash) hash.deep_transform_keys { |key| key.to_s.camelize(:lower).to_sym } end + # Returns the schema for the create request body, update request body, and response body. + # + # @return [Array] The schema for the create request body, update request body, and response body. def self.generate instance = new diff --git a/sig/schemable/definition.rbs b/sig/schemable/definition.rbs index 929f54f..a159d12 100644 --- a/sig/schemable/definition.rbs +++ b/sig/schemable/definition.rbs @@ -5,246 +5,28 @@ module Schemable attr_writer additional_create_request_attributes: Hash[Symbol, any] attr_writer additional_update_request_attributes: Hash[Symbol, any] - # Initializes the definition with the configuration. + def model: -> Class def initialize: -> void - - # 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. - # They are not required to be present in the request body. - # - # @return [Array] The attributes that are nullable in the request/response body. - # - # @example - # [:name, :email] + def serializer: -> Class? + def model_name: -> String + def attributes: -> Array[Symbol] + def array_types: -> Hash[Symbol, any] + def relationships: -> Hash[Symbol, any] def nullable_attributes: -> Array[Symbol] - - # Returns the additional create request attributes that are not automatically generated. These attributes are appended to the create request schema. - # - # @return [Hash] The additional create request attributes that are not automatically generated (if any). - # - # @example - # { - # name: { type: :string } - # } - def additional_create_request_attributes: -> Hash[Symbol, any] - - # Returns the additional update request attributes that are not automatically generated. These attributes are appended to the update request schema. - # - # @return [Hash] The additional update request attributes that are not automatically generated (if any). - # - # @example - # { - # name: { type: :string } - # } - def additional_update_request_attributes: -> Hash[Symbol, any] - - # Returns the additional response attributes that are not automatically generated. These attributes are appended to the response schema. - # - # @return [Hash] The additional response attributes that are not automatically generated (if any). - # - # @example - # { - # name: { type: :string } - # } - def additional_response_attributes: -> Hash[Symbol, any] - - # Returns the additional response relations that are not automatically generated. These relations are appended to the response schema's relationships. - # - # @return [Hash] The additional response relations that are not automatically generated (if any). - # - # @example - # { - # users: { - # type: :object, - # properties: { - # data: { - # type: :array, - # items: { - # type: :object, - # properties: { - # id: { type: :string }, - # type: { type: :string } - # } - # } - # } - # } - # } - # } - def additional_response_relations: -> Hash[Symbol, any] - - # Returns the additional response included that are not automatically generated. These included are appended to the response schema's included. - # - # @return [Hash] The additional response included that are not automatically generated (if any). - # - # @example - # { - # type: :object, - # properties: { - # id: { type: :string }, - # type: { type: :string }, - # attributes: { - # type: :object, - # properties: { - # name: { type: :string } - # } - # } - # } - # } + def serialized_instance: -> Hash[Symbol, any] + def self.generate: -> Array[Hash[Symbol, any]] + def excluded_response_included: -> Array[Symbol] + def excluded_response_relations: -> Array[Symbol] + def excluded_response_attributes: -> Array[Symbol] def additional_response_included: -> Hash[Symbol, any] - - # Returns the attributes that are excluded from the create request schema. - # These attributes are not required or not needed to be present in the create request body. - # - # @return [Array] The attributes that are excluded from the create request schema. - # - # @example - # [:id, :updated_at, :created_at] + def additional_response_relations: -> Hash[Symbol, any] + def additional_response_attributes: -> Hash[Symbol, any] def excluded_create_request_attributes: -> Array[Symbol] - - # Returns the attributes that are excluded from the response schema. - # These attributes are not needed to be present in the response body. - # - # @return [Array] The attributes that are excluded from the response schema. - # - # @example - # [:id, :updated_at, :created_at] - def excluded_response_attributes: -> Array[Symbol] - - # Returns the attributes that are excluded from the update request schema. - # These attributes are not required or not needed to be present in the update request body. - # - # @return [Array] The attributes that are excluded from the update request schema. - # - # @example - # [:id, :updated_at, :created_at] def excluded_update_request_attributes: -> Array[Symbol] - - # Returns the relationships that are excluded from the response schema. - # These relationships are not needed to be present in the response body. - # - # @return [Array] The relationships that are excluded from the response schema. - # - # @example - # [:users, :applicants] - def excluded_response_relations: -> Array[Symbol] - - # Returns the included that are excluded from the response schema. - # These included are not needed to be present in the response body. - # - # @return [Array] The included that are excluded from the response schema. - # - # @example - # [:users, :applicants] - # - # @todo - # This method is not used anywhere yet. - def excluded_response_included: -> Array[Symbol] - - # Returns the resource serializer to be used for serialization. - # - # @return [Class] The resource serializer class. - # - # @example - # V1::UserSerializer - def serializer: -> Class? - - # Returns the attributes defined in the serializer (Auto generated from the serializer), or from a custom method, or from attributes_names method. - # - # @return [Array] The attributes to be generated. - # - # @example - # [:id, :name, :email, :created_at, :updated_at] - def attributes: -> Array[Symbol] - - # Returns the relationships defined in the serializer. - # - # @return [Hash] The relationships defined in the serializer. - # - # @note Note that the format of the relationships is as follows: - # { belongs_to: { relationship_name: relationship_definition }, has_many: { relationship_name: relationship_definition, addition_to_included: { relationship_name: relationship_definition } } } - # - # @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. - # - # @example - # { - # belongs_to: { - # district: Swagger::Definitions::District, - # user: Swagger::Definitions::User - # }, - # has_many: { - # applicants: Swagger::Definitions::Applicant, - # }, - # addition_to_included: { - # applicants: Swagger::Definitions::Applicant - # } - # } - def relationships: -> Hash[Symbol, any] - - # Returns a hash of all the arrays defined for the model. The schema for each array is defined in the definition class manually. - # This method must be implemented in the definition class if there are any arrays. - # - # @return [Hash] The arrays of the model and their schemas. - # - # @example - # { - # metadata: { - # type: :array, - # items: { - # type: :object, nullable: true, - # properties: { name: { type: :string, nullable: true } } - # } - # } - # } - def array_types: -> Hash[Symbol, any] - - # Returns the attributes that are optional in the create request body. This means that they are not required to be present in the create request body thus they are taken out of the required array. - # - # @return [Array] The attributes that are optional in the create request body. - # - # @example - # [:name, :email] def optional_create_request_attributes: -> Array[Symbol] - - # Returns the attributes that are optional in the update request body. This means that they are not required to be present in the update request body thus they are taken out of the required array. - # - # @return [Array] The attributes that are optional in the update request body. - # - # @example - # [:name, :email] def optional_update_request_attributes: -> Array[Symbol] - - # Returns an instance of the model class that is already serialized into jsonapi format. - # - # @return [Hash] The serialized instance of the model class. - def serialized_instance: -> Hash[Symbol, any] - - # Returns the model class (Constantized from the definition class name) - # - # @return [Class] The model class (Constantized from the definition class name) - # - # @example - # User - def model: -> Class - - # Returns the model name. Used for schema type naming. - # - # @return [String] The model name. - # - # @example - # 'users' for the User model - # 'citizen_applications' for the CitizenApplication model - def model_name: -> String - - # Given a hash, it returns a new hash with all the keys camelized. - # - # @param hash [Array | Hash] The hash with all the keys camelized. - # - # @return [Array | Hash] The hash with all the keys camelized. - # - # @example - # { first_name: 'John', last_name: 'Doe' } => { firstName: 'John', lastName: 'Doe' } - def camelize_keys: (Hash[Symbol, any]) -> (Hash[Symbol, any] | Array[Hash[Symbol, any]]) - - # Returns the schema for the create request body, update request body, and response body. - def self.generate: -> Array[Hash[Symbol, any]] + def camelize_keys: (Hash[Symbol, any]) -> (Hash[Symbol, any]) + def additional_create_request_attributes: -> Hash[Symbol, any] + def additional_update_request_attributes: -> Hash[Symbol, any] end end From dc89944058be4bd702a1edd1ca9aebef7a832349 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Fri, 17 Nov 2023 15:59:52 +0300 Subject: [PATCH 58/87] Fixes typos and ensures type safety --- lib/schemable/attribute_schema_generator.rb | 4 ++-- lib/schemable/configuration.rb | 4 ++-- lib/schemable/definition.rb | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/schemable/attribute_schema_generator.rb b/lib/schemable/attribute_schema_generator.rb index 33eca63..27a149d 100644 --- a/lib/schemable/attribute_schema_generator.rb +++ b/lib/schemable/attribute_schema_generator.rb @@ -62,7 +62,7 @@ def generate_attribute_schema(attribute) attribute_hash = @model.fields[attribute.to_s] # Check if this attribute has a custom JSON Schema definition - return @model_definition.array_types[attribute] if @model_definition.array_types.keys.include?(attribute) + return @model_definition.array_types[attribute] if @model_definition.array_types.keys.include?(attribute.to_sym) return @model_definition.additional_response_attributes[attribute] if @model_definition.additional_response_attributes.keys.include?(attribute) # Check if this is an array attribute @@ -81,7 +81,7 @@ def generate_attribute_schema(attribute) attribute_hash = @model.columns_hash[attribute.to_s] # Check if this attribute has a custom JSON Schema definition - return @model_definition.array_types[attribute] if @model_definition.array_types.keys.include?(attribute) + return @model_definition.array_types[attribute] if @model_definition.array_types.keys.include?(attribute.to_sym) return @model_definition.additional_response_attributes[attribute] if @model_definition.additional_response_attributes.keys.include?(attribute) # Check if this is an array attribute diff --git a/lib/schemable/configuration.rb b/lib/schemable/configuration.rb index f90be88..df06993 100644 --- a/lib/schemable/configuration.rb +++ b/lib/schemable/configuration.rb @@ -17,7 +17,7 @@ class Configuration :enum_prefix_for_simple_enum, :enum_suffix_for_simple_enum, :infer_attributes_from_custom_method, - :infer_attributes_from_jsonapi_serializable, + :infer_attributes_from_jsonapi_serializable ) # Initializes a new Configuration instance with default values. @@ -46,7 +46,7 @@ def initialize # @param type_name [Symbol, String] The name of the type. # @return [Hash] The type mapper for the given type name. def type_mapper(type_name) - return @custom_type_mappers[type_name] if @custom_type_mappers.key?(type_name) + return @custom_type_mappers[type_name] if @custom_type_mappers.key?(type_name.to_sym) { text: { type: :string }, diff --git a/lib/schemable/definition.rb b/lib/schemable/definition.rb index 41c8844..ad159ee 100644 --- a/lib/schemable/definition.rb +++ b/lib/schemable/definition.rb @@ -3,7 +3,7 @@ module Schemable # It includes methods for handling attributes, relationships, and various request and response attributes. # The definition class is meant to be inherited by a class that represents a model. # This class should be configured to match the model's attributes and relationships. - # The defaullt configuration is set in this class, but can be overridden in the model's definition class. + # The default configuration is set in this class, but can be overridden in the model's definition class. # # @see Schemable class Definition From 6908df347aac88d69068916457b79d5bb8ee923d Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Fri, 17 Nov 2023 16:11:20 +0300 Subject: [PATCH 59/87] Ensures type safety --- lib/schemable/definition.rb | 2 +- sig/schemable/definition.rbs | 2 +- sig/schemable/included_schema_generator.rbs | 4 +--- sig/schemable/relationship_schema_generator.rbs | 4 +--- sig/schemable/request_schema_generator.rbs | 4 ++-- 5 files changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/schemable/definition.rb b/lib/schemable/definition.rb index ad159ee..0768f9f 100644 --- a/lib/schemable/definition.rb +++ b/lib/schemable/definition.rb @@ -297,7 +297,7 @@ def model_name # # @param hash [Hash] The hash with all the keys camelized. # - # @return [Hash] The hash with all the keys camelized. + # @return [Hash, Array] The hash with all the keys camelized. # # @example # { first_name: 'John', last_name: 'Doe' } => { firstName: 'John', lastName: 'Doe' } diff --git a/sig/schemable/definition.rbs b/sig/schemable/definition.rbs index a159d12..18db060 100644 --- a/sig/schemable/definition.rbs +++ b/sig/schemable/definition.rbs @@ -25,8 +25,8 @@ module Schemable def excluded_update_request_attributes: -> Array[Symbol] def optional_create_request_attributes: -> Array[Symbol] def optional_update_request_attributes: -> Array[Symbol] - def camelize_keys: (Hash[Symbol, any]) -> (Hash[Symbol, any]) def additional_create_request_attributes: -> Hash[Symbol, any] def additional_update_request_attributes: -> Hash[Symbol, any] + def camelize_keys: (Hash[Symbol, any]) -> (Array[Hash[Symbol, any]] | Hash[Symbol, any]) end end diff --git a/sig/schemable/included_schema_generator.rbs b/sig/schemable/included_schema_generator.rbs index 6677d99..616b146 100644 --- a/sig/schemable/included_schema_generator.rbs +++ b/sig/schemable/included_schema_generator.rbs @@ -5,9 +5,7 @@ module Schemable attr_reader relationships: Hash[Symbol, any] def initialize: (Definition) -> void - + def generate: (?relationships_to_exclude_from_expansion: Array[String], ?expand: bool) -> (Hash[Symbol, any]) def prepare_schema_for_included: (Definition, ?relationships_to_exclude_from_expansion: Array[String], ?expand: bool) -> Hash[Symbol, any] - - def generate: (?relationships_to_exclude_from_expansion: Array[String], ?expand: bool) -> (Hash[Symbol, any] | Array[Hash[Symbol, any]]) end end diff --git a/sig/schemable/relationship_schema_generator.rbs b/sig/schemable/relationship_schema_generator.rbs index b82bcb1..8d21c70 100644 --- a/sig/schemable/relationship_schema_generator.rbs +++ b/sig/schemable/relationship_schema_generator.rbs @@ -5,9 +5,7 @@ module Schemable attr_reader relationships: Hash[Symbol, any] def initialize: (Definition) -> void - - def generate: (?relationships_to_exclude_from_expansion: Array[String], ?expand: bool) -> (Hash[Symbol, any] | Array[Hash[Symbol, any]]) - def generate_schema: (String, ?collection: bool) -> Hash[Symbol, any] + def generate: (?relationships_to_exclude_from_expansion: Array[String], ?expand: bool) -> (Hash[Symbol, any]) end end diff --git a/sig/schemable/request_schema_generator.rbs b/sig/schemable/request_schema_generator.rbs index c6a99b0..64b719b 100644 --- a/sig/schemable/request_schema_generator.rbs +++ b/sig/schemable/request_schema_generator.rbs @@ -4,7 +4,7 @@ module Schemable attr_reader schema_modifier: SchemaModifier def initialize: (Definition) -> void - def generate_for_create: () -> (Hash[Symbol, any] | Array[Hash[Symbol, any]]) - def generate_for_update: () -> (Hash[Symbol, any] | Array[Hash[Symbol, any]]) + def generate_for_create: () -> (Hash[Symbol, any]) + def generate_for_update: () -> (Hash[Symbol, any]) end end From 6fa863ab939bc495c87456c36c982075c2271e3f Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Fri, 17 Nov 2023 16:14:28 +0300 Subject: [PATCH 60/87] Removes timestamps config --- README.md | 3 +-- lib/schemable/configuration.rb | 4 +--- sig/schemable/configuration.rbs | 1 - 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2c97f97..10a8398 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,7 @@ The Schemable gem provides a number of configuration options that can be used to | Option Name | Description | Default Value | | ----------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------- | -| `orm` | The ORM that is used in the application. The options are `:active_record` and `:mongoid`. | `:active_record` | -| `timestamps` | Whether or not to include the `created_at` and `updated_at` attributes in the response schema. | `true` | +| `orm` | The ORM that is used in the application. The options are `:active_record` and `:mongoid`. | `true` | | `float_as_string` | Whether or not to convert the `float` type to a `string` type in the schema. | `false` | | `decimal_as_string` | Whether or not to convert the `decimal` type to a `string` type in the schema. | `false` | | `custom_type_mappers` | A hash of custom type mappers that can be used to override the default type mappers. A specific method should be used, see annex 1.0 for more information. | `{}` | diff --git a/lib/schemable/configuration.rb b/lib/schemable/configuration.rb index df06993..09d1b57 100644 --- a/lib/schemable/configuration.rb +++ b/lib/schemable/configuration.rb @@ -1,13 +1,12 @@ module Schemable # The Configuration class provides a set of configuration options for the Schemable module. - # It includes options for setting the ORM, handling timestamps, custom type mappers, and more. + # It includes options for setting the ORM, handling enums, custom type mappers, and more. # It is worth noting that the configuration options are global, and will affect all Definitions. # # @see Schemable class Configuration attr_accessor( :orm, - :timestamps, :float_as_string, :decimal_as_string, :custom_type_mappers, @@ -22,7 +21,6 @@ class Configuration # Initializes a new Configuration instance with default values. def initialize - @timestamps = true @orm = :active_record # orm options are :active_record, :mongoid @float_as_string = false @custom_type_mappers = {} diff --git a/sig/schemable/configuration.rbs b/sig/schemable/configuration.rbs index 6a874ad..c5388a5 100644 --- a/sig/schemable/configuration.rbs +++ b/sig/schemable/configuration.rbs @@ -1,7 +1,6 @@ module Schemable class Configuration attr_accessor orm: Symbol - attr_accessor timestamps: bool attr_accessor float_as_string: bool attr_accessor decimal_as_string: bool attr_accessor disable_factory_bot: bool From 04f7ded55d85f8fff64a4868005c3659f34659e1 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Fri, 17 Nov 2023 16:30:01 +0300 Subject: [PATCH 61/87] Adds documentations for IncludedSchemaGenerator --- lib/schemable/included_schema_generator.rb | 38 ++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/lib/schemable/included_schema_generator.rb b/lib/schemable/included_schema_generator.rb index 721a2d0..f326839 100644 --- a/lib/schemable/included_schema_generator.rb +++ b/lib/schemable/included_schema_generator.rb @@ -1,13 +1,40 @@ module Schemable + # The IncludedSchemaGenerator class is responsible for generating the 'included' part of a JSON:API compliant response. + # This class generates schemas for related resources that should be included in the response. + # + # @see Schemable class IncludedSchemaGenerator attr_reader :model_definition, :schema_modifier, :relationships + # Initializes a new IncludedSchemaGenerator instance. + # + # @param model_definition [ModelDefinition] The model definition to generate the schema for. + # + # @example + # generator = IncludedSchemaGenerator.new(model_definition) def initialize(model_definition) @model_definition = model_definition @schema_modifier = SchemaModifier.new @relationships = @model_definition.relationships end + # Generates the 'included' part of the JSON:API response. + # It iterates over each relationship type (belongs_to, has_many) and for each relationship, + # it prepares a schema. If the 'expand' option is true, it also includes the relationships of the related resource in the schema. + # In that case, the 'addition_to_included' relationships are also included in the schema unless they are excluded from expansion. + # + # @param expand [Boolean] Whether to include the relationships of the related resource in the schema. + # @param relationships_to_exclude_from_expansion [Array] The relationships to exclude from the schema. + # + # @note Make sure to provide the names correctly in string format and pluralize them if necessary. + # For example, if you have a relationship named 'applicant', and an applicant has association + # named 'identity', you should provide 'identities' as the names of the relationship to exclude from expansion. + # In this case, the included schema of the applicant will not include the identity relationship. + # + # @example + # schema = generator.generate(expand: true, relationships_to_exclude_from_expansion: ['some_relationship']) + # + # @return [Hash] The generated schema. def generate(expand: false, relationships_to_exclude_from_expansion: []) return {} if @relationships.blank? return {} if @relationships == { belongs_to: {}, has_many: {} } @@ -52,6 +79,17 @@ def generate(expand: false, relationships_to_exclude_from_expansion: []) schema end + # Prepares the schema for a related resource to be included in the response. + # It generates the attribute and relationship schemas for the related resource and combines them into a single schema. + # + # @param model_definition [ModelDefinition] The model definition of the related resource. + # @param expand [Boolean] Whether to include the relationships of the related resource in the schema. + # @param relationships_to_exclude_from_expansion [Array] The relationships to exclude from the schema. + # + # @example + # included_schema = generator.prepare_schema_for_included(related_model_definition, expand: true, relationships_to_exclude_from_expansion: ['some_relationship']) + # + # @return [Hash] The generated schema for the related resource. def prepare_schema_for_included(model_definition, expand: false, relationships_to_exclude_from_expansion: []) attributes_schema = AttributeSchemaGenerator.new(model_definition).generate relationships_schema = RelationshipSchemaGenerator.new(model_definition).generate(relationships_to_exclude_from_expansion:, expand:) From f4f273428f50176cfa3e74287332ae4b418a5c1a Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Fri, 17 Nov 2023 16:45:57 +0300 Subject: [PATCH 62/87] Adds documentations for RelationshipSchemaGenerator --- .../relationship_schema_generator.rb | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/lib/schemable/relationship_schema_generator.rb b/lib/schemable/relationship_schema_generator.rb index a39ffe3..54fa65d 100644 --- a/lib/schemable/relationship_schema_generator.rb +++ b/lib/schemable/relationship_schema_generator.rb @@ -1,13 +1,35 @@ module Schemable + # The RelationshipSchemaGenerator class is responsible for generating the 'relationships' part of a JSON:API compliant response. + # This class generates schemas for each relationship of a model, including 'belongs_to' (and has_many) and 'has_many' relationships. + # + # @see Schemable class RelationshipSchemaGenerator attr_reader :model_definition, :schema_modifier, :relationships + # Initializes a new RelationshipSchemaGenerator instance. + # + # @param model_definition [ModelDefinition] The model definition to generate the schema for. + # + # @example + # generator = RelationshipSchemaGenerator.new(model_definition) def initialize(model_definition) @model_definition = model_definition @schema_modifier = SchemaModifier.new @relationships = model_definition.relationships end + # Generates the 'relationships' part of the JSON:API response. + # It iterates over each relationship type (belongs_to, has_many) and for each relationship, + # it prepares a schema unless the relationship is excluded from expansion. + # If the 'expand' option is true, it changes the schema to include type and id properties inside the 'meta' property. + # + # @param relationships_to_exclude_from_expansion [Array] The relationships to exclude from expansion. + # @param expand [Boolean] Whether to include the relationships of the related resource in the schema. + # + # @example + # schema = generator.generate(expand: true, relationships_to_exclude_from_expansion: [:some_relationship]) + # + # @return [Hash] The generated schema. def generate(relationships_to_exclude_from_expansion: [], expand: false) return {} if @relationships.blank? || @relationships == { belongs_to: {}, has_many: {} } @@ -49,6 +71,18 @@ def generate(relationships_to_exclude_from_expansion: [], expand: false) schema end + # Generates the schema for a specific relationship. + # If the 'collection' option is true, it generates a schema for a 'has_many' relationship, + # otherwise it generates a schema for a 'belongs_to' relationship. The difference between the two is that + # 'data' is an array in the 'has_many' relationship and an object in the 'belongs_to' relationship. + # + # @param type_name [String] The type of the related resource. + # @param collection [Boolean] Whether the relationship is a 'has_many' relationship. + # + # @example + # relationship_schema = generator.generate_schema('resource_type', collection: true) + # + # @return [Hash] The generated schema for the relationship. def generate_schema(type_name, collection: false) if collection { From d2f3c8f2c2d0efeac5953ebd6dbff37df8553826 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Fri, 17 Nov 2023 17:06:53 +0300 Subject: [PATCH 63/87] Adds documentations for RequestSchemaGenerator class --- lib/schemable/request_schema_generator.rb | 28 +++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lib/schemable/request_schema_generator.rb b/lib/schemable/request_schema_generator.rb index ceb8cf8..c66761a 100644 --- a/lib/schemable/request_schema_generator.rb +++ b/lib/schemable/request_schema_generator.rb @@ -1,12 +1,31 @@ module Schemable + # The RequestSchemaGenerator class is responsible for generating JSON schemas for create and update requests. + # This class generates schemas based on the model definition, including additional and excluded attributes. + # + # @see Schemable class RequestSchemaGenerator attr_reader :model_definition, :schema_modifier + # Initializes a new RequestSchemaGenerator instance. + # + # @param model_definition [ModelDefinition] The model definition to generate the schema for. + # + # @example + # generator = RequestSchemaGenerator.new(model_definition) def initialize(model_definition) @model_definition = model_definition @schema_modifier = SchemaModifier.new end + # Generates the JSON schema for a create request. + # It generates a schema for the model attributes and then modifies it based on the additional and excluded attributes for create requests. + # It also determines the required attributes based on the optional and nullable attributes. + # Note that it is presumed that the model is using the same fields/columns for create as well as responses. + # + # @example + # schema = generator.generate_for_create + # + # @return [Hash] The generated schema for create requests. def generate_for_create schema = { type: :object, @@ -32,6 +51,15 @@ def generate_for_create @schema_modifier.add_properties(schema, required_attributes, 'properties.data') end + # Generates the JSON schema for a update request. + # It generates a schema for the model attributes and then modifies it based on the additional and excluded attributes for update requests. + # It also determines the required attributes based on the optional and nullable attributes. + # Note that it is presumed that the model is using the same fields/columns for update as well as responses. + # + # @example + # schema = generator.generate_for_update + # + # @return [Hash] The generated schema for update requests. def generate_for_update schema = { type: :object, From e5229586af62856cf3df6c641c24730b9ccc2dbb Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Fri, 17 Nov 2023 17:15:23 +0300 Subject: [PATCH 64/87] Allows custom meta for response --- lib/schemable/configuration.rb | 4 ++ lib/schemable/response_schema_generator.rb | 53 ++++++++++++--------- sig/schemable/configuration.rbs | 6 ++- sig/schemable/response_schema_generator.rbs | 1 + 4 files changed, 39 insertions(+), 25 deletions(-) diff --git a/lib/schemable/configuration.rb b/lib/schemable/configuration.rb index 09d1b57..5dd8b87 100644 --- a/lib/schemable/configuration.rb +++ b/lib/schemable/configuration.rb @@ -9,12 +9,14 @@ class Configuration :orm, :float_as_string, :decimal_as_string, + :pagination_enabled, :custom_type_mappers, :disable_factory_bot, :use_serialized_instance, :custom_defined_enum_method, :enum_prefix_for_simple_enum, :enum_suffix_for_simple_enum, + :custom_meta_response_schema, :infer_attributes_from_custom_method, :infer_attributes_from_jsonapi_serializable ) @@ -24,10 +26,12 @@ def initialize @orm = :active_record # orm options are :active_record, :mongoid @float_as_string = false @custom_type_mappers = {} + @pagination_enabled = true @decimal_as_string = false @disable_factory_bot = true @use_serialized_instance = false @custom_defined_enum_method = nil + @custom_meta_response_schema = nil @enum_prefix_for_simple_enum = nil @enum_suffix_for_simple_enum = nil @infer_attributes_from_custom_method = nil diff --git a/lib/schemable/response_schema_generator.rb b/lib/schemable/response_schema_generator.rb index c59464d..5f6d38b 100644 --- a/lib/schemable/response_schema_generator.rb +++ b/lib/schemable/response_schema_generator.rb @@ -1,11 +1,12 @@ module Schemable class ResponseSchemaGenerator - attr_reader :model_definition, :model, :schema_modifier + attr_reader :model_definition, :model, :schema_modifier, :configuration def initialize(model_definition) @model_definition = model_definition @model = model_definition.model @schema_modifier = SchemaModifier.new + @configuration = Schemable.configuration end def generate(expand: false, relationships_to_exclude_from_expansion: [], collection: false) @@ -38,32 +39,38 @@ def generate(expand: false, relationships_to_exclude_from_expansion: [], collect end def meta - { - type: :object, - properties: { - page: { - type: :object, - properties: { - totalPages: { - type: :integer, - default: 1 - }, - count: { - type: :integer, - default: 1 - }, - rowsPerPage: { - type: :integer, - default: 1 - }, - currentPage: { - type: :integer, - default: 1 + return @configuration.custom_meta_response_schema if @configuration.custom_meta_response_schema.present? + + if @configuration.pagination_enabled + { + type: :object, + properties: { + page: { + type: :object, + properties: { + totalPages: { + type: :integer, + default: 1 + }, + count: { + type: :integer, + default: 1 + }, + rowsPerPage: { + type: :integer, + default: 1 + }, + currentPage: { + type: :integer, + default: 1 + } } } } } - } + else + {} + end end def jsonapi diff --git a/sig/schemable/configuration.rbs b/sig/schemable/configuration.rbs index c5388a5..c645625 100644 --- a/sig/schemable/configuration.rbs +++ b/sig/schemable/configuration.rbs @@ -3,14 +3,16 @@ module Schemable attr_accessor orm: Symbol attr_accessor float_as_string: bool attr_accessor decimal_as_string: bool + attr_accessor pagination_enabled: bool attr_accessor disable_factory_bot: bool attr_accessor use_serialized_instance: bool + attr_accessor custom_defined_enum_method: Symbol? attr_accessor enum_prefix_for_simple_enum: String? attr_accessor enum_suffix_for_simple_enum: String? - attr_accessor custom_defined_enum_method: Symbol? attr_accessor custom_type_mappers: Hash[Symbol, any] - attr_accessor infer_attributes_from_jsonapi_serializable: bool attr_accessor infer_attributes_from_custom_method: Symbol? + attr_accessor custom_meta_response_schema: Hash[Symbol, any]? + attr_accessor infer_attributes_from_jsonapi_serializable: bool def initialize: -> void diff --git a/sig/schemable/response_schema_generator.rbs b/sig/schemable/response_schema_generator.rbs index 22d45a2..84c0faa 100644 --- a/sig/schemable/response_schema_generator.rbs +++ b/sig/schemable/response_schema_generator.rbs @@ -3,6 +3,7 @@ module Schemable attr_reader model: Class attr_reader model_definition: Definition attr_reader schema_modifier: SchemaModifier + attr_reader configuration: Configuration def initialize: (Definition) -> void def meta: -> Hash[Symbol, any] From 561a90953bc9b9926c97350c0aa384743d313b00 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Fri, 17 Nov 2023 17:21:08 +0300 Subject: [PATCH 65/87] Adds documentations for ResponseSchemaGenerator --- lib/schemable/response_schema_generator.rb | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/lib/schemable/response_schema_generator.rb b/lib/schemable/response_schema_generator.rb index 5f6d38b..0f716ee 100644 --- a/lib/schemable/response_schema_generator.rb +++ b/lib/schemable/response_schema_generator.rb @@ -1,7 +1,17 @@ module Schemable + # The ResponseSchemaGenerator class is responsible for generating JSON schemas for responses. + # This class generates schemas based on the model definition, including attributes, relationships, and included resources. + # + # @see Schemable class ResponseSchemaGenerator attr_reader :model_definition, :model, :schema_modifier, :configuration + # Initializes a new ResponseSchemaGenerator instance. + # + # @param model_definition [ModelDefinition] The model definition to generate the schema for. + # + # @example + # generator = ResponseSchemaGenerator.new(model_definition) def initialize(model_definition) @model_definition = model_definition @model = model_definition.model @@ -9,6 +19,19 @@ def initialize(model_definition) @configuration = Schemable.configuration end + # Generates the JSON schema for a response. + # It generates a schema for the model attributes and relationships, and if the 'expand' option is true, + # it also includes the included resources in the schema. + # It also adds meta and jsonapi information to the schema. + # + # @param expand [Boolean] Whether to include the included resources in the schema. + # @param relationships_to_exclude_from_expansion [Array] The relationships to exclude from expansion in the schema. + # @param collection [Boolean] Whether the response is for a collection of resources. + # + # @example + # schema = generator.generate(expand: true, relationships_to_exclude_from_expansion: ['some_relationship'], collection: true) + # + # @return [Hash] The generated schema. def generate(expand: false, relationships_to_exclude_from_expansion: [], collection: false) data = { type: :object, @@ -38,6 +61,13 @@ def generate(expand: false, relationships_to_exclude_from_expansion: [], collect { type: :object, properties: schema }.compact_blank end + # Generates the JSON schema for the 'meta' part of a response. + # It returns a custom meta response schema if one is defined in the configuration, otherwise it generates a default meta schema. + # + # @example + # meta_schema = generator.meta + # + # @return [Hash] The generated schema for the 'meta' part of a response. def meta return @configuration.custom_meta_response_schema if @configuration.custom_meta_response_schema.present? @@ -73,6 +103,12 @@ def meta end end + # Generates the JSON schema for the 'jsonapi' part of a response. + # + # @example + # jsonapi_schema = generator.jsonapi + # + # @return [Hash] The generated schema for the 'jsonapi' part of a response. def jsonapi { type: :object, From 86460aec57b32e9622314c0ab01d8865e684eac9 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Fri, 17 Nov 2023 17:41:10 +0300 Subject: [PATCH 66/87] Removes factory bot option --- lib/schemable/configuration.rb | 2 -- sig/schemable/configuration.rbs | 1 - 2 files changed, 3 deletions(-) diff --git a/lib/schemable/configuration.rb b/lib/schemable/configuration.rb index 5dd8b87..97513f8 100644 --- a/lib/schemable/configuration.rb +++ b/lib/schemable/configuration.rb @@ -11,7 +11,6 @@ class Configuration :decimal_as_string, :pagination_enabled, :custom_type_mappers, - :disable_factory_bot, :use_serialized_instance, :custom_defined_enum_method, :enum_prefix_for_simple_enum, @@ -28,7 +27,6 @@ def initialize @custom_type_mappers = {} @pagination_enabled = true @decimal_as_string = false - @disable_factory_bot = true @use_serialized_instance = false @custom_defined_enum_method = nil @custom_meta_response_schema = nil diff --git a/sig/schemable/configuration.rbs b/sig/schemable/configuration.rbs index c645625..ecef3b3 100644 --- a/sig/schemable/configuration.rbs +++ b/sig/schemable/configuration.rbs @@ -4,7 +4,6 @@ module Schemable attr_accessor float_as_string: bool attr_accessor decimal_as_string: bool attr_accessor pagination_enabled: bool - attr_accessor disable_factory_bot: bool attr_accessor use_serialized_instance: bool attr_accessor custom_defined_enum_method: Symbol? attr_accessor enum_prefix_for_simple_enum: String? From 9b7c7d492fa78dc920533b8cda7129b27f799813 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Fri, 17 Nov 2023 17:59:07 +0300 Subject: [PATCH 67/87] Updates the gem's generators --- lib/generators/schemable/install_generator.rb | 16 +--- lib/generators/schemable/model_generator.rb | 32 ++++---- lib/templates/common_definitions.rb | 13 ---- lib/templates/schemable.rb | 78 +++++++++++++++++++ lib/templates/serializers_helper.rb | 7 -- 5 files changed, 97 insertions(+), 49 deletions(-) delete mode 100644 lib/templates/common_definitions.rb create mode 100644 lib/templates/schemable.rb delete mode 100644 lib/templates/serializers_helper.rb diff --git a/lib/generators/schemable/install_generator.rb b/lib/generators/schemable/install_generator.rb index dcfcb14..33d9af4 100644 --- a/lib/generators/schemable/install_generator.rb +++ b/lib/generators/schemable/install_generator.rb @@ -1,28 +1,18 @@ module Schemable class InstallGenerator < Rails::Generators::Base - source_root File.expand_path('../../templates', __dir__) - class_option :model_name, type: :string, default: 'Model', desc: 'Name of the model' def initialize(*args) super(*args) end def copy_initializer - target_path = 'spec/swagger/common_definitions.rb' - - if Rails.root.join(target_path).exist? - say_status('skipped', 'Common definitions already exists') - else - copy_file('common_definitions.rb', target_path) - end - - target_path = 'app/helpers/serializers_helper.rb' + target_path = 'config/initializers/schemable.rb' if Rails.root.join(target_path).exist? - say_status('skipped', 'Serializers helper already exists') + say_status('skipped', 'Schemable initializer already exists') else - copy_file('serializers_helper.rb', target_path) + copy_file('schemable.rb', target_path) end end end diff --git a/lib/generators/schemable/model_generator.rb b/lib/generators/schemable/model_generator.rb index 7fbdf03..bc48a64 100644 --- a/lib/generators/schemable/model_generator.rb +++ b/lib/generators/schemable/model_generator.rb @@ -21,21 +21,7 @@ def copy_initializer create_file(target_path, <<-FILE module Swagger module Definitions - class #{@model_name.classify} - - include Schemable - include SerializersHelper # This is a helper module that contains a method "serializers_map" that maps models to serializers - - attr_accessor :instance - - def initialize - @instance ||= JSONAPI::Serializable::Renderer.new.render(FactoryBot.create(:#{@model_name.underscore.downcase.singularize}), class: serializers_map, include: []) - end - - def serializer - V1::#{@model_name.classify}Serializer - end - + class #{@model_name.classify} < Schemable::Definition def excluded_create_request_attributes %i[updated_at created_at] end @@ -43,12 +29,26 @@ def excluded_create_request_attributes def excluded_update_request_attributes %i[updated_at created_at] end + + # Methods that maybe useful if factory_bot and jsonapi-rails are used to generate the instance + ################################################## + # include SerializersHelper # This is a helper module that contains a method "serializers_map" that maps models to serializers + # + # attr_accessor :instance + # + # def serialized_instance + # @instance ||= JSONAPI::Serializable::Renderer.new.render(FactoryBot.create(:#{@model_name.underscore.downcase.singularize}), class: serializers_map, include: []) + # end + # + # def serializer + # V1::#{@model_name.classify}Serializer + # end + ################################################## end end end FILE ) - end end end diff --git a/lib/templates/common_definitions.rb b/lib/templates/common_definitions.rb deleted file mode 100644 index ef54eda..0000000 --- a/lib/templates/common_definitions.rb +++ /dev/null @@ -1,13 +0,0 @@ -module SwaggerDefinitions - module CommonDefinitions - def self.aggregate - [ - # Import definitions like this: - # Swagger::Definitions::Model.definitions - - # Make sure in swagger_helper.rb's components section you have: - # schemas: SwaggerDefinitions::CommonDefinitions.aggregate - ].flatten.reduce({}, :merge) - end - end -end diff --git a/lib/templates/schemable.rb b/lib/templates/schemable.rb new file mode 100644 index 0000000..711be3c --- /dev/null +++ b/lib/templates/schemable.rb @@ -0,0 +1,78 @@ +Schemable.configure do |config| + # The following options are available for configuration. + # If you do not specify a configuration option, then its default value will be used. + # To configure them, uncomment them and set them to the desired value. + + # The ORM options are :active_record, :mongoid + # + # config.orm = :active_record + + # The gem uses `{ type: :number, format: :float }` for float attributes by default. + # If you want to use `{ type: :string }` instead, set this option to true. + # + # config.float_as_string = false + + # The gem uses `{ type: :number, format: :decimal }` for decimal attributes by default. + # If you want to use `{ type: :string }` instead, set this option to true. + # + # config.decimal_as_string = false + + # The gem by default sets the pagination_enabled option to true + # which means in the meta section of the response schema + # it will add the pagination links and the total count + # if you don't want to have the pagination links and the total count + # in the meta section of the response schema, set this option to false + # If you want to define your own meta schema, you can set the custom_meta_response_schema option + # + # config.pagination_enabled = true + # + # config.custom_meta_response_schema = nil + + # The gem allows for custom defined schema for a specific type + # for example if you wish to have all your arrays have the schema + # { type: :array, items: { type: string } } then use the below method to add to custom_type_mappers + # + # config.add_custom_type_mapper(:array, { type: :array, items: { type: string } }) + + # If you have a custom enum method defined on all of your model, you can set it here + # for example if you have a method called `base_attributes` on all of your models + # and you use that method to return an array of symbols that are the attributes + # to be serialized then you can set the below to `base_attributes` + # + # config.infer_attributes_from_custom_method = nil + + + # If you want to get the list of attributes from the jsonapi-rails gem's + # JSONAPI::Serializable::Resource class, set this option to true. + # It uses the attribute_blocks method to get the list of attributes. + # + # config.infer_attributes_from_jsonapi_serializable = false + + # Sometimes you may have virtual attributes that are not in the database + # Generating the schema for these attributes will fail, in that case you can + # add your logic to return an instance of the model that is serialized in + # jsonapi format and the gem will use that to generate the schema + # this is useful if you use factory_bot and jsonapi-rails to generate the instance + # check the commented out code in the definition template for an example + # Set this option to true to enable this feature + # + # config.use_serialized_instance = false + + # By default the gem uses activerecord's defined_enums method to get the enums + # with their keys and values, if you don't have this method defined on your model + # then please set the below option to the name of the method that returns the + # enums with their keys and values as a hash. This will handle the auto generation + # of the enum schema for you, with correct values. + # + # config.custom_defined_enum_method = nil + + + # If you use mongoid and simple_enum gem, you can set the below options to the prefix and suffix + # Since simple_enum uses the prefix and suffix to generate the enum methods, and the fields' names + # are usually the enum name with the prefix and suffix, the gem will remove the prefix and suffix + # from the field name to get the enum name and then use that to get the enum values + # + # config.enum_prefix_for_simple_enum = nil + # + # config.enum_suffix_for_simple_enum = nil +end diff --git a/lib/templates/serializers_helper.rb b/lib/templates/serializers_helper.rb deleted file mode 100644 index 902740d..0000000 --- a/lib/templates/serializers_helper.rb +++ /dev/null @@ -1,7 +0,0 @@ -module SerializersHelper - def serializers_map - { - # TheModel: V1::TheModelSerializer - }.freeze - end -end From 9aba52bca44c9702acc394eea48204ad9b888693 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Fri, 17 Nov 2023 20:58:06 +0300 Subject: [PATCH 68/87] Fixes rubocop offenses --- lib/templates/schemable.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/templates/schemable.rb b/lib/templates/schemable.rb index 711be3c..ff7c59a 100644 --- a/lib/templates/schemable.rb +++ b/lib/templates/schemable.rb @@ -41,7 +41,6 @@ # # config.infer_attributes_from_custom_method = nil - # If you want to get the list of attributes from the jsonapi-rails gem's # JSONAPI::Serializable::Resource class, set this option to true. # It uses the attribute_blocks method to get the list of attributes. @@ -66,7 +65,6 @@ # # config.custom_defined_enum_method = nil - # If you use mongoid and simple_enum gem, you can set the below options to the prefix and suffix # Since simple_enum uses the prefix and suffix to generate the enum methods, and the fields' names # are usually the enum name with the prefix and suffix, the gem will remove the prefix and suffix From 7fb82b33ff2772fa12d71b38c1004b9fe83ae6d8 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Fri, 17 Nov 2023 20:59:07 +0300 Subject: [PATCH 69/87] Updates gems --- Gemfile | 10 ++-- Gemfile.lock | 130 +++++++++++++++++++++++++++++----------------- schemable.gemspec | 7 +-- 3 files changed, 88 insertions(+), 59 deletions(-) diff --git a/Gemfile b/Gemfile index 2df8685..1531efa 100644 --- a/Gemfile +++ b/Gemfile @@ -4,12 +4,12 @@ source 'https://rubygems.org' gemspec -gem 'rake', '~> 13.0.6' -gem 'rspec', '~> 3.12' -gem 'rubocop', '~> 1.55' -gem 'rubocop-rails', '~> 2.20.2' +gem 'rake', '~> 13.1.0' +gem 'rspec', '~> 3.12.0' +gem 'rubocop', '~> 1.57.2' +gem 'rubocop-rails', '~> 2.22.1' group :development, :test do - gem 'factory_bot_rails', '~> 6.2' + gem 'factory_bot_rails', '~> 6.2.0' gem 'jsonapi-rails', '~> 0.4.1' end diff --git a/Gemfile.lock b/Gemfile.lock index b8afb76..acbf803 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,43 +2,58 @@ PATH remote: . specs: schemable (1.0.0) - factory_bot_rails (~> 6.2.0) - jsonapi-rails (~> 0.4.1) GEM remote: https://rubygems.org/ specs: - actionpack (7.0.4.3) - actionview (= 7.0.4.3) - activesupport (= 7.0.4.3) - rack (~> 2.0, >= 2.2.0) + actionpack (7.1.2) + actionview (= 7.1.2) + activesupport (= 7.1.2) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actionview (7.0.4.3) - activesupport (= 7.0.4.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actionview (7.1.2) + activesupport (= 7.1.2) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activesupport (7.0.4.3) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activesupport (7.1.2) + base64 + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) minitest (>= 5.1) + mutex_m tzinfo (~> 2.0) ast (2.4.2) + base64 (0.2.0) + bigdecimal (3.1.4) builder (3.2.4) concurrent-ruby (1.2.2) + connection_pool (2.4.1) crass (1.0.6) diff-lcs (1.5.0) + drb (2.2.0) + ruby2_keywords erubi (1.12.0) factory_bot (6.2.1) activesupport (>= 5.0.0) factory_bot_rails (6.2.0) factory_bot (~> 6.2.0) railties (>= 5.0.0) - i18n (1.13.0) + i18n (1.14.1) concurrent-ruby (~> 1.0) + io-console (0.6.0) + irb (1.9.0) + rdoc + reline (>= 0.3.8) json (2.6.3) jsonapi-deserializable (0.2.0) jsonapi-parser (0.1.1) @@ -52,36 +67,50 @@ GEM jsonapi-serializable (0.3.1) jsonapi-renderer (~> 0.2.0) language_server-protocol (3.17.0.3) - loofah (2.21.2) + loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) - method_source (1.0.0) - minitest (5.18.0) - nokogiri (1.14.4-x86_64-linux) + minitest (5.20.0) + mutex_m (0.2.0) + nokogiri (1.15.4-x86_64-linux) racc (~> 1.4) parallel (1.23.0) - parser (3.2.2.3) + parser (3.2.2.4) ast (~> 2.4.1) racc - racc (1.6.2) - rack (2.2.7) + psych (5.1.1.1) + stringio + racc (1.7.3) + rack (3.0.8) + rack-session (2.0.0) + rack (>= 3.0.0) rack-test (2.1.0) rack (>= 1.3) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + rackup (2.1.0) + rack (>= 3) + webrick (~> 1.8) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.5.0) - loofah (~> 2.19, >= 2.19.1) - railties (7.0.4.3) - actionpack (= 7.0.4.3) - activesupport (= 7.0.4.3) - method_source + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.1.2) + actionpack (= 7.1.2) + activesupport (= 7.1.2) + irb + rackup (>= 1.0.0) rake (>= 12.2) - thor (~> 1.0) - zeitwerk (~> 2.5) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.0.6) - regexp_parser (2.8.1) + rake (13.1.0) + rdoc (6.6.0) + psych (>= 4.0.0) + regexp_parser (2.8.2) + reline (0.4.0) + io-console (~> 0.5) rexml (3.2.6) rspec (3.12.0) rspec-core (~> 3.12.0) @@ -92,45 +121,48 @@ GEM rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-mocks (3.12.5) + rspec-mocks (3.12.6) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-support (3.12.0) - rubocop (1.55.0) + rspec-support (3.12.1) + rubocop (1.57.2) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.2.3) + parser (>= 3.2.2.4) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) rubocop-ast (>= 1.28.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.29.0) + rubocop-ast (1.30.0) parser (>= 3.2.1.0) - rubocop-rails (2.20.2) + rubocop-rails (2.22.1) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) ruby-progressbar (1.13.0) - thor (1.2.2) + ruby2_keywords (0.0.5) + stringio (3.0.9) + thor (1.3.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.4.2) - zeitwerk (2.6.8) + unicode-display_width (2.5.0) + webrick (1.8.1) + zeitwerk (2.6.12) PLATFORMS x86_64-linux DEPENDENCIES - factory_bot_rails (~> 6.2) + factory_bot_rails (~> 6.2.0) jsonapi-rails (~> 0.4.1) - rake (~> 13.0.6) - rspec (~> 3.12) - rubocop (~> 1.55) - rubocop-rails (~> 2.20.2) + rake (~> 13.1.0) + rspec (~> 3.12.0) + rubocop (~> 1.57.2) + rubocop-rails (~> 2.22.1) schemable! BUNDLED WITH - 2.4.12 + 2.4.17 diff --git a/schemable.gemspec b/schemable.gemspec index 2048249..a52a073 100644 --- a/schemable.gemspec +++ b/schemable.gemspec @@ -8,8 +8,8 @@ Gem::Specification.new do |spec| spec.authors = ['Muhammad Nawzad'] spec.email = ['hama127n@gmail.com'] - spec.summary = 'An opiniated Gem for Rails applications to auto generate schema in JSONAPI format.' - spec.description = "The schemable gem is an opiniated Gem for Rails applications to auto generate schema for models in JSONAPI format. It is designed to work with rswag's swagger documentation since it can generate the schemas for it." + spec.summary = 'An opinionated Gem for Rails applications to auto generate schema in JSONAPI format.' + spec.description = "The schemable gem is an opinionated Gem for Rails applications to auto generate schema for models in JSONAPI format. It is designed to work with rswag's swagger documentation since it can generate the schemas for it." spec.homepage = 'https://github.com/muhammadnawzad/schemable' spec.license = 'MIT' spec.required_ruby_version = '>= 3.1.2' @@ -29,8 +29,5 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] - spec.add_dependency 'factory_bot_rails', '~> 6.2.0' - spec.add_dependency 'jsonapi-rails', '~> 0.4.1' - spec.metadata['rubygems_mfa_required'] = 'true' end From 9abdfe2a77ed8a4bf1c632862c2af9e33f699de9 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Fri, 17 Nov 2023 23:42:57 +0300 Subject: [PATCH 70/87] Moves example to readme documentations --- lib/generators/schemable/model_generator.rb | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/lib/generators/schemable/model_generator.rb b/lib/generators/schemable/model_generator.rb index bc48a64..37b3d7b 100644 --- a/lib/generators/schemable/model_generator.rb +++ b/lib/generators/schemable/model_generator.rb @@ -29,21 +29,6 @@ def excluded_create_request_attributes def excluded_update_request_attributes %i[updated_at created_at] end - - # Methods that maybe useful if factory_bot and jsonapi-rails are used to generate the instance - ################################################## - # include SerializersHelper # This is a helper module that contains a method "serializers_map" that maps models to serializers - # - # attr_accessor :instance - # - # def serialized_instance - # @instance ||= JSONAPI::Serializable::Renderer.new.render(FactoryBot.create(:#{@model_name.underscore.downcase.singularize}), class: serializers_map, include: []) - # end - # - # def serializer - # V1::#{@model_name.classify}Serializer - # end - ################################################## end end end From 33a11e95a353b12931cf5c95c17ed1120f779108 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Fri, 17 Nov 2023 23:43:36 +0300 Subject: [PATCH 71/87] Updates the gem's documentation --- README.md | 418 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 312 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index 10a8398..e5f91a2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Schemable -The Schemable gem provides a simple way to define a schema for a Rails model in [JSONAPI](https://jsonapi.org/) format. It can automatically generate a schema for a model based on the model's factory and the model's attributes. It is also highly customizable, allowing you to modify the schema to your liking by overriding the default methods. +The Schemable gem provides a simple way to define a schema for a Rails model in [JSONAPI](https://jsonapi.org/) format. It can automatically generate a schema for a model based on the model's attributes. It is also highly customizable, allowing you to modify the schema to your liking by overriding configuration options and methods. + +This gem is preferably to be used with [RSwag](https://github.com/rswag/rswag) gem to generate the swagger documentation for your API. ## Installation @@ -21,10 +23,8 @@ Or install it yourself as: ## Usage The installation command above will install the Schemable gem and its dependencies. However, in order for Schemable to work, you must also implement your own logic to use the generated schemas to feed it to RSwag. - - - -The below are some command to generate some files to get you started: +The below command is to initialize the gem and generate the configuration file. ```ruby rails g schemable:install @@ -45,116 +45,313 @@ This will generate a definition file for the specified model in the `lib/swagger ### Configuration -The Schemable gem provides a number of configuration options that can be used to customize the behavior of the gem. The following is a list of the configuration options that are available: +The Schemable gem provides a number of configuration options that can be used to customize the behavior of the gem. The following is a list of the configuration options that are available. + +Please note that the configurations options below are defined in the `Schemable` module of the gem. To configure the gem, simply override the default values in the `config/initializers/schemable.rb` file. Also the changes will affect all the definition classes globally. -| Option Name | Description | Default Value | -| ----------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------- | -| `orm` | The ORM that is used in the application. The options are `:active_record` and `:mongoid`. | `true` | -| `float_as_string` | Whether or not to convert the `float` type to a `string` type in the schema. | `false` | -| `decimal_as_string` | Whether or not to convert the `decimal` type to a `string` type in the schema. | `false` | -| `custom_type_mappers` | A hash of custom type mappers that can be used to override the default type mappers. A specific method should be used, see annex 1.0 for more information. | `{}` | -| `disable_factory_bot` | Whether or not to disable the use of FactoryBot in the gem. To automatically generate serialized instance. See annex 1.1 for an example. | `true` | -| `use_serialized_instance` | Whether or not to use the serialized instance in the process of schema geenration as type fallback for virtual attributes. | `false` | -| `custom_defined_enum_method` | The name of the method that is used to get the enum keys and values. This allows applications with the orm `mongoid` define a method that mimicks what `defined_enums` does in `activerecord. Please see annex 1.2 for an example. | `nil` | -| `enum_prefix_for_simple_enum` | The prefix to be used for the enum values when `mongoid` is used. | `nil` | -| `enum_suffix_for_simple_enum` | The suffix to be used for the enum values when `mongoid` is used. | `nil` | -| `infer_attributes_from_custom_method` | The name of the custom method that is used to get the attributes to be generated in the schema. | `nil` | -| `infer_attributes_from_jsonapi_serializable` | Whether or not to infer the attributes from the JSONAPI::Serializable::Resource class. | `false` | +--- +| Option Name | Description | Default Value | +| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `orm` | The ORM that is used in the application. The options are `:active_record` and `:mongoid`. | `true` | +| `float_as_string` | Whether or not to convert the `float` type to a `string` type in the schema. | `false` | +| `decimal_as_string` | Whether or not to convert the `decimal` type to a `string` type in the schema. | `false` | +| `custom_type_mappers` | A hash of custom type mappers that can be used to override the default type mappers. A specific method should be used, see [Annex 1.0 - Add custom type mapper](#annex-10---add-custom-type-mapper) for more information. | `{}` | +| `use_serialized_instance` | Whether or not to use the serialized instance in the process of schema generation as type fallback for virtual attributes. See [Annex 1.1 - Use serialized instance](#annex-11---use-serialized-instance) for more information. | `false` | +| `custom_defined_enum_method` | The name of the method that is used to get the enum keys and values. This allows applications with the orm `mongoid` define a method that mimicks what `defined_enums` does in `activerecord`. Please see [Annex 1.2 - Custom defined enum method](#annex-12---custom-defined-enum-method) for an example. | `nil` | +| `enum_prefix_for_simple_enum` | The prefix to be used for the enum values when `mongoid` is used. | `nil` | +| `enum_suffix_for_simple_enum` | The suffix to be used for the enum values when `mongoid` is used. | `nil` | +| `infer_attributes_from_custom_method` | The name of the custom method that is used to get the attributes to be generated in the schema. See [Annex 1.3 - Infer attributes from custom method](#annex-13---infer-attributes-from-custom-method) for more information. | `nil` | +| `infer_attributes_from_jsonapi_serializable` | Whether or not to infer the attributes from the `JSONAPI::Serializable::Resource` class. See the previous example [Annex 1.1 - Use serialized instance](#annex-11---use-serialized-instance) for more information. | `false` | +| `custom_meta_response_schema` | A hash of custom meta response schema that can be used to override the default meta response schema. See [Annex 1.4 - Custom meta response schema](#annex-14---custom-meta-response-schema) for more information. | `nil` | +| `pagination_enabled` | Enable pagination schema generation in the `meta` section of the response schema. | `true` | +--- ### Customizing the Schema -The Schemable gem provides a number of methods that can be used to customize the schema. These methods are defined in the `Schemable` module of the gem. To customize the schema, simply override the default methods in the definition file for the model. The following is a list of the methods that can be overridden: - -| WARNING: please read the method inline documentation before overriding to avoid any unexpected behavior. | -| -------------------------------------------------------------------------------------------------------- | - -The list of methods that can be overridden are as follows: - -| Method Name | Description | -|----------------------------------|------------------------------------------------------------------------------------------------------------| -| `serializer` | Returns the serializer class. Useful when `infer_attributes_from_jsonapi_serializable` is used | -| `attributes` | Returns the attributes that are auto generated from the model's fields/columns. | -| `relationships` | Returns the relationships in the format of { belongs_to: {}, has_many: {}, addition_to_included: {} }. | -| `array_type` | Returns the type of arrays in the model that needs to be manually defined. | -| `optional_request_attributes` | Returns the attributes that are optional in the request schema. | -| `nullable_attributes` | Returns the attributes that are nullable in the request/response schema. | -| `additional_request_attributes` | Returns the attributes that are additional in the request schema. | -| `additional_response_attributes` | Returns the attributes that are additional in the response schema. | -| `additional_response_relations` | Returns the relationships that are additional in the response schema (Appended to relationships). | -| `additional_response_included` | Returns the included that are additional in the response schema (Appended to included). | -| `excluded_request_attributes` | Returns the attributes that are excluded from the request schema. | -| `excluded_response_attributes` | Returns the attributes that are excluded from the response schema. | -| `excluded_response_relations` | Returns the relationships that are excluded from the response schema. | -| `excluded_response_included` | (not implemented yet) Returns the included that are excluded from the response schema. | -| `serialized_instance` | Returns a serialized instance of the model, used for type generating as a fallback. | -| `model` | Returns the model class (Constantized from the definition class name). | -| `model_name` | Returns the model name. Used for schema type naming. | -| `definitions` | Returns the generated schemas in JSONAPI format (It is recommended to override this method to your liking) | - -The following is an example of a definition file for a model that has been customized: - -
-Click to view the example +The Schemable gem provides a number of methods that can be used to customize the schema. These methods are defined in the `Schemable::Definition` class of the gem. To customize the schema for a specific model, simply override the default methods in the `Schemable::Definition` class for the model. + +Please read the method inline documentation before overriding to avoid any unexpected behavior. + +The following is a list of the methods that can be overridden. (See the example in [Annex 1.5 - Highly Customized Definition](#annex-15---highly-customized-definition) for a highly customized definition file.) + +---- + +| Method Name | Description | +|---------------------------------------|------------------------------------------------------------------------------------------------------------| +| `serializer` | Returns the serializer of the model for the definition. | +| `attributes` | Returns the attributes for the definition based on the configuration. | +| `relationships` | Returns the relationships defined in the model. | +| `array_types` | Returns a hash of all the arrays defined for the model. | +| `optional_create_request_attributes` | Returns the attributes that are not required in the create request. | +| `optional_update_request_attributes` | Returns the attributes that are not required in the update request. | +| `nullable_attributes` | Returns the attributes that are nullable in the request/response body. | +| `additional_create_request_attributes`| Returns the additional create request attributes that are not automatically generated. | +| `additional_update_request_attributes`| Returns the additional update request attributes that are not automatically generated. | +| `additional_response_attributes` | Returns the additional response attributes that are not automatically generated. | +| `additional_response_relations` | Returns the additional response relations that are not automatically generated. | +| `additional_response_included` | Returns the additional response included that are not automatically generated. | +| `excluded_create_request_attributes` | Returns the attributes that are excluded from the create request schema. | +| `excluded_update_request_attributes` | Returns the attributes that are excluded from the update request schema. | +| `excluded_response_attributes` | Returns the attributes that are excluded from the response schema. | +| `excluded_response_relations` | Returns the relationships that are excluded from the response schema. | +| `excluded_response_included` | Returns the included that are excluded from the response schema. | +| `serialized_instance` | Returns an instance of the model class that is already serialized into jsonapi format. | +| `model` | Returns the model class (Constantized from the definition class name). | +| `model_name` | Returns the model name. Used for schema type naming. | +| `camelize_keys` | Given a hash, it returns a new hash with all the keys camelized. | +| `generate` | Returns the schema for the create request body, update request body, and response body. | +---- + +## Examples + +The followings are some examples of configuration of the gem to have different behaviors based on the application needs. In the above section, we have already seen how to generate the definition files for the models. The following examples will show how to customize the schema for the models. Also, we will see how to use the generated schema in RSwag to generate the swagger documentation for the API. + + +### Annex 1.0 - Add custom type mapper + +```ruby +# config/initializers/schemable.rb + +Schemable.configure do |config| + config.add_custom_type_mapper( + :string, + { type: :text } + ) +end + +``` + +### Annex 1.1 - Use serialized instance ```ruby -module Swagger - module Definitions - class UserApplication +# config/initializers/schemable.rb + +Schemable.configure do |config| + config.use_serialized_instance = true +end +``` + +Then in the definition file, you can override the `serialized_instance` method to return the serialized instance of the model. Let's also assume that we want to use the `JSONAPI::Serializable::Resource` class to serialize the instance. - include Schemable - include SerializersHelper +```ruby +# lib/swagger/definitions/user_application.rb +module Swagger + module Definitions + class User < Schemable::Definition attr_accessor :instance - def initialize - @instance ||= JSONAPI::Serializable::Renderer.new.render(FactoryBot.create(:user, :with_user_application_applicants), class: serializers_map, include: []) + def serializer + V1::UserSerializer end - def serializer - V1::UserApplicationSerializer + def serialized_instance + @instance ||= JSONAPI::Serializable::Renderer.new.render( + FactoryBot.create(:user), + class: { User: serializer }, + include: [] + ) + end + end + end +end +``` + +### Annex 1.2 - Custom defined enum method + +Let's assume that we also want to use mongoid in our application. In this case, we need to define a method that mimicks what `defined_enums` does in `activerecord`. Let's assume that we have a model called `User` that has an enum field called `status`. The following is an example of how to define the method: + +```ruby +# app/models/user.rb + +class User < ApplicationModel + include Mongoid::Document + include SimpleEnum::Mongoid + + as_enum :status, active: 0, inactive: 1 +end +``` + +Then in the `ApplicationModel` class, we can define the method as follows: + +```ruby +# app/models/application_model.rb + +def self.custom_defined_enum(suffix: '_cd', prefix: nil) + defined_enums = {} + enum_fields = if prefix + fields.select { |k, v| k.to_s.start_with?(prefix) } + else + fields.select { |k, v| k.to_s.end_with?(suffix) } + end + + enum_fields.each do |k, v| + enum_name = k.to_s.gsub(prefix || suffix, '') + enum = send(enum_name.pluralize) + + defined_enums[enum_name] = enum.hash + end + + defined_enums +end +``` +This method will work for all the models that have enum fields. Since Simple Enum gem defines enum fields with the suffix `_cd`, we can use the `suffix` option to get the enum fields. However, if the enum fields are defined with a different suffix, we can use the `prefix` option to get the enum fields. + +Now, we need to specify theses options in the configuration file: + +```ruby +# config/initializers/schemable.rb + +Schemable.configure do |config| + config.custom_defined_enum_method = :custom_defined_enum + config.enum_suffix_for_simple_enum = '_cd' +end +``` + +This will generate the schema for the enum fields in the model as follows: + +```ruby +{ + # ... + status: { + type: string, + enum: ['active', 'inactive'] + } + # ... +} +``` + +### Annex 1.3 - Infer attributes from custom method + +Sometimes, we may want to infer the attributes from a custom method. For example, let's assume that we have a model called `User` that has a method called `base_attributes` that returns an array of attributes. The following is an example of how to define the method: + +```ruby +# app/models/user.rb + +class User < ApplicationModel + def self.base_attributes + %i[ + id + name + email + status + roles + created_at + updated_at + ] + end +end +``` + +Then in the configuration file, we can override the `infer_attributes_from_custom_method` method to return the attributes from the custom method: + +```ruby +# config/initializers/schemable.rb + +Schemable.configure do |config| + config.infer_attributes_from_custom_method = :base_attributes +end +``` + +if we want to use the `base_attributes` method for only the User model, we can override the `attributes` method in the `Schemable::Definition` class as follows: + +```ruby +# lib/swagger/definitions/user.rb + +module Swagger + module Definitions + class User < Schemable::Definition + def attributes + model.base_attributes end + end + end +end +``` + +### Annex 1.4 - Custom meta response schema + +Sometimes, we may want to customize the meta response schema. For example, let's assume that we want to add a `total` attribute to the meta response schema. The following is an example of how to do that: +```ruby +# config/initializers/schemable.rb + +Schemable.configure do |config| + config.custom_meta_response_schema = { + type: :object, + properties: { + total: { type: :integer } + } + } +end +``` + +### Annex 1.5 - Highly Customized Definition + +The below is a definition file for a model that has been customized in a way that many of the methods have been overridden. This is just an example of how to customize the schema. You can customize the schema to your liking. The below hypothetical model's logic does not matter. The only thing that matters is the schema customization and being familiar with the methods that can be overridden. + + +
+ Click to expand + +```ruby +module Swagger + module Definitions + class Order < Schemable::Definition def relationships - { + @relationships ||= { belongs_to: { - category: Swagger::Definitions::Category, + address: Swagger::Definitions::Address.new }, has_many: { - applicants: Swagger::Definitions::Applicant, + items: Swagger::Definitions::Item.new + }, + addition_to_included: { + store: Swagger::Definitions::Store.new, + attachments: Swagger::Definitions::Upload.new } } end - def array_types - { - applicant_ids: + def excluded_create_request_attributes + create_params = model.create_params.select { |item| item.is_a?(Symbol) } + model.base_attributes + %i[comment applicable_transitions] - create_params + end + + def excluded_update_request_attributes + update_params = model.update_params.select { |item| item.is_a?(Symbol) } + model.base_attributes + %i[comment applicable_transitions] - update_params + end + + def additional_create_request_attributes + @additional_create_request_attributes ||= { + items_attributes: { type: :array, - items: - { - type: :string - }, - nullable: true + items: { + anyOf: [ + { + type: :object, + properties: Schemable::RequestSchemaGenerator.new(Swagger::Definitions::Item.new).generate_for_create.as_json['properties']['data']['properties'] + } + ] + } } } end - def excluded_request_attributes - %i[id updated_at created_at applicant_ids comment] - end - - def additional_request_attributes - { - applicants_attributes: + def additional_update_request_attributes + @additional_update_request_attributes ||={ + items_attributes: { type: :array, items: { anyOf: [ { type: :object, - properties: Swagger::Definitions::Applicant.new.request_schema.as_json['properties']['data']['properties'] + properties:Schemable::RequestSchemaGenerator.new(Swagger::Definitions::Item.new).generate_for_update.as_json['properties']['data']['properties'] } ] } @@ -164,49 +361,58 @@ module Swagger def additional_response_attributes { - comment: { type: :object, properties: {}, nullable: true } + comment: { type: :object, properties: {}, nullable: true }, + item_status: { type: :string, nullable: true }, + applicable_transitions: { + type: :array, + items: + { + type: :object, nullable: true, + properties: + { + name: { type: :string, nullable: true }, + metadata: { type: :object, nullable: true } + } + }, + nullable: true + } } end - def nested_relationships - { - applicants: { - belongs_to: { - district: Swagger::Definitions::District, - province: Swagger::Definitions::Province, - }, - has_many: { - attachments: Swagger::Definitions::Upload, - } - } - } + def nullable_attributes + %i[contact_number email] + end + + def optional_create_request_attributes + %i[contact_number email] + end + + def optional_update_request_attributes + %i[contact_number email] end - def self.definitions - schema_instance = self.new + def self.generate + schema_instance = new + [ - "#{schema_instance.model}Request": schema_instance.camelize_keys(schema_instance.request_schema), - "#{schema_instance.model}Response": schema_instance.camelize_keys(schema_instance.response_schema(expand: true, exclude_from_expansion: [:category], multi: true)), - "#{schema_instance.model}ResponseExpanded": schema_instance.camelize_keys(schema_instance.response_schema(expand: true, nested: true)) + "#{schema_instance.model}CreateRequest": schema_instance.camelize_keys(Schemable::RequestSchemaGenerator.new(schema_instance).generate_for_create), + "#{schema_instance.model}UpdateRequest": schema_instance.camelize_keys(Schemable::RequestSchemaGenerator.new(schema_instance).generate_for_update), + "#{schema_instance.model}Response": schema_instance.camelize_keys(Schemable::ResponseSchemaGenerator.new(schema_instance).generate(expand: true, collection: true, relationships_to_exclude_from_expansion: %w[addresses stores attachments])), + "#{schema_instance.model}ResponseExpanded": schema_instance.camelize_keys(Schemable::ResponseSchemaGenerator.new(schema_instance).generate(expand: true)) ] end end end end - ``` - -
- -## Development - -After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. - -To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). +
+ ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/schemable. This project is intended to be a safe, welcoming space for collaboration, and contributors. Please go to issues page to report any bugs or feature requests. Open issues are tagged with the `help wanted` label. If you would like to contribute, please fork the repository and submit a pull request. +Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/schemable. This project is intended to be a safe, welcoming space for collaboration, and contributors. Please go to issues page to report any bugs or feature requests. If you would like to contribute, please fork the repository and submit a pull request. + +To, use the gem locally, clone the repository and run `bundle install` to install dependencies. Then, run `bundle exec rspec` to run the tests. ## License From 5b32cb2b95eb299ce8c8d80dbf39137d70508b61 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Mon, 29 Jan 2024 10:53:20 +0300 Subject: [PATCH 72/87] Adds `default_value_for_enum_attributes` method to Schemable::Definition --- lib/schemable/definition.rb | 13 +++++++++++++ sig/schemable/definition.rbs | 1 + 2 files changed, 14 insertions(+) diff --git a/lib/schemable/definition.rb b/lib/schemable/definition.rb index 0768f9f..9fb2773 100644 --- a/lib/schemable/definition.rb +++ b/lib/schemable/definition.rb @@ -265,6 +265,19 @@ def excluded_response_included %i[] end + # Returns the default value for the enum attributes. + # + # @example + # { + # status: 'pending', + # flag: 0 + # } + # + # @return [Hash] The custom default values for the enum attributes. + def default_value_for_enum_attributes + {} + end + # Returns an instance of the model class that is already serialized into jsonapi format. # # @return [Hash] The serialized instance of the model class. diff --git a/sig/schemable/definition.rbs b/sig/schemable/definition.rbs index 18db060..64ac8bd 100644 --- a/sig/schemable/definition.rbs +++ b/sig/schemable/definition.rbs @@ -25,6 +25,7 @@ module Schemable def excluded_update_request_attributes: -> Array[Symbol] def optional_create_request_attributes: -> Array[Symbol] def optional_update_request_attributes: -> Array[Symbol] + def default_value_for_enum_attributes: -> Hash[Symbol, any] def additional_create_request_attributes: -> Hash[Symbol, any] def additional_update_request_attributes: -> Hash[Symbol, any] def camelize_keys: (Hash[Symbol, any]) -> (Array[Hash[Symbol, any]] | Hash[Symbol, any]) From d00737fc6fb0760e73448932a4bb80f12d163069 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Mon, 29 Jan 2024 10:53:37 +0300 Subject: [PATCH 73/87] Update gem versions --- Gemfile | 6 +- Gemfile.lock | 168 --------------------------------------------------- 2 files changed, 3 insertions(+), 171 deletions(-) diff --git a/Gemfile b/Gemfile index 1531efa..69bbf95 100644 --- a/Gemfile +++ b/Gemfile @@ -6,10 +6,10 @@ gemspec gem 'rake', '~> 13.1.0' gem 'rspec', '~> 3.12.0' -gem 'rubocop', '~> 1.57.2' -gem 'rubocop-rails', '~> 2.22.1' +gem 'rubocop', '~> 1.60.2' +gem 'rubocop-rails', '~> 2.23.1' group :development, :test do - gem 'factory_bot_rails', '~> 6.2.0' + gem 'factory_bot_rails', '~> 6.4.3' gem 'jsonapi-rails', '~> 0.4.1' end diff --git a/Gemfile.lock b/Gemfile.lock index acbf803..e69de29 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,168 +0,0 @@ -PATH - remote: . - specs: - schemable (1.0.0) - -GEM - remote: https://rubygems.org/ - specs: - actionpack (7.1.2) - actionview (= 7.1.2) - activesupport (= 7.1.2) - nokogiri (>= 1.8.5) - racc - rack (>= 2.2.4) - rack-session (>= 1.0.1) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - actionview (7.1.2) - activesupport (= 7.1.2) - builder (~> 3.1) - erubi (~> 1.11) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - activesupport (7.1.2) - base64 - bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - minitest (>= 5.1) - mutex_m - tzinfo (~> 2.0) - ast (2.4.2) - base64 (0.2.0) - bigdecimal (3.1.4) - builder (3.2.4) - concurrent-ruby (1.2.2) - connection_pool (2.4.1) - crass (1.0.6) - diff-lcs (1.5.0) - drb (2.2.0) - ruby2_keywords - erubi (1.12.0) - factory_bot (6.2.1) - activesupport (>= 5.0.0) - factory_bot_rails (6.2.0) - factory_bot (~> 6.2.0) - railties (>= 5.0.0) - i18n (1.14.1) - concurrent-ruby (~> 1.0) - io-console (0.6.0) - irb (1.9.0) - rdoc - reline (>= 0.3.8) - json (2.6.3) - jsonapi-deserializable (0.2.0) - jsonapi-parser (0.1.1) - jsonapi-rails (0.4.1) - jsonapi-parser (~> 0.1.0) - jsonapi-rb (~> 0.5.0) - jsonapi-rb (0.5.0) - jsonapi-deserializable (~> 0.2.0) - jsonapi-serializable (~> 0.3.0) - jsonapi-renderer (0.2.2) - jsonapi-serializable (0.3.1) - jsonapi-renderer (~> 0.2.0) - language_server-protocol (3.17.0.3) - loofah (2.22.0) - crass (~> 1.0.2) - nokogiri (>= 1.12.0) - minitest (5.20.0) - mutex_m (0.2.0) - nokogiri (1.15.4-x86_64-linux) - racc (~> 1.4) - parallel (1.23.0) - parser (3.2.2.4) - ast (~> 2.4.1) - racc - psych (5.1.1.1) - stringio - racc (1.7.3) - rack (3.0.8) - rack-session (2.0.0) - rack (>= 3.0.0) - rack-test (2.1.0) - rack (>= 1.3) - rackup (2.1.0) - rack (>= 3) - webrick (~> 1.8) - rails-dom-testing (2.2.0) - activesupport (>= 5.0.0) - minitest - nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) - loofah (~> 2.21) - nokogiri (~> 1.14) - railties (7.1.2) - actionpack (= 7.1.2) - activesupport (= 7.1.2) - irb - rackup (>= 1.0.0) - rake (>= 12.2) - thor (~> 1.0, >= 1.2.2) - zeitwerk (~> 2.6) - rainbow (3.1.1) - rake (13.1.0) - rdoc (6.6.0) - psych (>= 4.0.0) - regexp_parser (2.8.2) - reline (0.4.0) - io-console (~> 0.5) - rexml (3.2.6) - rspec (3.12.0) - rspec-core (~> 3.12.0) - rspec-expectations (~> 3.12.0) - rspec-mocks (~> 3.12.0) - rspec-core (3.12.2) - rspec-support (~> 3.12.0) - rspec-expectations (3.12.3) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-mocks (3.12.6) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-support (3.12.1) - rubocop (1.57.2) - json (~> 2.3) - language_server-protocol (>= 3.17.0) - parallel (~> 1.10) - parser (>= 3.2.2.4) - rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.1, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.30.0) - parser (>= 3.2.1.0) - rubocop-rails (2.22.1) - activesupport (>= 4.2.0) - rack (>= 1.1) - rubocop (>= 1.33.0, < 2.0) - ruby-progressbar (1.13.0) - ruby2_keywords (0.0.5) - stringio (3.0.9) - thor (1.3.0) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - unicode-display_width (2.5.0) - webrick (1.8.1) - zeitwerk (2.6.12) - -PLATFORMS - x86_64-linux - -DEPENDENCIES - factory_bot_rails (~> 6.2.0) - jsonapi-rails (~> 0.4.1) - rake (~> 13.1.0) - rspec (~> 3.12.0) - rubocop (~> 1.57.2) - rubocop-rails (~> 2.22.1) - schemable! - -BUNDLED WITH - 2.4.17 From e1a9a0d258291adcfd32d86d720ba7fe054197da Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Mon, 29 Jan 2024 10:54:01 +0300 Subject: [PATCH 74/87] Refactor enum attribute handling in attribute_schema_generator to set default enum value --- lib/schemable/attribute_schema_generator.rb | 22 ++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/schemable/attribute_schema_generator.rb b/lib/schemable/attribute_schema_generator.rb index 27a149d..1722613 100644 --- a/lib/schemable/attribute_schema_generator.rb +++ b/lib/schemable/attribute_schema_generator.rb @@ -101,9 +101,25 @@ def generate_attribute_schema(attribute) if @configuration.custom_defined_enum_method && @model.respond_to?(@configuration.custom_defined_enum_method) defined_enums = @model.send(@configuration.custom_defined_enum_method) enum_attribute = attribute.to_s.gsub(@configuration.enum_prefix_for_simple_enum || @configuration.enum_suffix_for_simple_enum, '').to_s - return @schema_modifier.add_properties(@response, { enum: defined_enums[enum_attribute].keys }, '.') if @response && defined_enums[enum_attribute].present? - elsif @model.respond_to?(:defined_enums) - return @schema_modifier.add_properties(@response, { enum: @model.defined_enums[attribute.to_s].keys }, '.') if @response && @model.defined_enums.key?(attribute.to_s) + if @response && defined_enums[enum_attribute].present? + return @schema_modifier.add_properties( + @response, + { + enum: defined_enums[enum_attribute].keys, + default: @model_definition.default_value_for_enum_attributes[attribute.to_sym] || defined_enums[enum_attribute].keys.first + }, + '.' + ) + end + elsif @model.respond_to?(:defined_enums) && @response && @model.defined_enums.key?(attribute.to_s) + return @schema_modifier.add_properties( + @response, + { + enum: @model.defined_enums[attribute.to_s].keys, + default: @model_definition.default_value_for_enum_attributes[attribute.to_sym] || @model.defined_enums[attribute.to_s].keys.first + }, + '.' + ) end return @response unless @response.nil? From e95a1432c1e9a3d9b36e6a34935bc64fdb720198 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Mon, 29 Jan 2024 10:55:07 +0300 Subject: [PATCH 75/87] Adds documentations for `default_value_for_enum_attributes` method --- README.md | 100 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 51 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index e5f91a2..dd0a53a 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,11 @@ gem 'schemable' And then execute: - $ bundle install + bundle install Or install it yourself as: - $ gem install schemable + gem install schemable ## Usage @@ -32,7 +32,6 @@ rails g schemable:install This will generate `schemable.rb` in your `config/initializers` directory. This file will contain the configuration for the Schemable gem. You can modify the configuration to your liking. For more information on the configuration options, see the [Configuration](#configuration) section below. - ### Generating Definition Files The Schemable gem provides a generator that can be used to generate definition files for your models. To generate a definition file for a model, run the following command: @@ -43,74 +42,75 @@ rails g schemable:model --model_name This will generate a definition file for the specified model in the `lib/swagger/definitions` directory. The definition file will be named `.rb`. This file will have the bare minimum code required to generate a schema for the model. You can then modify the definition file to your liking by overriding the default methods. For example, you can add or remove attributes from the schema, or you can add or remove relationships from the schema. You can also add custom attributes to the schema. For more information on how to customize the schema, see the [Customizing the Schema](#customizing-the-schema) section below. - ### Configuration + The Schemable gem provides a number of configuration options that can be used to customize the behavior of the gem. The following is a list of the configuration options that are available. Please note that the configurations options below are defined in the `Schemable` module of the gem. To configure the gem, simply override the default values in the `config/initializers/schemable.rb` file. Also the changes will affect all the definition classes globally. --- -| Option Name | Description | Default Value | -| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| `orm` | The ORM that is used in the application. The options are `:active_record` and `:mongoid`. | `true` | -| `float_as_string` | Whether or not to convert the `float` type to a `string` type in the schema. | `false` | -| `decimal_as_string` | Whether or not to convert the `decimal` type to a `string` type in the schema. | `false` | -| `custom_type_mappers` | A hash of custom type mappers that can be used to override the default type mappers. A specific method should be used, see [Annex 1.0 - Add custom type mapper](#annex-10---add-custom-type-mapper) for more information. | `{}` | -| `use_serialized_instance` | Whether or not to use the serialized instance in the process of schema generation as type fallback for virtual attributes. See [Annex 1.1 - Use serialized instance](#annex-11---use-serialized-instance) for more information. | `false` | -| `custom_defined_enum_method` | The name of the method that is used to get the enum keys and values. This allows applications with the orm `mongoid` define a method that mimicks what `defined_enums` does in `activerecord`. Please see [Annex 1.2 - Custom defined enum method](#annex-12---custom-defined-enum-method) for an example. | `nil` | -| `enum_prefix_for_simple_enum` | The prefix to be used for the enum values when `mongoid` is used. | `nil` | -| `enum_suffix_for_simple_enum` | The suffix to be used for the enum values when `mongoid` is used. | `nil` | -| `infer_attributes_from_custom_method` | The name of the custom method that is used to get the attributes to be generated in the schema. See [Annex 1.3 - Infer attributes from custom method](#annex-13---infer-attributes-from-custom-method) for more information. | `nil` | -| `infer_attributes_from_jsonapi_serializable` | Whether or not to infer the attributes from the `JSONAPI::Serializable::Resource` class. See the previous example [Annex 1.1 - Use serialized instance](#annex-11---use-serialized-instance) for more information. | `false` | -| `custom_meta_response_schema` | A hash of custom meta response schema that can be used to override the default meta response schema. See [Annex 1.4 - Custom meta response schema](#annex-14---custom-meta-response-schema) for more information. | `nil` | -| `pagination_enabled` | Enable pagination schema generation in the `meta` section of the response schema. | `true` | +| Option Name | Description | Default Value | +| -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `orm` | The ORM that is used in the application. The options are `:active_record` and `:mongoid`. | `true` | +| `float_as_string` | Whether or not to convert the `float` type to a `string` type in the schema. | `false` | +| `decimal_as_string` | Whether or not to convert the `decimal` type to a `string` type in the schema. | `false` | +| `custom_type_mappers` | A hash of custom type mappers that can be used to override the default type mappers. A specific method should be used, see [Annex 1.0 - Add custom type mapper](#annex-10---add-custom-type-mapper) for more information. | `{}` | +| `use_serialized_instance` | Whether or not to use the serialized instance in the process of schema generation as type fallback for virtual attributes. See [Annex 1.1 - Use serialized instance](#annex-11---use-serialized-instance) for more information. | `false` | +| `custom_defined_enum_method` | The name of the method that is used to get the enum keys and values. This allows applications with the orm `mongoid` define a method that mimicks what `defined_enums` does in `activerecord`. Please see [Annex 1.2 - Custom defined enum method](#annex-12---custom-defined-enum-method) for an example. | `nil` | +| `enum_prefix_for_simple_enum` | The prefix to be used for the enum values when `mongoid` is used. | `nil` | +| `enum_suffix_for_simple_enum` | The suffix to be used for the enum values when `mongoid` is used. | `nil` | +| `infer_attributes_from_custom_method` | The name of the custom method that is used to get the attributes to be generated in the schema. See [Annex 1.3 - Infer attributes from custom method](#annex-13---infer-attributes-from-custom-method) for more information. | `nil` | +| `infer_attributes_from_jsonapi_serializable` | Whether or not to infer the attributes from the `JSONAPI::Serializable::Resource` class. See the previous example [Annex 1.1 - Use serialized instance](#annex-11---use-serialized-instance) for more information. | `false` | +| `custom_meta_response_schema` | A hash of custom meta response schema that can be used to override the default meta response schema. See [Annex 1.4 - Custom meta response schema](#annex-14---custom-meta-response-schema) for more information. | `nil` | +| `pagination_enabled` | Enable pagination schema generation in the `meta` section of the response schema. | `true` | --- ### Customizing the Schema -The Schemable gem provides a number of methods that can be used to customize the schema. These methods are defined in the `Schemable::Definition` class of the gem. To customize the schema for a specific model, simply override the default methods in the `Schemable::Definition` class for the model. +The Schemable gem provides a number of methods that can be used to customize the schema. These methods are defined in the `Schemable::Definition` class of the gem. To customize the schema for a specific model, simply override the default methods in the `Schemable::Definition` class for the model. Please read the method inline documentation before overriding to avoid any unexpected behavior. The following is a list of the methods that can be overridden. (See the example in [Annex 1.5 - Highly Customized Definition](#annex-15---highly-customized-definition) for a highly customized definition file.) ----- - -| Method Name | Description | -|---------------------------------------|------------------------------------------------------------------------------------------------------------| -| `serializer` | Returns the serializer of the model for the definition. | -| `attributes` | Returns the attributes for the definition based on the configuration. | -| `relationships` | Returns the relationships defined in the model. | -| `array_types` | Returns a hash of all the arrays defined for the model. | -| `optional_create_request_attributes` | Returns the attributes that are not required in the create request. | -| `optional_update_request_attributes` | Returns the attributes that are not required in the update request. | -| `nullable_attributes` | Returns the attributes that are nullable in the request/response body. | -| `additional_create_request_attributes`| Returns the additional create request attributes that are not automatically generated. | -| `additional_update_request_attributes`| Returns the additional update request attributes that are not automatically generated. | -| `additional_response_attributes` | Returns the additional response attributes that are not automatically generated. | -| `additional_response_relations` | Returns the additional response relations that are not automatically generated. | -| `additional_response_included` | Returns the additional response included that are not automatically generated. | -| `excluded_create_request_attributes` | Returns the attributes that are excluded from the create request schema. | -| `excluded_update_request_attributes` | Returns the attributes that are excluded from the update request schema. | -| `excluded_response_attributes` | Returns the attributes that are excluded from the response schema. | -| `excluded_response_relations` | Returns the relationships that are excluded from the response schema. | -| `excluded_response_included` | Returns the included that are excluded from the response schema. | -| `serialized_instance` | Returns an instance of the model class that is already serialized into jsonapi format. | -| `model` | Returns the model class (Constantized from the definition class name). | -| `model_name` | Returns the model name. Used for schema type naming. | -| `camelize_keys` | Given a hash, it returns a new hash with all the keys camelized. | -| `generate` | Returns the schema for the create request body, update request body, and response body. | ----- +--- + +| Method Name | Description | +| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `serializer` | Returns the serializer of the model for the definition. | +| `attributes` | Returns the attributes for the definition based on the configuration. | +| `relationships` | Returns the relationships defined in the model. | +| `array_types` | Returns a hash of all the arrays defined for the model. | +| `optional_create_request_attributes` | Returns the attributes that are not required in the create request. | +| `optional_update_request_attributes` | Returns the attributes that are not required in the update request. | +| `nullable_attributes` | Returns the attributes that are nullable in the request/response body. | +| `additional_create_request_attributes` | Returns the additional create request attributes that are not automatically generated. | +| `additional_update_request_attributes` | Returns the additional update request attributes that are not automatically generated. | +| `additional_response_attributes` | Returns the additional response attributes that are not automatically generated. | +| `additional_response_relations` | Returns the additional response relations that are not automatically generated. | +| `additional_response_included` | Returns the additional response included that are not automatically generated. | +| `excluded_create_request_attributes` | Returns the attributes that are excluded from the create request schema. | +| `excluded_update_request_attributes` | Returns the attributes that are excluded from the update request schema. | +| `default_value_for_enum_attributes` | Returns the default value for the enum attributes. Used when you want a custom value for the default enum. By default the first key is used as default | +| `excluded_response_attributes` | Returns the attributes that are excluded from the response schema. | +| `excluded_response_relations` | Returns the relationships that are excluded from the response schema. | +| `excluded_response_included` | Returns the included that are excluded from the response schema. | +| `serialized_instance` | Returns an instance of the model class that is already serialized into jsonapi format. | +| `model` | Returns the model class (Constantized from the definition class name). | +| `model_name` | Returns the model name. Used for schema type naming. | +| `camelize_keys` | Given a hash, it returns a new hash with all the keys camelized. | +| `generate` | Returns the schema for the create request body, update request body, and response body. | + +--- ## Examples The followings are some examples of configuration of the gem to have different behaviors based on the application needs. In the above section, we have already seen how to generate the definition files for the models. The following examples will show how to customize the schema for the models. Also, we will see how to use the generated schema in RSwag to generate the swagger documentation for the API. - ### Annex 1.0 - Add custom type mapper - + ```ruby # config/initializers/schemable.rb @@ -197,6 +197,7 @@ def self.custom_defined_enum(suffix: '_cd', prefix: nil) defined_enums end ``` + This method will work for all the models that have enum fields. Since Simple Enum gem defines enum fields with the suffix `_cd`, we can use the `suffix` option to get the enum fields. However, if the enum fields are defined with a different suffix, we can use the `prefix` option to get the enum fields. Now, we need to specify theses options in the configuration file: @@ -405,12 +406,13 @@ module Swagger end end ``` + ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/schemable. This project is intended to be a safe, welcoming space for collaboration, and contributors. Please go to issues page to report any bugs or feature requests. If you would like to contribute, please fork the repository and submit a pull request. +Bug reports and pull requests are welcome on GitHub at . This project is intended to be a safe, welcoming space for collaboration, and contributors. Please go to issues page to report any bugs or feature requests. If you would like to contribute, please fork the repository and submit a pull request. To, use the gem locally, clone the repository and run `bundle install` to install dependencies. Then, run `bundle exec rspec` to run the tests. From 72bf6606da667907d5b7164f65739858fa71c89d Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Mon, 29 Jan 2024 10:55:17 +0300 Subject: [PATCH 76/87] Update Schemable version to 1.0.1 --- lib/schemable/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/schemable/version.rb b/lib/schemable/version.rb index 104ff67..468b85c 100644 --- a/lib/schemable/version.rb +++ b/lib/schemable/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Schemable - VERSION = '1.0.0' + VERSION = '1.0.1' end From a2fb1cf57e72e65332d0029615161fb75e36bf47 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Mon, 29 Jan 2024 10:56:28 +0300 Subject: [PATCH 77/87] Updates changelog for `v1.0.1` --- CHANGELOG.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2027bad..665ef73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ -## [Unreleased] +# Changelog +This file is used to list changes made in each version of the Schemable gem. -## [0.1.0] - 2023-05-10 +## Schemable 1.0.1 (2024-01-29) -- Initial release +* Added configuration for changing the default value of enums. By default first key is used, or alternatively default can be set manually by the method `default_value_for_enum_attributes` from the definition. + +## Schemable 1.0.0 (2023-11-17) + +* Initial release From 213a265dddaaa2e4f6be7b3f99083b8b9e693f53 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Tue, 30 Jan 2024 13:15:16 +0300 Subject: [PATCH 78/87] Add nullable_relationships method to Schemable::Definition --- lib/schemable/definition.rb | 12 ++++++++++++ sig/schemable/definition.rbs | 1 + 2 files changed, 13 insertions(+) diff --git a/lib/schemable/definition.rb b/lib/schemable/definition.rb index 9fb2773..f25d1b1 100644 --- a/lib/schemable/definition.rb +++ b/lib/schemable/definition.rb @@ -125,6 +125,18 @@ def nullable_attributes %i[] end + # Returns the relationships that are nullable in the response body. + # This means that they can be present in the response body but they can be null. + # They are not required to be present in the request body. + # + # @example + # ['users', 'applicant'] + # + # @return [Array] The attributes that are nullable in the response body. + def nullable_relationships + %w[] + end + # Returns the additional create request attributes that are not automatically generated. # These attributes are appended to the create request schema. # diff --git a/sig/schemable/definition.rbs b/sig/schemable/definition.rbs index 64ac8bd..a0b264d 100644 --- a/sig/schemable/definition.rbs +++ b/sig/schemable/definition.rbs @@ -13,6 +13,7 @@ module Schemable def array_types: -> Hash[Symbol, any] def relationships: -> Hash[Symbol, any] def nullable_attributes: -> Array[Symbol] + def nullable_relationships: -> Array[String] def serialized_instance: -> Hash[Symbol, any] def self.generate: -> Array[Hash[Symbol, any]] def excluded_response_included: -> Array[Symbol] From dbfb9c1dd7cab9c9b4bddc1786ca9518c7f93e83 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Tue, 30 Jan 2024 13:15:40 +0300 Subject: [PATCH 79/87] Refactor generate_schema method to handle nullable relationships --- .../relationship_schema_generator.rb | 67 ++++++++++--------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/lib/schemable/relationship_schema_generator.rb b/lib/schemable/relationship_schema_generator.rb index 54fa65d..fa5861a 100644 --- a/lib/schemable/relationship_schema_generator.rb +++ b/lib/schemable/relationship_schema_generator.rb @@ -84,36 +84,43 @@ def generate(relationships_to_exclude_from_expansion: [], expand: false) # # @return [Hash] The generated schema for the relationship. def generate_schema(type_name, collection: false) - if collection - { - type: :object, - properties: { - data: { - type: :array, - items: { - type: :object, - properties: { - id: { type: :string }, - type: { type: :string, default: type_name } - } - } - } - } - } - else - { - type: :object, - properties: { - data: { - type: :object, - properties: { - id: { type: :string }, - type: { type: :string, default: type_name } - } - } - } - } - end + schema = if collection + { + type: :object, + properties: { + data: { + type: :array, + items: { + type: :object, + properties: { + id: { type: :string }, + type: { type: :string, default: type_name } + } + } + } + } + } + else + { + type: :object, + properties: { + data: { + type: :object, + properties: { + id: { type: :string }, + type: { type: :string, default: type_name } + } + } + } + } + end + + # Modify the schema to nullable if the relationship is in nullable + is_relation_nullable = @model_definition.nullable_relationships.include?(type_name) + + return schema unless is_relation_nullable + + @schema_modifier.add_properties(schema, { nullable: true }, 'properties.data') end end end From 51d1113ff5a976b3812675afc7a491e9f12a77a9 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Tue, 30 Jan 2024 13:15:54 +0300 Subject: [PATCH 80/87] Adds docs for nullable_relationships method --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index dd0a53a..113ca00 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ The following is a list of the methods that can be overridden. (See the example | `optional_create_request_attributes` | Returns the attributes that are not required in the create request. | | `optional_update_request_attributes` | Returns the attributes that are not required in the update request. | | `nullable_attributes` | Returns the attributes that are nullable in the request/response body. | +| `nullable_relationships` | Returns the relationships that are nullable in the response body. | | `additional_create_request_attributes` | Returns the additional create request attributes that are not automatically generated. | | `additional_update_request_attributes` | Returns the additional update request attributes that are not automatically generated. | | `additional_response_attributes` | Returns the additional response attributes that are not automatically generated. | From e894e4621664dfd5a7d427d92a1be237a3427466 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Tue, 30 Jan 2024 13:16:32 +0300 Subject: [PATCH 81/87] Updates Changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 665ef73..87ed805 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog This file is used to list changes made in each version of the Schemable gem. +## Schemable 1.0.2 (2024-01-30) + +* Added configuration for making certain associations nullable in the response's relationship. This can be done by adding the name of the relation in the `nullable_relationships` method's array of strings. + ## Schemable 1.0.1 (2024-01-29) * Added configuration for changing the default value of enums. By default first key is used, or alternatively default can be set manually by the method `default_value_for_enum_attributes` from the definition. From 1afb31bccb99faf722aa75797b91a3efa8a3b74a Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Tue, 30 Jan 2024 13:17:17 +0300 Subject: [PATCH 82/87] Update Schemable gem version to 1.0.2 --- lib/schemable/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/schemable/version.rb b/lib/schemable/version.rb index 468b85c..bcfcf07 100644 --- a/lib/schemable/version.rb +++ b/lib/schemable/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Schemable - VERSION = '1.0.1' + VERSION = '1.0.2' end From 49992ff75b927b0e5eaf84f16482900718990452 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Tue, 30 Jan 2024 16:29:07 +0300 Subject: [PATCH 83/87] Add expand_nested parameter to generate method --- lib/schemable/response_schema_generator.rb | 10 +++++++--- sig/schemable/response_schema_generator.rbs | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/schemable/response_schema_generator.rb b/lib/schemable/response_schema_generator.rb index 0f716ee..1b5895f 100644 --- a/lib/schemable/response_schema_generator.rb +++ b/lib/schemable/response_schema_generator.rb @@ -27,12 +27,16 @@ def initialize(model_definition) # @param expand [Boolean] Whether to include the included resources in the schema. # @param relationships_to_exclude_from_expansion [Array] The relationships to exclude from expansion in the schema. # @param collection [Boolean] Whether the response is for a collection of resources. + # @param expand_nested [Boolean] Whether to include the nested relationships in the schema. # # @example - # schema = generator.generate(expand: true, relationships_to_exclude_from_expansion: ['some_relationship'], collection: true) + # schema = generator.generate(expand: true, relationships_to_exclude_from_expansion: ['some_relationship'], collection: true, expand_nested: true) # # @return [Hash] The generated schema. - def generate(expand: false, relationships_to_exclude_from_expansion: [], collection: false) + def generate(expand: false, relationships_to_exclude_from_expansion: [], collection: false, expand_nested: false) + # Override expand_nested if infer_expand_nested_from_expand is true + expand_nested = expand if @configuration.infer_expand_nested_from_expand + data = { type: :object, properties: { @@ -51,7 +55,7 @@ def generate(expand: false, relationships_to_exclude_from_expansion: [], collect schema = collection ? { data: { type: :array, items: data } } : { data: } if expand - included_schema = IncludedSchemaGenerator.new(@model_definition).generate(expand:, relationships_to_exclude_from_expansion:) + included_schema = IncludedSchemaGenerator.new(@model_definition).generate(expand: expand_nested, relationships_to_exclude_from_expansion:) @schema_modifier.add_properties(schema, included_schema, '.') end diff --git a/sig/schemable/response_schema_generator.rbs b/sig/schemable/response_schema_generator.rbs index 84c0faa..0ff9c97 100644 --- a/sig/schemable/response_schema_generator.rbs +++ b/sig/schemable/response_schema_generator.rbs @@ -8,6 +8,6 @@ module Schemable def initialize: (Definition) -> void def meta: -> Hash[Symbol, any] def jsonapi: -> Hash[Symbol, any] - def generate: (expand: bool, relationships_to_exclude_from_expansion: Array[Symbol], collection: bool) -> Hash[Symbol, any] + def generate: (expand: bool, relationships_to_exclude_from_expansion: Array[Symbol], collection: bool, expand_nested: bool) -> Hash[Symbol, any] end end From f90a997e30740e1ad2a60a90acee064fe9d8e590 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Tue, 30 Jan 2024 16:29:38 +0300 Subject: [PATCH 84/87] Adds infer_expand_nested_from_expand global configuration option --- lib/schemable/configuration.rb | 2 ++ sig/schemable/configuration.rbs | 1 + 2 files changed, 3 insertions(+) diff --git a/lib/schemable/configuration.rb b/lib/schemable/configuration.rb index 97513f8..fbe45f9 100644 --- a/lib/schemable/configuration.rb +++ b/lib/schemable/configuration.rb @@ -16,6 +16,7 @@ class Configuration :enum_prefix_for_simple_enum, :enum_suffix_for_simple_enum, :custom_meta_response_schema, + :infer_expand_nested_from_expand, :infer_attributes_from_custom_method, :infer_attributes_from_jsonapi_serializable ) @@ -32,6 +33,7 @@ def initialize @custom_meta_response_schema = nil @enum_prefix_for_simple_enum = nil @enum_suffix_for_simple_enum = nil + @infer_expand_nested_from_expand = false @infer_attributes_from_custom_method = nil @infer_attributes_from_jsonapi_serializable = false end diff --git a/sig/schemable/configuration.rbs b/sig/schemable/configuration.rbs index ecef3b3..02c2cb1 100644 --- a/sig/schemable/configuration.rbs +++ b/sig/schemable/configuration.rbs @@ -8,6 +8,7 @@ module Schemable attr_accessor custom_defined_enum_method: Symbol? attr_accessor enum_prefix_for_simple_enum: String? attr_accessor enum_suffix_for_simple_enum: String? + attr_accessor infer_expand_nested_from_expand: bool attr_accessor custom_type_mappers: Hash[Symbol, any] attr_accessor infer_attributes_from_custom_method: Symbol? attr_accessor custom_meta_response_schema: Hash[Symbol, any]? From d212a2904e08cf796e6420f0c99f547a922a0a7f Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Tue, 30 Jan 2024 16:29:58 +0300 Subject: [PATCH 85/87] Add infer_expand_nested_from_expand configuration docs --- README.md | 3 ++- lib/templates/schemable.rb | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 113ca00..e8c3a4f 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ Please note that the configurations options below are defined in the `Schemable` | `enum_prefix_for_simple_enum` | The prefix to be used for the enum values when `mongoid` is used. | `nil` | | `enum_suffix_for_simple_enum` | The suffix to be used for the enum values when `mongoid` is used. | `nil` | | `infer_attributes_from_custom_method` | The name of the custom method that is used to get the attributes to be generated in the schema. See [Annex 1.3 - Infer attributes from custom method](#annex-13---infer-attributes-from-custom-method) for more information. | `nil` | +| `infer_expand_nested_from_expand` | Configures `ResponseSchemaGenerator`'s `generate` method to prevent expansion for nested relationships. It globally set the value of `expand_nested` to the same value as `expand` by setting the configuration to `true` | `flase` | | `infer_attributes_from_jsonapi_serializable` | Whether or not to infer the attributes from the `JSONAPI::Serializable::Resource` class. See the previous example [Annex 1.1 - Use serialized instance](#annex-11---use-serialized-instance) for more information. | `false` | | `custom_meta_response_schema` | A hash of custom meta response schema that can be used to override the default meta response schema. See [Annex 1.4 - Custom meta response schema](#annex-14---custom-meta-response-schema) for more information. | `nil` | | `pagination_enabled` | Enable pagination schema generation in the `meta` section of the response schema. | `true` | @@ -400,7 +401,7 @@ module Swagger "#{schema_instance.model}CreateRequest": schema_instance.camelize_keys(Schemable::RequestSchemaGenerator.new(schema_instance).generate_for_create), "#{schema_instance.model}UpdateRequest": schema_instance.camelize_keys(Schemable::RequestSchemaGenerator.new(schema_instance).generate_for_update), "#{schema_instance.model}Response": schema_instance.camelize_keys(Schemable::ResponseSchemaGenerator.new(schema_instance).generate(expand: true, collection: true, relationships_to_exclude_from_expansion: %w[addresses stores attachments])), - "#{schema_instance.model}ResponseExpanded": schema_instance.camelize_keys(Schemable::ResponseSchemaGenerator.new(schema_instance).generate(expand: true)) + "#{schema_instance.model}ResponseExpanded": schema_instance.camelize_keys(Schemable::ResponseSchemaGenerator.new(schema_instance).generate(expand: true, expand_nested: true)) ] end end diff --git a/lib/templates/schemable.rb b/lib/templates/schemable.rb index ff7c59a..6400b46 100644 --- a/lib/templates/schemable.rb +++ b/lib/templates/schemable.rb @@ -41,6 +41,11 @@ # # config.infer_attributes_from_custom_method = nil + # If you want to recursively expand the relationships in the response schema + # then set this option to true, otherwise set it to false (default). + # + # config.infer_expand_nested_from_expand = true + # If you want to get the list of attributes from the jsonapi-rails gem's # JSONAPI::Serializable::Resource class, set this option to true. # It uses the attribute_blocks method to get the list of attributes. From 96911aab98586b5d77254281fcc1e4fe60c7f737 Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Tue, 30 Jan 2024 16:30:25 +0300 Subject: [PATCH 86/87] Updates change log for `v1.0.3` --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87ed805..ea1443b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog This file is used to list changes made in each version of the Schemable gem. +## Schemable 1.0.3 (2024-01-30) + +* Added configuration for preventing expansion for nested relationships. This can be done by setting the `expand_nested` to `true` when invoking `ResponseSchemaGenerator`'s `generate` instance method (e.g. `ResponseSchemaGenerator.new(instance).generate(expand: true, expand_nested: true)`. Additionally, you could globally set the value of `expand_nested` to the same value as `expand` by setting the configuration `infer_expand_nested_from_expand` to `true` in the `/config/initializers/schemable.rb`. + ## Schemable 1.0.2 (2024-01-30) * Added configuration for making certain associations nullable in the response's relationship. This can be done by adding the name of the relation in the `nullable_relationships` method's array of strings. From aaf209b8d2b079051028e8cd7c2c8bec37c8bf1e Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Tue, 30 Jan 2024 16:30:29 +0300 Subject: [PATCH 87/87] Bump version to 1.0.3 --- lib/schemable/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/schemable/version.rb b/lib/schemable/version.rb index bcfcf07..f2ec300 100644 --- a/lib/schemable/version.rb +++ b/lib/schemable/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Schemable - VERSION = '1.0.2' + VERSION = '1.0.3' end