require_relative "named_base_generator" require_relative "concerns/parent_controller" require_relative "concerns/override_controller" module Generators module Avo class ResourceGenerator < NamedBaseGenerator include Concerns::ParentController include Concerns::OverrideController source_root File.expand_path("templates", __dir__) namespace "avo:resource" class_option "model-class", desc: "The name of the model.", type: :string, required: false def create return if override_controller? template "resource/resource.tt", "app/avo/resources/#{resource_name}.rb" invoke "avo:controller", [resource_name], options end def resource_class class_name.remove(":").to_s end def controller_class "Avo::#{class_name.remove(":").pluralize}Controller" end def resource_name model_resource_name.to_s end def controller_name "#{model_resource_name.pluralize}_controller" end def current_models ActiveRecord::Base.connection.tables.map do |model| model.capitalize.singularize.camelize end rescue ActiveRecord::NoDatabaseError puts "Database not found, please create your database and regenerate the resource." [] rescue ActiveRecord::ConnectionNotEstablished puts "Database connection error, please create your database and regenerate the resource." [] end def class_from_args @class_from_args ||= options["model-class"]&.camelize || (class_name if class_name.include?("::")) end def model_class_from_args if class_from_args.present? || class_name.include?("::") "\n self.model_class = ::#{class_from_args || class_name}" end end private def model_class @model_class ||= class_from_args || singular_name end def model @model ||= model_class.classify.safe_constantize end def model_db_columns @model_db_columns ||= model.columns_hash.except(*db_columns_to_ignore) rescue ActiveRecord::NoDatabaseError puts "Database not found, please create your database and regenerate the resource." [] rescue ActiveRecord::ConnectionNotEstablished puts "Database connection error, please create your database and regenerate the resource." [] end def db_columns_to_ignore %w[id encrypted_password reset_password_token reset_password_sent_at remember_created_at created_at updated_at password_digest] end def reflections @reflections ||= model.reflections.reject do |name, _| reflections_sufixes_to_ignore.include?(name.split("_").pop) || reflections_to_ignore.include?(name) end end def reflections_sufixes_to_ignore %w[blob blobs tags] end def reflections_to_ignore %w[taggings] end def attachments @attachments ||= reflections.select do |_, reflection| reflection.options[:class_name] == "ActiveStorage::Attachment" end end def rich_texts @rich_texts ||= reflections.select do |_, reflection| reflection.options[:class_name] == "ActionText::RichText" end end def tags @tags ||= reflections.select { |_, reflection| reflection.options[:as] == :taggable } end def associations @associations ||= reflections.reject do |key| attachments.key?(key) || tags.key?(key) || rich_texts.key?(key) end end def fields @fields ||= {} end def invoked_by_model_generator? @options.dig("from_model_generator") end def generate_fields return generate_fields_from_args if invoked_by_model_generator? if model.blank? puts "Can't generate fields from model. '#{model_class}.rb' not found!" return end fields_from_model_db_columns fields_from_model_enums fields_from_model_attachements fields_from_model_associations fields_from_model_rich_texts fields_from_model_tags generated_fields_template end def generated_fields_template return if fields.blank? fields_string = "" fields.each do |field_name, field_options| # if field_options are not available (likely a missing resource for an association), skip the field fields_string += "\n # Could not generate a field for #{field_name}" and next unless field_options options = "" field_options[:options].each { |k, v| options += ", #{k}: #{v}" } if field_options[:options].present? fields_string += "\n #{field_string field_name, field_options[:field], options}" end fields_string end def field_string(name, type, options) "field :#{name}, as: :#{type}#{options}" end def generate_fields_from_args @args.each do |arg| name, type = arg.split(":") type = "string" if type.blank? fields[name] = field(name, type.to_sym) end generated_fields_template end def fields_from_model_rich_texts rich_texts.each do |name, _| fields[name.delete_prefix("rich_text_")] = {field: "trix"} end end def fields_from_model_tags tags.each do |name, _| fields[(remove_last_word_from name).pluralize] = {field: "tags"} end end def fields_from_model_associations associations.each do |name, association| fields[name] = if association.is_a? ActiveRecord::Reflection::ThroughReflection field_from_through_association(association) else associations_mapping[association.class] end end end def field_from_through_association(association) if association.through_reflection.is_a?(ActiveRecord::Reflection::HasManyReflection) || association.through_reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) { field: "has_many", options: { through: ":#{association.options[:through]}" } } else # If the through_reflection is not a HasManyReflection, add it to the fields hash using the class of the through_reflection # ex (team.rb): has_one :admin, through: :admin_membership, source: :user # we use the class of the through_reflection (HasOneReflection -> has_one :admin) to generate the field associations_mapping[association.through_reflection.class] end end def fields_from_model_attachements attachments.each do |name, attachment| fields[remove_last_word_from name] = attachments_mapping[attachment.class] end end # "hello_world_hehe".split('_') => ['hello', 'world', 'hehe'] # ['hello', 'world', 'hehe'].pop => ['hello', 'world'] # ['hello', 'world'].join('_') => "hello_world" def remove_last_word_from(snake_case_string) snake_case_string = snake_case_string.split("_") snake_case_string.pop snake_case_string.join("_") end def fields_from_model_enums model.defined_enums.each_key do |enum| fields[enum] = { field: "select", options: { enum: "::#{model_class.classify}.#{enum.pluralize}" } } end end def fields_from_model_db_columns model_db_columns.each do |name, data| fields[name] = field(name, data.type) end end def field(name, type) names_mapping[name.to_sym] || fields_mapping[type&.to_sym] || {field: "text"} end def associations_mapping { ActiveRecord::Reflection::BelongsToReflection => { field: "belongs_to" }, ActiveRecord::Reflection::HasOneReflection => { field: "has_one" }, ActiveRecord::Reflection::HasManyReflection => { field: "has_many" }, ActiveRecord::Reflection::HasAndBelongsToManyReflection => { field: "has_and_belongs_to_many" } } end def attachments_mapping { ActiveRecord::Reflection::HasOneReflection => { field: "file" }, ActiveRecord::Reflection::HasManyReflection => { field: "files" } } end def names_mapping { id: { field: "id" }, description: { field: "textarea" }, gravatar: { field: "gravatar" }, email: { field: "text" }, password: { field: "password" }, password_confirmation: { field: "password" }, stage: { field: "select" }, budget: { field: "currency" }, money: { field: "currency" }, country: { field: "country" } } end def fields_mapping { primary_key: { field: "id" }, string: { field: "text" }, text: { field: "textarea" }, integer: { field: "number" }, float: { field: "number" }, decimal: { field: "number" }, datetime: { field: "date_time" }, timestamp: { field: "date_time" }, time: { field: "date_time" }, date: { field: "date" }, binary: { field: "number" }, boolean: { field: "boolean" }, references: { field: "belongs_to" }, json: { field: "code" } } end end end end