conflow 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Conflow
4
+ module Redis
5
+ # Findable module allows to use .find method on models with identifiers. It requires additional field: :type
6
+ module Findable
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ base.field :type, :value
10
+ end
11
+
12
+ def initialize(*args)
13
+ super
14
+ self.type = self.class.name
15
+ end
16
+
17
+ # Adds .find method which accepts ID and returns model of proper (sub)type
18
+ module ClassMethods
19
+ def find(id)
20
+ class_name = ValueField.new(format(key_template + ":type", id: id)).value
21
+ raise ::Redis::CommandError, "#{name} with ID #{id} doesn't exist" unless class_name
22
+
23
+ Object.const_get(class_name).new(id)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Conflow
4
+ module Redis
5
+ # Represents Redis hash. It's methods mirror most used Hash methods.
6
+ class HashField < Field
7
+ include Enumerable
8
+
9
+ def [](field)
10
+ value = command(:hget, [key, field])
11
+ value ? JSON.parse(value) : value
12
+ end
13
+
14
+ def []=(field, value)
15
+ command :hset, [key, field, JSON.dump(value)]
16
+ end
17
+
18
+ def merge(hash)
19
+ command :hmset, [key, hash.flatten]
20
+ end
21
+
22
+ def delete(*fields)
23
+ command :hdel, [key, fields]
24
+ end
25
+
26
+ def overwrite(new_hash)
27
+ redis.with do |conn|
28
+ conn.pipelined do
29
+ conn.del(key)
30
+ conn.hmset(key, prepare_hash(new_hash).flatten)
31
+ end
32
+ end
33
+ end
34
+
35
+ def keys
36
+ command(:hkeys, [key]).map(&:to_sym)
37
+ end
38
+
39
+ def size
40
+ command :hlen, [key]
41
+ end
42
+
43
+ def to_h
44
+ command(:hgetall, [key]).each_with_object({}) do |(key, value), hash|
45
+ hash[key.to_sym] = JSON.parse(value)
46
+ end
47
+ end; alias to_hash to_h
48
+
49
+ def each(&block)
50
+ to_h.each(&block)
51
+ end
52
+
53
+ def ==(other)
54
+ case other
55
+ when Hash then to_h == other
56
+ when HashField then key == other.key || to_h == other.to_h
57
+ else super
58
+ end
59
+ end
60
+
61
+ def to_s
62
+ to_h.to_s
63
+ end
64
+
65
+ private
66
+
67
+ def prepare_hash(hash)
68
+ hash.each_with_object({}) do |(k, v), h|
69
+ h[k] = v && JSON.dump(v)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Conflow
4
+ module Redis
5
+ # Identifier changes logic of fields so that they can be found by an id.
6
+ # ID is a counter stored in redis under .counter_key
7
+ # Key is build with template stored in .key_template
8
+ module Identifier
9
+ # Extends base class with {ClassMethods}
10
+ def self.included(base)
11
+ base.extend ClassMethods
12
+ end
13
+
14
+ # class methods for classes with identifier
15
+ module ClassMethods
16
+ attr_writer :counter_key, :key_template
17
+
18
+ # Copies *counter_key* and *key_template* to child classes
19
+ def inherited(base)
20
+ base.instance_variable_set("@counter_key", counter_key)
21
+ base.instance_variable_set("@key_template", key_template)
22
+ super
23
+ end
24
+
25
+ # Redis key holding counter with IDs of model.
26
+ # @example default key
27
+ # class My::Super::Class < Conflow::Redis::Field
28
+ # include Conflow::Redis::Identifier
29
+ # end
30
+ #
31
+ # My::Super::Class.counter_key #=> "my:super:class:idcnt"
32
+ # @return [String] Redis key
33
+ def counter_key
34
+ @counter_key ||= [*name.downcase.split("::"), :idcnt].join(":")
35
+ end
36
+
37
+ # Template for building keys for fields using only the ID.
38
+ # @example default key
39
+ # class My::Super::Class < Conflow::Redis::Field
40
+ # include Conflow::Redis::Identifier
41
+ # end
42
+ #
43
+ # My::Super::Class.key_template #=> "my:super:class:%<id>d"
44
+ # @return [String] Template for building redis keys
45
+ def key_template
46
+ @key_template ||= [*name.downcase.split("::"), "%<id>d"].join(":")
47
+ end
48
+
49
+ # @return [Integer] next available ID
50
+ def generate_id
51
+ Conflow.redis.with { |conn| conn.incr(counter_key) }
52
+ end
53
+ end
54
+
55
+ # ID of the model
56
+ attr_reader :id
57
+
58
+ # Overrides logic of {Field#initialize}, allowing creating objects with ID instead of full key
59
+ # @param id [Integer] ID of the model
60
+ def initialize(id = self.class.generate_id)
61
+ @id = id.to_i
62
+ super(format(self.class.key_template, id: id))
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Conflow
4
+ module Redis
5
+ # Models adds .field method which allows to define fields easily.
6
+ module Model
7
+ # Extends base class with .field and .has_many methods
8
+ def self.included(base)
9
+ base.extend(ClassMethods)
10
+ end
11
+
12
+ # - if other object is a {Model} as well, it compares keys
13
+ # - standard Ruby comparison otherwise
14
+ # @return [Boolean] true if other is the same model, false otherwise
15
+ def ==(other)
16
+ other.is_a?(Model) ? key == other.key : super
17
+ end
18
+
19
+ # Methods for defining fields on model
20
+ module ClassMethods
21
+ # Defines Redis field accessors.
22
+ # @param name [Symbol] name of the field
23
+ # @param type [:hash, :array, :value, :sorted_set] type of the new field
24
+ #
25
+ # @see Conflow::Redis::HashField
26
+ # @see Conflow::Redis::ArrayField
27
+ # @see Conflow::Redis::ValueField
28
+ # @see Conflow::Redis::SortedSetField
29
+ # @example
30
+ # model_class.field :data, :hash
31
+ # instance = model_class.new
32
+ # instance.hash["something"] = 800
33
+ # instance.hash = { something: "else"}
34
+ # instance.hash["something"] #=> "else"
35
+ def field(name, type)
36
+ case type
37
+ when :hash then FieldBuilder.new(name, Conflow::Redis::HashField).call(self)
38
+ when :array then FieldBuilder.new(name, Conflow::Redis::ArrayField).call(self)
39
+ when :value then FieldBuilder.new(name, Conflow::Redis::ValueField).call(self)
40
+ when :sorted_set then FieldBuilder.new(name, Conflow::Redis::SortedSetField).call(self)
41
+ else raise ArgumentError, "Unknown type: #{type}. Should be one of: [:hash, :array]"
42
+ end
43
+ end
44
+
45
+ # Convienience method for defining relation-like accessor.
46
+ # @example
47
+ # has_many :jobs, Conflow::Job # defines #job_ids and #jobs
48
+ def has_many(name, klass, field_name: "#{name.to_s.chop}_ids")
49
+ field(field_name, :array)
50
+ define_method(name) { send(field_name).map { |id| klass.new(id) } }
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Conflow
4
+ module Redis
5
+ # Main class for scripts, handling logic of executing and caching scripts.
6
+ class Script
7
+ class << self
8
+ # @return [Boolean] whether scripts are cached or not
9
+ attr_reader :cache_scripts
10
+ # @return [String] LUA script of this Conflow::Redis::Script
11
+ attr_reader :script
12
+
13
+ # Sets cache_scripts option on inherited scripts
14
+ def inherited(base)
15
+ scripts << base
16
+ base.cache_scripts = cache_scripts
17
+ super
18
+ end
19
+
20
+ # @!attribute [rw] cache_scripts
21
+ # This options decides whether scripts used by the gem will be cached in Redis or not.
22
+ # See {https://redis.io/commands/eval Redis EVAL} and {https://redis.io/commands/evalsha Redis EVALSHA}.
23
+ # @example Disable caching scripts (set this in your initializer)
24
+ # Conflow::Redis::Script.cache_scripts = false
25
+ def cache_scripts=(value)
26
+ @cache_scripts = value
27
+
28
+ @command = value ? :sha_eval : :eval
29
+ scripts.each { |script_class| script_class.cache_scripts = cache_scripts }
30
+ end
31
+
32
+ # Executes script in Redis with given arguments.
33
+ #
34
+ # @overload call(keys, args = [])
35
+ # @param keys [Array<String>] Array of keys
36
+ # @param args [Array<Object>] Array of arguments of the script
37
+ def call(*args)
38
+ Conflow.redis.with { |conn| send(command, conn, args) }
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :command, :sha
44
+
45
+ def script=(script)
46
+ @script = script
47
+ @sha = Digest::SHA1.hexdigest(script)
48
+ end
49
+
50
+ def scripts
51
+ @scripts ||= []
52
+ end
53
+
54
+ def sha_eval(redis, args)
55
+ redis.evalsha(sha, *args)
56
+ rescue ::Redis::CommandError => e
57
+ raise unless e.message == "NOSCRIPT No matching script. Please use EVAL."
58
+ redis.script(:load, script)
59
+ retry
60
+ end
61
+
62
+ def eval(redis, args)
63
+ redis.eval(script, *args)
64
+ end
65
+ end
66
+
67
+ self.cache_scripts = true
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Conflow
4
+ module Redis
5
+ # Represents Redis sorted set. Closest Ruby representation is a Hash
6
+ # where keys are elements of the set and values represent score.
7
+ class SortedSetField < Field
8
+ # Adds one or more keys to the set.
9
+ # @param hash [Hash] hash of values and scores to be added
10
+ # @return [String] Redis response
11
+ #
12
+ # @example Adding multiple fields
13
+ # field.add(last: 10, tied: 2, second: 4, first: 2)
14
+ def add(hash)
15
+ command :zadd, [key, hash_to_array(hash)]
16
+ end
17
+
18
+ # Access score of given element.
19
+ # @param value [String, Symbol] element of the set
20
+ # @return [String] Score of the element (nil if element not present in set)
21
+ #
22
+ # @example
23
+ # field[:last] #=> 10
24
+ def [](value)
25
+ command :zscore, [key, value]
26
+ end
27
+
28
+ # Set score of given element.
29
+ # @param value [String, Symbol] element of the set
30
+ # @param rank [Numeric] score to be assigned
31
+ # @return [Integer] Number of added elements (1 if key didn't exist, 0 otherwise)
32
+ #
33
+ # @example
34
+ # field[:last] = 24 #=> 0
35
+ def []=(value, rank)
36
+ command :zadd, [key, rank, value]
37
+ end
38
+
39
+ # Number of elements in the set
40
+ # @return [Integer] Size of the set
41
+ #
42
+ # @example
43
+ # field.size #=> 4
44
+ def size
45
+ command :zcard, [key]
46
+ end
47
+
48
+ # Remove element from the set.
49
+ # @param value [String, Symbol] element of the set
50
+ # @return [Integer] Number of removed elements (1 if key existed, 0 otherwise)
51
+ #
52
+ # @example
53
+ # field.delete(:last) #=> 1
54
+ def delete(value)
55
+ command :zrem, [key, value]
56
+ end
57
+
58
+ # Return elements with given score
59
+ # @param score [Numeric, Hash]
60
+ # - when Numeric, only elements with that exact score will be returned
61
+ # - when Hash, elements within min..max range will be returned. See {https://redis.io/commands/zrange Redis docs}
62
+ # @option score [String, Numeric] :min minimal score
63
+ # @option score [String, Numeric] :max maximal score
64
+ # @return [Array<String>] Elements with given score
65
+ #
66
+ # @example with specific score
67
+ # field.where(score: 2) #=> ["first", "tie"]
68
+ # @example with only min set
69
+ # field.where(score: { min: 3 }) #=> ["last", "second"]
70
+ # @example with both min and max set
71
+ # field.where(score: { min: 3, max: "(10" }) #=> ["last"]
72
+ def where(score:)
73
+ command :zrangebyscore, [key, *prepare_score_bounds(score)]
74
+ end
75
+
76
+ # Removes elements of the set with given score and returns them.
77
+ # See {where} for details on how to choose score.
78
+ # @return [Array<String>] Elements with given score
79
+ def delete_if(score:)
80
+ score_bounds = prepare_score_bounds(score)
81
+
82
+ transaction do |conn|
83
+ conn.zrangebyscore key, *score_bounds
84
+ conn.zremrangebyscore key, *score_bounds
85
+ end[0]
86
+ end
87
+
88
+ # Returns first *n* elements of the sorted set
89
+ # @param num [Integer] amount of elements to be returned. Defaults to 1.
90
+ # @return [String, Array<String>] first *num* elements from the set
91
+ def first(num = 1)
92
+ result = command :zrange, [key, 0, num - 1]
93
+ num == 1 ? result[0] : result
94
+ end
95
+
96
+ # Returns last *n* elements of the sorted set
97
+ # @param num [Integer] amount of elements to be returned. Defaults to 1.
98
+ # @return [String, Array<String>] last *num* elements from the set
99
+ def last(num = 1)
100
+ result = command :zrevrange, [key, 0, num - 1]
101
+ num == 1 ? result[0] : result
102
+ end
103
+
104
+ # Creates regular Ruby Hash based on Redis values.
105
+ # @return [Hash] Hash representing this Sorted set
106
+ def to_h
107
+ Hash[command :zrange, [key, 0, -1, with_scores: true]]
108
+ end
109
+
110
+ # Removes old values from the set and overrides them with new.
111
+ # @param hash [Hash] new values of the set
112
+ # @return [String] Redis response
113
+ def overwrite(hash)
114
+ redis.with do |conn|
115
+ conn.pipelined do
116
+ conn.del(key)
117
+ conn.zadd(key, hash_to_array(hash))
118
+ end
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ def hash_to_array(hash)
125
+ ary = Array.new(hash.size * 2)
126
+
127
+ hash.each_with_object(ary).with_index do |((value, score), result), index|
128
+ result[index * 2] = score
129
+ result[index * 2 + 1] = value
130
+ end
131
+ end
132
+
133
+ def prepare_score_bounds(score)
134
+ case score
135
+ when Hash then { min: "-inf", max: "+inf" }.merge(score).values
136
+ when Numeric then [score, score]
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Conflow
4
+ module Redis
5
+ # Represents single value (Redis String). Values are serialized as JSON in order to preserve type.
6
+ class ValueField < Field
7
+ # @note *value* must be serializable through JSON.dump
8
+ # @param value [Object] new value to be saved
9
+ # @return [String] Redis response
10
+ def overwrite(value)
11
+ command :set, [key, JSON.dump(value)]
12
+ end
13
+
14
+ # @note *value* must be serializable through JSON.dump
15
+ # @param value [Object] value to be assigned to field (unless key already holds value)
16
+ # @return [String] Redis response
17
+ def default(value)
18
+ command :set, [key, JSON.dump(value), nx: true]
19
+ end
20
+
21
+ # @param other [Object] Object to compare value with. Handles Strings, Numerics,
22
+ # Symbols and other {ValueField} objects
23
+ # @return [String] Redis response
24
+ def ==(other)
25
+ case other
26
+ when String, Numeric then value == other
27
+ when Symbol then value.to_sym == other
28
+ when ValueField then key == other.key || to_s == other.to_s
29
+ else super
30
+ end
31
+ end
32
+
33
+ # @return [Boolean] true if object does not exist in Redis, else otherwise
34
+ def nil?
35
+ value.nil?
36
+ end
37
+
38
+ # @return [String, nil] String representation of value
39
+ def to_s
40
+ value&.to_s
41
+ end; alias to_str to_s
42
+
43
+ # @return [Object] JSON-parsed value present in Redis
44
+ def value
45
+ result = command(:get, [key])
46
+ result && JSON.parse(result)
47
+ end
48
+ end
49
+ end
50
+ end