diff --git a/README.md b/README.md index abf325ff..bac488d5 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,7 @@ you can do so with a simple environment variable, instead of editing the don't show default for given column types, separated by commas (e.g. `json,jsonb,hstore`) --ignore-unknown-models don't display warnings for bad model files --with-comment include database comments in model annotations + --with-comment-column include database comments in model annotations, as its own column, after all others ### Option: `additional_file_patterns` diff --git a/lib/annotate/annotate_models.rb b/lib/annotate/annotate_models.rb index ff01a3d9..dc2901a3 100644 --- a/lib/annotate/annotate_models.rb +++ b/lib/annotate/annotate_models.rb @@ -146,18 +146,37 @@ def get_schema_info(klass, header, options = {}) # rubocop:disable Metrics/Metho 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 = columns(klass, options) - cols.each do |col| + with_comments = with_comments?(klass, options) + with_comments_column = with_comments_column?(klass, options) + + # Precalculate Values + cols_meta = cols.map do |col| + col_comment = with_comments || with_comments_column ? col.comment&.gsub(/\n/, "\\n") : nil col_type = get_col_type(col) attrs = get_attributes(col, col_type, klass, options) - col_name = if with_comments?(klass, options) && col.comment - "#{col.name}(#{col.comment.gsub(/\n/, "\\n")})" + col_name = if with_comments && col_comment + "#{col.name}(#{col_comment})" else col.name end + simple_formatted_attrs = attrs.join(", ") + [col.name, { col_type: col_type, attrs: attrs, col_name: col_name, simple_formatted_attrs: simple_formatted_attrs, col_comment: col_comment }] + end.to_h + + # Output annotation + bare_max_attrs_length = cols_meta.map { |_, m| m[:simple_formatted_attrs].length }.max + + cols.each do |col| + col_type = cols_meta[col.name][:col_type] + attrs = cols_meta[col.name][:attrs] + col_name = cols_meta[col.name][:col_name] + simple_formatted_attrs = cols_meta[col.name][:simple_formatted_attrs] + col_comment = cols_meta[col.name][:col_comment] if options[:format_rdoc] info << sprintf("# %-#{max_size}.#{max_size}s%s", "*#{col_name}*::", attrs.unshift(col_type).join(", ")).rstrip + "\n" @@ -169,8 +188,10 @@ def get_schema_info(klass, header, options = {}) # rubocop:disable Metrics/Metho name_remainder = max_size - col_name.length - non_ascii_length(col_name) type_remainder = (md_type_allowance - 2) - col_type.length info << (sprintf("# **`%s`**%#{name_remainder}s | `%s`%#{type_remainder}s | `%s`", col_name, " ", col_type, " ", attrs.join(", ").rstrip)).gsub('``', ' ').rstrip + "\n" + elsif with_comments_column + info << format_default(col_name, max_size, col_type, bare_type_allowance, simple_formatted_attrs, bare_max_attrs_length, col_comment) else - info << format_default(col_name, max_size, col_type, bare_type_allowance, attrs) + info << format_default(col_name, max_size, col_type, bare_type_allowance, simple_formatted_attrs) end end @@ -798,6 +819,12 @@ def with_comments?(klass, options) klass.columns.any? { |col| !col.comment.nil? } end + def with_comments_column?(klass, options) + options[:with_comment_column] && + klass.columns.first.respond_to?(:comment) && + klass.columns.any? { |col| !col.comment.nil? } + end + def max_schema_info_width(klass, options) cols = columns(klass, options) @@ -814,8 +841,15 @@ def max_schema_info_width(klass, options) max_size end - def format_default(col_name, max_size, col_type, bare_type_allowance, attrs) - sprintf("# %s:%s %s", mb_chars_ljust(col_name, max_size), mb_chars_ljust(col_type, bare_type_allowance), attrs.join(", ")).rstrip + "\n" + # rubocop:disable Metrics/ParameterLists + def format_default(col_name, max_size, col_type, bare_type_allowance, simple_formatted_attrs, bare_max_attrs_length = 0, col_comment = nil) + sprintf( + "# %s:%s %s %s", + mb_chars_ljust(col_name, max_size), + mb_chars_ljust(col_type, bare_type_allowance), + mb_chars_ljust(simple_formatted_attrs, bare_max_attrs_length), + col_comment + ).rstrip + "\n" end def width(string) diff --git a/lib/annotate/constants.rb b/lib/annotate/constants.rb index 57a26151..0d322565 100644 --- a/lib/annotate/constants.rb +++ b/lib/annotate/constants.rb @@ -18,7 +18,7 @@ module Constants :trace, :timestamp, :exclude_serializers, :classified_sort, :show_foreign_keys, :show_complete_foreign_keys, :exclude_scaffolds, :exclude_controllers, :exclude_helpers, - :exclude_sti_subclasses, :ignore_unknown_models, :with_comment, + :exclude_sti_subclasses, :ignore_unknown_models, :with_comment, :with_comment_column, :show_check_constraints ].freeze diff --git a/lib/annotate/parser.rb b/lib/annotate/parser.rb index 3f5ebdb0..ad85caf5 100644 --- a/lib/annotate/parser.rb +++ b/lib/annotate/parser.rb @@ -304,6 +304,11 @@ def add_options_to_parser(option_parser) # rubocop:disable Metrics/MethodLength, "include database comments in model annotations") do env['with_comment'] = 'true' end + + option_parser.on('--with-comment-column', + "include database comments in model annotations, as its own column, after all others") do + env['with_comment_column'] = 'true' + end end end end diff --git a/spec/lib/annotate/annotate_models_spec.rb b/spec/lib/annotate/annotate_models_spec.rb index 33b22017..09647461 100644 --- a/spec/lib/annotate/annotate_models_spec.rb +++ b/spec/lib/annotate/annotate_models_spec.rb @@ -1308,6 +1308,146 @@ def mock_column(name, type, options = {}) end end end + + context 'when "with_comment_column" is specified in options' do + let :options do + { with_comment_column: 'yes' } + end + + context 'when columns have comments' do + let :columns do + [ + mock_column(:id, :integer, limit: 8, comment: 'ID'), + mock_column(:active, :boolean, limit: 1, comment: 'Active'), + mock_column(:name, :string, limit: 50, comment: 'Name'), + mock_column(:notes, :text, limit: 55, comment: 'Notes'), + mock_column(:no_comment, :text, limit: 20, comment: nil) + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null, primary key ID + # active :boolean not null Active + # name :string(50) not null Name + # notes :text(55) not null Notes + # no_comment :text(20) not null + # + EOS + end + + it 'works with option "with_comment_column"' do + is_expected.to eq expected_result + end + end + + context 'when columns have multibyte comments' do + let :columns do + [ + mock_column(:id, :integer, limit: 8, comment: 'ID'), + mock_column(:active, :boolean, limit: 1, comment: 'ACTIVE'), + mock_column(:name, :string, limit: 50, comment: 'NAME'), + mock_column(:notes, :text, limit: 55, comment: 'NOTES'), + mock_column(:cyrillic, :text, limit: 30, comment: 'Кириллица'), + mock_column(:japanese, :text, limit: 60, comment: '熊本大学 イタリア 宝島'), + mock_column(:arabic, :text, limit: 20, comment: 'لغة'), + mock_column(:no_comment, :text, limit: 20, comment: nil), + mock_column(:location, :geometry_collection, limit: nil, comment: nil) + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null, primary key ID + # active :boolean not null ACTIVE + # name :string(50) not null NAME + # notes :text(55) not null NOTES + # cyrillic :text(30) not null Кириллица + # japanese :text(60) not null 熊本大学 イタリア 宝島 + # arabic :text(20) not null لغة + # no_comment :text(20) not null + # location :geometry_collect not null + # + EOS + end + + it 'works with option "with_comment_column"' do + is_expected.to eq expected_result + end + end + + context 'when columns have multiline comments' do + let :columns do + [ + mock_column(:id, :integer, limit: 8, comment: 'ID'), + mock_column(:notes, :text, limit: 55, comment: "Notes.\nMay include things like notes."), + mock_column(:no_comment, :text, limit: 20, comment: nil) + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null, primary key ID + # notes :text(55) not null Notes.\\nMay include things like notes. + # no_comment :text(20) not null + # + EOS + end + + it 'works with option "with_comment_column"' do + is_expected.to eq expected_result + end + end + + context 'when geometry columns are included' do + let :columns do + [ + mock_column(:id, :integer, limit: 8), + mock_column(:active, :boolean, default: false, null: false), + mock_column(:geometry, :geometry, + geometric_type: 'Geometry', srid: 4326, + limit: { srid: 4326, type: 'geometry' }), + mock_column(:location, :geography, + geometric_type: 'Point', srid: 0, + limit: { srid: 0, type: 'geometry' }), + mock_column(:non_srid, :geography, + geometric_type: 'Point', + limit: { type: 'geometry' }) + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null, primary key + # active :boolean default(FALSE), not null + # geometry :geometry not null, geometry, 4326 + # location :geography not null, point, 0 + # non_srid :geography not null, point + # + EOS + end + + it 'works with option "with_comment_column"' do + is_expected.to eq expected_result + end + end + end end end end diff --git a/spec/lib/annotate/parser_spec.rb b/spec/lib/annotate/parser_spec.rb index 16a18e25..16084b02 100644 --- a/spec/lib/annotate/parser_spec.rb +++ b/spec/lib/annotate/parser_spec.rb @@ -560,5 +560,15 @@ module Annotate # rubocop:disable Metrics/ModuleLength Parser.parse([option]) end end + + describe '--with-comment-column' do + let(:option) { '--with-comment-column' } + let(:env_key) { 'with_comment_column' } + let(:set_value) { 'true' } + it 'sets the ENV variable' do + expect(ENV).to receive(:[]=).with(env_key, set_value) + Parser.parse([option]) + end + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 91df96e3..e461e55b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -36,4 +36,5 @@ RSpec.configure do |config| config.order = 'random' + config.filter_run_when_matching :focus end