conflow 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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