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
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Familia
4
+ class String < RedisType
5
+ def init; end
6
+
7
+ def size
8
+ to_s.size
9
+ end
10
+ alias length size
11
+
12
+ def empty?
13
+ size.zero?
14
+ end
15
+
16
+ def value
17
+ echo :value, caller[0..0] if Familia.debug
18
+ redis.setnx rediskey, @opts[:default] if @opts[:default]
19
+ from_redis redis.get(rediskey)
20
+ end
21
+ alias content value
22
+ alias get value
23
+
24
+ def to_s
25
+ value.to_s # value can return nil which to_s should not
26
+ end
27
+
28
+ def to_i
29
+ value.to_i
30
+ end
31
+
32
+ def value=(val)
33
+ ret = redis.set(rediskey, to_redis(val))
34
+ update_expiration
35
+ ret
36
+ end
37
+ alias replace value=
38
+ alias set value=
39
+
40
+ def setnx(val)
41
+ ret = redis.setnx(rediskey, to_redis(val))
42
+ update_expiration
43
+ ret
44
+ end
45
+
46
+ def increment
47
+ ret = redis.incr(rediskey)
48
+ update_expiration
49
+ ret
50
+ end
51
+ alias incr increment
52
+
53
+ def incrementby(val)
54
+ ret = redis.incrby(rediskey, val.to_i)
55
+ update_expiration
56
+ ret
57
+ end
58
+ alias incrby incrementby
59
+
60
+ def decrement
61
+ ret = redis.decr rediskey
62
+ update_expiration
63
+ ret
64
+ end
65
+ alias decr decrement
66
+
67
+ def decrementby(val)
68
+ ret = redis.decrby rediskey, val.to_i
69
+ update_expiration
70
+ ret
71
+ end
72
+ alias decrby decrementby
73
+
74
+ def append(val)
75
+ ret = redis.append rediskey, val
76
+ update_expiration
77
+ ret
78
+ end
79
+ alias << append
80
+
81
+ def getbit(offset)
82
+ redis.getbit rediskey, offset
83
+ end
84
+
85
+ def setbit(offset, val)
86
+ ret = redis.setbit rediskey, offset, val
87
+ update_expiration
88
+ ret
89
+ end
90
+
91
+ def getrange(spoint, epoint)
92
+ redis.getrange rediskey, spoint, epoint
93
+ end
94
+
95
+ def setrange(offset, val)
96
+ ret = redis.setrange rediskey, offset, val
97
+ update_expiration
98
+ ret
99
+ end
100
+
101
+ def getset(val)
102
+ ret = redis.getset rediskey, val
103
+ update_expiration
104
+ ret
105
+ end
106
+
107
+ def nil?
108
+ value.nil?
109
+ end
110
+
111
+ Familia::RedisType.register self, :string
112
+ Familia::RedisType.register self, :counter
113
+ Familia::RedisType.register self, :lock
114
+ end
115
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Familia
4
+ class Set < RedisType
5
+ def size
6
+ redis.scard rediskey
7
+ end
8
+ alias length size
9
+
10
+ def empty?
11
+ size.zero?
12
+ end
13
+
14
+ def add *values
15
+ values.flatten.compact.each { |v| redis.sadd? rediskey, to_redis(v) }
16
+ update_expiration
17
+ self
18
+ end
19
+
20
+ def <<(v)
21
+ add v
22
+ end
23
+
24
+ def members
25
+ echo :members, caller(1..1).first if Familia.debug
26
+ el = membersraw
27
+ multi_from_redis(*el)
28
+ end
29
+ alias all members
30
+ alias to_a members
31
+
32
+ def membersraw
33
+ redis.smembers(rediskey)
34
+ end
35
+
36
+ def each(&blk)
37
+ members.each(&blk)
38
+ end
39
+
40
+ def each_with_index(&blk)
41
+ members.each_with_index(&blk)
42
+ end
43
+
44
+ def collect(&blk)
45
+ members.collect(&blk)
46
+ end
47
+
48
+ def select(&blk)
49
+ members.select(&blk)
50
+ end
51
+
52
+ def eachraw(&blk)
53
+ membersraw.each(&blk)
54
+ end
55
+
56
+ def eachraw_with_index(&blk)
57
+ membersraw.each_with_index(&blk)
58
+ end
59
+
60
+ def collectraw(&blk)
61
+ membersraw.collect(&blk)
62
+ end
63
+
64
+ def selectraw(&blk)
65
+ membersraw.select(&blk)
66
+ end
67
+
68
+ def member?(val)
69
+ redis.sismember rediskey, to_redis(val)
70
+ end
71
+ alias include? member?
72
+
73
+ def delete(val)
74
+ redis.srem rediskey, to_redis(val)
75
+ end
76
+ alias remove delete
77
+ alias rem delete
78
+ alias del delete
79
+
80
+ def intersection *setkeys
81
+ # TODO
82
+ end
83
+
84
+ def pop
85
+ redis.spop rediskey
86
+ end
87
+
88
+ def move(dstkey, val)
89
+ redis.smove rediskey, dstkey, val
90
+ end
91
+
92
+ def random
93
+ from_redis randomraw
94
+ end
95
+
96
+ def randomraw
97
+ redis.srandmember(rediskey)
98
+ end
99
+
100
+ ## Make the value stored at KEY identical to the given list
101
+ # define_method :"#{name}_sync" do |*latest|
102
+ # latest = latest.flatten.compact
103
+ # # Do nothing if we're given an empty Array.
104
+ # # Otherwise this would clear all current values
105
+ # if latest.empty?
106
+ # false
107
+ # else
108
+ # # Convert to a list of index values if we got the actual objects
109
+ # latest = latest.collect { |obj| obj.index } if klass === latest.first
110
+ # current = send("#{name_plural}raw")
111
+ # added = latest-current
112
+ # removed = current-latest
113
+ # #Familia.info "#{self.index}: adding: #{added}"
114
+ # added.each { |v| self.send("add_#{name_singular}", v) }
115
+ # #Familia.info "#{self.index}: removing: #{removed}"
116
+ # removed.each { |v| self.send("remove_#{name_singular}", v) }
117
+ # true
118
+ # end
119
+ # end
120
+
121
+ Familia::RedisType.register self, :set
122
+ end
123
+ end
@@ -0,0 +1,129 @@
1
+ # rubocop:disable all
2
+
3
+ require 'securerandom'
4
+
5
+ module Familia
6
+ DIGEST_CLASS = Digest::SHA256
7
+
8
+ module Utils
9
+
10
+ def debug?
11
+ @debug == true
12
+ end
13
+
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
18
+
19
+ def join(*val)
20
+ val.compact.join(Familia.delim)
21
+ end
22
+
23
+ def split(val)
24
+ val.split(Familia.delim)
25
+ end
26
+
27
+ def rediskey(*val)
28
+ join(*val)
29
+ end
30
+
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
47
+
48
+ def now(name = Time.now)
49
+ name.utc.to_f
50
+ end
51
+
52
+ # A quantized timestamp
53
+ # e.g. 12:32 -> 12:30
54
+ #
55
+ def qnow(quantum = 10.minutes, now = Familia.now)
56
+ rounded = now - (now % quantum)
57
+ Time.at(rounded).utc.to_i
58
+ end
59
+
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)
65
+ end
66
+
67
+ def generate_sha_hash(elements)
68
+ concatenated_string = Familia.join(elements)
69
+ DIGEST_CLASS.hexdigest(concatenated_string)
70
+ end
71
+
72
+ # This method determines the appropriate value to return based on the class of the input argument.
73
+ # It uses a case statement to handle different classes:
74
+ # - For Symbol, String, Integer, and Float classes, it traces the operation and converts the value to a string.
75
+ # - For Familia::Horreum class, it traces the operation and returns the identifier of the value.
76
+ # - For TrueClass, FalseClass, and NilClass, it traces the operation and converts the value to a string ("true", "false", or "").
77
+ # - For any other class, it traces the operation and returns nil.
78
+ #
79
+ # Alternative names for `value_to_distinguish` could be `input_value`, `value`, or `object`.
80
+ def distinguisher(value_to_distinguish, strict_values = true)
81
+ case value_to_distinguish
82
+ when ::Symbol, ::String, ::Integer, ::Float
83
+ Familia.trace :TOREDIS_DISTINGUISHER, redis, "string", caller(1..1) if Familia.debug?
84
+ # Symbols and numerics are naturally serializable to strings
85
+ # so it's a relatively low risk operation.
86
+ value_to_distinguish.to_s
87
+
88
+ when ::TrueClass, ::FalseClass, ::NilClass
89
+ Familia.trace :TOREDIS_DISTINGUISHER, redis, "true/false/nil", caller(1..1) if Familia.debug?
90
+ # TrueClass, FalseClass, and NilClass are high risk because we can't
91
+ # reliably determine the original type of the value from the serialized
92
+ # string. This can lead to unexpected behavior when deserializing. For
93
+ # example, if a TrueClass value is serialized as "true" and then later
94
+ # deserialized as a String, it can cause errors in the application. Worse
95
+ # still, if a NilClass value is serialized as an empty string we lose the
96
+ # ability to distinguish between a nil value and an empty string when
97
+ #
98
+ raise Familia::HighRiskFactor, value_to_distinguish if strict_values
99
+ value_to_distinguish.to_s #=> "true", "false", ""
100
+
101
+ when Familia::Base, Class
102
+ Familia.trace :TOREDIS_DISTINGUISHER, redis, "base", caller(1..1) if Familia.debug?
103
+ if value_to_distinguish.is_a?(Class)
104
+ value_to_distinguish.name
105
+ else
106
+ value_to_distinguish.identifier
107
+ end
108
+
109
+ else
110
+ Familia.trace :TOREDIS_DISTINGUISHER, redis, "else1 #{strict_values}", caller(1..1) if Familia.debug?
111
+
112
+ if value_to_distinguish.class.ancestors.member?(Familia::Base)
113
+ Familia.trace :TOREDIS_DISTINGUISHER, redis, "isabase", caller(1..1) if Familia.debug?
114
+ value_to_distinguish.identifier
115
+
116
+ elsif dump_method && value_to_distinguish.respond_to?(dump_method)
117
+ Familia.trace :TOREDIS_DISTINGUISHER, redis, "#{value_to_distinguish.class}##{dump_method}", caller(1..1) if Familia.debug?
118
+ value_to_distinguish.send(dump_method)
119
+
120
+ else
121
+ Familia.trace :TOREDIS_DISTINGUISHER, redis, "else2 #{strict_values}", caller(1..1) if Familia.debug?
122
+ raise Familia::HighRiskFactor, value_to_distinguish if strict_values
123
+ nil
124
+ end
125
+ end
126
+ end
127
+
128
+ end
129
+ end
@@ -0,0 +1,25 @@
1
+ # rubocop:disable all
2
+
3
+ require 'yaml'
4
+
5
+ module Familia
6
+ module VERSION
7
+ def self.to_s
8
+ load_config
9
+ version = [@version[:MAJOR], @version[:MINOR], @version[:PATCH]].join('.')
10
+ version += "-#{@version[:PRE]}" if @version[:PRE]
11
+ version
12
+ end
13
+ alias inspect to_s
14
+
15
+ def self.version
16
+ @version ||= load_config
17
+ @version
18
+ end
19
+
20
+ def self.load_config
21
+ version_file_path = File.join(__dir__, '..', '..', 'VERSION.yml')
22
+ @version = YAML.load_file(version_file_path)
23
+ end
24
+ end
25
+ end
data/lib/familia.rb CHANGED
@@ -1,177 +1,72 @@
1
- # encoding: utf-8
2
- FAMILIA_LIB_HOME = File.expand_path File.dirname(__FILE__) unless defined?(FAMILIA_LIB_HOME)
1
+ # rubocop:disable all
2
+ # frozen_string_literal: true
3
+
4
+ require 'redis'
3
5
  require 'uri/redis'
