familia 0.10.2 → 1.0.0.pre.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.pre-commit-config.yaml +1 -1
  4. data/.rubocop.yml +75 -0
  5. data/.rubocop_todo.yml +63 -0
  6. data/Gemfile +6 -1
  7. data/Gemfile.lock +47 -15
  8. data/README.md +11 -12
  9. data/VERSION.yml +4 -3
  10. data/familia.gemspec +18 -13
  11. data/lib/familia/base.rb +33 -0
  12. data/lib/familia/connection.rb +87 -0
  13. data/lib/familia/core_ext.rb +119 -124
  14. data/lib/familia/errors.rb +33 -0
  15. data/lib/familia/features/api_version.rb +19 -0
  16. data/lib/familia/features/atomic_saves.rb +8 -0
  17. data/lib/familia/features/quantizer.rb +35 -0
  18. data/lib/familia/features/safe_dump.rb +175 -0
  19. data/lib/familia/features.rb +51 -0
  20. data/lib/familia/horreum/class_methods.rb +240 -0
  21. data/lib/familia/horreum/commands.rb +59 -0
  22. data/lib/familia/horreum/relations_management.rb +141 -0
  23. data/lib/familia/horreum/serialization.rb +154 -0
  24. data/lib/familia/horreum/settings.rb +63 -0
  25. data/lib/familia/horreum/utils.rb +43 -0
  26. data/lib/familia/horreum.rb +198 -0
  27. data/lib/familia/logging.rb +249 -0
  28. data/lib/familia/redistype/commands.rb +56 -0
  29. data/lib/familia/redistype/serialization.rb +110 -0
  30. data/lib/familia/redistype.rb +185 -0
  31. data/lib/familia/settings.rb +38 -0
  32. data/lib/familia/types/hashkey.rb +108 -0
  33. data/lib/familia/types/list.rb +155 -0
  34. data/lib/familia/types/sorted_set.rb +234 -0
  35. data/lib/familia/types/string.rb +115 -0
  36. data/lib/familia/types/unsorted_set.rb +123 -0
  37. data/lib/familia/utils.rb +129 -0
  38. data/lib/familia/version.rb +25 -0
  39. data/lib/familia.rb +56 -161
  40. data/lib/redis_middleware.rb +109 -0
  41. data/try/00_familia_try.rb +5 -4
  42. data/try/10_familia_try.rb +21 -17
  43. data/try/20_redis_type_try.rb +67 -0
  44. data/try/{21_redis_object_zset_try.rb → 21_redis_type_zset_try.rb} +2 -2
  45. data/try/{22_redis_object_set_try.rb → 22_redis_type_set_try.rb} +2 -2
  46. data/try/{23_redis_object_list_try.rb → 23_redis_type_list_try.rb} +2 -2
  47. data/try/{24_redis_object_string_try.rb → 24_redis_type_string_try.rb} +6 -6
  48. data/try/{25_redis_object_hash_try.rb → 25_redis_type_hash_try.rb} +3 -3
  49. data/try/26_redis_bool_try.rb +10 -6
  50. data/try/27_redis_horreum_try.rb +40 -0
  51. data/try/30_familia_object_try.rb +21 -20
  52. data/try/35_feature_safedump_try.rb +83 -0
  53. data/try/40_customer_try.rb +140 -0
  54. data/try/41_customer_safedump_try.rb +86 -0
  55. data/try/test_helpers.rb +186 -0
  56. metadata +50 -47
  57. data/lib/familia/helpers.rb +0 -70
  58. data/lib/familia/object.rb +0 -533
  59. data/lib/familia/redisobject.rb +0 -1017
  60. data/lib/familia/test_helpers.rb +0 -40
  61. data/lib/familia/tools.rb +0 -67
  62. data/try/20_redis_object_try.rb +0 -44
@@ -1,140 +1,135 @@
1
- # encoding: utf-8
2
- class Symbol
3
- unless method_defined?(:to_proc)
4
- def to_proc
5
- proc { |obj, *args| obj.send(self, *args) }
6
- end
7
- end
8
- end
1
+ # frozen_string_literal: true
9
2
 
10
- class Hash
11
- unless method_defined?(:to_json)
12
- def to_json
13
- MultiJson.encode self
14
- end
15
- def self.from_json str
16
- MultiJson.decode str
17
- end
18
- end
19
- end
20
- class Array
21
- unless method_defined?(:to_json)
22
- def to_json
23
- MultiJson.encode self
24
- end
25
- def self.from_json str
26
- MultiJson.decode str
27
- end
28
- end
29
- end
30
-
31
- # Assumes Time::Units and Numeric mixins are available.
3
+ # Extends the String class with time-related functionality
4
+ #
5
+ # This implementaton assumes Time::Units and Numeric mixins are available.
6
+ #
32
7
  class String
