├── .rspec ├── .editorconfig ├── gems.rb ├── .gitignore ├── Rakefile ├── spec ├── spec_helper.rb ├── protocol │ └── redis │ │ ├── methods │ │ ├── server_spec.rb │ │ ├── connection_spec.rb │ │ ├── helper.rb │ │ ├── generic_spec.rb │ │ ├── sorted_sets_spec.rb │ │ ├── streams_spec.rb │ │ └── strings_spec.rb │ │ ├── methods_spec.rb │ │ ├── connection_context.rb │ │ └── connection_spec.rb └── extensions │ └── chomp.rb ├── protocol-redis.gemspec ├── .github └── workflows │ └── development.yml ├── lib └── protocol │ ├── redis.rb │ └── redis │ ├── version.rb │ ├── error.rb │ ├── methods │ ├── pubsub.rb │ ├── server.rb │ ├── connection.rb │ ├── counting.rb │ ├── scripting.rb │ ├── hashes.rb │ ├── sets.rb │ ├── streams.rb │ ├── lists.rb │ ├── strings.rb │ ├── geospatial.rb │ ├── generic.rb │ └── sorted_sets.rb │ ├── methods.rb │ └── connection.rb ├── bake └── protocol │ └── redis │ ├── methods.trenni │ └── generate.rb ├── benchmark └── call.rb └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --warnings 3 | --require spec_helper -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | 7 | -------------------------------------------------------------------------------- /gems.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | source 'https://rubygems.org' 3 | 4 | # Specify your gem's dependencies in protocol-redis.gemspec 5 | gemspec 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /gems.locked 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | 11 | # rspec failure tracking 12 | .rspec_status 13 | .covered.db 14 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | 6 | Dir.glob('tasks/**/*.rake').each{|path| load(path)} 7 | 8 | RSpec::Core::RakeTask.new 9 | 10 | task :default => :spec 11 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'covered/rspec' 4 | 5 | if RUBY_VERSION < "2.4.0" 6 | require_relative "extensions/chomp" 7 | end 8 | 9 | RSpec.configure do |config| 10 | config.disable_monkey_patching 11 | 12 | # Enable flags like --only-failures and --next-failure 13 | config.example_status_persistence_file_path = ".rspec_status" 14 | 15 | config.expect_with :rspec do |c| 16 | c.syntax = :expect 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/protocol/redis/methods/server_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | 3 | require 'protocol/redis/methods/server' 4 | 5 | RSpec.describe Protocol::Redis::Methods::Server do 6 | let(:object) {Object.including(Protocol::Redis::Methods::Server).new} 7 | 8 | describe '#info' do 9 | # This is an incomplete response but contains all we need for the test 10 | let(:example_info_response) do 11 | "# Server\r\nredis_version:5.0.7\r\nprocess_id:123\r\n" 12 | end 13 | 14 | it 'can generate correct arguments' do 15 | expect(object).to receive(:call).with('INFO').and_return(example_info_response) 16 | 17 | response = object.info 18 | expect(response[:redis_version]).to eq '5.0.7' 19 | expect(response[:process_id]).to eq '123' 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/protocol/redis/methods/connection_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | 3 | require 'protocol/redis/methods/connection' 4 | 5 | RSpec.describe Protocol::Redis::Methods::Connection do 6 | let(:object) {Object.including(Protocol::Redis::Methods::Connection).new} 7 | 8 | describe '#auth' do 9 | it 'generates correct arguments for password' do 10 | expect(object).to receive(:call).with('AUTH', 'hunter2').and_return("OK") 11 | 12 | response = object.auth("hunter2") 13 | expect(response).to be_truthy 14 | end 15 | 16 | it 'generates correct arguments for username & password' do 17 | expect(object).to receive(:call).with('AUTH', 'AzureDiamond', 'hunter2').and_return("OK") 18 | 19 | response = object.auth("AzureDiamond", "hunter2") 20 | expect(response).to be_truthy 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /protocol-redis.gemspec: -------------------------------------------------------------------------------- 1 | 2 | require_relative "lib/protocol/redis/version" 3 | 4 | Gem::Specification.new do |spec| 5 | spec.name = "protocol-redis" 6 | spec.version = Protocol::Redis::VERSION 7 | 8 | spec.summary = "A transport agnostic RESP protocol client/server." 9 | spec.authors = ["Samuel Williams", "Huba Nagy"] 10 | spec.license = "MIT" 11 | 12 | spec.homepage = "https://github.com/socketry/protocol-redis" 13 | 14 | spec.metadata = { 15 | "funding_uri" => "https://github.com/sponsors/ioquatix", 16 | } 17 | 18 | spec.files = Dir.glob('{lib}/**/*', File::FNM_DOTMATCH, base: __dir__) 19 | 20 | spec.required_ruby_version = ">= 2.5" 21 | 22 | spec.add_development_dependency "async-http" 23 | spec.add_development_dependency "bake" 24 | spec.add_development_dependency "bake-bundler" 25 | spec.add_development_dependency "bake-modernize" 26 | spec.add_development_dependency "benchmark-ips" 27 | spec.add_development_dependency "bundler" 28 | spec.add_development_dependency "covered" 29 | spec.add_development_dependency "rspec", "~> 3.6" 30 | spec.add_development_dependency "trenni" 31 | end 32 | -------------------------------------------------------------------------------- /.github/workflows/development.yml: -------------------------------------------------------------------------------- 1 | name: Development 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{matrix.os}}-latest 8 | continue-on-error: ${{matrix.experimental}} 9 | 10 | strategy: 11 | matrix: 12 | os: 13 | - ubuntu 14 | - macos 15 | 16 | ruby: 17 | - 2.5 18 | - 2.6 19 | - 2.7 20 | 21 | experimental: [false] 22 | env: [""] 23 | 24 | include: 25 | - os: ubuntu 26 | ruby: truffleruby 27 | experimental: true 28 | - os: ubuntu 29 | ruby: jruby 30 | experimental: true 31 | - os: ubuntu 32 | ruby: head 33 | experimental: true 34 | 35 | steps: 36 | - uses: actions/checkout@v2 37 | - uses: ruby/setup-ruby@v1 38 | with: 39 | ruby-version: ${{matrix.ruby}} 40 | 41 | - name: Install dependencies 42 | run: ${{matrix.env}} bundle install 43 | 44 | - name: Run tests 45 | timeout-minutes: 5 46 | run: ${{matrix.env}} bundle exec rspec 47 | -------------------------------------------------------------------------------- /lib/protocol/redis.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2018, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require_relative 'redis/version' 24 | require_relative 'redis/connection' 25 | -------------------------------------------------------------------------------- /lib/protocol/redis/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2018, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | module Protocol 24 | module Redis 25 | VERSION = "0.6.1" 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/protocol/redis/methods_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2019, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require 'protocol/redis/methods' 24 | 25 | RSpec.describe Protocol::Redis::Methods do 26 | end 27 | -------------------------------------------------------------------------------- /lib/protocol/redis/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2019, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | module Protocol 24 | module Redis 25 | class Error < StandardError 26 | end 27 | 28 | class ServerError < Error 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/protocol/redis/methods/helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2019, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | class Class 24 | def including(namespace) 25 | subclass = Class.new(self) 26 | 27 | subclass.include(namespace) 28 | 29 | return subclass 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/extensions/chomp.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2019, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | module Chomp 24 | # https://github.com/socketry/protocol-http1/pull/1 25 | # https://docs.ruby-lang.org/en/2.3.0/IO.html#method-i-gets 26 | # https://docs.ruby-lang.org/en/2.4.0/IO.html#method-i-gets 27 | def gets(sep = $/, chomp: false) 28 | chomp ? super(sep).chomp : super(sep) 29 | end 30 | end 31 | 32 | IO.prepend(Chomp) 33 | -------------------------------------------------------------------------------- /spec/protocol/redis/connection_context.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2019, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require 'protocol/redis/connection' 24 | 25 | require 'socket' 26 | 27 | RSpec.shared_context Protocol::Redis::Connection do 28 | let(:sockets) {Socket.pair(Socket::PF_UNIX, Socket::SOCK_STREAM)} 29 | 30 | let(:client) {Protocol::Redis::Connection.new(sockets.first)} 31 | let(:server) {Protocol::Redis::Connection.new(sockets.last)} 32 | end 33 | -------------------------------------------------------------------------------- /spec/protocol/redis/connection_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2018, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require 'protocol/redis/connection' 24 | require_relative 'connection_context' 25 | 26 | RSpec.describe Protocol::Redis::Connection do 27 | include_context Protocol::Redis::Connection 28 | 29 | describe '#write_object' do 30 | it "can write strings" do 31 | client.write_object("Hello World!") 32 | 33 | expect(server.read_object).to be == "Hello World!" 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/protocol/redis/methods/pubsub.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2018, by Samuel G. D. Williams. 4 | # Copyright, 2018, by Huba Nagy. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | 24 | module Protocol 25 | module Redis 26 | module Methods 27 | module Pubsub 28 | # Post a message to a channel. 29 | # @see https://redis.io/commands/publish 30 | # @param channel [String] 31 | # @param message [String] 32 | def publish(channel, message) 33 | call('PUBLISH', channel, message) 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/protocol/redis/methods/generic_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2019, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require_relative 'helper' 24 | 25 | require 'protocol/redis/methods/generic' 26 | 27 | RSpec.describe Protocol::Redis::Methods::Generic do 28 | let(:object) {Object.including(described_class).new} 29 | 30 | describe '#exists?' do 31 | let(:key_1) {"mykey"} 32 | let(:key_2) {"yourkey"} 33 | 34 | it "can generate correct arguments" do 35 | expect(object).to receive(:exists).with(key_1, key_2).and_return(1) 36 | 37 | expect(object.exists?(key_1, key_2)).to be true 38 | end 39 | end 40 | end -------------------------------------------------------------------------------- /bake/protocol/redis/methods.trenni: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2018, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | module Protocol 24 | module Redis 25 | module Methods 26 | module #{self[:module_name]} 27 | 34 | 35 | 36 | def #{method_name}(*arguments) 37 | call(#{command.to_s.dump}, *arguments) 38 | end 39 | 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /benchmark/call.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Copyright, 2020, by Samuel G. D. Williams. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | 24 | require 'benchmark/ips' 25 | 26 | GC.disable 27 | 28 | def call(*arguments) 29 | arguments.size 30 | end 31 | 32 | Benchmark.ips do |benchmark| 33 | benchmark.time = 5 34 | benchmark.warmup = 1 35 | 36 | benchmark.report("*arguments") do |count| 37 | while count > 0 38 | arguments = ["foo", "bar", "baz"] 39 | call(*arguments) 40 | 41 | count -= 1 42 | end 43 | end 44 | 45 | benchmark.report("argument, *arguments") do |count| 46 | while count > 0 47 | arguments = ["bar", "baz"] 48 | call("foo", *arguments) 49 | 50 | count -= 1 51 | end 52 | end 53 | 54 | benchmark.compare! 55 | end -------------------------------------------------------------------------------- /lib/protocol/redis/methods/server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2018, by Samuel G. D. Williams. 4 | # Copyright, 2018, by Huba Nagy. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | 24 | module Protocol 25 | module Redis 26 | module Methods 27 | module Server 28 | # Get information and statistics about the server. 29 | # @see https://redis.io/commands/info 30 | # @param section [String] 31 | def info 32 | metadata = {} 33 | 34 | call('INFO').each_line(Redis::Connection::CRLF) do |line| 35 | key, value = line.split(':') 36 | 37 | if value 38 | metadata[key.to_sym] = value.chomp! 39 | end 40 | end 41 | 42 | return metadata 43 | end 44 | 45 | # Remove all keys from the current database. 46 | # @see https://redis.io/commands/flushdb 47 | # @param async [Enum] 48 | def flushdb! 49 | call('FLUSHDB') 50 | end 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/protocol/redis/methods/connection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2018, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | module Protocol 24 | module Redis 25 | module Methods 26 | module Connection 27 | # Authenticate to the server. 28 | # @see https://redis.io/commands/auth 29 | # @param username [String] Optional username, if Redis ACLs are used. 30 | # @param password [String] Required password. 31 | def auth(*arguments) 32 | call("AUTH", *arguments) 33 | end 34 | 35 | # Echo the given string. 36 | # @see https://redis.io/commands/echo 37 | # @param message [String] 38 | def echo(message) 39 | call("ECHO", message) 40 | end 41 | 42 | # Ping the server. 43 | # @see https://redis.io/commands/ping 44 | # @param message [String] 45 | def ping(message) 46 | call("PING", message) 47 | end 48 | 49 | # Close the connection. 50 | # @see https://redis.io/commands/quit 51 | def quit 52 | call("QUIT") 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/protocol/redis/methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2019, by Mikael Henriksson. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require_relative 'methods/generic' 24 | require_relative 'methods/connection' 25 | require_relative 'methods/server' 26 | require_relative 'methods/geospatial' 27 | 28 | require_relative 'methods/counting' 29 | 30 | require_relative 'methods/hashes' 31 | require_relative 'methods/lists' 32 | require_relative 'methods/strings' 33 | require_relative 'methods/sorted_sets' 34 | 35 | require_relative 'methods/pubsub' 36 | 37 | module Protocol 38 | module Redis 39 | module Methods 40 | def self.included(klass) 41 | klass.include Methods::Generic 42 | klass.include Methods::Connection 43 | klass.include Methods::Server 44 | klass.include Methods::Geospatial 45 | 46 | klass.include Methods::Counting 47 | 48 | klass.include Methods::Hashes 49 | klass.include Methods::Lists 50 | klass.include Methods::SortedSets 51 | klass.include Methods::Strings 52 | 53 | klass.include Methods::Pubsub 54 | end 55 | end 56 | end 57 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Protocol::Redis 2 | 3 | Implements the RESP2 and [RESP3](https://github.com/antirez/RESP3) Redis protocols. 4 | 5 | [![Development Status](https://github.com/socketry/protocol-redis/workflows/Development/badge.svg)](https://github.com/socketry/protocol-redis/actions?workflow=Development) 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'protocol-redis' 13 | ``` 14 | 15 | And then execute: 16 | 17 | $ bundle 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install protocol-redis 22 | 23 | ## Usage 24 | 25 | ```ruby 26 | sockets = Socket.pair(Socket::PF_UNIX, Socket::SOCK_STREAM) 27 | 28 | client = Protocol::Redis::Connection.new(sockets.first) 29 | server = Protocol::Redis::Connection.new(sockets.last) 30 | 31 | client.write_object("Hello World!") 32 | puts server.read_object 33 | # => "Hello World!" 34 | ``` 35 | 36 | ## Contributing 37 | 38 | 1. Fork it 39 | 2. Create your feature branch (`git checkout -b my-new-feature`) 40 | 3. Commit your changes (`git commit -am 'Add some feature'`) 41 | 4. Push to the branch (`git push origin my-new-feature`) 42 | 5. Create new Pull Request 43 | 44 | ## License 45 | 46 | Released under the MIT license. 47 | 48 | Copyright, 2019, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams). 49 | Copyright, 2019, by Huba Z. Nagy. 50 | 51 | Permission is hereby granted, free of charge, to any person obtaining a copy 52 | of this software and associated documentation files (the "Software"), to deal 53 | in the Software without restriction, including without limitation the rights 54 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 55 | copies of the Software, and to permit persons to whom the Software is 56 | furnished to do so, subject to the following conditions: 57 | 58 | The above copyright notice and this permission notice shall be included in 59 | all copies or substantial portions of the Software. 60 | 61 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 62 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 63 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 64 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 65 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 66 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 67 | THE SOFTWARE. 68 | -------------------------------------------------------------------------------- /lib/protocol/redis/methods/counting.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2018, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | module Protocol 24 | module Redis 25 | module Methods 26 | module Counting 27 | # Adds the specified elements to the specified HyperLogLog. O(1) to add every element. 28 | # @see https://redis.io/commands/pfadd 29 | # @param key [Key] 30 | # @param element [String] 31 | def pfadd(key, element, *elements) 32 | call("PFADD", key, element, *elements) 33 | end 34 | 35 | # Return the approximated cardinality of the set(s) observed by the HyperLogLog at key(s). O(1) with a very small average constant time when called with a single key. O(N) with N being the number of keys, and much bigger constant times, when called with multiple keys. 36 | # @see https://redis.io/commands/pfcount 37 | # @param key [Key] 38 | def pfcount(key, *keys) 39 | call("PFCOUNT", key, *keys) 40 | end 41 | 42 | # Merge N different HyperLogLogs into a single one. O(N) to merge N HyperLogLogs, but with high constant times. 43 | # @see https://redis.io/commands/pfmerge 44 | # @param destkey [Key] 45 | # @param sourcekey [Key] 46 | def pfmerge(destkey, sourcekey, *sourcekeys) 47 | call("PFMERGE", destkey, sourcekey, *sourcekeys) 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/protocol/redis/methods/scripting.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2018, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | module Protocol 24 | module Redis 25 | module Methods 26 | module Scripting 27 | # Execute a Lua script server side. Depends on the script that is executed. 28 | # @see https://redis.io/commands/eval 29 | # @param script [String] 30 | # @param numkeys [Integer] 31 | # @param key [Key] 32 | # @param arg [String] 33 | def eval(*arguments) 34 | call("EVAL", *arguments) 35 | end 36 | 37 | # Execute a Lua script server side. Depends on the script that is executed. 38 | # @see https://redis.io/commands/evalsha 39 | # @param sha1 [String] 40 | # @param numkeys [Integer] 41 | # @param key [Key] 42 | # @param arg [String] 43 | def evalsha(*arguments) 44 | call("EVALSHA", *arguments) 45 | end 46 | 47 | # Set the debug mode for executed scripts. O(1). 48 | # @see https://redis.io/commands/script debug 49 | # @param mode [Enum] 50 | def script_debug(*arguments) 51 | call("SCRIPT DEBUG", *arguments) 52 | end 53 | 54 | # Check existence of scripts in the script cache. O(N) with N being the number of scripts to check (so checking a single script is an O(1) operation). 55 | # @see https://redis.io/commands/script exists 56 | # @param sha1 [String] 57 | def script_exists(*arguments) 58 | call("SCRIPT EXISTS", *arguments) 59 | end 60 | 61 | # Remove all the scripts from the script cache. O(N) with N being the number of scripts in cache. 62 | # @see https://redis.io/commands/script flush 63 | def script_flush(*arguments) 64 | call("SCRIPT FLUSH", *arguments) 65 | end 66 | 67 | # Kill the script currently in execution. O(1). 68 | # @see https://redis.io/commands/script kill 69 | def script_kill(*arguments) 70 | call("SCRIPT KILL", *arguments) 71 | end 72 | 73 | # Load the specified Lua script into the script cache. O(N) with N being the length in bytes of the script body. 74 | # @see https://redis.io/commands/script load 75 | # @param script [String] 76 | def script_load(*arguments) 77 | call("SCRIPT LOAD", *arguments) 78 | end 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/protocol/redis/methods/sorted_sets_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2019, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require_relative 'helper' 24 | 25 | require 'protocol/redis/methods/sorted_sets' 26 | 27 | RSpec.describe Protocol::Redis::Methods::SortedSets do 28 | let(:object) {Object.including(Protocol::Redis::Methods::SortedSets).new} 29 | let(:set_name) {'test'} 30 | 31 | describe '#zadd' do 32 | let(:timestamp) { Time.now.to_f } 33 | 34 | it "can generate correct arguments" do 35 | expect(object).to receive(:call).with('ZADD', set_name, timestamp, 'payload') 36 | 37 | object.zadd(set_name, timestamp, 'payload') 38 | end 39 | 40 | it "can generate correct arguments with options" do 41 | expect(object).to receive(:call).with('ZADD', set_name, 'XX', 'CH', 'INCR', timestamp, 'payload') 42 | 43 | object.zadd(set_name, timestamp, 'payload', update: true, change: true, increment: true) 44 | end 45 | 46 | it "can generate correct multiple arguments" do 47 | expect(object).to receive(:call).with('ZADD', set_name, 'XX', 'CH', 'INCR', timestamp, 'payload-1', timestamp, 'payload-2') 48 | 49 | object.zadd(set_name, timestamp, 'payload-1', timestamp, 'payload-2', update: true, change: true, increment: true) 50 | end 51 | end 52 | 53 | describe '#zrange' do 54 | it "can generate correct arguments" do 55 | expect(object).to receive(:call).with('ZRANGE', set_name, 0, 0) 56 | 57 | object.zrange(set_name, 0, 0) 58 | end 59 | 60 | it "can generate correct arguments with options" do 61 | expect(object).to receive(:call).with('ZRANGE', set_name, 0, 0, 'WITHSCORES') 62 | 63 | object.zrange(set_name, 0, 0, with_scores: true) 64 | end 65 | end 66 | 67 | describe '#zrangebyscore' do 68 | it "can generate correct arguments" do 69 | expect(object).to receive(:call).with('ZRANGEBYSCORE', set_name, 0, 0) 70 | 71 | object.zrangebyscore(set_name, 0, 0) 72 | end 73 | 74 | it "can generate correct arguments with WITHSCORES options" do 75 | expect(object).to receive(:call).with('ZRANGEBYSCORE', set_name, 0, 0, 'WITHSCORES') 76 | 77 | object.zrangebyscore(set_name, 0, 0, with_scores: true) 78 | end 79 | 80 | it "can generate correct arguments with WITHSCORES options" do 81 | expect(object).to receive(:call).with('ZRANGEBYSCORE', set_name, 0, 0, 'WITHSCORES', 'LIMIT', 0, 10) 82 | 83 | object.zrangebyscore(set_name, 0, 0, with_scores: true, limit: [0, 10]) 84 | end 85 | end 86 | 87 | describe '#zrem' do 88 | let(:member_name) { 'test_member' } 89 | 90 | it "can generate correct arguments" do 91 | expect(object).to receive(:call).with('ZREM', set_name, member_name) 92 | 93 | object.zrem(set_name, member_name) 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/protocol/redis/connection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2019, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require_relative 'error' 24 | 25 | module Protocol 26 | module Redis 27 | class Connection 28 | CRLF = "\r\n".freeze 29 | 30 | def initialize(stream) 31 | @stream = stream 32 | 33 | # Number of requests sent: 34 | @count = 0 35 | end 36 | 37 | attr :stream 38 | 39 | # @attr [Integer] Number of requests sent. 40 | attr :count 41 | 42 | def close 43 | @stream.close 44 | end 45 | 46 | class << self 47 | alias client new 48 | end 49 | 50 | def flush 51 | @stream.flush 52 | end 53 | 54 | def closed? 55 | @stream.closed? 56 | end 57 | 58 | # The redis server doesn't want actual objects (e.g. integers) but only bulk strings. So, we inline it for performance. 59 | def write_request(arguments) 60 | write_lines("*#{arguments.size}") 61 | 62 | @count += 1 63 | 64 | arguments.each do |argument| 65 | string = argument.to_s 66 | 67 | write_lines("$#{string.bytesize}", string) 68 | end 69 | end 70 | 71 | def write_object(object) 72 | case object 73 | when String 74 | write_lines("$#{object.bytesize}", object) 75 | when Array 76 | write_array(object) 77 | when Integer 78 | write_lines(":#{object}") 79 | else 80 | write_object(object.to_redis) 81 | end 82 | end 83 | 84 | def read_data(length) 85 | buffer = @stream.read(length) or @stream.eof! 86 | 87 | # Eat trailing whitespace because length does not include the CRLF: 88 | @stream.read(2) or @stream.eof! 89 | 90 | return buffer 91 | end 92 | 93 | def read_object 94 | line = read_line or raise EOFError 95 | 96 | token = line.slice!(0, 1) 97 | 98 | case token 99 | when '$' 100 | length = line.to_i 101 | 102 | if length == -1 103 | return nil 104 | else 105 | return read_data(length) 106 | end 107 | when '*' 108 | count = line.to_i 109 | 110 | # Null array (https://redis.io/topics/protocol#resp-arrays): 111 | return nil if count == -1 112 | 113 | array = Array.new(count) {read_object} 114 | 115 | return array 116 | when ':' 117 | return line.to_i 118 | 119 | when '-' 120 | raise ServerError.new(line) 121 | 122 | when '+' 123 | return line 124 | 125 | else 126 | @stream.flush 127 | 128 | raise NotImplementedError, "Implementation for token #{token} missing" 129 | end 130 | 131 | # TODO: If an exception (e.g. Async::TimeoutError) propagates out of this function, perhaps @stream should be closed? Otherwise it might be in a weird state. 132 | end 133 | 134 | alias read_response read_object 135 | 136 | private 137 | 138 | # In the case of Redis, we do not want to perform a flush in every line, 139 | # because each Redis command contains several lines. Flushing once per 140 | # command is more efficient because it avoids unnecessary writes to the 141 | # socket. 142 | def write_lines(*arguments) 143 | if arguments.empty? 144 | @stream.write(CRLF) 145 | else 146 | arguments.each do |arg| 147 | @stream.write(arg) 148 | @stream.write(CRLF) 149 | end 150 | end 151 | end 152 | 153 | def read_line 154 | @stream.gets(CRLF, chomp: true) 155 | end 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /lib/protocol/redis/methods/hashes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2019, by Mikael Henriksson. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | module Protocol 24 | module Redis 25 | module Methods 26 | module Hashes 27 | # Get the number of fields in a hash. O(1). 28 | # @see https://redis.io/commands/hlen 29 | # @param key [Key] 30 | def hlen(key) 31 | call('HLEN', key) 32 | end 33 | 34 | # Set the string value of a hash field. O(1) for each field/value pair added, so O(N) to add N field/value pairs when the command is called with multiple field/value pairs. 35 | # @see https://redis.io/commands/hset 36 | # @param key [Key] 37 | def hset(key, field, value) 38 | call('HSET', key, field, value) 39 | end 40 | 41 | # Set the value of a hash field, only if the field does not exist. O(1). 42 | # @see https://redis.io/commands/hsetnx 43 | # @param key [Key] 44 | # @param field [String] 45 | # @param value [String] 46 | def hsetnx(key, field, value) 47 | call('HSETNX', key, field, value) 48 | end 49 | 50 | # Set multiple hash fields to multiple values. O(N) where N is the number of fields being set. 51 | # @see https://redis.io/commands/hmset 52 | # @param key [Key] 53 | def hmset(key, *attrs) 54 | call('HMSET', key, *attrs) 55 | end 56 | 57 | # Get the value of a hash field. O(1). 58 | # @see https://redis.io/commands/hget 59 | # @param key [Key] 60 | # @param field [String] 61 | def hget(key, field) 62 | call('HGET', key, field) 63 | end 64 | 65 | # Get the values of all the given hash fields. O(N) where N is the number of fields being requested. 66 | # @see https://redis.io/commands/hmget 67 | # @param key [Key] 68 | # @param field [String] 69 | def hmget(key, *fields, &blk) 70 | call('HMGET', key, *fields, &blk) 71 | end 72 | 73 | # Delete one or more hash fields. O(N) where N is the number of fields to be removed. 74 | # @see https://redis.io/commands/hdel 75 | # @param key [Key] 76 | # @param field [String] 77 | def hdel(key, *fields) 78 | call('HDEL', key, *fields) 79 | end 80 | 81 | # Determine if a hash field exists. O(1). 82 | # @see https://redis.io/commands/hexists 83 | # @param key [Key] 84 | # @param field [String] 85 | def hexists(key, field) 86 | call('HEXISTS', key, field) 87 | end 88 | 89 | # Increment the integer value of a hash field by the given number. O(1). 90 | # @see https://redis.io/commands/hincrby 91 | # @param key [Key] 92 | # @param field [String] 93 | # @param increment [Integer] 94 | def hincrby(key, field, increment) 95 | call('HINCRBY', key, field, increment) 96 | end 97 | 98 | # Increment the float value of a hash field by the given amount. O(1). 99 | # @see https://redis.io/commands/hincrbyfloat 100 | # @param key [Key] 101 | # @param field [String] 102 | # @param increment [Double] 103 | def hincrbyfloat(key, field, increment) 104 | call('HINCRBYFLOAT', key, field, increment) 105 | end 106 | 107 | # Get all the fields in a hash. O(N) where N is the size of the hash. 108 | # @see https://redis.io/commands/hkeys 109 | # @param key [Key] 110 | def hkeys(key) 111 | call('HKEYS', key) 112 | end 113 | 114 | # Get all the values in a hash. O(N) where N is the size of the hash. 115 | # @see https://redis.io/commands/hvals 116 | # @param key [Key] 117 | def hvals(key) 118 | call('HVALS', key) 119 | end 120 | 121 | # Get all the fields and values in a hash. O(N) where N is the size of the hash. 122 | # @see https://redis.io/commands/hgetall 123 | # @param key [Key] 124 | def hgetall(key) 125 | call('HGETALL', key) 126 | end 127 | end 128 | end 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /bake/protocol/redis/generate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2018, by Samuel G. D. Williams. 4 | # Copyright, 2018, by Huba Nagy. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | 24 | require 'async' 25 | 26 | def commands 27 | require 'async/http/internet' 28 | require 'json' 29 | 30 | @commands = fetch_commands 31 | 32 | @commands.each do |command, command_spec| 33 | method_name = command.to_s.downcase.split(/[\s\-_]+/).join('_') 34 | command_spec[:method_name] = method_name 35 | end 36 | 37 | # There is a bit of a discrepancy between how the groups appear in the JSON and how they appear in the compiled documentation, this is a mapping from `commands.json` to documentation: 38 | @groups = { 39 | 'generic' => 'generic', 40 | 'string' => 'strings', 41 | 'list' => 'lists', 42 | 'set' => 'sets', 43 | 'sorted_set' => 'sorted_sets', 44 | 'hash' => 'hashes', 45 | 'connection' => 'connection', 46 | 'server' => 'server', 47 | 'scripting' => 'scripting', 48 | 'hyperloglog' => 'counting', 49 | 'cluster' => 'cluster', 50 | 'geo' => 'geospatial', 51 | 'stream' => 'streams' 52 | }.freeze 53 | end 54 | 55 | def methods 56 | require 'trenni/template' 57 | 58 | self.commands unless defined?(@commands) 59 | 60 | template = Trenni::Template.load_file(File.expand_path("methods.trenni", __dir__)) 61 | 62 | @groups.each_pair do |spec_group, group| 63 | puts "Processing #{spec_group}..." 64 | 65 | path = File.expand_path("lib/protocol/redis/methods/#{group}.rb", context.root) 66 | 67 | if File.exist?(path) 68 | puts "File already exists #{path}, skipping!" 69 | next 70 | end 71 | 72 | group_commands = @commands.select do |command, command_spec| 73 | command_spec[:group] == spec_group 74 | end 75 | 76 | output = template.to_string({ 77 | module_name: module_name(group), 78 | group_commands: group_commands, 79 | }) 80 | 81 | File.write(path, output) 82 | 83 | break 84 | end 85 | end 86 | 87 | def documentation 88 | self.commands unless defined?(@commands) 89 | 90 | @groups.each_pair do |spec_group, group| 91 | puts "Processing #{spec_group}..." 92 | 93 | path = "lib/protocol/redis/methods/#{group}.rb" 94 | 95 | unless File.exist?(path) 96 | puts "Could not find #{path}, skipping!" 97 | next 98 | end 99 | 100 | lines = File.readlines(path) 101 | 102 | group_commands = @commands.select do |command, command_spec| 103 | command_spec[:group] == spec_group 104 | end 105 | 106 | puts "\tFound #{group_commands.length} commands in this group." 107 | 108 | group_commands.each do |command, command_spec| 109 | puts "\tProcessing #{command}..." 110 | 111 | if offset = lines.find_index{|line| line.include?("def #{command_spec[:method_name]}")} 112 | puts "Found #{command} at line #{offset}." 113 | 114 | /(?\s+)def/ =~ lines[offset] 115 | 116 | start = offset 117 | while true 118 | break unless lines[start-1] =~ /\s+#(.*?)\n/ 119 | start -= 1 120 | end 121 | 122 | # Remove the comments: 123 | lines.slice!(start...offset) 124 | 125 | summary = [ 126 | normalize(command_spec[:summary]), 127 | normalize(command_spec[:complexity]) 128 | ].compact 129 | 130 | comments = [ 131 | summary.join(' '), 132 | "@see https://redis.io/commands/#{command.to_s.downcase}" 133 | ] 134 | 135 | command_spec[:arguments]&.each do |argument| 136 | next if argument[:command] or argument[:type].nil? or argument[:type].is_a?(Array) 137 | comments << "@param #{argument[:name]} [#{argument[:type].capitalize}]" 138 | end 139 | 140 | lines.insert(start, comments.map{|comment| "#{indentation}\# #{comment}\n"}) 141 | else 142 | puts "Could not find #{command} definition!" 143 | end 144 | end 145 | 146 | File.write(path, lines.join) 147 | end 148 | end 149 | 150 | private 151 | 152 | def fetch_commands 153 | Async do 154 | internet = Async::HTTP::Internet.new 155 | 156 | response = internet.get("https://raw.githubusercontent.com/antirez/redis-doc/master/commands.json") 157 | 158 | JSON.parse(response.read, symbolize_names: true) 159 | ensure 160 | internet&.close 161 | end.wait 162 | end 163 | 164 | def normalize(sentence) 165 | return nil if sentence.nil? 166 | 167 | sentence = sentence.strip 168 | 169 | if sentence.end_with?(".") 170 | return sentence 171 | else 172 | return "#{sentence}." 173 | end 174 | end 175 | 176 | def module_name(group) 177 | group.split('_').collect(&:capitalize).join 178 | end 179 | -------------------------------------------------------------------------------- /lib/protocol/redis/methods/sets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2018, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | module Protocol 24 | module Redis 25 | module Methods 26 | module Sets 27 | # Add one or more members to a set. O(1) for each element added, so O(N) to add N elements when the command is called with multiple arguments. 28 | # @see https://redis.io/commands/sadd 29 | # @param key [Key] 30 | # @param member [String] 31 | def sadd(*arguments) 32 | call("SADD", *arguments) 33 | end 34 | 35 | # Get the number of members in a set. O(1). 36 | # @see https://redis.io/commands/scard 37 | # @param key [Key] 38 | def scard(*arguments) 39 | call("SCARD", *arguments) 40 | end 41 | 42 | # Subtract multiple sets. O(N) where N is the total number of elements in all given sets. 43 | # @see https://redis.io/commands/sdiff 44 | # @param key [Key] 45 | def sdiff(*arguments) 46 | call("SDIFF", *arguments) 47 | end 48 | 49 | # Subtract multiple sets and store the resulting set in a key. O(N) where N is the total number of elements in all given sets. 50 | # @see https://redis.io/commands/sdiffstore 51 | # @param destination [Key] 52 | # @param key [Key] 53 | def sdiffstore(*arguments) 54 | call("SDIFFSTORE", *arguments) 55 | end 56 | 57 | # Intersect multiple sets. O(N*M) worst case where N is the cardinality of the smallest set and M is the number of sets. 58 | # @see https://redis.io/commands/sinter 59 | # @param key [Key] 60 | def sinter(*arguments) 61 | call("SINTER", *arguments) 62 | end 63 | 64 | # Intersect multiple sets and store the resulting set in a key. O(N*M) worst case where N is the cardinality of the smallest set and M is the number of sets. 65 | # @see https://redis.io/commands/sinterstore 66 | # @param destination [Key] 67 | # @param key [Key] 68 | def sinterstore(*arguments) 69 | call("SINTERSTORE", *arguments) 70 | end 71 | 72 | # Determine if a given value is a member of a set. O(1). 73 | # @see https://redis.io/commands/sismember 74 | # @param key [Key] 75 | # @param member [String] 76 | def sismember(*arguments) 77 | call("SISMEMBER", *arguments) 78 | end 79 | 80 | # Get all the members in a set. O(N) where N is the set cardinality. 81 | # @see https://redis.io/commands/smembers 82 | # @param key [Key] 83 | def smembers(*arguments) 84 | call("SMEMBERS", *arguments) 85 | end 86 | 87 | # Move a member from one set to another. O(1). 88 | # @see https://redis.io/commands/smove 89 | # @param source [Key] 90 | # @param destination [Key] 91 | # @param member [String] 92 | def smove(*arguments) 93 | call("SMOVE", *arguments) 94 | end 95 | 96 | # Remove and return one or multiple random members from a set. O(1). 97 | # @see https://redis.io/commands/spop 98 | # @param key [Key] 99 | # @param count [Integer] 100 | def spop(*arguments) 101 | call("SPOP", *arguments) 102 | end 103 | 104 | # Get one or multiple random members from a set. Without the count argument O(1), otherwise O(N) where N is the absolute value of the passed count. 105 | # @see https://redis.io/commands/srandmember 106 | # @param key [Key] 107 | # @param count [Integer] 108 | def srandmember(*arguments) 109 | call("SRANDMEMBER", *arguments) 110 | end 111 | 112 | # Remove one or more members from a set. O(N) where N is the number of members to be removed. 113 | # @see https://redis.io/commands/srem 114 | # @param key [Key] 115 | # @param member [String] 116 | def srem(*arguments) 117 | call("SREM", *arguments) 118 | end 119 | 120 | # Add multiple sets. O(N) where N is the total number of elements in all given sets. 121 | # @see https://redis.io/commands/sunion 122 | # @param key [Key] 123 | def sunion(*arguments) 124 | call("SUNION", *arguments) 125 | end 126 | 127 | # Add multiple sets and store the resulting set in a key. O(N) where N is the total number of elements in all given sets. 128 | # @see https://redis.io/commands/sunionstore 129 | # @param destination [Key] 130 | # @param key [Key] 131 | def sunionstore(*arguments) 132 | call("SUNIONSTORE", *arguments) 133 | end 134 | 135 | # Incrementally iterate Set elements. O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection.. 136 | # @see https://redis.io/commands/sscan 137 | # @param key [Key] 138 | # @param cursor [Integer] 139 | def sscan(*arguments) 140 | call("SSCAN", *arguments) 141 | end 142 | end 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /spec/protocol/redis/methods/streams_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2019, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require_relative 'helper' 24 | 25 | require 'protocol/redis/methods/streams' 26 | 27 | RSpec.describe Protocol::Redis::Methods::Streams do 28 | let(:object) {Object.including(Protocol::Redis::Methods::Streams).new} 29 | 30 | describe '#xinfo' do 31 | let(:stream) {"STREAM"} 32 | let(:key) {"mystream"} 33 | 34 | it "can generate correct arguments" do 35 | expect(object).to receive(:call).with("XINFO", stream, key) 36 | 37 | object.xinfo(stream, key) 38 | end 39 | end 40 | 41 | describe '#xadd' do 42 | let(:key_name) {"mykey"} 43 | let(:key) {"name"} 44 | let(:value) {"sara"} 45 | 46 | it "can generate correct arguments" do 47 | expect(object).to receive(:call).with("XADD", key_name, key, value) 48 | 49 | object.xadd(key_name, key, value) 50 | end 51 | end 52 | 53 | describe '#xtrim' do 54 | let(:key_name) {"mykey"} 55 | let(:value) {1000} 56 | 57 | it "can generate correct arguments" do 58 | expect(object).to receive(:call).with("XTRIM", key_name, "MAXLEN", value) 59 | 60 | object.xtrim(key_name, "MAXLEN", value) 61 | end 62 | end 63 | 64 | describe '#xdel' do 65 | let(:key_name) {"mykey"} 66 | let(:id) {"1000001234-0"} 67 | 68 | it "can generate correct arguments" do 69 | expect(object).to receive(:call).with("XDEL", key_name, id) 70 | 71 | object.xdel(key_name, id) 72 | end 73 | end 74 | 75 | describe '#xrange' do 76 | let(:key_name) {"mykey"} 77 | let(:id) {"1000001234-0"} 78 | let(:count_value) {"1"} 79 | 80 | it "can generate correct arguments" do 81 | expect(object).to receive(:call).with("XRANGE", key_name, id,"+ COUNT", count_value) 82 | 83 | object.xrange(key_name, id,"+ COUNT", count_value) 84 | end 85 | end 86 | 87 | describe '#xrevrange' do 88 | let(:key_name) {"mykey"} 89 | let(:id) {"1000001234-0"} 90 | let(:count_value) {"1"} 91 | 92 | it "can generate correct arguments" do 93 | expect(object).to receive(:call).with("XREVRANGE", key_name, id,"+ COUNT", count_value) 94 | 95 | object.xrevrange(key_name, id,"+ COUNT", count_value) 96 | end 97 | end 98 | 99 | describe '#xlen' do 100 | let(:key) {"mystream"} 101 | 102 | it "can generate correct arguments" do 103 | expect(object).to receive(:call).with("XLEN", key) 104 | 105 | object.xlen(key) 106 | end 107 | end 108 | 109 | describe '#xread' do 110 | let(:key_name) {"mykey"} 111 | let(:id) {"1000001234-0"} 112 | let(:count_value) {"1"} 113 | 114 | it "can generate correct arguments" do 115 | expect(object).to receive(:call).with("XREAD", "+ COUNT", count_value, "STREAMS", key_name, "writers", id) 116 | 117 | object.xread("+ COUNT", count_value, "STREAMS", key_name, "writers", id) 118 | end 119 | end 120 | 121 | describe '#xgroup' do 122 | let(:key_name) {"mykey"} 123 | let(:group_name) {"mygroup"} 124 | 125 | it "can generate correct arguments" do 126 | expect(object).to receive(:call).with("XGROUP", "CREATE", key_name, group_name, "$") 127 | 128 | object.xgroup("CREATE", key_name, group_name, "$") 129 | end 130 | end 131 | 132 | describe '#xreadgroup' do 133 | let(:key_name) {"mykey"} 134 | let(:id) {"1000001234-0"} 135 | let(:count_value) {"1"} 136 | let(:group_name) {"mygroup"} 137 | let(:consumer_name) {"myconsumer"} 138 | 139 | it "can generate correct arguments" do 140 | expect(object).to receive(:call).with("XREADGROUP", "GROUP", group_name, consumer_name, "+ COUNT", count_value, "STREAMS", key_name, "writers", id) 141 | 142 | object.xreadgroup("GROUP", group_name, consumer_name, "+ COUNT", count_value, "STREAMS", key_name, "writers", id) 143 | end 144 | end 145 | 146 | describe '#xack' do 147 | let(:key_name) {"mykey"} 148 | let(:group_name) {"mygroup"} 149 | let(:id) {"1000001234-0"} 150 | 151 | it "can generate correct arguments" do 152 | expect(object).to receive(:call).with("XACK", key_name, group_name, id) 153 | 154 | object.xack(key_name, group_name, id) 155 | end 156 | end 157 | 158 | describe '#xclaim' do 159 | let(:key_name) {"mykey"} 160 | let(:id) {"1000001234-0"} 161 | let(:min_idle_time) {"360000"} 162 | let(:group_name) {"mygroup"} 163 | let(:consumer_name) {"myconsumer"} 164 | 165 | it "can generate correct arguments" do 166 | expect(object).to receive(:call).with("XCLAIM", key_name, group_name, consumer_name, min_idle_time, id) 167 | 168 | object.xclaim(key_name, group_name, consumer_name, min_idle_time, id) 169 | end 170 | end 171 | 172 | describe '#xpending' do 173 | let(:key_name) {"mykey"} 174 | let(:group_name) {"mygroup"} 175 | 176 | it "can generate correct arguments" do 177 | expect(object).to receive(:call).with("XPENDING", key_name, group_name) 178 | 179 | object.xpending(key_name, group_name) 180 | end 181 | end 182 | end 183 | -------------------------------------------------------------------------------- /lib/protocol/redis/methods/streams.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2018, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | module Protocol 24 | module Redis 25 | module Methods 26 | module Streams 27 | # Get information on streams and consumer groups. O(N) with N being the number of returned items for the subcommands CONSUMERS and GROUPS. The STREAM subcommand is O(log N) with N being the number of items in the stream. 28 | # @see https://redis.io/commands/xinfo 29 | # @param help [Enum] 30 | def xinfo(*arguments) 31 | call("XINFO", *arguments) 32 | end 33 | 34 | # Appends a new entry to a stream. O(1). 35 | # @see https://redis.io/commands/xadd 36 | # @param key [Key] 37 | # @param ID [String] 38 | def xadd(*arguments) 39 | call("XADD", *arguments) 40 | end 41 | 42 | # Trims the stream to (approximately if '~' is passed) a certain size. O(N), with N being the number of evicted entries. Constant times are very small however, since entries are organized in macro nodes containing multiple entries that can be released with a single deallocation. 43 | # @see https://redis.io/commands/xtrim 44 | # @param key [Key] 45 | # @param strategy [Enum] 46 | # @param approx [Enum] 47 | # @param count [Integer] 48 | def xtrim(*arguments) 49 | call("XTRIM", *arguments) 50 | end 51 | 52 | # Removes the specified entries from the stream. Returns the number of items actually deleted, that may be different from the number of IDs passed in case certain IDs do not exist. O(1) for each single item to delete in the stream, regardless of the stream size. 53 | # @see https://redis.io/commands/xdel 54 | # @param key [Key] 55 | # @param ID [String] 56 | def xdel(*arguments) 57 | call("XDEL", *arguments) 58 | end 59 | 60 | # Return a range of elements in a stream, with IDs matching the specified IDs interval. O(N) with N being the number of elements being returned. If N is constant (e.g. always asking for the first 10 elements with COUNT), you can consider it O(1). 61 | # @see https://redis.io/commands/xrange 62 | # @param key [Key] 63 | # @param start [String] 64 | # @param end [String] 65 | def xrange(*arguments) 66 | call("XRANGE", *arguments) 67 | end 68 | 69 | # Return a range of elements in a stream, with IDs matching the specified IDs interval, in reverse order (from greater to smaller IDs) compared to XRANGE. O(N) with N being the number of elements returned. If N is constant (e.g. always asking for the first 10 elements with COUNT), you can consider it O(1). 70 | # @see https://redis.io/commands/xrevrange 71 | # @param key [Key] 72 | # @param end [String] 73 | # @param start [String] 74 | def xrevrange(*arguments) 75 | call("XREVRANGE", *arguments) 76 | end 77 | 78 | # Return the number of entires in a stream. O(1). 79 | # @see https://redis.io/commands/xlen 80 | # @param key [Key] 81 | def xlen(*arguments) 82 | call("XLEN", *arguments) 83 | end 84 | 85 | # Return never seen elements in multiple streams, with IDs greater than the ones reported by the caller for each stream. Can block. For each stream mentioned: O(N) with N being the number of elements being returned, it means that XREAD-ing with a fixed COUNT is O(1). Note that when the BLOCK option is used, XADD will pay O(M) time in order to serve the M clients blocked on the stream getting new data. 86 | # @see https://redis.io/commands/xread 87 | # @param streams [Enum] 88 | # @param key [Key] 89 | # @param id [String] 90 | def xread(*arguments) 91 | call("XREAD", *arguments) 92 | end 93 | 94 | # Create, destroy, and manage consumer groups. O(1) for all the subcommands, with the exception of the DESTROY subcommand which takes an additional O(M) time in order to delete the M entries inside the consumer group pending entries list (PEL). 95 | # @see https://redis.io/commands/xgroup 96 | def xgroup(*arguments) 97 | call("XGROUP", *arguments) 98 | end 99 | 100 | # Return new entries from a stream using a consumer group, or access the history of the pending entries for a given consumer. Can block. For each stream mentioned: O(M) with M being the number of elements returned. If M is constant (e.g. always asking for the first 10 elements with COUNT), you can consider it O(1). On the other side when XREADGROUP blocks, XADD will pay the O(N) time in order to serve the N clients blocked on the stream getting new data. 101 | # @see https://redis.io/commands/xreadgroup 102 | # @param noack [Enum] 103 | # @param streams [Enum] 104 | # @param key [Key] 105 | # @param ID [String] 106 | def xreadgroup(*arguments) 107 | call("XREADGROUP", *arguments) 108 | end 109 | 110 | # Marks a pending message as correctly processed, effectively removing it from the pending entries list of the consumer group. Return value of the command is the number of messages successfully acknowledged, that is, the IDs we were actually able to resolve in the PEL. O(1) for each message ID processed. 111 | # @see https://redis.io/commands/xack 112 | # @param key [Key] 113 | # @param group [String] 114 | # @param ID [String] 115 | def xack(*arguments) 116 | call("XACK", *arguments) 117 | end 118 | 119 | # Changes (or acquires) ownership of a message in a consumer group, as if the message was delivered to the specified consumer. O(log N) with N being the number of messages in the PEL of the consumer group. 120 | # @see https://redis.io/commands/xclaim 121 | # @param key [Key] 122 | # @param group [String] 123 | # @param consumer [String] 124 | # @param min-idle-time [String] 125 | # @param ID [String] 126 | def xclaim(*arguments) 127 | call("XCLAIM", *arguments) 128 | end 129 | 130 | # Return information and entries from a stream consumer group pending entries list, that are messages fetched but never acknowledged. O(N) with N being the number of elements returned, so asking for a small fixed number of entries per call is O(1). When the command returns just the summary it runs in O(1) time assuming the list of consumers is small, otherwise there is additional O(N) time needed to iterate every consumer. 131 | # @see https://redis.io/commands/xpending 132 | # @param key [Key] 133 | # @param group [String] 134 | # @param consumer [String] 135 | def xpending(*arguments) 136 | call("XPENDING", *arguments) 137 | end 138 | end 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /spec/protocol/redis/methods/strings_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2019, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require_relative 'helper' 24 | 25 | require 'protocol/redis/methods/strings' 26 | 27 | RSpec.describe Protocol::Redis::Methods::Strings do 28 | let(:object) {Object.including(Protocol::Redis::Methods::Strings).new} 29 | 30 | describe '#append' do 31 | let(:key) {"mykey"} 32 | let(:value) {"UpdatedValue"} 33 | 34 | it "can generate correct arguments" do 35 | expect(object).to receive(:call).with("APPEND", key, value) 36 | 37 | object.append(key, value) 38 | end 39 | end 40 | 41 | describe '#bitcount' do 42 | let(:key) {"mykey"} 43 | 44 | it "can generate correct arguments" do 45 | expect(object).to receive(:call).with("BITCOUNT", key, 0, 0) 46 | 47 | object.bitcount(key, 0, 0) 48 | end 49 | end 50 | 51 | describe '#decr' do 52 | let(:key) {"mykey"} 53 | 54 | it "can generate correct arguments" do 55 | expect(object).to receive(:call).with("DECR", key) 56 | 57 | object.decr(key) 58 | end 59 | end 60 | 61 | describe '#decrby' do 62 | let(:key) {"mykey"} 63 | let(:decrement) {4} 64 | 65 | it "can generate correct arguments" do 66 | expect(object).to receive(:call).with("DECRBY", key, decrement) 67 | 68 | object.decrby(key, decrement) 69 | end 70 | end 71 | 72 | describe '#get' do 73 | let(:key) {"mykey"} 74 | 75 | it "can generate correct arguments" do 76 | expect(object).to receive(:call).with("GET", key) 77 | 78 | object.get(key) 79 | end 80 | end 81 | 82 | describe '#getbit' do 83 | let(:key) {"mykey"} 84 | let(:offset) {4} 85 | 86 | it "can generate correct arguments" do 87 | expect(object).to receive(:call).with("GETBIT", key, offset) 88 | 89 | object.getbit(key, offset) 90 | end 91 | end 92 | 93 | describe '#getrange' do 94 | let(:key) {"mykey"} 95 | let(:start_index) {0} 96 | let(:end_index) {3} 97 | 98 | it "can generate correct arguments" do 99 | expect(object).to receive(:call).with("GETRANGE", key, start_index, end_index) 100 | 101 | object.getrange(key, start_index, end_index) 102 | end 103 | end 104 | 105 | describe '#getset' do 106 | let(:key) {"mykey"} 107 | let(:value) {"newkey"} 108 | 109 | it "can generate correct arguments" do 110 | expect(object).to receive(:call).with("GETSET", key, value) 111 | 112 | object.getset(key, value) 113 | end 114 | end 115 | 116 | describe '#incr' do 117 | let(:key) {"mykey"} 118 | 119 | it "can generate correct arguments" do 120 | expect(object).to receive(:call).with("INCR", key) 121 | 122 | object.incr(key) 123 | end 124 | end 125 | 126 | describe '#incrby' do 127 | let(:key) {"mykey"} 128 | let(:increment) {3} 129 | 130 | it "can generate correct arguments" do 131 | expect(object).to receive(:call).with("INCRBY", key, increment) 132 | 133 | object.incrby(key, increment) 134 | end 135 | end 136 | 137 | describe '#incrbyfloat' do 138 | let(:key) {"mykey"} 139 | let(:increment) {3.5} 140 | 141 | it "can generate correct arguments" do 142 | expect(object).to receive(:call).with("INCRBYFLOAT", key, increment) 143 | 144 | object.incrbyfloat(key, increment) 145 | end 146 | end 147 | 148 | describe '#mget' do 149 | let(:key1) {"mykey1"} 150 | let(:key2) {"mykey2"} 151 | 152 | it "can generate correct arguments" do 153 | expect(object).to receive(:call).with("MGET", key1, key2) 154 | 155 | object.mget(key1, key2) 156 | end 157 | end 158 | 159 | describe '#mset' do 160 | let(:pairs) {{"mykey1" => "myvalue1", "mykey2" => "myvalue2"}} 161 | 162 | it "can generate correct arguments" do 163 | expect(object).to receive(:call).with("MSET", "mykey1", "myvalue1", "mykey2", "myvalue2") 164 | 165 | object.mset(pairs) 166 | end 167 | end 168 | 169 | describe '#msetnx' do 170 | let(:pairs) {{"mykey1" => "myvalue1", "mykey2" => "myvalue2"}} 171 | 172 | it "can generate correct arguments" do 173 | expect(object).to receive(:call).with("MSETNX", "mykey1", "myvalue1", "mykey2", "myvalue2") 174 | 175 | object.msetnx(pairs) 176 | end 177 | end 178 | 179 | describe '#psetex' do 180 | let(:key) {"mykey"} 181 | let(:milliseconds) {1000} 182 | let(:value) {"myvalue"} 183 | 184 | it "can generate correct arguments" do 185 | expect(object).to receive(:call).with("PSETEX", key, milliseconds, value) 186 | 187 | object.psetex(key, milliseconds, value) 188 | end 189 | end 190 | 191 | describe '#set' do 192 | let(:key) {"mykey"} 193 | let(:value) {"newkey"} 194 | 195 | it "can generate correct arguments" do 196 | expect(object).to receive(:call).with("SET", key, value) 197 | 198 | object.set(key, value) 199 | end 200 | 201 | it "can generate correct arguments with SECONDS EXPIRY and KEY EXISTS options" do 202 | expect(object).to receive(:call).with("SET", key, value, 'EX', 60, 'XX') 203 | 204 | object.set(key, value, update: true, seconds: 60) 205 | end 206 | 207 | it "can generate correct arguments with MILLISECONDS EXPIRY and KEY NOT EXISTS options" do 208 | expect(object).to receive(:call).with("SET", key, value, 'PX', 60, 'NX') 209 | 210 | object.set(key, value, update: false, milliseconds: 60) 211 | end 212 | end 213 | 214 | describe '#setbit' do 215 | let(:key) {"mykey"} 216 | let(:offset) {2} 217 | let(:value) {"myvalue"} 218 | 219 | it "can generate correct arguments" do 220 | expect(object).to receive(:call).with("SETBIT", key, offset, value) 221 | 222 | object.setbit(key, offset, value) 223 | end 224 | end 225 | 226 | describe '#setex' do 227 | let(:key) {"mykey"} 228 | let(:seconds) {2} 229 | let(:value) {"myvalue"} 230 | 231 | it "can generate correct arguments" do 232 | expect(object).to receive(:call).with("SETEX", key, seconds, value) 233 | 234 | object.setex(key, seconds, value) 235 | end 236 | end 237 | 238 | describe '#setnx' do 239 | let(:key) {"mykey"} 240 | let(:value) {"myvalue"} 241 | 242 | it "can generate correct arguments" do 243 | expect(object).to receive(:call).with("SETNX", key, value) 244 | 245 | object.setnx(key, value) 246 | end 247 | end 248 | 249 | describe '#setrange' do 250 | let(:key) {"mykey"} 251 | let(:offset) {2} 252 | let(:value) {"myvalue"} 253 | 254 | it "can generate correct arguments" do 255 | expect(object).to receive(:call).with("SETRANGE", key, offset, value) 256 | 257 | object.setrange(key, offset, value) 258 | end 259 | end 260 | 261 | describe '#strlen' do 262 | let(:key) {"mykey"} 263 | 264 | it "can generate correct arguments" do 265 | expect(object).to receive(:call).with("STRLEN", key) 266 | 267 | object.strlen(key) 268 | end 269 | end 270 | end 271 | -------------------------------------------------------------------------------- /lib/protocol/redis/methods/lists.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2018, by Samuel G. D. Williams. 4 | # Copyright, 2018, by Huba Nagy. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | 24 | module Protocol 25 | module Redis 26 | module Methods 27 | module Lists 28 | # Remove and get the first element in a list, or block until one is available. O(1). 29 | # @see https://redis.io/commands/blpop 30 | # @param key [Key] 31 | # @param timeout [Integer] 32 | def blpop(*keys, timeout: 0) 33 | call('BLPOP', *keys, timeout) 34 | end 35 | 36 | # Remove and get the last element in a list, or block until one is available. O(1). 37 | # @see https://redis.io/commands/brpop 38 | # @param key [Key] 39 | # @param timeout [Integer] 40 | def brpop(*keys, timeout: 0) 41 | call('BRPOP', *keys, timeout) 42 | end 43 | 44 | # Pop an element from a list, push it to another list and return it; or block until one is available. O(1). 45 | # @see https://redis.io/commands/brpoplpush 46 | # @param source [Key] 47 | # @param destination [Key] 48 | # @param timeout [Integer] 49 | def brpoplpush(source, destination, timeout) 50 | call('BRPOPLPUSH', source, destination, timeout) 51 | end 52 | 53 | # Get an element from a list by its index. O(N) where N is the number of elements to traverse to get to the element at index. This makes asking for the first or the last element of the list O(1). 54 | # @see https://redis.io/commands/lindex 55 | # @param key [Key] 56 | # @param index [Integer] 57 | def lindex(key, index) 58 | call('LINDEX', key, index) 59 | end 60 | 61 | # Insert an element before or after another element in a list. O(N) where N is the number of elements to traverse before seeing the value pivot. This means that inserting somewhere on the left end on the list (head) can be considered O(1) and inserting somewhere on the right end (tail) is O(N). 62 | # @see https://redis.io/commands/linsert 63 | # @param key [Key] 64 | # @param where [Enum] 65 | # @param pivot [String] 66 | # @param element [String] 67 | def linsert(key, position=:before, index, value) 68 | if position == :before 69 | offset = 'BEFORE' 70 | else 71 | offset = 'AFTER' 72 | end 73 | 74 | call('LINSERT', key, offset, index, value) 75 | end 76 | 77 | # Get the length of a list. O(1). 78 | # @see https://redis.io/commands/llen 79 | # @param key [Key] 80 | def llen(key) 81 | call('LLEN', key) 82 | end 83 | 84 | # Remove and get the first element in a list. O(1). 85 | # @see https://redis.io/commands/lpop 86 | # @param key [Key] 87 | def lpop(key) 88 | call('LPOP', key) 89 | end 90 | 91 | # Prepend one or multiple elements to a list. O(1) for each element added, so O(N) to add N elements when the command is called with multiple arguments. 92 | # @see https://redis.io/commands/lpush 93 | # @param key [Key] 94 | # @param element [String] 95 | def lpush(key, value, *values) 96 | case value 97 | when Array 98 | values = value 99 | else 100 | values = [value] + values 101 | end 102 | 103 | call('LPUSH', key, *values) 104 | end 105 | 106 | # Prepend an element to a list, only if the list exists. O(1) for each element added, so O(N) to add N elements when the command is called with multiple arguments. 107 | # @see https://redis.io/commands/lpushx 108 | # @param key [Key] 109 | # @param element [String] 110 | def lpushx(key, value) 111 | call('LPUSHX', key, value) 112 | end 113 | 114 | # Get a range of elements from a list. O(S+N) where S is the distance of start offset from HEAD for small lists, from nearest end (HEAD or TAIL) for large lists; and N is the number of elements in the specified range. 115 | # @see https://redis.io/commands/lrange 116 | # @param key [Key] 117 | # @param start [Integer] 118 | # @param stop [Integer] 119 | def lrange(key, start, stop) 120 | call('LRANGE', key, start, stop) 121 | end 122 | 123 | # Remove elements from a list. O(N+M) where N is the length of the list and M is the number of elements removed. 124 | # @see https://redis.io/commands/lrem 125 | # @param key [Key] 126 | # @param count [Integer] 127 | # @param element [String] 128 | def lrem(key, count, value) 129 | call('LREM', key, count, value) 130 | end 131 | 132 | # Set the value of an element in a list by its index. O(N) where N is the length of the list. Setting either the first or the last element of the list is O(1). 133 | # @see https://redis.io/commands/lset 134 | # @param key [Key] 135 | # @param index [Integer] 136 | # @param element [String] 137 | def lset(key, index, values) 138 | call('LSET', key, index, values) 139 | end 140 | 141 | # Trim a list to the specified range. O(N) where N is the number of elements to be removed by the operation. 142 | # @see https://redis.io/commands/ltrim 143 | # @param key [Key] 144 | # @param start [Integer] 145 | # @param stop [Integer] 146 | def ltrim(key, start, stop) 147 | call('LTRIM', key, start, stop) 148 | end 149 | 150 | # Remove and get the last element in a list. O(1). 151 | # @see https://redis.io/commands/rpop 152 | # @param key [Key] 153 | def rpop(key) 154 | call('RPOP', key) 155 | end 156 | 157 | # Remove the last element in a list, prepend it to another list and return it. O(1). 158 | # @see https://redis.io/commands/rpoplpush 159 | # @param source [Key] 160 | # @param destination [Key] 161 | def rpoplpush(source, destination=nil) 162 | destination = source if destination.nil? 163 | 164 | call('RPOPLPUSH', source, destination) 165 | end 166 | 167 | # Append one or multiple elements to a list. O(1) for each element added, so O(N) to add N elements when the command is called with multiple arguments. 168 | # @see https://redis.io/commands/rpush 169 | # @param key [Key] 170 | # @param element [String] 171 | def rpush(key, value, *values) 172 | case value 173 | when Array 174 | values = value 175 | else 176 | values = [value] + values 177 | end 178 | 179 | call('RPUSH', key, *values) 180 | end 181 | 182 | # Append an element to a list, only if the list exists. O(1) for each element added, so O(N) to add N elements when the command is called with multiple arguments. 183 | # @see https://redis.io/commands/rpushx 184 | # @param key [Key] 185 | # @param element [String] 186 | def rpushx(key, value) 187 | call('RPUSHX', key, value) 188 | end 189 | end 190 | end 191 | end 192 | end 193 | -------------------------------------------------------------------------------- /lib/protocol/redis/methods/strings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2018, by Samuel G. D. Williams. 4 | # Copyright, 2018, by Huba Nagy. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | 24 | module Protocol 25 | module Redis 26 | module Methods 27 | module Strings 28 | # Append a value to a key. O(1). The amortized time complexity is O(1) assuming the appended value is small and the already present value is of any size, since the dynamic string library used by Redis will double the free space available on every reallocation. 29 | # @see https://redis.io/commands/append 30 | # @param key [Key] 31 | # @param value [String] 32 | def append(key, value) 33 | call('APPEND', key, value) 34 | end 35 | 36 | # Count set bits in a string. O(N). 37 | # @see https://redis.io/commands/bitcount 38 | # @param key [Key] 39 | def bitcount(key, *range) 40 | call('BITCOUNT', key, *range) 41 | end 42 | 43 | # Decrement the integer value of a key by one. O(1). 44 | # @see https://redis.io/commands/decr 45 | # @param key [Key] 46 | def decr(key) 47 | call('DECR', key) 48 | end 49 | 50 | # Decrement the integer value of a key by the given number. O(1). 51 | # @see https://redis.io/commands/decrby 52 | # @param key [Key] 53 | # @param decrement [Integer] 54 | def decrby(key, decrement) 55 | call('DECRBY', key, decrement) 56 | end 57 | 58 | # Get the value of a key. O(1). 59 | # @see https://redis.io/commands/get 60 | # @param key [Key] 61 | def get(key) 62 | call('GET', key) 63 | end 64 | 65 | # Returns the bit value at offset in the string value stored at key. O(1). 66 | # @see https://redis.io/commands/getbit 67 | # @param key [Key] 68 | # @param offset [Integer] 69 | def getbit(key, offset) 70 | call('GETBIT', key, offset) 71 | end 72 | 73 | # Get a substring of the string stored at a key. O(N) where N is the length of the returned string. The complexity is ultimately determined by the returned length, but because creating a substring from an existing string is very cheap, it can be considered O(1) for small strings. 74 | # @see https://redis.io/commands/getrange 75 | # @param key [Key] 76 | # @param start [Integer] 77 | # @param end [Integer] 78 | def getrange(key, start_index, end_index) 79 | call('GETRANGE', key, start_index, end_index) 80 | end 81 | 82 | # Set the string value of a key and return its old value. O(1). 83 | # @see https://redis.io/commands/getset 84 | # @param key [Key] 85 | # @param value [String] 86 | def getset(key, value) 87 | call('GETSET', key, value) 88 | end 89 | 90 | # Increment the integer value of a key by one. O(1). 91 | # @see https://redis.io/commands/incr 92 | # @param key [Key] 93 | def incr(key) 94 | call('INCR', key) 95 | end 96 | 97 | # Increment the integer value of a key by the given amount. O(1). 98 | # @see https://redis.io/commands/incrby 99 | # @param key [Key] 100 | # @param increment [Integer] 101 | def incrby(key, increment) 102 | call('INCRBY', key, increment) 103 | end 104 | 105 | # Increment the float value of a key by the given amount. O(1). 106 | # @see https://redis.io/commands/incrbyfloat 107 | # @param key [Key] 108 | # @param increment [Double] 109 | def incrbyfloat(key, increment) 110 | call('INCRBYFLOAT', key, increment) 111 | end 112 | 113 | # Get the values of all the given keys. O(N) where N is the number of keys to retrieve. 114 | # @see https://redis.io/commands/mget 115 | # @param key [Key] 116 | def mget(key, *keys) 117 | call('MGET', key, *keys) 118 | end 119 | 120 | # Set multiple keys to multiple values. O(N) where N is the number of keys to set. 121 | # @see https://redis.io/commands/mset 122 | def mset(pairs) 123 | flattened_pairs = pairs.keys.zip(pairs.values).flatten 124 | 125 | call('MSET', *flattened_pairs) 126 | end 127 | 128 | # Set multiple keys to multiple values, only if none of the keys exist. O(N) where N is the number of keys to set. 129 | # @see https://redis.io/commands/msetnx 130 | def msetnx(pairs) 131 | flattened_pairs = pairs.keys.zip(pairs.values).flatten 132 | 133 | call('MSETNX', *flattened_pairs) 134 | end 135 | 136 | # Set the value and expiration in milliseconds of a key. O(1). 137 | # @see https://redis.io/commands/psetex 138 | # @param key [Key] 139 | # @param milliseconds [Integer] 140 | # @param value [String] 141 | def psetex(key, milliseconds, value) 142 | call('PSETEX', key, milliseconds, value) 143 | end 144 | 145 | # Set the string value of a key. O(1). 146 | # @see https://redis.io/commands/set 147 | # @param key [Key] 148 | # @param value [String] 149 | # @param expiration [Enum] 150 | # @param update [Boolean, nil] If true, only update elements that already exist (never add elements). If false, don't update existing elements (only add new elements). 151 | def set(key, value, update: nil, seconds: nil, milliseconds: nil) 152 | arguments = [] 153 | 154 | if seconds 155 | arguments << 'EX' << seconds 156 | end 157 | 158 | if milliseconds 159 | arguments << 'PX' << milliseconds 160 | end 161 | 162 | if update == true 163 | arguments << "XX" 164 | elsif update == false 165 | arguments << "NX" 166 | end 167 | 168 | call('SET', key, value, *arguments) 169 | end 170 | 171 | # Sets or clears the bit at offset in the string value stored at key. O(1). 172 | # @see https://redis.io/commands/setbit 173 | # @param key [Key] 174 | # @param offset [Integer] 175 | # @param value [Integer] 176 | def setbit(key, offset, value) 177 | call('SETBIT', key, offset, value) 178 | end 179 | 180 | # Set the value and expiration of a key. O(1). 181 | # @see https://redis.io/commands/setex 182 | # @param key [Key] 183 | # @param seconds [Integer] 184 | # @param value [String] 185 | def setex(key, seconds, value) 186 | call('SETEX', key, seconds, value) 187 | end 188 | 189 | # Set the value of a key, only if the key does not exist. O(1). 190 | # @return [Boolean] if the key was set. 191 | # @see https://redis.io/commands/setnx 192 | # @param key [Key] 193 | # @param value [String] 194 | def setnx(key, value) 195 | call('SETNX', key, value) == 1 196 | end 197 | 198 | # Overwrite part of a string at key starting at the specified offset. O(1), not counting the time taken to copy the new string in place. Usually, this string is very small so the amortized complexity is O(1). Otherwise, complexity is O(M) with M being the length of the value argument. 199 | # @see https://redis.io/commands/setrange 200 | # @param key [Key] 201 | # @param offset [Integer] 202 | # @param value [String] 203 | def setrange(key, offset, value) 204 | call('SETRANGE', key, offset, value) 205 | end 206 | 207 | # Get the length of the value stored in a key. O(1). 208 | # @see https://redis.io/commands/strlen 209 | # @param key [Key] 210 | def strlen(key) 211 | call('STRLEN', key) 212 | end 213 | end 214 | end 215 | end 216 | end 217 | -------------------------------------------------------------------------------- /lib/protocol/redis/methods/geospatial.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2018, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | module Protocol 24 | module Redis 25 | module Methods 26 | module Geospatial 27 | # Add one or more geospatial items in the geospatial index represented using a sorted set. O(log(N)) for each item added, where N is the number of elements in the sorted set. 28 | # @see https://redis.io/commands/geoadd 29 | # @param key [Key] 30 | def geoadd(key, longitude, latitude, member, *arguments) 31 | call("GEOADD", longitude, latitude, member, *arguments) 32 | end 33 | 34 | # Returns members of a geospatial index as standard geohash strings. O(log(N)) for each member requested, where N is the number of elements in the sorted set. 35 | # @see https://redis.io/commands/geohash 36 | # @param key [Key] 37 | # @param member [String] 38 | def geohash(key, member, *members) 39 | call("GEOHASH", key, member, *members) 40 | end 41 | 42 | # Returns longitude and latitude of members of a geospatial index. O(log(N)) for each member requested, where N is the number of elements in the sorted set. 43 | # @see https://redis.io/commands/geopos 44 | # @param key [Key] 45 | # @param member [String] 46 | def geopos(key, member, *members) 47 | call("GEOPOS", key, member, *members) 48 | end 49 | 50 | # Returns the distance between two members of a geospatial index. O(log(N)). 51 | # @see https://redis.io/commands/geodist 52 | # @param key [Key] 53 | # @param member1 [String] 54 | # @param member2 [String] 55 | # @param unit [Enum] Distance scale to use, one of "m" (meters), "km" (kilometers), "mi" (miles) or "ft" (feet). 56 | def geodist(key, from, to, unit = "m") 57 | call("GEODIST", key, from, to, unit) 58 | end 59 | 60 | # Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point. O(N+log(M)) where N is the number of elements inside the bounding box of the circular area delimited by center and radius and M is the number of items inside the index. 61 | # @see https://redis.io/commands/georadius 62 | # @param key [Key] 63 | # @param longitude [Double] 64 | # @param latitude [Double] 65 | # @param radius [Double] 66 | # @param unit [Enum] 67 | # @param count [Integer] Limit the number of results to at most this many. 68 | # @param order [Symbol] `:ASC` Sort returned items from the nearest to the farthest, relative to the center. `:DESC` Sort returned items from the farthest to the nearest, relative to the center. 69 | # @param with_coordinates [Boolean] Also return the longitude,latitude coordinates of the matching items. 70 | # @param with_distance [Boolean] Also return the distance of the returned items from the specified center. The distance is returned in the same unit as the unit specified as the radius argument of the command. 71 | # @param with_hash [Boolean] Also return the raw geohash-encoded sorted set score of the item, in the form of a 52 bit unsigned integer. This is only useful for low level hacks or debugging and is otherwise of little interest for the general user. 72 | # @param store [Key] 73 | # @param store_distance [Key] 74 | def georadius(key, longitude, latitude, radius, unit = "m", with_coordinates: false, with_distance: false, with_hash: false, count: nil, order: nil, store: nil, store_distance: nil) 75 | arguments = [key, longitude, latitude, radius, unit] 76 | 77 | if with_coordinates 78 | arguments.append("WITHCOORD") 79 | end 80 | 81 | if with_distance 82 | arguments.append("WITHDIST") 83 | end 84 | 85 | if with_hash 86 | arguments.append("WITHHASH") 87 | end 88 | 89 | if count 90 | arguments.append("COUNT", count) 91 | end 92 | 93 | if order 94 | arguments.append(order) 95 | end 96 | 97 | readonly = true 98 | 99 | if store 100 | arguments.append("STORE", store) 101 | readonly = false 102 | end 103 | 104 | if store_distance 105 | arguments.append("STOREDIST", storedist) 106 | readonly = false 107 | end 108 | 109 | # https://redis.io/commands/georadius#read-only-variants 110 | if readonly 111 | call("GEORADIUS_RO", *arguments) 112 | else 113 | call("GEORADIUS", *arguments) 114 | end 115 | end 116 | 117 | # Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member. O(N+log(M)) where N is the number of elements inside the bounding box of the circular area delimited by center and radius and M is the number of items inside the index. 118 | # @see https://redis.io/commands/georadiusbymember 119 | # @param key [Key] 120 | # @param member [String] 121 | # @param radius [Double] 122 | # @param unit [Enum] 123 | # @param count [Integer] Limit the number of results to at most this many. 124 | # @param order [Symbol] `:ASC` Sort returned items from the nearest to the farthest, relative to the center. `:DESC` Sort returned items from the farthest to the nearest, relative to the center. 125 | # @param with_coordinates [Boolean] Also return the longitude,latitude coordinates of the matching items. 126 | # @param with_distance [Boolean] Also return the distance of the returned items from the specified center. The distance is returned in the same unit as the unit specified as the radius argument of the command. 127 | # @param with_hash [Boolean] Also return the raw geohash-encoded sorted set score of the item, in the form of a 52 bit unsigned integer. This is only useful for low level hacks or debugging and is otherwise of little interest for the general user. 128 | # @param store [Key] 129 | # @param store_distance [Key] 130 | def georadiusbymember(key, member, radius, unit = "m", with_coordinates: false, with_distance: false, with_hash: false, count: nil, order: nil, store: nil, store_distance: nil) 131 | arguments = [key, member, radius, unit] 132 | 133 | if with_coordinates 134 | arguments.append("WITHCOORD") 135 | end 136 | 137 | if with_distance 138 | arguments.append("WITHDIST") 139 | end 140 | 141 | if with_hash 142 | arguments.append("WITHHASH") 143 | end 144 | 145 | if count 146 | arguments.append("COUNT", count) 147 | end 148 | 149 | if order 150 | arguments.append(order) 151 | end 152 | 153 | if store 154 | arguments.append("STORE", store) 155 | end 156 | 157 | if store_distance 158 | arguments.append("STOREDIST", storedist) 159 | end 160 | 161 | readonly = true 162 | 163 | if store 164 | arguments.append("STORE", store) 165 | readonly = false 166 | end 167 | 168 | if store_distance 169 | arguments.append("STOREDIST", storedist) 170 | readonly = false 171 | end 172 | 173 | # https://redis.io/commands/georadius#read-only-variants 174 | if readonly 175 | call("GEORADIUSBYMEMBER_RO", *arguments) 176 | else 177 | call("GEORADIUSBYMEMBER", *arguments) 178 | end 179 | end 180 | end 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /lib/protocol/redis/methods/generic.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2018, by Samuel G. D. Williams. 4 | # Copyright, 2018, by Huba Nagy. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | 24 | require 'date' 25 | 26 | module Protocol 27 | module Redis 28 | module Methods 29 | module Generic 30 | # Delete a key. O(N) where N is the number of keys that will be removed. When a key to remove holds a value other than a string, the individual complexity for this key is O(M) where M is the number of elements in the list, set, sorted set or hash. Removing a single key that holds a string value is O(1). 31 | # @see https://redis.io/commands/del 32 | # @param key [Key] 33 | def del(*keys) 34 | if keys.any? 35 | call('DEL', *keys) 36 | end 37 | end 38 | 39 | # Return a serialized version of the value stored at the specified key. O(1) to access the key and additional O(N*M) to serialized it, where N is the number of Redis objects composing the value and M their average size. For small string values the time complexity is thus O(1)+O(1*M) where M is small, so simply O(1). 40 | # @see https://redis.io/commands/dump 41 | # @param key [Key] 42 | def dump(key) 43 | call('DUMP', key) 44 | end 45 | 46 | # Determine if a key exists. O(1). 47 | # @see https://redis.io/commands/exists 48 | # @param key [Key] 49 | # @return [Integer] 50 | def exists(key, *keys) 51 | call('EXISTS', key, *keys) 52 | end 53 | 54 | # Boolean oversion of `exists` 55 | # @param key [Key] 56 | # @return [Boolean] 57 | def exists?(key, *keys) 58 | exists(key, *keys) > 0 59 | end 60 | 61 | # Set a key's time to live in seconds. O(1). 62 | # @see https://redis.io/commands/expire 63 | # @param key [Key] 64 | # @param seconds [Integer] 65 | def expire(key, seconds) 66 | call('EXPIRE', key, seconds) 67 | end 68 | 69 | # Set the expiration for a key as a UNIX timestamp. O(1). 70 | # @see https://redis.io/commands/expireat 71 | # @param key [Key] 72 | # @param timestamp [Posix time] 73 | def expireat(key, time) 74 | case time 75 | when DateTime, Time, Date 76 | timestamp = time.strftime('%s').to_i 77 | else 78 | timestamp = time 79 | end 80 | 81 | call('EXPIREAT', key, timestamp) 82 | end 83 | 84 | # Find all keys matching the given pattern. O(N) with N being the number of keys in the database, under the assumption that the key names in the database and the given pattern have limited length. 85 | # @see https://redis.io/commands/keys 86 | # @param pattern [Pattern] 87 | def keys(pattern) 88 | call('KEYS', pattern) 89 | end 90 | 91 | # Atomically transfer a key from a Redis instance to another one. This command actually executes a DUMP+DEL in the source instance, and a RESTORE in the target instance. See the pages of these commands for time complexity. Also an O(N) data transfer between the two instances is performed. 92 | # @see https://redis.io/commands/migrate 93 | # @param host [String] 94 | # @param port [String] 95 | # @param key [Enum] 96 | # @param destination-db [Integer] 97 | # @param timeout [Integer] 98 | # @param copy [Enum] 99 | # @param replace [Enum] 100 | def migrate(host, port, destination = 0, keys:, timeout: 0, copy: false, replace: false, auth: nil) 101 | raise ArgumentError, "Must provide keys" if keys.empty? 102 | 103 | arguments = [host, port] 104 | 105 | if keys.size == 1 106 | arguments.append(*keys) 107 | else 108 | arguments.append("") 109 | end 110 | 111 | arguments.append(destination, timeout) 112 | 113 | if copy 114 | arguments.append("COPY") 115 | end 116 | 117 | if replace 118 | arguments.append("REPLACE") 119 | end 120 | 121 | if auth 122 | arguments.append("AUTH", auth) 123 | end 124 | 125 | if keys.size > 1 126 | arguments.append("KEYS", *keys) 127 | end 128 | 129 | call("MIGRATE", *arguments) 130 | end 131 | 132 | # Move a key to another database. O(1). 133 | # @see https://redis.io/commands/move 134 | # @param key [Key] 135 | # @param db [Integer] 136 | def move(key, db) 137 | call('MOVE', key, db) 138 | end 139 | 140 | # Inspect the internals of Redis objects. O(1) for all the currently implemented subcommands. 141 | # @see https://redis.io/commands/object 142 | # @param subcommand [String] 143 | # @param arguments [String] 144 | def object(subcommand, *arguments) 145 | call('OBJECT', subcommand, *arguments) 146 | end 147 | 148 | # Remove the expiration from a key. O(1). 149 | # @see https://redis.io/commands/persist 150 | # @param key [Key] 151 | def persist(key) 152 | call('PERSIST', key) 153 | end 154 | 155 | # Set a key's time to live in milliseconds. O(1). 156 | # @see https://redis.io/commands/pexpire 157 | # @param key [Key] 158 | # @param milliseconds [Integer] 159 | def pexpire(key, milliseconds) 160 | call('PEXPIRE', milliseconds) 161 | end 162 | 163 | # Set the expiration for a key as a UNIX timestamp specified in milliseconds. O(1). 164 | # @see https://redis.io/commands/pexpireat 165 | # @param key [Key] 166 | # @param milliseconds-timestamp [Posix time] 167 | def pexpireat(key, time) 168 | case time.class 169 | when DateTime, Time, Date 170 | timestamp = time.strftime('%Q').to_i 171 | else 172 | timestamp = time 173 | end 174 | 175 | call('PEXPIREAT', key, timestamp) 176 | end 177 | 178 | # Get the time to live for a key in milliseconds. O(1). 179 | # @see https://redis.io/commands/pttl 180 | # @param key [Key] 181 | def pttl(key) 182 | call('PTTL', key) 183 | end 184 | 185 | # Return a random key from the keyspace. O(1). 186 | # @see https://redis.io/commands/randomkey 187 | def randomkey 188 | call('RANDOMKEY') 189 | end 190 | 191 | # Rename a key. O(1). 192 | # @see https://redis.io/commands/rename 193 | # @param key [Key] 194 | # @param newkey [Key] 195 | def rename(key, new_key) 196 | call('RENAME', key, new_key) 197 | end 198 | 199 | # Rename a key, only if the new key does not exist. O(1). 200 | # @see https://redis.io/commands/renamenx 201 | # @param key [Key] 202 | # @param newkey [Key] 203 | def renamenx(key, new_key) 204 | call('RENAMENX', key, new_key) 205 | end 206 | 207 | # Create a key using the provided serialized value, previously obtained using DUMP. O(1) to create the new key and additional O(N*M) to reconstruct the serialized value, where N is the number of Redis objects composing the value and M their average size. For small string values the time complexity is thus O(1)+O(1*M) where M is small, so simply O(1). However for sorted set values the complexity is O(N*M*log(N)) because inserting values into sorted sets is O(log(N)). 208 | # @see https://redis.io/commands/restore 209 | # @param key [Key] 210 | # @param ttl [Integer] 211 | # @param serialized-value [String] 212 | # @param replace [Enum] 213 | # @param absttl [Enum] 214 | def restore(key, serialized_value, ttl=0) 215 | call('RESTORE', key, ttl, serialized_value) 216 | end 217 | 218 | # Incrementally iterate the keys space. O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection. 219 | # @see https://redis.io/commands/scan 220 | # @param cursor [Integer] 221 | def scan(cursor, match: nil, count: nil, type: nil) 222 | arguments = [cursor] 223 | 224 | if match 225 | arguments.append("MATCH", match) 226 | end 227 | 228 | if count 229 | arguments.append("COUNT", count) 230 | end 231 | 232 | if type 233 | arguments.append("TYPE", type) 234 | end 235 | 236 | call("SCAN", *arguments) 237 | end 238 | 239 | # Sort the elements in a list, set or sorted set. O(N+M*log(M)) where N is the number of elements in the list or set to sort, and M the number of returned elements. When the elements are not sorted, complexity is currently O(N) as there is a copy step that will be avoided in next releases. 240 | # @see https://redis.io/commands/sort 241 | # @param key [Key] 242 | # @param order [Enum] 243 | # @param sorting [Enum] 244 | def sort(key, by: nil, offset: nil, count: nil, get: nil, order: 'ASC', alpha: false, store: nil) 245 | arguments = [] 246 | 247 | if by 248 | arguments.append("BY", by) 249 | end 250 | 251 | if offset and count 252 | arguments.append("LIMIT", offset, count) 253 | end 254 | 255 | get&.each do |pattern| 256 | arguments.append("GET", pattern) 257 | end 258 | 259 | if order 260 | arguments.append(order) 261 | end 262 | 263 | if alpha 264 | arguments.append("ALPHA") 265 | end 266 | 267 | if store 268 | arguments.append("STORE", store) 269 | end 270 | 271 | call('SORT', *arguments) 272 | end 273 | 274 | # Alters the last access time of a key(s). Returns the number of existing keys specified. O(N) where N is the number of keys that will be touched. 275 | # @see https://redis.io/commands/touch 276 | # @param key [Key] 277 | def touch(key, *keys) 278 | call('TOUCH', key, *keys) 279 | end 280 | 281 | # Get the time to live for a key. O(1). 282 | # @see https://redis.io/commands/ttl 283 | # @param key [Key] 284 | def ttl(key) 285 | call('TTL', key) 286 | end 287 | 288 | # Determine the type stored at key. O(1). 289 | # @see https://redis.io/commands/type 290 | # @param key [Key] 291 | def type(key) 292 | call('TYPE', key) 293 | end 294 | 295 | # Delete a key asynchronously in another thread. Otherwise it is just as DEL, but non blocking. O(1) for each key removed regardless of its size. Then the command does O(N) work in a different thread in order to reclaim memory, where N is the number of allocations the deleted objects where composed of. 296 | # @see https://redis.io/commands/unlink 297 | # @param key [Key] 298 | def unlink(key) 299 | call('UNLINK', key) 300 | end 301 | 302 | # Wait for the synchronous replication of all the write commands sent in the context of the current connection. O(1). 303 | # @see https://redis.io/commands/wait 304 | # @param numreplicas [Integer] 305 | # @param timeout [Integer] 306 | def wait(newreplicas, timeout = 0) 307 | call("WAIT", numreplicas, timeout) 308 | end 309 | end 310 | end 311 | end 312 | end 313 | -------------------------------------------------------------------------------- /lib/protocol/redis/methods/sorted_sets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2018, by Samuel G. D. Williams. 4 | # Copyright, 2020, by Dimitry Chopey. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | 24 | module Protocol 25 | module Redis 26 | module Methods 27 | module SortedSets 28 | # Remove and return the member with the lowest score from one or more sorted sets, or block until one is available. O(log(N)) with N being the number of elements in the sorted set. 29 | # @see https://redis.io/commands/bzpopmin 30 | # @param key [Key] 31 | # @param timeout [Integer] 32 | def bzpopmin(*keys, timeout: 0) 33 | call("BZPOPMIN", *keys, timeout) 34 | end 35 | 36 | # Remove and return the member with the highest score from one or more sorted sets, or block until one is available. O(log(N)) with N being the number of elements in the sorted set. 37 | # @see https://redis.io/commands/bzpopmax 38 | # @param key [Key] 39 | # @param timeout [Integer] 40 | def bzpopmax(*keys, timeout: 0) 41 | call("BZPOPMAX", *keys, timeout: 0) 42 | end 43 | 44 | # Add one or more members to a sorted set, or update its score if it already exists. O(log(N)) for each item added, where N is the number of elements in the sorted set. 45 | # @see https://redis.io/commands/zadd 46 | # @param key [Key] 47 | # @param score [Double] 48 | # @param member [String] 49 | # @param others [Array] an array of `score`, `member` elements. 50 | # @param update [Boolean, nil] If true, only update elements that already exist (never add elements). If false, don't update existing elements (only add new elements). 51 | # @param change [Boolean] Modify the return value from the number of new elements added, 52 | # to the total number of elements changed; changed elements are new elements added 53 | # and elements already existing for which the score was updated. 54 | # @param increment [Boolean] When this option is specified ZADD acts like ZINCRBY; 55 | # only one score-element pair can be specified in this mode. 56 | def zadd(key, score, member, *others, update: nil, change: false, increment: false) 57 | arguments = ["ZADD", key] 58 | 59 | if update == true 60 | arguments.push("XX") 61 | elsif update == false 62 | arguments.push("NX") 63 | end 64 | 65 | arguments.push("CH") if change 66 | arguments.push("INCR") if increment 67 | 68 | arguments.push(score, member) 69 | arguments.push(*others) 70 | 71 | call(*arguments) 72 | end 73 | 74 | # Get the number of members in a sorted set. O(1). 75 | # @see https://redis.io/commands/zcard 76 | # @param key [Key] 77 | def zcard(key) 78 | call("ZCARD", key) 79 | end 80 | 81 | # Count the members in a sorted set with scores within the given values. O(log(N)) with N being the number of elements in the sorted set. 82 | # @see https://redis.io/commands/zcount 83 | # @param key [Key] 84 | # @param min [Double] 85 | # @param max [Double] 86 | def zcount(key, min, max) 87 | call("ZCOUNT", key, min, max) 88 | end 89 | 90 | # Increment the score of a member in a sorted set. O(log(N)) where N is the number of elements in the sorted set. 91 | # @see https://redis.io/commands/zincrby 92 | # @param key [Key] 93 | # @param increment [Integer] 94 | # @param member [String] 95 | def zincrby(key, increment, member) 96 | call("ZINCRBY", key, amount, member) 97 | end 98 | 99 | # Intersect multiple sorted sets and store the resulting sorted set in a new key. O(N*K)+O(M*log(M)) worst case with N being the smallest input sorted set, K being the number of input sorted sets and M being the number of elements in the resulting sorted set. 100 | # @see https://redis.io/commands/zinterstore 101 | # @param destination [Key] 102 | # @param keys [Array] 103 | # @param weights [Array] 104 | # @param aggregate [Enum] one of sum, min, max. 105 | def zinterstore(destination, keys, weights = nil, aggregate: nil) 106 | arguments = [] 107 | 108 | if weights 109 | if weights.size != keys.size 110 | raise ArgumentError, "#{weights.size} weights given for #{keys.size} keys!" 111 | end 112 | 113 | arguments.push("WEIGHTS") 114 | arguments.concat(weights) 115 | end 116 | 117 | if aggregate 118 | arguments.push("AGGREGATE", aggregate) 119 | end 120 | 121 | call("ZINTERSTORE", destination, keys.size, *keys, *arguments) 122 | end 123 | 124 | # Count the number of members in a sorted set between a given lexicographical range. O(log(N)) with N being the number of elements in the sorted set. 125 | # @see https://redis.io/commands/zlexcount 126 | # @param key [Key] 127 | # @param min [String] 128 | # @param max [String] 129 | def zlexcount(key, min, max) 130 | call("ZLEXCOUNT", key, min, max) 131 | end 132 | 133 | # Remove and return members with the highest scores in a sorted set. O(log(N)*M) with N being the number of elements in the sorted set, and M being the number of elements popped. 134 | # @see https://redis.io/commands/zpopmax 135 | # @param key [Key] 136 | # @param count [Integer] 137 | def zpopmax(key, count = 1) 138 | call("ZPOPMAX", key, count) 139 | end 140 | 141 | # Remove and return members with the lowest scores in a sorted set. O(log(N)*M) with N being the number of elements in the sorted set, and M being the number of elements popped. 142 | # @see https://redis.io/commands/zpopmin 143 | # @param key [Key] 144 | # @param count [Integer] 145 | def zpopmin(key, count = 1) 146 | call("ZPOPMIN", key, count = 1) 147 | end 148 | 149 | # Return a range of members in a sorted set, by index. O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements returned. 150 | # @see https://redis.io/commands/zrange 151 | # @param key [Key] 152 | # @param start [Integer] 153 | # @param stop [Integer] 154 | # @param with_scores [Boolean] Return the scores of the elements together with the elements. 155 | def zrange(key, start, stop, with_scores: false) 156 | arguments = [start, stop] 157 | 158 | arguments.push("WITHSCORES") if with_scores 159 | 160 | call("ZRANGE", key, *arguments) 161 | end 162 | 163 | # Return a range of members in a sorted set, by lexicographical range. O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)). 164 | # @see https://redis.io/commands/zrangebylex 165 | # @param key [Key] 166 | # @param min [String] 167 | # @param max [String] 168 | # @param limit [Tuple] Limit the results to the specified `offset` and `count` items. 169 | def zrangebylex(key, min, max, limit: nil) 170 | if limit 171 | arguments = ["LIMIT", *limit] 172 | end 173 | 174 | call("ZRANGEBYLEX", key, min, max, *arguments) 175 | end 176 | 177 | # Return a range of members in a sorted set, by lexicographical range, ordered from higher to lower strings. O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)). 178 | # @see https://redis.io/commands/zrevrangebylex 179 | # @param key [Key] 180 | # @param max [String] 181 | # @param min [String] 182 | def zrevrangebylex(key, min, max, limit: nil) 183 | if limit 184 | arguments = ["LIMIT", *limit] 185 | end 186 | 187 | call("ZREVRANGEBYLEX", key, min, max, *arguments) 188 | end 189 | 190 | # Return a range of members in a sorted set, by score. O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)). 191 | # @see https://redis.io/commands/zrangebyscore 192 | # @param key [Key] 193 | # @param min [Integer] 194 | # @param max [Integer] 195 | # @param with_scores [Boolean] Return the scores of the elements together with the elements. 196 | # @param limit [Tuple] Limit the results to the specified `offset` and `count` items. 197 | # 198 | # @example Retrieve the first 10 members with score `>= 0` and `<= 100` 199 | # redis.zrangebyscore("zset", "0", "100", limit: [0, 10]) 200 | def zrangebyscore(key, min, max, with_scores: false, limit: nil) 201 | arguments = [min, max] 202 | 203 | arguments.push('WITHSCORES') if with_scores 204 | arguments.push('LIMIT', *limit) if limit 205 | 206 | call('ZRANGEBYSCORE', key, *arguments) 207 | end 208 | 209 | # Determine the index of a member in a sorted set. O(log(N)). 210 | # @see https://redis.io/commands/zrank 211 | # @param key [Key] 212 | # @param member [String] 213 | def zrank(key, member) 214 | call("ZRANK", key, member) 215 | end 216 | 217 | # Remove one or more members from a sorted set. O(M*log(N)) with N being the number of elements in the sorted set and M the number of elements to be removed. 218 | # @see https://redis.io/commands/zrem 219 | # @param key [Key] 220 | # @param member [String] 221 | def zrem(key, member) 222 | call("ZREM", key, member) 223 | end 224 | 225 | # Remove all members in a sorted set between the given lexicographical range. O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements removed by the operation. 226 | # @see https://redis.io/commands/zremrangebylex 227 | # @param key [Key] 228 | # @param min [String] 229 | # @param max [String] 230 | def zremrangebylex(key, min, max) 231 | call("ZREMRANGEBYLEX", key, min, max) 232 | end 233 | 234 | # Remove all members in a sorted set within the given indexes. O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements removed by the operation. 235 | # @see https://redis.io/commands/zremrangebyrank 236 | # @param key [Key] 237 | # @param start [Integer] 238 | # @param stop [Integer] 239 | def zremrangebyrank(key, start, stop) 240 | call("ZREMRANGEBYRANK", key, start, stop) 241 | end 242 | 243 | # Remove all members in a sorted set within the given scores. O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements removed by the operation. 244 | # @see https://redis.io/commands/zremrangebyscore 245 | # @param key [Key] 246 | # @param min [Double] 247 | # @param max [Double] 248 | def zremrangebyscore(key, min, max) 249 | call("ZREMRANGEBYSCORE", key, min, max) 250 | end 251 | 252 | # Return a range of members in a sorted set, by index, with scores ordered from high to low. O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements returned. 253 | # @see https://redis.io/commands/zrevrange 254 | # @param key [Key] 255 | # @param start [Integer] 256 | # @param stop [Integer] 257 | # @param withscores [Enum] 258 | def zrevrange(key, min, max, with_scores: false) 259 | arguments = [min, max] 260 | 261 | arguments.push('WITHSCORES') if with_scores 262 | 263 | call("ZREVRANGE", key, *arguments) 264 | end 265 | 266 | # Return a range of members in a sorted set, by score, with scores ordered from high to low. O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)). 267 | # @see https://redis.io/commands/zrevrangebyscore 268 | # @param key [Key] 269 | # @param max [Double] 270 | # @param min [Double] 271 | # @param withscores [Enum] 272 | def zrevrangebyscore(key, min, max, with_scores: false, limit: nil) 273 | arguments = [min, max] 274 | 275 | arguments.push('WITHSCORES') if with_scores 276 | arguments.push('LIMIT', *limit) if limit 277 | 278 | call("ZREVRANGEBYSCORE", key, *arguments) 279 | end 280 | 281 | # Determine the index of a member in a sorted set, with scores ordered from high to low. O(log(N)). 282 | # @see https://redis.io/commands/zrevrank 283 | # @param key [Key] 284 | # @param member [String] 285 | def zrevrank(key, member) 286 | call("ZREVRANK", key, member) 287 | end 288 | 289 | # Get the score associated with the given member in a sorted set. O(1). 290 | # @see https://redis.io/commands/zscore 291 | # @param key [Key] 292 | # @param member [String] 293 | def zscore(key, member) 294 | call("ZSCORE", key, member) 295 | end 296 | 297 | # Add multiple sorted sets and store the resulting sorted set in a new key. O(N)+O(M log(M)) with N being the sum of the sizes of the input sorted sets, and M being the number of elements in the resulting sorted set. 298 | # @see https://redis.io/commands/zunionstore 299 | # @param destination [Key] 300 | # @param numkeys [Integer] 301 | # @param key [Key] 302 | def zunionstore(*arguments) 303 | call("ZUNIONSTORE", *arguments) 304 | end 305 | 306 | # Incrementally iterate sorted sets elements and associated scores. O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection.. 307 | # @see https://redis.io/commands/zscan 308 | # @param key [Key] 309 | # @param cursor [Integer] 310 | def zscan(key, cursor = 0, match: nil, count: nil) 311 | arguments = [key, cursor] 312 | 313 | if match 314 | arguments.push("MATCH", match) 315 | end 316 | 317 | if count 318 | arguments.push("COUNT", count) 319 | end 320 | 321 | call("ZSCAN", *arguments) 322 | end 323 | end 324 | end 325 | end 326 | end 327 | --------------------------------------------------------------------------------