paper_trail-human 0.3.0 → 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 +21 -0
- data/lib/paper_trail/human/adapters/formatters.rb +13 -0
- 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 +2 -1
- data/lib/paper_trail/human/core/field_formatter.rb +12 -4
- data/lib/paper_trail/human/core/item_name_loader.rb +53 -0
- data/lib/paper_trail/human/core/presenter.rb +10 -6
- data/lib/paper_trail/human/core/relation_loader.rb +61 -0
- data/lib/paper_trail/human/version.rb +1 -1
- data/lib/paper_trail/human.rb +14 -19
- metadata +5 -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,26 @@
|
|
|
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
|
+
|
|
14
|
+
## [0.3.1] - 2026-05-31
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- N+1 queries on `item_name` in `format_collection` and `timeline` (batch preloads records)
|
|
18
|
+
- `Number` resolver with `format: :currency` and `unit: nil` no longer produces leading space
|
|
19
|
+
- `format_collection` with `as:` option now joins entries with separators (double newline for text/markdown, newline for HTML)
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Extracted `ItemNameLoader` and `RelationLoader` from `BatchPresenter` for better cohesion
|
|
23
|
+
|
|
3
24
|
## [0.3.0] - 2026-05-30
|
|
4
25
|
|
|
5
26
|
### Added
|
|
@@ -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
|
|
@@ -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
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaperTrail
|
|
4
|
+
module Human
|
|
5
|
+
module Core
|
|
6
|
+
class ItemNameLoader
|
|
7
|
+
def initialize(configuration)
|
|
8
|
+
@configuration = configuration
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def preload(versions)
|
|
12
|
+
grouped = group_batchable(versions)
|
|
13
|
+
|
|
14
|
+
grouped.each_with_object({}) do |(item_type, ids), cache|
|
|
15
|
+
load_names(item_type, ids, cache)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def resolve(version, cache)
|
|
20
|
+
model_config = @configuration.config_for(version.item_type)
|
|
21
|
+
return nil unless model_config&.item_name_attribute
|
|
22
|
+
|
|
23
|
+
attr = model_config.item_name_attribute
|
|
24
|
+
return attr.call(version) if attr.respond_to?(:call)
|
|
25
|
+
|
|
26
|
+
cache[[version.item_type, version.item_id]]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def group_batchable(versions)
|
|
32
|
+
versions.each_with_object({}) do |version, grouped|
|
|
33
|
+
model_config = @configuration.config_for(version.item_type)
|
|
34
|
+
next unless model_config&.item_name_attribute
|
|
35
|
+
next if model_config.item_name_attribute.respond_to?(:call)
|
|
36
|
+
|
|
37
|
+
(grouped[version.item_type] ||= Set.new).add(version.item_id)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def load_names(item_type, ids, cache)
|
|
42
|
+
attr = @configuration.config_for(item_type).item_name_attribute
|
|
43
|
+
klass = Object.const_get(item_type)
|
|
44
|
+
klass.where(id: ids.to_a).each do |record|
|
|
45
|
+
cache[[item_type, record.id]] = record.public_send(attr)
|
|
46
|
+
end
|
|
47
|
+
rescue NameError
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -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) }
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaperTrail
|
|
4
|
+
module Human
|
|
5
|
+
module Core
|
|
6
|
+
class RelationLoader
|
|
7
|
+
def initialize(configuration)
|
|
8
|
+
@configuration = configuration
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def preload(versions_data)
|
|
12
|
+
relation_fields = collect_relation_fields(versions_data)
|
|
13
|
+
return {} if relation_fields.empty?
|
|
14
|
+
|
|
15
|
+
relation_fields.each_with_object({}) do |(key, ids), cache|
|
|
16
|
+
class_name, attribute = key
|
|
17
|
+
cache[key] = load_records(class_name, attribute, ids)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def collect_relation_fields(versions_data)
|
|
24
|
+
result = Hash.new { |h, k| h[k] = Set.new }
|
|
25
|
+
|
|
26
|
+
versions_data.each do |version, changes|
|
|
27
|
+
collect_from_version(result, version, changes)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
result
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def collect_from_version(result, version, changes)
|
|
34
|
+
model_config = @configuration.config_for(version.item_type)
|
|
35
|
+
return unless model_config
|
|
36
|
+
|
|
37
|
+
changes.each do |field, values|
|
|
38
|
+
field_cfg = model_config.fields[field.to_s]
|
|
39
|
+
next unless field_cfg && field_cfg[:type] == :relation
|
|
40
|
+
|
|
41
|
+
key = relation_key(field_cfg)
|
|
42
|
+
Array(values).compact.each { |v| result[key].add(v) }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def relation_key(field_cfg)
|
|
47
|
+
class_name = field_cfg[:options][:class_name] || field_cfg[:options][:class].to_s
|
|
48
|
+
attribute = field_cfg[:options][:attribute] || :name
|
|
49
|
+
[class_name, attribute]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def load_records(class_name, attribute, ids)
|
|
53
|
+
klass = Object.const_get(class_name)
|
|
54
|
+
klass.where(id: ids.to_a).to_h { |record| [record.id, record.public_send(attribute)] }
|
|
55
|
+
rescue NameError
|
|
56
|
+
{}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/paper_trail/human.rb
CHANGED
|
@@ -9,16 +9,8 @@ require_relative 'human/core/presenter'
|
|
|
9
9
|
require_relative 'human/core/batch_presenter'
|
|
10
10
|
require_relative 'human/core/timeline'
|
|
11
11
|
require_relative 'human/ports/resolver'
|
|
12
|
-
require_relative 'human/adapters/resolvers
|
|
13
|
-
require_relative 'human/adapters/
|
|
14
|
-
require_relative 'human/adapters/resolvers/boolean'
|
|
15
|
-
require_relative 'human/adapters/resolvers/custom'
|
|
16
|
-
require_relative 'human/adapters/resolvers/text'
|
|
17
|
-
require_relative 'human/adapters/resolvers/date'
|
|
18
|
-
require_relative 'human/adapters/resolvers/number'
|
|
19
|
-
require_relative 'human/adapters/formatters/text'
|
|
20
|
-
require_relative 'human/adapters/formatters/markdown'
|
|
21
|
-
require_relative 'human/adapters/formatters/html'
|
|
12
|
+
require_relative 'human/adapters/resolvers'
|
|
13
|
+
require_relative 'human/adapters/formatters'
|
|
22
14
|
|
|
23
15
|
module PaperTrail
|
|
24
16
|
module Human
|
|
@@ -28,9 +20,9 @@ module PaperTrail
|
|
|
28
20
|
private_constant :MUTEX
|
|
29
21
|
|
|
30
22
|
FORMATTERS = {
|
|
31
|
-
text: Adapters::Formatters::Text,
|
|
32
|
-
markdown: Adapters::Formatters::Markdown,
|
|
33
|
-
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'
|
|
34
26
|
}.freeze
|
|
35
27
|
private_constant :FORMATTERS
|
|
36
28
|
|
|
@@ -57,18 +49,21 @@ module PaperTrail
|
|
|
57
49
|
as ? results.map { |r| formatter(as).call(r) } : results
|
|
58
50
|
end
|
|
59
51
|
|
|
52
|
+
def timeline(versions, group_by: :day, only: nil, except: nil)
|
|
53
|
+
Core::Timeline.new(configuration).call(versions, group_by: group_by, only: only, except: except)
|
|
54
|
+
end
|
|
55
|
+
|
|
60
56
|
private
|
|
61
57
|
|
|
62
58
|
def formatter(type)
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
65
63
|
|
|
64
|
+
klass = Object.const_get(klass) if klass.is_a?(String)
|
|
66
65
|
klass.new
|
|
67
66
|
end
|
|
68
|
-
|
|
69
|
-
def timeline(versions, group_by: :day, only: nil, except: nil)
|
|
70
|
-
Core::Timeline.new(configuration).call(versions, group_by: group_by, only: only, except: except)
|
|
71
|
-
end
|
|
72
67
|
end
|
|
73
68
|
end
|
|
74
69
|
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
|
|
@@ -67,7 +69,9 @@ files:
|
|
|
67
69
|
- lib/paper_trail/human/core/change_extractor.rb
|
|
68
70
|
- lib/paper_trail/human/core/event_translator.rb
|
|
69
71
|
- lib/paper_trail/human/core/field_formatter.rb
|
|
72
|
+
- lib/paper_trail/human/core/item_name_loader.rb
|
|
70
73
|
- lib/paper_trail/human/core/presenter.rb
|
|
74
|
+
- lib/paper_trail/human/core/relation_loader.rb
|
|
71
75
|
- lib/paper_trail/human/core/timeline.rb
|
|
72
76
|
- lib/paper_trail/human/ports/resolver.rb
|
|
73
77
|
- lib/paper_trail/human/railtie.rb
|