Class: PaperTrailDiff::PaperTrailAdapter
- Inherits:
-
Object
- Object
- PaperTrailDiff::PaperTrailAdapter
- 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.
:current
Instance Method Summary collapse
-
#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.
-
#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.
-
#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.
-
#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.
-
#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.
-
#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.
-
#batched_root_analyzer(activity) ⇒ BatchedRootAnalyzer
: (bool) -> BatchedRootAnalyzer.
-
#build_activity_snapshotter ⇒ ActivitySnapshotProvider
: () -> ActivitySnapshotProvider.
-
#build_historical_store ⇒ HistoricalSnapshotStore
: () -> HistoricalSnapshotStore.
-
#build_snapshotters ⇒ void
: () -> void.
-
#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. -
#compare(from_endpoint, to_endpoint) ⇒ Diff
: (untyped, untyped) -> Diff.
-
#compare_many(comparisons) ⇒ comparison_results
Compares independent pairs and returns immutable results keyed by item identity.
-
#historical_snapshot(version) ⇒ RecordSnapshot?
: (untyped) -> RecordSnapshot?.
-
#initialize(associations:, ignore:, reload_live_endpoints: true) ⇒ PaperTrailAdapter
constructor
: (associations: Array[String | Symbol], ignore: ignore_option, ?reload_live_endpoints: bool) -> void.
-
#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.
-
#live_snapshot(record) ⇒ RecordSnapshot
: (untyped) -> RecordSnapshot.
-
#normalize_live_snapshot(current) ⇒ RecordSnapshot
: (untyped) -> RecordSnapshot.
-
#notify_ambiguous_association_boundary(from_endpoint, to_endpoint) ⇒ void
Association membership is resolved by timestamp, so endpoints sharing one cannot be told apart and any association change between them is invisible.
-
#reject_live_habtm_activity!(model_class) ⇒ void
: (untyped) -> void.
-
#snapshot_at(root_endpoint, context_endpoint) ⇒ RecordSnapshot?
: (untyped, untyped) -> RecordSnapshot?.
-
#snapshot_for_endpoint(endpoint) ⇒ RecordSnapshot?
: (untyped) -> RecordSnapshot?.
-
#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.
-
#uncached_historical_snapshot(version) ⇒ RecordSnapshot?
: (untyped) -> RecordSnapshot?.
Constructor Details
#initialize(associations:, ignore:, reload_live_endpoints: true) ⇒ PaperTrailAdapter
: (associations: Array[String | Symbol], ignore: ignore_option, ?reload_live_endpoints: bool) -> void
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
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
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
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
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]
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
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
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_snapshotter ⇒ ActivitySnapshotProvider
: () -> 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_store ⇒ HistoricalSnapshotStore
: () -> 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_snapshotters ⇒ void
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
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
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
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?
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
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
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
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
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
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? = "live-ended activity cannot reconstruct HABTM event boundaries: #{paths.join(', ')}" raise UnsupportedLiveActivityError, end |
#snapshot_at(root_endpoint, context_endpoint) ⇒ RecordSnapshot?
: (untyped, untyped) -> RecordSnapshot?
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?
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
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?
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 |