4
- require 'gibbler'
5
- require 'familia/core_ext'
6
- require 'multi_json'
7
6
 
8
- module Familia
9
- module VERSION
10
- def self.to_s
11
- load_config
12
- [@version[:MAJOR], @version[:MINOR], @version[:PATCH]].join('.')
13
- end
14
- alias_method :inspect, :to_s
15
- def self.load_config
16
- require 'yaml'
17
- @version ||= YAML.load_file(File.join(FAMILIA_LIB_HOME, '..', 'VERSION.yml'))
18
- end
19
- end
20
- end
7
+ require_relative 'familia/core_ext'
8
+ require_relative 'familia/errors'
9
+ require_relative 'familia/version'
21
10
 
11
+ # Familia - A family warehouse for Redis
12
+ #
13
+ # Familia provides a way to organize and store Ruby objects in Redis.
14
+ # It includes various modules and classes to facilitate object-Redis interactions.
15
+ #
16
+ # @example Basic usage
17
+ # class Flower < Familia::Horreum
18
+ #
19
+ # identifier :my_identifier_method
20
+ # field :token
21
+ # field :name
22
+ # list :owners
23
+ # set :tags
24
+ # zset :metrics
25
+ # hash :props
26
+ # string :value, :default => "GREAT!"
27
+ # end
28
+ #
29
+ # @see https://github.com/delano/familia
30
+ #
22
31
  module Familia
