snail_trail 0.0.1 → 1.0.0.rc.pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/lib/generators/snail_trail/install/USAGE +3 -0
  4. data/lib/generators/snail_trail/install/install_generator.rb +108 -0
  5. data/lib/generators/snail_trail/install/templates/add_object_changes_to_versions.rb.erb +12 -0
  6. data/lib/generators/snail_trail/install/templates/add_transaction_id_column_to_versions.rb.erb +12 -0
  7. data/lib/generators/snail_trail/install/templates/create_versions.rb.erb +41 -0
  8. data/lib/generators/snail_trail/migration_generator.rb +38 -0
  9. data/lib/generators/snail_trail/update_item_subtype/USAGE +4 -0
  10. data/lib/generators/snail_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb +85 -0
  11. data/lib/generators/snail_trail/update_item_subtype/update_item_subtype_generator.rb +19 -0
  12. data/lib/snail_trail/attribute_serializers/README.md +10 -0
  13. data/lib/snail_trail/attribute_serializers/attribute_serializer_factory.rb +41 -0
  14. data/lib/snail_trail/attribute_serializers/cast_attribute_serializer.rb +51 -0
  15. data/lib/snail_trail/attribute_serializers/object_attribute.rb +51 -0
  16. data/lib/snail_trail/attribute_serializers/object_changes_attribute.rb +54 -0
  17. data/lib/snail_trail/cleaner.rb +60 -0
  18. data/lib/snail_trail/compatibility.rb +51 -0
  19. data/lib/snail_trail/config.rb +40 -0
  20. data/lib/snail_trail/errors.rb +33 -0
  21. data/lib/snail_trail/events/base.rb +343 -0
  22. data/lib/snail_trail/events/create.rb +32 -0
  23. data/lib/snail_trail/events/destroy.rb +42 -0
  24. data/lib/snail_trail/events/update.rb +76 -0
  25. data/lib/snail_trail/frameworks/active_record/models/snail_trail/version.rb +16 -0
  26. data/lib/snail_trail/frameworks/active_record.rb +12 -0
  27. data/lib/snail_trail/frameworks/cucumber.rb +33 -0
  28. data/lib/snail_trail/frameworks/rails/controller.rb +103 -0
  29. data/lib/snail_trail/frameworks/rails/railtie.rb +34 -0
  30. data/lib/snail_trail/frameworks/rails.rb +3 -0
  31. data/lib/snail_trail/frameworks/rspec/helpers.rb +29 -0
  32. data/lib/snail_trail/frameworks/rspec.rb +42 -0
  33. data/lib/snail_trail/has_snail_trail.rb +92 -0
  34. data/lib/snail_trail/model_config.rb +265 -0
  35. data/lib/snail_trail/queries/versions/where_attribute_changes.rb +50 -0
  36. data/lib/snail_trail/queries/versions/where_object.rb +65 -0
  37. data/lib/snail_trail/queries/versions/where_object_changes.rb +70 -0
  38. data/lib/snail_trail/queries/versions/where_object_changes_from.rb +57 -0
  39. data/lib/snail_trail/queries/versions/where_object_changes_to.rb +57 -0
  40. data/lib/snail_trail/record_history.rb +51 -0
  41. data/lib/snail_trail/record_trail.rb +375 -0
  42. data/lib/snail_trail/reifier.rb +147 -0
  43. data/lib/snail_trail/request.rb +180 -0
  44. data/lib/snail_trail/serializers/json.rb +36 -0
  45. data/lib/snail_trail/serializers/yaml.rb +68 -0
  46. data/lib/snail_trail/type_serializers/postgres_array_serializer.rb +35 -0
  47. data/lib/snail_trail/version_concern.rb +407 -0
  48. data/lib/snail_trail/version_number.rb +23 -0
  49. data/lib/snail_trail.rb +141 -1
  50. metadata +371 -15
  51. data/lib/snail_trail/version.rb +0 -5
