Class: PaperTrailDiff::PaperTrailAdapter

Inherits:
Object
  • Object
show all
Defined in:
lib/paper_trail_diff/paper_trail_adapter.rb,
sig/generated/paper_trail_diff/paper_trail_adapter.rbs

Overview

PaperTrail/ActiveRecord boundary that produces plain record snapshots. It is deliberately the widest class here: it fronts every public operation and is the only place allowed to know about both PaperTrail and the pure engine. Reconstruction logic lives in the collaborators it wires together.

Constant Summary collapse

CLOSE_ON_CURRENT =

The only thing a wall-clock window can close on besides a later version.

Returns:

  • (::Symbol)
:current

Instance Method Summary collapse

Constructor Details

#initialize(associations:, ignore:, reload_live_endpoints: true) ⇒ PaperTrailAdapter

: (associations: Array[String | Symbol], ignore: ignore_option, ?reload_live_endpoints: bool) -> void

Parameters:

  • associations: (Array[String | Symbol])
  • ignore: (ignore_option)
  • reload_live_endpoints: (Boolean) (defaults to: true)


14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 14

def initialize(associations:, ignore:, reload_live_endpoints: true)
  @association_tree = AssociationTree.build(associations)
  @ignore_policy = IgnorePolicy.build(ignore, association_paths: @association_tree.paths)
  @traversal = AssociationTraversal.new(@association_tree)
  @traversal_preparer = TraversalPreparer.new(tree: @association_tree, traversal: @traversal)
  @live_endpoints = LiveEndpointProvider.new(
    tree: @association_tree, traversal: @traversal, reload: reload_live_endpoints
  )
  @instrumentation_payload = @live_endpoints.comparison_payload
  @snapshot_pool = SnapshotPool.new
  @normalizer = SnapshotNormalizer.new(
    tree: @association_tree, ignore_policy: @ignore_policy,
    traversal: @traversal, pool: @snapshot_pool
  )
  build_snapshotters
end

Instance Method Details

#activity_builder(record, from:, to:, within:, version_scope: nil, live_endpoint: nil, snapshots: false, group: nil) ⇒ ActivityTimelineBuilder

: (untyped, from: untyped, to: untyped, within: untyped, ?version_scope: untyped, ?live_endpoint: untyped, ?snapshots: bool, ?group: Symbol?) -> ActivityTimelineBuilder

Parameters:

  • (Object)
  • from: (Object)
  • to: (Object)
  • within: (Object)
  • version_scope: (Object) (defaults to: nil)
  • live_endpoint: (Object) (defaults to: nil)
  • snapshots: (Boolean) (defaults to: false)
  • group: (Symbol, nil) (defaults to: nil)

Returns:



256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 256

def activity_builder(record, from:, to:, within:, version_scope: nil, live_endpoint: nil, snapshots: false, group: nil) # rubocop:disable Metrics/ParameterLists, Layout/LineLength
  ActivityTimelineBuilder.new(
    record,
    range: TimelineRange.new(
      record, from: from, to: to, within: within, version_scope: version_scope,
              live_endpoint: live_endpoint
    ),
    tree: @association_tree,
    snapshotter: @activity_snapshotter,
    snapshots: snapshots,
    group: group
  )
end

#activity_timeline(record, from:, to:, within:, version_scope: nil, close_on: nil, snapshots: false, group: nil) ⇒ Array[ActivityStep]

: (untyped, from: untyped, to: untyped, within: untyped, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool, ?group: Symbol?) -> Array

Parameters:

  • (Object)
  • from: (Object)
  • to: (Object)
  • within: (Object)
  • version_scope: (Object) (defaults to: nil)
  • close_on: (Symbol, nil) (defaults to: nil)
  • snapshots: (Boolean) (defaults to: false)
  • group: (Symbol, nil) (defaults to: nil)

Returns:



72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 72

def activity_timeline(record, from:, to:, within:, version_scope: nil, close_on: nil, snapshots: false, group: nil) # rubocop:disable Metrics/ParameterLists, Layout/LineLength
  payload = @instrumentation_payload.merge(model_type: record.class.base_class.name.to_s)
  Instrumentation.instrument('activity_timeline', payload) do
    @traversal_preparer.call(record.class, historical: true)
    live = live_endpoint_for(record, close_on, within)
    reject_live_habtm_activity!(record.class) if Endpoint.record?(to) || live
    steps = activity_builder(
      record, from: from, to: to, within: within, version_scope: version_scope,
              live_endpoint: live, snapshots: snapshots, group: group
    ).build
    payload[:step_count] = steps.length
    steps
  end
end

#analyze(record, from:, to:, within:, activity: false, version_scope: nil, close_on: nil, snapshots: false, group: nil) ⇒ Analysis

