annotate 3.0.3 → 3.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ ## Prerequisite
2
+
3
+ - Install "git-flow" (`brew install git-flow`)
4
+ - Install "bump" gem (`gem install bump`)
5
+
6
+
7
+ ## Perform a release
8
+
9
+ - `git flow release start <release>`
10
+ - Update the `CHANGELOG.md` file
11
+ - `bump current`
12
+ - `bump patch`
13
+ - `rm -rf dist`
14
+ - `rake spec`
15
+ - `rake gem`
16
+ - `git flow release finish <release>`
17
+
18
+ - `rake gem:publish`
19
+
@@ -7,14 +7,14 @@ Gem::Specification.new do |s|
7
7
  s.name = 'annotate'
8
8
  s.version = Annotate.version
9
9
 
10
- s.required_ruby_version = '>= 2.2.0'
10
+ s.required_ruby_version = '>= 2.4.0'
11
11
  s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version=
12
12
  s.authors = ['Alex Chaffee', 'Cuong Tran', 'Marcos Piccinini', 'Turadg Aleahmad', 'Jon Frisby']
13
13
  s.description = 'Annotates Rails/ActiveRecord Models, routes, fixtures, and others based on the database schema.'
14
14
  s.email = ['alex@stinky.com', 'cuong.tran@gmail.com', 'x@nofxx.com', 'turadg@aleahmad.net', 'jon@cloudability.com']
15
15
  s.executables = ['annotate']
16
- s.extra_rdoc_files = ['README.rdoc', 'CHANGELOG.rdoc', 'TODO.rdoc']
17
- s.files = `git ls-files -z LICENSE.txt *.rdoc *.gemspec bin lib`.split("\x0")
16
+ s.extra_rdoc_files = ['README.md', 'CHANGELOG.md']
17
+ s.files = `git ls-files -z LICENSE.txt *.md *.gemspec bin lib`.split("\x0")
18
18
  s.homepage = 'http://github.com/ctran/annotate_models'
19
19
  s.licenses = ['Ruby']
20
20
  s.require_paths = ['lib']
@@ -26,7 +26,7 @@ exit if options_result[:exit]
26
26
  options = Annotate.setup_options(
27
27
  is_rake: ENV['is_rake'] && !ENV['is_rake'].empty?
28
28
  )
29
- Annotate.eager_load(options) if Annotate.include_models?
29
+ Annotate.eager_load(options) if Annotate::Helpers.include_models?
30
30
 
31
- AnnotateModels.send(options_result[:target_action], options) if Annotate.include_models?
32
- AnnotateRoutes.send(options_result[:target_action], options) if Annotate.include_routes?
31
+ AnnotateModels.send(options_result[:target_action], options) if Annotate::Helpers.include_models?
32
+ AnnotateRoutes.send(options_result[:target_action], options) if Annotate::Helpers.include_routes?
@@ -1,10 +1,9 @@
1
- # rubocop:disable Metrics/ModuleLength
2
-
3
1
  $LOAD_PATH.unshift(File.dirname(__FILE__))
4
2
  require 'annotate/version'
5
3
  require 'annotate/annotate_models'
6
4
  require 'annotate/annotate_routes'
7
5
  require 'annotate/constants'
6
+ require 'annotate/helpers'
8
7
 
9
8
  begin
10
9
  # ActiveSupport 3.x...
@@ -17,36 +16,6 @@ rescue StandardError
17
16
  end
18
17
 
19
18
  module Annotate
