familia 1.0.0.pre.rc3 → 1.0.0.pre.rc4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +198 -48
  4. data/VERSION.yml +1 -1
  5. data/lib/familia/base.rb +29 -1
  6. data/lib/familia/features/expiration.rb +90 -0
  7. data/lib/familia/features/quantization.rb +56 -0
  8. data/lib/familia/features/safe_dump.rb +2 -33
  9. data/lib/familia/features.rb +5 -4
  10. data/lib/familia/horreum/class_methods.rb +112 -46
  11. data/lib/familia/horreum/commands.rb +9 -3
  12. data/lib/familia/horreum/relations_management.rb +2 -2
  13. data/lib/familia/horreum/serialization.rb +23 -42
  14. data/lib/familia/horreum/settings.rb +0 -8
  15. data/lib/familia/horreum/utils.rb +0 -1
  16. data/lib/familia/horreum.rb +1 -1
  17. data/lib/familia/logging.rb +26 -4
  18. data/lib/familia/redistype/serialization.rb +60 -38
  19. data/lib/familia/redistype.rb +45 -17
  20. data/lib/familia/settings.rb +11 -1
  21. data/lib/familia/tools.rb +68 -0
  22. data/lib/familia/types/hashkey.rb +5 -5
  23. data/lib/familia/types/list.rb +2 -2
  24. data/lib/familia/types/sorted_set.rb +12 -12
  25. data/lib/familia/types/string.rb +1 -1
  26. data/lib/familia/types/unsorted_set.rb +2 -2
  27. data/lib/familia/utils.rb +106 -51
  28. data/lib/familia/version.rb +2 -2
  29. data/try/10_familia_try.rb +4 -4
  30. data/try/20_redis_type_try.rb +9 -6
  31. data/try/26_redis_bool_try.rb +1 -1
  32. data/try/27_redis_horreum_try.rb +1 -1
  33. data/try/30_familia_object_try.rb +3 -2
  34. data/try/40_customer_try.rb +3 -3
  35. data/try/test_helpers.rb +9 -2
  36. metadata +5 -5
  37. data/lib/familia/features/api_version.rb +0 -19
  38. data/lib/familia/features/atomic_saves.rb +0 -8
  39. data/lib/familia/features/quantizer.rb +0 -35
data/lib/familia/utils.rb CHANGED
@@ -7,61 +7,116 @@ module Familia
7
7
 
8
8
  module Utils
9
9
 
10
- def debug?
11
- @debug == true
12
- end
10
+ # Checks if debug mode is enabled
11
+ #
12
+ # e.g. Familia.debug = true
13
+ #
14
+ # @return [Boolean] true if debug mode is on, false otherwise
15
+ def debug?
16
+ @debug == true
17
+ end
13
18
 
14
- def generate_id
15
- input = SecureRandom.hex(32) # 16=128 bits, 32=256 bits
16
- Digest::SHA256.hexdigest(input).to_i(16).to_s(36) # base-36 encoding
17
- end
19
+ # Generates a unique ID using SHA256 and base-36 encoding
20
+ # @param length [Integer] length of the random input in bytes (default: 32)
21
+ # @param encoding [Integer] base encoding for the output (default: 36)
22
+ # @return [String] a unique identifier
23
+ #
24
+ # @example Generate a default ID
25
+ # Familia.generate_id
26
+ # # => "kuk79w6uxg81tk0kn5hsl6pr7ic16e9p6evjifzozkda9el6z"
27
+ #
28
+ # @example Generate a shorter ID with 16 bytes input
29
+ # Familia.generate_id(length: 16)
30
+ # # => "z6gqw1b7ftzpvapydkt0iah0h0bev5hkhrs4mkf1gq4nq5csa"
31
+ #
32
+ # @example Generate an ID with hexadecimal encoding
33
+ # Familia.generate_id(encoding: 16)
34
+ # # => "d06a2a70cba543cd2bbd352c925bc30b0a9029ca79e72d6556f8d6d8603d5716"
35
+ #
36
+ # @example Generate a shorter ID with custom encoding
37
+ # Familia.generate_id(length: 8, encoding: 32)
38
+ # # => "193tosc85k3u513do2mtmibchpd2ruh5l3nsp6dnl0ov1i91h7m7"
39
+ #
40
+ def generate_id(length: 32, encoding: 36)
41
+ raise ArgumentError, "Encoding must be between 2 and 32" unless (1..32).include?(encoding)
42
+
43
+ input = SecureRandom.hex(length)
44
+ Digest::SHA256.hexdigest(input).to_i(16).to_s(encoding)
45
+ end
18
46
 
