Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to export annotations to a single file, support :with_comment_column in markdown, annotate Enumerize model attributes #1020

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@ when using `SpatialAdapter`, `PostgisAdapter` or `PostGISAdapter`:
# path :geometry line_string, 4326
```

It also annotates models with [Enumerize](https://github.com/brainspec/enumerize) attributes, documenting the possible values as a comment:

```ruby
class Document < ApplicationRecord
extend Enumerize
enumerize :document_type, in: %i[standard basic], default: :standard
# ...
end

# == Schema Information
#
# Table name: documents
#
# id :bigint(8) not null, primary key
# document_type :string default("standard"), not null Enum: [standard basic]
#
```

Also, if you pass the `-r` option, it'll annotate `routes.rb` with the output of `rake routes`.


Expand Down Expand Up @@ -148,6 +166,17 @@ Everything above applies, except that `--routes` is not meaningful,
and you will probably need to explicitly set one or more `--require` option(s), and/or one or more `--model-dir` options
to inform `annotate` about the structure of your project and help it bootstrap and load the relevant code.

### Export to a single file

To export annotations to a single file, specifiy a filename with the `--export-file` option:

annotate --export-file=doc/annotated_models.md

The default format for exports is `markdown` but it also supports `bare`:

annotate --export-file=doc/annotated_models.txt --export-file-format=bare
annotate --export-file=doc/annotated_models.md --export-file-format=markdown

## Configuration

If you want to always skip annotations on a particular model, add this string
Expand Down Expand Up @@ -251,6 +280,9 @@ you can do so with a simple environment variable, instead of editing the
--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
--export-file FILE Export schema infomation to a single file
--export-file-format FORMAT [markdown|bare]
Export schema infomation as markdown or plain text

### Option: `additional_file_patterns`

Expand Down
3 changes: 3 additions & 0 deletions lib/annotate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ def self.setup_options(options = {})
Constants::PATH_OPTIONS.each do |key|
options[key] = !ENV[key.to_s].blank? ? ENV[key.to_s].split(',') : []
end
Constants::EXPORT_FILE_OPTIONS.each do |key|
options[key] = !ENV[key.to_s].blank? ? ENV[key.to_s] : nil
end

options[:additional_file_patterns] ||= []
options[:additional_file_patterns] = options[:additional_file_patterns].split(',') if options[:additional_file_patterns].is_a?(String)
Expand Down
60 changes: 55 additions & 5 deletions lib/annotate/annotate_models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,17 @@ def get_schema_info(klass, header, options = {}) # rubocop:disable Metrics/Metho
max_size = max_schema_info_width(klass, options)
md_names_overhead = 6
md_type_allowance = 18
md_attributes_allowance = 45
bare_type_allowance = 16

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"
if options[:with_comment_column]
info << sprintf( "# %-#{max_size + md_names_overhead}.#{max_size + md_names_overhead}s | %-#{md_type_allowance}.#{md_type_allowance}s | %-#{md_attributes_allowance}.#{md_attributes_allowance}s | %s\n", 'Name', 'Type', 'Attributes', 'Comments' )
info << "# #{ '-' * ( max_size + md_names_overhead ) } | #{'-' * md_type_allowance} | #{'-' * md_attributes_allowance} | #{ '-' * 27 }\n"
else
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
end

cols = columns(klass, options)
Expand All @@ -165,7 +170,11 @@ def get_schema_info(klass, header, options = {}) # rubocop:disable Metrics/Metho
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 }]
col_enum_values = nil
if klass.respond_to?(:enumerized_attributes)
col_enum_values = klass.enumerized_attributes[col.name]&.values
end
[col.name, { col_type: col_type, attrs: attrs, col_name: col_name, simple_formatted_attrs: simple_formatted_attrs, col_comment: col_comment, col_enum_values: col_enum_values }]
end.to_h

# Output annotation
Expand All @@ -177,6 +186,7 @@ def get_schema_info(klass, header, options = {}) # rubocop:disable Metrics/Metho
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]
col_enum_values = cols_meta[col.name][:col_enum_values]

if options[:format_rdoc]
info << sprintf("# %-#{max_size}.#{max_size}s<tt>%s</tt>", "*#{col_name}*::", attrs.unshift(col_type).join(", ")).rstrip + "\n"
Expand All @@ -187,7 +197,16 @@ def get_schema_info(klass, header, options = {}) # rubocop:disable Metrics/Metho
elsif options[:format_markdown]
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"
if options[:with_comment_column]
attrs_string = attrs.join(", ").rstrip
attrs_remainder = (md_attributes_allowance - 2) - attrs_string.length
if col_enum_values.present?
col_comment = "#{col_comment} Enum: `#{col_enum_values.join("`, `")}`".strip
end
info << (sprintf("# **`%s`**%#{name_remainder}s | `%s`%#{type_remainder}s | `%s`%#{attrs_remainder}s | %s", col_name, " ", col_type, " ", attrs_string, " ", col_comment)).gsub('``', ' ').rstrip + "\n"
else
info << (sprintf("# **`%s`**%#{name_remainder}s | `%s`%#{type_remainder}s | `%s`", col_name, " ", col_type, " ", attrs.join(", ").rstrip)).gsub('``', ' ').rstrip + "\n"
end
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
Expand Down Expand Up @@ -546,6 +565,15 @@ def annotate(klass, file, header, options = {})
annotated << model_file_name
end

