├── .project ├── .properties ├── .smalltalk.ston ├── .travis.yml ├── BaselineOfSimpleRedisClient ├── BaselineOfSimpleRedisClient.class.st └── package.st ├── LICENSE ├── README.md └── SimpleRedisClient ├── SimpleRedisClient.class.st ├── SimpleRedisClientTests.class.st └── package.st /.project: -------------------------------------------------------------------------------- 1 | { 2 | 'srcDirectory' : '' 3 | } -------------------------------------------------------------------------------- /.properties: -------------------------------------------------------------------------------- 1 | { 2 | #format : #tonel 3 | }e 4 | } -------------------------------------------------------------------------------- /.smalltalk.ston: -------------------------------------------------------------------------------- 1 | SmalltalkCISpec { 2 | #loading : [ 3 | SCIMetacelloLoadSpec { 4 | #baseline : 'SimpleRedisClient', 5 | #platforms : [ #pharo ] 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: smalltalk 2 | sudo: enabled 3 | 4 | os: 5 | - linux 6 | 7 | smalltalk: 8 | - Pharo-7.0 9 | - Pharo-6.1 10 | - Pharo64-7.0 11 | 12 | services: 13 | - redis-server 14 | -------------------------------------------------------------------------------- /BaselineOfSimpleRedisClient/BaselineOfSimpleRedisClient.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am BaselineOfSimpleRedisClient, I am used to load the code of SimpleRedisClient. 3 | I am a BaselineOf. 4 | " 5 | Class { 6 | #name : #BaselineOfSimpleRedisClient, 7 | #superclass : #BaselineOf, 8 | #category : #BaselineOfSimpleRedisClient 9 | } 10 | 11 | { #category : #baselines } 12 | BaselineOfSimpleRedisClient >> baseline: spec [ 13 | 14 | spec for: #common do: [ spec package: 'SimpleRedisClient' ] 15 | ] 16 | -------------------------------------------------------------------------------- /BaselineOfSimpleRedisClient/package.st: -------------------------------------------------------------------------------- 1 | Package { #name : #BaselineOfSimpleRedisClient } 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sven Van Caekenberghe 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleRedisClient 2 | A minimal Redis client for Pharo 3 | 4 | [![Build Status](https://travis-ci.org/svenvc/SimpleRedisClient.svg?branch=master)](https://travis-ci.org/svenvc/SimpleRedisClient) 5 | 6 | Redis is a networked, in-memory key-value store with optional durability, supporting different kinds of abstract data structures. Redis can be used to implement various server side architectural patterns. 7 | 8 | - https://redis.io 9 | - https://en.wikipedia.org/wiki/Redis 10 | - https://redis.io/topics/protocol 11 | 12 | The following Medium article in the Concerning Pharo publication explains SimpleRedisClient and its unit tests in detail: 13 | 14 | - TITLE Quick write me a Redis client 15 | - SUBTITLE A beautiful protocol makes implementation easy 16 | - URL https://medium.com/concerning-pharo/quick-write-me-a-redis-client-5fbe4ddfb13d 17 | 18 | To load the code in Pharo 6.1 or later, open World > Tools > Iceberg, click + to add a repository, select the option Clone From github.com and enter svenvc as owner name and SimpleRedisClient as project name. With the new repository selected, select Metacello > Install baseline of SimpleRedisClient. 19 | -------------------------------------------------------------------------------- /SimpleRedisClient/SimpleRedisClient.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am SimpleRedisClient, a minimal client to Redis, a networked, in-memory key-value store with optional durability supporting different kinds of abstract data structures. 3 | 4 | Example 5 | 6 | SimpleRedisClient new in: [ :client | 7 | [ client open; execute: { #ECHO. 'This is Pharo' } ] ensure: [ client close ] ]. 8 | 9 | About Redis 10 | 11 | https://redis.io 12 | https://en.wikipedia.org/wiki/Redis 13 | https://redis.io/topics/protocol 14 | 15 | Source code repository 16 | 17 | https://github.com/svenvc/SimpleRedisClient 18 | 19 | Article 20 | 21 | https://medium.com/concerning-pharo/quick-write-me-a-redis-client-5fbe4ddfb13d 22 | 23 | " 24 | Class { 25 | #name : #SimpleRedisClient, 26 | #superclass : #Object, 27 | #instVars : [ 28 | 'host', 29 | 'port', 30 | 'connection', 31 | 'in', 32 | 'out' 33 | ], 34 | #category : #SimpleRedisClient 35 | } 36 | 37 | { #category : #'initialize-release' } 38 | SimpleRedisClient >> close [ 39 | connection 40 | ifNotNil: [ 41 | [ connection close ] on: Error do: [ #ignore ]. 42 | in := out := connection := nil ] 43 | ] 44 | 45 | { #category : #convenience } 46 | SimpleRedisClient >> execute: commandArgs [ 47 | self writeCommand: commandArgs. 48 | ^ self readReply 49 | ] 50 | 51 | { #category : #convenience } 52 | SimpleRedisClient >> executeInline: command [ 53 | self writeInlineCommand: command. 54 | ^ self readReply 55 | ] 56 | 57 | { #category : #accessing } 58 | SimpleRedisClient >> host [ 59 | ^ host ifNil: [ host := 'localhost' ] 60 | ] 61 | 62 | { #category : #accessing } 63 | SimpleRedisClient >> host: string [ 64 | host := string 65 | ] 66 | 67 | { #category : #'initialize-release' } 68 | SimpleRedisClient >> open [ 69 | self close. 70 | connection := ZdcSocketStream openConnectionToHostNamed: self host port: self port. 71 | in := ZnCharacterReadStream on: connection. 72 | out := ZnCharacterWriteStream on: connection 73 | ] 74 | 75 | { #category : #accessing } 76 | SimpleRedisClient >> port [ 77 | ^ port ifNil: [ port := 6379 ] 78 | ] 79 | 80 | { #category : #accessing } 81 | SimpleRedisClient >> port: integer [ 82 | port := integer 83 | ] 84 | 85 | { #category : #reading } 86 | SimpleRedisClient >> readArray [ 87 | | length array | 88 | length := in nextLine asInteger. 89 | length = -1 ifTrue: [ ^ nil ]. 90 | array := Array new: length streamContents: [ :elements | 91 | length timesRepeat: [ elements nextPut: self readReply ] ]. 92 | ^ array 93 | ] 94 | 95 | { #category : #reading } 96 | SimpleRedisClient >> readBulkString [ 97 | | byteCount bytes | 98 | byteCount := in nextLine asInteger. 99 | byteCount = -1 ifTrue: [ ^ nil ]. 100 | bytes := in wrappedStream next: byteCount. 101 | in nextLine. 102 | ^ in encoder decodeBytes: bytes 103 | ] 104 | 105 | { #category : #reading } 106 | SimpleRedisClient >> readReply [ 107 | | first | 108 | first := in next. 109 | first = $+ ifTrue: [ ^ in nextLine ]. 110 | first = $: ifTrue: [ ^ in nextLine asInteger ]. 111 | first = $- ifTrue: [ ^ self error: in nextLine ]. 112 | first = $* ifTrue: [ ^ self readArray ]. 113 | first = $$ ifTrue: [ ^ self readBulkString ]. 114 | self error: 'Unknown reply type' 115 | ] 116 | 117 | { #category : #writing } 118 | SimpleRedisClient >> writeCommand: args [ 119 | out nextPut: $*; print: args size; crlf. 120 | args do: [ :each | 121 | | string byteCount | 122 | string := each asString. 123 | byteCount := out encoder encodedByteCountForString: string. 124 | out 125 | nextPut: $$; print: byteCount; crlf; 126 | nextPutAll: string; crlf ]. 127 | out flush 128 | ] 129 | 130 | { #category : #writing } 131 | SimpleRedisClient >> writeInlineCommand: string [ 132 | out nextPutAll: string; crlf; flush 133 | ] 134 | -------------------------------------------------------------------------------- /SimpleRedisClient/SimpleRedisClientTests.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am SimpleRedisClientTests, holding unit tests as well as executeable examples and documentation for SimpleRedisClient. 3 | " 4 | Class { 5 | #name : #SimpleRedisClientTests, 6 | #superclass : #TestCase, 7 | #instVars : [ 8 | 'client' 9 | ], 10 | #category : 'SimpleRedisClient' 11 | } 12 | 13 | { #category : #running } 14 | SimpleRedisClientTests >> setUp [ 15 | super setUp. 16 | client := SimpleRedisClient new. 17 | client open 18 | ] 19 | 20 | { #category : #running } 21 | SimpleRedisClientTests >> tearDown [ 22 | client close. 23 | super tearDown 24 | ] 25 | 26 | { #category : #testing } 27 | SimpleRedisClientTests >> testEcho [ 28 | | string | 29 | string := 'STR-' , 99 atRandom asString. 30 | self assert: (client execute: { #ECHO. string }) equals: string 31 | ] 32 | 33 | { #category : #testing } 34 | SimpleRedisClientTests >> testHashSimple [ 35 | client execute: #(DEL myhash). 36 | self assert: (client execute: #(HGET myhash foo)) isNil. 37 | self assert: (client execute: #(HSET myhash foo one)) equals: 1. 38 | self assert: (client execute: #(HGET myhash foo)) equals: #one. 39 | self assert: (client execute: #(HSET myhash bar two)) equals: 1. 40 | self assert: (client execute: #(HGET myhash bar)) equals: #two. 41 | (Dictionary newFromPairs: (client execute: #(HGETALL myhash))) in: [ :dictionary | 42 | self assert: (dictionary at: #foo) equals: #one. 43 | self assert: (dictionary at: #bar) equals: #two ]. 44 | client execute: #(DEL myhash). 45 | ] 46 | 47 | { #category : #testing } 48 | SimpleRedisClientTests >> testInfo [ 49 | | redisInfo | 50 | redisInfo := Dictionary new. 51 | (client execute: #(INFO)) lines 52 | reject: [ :line | 53 | line isEmpty or: [ line first = $# ] ] 54 | thenCollect: [ :line | 55 | | keyValue | 56 | keyValue := $: split: line. 57 | redisInfo at: keyValue first put: keyValue second ]. 58 | self assert: (redisInfo at: 'redis_mode') equals: 'standalone' 59 | ] 60 | 61 | { #category : #testing } 62 | SimpleRedisClientTests >> testListSimple [ 63 | client execute: #(DEL mylist). 64 | self assert: (client execute: #(LPUSH mylist one)) equals: 1. 65 | self assert: (client execute: #(LPUSH mylist two three)) equals: 3. 66 | self assert: (client execute: #(LPOP mylist)) equals: #three. 67 | self assert: (client execute: #(LINDEX mylist 0)) equals: #two. 68 | self assert: (client execute: #(LLEN mylist)) equals: 2. 69 | client execute: #(DEL mylist). 70 | 71 | ] 72 | 73 | { #category : #testing } 74 | SimpleRedisClientTests >> testPing [ 75 | self assert: (client executeInline: #PING) equals: #PONG 76 | ] 77 | 78 | { #category : #testing } 79 | SimpleRedisClientTests >> testPubSubSimple [ 80 | | semaphore string | 81 | string := 'STR-' , 99 atRandom asString. 82 | semaphore := Semaphore new. 83 | client execute: #(DEL mychannel). 84 | 85 | [ 86 | | anotherClient | 87 | semaphore wait. 88 | anotherClient := SimpleRedisClient new. 89 | anotherClient open. 90 | self assert: (anotherClient execute: { #PUBLISH. #mychannel. string }) > 0. 91 | anotherClient close 92 | ] forkAt: Processor userBackgroundPriority named: 'testPubSubSimple'. 93 | 94 | self assert: (client execute: #(SUBSCRIBE mychannel)) equals: #(subscribe mychannel 1). 95 | semaphore signal. 96 | "Block waiting for data distributed over the channel" 97 | self assert: client readReply equals: { #message. #mychannel. string }. 98 | self assert: (client execute: #(UNSUBSCRIBE mychannel)) equals: #(unsubscribe mychannel 0). 99 | client execute: #(DEL mychannel). 100 | ] 101 | 102 | { #category : #testing } 103 | SimpleRedisClientTests >> testQueueSimple [ 104 | | string semaphore | 105 | string := 'STR-' , 99 atRandom asString. 106 | semaphore := Semaphore new. 107 | client execute: #(DEL myqueue). 108 | 109 | [ 110 | | anotherClient | 111 | anotherClient := SimpleRedisClient new. 112 | anotherClient open. 113 | semaphore signal. 114 | "Block waiting for data entering the queue" 115 | self assert: (anotherClient execute: #(BRPOP myqueue 0)) equals: { #myqueue. string }. 116 | semaphore signal. 117 | anotherClient close 118 | ] forkAt: Processor userSchedulingPriority named: 'testQueueSimple'. 119 | 120 | semaphore wait. 121 | self assert: (client execute: { #LPUSH. #myqueue. string }) > 0. 122 | semaphore wait. 123 | client execute: #(DEL myqueue). 124 | ] 125 | 126 | { #category : #testing } 127 | SimpleRedisClientTests >> testQuit [ 128 | self assert: (client executeInline: #QUIT) equals: #OK 129 | ] 130 | 131 | { #category : #testing } 132 | SimpleRedisClientTests >> testSetSimple [ 133 | client execute: #(DEL myset). 134 | self assert: (client execute: #(SADD myset one)) equals: 1. 135 | self assert: (client execute: #(SISMEMBER myset one)) equals: 1. 136 | self assert: (client execute: #(SADD myset one)) equals: 0. 137 | self assert: (client execute: #(SADD myset two)) equals: 1. 138 | self assert: (client execute: #(SADD myset three)) equals: 1. 139 | self assert: (client execute: #(SCARD myset)) equals: 3. 140 | self assert: (client execute: #(SMEMBERS myset)) asSet equals: #(one two three) asSet. 141 | client execute: #(DEL myset). 142 | ] 143 | 144 | { #category : #testing } 145 | SimpleRedisClientTests >> testSimpleCounter [ 146 | client execute: #(DEL mycounter). 147 | self assert: (client execute: #(INCR mycounter)) equals: 1. 148 | self assert: (client execute: #(INCR mycounter)) equals: 2. 149 | self assert: (client execute: #(GET mycounter)) equals: '2'. 150 | self assert: (client execute: #(DECR mycounter)) equals: 1. 151 | self assert: (client execute: #(INCRBY mycounter 10)) equals: 11. 152 | client execute: #(DEL mycounter). 153 | ] 154 | 155 | { #category : #testing } 156 | SimpleRedisClientTests >> testStringGetSetSimple [ 157 | | string | 158 | string := 'STR-' , 99 atRandom asString. 159 | self assert: (client execute: #(DEL foo)) >= 0. 160 | self assert: (client execute: #(EXISTS foo)) equals: 0. 161 | self assert: (client execute: #(GET foo)) isNil. 162 | self assert: (client execute: { #SET. #foo. string }) equals: #OK. 163 | self assert: (client execute: #(GET foo)) equals: string. 164 | self assert: (client execute: #(EXISTS foo)) equals: 1. 165 | self assert: (client execute: #(DEL foo)) > 0. 166 | 167 | ] 168 | -------------------------------------------------------------------------------- /SimpleRedisClient/package.st: -------------------------------------------------------------------------------- 1 | Package { #name : #SimpleRedisClient } 2 | --------------------------------------------------------------------------------