From 213fecefd2b5ebbd0b5b18d2da2fe1840c70b1ad Mon Sep 17 00:00:00 2001 From: Muhammad Nawzad Date: Thu, 9 Nov 2023 10:45:25 +0300 Subject: [PATCH] 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