19
- def join(*val)
20
- val.compact.join(Familia.delim)
21
- end
47
+ # Joins array elements with Familia delimiter
48
+ # @param val [Array] elements to join
49
+ # @return [String] joined string
50
+ def join(*val)
51
+ val.compact.join(Familia.delim)
52
+ end
22
53
 
23
- def split(val)
24
- val.split(Familia.delim)
25
- end
54
+ # Splits a string using Familia delimiter
55
+ # @param val [String] string to split
56
+ # @return [Array] split elements
57
+ def split(val)
58
+ val.split(Familia.delim)
59
+ end
26
60
 
27
- def rediskey(*val)
28
- join(*val)
29
- end
61
+ # Creates a Redis key from given values
62
+ # @param val [Array] elements to join for the key
63
+ # @return [String] Redis key
64
+ def rediskey(*val)
65
+ join(*val)
66
+ end
30
67
 
31
- def redisuri(uri)
32
- generic_uri = URI.parse(uri.to_s)
33
-
34
- # Create a new URI::Redis object
35
- redis_uri = URI::Redis.build(
36
- scheme: generic_uri.scheme,
37
- userinfo: generic_uri.userinfo,
38
- host: generic_uri.host,
39
- port: generic_uri.port,
40
- path: generic_uri.path,
41
- query: generic_uri.query,
42
- fragment: generic_uri.fragment
43
- )
44
-
45
- redis_uri
46
- end
68
+ # Converts a generic URI to a Redis URI
69
+ # @param uri [String, URI] URI to convert
70
+ # @return [URI::Redis] Redis URI object
71
+ def redisuri(uri)
72
+ generic_uri = URI.parse(uri.to_s)
73
+
74
+ # Create a new URI::Redis object
75
+ URI::Redis.build(
76
+ scheme: generic_uri.scheme,
77
+ userinfo: generic_uri.userinfo,
78
+ host: generic_uri.host,
79
+ port: generic_uri.port,
80
+ path: generic_uri.path,
81
+ query: generic_uri.query,
82
+ fragment: generic_uri.fragment
83
+ )
84
+ end
85
+
86
+ # Returns current time in UTC as a float
87
+ # @param name [Time] time object (default: current time)
88
+ # @return [Float] time in seconds since epoch
89
+ def Familia.now(name = Time.now)
90
+ name.utc.to_f
91
+ end
47
92
 
48
- def now(name = Time.now)
49
- name.utc.to_f
50
- end
51
93
 
52
94
  # A quantized timestamp
53
- # e.g. 12:32 -> 12:30
54
95
  #
55
- def qnow(quantum = 10.minutes, now = Familia.now)
56
- rounded = now - (now % quantum)
57
- Time.at(rounded).utc.to_i
58
- end
96
+ # @param quantum [Integer] The time quantum in seconds (default: 10 minutes).
97
+ # @param pattern [String, nil] The strftime pattern to format the timestamp.
98
+ # @param time [Integer, Float, Time, nil] A specific time to quantize (default: current time).
99
+ # @return [Integer, String] A unix timestamp or formatted timestamp string.
100
+ #
101
+ # @example
102
+ # Familia.qstamp # Returns an integer timestamp rounded to the nearest 10 minutes
103
+ # Familia.qstamp(1.hour) # Uses 1 hour quantum
104
+ # Familia.qstamp(10.minutes, pattern: '%H:%M') # Returns a formatted string like "12:30"
105
+ # Familia.qstamp(10.minutes, time: 1302468980) # Quantizes the given Unix timestamp
106
+ # Familia.qstamp(10.minutes, time: Time.now) # Quantizes the given Time object
107
+ # Familia.qstamp(10.minutes, pattern: '%H:%M', time: 1302468980) # Formats a specific time
108
+ #
109
+ def qstamp(quantum = 10.minutes, pattern: nil, time: nil)
110
+ time ||= Familia.now
111
+ time = time.to_f if time.is_a?(Time)
112
+
113
+ rounded = time - (time % quantum)
59
114
 
60
- def qstamp(quantum = nil, pattern = nil, now = Familia.now)
61
- quantum ||= ttl || 10.minutes
62
- pattern ||= '%H%M'
63
- rounded = now - (now % quantum)
64
- Time.at(rounded).utc.strftime(pattern)
115
+ if pattern
116
+ Time.at(rounded).utc.strftime(pattern)
117
+ else
118
+ Time.at(rounded).utc.to_i
119
+ end
65
120
  end
66
121
 
67
122
  def generate_sha_hash(*elements)
@@ -80,13 +135,13 @@ module Familia
80
135
  def distinguisher(value_to_distinguish, strict_values = true)
81
136
  case value_to_distinguish
82
137
  when ::Symbol, ::String, ::Integer, ::Float
83
- Familia.trace :TOREDIS_DISTINGUISHER, redis, "string", caller(1..1) if Familia.debug?
138
+ #Familia.trace :TOREDIS_DISTINGUISHER, redis, "string", caller(1..1) if Familia.debug?
84
139
  # Symbols and numerics are naturally serializable to strings
85
140
  # so it's a relatively low risk operation.
86
141
  value_to_distinguish.to_s
87
142
 
88
143
  when ::TrueClass, ::FalseClass, ::NilClass
89
- Familia.trace :TOREDIS_DISTINGUISHER, redis, "true/false/nil", caller(1..1) if Familia.debug?
144
+ #Familia.trace :TOREDIS_DISTINGUISHER, redis, "true/false/nil", caller(1..1) if Familia.debug?
90
145
  # TrueClass, FalseClass, and NilClass are high risk because we can't
91
146
  # reliably determine the original type of the value from the serialized
92
147
  # string. This can lead to unexpected behavior when deserializing. For
@@ -99,7 +154,7 @@ module Familia
99
154
  value_to_distinguish.to_s #=> "true", "false", ""
100
155
 
101
156
  when Familia::Base, Class
102
- Familia.trace :TOREDIS_DISTINGUISHER, redis, "base", caller(1..1) if Familia.debug?
157
+ #Familia.trace :TOREDIS_DISTINGUISHER, redis, "base", caller(1..1) if Familia.debug?
103
158
  if value_to_distinguish.is_a?(Class)
104
159
  value_to_distinguish.name
105
160
  else
@@ -107,14 +162,14 @@ module Familia
107
162
  end
108
163
 
109
164
  else
110
- Familia.trace :TOREDIS_DISTINGUISHER, redis, "else1 #{strict_values}", caller(1..1) if Familia.debug?
165
+ #Familia.trace :TOREDIS_DISTINGUISHER, redis, "else1 #{strict_values}", caller(1..1) if Familia.debug?
111
166
 
112
167
  if value_to_distinguish.class.ancestors.member?(Familia::Base)
113
- Familia.trace :TOREDIS_DISTINGUISHER, redis, "isabase", caller(1..1) if Familia.debug?
168
+ #Familia.trace :TOREDIS_DISTINGUISHER, redis, "isabase", caller(1..1) if Familia.debug?
114
169
  value_to_distinguish.identifier
115
170
 
116
171
  else
117
- Familia.trace :TOREDIS_DISTINGUISHER, redis, "else2 #{strict_values}", caller(1..1) if Familia.debug?
172
+ #Familia.trace :TOREDIS_DISTINGUISHER, redis, "else2 #{strict_values}", caller(1..1) if Familia.debug?
118
173
  raise Familia::HighRiskFactor, value_to_distinguish if strict_values
119
174
  nil
120
175
  end
@@ -1,4 +1,4 @@
1
- # rubocop:disable all
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'yaml'
4
4
 
@@ -7,7 +7,7 @@ module Familia
7
7
  def self.to_s
8
8
  load_config
9
9
  version = [@version[:MAJOR], @version[:MINOR], @version[:PATCH]].join('.')
10
- version += "-#{@version[:PRE]}" if @version[:PRE]
10
+ version = "#{version}-#{@version[:PRE]}" if @version[:PRE]
11
11
  version
12
12
  end
13
13
  alias inspect to_s
@@ -44,14 +44,14 @@ parsed_time = Familia.now(Time.parse('2011-04-10 20:56:20 UTC').utc)
44
44
  #=> [1302468980.0, true, true]
45
45
 
46
46
  ## Familia.qnow
47
- Familia.qnow 10.minutes, 1302468980
47
+ Familia.qstamp 10.minutes, time: 1302468980
48
48
  #=> 1302468600
49
49
 
50
50
  ## Familia::Object.qstamp
51
- Limiter.qstamp 10.minutes, '%H:%M', 1302468980
51
+ Limiter.qstamp(10.minutes, pattern: '%H:%M', time: 1302468980)
52
52
  #=> '20:50'
53
53
 
54
54
  ## Familia::Object#qstamp
55
55
  limiter = Limiter.new :request
56
- limiter.qstamp 10.minutes, '%H:%M', 1302468980
57
- ##=> '20:50'
56
+ limiter.qstamp(10.minutes, pattern: '%H:%M', time: 1302468980)
57
+ #=> '20:50'
@@ -1,9 +1,7 @@
1
1
 
2
2
  require_relative '../lib/familia'
3
- require_relative '../lib/familia/features/quantizer'
4
3
  require_relative './test_helpers'
5
4
 
6
- #Familia.apiversion = 'v1'
7
5
 
8
6
  @limiter1 = Limiter.new :requests
9
7
 
@@ -24,7 +22,6 @@ p [@a.name, @b.name]
24
22
  @a.owners.frozen?
25
23
  #=> true
26
24
 
27
-
28
25
  ## Limiter#qstamp
29
26
  @limiter1.counter.qstamp(10.minutes, '%H:%M', 1302468980)
30
27
  ##=> '20:50'
@@ -35,8 +32,10 @@ p [@a.name, @b.name]
35
32
 
36
33
  ## Limiter#qstamp as a number
37
34
  @limiter2 = Limiter.new :requests
38
- @limiter2.counter.qstamp(10.minutes, pattern=nil, now=1302468980)
39
- ##=> 1302468600
35
+ p [@limiter1.ttl, @limiter2.ttl]
36
+ p [@limiter1.counter.parent.ttl, @limiter2.counter.parent.ttl]
37
+ @limiter2.counter.qstamp(10.minutes, pattern: nil, time: 1302468980)
38
+ #=> 1302468600
40
39
 
41
40
  ## Redis Types can be stored to quantized numeric suffix. This
42
41
  ## tryouts is disabled b/c `RedisType#rediskey` takes no args
@@ -51,10 +50,14 @@ p [@a.name, @b.name]
51
50
  @limiter1.counter.increment
52
51
  #=> 1
53
52
 
54
- ## Check ttl
53
+ ## Check counter ttl
55
54
  @limiter1.counter.ttl
56
55
  #=> 3600.0
57
56
 
57
+ ## Check limiter ttl
58
+ @limiter1.ttl
59
+ #=> 1800.0
60
+
58
61
  ## Check ttl for a different instance
59
62
  ## (this exists to make sure options are cloned for each instance)
60
63
  @limiter3 = Limiter.new :requests
@@ -1,7 +1,7 @@
1
1
  require_relative '../lib/familia'
2
2
  require_relative './test_helpers'
3
3
 
4
- Familia.debug = true
4
+ Familia.debug = false
5
5
 
6
6
  @hashkey = Familia::HashKey.new 'key'
7
7
 
@@ -1,7 +1,7 @@
1
1
  require_relative '../lib/familia'
2
2
  require_relative './test_helpers'
3
3
 
4
- Familia.debug = true
4
+ Familia.debug = false
5
5
 
6
6
  @identifier = 'tryouts-27@onetimesecret.com'
7
7
  @customer = Customer.new @identifier
@@ -1,6 +1,7 @@
1
1
  require_relative '../lib/familia'
2
2
  require_relative './test_helpers'
3
- #Familia.apiversion = 'v1'
3
+
4
+ Familia.debug = false
4
5
 
5
6
  @a = Bone.new 'atoken', 'akey'
6
7
 
@@ -36,7 +37,7 @@ Customer.values.all.collect(&:custid)
36
37
  ##=> ['delano']
37
38
 
38
39
  ## Familia.from_redis
39
- obj = Customer.from_redis :delano
40
+ obj = Customer.from_identifier :delano
40
41
  [obj.class, obj.custid]
41
42
  #=> [Customer, 'delano']
42
43
 
@@ -18,7 +18,7 @@ require_relative './test_helpers'
18
18
  #=> true
19
19
 
20
20
  ## Customer can be retrieved by identifier
21
- retrieved_customer = Customer.from_redis("test@example.com")
21
+ retrieved_customer = Customer.from_identifier("test@example.com")
22
22
  retrieved_customer.custid
23
23
  #=> "test@example.com"
24
24
 
@@ -35,7 +35,7 @@ retrieved_customer.custid
35
35
  @customer.planid = "premium"
36
36
  @customer.save
37
37
  ident = @customer.identifier
38
- Customer.from_redis(ident).planid
38
+ Customer.from_identifier(ident).planid
39
39
  #=> "premium"
40
40
 
41
41
  ## Customer can increment secrets_created counter
@@ -90,7 +90,7 @@ Customer.instances.member?(@customer)
90
90
 
91
91
  ## Customer can be destroyed
92
92
  ret = @customer.destroy!
93
- cust = Customer.from_redis("test@example.com")
93
+ cust = Customer.from_identifier("test@example.com")
94
94
  exists = Customer.exists?("test@example.com")
95
95
  [ret, cust.nil?, exists]
96
96
  #=> [true, true, false]
data/try/test_helpers.rb CHANGED
@@ -3,8 +3,7 @@
3
3
  require 'digest'
4
4
  require_relative '../lib/familia'
5
5
 
6
- # ENV['FAMILIA_TRACE'] = '1'
7
- #Familia.debug = true
6
+ Familia.debug = false # also # ENV['FAMILIA_TRACE'] = '1'
8
7
  Familia.enable_redis_logging = true
9
8
  Familia.enable_redis_counter = true
10
9
 
@@ -33,6 +32,7 @@ class Customer < Familia::Horreum
33
32
  ttl 5.years
34
33
 
35
34
  feature :safe_dump
35
+ #feature :expiration
36
36
  #feature :api_version
37
37
 
38
38
  # NOTE: The SafeDump mixin caches the safe_dump_field_map so updating this list
@@ -144,10 +144,13 @@ end
144
144
 
145
145
  class CustomDomain < Familia::Horreum
146
146
 
147
+ feature :expiration
148
+
147
149
  class_sorted_set :values, key: 'onetime:customdomain:values'
148
150
 
149
151
  identifier :derive_id
150
152
 
153
+
151
154
  field :display_domain
152
155
  field :custid
153
156
  field :base_domain
@@ -182,6 +185,10 @@ end
182
185
 
183
186
  class Limiter < Familia::Horreum
184
187
 
188
+ feature :expiration
189
+ feature :quantization
190
+
191
+ ttl 30.minutes
185
192
  identifier :name
186
193
  field :name
187
194
  # No :key field (so we can test hte behaviour in Horreum#initialize)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: familia
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre.rc3
4
+ version: 1.0.0.pre.rc4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delano Mandelbaum
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-17 00:00:00.000000000 Z
11
+ date: 2024-08-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -82,9 +82,8 @@ files:
82
82
  - lib/familia/core_ext.rb
83
83
  - lib/familia/errors.rb
84
84
  - lib/familia/features.rb
85
- - lib/familia/features/api_version.rb
86
- - lib/familia/features/atomic_saves.rb
87
- - lib/familia/features/quantizer.rb
85
+ - lib/familia/features/expiration.rb
86
+ - lib/familia/features/quantization.rb
88
87
  - lib/familia/features/safe_dump.rb
89
88
  - lib/familia/horreum.rb
90
89
  - lib/familia/horreum/class_methods.rb
@@ -99,6 +98,7 @@ files:
99
98
  - lib/familia/redistype/serialization.rb
100
99
  - lib/familia/refinements.rb
101
100
  - lib/familia/settings.rb
101
+ - lib/familia/tools.rb
102
102
  - lib/familia/types/hashkey.rb
103
103
  - lib/familia/types/list.rb
104
104
  - lib/familia/types/sorted_set.rb
@@ -1,19 +0,0 @@
1
- # rubocop:disable all
2
- #
3
- module Familia::Features
4
- module ApiVersion
5
-
6
- def apiversion(val = nil, &blk)
7
- if blk.nil?
8
- @apiversion = val if val
9
- else
10
- tmp = @apiversion
11
- @apiversion = val
12
- yield
13
- @apiversion = tmp
14
- end
15
- @apiversion
16
- end
17
-
18
- end
19
- end
@@ -1,8 +0,0 @@
1
- # rubocop:disable all
2
-
3
- module Familia::Features
4
- module AtomicSaves
5
-
6
-
7
- end
8
- end
@@ -1,35 +0,0 @@
1
- # rubocop:disable all
2
-
3
- module Familia::Features
4
-
5
- module Quantizer
6
-
7
- # From Familia::RedisType
8
- #
9
- def qstamp(quantum = nil, pattern = nil, now = Familia.now)
10
- quantum ||= @opts[:quantize] || ttl || 10.minutes
11
- case quantum
12
- when Numeric
13
- # Handle numeric quantum (e.g., seconds, minutes)
14
- when Array
15
- quantum, pattern = *quantum
16
- end
17
- now ||= Familia.now
18
- rounded = now - (now % quantum)
19
-
20
- if pattern.nil?
21
- Time.at(rounded).utc.to_i # 3605 -> 3600
22
- else
23
- Time.at(rounded).utc.strftime(pattern || '%H%M') # 3605 -> '1:00'
24
- end
25
-
26
- end
27
-
28
- # From Familia::Horreum::InstanceMethods:
29
- #
30
- #def qstamp(_quantum = nil, pattern = nil, now = Familia.now)
31
- # self.class.qstamp ttl, pattern, now
32
- #end
33
-
34
- end
35
- end