paper_trail_diff 0.5.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 +52 -0
- data/README.md +146 -7
- data/lib/paper_trail_diff/activity_root_steps.rb +19 -7
- data/lib/paper_trail_diff/activity_timeline_builder.rb +15 -8
- data/lib/paper_trail_diff/analysis.rb +19 -5
- data/lib/paper_trail_diff/analysis_batch.rb +35 -13
- data/lib/paper_trail_diff/batched_root_analyzer.rb +9 -9
- data/lib/paper_trail_diff/batched_root_versions.rb +62 -29
- data/lib/paper_trail_diff/paper_trail_adapter.rb +72 -25
- data/lib/paper_trail_diff/root_version_plan.rb +99 -0
- data/lib/paper_trail_diff/root_version_selection.rb +155 -0
- data/lib/paper_trail_diff/step.rb +14 -5
- data/lib/paper_trail_diff/time_activity_timeline_builder.rb +99 -29
- data/lib/paper_trail_diff/time_version_range.rb +22 -24
- data/lib/paper_trail_diff/timeline_builder.rb +32 -20
- data/lib/paper_trail_diff/timeline_range.rb +34 -7
- 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/version_range.rb +45 -5
- data/lib/paper_trail_diff/version_scope_filter.rb +37 -0
- data/lib/paper_trail_diff.rb +32 -13
- data/sig/generated/paper_trail_diff/activity_root_steps.rbs +7 -2
- data/sig/generated/paper_trail_diff/activity_timeline_builder.rbs +6 -2
- data/sig/generated/paper_trail_diff/analysis.rbs +13 -4
- data/sig/generated/paper_trail_diff/analysis_batch.rbs +15 -6
- data/sig/generated/paper_trail_diff/batched_root_analyzer.rbs +4 -4
- data/sig/generated/paper_trail_diff/batched_root_versions.rbs +28 -14
- data/sig/generated/paper_trail_diff/paper_trail_adapter.rbs +26 -10
- data/sig/generated/paper_trail_diff/root_version_plan.rbs +66 -0
- data/sig/generated/paper_trail_diff/root_version_selection.rbs +88 -0
- data/sig/generated/paper_trail_diff/step.rbs +5 -2
- data/sig/generated/paper_trail_diff/time_activity_timeline_builder.rbs +41 -15
- data/sig/generated/paper_trail_diff/time_version_range.rbs +9 -9
- data/sig/generated/paper_trail_diff/timeline_builder.rbs +9 -6
- data/sig/generated/paper_trail_diff/timeline_range.rbs +13 -2
- data/sig/generated/paper_trail_diff/timeline_snapshot_provider.rbs +11 -2
- data/sig/generated/paper_trail_diff/version_range.rbs +17 -2
- data/sig/generated/paper_trail_diff/version_scope_filter.rbs +22 -0
- data/sig/generated/paper_trail_diff.rbs +8 -8
- metadata +10 -4
|
@@ -4,34 +4,41 @@
|
|
|
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) -> void
|
|
8
|
-
def initialize(record, time_range:)
|
|
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
|
+
@version_scope = version_scope
|
|
12
|
+
@live_endpoint = live_endpoint
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
#: (?context_required: bool) -> Array[untyped]
|
|
14
16
|
def select(context_required: false)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
empty = [] #: Array[untyped]
|
|
18
|
-
return empty.freeze if selected.empty? && !context_required
|
|
19
|
-
|
|
20
|
-
trailing = trailing_version(relation)
|
|
21
|
-
unless trailing
|
|
22
|
-
return selected.freeze if terminal_destroy?(selected)
|
|
23
|
-
|
|
24
|
-
message = 'time range requires a later root version to reconstruct its final change'
|
|
25
|
-
raise IncompleteTimeRangeError, message
|
|
26
|
-
end
|
|
17
|
+
select_plan(context_required: context_required).versions
|
|
18
|
+
end
|
|
27
19
|
|
|
28
|
-
|
|
20
|
+
#: (?context_required: bool) -> RootVersionPlan
|
|
21
|
+
def select_plan(context_required: false)
|
|
22
|
+
relation = versions_relation
|
|
23
|
+
in_range = ordered(@time_range.scope(relation).to_a)
|
|
24
|
+
RootVersionSelection.new(
|
|
25
|
+
in_range: in_range,
|
|
26
|
+
selected: VersionScopeFilter.new(@version_scope).call(@time_range.scope(relation),
|
|
27
|
+
in_range),
|
|
28
|
+
after_range: trailing_version(relation),
|
|
29
|
+
windowed: true,
|
|
30
|
+
context_required: context_required,
|
|
31
|
+
filtered: !@version_scope.nil?,
|
|
32
|
+
live_endpoint: @live_endpoint
|
|
33
|
+
).call
|
|
29
34
|
end
|
|
30
35
|
|
|
31
36
|
private
|
|
32
37
|
|
|
33
38
|
# @rbs @record: untyped
|
|
34
39
|
# @rbs @time_range: TimeRange
|
|
40
|
+
# @rbs @version_scope: untyped
|
|
41
|
+
# @rbs @live_endpoint: untyped
|
|
35
42
|
|
|
36
43
|
#: () -> untyped
|
|
37
44
|
def versions_relation
|
|
@@ -42,15 +49,6 @@ module PaperTrailDiff
|
|
|
42
49
|
raise InvalidTimelineRangeError, message, cause: e
|
|
43
50
|
end
|
|
44
51
|
|
|
45
|
-
# A window closing on the record's own destruction needs no later version:
|
|
46
|
-
# the destroy reveals the preceding mutation and nothing can follow it, so
|
|
47
|
-
# demanding a checkpoint that can never be written would reject the range
|
|
48
|
-
# permanently.
|
|
49
|
-
#: (Array[untyped]) -> bool
|
|
50
|
-
def terminal_destroy?(versions)
|
|
51
|
-
versions.last&.event.to_s == 'destroy'
|
|
52
|
-
end
|
|
53
|
-
|
|
54
52
|
#: (untyped) -> untyped
|
|
55
53
|
def trailing_version(relation)
|
|
56
54
|
@time_range.trailing_scope(relation).reorder(created_at: :asc, id: :asc).first
|
|
@@ -4,10 +4,17 @@
|
|
|
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]?) -> void
|
|
8
|
-
def initialize(
|
|
7
|
+
#: (untyped, from: untyped, to: untyped, snapshotter: untyped, ?within: untyped, ?versions: Array[untyped]?, ?version_scope: untyped, ?plan: RootVersionPlan?, ?live_endpoint: untyped) -> void
|
|
8
|
+
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
9
|
+
record, from:, to:, snapshotter:, within: nil, versions: nil, version_scope: nil, plan: nil,
|
|
10
|
+
live_endpoint: nil
|
|
11
|
+
)
|
|
9
12
|
@record = record
|
|
10
|
-
@range = TimelineRange.new(
|
|
13
|
+
@range = TimelineRange.new(
|
|
14
|
+
record, from: from, to: to, within: within,
|
|
15
|
+
versions: versions, version_scope: version_scope, plan: plan,
|
|
16
|
+
live_endpoint: live_endpoint
|
|
17
|
+
)
|
|
11
18
|
@snapshotter = snapshotter
|
|
12
19
|
end
|
|
13
20
|
|
|
@@ -22,7 +29,9 @@ module PaperTrailDiff
|
|
|
22
29
|
steps, first_snapshot, last_snapshot = compare_history(selected_versions)
|
|
23
30
|
Analysis.new(
|
|
24
31
|
diff: Engine.compare(first_snapshot, last_snapshot),
|
|
25
|
-
timeline: steps
|
|
32
|
+
timeline: steps,
|
|
33
|
+
from_snapshot: first_snapshot,
|
|
34
|
+
to_snapshot: last_snapshot
|
|
26
35
|
)
|
|
27
36
|
end
|
|
28
37
|
|
|
@@ -32,24 +41,27 @@ module PaperTrailDiff
|
|
|
32
41
|
# @rbs @range: TimelineRange
|
|
33
42
|
# @rbs @snapshotter: untyped
|
|
34
43
|
|
|
35
|
-
#: (
|
|
36
|
-
def compare_history(
|
|
37
|
-
return empty_history if
|
|
44
|
+
#: (RootVersionPlan) -> [Array[Step], RecordSnapshot?, RecordSnapshot?]
|
|
45
|
+
def compare_history(plan)
|
|
46
|
+
return empty_history if plan.empty?
|
|
38
47
|
|
|
48
|
+
versions = plan.versions
|
|
39
49
|
@snapshotter.prepare(@record, versions) if @snapshotter.respond_to?(:prepare)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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|
|
|
58
|
+
Step.new(
|
|
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))
|
|
48
63
|
)
|
|
49
|
-
previous_snapshot = current_snapshot
|
|
50
|
-
step
|
|
51
64
|
end.freeze
|
|
52
|
-
[steps, first_snapshot, previous_snapshot]
|
|
53
65
|
end
|
|
54
66
|
|
|
55
67
|
#: () -> [Array[Step], nil, nil]
|
|
@@ -58,9 +70,9 @@ module PaperTrailDiff
|
|
|
58
70
|
[steps.freeze, nil, nil]
|
|
59
71
|
end
|
|
60
72
|
|
|
61
|
-
#: () ->
|
|
73
|
+
#: () -> RootVersionPlan
|
|
62
74
|
def selected_versions
|
|
63
|
-
@range.
|
|
75
|
+
@range.select_plan
|
|
64
76
|
end
|
|
65
77
|
end
|
|
66
78
|
end
|
|
@@ -10,10 +10,13 @@ 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]?) -> void
|
|
14
|
-
def initialize(record, from:, to:, within:, versions: nil)
|
|
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
|
|
18
|
+
@versions = (plan ? plan.versions : versions)&.freeze
|
|
19
|
+
@version_scope = version_scope
|
|
17
20
|
@requested_from = from
|
|
18
21
|
@requested_to = to
|
|
19
22
|
@time_range = build_time_range(within)
|
|
@@ -32,6 +35,27 @@ module PaperTrailDiff
|
|
|
32
35
|
|
|
33
36
|
# A batch may have selected these versions already, in which case reselecting
|
|
34
37
|
# them per record would undo the batching.
|
|
38
|
+
# The plan says which pairs of versions become steps, which a filter can
|
|
39
|
+
# make different from adjacent pairs of the selected versions.
|
|
40
|
+
#: (?context_required: bool) -> RootVersionPlan
|
|
41
|
+
def select_plan(context_required: false)
|
|
42
|
+
preselected = @plan
|
|
43
|
+
return preselected if preselected
|
|
44
|
+
|
|
45
|
+
range = time_range
|
|
46
|
+
if range
|
|
47
|
+
return TimeVersionRange.new(
|
|
48
|
+
@record, time_range: range, version_scope: @version_scope,
|
|
49
|
+
live_endpoint: @live_endpoint
|
|
50
|
+
).select_plan(context_required: context_required)
|
|
51
|
+
end
|
|
52
|
+
return RootVersionPlan.empty if unresolved?
|
|
53
|
+
|
|
54
|
+
VersionRange.new(
|
|
55
|
+
@record, from: @from, to: @to, version_scope: @version_scope
|
|
56
|
+
).select_plan_for_range
|
|
57
|
+
end
|
|
58
|
+
|
|
35
59
|
#: (?context_required: bool) -> Array[untyped]
|
|
36
60
|
def select(context_required: false)
|
|
37
61
|
preselected = @versions
|
|
@@ -39,13 +63,13 @@ module PaperTrailDiff
|
|
|
39
63
|
|
|
40
64
|
range = time_range
|
|
41
65
|
if range
|
|
42
|
-
return TimeVersionRange.new(
|
|
43
|
-
|
|
44
|
-
)
|
|
66
|
+
return TimeVersionRange.new(
|
|
67
|
+
@record, time_range: range, version_scope: @version_scope
|
|
68
|
+
).select(context_required: context_required)
|
|
45
69
|
end
|
|
46
70
|
return empty_versions if unresolved?
|
|
47
71
|
|
|
48
|
-
VersionRange.new(@record, from: @from, to: @to).select
|
|
72
|
+
VersionRange.new(@record, from: @from, to: @to, version_scope: @version_scope).select
|
|
49
73
|
end
|
|
50
74
|
|
|
51
75
|
#: () -> bool
|
|
@@ -68,6 +92,9 @@ module PaperTrailDiff
|
|
|
68
92
|
|
|
69
93
|
# @rbs @record: untyped
|
|
70
94
|
# @rbs @versions: Array[untyped]?
|
|
95
|
+
# @rbs @plan: RootVersionPlan?
|
|
96
|
+
# @rbs @version_scope: untyped
|
|
97
|
+
# @rbs @live_endpoint: untyped
|
|
71
98
|
# @rbs @requested_from: untyped
|
|
72
99
|
# @rbs @requested_to: untyped
|
|
73
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
|
|
@@ -4,11 +4,34 @@
|
|
|
4
4
|
module PaperTrailDiff
|
|
5
5
|
# Selects an inclusive, chronological range from a record's root versions.
|
|
6
6
|
class VersionRange
|
|
7
|
-
#: (untyped, from: untyped, to: untyped) -> void
|
|
8
|
-
def initialize(record, from:, to:)
|
|
7
|
+
#: (untyped, from: untyped, to: untyped, ?version_scope: untyped) -> void
|
|
8
|
+
def initialize(record, from:, to:, version_scope: nil)
|
|
9
9
|
@record = record
|
|
10
10
|
@from = from
|
|
11
11
|
@to = to
|
|
12
|
+
@version_scope = version_scope
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Same selection as `select`, but keeping the step pairs a filter implies.
|
|
16
|
+
#: () -> RootVersionPlan
|
|
17
|
+
def select_plan_for_range
|
|
18
|
+
relation = validated_relation
|
|
19
|
+
select_plan(
|
|
20
|
+
relation.where(created_at: @from.created_at..@to.created_at),
|
|
21
|
+
through: @to
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
#: () -> untyped
|
|
26
|
+
def validated_relation
|
|
27
|
+
relation = versions_relation
|
|
28
|
+
validate_boundary!(@from, relation, boundary: :from)
|
|
29
|
+
validate_boundary!(@to, relation, boundary: :to)
|
|
30
|
+
if Support.compare_versions(@from, @to).positive?
|
|
31
|
+
raise InvalidTimelineRangeError, '`from` version must not follow `to` version'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
relation
|
|
12
35
|
end
|
|
13
36
|
|
|
14
37
|
#: () -> Array[untyped]
|
|
@@ -38,6 +61,7 @@ module PaperTrailDiff
|
|
|
38
61
|
# @rbs @record: untyped
|
|
39
62
|
# @rbs @from: untyped
|
|
40
63
|
# @rbs @to: untyped
|
|
64
|
+
# @rbs @version_scope: untyped
|
|
41
65
|
|
|
42
66
|
#: () -> untyped
|
|
43
67
|
def versions_relation
|
|
@@ -50,13 +74,29 @@ module PaperTrailDiff
|
|
|
50
74
|
|
|
51
75
|
#: (untyped, ?through: untyped) -> Array[untyped]
|
|
52
76
|
def select_relation(relation, through: nil)
|
|
53
|
-
|
|
77
|
+
select_plan(relation, through: through).versions
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
#: (untyped, ?through: untyped) -> RootVersionPlan
|
|
81
|
+
def select_plan(relation, through: nil)
|
|
82
|
+
in_range = ordered(relation.to_a.select do |version|
|
|
54
83
|
next false if Support.compare_versions(@from, version).positive?
|
|
55
84
|
next true unless through
|
|
56
85
|
|
|
57
86
|
Support.compare_versions(version, through) <= 0
|
|
58
|
-
end
|
|
59
|
-
|
|
87
|
+
end)
|
|
88
|
+
RootVersionSelection.new(
|
|
89
|
+
in_range: in_range,
|
|
90
|
+
selected: VersionScopeFilter.new(@version_scope).call(relation, in_range),
|
|
91
|
+
after_range: nil,
|
|
92
|
+
windowed: false,
|
|
93
|
+
filtered: !@version_scope.nil?
|
|
94
|
+
).call
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
#: (Array[untyped]) -> Array[untyped]
|
|
98
|
+
def ordered(versions)
|
|
99
|
+
versions.sort_by { |version| Support.chronological_version_key(version) }
|
|
60
100
|
end
|
|
61
101
|
|
|
62
102
|
#: (untyped, untyped, boundary: Symbol) -> void
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module PaperTrailDiff
|
|
5
|
+
# Applies a caller's version filter to a range, naming which of its versions
|
|
6
|
+
# count as selected mutations. The unfiltered versions stay available to the
|
|
7
|
+
# caller, because the one that reveals the last selected mutation is drawn
|
|
8
|
+
# from them.
|
|
9
|
+
class VersionScopeFilter
|
|
10
|
+
#: (untyped) -> void
|
|
11
|
+
def initialize(scope)
|
|
12
|
+
@scope = validated(scope)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
#: (untyped, Array[untyped]) -> Array[untyped]
|
|
16
|
+
def call(relation, in_range)
|
|
17
|
+
scope = @scope
|
|
18
|
+
return in_range unless scope
|
|
19
|
+
|
|
20
|
+
# A narrowed relation is the expected return. Active Support also gives
|
|
21
|
+
# `pluck` to plain enumerables, so an array of versions works too.
|
|
22
|
+
chosen = Set.new(scope.call(relation).pluck(:id))
|
|
23
|
+
in_range.select { |version| chosen.include?(version.id) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# @rbs @scope: untyped
|
|
29
|
+
|
|
30
|
+
#: (untyped) -> untyped
|
|
31
|
+
def validated(scope)
|
|
32
|
+
return scope if scope.nil? || scope.respond_to?(:call)
|
|
33
|
+
|
|
34
|
+
raise ConfigurationError, 'version_scope: must respond to call'
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/paper_trail_diff.rb
CHANGED
|
@@ -39,6 +39,9 @@ require_relative 'paper_trail_diff/live_endpoint_provider'
|
|
|
39
39
|
require_relative 'paper_trail_diff/live_graph_collector'
|
|
40
40
|
require_relative 'paper_trail_diff/batch_boundary_resolver'
|
|
41
41
|
require_relative 'paper_trail_diff/comparison_batch'
|
|
42
|
+
require_relative 'paper_trail_diff/root_version_plan'
|
|
43
|
+
require_relative 'paper_trail_diff/version_scope_filter'
|
|
44
|
+
require_relative 'paper_trail_diff/root_version_selection'
|
|
42
45
|
require_relative 'paper_trail_diff/batched_root_versions'
|
|
43
46
|
require_relative 'paper_trail_diff/snapshot_normalizer'
|
|
44
47
|
require_relative 'paper_trail_diff/historical_snapshot_store'
|
|
@@ -142,27 +145,31 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
|
|
|
142
145
|
end
|
|
143
146
|
|
|
144
147
|
# Compares every adjacent reconstructed state in an inclusive version range.
|
|
145
|
-
#: (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option) -> 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]
|
|
146
149
|
def timeline( # rubocop:disable Metrics/ParameterLists
|
|
147
150
|
record,
|
|
148
151
|
from: nil,
|
|
149
152
|
to: nil,
|
|
150
153
|
within: nil,
|
|
151
154
|
associations: [],
|
|
152
|
-
ignore: DEFAULT_IGNORED_ATTRIBUTES
|
|
155
|
+
ignore: DEFAULT_IGNORED_ATTRIBUTES,
|
|
156
|
+
version_scope: nil,
|
|
157
|
+
close_on: nil
|
|
153
158
|
)
|
|
154
159
|
PaperTrailAdapter.new(associations: associations, ignore: ignore).timeline(
|
|
155
160
|
record,
|
|
156
161
|
from: from,
|
|
157
162
|
to: to,
|
|
158
|
-
within: within
|
|
163
|
+
within: within,
|
|
164
|
+
version_scope: version_scope,
|
|
165
|
+
close_on: close_on
|
|
159
166
|
)
|
|
160
167
|
end
|
|
161
168
|
|
|
162
169
|
# Compares adjacent root and selected-descendant activity boundaries.
|
|
163
170
|
# `reload_live_endpoints:` applies only when `to:` is a current record; the
|
|
164
171
|
# other range forms never read live state.
|
|
165
|
-
#: (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?reload_live_endpoints: bool) -> 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]
|
|
166
173
|
def activity_timeline( # rubocop:disable Metrics/ParameterLists
|
|
167
174
|
record,
|
|
168
175
|
from: nil,
|
|
@@ -170,7 +177,9 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
|
|
|
170
177
|
within: nil,
|
|
171
178
|
associations: [],
|
|
172
179
|
ignore: DEFAULT_IGNORED_ATTRIBUTES,
|
|
173
|
-
reload_live_endpoints: true
|
|
180
|
+
reload_live_endpoints: true,
|
|
181
|
+
version_scope: nil,
|
|
182
|
+
close_on: nil
|
|
174
183
|
)
|
|
175
184
|
PaperTrailAdapter.new(
|
|
176
185
|
associations: associations,
|
|
@@ -180,12 +189,14 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
|
|
|
180
189
|
record,
|
|
181
190
|
from: from,
|
|
182
191
|
to: to,
|
|
183
|
-
within: within
|
|
192
|
+
within: within,
|
|
193
|
+
version_scope: version_scope,
|
|
194
|
+
close_on: close_on
|
|
184
195
|
)
|
|
185
196
|
end
|
|
186
197
|
|
|
187
198
|
# Builds an endpoint diff and root-checkpoint timeline while normalizing each version once.
|
|
188
|
-
#: (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?activity: bool) -> 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
|
|
189
200
|
def analyze( # rubocop:disable Metrics/ParameterLists
|
|
190
201
|
record,
|
|
191
202
|
from: nil,
|
|
@@ -193,32 +204,40 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
|
|
|
193
204
|
within: nil,
|
|
194
205
|
associations: [],
|
|
195
206
|
ignore: DEFAULT_IGNORED_ATTRIBUTES,
|
|
196
|
-
activity: false
|
|
207
|
+
activity: false,
|
|
208
|
+
version_scope: nil,
|
|
209
|
+
close_on: nil
|
|
197
210
|
)
|
|
198
211
|
PaperTrailAdapter.new(associations: associations, ignore: ignore).analyze(
|
|
199
212
|
record,
|
|
200
213
|
from: from,
|
|
201
214
|
to: to,
|
|
202
215
|
within: within,
|
|
203
|
-
activity: activity
|
|
216
|
+
activity: activity,
|
|
217
|
+
version_scope: version_scope,
|
|
218
|
+
close_on: close_on
|
|
204
219
|
)
|
|
205
220
|
end
|
|
206
221
|
|
|
207
222
|
# Analyzes many roots over one shared time window, preparing their selected
|
|
208
223
|
# history once for the batch instead of once per record. Roots with no
|
|
209
224
|
# versions in the window return an empty `Analysis`.
|
|
210
|
-
#: (Array[untyped], ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?activity: bool) -> Hash[identity, Analysis]
|
|
211
|
-
def analyze_many(
|
|
225
|
+
#: (Array[untyped], ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?) -> Hash[identity, Analysis]
|
|
226
|
+
def analyze_many( # rubocop:disable Metrics/ParameterLists
|
|
212
227
|
records,
|
|
213
228
|
within: nil,
|
|
214
229
|
associations: [],
|
|
215
230
|
ignore: DEFAULT_IGNORED_ATTRIBUTES,
|
|
216
|
-
activity: false
|
|
231
|
+
activity: false,
|
|
232
|
+
version_scope: nil,
|
|
233
|
+
close_on: nil
|
|
217
234
|
)
|
|
218
235
|
PaperTrailAdapter.new(associations: associations, ignore: ignore).analyze_many(
|
|
219
236
|
records,
|
|
220
237
|
within: within,
|
|
221
|
-
activity: activity
|
|
238
|
+
activity: activity,
|
|
239
|
+
version_scope: version_scope,
|
|
240
|
+
close_on: close_on
|
|
222
241
|
)
|
|
223
242
|
end
|
|
224
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]
|
|
@@ -30,6 +30,10 @@ module PaperTrailDiff
|
|
|
30
30
|
# : () -> Array[ActivityStep]
|
|
31
31
|
def no_steps: () -> Array[ActivityStep]
|
|
32
32
|
|
|
33
|
+
# Every boundary in the span, filtered out or not. A filter narrows where the
|
|
34
|
+
# span starts and ends, but the sequence inside it stays complete: dropping
|
|
35
|
+
# boundaries would fold the changes they carried into a neighbouring step and
|
|
36
|
+
# credit them to whoever made that one.
|
|
33
37
|
# : () -> Array[ActivityStep]
|
|
34
38
|
def build_between_versions: () -> Array[ActivityStep]
|
|
35
39
|
|
|
@@ -67,8 +71,8 @@ module PaperTrailDiff
|
|
|
67
71
|
# Only the activity view gains the closing removal. The endpoint diff and
|
|
68
72
|
# the root timeline keep their `compare` and `timeline` semantics, under
|
|
69
73
|
# which the state at a destroy version is the state before the deletion.
|
|
70
|
-
# : (
|
|
71
|
-
def build_analysis: (
|
|
74
|
+
# : (RootVersionPlan, Array[ActivityEvent], ActivityHistory) -> Analysis
|
|
75
|
+
def build_analysis: (RootVersionPlan, Array[ActivityEvent], ActivityHistory) -> Analysis
|
|
72
76
|
|
|
73
77
|
# : () -> TimeActivityTimelineBuilder
|
|
74
78
|
def time_builder: () -> TimeActivityTimelineBuilder
|
|
@@ -9,15 +9,24 @@ module PaperTrailDiff
|
|
|
9
9
|
|
|
10
10
|
attr_reader activity_timeline: Array[ActivityStep]?
|
|
11
11
|
|
|
12
|
+
attr_reader from_snapshot: RecordSnapshot?
|
|
13
|
+
|
|
14
|
+
attr_reader to_snapshot: RecordSnapshot?
|
|
15
|
+
|
|
12
16
|
# The result for a record whose requested history contains no versions,
|
|
13
17
|
# which is an empty history rather than a failed request.
|
|
14
18
|
# : () -> Analysis
|
|
15
19
|
def self.empty: () -> Analysis
|
|
16
20
|
|
|
17
|
-
#
|
|
18
|
-
|
|
21
|
+
# The reconstructed states the diff was taken between. A report that has to
|
|
22
|
+
# render unchanged columns needs the whole final state, not only what moved.
|
|
23
|
+
# : (diff: Diff, timeline: Array[Step], ?activity_timeline: Array[ActivityStep]?, ?from_snapshot: RecordSnapshot?, ?to_snapshot: RecordSnapshot?) -> void
|
|
24
|
+
def initialize: (diff: Diff, timeline: Array[Step], ?activity_timeline: Array[ActivityStep]?, ?from_snapshot: RecordSnapshot?, ?to_snapshot: RecordSnapshot?) -> void
|
|
19
25
|
|
|
20
|
-
#
|
|
21
|
-
|
|
26
|
+
# Reconstructed endpoint states are opt-in, because they carry the whole
|
|
27
|
+
# selected graph whether or not anything changed, which dwarfs the rest of
|
|
28
|
+
# the payload for a wide graph.
|
|
29
|
+
# : (?snapshots: bool) -> Hash[Symbol, untyped]
|
|
30
|
+
def to_h: (?snapshots: bool) -> Hash[Symbol, untyped]
|
|
22
31
|
end
|
|
23
32
|
end
|
|
@@ -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) -> void
|
|
9
|
-
def initialize: (Array[untyped], time_range: TimeRange?, live_loader: untyped, history_preparer: untyped, analyzer: 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
|
|
@@ -23,6 +25,13 @@ module PaperTrailDiff
|
|
|
23
25
|
|
|
24
26
|
@time_range: TimeRange?
|
|
25
27
|
|
|
28
|
+
@version_scope: untyped
|
|
29
|
+
|
|
30
|
+
# A filter is a callable that narrows the version relation, so it is checked
|
|
31
|
+
# up front rather than failing partway through a batch.
|
|
32
|
+
# : (untyped) -> untyped
|
|
33
|
+
def validated_scope: (untyped) -> untyped
|
|
34
|
+
|
|
26
35
|
# : () -> Array[untyped]
|
|
27
36
|
def validated_records: () -> Array[untyped]
|
|
28
37
|
|
|
@@ -31,12 +40,12 @@ module PaperTrailDiff
|
|
|
31
40
|
# The roots are preloaded first, because preparation reads their current
|
|
32
41
|
# association state as a fallback and would otherwise walk it one root at a
|
|
33
42
|
# time.
|
|
34
|
-
# : (Array[untyped], Hash[Array[String], Array[untyped]
|
|
35
|
-
def prepare: (Array[untyped], Hash[Array[String], Array[untyped]
|
|
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
|
|
36
45
|
|
|
37
46
|
# A root with no versions in range has nothing to report, which is an empty
|
|
38
47
|
# result rather than a failed request.
|
|
39
|
-
# : (untyped,
|
|
40
|
-
def analysis_for: (untyped,
|
|
48
|
+
# : (untyped, RootVersionPlan) -> Analysis
|
|
49
|
+
def analysis_for: (untyped, RootVersionPlan) -> Analysis
|
|
41
50
|
end
|
|
42
51
|
end
|
|
@@ -7,8 +7,8 @@ module PaperTrailDiff
|
|
|
7
7
|
# : (tree: AssociationTree, timeline_snapshotter: untyped, activity_snapshotter: untyped, preparer: untyped, activity: bool) -> void
|
|
8
8
|
def initialize: (tree: AssociationTree, timeline_snapshotter: untyped, activity_snapshotter: untyped, preparer: untyped, activity: bool) -> void
|
|
9
9
|
|
|
10
|
-
# : (untyped,
|
|
11
|
-
def call: (untyped,
|
|
10
|
+
# : (untyped, RootVersionPlan) -> Analysis
|
|
11
|
+
def call: (untyped, RootVersionPlan) -> Analysis
|
|
12
12
|
|
|
13
13
|
private
|
|
14
14
|
|
|
@@ -22,7 +22,7 @@ module PaperTrailDiff
|
|
|
22
22
|
|
|
23
23
|
@tree: AssociationTree
|
|
24
24
|
|
|
25
|
-
# : (untyped,
|
|
26
|
-
def activity_analysis: (untyped,
|
|
25
|
+
# : (untyped, RootVersionPlan) -> Analysis
|
|
26
|
+
def activity_analysis: (untyped, RootVersionPlan) -> Analysis
|
|
27
27
|
end
|
|
28
28
|
end
|