@@ -0,0 +1,407 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "snail_trail/attribute_serializers/object_changes_attribute"
4
+ require "snail_trail/queries/versions/where_attribute_changes"
5
+ require "snail_trail/queries/versions/where_object"
6
+ require "snail_trail/queries/versions/where_object_changes"
7
+ require "snail_trail/queries/versions/where_object_changes_from"
8
+ require "snail_trail/queries/versions/where_object_changes_to"
9
+
10
+ module SnailTrail
11
+ # Originally, SnailTrail did not provide this module, and all of this
12
+ # functionality was in `SnailTrail::Version`. That model still exists (and is
13
+ # used by most apps) but by moving the functionality to this module, people
14
+ # can include this concern instead of sub-classing the `Version` model.
15
+ module VersionConcern
16
+ extend ::ActiveSupport::Concern
17
+
18
+ E_YAML_PERMITTED_CLASSES = <<-EOS.squish.freeze
19
+ SnailTrail encountered a Psych::DisallowedClass error during
20
+ deserialization of YAML column, indicating that
21
+ yaml_column_permitted_classes has not been configured correctly. %s
22
+ EOS
23
+
24
+ included do
25
+ belongs_to :item, polymorphic: true, optional: true, inverse_of: false
26
+ validates_presence_of :event
27
+ after_create :enforce_version_limit!
28
+ scope :within_transaction, ->(id) { where(transaction_id: id) }
29
+ end
30
+
31
+ # :nodoc:
32
+ module ClassMethods
33
+ def with_item_keys(item_type, item_id)
34
+ where item_type: item_type, item_id: item_id
35
+ end
36
+
37
+ def creates
38
+ where event: "create"
39
+ end
40
+
41
+ def updates
42
+ where event: "update"
43
+ end
44
+
45
+ def destroys
46
+ where event: "destroy"
47
+ end
48
+
49
+ def not_creates
50
+ where.not(event: "create")
51
+ end
52
+
53
+ def between(start_time, end_time)
54
+ where(
55
+ arel_table[:created_at].gt(start_time).
56
+ and(arel_table[:created_at].lt(end_time))
57
+ ).order(timestamp_sort_order)
58
+ end
59
+
60
+ # Defaults to using the primary key as the secondary sort order if
61
+ # possible.
62
+ def timestamp_sort_order(direction = "asc")
63
+ [arel_table[:created_at].send(direction.downcase)].tap do |array|
64
+ array << arel_table[primary_key].send(direction.downcase) if primary_key_is_int?
65
+ end
66
+ end
67
+
68
+ # Given an attribute like `"name"`, query the `versions.object_changes`
69
+ # column for any changes that modified the provided attribute.
70
+ #
71
+ # @api public
72
+ def where_attribute_changes(attribute)
73
+ unless attribute.is_a?(String) || attribute.is_a?(Symbol)
74
+ raise ArgumentError, "expected to receive a String or Symbol"
75
+ end
76
+
77
+ Queries::Versions::WhereAttributeChanges.new(self, attribute).execute
78
+ end
79
+
80
+ # Given a hash of attributes like `name: 'Joan'`, query the
81
+ # `versions.objects` column.
82
+ #
83
+ # ```
84
+ # SELECT "versions".*
85
+ # FROM "versions"
86
+ # WHERE ("versions"."object" LIKE '%
87
+ # name: Joan
88
+ # %')
89
+ # ```
90
+ #
91
+ # This is useful for finding versions where a given attribute had a given
92
+ # value. Imagine, in the example above, that Joan had changed her name
93
+ # and we wanted to find the versions before that change.
94
+ #
95
+ # Based on the data type of the `object` column, the appropriate SQL
96
+ # operator is used. For example, a text column will use `like`, and a
97
+ # jsonb column will use `@>`.
98
+ #
99
+ # @api public
100
+ def where_object(args = {})
101
+ raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash)
102
+ Queries::Versions::WhereObject.new(self, args).execute
103
+ end
104
+
105
+ # Given a hash of attributes like `name: 'Joan'`, query the
106
+ # `versions.objects_changes` column.
107
+ #
108
+ # ```
109
+ # SELECT "versions".*
110
+ # FROM "versions"
111
+ # WHERE .. ("versions"."object_changes" LIKE '%
112
+ # name:
113
+ # - Joan
114
+ # %' OR "versions"."object_changes" LIKE '%
115
+ # name:
116
+ # -%
117
+ # - Joan
118
+ # %')
119
+ # ```
120
+ #
121
+ # This is useful for finding versions immediately before and after a given
122
+ # attribute had a given value. Imagine, in the example above, that someone
123
+ # changed their name to Joan and we wanted to find the versions
124
+ # immediately before and after that change.
125
+ #
126
+ # Based on the data type of the `object` column, the appropriate SQL
127
+ # operator is used. For example, a text column will use `like`, and a
128
+ # jsonb column will use `@>`.
129
+ #
130
+ # @api public
131
+ def where_object_changes(args = {})
132
+ raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash)
133
+ Queries::Versions::WhereObjectChanges.new(self, args).execute
134
+ end
135
+
136
+ # Given a hash of attributes like `name: 'Joan'`, query the
137
+ # `versions.objects_changes` column for changes where the version changed
138
+ # from the hash of attributes to other values.
139
+ #
140
+ # This is useful for finding versions where the attribute started with a
141
+ # known value and changed to something else. This is in comparison to
142
+ # `where_object_changes` which will find both the changes before and
143
+ # after.
144
+ #
145
+ # @api public
146
+ def where_object_changes_from(args = {})
147
+ raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash)
148
+ Queries::Versions::WhereObjectChangesFrom.new(self, args).execute
149
+ end
150
+
151
+ # Given a hash of attributes like `name: 'Joan'`, query the
152
+ # `versions.objects_changes` column for changes where the version changed
153
+ # to the hash of attributes from other values.
154
+ #
155
+ # This is useful for finding versions where the attribute started with an
156
+ # unknown value and changed to a known value. This is in comparison to
157
+ # `where_object_changes` which will find both the changes before and
158
+ # after.
159
+ #
160
+ # @api public
161
+ def where_object_changes_to(args = {})
162
+ raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash)
163
+ Queries::Versions::WhereObjectChangesTo.new(self, args).execute
164
+ end
165
+
166
+ def primary_key_is_int?
167
+ @primary_key_is_int ||= columns_hash[primary_key].type == :integer
168
+ rescue StandardError # TODO: Rescue something more specific
169
+ true
170
+ end
171
+
172
+ # Returns whether the `object` column is using the `json` type supported
173
+ # by PostgreSQL.
174
+ def object_col_is_json?
175
+ %i[json jsonb].include?(columns_hash["object"].type)
176
+ end
177
+
178
+ # Returns whether the `object_changes` column is using the `json` type
179
+ # supported by PostgreSQL.
180
+ def object_changes_col_is_json?
181
+ %i[json jsonb].include?(columns_hash["object_changes"].try(:type))
182
+ end
183
+
184
+ # Returns versions before `obj`.
185
+ #
186
+ # @param obj - a `Version` or a timestamp
187
+ # @param timestamp_arg - boolean - When true, `obj` is a timestamp.
188
+ # Default: false.
189
+ # @return `ActiveRecord::Relation`
190
+ # @api public
191
+ # rubocop:disable Style/OptionalBooleanParameter
192
+ def preceding(obj, timestamp_arg = false)
193
+ if timestamp_arg != true && primary_key_is_int?
194
+ preceding_by_id(obj)
195
+ else
196
+ preceding_by_timestamp(obj)
197
+ end
198
+ end
199
+ # rubocop:enable Style/OptionalBooleanParameter
200
+
201
+ # Returns versions after `obj`.
202
+ #
203
+ # @param obj - a `Version` or a timestamp
204
+ # @param timestamp_arg - boolean - When true, `obj` is a timestamp.
205
+ # Default: false.
206
+ # @return `ActiveRecord::Relation`
207
+ # @api public
208
+ # rubocop:disable Style/OptionalBooleanParameter
209
+ def subsequent(obj, timestamp_arg = false)
210
+ if timestamp_arg != true && primary_key_is_int?
211
+ subsequent_by_id(obj)
212
+ else
213
+ subsequent_by_timestamp(obj)
214
+ end
215
+ end
216
+ # rubocop:enable Style/OptionalBooleanParameter
217
+
218
+ private
219
+
220
+ # @api private
221
+ def preceding_by_id(obj)
222
+ where(arel_table[primary_key].lt(obj.id)).order(arel_table[primary_key].desc)
223
+ end
224
+
225
+ # @api private
226
+ def preceding_by_timestamp(obj)
227
+ obj = obj.send(:created_at) if obj.is_a?(self)
228
+ where(arel_table[:created_at].lt(obj)).
229
+ order(timestamp_sort_order("desc"))
230
+ end
231
+
232
+ # @api private
233
+ def subsequent_by_id(version)
234
+ where(arel_table[primary_key].gt(version.id)).order(arel_table[primary_key].asc)
235
+ end
236
+
237
+ # @api private
238
+ def subsequent_by_timestamp(obj)
239
+ obj = obj.send(:created_at) if obj.is_a?(self)
240
+ where(arel_table[:created_at].gt(obj)).order(timestamp_sort_order)
241
+ end
242
+ end
243
+
244
+ # @api private
245
+ def object_deserialized
246
+ if self.class.object_col_is_json?
247
+ object
248
+ else
249
+ SnailTrail.serializer.load(object)
250
+ end
251
+ end
252
+
253
+ # Restore the item from this version.
254
+ #
255
+ # Options:
256
+ #
257
+ # - :mark_for_destruction
258
+ # - `true` - Mark the has_one/has_many associations that did not exist in
259
+ # the reified version for destruction, instead of removing them.
260
+ # - `false` - Default. Useful for persisting the reified version.
261
+ # - :dup
262
+ # - `false` - Default.
263
+ # - `true` - Always create a new object instance. Useful for
264
+ # comparing two versions of the same object.
265
+ # - :unversioned_attributes
266
+ # - `:nil` - Default. Attributes undefined in version record are set to
267
+ # nil in reified record.
268
+ # - `:preserve` - Attributes undefined in version record are not modified.
269
+ #
270
+ def reify(options = {})
271
+ unless self.class.column_names.include? "object"
272
+ raise Error, "reify requires an object column"
273
+ end
274
+ return nil if object.nil?
275
+ ::SnailTrail::Reifier.reify(self, options)
276
+ end
277
+
278
+ # Returns what changed in this version of the item.
279
+ # `ActiveModel::Dirty#changes`. returns `nil` if your `versions` table does
280
+ # not have an `object_changes` text column.
281
+ def changeset
282
+ return nil unless self.class.column_names.include? "object_changes"
283
+ @changeset ||= load_changeset
284
+ end
285
+
286
+ # Returns who put the item into the state stored in this version.
287
+ def snail_trail_originator
288
+ @snail_trail_originator ||= previous.try(:whodunnit)
289
+ end
290
+
291
+ # Returns who changed the item from the state it had in this version. This
292
+ # is an alias for `whodunnit`.
293
+ def terminator
294
+ @terminator ||= whodunnit
295
+ end
296
+ alias version_author terminator
297
+
298
+ def next
299
+ @next ||= sibling_versions.subsequent(self).first
300
+ end
301
+
302
+ def previous
303
+ @previous ||= sibling_versions.preceding(self).first
304
+ end
305
+
306
+ # Returns an integer representing the chronological position of the
307
+ # version among its siblings. The "create" event, for example, has an index
308
+ # of 0.
309
+ #
310
+ # @api public
311
+ def index
312
+ @index ||= RecordHistory.new(sibling_versions, self.class).index(self)
313
+ end
314
+
315
+ private
316
+
317
+ # @api private
318
+ def load_changeset
319
+ if SnailTrail.config.object_changes_adapter.respond_to?(:load_changeset)
320
+ return SnailTrail.config.object_changes_adapter.load_changeset(self)
321
+ end
322
+
323
+ # First, deserialize the `object_changes` column.
324
+ changes = ActiveSupport::HashWithIndifferentAccess.new(object_changes_deserialized)
325
+
326
+ # The next step is, perhaps unfortunately, called "de-serialization",
327
+ # and appears to be responsible for custom attribute serializers. For an
328
+ # example of a custom attribute serializer, see
329
+ # `Person::TimeZoneSerializer` in the test suite.
330
+ #
331
+ # Is `item.class` good enough? Does it handle `inheritance_column`
332
+ # as well as `Reifier#version_reification_class`? We were using
333
+ # `item_type.constantize`, but that is problematic when the STI parent
334
+ # is not versioned. (See `Vehicle` and `Car` in the test suite).
335
+ #
336
+ # Note: `item` returns nil if `event` is "destroy".
337
+ unless item.nil?
338
+ AttributeSerializers::ObjectChangesAttribute.
339
+ new(item.class).
340
+ deserialize(changes)
341
+ end
342
+
343
+ # Finally, return a Hash mapping each attribute name to
344
+ # a two-element array representing before and after.
345
+ changes
346
+ end
347
+
348
+ # If the `object_changes` column is a Postgres JSON column, then
349
+ # ActiveRecord will deserialize it for us. Otherwise, it's a string column
350
+ # and we must deserialize it ourselves.
351
+ # @api private
352
+ def object_changes_deserialized
353
+ if self.class.object_changes_col_is_json?
354
+ object_changes
355
+ else
356
+ begin
357
+ SnailTrail.serializer.load(object_changes)
358
+ rescue StandardError => e
359
+ if defined?(::Psych::Exception) && e.instance_of?(::Psych::Exception)
360
+ ::Kernel.warn format(E_YAML_PERMITTED_CLASSES, e)
361
+ end
362
+ {}
363
+ end
364
+ end
365
+ end
366
+
367
+ # Enforces the `version_limit`, if set. Default: no limit.
368
+ # @api private
369
+ def enforce_version_limit!
370
+ limit = version_limit
371
+ return unless limit.is_a? Numeric
372
+ previous_versions = sibling_versions.not_creates.
373
+ order(self.class.timestamp_sort_order("asc"))
374
+ return unless previous_versions.size > limit
375
+ excess_versions = previous_versions - previous_versions.last(limit)
376
+ excess_versions.map(&:destroy)
377
+ end
378
+
379
+ # @api private
380
+ def sibling_versions
381
+ @sibling_versions ||= self.class.with_item_keys(item_type, item_id)
382
+ end
383
+
384
+ # See docs section 2.e. Limiting the Number of Versions Created.
385
+ # The version limit can be global or per-model.
386
+ #
387
+ # @api private
388
+ def version_limit
389
+ klass = item.class
390
+ if limit_option?(klass)
391
+ klass.snail_trail_options[:limit]
392
+ elsif base_class_limit_option?(klass)
393
+ klass.base_class.snail_trail_options[:limit]
394
+ else
395
+ SnailTrail.config.version_limit
396
+ end
397
+ end
398
+
399
+ def limit_option?(klass)
400
+ klass.respond_to?(:snail_trail_options) && klass.snail_trail_options.key?(:limit)
401
+ end
402
+
403
+ def base_class_limit_option?(klass)
404
+ klass.respond_to?(:base_class) && limit_option?(klass.base_class)
405
+ end
406
+ end
407
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SnailTrail
4
+ # The version number of the snail_trail gem. Not to be confused with
5
+ # `SnailTrail::Version`. Ruby constants are case-sensitive, apparently,
6
+ # and they are two different modules! It would be nice to remove `VERSION`,
7
+ # because of this confusion, but it's not worth the breaking change.
8
+ # People are encouraged to use `SnailTrail.gem_version` instead.
9
+ module VERSION
10
+ MAJOR = 1
11
+ MINOR = 0
12
+ TINY = 0
13
+
14
+ # Set PRE to nil unless it's a pre-release (beta, rc, etc.)
15
+ PRE = 'rc-1'
16
+
17
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".").freeze
18
+
19
+ def self.to_s
20
+ STRING
21
+ end
22
+ end
23
+ end
data/lib/snail_trail.rb CHANGED
@@ -1,6 +1,146 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'snail_trail/version'
3
+ # AR does not require all of AS, but ST does. ST uses core_ext like
4
+ # `String#squish`, so we require `active_support/all`. Instead of eagerly
5
+ # loading all of AS here, we could put specific `require`s in only the various
6
+ # ST files that need them, but this seems easier to troubleshoot, though it may
7
+ # add a few milliseconds to rails boot time. If that becomes a pain point, we
8
+ # can revisit this decision.
9
+ require "active_support/all"
4
10
 