8
+ # Converts a string representation of time to seconds
9
+ #
10
+ # @example
11
+ # "60m".in_seconds #=> 3600.0
12
+ #
13
+ # @return [Float, nil] The time in seconds, or nil if the string is invalid
33
14
  def in_seconds
34
- # "60m" => ["60", "m"]
35
- q,u = self.scan(/([\d\.]+)([s,m,h])?/).flatten
15
+ q, u = scan(/([\d.]+)([smh])?/).flatten
36
16
  q &&= q.to_f and u ||= 's'
37
- q &&= q.in_seconds(u)
17
+ q&.in_seconds(u)
38
18
  end
39
19
  end
40
20
 
41
- unless defined?(Time::Units)
42
- class Time
43
- module Units
44
- PER_MICROSECOND = 0.000001.freeze
45
- PER_MILLISECOND = 0.001.freeze
46
- PER_MINUTE = 60.0.freeze
47
- PER_HOUR = 3600.0.freeze
48
- PER_DAY = 86400.0.freeze
49
-
50
- def microseconds() seconds * PER_MICROSECOND end
51
- def milliseconds() seconds * PER_MILLISECOND end
52
- def seconds() self end
53
- def minutes() seconds * PER_MINUTE end
54
- def hours() seconds * PER_HOUR end
55
- def days() seconds * PER_DAY end
56
- def weeks() seconds * PER_DAY * 7 end
57
- def years() seconds * PER_DAY * 365 end
58
-
59
- def in_years() seconds / PER_DAY / 365 end
60
- def in_weeks() seconds / PER_DAY / 7 end
61
- def in_days() seconds / PER_DAY end
62
- def in_hours() seconds / PER_HOUR end
63
- def in_minutes() seconds / PER_MINUTE end
64
- def in_milliseconds() seconds / PER_MILLISECOND end
65
- def in_microseconds() seconds / PER_MICROSECOND end
66
-
67
- def in_time
68
- Time.at(self).utc
69
- end
70
-
71
- def in_seconds(u=nil)
72
- case u.to_s
73
- when /\A(y)|(years?)\z/
74
- years
75
- when /\A(w)|(weeks?)\z/
76
- weeks
77
- when /\A(d)|(days?)\z/
78
- days
79
- when /\A(h)|(hours?)\z/
80
- hours
81
- when /\A(m)|(minutes?)\z/
82
- minutes
83
- when /\A(ms)|(milliseconds?)\z/
84
- milliseconds
85
- when /\A(us)|(microseconds?)|(μs)\z/
86
- microseconds
87
- else
88
- self
89
- end
90
- end
91
-
92
- ## JRuby doesn't like using instance_methods.select here.
93
- ## It could be a bug or something quirky with Attic
94
- ## (although it works in 1.8 and 1.9). The error:
95
- ##
96
- ## lib/attic.rb:32:in `select': yield called out of block (LocalJumpError)
97
- ## lib/stella/mixins/numeric.rb:24
98
- ##
99
- ## Create singular methods, like hour and day.
100
- # instance_methods.select.each do |plural|
101
- # singular = plural.to_s.chop
102
- # alias_method singular, plural
103
- # end
104
-
105
- alias_method :ms, :milliseconds
106
- alias_method :'μs', :microseconds
107
- alias_method :second, :seconds
108
- alias_method :minute, :minutes
109
- alias_method :hour, :hours
110
- alias_method :day, :days
111
- alias_method :week, :weeks
112
- alias_method :year, :years
21
+ # Extends the Time class with additional time unit functionality
22
+ class Time
23
+ # Provides methods for working with various time units
24
+ module Units
25
+ # rubocop:disable Style/SingleLineMethods, Layout/ExtraSpacing
113
26
 
114
- end
115
- end
27
+ PER_MICROSECOND = 0.000001
28
+ PER_MILLISECOND = 0.001
29
+ PER_MINUTE = 60.0
30
+ PER_HOUR = 3600.0
31
+ PER_DAY = 86_400.0
32
+
33
+ # Conversion methods
34
+ #
35
+ # From other time units -> seconds
36
+ #
37
+ def microseconds() seconds * PER_MICROSECOND end
38
+ def milliseconds() seconds * PER_MILLISECOND end
39
+ def seconds() self end
40
+ def minutes() seconds * PER_MINUTE end
41
+ def hours() seconds * PER_HOUR end
42
+ def days() seconds * PER_DAY end
43
+ def weeks() seconds * PER_DAY * 7 end
44
+ def years() seconds * PER_DAY * 365 end
116
45
 
117
- class Numeric
118
- include Time::Units
46
+ # From seconds -> other time units
47
+ #
48
+ def in_years() seconds / PER_DAY / 365 end
49
+ def in_weeks() seconds / PER_DAY / 7 end
50
+ def in_days() seconds / PER_DAY end
51
+ def in_hours() seconds / PER_HOUR end
52
+ def in_minutes() seconds / PER_MINUTE end
53
+ def in_milliseconds() seconds / PER_MILLISECOND end
54
+ def in_microseconds() seconds / PER_MICROSECOND end
119
55
 
120
- def to_ms
121
- (self*1000.to_f)
56
+ #
57
+ # Converts seconds to a Time object
58
+ #
59
+ # @return [Time] A Time object representing the seconds
60
+ def in_time
61
+ Time.at(self).utc
122
62
  end
123
-
124
- # TODO: Use 1024?
125
- def to_bytes
126
- args = case self.abs.to_i
127
- when (1000)..(1000**2)
128
- '%3.2f%s' % [(self / 1000.to_f).to_s, 'KB']
129
- when (1000**2)..(1000**3)
130
- '%3.2f%s' % [(self / (1000**2).to_f).to_s, 'MB']
131
- when (1000**3)..(1000**4)
132
- '%3.2f%s' % [(self / (1000**3).to_f).to_s, 'GB']
133
- when (1000**4)..(1000**6)
134
- '%3.2f%s' % [(self / (1000**4).to_f).to_s, 'TB']
63
+
64
+ # Converts seconds to the specified time unit
65
+ #
66
+ # @param u [String, Symbol] The unit to convert to (e.g., 'y', 'w', 'd', 'h', 'm', 'ms', 'us')
67
+ # @return [Float] The converted time value
68
+ def in_seconds(u = nil)
69
+ case u.to_s
70
+ when /\A(y)|(years?)\z/
71
+ years
72
+ when /\A(w)|(weeks?)\z/
73
+ weeks
74
+ when /\A(d)|(days?)\z/
75
+ days
76
+ when /\A(h)|(hours?)\z/
77
+ hours
78
+ when /\A(m)|(minutes?)\z/
79
+ minutes
80
+ when /\A(ms)|(milliseconds?)\z/
81
+ milliseconds
82
+ when /\A(us)|(microseconds?)|(μs)\z/
83
+ microseconds
135
84
  else
136
- [self.to_i, 'B'].join
85
+ self
137
86
  end
138
87
  end
88
+
89
+ # Starring Jennifer Garner, Victor Garber, and Carl Lumbly
90
+ alias ms milliseconds
91
+ alias μs microseconds
92
+ alias second seconds
93
+ alias minute minutes
94
+ alias hour hours
95
+ alias day days
96
+ alias week weeks
97
+ alias year years
98
+
99
+ # rubocop:enable Style/SingleLineMethods, Layout/ExtraSpacing
100
+ end
101
+ end
102
+
103
+ # Extends the Numeric class with time unit and byte conversion functionality
104
+ class Numeric
105
+ include Time::Units
106
+
107
+ # Converts the number to milliseconds
108
+ #
109
+ # @return [Float] The number in milliseconds
110
+ def to_ms
111
+ (self * 1000.to_f)
112
+ end
113
+
114
+ # Converts the number to a human-readable byte representation using binary units
115
+ #
116
+ # @return [String] A string representing the number in bytes, KiB, MiB, GiB, or TiB
117
+ #
118
+ # @example
119
+ # 1024.to_bytes #=> "1.00 KiB"
120
+ # 2_097_152.to_bytes #=> "2.00 MiB"
121
+ # 3_221_225_472.to_bytes #=> "3.00 GiB"
122
+ #
123
+ def to_bytes
124
+ units = %w[B KiB MiB GiB TiB]
125
+ size = abs.to_f
126
+ unit = 0
127
+
128
+ while size > 1024 && unit < units.length - 1
129
+ size /= 1024
130
+ unit += 1
131
+ end
132
+
133
+ format('%3.2f %s', size, units[unit])
139
134
  end
