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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +198 -48
- data/VERSION.yml +1 -1
- data/lib/familia/base.rb +29 -1
- data/lib/familia/features/expiration.rb +90 -0
- data/lib/familia/features/quantization.rb +56 -0
- data/lib/familia/features/safe_dump.rb +2 -33
- data/lib/familia/features.rb +5 -4
- data/lib/familia/horreum/class_methods.rb +112 -46
- data/lib/familia/horreum/commands.rb +9 -3
- data/lib/familia/horreum/relations_management.rb +2 -2
- data/lib/familia/horreum/serialization.rb +23 -42
- data/lib/familia/horreum/settings.rb +0 -8
- data/lib/familia/horreum/utils.rb +0 -1
- data/lib/familia/horreum.rb +1 -1
- data/lib/familia/logging.rb +26 -4
- data/lib/familia/redistype/serialization.rb +60 -38
- data/lib/familia/redistype.rb +45 -17
- data/lib/familia/settings.rb +11 -1
- data/lib/familia/tools.rb +68 -0
- data/lib/familia/types/hashkey.rb +5 -5
- data/lib/familia/types/list.rb +2 -2
- data/lib/familia/types/sorted_set.rb +12 -12
- data/lib/familia/types/string.rb +1 -1
- data/lib/familia/types/unsorted_set.rb +2 -2
- data/lib/familia/utils.rb +106 -51
- data/lib/familia/version.rb +2 -2
- data/try/10_familia_try.rb +4 -4
- data/try/20_redis_type_try.rb +9 -6
- data/try/26_redis_bool_try.rb +1 -1
- data/try/27_redis_horreum_try.rb +1 -1
- data/try/30_familia_object_try.rb +3 -2
- data/try/40_customer_try.rb +3 -3
- data/try/test_helpers.rb +9 -2
- metadata +5 -5
- data/lib/familia/features/api_version.rb +0 -19
- data/lib/familia/features/atomic_saves.rb +0 -8
- 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
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
data/lib/familia/version.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
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
|
10
|
+
version = "#{version}-#{@version[:PRE]}" if @version[:PRE]
|
11
11
|
version
|
12
12
|
end
|
13
13
|
alias inspect to_s
|
data/try/10_familia_try.rb
CHANGED
@@ -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.
|
47
|
+
Familia.qstamp 10.minutes, time: 1302468980
|
48
48
|
#=> 1302468600
|
49
49
|
|
50
50
|
## Familia::Object.qstamp
|
51
|
-
Limiter.qstamp
|
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
|
57
|
-
|
56
|
+
limiter.qstamp(10.minutes, pattern: '%H:%M', time: 1302468980)
|
57
|
+
#=> '20:50'
|
data/try/20_redis_type_try.rb
CHANGED
@@ -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
|
-
@
|
39
|
-
|
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
|
data/try/26_redis_bool_try.rb
CHANGED
data/try/27_redis_horreum_try.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require_relative '../lib/familia'
|
2
2
|
require_relative './test_helpers'
|
3
|
-
|
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.
|
40
|
+
obj = Customer.from_identifier :delano
|
40
41
|
[obj.class, obj.custid]
|
41
42
|
#=> [Customer, 'delano']
|
42
43
|
|
data/try/40_customer_try.rb
CHANGED
@@ -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.
|
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.
|
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.
|
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.
|
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-
|
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/
|
86
|
-
- lib/familia/features/
|
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,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
|