5 Commits

Author SHA1 Message Date
hellerve fafa2b40d2 use new sockets lib 2026-04-11 11:20:25 +02:00
hellerve 692fe51728 update docs 2026-04-03 11:37:12 +02:00
hellerve d88b0631a6 clarify redis version 2026-04-03 11:07:53 +02:00
hellerve 0b64365e39 add nested types to redis 2026-04-03 10:57:06 +02:00
hellerve ed02b8aadb update url 2021-10-27 21:17:16 -04:00
7 changed files with 6732 additions and 4892 deletions
+46 -6
View File
@@ -1,20 +1,60 @@
# redis # redis
is a Redis client library for Carp. A Redis client library for Carp, supporting Redis 7.x.
## Installation ## Installation
```clojure ```clojure
(load "https://veitheller.de/git/carpentry/redis.git@master") (load "https://git.veitheller.de/carpentry/redis.git@0.2.0")
``` ```
## Usage ## Usage
This package contains functions to convert to and from the Redis protocol, ```clojure
and handling Redis connections and commands. The documentation lives (load "redis.carp")
[here](https://veitheller.de/redis).
You can also look at the examples in the [`examples`](./examples) directory. (defn main []
(match (Redis.open "127.0.0.1")
(Result.Success r)
(do
(println* &(Redis.ping &r))
(println* &(Redis.set &r @"key" @"value"))
(println* &(Redis.get &r @"key"))
; list operations return nested RESP arrays
(println* &(Redis.rpush &r @"mylist" @"a"))
(println* &(Redis.rpush &r @"mylist" @"b"))
(println* &(Redis.lrange &r @"mylist" @"0" @"-1"))
; pattern match on responses
(match (Redis.get &r @"key")
(Result.Success resp)
(match resp
(RESP.Str s) (println* "got: " &s)
(RESP.Null) (println* "key not found")
_ (println* "unexpected type"))
(Result.Error e) (println* "error: " &e))
(Redis.close r))
(Result.Error err) (IO.errorln &err)))
```
The library provides thin wrappers around all standard Redis commands through
7.2, so you can call them directly (e.g. `Redis.get`, `Redis.hset`,
`Redis.lrange`). For commands with complex or variadic arguments, use
`Redis.send` directly:
```clojure
(Redis.send &r @"SET" &[(to-redis @"key") (to-redis @"value")])
(Redis.read &r)
```
The RESP type supports recursive arrays via `Box`, so nested structures
(e.g. from `XRANGE` or `COMMAND`) are decoded faithfully.
Full API documentation lives [here](https://veitheller.de/redis).
More examples are in the [`examples`](./examples) directory.
<hr/> <hr/>
+224 -183
View File
@@ -9,7 +9,7 @@
<body> <body>
<div class="content"> <div class="content">
<div class="logo"> <div class="logo">
<a href="https://veitheller/git/carpentry/redis"> <a href="https://git.veitheller.de/carpentry/redis">
<img src=""> <img src="">
</a> </a>
<div class="title"> <div class="title">
@@ -30,218 +30,259 @@
</ul> </ul>
</div> </div>
</div> </div>
<h1> <div class="module">
RESP <h1>
</h1> RESP
<div class="module-description"> </h1>
<p>is a wrapper around the <a href="https://redis.io/topics/protocol">Redis Serialization <div class="module-description">
Protocol</a>. You can create all types—though <p>is a wrapper around the <a href="https://redis.io/topics/protocol">Redis Serialization
creating arrays is a little unsightly due to the absence of recursive types, Protocol</a>. You can create all types,
stringify the built types into strings using <a href="#str"><code>str</code></a>, and decoding from stringify the built types into strings using <a href="#str"><code>str</code></a>, and decode from
the string protocol using <a href="#from-string"><code>from-string</code></a>.</p> the string protocol using <a href="#from-string"><code>from-string</code></a>. Arrays are fully
supported, including nested arrays.</p>
<pre><code>; decoding
(RESP.from-string &quot;+OK\r\n&quot;) ; =&gt; (Success (Str @&quot;OK&quot;))
(RESP.from-string &quot;:42\r\n&quot;) ; =&gt; (Success (Integer 42))
(RESP.from-string &quot;$-1\r\n&quot;) ; =&gt; (Success (Null))
; encoding
(str &amp;(RESP.Str @&quot;hi&quot;)) ; =&gt; &quot;$2\r\nhi\r\n&quot;
(str &amp;(RESP.Integer 42)) ; =&gt; &quot;:42\r\n&quot;
; pattern matching on responses
(match (Redis.get &amp;r @&quot;key&quot;)
(Result.Success resp)
(match resp
(RESP.Str s) (println* &quot;got: &quot; &amp;s)
(RESP.Null) (println* &quot;not found&quot;)
(RESP.Arr items) (println* &quot;array of &quot; &amp;(Int.str (Array.length &amp;items)))
_ (println* &quot;other&quot;))
(Result.Error e) (println* &quot;error: &quot; &amp;e))
</code></pre>
<p>If you want your types to be supported when encoding, youll have to implement <p>If you want your types to be supported when encoding, youll have to implement
the interface <code>to-redis</code>, the signature of which is <code>(Fn [a] RESP))</code>.</p> the interface <code>to-redis</code>, the signature of which is <code>(Fn [a] RESP))</code>.</p>
</div>
<div class="binder">
<a class="anchor" href="#Arr">
<h3 id="Arr">
Arr
</h3>
</a>
<div class="description">
instantiate
</div> </div>
<p class="sig"> <div class="binder">
(Fn [(Array String)] RESP) <a class="anchor" href="#Arr">
</p> <h3 id="Arr">
<span> Arr
</h3>
</a>
<div class="description">
instantiate
</div>
<p class="sig">
(Fn [(Array (Box RESP))] RESP)
</p>
<span>
</span> </span>
<p class="doc"> <p class="doc">
<p>creates a <code>Arr</code>.</p> <p>creates a <code>Arr</code>.</p>
</p> </p>
</div>
<div class="binder">
<a class="anchor" href="#Err">
<h3 id="Err">
Err
</h3>
</a>
<div class="description">
instantiate
</div> </div>
<p class="sig"> <div class="binder">
(Fn [String] RESP) <a class="anchor" href="#Err">
</p> <h3 id="Err">
<span> Err
</h3>
</a>
<div class="description">
instantiate
</div>
<p class="sig">
(Fn [String] RESP)
</p>
<span>
</span> </span>
<p class="doc"> <p class="doc">
<p>creates a <code>Err</code>.</p> <p>creates a <code>Err</code>.</p>
</p> </p>
</div>
<div class="binder">
<a class="anchor" href="#Integer">
<h3 id="Integer">
Integer
</h3>
</a>
<div class="description">
instantiate
</div> </div>
<p class="sig"> <div class="binder">
(Fn [Int] RESP) <a class="anchor" href="#Integer">
</p> <h3 id="Integer">
<span> Integer
</h3>
</a>
<div class="description">
instantiate
</div>
<p class="sig">
(Fn [Int] RESP)
</p>
<span>
</span> </span>
<p class="doc"> <p class="doc">
<p>creates a <code>Integer</code>.</p> <p>creates a <code>Integer</code>.</p>
</p> </p>
</div>
<div class="binder">
<a class="anchor" href="#Null">
<h3 id="Null">
Null
</h3>
</a>
<div class="description">
instantiate
</div> </div>
<p class="sig"> <div class="binder">
(Fn [] RESP) <a class="anchor" href="#Null">
</p> <h3 id="Null">
<span> Null
</h3>
</a>
<div class="description">
instantiate
</div>
<p class="sig">
(Fn [] RESP)
</p>
<span>
</span> </span>
<p class="doc"> <p class="doc">
<p>creates a <code>Null</code>.</p> <p>creates a <code>Null</code>.</p>
</p> </p>
</div>
<div class="binder">
<a class="anchor" href="#Str">
<h3 id="Str">
Str
</h3>
</a>
<div class="description">
instantiate
</div> </div>
<p class="sig"> <div class="binder">
(Fn [String] RESP) <a class="anchor" href="#Str">
</p> <h3 id="Str">
<span> Str
</h3>
</a>
<div class="description">
instantiate
</div>
<p class="sig">
(Fn [String] RESP)
</p>
<span>
</span> </span>
<p class="doc"> <p class="doc">
<p>creates a <code>Str</code>.</p> <p>creates a <code>Str</code>.</p>
</p> </p>
</div>
<div class="binder">
<a class="anchor" href="#copy">
<h3 id="copy">
copy
</h3>
</a>
<div class="description">
instantiate
</div> </div>
<p class="sig"> <div class="binder">
(Fn [(Ref RESP a)] RESP) <a class="anchor" href="#copy">
</p> <h3 id="copy">
<span> copy
</h3>
</a>
<div class="description">
instantiate
</div>
<p class="sig">
(Fn [(Ref RESP a)] RESP)
</p>
<span>
</span> </span>
<p class="doc"> <p class="doc">
<p>copies a <code>RESP</code>.</p> <p>copies a <code>RESP</code>.</p>
</p> </p>
</div>
<div class="binder">
<a class="anchor" href="#from-string">
<h3 id="from-string">
from-string
</h3>
</a>
<div class="description">
defn
</div> </div>
<p class="sig"> <div class="binder">
(Fn [(Ref String a)] (Result RESP String)) <a class="anchor" href="#delete">
</p> <h3 id="delete">
<pre class="args"> delete
(from-string s) </h3>
</pre> </a>
<p class="doc"> <div class="description">
<p>converts a RESP string into a <code>RESP</code> data structure.</p> instantiate
</div>
<p class="sig">
(Fn [RESP] ())
</p>
<span>
</p> </span>
</div> <p class="doc">
<div class="binder"> <p>deletes a <code>RESP</code>. This should usually not be called manually.</p>
<a class="anchor" href="#get-tag">
<h3 id="get-tag"> </p>
get-tag
</h3>
</a>
<div class="description">
instantiate
</div> </div>
<p class="sig"> <div class="binder">
(Fn [(Ref RESP a)] Int) <a class="anchor" href="#from-string">
</p> <h3 id="from-string">
<span> from-string
</h3>
</a>
<div class="description">
defn
</div>
<p class="sig">
(Fn [(Ref String a)] (Result RESP String))
</p>
<pre class="args">
(from-string s)
</pre>
<p class="doc">
<p>converts a RESP string into a <code>RESP</code> data structure.</p>
</span> </p>
<p class="doc">
<p>Gets the tag from a <code>RESP</code>.</p>
</p>
</div>
<div class="binder">
<a class="anchor" href="#prn">
<h3 id="prn">
prn
</h3>
</a>
<div class="description">
instantiate
</div> </div>
<p class="sig"> <div class="binder">
(Fn [(Ref RESP a)] String) <a class="anchor" href="#get-tag">
</p> <h3 id="get-tag">
<span> get-tag
</h3>
</a>
<div class="description">
instantiate
</div>
<p class="sig">
(Fn [(Ref RESP a)] Int)
</p>
<span>
</span> </span>
<p class="doc"> <p class="doc">
<p>converts a <code>RESP</code> to a string.</p> <p>Gets the tag from a <code>RESP</code>.</p>
</p> </p>
</div>
<div class="binder">
<a class="anchor" href="#str">
<h3 id="str">
str
</h3>
</a>
<div class="description">
defn
</div> </div>
<p class="sig"> <div class="binder">
(Fn [(Ref RESP a)] String) <a class="anchor" href="#prn">
</p> <h3 id="prn">
<pre class="args"> prn
(str r) </h3>
</pre> </a>
<p class="doc"> <div class="description">
<p>converts a <code>RESP</code> to a string.</p> instantiate
</div>
<p class="sig">
(Fn [(Ref RESP a)] String)
</p>
<span>
</p> </span>
<p class="doc">
<p>converts a <code>RESP</code> to a string.</p>
</p>
</div>
<div class="binder">
<a class="anchor" href="#str">
<h3 id="str">
str
</h3>
</a>
<div class="description">
defn
</div>
<p class="sig">
(Fn [(Ref RESP a)] String)
</p>
<pre class="args">
(str r)
</pre>
<p class="doc">
<p>converts a <code>RESP</code> to a string.</p>
</p>
</div>
</div> </div>
</div> </div>
</body> </body>
+6117 -4574
View File
File diff suppressed because it is too large Load Diff
+22 -4
View File
@@ -8,7 +8,7 @@
</head> </head>
<body> <body>
<div class="content"> <div class="content">
<a href="https://veitheller/git/carpentry/redis"> <a href="https://git.veitheller.de/carpentry/redis">
<div class="logo"> <div class="logo">
<img src="" alt="Logo"> <img src="" alt="Logo">
<div class="index"> <div class="index">
@@ -30,9 +30,27 @@
<h1> <h1>
redis redis
</h1> </h1>
<p>is a collection of modules for talking <p>is a Redis client library for Carp, supporting Redis 7.x.</p>
to Redis.</p> <h2>Installation</h2>
<pre><code>(load &quot;https://veitheller/git/carpentry/redis@master&quot;) <pre><code>(load &quot;https://git.veitheller.de/carpentry/redis.git@0.2.0&quot;)
</code></pre>
<h2>Example</h2>
<pre><code>(defn main []
(match (Redis.open &quot;127.0.0.1&quot;)
(Result.Success r)
(do
(println* &amp;(Redis.set &amp;r @&quot;key&quot; @&quot;value&quot;))
(println* &amp;(Redis.get &amp;r @&quot;key&quot;))
(println* &amp;(Redis.lrange &amp;r @&quot;mylist&quot; @&quot;0&quot; @&quot;-1&quot;))
(match (Redis.get &amp;r @&quot;key&quot;)
(Result.Success resp)
(match resp
(RESP.Str s) (println* &quot;got: &quot; &amp;s)
(RESP.Null) (println* &quot;not found&quot;)
_ (println* &quot;unexpected type&quot;))
(Result.Error e) (println* &quot;error: &quot; &amp;e))
(Redis.close r))
(Result.Error err) (IO.errorln &amp;err)))
</code></pre> </code></pre>
</div> </div>
+64 -14
View File
@@ -1,22 +1,72 @@
(load "redis.carp") (load "redis.carp")
(defn main [] (defn main []
(match (Redis.open "127.0.0.1") (match (Redis.open "127.0.0.1")
(Result.Success r) (Result.Success r)
(do (do
(Redis.send &r "PING" []) ; basic connectivity
(println* &(Redis.read &r)) (println* &(Redis.ping &r))
(println* &(Redis.echo &r @"hello from carp!"))
(Redis.send &r "PING" [(Box (to-redis @"hiiiii"))]) ; strings
(println* &(Redis.read &r)) (println* &(Redis.set &r @"language" @"carp"))
(println* &(Redis.get &r @"language"))
(println* &(Redis.echo &r @"hi")) ; lists — array responses are proper nested RESP values
(println* &(Redis.rpush &r @"queue" @"task-a"))
(println* &(Redis.rpush &r @"queue" @"task-b"))
(println* &(Redis.rpush &r @"queue" @"task-c"))
(println* &(Redis.lrange &r @"queue" @"0" @"-1"))
(println* &(Redis.lpop &r @"queue"))
(println* &(Redis.llen &r @"queue"))
(println* &(Redis.rpush &r @"mylist" @"1")) ; hashes
(println* &(Redis.rpush &r @"mylist" @"2")) (println* &(Redis.hset &r @"user:1" @"name" @"alice"))
(println* &(Redis.lrange &r @"mylist" @"-100" @"100")) (println* &(Redis.hset &r @"user:1" @"role" @"admin"))
(println* &(Redis.hgetall &r @"user:1"))
(println* &(Redis.latency-help &r)) ; sets
(println* &(Redis.sadd &r @"tags" @"functional"))
(println* &(Redis.sadd &r @"tags" @"systems"))
(println* &(Redis.sadd &r @"tags" @"compiled"))
(println* &(Redis.smembers &r @"tags"))
(Redis.close r)) ; counters
(Result.Error err) (IO.errorln &err))) (println* &(Redis.set &r @"hits" @"0"))
(println* &(Redis.incr &r @"hits"))
(println* &(Redis.incrby &r @"hits" @"99"))
(println* &(Redis.get &r @"hits"))
; key expiry
(println* &(Redis.set &r @"session" @"abc123"))
(println* &(Redis.expire &r @"session" @"300"))
(println* &(Redis.ttl &r @"session"))
; pattern matching on RESP values
(match (Redis.lrange &r @"queue" @"0" @"-1")
(Result.Success resp)
(match resp
(RESP.Arr items)
(println* "remaining tasks: " &(Int.str (Array.length &items)))
_ (println* "unexpected response"))
(Result.Error e) (println* "error: " &e))
; streams — xrange returns nested arrays (array of [id, [field, value, ...]])
(println* &(Redis.xadd &r @"events" @"*" @"type" @"click"))
(println* &(Redis.xadd &r @"events" @"*" @"type" @"scroll"))
(println* &(Redis.xrange &r @"events" @"-" @"+"))
; missing key returns Null
(println* &(Redis.get &r @"nonexistent"))
; cleanup
(println* &(Redis.del &r @"language"))
(println* &(Redis.del &r @"queue"))
(println* &(Redis.del &r @"user:1"))
(println* &(Redis.del &r @"tags"))
(println* &(Redis.del &r @"hits"))
(println* &(Redis.del &r @"session"))
(println* &(Redis.del &r @"events"))
(Redis.close r))
(Result.Error err) (IO.errorln &err)))
+26 -4
View File
@@ -3,13 +3,35 @@
(Project.config "title" "redis") (Project.config "title" "redis")
(Project.config "docs-directory" "./docs/") (Project.config "docs-directory" "./docs/")
(Project.config "docs-logo" "") (Project.config "docs-logo" "")
(Project.config "docs-url" "https://veitheller/git/carpentry/redis") (Project.config "docs-url" "https://git.veitheller.de/carpentry/redis")
(Project.config "docs-styling" "../style.css") (Project.config "docs-styling" "../style.css")
(Project.config "docs-prelude" "is a collection of modules for talking (Project.config "docs-prelude" "is a Redis client library for Carp, supporting Redis 7.x.
to Redis.
## Installation
``` ```
(load \"https://veitheller/git/carpentry/redis@master\") (load \"https://git.veitheller.de/carpentry/redis.git@0.2.0\")
```
## Example
```
(defn main []
(match (Redis.open \"127.0.0.1\")
(Result.Success r)
(do
(println* &(Redis.set &r @\"key\" @\"value\"))
(println* &(Redis.get &r @\"key\"))
(println* &(Redis.lrange &r @\"mylist\" @\"0\" @\"-1\"))
(match (Redis.get &r @\"key\")
(Result.Success resp)
(match resp
(RESP.Str s) (println* \"got: \" &s)
(RESP.Null) (println* \"not found\")
_ (println* \"unexpected type\"))
(Result.Error e) (println* \"error: \" &e))
(Redis.close r))
(Result.Error err) (IO.errorln &err)))
``` ```
") ")
+220 -94
View File
@@ -1,4 +1,4 @@
(load "git@github.com:carpentry-org/sockets@0.0.2") (load "git@github.com:carpentry-org/socket@0.1.1")
(deftype RESP (deftype RESP
(Null []) (Null [])
@@ -11,6 +11,10 @@
(defmodule RESP (defmodule RESP
(use-all Array Maybe Pattern Result) (use-all Array Maybe Pattern Result)
(hidden c)
(private c)
(def c (prn &@&(Arr [(Box.init (Null))])))
(defn str [r] (defn str [r]
(match @r (match @r
(Null) @"$-1\r\n" (Null) @"$-1\r\n"
@@ -18,84 +22,86 @@
(Err s) (fmt "-%s\r\n" &s) (Err s) (fmt "-%s\r\n" &s)
(Integer i) (fmt ":%d\r\n" i) (Integer i) (fmt ":%d\r\n" i)
(Arr a) (Arr a)
(fmt "*%d\r\n%s" (let-do [parts [(fmt "*%d\r\n" (Array.length &a))]]
(Array.length &a) (for [i 0 (Array.length &a)]
&(String.concat &(Array.copy-map &(fn [b] (str (unbox b))) &a))))) (Array.push-back! &parts (str (Box.peek (Array.unsafe-nth &a i)))))
(String.concat &parts))))
(hidden decode-bulk-string) (hidden decode-one)
(private decode-bulk-string) (private decode-one)
(defn decode-bulk-string [s] (defn decode-one [s pos]
(if (starts-with? s "-1\r\n") (if (>= pos (String.length s))
(Success (Pair 4 (Null))) (Success (Pair.init (Null) pos))
(let [splt (split #"\r\n" s) (case (String.char-at s pos)
l (unsafe-first &splt)] \+ (let [rest-start (+ pos 1)
(match (from-string l) idx (String.index-of-from s \return rest-start)]
(Nothing) (Error @"Error decoding bulk string: does not start with length!") (if (= idx -1)
(Just il) (Error @"Malformed simple string")
(Success (Success (Pair.init
(Pair (+ il 3) (Str (String.slice s rest-start idx))
(Str (+ idx 2)))))
(String.prefix &(join "\r\n" &(suffix &splt 1)) il)))))))) \- (let [rest-start (+ pos 1)
idx (String.index-of-from s \return rest-start)]
(private from-string-) (if (= idx -1)
(hidden from-string-) (Error @"Malformed error string")
(sig from-string- (Fn [&String] (Result (Pair Int RESP) String))) (Success (Pair.init
(defn from-string- [s] (Error @"dummy")) (Err (String.slice s rest-start idx))
(+ idx 2)))))
(hidden decode-arr) \: (let [rest-start (+ pos 1)
(private decode-arr) idx (String.index-of-from s \return rest-start)]
(defn decode-arr [is] (if (= idx -1)
(if (starts-with? &is "0\r\n") (Error @"Malformed integer")
(Success (Pair 4 (Arr []))) (match (Int.from-string &(String.slice s rest-start idx))
(let [splt (split #"\r\n" &(chomp &is)) (Maybe.Nothing) (Error @"Could not parse integer in result.")
sl (unsafe-first &splt) (Maybe.Just n) (Success (Pair.init
s (join "\r\n" &(suffix &splt 1))] (Integer n)
(match (from-string sl) (+ idx 2))))))
(Nothing) \$ (let [rest-start (+ pos 1)
(Error @"Error decoding array: does not start with length!") idx (String.index-of-from s \return rest-start)]
(Just l) (if (= idx -1)
(let-do [a (Array.allocate l) (Error @"Malformed bulk string")
consumed 0 (match (Int.from-string &(String.slice s rest-start idx))
err @""] (Maybe.Nothing) (Error @"Error decoding bulk string: does not start with length!")
(for [i 0 (- l 1)] (Maybe.Just len)
(match (from-string- &s) (if (= len -1)
(Error msg) (Success (Pair.init (Null) (+ idx 2)))
(do (let [data-start (+ idx 2)]
(set! err msg) (Success (Pair.init
(break)) (Str (String.slice s data-start (+ data-start len)))
(Success p) (+ data-start (+ len 2)))))))))
(do \* (let [rest-start (+ pos 1)
(+= consumed @(Pair.a &p)) idx (String.index-of-from s \return rest-start)]
(aset-uninitialized! &a i (Box @(Pair.b &p))) (if (= idx -1)
(set! s (String.suffix &s @(Pair.a &p)))))) (Error @"Malformed array")
(if (= &err "") (match (Int.from-string &(String.slice s rest-start idx))
(Success (Pair consumed (Arr a))) (Maybe.Nothing) (Error @"Error decoding array: does not start with length!")
(Error err))))))) (Maybe.Just len)
(if (= len -1)
(defn from-string- [s] (Success (Pair.init (Null) (+ idx 2)))
(if (empty? s) (if (= len 0)
(Success (Pair 0 (Null))) (Success (Pair.init (Arr []) (+ idx 2)))
(case (head s) (let-do [cur (+ idx 2)
\+ a (Array.allocate len)
(let [f (unsafe-first &(split #"\r\n" &(tail s)))] err @""]
(Success (Pair (String.length f) (Str @f)))) (for [i 0 len]
\- (match (decode-one s cur)
(let [f (unsafe-first &(split #"\r\n" &(tail s)))] (Result.Error e) (do (set! err e) (break))
(Success (Pair (String.length f) (Err @f)))) (Result.Success p)
\: (do
(let [f (unsafe-first &(split #"\r\n" &(tail s)))] (aset-uninitialized! &a i (Box.init @(Pair.a &p)))
(match (from-string f) (set! cur @(Pair.b &p)))))
(Maybe.Nothing) (if (= &err "")
(Error @"Could not parse integer in result.") (Success (Pair.init (Arr a) cur))
(Maybe.Just i) (Error err))))))))
(Success (Pair (String.length f) (Integer i))))) (Error (fmt "Malformed RESP data: got %s" &(String.slice s pos (+ pos 1)))))))
\$ (decode-bulk-string &(tail s))
\* (decode-arr (tail s))
(Error (fmt "Malformed RESP data: got %s" s)))))
(doc from-string "converts a RESP string into a `RESP` data structure.") (doc from-string "converts a RESP string into a `RESP` data structure.")
(defn from-string [s] (defn from-string [s]
(Result.map (from-string- s) &(fn [p] @(Pair.b &p)))) (if (empty? s)
(Success (Null))
(match (decode-one s 0)
(Result.Error e) (Error e)
(Result.Success p) (Success @(Pair.a &p)))))
(definterface to-redis (Fn [a] RESP)) (definterface to-redis (Fn [a] RESP))
) )
@@ -111,18 +117,17 @@
) )
(deftype Redis [ (deftype Redis [
sock Socket sock TcpStream
]) ])
(defmodule Redis (defmodule Redis
(use-all Array Result Socket) (use-all Array Result)
(doc open-on "opens the connection to Redis on port `port`.") (doc open-on "opens the connection to Redis on port `port`.")
(defn open-on [host port] (defn open-on [host port]
(let [s (setup-client host port)] (match (TcpStream.connect host port)
(if (valid? &s) (Result.Success s) (Success (init s))
(Success (init s)) (Result.Error e) (Error (fmt "Couldnt connect to %s:%d: %s" host port &e))))
(Error (fmt "Couldnt connect to %s:%d" host port)))))
(doc open "opens the connection to Redis on port 6379. (doc open "opens the connection to Redis on port 6379.
@@ -130,15 +135,19 @@ For variable port numbers please check out [`open-on`](#open-on).")
(defn open [host] (open-on host 6379)) (defn open [host] (open-on host 6379))
(doc read "reads a `RESP` object from Redis.") (doc read "reads a `RESP` object from Redis.")
(defn read [r] (RESP.from-string &(Socket.read (sock r)))) (defn read [r]
(match (the (Result String String) (TcpStream.read (sock r)))
(Result.Success s) (RESP.from-string &s)
(Result.Error e) (Error e)))
(doc send "sends the command `cmd` with the arguments `args` to Redis.") (doc send "sends the command `cmd` with the arguments `args` to Redis.")
(defn send [r cmd args] (defn send [r cmd args]
(if (empty? &args) (let [cmd-parts (copy-map &(fn [x] (Box.init (to-redis @x))) &(Pattern.split #" " &cmd))
(Socket.send (sock r) &(fmt "%s\r\n" cmd)) msg (str &(RESP.Arr (concat &[cmd-parts (copy-map &(fn [x] (Box.init @x)) args)])))]
(Socket.send (sock r) &(str &(RESP.Arr (concat &[[(Box (to-redis @cmd))] args])))))) (ignore (TcpStream.send (sock r) &msg))))
(doc close "closes the connection to Redis.") (doc close "closes the connection to Redis.")
(defn close [r] (Socket.close @(sock &r))) (defn close [r] (TcpStream.close @(sock &r)))
) )
(defndynamic rtreat- [s] (defndynamic rtreat- [s]
@@ -150,7 +159,7 @@ For variable port numbers please check out [`open-on`](#open-on).")
(defndynamic rconv- [args] (defndynamic rconv- [args]
(if (= (length args) 0) (if (= (length args) 0)
(array) (array)
(cons (list 'Box (list 'to-redis (car args))) (rconv- (cdr args))))) (cons (list 'to-redis (car args)) (rconv- (cdr args)))))
(defmacro defredis [cmd :rest args] (defmacro defredis [cmd :rest args]
(eval (eval
@@ -163,7 +172,7 @@ It takes the same arguments as the [Redis command](https://redis.io/commands/"
])) ]))
(list 'defn cmd (collect-into (cons 'r args) array) (list 'defn cmd (collect-into (cons 'r args) array)
(list 'do (list 'do
(list 'Redis.send 'r (rtreat- (Symbol.str cmd)) (rconv- args)) (list 'Redis.send 'r (list 'copy (rtreat- (Symbol.str cmd))) (list 'ref (rconv- args)))
'(Redis.read r)))))) '(Redis.read r))))))
; these commands were scraped from redis.io on the 9th of Feb 2020 ; these commands were scraped from redis.io on the 9th of Feb 2020
@@ -409,16 +418,133 @@ It takes the same arguments as the [Redis command](https://redis.io/commands/"
(defredis latency-latest) (defredis latency-latest)
(defredis latency-reset) (defredis latency-reset)
(defredis latency-help) (defredis latency-help)
; commands added in Redis 6.0
(defredis acl-cat)
(defredis acl-deluser username)
(defredis acl-genpass)
(defredis acl-getuser username)
(defredis acl-list)
(defredis acl-load)
(defredis acl-log)
(defredis acl-save)
(defredis acl-setuser username rule)
(defredis acl-users)
(defredis acl-whoami)
(defredis client-caching mode)
(defredis client-getredir)
(defredis client-tracking mode)
(defredis client-trackinginfo)
(defredis hello)
(defredis reset)
(defredis lpos key element)
; commands added in Redis 6.2
(defredis copy source destination)
(defredis getdel key)
(defredis getex key)
(defredis lmove source destination wherefrom whereto)
(defredis blmove source destination wherefrom whereto timeout)
(defredis smismember key member)
(defredis zmscore key member)
(defredis zdiff numkeys key)
(defredis zdiffstore destination numkeys key)
(defredis zunion numkeys key)
(defredis zinter numkeys key)
(defredis zrangestore dst src min max)
(defredis zrandmember key)
(defredis hrandfield key)
(defredis geosearch key)
(defredis geosearchstore destination source)
(defredis xautoclaim key group consumer min-idle-time start)
(defredis xgroup-createconsumer key groupname consumername)
(defredis xgroup-delconsumer key groupname consumername)
; commands added in Redis 7.0
(defredis function-load library-code)
(defredis function-delete library-name)
(defredis function-dump)
(defredis function-restore serialized-value)
(defredis function-list)
(defredis function-stats)
(defredis function-flush)
(defredis fcall function numkeys)
(defredis fcall_ro function numkeys)
(defredis eval_ro script numkeys key)
(defredis evalsha_ro sha1 numkeys key)
(defredis sort_ro key)
(defredis lmpop numkeys key)
(defredis blmpop timeout numkeys key)
(defredis zmpop numkeys key)
(defredis bzmpop timeout numkeys key)
(defredis zintercard numkeys key)
(defredis sintercard numkeys key)
(defredis expiretime key)
(defredis pexpiretime key)
(defredis lcs key1 key2)
(defredis cluster-shards)
(defredis cluster-links)
(defredis cluster-addslotsrange slot)
(defredis cluster-delslotsrange slot)
(defredis client-no-evict mode)
(defredis ssubscribe channel)
(defredis sunsubscribe)
(defredis spublish channel message)
(defredis command-docs command-name)
(defredis command-list)
(defredis latency-histogram)
(defredis module-loadex path)
; commands added in Redis 7.2
(defredis client-setinfo attr value)
(defredis client-no-touch mode)
(defredis waitaof numlocal numreplicas timeout)
(doc Redis "is a wrapper around Redis connections. It supports opening a (doc Redis "is a wrapper around Redis connections. It supports opening a
connection using [`open`](#open) or [`open-on`](#open-on), reading from and connection using [`open`](#open) or [`open-on`](#open-on), reading from and
sending to the connection (using [`read`](#read) and [`send`](#send), sending to the connection (using [`read`](#read) and [`send`](#send),
respectively), and contains thin wrappers around all Redis commands (everything respectively), and contains thin wrappers around all Redis commands through 7.2.
else).")
```
(match (Redis.open \"127.0.0.1\")
(Result.Success r)
(do
(println* &(Redis.set &r @\"key\" @\"val\"))
(println* &(Redis.get &r @\"key\"))
(println* &(Redis.lrange &r @\"list\" @\"0\" @\"-1\"))
(Redis.close r))
(Result.Error err) (IO.errorln &err))
```")
(doc RESP "is a wrapper around the [Redis Serialization (doc RESP "is a wrapper around the [Redis Serialization
Protocol](https://redis.io/topics/protocol). You can create all types—though Protocol](https://redis.io/topics/protocol). You can create all types,
creating arrays is a little unsightly due to the absence of recursive types—, stringify the built types into strings using [`str`](#str), and decode from
stringify the built types into strings using [`str`](#str), and decoding from the string protocol using [`from-string`](#from-string). Arrays are fully
the string protocol using [`from-string`](#from-string). supported, including nested arrays.
```
; decoding
(RESP.from-string \"+OK\\r\\n\") ; => (Success (Str @\"OK\"))
(RESP.from-string \":42\\r\\n\") ; => (Success (Integer 42))
(RESP.from-string \"$-1\\r\\n\") ; => (Success (Null))
; encoding
(str &(RESP.Str @\"hi\")) ; => \"$2\\r\\nhi\\r\\n\"
(str &(RESP.Integer 42)) ; => \":42\\r\\n\"
; pattern matching on responses
(match (Redis.get &r @\"key\")
(Result.Success resp)
(match resp
(RESP.Str s) (println* \"got: \" &s)
(RESP.Null) (println* \"not found\")
(RESP.Arr items) (println* \"array of \" &(Int.str (Array.length &items)))
_ (println* \"other\"))
(Result.Error e) (println* \"error: \" &e))
```
If you want your types to be supported when encoding, youll have to implement If you want your types to be supported when encoding, youll have to implement
the interface `to-redis`, the signature of which is `(Fn [a] RESP))`.") the interface `to-redis`, the signature of which is `(Fn [a] RESP))`.")