model_timeline 0.1.0 → 0.1.2

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: 7406b06646ef46c0b8d2541835ebeaa5e0f71638c600d0b7de45903bb36b2c81
4
- data.tar.gz: dad655ebda19102a5e09eb3a51d07ac15cfbca051527ee3c3a71d979ab2a4543
3
+ metadata.gz: 9d1c5aacdbe6970e7724f4f40d81c2e67b0f7739b7efe4784adb773a88d142a4
4
+ data.tar.gz: 38fc7b5502adf1a18809f8fd7aa54d3f5906888a9e37ed3f6b8fa9ef232e893c
5
5
  SHA512:
6
- metadata.gz: 4f618bea0cc20328d5d2a61352c753ec39fbe04f90b7f909ba93df236ab28a4e239515c3b0b70913381149e99996c189ebb41afe5e2a2d6649e3fdb0fabf24b4
7
- data.tar.gz: d145d2798077f4bbb13191f966d8b73433330e5360b808862f14ddc3f1e4d7e6d97d231ad9e0828a5f63f4f759453550eb5cb132781a9a5dfa15b21b816ec36b
6
+ metadata.gz: 5710fbe04cd8871236b90891e2298098b4d986b9d63e4f0c5943af5592b0e4ee47bf248490f2f508992a3a39ea50f20839f11d5b043196cbb8e6a527925d6d1c
7
+ data.tar.gz: 584e6aeb8875bffd927d86532e0b05cb2989027df80b6b5cf0ed15d587bfdce1f65f9f5cb6a92b8a88dc7e416ac56c653604d6f9635a6dc91a53ab4f97046192
data/README.md CHANGED
@@ -127,7 +127,6 @@ end
127
127
 
128
128
  ### Using Metadata
129
129
 
130
- <!-- ! THIS IS WRONG, NEEDS TO BE UPDATED. -->
131
130
  ModelTimeline allows you to include custom metadata with your timeline entries, which is especially useful for tracking changes across related entities or adding domain-specific context.
132
131
 
133
132
  #### Adding Metadata Through Configuration
@@ -147,8 +146,7 @@ end
147
146
  ```
148
147
 
149
148
  If your timeline table has columns that match the keys in your `meta` hash, these values will be stored in
150
- those dedicated columns. Otherwise they will be ignored. (This might change in the future and a metadata column might be
151
- added to put additional metadata that doesn't have a dedicated column.)
149
+ those dedicated columns. Otherwise, they will be stored inside `metadata` column.
152
150
 
153
151
  #### Adding Metadata at Runtime
154
152
 
@@ -172,13 +170,16 @@ For tracking related entities more effectively, you can create a custom timeline
172
170
  class CreatePostTimelineEntries < ActiveRecord::Migration[6.1]
173
171
  def change
174
172
  create_table :post_timeline_entries do |t|
175
- # Standard ModelTimeline columns
176
- t.string :timelineable_type
177
- t.integer :timelineable_id
178
- t.string :action
179
- t.jsonb :object_changes
180
- t.integer :user_id
181
- t.string :ip_address
173
+ # Default Columns - All of them are required.
174
+ t.string :timelineable_type
175
+ t.bigint :timelineable_id
176
+ t.string :action, null: false
177
+ t.jsonb :object_changes, default: {}, null: false
178
+ t.jsonb :metadata, default: {}, null: false
179
+ t.string :user_type
180
+ t.bigint :user_id
181
+ t.string :username
182
+ t.inet :ip_address
182
183
 
183
184
  # Custom columns that can be populated via the meta option
184
185
  t.integer :post_id
@@ -186,9 +187,12 @@ class CreatePostTimelineEntries < ActiveRecord::Migration[6.1]
186
187
  t.timestamps
187
188
  end
188
189
 
189
- add_index :post_timeline_entries, [:timelineable_type, :timelineable_id]
190
- add_index :post_timeline_entries, :post_id
191
- add_index :post_timeline_entries, :user_id
190
+ add_index :post_timeline_entries, [:timelineable_type, :timelineable_id], name: 'idx_timeline_on_timelineable'
191
+ add_index :post_timeline_entries, [:user_type, :user_id], name: 'idx_timeline_on_user'
192
+ add_index :post_timeline_entries, :object_changes, using: :gin, name: 'idx_timeline_on_changes'
193
+ add_index :post_timeline_entries, :metadata, using: :gin, name: 'idx_timeline_on_meta'
194
+ add_index :post_timeline_entries, :ip_address, name: 'idx_timeline_on_ip'
195
+ add_index :post_timeline_entries, :post_id, name: 'idx_timeline_on_post_id'
192
196
  end
193
197
  end
194
198
  ```