140
135
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Familia
4
+ class Problem < RuntimeError; end
5
+ class NoIdentifier < Problem; end
6
+ class NonUniqueKey < Problem; end
7
+
8
+ class HighRiskFactor < Problem
9
+ attr_reader :value
10
+
11
+ def initialize(value)
12
+ @value = value
13
+ super
14
+ end
15
+
16
+ def message
17
+ "High risk factor for serialization bugs: #{value}<#{value.class}>"
18
+ end
19
+ end
20
+
21
+ class NotConnected < Problem
22
+ attr_reader :uri
23
+
24
+ def initialize(uri)
25
+ @uri = uri
26
+ super
27
+ end
28
+
29
+ def message
30
+ "No client for #{uri.serverid}"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,8 @@
1
+ # rubocop:disable all
2
+
3
+ module Familia::Features
4
+ module AtomicSaves
5
+
6
+
7
+ end
8
+ end
@@ -0,0 +1,35 @@
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
@@ -0,0 +1,175 @@
1
+ # rubocop:disable all
2
+ # frozen_string_literal: true
3
+
4
+
5
+ module Familia::Features
6
+ # SafeDump is a mixin that allows models to define a list of fields that are
7
+ # safe to dump. This is useful for serializing objects to JSON or other
8
+ # formats where you want to ensure that only certain fields are exposed.
9
+ #
10
+ # To use SafeDump, include it in your model and define a list of fields that
11
+ # are safe to dump. The fields can be either symbols or hashes. If a field is
12
+ # a symbol, the method with the same name will be called on the object to
13
+ # retrieve the value. If the field is a hash, the key is the field name and
14
+ # the value is a lambda that will be called with the object as an argument.
15
+ # the hash syntax allows you to:
16
+ # * define a field name that is different from the method name
17
+ # * define a field that requires some computation on-the-fly
18
+ # * define a field that is not a method on the object
19
+ #
20
+ # Example:
21
+ #
22
+ # @safe_dump_fields = [
23
+ # :objid,
24
+ # :updated,
25
+ # :created,
26
+ # { :active => ->(obj) { obj.active? } }
27
+ # ]
28
+ #
29
+ # Internally, all fields are normalized to the hash syntax and store in
30
+ # @safe_dump_field_map. `SafeDump.safe_dump_fields` returns only the list
31
+ # of symbols in the order they were defined. From the example above, it would
32
+ # return `[:objid, :updated, :created, :active]`.
33
+ #
34
+ module SafeDump
35
+ @dump_method = :to_json
36
+ @load_method = :from_json
37
+
38
+ @safe_dump_fields = []
39
+ @safe_dump_field_map = {}
40
+
41
+ module ClassMethods
42
+ def set_safe_dump_fields(*fields)
43
+ @safe_dump_fields = fields
44
+ end
45
+
46
+ # `SafeDump.safe_dump_fields` returns only the list
47
+ # of symbols in the order they were defined.
48
+ def safe_dump_fields
49
+ @safe_dump_fields.map do |field|
50
+ field.is_a?(Symbol) ? field : field.keys.first
51
+ end
52
+ end
53
+
54
+ # `SafeDump.safe_dump_field_map` returns the field map
55
+ # that is used to dump the fields. The keys are the
56
+ # field names and the values are callables that will
57
+ # expect to receive the instance object as an argument.
58
+ #
59
+ # The map is cached on the first call to this method.
60
+ #
61
+ def safe_dump_field_map
62
+ return @safe_dump_field_map if @safe_dump_field_map.any?
63
+
64
+ # Operate directly on the @safe_dump_fields array to
65
+ # build the map. This way we'll get the elements defined
66
+ # in the hash syntax (i.e. since the safe_dump_fields getter
67
+ # method returns only the symbols).
68
+ @safe_dump_field_map = @safe_dump_fields.each_with_object({}) do |el, map|
69
+ if el.is_a?(Symbol)
70
+ field_name = el
71
+ callable = lambda { |obj|
72
+ if obj.respond_to?(:[]) && obj[field_name]
73
+ obj[field_name] # Familia::RedisType classes
74
+ elsif obj.respond_to?(field_name)
75
+ obj.send(field_name) # Onetime::Models::RedisHash classes via method_missing 😩
76
+ end
77
+ }
78
+ else
79
+ field_name = el.keys.first
80
+ callable = el.values.first
81
+ end
82
+ map[field_name] = callable
83
+ end
84
+ end
85
+ end
86
+
87
+ def self.included base
88
+ Familia.ld "[Feature] Enabling SafeDump for #{base})"
89
+ base.extend ClassMethods
90
+
91
+ # Optionally define safe_dump_fields in the class to make
92
+ # sure we always have an array to work with.
93
+ unless base.instance_variable_defined?(:@safe_dump_fields)
94
+ base.instance_variable_set(:@safe_dump_fields, [])
95
+ end
96
+
97
+ # Ditto for the field map
98
+ unless base.instance_variable_defined?(:@safe_dump_field_map)
99
+ base.instance_variable_set(:@safe_dump_field_map, {})
100
+ end
101
+ end
102
+
103
+ # Returns a hash of safe fields and their values. This method
104
+ # calls the callables defined in the safe_dump_field_map with
105
+ # the instance object as an argument.
106
+ #
107
+ # The return values are not cached, so if you call this method
108
+ # multiple times, the callables will be called each time.
109
+ #
110
+ # Example:
111
+ #
112
+ # class Customer < Familia::HashKey
113
+ # include SafeDump
114
+ # @safe_dump_fields = [
115
+ # :name,
116
+ # { :active => ->(cust) { cust.active? } }
117
+ # ]
118
+ #
119
+ # def active?
120
+ # true # or false
121
+ # end
122
+ #
123
+ # cust = Customer.new :name => 'Lucy'
124
+ # cust.safe_dump
125
+ # #=> { :name => 'Lucy', :active => true }
126
+ #
127
+ def safe_dump
128
+ self.class.safe_dump_field_map.transform_values do |callable|
129
+ transformed_value = callable.call(self)
130
+
131
+ # If the value is a relative ancestor of SafeDump we can
132
+ # call safe_dump on it, otherwise we'll just return the value as-is.
133
+ if transformed_value.is_a?(SafeDump)
134
+ transformed_value.safe_dump
135
+ else
136
+ transformed_value
137
+ end
138
+ end
139
+ end
140
+
141
+ extend ClassMethods
142
+
143
+ Familia::Base.add_feature self, :safe_dump
144
+ end
145
+
146
+ end
147
+
148
+
149
+ __END__
150
+
151
+ if value_to_distunguish.is_a?(Familia::Horreum)
152
+ Familia.trace :DISTINGUISHER, redis, "horreum", caller(1..1) if Familia.debug?
153
+ value_to_distunguish.identifier
154
+ elsif dump_method && value_to_distunguish.respond_to?(dump_method)
155
+ Familia.trace :DISTINGUISHER, redis, "#{value_to_distunguish.class}##{dump_method}", caller(1..1) if Familia.debug?
156
+ value_to_distunguish.send(dump_method)
157
+ else
158
+ if dump_method
159
+ msg = if dump_method.to_s.empty?
160
+ "No dump_method available for #{value_to_distunguish.class}"
161
+ else
162
+ "No such method: #{value_to_distunguish.class}##{dump_method}"
163
+ end
164
+ raise Familia::Problem, msg
165
+ else
166
+ Familia.trace :DISTINGUISHER, redis, "else", caller(1..1) if Familia.debug?
167
+ nil
168
+ end
169
+ end
170
+
171
+
172
+ if ret.nil? && dump_method && val.respond_to?(dump_method)
173
+ Familia.trace :TOREDIS, redis, "#{val.class}##{dump_method}", caller(1..1) if Familia.debug?
174
+ val.send dump_method
175
+ end
@@ -0,0 +1,51 @@
1
+ # rubocop:disable all
2
+
3
+ module Familia
4
+
5
+ @features_enabled = nil
6
+
7
+ module Features
8
+
9
+ attr_reader :features_enabled
10
+
11
+ def feature(val = nil)
12
+ @features_enabled ||= []
13
+
14
+ Familia.ld "[Familia::Settings] feature: #{val.inspect}"
15
+
16
+ # If there's a value provied check that it's a valid feature
17
+ if val
18
+ val = val.to_sym
19
+ raise Familia::Problem, "Unsupported feature: #{val}" unless Familia::Base.features.key?(val)
20
+
21
+ # If the feature is already enabled, do nothing but log about it
22
+ if @features_enabled.member?(val)
23
+ Familia.warn "[Familia::Settings] feature already enabled: #{val}"
24
+ return
25
+ end
26
+
27
+ klass = Familia::Base.features[val]
28
+
29
+ # Extend the Familia::Base subclass (e.g. Customer) with the feature module
30
+ include klass
31
+
32
+ # NOTE: We may also want to extend Familia::RedisType here so that we can
33
+ # call safe_dump on relations fields (e.g. list, set, zset, hashkey). Or
34
+ # maybe that only makes sense for hashk/object relations.
35
+ #
36
+ # We'd need to avoid it getting included multiple times (i.e. once for each
37
+ # Familia::Horreum subclass that includes the feature).
38
+
39
+ # Now that the feature is loaded successfully, add it to the list
40
+ # enabled features for Familia::Base classes.
41
+ @features_enabled << val
42
+ end
43
+
44
+ features_enabled
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+
51
+ require_relative 'features/safe_dump'