: (untyped, from: untyped, to: untyped, within: untyped, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool, ?group: Symbol?) -> Analysis

Parameters:

  • (Object)
  • from: (Object)
  • to: (Object)
  • within: (Object)
  • activity: (Boolean) (defaults to: false)
  • version_scope: (Object) (defaults to: nil)
  • close_on: (Symbol, nil) (defaults to: nil)
  • snapshots: (Boolean) (defaults to: false)
  • group: (Symbol, nil) (defaults to: nil)

Returns:



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 88

def analyze(record, from:, to:, within:, activity: false, version_scope: nil, close_on: nil, snapshots: false, group: nil) # rubocop:disable Metrics/ParameterLists, Layout/LineLength
  @traversal_preparer.call(record.class, historical: true)
  live = live_endpoint_for(record, close_on, within)
  if activity
    return analyze_activity(
      record, from: from, to: to, within: within, version_scope: version_scope,
              live_endpoint: live, snapshots: snapshots, group: group
    )
  end

  TimelineBuilder.new(
    record, from: from, to: to, within: within, version_scope: version_scope,
            live_endpoint: live, snapshotter: @timeline_snapshotter
  ).analyze
end

#analyze_activity(record, from:, to:, within:, version_scope:, live_endpoint:, snapshots: false, group: nil) ⇒ Analysis

: (untyped, from: untyped, to: untyped, within: untyped, version_scope: untyped, live_endpoint: untyped, ?snapshots: bool, ?group: Symbol?) -> Analysis

Parameters:

  • (Object)
  • from: (Object)
  • to: (Object)
  • within: (Object)
  • version_scope: (Object)
  • live_endpoint: (Object)
  • snapshots: (Boolean) (defaults to: false)
  • group: (Symbol, nil) (defaults to: nil)

Returns:



155
156
157
158
159
160
161
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 155

def analyze_activity(record, from:, to:, within:, version_scope:, live_endpoint:, snapshots: false, group: nil) # rubocop:disable Metrics/ParameterLists, Layout/LineLength
  reject_live_habtm_activity!(record.class) if live_endpoint
  activity_builder(
    record, from: from, to: to, within: within, version_scope: version_scope,
            live_endpoint: live_endpoint, snapshots: snapshots, group: group
  ).analyze
end

#analyze_many(records, within:, activity: false, version_scope: nil, close_on: nil) ⇒ Hash[identity, Analysis]

Analyzes many roots over one shared range, preparing their history once. : (Array, within: untyped, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?) -> Hash[identity, Analysis]

Parameters:

  • (Array[untyped])
  • within: (Object)
  • activity: (Boolean) (defaults to: false)
  • version_scope: (Object) (defaults to: nil)
  • close_on: (Symbol, nil) (defaults to: nil)

Returns:



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 106

def analyze_many(records, within:, activity: false, version_scope: nil, close_on: nil)
  count = records.is_a?(Array) ? records.length : 0
  payload = @instrumentation_payload.merge(comparison_count: count)
  Instrumentation.instrument('analyze_many', payload) do
    AnalysisBatch.new(
      records,
      time_range: within.nil? ? nil : TimeRange.new(within),
      version_scope: version_scope,
      close_on_current: close_on_current?(close_on, within),
      live_loader: @live_endpoints.method(:call),
      history_preparer: @historical_store.method(:prepare_batch),
      analyzer: batched_root_analyzer(activity)
    ).call
  end
end

#analyze_scope(scope, limit:, within:, activity: false, version_scope: nil, close_on: nil) ⇒ ScopedAnalysis

Selects the roots from a relation before running the ordinary batch, so a caller reporting on a population does not have to rediscover which of its members changed. The roots the relation could not reach come back named rather than dropped -- see PaperTrailDiff.analyze_scope. : (untyped, limit: Integer?, within: untyped, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?) -> ScopedAnalysis

Parameters:

  • (Object)
  • limit: (Integer, nil)
  • within: (Object)
  • activity: (Boolean) (defaults to: false)
  • version_scope: (Object) (defaults to: nil)
  • close_on: (Symbol, nil) (defaults to: nil)

Returns:



127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 127

def analyze_scope(scope, limit:, within:, activity: false, version_scope: nil, close_on: nil) # rubocop:disable Metrics/ParameterLists
  raise ConfigurationError, 'limit: is required when selecting roots by scope' if limit.nil?

  selection = ScopedRootSelection.new(
    scope, time_range: within.nil? ? nil : TimeRange.new(within), limit: limit
  ).call
  ScopedAnalysis.new(
    analyses: analyze_many(selection.records, within: within, activity: activity,
                                              version_scope: version_scope, close_on: close_on),
    unreachable: selection.unreachable
  )