23
- include Gibbler::Complex
24
- @secret = '1-800-AWESOME' # Should be modified via Familia.secret = ''
25
- @apiversion = nil
26
- @uri = URI.parse 'redis://127.0.0.1'
27
- @delim = ':'
28
- @clients = {}
29
- @classes = []
30
- @suffix = :object.freeze
31
- @index = :id.freeze
32
+
32
33
  @debug = false
33
- @dump_method = :to_json
34
- @load_method = :from_json
34
+ @members = []
35
35
 
36
36
  class << self
37
- attr_reader :clients, :uri, :logger
38
- attr_accessor :debug, :secret, :delim, :dump_method, :load_method
39
- attr_writer :apiversion
37
+ attr_accessor :debug
38
+ attr_reader :members
40
39
 
41
- alias_method :url, :uri
42
- def debug?() @debug == true end
43
- def info *msg
44
- STDERR.puts *msg
45
- end
46
- def classes with_redis_objects=false
47
- with_redis_objects ? [@classes, RedisObject.classes].flatten : @classes
48
- end
49
- def ld *msg
50
- info *msg if debug?
51
- end
52
- def trace label, redis_instance, ident, context=nil
53
- return unless Familia.debug?
54
- codeline = if context
55
- context = [context].flatten
56
- context.reject! { |line| line =~ /lib\/familia/ }
57
- context.first
58
- end
59
- info "[%s] -> %s <- %s %s" % [label, codeline, redis_instance.id, ident]
60
- end
61
- def uri= v
62
- v = URI.parse v unless URI === v
63
- @uri = v
64
- end
65
- # A convenience method for returning the appropriate Redis
66
- # connection. If +uri+ is an Integer, we'll treat it as a
67
- # database number. If it's a String, we'll treat it as a
68
- # full URI (e.g. redis://1.2.3.4/15).
69
- # Otherwise we'll return the default connection.
70
- def redis(uri=nil)
71
- if Integer === uri
72
- tmp = Familia.uri
73
- tmp.db = uri
74
- uri = tmp
75
- elsif String === uri
76
- uri &&= URI.parse uri
77
- end
78
- uri ||= Familia.uri
79
- connect(uri) unless @clients[uri.serverid]
80
- @clients[uri.serverid]
81
- end
82
- def log(level, path)
83
- logger = Log4r::Logger.new('familia')
84
- logger.outputters = Log4r::FileOutputter.new 'familia', :filename => path
85
- logger.level = Log4r.const_get(level)
86
- logger
87
- end
88
- def connect(uri=nil)
89
- uri &&= URI.parse uri if String === uri
90
- uri ||= Familia.uri
91
- conf = uri.conf
92
- redis = Redis.new conf
93
- Familia.trace(:CONNECT, redis, conf.inspect, caller[0..3])
94
- @clients[uri.serverid] = redis
95
- end
96
- def reconnect_all!
97
- Familia.classes.each do |klass|
98
- klass.redis.client.reconnect
99
- Familia.info "#{klass} ping: #{klass.redis.ping}" if debug?
100
- end
101
- end
102
- def connected?(uri=nil)
103
- uri &&= URI.parse uri if String === uri
104
- @clients.has_key?(uri.serverid)
105
- end
106
- def default_suffix(a=nil) @suffix = a if a; @suffix end
107
- def default_suffix=(a) @suffix = a end
108
- def index(r=nil) @index = r if r; @index end
109
- def index=(r) @index = r; r end
110
- def join(*r) r.join(Familia.delim) end
111
- def split(r) r.split(Familia.delim) end
112
- def rediskey *args
113
- el = args.flatten.compact
114
- el.unshift @apiversion unless @apiversion.nil?
115
- el.join(Familia.delim)
40
+ def included(member)
41
+ raise Problem, "#{member} should subclass Familia::Horreum"
116
42
  end
