core/lib/refinery/extension_generation.rb
module Refinery
module ExtensionGeneration
def self.included(base)
base.class_eval do
argument :attributes,
type: :array,
default: [],
banner: 'field:type field:type'
class_option :namespace,
type: :string,
default: '',
banner: 'NAMESPACE',
required: false
class_option :authors,
type: :array,
default: [],
banner: 'author author',
required: false,
desc: 'Indicates authors of this extension'
class_option :extension,
type: :string,
default: '',
banner: 'ENGINE',
required: false
class_option :i18n,
type: :array,
default: [],
required: false,
banner: 'field field',
desc: 'Indicates generated fields'
class_option :install,
type: :boolean,
default: false,
required: false,
banner: nil,
desc: 'Bundles and runs the generated generator, rake db:migrate, rake db:seed for you'
remove_class_option :skip_namespace
end
end
def namespacing
@namespacing ||= if options[:namespace].present?
# Use exactly what the user requested, not a pluralised version.
options[:namespace].to_s.camelize
else
# If the user has passed an engine, we want to generate it inside of
# that extension.
if options[:extension].present?
options[:extension].to_s.camelize
else
class_name.pluralize
end
end
end
def extension_name
@extension_name ||= options[:extension].presence || singular_name
end
def extension_authors
@extension_authors ||= options[:authors].presence
end
def extension_class_name
@extension_class_name ||= extension_name.camelize
end
def extension_plural_class_name
@extension_plural_class_name ||= if options[:extension].present?
# Use exactly what the user requested, not a plural version.
extension_class_name
else
extension_class_name.pluralize
end
end
def extension_plural_name
@extension_plural_name ||= if options[:extension].present?
# Use exactly what the user requested, not a plural version.
extension_name
else
extension_name.pluralize
end
end
def localized?
localized_attributes.any?
end
def localized_attributes
@localized_attributes ||= attributes.select{ |a| options[:i18n].include?(a.name)}
end
def localized_table_name
localized_table_name = [ 'refinery']
localized_table_name << namespacing.underscore if table_name != namespacing.underscore.pluralize
localized_table_name << [ singular_table_name, 'translations']
localized_table_name.join('_')
end
def attributes_for_translation_table
localized_attributes.inject([]) { |memo, attr| memo << ":#{attr.name} => :#{attr.type}"}.join(', ')
end
def string_attributes
@string_attributes ||= attributes.select { |a| /string|text/ === a.refinery_type.to_s}.uniq
end
def image_attributes
@image_attributes ||= attributes.select { |a| a.refinery_type == :image }.uniq
end
def resource_attributes
@resource_attributes ||= attributes.select { |a| a.refinery_type == :resource }.uniq
end
protected
def append_extension_to_gemfile!
unless Rails.env.test? || (self.behavior != :revoke && extension_in_gemfile?)
path = extension_pathname.parent.relative_path_from(gemfile.parent)
append_file gemfile, "\ngem '#{gem_name}', path: '#{path}'"
end
end
def clash_keywords
@clash_keywords ||= begin
clash_keywords = []
unless (clash_file = source_pathname.parent.join('clash_keywords.yml')).file?
clash_file = source_pathname.parent.parent.join('clash_keywords.yml')
end
clash_keywords = YAML.load_file(clash_file) if clash_file.file?
clash_keywords
end
end
def default_generate!
sanity_check!
evaluate_templates!
unless options[:pretend]
merge_existing_files! if existing_extension?
copy_or_merge_seeds! if self.behavior != :revoke
append_extension_to_gemfile!
end
install! if options[:install]
finalize_extension!
end
def destination_pathname
@destination_pathname ||= Pathname.new(self.destination_root.to_s)
end
def extension_pathname
destination_pathname.join('vendor', 'extensions', extension_plural_name)
end
def extension_path_for(path, extension, apply_tmp = true)
path = extension_pathname.join path.sub(%r{#{source_pathname}/?}, '')
path = substitute_path_placeholders path
if options[:namespace].present? || options[:extension].present?
path = increment_migration_timestamp(path)
# Detect whether this is a special file that needs to get merged not overwritten.
# This is important only when nesting extensions.
# Routes and #{gem_name}\.rb have an .erb extension as path points to the generator template
# We have to exclude it when checking if the file already exists and include it in the regexps
path = extension_path_for_nested_extension(path, apply_tmp) if options[:extension].present?
end
path
end
def erase_destination!
if Pathname.glob(extension_pathname.join('**', '*')).all?(&:directory?)
say_status :remove, relative_to_original_destination_root(extension_pathname.to_s), true
FileUtils.rm_rf extension_pathname unless options[:pretend]
end
end
def evaluate_templates!
viable_templates.each do |source_path, destination_path|
next if /seeds.rb.erb/ === source_path.to_s && self.behavior != :revoke
destination_path.sub!('.erb', '') if source_path.to_s !~ /views/
template source_path, destination_path
end
end
def existing_extension?
options[:extension].present? && extension_pathname.directory?
end
def exit_with_message!(message)
STDERR.puts "\n#{message}\n\n"
exit 1
end
def extension_in_gemfile?
gemfile.read.scan(%r{#{gem_name}}).any?
end
def finalize_extension!
if self.behavior != :revoke && !options[:pretend]
instruct_user!
else
erase_destination!
end
end
def gem_name
"refinerycms-#{extension_plural_name}"
end
def gemfile
@gemfile ||= begin
Bundler.default_gemfile || destination_pathname.join('Gemfile')
end
end
def generator_command
raise "You must override the method 'generator_command' in your generator."
end
def install!
run "bundle install"
run "rails generate refinery:#{extension_plural_name}"
run "rake db:migrate"
run "rake db:seed"
end
def merge_existing_files!
# go through all of the temporary files and merge what we need into the current files.
tmp_directories = []
globs = %w[config/locales/*.yml config/routes.rb.erb lib/refinerycms-extension_plural_name.rb.erb]
Pathname.glob(source_pathname.join("{#{globs.join(',')}}"), File::FNM_DOTMATCH).each do |path|
# get the path to the current tmp file.
# Both the new and current paths need to strip the .erb portion from the generator template
new_file_path = extension_path_for(path, extension_name).sub(/\.erb$/, '')
tmp_directories << new_file_path.split.first
current_path = extension_path_for(path, extension_name, false).sub(/\.erb$/, '')
FileMerger.new(self, current_path, new_file_path, :to => current_path, :mode => 'w+').call
end
tmp_directories.uniq.each(&:rmtree)
end
def copy_or_merge_seeds!
FileMerger.new(self, source_seed_file, destination_seed_file).call
end
def instruct_user!
unless Rails.env.test?
puts "------------------------"
if options[:install]
puts "Your extension has been generated and installed."
else
puts "Now run:"
puts "bundle install"
puts "rails generate refinery:#{extension_plural_name}"
puts "rake db:migrate"
puts "rake db:seed"
end
puts "Please restart your rails server."
puts "------------------------"
end
end
def reject_file?(file)
!localized? && file.to_s.include?('locale_picker')
end
def reject_template?(file)
file.directory? || reject_file?(file)
end
def sanity_check!
prevent_clashes!
prevent_uncountability!
prevent_empty_attributes!
prevent_invalid_extension!
end
def source_pathname
@source_pathname ||= Pathname.new(self.class.source_root.to_s)
end
private
def extension_path_for_nested_extension(path, apply_tmp)
return nil if !path.sub(/\.erb$/, '').file? &&
%r{readme.md|(lib/)?#{plural_name}.rb$} === path.to_s
if apply_tmp && %r{(locales/.*\.yml)|((config/routes|#{gem_name})\.rb\.erb)$} === path.to_s
path = path.dirname + 'tmp' + path.basename
end
path
end
def increment_migration_timestamp(path)
# Increment the migration file leading number
# Only relevant for nested or namespaced extensions, where a previous migration exists
return path unless %r{/migrate/\d+.*\.rb.erb\z} === path.to_s && last_migration_file(path)
path.sub(%r{\d+_}) { |m| "#{last_migration_file(path).match(%r{migrate/(\d+)_})[1].to_i + 1}_" }
end
def last_migration_file(path)
Dir[destination_pathname.join(path.dirname + '*.rb')].sort.last
end
def prevent_clashes!
if clash_keywords.member?(singular_name.downcase)
exit_with_message!("Please choose a different name. The generated code would fail for class '#{singular_name}' as it conflicts with a reserved keyword.")
end
end
def prevent_uncountability!
if singular_name == plural_name
message = if singular_name.singularize == singular_name
"The extension name you specified will not work as the singular name is equal to the plural name."
else
"Please specify the singular name '#{singular_name.singularize}' instead of '#{plural_name}'."
end
exit_with_message! message
end
end
def prevent_empty_attributes!
if attributes.empty? && self.behavior != :revoke
exit_with_message! "You must specify a name and at least one field." \
"\nFor help, run: #{generator_command}"
end
end
def prevent_invalid_extension!
if options[:extension].present? && !extension_pathname.directory?
exit_with_message! "You can't use '--extension #{options[:extension]}' because an" \
" extension with the name '#{options[:extension]}' doesn't exist."
end
end
def source_seed_file
source_pathname.join 'db', 'seeds.rb.erb'
end
def destination_seed_file
destination_pathname.join extension_path_for(source_seed_file.sub('.erb', ''), extension_name)
end
def substitute_path_placeholders(path)
Pathname.new path.to_s.gsub('extension_plural_name', extension_plural_name).
gsub('plural_name', plural_name).
gsub('singular_name', singular_name).
gsub('namespace', namespacing.underscore)
end
def index_route
if namespacing.underscore == plural_name
'/' + plural_name
else
'/' + namespacing.underscore + '/' + plural_name
end
end
def viable_templates
@viable_templates ||= begin
all_templates.reject(&method(:reject_template?)).inject({}) do |hash, path|
if (destination_path = extension_path_for(path, extension_name)).present?
hash[path.to_s] = destination_path.to_s
end
hash
end
end
end
def all_templates
Pathname.glob source_pathname.join('**', '**')
end
class FileMerger
def initialize(templater, source, destination, options = {})
@templater = templater
@source = source
@destination = destination
@options = {:to => @destination, :mode => 'a+'}.merge(options)
end
def call
if %r{\.erb$} === @source.basename.to_s
templated_merge!
else
merge!
end
end
def contents
merged_file_contents
end
private
def merge!(contents = merged_file_contents)
@options[:to].open(@options[:mode]) { |file| file.puts contents }
end
def merged_file_contents
case @destination.to_s
# merge translation files together.
when %r{.yml$} then merge_yaml
# append any routes from the new file to the current one.
when %r{/routes.rb$} then merge_rb
# simply append the file contents
else @source.read + @destination.read
end
end
def templated_merge!
Dir.mktmpdir do |tmp|
tmp = Pathname.new(tmp)
@templater.template @source, tmp.join(@source.basename), :verbose => false
merge! tmp.join(@source.basename).read.to_s
end
end
# merge_rb is only used for merging routes.rb
# Put destination lines first, so that extension namespaced routes precede the default extension route
def merge_rb
(destination_lines[0..-2] + source_lines[1..-2] + [destination_lines.last]).join "\n"
end
def merge_yaml
YAML::load(@destination.read).deep_merge(YAML::load(@source.read)).
to_yaml.gsub(%r{^---\n}, '')
end
def source_lines
@source_lines ||= read_lines @source
end
def destination_lines
@destination_lines ||= read_lines @destination
end
def read_lines(file)
file.read.to_s.split "\n"
end
end
private_constant :FileMerger
end
end