Class: PaperTrailDiff::RootVersionSelection
- Inherits:
-
Object
- Object
- PaperTrailDiff::RootVersionSelection
- 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.
'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
-
#call ⇒ RootVersionPlan
: () -> RootVersionPlan.
- #chronological(versions) ⇒ Array[untyped]
-
#closing_versions ⇒ Array[untyped]
Nothing can follow a destroy, so it never pairs into a step.
-
#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.
-
#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.
-
#filtered_plan_for(steps, revealing) ⇒ RootVersionPlan
: (Array[[untyped, untyped]], untyped) -> RootVersionPlan.
-
#immediate_successor(version) ⇒ Object
: (untyped) -> untyped.
- #initialize(in_range:, selected:, after_range:, windowed:, context_required: false, filtered: false, live_endpoint: nil) ⇒ RootVersionSelection constructor
-
#revealing_version ⇒ Object
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.
-
#spanned(versions) ⇒ Array[untyped]
Everything the span passes through, filtered out or not.
-
#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.
-
#without_selection ⇒ RootVersionPlan
An activity view still needs a root to reconstruct from even when no root version falls inside the window, because descendants may have moved.
Constructor Details
#initialize(in_range:, selected:, after_range:, windowed:, context_required: false, filtered: false, live_endpoint: nil) ⇒ RootVersionSelection
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
#call ⇒ RootVersionPlan
: () -> RootVersionPlan
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]
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_versions ⇒ Array[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
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
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
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
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
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_version ⇒ Object
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
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]
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
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_selection ⇒ RootVersionPlan
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
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 |