end

#batched_root_analyzer(activity) ⇒ BatchedRootAnalyzer

: (bool) -> BatchedRootAnalyzer

Parameters:

  • (Boolean)

Returns:



217
218
219
220
221
222
223
224
225
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 217

def batched_root_analyzer(activity)
  BatchedRootAnalyzer.new(
    tree: @association_tree,
    timeline_snapshotter: @timeline_snapshotter,
    activity_snapshotter: @activity_snapshotter,
    preparer: @traversal_preparer.method(:call),
    activity: activity
  )
end

#build_activity_snapshotterActivitySnapshotProvider

: () -> ActivitySnapshotProvider



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 238

def build_activity_snapshotter
  refresher = BranchSnapshotRefresher.new(
    tree: @association_tree, ignore_policy: @ignore_policy,
    traversal: @traversal, pool: @snapshot_pool,
    normalizer: @normalizer,
    full_snapshotter: method(:snapshot_at),
    partial_snapshotter: @historical_store.method(:custom),
    association_reader: @historical_store.method(:association_reader),
    record_transition: @historical_store.method(:record_transition)
  )
  ActivitySnapshotProvider.new(
    snapshotter: method(:snapshot_at),
    refresher: refresher,
    preparer: @historical_store.method(:prepare)
  )
end

#build_historical_storeHistoricalSnapshotStore

: () -> HistoricalSnapshotStore



228
229
230
231
232
233
234
235
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 228

def build_historical_store
  HistoricalSnapshotStore.new(
    tree: @association_tree,
    traversal: @traversal,
    normalizer: @normalizer,
    preparer: @traversal_preparer.method(:call)
  )
end

#build_snapshottersvoid

This method returns an undefined value.

: () -> void



208
209
210
211
212
213
214
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 208

def build_snapshotters
  @historical_store = build_historical_store
  @timeline_snapshotter = TimelineSnapshotProvider.new(
    @historical_store, live_snapshotter: method(:live_snapshot)
  )
  @activity_snapshotter = build_activity_snapshotter
end

#close_on_current?(close_on, within) ⇒ Boolean

close_on: names what ends a wall-clock window, so it is meaningless for a range whose endpoints the caller already gave explicitly. : (Symbol?, untyped) -> bool

Parameters:

  • (Symbol, nil)
  • (Object)

Returns:

  • (Boolean)


187
188
189
190
191
192
193
194
195
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 187

def close_on_current?(close_on, within)
  return false if close_on.nil?
  unless close_on == CLOSE_ON_CURRENT
    raise ConfigurationError, "close_on: must be #{CLOSE_ON_CURRENT.inspect} or nil"
  end
  raise ConfigurationError, 'close_on: requires `within`' if within.nil?

  true
end

#compare(from_endpoint, to_endpoint) ⇒ Diff

: (untyped, untyped) -> Diff

Parameters:

  • (Object)
  • (Object)

Returns:



32
33
34
35
36
37
38
39
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 32

def compare(from_endpoint, to_endpoint)
  payload = @instrumentation_payload.merge(comparison_count: 1)
  Instrumentation.instrument('compare', payload) do
    Endpoint.validate_pair!(from_endpoint, to_endpoint)
    notify_ambiguous_association_boundary(from_endpoint, to_endpoint)
    Engine.compare(snapshot_for_endpoint(from_endpoint), snapshot_for_endpoint(to_endpoint))
  end
end

#compare_many(comparisons) ⇒ comparison_results

Compares independent pairs and returns immutable results keyed by item identity. : (Array) -> comparison_results

Parameters:

  • (Array[comparison_input])

Returns:

  • (comparison_results)


43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 43

def compare_many(comparisons)
  count = comparisons.is_a?(Array) ? comparisons.length : 0
  payload = @instrumentation_payload.merge(comparison_count: count)
  Instrumentation.instrument('compare_many', payload) do
    ComparisonBatch.new(
      comparisons,
      live_loader: @live_endpoints.method(:call), preparer: @traversal_preparer.method(:call),
      history_preparer: @historical_store.method(:prepare_batch),
      historical_snapshotter: method(:historical_snapshot),
      live_normalizer: method(:normalize_live_snapshot)
    ).call
  end
end

#historical_snapshot(version) ⇒ RecordSnapshot?

: (untyped) -> RecordSnapshot?

Parameters:

  • (Object)

Returns:



285
286
287
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 285

def historical_snapshot(version)
  snapshot_at(version, version)
end

#live_endpoint_for(record, close_on, within) ⇒ Object

A destroyed root has no current state to close on, and its own destroy version already terminates the history. : (untyped, Symbol?, untyped) -> untyped