117
- def apiversion(r=nil, &blk)
118
- if blk.nil?
119
- @apiversion = r if r;
120
- else
121
- tmp = @apiversion
122
- @apiversion = r
123
- blk.call
124
- @apiversion = tmp
125
- end
126
- @apiversion
127
- end
128
- def now n=Time.now
129
- n.utc.to_i
130
- end
131
- # A quantized timestamp
132
- # e.g. 12:32 -> 12:30
43
+
44
+ # A convenience pattern for configuring Familia.
45
+ #
46
+ # @example
47
+ # Familia.configure do |config|
48
+ # config.debug = true
49
+ # config.enable_redis_logging = true
50
+ # end
51
+ #
133
52
  #
134
- def qnow quantum=10.minutes, now=Familia.now
135
- rounded = now - (now % quantum)
136
- Time.at(rounded).utc.to_i
53
+ def configure
54
+ yield self
137
55
  end
138
56
  end
139
57
 
140
- class Problem < RuntimeError; end
141
- class NoIndex < Problem; end
142
- class NonUniqueKey < Problem; end
143
- class NotConnected < Problem
144
- attr_reader :uri
145
- def initialize uri
146
- @uri = uri
147
- end
148
- def message
149
- "No client for #{uri.serverid}"
150
- end
151
- end
152
-
153
- def self.included(obj)
154
- obj.send :include, Familia::InstanceMethods
155
- obj.send :include, Gibbler::Complex
156
- obj.extend Familia::ClassMethods
157
- obj.class_zset :instances, :class => obj, :reference => true
158
- Familia.classes << obj
159
- end
160
-
161
- require 'familia/object'
162
- require 'familia/helpers'
58
+ require_relative 'familia/logging'
59
+ require_relative 'familia/connection'
60
+ require_relative 'familia/settings'
61
+ require_relative 'familia/utils'
163
62
 
63
+ extend Logging
64
+ extend Connection
65
+ extend Settings
66
+ extend Utils
164
67
  end
165
68
 
166
-
167
- module Familia
168
- module Collector
169
- def klasses
170
- @klasses ||= []
171
- @klasses
172
- end
173
- def included(obj)
174
- self.klasses << obj
175
- end
176
- end
177
- end
69
+ require_relative 'familia/base'
70
+ require_relative 'familia/features'
71
+ require_relative 'familia/redistype'
72
+ require_relative 'familia/horreum'