familia 2.0.0.pre5 → 2.0.0.pre7
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/.github/workflows/claude-code-review.yml +57 -0
- data/.github/workflows/claude.yml +71 -0
- data/.gitignore +5 -1
- data/.rubocop.yml +3 -0
- data/CLAUDE.md +32 -10
- data/Gemfile +2 -2
- data/Gemfile.lock +4 -3
- data/docs/wiki/API-Reference.md +95 -18
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
- data/docs/wiki/Expiration-Feature-Guide.md +596 -0
- data/docs/wiki/Feature-System-Guide.md +631 -0
- data/docs/wiki/Features-System-Developer-Guide.md +892 -0
- data/docs/wiki/Field-System-Guide.md +784 -0
- data/docs/wiki/Home.md +82 -15
- data/docs/wiki/Implementation-Guide.md +126 -33
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/Relationships-Guide.md +684 -0
- data/docs/wiki/Security-Model.md +65 -25
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/examples/bit_encoding_integration.rb +237 -0
- data/examples/redis_command_validation_example.rb +231 -0
- data/examples/relationships_basic.rb +273 -0
- data/lib/familia/base.rb +1 -1
- data/lib/familia/connection.rb +3 -3
- data/lib/familia/data_type/types/counter.rb +38 -0
- data/lib/familia/data_type/types/hashkey.rb +18 -0
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/data_type/types/string.rb +9 -2
- data/lib/familia/data_type.rb +9 -6
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +21 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
- data/lib/familia/encryption.rb +1 -1
- data/lib/familia/errors.rb +17 -3
- data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
- data/lib/familia/features/encrypted_fields.rb +413 -4
- data/lib/familia/features/expiration.rb +319 -33
- data/lib/familia/features/quantization.rb +385 -44
- data/lib/familia/features/relationships/cascading.rb +438 -0
- data/lib/familia/features/relationships/indexing.rb +370 -0
- data/lib/familia/features/relationships/membership.rb +503 -0
- data/lib/familia/features/relationships/permission_management.rb +264 -0
- data/lib/familia/features/relationships/querying.rb +620 -0
- data/lib/familia/features/relationships/redis_operations.rb +274 -0
- data/lib/familia/features/relationships/score_encoding.rb +442 -0
- data/lib/familia/features/relationships/tracking.rb +379 -0
- data/lib/familia/features/relationships.rb +466 -0
- data/lib/familia/features/safe_dump.rb +1 -1
- data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
- data/lib/familia/features/transient_fields.rb +192 -10
- data/lib/familia/features.rb +2 -1
- data/lib/familia/field_type.rb +5 -2
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
- data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
- data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
- data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +17 -17
- data/lib/familia/validation/command_recorder.rb +336 -0
- data/lib/familia/validation/expectations.rb +519 -0
- data/lib/familia/validation/test_helpers.rb +443 -0
- data/lib/familia/validation/validator.rb +412 -0
- data/lib/familia/validation.rb +140 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +1 -1
- data/try/core/create_method_try.rb +240 -0
- data/try/core/database_consistency_try.rb +299 -0
- data/try/core/errors_try.rb +25 -4
- data/try/core/familia_try.rb +1 -1
- data/try/core/persistence_operations_try.rb +297 -0
- data/try/data_types/counter_try.rb +93 -0
- data/try/data_types/lock_try.rb +133 -0
- data/try/debugging/debug_aad_process.rb +82 -0
- data/try/debugging/debug_concealed_internal.rb +59 -0
- data/try/debugging/debug_concealed_reveal.rb +61 -0
- data/try/debugging/debug_context_aad.rb +68 -0
- data/try/debugging/debug_context_simple.rb +80 -0
- data/try/debugging/debug_cross_context.rb +62 -0
- data/try/debugging/debug_database_load.rb +64 -0
- data/try/debugging/debug_encrypted_json_check.rb +53 -0
- data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
- data/try/debugging/debug_exists_lifecycle.rb +54 -0
- data/try/debugging/debug_field_decrypt.rb +74 -0
- data/try/debugging/debug_fresh_cross_context.rb +73 -0
- data/try/debugging/debug_load_path.rb +66 -0
- data/try/debugging/debug_method_definition.rb +46 -0
- data/try/debugging/debug_method_resolution.rb +41 -0
- data/try/debugging/debug_minimal.rb +24 -0
- data/try/debugging/debug_provider.rb +68 -0
- data/try/debugging/debug_secure_behavior.rb +73 -0
- data/try/debugging/debug_string_class.rb +46 -0
- data/try/debugging/debug_test.rb +46 -0
- data/try/debugging/debug_test_design.rb +80 -0
- data/try/edge_cases/hash_symbolization_try.rb +1 -0
- data/try/edge_cases/reserved_keywords_try.rb +1 -0
- data/try/edge_cases/string_coercion_try.rb +2 -0
- data/try/encryption/encryption_core_try.rb +6 -4
- data/try/features/categorical_permissions_try.rb +515 -0
- data/try/features/encrypted_fields_core_try.rb +19 -11
- data/try/features/encrypted_fields_integration_try.rb +66 -70
- data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
- data/try/features/encrypted_fields_security_try.rb +151 -144
- data/try/features/encryption_fields/aad_protection_try.rb +108 -23
- data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
- data/try/features/encryption_fields/context_isolation_try.rb +30 -8
- data/try/features/encryption_fields/error_conditions_try.rb +6 -6
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
- data/try/features/encryption_fields/fresh_key_try.rb +27 -22
- data/try/features/encryption_fields/key_rotation_try.rb +16 -10
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +6 -6
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/relationships_edge_cases_try.rb +145 -0
- data/try/features/relationships_performance_minimal_try.rb +132 -0
- data/try/features/relationships_performance_simple_try.rb +155 -0
- data/try/features/relationships_performance_try.rb +420 -0
- data/try/features/relationships_performance_working_try.rb +144 -0
- data/try/features/relationships_try.rb +237 -0
- data/try/features/safe_dump_try.rb +3 -0
- data/try/features/transient_fields/redacted_string_try.rb +2 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
- data/try/features/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields_integration_try.rb +1 -1
- data/try/helpers/test_helpers.rb +26 -1
- data/try/horreum/base_try.rb +14 -8
- data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
- data/try/horreum/initialization_try.rb +1 -1
- data/try/horreum/relations_try.rb +2 -2
- data/try/horreum/serialization_persistent_fields_try.rb +8 -8
- data/try/horreum/serialization_try.rb +39 -4
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +1 -1
- data/try/validation/atomic_operations_try.rb.disabled +320 -0
- data/try/validation/command_validation_try.rb.disabled +207 -0
- data/try/validation/performance_validation_try.rb.disabled +324 -0
- data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
- metadata +81 -12
- data/TEST_COVERAGE.md +0 -40
- data/lib/familia/features/relatable_objects.rb +0 -125
- data/lib/familia/horreum/serialization.rb +0 -473
- data/try/features/relatable_objects_try.rb +0 -220
@@ -1,58 +1,399 @@
|
|
1
1
|
# lib/familia/features/quantization.rb
|
2
2
|
|
3
|
-
module Familia
|
4
|
-
module
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
module Familia
|
4
|
+
module Features
|
5
|
+
# Quantization is a feature that provides time-based data bucketing and quantized
|
6
|
+
# timestamp generation for Familia objects. It enables efficient time-series data
|
7
|
+
# storage, analytics aggregation, and temporal cache key generation by rounding
|
8
|
+
# timestamps to specific intervals.
|
9
|
+
#
|
10
|
+
# This feature is particularly useful for:
|
11
|
+
# - Time-series data collection and storage
|
12
|
+
# - Analytics data bucketing by time intervals
|
13
|
+
# - Cache key generation with time-based expiration
|
14
|
+
# - Log aggregation by time periods
|
15
|
+
# - Metrics collection with reduced granularity
|
16
|
+
# - Rate limiting with time windows
|
17
|
+
#
|
18
|
+
# Example:
|
19
|
+
#
|
20
|
+
# class AnalyticsEvent < Familia::Horreum
|
21
|
+
# feature :quantization
|
22
|
+
# default_expiration 1.hour # Used as default quantum
|
23
|
+
#
|
24
|
+
# identifier_field :event_id
|
25
|
+
# field :event_id, :event_type, :user_id, :data, :timestamp
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# # Generate quantized timestamps
|
29
|
+
# AnalyticsEvent.qstamp(1.hour) # => 1672531200 (rounded to hour)
|
30
|
+
# AnalyticsEvent.qstamp(1.hour, '%Y%m%d%H') # => "2023010114" (formatted)
|
31
|
+
# AnalyticsEvent.qstamp([1.hour, '%Y%m%d%H']) # => "2023010114" (array syntax)
|
32
|
+
#
|
33
|
+
# # Instance method also available
|
34
|
+
# event = AnalyticsEvent.new
|
35
|
+
# event.qstamp(15.minutes) # => 1672531800 (15-min buckets)
|
36
|
+
#
|
37
|
+
# Time Bucketing:
|
38
|
+
#
|
39
|
+
# Quantization rounds timestamps to specific intervals, creating consistent
|
40
|
+
# time buckets for data aggregation:
|
41
|
+
#
|
42
|
+
# # Current time: 2023-01-01 14:37:42
|
43
|
+
# User.qstamp(1.hour) # => 1672531200 (14:00:00)
|
44
|
+
# User.qstamp(15.minutes) # => 1672532100 (14:35:00)
|
45
|
+
# User.qstamp(1.day) # => 1672531200 (00:00:00)
|
46
|
+
#
|
47
|
+
# Formatted Timestamps:
|
48
|
+
#
|
49
|
+
# Use strftime patterns to generate formatted timestamp strings:
|
50
|
+
#
|
51
|
+
# User.qstamp(1.hour, pattern: '%Y%m%d%H') # => "2023010114"
|
52
|
+
# User.qstamp(1.day, pattern: '%Y-%m-%d') # => "2023-01-01"
|
53
|
+
# User.qstamp(1.week, pattern: '%Y-W%W') # => "2023-W01"
|
54
|
+
#
|
55
|
+
# Custom Time Reference:
|
56
|
+
#
|
57
|
+
# Specify a custom time instead of using the current time:
|
58
|
+
#
|
59
|
+
# custom_time = Time.parse('2023-06-15 14:30:45')
|
60
|
+
# User.qstamp(1.hour, time: custom_time) # => 1686834000 (14:00:00)
|
61
|
+
# User.qstamp(1.day, time: custom_time, pattern: '%Y%m%d') # => "20230615"
|
62
|
+
#
|
63
|
+
# Integration Patterns:
|
64
|
+
#
|
65
|
+
# # Time-based cache keys
|
66
|
+
# class MetricsCache < Familia::Horreum
|
67
|
+
# feature :quantization
|
68
|
+
# identifier_field :cache_key
|
69
|
+
#
|
70
|
+
# field :cache_key, :data, :computed_at
|
71
|
+
# hashkey :hourly_metrics
|
72
|
+
#
|
73
|
+
# def self.hourly_cache_key(metric_name)
|
74
|
+
# timestamp = qstamp(1.hour, pattern: '%Y%m%d%H')
|
75
|
+
# "metrics:#{metric_name}:#{timestamp}"
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
# def self.daily_cache_key(metric_name)
|
79
|
+
# timestamp = qstamp(1.day, pattern: '%Y%m%d')
|
80
|
+
# "daily_metrics:#{metric_name}:#{timestamp}"
|
81
|
+
# end
|
82
|
+
# end
|
83
|
+
#
|
84
|
+
# # Usage
|
85
|
+
# hourly_key = MetricsCache.hourly_cache_key('page_views')
|
86
|
+
# # => "metrics:page_views:2023010114"
|
87
|
+
#
|
88
|
+
# # Analytics data bucketing
|
89
|
+
# class UserActivity < Familia::Horreum
|
90
|
+
# feature :quantization
|
91
|
+
# identifier_field :bucket_id
|
92
|
+
#
|
93
|
+
# field :bucket_id, :user_count, :event_count, :bucket_time
|
94
|
+
#
|
95
|
+
# def self.record_activity(user_id, event_type)
|
96
|
+
# # Create hourly buckets
|
97
|
+
# bucket_time = qstamp(1.hour)
|
98
|
+
# bucket_id = "activity:#{qstamp(1.hour, pattern: '%Y%m%d%H')}"
|
99
|
+
#
|
100
|
+
# activity = find_or_create(bucket_id) do
|
101
|
+
# new(bucket_id: bucket_id, bucket_time: bucket_time,
|
102
|
+
# user_count: 0, event_count: 0)
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
# activity.event_count += 1
|
106
|
+
# activity.save
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# def self.activity_for_hour(time = Time.now)
|
110
|
+
# bucket_id = "activity:#{qstamp(1.hour, time: time, pattern: '%Y%m%d%H')}"
|
111
|
+
# find(bucket_id)
|
112
|
+
# end
|
113
|
+
# end
|
114
|
+
#
|
115
|
+
# # Time-series data storage
|
116
|
+
# class TimeSeriesMetric < Familia::Horreum
|
117
|
+
# feature :quantization
|
118
|
+
# identifier_field :series_key
|
119
|
+
#
|
120
|
+
# field :series_key, :metric_name, :interval, :value, :timestamp
|
121
|
+
# zset :data_points # score = timestamp, member = value
|
122
|
+
#
|
123
|
+
# def self.record_metric(metric_name, value, interval = 5.minutes)
|
124
|
+
# timestamp = qstamp(interval)
|
125
|
+
# series_key = "#{metric_name}:#{interval.to_i}"
|
126
|
+
#
|
127
|
+
# metric = find_or_create(series_key) do
|
128
|
+
# new(series_key: series_key, metric_name: metric_name,
|
129
|
+
# interval: interval.to_i)
|
130
|
+
# end
|
131
|
+
#
|
132
|
+
# metric.data_points.add(timestamp, value)
|
133
|
+
# metric.timestamp = timestamp
|
134
|
+
# metric.value = value
|
135
|
+
# metric.save
|
136
|
+
# end
|
137
|
+
#
|
138
|
+
# def self.get_series(metric_name, interval, start_time, end_time)
|
139
|
+
# series_key = "#{metric_name}:#{interval.to_i}"
|
140
|
+
# metric = find(series_key)
|
141
|
+
# return [] unless metric
|
142
|
+
#
|
143
|
+
# start_bucket = qstamp(interval, time: start_time)
|
144
|
+
# end_bucket = qstamp(interval, time: end_time)
|
145
|
+
# metric.data_points.range_by_score(start_bucket, end_bucket, with_scores: true)
|
146
|
+
# end
|
147
|
+
# end
|
148
|
+
#
|
149
|
+
# Quantum Calculation:
|
150
|
+
#
|
151
|
+
# The quantum (time interval) determines the bucket size:
|
152
|
+
# - 1.minute: Buckets every minute (00, 01, 02, ...)
|
153
|
+
# - 5.minutes: Buckets every 5 minutes (00, 05, 10, 15, ...)
|
154
|
+
# - 1.hour: Buckets every hour (00:00, 01:00, 02:00, ...)
|
155
|
+
# - 1.day: Daily buckets (00:00:00 each day)
|
156
|
+
# - 1.week: Weekly buckets (start of week)
|
157
|
+
#
|
158
|
+
# Understanding Quantum Boundaries:
|
159
|
+
#
|
160
|
+
# # Current time: 2023-01-01 14:37:42
|
161
|
+
#
|
162
|
+
# # 1.hour quantum (rounds down to hour boundary)
|
163
|
+
# qstamp(1.hour) # => 1672531200 (2023-01-01 14:00:00)
|
164
|
+
#
|
165
|
+
# # 15.minutes quantum (rounds down to 15-minute boundary)
|
166
|
+
# qstamp(15.minutes) # => 1672532100 (2023-01-01 14:30:00)
|
167
|
+
#
|
168
|
+
# # 1.day quantum (rounds down to day boundary)
|
169
|
+
# qstamp(1.day) # => 1672531200 (2023-01-01 00:00:00)
|
170
|
+
#
|
171
|
+
# Cross-Timezone Considerations:
|
172
|
+
#
|
173
|
+
# class GlobalMetrics < Familia::Horreum
|
174
|
+
# feature :quantization
|
175
|
+
#
|
176
|
+
# def self.utc_hourly_key(metric_name)
|
177
|
+
# # Always use UTC for consistent global buckets
|
178
|
+
# timestamp = qstamp(1.hour, time: Time.now.utc, pattern: '%Y%m%d%H')
|
179
|
+
# "global:#{metric_name}:#{timestamp}"
|
180
|
+
# end
|
181
|
+
#
|
182
|
+
# def self.local_daily_key(metric_name, timezone = 'America/New_York')
|
183
|
+
# # Use local timezone for region-specific buckets
|
184
|
+
# local_time = Time.now.in_time_zone(timezone)
|
185
|
+
# timestamp = qstamp(1.day, time: local_time, pattern: '%Y%m%d')
|
186
|
+
# "#{timezone.gsub('/', '_')}:#{metric_name}:#{timestamp}"
|
187
|
+
# end
|
188
|
+
# end
|
189
|
+
#
|
190
|
+
# Performance Optimization:
|
191
|
+
#
|
192
|
+
# class OptimizedQuantization < Familia::Horreum
|
193
|
+
# feature :quantization
|
194
|
+
#
|
195
|
+
# # Cache quantized timestamps to avoid repeated calculations
|
196
|
+
# def self.cached_qstamp(quantum, pattern: nil, time: nil)
|
197
|
+
# cache_key = "qstamp:#{quantum}:#{pattern}:#{(time || Time.now).to_i / quantum}"
|
198
|
+
# Rails.cache.fetch(cache_key, expires_in: quantum) do
|
199
|
+
# qstamp(quantum, pattern: pattern, time: time)
|
200
|
+
# end
|
201
|
+
# end
|
202
|
+
#
|
203
|
+
# # Batch quantize multiple timestamps
|
204
|
+
# def self.batch_quantize(timestamps, quantum)
|
205
|
+
# timestamps.map { |ts| Familia.qstamp(quantum, time: ts) }
|
206
|
+
# end
|
207
|
+
#
|
208
|
+
# # Pre-generate bucket timestamps for a time range
|
209
|
+
# def self.pregenerate_buckets(start_time, end_time, quantum)
|
210
|
+
# buckets = []
|
211
|
+
# current = Familia.qstamp(quantum, time: start_time)
|
212
|
+
# end_bucket = Familia.qstamp(quantum, time: end_time)
|
213
|
+
#
|
214
|
+
# while current <= end_bucket
|
215
|
+
# buckets << current
|
216
|
+
# current += quantum
|
217
|
+
# end
|
218
|
+
# buckets
|
219
|
+
# end
|
220
|
+
# end
|
221
|
+
#
|
222
|
+
# Error Handling:
|
223
|
+
#
|
224
|
+
# The feature validates quantum values and provides descriptive errors:
|
225
|
+
#
|
226
|
+
# User.qstamp(0) # => ArgumentError: Quantum must be positive
|
227
|
+
# User.qstamp(-5) # => ArgumentError: Quantum must be positive
|
228
|
+
# User.qstamp("invalid") # => ArgumentError: Quantum must be positive
|
229
|
+
#
|
230
|
+
# Default Quantum Behavior:
|
231
|
+
#
|
232
|
+
# If no quantum is specified, the feature uses default_expiration or 10.minutes:
|
233
|
+
#
|
234
|
+
# class MyModel < Familia::Horreum
|
235
|
+
# feature :quantization
|
236
|
+
# default_expiration 1.hour
|
237
|
+
# end
|
238
|
+
#
|
239
|
+
# MyModel.qstamp() # Uses 1.hour as quantum
|
240
|
+
#
|
241
|
+
# class NoDefault < Familia::Horreum
|
242
|
+
# feature :quantization
|
243
|
+
# end
|
244
|
+
#
|
245
|
+
# NoDefault.qstamp() # Uses 10.minutes as fallback quantum
|
246
|
+
#
|
247
|
+
module Quantization
|
248
|
+
def self.included(base)
|
249
|
+
Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
|
250
|
+
base.extend ClassMethods
|
251
|
+
end
|
9
252
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
253
|
+
module ClassMethods
|
254
|
+
# Generates a quantized timestamp based on the given parameters
|
255
|
+
#
|
256
|
+
# This method rounds the current time to the nearest quantum and optionally
|
257
|
+
# formats it according to the given pattern. It's useful for creating
|
258
|
+
# time-based buckets or keys with reduced granularity.
|
259
|
+
#
|
260
|
+
# @param quantum [Numeric, Array, nil] The time quantum in seconds or an array of [quantum, pattern]
|
261
|
+
# @param pattern [String, nil] The strftime pattern to format the timestamp
|
262
|
+
# @param time [Time, nil] The reference time (default: current time)
|
263
|
+
# @return [Integer, String] A unix timestamp or formatted timestamp string
|
264
|
+
#
|
265
|
+
# @example Generate hourly bucket timestamp
|
266
|
+
# User.qstamp(1.hour) # => 1672531200 (rounded to hour boundary)
|
267
|
+
#
|
268
|
+
# @example Generate formatted timestamp
|
269
|
+
# User.qstamp(1.hour, pattern: '%Y%m%d%H') # => "2023010114"
|
270
|
+
#
|
271
|
+
# @example Using array syntax
|
272
|
+
# User.qstamp([1.hour, '%Y%m%d%H']) # => "2023010114"
|
273
|
+
#
|
274
|
+
# @example With custom time reference
|
275
|
+
# custom_time = Time.parse('2023-06-15 14:30:45')
|
276
|
+
# User.qstamp(1.hour, time: custom_time) # => 1686834000
|
277
|
+
#
|
278
|
+
# @raise [ArgumentError] If quantum is not positive
|
279
|
+
#
|
280
|
+
def qstamp(quantum = nil, pattern: nil, time: nil)
|
281
|
+
# Handle array input format: [quantum, pattern]
|
282
|
+
if quantum.is_a?(Array)
|
283
|
+
quantum, pattern = quantum
|
284
|
+
end
|
285
|
+
|
286
|
+
# Use default quantum if none specified
|
287
|
+
# Priority: provided quantum > class default_expiration > 10.minutes fallback
|
288
|
+
quantum ||= default_expiration || 10.minutes
|
289
|
+
|
290
|
+
# Validate quantum value
|
291
|
+
unless quantum.is_a?(Numeric) && quantum.positive?
|
292
|
+
raise ArgumentError, "Quantum must be positive (#{quantum.inspect} given)"
|
293
|
+
end
|
294
|
+
|
295
|
+
# Delegate to Familia.qstamp for the actual calculation
|
296
|
+
Familia.qstamp(quantum, pattern: pattern, time: time)
|
297
|
+
end
|
298
|
+
|
299
|
+
# Generate multiple quantized timestamps for a time range
|
300
|
+
#
|
301
|
+
# @param start_time [Time] Start of the time range
|
302
|
+
# @param end_time [Time] End of the time range
|
303
|
+
# @param quantum [Numeric] Time quantum in seconds
|
304
|
+
# @param pattern [String, nil] Optional strftime pattern
|
305
|
+
# @return [Array] Array of quantized timestamps
|
306
|
+
#
|
307
|
+
# @example Generate hourly buckets for a day
|
308
|
+
# start_time = Time.parse('2023-01-01 00:00:00')
|
309
|
+
# end_time = Time.parse('2023-01-01 23:59:59')
|
310
|
+
# User.qstamp_range(start_time, end_time, 1.hour)
|
311
|
+
# # => [1672531200, 1672534800, 1672538400, ...] (24 hourly buckets)
|
312
|
+
#
|
313
|
+
def qstamp_range(start_time, end_time, quantum, pattern: nil)
|
314
|
+
timestamps = []
|
315
|
+
current = qstamp(quantum, time: start_time)
|
316
|
+
end_bucket = qstamp(quantum, time: end_time)
|
317
|
+
|
318
|
+
while current <= end_bucket
|
319
|
+
if pattern
|
320
|
+
timestamps << Time.at(current).strftime(pattern)
|
321
|
+
else
|
322
|
+
timestamps << current
|
323
|
+
end
|
324
|
+
current += quantum
|
325
|
+
end
|
326
|
+
|
327
|
+
timestamps
|
328
|
+
end
|
329
|
+
|
330
|
+
# Check if a timestamp falls within a quantized bucket
|
331
|
+
#
|
332
|
+
# @param timestamp [Time, Integer] The timestamp to check
|
333
|
+
# @param quantum [Numeric] The quantum interval
|
334
|
+
# @param bucket_time [Time, Integer] The bucket reference time
|
335
|
+
# @return [Boolean] true if timestamp falls in the bucket
|
336
|
+
#
|
337
|
+
# @example Check if event falls in hourly bucket
|
338
|
+
# event_time = Time.parse('2023-01-01 14:37:42')
|
339
|
+
# bucket_time = Time.parse('2023-01-01 14:00:00')
|
340
|
+
# User.in_bucket?(event_time, 1.hour, bucket_time) # => true
|
341
|
+
#
|
342
|
+
def in_bucket?(timestamp, quantum, bucket_time)
|
343
|
+
timestamp = timestamp.to_i if timestamp.respond_to?(:to_i)
|
344
|
+
bucket_time = bucket_time.to_i if bucket_time.respond_to?(:to_i)
|
345
|
+
bucket_start = qstamp(quantum, time: Time.at(bucket_time))
|
346
|
+
bucket_end = bucket_start + quantum - 1
|
347
|
+
|
348
|
+
timestamp >= bucket_start && timestamp <= bucket_end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
# Instance method version of qstamp
|
17
353
|
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
# or keys with reduced granularity.
|
354
|
+
# Generates a quantized timestamp using the same logic as the class method,
|
355
|
+
# but can access instance-specific default expiration settings.
|
21
356
|
#
|
22
|
-
# @
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
357
|
+
# @param quantum [Numeric, Array, nil] The time quantum in seconds or array format
|
358
|
+
# @param pattern [String, nil] The strftime pattern to format the timestamp
|
359
|
+
# @param time [Time, nil] The reference time (default: current time)
|
360
|
+
# @return [Integer, String] A unix timestamp or formatted timestamp string
|
26
361
|
#
|
27
|
-
# @
|
362
|
+
# @example Instance usage
|
363
|
+
# event = AnalyticsEvent.new
|
364
|
+
# event.qstamp(15.minutes) # => 1672532100
|
28
365
|
#
|
29
366
|
def qstamp(quantum = nil, pattern: nil, time: nil)
|
30
|
-
#
|
31
|
-
quantum
|
32
|
-
|
33
|
-
# Previously we erronously included `@opts.fetch(:quantize, nil)` in
|
34
|
-
# the list of default values here, but @opts is for horreum instances
|
35
|
-
# not at the class level. This method `qstamp` is part of the initial
|
36
|
-
# definition for whatever horreum subclass we're in right now. That's
|
37
|
-
# why default_expiration works (e.g. `class Plop; feature :quantization; default_expiration 90; end`).
|
38
|
-
quantum ||= default_expiration || 10.minutes
|
39
|
-
|
40
|
-
# Validate quantum
|
41
|
-
unless quantum.is_a?(Numeric) && quantum.positive?
|
42
|
-
raise ArgumentError, "Quantum must be positive (#{quantum.inspect} given)"
|
43
|
-
end
|
44
|
-
|
45
|
-
# Call Familia.qstamp with our processed parameters
|
46
|
-
Familia.qstamp(quantum, pattern: pattern, time: time)
|
367
|
+
# Use instance default_expiration if available, otherwise delegate to class
|
368
|
+
quantum ||= default_expiration if respond_to?(:default_expiration)
|
369
|
+
self.class.qstamp(quantum, pattern: pattern, time: time)
|
47
370
|
end
|
48
|
-
end
|
49
371
|
|
50
|
-
|
51
|
-
|
52
|
-
|
372
|
+
# Generate a quantized identifier for this instance
|
373
|
+
#
|
374
|
+
# Creates a time-based identifier using the instance's identifier and
|
375
|
+
# a quantized timestamp. Useful for creating time-bucketed cache keys
|
376
|
+
# or grouping identifiers.
|
377
|
+
#
|
378
|
+
# @param quantum [Numeric] Time quantum in seconds
|
379
|
+
# @param pattern [String, nil] Optional strftime pattern
|
380
|
+
# @param separator [String] Separator between identifier and timestamp
|
381
|
+
# @return [String] Combined identifier with quantized timestamp
|
382
|
+
#
|
383
|
+
# @example Generate time-based cache key
|
384
|
+
# user = User.new(id: 123)
|
385
|
+
# user.quantized_identifier(1.hour) # => "123:1672531200"
|
386
|
+
# user.quantized_identifier(1.hour, pattern: '%Y%m%d%H') # => "123:2023010114"
|
387
|
+
#
|
388
|
+
def quantized_identifier(quantum, pattern: nil, separator: ':')
|
389
|
+
timestamp = qstamp(quantum, pattern: pattern)
|
390
|
+
base_id = respond_to?(:identifier) ? identifier : object_id
|
391
|
+
"#{base_id}#{separator}#{timestamp}"
|
392
|
+
end
|
53
393
|
|
54
|
-
|
394
|
+
extend ClassMethods
|
55
395
|
|
56
|
-
|
396
|
+
Familia::Base.add_feature self, :quantization
|
397
|
+
end
|
57
398
|
end
|
58
399
|
end
|