20
- ##
21
- # The set of available options to customize the behavior of Annotate.
22
- #
23
- POSITION_OPTIONS = [
24
- :position_in_routes, :position_in_class, :position_in_test,
25
- :position_in_fixture, :position_in_factory, :position,
26
- :position_in_serializer
27
- ].freeze
28
- FLAG_OPTIONS = [
29
- :show_indexes, :simple_indexes, :include_version, :exclude_tests,
30
- :exclude_fixtures, :exclude_factories, :ignore_model_sub_dir,
31
- :format_bare, :format_rdoc, :format_markdown, :sort, :force, :frozen,
32
- :trace, :timestamp, :exclude_serializers, :classified_sort,
33
- :show_foreign_keys, :show_complete_foreign_keys,
34
- :exclude_scaffolds, :exclude_controllers, :exclude_helpers,
35
- :exclude_sti_subclasses, :ignore_unknown_models, :with_comment
36
- ].freeze
37
- OTHER_OPTIONS = [
38
- :additional_file_patterns, :ignore_columns, :skip_on_db_migrate, :wrapper_open, :wrapper_close,
39
- :wrapper, :routes, :models, :hide_limit_column_types, :hide_default_column_types,
40
- :ignore_routes, :active_admin
41
- ].freeze
42
- PATH_OPTIONS = [
43
- :require, :model_dir, :root_dir
44
- ].freeze
45
-
46
- def self.all_options
47
- [POSITION_OPTIONS, FLAG_OPTIONS, PATH_OPTIONS, OTHER_OPTIONS]
48
- end
49
-
50
19
  ##
51
20
  # Set default values that can be overridden via environment variables.
52
21
  #
@@ -54,9 +23,9 @@ module Annotate
54
23
  return if @has_set_defaults
55
24
  @has_set_defaults = true
56
25
 
57
- options = HashWithIndifferentAccess.new(options)
26
+ options = ActiveSupport::HashWithIndifferentAccess.new(options)
58
27
 
59
- all_options.flatten.each do |key|
28
+ Constants::ALL_ANNOTATE_OPTIONS.flatten.each do |key|
60
29
  if options.key?(key)
61
30
  default_value = if options[key].is_a?(Array)
62
31
  options[key].join(',')
@@ -74,69 +43,42 @@ module Annotate
74
43
  # TODO: what is the difference between this and set_defaults?
75
44
  #
76
45
  def self.setup_options(options = {})
77
- POSITION_OPTIONS.each do |key|
78
- options[key] = fallback(ENV[key.to_s], ENV['position'], 'before')
46
+ Constants::POSITION_OPTIONS.each do |key|
47
+ options[key] = Annotate::Helpers.fallback(ENV[key.to_s], ENV['position'], 'before')
79
48
  end
80
- FLAG_OPTIONS.each do |key|
81
- options[key] = true?(ENV[key.to_s])
49
+ Constants::FLAG_OPTIONS.each do |key|
50
+ options[key] = Annotate::Helpers.true?(ENV[key.to_s])
82
51
  end
83
- OTHER_OPTIONS.each do |key|
52
+ Constants::OTHER_OPTIONS.each do |key|
84
53
  options[key] = !ENV[key.to_s].blank? ? ENV[key.to_s] : nil
85
54
  end
86
- PATH_OPTIONS.each do |key|
55
+ Constants::PATH_OPTIONS.each do |key|
87
56
  options[key] = !ENV[key.to_s].blank? ? ENV[key.to_s].split(',') : []
88
57
  end
89
58
 
90
59
  options[:additional_file_patterns] ||= []
60
+ options[:additional_file_patterns] = options[:additional_file_patterns].split(',') if options[:additional_file_patterns].is_a?(String)
91
61
  options[:model_dir] = ['app/models'] if options[:model_dir].empty?
92
62
 
93
63
  options[:wrapper_open] ||= options[:wrapper]
94
64
  options[:wrapper_close] ||= options[:wrapper]
95
65
 
96
66
  # These were added in 2.7.0 but so this is to revert to old behavior by default
97
- options[:exclude_scaffolds] = Annotate.true?(ENV.fetch('exclude_scaffolds', 'true'))
98
- options[:exclude_controllers] = Annotate.true?(ENV.fetch('exclude_controllers', 'true'))
99
- options[:exclude_helpers] = Annotate.true?(ENV.fetch('exclude_helpers', 'true'))
67
+ options[:exclude_scaffolds] = Annotate::Helpers.true?(ENV.fetch('exclude_scaffolds', 'true'))
68
+ options[:exclude_controllers] = Annotate::Helpers.true?(ENV.fetch('exclude_controllers', 'true'))
69
+ options[:exclude_helpers] = Annotate::Helpers.true?(ENV.fetch('exclude_helpers', 'true'))
100
70
 
101
71
  options
102
72
  end
