Class: AnnotateRb::ModelAnnotator::ModelWrapper

Inherits:
Object
  • Object
show all
Defined in:
lib/annotate_rb/model_annotator/model_wrapper.rb

Constant Summary collapse

DEFAULT_TIMESTAMP_COLUMNS =

Should be the wrapper for an ActiveRecord model that serves as the source of truth of the model of the model that we’re annotating

%w[created_at updated_at]

Instance Method Summary collapse

Constructor Details

#initialize(klass, options) ⇒ ModelWrapper

Returns a new instance of ModelWrapper.



11
12
13
14
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 11

def initialize(klass, options)
  @klass = klass
  @options = options
end

Instance Method Details

#_retrieve_indexes_from_tableObject



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 142

def _retrieve_indexes_from_table
  table_name = @klass.table_name
  return [] unless table_name

  indexes = connection.indexes(table_name)
  return indexes if indexes.any? || !@klass.table_name_prefix

  # Try to search the table without prefix
  table_name_without_prefix = table_name.to_s.sub(@klass.table_name_prefix.to_s, "")
  begin
    connection.indexes(table_name_without_prefix)
  rescue ActiveRecord::StatementInvalid => _e
    # Mysql2 adapter behaves differently than Sqlite3 and Postgres adapter.
    # If `table_name_without_prefix` does not exist, Mysql2 will raise,
    # the other adapters will return an empty array.
    #
    # See: https://github.com/rails/rails/issues/51205
    []
  end
end

#built_attributesObject



126
127
128
129
130
131
132
133
134
135
136
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 126

def built_attributes
  @built_attributes ||= begin
    table_indices = retrieve_indexes_from_table
    columns.map do |column|
      is_primary_key = is_column_primary_key?(column.name)
      column_indices = table_indices.select { |ind| ind.columns.include?(column.name) }
      built = ColumnAnnotation::AttributesBuilder.new(column, @options, is_primary_key, column_indices, column_defaults).build
      [column.name, built]
    end.to_h
  end
end

#classified_sort(cols, grouped_polymorphic) ⇒ Object



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 177

def classified_sort(cols, grouped_polymorphic)
  rest_cols = []
  timestamps = []
  associations = []
  id = nil

  # specs don't load defaults, so ensure we have defaults here
  timestamp_columns = @options[:timestamp_columns] || DEFAULT_TIMESTAMP_COLUMNS

  col_names = cols.map(&:name)

  cols.each do |c|
    if c.name.eql?("id")
      id = c
    elsif timestamp_columns.include?(c.name)
      timestamps << c
    elsif c.name[-3, 3].eql?("_id")
      associations << c
    elsif c.name[-5, 5].eql?("_type") && col_names.include?(c.name.sub(/_type$/, "_id")) && grouped_polymorphic
      # This is a polymorphic association's type column
      associations << c
    else
      rest_cols << c
    end
  end

  timestamp_order = timestamp_columns.each_with_index.to_h
  timestamps.sort_by! { |col| timestamp_order[col.name] }
  [rest_cols, associations].each { |a| a.sort_by!(&:name) }

  ([id] << rest_cols << timestamps << associations).flatten.compact
end

#column_defaultsObject



71
72
73
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 71

def column_defaults
  @klass.column_defaults
end

#columnsObject

Gets the columns of the ActiveRecord model, processes them, and then returns them.



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 17

def columns
  @columns ||=
    begin
      cols = raw_columns
      cols += translated_columns

      ignore_columns = @options[:ignore_columns]
      if ignore_columns
        cols = cols.reject do |col|
          col.name.match?(/#{ignore_columns}/)
        end
      end

      cols = cols.sort_by(&:name) if @options[:sort]
      cols = classified_sort(cols, @options[:grouped_polymorphic]) if @options[:classified_sort]

      cols
    end
end

#connectionObject



37
38
39
40
41
42
43
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 37

def connection
  if @klass.respond_to?(:lease_connection)
    @klass.lease_connection
  else
    @klass.connection
  end
end

#database_nameObject



45
46
47
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 45

def database_name
  connection.pool.db_config.name
end

#enum_typesObject



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 222

def enum_types
  @enum_types ||=
    if connection.respond_to?(:enum_types)
      begin
        # enum values may be a String or an Array depending on the Rails version.
        # See: https://github.com/rails/rails/pull/54141
        connection.enum_types.map do |name, values|
          [name, values.is_a?(Array) ? values : values.to_s.split(",")]
        end
      rescue ActiveRecord::StatementInvalid
        []
      end
    else
      []
    end
end

#has_table_comments?Boolean

Returns:

  • (Boolean)


66
67
68
69
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 66

def has_table_comments?
  connection.respond_to?(:table_comment) &&
    connection.table_comment(@klass.table_name).present?
end

#ignored_translation_table_columnsObject

These are the columns that the globalize gem needs to work but are not necessary for the models to be displayed as annotations.



212
213
214
215
216
217
218
219
220
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 212

def ignored_translation_table_columns
  [
    :id,
    :created_at,
    :updated_at,
    :locale,
    @klass.name.foreign_key.to_sym
  ]
end

#is_column_primary_key?(column_name) ⇒ Boolean

Returns:

  • (Boolean)


120
121
122
123
124
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 120

def is_column_primary_key?(column_name)
  return false unless primary_key

  Array(primary_key).map(&:to_sym).include?(column_name.to_sym)
end

#max_schema_info_widthObject

Calculates the max width of the schema for the model by looking at the columns, schema comments, with respect to the options.



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 97

def max_schema_info_width
  @max_schema_info_width ||=
    begin
      cols = columns

      position_of_column_comment = @options.with_default_fallback(:position_of_column_comment)
      if with_comments? && position_of_column_comment == :with_name
        column_widths = cols.map do |column|
          column.name.size + (column.comment ? Helper.width(column.comment) : 0)
        end

        max_size = column_widths.max || 0
        max_size += 2
      else
        max_size = cols.map(&:name).map(&:size).max
      end

      max_size += @options[:format_rdoc] ? 5 : 1

      max_size
    end
end

#migration_versionObject



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 239

def migration_version
  return 0 unless @options[:include_version]

  # Multi-database support: Cache migration versions per database connection to handle
  # different schema versions across primary/secondary databases correctly.
  # Example: primary → "current_version_primary", secondary → "current_version_secondary"
  cache_key = "current_version_#{database_name}".to_sym

  if @options.get_state(cache_key).nil?
    migration_version = begin
      # Rails 7.1+ moved migration_context from ConnectionAdapter to ConnectionPool.
      # ConnectionAdapter#migration_context was removed in Rails 7.2.
      # See: https://github.com/rails/rails/pull/51162
      if connection.pool.respond_to?(:migration_context)
        connection.pool.migration_context.current_version
      else
        connection.migration_context.current_version
      end
    rescue
      0
    end

    @options.set_state(cache_key, migration_version)
  end

  @options.get_state(cache_key)
end

#model_nameObject



91
92
93
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 91

def model_name
  @klass.name.underscore
end

#position_of_column_commentObject



173
174
175
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 173

def position_of_column_comment
  @position_of_column_comment ||= @options[:position_of_column_comment]
end

#primary_keyObject



54
55
56
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 54

def primary_key
  @klass.primary_key
end

#raw_columnsObject

Returns the unmodified model columns



50
51
52
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 50

def raw_columns
  @raw_columns ||= @klass.columns
end

#retrieve_indexes_from_tableObject



138
139
140
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 138

def retrieve_indexes_from_table
  @indexes_from_table ||= _retrieve_indexes_from_table
end

#table_commentsObject



62
63
64
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 62

def table_comments
  connection.table_comment(@klass.table_name)
end

#table_exists?Boolean

Returns:

  • (Boolean)


58
59
60
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 58

def table_exists?
  @klass.table_exists?
end

#table_nameObject



87
88
89
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 87

def table_name
  @klass.table_name
end

#translated_columnsObject

Add columns managed by the globalize gem if this gem is being used. TODO: Audit if this is still needed, it seems like Globalize gem is no longer maintained



77
78
79
80
81
82
83
84
85
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 77

def translated_columns
  return [] unless @klass.respond_to?(:translation_class)

  ignored_cols = ignored_translation_table_columns

  @klass.translation_class.columns.reject do |col|
    ignored_cols.include? col.name.to_sym
  end
end

#with_comments?Boolean

Returns:

  • (Boolean)


163
164
165
166
167
168
169
170
171
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 163

def with_comments?
  return @with_comments if instance_variable_defined?(:@with_comments)

  @with_comments =
    @options[:with_comment] &&
    @options[:with_column_comments] &&
    raw_columns.first.respond_to?(:comment) &&
    raw_columns.map(&:comment).any? { |comment| !comment.nil? }
end