redstruct 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,78 @@
1
+ module Redstruct
2
+ module Types
3
+ class List < Redstruct::Types::Struct
4
+ include Redstruct::Utils::Scriptable
5
+
6
+ def clear
7
+ delete
8
+ end
9
+
10
+ def empty?
11
+ return !exists?
12
+ end
13
+
14
+ def [](index)
15
+ return self.connection.lindex(@key, index.to_i)
16
+ end
17
+
18
+ def []=(index, value)
19
+ return self.connection.lset(@key, index.to_i, value)
20
+ end
21
+
22
+ def append(*elements, max: 0)
23
+ max = max.to_i
24
+ return self.connection.rpush(@key, elements) if max <= 0
25
+ return push_and_trim_script(keys: @key, argv: [max - 1, 0] + elements)
26
+ end
27
+
28
+ def prepend(*elements, max: nil)
29
+ max = max.to_i
30
+ return self.connection.lpush(@key, elements) if max <= 0
31
+ return push_and_trim_script(keys: @key, argv: [max - 1, 1] + elements)
32
+ end
33
+
34
+ def pop(timeout: nil)
35
+ options = {}
36
+ options[:timeout] = timeout.to_i unless timeout.nil?
37
+ return self.connection.blpop(@key, options)&.last
38
+ end
39
+
40
+ def remove(value, count: 1)
41
+ count = [1, count.to_i].max
42
+ self.connection.lrem(@key, count, value)
43
+ end
44
+
45
+ def size
46
+ return self.connection.llen(@key)
47
+ end
48
+
49
+ def slice(start = 0, length = -1)
50
+ return self.connection.lrange(@key, start.to_i, length.to_i)
51
+ end
52
+
53
+ def to_a
54
+ return slice(0, -1)
55
+ end
56
+
57
+ # Appends or prepends (argv[1]) a number of items (argv[2]) to a list (keys[1]),
58
+ # then trims it out to size (argv[3])
59
+ # @param [Array<(::String)>] keys First key should be the key to the list to prepend to and resize
60
+ # @param [Array<(Fixnum, Fixnum, Array<::String>)>] argv The maximum size of the list; if 1, will lpush, otherwise rpush; the list of items to prepend
61
+ # @return [Fixnum] The length of the list after the operation
62
+ defscript :push_and_trim_script, <<~LUA
63
+ local max = tonumber(table.remove(ARGV, 1))
64
+ local prepend = tonumber(table.remove(ARGV, 1)) == 1
65
+ local push = prepend and 'lpush' or 'rpush'
66
+
67
+ local size = redis.call(push, KEYS[1], unpack(ARGV))
68
+ if size > max then
69
+ redis.call('ltrim', KEYS[1], 0, max)
70
+ size = max + 1
71
+ end
72
+
73
+ return size
74
+ LUA
75
+ protected :push_and_trim_script
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,56 @@
1
+ require 'digest'
2
+
3
+ module Redstruct
4
+ module Types
5
+ # It is recommended you flush your script cache on the redis server every once in a while
6
+ class Script < Redstruct::Types::Base
7
+ ERROR_MESSAGE_PREFIX = 'NOSCRIPT'.freeze
8
+
9
+ # @return [::String] The Lua script to evaluate
10
+ attr_reader :script
11
+
12
+ def initialize(script:, **options)
13
+ script = script&.strip
14
+ raise(Redstruct::Error, 'No source script given') if script.empty?
15
+
16
+ super(**options)
17
+ self.script = script
18
+ end
19
+
20
+ def script=(script)
21
+ @sha1 = nil
22
+ @script = script.dup.freeze
23
+ end
24
+
25
+ def sha1
26
+ return @sha1 ||= begin
27
+ Digest::SHA1.hexdigest(@script)
28
+ end
29
+ end
30
+
31
+ def exists?
32
+ return self.connection.script(:exists, self.sha1)
33
+ end
34
+
35
+ def load
36
+ @sha1 = self.connection.script(:load, @script)
37
+ return @sha1
38
+ end
39
+
40
+ def eval(keys:, argv:)
41
+ keys = [keys] unless keys.is_a?(Array)
42
+ argv = [argv] unless argv.is_a?(Array)
43
+ self.connection.evalsha(self.sha1, keys, argv)
44
+ rescue Redis::CommandError => err
45
+ raise unless err.message.start_with?(ERROR_MESSAGE_PREFIX)
46
+ self.connection.eval(@script, keys, argv)
47
+ end
48
+
49
+ # :nocov:
50
+ def inspectable_attributes
51
+ return super.merge(sha1: self.sha1, script: @script.slice(0, 20))
52
+ end
53
+ # :nocov:
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,96 @@
1
+ module Redstruct
2
+ module Types
3
+ # Note: keep in mind Redis converts everything to a string on the DB side
4
+ class Set < Redstruct::Types::Struct
5
+ def clear
6
+ delete
7
+ end
8
+
9
+ def random(count: 1)
10
+ return self.connection.srandmember(@key, count.to_i)
11
+ end
12
+
13
+ def empty?
14
+ return !exists?
15
+ end
16
+
17
+ def contain?(member)
18
+ return self.connection.sismember(@key, member)
19
+ end
20
+ alias_method :include?, :contain?
21
+
22
+ def to_a
23
+ return self.connection.smembers(@key)
24
+ end
25
+
26
+ def add(*members)
27
+ return self.connection.sadd(@key, members)
28
+ end
29
+ alias_method :<<, :add
30
+
31
+ def size
32
+ return self.connection.scard(@key).to_i
33
+ end
34
+
35
+ def -(other)
36
+ return ::Set.new(self.connection.sdiff(@key, other.key))
37
+ end
38
+
39
+ def +(other)
40
+ return ::Set.new(self.connection.sunion(@key, other.key))
41
+ end
42
+
43
+ def |(other)
44
+ return ::Set.new(self.connection.sinter(@key, other.key))
45
+ end
46
+
47
+ def difference(other, dest: nil)
48
+ destination = coerce_destination(dest)
49
+ return self - other if destination.nil?
50
+
51
+ self.connection.sdiffstore(destination.key, @key, other.key)
52
+ return destination
53
+ end
54
+
55
+ def intersection(other, dest: nil)
56
+ destination = coerce_destination(dest)
57
+ return self - other if destination.nil?
58
+
59
+ self.connection.sinterstore(destination.key, @key, other.key)
60
+ return destination
61
+ end
62
+
63
+ def union(other, dest: nil)
64
+ destination = coerce_destination(dest)
65
+ return self - other if destination.nil?
66
+
67
+ self.connection.sunionstore(destination.key, @key, other.key)
68
+ return destination
69
+ end
70
+
71
+ def pop(count: 1)
72
+ return self.connection.spop(@key, count.to_i)
73
+ end
74
+
75
+ def remove(*members)
76
+ return self.connection.srem(@key, *members)
77
+ end
78
+
79
+ def each(options = {}, &block)
80
+ return self.connection.sscan_each(@key, options, &block)
81
+ end
82
+
83
+ def coerce_destination(dest)
84
+ return case dest
85
+ when ::String
86
+ @factory.set(dest)
87
+ when self.class
88
+ dest
89
+ else
90
+ nil
91
+ end
92
+ end
93
+ private :coerce_destination
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,15 @@
1
+ module Redstruct
2
+ module Types
3
+ class SortedSet < Redstruct::Types::Struct
4
+ DEFAULT_SCORE = 1.0
5
+
6
+ def add(options = {}, *items)
7
+ defaults = { nx: false, xx: false, ch: false }
8
+ end
9
+
10
+ def <<(item)
11
+ return self.connection.zadd(@key, DEFAULT_SCORE, item)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,64 @@
1
+ module Redstruct
2
+ module Types
3
+ class String < Redstruct::Types::Struct
4
+ include Redstruct::Utils::Scriptable, Redstruct::Utils::Coercion
5
+
6
+ # @return [::String] The string value stored in the database
7
+ def get
8
+ return self.connection.get(@key)
9
+ end
10
+
11
+ # @param [Object] value The object to store; note, it will be stored using a string representation
12
+ # @param [Integer] expiry The expiry time in seconds; if nil, will never expire
13
+ # @param [Boolean] nx Not Exists: if true, will not set the key if it already existed
14
+ # @param [Boolean] xx Already Exists: if true, will set the key only if it already existed
15
+ # @return [Boolean] True if set, false otherwise
16
+ def set(value, expiry: nil, nx: nil, xx: nil)
17
+ options = {}
18
+ options[:ex] = expiry.to_i unless expiry.nil?
19
+ options[:nx] = nx unless nx.nil?
20
+ options[:xx] = xx unless xx.nil?
21
+
22
+ self.connection.set(@key, value, options) == 'OK'
23
+ end
24
+
25
+ # @param [::String] value The value to compare with
26
+ # @return [Boolean] True if deleted, false otherwise
27
+ def delete_if_equals(value)
28
+ coerce_bool(delete_if_equals_script(keys: @key, argv: value))
29
+ end
30
+
31
+ # @param [Object] value The object to store; note, it will be stored using a string representation
32
+ # @return [::String] The old value before setting it
33
+ def getset(value)
34
+ self.connection.getset(@key, value)
35
+ end
36
+
37
+ # @return [Fixnum] The length of the string
38
+ def length
39
+ self.connection.strlen(@key)
40
+ end
41
+
42
+ # @param [Fixnum] start Starting index of the slice
43
+ # @param [Fixnum] length Length of the slice; negative numbers start counting from the right (-1 = end)
44
+ # @return [Array<::String>] The requested slice from <start> with length <length>
45
+ def slice(start = 0, length = -1)
46
+ length = start + length if length >= 0
47
+ return self.connection.getrange(@key, start, length)
48
+ end
49
+
50
+ # Deletes the key (keys[1]) iff the value is equal to argv[1].
51
+ # @param [Array<(::String)>] keys The key to delete
52
+ # @param [Array<(::String)>] argv The value to compare with
53
+ # @return [Fixnum] 1 if deleted, 0 otherwise
54
+ defscript :delete_if_equals_script, <<~LUA
55
+ local deleted = false
56
+ if redis.call("get", KEYS[1]) == ARGV[1] then
57
+ deleted = redis.call("del", KEYS[1])
58
+ end
59
+
60
+ return deleted
61
+ LUA
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,41 @@
1
+ require 'forwardable'
2
+
3
+ module Redstruct
4
+ module Types
5
+ class Struct < Redstruct::Types::Base
6
+ include Redstruct::Utils::Inspectable
7
+
8
+ # @return [Boolean] Returns true if it exists in redis, false otherwise
9
+ def exists?
10
+ return self.connection.exists(@key)
11
+ end
12
+
13
+ # @return [Fixnum] 0 if nothing was deleted in the DB, 1 if it was
14
+ def delete
15
+ self.connection.del(@key)
16
+ end
17
+
18
+ def expire(ttl)
19
+ self.connection.expire(@key, ttl)
20
+ end
21
+
22
+ def expire_at(time)
23
+ self.connection.expire_at(@key, time.to_i)
24
+ end
25
+
26
+ def persist
27
+ self.connection.persist(@key)
28
+ end
29
+
30
+ def type
31
+ self.connection.type(@key)
32
+ end
33
+
34
+ # :nocov:
35
+ def inspectable_attributes
36
+ super.merge(key: @key)
37
+ end
38
+ # :nocov:
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,33 @@
1
+ module Redstruct
2
+ module Utils
3
+ # Coercion utilities to map Redis replies to Ruby types, or vice-versa
4
+ module Coercion
5
+ # Coerces the value into an array.
6
+ # Returns the value if it is already an array (or subclass)
7
+ # Returns value.to_a if it responds to to_a
8
+ # Returns [value] otherwise
9
+ # @param [Object] value The value to coerce
10
+ # @return [Array] The coerced value
11
+ def coerce_array(value)
12
+ return [] if value.nil?
13
+ return value if value.is_a?(Array)
14
+ return value.to_a if value.respond_to?(:to_a)
15
+ return [value]
16
+ end
17
+ module_function :coerce_array
18
+
19
+ # Coerces an object into a boolean:
20
+ # If nil or 0 (after .to_i) => false
21
+ # True otherwise
22
+ # @param [Object] value The object to coerce into a bool
23
+ # @return [Boolean] Coerced value
24
+ def coerce_bool(value)
25
+ return false if value.nil?
26
+ return false if value.to_i == 0
27
+
28
+ return true
29
+ end
30
+ module_function :coerce_bool
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ module Redstruct
2
+ module Utils
3
+ module Inspectable
4
+ def inspect
5
+ attributes = inspectable_attributes.map do |key, value|
6
+ "#{key}: <#{value.inspect}>"
7
+ end
8
+
9
+ return "#{self.class.name}: #{attributes.join(', ')}"
10
+ end
11
+
12
+ def inspectable_attributes
13
+ {}
14
+ end
15
+
16
+ def to_s
17
+ return inspect
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ module Redstruct
2
+ module Utils
3
+ module Scriptable
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ module ClassMethods
9
+ def defscript(id, source)
10
+ constant = "SCRIPT_SOURCE_#{id.upcase}"
11
+ class_eval <<~METHOD, __FILE__, __LINE__ + 1
12
+ #{constant} = { id: '#{id}'.freeze, source: %(#{source}).freeze }.freeze
13
+ def #{id}(keys: [], argv: [])
14
+ return @factory.script(#{constant}[:id], #{constant}[:source]).eval(keys: keys, argv: argv)
15
+ end
16
+ METHOD
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module Redstruct
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,32 @@
1
+ module Redstruct
2
+ class DefscriptHandler < YARD::Handlers::Ruby::Base
3
+ GROUP_NAME = 'Lua Scripts'.freeze
4
+
5
+ handles method_call(:defscript)
6
+ namespace_only
7
+
8
+ process do
9
+ method_name = statement.parameters[0].jump(:ident).source
10
+ method_body = strip_heredoc(statement.parameters[1].source)
11
+
12
+ script = YARD::CodeObjects::MethodObject.new(namespace, method_name, scope)
13
+ script.parameters = [['keys', []], ['argv', []]]
14
+ script.source = method_body
15
+ script.source_type = :lua
16
+ script.dynamic = true
17
+ script.group = GROUP_NAME
18
+
19
+ register(script)
20
+ end
21
+
22
+ def strip_heredoc(string)
23
+ if string.start_with?('<<')
24
+ lines = string.split("\n")
25
+ string = lines[1...-1].join("\n").strip
26
+ end
27
+
28
+ return string
29
+ end
30
+ private :strip_heredoc
31
+ end
32
+ end
data/lib/redstruct.rb ADDED
@@ -0,0 +1,65 @@
1
+ # Dependencies
2
+ require 'redis'
3
+ require 'connection_pool'
4
+
5
+ # Utility
6
+ require 'redstruct/version'
7
+ require 'redstruct/utils/inspectable'
8
+ require 'redstruct/utils/scriptable'
9
+ require 'redstruct/utils/coercion'
10
+
11
+ # Core
12
+ require 'redstruct/connection'
13
+ require 'redstruct/configuration'
14
+ require 'redstruct/error'
15
+ require 'redstruct/types/base'
16
+
17
+ # Factory
18
+ require 'redstruct/factory/creation'
19
+ require 'redstruct/factory/deserialization'
20
+ require 'redstruct/factory'
21
+
22
+ # Base data types
23
+ require 'redstruct/types/struct'
24
+ require 'redstruct/types/string'
25
+ require 'redstruct/types/counter'
26
+ require 'redstruct/types/hash'
27
+ require 'redstruct/types/list'
28
+ require 'redstruct/types/script'
29
+ require 'redstruct/types/set'
30
+ require 'redstruct/types/sorted_set'
31
+
32
+ module Redstruct
33
+ class << self
34
+ def config
35
+ return @config ||= Configuration.new
36
+ end
37
+
38
+ def factories
39
+ return @factories ||= {}
40
+ end
41
+
42
+ def [](key)
43
+ factory = factories[key]
44
+ factory = make(name: key) if factory.nil?
45
+
46
+ return factory
47
+ end
48
+
49
+ def []=(key, factory)
50
+ if factory.nil?
51
+ factories.delete(key)
52
+ else
53
+ factories[key] = factory
54
+ end
55
+ end
56
+
57
+ def make(name: nil, pool: nil, namespace: nil)
58
+ factory = Redstruct::Factory.new(pool: pool, namespace: namespace)
59
+ name = Redstruct if name.nil?
60
+ self[name] = factory unless name.nil?
61
+
62
+ return factory
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,4 @@
1
+ require 'test_helper'
2
+
3
+ class RedstructTest < Minitest::Test
4
+ end
@@ -0,0 +1,4 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'redis/data'
3
+
4
+ require 'minitest/autorun'
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redstruct
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nicolas Pepin-Perreault
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-09-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: connection_pool
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.12'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.12'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '5.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '5.0'
83
+ description: Provides higher level data structures in Ruby using standard Redis commands.
84
+ Also provides basic object mapping for pre-existing types.
85
+ email:
86
+ - nicolas.pepin-perreault@offerista.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - README.md
92
+ - Rakefile
93
+ - lib/redstruct.rb
94
+ - lib/redstruct/configuration.rb
95
+ - lib/redstruct/connection.rb
96
+ - lib/redstruct/error.rb
97
+ - lib/redstruct/factory.rb
98
+ - lib/redstruct/factory/creation.rb
99
+ - lib/redstruct/factory/deserialization.rb
100
+ - lib/redstruct/hls.rb
101
+ - lib/redstruct/hls/lock.rb
102
+ - lib/redstruct/hls/queue.rb
103
+ - lib/redstruct/types/base.rb
104
+ - lib/redstruct/types/counter.rb
105
+ - lib/redstruct/types/hash.rb
106
+ - lib/redstruct/types/list.rb
107
+ - lib/redstruct/types/script.rb
108
+ - lib/redstruct/types/set.rb
109
+ - lib/redstruct/types/sorted_set.rb
110
+ - lib/redstruct/types/string.rb
111
+ - lib/redstruct/types/struct.rb
112
+ - lib/redstruct/utils/coercion.rb
113
+ - lib/redstruct/utils/inspectable.rb
114
+ - lib/redstruct/utils/scriptable.rb
115
+ - lib/redstruct/version.rb
116
+ - lib/redstruct/yard/defscript_handler.rb
117
+ - test/redstruct/restruct_test.rb
118
+ - test/test_helper.rb
119
+ homepage: https://npepinpe.github.com/redstruct/
120
+ licenses:
121
+ - MIT
122
+ metadata: {}
123
+ post_install_message:
124
+ rdoc_options: []
125
+ require_paths:
126
+ - lib
127
+ required_ruby_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ required_rubygems_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ requirements: []
138
+ rubyforge_project:
139
+ rubygems_version: 2.6.6
140
+ signing_key:
141
+ specification_version: 4
142
+ summary: Higher level data structures for Redis.
143
+ test_files:
144
+ - test/redstruct/restruct_test.rb
145
+ - test/test_helper.rb