103
73
 
104
- def self.reset_options
105
- all_options.flatten.each { |key| ENV[key.to_s] = nil }
106
- end
107
-
108
- def self.skip_on_migration?
109
- ENV['ANNOTATE_SKIP_ON_DB_MIGRATE'] =~ Constants::TRUE_RE || ENV['skip_on_db_migrate'] =~ Constants::TRUE_RE
110
- end
111
-
112
- def self.include_routes?
113
- ENV['routes'] =~ Constants::TRUE_RE
114
- end
115
-
116
- def self.include_models?
117
- ENV['models'] =~ Constants::TRUE_RE
118
- end
119
-
120
- def self.loaded_tasks=(val)
121
- @loaded_tasks = val
122
- end
123
-
124
- def self.loaded_tasks
125
- @loaded_tasks
126
- end
127
-
128
74
  def self.load_tasks
129
- return if loaded_tasks
130
- self.loaded_tasks = true
75
+ return if @tasks_loaded
131
76
 
132
77
  Dir[File.join(File.dirname(__FILE__), 'tasks', '**/*.rake')].each do |rake|
133
78
  load rake
134
79
  end
135
- end
136
80
 
137
- def self.load_requires(options)
138
- options[:require].count > 0 &&
139
- options[:require].each { |path| require path }
81
+ @tasks_loaded = true
140
82
  end
141
83
 
142
84
  def self.eager_load(options)
@@ -192,13 +134,12 @@ module Annotate
192
134
  Rake::Task[:set_annotation_options].invoke
193
135
  end
194
136
 
195
- def self.fallback(*args)
196
- args.detect { |arg| !arg.blank? }
197
- end
137
+ class << self
138
+ private
198
139
 
199
- def self.true?(val)
200
- return false if val.blank?
201
- return false unless val =~ Constants::TRUE_RE
202
- true
140
+ def load_requires(options)
141
+ options[:require].count > 0 &&
142
+ options[:require].each { |path| require path }
143
+ end
203
144
  end
204
145
  end
@@ -38,9 +38,9 @@ module AnnotateModels
38
38
  BLUEPRINTS_TEST_DIR = File.join('test', "blueprints")
39
39
  BLUEPRINTS_SPEC_DIR = File.join('spec', "blueprints")
40
40
 
41
- # Factory Girl http://github.com/thoughtbot/factory_girl
42
- FACTORY_GIRL_TEST_DIR = File.join('test', "factories")
43
- FACTORY_GIRL_SPEC_DIR = File.join('spec', "factories")
41
+ # Factory Bot https://github.com/thoughtbot/factory_bot
42
+ FACTORY_BOT_TEST_DIR = File.join('test', "factories")
43
+ FACTORY_BOT_SPEC_DIR = File.join('spec', "factories")
44
44
 
45
45
  # Fabrication https://github.com/paulelliott/fabrication.git
46
46
  FABRICATORS_TEST_DIR = File.join('test', "fabricators")
@@ -82,6 +82,8 @@ module AnnotateModels
82
82
  }
83
83
  }.freeze
84
84
 
