lib/generators/avo/resource_generator.rb
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