From 916e67978f97e2eb7ca6a9703ec53c4de865ad1e Mon Sep 17 00:00:00 2001 From: Kevin Southworth Date: Thu, 6 Jun 2024 13:22:43 -0400 Subject: [PATCH 01/10] ability to export model annotations to a single file --- lib/annotate.rb | 3 ++ lib/annotate/annotate_models.rb | 31 +++++++++++++++++++ lib/annotate/constants.rb | 6 +++- lib/annotate/parser.rb | 12 +++++++ .../templates/auto_annotate_models.rake | 4 ++- lib/tasks/annotate_models.rake | 2 ++ 6 files changed, 56 insertions(+), 2 deletions(-) diff --git a/lib/annotate.rb b/lib/annotate.rb index 7c54e9ea6..744fd9361 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 dc2901a32..e556f7f56 100644 --- a/lib/annotate/annotate_models.rb +++ b/lib/annotate/annotate_models.rb @@ -546,6 +546,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 +720,11 @@ def do_annotations(options = {}) header << "\n# Schema version: #{version}" end + @annotated_exports = {} + if options[:export_file].present? && File.exists?(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 +734,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 0d3225659..fd61c2fea 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 ad85caf50..2d90d4363 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,17 @@ 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/generators/annotate/templates/auto_annotate_models.rake b/lib/generators/annotate/templates/auto_annotate_models.rake index 61cdcd7a1..9377bf50d 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 776f97ba3..aa647d5ef 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 From 4a656f14c6bd4cf42f32ebed4ded1dd421f13c6b Mon Sep 17 00:00:00 2001 From: Kevin Southworth Date: Thu, 6 Jun 2024 13:45:55 -0400 Subject: [PATCH 02/10] support :with_comment_column option in markdown format --- lib/annotate/annotate_models.rb | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/annotate/annotate_models.rb b/lib/annotate/annotate_models.rb index e556f7f56..56ac326f2 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) @@ -187,7 +192,13 @@ 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 + info << (sprintf("# **`%s`**%#{name_remainder}s | `%s`%#{type_remainder}s | `%s`%#{attrs_remainder}s | %s", col_name, " ", col_type, " ", attrs_string, " ", col_comment&.strip)).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 From 17014385da7a828ed5deafc51a3944398cf7c17c Mon Sep 17 00:00:00 2001 From: Kevin Southworth Date: Thu, 6 Jun 2024 14:21:43 -0400 Subject: [PATCH 03/10] annotate Enumerized model attributes as comments Enumerize gem https://github.com/brainspec/enumerize --- lib/annotate/annotate_models.rb | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/annotate/annotate_models.rb b/lib/annotate/annotate_models.rb index 56ac326f2..fc42b2023 100644 --- a/lib/annotate/annotate_models.rb +++ b/lib/annotate/annotate_models.rb @@ -170,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 @@ -182,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" @@ -195,11 +200,17 @@ def get_schema_info(klass, header, options = {}) # rubocop:disable Metrics/Metho if options[:with_comment_column] attrs_string = attrs.join(", ").rstrip attrs_remainder = (md_attributes_allowance - 2) - attrs_string.length - info << (sprintf("# **`%s`**%#{name_remainder}s | `%s`%#{type_remainder}s | `%s`%#{attrs_remainder}s | %s", col_name, " ", col_type, " ", attrs_string, " ", col_comment&.strip)).gsub('``', ' ').rstrip + "\n" + 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 + if col_enum_values.present? + col_comment = "#{col_comment} Enum: [#{col_enum_values.join(" ")}]".strip + end 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, simple_formatted_attrs) From 0facbd66dd566fbc7a4dd207b150e5db132fa4fd Mon Sep 17 00:00:00 2001 From: Kevin Southworth Date: Thu, 6 Jun 2024 14:31:03 -0400 Subject: [PATCH 04/10] Update README.md with export file options --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index bac488d5d..313cd594e 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,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 +262,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` From 301955631090bf257e28034bdcbd8d689f5a6973 Mon Sep 17 00:00:00 2001 From: Kevin Southworth Date: Thu, 6 Jun 2024 14:37:33 -0400 Subject: [PATCH 05/10] Update READM.md to mention Enumerize gem support --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 313cd594e..27ff90b60 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`. From 785af910fca799aa0a1f55c2e64bee58f38bf7db Mon Sep 17 00:00:00 2001 From: Kevin Southworth Date: Thu, 6 Jun 2024 14:56:59 -0400 Subject: [PATCH 06/10] document Enumerize values even if no table comment exists --- lib/annotate/annotate_models.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/annotate/annotate_models.rb b/lib/annotate/annotate_models.rb index fc42b2023..267e94ea6 100644 --- a/lib/annotate/annotate_models.rb +++ b/lib/annotate/annotate_models.rb @@ -207,9 +207,9 @@ def get_schema_info(klass, header, options = {}) # rubocop:disable Metrics/Metho 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 + elsif with_comments_column || col_enum_values.present? if col_enum_values.present? - col_comment = "#{col_comment} Enum: [#{col_enum_values.join(" ")}]".strip + col_comment = "#{col_comment} Enum: #{col_enum_values.join(" | ")}".strip end info << format_default(col_name, max_size, col_type, bare_type_allowance, simple_formatted_attrs, bare_max_attrs_length, col_comment) else From 1da3d2f882f2f49575fe64983ca6ca8a4fa220cf Mon Sep 17 00:00:00 2001 From: Kevin Southworth Date: Thu, 6 Jun 2024 14:59:16 -0400 Subject: [PATCH 07/10] bump version to 3.2.1 --- lib/annotate/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/annotate/version.rb b/lib/annotate/version.rb index e103c6b87..f63d8a3e9 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 From 764185f6ac1e455db9a30d4e1d2c67d14569cd8c Mon Sep 17 00:00:00 2001 From: Kevin Southworth Date: Thu, 6 Jun 2024 15:07:25 -0400 Subject: [PATCH 08/10] don't document Enumerize values in model classes it's redundant to document Enumerize values in model annotations, since the values are already specified by the `enumerize` class method --- lib/annotate/annotate_models.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/annotate/annotate_models.rb b/lib/annotate/annotate_models.rb index 267e94ea6..32186ffed 100644 --- a/lib/annotate/annotate_models.rb +++ b/lib/annotate/annotate_models.rb @@ -207,10 +207,7 @@ def get_schema_info(klass, header, options = {}) # rubocop:disable Metrics/Metho 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 || col_enum_values.present? - if col_enum_values.present? - col_comment = "#{col_comment} Enum: #{col_enum_values.join(" | ")}".strip - 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 info << format_default(col_name, max_size, col_type, bare_type_allowance, simple_formatted_attrs) From 7f8ad3d0caa09f22f625b20623753cf6356e517d Mon Sep 17 00:00:00 2001 From: Kevin Southworth Date: Thu, 6 Jun 2024 15:14:53 -0400 Subject: [PATCH 09/10] fix Rubocop warnings --- lib/annotate/annotate_models.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/annotate/annotate_models.rb b/lib/annotate/annotate_models.rb index 32186ffed..12f5968ec 100644 --- a/lib/annotate/annotate_models.rb +++ b/lib/annotate/annotate_models.rb @@ -740,7 +740,7 @@ def do_annotations(options = {}) end @annotated_exports = {} - if options[:export_file].present? && File.exists?(options[:export_file]) + if options[:export_file].present? && File.exist?(options[:export_file]) File.truncate(options[:export_file], 0) end @@ -758,13 +758,13 @@ def do_annotations(options = {}) # 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| + @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:") + f.puts export_info.gsub(header, "").gsub(/^#\s/, "").gsub(/Table name:/, "# Table:") else - f.puts export_info.gsub(header,"").gsub(/^#\s/, "") + f.puts export_info.gsub(header, "").gsub(/^#\s/, "") end end end From 34a00b0b8288456ffda188d7b25da052ccfbd602 Mon Sep 17 00:00:00 2001 From: Kevin Southworth Date: Thu, 6 Jun 2024 15:15:34 -0400 Subject: [PATCH 10/10] fix rubocop warning --- lib/annotate/parser.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/annotate/parser.rb b/lib/annotate/parser.rb index 2d90d4363..45f0c612a 100644 --- a/lib/annotate/parser.rb +++ b/lib/annotate/parser.rb @@ -320,7 +320,6 @@ def add_options_to_parser(option_parser) # rubocop:disable Metrics/MethodLength, 'Export schema infomation as markdown or plain text') do |export_file_format| env["export_file_format"] = export_file_format end - end end end