paper_trail_diff 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +49 -0
- data/README.md +92 -11
- data/lib/paper_trail_diff/activity_root_steps.rb +17 -7
- data/lib/paper_trail_diff/analysis_batch.rb +11 -7
- data/lib/paper_trail_diff/batched_root_versions.rb +21 -6
- data/lib/paper_trail_diff/live_endpoint_provider.rb +21 -2
- data/lib/paper_trail_diff/paper_trail_adapter.rb +63 -23
- data/lib/paper_trail_diff/root_version_plan.rb +16 -3
- data/lib/paper_trail_diff/root_version_selection.rb +43 -14
- data/lib/paper_trail_diff/step.rb +14 -5
- data/lib/paper_trail_diff/time_activity_timeline_builder.rb +86 -26
- data/lib/paper_trail_diff/time_version_range.rb +6 -3
- data/lib/paper_trail_diff/timeline_builder.rb +17 -8
- data/lib/paper_trail_diff/timeline_range.rb +6 -3
- data/lib/paper_trail_diff/timeline_snapshot_provider.rb +20 -4
- data/lib/paper_trail_diff/version.rb +1 -1
- data/lib/paper_trail_diff.rb +20 -12
- data/sig/generated/paper_trail_diff/activity_root_steps.rbs +7 -2
- data/sig/generated/paper_trail_diff/analysis_batch.rbs +6 -4
- data/sig/generated/paper_trail_diff/batched_root_versions.rbs +11 -4
- data/sig/generated/paper_trail_diff/live_endpoint_provider.rbs +9 -0
- data/sig/generated/paper_trail_diff/paper_trail_adapter.rbs +26 -10
- data/sig/generated/paper_trail_diff/root_version_plan.rbs +12 -2
- data/sig/generated/paper_trail_diff/root_version_selection.rbs +16 -2
- data/sig/generated/paper_trail_diff/step.rbs +5 -2
- data/sig/generated/paper_trail_diff/time_activity_timeline_builder.rbs +33 -12
- data/sig/generated/paper_trail_diff/time_version_range.rbs +4 -2
- data/sig/generated/paper_trail_diff/timeline_builder.rbs +5 -2
- data/sig/generated/paper_trail_diff/timeline_range.rbs +4 -2
- data/sig/generated/paper_trail_diff/timeline_snapshot_provider.rbs +11 -2
- data/sig/generated/paper_trail_diff.rbs +8 -8
- metadata +4 -4
|
@@ -11,11 +11,17 @@ module PaperTrailDiff
|
|
|
11
11
|
# Both the single-record and the batched selectors resolve this here, because
|
|
12
12
|
# the rule is subtle enough that two copies of it would drift apart.
|
|
13
13
|
class RootVersionSelection
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
# An error is a specification, so it names the way out as well as the wall.
|
|
15
|
+
# A window ending at the present can never gain a later version, which makes
|
|
16
|
+
# "write a checkpoint afterwards" impossible advice on its own.
|
|
17
|
+
INCOMPLETE = 'time range requires a later root version to reconstruct its ' \
|
|
18
|
+
'final change: pass close_on: :current to end at current state, ' \
|
|
19
|
+
'or narrow the window to end before the last recorded version'
|
|
20
|
+
|
|
21
|
+
#: (in_range: Array[untyped], selected: Array[untyped], after_range: untyped, windowed: bool, ?context_required: bool, ?filtered: bool, ?live_endpoint: untyped) -> void
|
|
17
22
|
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
18
|
-
in_range:, selected:, after_range:, windowed:, context_required: false, filtered: false
|
|
23
|
+
in_range:, selected:, after_range:, windowed:, context_required: false, filtered: false,
|
|
24
|
+
live_endpoint: nil
|
|
19
25
|
)
|
|
20
26
|
@in_range = in_range
|
|
21
27
|
@selected = selected
|
|
@@ -23,18 +29,18 @@ module PaperTrailDiff
|
|
|
23
29
|
@windowed = windowed
|
|
24
30
|
@context_required = context_required
|
|
25
31
|
@filtered = filtered
|
|
32
|
+
@live_endpoint = live_endpoint
|
|
26
33
|
end
|
|
27
34
|
|
|
28
35
|
#: () -> RootVersionPlan
|
|
29
36
|
def call
|
|
30
37
|
return without_selection if @selected.empty?
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
raise IncompleteTimeRangeError, INCOMPLETE if !
|
|
34
|
-
return filtered_plan(
|
|
39
|
+
closing = revealing_version || @live_endpoint
|
|
40
|
+
raise IncompleteTimeRangeError, INCOMPLETE if !closing && @windowed && !terminal_destroy?
|
|
41
|
+
return filtered_plan(closing) if @filtered
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
RootVersionPlan.contiguous(versions, context_version: revealing)
|
|
43
|
+
contiguous_plan(closing)
|
|
38
44
|
end
|
|
39
45
|
|
|
40
46
|
private
|
|
@@ -45,6 +51,22 @@ module PaperTrailDiff
|
|
|
45
51
|
# @rbs @windowed: bool
|
|
46
52
|
# @rbs @context_required: bool
|
|
47
53
|
# @rbs @filtered: bool
|
|
54
|
+
# @rbs @live_endpoint: untyped
|
|
55
|
+
|
|
56
|
+
# A window reaching past the last recorded version has only the live record
|
|
57
|
+
# left to show what its final mutation produced. That record is a closing
|
|
58
|
+
# boundary rather than a selected mutation, so it never joins `versions`.
|
|
59
|
+
#: (untyped) -> RootVersionPlan
|
|
60
|
+
def contiguous_plan(closing)
|
|
61
|
+
return RootVersionPlan.contiguous(@selected) unless closing
|
|
62
|
+
unless Endpoint.record?(closing)
|
|
63
|
+
return RootVersionPlan.contiguous(@selected + [closing], context_version: closing)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
steps = @selected.each_cons(2).map { |from, to| [from, to] } #: Array[[untyped, untyped]]
|
|
67
|
+
steps << [@selected.last, closing]
|
|
68
|
+
RootVersionPlan.new(versions: @selected, steps: steps, closing_record: closing)
|
|
69
|
+
end
|
|
48
70
|
|
|
49
71
|
# An activity view still needs a root to reconstruct from even when no root
|
|
50
72
|
# version falls inside the window, because descendants may have moved.
|
|
@@ -68,13 +90,20 @@ module PaperTrailDiff
|
|
|
68
90
|
successor = version.equal?(@selected.last) ? revealing : immediate_successor(version)
|
|
69
91
|
[version, successor] if successor
|
|
70
92
|
end #: Array[[untyped, untyped]]
|
|
71
|
-
|
|
93
|
+
filtered_plan_for(steps, revealing)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
#: (Array[[untyped, untyped]], untyped) -> RootVersionPlan
|
|
97
|
+
def filtered_plan_for(steps, revealing)
|
|
98
|
+
live = revealing if Endpoint.record?(revealing)
|
|
99
|
+
versions = chronological(
|
|
100
|
+
(steps.flatten(1) + closing_versions).reject { |entry| Endpoint.record?(entry) }
|
|
101
|
+
)
|
|
72
102
|
RootVersionPlan.new(
|
|
73
|
-
versions: versions,
|
|
74
|
-
|
|
75
|
-
context_version: revealing,
|
|
103
|
+
versions: versions, steps: steps,
|
|
104
|
+
context_version: (revealing unless live),
|
|
76
105
|
reconstruction_versions: spanned(versions),
|
|
77
|
-
mutations: @selected
|
|
106
|
+
mutations: @selected, closing_record: live
|
|
78
107
|
)
|
|
79
108
|
end
|
|
80
109
|
|
|
@@ -10,12 +10,20 @@ module PaperTrailDiff
|
|
|
10
10
|
attr_reader :to_boundary #: ActivityBoundary
|
|
11
11
|
attr_reader :diff #: Diff
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
# `to_version` is nil for the one step that closes on current state, which a
|
|
14
|
+
# window reaching past the last recorded version has to do. Read
|
|
15
|
+
# `to_boundary` instead when a caller may have opted into that.
|
|
16
|
+
#: (from_version: untyped, to_version: untyped, diff: Diff, ?captured_at: untyped) -> void
|
|
17
|
+
def initialize(from_version:, to_version:, diff:, captured_at: nil)
|
|
18
|
+
live = Endpoint.record?(to_version)
|
|
15
19
|
@from_version = from_version
|
|
16
|
-
@to_version = to_version
|
|
20
|
+
@to_version = (to_version unless live)
|
|
17
21
|
@from_boundary = ActivityBoundary.from_version(from_version)
|
|
18
|
-
@to_boundary =
|
|
22
|
+
@to_boundary = if live
|
|
23
|
+
ActivityBoundary.current(to_version, captured_at: captured_at)
|
|
24
|
+
else
|
|
25
|
+
ActivityBoundary.from_version(to_version)
|
|
26
|
+
end
|
|
19
27
|
@diff = diff
|
|
20
28
|
freeze
|
|
21
29
|
end
|
|
@@ -29,7 +37,8 @@ module PaperTrailDiff
|
|
|
29
37
|
def to_h
|
|
30
38
|
{
|
|
31
39
|
from_version_id: from_version.id,
|
|
32
|
-
to_version_id: to_version
|
|
40
|
+
to_version_id: to_version&.id,
|
|
41
|
+
to_boundary: to_boundary.to_h,
|
|
33
42
|
diff: diff.to_h
|
|
34
43
|
}
|
|
35
44
|
end
|
|
@@ -14,19 +14,23 @@ module PaperTrailDiff
|
|
|
14
14
|
|
|
15
15
|
#: () -> Array[ActivityStep]
|
|
16
16
|
def build
|
|
17
|
-
history, _plan, closing = history_and_versions
|
|
17
|
+
history, _plan, closing, = history_and_versions
|
|
18
18
|
activity_steps(history, closing)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
#: () -> Analysis
|
|
22
22
|
def analyze
|
|
23
|
-
history, plan, closing = history_and_versions
|
|
23
|
+
history, plan, closing, closing_snapshot, captured_at = history_and_versions
|
|
24
|
+
final = closing_snapshot || history.last_snapshot
|
|
24
25
|
Analysis.new(
|
|
25
|
-
diff: Engine.compare(history.first_snapshot,
|
|
26
|
-
timeline: ActivityRootSteps.call(
|
|
26
|
+
diff: Engine.compare(history.first_snapshot, final),
|
|
27
|
+
timeline: ActivityRootSteps.call(
|
|
28
|
+
plan, history.root_snapshots,
|
|
29
|
+
closing_snapshot: closing_snapshot, captured_at: captured_at
|
|
30
|
+
),
|
|
27
31
|
activity_timeline: activity_steps(history, closing),
|
|
28
32
|
from_snapshot: history.first_snapshot,
|
|
29
|
-
to_snapshot:
|
|
33
|
+
to_snapshot: final
|
|
30
34
|
)
|
|
31
35
|
end
|
|
32
36
|
|
|
@@ -37,19 +41,46 @@ module PaperTrailDiff
|
|
|
37
41
|
# @rbs @tree: AssociationTree
|
|
38
42
|
# @rbs @snapshotter: untyped
|
|
39
43
|
|
|
40
|
-
#: () -> [ActivityHistory, RootVersionPlan, ActivityStep
|
|
44
|
+
#: () -> [ActivityHistory, RootVersionPlan, ActivityStep?, RecordSnapshot?, untyped]
|
|
41
45
|
def history_and_versions
|
|
42
46
|
plan = @range.select_plan(context_required: !@tree.empty?)
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
return [ActivityHistory.empty, plan, nil, nil, nil] if plan.reconstruction_versions.empty?
|
|
48
|
+
|
|
49
|
+
# A window closing on current state runs to the instant it is captured,
|
|
50
|
+
# not to the last root version, or descendants that moved after that
|
|
51
|
+
# version would be missing from a report that claims to reach now.
|
|
52
|
+
history_for(plan, plan.closing_record ? Time.now.utc : nil)
|
|
53
|
+
end
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
#: (RootVersionPlan, untyped) -> [ActivityHistory, RootVersionPlan, ActivityStep?, RecordSnapshot?, untyped]
|
|
56
|
+
def history_for(plan, captured_at)
|
|
57
|
+
root_versions = plan.reconstruction_versions
|
|
58
|
+
events = events_through(root_versions, captured_at)
|
|
48
59
|
selected = selected_events(events, plan)
|
|
49
|
-
|
|
60
|
+
unless time_events?(selected, events, plan)
|
|
61
|
+
return [ActivityHistory.empty, plan, nil, nil, nil]
|
|
62
|
+
end
|
|
50
63
|
|
|
51
64
|
history = build_history(root_versions, events)
|
|
52
|
-
|
|
65
|
+
snapshot = current_snapshot(plan)
|
|
66
|
+
closing = closing_step(history, selected.last, plan, captured_at, snapshot)
|
|
67
|
+
[history, plan, closing, snapshot, captured_at]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# One live read serves both the closing activity step and the endpoint the
|
|
71
|
+
# analysis reports, which would otherwise reconstruct the graph twice.
|
|
72
|
+
#: (RootVersionPlan) -> RecordSnapshot?
|
|
73
|
+
def current_snapshot(plan)
|
|
74
|
+
record = plan.closing_record
|
|
75
|
+
return unless record
|
|
76
|
+
|
|
77
|
+
@snapshotter.call(record, record)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
#: (Array[untyped], untyped) -> Array[ActivityEvent]
|
|
81
|
+
def events_through(root_versions, captured_at)
|
|
82
|
+
prepare_history(root_versions, end_at: captured_at)
|
|
83
|
+
collect_events(root_versions, range_end: captured_at)
|
|
53
84
|
end
|
|
54
85
|
|
|
55
86
|
#: (Array[untyped], Array[ActivityEvent]) -> ActivityHistory
|
|
@@ -83,12 +114,20 @@ module PaperTrailDiff
|
|
|
83
114
|
end
|
|
84
115
|
end
|
|
85
116
|
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
117
|
+
# A window either closes on the absence its final destruction leaves, or on
|
|
118
|
+
# current state when it reaches past everything recorded. Both are boundaries
|
|
119
|
+
# no version can supply.
|
|
120
|
+
#: (ActivityHistory, ActivityEvent?, RootVersionPlan, untyped, RecordSnapshot?) -> ActivityStep?
|
|
121
|
+
def closing_step(history, event, plan, captured_at, snapshot)
|
|
122
|
+
return unless event
|
|
123
|
+
return destroyed_step(history, event) if terminal_destroy?(event)
|
|
124
|
+
return unless plan.closing_record
|
|
125
|
+
|
|
126
|
+
current_step(history, plan.closing_record, captured_at, snapshot)
|
|
127
|
+
end
|
|
91
128
|
|
|
129
|
+
#: (ActivityHistory, ActivityEvent) -> ActivityStep
|
|
130
|
+
def destroyed_step(history, event)
|
|
92
131
|
version = event.version
|
|
93
132
|
ActivityStep.new(
|
|
94
133
|
from_boundary: ActivityBoundary.from_version(version),
|
|
@@ -97,6 +136,21 @@ module PaperTrailDiff
|
|
|
97
136
|
)
|
|
98
137
|
end
|
|
99
138
|
|
|
139
|
+
# The last boundary reached is where current state is compared from, which
|
|
140
|
+
# is the final event rather than the final root version once descendants
|
|
141
|
+
# have moved since it.
|
|
142
|
+
#: (ActivityHistory, untyped, untyped, RecordSnapshot?) -> ActivityStep?
|
|
143
|
+
def current_step(history, record, captured_at, snapshot)
|
|
144
|
+
previous = history.steps.last&.to_boundary
|
|
145
|
+
return unless previous
|
|
146
|
+
|
|
147
|
+
ActivityStep.new(
|
|
148
|
+
from_boundary: previous,
|
|
149
|
+
to_boundary: ActivityBoundary.current(record, captured_at: captured_at),
|
|
150
|
+
diff: Engine.compare(history.last_snapshot, snapshot)
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
100
154
|
#: (ActivityEvent?) -> bool
|
|
101
155
|
def terminal_destroy?(event)
|
|
102
156
|
return false unless event
|
|
@@ -104,32 +158,38 @@ module PaperTrailDiff
|
|
|
104
158
|
event.root? && event.version.event.to_s == 'destroy'
|
|
105
159
|
end
|
|
106
160
|
|
|
107
|
-
#: (Array[untyped]) -> void
|
|
108
|
-
def prepare_history(root_versions)
|
|
161
|
+
#: (Array[untyped], ?end_at: untyped) -> void
|
|
162
|
+
def prepare_history(root_versions, end_at: nil)
|
|
109
163
|
return unless @snapshotter.respond_to?(:prepare)
|
|
110
164
|
|
|
111
|
-
|
|
165
|
+
start_at = @range.begin_time
|
|
166
|
+
return @snapshotter.prepare(@record, root_versions, start_at: start_at) unless end_at
|
|
167
|
+
|
|
168
|
+
@snapshotter.prepare(@record, root_versions, start_at: start_at, end_at: end_at)
|
|
112
169
|
end
|
|
113
170
|
|
|
114
|
-
#: (Array[untyped]) -> Array[ActivityEvent]
|
|
115
|
-
def collect_events(root_versions)
|
|
171
|
+
#: (Array[untyped], ?range_end: untyped) -> Array[ActivityEvent]
|
|
172
|
+
def collect_events(root_versions, range_end: nil)
|
|
116
173
|
ActivityVersionCollector.new(
|
|
117
174
|
@record,
|
|
118
175
|
root_versions: root_versions,
|
|
119
176
|
tree: @tree,
|
|
120
177
|
traversal: AssociationTraversal.new(@tree),
|
|
121
178
|
range_start: @range.begin_time,
|
|
122
|
-
range_end: root_versions.last
|
|
179
|
+
range_end: range_end || root_versions.last
|
|
123
180
|
).call
|
|
124
181
|
end
|
|
125
182
|
|
|
126
|
-
#: (Array[ActivityEvent], Array[ActivityEvent]) -> bool
|
|
127
|
-
def time_events?(selected, events)
|
|
183
|
+
#: (Array[ActivityEvent], Array[ActivityEvent], RootVersionPlan) -> bool
|
|
184
|
+
def time_events?(selected, events, plan)
|
|
128
185
|
return false if selected.empty?
|
|
129
186
|
return true if later_event?(selected.last, events)
|
|
130
187
|
return true if terminal_destroy?(selected.last)
|
|
188
|
+
return true if plan.closing_record
|
|
131
189
|
|
|
132
|
-
message = 'time range requires a later activity boundary to reconstruct its
|
|
190
|
+
message = 'time range requires a later activity boundary to reconstruct its ' \
|
|
191
|
+
'final change: pass close_on: :current to end at current state, ' \
|
|
192
|
+
'or narrow the window to end before the last recorded boundary'
|
|
133
193
|
raise IncompleteTimeRangeError, message
|
|
134
194
|
end
|
|
135
195
|
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
module PaperTrailDiff
|
|
5
5
|
# Selects in-range root versions plus one later reconstruction boundary.
|
|
6
6
|
class TimeVersionRange
|
|
7
|
-
#: (untyped, time_range: TimeRange, ?version_scope: untyped) -> void
|
|
8
|
-
def initialize(record, time_range:, version_scope: nil)
|
|
7
|
+
#: (untyped, time_range: TimeRange, ?version_scope: untyped, ?live_endpoint: untyped) -> void
|
|
8
|
+
def initialize(record, time_range:, version_scope: nil, live_endpoint: nil)
|
|
9
9
|
@record = record
|
|
10
10
|
@time_range = time_range
|
|
11
11
|
@version_scope = version_scope
|
|
12
|
+
@live_endpoint = live_endpoint
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
#: (?context_required: bool) -> Array[untyped]
|
|
@@ -27,7 +28,8 @@ module PaperTrailDiff
|
|
|
27
28
|
after_range: trailing_version(relation),
|
|
28
29
|
windowed: true,
|
|
29
30
|
context_required: context_required,
|
|
30
|
-
filtered: !@version_scope.nil
|
|
31
|
+
filtered: !@version_scope.nil?,
|
|
32
|
+
live_endpoint: @live_endpoint
|
|
31
33
|
).call
|
|
32
34
|
end
|
|
33
35
|
|
|
@@ -36,6 +38,7 @@ module PaperTrailDiff
|
|
|
36
38
|
# @rbs @record: untyped
|
|
37
39
|
# @rbs @time_range: TimeRange
|
|
38
40
|
# @rbs @version_scope: untyped
|
|
41
|
+
# @rbs @live_endpoint: untyped
|
|
39
42
|
|
|
40
43
|
#: () -> untyped
|
|
41
44
|
def versions_relation
|
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
module PaperTrailDiff
|
|
5
5
|
# Selects and compares a chronological slice of a record's version history.
|
|
6
6
|
class TimelineBuilder
|
|
7
|
-
#: (untyped, from: untyped, to: untyped, snapshotter: untyped, ?within: untyped, ?versions: Array[untyped]?, ?version_scope: untyped, ?plan: RootVersionPlan?) -> void
|
|
7
|
+
#: (untyped, from: untyped, to: untyped, snapshotter: untyped, ?within: untyped, ?versions: Array[untyped]?, ?version_scope: untyped, ?plan: RootVersionPlan?, ?live_endpoint: untyped) -> void
|
|
8
8
|
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
9
|
-
record, from:, to:, snapshotter:, within: nil, versions: nil, version_scope: nil, plan: nil
|
|
9
|
+
record, from:, to:, snapshotter:, within: nil, versions: nil, version_scope: nil, plan: nil,
|
|
10
|
+
live_endpoint: nil
|
|
10
11
|
)
|
|
11
12
|
@record = record
|
|
12
13
|
@range = TimelineRange.new(
|
|
13
14
|
record, from: from, to: to, within: within,
|
|
14
|
-
versions: versions, version_scope: version_scope, plan: plan
|
|
15
|
+
versions: versions, version_scope: version_scope, plan: plan,
|
|
16
|
+
live_endpoint: live_endpoint
|
|
15
17
|
)
|
|
16
18
|
@snapshotter = snapshotter
|
|
17
19
|
end
|
|
@@ -45,14 +47,21 @@ module PaperTrailDiff
|
|
|
45
47
|
|
|
46
48
|
versions = plan.versions
|
|
47
49
|
@snapshotter.prepare(@record, versions) if @snapshotter.respond_to?(:prepare)
|
|
48
|
-
steps = plan
|
|
50
|
+
steps = build_steps(plan)
|
|
51
|
+
[steps, @snapshotter.call(versions.first), @snapshotter.call(plan.final_endpoint)]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
#: (RootVersionPlan) -> Array[Step]
|
|
55
|
+
def build_steps(plan)
|
|
56
|
+
captured_at = Time.now.utc if plan.closing_record
|
|
57
|
+
plan.steps.map do |from_endpoint, to_endpoint|
|
|
49
58
|
Step.new(
|
|
50
|
-
from_version:
|
|
51
|
-
to_version:
|
|
52
|
-
|
|
59
|
+
from_version: from_endpoint,
|
|
60
|
+
to_version: to_endpoint,
|
|
61
|
+
captured_at: captured_at,
|
|
62
|
+
diff: Engine.compare(@snapshotter.call(from_endpoint), @snapshotter.call(to_endpoint))
|
|
53
63
|
)
|
|
54
64
|
end.freeze
|
|
55
|
-
[steps, @snapshotter.call(versions.first), @snapshotter.call(versions.last)]
|
|
56
65
|
end
|
|
57
66
|
|
|
58
67
|
#: () -> [Array[Step], nil, nil]
|
|
@@ -10,10 +10,11 @@ module PaperTrailDiff
|
|
|
10
10
|
attr_reader :to #: untyped
|
|
11
11
|
attr_reader :time_range #: TimeRange?
|
|
12
12
|
|
|
13
|
-
#: (untyped, from: untyped, to: untyped, within: untyped, ?versions: Array[untyped]?, ?version_scope: untyped, ?plan: RootVersionPlan?) -> void
|
|
14
|
-
def initialize(record, from:, to:, within:, versions: nil, version_scope: nil, plan: nil) # rubocop:disable Metrics/ParameterLists
|
|
13
|
+
#: (untyped, from: untyped, to: untyped, within: untyped, ?versions: Array[untyped]?, ?version_scope: untyped, ?plan: RootVersionPlan?, ?live_endpoint: untyped) -> void
|
|
14
|
+
def initialize(record, from:, to:, within:, versions: nil, version_scope: nil, plan: nil, live_endpoint: nil) # rubocop:disable Metrics/ParameterLists, Layout/LineLength
|
|
15
15
|
@record = record
|
|
16
16
|
@plan = plan
|
|
17
|
+
@live_endpoint = live_endpoint
|
|
17
18
|
@versions = (plan ? plan.versions : versions)&.freeze
|
|
18
19
|
@version_scope = version_scope
|
|
19
20
|
@requested_from = from
|
|
@@ -44,7 +45,8 @@ module PaperTrailDiff
|
|
|
44
45
|
range = time_range
|
|
45
46
|
if range
|
|
46
47
|
return TimeVersionRange.new(
|
|
47
|
-
@record, time_range: range, version_scope: @version_scope
|
|
48
|
+
@record, time_range: range, version_scope: @version_scope,
|
|
49
|
+
live_endpoint: @live_endpoint
|
|
48
50
|
).select_plan(context_required: context_required)
|
|
49
51
|
end
|
|
50
52
|
return RootVersionPlan.empty if unresolved?
|
|
@@ -92,6 +94,7 @@ module PaperTrailDiff
|
|
|
92
94
|
# @rbs @versions: Array[untyped]?
|
|
93
95
|
# @rbs @plan: RootVersionPlan?
|
|
94
96
|
# @rbs @version_scope: untyped
|
|
97
|
+
# @rbs @live_endpoint: untyped
|
|
95
98
|
# @rbs @requested_from: untyped
|
|
96
99
|
# @rbs @requested_to: untyped
|
|
97
100
|
# @rbs @from: untyped
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
module PaperTrailDiff
|
|
5
5
|
# Gives TimelineBuilder one-argument access to a range-prepared snapshot store.
|
|
6
6
|
class TimelineSnapshotProvider
|
|
7
|
-
#: (HistoricalSnapshotStore) -> void
|
|
8
|
-
def initialize(store)
|
|
7
|
+
#: (HistoricalSnapshotStore, ?live_snapshotter: untyped) -> void
|
|
8
|
+
def initialize(store, live_snapshotter: nil)
|
|
9
9
|
@store = store
|
|
10
|
+
@live_snapshotter = live_snapshotter
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
#: (untyped, Array[untyped]) -> void
|
|
@@ -14,11 +15,26 @@ module PaperTrailDiff
|
|
|
14
15
|
@store.prepare(record, versions)
|
|
15
16
|
end
|
|
16
17
|
|
|
18
|
+
# A window closing on current state ends at the live record rather than at a
|
|
19
|
+
# version, which is the one endpoint no version history can reconstruct.
|
|
17
20
|
#: (untyped) -> RecordSnapshot?
|
|
18
|
-
def call(
|
|
19
|
-
|
|
21
|
+
def call(endpoint)
|
|
22
|
+
return live_snapshot(endpoint) if Endpoint.record?(endpoint)
|
|
23
|
+
|
|
24
|
+
@store.uncached(endpoint, endpoint)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
#: (untyped) -> RecordSnapshot?
|
|
30
|
+
def live_snapshot(record)
|
|
31
|
+
snapshotter = @live_snapshotter
|
|
32
|
+
raise InvalidTimelineRangeError, 'live endpoints are unavailable here' unless snapshotter
|
|
33
|
+
|
|
34
|
+
snapshotter.call(record)
|
|
20
35
|
end
|
|
21
36
|
|
|
22
37
|
# @rbs @store: HistoricalSnapshotStore
|
|
38
|
+
# @rbs @live_snapshotter: untyped
|
|
23
39
|
end
|
|
24
40
|
end
|
data/lib/paper_trail_diff.rb
CHANGED
|
@@ -145,7 +145,7 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
|
|
|
145
145
|
end
|
|
146
146
|
|
|
147
147
|
# Compares every adjacent reconstructed state in an inclusive version range.
|
|
148
|
-
#: (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?version_scope: untyped) -> Array[Step]
|
|
148
|
+
#: (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?version_scope: untyped, ?close_on: Symbol?) -> Array[Step]
|
|
149
149
|
def timeline( # rubocop:disable Metrics/ParameterLists
|
|
150
150
|
record,
|
|
151
151
|
from: nil,
|
|
@@ -153,21 +153,23 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
|
|
|
153
153
|
within: nil,
|
|
154
154
|
associations: [],
|
|
155
155
|
ignore: DEFAULT_IGNORED_ATTRIBUTES,
|
|
156
|
-
version_scope: nil
|
|
156
|
+
version_scope: nil,
|
|
157
|
+
close_on: nil
|
|
157
158
|
)
|
|
158
159
|
PaperTrailAdapter.new(associations: associations, ignore: ignore).timeline(
|
|
159
160
|
record,
|
|
160
161
|
from: from,
|
|
161
162
|
to: to,
|
|
162
163
|
within: within,
|
|
163
|
-
version_scope: version_scope
|
|
164
|
+
version_scope: version_scope,
|
|
165
|
+
close_on: close_on
|
|
164
166
|
)
|
|
165
167
|
end
|
|
166
168
|
|
|
167
169
|
# Compares adjacent root and selected-descendant activity boundaries.
|
|
168
170
|
# `reload_live_endpoints:` applies only when `to:` is a current record; the
|
|
169
171
|
# other range forms never read live state.
|
|
170
|
-
#: (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?reload_live_endpoints: bool, ?version_scope: untyped) -> Array[ActivityStep]
|
|
172
|
+
#: (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?reload_live_endpoints: bool, ?version_scope: untyped, ?close_on: Symbol?) -> Array[ActivityStep]
|
|
171
173
|
def activity_timeline( # rubocop:disable Metrics/ParameterLists
|
|
172
174
|
record,
|
|
173
175
|
from: nil,
|
|
@@ -176,7 +178,8 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
|
|
|
176
178
|
associations: [],
|
|
177
179
|
ignore: DEFAULT_IGNORED_ATTRIBUTES,
|
|
178
180
|
reload_live_endpoints: true,
|
|
179
|
-
version_scope: nil
|
|
181
|
+
version_scope: nil,
|
|
182
|
+
close_on: nil
|
|
180
183
|
)
|
|
181
184
|
PaperTrailAdapter.new(
|
|
182
185
|
associations: associations,
|
|
@@ -187,12 +190,13 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
|
|
|
187
190
|
from: from,
|
|
188
191
|
to: to,
|
|
189
192
|
within: within,
|
|
190
|
-
version_scope: version_scope
|
|
193
|
+
version_scope: version_scope,
|
|
194
|
+
close_on: close_on
|
|
191
195
|
)
|
|
192
196
|
end
|
|
193
197
|
|
|
194
198
|
# Builds an endpoint diff and root-checkpoint timeline while normalizing each version once.
|
|
195
|
-
#: (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?activity: bool, ?version_scope: untyped) -> Analysis
|
|
199
|
+
#: (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?) -> Analysis
|
|
196
200
|
def analyze( # rubocop:disable Metrics/ParameterLists
|
|
197
201
|
record,
|
|
198
202
|
from: nil,
|
|
@@ -201,7 +205,8 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
|
|
|
201
205
|
associations: [],
|
|
202
206
|
ignore: DEFAULT_IGNORED_ATTRIBUTES,
|
|
203
207
|
activity: false,
|
|
204
|
-
version_scope: nil
|
|
208
|
+
version_scope: nil,
|
|
209
|
+
close_on: nil
|
|
205
210
|
)
|
|
206
211
|
PaperTrailAdapter.new(associations: associations, ignore: ignore).analyze(
|
|
207
212
|
record,
|
|
@@ -209,27 +214,30 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
|
|
|
209
214
|
to: to,
|
|
210
215
|
within: within,
|
|
211
216
|
activity: activity,
|
|
212
|
-
version_scope: version_scope
|
|
217
|
+
version_scope: version_scope,
|
|
218
|
+
close_on: close_on
|
|
213
219
|
)
|
|
214
220
|
end
|
|
215
221
|
|
|
216
222
|
# Analyzes many roots over one shared time window, preparing their selected
|
|
217
223
|
# history once for the batch instead of once per record. Roots with no
|
|
218
224
|
# versions in the window return an empty `Analysis`.
|
|
219
|
-
#: (Array[untyped], ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?activity: bool, ?version_scope: untyped) -> Hash[identity, Analysis]
|
|
225
|
+
#: (Array[untyped], ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?) -> Hash[identity, Analysis]
|
|
220
226
|
def analyze_many( # rubocop:disable Metrics/ParameterLists
|
|
221
227
|
records,
|
|
222
228
|
within: nil,
|
|
223
229
|
associations: [],
|
|
224
230
|
ignore: DEFAULT_IGNORED_ATTRIBUTES,
|
|
225
231
|
activity: false,
|
|
226
|
-
version_scope: nil
|
|
232
|
+
version_scope: nil,
|
|
233
|
+
close_on: nil
|
|
227
234
|
)
|
|
228
235
|
PaperTrailAdapter.new(associations: associations, ignore: ignore).analyze_many(
|
|
229
236
|
records,
|
|
230
237
|
within: within,
|
|
231
238
|
activity: activity,
|
|
232
|
-
version_scope: version_scope
|
|
239
|
+
version_scope: version_scope,
|
|
240
|
+
close_on: close_on
|
|
233
241
|
)
|
|
234
242
|
end
|
|
235
243
|
|
|
@@ -4,8 +4,13 @@ module PaperTrailDiff
|
|
|
4
4
|
# Root checkpoint steps recovered from the snapshots an activity pass retained,
|
|
5
5
|
# so a combined result does not reconstruct the same boundaries twice.
|
|
6
6
|
module ActivityRootSteps
|
|
7
|
-
#
|
|
8
|
-
|
|
7
|
+
# A window closing on current state ends at the live record, which no
|
|
8
|
+
# version-keyed snapshot can supply, so that endpoint is passed in.
|
|
9
|
+
# : (RootVersionPlan, Hash[Array[untyped], RecordSnapshot?], ?closing_snapshot: RecordSnapshot?, ?captured_at: untyped) -> Array[Step]
|
|
10
|
+
def self?.call: (RootVersionPlan, Hash[Array[untyped], RecordSnapshot?], ?closing_snapshot: RecordSnapshot?, ?captured_at: untyped) -> Array[Step]
|
|
11
|
+
|
|
12
|
+
# : (untyped, Hash[Array[untyped], RecordSnapshot?], RecordSnapshot?) -> RecordSnapshot?
|
|
13
|
+
def self?.snapshot_for: (untyped, Hash[Array[untyped], RecordSnapshot?], RecordSnapshot?) -> RecordSnapshot?
|
|
9
14
|
|
|
10
15
|
# : (untyped) -> Array[untyped]
|
|
11
16
|
def self?.version_key: (untyped) -> Array[untyped]
|
|
@@ -5,8 +5,8 @@ module PaperTrailDiff
|
|
|
5
5
|
# versions and preparing their association history once for the whole batch
|
|
6
6
|
# rather than once per root.
|
|
7
7
|
class AnalysisBatch
|
|
8
|
-
# : (Array[untyped], time_range: TimeRange?, live_loader: untyped, history_preparer: untyped, analyzer: untyped, ?version_scope: untyped) -> void
|
|
9
|
-
def initialize: (Array[untyped], time_range: TimeRange?, live_loader: untyped, history_preparer: untyped, analyzer: untyped, ?version_scope: untyped) -> void
|
|
8
|
+
# : (Array[untyped], time_range: TimeRange?, live_loader: untyped, history_preparer: untyped, analyzer: untyped, ?version_scope: untyped, ?close_on_current: bool) -> void
|
|
9
|
+
def initialize: (Array[untyped], time_range: TimeRange?, live_loader: untyped, history_preparer: untyped, analyzer: untyped, ?version_scope: untyped, ?close_on_current: bool) -> void
|
|
10
10
|
|
|
11
11
|
# : () -> Hash[identity, Analysis]
|
|
12
12
|
def call: () -> Hash[identity, Analysis]
|
|
@@ -15,6 +15,8 @@ module PaperTrailDiff
|
|
|
15
15
|
|
|
16
16
|
@analyzer: untyped
|
|
17
17
|
|
|
18
|
+
@close_on_current: bool
|
|
19
|
+
|
|
18
20
|
@history_preparer: untyped
|
|
19
21
|
|
|
20
22
|
@live_loader: untyped
|
|
@@ -38,8 +40,8 @@ module PaperTrailDiff
|
|
|
38
40
|
# The roots are preloaded first, because preparation reads their current
|
|
39
41
|
# association state as a fallback and would otherwise walk it one root at a
|
|
40
42
|
# time.
|
|
41
|
-
# : (Array[untyped], Hash[Array[String], RootVersionPlan]) -> void
|
|
42
|
-
def prepare: (Array[untyped], Hash[Array[String], RootVersionPlan]) -> void
|
|
43
|
+
# : (Array[untyped], Hash[Array[String], RootVersionPlan], Hash[Array[String], untyped]) -> void
|
|
44
|
+
def prepare: (Array[untyped], Hash[Array[String], RootVersionPlan], Hash[Array[String], untyped]) -> void
|
|
43
45
|
|
|
44
46
|
# A root with no versions in range has nothing to report, which is an empty
|
|
45
47
|
# result rather than a failed request.
|
|
@@ -5,8 +5,8 @@ module PaperTrailDiff
|
|
|
5
5
|
# queries. Only the range forms that mean the same thing for every root are
|
|
6
6
|
# supported: a shared wall-clock window, or each root's own whole history.
|
|
7
7
|
class BatchedRootVersions
|
|
8
|
-
# : (Array[untyped], time_range: TimeRange?, ?version_scope: untyped) -> void
|
|
9
|
-
def initialize: (Array[untyped], time_range: TimeRange?, ?version_scope: untyped) -> void
|
|
8
|
+
# : (Array[untyped], time_range: TimeRange?, ?version_scope: untyped, ?live_endpoints: Hash[Array[String], untyped]?) -> void
|
|
9
|
+
def initialize: (Array[untyped], time_range: TimeRange?, ?version_scope: untyped, ?live_endpoints: Hash[Array[String], untyped]?) -> void
|
|
10
10
|
|
|
11
11
|
# Returns a plan per record identity.
|
|
12
12
|
# : () -> Hash[Array[String], RootVersionPlan]
|
|
@@ -14,6 +14,8 @@ module PaperTrailDiff
|
|
|
14
14
|
|
|
15
15
|
private
|
|
16
16
|
|
|
17
|
+
@live_endpoints: Hash[Array[String], untyped]?
|
|
18
|
+
|
|
17
19
|
@records: Array[untyped]
|
|
18
20
|
|
|
19
21
|
@time_range: TimeRange?
|
|
@@ -26,8 +28,13 @@ module PaperTrailDiff
|
|
|
26
28
|
# : (untyped, Array[untyped]) -> [Hash[String, Array[untyped]], Set[untyped]?, Hash[String, untyped]]
|
|
27
29
|
def model_versions: (untyped, Array[untyped]) -> [ Hash[String, Array[untyped]], Set[untyped]?, Hash[String, untyped] ]
|
|
28
30
|
|
|
29
|
-
# : (Array[untyped], Set[untyped]?, untyped) -> RootVersionPlan
|
|
30
|
-
def versions_for: (Array[untyped], Set[untyped]?, untyped) -> RootVersionPlan
|
|
31
|
+
# : (Array[untyped], Set[untyped]?, untyped, untyped) -> RootVersionPlan
|
|
32
|
+
def versions_for: (Array[untyped], Set[untyped]?, untyped, untyped) -> RootVersionPlan
|
|
33
|
+
|
|
34
|
+
# The batch already loaded every root, so closing on current state reuses
|
|
35
|
+
# that rather than reading each record again.
|
|
36
|
+
# : (Array[String], untyped) -> untyped
|
|
37
|
+
def live_endpoint: (Array[String], untyped) -> untyped
|
|
31
38
|
|
|
32
39
|
# One extra query names the selected mutations without discarding the
|
|
33
40
|
# unfiltered versions the successor lookup still needs.
|