@@ -381,11 +385,6 @@ Configure RSpec to work with ModelTimeline:
381
385
  require 'model_timeline/rspec'
382
386
 
383
387
  RSpec.configure do |config|
384
- # This disables ModelTimeline by default in tests for better performance
385
- config.before(:suite) do
386
- ModelTimeline.disable!
387
- end
388
-
389
388
  # Include the RSpec helpers and matchers
390
389
  config.include ModelTimeline::RSpec
391
390
  end
@@ -436,17 +435,65 @@ expect(user).to have_timeline_entries
436
435
  expect(user).to have_timeline_entries(3)
437
436
 
438
437
  # Check for entries with a specific action
439
- expect(user).to have_timelined_action(:update)
438
+ expect(user).to have_timeline_entry_action(:update)
440
439
 
441
440
  # Check if a specific attribute was changed
442
- expect(user).to have_timelined_change(:email)
441
+ expect(user).to have_timeline_entry_change(:email)
443
442
 
444
443
  # Check if an attribute was changed to a specific value
445
- expect(user).to have_timelined_entry(:status, 'active')
444
+ expect(user).to have_timeline_entry(:status, 'active')
445
+
446
+ # Check if an entry was created with expected metadata
447
+ expect(user).to have_timeline_entry_metadata(foo: 'bar', baz: 'biz')
446
448
  ```
447
449
 
448
450
  These matchers make it easy to test that your application is correctly tracking model changes.
449
451
 
452
+ ##### Matchers for custom models/tables
453
+
454
+ You can add a configuration in your support file to create matchers for your association.
455
+ Given a model like this:
456
+
457
+ ```ruby
458
+ class User < ApplicationRecord
459
+ has_timeline :security_events, class_name: 'SecurityTimelineEntry'
460
+ end
461
+ ```
462
+
463
+ You should set a configuration in your RSpec support file with:
464
+ ```ruby
465
+ # spec/support/model_timeline.rb
466
+ require 'model_timeline/rspec'
467
+
468
+ RSpec.configure do |config|
469
+ # Include the RSpec helpers and matchers
470
+ config.include ModelTimeline::RSpec
471
+ config.include ModelTimeline::RSpec::Matchers.define_timeline_matchers_for(:security_events)
472
+ end
473
+ ```
474
+
475
+ Then you have those new matchers:
476
+
477
+ ```ruby
478
+ # Check for any timeline entries
479
+ expect(user).to have_security_events
480
+
481
+ # Check for a specific number of entries
482
+ expect(user).to have_security_events(3)
483
+
484
+ # Check for entries with a specific action
485
+ expect(user).to have_security_event_action(:update)
486
+
487
+ # Check if a specific attribute was changed
488
+ expect(user).to have_security_event_change(:email)
489
+
490
+ # Check if an attribute was changed to a specific value
491
+ expect(user).to have_security_event(:status, 'active')
492
+
493
+ # Check if an entry was created with expected metadata
494
+ expect(user).to have_security_event_metadata(foo: 'bar', baz: 'biz')
495
+ ```
496
+
450
497
 
451
498
  ## License
452
499
 
@@ -7,6 +7,7 @@ class CreateModelTimelineTables < ActiveRecord::Migration[<%= ActiveRecord::Migr
7
7
 
8
8
  # Use PostgreSQL's JSONB type for better performance
9
9
  t.jsonb :object_changes, default: {}, null: false
10
+ t.jsonb :metadata, default: {}, null: false
10
11
 
11
12
  # Polymorphic user association
12
13
  t.string :user_type
@@ -22,6 +23,7 @@ class CreateModelTimelineTables < ActiveRecord::Migration[<%= ActiveRecord::Migr
22
23
  add_index :<%= @table_name %>, [:timelineable_type, :timelineable_id], name: 'idx_timeline_on_timelineable'
23
24
  add_index :<%= @table_name %>, [:user_type, :user_id], name: 'idx_timeline_on_user'
24
25
  add_index :<%= @table_name %>, :object_changes, using: :gin, name: 'idx_timeline_on_changes'
26
+ add_index :<%= @table_name %>, :metadata, using: :gin, name: 'idx_timeline_on_meta'
25
27
  add_index :<%= @table_name %>, :ip_address, name: 'idx_timeline_on_ip'
26
28
  end
27
29
  end
@@ -2,7 +2,6 @@
2
2
 
3
3
  module ModelTimeline
4
4
  module RSpec
5
- # rubocop:disable Naming/PredicateName
6
5
  # Custom RSpec matchers for testing model timeline entries
7
6
  #
8
7
  # These matchers help you test the timeline entries created by the ModelTimeline gem.
@@ -16,58 +15,78 @@ module ModelTimeline
16
15
  # expect(user).to have_timeline_entries(3)
17
16
  #
18
17
  # # Check for specific action
19
- # expect(user).to have_timelined_action(:update)
18
+ # expect(user).to have_timeline_entry_action(:update)
20
19
  #
21
20
  # # Check for changes to a specific attribute
22
- # expect(user).to have_timelined_change(:email)
21
+ # expect(user).to have_timeline_entry_change(:email)
23
22
  #
24
23
  # # Check for specific value change
25
- # expect(user).to have_timelined_entry(:status, "active")
24
+ # expect(user).to have_timeline_entry(:status, "active")
26
25
  module Matchers
27
- # Check if a model has timeline entries
28
- #
29
- # @param count [Integer, nil] The expected number of timeline entries (optional)
30
- # @return [HaveTimelineEntriesMatcher] A matcher that checks for timeline entries
31
- # @example Without count parameter
32
- # expect(user).to have_timeline_entries
33
- # @example With count parameter
34
- # expect(user).to have_timeline_entries(3)
35
- def have_timeline_entries(count = nil)
36
- HaveTimelineEntriesMatcher.new(count)
26
+ def self.included(base)
27
+ base.include define_timeline_matchers_for(:timeline_entries)
37
28
  end
38
29
 
39
- # Check if a specific action was recorded in the timeline
40
- #
41
- # @param action [String, Symbol] The action name to look for
42
- # @return [HaveTimelinedAction] A matcher that checks for a specific action
43
- # @example
44
- # expect(user).to have_timelined_action(:create)
45
- # expect(user).to have_timelined_action("update")
46
- def have_timelined_action(action)
47
- HaveTimelinedAction.new(action)
48
- end
30
+ def self.define_timeline_matchers_for(association_name)
31
+ association_name_singularized = association_name.to_s.singularize
49
32
 
50
- # Check if a specific attribute change was recorded in the timeline
51
- #
52
- # @param attribute [String, Symbol] The attribute name to check for changes
53
- # @return [HaveTimelinedChange] A matcher that checks if an attribute was changed
54
- # @example
55
- # expect(user).to have_timelined_change(:email)
56
- # expect(user).to have_timelined_change("status")
57
- def have_timelined_change(attribute)
58
- HaveTimelinedChange.new(attribute)
59
- end
33
+ Module.new do
34
+ # Check if a model has timeline entries
35
+ #
36
+ # @param count [Integer, nil] The expected number of timeline entries (optional)
37
+ # @return [HaveTimelineEntriesMatcher] A matcher that checks for timeline entries
38
+ # @example Without count parameter
39
+ # expect(user).to have_timeline_entries
40
+ # @example With count parameter
41
+ # expect(user).to have_timeline_entries(3)
42
+ define_method(:"have_#{association_name}") do |count = nil|
43
+ HaveTimelineEntriesMatcher.new(count, association_name)
44
+ end
60
45
 
61
- # Check if an attribute was changed to a specific value in the timeline
62
- #
63
- # @param attribute [String, Symbol] The attribute name to check
64
- # @param value [Object] The value to check for
65
- # @return [HaveTimelinedEntry] A matcher that checks for specific attribute values
66
- # @example
67
- # expect(user).to have_timelined_entry(:status, "active")
68
- # expect(user).to have_timelined_entry(:role, :admin)
69
- def have_timelined_entry(attribute, value)
70
- HaveTimelinedEntry.new(attribute, value)
46
+ # Check if a specific action was recorded in the timeline
47
+ #
48
+ # @param action [String, Symbol] The action name to look for
49
+ # @return [HaveTimelineAction] A matcher that checks for a specific action
50
+ # @example
51
+ # expect(user).to have_timeline_entry_action(:create)
52
+ # expect(user).to have_timeline_entry_action("update")
53
+ define_method(:"have_#{association_name_singularized}_action") do |action|
54
+ HaveTimelineAction.new(action, association_name)
55
+ end
56
+
57
+ # Check if a specific attribute change was recorded in the timeline
58
+ #
59
+ # @param attribute [String, Symbol] The attribute name to check for changes
60
+ # @return [HaveTimelineChange] A matcher that checks if an attribute was changed
61
+ # @example
62
+ # expect(user).to have_timeline_entry_change(:email)
63
+ # expect(user).to have_timeline_entry_change("status")
64
+ define_method(:"have_#{association_name_singularized}_change") do |attribute|
65
+ HaveTimelineChange.new(attribute, association_name)
66
+ end
67
+
68
+ # Check if an attribute was changed to a specific value in the timeline
69
+ #
70
+ # @param attribute [String, Symbol] The attribute name to check
71
+ # @param value [Object] The value to check for
72
+ # @return [HaveTimelineEntry] A matcher that checks for specific attribute values
73
+ # @example
74
+ # expect(user).to have_timeline_entry(:status, "active")
75
+ # expect(user).to have_timeline_entry(:role, :admin)
76
+ define_method(:"have_#{association_name_singularized}") do |attribute, value|
77
+ HaveTimelineEntry.new(attribute, value, association_name)
78
+ end
79
+
80
+ # Check if a model has timeline entries with specific metadata
81
+ #
82
+ # @param expected_metadata [Hash] The metadata key-value pairs to check for
83
+ # @return [HaveTimelineEntryMetadata] A matcher that checks for specific metadata
84
+ # @example
85
+ # expect(user).to have_timeline_entry_metadata(foo: 'bar', baz: 'biz')
86
+ define_method(:"have_#{association_name_singularized}_metadata") do |expected_metadata|
87
+ HaveTimelineEntryMetadata.new(expected_metadata, association_name)
88
+ end
89
+ end
71
90
  end
72
91
 
73
92
  # RSpec matcher to check if a model has timeline entries
@@ -77,8 +96,9 @@ module ModelTimeline
77
96
  # Initialize the matcher
78
97
  #
79
98
  # @param expected_count [Integer, nil] The expected number of timeline entries
80
- def initialize(expected_count)
99
+ def initialize(expected_count, association_name)
81
100
  @expected_count = expected_count
101
+ @association_name = association_name
82
102
  end
83
103
 
84
104
  # Check if the subject matches the expectations
@@ -88,9 +108,9 @@ module ModelTimeline
88
108
  def matches?(subject)
89
109
  @subject = subject
90
110
  if @expected_count.nil?
91
- subject.timeline_entries.any?
111
+ subject.public_send(@association_name).any?
92
112
  else
93
- subject.timeline_entries.count == @expected_count
113
+ subject.public_send(@association_name).count == @expected_count
94
114
  end
95
115
  end
96
116
 
@@ -102,7 +122,7 @@ module ModelTimeline
102
122
  "expected #{@subject} to have timeline entries, but found none"
103
123
  else
104
124
  "expected #{@subject} to have #{@expected_count} timeline entries, " \
105
- "but found #{@subject.timeline_entries.count}"
125
+ "but found #{@subject.public_send(@association_name).count}"
106
126
  end
107
127
  end
108
128
 
@@ -111,7 +131,7 @@ module ModelTimeline
111
131
  # @return [String] A descriptive failure message for negated expectations
112
132
  def failure_message_when_negated
113
133
  if @expected_count.nil?
114
- "expected #{@subject} not to have any timeline entries, but found #{@subject.timeline_entries.count}"
134
+ "expected #{@subject} not to have any timeline entries, but found #{@subject.public_send(@association_name).count}"
115
135
  else
116
136
  "expected #{@subject} not to have #{@expected_count} timeline entries, but found exactly that many"
117
137
  end
@@ -121,12 +141,13 @@ module ModelTimeline
121
141
  # RSpec matcher to check if a model has timeline entries with a specific action
122
142
  #
123
143
  # @api private
124
- class HaveTimelinedAction
144
+ class HaveTimelineAction
125
145
  # Initialize the matcher
126
146
  #
127
147
  # @param action [String, Symbol] The action to look for
128
- def initialize(action)
148
+ def initialize(action, association_name)
129
149
  @action = action.to_s
150
+ @association_name = association_name
130
151
  end
131
152
 
132
153
  # Check if the subject matches the expectations
@@ -135,7 +156,7 @@ module ModelTimeline
135
156
  # @return [Boolean] True if the model has timeline entries with the specified action
136
157
  def matches?(subject)
137
158
  @subject = subject
138
- subject.timeline_entries.where(action: @action).exists?
159
+ @subject.public_send(@association_name).where(action: @action).exists?
139
160
  end
140
161
 
141
162
  # Message displayed when the expectation fails
@@ -156,12 +177,13 @@ module ModelTimeline
156
177
  # RSpec matcher to check if a model has timeline entries with changes to a specific attribute
157
178
  #
158
179
  # @api private
159
- class HaveTimelinedChange
180
+ class HaveTimelineChange
160
181
  # Initialize the matcher
161
182
  #
162
183
  # @param attribute [String, Symbol] The attribute to check for changes
163
- def initialize(attribute)
184
+ def initialize(attribute, association_name)
164
185
  @attribute = attribute.to_s
186
+ @association_name = association_name
165
187
  end
166
188
 
167
189
  # Check if the subject matches the expectations
@@ -170,7 +192,7 @@ module ModelTimeline
170
192
  # @return [Boolean] True if the model has timeline entries with changes to the specified attribute
171
193
  def matches?(subject)
172
194
  @subject = subject
173
- subject.timeline_entries.with_changed_attribute(@attribute).exists?
195
+ @subject.public_send(@association_name).with_changed_attribute(@attribute).exists?
174
196
  end
175
197
 
176
198
  # Message displayed when the expectation fails
@@ -191,14 +213,15 @@ module ModelTimeline
191
213
  # RSpec matcher to check if a model has timeline entries where an attribute changed to a specific value
192
214
  #
193
215
  # @api private
194
- class HaveTimelinedEntry
216
+ class HaveTimelineEntry
195
217
  # Initialize the matcher
196
218
  #
197
219
  # @param attribute [String, Symbol] The attribute to check
198
220
  # @param value [Object] The value the attribute should have changed to
199
- def initialize(attribute, value)
221
+ def initialize(attribute, value, association_name)
200
222
  @attribute = attribute.to_s
201
223
  @value = value
224
+ @association_name = association_name
202
225
  end
203
226
 
204
227
  # Check if the subject matches the expectations
@@ -207,7 +230,7 @@ module ModelTimeline
207
230
  # @return [Boolean] True if the model has timeline entries where the attribute changed to the specified value
208
231
  def matches?(subject)
209
232
  @subject = subject
210
- subject.timeline_entries.with_changed_value(@attribute, @value).exists?
233
+ @subject.public_send(@association_name).with_changed_value(@attribute, @value).exists?
211
234
  end
212
235
 
213
236
  # Message displayed when the expectation fails
@@ -224,7 +247,52 @@ module ModelTimeline
224
247
  "expected #{@subject} not to have tracked '#{@attribute}' changing to '#{@value}', but such a change was found"
225
248
  end
226
249
  end
250
+
251
+ # RSpec matcher to check if a model has timeline entries with specific metadata
252
+ #
253
+ # @api private
254
+ class HaveTimelineEntryMetadata
255
+ # Initialize the matcher
256
+ #
257
+ # @param expected_metadata [Hash] The metadata key-value pairs to check for
258
+ # @param association_name [Symbol] The name of the timeline association
259
+ def initialize(expected_metadata, association_name)
260
+ @expected_metadata = expected_metadata
261
+ @association_name = association_name
262
+ end
263
+
264
+ # Check if the subject matches the expectations
265
+ #
266
+ # @param subject [Object] The model to check for timeline entries
267
+ # @return [Boolean] True if the model has timeline entries with the specified metadata
268
+ def matches?(subject)
269
+ @subject = subject
270
+
271
+ # Build a query that checks for each key-value pair in the metadata
272
+ entries = subject.public_send(@association_name)
273
+
274
+ # Construct queries for each metadata key-value pair
275
+ @expected_metadata.all? do |key, value|
276
+ # Use a JSON containment query to check if the metadata contains the key-value pair
277
+ # This syntax works with PostgreSQL's JSONB containment operator @>
278
+ entries.where('metadata @> ?', { key.to_s => value }.to_json).exists?
279
+ end
280
+ end
281
+
282
+ # Message displayed when the expectation fails
283
+ #
284
+ # @return [String] A descriptive failure message
285
+ def failure_message
286
+ "expected #{@subject} to have timeline entries with metadata #{@expected_metadata.inspect}, but none was found"
287
+ end
288
+
289
+ # Message displayed when the negated expectation fails
290
+ #
291
+ # @return [String] A descriptive failure message for negated expectations
292
+ def failure_message_when_negated
293
+ "expected #{@subject} not to have timeline entries with metadata #{@expected_metadata.inspect}, but such entries were found"
294
+ end
295
+ end
227
296
  end
228
- # rubocop:enable Naming/PredicateName
229
297
  end
230
298
  end
@@ -20,7 +20,7 @@ module ModelTimeline
20
20
  # describe User, :with_timeline do
21
21
  # it "records timeline entries when updated" do
22
22
  # user.update(name: "New Name")
23
- # expect(user).to have_timelined_change(:name)
23
+ # expect(user).to have_timeline_entry_change(:name)
24
24
  # end
25
25
  # end
26
26
  module RSpec
@@ -107,9 +107,10 @@ module ModelTimeline
107
107
  timelineable_id: id,
108
108
  action: action,
109
109
  object_changes: object_changes,
110
+ metadata: object_metadata(config),
110
111
  ip_address: current_ip_address,
111
112
  **current_user_attributes,
112
- **collect_metadata(config[:meta], config_key)
113
+ **column_metadata(config)
113
114
  )
114
115
  end
115
116
 
@@ -128,9 +129,10 @@ module ModelTimeline
128
129
  timelineable_id: id,
129
130
  action: 'destroy',
130
131
  object_changes: {},
132
+ metadata: object_metadata(config),
131
133
  ip_address: current_ip_address,
132
134
  **current_user_attributes,
133
- **collect_metadata(config[:meta], config_key)
135
+ **column_metadata(config)
134
136
  )
135
137
  end
136
138
 
@@ -183,32 +185,64 @@ module ModelTimeline
183
185
 
184
186
  # Collects metadata for the timeline entry
185
187
  #
186
- # @param meta_config [Hash] The metadata configuration
187
- # @param config_key [String] The configuration key for this model
188
+ # @param meta_config [Hash, Proc] The metadata configuration
188
189
  # @return [Hash] Collected metadata
189
- def collect_metadata(meta_config, config_key)
190
- config = self.class.loggers[config_key]
190
+ def collect_metadata(meta_config)
191
191
  metadata = {}
192
192
 
193
193
  # First, add any thread-level metadata
194
194
  metadata.merge!(ModelTimeline.metadata)
195
195
 
196
- # Then, add any model-specific metadata defined in config
197
- meta_config.each do |key, value|
198
- resolved_value = case value
199
- when Proc
200
- instance_exec(self, &value)
201
- when Symbol
202
- respond_to?(value) ? send(value) : value
203
- else
204
- value
205
- end
206
- metadata[key] = resolved_value
196
+ # Then, add any model-specific metadata from config
197
+ metadata.merge!(resolve_metadata_value(meta_config))
198
+
199
+ metadata
200
+ end
201
+
202
+ # Recursively resolves metadata values
203
+ #
204
+ # @param value [Hash, Proc, Symbol, Object] The value to resolve
205
+ # @return [Hash, Object] The resolved value
206
+ def resolve_metadata_value(value)
207
+ case value
208
+ when Proc
209
+ # If it's a Proc, execute it and process the result recursively
210
+ proc_result = instance_exec(self, &value)
211
+ proc_result.is_a?(Hash) ? process_metadata_hash(proc_result) : proc_result
212
+ when Hash
213
+ # If it's a Hash, process each key-value pair
214
+ process_metadata_hash(value)
215
+ when Symbol
216
+ # If it's a Symbol, try to call the method
217
+ respond_to?(value) ? send(value) : value
218
+ else
219
+ # Any other value, return as-is
220
+ value
207
221
  end
222
+ end
208
223
 
209
- # Only include keys that exist as columns in the timeline entry table
224
+ # Processes a metadata hash by resolving each value
225
+ #
226
+ # @param hash [Hash] The hash to process
227
+ # @return [Hash] The processed hash
228
+ def process_metadata_hash(hash)
229
+ result = {}
230
+ hash.each do |key, val|
231
+ result[key] = resolve_metadata_value(val)
232
+ end
233
+ result
234
+ end
235
+
236
+ def column_metadata(config)
237
+ metadata = collect_metadata(config[:meta])
210
238
  column_names = config[:klass].column_names.map(&:to_sym)
211
239
  metadata.slice(*column_names)
212
240
  end
241
+
242
+ def object_metadata(config)
243
+ metadata = collect_metadata(config[:meta])
244
+ column_names = config[:klass].column_names.map(&:to_sym)
245
+ metadata.except(*column_names)
246
+ end
213
247
  end
214
248
  end
@@ -5,5 +5,5 @@ module ModelTimeline
5
5
  # Follows semantic versioning (https://semver.org/).
6
6
  #
7
7
  # @return [String] The current version in the format "MAJOR.MINOR.PATCH"
8
- VERSION = '0.1.0'
8
+ VERSION = '0.1.2'
9
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: model_timeline
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexandre Stapenhorst
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-06-03 00:00:00.000000000 Z
11
+ date: 2025-06-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg