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



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 157

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



141
142
143
144
145
146
147
148
149
150
151
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 141

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



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 192

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

Returns column defaults for annotations.

Model#column_defaults reflects attribute :foo, default: X overrides, which would incorrectly show the Ruby-side default in annotations instead of the DB schema default. To preserve model-level decorations such as enum labels or TimeZoneConverter on datetime columns, we start from column_defaults and only substitute the DB schema value for attributes whose raw default was replaced.



79
80
81
82
83
84
85
86
87
88
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 79

def column_defaults
  @column_defaults ||= @klass.column_defaults.each_with_object({}) do |(name, value), result|
    column = @klass.columns_hash[name]
    result[name] = if attribute_default_overridden?(name, column)
      schema_default_for(column)
    else
      enum_default(name, column, value)
    end
  end
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



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 237

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.



227
228
229
230
231
232
233
234
235
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 227

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)


135
136
137
138
139
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 135

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.



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 112

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



254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 254

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



106
107
108
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 106

def model_name
  @klass.name.underscore
end

#position_of_column_commentObject



188
189
190
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 188

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



153
154
155
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 153

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



102
103
104
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 102

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



92
93
94
95
96
97
98
99
100
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 92

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)


178
179
180
181
182
183
184
185
186
# File 'lib/annotate_rb/model_annotator/model_wrapper.rb', line 178

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