Class: PaperTrailDiff::TimeActivityTimelineBuilder

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

Overview

Builds activity views for mutations selected by a wall-clock range.

Instance Method Summary collapse

Methods included from ActivityGrouping

#group_steps, #grouping?

Constructor Details

#initialize(record, range:, tree:, snapshotter:, snapshots: false, group: nil) ⇒ TimeActivityTimelineBuilder

: (untyped, range: TimelineRange, tree: AssociationTree, snapshotter: untyped, ?snapshots: bool, ?group: Symbol?) -> void

Parameters:

  • (Object)
  • range: (TimelineRange)
  • tree: (AssociationTree)
  • snapshotter: (Object)
  • snapshots: (Boolean) (defaults to: false)
  • group: (Symbol, nil) (defaults to: nil)


10
11
12
13
14
15
16
17
18
19
20
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 10

def initialize(record, range:, tree:, snapshotter:, snapshots: false, group: nil) # rubocop:disable Metrics/ParameterLists
  @snapshots = snapshots
  @group = group
  # Merging a group compares its outer states, so the snapshots must survive
  # the build even when the caller did not ask to keep them.
  @retain = snapshots || grouping?
  @record = record
  @range = range
  @tree = tree
  @snapshotter = snapshotter
end

Instance Method Details

#activity_steps(history, closing) ⇒ Array[ActivityStep]

: (ActivityHistory, ActivityStep?) -> Array

Parameters:

Returns:



106
107
108
109
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 106

def activity_steps(history, closing)
  steps = closing ? history.steps + [closing] : history.steps
  group_steps(steps).freeze
end

#analyzeAnalysis

: () -> Analysis

Returns:



29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 29

def analyze
  history, plan, closing, closing_snapshot, captured_at = history_and_versions
  final = closing_snapshot || history.last_snapshot
  Analysis.new(
    diff: Engine.compare(history.first_snapshot, final),
    timeline: ActivityRootSteps.call(
      plan, history.root_snapshots,
      closing_snapshot: closing_snapshot, captured_at: captured_at
    ),
    activity_timeline: activity_steps(history, closing),
    from_snapshot: history.first_snapshot,
    to_snapshot: final
  )
end

#buildArray[ActivityStep]

: () -> Array

Returns:



23
24
25
26
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 23

def build
  history, _plan, closing, = history_and_versions
  activity_steps(history, closing)
end

#build_history(root_versions, events) ⇒ ActivityHistory

: (Array, Array) -> ActivityHistory

Parameters:

Returns:



95
96
97
98
99
100
101
102
103
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 95

def build_history(root_versions, events)
  ActivityHistoryBuilder.new(
    root_versions,
    events,
    @snapshotter,
    include_step: ->(event) { @range.include?(event.version) },
    snapshots: @retain
  ).call
end

#closing_step(history, event, plan, captured_at, snapshot) ⇒ ActivityStep?

A window either closes on the absence its final destruction leaves, or on current state when it reaches past everything recorded. Both are boundaries no version can supply. : (ActivityHistory, ActivityEvent?, RootVersionPlan, untyped, RecordSnapshot?) -> ActivityStep?

Parameters:

Returns:



129
130
131
132
133
134
135
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 129

def closing_step(history, event, plan, captured_at, snapshot)
  return unless event
  return destroyed_step(history, event) if terminal_destroy?(event)
  return unless plan.closing_record

  current_step(history, plan.closing_record, captured_at, snapshot)
end

#collect_events(root_versions, range_end: nil) ⇒ Array[ActivityEvent]

: (Array, ?range_end: untyped) -> Array

Parameters:

  • (Array[untyped])
  • range_end: (Object) (defaults to: nil)

Returns:



181
182
183
184
185
186
187
188
189
190
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 181

def collect_events(root_versions, range_end: nil)
  ActivityVersionCollector.new(
    @record,
    root_versions: root_versions,
    tree: @tree,
    traversal: AssociationTraversal.new(@tree),
    range_start: @range.begin_time,
    range_end: range_end || root_versions.last
  ).call
end

#current_snapshot(plan) ⇒ RecordSnapshot?

One live read serves both the closing activity step and the endpoint the analysis reports, which would otherwise reconstruct the graph twice. : (RootVersionPlan) -> RecordSnapshot?

Parameters:

Returns:



81
82
83
84
85
86
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 81

def current_snapshot(plan)
  record = plan.closing_record
  return unless record

  @snapshotter.call(record, record)
end

#current_step(history, record, captured_at, snapshot) ⇒ ActivityStep?

The last boundary reached is where current state is compared from, which is the final event rather than the final root version once descendants have moved since it. : (ActivityHistory, untyped, untyped, RecordSnapshot?) -> ActivityStep?

Parameters:

Returns:



152
153
154
155
156
157
158
159
160
161
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 152

def current_step(history, record, captured_at, snapshot)
  previous = history.steps.last&.to_boundary
  return unless previous

  ActivityStep.between(
    from_boundary: previous,
    to_boundary: ActivityBoundary.current(record, captured_at: captured_at),
    from_snapshot: history.last_snapshot, to_snapshot: snapshot, retain: @retain
  )