11
+ # We used to `require "active_record"` here, but that was [replaced with a
12
+ # Railtie](https://github.com/BrandsInsurance/snail_trail/pull/1281) in ST 12.
13
+ # As a result, we cannot reference `ActiveRecord` in this file (ie. until our
14
+ # Railtie has loaded). If we did, it would cause [problems with non-Rails
15
+ # projects](https://github.com/BrandsInsurance/snail_trail/pull/1401).
16
+
17
+ require "snail_trail/errors"
18
+ require "snail_trail/cleaner"
19
+ require "snail_trail/compatibility"
20
+ require "snail_trail/config"
21
+ require "snail_trail/record_history"
22
+ require "snail_trail/request"
23
+ require "snail_trail/version_number"
24
+ require "snail_trail/serializers/json"
25
+
26
+ # An ActiveRecord extension that tracks changes to your models, for auditing or
27
+ # versioning.
5
28
  module SnailTrail
29
+ E_TIMESTAMP_FIELD_CONFIG = <<-EOS.squish.freeze
30
+ SnailTrail.timestamp_field= has been removed, without replacement. It is no
31
+ longer configurable. The timestamp column in the versions table must now be
32
+ named created_at.
33
+ EOS
34
+
35
+ extend SnailTrail::Cleaner
36
+
37
+ class << self
38
+ def transaction?
39
+ ::ActiveRecord::Base.connection.open_transactions.positive?
40
+ end
41
+
42
+ # Switches SnailTrail on or off, for all threads.
43
+ # @api public
44
+ def enabled=(value)
45
+ SnailTrail.config.enabled = value
46
+ end
47
+
48
+ # Returns `true` if SnailTrail is on, `false` otherwise. This is the
49
+ # on/off switch that affects all threads. Enabled by default.
50
+ # @api public
51
+ def enabled?
52
+ !!SnailTrail.config.enabled
53
+ end
54
+
55
+ # Returns SnailTrail's `::Gem::Version`, convenient for comparisons. This is
56
+ # recommended over `::SnailTrail::VERSION::STRING`.
57
+ #
58
+ # Added in 7.0.0
59
+ #
60
+ # @api public
61
+ def gem_version
62
+ ::Gem::Version.new(VERSION::STRING)
63
+ end
64
+
65
+ # Set variables for the current request, eg. whodunnit.
66
+ #
67
+ # All request-level variables are now managed here, as of ST 9. Having the
68
+ # word "request" right there in your application code will remind you that
69
+ # these variables only affect the current request, not all threads.
70
+ #
71
+ # Given a block, temporarily sets the given `options`, executes the block,
72
+ # and returns the value of the block.
73
+ #
74
+ # Without a block, this currently just returns `SnailTrail::Request`.
75
+ # However, please do not use `SnailTrail::Request` directly. Currently,
76
+ # `Request` is a `Module`, but in the future it is quite possible we may
77
+ # make it a `Class`. If we make such a choice, we will not provide any
78
+ # warning and will not treat it as a breaking change. You've been warned :)
79
+ #
80
+ # @api public
81
+ def request(options = nil, &block)
82
+ if options.nil? && !block
83
+ Request
84
+ else
85
+ Request.with(options, &block)
86
+ end
87
+ end
88
+
89
+ # Set the field which records when a version was created.
90
+ # @api public
91
+ def timestamp_field=(_field_name)
92
+ raise Error, E_TIMESTAMP_FIELD_CONFIG
93
+ end
94
+
95
+ # Set the SnailTrail serializer. This setting affects all threads.
96
+ # @api public
97
+ def serializer=(value)
98
+ SnailTrail.config.serializer = value
99
+ end
100
+
101
+ # Get the SnailTrail serializer used by all threads.
102
+ # @api public
103
+ def serializer
104
+ SnailTrail.config.serializer
105
+ end
106
+
107
+ # Returns SnailTrail's global configuration object, a singleton. These
108
+ # settings affect all threads.
109
+ # @api public
110
+ def config
111
+ @config ||= SnailTrail::Config.instance
112
+ yield @config if block_given?
113
+ @config
114
+ end
115
+ alias configure config
116
+
117
+ # @api public
118
+ def version
119
+ VERSION::STRING
120
+ end
121
+
122
+ def active_record_gte_7_0?
123
+ @active_record_gte_7_0 ||= ::ActiveRecord.gem_version >= ::Gem::Version.new("7.0.0")
124
+ end
125
+
126
+ def deprecator
127
+ @deprecator ||= ActiveSupport::Deprecation.new("16.0", "SnailTrail")
128
+ end
129
+ end
130
+ end
131
+
132
+ # ST is built on ActiveRecord, but does not require Rails. If Rails is defined,
133
+ # our Railtie makes sure not to load the AR-dependent parts of ST until AR is
134
+ # ready. A typical Rails `application.rb` has:
135
+ #
136
+ # ```
137
+ # require 'rails/all' # Defines `Rails`
138
+ # Bundler.require(*Rails.groups) # require 'snail_trail' (this file)
139
+ # ```
140
+ #
141
+ # Non-rails applications should take similar care to load AR before ST.
142
+ if defined?(Rails)
143
+ require "snail_trail/frameworks/rails"
144
+ else
145
+ require "snail_trail/frameworks/active_record"
6
146
  end