paper_trail_diff 0.6.0 → 0.7.0
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 +28 -0
- data/README.md +85 -9
- 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/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 +36 -12
- data/lib/paper_trail_diff/step.rb +14 -5
- data/lib/paper_trail_diff/time_activity_timeline_builder.rb +83 -25
- 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/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 +13 -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
|
@@ -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,30 +158,34 @@ 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
190
|
message = 'time range requires a later activity boundary to reconstruct its final change'
|
|
133
191
|
raise IncompleteTimeRangeError, message
|
|
@@ -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.
|
|
@@ -6,6 +6,9 @@ module PaperTrailDiff
|
|
|
6
6
|
# the only place allowed to know about both PaperTrail and the pure engine.
|
|
7
7
|
# Reconstruction logic lives in the collaborators it wires together.
|
|
8
8
|
class PaperTrailAdapter
|
|
9
|
+
# The only thing a wall-clock window can close on besides a later version.
|
|
10
|
+
CLOSE_ON_CURRENT: ::Symbol
|
|
11
|
+
|
|
9
12
|
# : (associations: Array[String | Symbol], ignore: ignore_option, ?reload_live_endpoints: bool) -> void
|
|
10
13
|
def initialize: (associations: Array[String | Symbol], ignore: ignore_option, ?reload_live_endpoints: bool) -> void
|
|
11
14
|
|
|
@@ -16,18 +19,18 @@ module PaperTrailDiff
|
|
|
16
19
|
# : (Array[comparison_input]) -> comparison_results
|
|
17
20
|
def compare_many: (Array[comparison_input]) -> comparison_results
|
|
18
21
|
|
|
19
|
-
# : (untyped, from: untyped, to: untyped, within: untyped, ?version_scope: untyped) -> Array[Step]
|
|
20
|
-
def timeline: (untyped, from: untyped, to: untyped, within: untyped, ?version_scope: untyped) -> Array[Step]
|
|
22
|
+
# : (untyped, from: untyped, to: untyped, within: untyped, ?version_scope: untyped, ?close_on: Symbol?) -> Array[Step]
|
|
23
|
+
def timeline: (untyped, from: untyped, to: untyped, within: untyped, ?version_scope: untyped, ?close_on: Symbol?) -> Array[Step]
|
|
21
24
|
|
|
22
|
-
# : (untyped, from: untyped, to: untyped, within: untyped, ?version_scope: untyped) -> Array[ActivityStep]
|
|
23
|
-
def activity_timeline: (untyped, from: untyped, to: untyped, within: untyped, ?version_scope: untyped) -> Array[ActivityStep]
|
|
25
|
+
# : (untyped, from: untyped, to: untyped, within: untyped, ?version_scope: untyped, ?close_on: Symbol?) -> Array[ActivityStep]
|
|
26
|
+
def activity_timeline: (untyped, from: untyped, to: untyped, within: untyped, ?version_scope: untyped, ?close_on: Symbol?) -> Array[ActivityStep]
|
|
24
27
|
|
|
25
|
-
# : (untyped, from: untyped, to: untyped, within: untyped, ?activity: bool, ?version_scope: untyped) -> Analysis
|
|
26
|
-
def analyze: (untyped, from: untyped, to: untyped, within: untyped, ?activity: bool, ?version_scope: untyped) -> Analysis
|
|
28
|
+
# : (untyped, from: untyped, to: untyped, within: untyped, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?) -> Analysis
|
|
29
|
+
def analyze: (untyped, from: untyped, to: untyped, within: untyped, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?) -> Analysis
|
|
27
30
|
|
|
28
31
|
# Analyzes many roots over one shared range, preparing their history once.
|
|
29
|
-
# : (Array[untyped], within: untyped, ?activity: bool, ?version_scope: untyped) -> Hash[identity, Analysis]
|
|
30
|
-
def analyze_many: (Array[untyped], within: untyped, ?activity: bool, ?version_scope: untyped) -> Hash[identity, Analysis]
|
|
32
|
+
# : (Array[untyped], within: untyped, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?) -> Hash[identity, Analysis]
|
|
33
|
+
def analyze_many: (Array[untyped], within: untyped, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?) -> Hash[identity, Analysis]
|
|
31
34
|
|
|
32
35
|
private
|
|
33
36
|
|
|
@@ -53,6 +56,19 @@ module PaperTrailDiff
|
|
|
53
56
|
|
|
54
57
|
@traversal_preparer: TraversalPreparer
|
|
55
58
|
|
|
59
|
+
# : (untyped, from: untyped, to: untyped, within: untyped, version_scope: untyped, live_endpoint: untyped) -> Analysis
|
|
60
|
+
def analyze_activity: (untyped, from: untyped, to: untyped, within: untyped, version_scope: untyped, live_endpoint: untyped) -> Analysis
|
|
61
|
+
|
|
62
|
+
# `close_on:` names what ends a wall-clock window, so it is meaningless for a
|
|
63
|
+
# range whose endpoints the caller already gave explicitly.
|
|
64
|
+
# : (Symbol?, untyped) -> bool
|
|
65
|
+
def close_on_current?: (Symbol?, untyped) -> bool
|
|
66
|
+
|
|
67
|
+
# A destroyed root has no current state to close on, and its own destroy
|
|
68
|
+
# version already terminates the history.
|
|
69
|
+
# : (untyped, Symbol?, untyped) -> untyped
|
|
70
|
+
def live_endpoint_for: (untyped, Symbol?, untyped) -> untyped
|
|
71
|
+
|
|
56
72
|
# : () -> void
|
|
57
73
|
def build_snapshotters: () -> void
|
|
58
74
|
|
|
@@ -65,8 +81,8 @@ module PaperTrailDiff
|
|
|
65
81
|
# : () -> ActivitySnapshotProvider
|
|
66
82
|
def build_activity_snapshotter: () -> ActivitySnapshotProvider
|
|
67
83
|
|
|
68
|
-
# : (untyped, from: untyped, to: untyped, within: untyped, ?version_scope: untyped) -> ActivityTimelineBuilder
|
|
69
|
-
def activity_builder: (untyped, from: untyped, to: untyped, within: untyped, ?version_scope: untyped) -> ActivityTimelineBuilder
|
|
84
|
+
# : (untyped, from: untyped, to: untyped, within: untyped, ?version_scope: untyped, ?live_endpoint: untyped) -> ActivityTimelineBuilder
|
|
85
|
+
def activity_builder: (untyped, from: untyped, to: untyped, within: untyped, ?version_scope: untyped, ?live_endpoint: untyped) -> ActivityTimelineBuilder
|
|
70
86
|
|
|
71
87
|
# : (untyped) -> void
|
|
72
88
|
def reject_live_habtm_activity!: (untyped) -> void
|
|
@@ -23,6 +23,11 @@ module PaperTrailDiff
|
|
|
23
23
|
|
|
24
24
|
attr_reader context_version: untyped
|
|
25
25
|
|
|
26
|
+
# The live record a window closes on when no later version can reveal its
|
|
27
|
+
# final mutation. It is a boundary, never a version, so it stays out of
|
|
28
|
+
# `versions` and out of anything that reconstructs from version history.
|
|
29
|
+
attr_reader closing_record: untyped
|
|
30
|
+
|
|
26
31
|
# The root versions this plan reports as mutations, which excludes any
|
|
27
32
|
# version present only to reveal what the last of them produced.
|
|
28
33
|
attr_reader mutations: Array[untyped]
|
|
@@ -34,12 +39,17 @@ module PaperTrailDiff
|
|
|
34
39
|
# : () -> RootVersionPlan
|
|
35
40
|
def self.empty: () -> RootVersionPlan
|
|
36
41
|
|
|
37
|
-
# : (versions: Array[untyped], steps: Array[[untyped, untyped]], ?context_version: untyped, ?reconstruction_versions: Array[untyped]?, ?mutations: Array[untyped]?) -> void
|
|
38
|
-
def initialize: (versions: Array[untyped], steps: Array[[ untyped, untyped ]], ?context_version: untyped, ?reconstruction_versions: Array[untyped]?, ?mutations: Array[untyped]?) -> void
|
|
42
|
+
# : (versions: Array[untyped], steps: Array[[untyped, untyped]], ?context_version: untyped, ?reconstruction_versions: Array[untyped]?, ?mutations: Array[untyped]?, ?closing_record: untyped) -> void
|
|
43
|
+
def initialize: (versions: Array[untyped], steps: Array[[ untyped, untyped ]], ?context_version: untyped, ?reconstruction_versions: Array[untyped]?, ?mutations: Array[untyped]?, ?closing_record: untyped) -> void
|
|
39
44
|
|
|
40
45
|
# : () -> bool
|
|
41
46
|
def empty?: () -> bool
|
|
42
47
|
|
|
48
|
+
# What the range's final state is read from, which is the live record when
|
|
49
|
+
# the window closes on current state and the last version otherwise.
|
|
50
|
+
# : () -> untyped
|
|
51
|
+
def final_endpoint: () -> untyped
|
|
52
|
+
|
|
43
53
|
# : (untyped) -> bool
|
|
44
54
|
def mutation?: (untyped) -> bool
|
|
45
55
|
|