paper_trail_diff 0.3.0 → 0.3.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.
Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/README.md +9 -0
  4. data/lib/paper_trail_diff/activity_belongs_to_event_applier.rb +126 -0
  5. data/lib/paper_trail_diff/activity_collection_event_applier.rb +137 -0
  6. data/lib/paper_trail_diff/activity_collection_record_updater.rb +67 -0
  7. data/lib/paper_trail_diff/activity_collection_route_change.rb +21 -0
  8. data/lib/paper_trail_diff/activity_collection_route_updater.rb +128 -0
  9. data/lib/paper_trail_diff/activity_event_record_normalizer.rb +42 -0
  10. data/lib/paper_trail_diff/activity_event_record_resolver.rb +120 -0
  11. data/lib/paper_trail_diff/activity_event_route_finder.rb +112 -0
  12. data/lib/paper_trail_diff/activity_event_snapshot_refresher.rb +56 -650
  13. data/lib/paper_trail_diff/activity_relationship.rb +55 -0
  14. data/lib/paper_trail_diff/branch_snapshot_refresher.rb +2 -0
  15. data/lib/paper_trail_diff/version.rb +1 -1
  16. data/lib/paper_trail_diff.rb +9 -0
  17. data/sig/generated/paper_trail_diff/activity_belongs_to_event_applier.rbs +41 -0
  18. data/sig/generated/paper_trail_diff/activity_collection_event_applier.rbs +46 -0
  19. data/sig/generated/paper_trail_diff/activity_collection_record_updater.rbs +25 -0
  20. data/sig/generated/paper_trail_diff/activity_collection_route_change.rbs +17 -0
  21. data/sig/generated/paper_trail_diff/activity_collection_route_updater.rbs +44 -0
  22. data/sig/generated/paper_trail_diff/activity_event_record_normalizer.rbs +19 -0
  23. data/sig/generated/paper_trail_diff/activity_event_record_resolver.rbs +44 -0
  24. data/sig/generated/paper_trail_diff/activity_event_route_finder.rbs +54 -0
  25. data/sig/generated/paper_trail_diff/activity_event_snapshot_refresher.rbs +18 -110
  26. data/sig/generated/paper_trail_diff/activity_relationship.rbs +23 -0
  27. metadata +22 -4
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module PaperTrailDiff
5
+ # Reconstructs an event's post-change record or immutable scalar delta.
6
+ class ActivityEventRecordResolver
7
+ RECORD_AFTER_HANDLERS = {
8
+ 'create' => :created_record_after,
9
+ 'update' => :updated_record_after
10
+ }.freeze
11
+ private_constant :RECORD_AFTER_HANDLERS
12
+
13
+ #: (?record_transition: untyped) -> void
14
+ def initialize(record_transition: nil)
15
+ @record_transition = record_transition
16
+ end
17
+
18
+ #: (untyped) -> untyped
19
+ def record_after(version)
20
+ return if version.event.to_s == 'destroy'
21
+
22
+ changed_record = changed_record_after(version)
23
+ return changed_record if changed_record
24
+
25
+ successor = version.next
26
+ record = successor&.reify(dup: true)
27
+ return record if record
28
+
29
+ model_class = Endpoint.model_class(version)
30
+ criteria = { model_class.primary_key => version.item_id } #: Hash[untyped, untyped]
31
+ model_class.unscoped.find_by(criteria)
32
+ end
33
+
34
+ #: (untyped) -> ActivitySnapshotDelta?
35
+ def snapshot_delta(version)
36
+ return unless version.event.to_s == 'update' && version.object
37
+
38
+ transition = @record_transition&.call(version)
39
+ return unless transition
40
+
41
+ ActivitySnapshotDelta.new(
42
+ before_attributes: transition.fetch(0),
43
+ after_attributes: transition.fetch(1)
44
+ )
45
+ end
46
+
47
+ #: (untyped) -> untyped
48
+ def changed_record_after(version)
49
+ handler = RECORD_AFTER_HANDLERS[version.event.to_s]
50
+ send(handler, version) if handler
51
+ end
52
+
53
+ #: (untyped) -> untyped
54
+ def created_record_after(version)
55
+ model_class = Endpoint.model_class(version)
56
+ changes = deserialized_changeset(version, model_class)
57
+ return unless changes.respond_to?(:each) && !changes.empty?
58
+
59
+ model_class.new(after_attributes(changes, model_class))
60
+ end
61
+
62
+ #: (untyped) -> untyped
63
+ def updated_record_after(version)
64
+ record = version.reify(dup: true)
65
+ changes = deserialized_changeset(version, record&.class)
66
+ apply_changes(record, changes)
67
+ end
68
+
69
+ # Mirrors PaperTrail's changeset deserialization without resolving
70
+ # +version.item+, which would issue one live-record query per event.
71
+ #: (untyped, untyped) -> untyped
72
+ def deserialized_changeset(version, model_class)
73
+ return unless model_class && version.class.column_names.include?('object_changes')
74
+
75
+ paper_trail = Object.const_get(:PaperTrail) #: untyped
76
+ adapter = paper_trail.config.object_changes_adapter
77
+ return adapter.load_changeset(version) if adapter.respond_to?(:load_changeset)
78
+
79
+ standard_changeset(paper_trail, version, model_class)
80
+ end
81
+
82
+ private
83
+
84
+ # @rbs @record_transition: untyped
85
+
86
+ #: (untyped, untyped) -> Hash[untyped, untyped]
87
+ def after_attributes(changes, model_class)
88
+ names = model_class.attribute_names
89
+ attributes = {} #: Hash[untyped, untyped]
90
+ changes.each do |name, values|
91
+ next unless names.include?(name.to_s)
92
+
93
+ attributes[name] = values.last
94
+ end
95
+ attributes
96
+ end
97
+
98
+ #: (untyped, untyped) -> untyped
99
+ def apply_changes(record, changes)
100
+ return unless record && changes.respond_to?(:each) && !changes.empty?
101
+
102
+ changes.each do |name, values|
103
+ next unless record.has_attribute?(name) && values.respond_to?(:last)
104
+
105
+ record[name] = values.last
106
+ end
107
+ record
108
+ end
109
+
110
+ #: (untyped, untyped, untyped) -> untyped
111
+ def standard_changeset(paper_trail, version, model_class)
112
+ raw_changes = version.send(:object_changes_deserialized)
113
+ active_support = Object.const_get(:ActiveSupport) #: untyped
114
+ changes = active_support.const_get(:HashWithIndifferentAccess).new(raw_changes)
115
+ serializers = paper_trail.const_get(:AttributeSerializers)
116
+ serializers.const_get(:ObjectChangesAttribute).new(model_class).deserialize(changes)
117
+ changes
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module PaperTrailDiff
5
+ # Finds explicitly selected collection and belongs-to routes for one event type.
6
+ class ActivityEventRouteFinder
7
+ #: (AssociationTraversal) -> void
8
+ def initialize(traversal)
9
+ @traversal = traversal
10
+ @collection_model = nil
11
+ @collection_tree = nil #: AssociationTree?
12
+ @collection_type = nil #: String?
13
+ @collection_path = nil #: String?
14
+ @collection_routes = nil #: Array[Array[untyped]]?
15
+ @belongs_to_model = nil
16
+ @belongs_to_tree = nil #: AssociationTree?
17
+ @belongs_to_type = nil #: String?
18
+ @belongs_to_path = nil #: String?
19
+ @belongs_to_routes = nil #: Array[Array[untyped]]?
20
+ end
21
+
22
+ #: (untyped, AssociationTree, String, ?path: String) -> Array[Array[untyped]]
23
+ def collection_routes(model_class, tree, target_type, path: '')
24
+ cached = @collection_routes
25
+ if cached && @collection_model.equal?(model_class) && @collection_tree.equal?(tree) &&
26
+ @collection_type == target_type && @collection_path == path
27
+ return cached
28
+ end
29
+
30
+ computed = freeze_routes(routes(model_class, tree, target_type, :has_many, path: path))
31
+ @collection_model = model_class
32
+ @collection_tree = tree
33
+ @collection_type = Support.immutable_copy(target_type)
34
+ @collection_path = Support.immutable_copy(path)
35
+ @collection_routes = computed
36
+ end
37
+
38
+ #: (untyped, AssociationTree, String, ?path: String) -> Array[Array[untyped]]
39
+ def belongs_to_routes(model_class, tree, target_type, path: '')
40
+ cached = @belongs_to_routes
41
+ if cached && @belongs_to_model.equal?(model_class) && @belongs_to_tree.equal?(tree) &&
42
+ @belongs_to_type == target_type && @belongs_to_path == path
43
+ return cached
44
+ end
45
+
46
+ computed = freeze_routes(routes(model_class, tree, target_type, :belongs_to, path: path))
47
+ @belongs_to_model = model_class
48
+ @belongs_to_tree = tree
49
+ @belongs_to_type = Support.immutable_copy(target_type)
50
+ @belongs_to_path = Support.immutable_copy(path)
51
+ @belongs_to_routes = computed
52
+ end
53
+
54
+ private
55
+
56
+ # @rbs @traversal: AssociationTraversal
57
+ # @rbs @collection_model: untyped
58
+ # @rbs @collection_tree: AssociationTree?
59
+ # @rbs @collection_type: String?
60
+ # @rbs @collection_path: String?
61
+ # @rbs @collection_routes: Array[Array[untyped]]?
62
+ # @rbs @belongs_to_model: untyped
63
+ # @rbs @belongs_to_tree: AssociationTree?
64
+ # @rbs @belongs_to_type: String?
65
+ # @rbs @belongs_to_path: String?
66
+ # @rbs @belongs_to_routes: Array[Array[untyped]]?
67
+
68
+ #: (Array[Array[untyped]]) -> Array[Array[untyped]]
69
+ def freeze_routes(routes)
70
+ routes.each do |route|
71
+ route.each(&:freeze)
72
+ route.freeze
73
+ end.freeze
74
+ end
75
+
76
+ #: (untyped, AssociationTree, String, Symbol, path: String) -> Array[Array[untyped]]
77
+ def routes(model_class, tree, target_type, macro, path: '')
78
+ @traversal.reflections_for(model_class, tree, path: path).flat_map do |reflection|
79
+ routes_for_reflection(reflection, tree, target_type, macro, path)
80
+ end
81
+ end
82
+
83
+ #: (untyped, AssociationTree, String, Symbol, String) -> Array[Array[untyped]]
84
+ def routes_for_reflection(reflection, tree, target_type, macro, path)
85
+ name = reflection.name.to_s
86
+ subtree = tree.child(name)
87
+ return [] unless subtree
88
+
89
+ child_path = Support.association_path(path, name)
90
+ entry = [name, reflection, subtree, child_path]
91
+ direct = [] #: Array[Array[untyped]]
92
+ direct << [entry] if direct_route?(reflection, target_type, macro)
93
+ return direct if subtree.empty? || reflection.polymorphic?
94
+
95
+ nested = routes(reflection.klass, subtree, target_type, macro, path: child_path)
96
+ direct.concat(nested.map { |route| [entry, *route] })
97
+ end
98
+
99
+ #: (untyped, String, Symbol) -> bool
100
+ def direct_route?(reflection, target_type, macro)
101
+ reflection.macro == macro && supported_direct_reflection?(reflection, macro) &&
102
+ reflection.klass.base_class.name.to_s == target_type
103
+ end
104
+
105
+ #: (untyped, Symbol) -> bool
106
+ def supported_direct_reflection?(reflection, macro)
107
+ return !reflection.options[:through] if macro == :has_many
108
+
109
+ !reflection.polymorphic?
110
+ end
111
+ end
112
+ end