end

#destroyed_step(history, event) ⇒ ActivityStep

: (ActivityHistory, ActivityEvent) -> ActivityStep

Parameters:

Returns:



138
139
140
141
142
143
144
145
146
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 138

def destroyed_step(history, event)
  version = event.version
  ActivityStep.between(
    from_boundary: ActivityBoundary.from_version(version),
    to_boundary: ActivityBoundary.destroyed(version),
    from_snapshot: history.root_snapshots[ActivityRootSteps.version_key(version)],
    to_snapshot: nil, retain: @retain
  )
end

#events_through(root_versions, captured_at) ⇒ Array[ActivityEvent]

: (Array, untyped) -> Array

Parameters:

  • (Array[untyped])
  • (Object)

Returns:



89
90
91
92
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 89

def events_through(root_versions, captured_at)
  prepare_history(root_versions, end_at: captured_at)
  collect_events(root_versions, range_end: captured_at)
end

#history_and_versions[ ActivityHistory, RootVersionPlan, ActivityStep?, RecordSnapshot?, untyped ]

: () -> [ActivityHistory, RootVersionPlan, ActivityStep?, RecordSnapshot?, untyped]



53
54
55
56
57
58
59
60
61
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 53

def history_and_versions
  plan = @range.select_plan(context_required: !@tree.empty?)
  return [ActivityHistory.empty, plan, nil, nil, nil] if plan.reconstruction_versions.empty?

  # A window closing on current state runs to the instant it is captured,
  # not to the last root version, or descendants that moved after that
  # version would be missing from a report that claims to reach now.
  history_for(plan, plan.closing_record ? Time.now.utc : nil)
end

#history_for(plan, captured_at) ⇒ [ ActivityHistory, RootVersionPlan, ActivityStep?, RecordSnapshot?, untyped ]

: (RootVersionPlan, untyped) -> [ActivityHistory, RootVersionPlan, ActivityStep?, RecordSnapshot?, untyped]

Parameters:

Returns:



64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 64

def history_for(plan, captured_at)
  root_versions = plan.reconstruction_versions
  events = events_through(root_versions, captured_at)
  selected = selected_events(events, plan)
  unless time_events?(selected, events, plan)
    return [ActivityHistory.empty, plan, nil, nil, nil]
  end

  history = build_history(root_versions, events)
  snapshot = current_snapshot(plan)
  closing = closing_step(history, selected.last, plan, captured_at, snapshot)
  [history, plan, closing, snapshot, captured_at]
end

#later_event?(selected, events) ⇒ Boolean

: (ActivityEvent, Array) -> bool

Parameters:

Returns:

  • (Boolean)


206
207
208
209
210
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 206

def later_event?(selected, events)
  events.any? do |event|
    Support.compare_versions(selected.version, event.version).negative?
  end
end

#prepare_history(root_versions, end_at: nil) ⇒ void

This method returns an undefined value.

: (Array, ?end_at: untyped) -> void

Parameters:

  • (Array[untyped])
  • end_at: (Object) (defaults to: nil)


171
172
173
174
175
176
177
178
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 171

def prepare_history(root_versions, end_at: nil)
  return unless @snapshotter.respond_to?(:prepare)

  start_at = @range.begin_time
  return @snapshotter.prepare(@record, root_versions, start_at: start_at) unless end_at

  @snapshotter.prepare(@record, root_versions, start_at: start_at, end_at: end_at)
end

#selected_events(events, plan) ⇒ Array[ActivityEvent]

Root versions inside the window that the plan does not report are context rather than mutations: the version appended to reveal the last selected change, and anything a filter excluded but the replay still walks. Neither may be counted as a selected mutation. Descendant events are not filtered, so window membership is the whole test for them. : (Array, RootVersionPlan) -> Array

Parameters:

Returns:



117
118
119
120
121
122
123
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 117

def selected_events(events, plan)
  events.select do |event|
    next false unless @range.include?(event.version)

    !event.root? || plan.mutation?(event.version)
  end
end

#terminal_destroy?(event) ⇒ Boolean

: (ActivityEvent?) -> bool

Parameters:

Returns:

  • (Boolean)


164
165
166
167
168
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 164

def terminal_destroy?(event)
  return false unless event

  event.root? && event.version.event.to_s == 'destroy'
end

#time_events?(selected, events, plan) ⇒ Boolean

: (Array, Array, RootVersionPlan) -> bool

Parameters:

Returns:

  • (Boolean)


193
194
195
196
197
198
199
200
201
202
203
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 193

def time_events?(selected, events, plan)
  return false if selected.empty?
  return true if later_event?(selected.last, events)
  return true if terminal_destroy?(selected.last)
  return true if plan.closing_record

  message = 'time range requires a later activity boundary to reconstruct its ' \
            'final change: pass close_on: :current to end at current state, ' \
            'or narrow the window to end before the last recorded boundary'
  raise IncompleteTimeRangeError, message
end