Parameters:

  • (Object)
  • (Symbol, nil)
  • (Object)

Returns:

  • (Object)


200
201
202
203
204
205
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 200

def live_endpoint_for(record, close_on, within)
  return unless close_on_current?(close_on, within)
  return unless Endpoint.record?(record) && !record.destroyed?

  record
end

#live_snapshot(record) ⇒ RecordSnapshot

: (untyped) -> RecordSnapshot

Parameters:

  • (Object)

Returns:



302
303
304
305
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 302

def live_snapshot(record)
  current = @live_endpoints.call([record]).fetch(Endpoint.identity(record))
  normalize_live_snapshot(current)
end

#normalize_live_snapshot(current) ⇒ RecordSnapshot

: (untyped) -> RecordSnapshot

Parameters:

  • (Object)

Returns:



308
309
310
311
312
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 308

def normalize_live_snapshot(current)
  @traversal_preparer.call(current.class, historical: false)
  @normalizer.call(current, reifier: LiveAssociationReader.new) ||
    raise(InvalidEndpointError, 'current record endpoint could not be normalized')
end

#notify_ambiguous_association_boundary(from_endpoint, to_endpoint) ⇒ void

This method returns an undefined value.

Association membership is resolved by timestamp, so endpoints sharing one cannot be told apart and any association change between them is invisible. The result may still be correct -- nothing associated may have changed -- and the gem cannot tell which, since not seeing the change is the symptom. So it reports the condition and leaves the judgement to the application. : (untyped, untyped) -> void

Parameters:

  • (Object)
  • (Object)


169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 169

def notify_ambiguous_association_boundary(from_endpoint, to_endpoint)
  return if @association_tree.empty?
  return unless Endpoint.version?(from_endpoint) && Endpoint.version?(to_endpoint)
  return unless from_endpoint.created_at == to_endpoint.created_at

  Instrumentation.notify(
    'ambiguous_association_boundary',
    @instrumentation_payload.merge(
      item_type: from_endpoint.item_type.to_s,
      version_ids: [from_endpoint.id, to_endpoint.id].freeze,
      recorded_at: from_endpoint.created_at
    )
  )
end

#reject_live_habtm_activity!(model_class) ⇒ void

This method returns an undefined value.

: (untyped) -> void

Parameters:

  • (Object)


271
272
273
274
275
276
277
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 271

def reject_live_habtm_activity!(model_class)
  paths = @traversal.habtm_paths(model_class)
  return if paths.empty?

  message = "live-ended activity cannot reconstruct HABTM event boundaries: #{paths.join(', ')}"
  raise UnsupportedLiveActivityError, message
end

#snapshot_at(root_endpoint, context_endpoint) ⇒ RecordSnapshot?

: (untyped, untyped) -> RecordSnapshot?

Parameters:

  • (Object)
  • (Object)

Returns:



295
296
297
298
299
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 295

def snapshot_at(root_endpoint, context_endpoint)
  return live_snapshot(root_endpoint) if Endpoint.record?(context_endpoint)

  @historical_store.call(root_endpoint, context_endpoint)
end

#snapshot_for_endpoint(endpoint) ⇒ RecordSnapshot?

: (untyped) -> RecordSnapshot?

Parameters:

  • (Object)

Returns:



280
281
282
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 280

def snapshot_for_endpoint(endpoint)
  Endpoint.version?(endpoint) ? historical_snapshot(endpoint) : live_snapshot(endpoint)
end

#timeline(record, from:, to:, within:, version_scope: nil, close_on: nil) ⇒ Array[Step]

: (untyped, from: untyped, to: untyped, within: untyped, ?version_scope: untyped, ?close_on: Symbol?) -> Array

Parameters:

  • (Object)
  • from: (Object)
  • to: (Object)
  • within: (Object)
  • version_scope: (Object) (defaults to: nil)
  • close_on: (Symbol, nil) (defaults to: nil)

Returns:



58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 58

def timeline(record, from:, to:, within:, version_scope: nil, close_on: nil) # rubocop:disable Metrics/ParameterLists
  @traversal_preparer.call(record.class, historical: true)
  TimelineBuilder.new(
    record,
    from: from,
    to: to,
    within: within,
    version_scope: version_scope,
    live_endpoint: live_endpoint_for(record, close_on, within),
    snapshotter: @timeline_snapshotter
  ).build
end

#uncached_historical_snapshot(version) ⇒ RecordSnapshot?

: (untyped) -> RecordSnapshot?

Parameters:

  • (Object)

Returns:



290
291
292
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 290

def uncached_historical_snapshot(version)
  @historical_store.uncached(version, version)
end