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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f7c20298a4e2c9c68d4fe4b9a4e403b571fe3590d8970b32fd9cf20a8a902df9
4
- data.tar.gz: 7f0e47633690bed45da988a5ff96f26cb9c83e61635710c27e15923da25322ce
3
+ metadata.gz: 4dea24fd23784bda5b3b0f6d704f5f3a53a2ab00c8ed8500fe69a1b432eb3f47
4
+ data.tar.gz: 6c074d6f13efd43f93e1c201e36d9310420095c516980bc80c74dc6116551105
5
5
  SHA512:
6
- metadata.gz: faa5dd4c0f7d9b3598656b603225bab94fbcabe1e3fdc2a997a70e4357bfcf040508aa1c332dd0b13ca64b3ee8f6869f30df4a2e2a6edfc49e6607d602aa32e2
7
- data.tar.gz: 319ccffdef16b6a533e7ec47ba422c2cc14d5a35dd75e8127064b0b5d523df15f682b269df89b8319cfe376722dbd0b3dec5b65b9bbb95f48e12cabbf15ddad8
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
@@ -45,7 +45,7 @@ module PaperTrail
45
45
  end
46
46
 
47
47
  def format_currency(num)
48
- @unit ? "#{@unit} #{format_number(num)}" : format_number(num)
48
+ "#{@unit} #{format_number(num)}"
49
49
  end
50
50
 
51
51
  def format_percentage(num)
@@ -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 = @relation_loader.preload(versions_data)
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, item_names, only: only, except: except)
17
+ format_version(version, changes, preloaded, only: only, except: except)
21
18
  end
22
19
  end
23
20
 
24
21
  private
25
22
 
26
- # rubocop:disable Metrics/ParameterLists
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, version.item_type,
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
- def base_result(version, changes, formatter, only: nil, except: nil)
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' then result.delete(:previous_value)
78
- when 'destroy' then result.delete(:value)
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
- class_name = RESOLVER_MAP[config[:type]]
47
- raise Error, "Unknown resolver type: #{config[:type]}" unless class_name
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
- model_config = @configuration.config_for(version.item_type)
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) }
@@ -2,6 +2,6 @@
2
2
 
3
3
  module PaperTrail
4
4
  module Human
5
- VERSION = '0.3.1'
5
+ VERSION = '0.4.0'
6
6
  end
7
7
  end
@@ -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/relation'
15
- require_relative 'human/adapters/resolvers/enum'
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
- return results unless as
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
- klass = FORMATTERS[type.to_sym]
74
- raise Error, "Unknown format: #{type}. Available: #{FORMATTERS.keys.join(', ')}" unless klass
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.3.1
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