lib/annotate/annotate_models.rb in annotate-2.6.10 vs lib/annotate/annotate_models.rb in annotate-2.7.0

- old
+ new

@@ -7,18 +7,26 @@ PREFIX = "== Schema Information" PREFIX_MD = "## Schema Information" END_MARK = "== Schema Information End" PATTERN = /^\r?\n?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?\r?\n(#.*\r?\n)*(\r?\n)*/ + MATCHED_TYPES = %w(test fixture factory serializer scaffold controller helper) + # File.join for windows reverse bar compat? # I dont use windows, can`t test UNIT_TEST_DIR = File.join("test", "unit") MODEL_TEST_DIR = File.join("test", "models") # since rails 4.0 SPEC_MODEL_DIR = File.join("spec", "models") FIXTURE_TEST_DIR = File.join("test", "fixtures") FIXTURE_SPEC_DIR = File.join("spec", "fixtures") + # Other test files + CONTROLLER_TEST_DIR = File.join("test", "controllers") + CONTROLLER_SPEC_DIR = File.join("spec", "controllers") + REQUEST_SPEC_DIR = File.join("spec", "requests") + ROUTING_SPEC_DIR = File.join("spec", "routing") + # Object Daddy http://github.com/flogic/object_daddy/tree/master EXEMPLARS_TEST_DIR = File.join("test", "exemplars") EXEMPLARS_SPEC_DIR = File.join("spec", "exemplars") # Machinist http://github.com/notahat/machinist @@ -36,41 +44,16 @@ # Serializers https://github.com/rails-api/active_model_serializers SERIALIZERS_DIR = File.join("app", "serializers") SERIALIZERS_TEST_DIR = File.join("test", "serializers") SERIALIZERS_SPEC_DIR = File.join("spec", "serializers") + # Controller files + CONTROLLER_DIR = File.join("app", "controllers") - TEST_PATTERNS = [ - File.join(UNIT_TEST_DIR, "%MODEL_NAME%_test.rb"), - File.join(MODEL_TEST_DIR, "%MODEL_NAME%_test.rb"), - File.join(SPEC_MODEL_DIR, "%MODEL_NAME%_spec.rb"), - ] + # Helper files + HELPER_DIR = File.join("app", "helpers") - FIXTURE_PATTERNS = [ - File.join(FIXTURE_TEST_DIR, "%TABLE_NAME%.yml"), - File.join(FIXTURE_SPEC_DIR, "%TABLE_NAME%.yml"), - ] - - FACTORY_PATTERNS = [ - File.join(EXEMPLARS_TEST_DIR, "%MODEL_NAME%_exemplar.rb"), - File.join(EXEMPLARS_SPEC_DIR, "%MODEL_NAME%_exemplar.rb"), - File.join(BLUEPRINTS_TEST_DIR, "%MODEL_NAME%_blueprint.rb"), - File.join(BLUEPRINTS_SPEC_DIR, "%MODEL_NAME%_blueprint.rb"), - File.join(FACTORY_GIRL_TEST_DIR, "%MODEL_NAME%_factory.rb"), # (old style) - File.join(FACTORY_GIRL_SPEC_DIR, "%MODEL_NAME%_factory.rb"), # (old style) - File.join(FACTORY_GIRL_TEST_DIR, "%TABLE_NAME%.rb"), # (new style) - File.join(FACTORY_GIRL_SPEC_DIR, "%TABLE_NAME%.rb"), # (new style) - File.join(FABRICATORS_TEST_DIR, "%MODEL_NAME%_fabricator.rb"), - File.join(FABRICATORS_SPEC_DIR, "%MODEL_NAME%_fabricator.rb"), - ] - - SERIALIZER_PATTERNS = [ - File.join(SERIALIZERS_DIR, "%MODEL_NAME%_serializer.rb"), - File.join(SERIALIZERS_TEST_DIR, "%MODEL_NAME%_serializer_spec.rb"), - File.join(SERIALIZERS_SPEC_DIR, "%MODEL_NAME%_serializer_spec.rb") - ] - # Don't show limit (#) on these column types # Example: show "integer" instead of "integer(4)" NO_LIMIT_COL_TYPES = ["integer", "boolean"] class << self @@ -80,19 +63,86 @@ def model_dir=(dir) @model_dir = dir end + def root_dir + @root_dir.is_a?(Array) ? @root_dir : [@root_dir || ""] + end + + def root_dir=(dir) + @root_dir = dir + end + + def get_patterns(pattern_types=MATCHED_TYPES) + current_patterns = [] + root_dir.each do |root_directory| + Array(pattern_types).each do |pattern_type| + current_patterns += case pattern_type + when 'test' + [ + File.join(root_directory, UNIT_TEST_DIR, "%MODEL_NAME%_test.rb"), + File.join(root_directory, MODEL_TEST_DIR, "%MODEL_NAME%_test.rb"), + File.join(root_directory, SPEC_MODEL_DIR, "%MODEL_NAME%_spec.rb"), + ] + when 'fixture' + [ + File.join(root_directory, FIXTURE_TEST_DIR, "%TABLE_NAME%.yml"), + File.join(root_directory, FIXTURE_SPEC_DIR, "%TABLE_NAME%.yml"), + File.join(root_directory, FIXTURE_TEST_DIR, "%PLURALIZED_MODEL_NAME%.yml"), + File.join(root_directory, FIXTURE_SPEC_DIR, "%PLURALIZED_MODEL_NAME%.yml"), + ] + when 'scaffold' + [ + File.join(root_directory, CONTROLLER_TEST_DIR, "%PLURALIZED_MODEL_NAME%_controller_test.rb"), + File.join(root_directory, CONTROLLER_SPEC_DIR, "%PLURALIZED_MODEL_NAME%_controller_spec.rb"), + File.join(root_directory, REQUEST_SPEC_DIR, "%PLURALIZED_MODEL_NAME%_spec.rb"), + File.join(root_directory, ROUTING_SPEC_DIR, "%PLURALIZED_MODEL_NAME%_routing_spec.rb"), + ] + when 'factory' + [ + File.join(root_directory, EXEMPLARS_TEST_DIR, "%MODEL_NAME%_exemplar.rb"), + File.join(root_directory, EXEMPLARS_SPEC_DIR, "%MODEL_NAME%_exemplar.rb"), + File.join(root_directory, BLUEPRINTS_TEST_DIR, "%MODEL_NAME%_blueprint.rb"), + File.join(root_directory, BLUEPRINTS_SPEC_DIR, "%MODEL_NAME%_blueprint.rb"), + File.join(root_directory, FACTORY_GIRL_TEST_DIR, "%MODEL_NAME%_factory.rb"), # (old style) + File.join(root_directory, FACTORY_GIRL_SPEC_DIR, "%MODEL_NAME%_factory.rb"), # (old style) + File.join(root_directory, FACTORY_GIRL_TEST_DIR, "%TABLE_NAME%.rb"), # (new style) + File.join(root_directory, FACTORY_GIRL_SPEC_DIR, "%TABLE_NAME%.rb"), # (new style) + File.join(root_directory, FABRICATORS_TEST_DIR, "%MODEL_NAME%_fabricator.rb"), + File.join(root_directory, FABRICATORS_SPEC_DIR, "%MODEL_NAME%_fabricator.rb"), + ] + when 'serializer' + [ + File.join(root_directory, SERIALIZERS_DIR, "%MODEL_NAME%_serializer.rb"), + File.join(root_directory, SERIALIZERS_TEST_DIR, "%MODEL_NAME%_serializer_spec.rb"), + File.join(root_directory, SERIALIZERS_SPEC_DIR, "%MODEL_NAME%_serializer_spec.rb") + ] + when 'controller' + [ + File.join(root_directory, CONTROLLER_DIR, "%PLURALIZED_MODEL_NAME%_controller.rb") + ] + when 'helper' + [ + File.join(root_directory, HELPER_DIR, "%PLURALIZED_MODEL_NAME%_helper.rb") + ] + end + end + end + current_patterns.map{ |p| p.sub(/^[\/]*/, '') } + end + # Simple quoting for the default column value def quote(value) case value when NilClass then "NULL" when TrueClass then "TRUE" when FalseClass then "FALSE" when Float, Fixnum, Bignum then value.to_s # BigDecimals need to be output in a non-normalized form and quoted. when BigDecimal then value.to_s('F') + when Array then value.map {|v| quote(v)} else value.inspect end end @@ -125,32 +175,36 @@ if(options[:format_markdown]) info<< sprintf( "# %-#{max_size + md_names_overhead}.#{max_size + md_names_overhead}s | %-#{md_type_allowance}.#{md_type_allowance}s | %s\n", 'Name', 'Type', 'Attributes' ) info<< "# #{ '-' * ( max_size + md_names_overhead ) } | #{'-' * md_type_allowance} | #{ '-' * 27 }\n" end - cols = klass.columns - if options[:ignore_columns] - cols.reject! { |col| col.name.match(/#{options[:ignore_columns]}/) } - end + cols = if ignore_columns = options[:ignore_columns] + klass.columns.reject do |col| + col.name.match(/#{ignore_columns}/) + end + else + klass.columns + end cols = cols.sort_by(&:name) if(options[:sort]) cols = classified_sort(cols) if(options[:classified_sort]) cols.each do |col| + col_type = (col.type || col.sql_type).to_s + attrs = [] - attrs << "default(#{schema_default(klass, col)})" unless col.default.nil? + attrs << "default(#{schema_default(klass, col)})" unless col.default.nil? || col_type == "jsonb" attrs << "not null" unless col.null attrs << "primary key" if klass.primary_key && (klass.primary_key.is_a?(Array) ? klass.primary_key.collect{|c|c.to_sym}.include?(col.name.to_sym) : col.name.to_sym == klass.primary_key.to_sym) - col_type = (col.type || col.sql_type).to_s if col_type == "decimal" col_type << "(#{col.precision}, #{col.scale})" elsif col_type != "spatial" if (col.limit) if col.limit.is_a? Array attrs << "(#{col.limit.join(', ')})" else - col_type << "(#{col.limit})" unless NO_LIMIT_COL_TYPES.include?(col_type) + col_type << "(#{col.limit})" unless hide_limit?(col_type, options) end end end # Check out if we got an array column @@ -225,18 +279,31 @@ end end return index_info end + def hide_limit?(col_type, options) + excludes = + if options[:hide_limit_column_types].blank? + NO_LIMIT_COL_TYPES + else + options[:hide_limit_column_types].split(',') + end + + excludes.include?(col_type) + end + def get_foreign_key_info(klass, options={}) if(options[:format_markdown]) fk_info = "#\n# ### Foreign Keys\n#\n" else fk_info = "#\n# Foreign Keys\n#\n" end - foreign_keys = klass.connection.respond_to?(:foreign_keys) ? klass.connection.foreign_keys(klass.table_name) : [] + return "" unless klass.connection.supports_foreign_keys? && klass.connection.respond_to?(:foreign_keys) + + foreign_keys = klass.connection.foreign_keys(klass.table_name) return "" if foreign_keys.empty? max_size = foreign_keys.collect{|fk| fk.name.size}.max + 1 foreign_keys.sort_by{|fk| fk.name}.each do |fk| ref_info = "#{fk.column} => #{fk.to_table}.#{fk.primary_key}" @@ -272,12 +339,12 @@ column_pattern = /^#[\t ]+[\w\*`]+[\t ]+.+$/ old_columns = old_header && old_header.scan(column_pattern).sort new_columns = new_header && new_header.scan(column_pattern).sort - encoding = Regexp.new(/(^#\s*encoding:.*\n)|(^# coding:.*\n)|(^# -\*- coding:.*\n)|(^# -\*- encoding\s?:.*\n)/) - encoding_header = old_content.match(encoding).to_s + magic_comment_matcher= Regexp.new(/(^#\s*encoding:.*\n)|(^# coding:.*\n)|(^# -\*- coding:.*\n)|(^# -\*- encoding\s?:.*\n)|(^#\s*frozen_string_literal:.+\n)|(^# -\*- frozen_string_literal\s*:.+-\*-\n)/) + magic_comments= old_content.scan(magic_comment_matcher).flatten.compact if old_columns == new_columns && !options[:force] return false else # Replace inline the old schema info with the new schema info @@ -286,21 +353,21 @@ if new_content.end_with?(info_block + "\n") new_content = old_content.sub(PATTERN, "\n" + info_block) end wrapper_open = options[:wrapper_open] ? "# #{options[:wrapper_open]}\n" : "" - wrapper_close = options[:wrapper_close] ? "\n# #{options[:wrapper_close]}" : "" + wrapper_close = options[:wrapper_close] ? "# #{options[:wrapper_close]}\n" : "" wrapped_info_block = "#{wrapper_open}#{info_block}#{wrapper_close}" # if there *was* no old schema info (no substitution happened) or :force was passed, # we simply need to insert it in correct position if new_content == old_content || options[:force] - old_content.sub!(encoding, '') + old_content.sub!(magic_comment_matcher, '') old_content.sub!(PATTERN, '') new_content = %w(after bottom).include?(options[position].to_s) ? - (encoding_header + (old_content.rstrip + "\n\n" + wrapped_info_block)) : - (encoding_header + wrapped_info_block + "\n" + old_content) + (magic_comments.join + (old_content.rstrip + "\n\n" + wrapped_info_block)) : + (magic_comments.join + wrapped_info_block + "\n" + old_content) end File.open(file_name, "wb") { |f| f.puts new_content } return true end @@ -338,10 +405,13 @@ # :position_in_serializer<Symbol>:: where to place the annotated section in serializer file # :exclude_tests<Symbol>:: whether to skip modification of test/spec files # :exclude_fixtures<Symbol>:: whether to skip modification of fixture files # :exclude_factories<Symbol>:: whether to skip modification of factory files # :exclude_serializers<Symbol>:: whether to skip modification of serializer files + # :exclude_scaffolds<Symbol>:: whether to skip modification of scaffold files + # :exclude_controllers<Symbol>:: whether to skip modification of controller files + # :exclude_helpers<Symbol>:: whether to skip modification of helper files # def annotate(klass, file, header, options={}) begin info = get_schema_info(klass, header, options) did_annotate = false @@ -351,19 +421,18 @@ if annotate_one_file(model_file_name, info, :position_in_class, options_with_position(options, :position_in_class)) did_annotate = true end - %w(test fixture factory serializer).each do |key| + MATCHED_TYPES.each do |key| exclusion_key = "exclude_#{key.pluralize}".to_sym - patterns_constant = "#{key.upcase}_PATTERNS".to_sym position_key = "position_in_#{key}".to_sym unless options[exclusion_key] - did_annotate = self.const_get(patterns_constant). - map { |file| resolve_filename(file, model_name, table_name) }. - map { |file| annotate_one_file(file, info, position_key, options_with_position(options, position_key)) }. + did_annotate = self.get_patterns(key). + map { |f| resolve_filename(f, model_name, table_name) }. + map { |f| annotate_one_file(f, info, position_key, options_with_position(options, position_key)) }. detect { |result| result } || did_annotate end end return did_annotate @@ -417,15 +486,15 @@ # in subdirectories without namespacing. def get_model_class(file) model_path = file.gsub(/\.rb$/, '') model_dir.each { |dir| model_path = model_path.gsub(/^#{dir}/, '').gsub(/^\//, '') } begin - get_loaded_model(model_path) or raise LoadError.new("cannot load a model from #{file}") + get_loaded_model(model_path) or raise BadModelFileError.new rescue LoadError # this is for non-rails projects, which don't get Rails auto-require magic file_path = File.expand_path(file) - if File.file?(file_path) && Kernel.require(file_path) + if File.file?(file_path) && silence_warnings { Kernel.require(file_path) } retry elsif model_path.match(/\//) model_path = model_path.split('/')[1..-1].join('/').to_s retry else @@ -463,17 +532,18 @@ header << "\n# Schema version: #{version}" end end self.model_dir = options[:model_dir] if options[:model_dir] + self.root_dir = options[:root_dir] if options[:root_dir] annotated = [] get_model_files(options).each do |file| annotate_model_file(annotated, File.join(file), header, options) end if annotated.empty? - puts "Nothing to annotate." + puts "Model files unchanged." else puts "Annotated (#{annotated.length}): #{annotated.join(', ')}" end end @@ -481,21 +551,27 @@ begin return false if (/# -\*- SkipSchemaAnnotations.*/ =~ (File.exist?(file) ? File.read(file) : '') ) klass = get_model_class(file) if klass && klass < ActiveRecord::Base && !klass.abstract_class? && klass.table_exists? if annotate(klass, file, header, options) - annotated << klass + annotated << file end end + rescue BadModelFileError => e + unless options[:ignore_unknown_models] + puts "Unable to annotate #{file}: #{e.message}" + puts "\t" + e.backtrace.join("\n\t") if options[:trace] + end rescue Exception => e puts "Unable to annotate #{file}: #{e.message}" puts "\t" + e.backtrace.join("\n\t") if options[:trace] end end def remove_annotations(options={}) self.model_dir = options[:model_dir] if options[:model_dir] + self.root_dir = options[:root_dir] if options[:root_dir] deannotated = [] deannotated_klass = false get_model_files(options).each do |file| file = File.join(file) begin @@ -504,15 +580,15 @@ model_name = klass.name.underscore table_name = klass.table_name model_file_name = file deannotated_klass = true if(remove_annotation_of_file(model_file_name)) - (TEST_PATTERNS + FIXTURE_PATTERNS + FACTORY_PATTERNS + SERIALIZER_PATTERNS). - map { |file| resolve_filename(file, model_name, table_name) }. - each do |file| - if File.exist?(file) - remove_annotation_of_file(file) + get_patterns. + map { |f| resolve_filename(f, model_name, table_name) }. + each do |f| + if File.exist?(f) + remove_annotation_of_file(f) deannotated_klass = true end end end deannotated << klass if(deannotated_klass) @@ -525,10 +601,11 @@ end def resolve_filename(filename_template, model_name, table_name) return filename_template. gsub('%MODEL_NAME%', model_name). + gsub('%PLURALIZED_MODEL_NAME%', model_name.pluralize). gsub('%TABLE_NAME%', table_name || model_name.pluralize) end def classified_sort(cols) rest_cols = [] @@ -548,8 +625,22 @@ end end [rest_cols, timestamps, associations].each {|a| a.sort_by!(&:name) } return ([id] << rest_cols << timestamps << associations).flatten + end + + # Ignore warnings for the duration of the block () + def silence_warnings + old_verbose, $VERBOSE = $VERBOSE, nil + yield + ensure + $VERBOSE = old_verbose + end + end + + class BadModelFileError < LoadError + def to_s + "file doesn't contain a valid model class" end end end