annotaterb 4.1.1 → 4.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +68 -0
- data/VERSION +1 -1
- data/exe/annotaterb +7 -7
- data/lib/annotate_rb/active_record_patch.rb +2 -0
- data/lib/annotate_rb/commands/annotate_models.rb +0 -1
- data/lib/annotate_rb/commands/annotate_routes.rb +0 -1
- data/lib/annotate_rb/commands/print_help.rb +0 -1
- data/lib/annotate_rb/commands/print_version.rb +0 -1
- data/lib/annotate_rb/commands.rb +4 -4
- data/lib/annotate_rb/config_finder.rb +1 -1
- data/lib/annotate_rb/config_loader.rb +2 -2
- data/lib/annotate_rb/core.rb +3 -3
- data/lib/annotate_rb/helper.rb +16 -0
- data/lib/annotate_rb/model_annotator/{annotation_generator.rb → annotation_builder.rb} +18 -14
- data/lib/annotate_rb/model_annotator/annotation_decider.rb +10 -12
- data/lib/annotate_rb/model_annotator/annotation_diff.rb +1 -1
- data/lib/annotate_rb/model_annotator/annotation_diff_generator.rb +9 -9
- data/lib/annotate_rb/model_annotator/annotation_pattern_generator.rb +3 -3
- data/lib/annotate_rb/model_annotator/annotator.rb +12 -53
- data/lib/annotate_rb/model_annotator/column_annotation/annotation_builder.rb +135 -0
- data/lib/annotate_rb/model_annotator/column_annotation/attributes_builder.rb +104 -0
- data/lib/annotate_rb/model_annotator/column_annotation/column_wrapper.rb +103 -0
- data/lib/annotate_rb/model_annotator/column_annotation/type_builder.rb +54 -0
- data/lib/annotate_rb/model_annotator/column_annotation.rb +12 -0
- data/lib/annotate_rb/model_annotator/file_builder.rb +58 -0
- data/lib/annotate_rb/model_annotator/file_components.rb +78 -0
- data/lib/annotate_rb/model_annotator/file_name_resolver.rb +3 -3
- data/lib/annotate_rb/model_annotator/foreign_key_annotation/annotation_builder.rb +57 -0
- data/lib/annotate_rb/model_annotator/foreign_key_annotation.rb +9 -0
- data/lib/annotate_rb/model_annotator/index_annotation/annotation_builder.rb +113 -0
- data/lib/annotate_rb/model_annotator/index_annotation.rb +9 -0
- data/lib/annotate_rb/model_annotator/magic_comment_parser.rb +32 -0
- data/lib/annotate_rb/model_annotator/model_class_getter.rb +6 -6
- data/lib/annotate_rb/model_annotator/model_files_getter.rb +12 -10
- data/lib/annotate_rb/model_annotator/model_wrapper.rb +44 -37
- data/lib/annotate_rb/model_annotator/pattern_getter.rb +142 -10
- data/lib/annotate_rb/model_annotator/project_annotation_remover.rb +65 -0
- data/lib/annotate_rb/model_annotator/project_annotator.rb +63 -0
- data/lib/annotate_rb/model_annotator/related_files_list_builder.rb +16 -18
- data/lib/annotate_rb/model_annotator/single_file_annotation_remover.rb +37 -0
- data/lib/annotate_rb/model_annotator/single_file_annotator.rb +49 -0
- data/lib/annotate_rb/model_annotator/{file_annotator_instruction.rb → single_file_annotator_instruction.rb} +2 -2
- data/lib/annotate_rb/model_annotator/single_file_remove_annotation_instruction.rb +15 -0
- data/lib/annotate_rb/model_annotator.rb +25 -26
- data/lib/annotate_rb/options.rb +20 -25
- data/lib/annotate_rb/parser.rb +150 -142
- data/lib/annotate_rb/rake_bootstrapper.rb +8 -8
- data/lib/annotate_rb/route_annotator/annotation_processor.rb +7 -8
- data/lib/annotate_rb/route_annotator/annotator.rb +2 -2
- data/lib/annotate_rb/route_annotator/base_processor.rb +3 -3
- data/lib/annotate_rb/route_annotator/header_generator.rb +15 -15
- data/lib/annotate_rb/route_annotator/helper.rb +9 -9
- data/lib/annotate_rb/route_annotator/removal_processor.rb +4 -4
- data/lib/annotate_rb/route_annotator.rb +6 -6
- data/lib/annotate_rb/runner.rb +0 -4
- data/lib/annotate_rb/tasks/annotate_models_migrate.rake +5 -5
- data/lib/annotate_rb.rb +19 -19
- data/lib/generators/annotate_rb/install/install_generator.rb +3 -2
- data/lib/generators/annotate_rb/install/templates/annotate_rb.rake +1 -1
- metadata +22 -16
- data/lib/annotate_rb/model_annotator/column_annotation_builder.rb +0 -92
- data/lib/annotate_rb/model_annotator/column_attributes_builder.rb +0 -102
- data/lib/annotate_rb/model_annotator/column_type_builder.rb +0 -51
- data/lib/annotate_rb/model_annotator/column_wrapper.rb +0 -84
- data/lib/annotate_rb/model_annotator/constants.rb +0 -22
- data/lib/annotate_rb/model_annotator/file_annotation_remover.rb +0 -25
- data/lib/annotate_rb/model_annotator/file_annotator.rb +0 -77
- data/lib/annotate_rb/model_annotator/file_patterns.rb +0 -129
- data/lib/annotate_rb/model_annotator/foreign_key_annotation_builder.rb +0 -55
- data/lib/annotate_rb/model_annotator/helper.rb +0 -107
- data/lib/annotate_rb/model_annotator/index_annotation_builder.rb +0 -74
- data/lib/annotate_rb/model_annotator/model_file_annotator.rb +0 -55
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnnotateRb
|
4
|
+
module ModelAnnotator
|
5
|
+
module ColumnAnnotation
|
6
|
+
class AttributesBuilder
|
7
|
+
# Don't show default value for these column types
|
8
|
+
NO_DEFAULT_COL_TYPES = %w[json jsonb hstore].freeze
|
9
|
+
|
10
|
+
def initialize(column, options, is_primary_key, column_indices)
|
11
|
+
@column = ColumnWrapper.new(column)
|
12
|
+
@options = options
|
13
|
+
@is_primary_key = is_primary_key
|
14
|
+
@column_indices = column_indices
|
15
|
+
end
|
16
|
+
|
17
|
+
# Get the list of attributes that should be included in the annotation for
|
18
|
+
# a given column.
|
19
|
+
def build
|
20
|
+
column_type = @column.column_type_string
|
21
|
+
attrs = []
|
22
|
+
|
23
|
+
unless @column.default.nil? || hide_default?
|
24
|
+
schema_default = "default(#{@column.default_string})"
|
25
|
+
|
26
|
+
attrs << schema_default
|
27
|
+
end
|
28
|
+
|
29
|
+
if @column.unsigned?
|
30
|
+
attrs << "unsigned"
|
31
|
+
end
|
32
|
+
|
33
|
+
if !@column.null
|
34
|
+
attrs << "not null"
|
35
|
+
end
|
36
|
+
|
37
|
+
if @is_primary_key
|
38
|
+
attrs << "primary key"
|
39
|
+
end
|
40
|
+
|
41
|
+
is_special_type = %w[spatial geometry geography].include?(column_type)
|
42
|
+
is_decimal_type = column_type == "decimal"
|
43
|
+
|
44
|
+
if !is_decimal_type && !is_special_type
|
45
|
+
if @column.limit && !@options[:format_yard]
|
46
|
+
if @column.limit.is_a?(Array)
|
47
|
+
attrs << "(#{@column.limit.join(", ")})"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Check out if we got an array column
|
53
|
+
if @column.array?
|
54
|
+
attrs << "is an Array"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Check out if we got a geometric column
|
58
|
+
# and print the type and SRID
|
59
|
+
if @column.geometry_type?
|
60
|
+
attrs << "#{@column.geometry_type}, #{@column.srid}"
|
61
|
+
elsif @column.geometric_type? && @column.geometric_type.present?
|
62
|
+
attrs << "#{@column.geometric_type.to_s.downcase}, #{@column.srid}"
|
63
|
+
end
|
64
|
+
|
65
|
+
# Check if the column has indices and print "indexed" if true
|
66
|
+
# If the index includes another column, print it too.
|
67
|
+
if @options[:simple_indexes]
|
68
|
+
# Note: there used to be a klass.table_exists? call here, but removed it as it seemed unnecessary.
|
69
|
+
|
70
|
+
sorted_column_indices&.each do |index|
|
71
|
+
indexed_columns = index.columns.reject { |i| i == @column.name }
|
72
|
+
|
73
|
+
attrs << if indexed_columns.empty?
|
74
|
+
"indexed"
|
75
|
+
else
|
76
|
+
"indexed => [#{indexed_columns.join(", ")}]"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
attrs
|
82
|
+
end
|
83
|
+
|
84
|
+
def sorted_column_indices
|
85
|
+
# Not sure why there were & safe accessors here, but keeping in for time being.
|
86
|
+
sorted_indices = @column_indices&.sort_by(&:name)
|
87
|
+
|
88
|
+
_sorted_indices = sorted_indices.reject { |ind| ind.columns.is_a?(String) }
|
89
|
+
end
|
90
|
+
|
91
|
+
def hide_default?
|
92
|
+
excludes =
|
93
|
+
if @options[:hide_default_column_types].blank?
|
94
|
+
NO_DEFAULT_COL_TYPES
|
95
|
+
else
|
96
|
+
@options[:hide_default_column_types].split(",")
|
97
|
+
end
|
98
|
+
|
99
|
+
excludes.include?(@column.column_type_string)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnnotateRb
|
4
|
+
module ModelAnnotator
|
5
|
+
module ColumnAnnotation
|
6
|
+
class ColumnWrapper
|
7
|
+
def initialize(column)
|
8
|
+
@column = column
|
9
|
+
end
|
10
|
+
|
11
|
+
def default
|
12
|
+
# Note: Used to be klass.column_defaults[name], where name is the column name.
|
13
|
+
# Looks to be identical, but keeping note here in case there are differences.
|
14
|
+
_column_default = @column.default
|
15
|
+
end
|
16
|
+
|
17
|
+
def default_string
|
18
|
+
quote(@column.default)
|
19
|
+
end
|
20
|
+
|
21
|
+
def type
|
22
|
+
@column.type
|
23
|
+
end
|
24
|
+
|
25
|
+
def column_type_string
|
26
|
+
if (@column.respond_to?(:bigint?) && @column.bigint?) || /\Abigint\b/ =~ @column.sql_type
|
27
|
+
"bigint"
|
28
|
+
else
|
29
|
+
(@column.type || @column.sql_type).to_s
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def unsigned?
|
34
|
+
@column.respond_to?(:unsigned?) && @column.unsigned?
|
35
|
+
end
|
36
|
+
|
37
|
+
def null
|
38
|
+
@column.null
|
39
|
+
end
|
40
|
+
|
41
|
+
def precision
|
42
|
+
@column.precision
|
43
|
+
end
|
44
|
+
|
45
|
+
def scale
|
46
|
+
@column.scale
|
47
|
+
end
|
48
|
+
|
49
|
+
def limit
|
50
|
+
@column.limit
|
51
|
+
end
|
52
|
+
|
53
|
+
def geometry_type?
|
54
|
+
@column.respond_to?(:geometry_type)
|
55
|
+
end
|
56
|
+
|
57
|
+
def geometry_type
|
58
|
+
# TODO: Check if we need to check if it responds before accessing the geometry type
|
59
|
+
@column.geometry_type
|
60
|
+
end
|
61
|
+
|
62
|
+
def geometric_type?
|
63
|
+
@column.respond_to?(:geometric_type)
|
64
|
+
end
|
65
|
+
|
66
|
+
def geometric_type
|
67
|
+
# TODO: Check if we need to check if it responds before accessing the geometric type
|
68
|
+
@column.geometric_type
|
69
|
+
end
|
70
|
+
|
71
|
+
def srid
|
72
|
+
# TODO: Check if we need to check if it responds before accessing the srid
|
73
|
+
@column.srid
|
74
|
+
end
|
75
|
+
|
76
|
+
def array?
|
77
|
+
@column.respond_to?(:array) && @column.array
|
78
|
+
end
|
79
|
+
|
80
|
+
def name
|
81
|
+
@column.name
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
# Simple quoting for the default column value
|
87
|
+
def quote(value)
|
88
|
+
case value
|
89
|
+
when NilClass then "NULL"
|
90
|
+
when TrueClass then "TRUE"
|
91
|
+
when FalseClass then "FALSE"
|
92
|
+
when Float, Integer then value.to_s
|
93
|
+
# BigDecimals need to be output in a non-normalized form and quoted.
|
94
|
+
when BigDecimal then value.to_s("F")
|
95
|
+
when Array then value.map { |v| quote(v) }
|
96
|
+
else
|
97
|
+
value.inspect
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnnotateRb
|
4
|
+
module ModelAnnotator
|
5
|
+
module ColumnAnnotation
|
6
|
+
class TypeBuilder
|
7
|
+
# Don't show limit (#) on these column types
|
8
|
+
# Example: show "integer" instead of "integer(4)"
|
9
|
+
NO_LIMIT_COL_TYPES = %w[integer bigint boolean].freeze
|
10
|
+
|
11
|
+
def initialize(column, options)
|
12
|
+
@column = ColumnWrapper.new(column)
|
13
|
+
@options = options
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the formatted column type as a string.
|
17
|
+
def build
|
18
|
+
column_type = @column.column_type_string
|
19
|
+
|
20
|
+
formatted_column_type = column_type
|
21
|
+
|
22
|
+
is_special_type = %w[spatial geometry geography].include?(column_type)
|
23
|
+
is_decimal_type = column_type == "decimal"
|
24
|
+
|
25
|
+
if is_decimal_type
|
26
|
+
formatted_column_type = "decimal(#{@column.precision}, #{@column.scale})"
|
27
|
+
elsif is_special_type
|
28
|
+
# Do nothing. Kept as a code fragment in case we need to do something here.
|
29
|
+
elsif @column.limit && !@options[:format_yard]
|
30
|
+
# Unsure if Column#limit will ever be an array. May be safe to remove.
|
31
|
+
if !@column.limit.is_a?(Array) && !hide_limit?
|
32
|
+
formatted_column_type = column_type + "(#{@column.limit})"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
formatted_column_type
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def hide_limit?
|
42
|
+
excludes =
|
43
|
+
if @options[:hide_limit_column_types].blank?
|
44
|
+
NO_LIMIT_COL_TYPES
|
45
|
+
else
|
46
|
+
@options[:hide_limit_column_types].split(",")
|
47
|
+
end
|
48
|
+
|
49
|
+
excludes.include?(@column.column_type_string)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnnotateRb
|
4
|
+
module ModelAnnotator
|
5
|
+
module ColumnAnnotation
|
6
|
+
autoload :AttributesBuilder, "annotate_rb/model_annotator/column_annotation/attributes_builder"
|
7
|
+
autoload :TypeBuilder, "annotate_rb/model_annotator/column_annotation/type_builder"
|
8
|
+
autoload :ColumnWrapper, "annotate_rb/model_annotator/column_annotation/column_wrapper"
|
9
|
+
autoload :AnnotationBuilder, "annotate_rb/model_annotator/column_annotation/annotation_builder"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnnotateRb
|
4
|
+
module ModelAnnotator
|
5
|
+
# Generates the text file content with annotations, these are then to be written to filesystem.
|
6
|
+
class FileBuilder
|
7
|
+
def initialize(file_components, new_annotations, annotation_position, options)
|
8
|
+
@file_components = file_components
|
9
|
+
@new_annotations = new_annotations
|
10
|
+
@annotation_position = annotation_position
|
11
|
+
@options = options
|
12
|
+
|
13
|
+
@new_wrapped_annotations = wrapped_content(new_annotations)
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate_content_with_new_annotations
|
17
|
+
# Need to keep `.to_s` for now since the it can be either a String or Symbol
|
18
|
+
annotation_write_position = @options[@annotation_position].to_s
|
19
|
+
|
20
|
+
_content = if %w[after bottom].include?(annotation_write_position)
|
21
|
+
@file_components.magic_comments + (@file_components.pure_file_content.rstrip + "\n\n" + @new_wrapped_annotations)
|
22
|
+
elsif @file_components.magic_comments.empty?
|
23
|
+
@file_components.magic_comments + @new_wrapped_annotations + @file_components.pure_file_content.lstrip
|
24
|
+
else
|
25
|
+
@file_components.magic_comments + "\n" + @new_wrapped_annotations + @file_components.pure_file_content.lstrip
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def update_existing_annotations
|
30
|
+
return "" if !@file_components.has_annotations?
|
31
|
+
|
32
|
+
annotation_pattern = AnnotationPatternGenerator.call(@options)
|
33
|
+
|
34
|
+
new_annotation = @file_components.space_before_annotation + @new_wrapped_annotations + @file_components.space_after_annotation
|
35
|
+
|
36
|
+
_content = @file_components.current_file_content.sub(annotation_pattern, new_annotation)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def wrapped_content(content)
|
42
|
+
wrapper_open = if @options[:wrapper_open]
|
43
|
+
"# #{@options[:wrapper_open]}\n"
|
44
|
+
else
|
45
|
+
""
|
46
|
+
end
|
47
|
+
|
48
|
+
wrapper_close = if @options[:wrapper_close]
|
49
|
+
"# #{@options[:wrapper_close]}\n"
|
50
|
+
else
|
51
|
+
""
|
52
|
+
end
|
53
|
+
|
54
|
+
_wrapped_info_block = "#{wrapper_open}#{content}#{wrapper_close}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnnotateRb
|
4
|
+
module ModelAnnotator
|
5
|
+
class FileComponents
|
6
|
+
SKIP_ANNOTATION_STRING = "# -*- SkipSchemaAnnotations"
|
7
|
+
SOME_PATTERN = /\A(?<start>\s*).*?\n(?<end>\s*)\z/m # Unsure what this pattern is
|
8
|
+
|
9
|
+
def initialize(file_content, new_annotations, options)
|
10
|
+
@file_content = file_content
|
11
|
+
@diff = AnnotationDiffGenerator.new(file_content, new_annotations).generate
|
12
|
+
@options = options
|
13
|
+
@annotation_pattern = AnnotationPatternGenerator.call(options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def current_file_content
|
17
|
+
@file_content
|
18
|
+
end
|
19
|
+
|
20
|
+
# TODO: Rename method once it's clear what this actually does
|
21
|
+
def space_before_annotation
|
22
|
+
return @space_before_annotation if defined?(@space_before_annotation)
|
23
|
+
|
24
|
+
match = current_annotations.match(SOME_PATTERN)
|
25
|
+
@space_before_annotation = if match
|
26
|
+
match[:start]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# TODO: Rename method once it's clear what this actually does
|
31
|
+
def space_after_annotation
|
32
|
+
return @space_after_annotation if defined?(@space_after_annotation)
|
33
|
+
|
34
|
+
match = current_annotations.match(SOME_PATTERN)
|
35
|
+
@space_after_annotation = if match
|
36
|
+
match[:end]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def pure_file_content
|
41
|
+
@pure_file_content ||=
|
42
|
+
begin
|
43
|
+
content_without_magic_comments = @file_content.gsub(MagicCommentParser::MAGIC_COMMENTS_REGEX, "")
|
44
|
+
content_without_annotations = content_without_magic_comments.sub(@annotation_pattern, "")
|
45
|
+
|
46
|
+
content_without_annotations
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def magic_comments
|
51
|
+
@magic_comments ||= MagicCommentParser.call(@file_content)
|
52
|
+
end
|
53
|
+
|
54
|
+
def has_skip_string?
|
55
|
+
@has_skip_string ||= @file_content.include?(SKIP_ANNOTATION_STRING)
|
56
|
+
end
|
57
|
+
|
58
|
+
def has_annotations?
|
59
|
+
@has_annotations ||= @diff.current_columns.present?
|
60
|
+
end
|
61
|
+
|
62
|
+
def annotations_changed?
|
63
|
+
@has_annotations_changed ||= @diff.changed?
|
64
|
+
end
|
65
|
+
|
66
|
+
def current_annotations
|
67
|
+
@current_annotations ||=
|
68
|
+
if has_annotations?
|
69
|
+
# `#has_annotations?` uses a different regex pattern than the one in `@annotation_pattern`,
|
70
|
+
# this could lead to unexpected behavior
|
71
|
+
@file_content.match(@annotation_pattern).to_s
|
72
|
+
else
|
73
|
+
""
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -6,9 +6,9 @@ module AnnotateRb
|
|
6
6
|
class << self
|
7
7
|
def call(filename_template, model_name, table_name)
|
8
8
|
filename_template
|
9
|
-
.gsub(
|
10
|
-
.gsub(
|
11
|
-
.gsub(
|
9
|
+
.gsub("%MODEL_NAME%", model_name)
|
10
|
+
.gsub("%PLURALIZED_MODEL_NAME%", model_name.pluralize)
|
11
|
+
.gsub("%TABLE_NAME%", table_name || model_name.pluralize)
|
12
12
|
end
|
13
13
|
end
|
14
14
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnnotateRb
|
4
|
+
module ModelAnnotator
|
5
|
+
module ForeignKeyAnnotation
|
6
|
+
class AnnotationBuilder
|
7
|
+
def initialize(model, options)
|
8
|
+
@model = model
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
def build
|
13
|
+
fk_info = if @options[:format_markdown]
|
14
|
+
"#\n# ### Foreign Keys\n#\n"
|
15
|
+
else
|
16
|
+
"#\n# Foreign Keys\n#\n"
|
17
|
+
end
|
18
|
+
|
19
|
+
return "" unless @model.connection.respond_to?(:supports_foreign_keys?) &&
|
20
|
+
@model.connection.supports_foreign_keys? && @model.connection.respond_to?(:foreign_keys)
|
21
|
+
|
22
|
+
foreign_keys = @model.connection.foreign_keys(@model.table_name)
|
23
|
+
return "" if foreign_keys.empty?
|
24
|
+
|
25
|
+
format_name = lambda do |fk|
|
26
|
+
return fk.options[:column] if fk.name.blank?
|
27
|
+
|
28
|
+
@options[:show_complete_foreign_keys] ? fk.name : fk.name.gsub(/(?<=^fk_rails_)[0-9a-f]{10}$/, "...")
|
29
|
+
end
|
30
|
+
|
31
|
+
max_size = foreign_keys.map(&format_name).map(&:size).max + 1
|
32
|
+
foreign_keys.sort_by { |fk| [format_name.call(fk), fk.column] }.each do |fk|
|
33
|
+
ref_info = "#{fk.column} => #{fk.to_table}.#{fk.primary_key}"
|
34
|
+
constraints_info = ""
|
35
|
+
constraints_info += "ON DELETE => #{fk.on_delete} " if fk.on_delete
|
36
|
+
constraints_info += "ON UPDATE => #{fk.on_update} " if fk.on_update
|
37
|
+
constraints_info = constraints_info.strip
|
38
|
+
|
39
|
+
fk_info += if @options[:format_markdown]
|
40
|
+
format("# * `%s`%s:\n# * **`%s`**\n",
|
41
|
+
format_name.call(fk),
|
42
|
+
constraints_info.blank? ? "" : " (_#{constraints_info}_)",
|
43
|
+
ref_info)
|
44
|
+
else
|
45
|
+
format("# %-#{max_size}.#{max_size}s %s %s",
|
46
|
+
format_name.call(fk),
|
47
|
+
"(#{ref_info})",
|
48
|
+
constraints_info).rstrip + "\n"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
fk_info
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnnotateRb
|
4
|
+
module ModelAnnotator
|
5
|
+
module IndexAnnotation
|
6
|
+
class AnnotationBuilder
|
7
|
+
INDEX_CLAUSES = {
|
8
|
+
unique: {
|
9
|
+
default: "UNIQUE",
|
10
|
+
markdown: "_unique_"
|
11
|
+
},
|
12
|
+
where: {
|
13
|
+
default: "WHERE",
|
14
|
+
markdown: "_where_"
|
15
|
+
},
|
16
|
+
using: {
|
17
|
+
default: "USING",
|
18
|
+
markdown: "_using_"
|
19
|
+
}
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
def initialize(model, options)
|
23
|
+
@model = model
|
24
|
+
@options = options
|
25
|
+
end
|
26
|
+
|
27
|
+
def build
|
28
|
+
index_info = if @options[:format_markdown]
|
29
|
+
"#\n# ### Indexes\n#\n"
|
30
|
+
else
|
31
|
+
"#\n# Indexes\n#\n"
|
32
|
+
end
|
33
|
+
|
34
|
+
indexes = @model.retrieve_indexes_from_table
|
35
|
+
return "" if indexes.empty?
|
36
|
+
|
37
|
+
max_size = indexes.collect { |index| index.name.size }.max + 1
|
38
|
+
indexes.sort_by(&:name).each do |index|
|
39
|
+
index_info += if @options[:format_markdown]
|
40
|
+
final_index_string_in_markdown(index)
|
41
|
+
else
|
42
|
+
final_index_string(index, max_size)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
index_info
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def index_using_info(index, format = :default)
|
52
|
+
value = index.try(:using) && index.using.try(:to_sym)
|
53
|
+
if !value.blank? && value != :btree
|
54
|
+
" #{INDEX_CLAUSES[:using][format]} #{value}"
|
55
|
+
else
|
56
|
+
""
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def index_where_info(index, format = :default)
|
61
|
+
value = index.try(:where).try(:to_s)
|
62
|
+
if value.blank?
|
63
|
+
""
|
64
|
+
else
|
65
|
+
" #{INDEX_CLAUSES[:where][format]} #{value}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def index_unique_info(index, format = :default)
|
70
|
+
index.unique ? " #{INDEX_CLAUSES[:unique][format]}" : ""
|
71
|
+
end
|
72
|
+
|
73
|
+
def final_index_string_in_markdown(index)
|
74
|
+
details = format(
|
75
|
+
"%s%s%s",
|
76
|
+
index_unique_info(index, :markdown),
|
77
|
+
index_where_info(index, :markdown),
|
78
|
+
index_using_info(index, :markdown)
|
79
|
+
).strip
|
80
|
+
details = " (#{details})" unless details.blank?
|
81
|
+
|
82
|
+
format(
|
83
|
+
"# * `%s`%s:\n# * **`%s`**\n",
|
84
|
+
index.name,
|
85
|
+
details,
|
86
|
+
index_columns_info(index).join("`**\n# * **`")
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
def final_index_string(index, max_size)
|
91
|
+
format(
|
92
|
+
"# %-#{max_size}.#{max_size}s %s%s%s%s",
|
93
|
+
index.name,
|
94
|
+
"(#{index_columns_info(index).join(",")})",
|
95
|
+
index_unique_info(index),
|
96
|
+
index_where_info(index),
|
97
|
+
index_using_info(index)
|
98
|
+
).rstrip + "\n"
|
99
|
+
end
|
100
|
+
|
101
|
+
def index_columns_info(index)
|
102
|
+
Array(index.columns).map do |col|
|
103
|
+
if index.try(:orders) && index.orders[col.to_s]
|
104
|
+
"#{col} #{index.orders[col.to_s].upcase}"
|
105
|
+
else
|
106
|
+
col.to_s.gsub("\r", '\r').gsub("\n", '\n')
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnnotateRb
|
4
|
+
module ModelAnnotator
|
5
|
+
# Extracts magic comments strings and returns them
|
6
|
+
class MagicCommentParser
|
7
|
+
MAGIC_COMMENTS = [
|
8
|
+
HASH_ENCODING = /(^#\s*encoding:.*(?:\n|r\n))/,
|
9
|
+
HASH_CODING = /(^# coding:.*(?:\n|\r\n))/,
|
10
|
+
HASH_FROZEN_STRING = /(^#\s*frozen_string_literal:.+(?:\n|\r\n))/,
|
11
|
+
STAR_ENCODING = /(^# -\*- encoding\s?:.*(?:\n|\r\n))/,
|
12
|
+
STAR_CODING = /(^# -\*- coding:.*(?:\n|\r\n))/,
|
13
|
+
STAR_FROZEN_STRING = /(^# -\*- frozen_string_literal\s*:.+-\*-(?:\n|\r\n))/,
|
14
|
+
SORBET_TYPED_STRING = /(^#\s*typed:.*(?:\n|r\n))/.freeze
|
15
|
+
].freeze
|
16
|
+
|
17
|
+
MAGIC_COMMENTS_REGEX = Regexp.union(*MAGIC_COMMENTS).freeze
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def call(content)
|
21
|
+
magic_comments = content.scan(MAGIC_COMMENTS_REGEX).flatten.compact
|
22
|
+
|
23
|
+
if magic_comments.any?
|
24
|
+
magic_comments.join
|
25
|
+
else
|
26
|
+
""
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -8,8 +8,8 @@ module AnnotateRb
|
|
8
8
|
# Check for namespaced models in subdirectories as well as models
|
9
9
|
# in subdirectories without namespacing.
|
10
10
|
def call(file, options)
|
11
|
-
model_path = file.gsub(/\.rb$/,
|
12
|
-
options[:model_dir].each { |dir| model_path = model_path.gsub(/^#{dir}/,
|
11
|
+
model_path = file.gsub(/\.rb$/, "")
|
12
|
+
options[:model_dir].each { |dir| model_path = model_path.gsub(/^#{dir}/, "").gsub(/^\//, "") }
|
13
13
|
|
14
14
|
begin
|
15
15
|
get_loaded_model(model_path, file) || raise(BadModelFileError.new)
|
@@ -18,8 +18,8 @@ module AnnotateRb
|
|
18
18
|
file_path = File.expand_path(file)
|
19
19
|
if File.file?(file_path) && Kernel.require(file_path)
|
20
20
|
retry
|
21
|
-
elsif model_path
|
22
|
-
model_path = model_path.split(
|
21
|
+
elsif /\//.match?(model_path)
|
22
|
+
model_path = model_path.split("/")[1..-1].join("/").to_s
|
23
23
|
retry
|
24
24
|
else
|
25
25
|
raise
|
@@ -39,7 +39,7 @@ module AnnotateRb
|
|
39
39
|
absolute_file = File.expand_path(file)
|
40
40
|
model_paths =
|
41
41
|
$LOAD_PATH.select { |path| absolute_file.include?(path) }
|
42
|
-
|
42
|
+
.map { |path| absolute_file.sub(path, "").sub(/\.rb$/, "").sub(/^\//, "") }
|
43
43
|
model_paths
|
44
44
|
.map { |path| get_loaded_model_by_path(path) }
|
45
45
|
.find { |loaded_model| !loaded_model.nil? }
|
@@ -51,7 +51,7 @@ module AnnotateRb
|
|
51
51
|
rescue StandardError, LoadError
|
52
52
|
# Revert to the old way but it is not really robust
|
53
53
|
ObjectSpace.each_object(::Class)
|
54
|
-
|
54
|
+
.select do |c|
|
55
55
|
Class === c && # note: we use === to avoid a bug in activesupport 2.3.14 OptionMerger vs. is_a?
|
56
56
|
c.ancestors.respond_to?(:include?) && # to fix FactoryGirl bug, see https://github.com/ctran/annotate_models/pull/82
|
57
57
|
c.ancestors.include?(::ActiveRecord::Base)
|