├── .gitignore ├── README.md ├── shard.yml ├── spec ├── redis │ ├── client_spec.cr │ └── protocol_spec.cr └── spec_helper.cr └── src ├── client.cr ├── command_error.cr ├── protocol.cr └── redis.cr /.gitignore: -------------------------------------------------------------------------------- 1 | .crystal/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > This library is no longer supported or updated by the Manas.Tech, 3 | > therefore we have archived the repository. 4 | > 5 | > The contents are still available readonly and continue to work as a 6 | > [shards](https://github.com/crystal-lang/shards/) dependency. 7 | > 8 | > We recommend switching to https://github.com/stefanwille/crystal-redis instead. 9 | > 10 | > If you wish to continue development yourself, we recommend you fork it. 11 | > We can also arrange to transfer ownership. 12 | > 13 | > If you have further questions, please reach out on https://forum.crystal-lang.org 14 | > or crystal@manas.tech 15 | 16 | crystal_redis 17 | ============= 18 | 19 | Pure Crystal implementation of a Redis client (work in progress). 20 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: redis 2 | version: 0.4.0 3 | 4 | authors: 5 | - Ary Borenszweig 6 | -------------------------------------------------------------------------------- /spec/redis/client_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Redis::Client do 4 | it "set and get" do 5 | Redis.open do |client| 6 | client.set("foo", "bar").should eq("OK") 7 | client.get("foo").should eq("bar") 8 | end 9 | end 10 | 11 | it "set and get with []" do 12 | Redis.open do |client| 13 | client["foo"] = "bar" 14 | client["foo"].should eq("bar") 15 | client.del("foo") 16 | client["foo"]?.should be_nil 17 | end 18 | end 19 | 20 | it "set and get number" do 21 | Redis.open do |client| 22 | client.set("foo", 1).should eq("OK") 23 | client.get("foo").should eq("1") 24 | client.set(1, 2).should eq("OK") 25 | client.get(1).should eq("2") 26 | end 27 | end 28 | 29 | it "del one key" do 30 | Redis.open do |client| 31 | client.set("foo", "bar").should eq("OK") 32 | client.del("foo").should eq(1) 33 | client.get("foo").should be_nil 34 | end 35 | end 36 | 37 | it "del many keys" do 38 | Redis.open do |client| 39 | client.set("foo", "bar").should eq("OK") 40 | client.set("baz", "qux").should eq("OK") 41 | client.del("foo", "baz").should eq(2) 42 | end 43 | end 44 | 45 | it "del one numeric key" do 46 | Redis.open do |client| 47 | client.set(1, "bar").should eq("OK") 48 | client.del(1).should eq(1) 49 | client.get(1).should be_nil 50 | end 51 | end 52 | 53 | it "exists" do 54 | Redis.open do |client| 55 | client.set("foo", "bar").should eq("OK") 56 | client.exists("foo").should be_true 57 | client.del("foo") 58 | client.exists("foo").should be_false 59 | end 60 | end 61 | 62 | it "incr and decr" do 63 | Redis.open do |client| 64 | client.del("foo") 65 | client.incr("foo").should eq(1) 66 | client.incr("foo").should eq(2) 67 | client.decr("foo").should eq(1) 68 | client.decr("foo").should eq(0) 69 | client.decr("foo").should eq(-1) 70 | client.decr("foo").should eq(-2) 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/redis/protocol_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | def it_reads(string, expected, file = __FILE__, line = __LINE__) 4 | it "reads #{string}", file, line do 5 | Redis::Protocol.read(IO::Memory.new(string)).should eq(expected) 6 | end 7 | end 8 | 9 | describe Redis::Protocol do 10 | describe "read" do 11 | it_reads "+OK\r\n", "OK" 12 | it_reads "+Hello world\r\n", "Hello world" 13 | 14 | it_reads ":0\r\n", 0 15 | it_reads ":1000\r\n", 1000 16 | 17 | it_reads "$6\r\nfoobar\r\n", "foobar" 18 | it_reads "$0\r\n\r\n", "" 19 | it_reads "$-1\r\n", nil 20 | 21 | it_reads "*0\r\n", [] of Redis::ResponseType 22 | it_reads "*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n", ["foo", "bar"] 23 | it_reads "*-1\r\n", nil 24 | it_reads "*3\r\n:1\r\n:2\r\n:3\r\n", [1, 2, 3] 25 | it_reads "*5\r\n:1\r\n:2\r\n:3\r\n:4\r\n$6\r\nfoobar\r\n", [1, 2, 3, 4, "foobar"] 26 | it_reads "*3\r\n$3\r\nfoo\r\n$-1\r\n$3\r\nbar\r\n", ["foo", nil, "bar"] 27 | 28 | it "raises CommandError on error" do 29 | expect_raises Redis::CommandError, "OH NO!" do 30 | Redis::Protocol.read(IO::Memory.new("-OH NO!")) 31 | end 32 | end 33 | end 34 | 35 | describe "write" do 36 | it "writes nil" do 37 | io = IO::Memory.new 38 | Redis::Protocol.write(nil, io) 39 | io.to_s.should eq("$-1\r\n") 40 | end 41 | 42 | it "writes bulk string" do 43 | io = IO::Memory.new 44 | Redis::Protocol.write("hello", io) 45 | io.to_s.should eq("$5\r\nhello\r\n") 46 | end 47 | 48 | it "writes integer" do 49 | io = IO::Memory.new 50 | Redis::Protocol.write(1234, io) 51 | io.to_s.should eq(":1234\r\n") 52 | end 53 | 54 | it "writes array" do 55 | io = IO::Memory.new 56 | Redis::Protocol.array(5, io) do 57 | Redis::Protocol.write(1, io) 58 | Redis::Protocol.write(2, io) 59 | Redis::Protocol.write(3, io) 60 | Redis::Protocol.write(4, io) 61 | Redis::Protocol.write("foobar", io) 62 | end 63 | io.to_s.should eq("*5\r\n:1\r\n:2\r\n:3\r\n:4\r\n$6\r\nfoobar\r\n") 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/redis" 3 | -------------------------------------------------------------------------------- /src/client.cr: -------------------------------------------------------------------------------- 1 | require "./protocol" 2 | 3 | class Redis::Client 4 | include Protocol 5 | 6 | def initialize(host = nil, port = nil) 7 | host ||= DEFAULT_HOST 8 | port ||= DEFAULT_PORT 9 | @socket = TCPSocket.new host, port 10 | end 11 | 12 | def self.open(host = nil, port = nil) 13 | client = new(host, port) 14 | begin 15 | yield client 16 | ensure 17 | client.disconnect 18 | end 19 | end 20 | 21 | def disconnect 22 | @socket.close 23 | end 24 | 25 | def del(*keys) 26 | command "DEL", *keys, &.to_s 27 | end 28 | 29 | def exists(key) 30 | bool "EXISTS", key.to_s 31 | end 32 | 33 | def get(key) 34 | string? "GET", key.to_s 35 | end 36 | 37 | def incr(key) 38 | int "INCR", key.to_s 39 | end 40 | 41 | def decr(key) 42 | int "DECR", key.to_s 43 | end 44 | 45 | def set(key, value) 46 | command "SET", key.to_s, value.to_s 47 | end 48 | 49 | def [](key) 50 | string "GET", key.to_s 51 | end 52 | 53 | def []?(key) 54 | get key 55 | end 56 | 57 | def []=(key, value) 58 | set key, value 59 | end 60 | 61 | private def bool(name, *args) 62 | command(name, *args) == 1 63 | end 64 | 65 | private def int(name, *args) 66 | command(name, *args).as Int64 67 | end 68 | 69 | private def string(name, *args) 70 | command(name, *args).as String 71 | end 72 | 73 | private def string?(name, *args) 74 | command(name, *args).as String? 75 | end 76 | 77 | private def command(name, *args) 78 | command(name, *args) { |x| x } 79 | end 80 | 81 | private def command(name, *args) 82 | array(args.size + 1, @socket) do 83 | write name, @socket 84 | args.each do |arg| 85 | write yield(arg), @socket 86 | end 87 | end 88 | @socket.flush 89 | 90 | read @socket 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /src/command_error.cr: -------------------------------------------------------------------------------- 1 | class Redis::CommandError < ::Exception 2 | end 3 | -------------------------------------------------------------------------------- /src/protocol.cr: -------------------------------------------------------------------------------- 1 | module Redis 2 | alias ResponseType = Nil | Int64 | String | Array(ResponseType) 3 | 4 | module Protocol 5 | extend self 6 | 7 | def read(io) 8 | case io.read_byte.try &.chr 9 | when '+' 10 | read_string(io) 11 | when '-' 12 | raise CommandError.new read_string(io) 13 | when ':' 14 | read_number(io) 15 | when '$' 16 | length = read_number(io).to_i32 17 | return nil if length == -1 18 | 19 | value = String.new(length) do |buffer| 20 | io.read_fully(Slice.new(buffer, length)) 21 | {length, 0} 22 | end 23 | io.read_byte # \r 24 | io.read_byte # \n 25 | value 26 | when '*' 27 | length = read_number(io) 28 | return nil if length == -1 29 | 30 | Array.new(length.to_i32) { read(io).as ResponseType } 31 | else 32 | nil 33 | end 34 | end 35 | 36 | def write(value : Nil, io) 37 | io << "$-1\r\n" 38 | end 39 | 40 | def write(value : Int, io) 41 | io << ':' << value << "\r\n" 42 | end 43 | 44 | def write(value : String, io) 45 | io << '$' << value.bytesize << "\r\n" << value << "\r\n" 46 | end 47 | 48 | def array(length, io) 49 | io << '*' << length << "\r\n" 50 | yield 51 | end 52 | 53 | private def read_string(io) 54 | io.gets.not_nil!.chomp 55 | end 56 | 57 | private def read_number(io) 58 | length = 0_i64 59 | negative = false 60 | char = read_char(io) 61 | if char == '-' 62 | negative = true 63 | char = read_char(io) 64 | end 65 | while char.ascii_number? 66 | length = length * 10 + (char - '0') 67 | char = read_char(io) 68 | end 69 | io.read_byte # \n 70 | negative ? -length : length 71 | end 72 | 73 | private def read_char(io) 74 | io.read_byte.not_nil!.chr 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /src/redis.cr: -------------------------------------------------------------------------------- 1 | require "socket" 2 | 3 | module Redis 4 | DEFAULT_HOST = "127.0.0.1" 5 | DEFAULT_PORT = 6379 6 | 7 | def self.new(host = nil, port = nil) 8 | Client.new(host, port) 9 | end 10 | 11 | def self.open(host = nil, port = nil) 12 | Client.open(host, port) do |client| 13 | yield client 14 | end 15 | end 16 | end 17 | 18 | require "./*" 19 | --------------------------------------------------------------------------------