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:



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

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:



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

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:



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

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:



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

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:



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

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:



177
178
179
180
181
182
183
184
185
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 177

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



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 198

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



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

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



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

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)


147
148
149
150
151
152
153
154
155
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 147

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


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

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:



244
245
246
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 244

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)


160
161
162
163
164
165
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 160

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:



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

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:



267
268
269
270
271
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 267

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

#reject_live_habtm_activity!(model_class) ⇒ void

This method returns an undefined value.

: (untyped) -> void

Parameters:

  • (Object)


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

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:



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

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:



239
240
241
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 239

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:



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

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:



249
250
251
# File 'lib/paper_trail_diff/paper_trail_adapter.rb', line 249

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