diff --git a/README.md b/README.md index bac488d5..27ff90b6 100644 --- a/README.md +++ b/README.md @@ -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`. @@ -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 @@ -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` diff --git a/lib/annotate.rb b/lib/annotate.rb index 7c54e9ea..744fd936 100644 --- a/lib/annotate.rb +++ b/lib/annotate.rb @@ -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) diff --git a/lib/annotate/annotate_models.rb b/lib/annotate/annotate_models.rb index dc2901a3..12f5968e 100644 --- a/lib/annotate/annotate_models.rb +++ b/lib/annotate/annotate_models.rb @@ -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) @@ -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 @@ -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%s", "*#{col_name}*::", attrs.unshift(col_type).join(", ")).rstrip + "\n" @@ -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 @@ -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 @@ -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) @@ -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 diff --git a/lib/annotate/constants.rb b/lib/annotate/constants.rb index 0d322565..fd61c2fe 100644 --- a/lib/annotate/constants.rb +++ b/lib/annotate/constants.rb @@ -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 diff --git a/lib/annotate/parser.rb b/lib/annotate/parser.rb index ad85caf5..45f0c612 100644 --- a/lib/annotate/parser.rb +++ b/lib/annotate/parser.rb @@ -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 @@ -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 diff --git a/lib/annotate/version.rb b/lib/annotate/version.rb index e103c6b8..f63d8a3e 100644 --- a/lib/annotate/version.rb +++ b/lib/annotate/version.rb @@ -1,5 +1,5 @@ module Annotate def self.version - '3.2.0' + '3.2.1' end end diff --git a/lib/generators/annotate/templates/auto_annotate_models.rake b/lib/generators/annotate/templates/auto_annotate_models.rake index 61cdcd7a..9377bf50 100644 --- a/lib/generators/annotate/templates/auto_annotate_models.rake +++ b/lib/generators/annotate/templates/auto_annotate_models.rake @@ -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 diff --git a/lib/tasks/annotate_models.rake b/lib/tasks/annotate_models.rake index 776f97ba..aa647d5e 100644 --- a/lib/tasks/annotate_models.rake +++ b/lib/tasks/annotate_models.rake @@ -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