if options[:export_file].present?
export_options = options.clone
export_options[:format_bare] = options[:export_file_format] == "bare"
export_options[:format_markdown] = options[:export_file_format] == "markdown"
export_info = get_schema_info(klass, header, export_options)
schemaless_table_name = table_name.split('.').last
@annotated_exports[schemaless_table_name] = export_info
end

matched_types(options).each do |key|
exclusion_key = "exclude_#{key.pluralize}".to_sym
position_key = "position_in_#{key}".to_sym
Expand Down Expand Up @@ -711,6 +739,11 @@ def do_annotations(options = {})
header << "\n# Schema version: #{version}"
end

@annotated_exports = {}
if options[:export_file].present? && File.exist?(options[:export_file])
File.truncate(options[:export_file], 0)
end

annotated = []
get_model_files(options).each do |path, filename|
annotate_model_file(annotated, File.join(path, filename), header, options)
Expand All @@ -720,6 +753,23 @@ def do_annotations(options = {})
puts 'Model files unchanged.'
else
puts "Annotated (#{annotated.length}): #{annotated.join(', ')}"

if options[:export_file].present?
# apply post-processing to the exported content:
# - remove the "Schema info" headers
# - remove leading comment chars (#) since the exported file is not a ruby file
@annotated_exports.sort.each do |_table_name, export_info|
File.open(options[:export_file], "ab") do |f|
if options[:export_file_format] == "markdown"
# for markdown, format the table name line with an H1 header style
f.puts export_info.gsub(header, "").gsub(/^#\s/, "").gsub(/Table name:/, "# Table:")
else
f.puts export_info.gsub(header, "").gsub(/^#\s/, "")
end
end
end
puts "Exported to file: #{options[:export_file]}"
end
end
end

Expand Down
6 changes: 5 additions & 1 deletion lib/annotate/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ module Constants
:require, :model_dir, :root_dir
].freeze

EXPORT_FILE_OPTIONS = [
:export_file, :export_file_format
].freeze

ALL_ANNOTATE_OPTIONS = [
POSITION_OPTIONS, FLAG_OPTIONS, OTHER_OPTIONS, PATH_OPTIONS
POSITION_OPTIONS, FLAG_OPTIONS, OTHER_OPTIONS, PATH_OPTIONS, EXPORT_FILE_OPTIONS
].freeze
end
end
11 changes: 11 additions & 0 deletions lib/annotate/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def self.parse(args, env = {})
FILE_TYPE_POSITIONS = %w[position_in_class position_in_factory position_in_fixture position_in_test position_in_routes position_in_serializer].freeze
EXCLUSION_LIST = %w[tests fixtures factories serializers].freeze
FORMAT_TYPES = %w[bare rdoc yard markdown].freeze
EXPORT_FILE_FORMAT_TYPES = %w[markdown bare].freeze

def initialize(args, env)
@args = args
Expand Down Expand Up @@ -309,6 +310,16 @@ def add_options_to_parser(option_parser) # rubocop:disable Metrics/MethodLength,
"include database comments in model annotations, as its own column, after all others") do
env['with_comment_column'] = 'true'
end

option_parser.on("--export-file FILE",
"Export schema infomation to a single file") do |export_file|
env["export_file"] = export_file
end

option_parser.on("--export-file-format FORMAT [markdown|bare]", EXPORT_FILE_FORMAT_TYPES,
'Export schema infomation as markdown or plain text') do |export_file_format|
env["export_file_format"] = export_file_format
end
end
end
end
2 changes: 1 addition & 1 deletion lib/annotate/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Annotate
def self.version
'3.2.0'
'3.2.1'
end
end
4 changes: 3 additions & 1 deletion lib/generators/annotate/templates/auto_annotate_models.rake
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ if Rails.env.development?
'wrapper_open' => nil,
'wrapper_close' => nil,
'with_comment' => 'true',
'with_comment_column' => 'false'
'with_comment_column' => 'false',
'export_file' => nil,
'export_file_format' => 'markdown'
)
end

Expand Down
2 changes: 2 additions & 0 deletions lib/tasks/annotate_models.rake
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ task annotate_models: :environment do
options[:with_comment] = Annotate::Helpers.true?(ENV['with_comment'])
options[:with_comment_column] = Annotate::Helpers.true?(ENV['with_comment_column'])
options[:ignore_unknown_models] = Annotate::Helpers.true?(ENV.fetch('ignore_unknown_models', 'false'))
options[:export_file] = ENV.fetch('export_file', nil)
options[:export_file_format] = ENV.fetch('export_file_format', 'markdown')

AnnotateModels.do_annotations(options)
end
Expand Down
Loading