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) ⇒ ActivityTimelineBuilder

: (untyped, from: untyped, to: untyped, within: untyped, ?version_scope: untyped, ?live_endpoint: untyped, ?snapshots: bool) -> 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)

Returns:



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

def activity_builder(record, from:, to:, within:, version_scope: nil, live_endpoint: nil, snapshots: false) # 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
  )
end

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

: (untyped, from: untyped, to: untyped, within: untyped, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool) -> 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)

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) # 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
    ).build
    payload[:step_count] = steps.length
    steps
  end
end

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

: (untyped, from: untyped, to: untyped, within: untyped, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool) -> 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)

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) # 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
    )
  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) ⇒ Analysis

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

Parameters:

  • (Object)
  • from: (Object)
  • to: (Object)
  • within: (Object)
  • version_scope: (Object)
  • live_endpoint: (Object)
  • snapshots: (Boolean) (defaults to: false)

Returns:



137
138
139
140
141
142
143
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 137

def analyze_activity(record, from:, to:, within:, version_scope:, live_endpoint:, snapshots: false) # 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
  ).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

#batched_root_analyzer(activity) ⇒ BatchedRootAnalyzer

: (bool) -> BatchedRootAnalyzer

Parameters:

  • (Boolean)

Returns:



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

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



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 220

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



210
211
212
213
214
215
216
217
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 210

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



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

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)


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

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:



266
267
268
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 266

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)


182
183
184
185
186
187
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 182

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:



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

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:



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

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)


151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 151

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)


252
253
254
255
256
257
258
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 252

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:



276
277
278
279
280
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 276

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:



261
262
263
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 261

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:



271
272
273
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 271

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