85
+ MAGIC_COMMENT_MATCHER = Regexp.new(/(^#\s*encoding:.*(?:\n|r\n))|(^# coding:.*(?:\n|\r\n))|(^# -\*- coding:.*(?:\n|\r\n))|(^# -\*- encoding\s?:.*(?:\n|\r\n))|(^#\s*frozen_string_literal:.+(?:\n|\r\n))|(^# -\*- frozen_string_literal\s*:.+-\*-(?:\n|\r\n))/).freeze
86
+
85
87
  class << self
86
88
  def annotate_pattern(options = {})
87
89
  if options[:wrapper_open]
@@ -140,12 +142,12 @@ module AnnotateModels
140
142
  File.join(root_directory, EXEMPLARS_SPEC_DIR, "%MODEL_NAME%_exemplar.rb"),
141
143
  File.join(root_directory, BLUEPRINTS_TEST_DIR, "%MODEL_NAME%_blueprint.rb"),
142
144
  File.join(root_directory, BLUEPRINTS_SPEC_DIR, "%MODEL_NAME%_blueprint.rb"),
143
- File.join(root_directory, FACTORY_GIRL_TEST_DIR, "%MODEL_NAME%_factory.rb"), # (old style)
144
- File.join(root_directory, FACTORY_GIRL_SPEC_DIR, "%MODEL_NAME%_factory.rb"), # (old style)
145
- File.join(root_directory, FACTORY_GIRL_TEST_DIR, "%TABLE_NAME%.rb"), # (new style)
146
- File.join(root_directory, FACTORY_GIRL_SPEC_DIR, "%TABLE_NAME%.rb"), # (new style)
147
- File.join(root_directory, FACTORY_GIRL_TEST_DIR, "%PLURALIZED_MODEL_NAME%.rb"), # (new style)
148
- File.join(root_directory, FACTORY_GIRL_SPEC_DIR, "%PLURALIZED_MODEL_NAME%.rb"), # (new style)
145
+ File.join(root_directory, FACTORY_BOT_TEST_DIR, "%MODEL_NAME%_factory.rb"), # (old style)
146
+ File.join(root_directory, FACTORY_BOT_SPEC_DIR, "%MODEL_NAME%_factory.rb"), # (old style)
147
+ File.join(root_directory, FACTORY_BOT_TEST_DIR, "%TABLE_NAME%.rb"), # (new style)
148
+ File.join(root_directory, FACTORY_BOT_SPEC_DIR, "%TABLE_NAME%.rb"), # (new style)
149
+ File.join(root_directory, FACTORY_BOT_TEST_DIR, "%PLURALIZED_MODEL_NAME%.rb"), # (new style)
150
+ File.join(root_directory, FACTORY_BOT_SPEC_DIR, "%PLURALIZED_MODEL_NAME%.rb"), # (new style)
149
151
  File.join(root_directory, FABRICATORS_TEST_DIR, "%MODEL_NAME%_fabricator.rb"),
150
152
  File.join(root_directory, FABRICATORS_SPEC_DIR, "%MODEL_NAME%_fabricator.rb")
151
153
  ]
@@ -154,7 +156,7 @@ module AnnotateModels
154
156
  def serialize_files(root_directory)
155
157
  [
156
158
  File.join(root_directory, SERIALIZERS_DIR, "%MODEL_NAME%_serializer.rb"),
157
- File.join(root_directory, SERIALIZERS_TEST_DIR, "%MODEL_NAME%_serializer_spec.rb"),
159
+ File.join(root_directory, SERIALIZERS_TEST_DIR, "%MODEL_NAME%_serializer_test.rb"),
158
160
  File.join(root_directory, SERIALIZERS_SPEC_DIR, "%MODEL_NAME%_serializer_spec.rb")
159
161
  ]
160
162
  end
@@ -244,16 +246,7 @@ module AnnotateModels
244
246
  info << "# #{ '-' * ( max_size + md_names_overhead ) } | #{'-' * md_type_allowance} | #{ '-' * 27 }\n"
245
247
  end
246
248
 
247
- cols = if ignore_columns = options[:ignore_columns]
248
- klass.columns.reject do |col|
249
- col.name.match(/#{ignore_columns}/)
250
- end
251
- else
252
- klass.columns
253
- end
254
-
255
- cols = cols.sort_by(&:name) if options[:sort]
256
- cols = classified_sort(cols) if options[:classified_sort]
249
+ cols = columns(klass, options)
257
250
  cols.each do |col|
258
251
  col_type = get_col_type(col)
259
252
  attrs = []
@@ -264,8 +257,8 @@ module AnnotateModels
264
257
 
265
258
  if col_type == 'decimal'
266
259
  col_type << "(#{col.precision}, #{col.scale})"
267
- elsif col_type != 'spatial'
268
- if col.limit
260
+ elsif !%w[spatial geometry geography].include?(col_type)
261
+ if col.limit && !options[:format_yard]
269
262
  if col.limit.is_a? Array
270
263
  attrs << "(#{col.limit.join(', ')})"
271
264
  else
@@ -304,8 +297,12 @@ module AnnotateModels
304
297
  end
305
298
  if options[:format_rdoc]
306
299
  info << sprintf("# %-#{max_size}.#{max_size}s<tt>%s</tt>", "*#{col_name}*::", attrs.unshift(col_type).join(", ")).rstrip + "\n"
300
+ elsif options[:format_yard]
301
+ info << sprintf("# @!attribute #{col_name}") + "\n"
302
+ ruby_class = col.respond_to?(:array) && col.array ? "Array<#{map_col_type_to_ruby_classes(col_type)}>": map_col_type_to_ruby_classes(col_type)
303
+ info << sprintf("# @return [#{ruby_class}]") + "\n"
307
304
  elsif options[:format_markdown]
308
- name_remainder = max_size - col_name.length
305
+ name_remainder = max_size - col_name.length - non_ascii_length(col_name)
309
306
  type_remainder = (md_type_allowance - 2) - col_type.length
310
307
  info << (sprintf("# **`%s`**%#{name_remainder}s | `%s`%#{type_remainder}s | `%s`", col_name, " ", col_type, " ", attrs.join(", ").rstrip)).gsub('``', ' ').rstrip + "\n"
311
308
  else
@@ -472,7 +469,10 @@ module AnnotateModels
472
469
  foreign_keys = klass.connection.foreign_keys(klass.table_name)
473
470
  return '' if foreign_keys.empty?
474
471
 
475
- format_name = ->(fk) { options[:show_complete_foreign_keys] ? fk.name : fk.name.gsub(/(?<=^fk_rails_)[0-9a-f]{10}$/, '...') }
472
+ format_name = lambda do |fk|
473
+ return fk.options[:column] if fk.name.blank?
474
+ options[:show_complete_foreign_keys] ? fk.name : fk.name.gsub(/(?<=^fk_rails_)[0-9a-f]{10}$/, '...')
475
+ end
476
476
 
477
477
  max_size = foreign_keys.map(&format_name).map(&:size).max + 1
478
478
  foreign_keys.sort_by {|fk| [format_name.call(fk), fk.column]}.each do |fk|
@@ -515,7 +515,7 @@ module AnnotateModels
515
515
  old_header = old_content.match(header_pattern).to_s
516
516
  new_header = info_block.match(header_pattern).to_s
517
517
 
518
- column_pattern = /^#[\t ]+[\w\*`]+[\t ]+.+$/
518
+ column_pattern = /^#[\t ]+[\w\*\.`]+[\t ]+.+$/
519
519
  old_columns = old_header && old_header.scan(column_pattern).sort
520
520
  new_columns = new_header && new_header.scan(column_pattern).sort
521
521
 
@@ -534,15 +534,15 @@ module AnnotateModels
534
534
  # need to insert it in correct position
535
535
  if old_annotation.empty? || options[:force]
536
536
  magic_comments_block = magic_comments_as_string(old_content)
537
- old_content.gsub!(magic_comment_matcher, '')
537
+ old_content.gsub!(MAGIC_COMMENT_MATCHER, '')
538
538
  old_content.sub!(annotate_pattern(options), '')
539
539
 
540
540
  new_content = if %w(after bottom).include?(options[position].to_s)
541
541
  magic_comments_block + (old_content.rstrip + "\n\n" + wrapped_info_block)
542
542
  elsif magic_comments_block.empty?
543
- magic_comments_block + wrapped_info_block + "\n" + old_content
543
+ magic_comments_block + wrapped_info_block + old_content.lstrip
544
544
  else
545
- magic_comments_block + "\n" + wrapped_info_block + "\n" + old_content
545
+ magic_comments_block + "\n" + wrapped_info_block + old_content.lstrip
546
546
  end
547
547
  else
548
548
  # replace the old annotation with the new one
@@ -558,12 +558,8 @@ module AnnotateModels
558
558
  true
559
559
  end
560
560
 
561
- def magic_comment_matcher
562
- Regexp.new(/(^#\s*encoding:.*(?:\n|r\n))|(^# coding:.*(?:\n|\r\n))|(^# -\*- coding:.*(?:\n|\r\n))|(^# -\*- encoding\s?:.*(?:\n|\r\n))|(^#\s*frozen_string_literal:.+(?:\n|\r\n))|(^# -\*- frozen_string_literal\s*:.+-\*-(?:\n|\r\n))/)
563
- end
564
-
565
561
  def magic_comments_as_string(content)
566
- magic_comments = content.scan(magic_comment_matcher).flatten.compact
562
+ magic_comments = content.scan(MAGIC_COMMENT_MATCHER).flatten.compact
567
563
 
568
564
  if magic_comments.any?
569
565
  magic_comments.join
@@ -902,13 +898,15 @@ module AnnotateModels
902
898
  end
903
899
 
904
900
  def max_schema_info_width(klass, options)
901
+ cols = columns(klass, options)
902
+
905
903
  if with_comments?(klass, options)
906
- max_size = klass.columns.map do |column|
904
+ max_size = cols.map do |column|
907
905
  column.name.size + (column.comment ? width(column.comment) : 0)
908
906
  end.max || 0
909
907
  max_size += 2
910
908
  else
911
- max_size = klass.column_names.map(&:size).max
909
+ max_size = cols.map(&:name).map(&:size).max
912
910
  end
913
911
  max_size += options[:format_rdoc] ? 5 : 1
914
912
 
@@ -920,7 +918,7 @@ module AnnotateModels
920
918
  end
921
919
 
922
920
  def width(string)
923
- string.chars.inject(0) { |acc, elem| acc + (elem.bytesize == 1 ? 1 : 2) }
921
+ string.chars.inject(0) { |acc, elem| acc + (elem.bytesize == 3 ? 2 : 1) }
924
922
  end
925
923
 
926
924
  def mb_chars_ljust(string, length)
@@ -932,6 +930,72 @@ module AnnotateModels
932
930
  string[0..length-1]
933
931
  end
934
932
  end
933
+
934
+ def non_ascii_length(string)
935
+ string.to_s.chars.reject(&:ascii_only?).length
936
+ end
937
+
938
+ def map_col_type_to_ruby_classes(col_type)
939
+ case col_type
940
+ when 'integer' then Integer.to_s
941
+ when 'float' then Float.to_s
942
+ when 'decimal' then BigDecimal.to_s
943
+ when 'datetime', 'timestamp', 'time' then Time.to_s
944
+ when 'date' then Date.to_s
945
+ when 'text', 'string', 'binary', 'inet', 'uuid' then String.to_s
946
+ when 'json', 'jsonb' then Hash.to_s
947
+ when 'boolean' then 'Boolean'
948
+ end
949
+ end
950
+
951
+ def columns(klass, options)
952
+ cols = klass.columns
953
+ cols += translated_columns(klass)
954
+
955
+ if ignore_columns = options[:ignore_columns]
956
+ cols = cols.reject do |col|
957
+ col.name.match(/#{ignore_columns}/)
958
+ end
959
+ end
960
+
961
+ cols = cols.sort_by(&:name) if options[:sort]
962
+ cols = classified_sort(cols) if options[:classified_sort]
963
+
964
+ cols
965
+ end
966
+
967
+ ##
968
+ # Add columns managed by the globalize gem if this gem is being used.
969
+ def translated_columns(klass)
970
+ return [] unless klass.respond_to? :translation_class
971
+
972
+ ignored_cols = ignored_translation_table_colums(klass)
973
+ klass.translation_class.columns.reject do |col|
974
+ ignored_cols.include? col.name.to_sym
975
+ end
976
+ end
977
+
978
+ ##
979
+ # These are the columns that the globalize gem needs to work but
980
+ # are not necessary for the models to be displayed as annotations.
981
+ def ignored_translation_table_colums(klass)
982
+ # Construct the foreign column name in the translations table
983
+ # eg. Model: Car, foreign column name: car_id
984
+ foreign_column_name = [
985
+ klass.translation_class.to_s
986
+ .gsub('::Translation', '').gsub('::', '_')
987
+ .downcase,
988
+ '_id'
989
+ ].join.to_sym
990
+
991
+ [
992
+ :id,
993
+ :created_at,
994
+ :updated_at,
995
+ :locale,
996
+ foreign_column_name
997
+ ]
998
+ end
935
999
  end
936
1000
 
937
1001
  class BadModelFileError < LoadError