redstruct 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,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