Class: PaperTrailDiff::RootVersionSelection

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

Overview

Decides which versions a range reports, and which one reveals the last of them. A version records the state before its own event, so whatever follows the final selected mutation is the only thing that can show what it produced. That successor is reconstruction context rather than a reported mutation, so a filter must never remove it.

Both the single-record and the batched selectors resolve this here, because the rule is subtle enough that two copies of it would drift apart.

Constant Summary collapse

INCOMPLETE =

An error is a specification, so it names the way out as well as the wall. A window ending at the present can never gain a later version, which makes "write a checkpoint afterwards" impossible advice on its own.

Returns:

  • (::String)
'time range requires a later root version to reconstruct its ' \
'final change: pass close_on: :current to end at current state, ' \
'or narrow the window to end before the last recorded version'

Instance Method Summary collapse

Constructor Details

#initialize(in_range:, selected:, after_range:, windowed:, context_required: false, filtered: false, live_endpoint: nil) ⇒ RootVersionSelection

: (in_range: Array, selected: Array, after_range: untyped, windowed: bool, ?context_required: bool, ?filtered: bool, ?live_endpoint: untyped) -> void

Parameters:

  • in_range: (Array[untyped])
  • selected: (Array[untyped])
  • after_range: (Object)
  • windowed: (Boolean)
  • context_required: (Boolean) (defaults to: false)
  • filtered: (Boolean) (defaults to: false)
  • live_endpoint: (Object) (defaults to: nil)


22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/paper_trail_diff/root_version_selection.rb', line 22

def initialize( # rubocop:disable Metrics/ParameterLists
  in_range:, selected:, after_range:, windowed:, context_required: false, filtered: false,
  live_endpoint: nil
)
  @in_range = in_range
  @selected = selected
  @after_range = after_range
  @windowed = windowed
  @context_required = context_required
  @filtered = filtered
  @live_endpoint = live_endpoint
end

Instance Method Details

#callRootVersionPlan

: () -> RootVersionPlan

Returns:



36
37
38
39
40
41
42
43
44
# File 'lib/paper_trail_diff/root_version_selection.rb', line 36

def call
  return without_selection if @selected.empty?

  closing = revealing_version || @live_endpoint
  raise IncompleteTimeRangeError, INCOMPLETE if !closing && @windowed && !terminal_destroy?
  return filtered_plan(closing) if @filtered

  contiguous_plan(closing)
end

#chronological(versions) ⇒ Array[untyped]

: (Array) -> Array

Parameters:

  • (Array[untyped])

Returns:

  • (Array[untyped])


136
137
138
# File 'lib/paper_trail_diff/root_version_selection.rb', line 136

def chronological(versions)
  Support.chronological_sort(versions.uniq { |version| [version.class.name, version.id] })
end

#closing_versionsArray[untyped]

Nothing can follow a destroy, so it never pairs into a step. It is still a selected mutation, and the activity view closes on the absence it leaves, so it has to stay a boundary or a filtered report loses the deletion entirely — the one event it can least afford to drop. : () -> Array

Returns:

  • (Array[untyped])


115
116
117
# File 'lib/paper_trail_diff/root_version_selection.rb', line 115

def closing_versions
  terminal_destroy? ? [@selected.last] : []
end

#contiguous_plan(closing) ⇒ RootVersionPlan

A window reaching past the last recorded version has only the live record left to show what its final mutation produced. That record is a closing boundary rather than a selected mutation, so it never joins versions. : (untyped) -> RootVersionPlan

Parameters:

  • (Object)

Returns:



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

def contiguous_plan(closing)
  return RootVersionPlan.contiguous(@selected) unless closing
  unless Endpoint.record?(closing)
    return RootVersionPlan.contiguous(@selected + [closing], context_version: closing)
  end

  steps = @selected.each_cons(2).map { |from, to| [from, to] } #: Array[[untyped, untyped]]
  steps << [@selected.last, closing]
  RootVersionPlan.new(versions: @selected, steps: steps, closing_record: closing)
end

#filtered_plan(revealing) ⇒ RootVersionPlan

Each selected mutation is bounded by the version that reveals it, not by the next mutation that happened to be selected. Bounding by the next selection would fold anything filtered out in between into it, so the same edit would read differently depending on what followed it. : (untyped) -> RootVersionPlan

Parameters:

  • (Object)

Returns:



88
89
90
91
92
93
94
# File 'lib/paper_trail_diff/root_version_selection.rb', line 88

def filtered_plan(revealing)
  steps = @selected.filter_map do |version|
    successor = version.equal?(@selected.last) ? revealing : immediate_successor(version)
    [version, successor] if successor
  end #: Array[[untyped, untyped]]
  filtered_plan_for(steps, revealing)
end

#filtered_plan_for(steps, revealing) ⇒ RootVersionPlan

: (Array[[untyped, untyped]], untyped) -> RootVersionPlan

Parameters:

  • (Array[[ untyped, untyped ]])
  • (Object)

Returns:



97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/paper_trail_diff/root_version_selection.rb', line 97

def filtered_plan_for(steps, revealing)
  live = revealing if Endpoint.record?(revealing)
  versions = chronological(
    (steps.flatten(1) + closing_versions).reject { |entry| Endpoint.record?(entry) }
  )
  RootVersionPlan.new(
    versions: versions, steps: steps,
    context_version: (revealing unless live),
    reconstruction_versions: spanned(versions),
    mutations: @selected, closing_record: live
  )
end

#immediate_successor(version) ⇒ Object

: (untyped) -> untyped

Parameters:

  • (Object)

Returns:

  • (Object)


141
142
143
# File 'lib/paper_trail_diff/root_version_selection.rb', line 141

def immediate_successor(version)
  @in_range.find { |candidate| Support.compare_versions(version, candidate).negative? }
end

#revealing_versionObject

A version left out by a filter is still the state the last selected change produced, so it is preferred over anything after the range. : () -> untyped

Returns:

  • (Object)


148
149
150
# File 'lib/paper_trail_diff/root_version_selection.rb', line 148

def revealing_version
  immediate_successor(@selected.last) || @after_range
end

#spanned(versions) ⇒ Array[untyped]

Everything the span passes through, filtered out or not. A replay that skipped the excluded versions would carry a stale state into the next reported step, so the two selected mutations either side of a gap would disagree about what the record looked like between them. : (Array) -> Array

Parameters:

  • (Array[untyped])

Returns:

  • (Array[untyped])


124
125
126
127
128
129
130
131
132
133
# File 'lib/paper_trail_diff/root_version_selection.rb', line 124

def spanned(versions)
  first = versions.first
  last = versions.last
  return versions unless first && last

  @in_range.select do |candidate|
    !Support.compare_versions(first, candidate).positive? &&
      !Support.compare_versions(candidate, last).positive?
  end
end

#terminal_destroy?Boolean

A range closing on the record's own destruction needs no later version: nothing can follow it, so demanding one would reject the range forever. : () -> bool

Returns:

  • (Boolean)


155
156
157
# File 'lib/paper_trail_diff/root_version_selection.rb', line 155

def terminal_destroy?
  @selected.last&.event.to_s == 'destroy'
end

#without_selectionRootVersionPlan

An activity view still needs a root to reconstruct from even when no root version falls inside the window, because descendants may have moved. : () -> RootVersionPlan

Returns:



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

def without_selection
  return RootVersionPlan.empty unless @context_required
  if @after_range
    return RootVersionPlan.contiguous([@after_range], context_version: @after_range)
  end

  raise IncompleteTimeRangeError, INCOMPLETE
end