Class: PaperTrailDiff::TimeActivityTimelineBuilder

Inherits:
Object
  • Object
show all
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

Constructor Details

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

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

Parameters:



8
9
10
11
12
13
14
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 8

def initialize(record, range:, tree:, snapshotter:, snapshots: false)
  @snapshots = snapshots
  @record = record
  @range = range
  @tree = tree
  @snapshotter = snapshotter
end

Instance Method Details

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

: (ActivityHistory, ActivityStep?) -> Array

Parameters:

Returns:



100
101
102
103
104
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 100

def activity_steps(history, closing)
  return history.steps unless closing

  (history.steps + [closing]).freeze
end

#analyzeAnalysis

: () -> Analysis

Returns:



23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 23

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:



17
18
19
20
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 17

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

#build_history(root_versions, events) ⇒ ActivityHistory

: (Array, Array) -> ActivityHistory

Parameters:

Returns:



89
90
91
92
93
94
95
96
97
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 89

def build_history(root_versions, events)
  ActivityHistoryBuilder.new(
    root_versions,
    events,
    @snapshotter,
    include_step: ->(event) { @range.include?(event.version) },
    snapshots: @snapshots
  ).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:



124
125
126
127
128
129
130
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 124

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:



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

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:



75
76
77
78
79
80
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 75

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:



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

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: @snapshots
  )
end

#destroyed_step(history, event) ⇒ ActivityStep

: (ActivityHistory, ActivityEvent) -> ActivityStep

Parameters:

Returns:



133
134
135
136
137
138
139
140
141
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 133

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: @snapshots
  )
end

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

: (Array, untyped) -> Array

Parameters:

  • (Array[untyped])
  • (Object)

Returns:



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

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]



47
48
49
50
51
52
53
54
55
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 47

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:



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

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)


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

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)


166
167
168
169
170
171
172
173
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 166

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:



112
113
114
115
116
117
118
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 112

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)


159
160
161
162
163
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 159

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)


188
189
190
191
192
193
194
195
196
197
198
# File 'lib/paper_trail_diff/time_activity_timeline_builder.rb', line 188

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