Skip to content
Open
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