paper_trail-human 0.3.1 → 0.4.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 +11 -0
- data/lib/paper_trail/human/adapters/formatters.rb +13 -0
- data/lib/paper_trail/human/adapters/resolvers/number.rb +1 -1
- data/lib/paper_trail/human/adapters/resolvers.rb +17 -0
- data/lib/paper_trail/human/configuration.rb +11 -1
- data/lib/paper_trail/human/core/batch_presenter.rb +69 -26
- data/lib/paper_trail/human/core/field_formatter.rb +12 -4
- data/lib/paper_trail/human/core/presenter.rb +10 -6
- data/lib/paper_trail/human/version.rb +1 -1
- data/lib/paper_trail/human.rb +11 -22
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4dea24fd23784bda5b3b0f6d704f5f3a53a2ab00c8ed8500fe69a1b432eb3f47
|
|
4
|
+
data.tar.gz: 6c074d6f13efd43f93e1c201e36d9310420095c516980bc80c74dc6116551105
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bf9745f8aba245105b94e41f5739ec4358d657311d7cc9b5527db3353ff1e217243955f3f033cd576f98fd0d8fa0a6079f927496f101a4be31813bc734ac75bd
|
|
7
|
+
data.tar.gz: e5fb5fc24f36be842103d727af163f64545c0208229178dd7bc93f2559b09022d5177cffb4c82e448aed78a21a5564a6ff79376875184c9501b298685ff37e37
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-05-31
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Custom resolver registration via `config.register_resolver(:type, MyResolver)`
|
|
7
|
+
- Custom formatter registration via `config.register_formatter(:type, MyFormatter)`
|
|
8
|
+
- Lazy loading of adapters with `autoload` (faster boot for apps using few resolvers)
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Extracted `Adapters::Resolvers` and `Adapters::Formatters` into autoload modules
|
|
12
|
+
- `FORMATTERS` constant uses string references for deferred loading
|
|
13
|
+
|
|
3
14
|
## [0.3.1] - 2026-05-31
|
|
4
15
|
|
|
5
16
|
### Fixed
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaperTrail
|
|
4
|
+
module Human
|
|
5
|
+
module Adapters
|
|
6
|
+
module Formatters
|
|
7
|
+
autoload :Text, 'paper_trail/human/adapters/formatters/text'
|
|
8
|
+
autoload :Markdown, 'paper_trail/human/adapters/formatters/markdown'
|
|
9
|
+
autoload :Html, 'paper_trail/human/adapters/formatters/html'
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaperTrail
|
|
4
|
+
module Human
|
|
5
|
+
module Adapters
|
|
6
|
+
module Resolvers
|
|
7
|
+
autoload :Relation, 'paper_trail/human/adapters/resolvers/relation'
|
|
8
|
+
autoload :Enum, 'paper_trail/human/adapters/resolvers/enum'
|
|
9
|
+
autoload :Boolean, 'paper_trail/human/adapters/resolvers/boolean'
|
|
10
|
+
autoload :Custom, 'paper_trail/human/adapters/resolvers/custom'
|
|
11
|
+
autoload :Text, 'paper_trail/human/adapters/resolvers/text'
|
|
12
|
+
autoload :Date, 'paper_trail/human/adapters/resolvers/date'
|
|
13
|
+
autoload :Number, 'paper_trail/human/adapters/resolvers/number'
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -7,7 +7,7 @@ module PaperTrail
|
|
|
7
7
|
|
|
8
8
|
attr_accessor :whodunnit_resolver, :extend_version_model, :field_name_resolver,
|
|
9
9
|
:translate_events, :after_format
|
|
10
|
-
attr_reader :ignored_fields
|
|
10
|
+
attr_reader :ignored_fields, :custom_resolvers, :custom_formatters
|
|
11
11
|
|
|
12
12
|
def initialize
|
|
13
13
|
@model_configs = {}
|
|
@@ -17,6 +17,8 @@ module PaperTrail
|
|
|
17
17
|
@translate_events = false
|
|
18
18
|
@extend_version_model = false
|
|
19
19
|
@after_format = nil
|
|
20
|
+
@custom_resolvers = {}
|
|
21
|
+
@custom_formatters = {}
|
|
20
22
|
@mutex = Mutex.new
|
|
21
23
|
end
|
|
22
24
|
|
|
@@ -30,6 +32,14 @@ module PaperTrail
|
|
|
30
32
|
@mutex.synchronize { @model_configs[model_name.to_s] = model_config.freeze }
|
|
31
33
|
end
|
|
32
34
|
|
|
35
|
+
def register_resolver(type, klass)
|
|
36
|
+
@mutex.synchronize { @custom_resolvers[type.to_sym] = klass }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def register_formatter(type, klass)
|
|
40
|
+
@mutex.synchronize { @custom_formatters[type.to_sym] = klass }
|
|
41
|
+
end
|
|
42
|
+
|
|
33
43
|
def config_for(model_name)
|
|
34
44
|
@model_configs[model_name.to_s]
|
|
35
45
|
end
|
|
@@ -7,45 +7,30 @@ module PaperTrail
|
|
|
7
7
|
def initialize(configuration)
|
|
8
8
|
@configuration = configuration
|
|
9
9
|
@change_extractor = ChangeExtractor.new
|
|
10
|
-
@item_name_loader = ItemNameLoader.new(configuration)
|
|
11
|
-
@relation_loader = RelationLoader.new(configuration)
|
|
12
10
|
end
|
|
13
11
|
|
|
14
12
|
def call(versions, only: nil, except: nil)
|
|
15
13
|
versions_data = versions.map { |v| [v, @change_extractor.call(v)] }
|
|
16
|
-
preloaded =
|
|
17
|
-
item_names = @item_name_loader.preload(versions)
|
|
14
|
+
preloaded = preload_relations(versions_data)
|
|
18
15
|
|
|
19
16
|
versions_data.map do |version, changes|
|
|
20
|
-
format_version(version, changes, preloaded,
|
|
17
|
+
format_version(version, changes, preloaded, only: only, except: except)
|
|
21
18
|
end
|
|
22
19
|
end
|
|
23
20
|
|
|
24
21
|
private
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
def format_version(version, changes, preloaded, item_names, only: nil, except: nil)
|
|
28
|
-
# rubocop:enable Metrics/ParameterLists
|
|
29
|
-
formatter = build_formatter(version, preloaded)
|
|
30
|
-
result = base_result(version, changes, formatter, only: only, except: except)
|
|
31
|
-
|
|
32
|
-
item_name = @item_name_loader.resolve(version, item_names)
|
|
33
|
-
result[:item_name] = item_name if item_name
|
|
34
|
-
|
|
35
|
-
apply_after_format(result, version)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def build_formatter(version, preloaded)
|
|
23
|
+
def format_version(version, changes, preloaded, only: nil, except: nil)
|
|
39
24
|
model_config = @configuration.config_for(version.item_type)
|
|
40
|
-
FieldFormatter.new(
|
|
41
|
-
model_config,
|
|
25
|
+
formatter = FieldFormatter.new(
|
|
26
|
+
model_config,
|
|
27
|
+
version.item_type,
|
|
42
28
|
field_name_resolver: @configuration.field_name_resolver,
|
|
43
|
-
preloaded: preloaded
|
|
29
|
+
preloaded: preloaded,
|
|
30
|
+
custom_resolvers: @configuration.custom_resolvers
|
|
44
31
|
)
|
|
45
|
-
end
|
|
46
32
|
|
|
47
|
-
|
|
48
|
-
{
|
|
33
|
+
result = {
|
|
49
34
|
user: @configuration.resolve_whodunnit(version.whodunnit),
|
|
50
35
|
event: EventTranslator.call(version.event, translate: @configuration.translate_events),
|
|
51
36
|
model: version.item_type,
|
|
@@ -53,6 +38,11 @@ module PaperTrail
|
|
|
53
38
|
created_at: version.created_at,
|
|
54
39
|
fields: build_fields(changes, formatter, version.event, only: only, except: except)
|
|
55
40
|
}
|
|
41
|
+
|
|
42
|
+
item_name = @configuration.resolve_item_name(version)
|
|
43
|
+
result[:item_name] = item_name if item_name
|
|
44
|
+
|
|
45
|
+
apply_after_format(result, version)
|
|
56
46
|
end
|
|
57
47
|
|
|
58
48
|
def build_fields(changes, formatter, event, only: nil, except: nil)
|
|
@@ -65,6 +55,7 @@ module PaperTrail
|
|
|
65
55
|
def filter_field?(field, only, except)
|
|
66
56
|
field_s = field.to_s
|
|
67
57
|
return only.map(&:to_s).include?(field_s) if only
|
|
58
|
+
|
|
68
59
|
return !except.map(&:to_s).include?(field_s) if except
|
|
69
60
|
|
|
70
61
|
true
|
|
@@ -73,13 +64,65 @@ module PaperTrail
|
|
|
73
64
|
def format_field(formatter, field, values, event)
|
|
74
65
|
previous_value, new_value = Array(values)
|
|
75
66
|
result = formatter.call(field, previous_value, new_value)
|
|
67
|
+
|
|
76
68
|
case event
|
|
77
|
-
when 'create'
|
|
78
|
-
|
|
69
|
+
when 'create'
|
|
70
|
+
result.delete(:previous_value)
|
|
71
|
+
when 'destroy'
|
|
72
|
+
result.delete(:value)
|
|
79
73
|
end
|
|
74
|
+
|
|
80
75
|
result
|
|
81
76
|
end
|
|
82
77
|
|
|
78
|
+
def preload_relations(versions_data)
|
|
79
|
+
relation_fields = collect_relation_fields(versions_data)
|
|
80
|
+
return {} if relation_fields.empty?
|
|
81
|
+
|
|
82
|
+
relation_fields.each_with_object({}) do |(key, ids), cache|
|
|
83
|
+
class_name, attribute = key
|
|
84
|
+
cache[key] = load_records(class_name, attribute, ids)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def collect_relation_fields(versions_data)
|
|
89
|
+
result = Hash.new { |h, k| h[k] = Set.new }
|
|
90
|
+
|
|
91
|
+
versions_data.each do |version, changes|
|
|
92
|
+
collect_from_version(result, version, changes)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
result
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def collect_from_version(result, version, changes)
|
|
99
|
+
model_config = @configuration.config_for(version.item_type)
|
|
100
|
+
return unless model_config
|
|
101
|
+
|
|
102
|
+
changes.each do |field, values|
|
|
103
|
+
field_cfg = model_config.fields[field.to_s]
|
|
104
|
+
next unless field_cfg && field_cfg[:type] == :relation
|
|
105
|
+
|
|
106
|
+
key = relation_key(field_cfg)
|
|
107
|
+
Array(values).compact.each { |v| result[key].add(v) }
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def relation_key(field_cfg)
|
|
112
|
+
class_name = field_cfg[:options][:class_name] || field_cfg[:options][:class].to_s
|
|
113
|
+
attribute = field_cfg[:options][:attribute] || :name
|
|
114
|
+
[class_name, attribute]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def load_records(class_name, attribute, ids)
|
|
118
|
+
klass = Object.const_get(class_name)
|
|
119
|
+
klass.where(id: ids.to_a).to_h do |record|
|
|
120
|
+
[record.id, record.public_send(attribute)]
|
|
121
|
+
end
|
|
122
|
+
rescue NameError
|
|
123
|
+
{}
|
|
124
|
+
end
|
|
125
|
+
|
|
83
126
|
def apply_after_format(result, version)
|
|
84
127
|
return result unless @configuration.after_format
|
|
85
128
|
|
|
@@ -14,11 +14,12 @@ module PaperTrail
|
|
|
14
14
|
number: 'PaperTrail::Human::Adapters::Resolvers::Number'
|
|
15
15
|
}.freeze
|
|
16
16
|
|
|
17
|
-
def initialize(model_config, item_type, field_name_resolver: nil, preloaded: nil)
|
|
17
|
+
def initialize(model_config, item_type, field_name_resolver: nil, preloaded: nil, custom_resolvers: {})
|
|
18
18
|
@model_config = model_config
|
|
19
19
|
@item_type = item_type
|
|
20
20
|
@field_name_resolver = field_name_resolver
|
|
21
21
|
@preloaded = preloaded || {}
|
|
22
|
+
@custom_resolvers = custom_resolvers
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def call(field_name, previous_value, new_value)
|
|
@@ -43,16 +44,23 @@ module PaperTrail
|
|
|
43
44
|
def build_resolver(config, field_name = nil)
|
|
44
45
|
return nil unless config
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
raise Error, "Unknown resolver type: #{config[:type]}" unless
|
|
47
|
+
klass = resolve_class(config[:type])
|
|
48
|
+
raise Error, "Unknown resolver type: #{config[:type]}" unless klass
|
|
48
49
|
|
|
49
|
-
klass = Object.const_get(class_name)
|
|
50
50
|
opts = config[:options]
|
|
51
51
|
opts = opts.merge(cache: relation_cache(config)) if config[:type] == :relation
|
|
52
52
|
opts = opts.merge(field: field_name) if config[:type] == :enum && opts[:from_model]
|
|
53
53
|
klass.new(**opts)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
def resolve_class(type)
|
|
57
|
+
if @custom_resolvers.key?(type)
|
|
58
|
+
@custom_resolvers[type]
|
|
59
|
+
elsif RESOLVER_MAP.key?(type)
|
|
60
|
+
Object.const_get(RESOLVER_MAP[type])
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
56
64
|
def relation_cache(config)
|
|
57
65
|
class_name = config[:options][:class_name] || config[:options][:class].to_s
|
|
58
66
|
attribute = config[:options][:attribute] || :name
|
|
@@ -11,12 +11,7 @@ module PaperTrail
|
|
|
11
11
|
|
|
12
12
|
def call(version, only: nil, except: nil)
|
|
13
13
|
changes = @change_extractor.call(version)
|
|
14
|
-
|
|
15
|
-
formatter = FieldFormatter.new(
|
|
16
|
-
model_config,
|
|
17
|
-
version.item_type,
|
|
18
|
-
field_name_resolver: @configuration.field_name_resolver
|
|
19
|
-
)
|
|
14
|
+
formatter = build_formatter(version)
|
|
20
15
|
|
|
21
16
|
result = {
|
|
22
17
|
user: @configuration.resolve_whodunnit(version.whodunnit),
|
|
@@ -35,6 +30,15 @@ module PaperTrail
|
|
|
35
30
|
|
|
36
31
|
private
|
|
37
32
|
|
|
33
|
+
def build_formatter(version)
|
|
34
|
+
model_config = @configuration.config_for(version.item_type)
|
|
35
|
+
FieldFormatter.new(
|
|
36
|
+
model_config, version.item_type,
|
|
37
|
+
field_name_resolver: @configuration.field_name_resolver,
|
|
38
|
+
custom_resolvers: @configuration.custom_resolvers
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
38
42
|
def build_fields(changes, formatter, event, only: nil, except: nil)
|
|
39
43
|
changes
|
|
40
44
|
.reject { |field, _| @configuration.ignored_fields.include?(field.to_s) }
|
data/lib/paper_trail/human.rb
CHANGED
|
@@ -7,20 +7,10 @@ require_relative 'human/core/field_formatter'
|
|
|
7
7
|
require_relative 'human/core/event_translator'
|
|
8
8
|
require_relative 'human/core/presenter'
|
|
9
9
|
require_relative 'human/core/batch_presenter'
|
|
10
|
-
require_relative 'human/core/item_name_loader'
|
|
11
|
-
require_relative 'human/core/relation_loader'
|
|
12
10
|
require_relative 'human/core/timeline'
|
|
13
11
|
require_relative 'human/ports/resolver'
|
|
14
|
-
require_relative 'human/adapters/resolvers
|
|
15
|
-
require_relative 'human/adapters/
|
|
16
|
-
require_relative 'human/adapters/resolvers/boolean'
|
|
17
|
-
require_relative 'human/adapters/resolvers/custom'
|
|
18
|
-
require_relative 'human/adapters/resolvers/text'
|
|
19
|
-
require_relative 'human/adapters/resolvers/date'
|
|
20
|
-
require_relative 'human/adapters/resolvers/number'
|
|
21
|
-
require_relative 'human/adapters/formatters/text'
|
|
22
|
-
require_relative 'human/adapters/formatters/markdown'
|
|
23
|
-
require_relative 'human/adapters/formatters/html'
|
|
12
|
+
require_relative 'human/adapters/resolvers'
|
|
13
|
+
require_relative 'human/adapters/formatters'
|
|
24
14
|
|
|
25
15
|
module PaperTrail
|
|
26
16
|
module Human
|
|
@@ -30,9 +20,9 @@ module PaperTrail
|
|
|
30
20
|
private_constant :MUTEX
|
|
31
21
|
|
|
32
22
|
FORMATTERS = {
|
|
33
|
-
text: Adapters::Formatters::Text,
|
|
34
|
-
markdown: Adapters::Formatters::Markdown,
|
|
35
|
-
html: Adapters::Formatters::Html
|
|
23
|
+
text: 'PaperTrail::Human::Adapters::Formatters::Text',
|
|
24
|
+
markdown: 'PaperTrail::Human::Adapters::Formatters::Markdown',
|
|
25
|
+
html: 'PaperTrail::Human::Adapters::Formatters::Html'
|
|
36
26
|
}.freeze
|
|
37
27
|
private_constant :FORMATTERS
|
|
38
28
|
|
|
@@ -56,11 +46,7 @@ module PaperTrail
|
|
|
56
46
|
|
|
57
47
|
def format_collection(versions, only: nil, except: nil, as: nil)
|
|
58
48
|
results = Core::BatchPresenter.new(configuration).call(versions, only: only, except: except)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
formatted = results.map { |r| formatter(as).call(r) }
|
|
62
|
-
separator = as.to_sym == :html ? "\n" : "\n\n"
|
|
63
|
-
formatted.join(separator)
|
|
49
|
+
as ? results.map { |r| formatter(as).call(r) } : results
|
|
64
50
|
end
|
|
65
51
|
|
|
66
52
|
def timeline(versions, group_by: :day, only: nil, except: nil)
|
|
@@ -70,9 +56,12 @@ module PaperTrail
|
|
|
70
56
|
private
|
|
71
57
|
|
|
72
58
|
def formatter(type)
|
|
73
|
-
|
|
74
|
-
|
|
59
|
+
sym = type.to_sym
|
|
60
|
+
klass = configuration.custom_formatters[sym] || FORMATTERS[sym]
|
|
61
|
+
available = (FORMATTERS.keys + configuration.custom_formatters.keys).join(', ')
|
|
62
|
+
raise Error, "Unknown format: #{type}. Available: #{available}" unless klass
|
|
75
63
|
|
|
64
|
+
klass = Object.const_get(klass) if klass.is_a?(String)
|
|
76
65
|
klass.new
|
|
77
66
|
end
|
|
78
67
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: paper_trail-human
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gabriel
|
|
@@ -52,9 +52,11 @@ files:
|
|
|
52
52
|
- lib/generators/paper_trail/human/templates/initializer.rb
|
|
53
53
|
- lib/paper_trail-human.rb
|
|
54
54
|
- lib/paper_trail/human.rb
|
|
55
|
+
- lib/paper_trail/human/adapters/formatters.rb
|
|
55
56
|
- lib/paper_trail/human/adapters/formatters/html.rb
|
|
56
57
|
- lib/paper_trail/human/adapters/formatters/markdown.rb
|
|
57
58
|
- lib/paper_trail/human/adapters/formatters/text.rb
|
|
59
|
+
- lib/paper_trail/human/adapters/resolvers.rb
|
|
58
60
|
- lib/paper_trail/human/adapters/resolvers/boolean.rb
|
|
59
61
|
- lib/paper_trail/human/adapters/resolvers/custom.rb
|
|
60
62
|
- lib/paper_trail/human/adapters/resolvers/date.rb
|