Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fafa2b40d2 | |||
| 692fe51728 | |||
| d88b0631a6 | |||
| 0b64365e39 | |||
| ed02b8aadb |
@@ -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/>
|
||||||
|
|
||||||
|
|||||||
+47
-6
@@ -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,15 +30,35 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="module">
|
||||||
<h1>
|
<h1>
|
||||||
RESP
|
RESP
|
||||||
</h1>
|
</h1>
|
||||||
<div class="module-description">
|
<div class="module-description">
|
||||||
<p>is a wrapper around the <a href="https://redis.io/topics/protocol">Redis Serialization
|
<p>is a wrapper around the <a href="https://redis.io/topics/protocol">Redis Serialization
|
||||||
Protocol</a>. You can create all types—though
|
Protocol</a>. 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 <a href="#str"><code>str</code></a>, and decode from
|
||||||
stringify the built types into strings using <a href="#str"><code>str</code></a>, and decoding from
|
the string protocol using <a href="#from-string"><code>from-string</code></a>. Arrays are fully
|
||||||
the string protocol using <a href="#from-string"><code>from-string</code></a>.</p>
|
supported, including nested arrays.</p>
|
||||||
|
<pre><code>; 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))
|
||||||
|
</code></pre>
|
||||||
<p>If you want your types to be supported when encoding, you’ll have to implement
|
<p>If you want your types to be supported when encoding, you’ll 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>
|
||||||
|
|
||||||
@@ -53,7 +73,7 @@ the interface <code>to-redis</code>, the signature of which is <code>(Fn [a] RES
|
|||||||
instantiate
|
instantiate
|
||||||
</div>
|
</div>
|
||||||
<p class="sig">
|
<p class="sig">
|
||||||
(Fn [(Array String)] RESP)
|
(Fn [(Array (Box RESP))] RESP)
|
||||||
</p>
|
</p>
|
||||||
<span>
|
<span>
|
||||||
|
|
||||||
@@ -163,6 +183,26 @@ the interface <code>to-redis</code>, the signature of which is <code>(Fn [a] RES
|
|||||||
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="binder">
|
||||||
|
<a class="anchor" href="#delete">
|
||||||
|
<h3 id="delete">
|
||||||
|
delete
|
||||||
|
</h3>
|
||||||
|
</a>
|
||||||
|
<div class="description">
|
||||||
|
instantiate
|
||||||
|
</div>
|
||||||
|
<p class="sig">
|
||||||
|
(Fn [RESP] ())
|
||||||
|
</p>
|
||||||
|
<span>
|
||||||
|
|
||||||
|
</span>
|
||||||
|
<p class="doc">
|
||||||
|
<p>deletes a <code>RESP</code>. This should usually not be called manually.</p>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div class="binder">
|
<div class="binder">
|
||||||
<a class="anchor" href="#from-string">
|
<a class="anchor" href="#from-string">
|
||||||
<h3 id="from-string">
|
<h3 id="from-string">
|
||||||
@@ -244,5 +284,6 @@ the interface <code>to-redis</code>, the signature of which is <code>(Fn [a] RES
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+1558
-15
File diff suppressed because it is too large
Load Diff
@@ -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 "https://veitheller/git/carpentry/redis@master")
|
<pre><code>(load "https://git.veitheller.de/carpentry/redis.git@0.2.0")
|
||||||
|
</code></pre>
|
||||||
|
<h2>Example</h2>
|
||||||
|
<pre><code>(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)))
|
||||||
</code></pre>
|
</code></pre>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
+59
-9
@@ -4,19 +4,69 @@
|
|||||||
(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"))
|
||||||
|
|
||||||
|
; counters
|
||||||
|
(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))
|
(Redis.close r))
|
||||||
(Result.Error err) (IO.errorln &err)))
|
(Result.Error err) (IO.errorln &err)))
|
||||||
|
|||||||
+26
-4
@@ -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)))
|
||||||
```
|
```
|
||||||
")
|
")
|
||||||
|
|
||||||
|
|||||||
+217
-91
@@ -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))
|
||||||
|
(Maybe.Nothing) (Error @"Error decoding bulk string: does not start with length!")
|
||||||
|
(Maybe.Just len)
|
||||||
|
(if (= len -1)
|
||||||
|
(Success (Pair.init (Null) (+ idx 2)))
|
||||||
|
(let [data-start (+ idx 2)]
|
||||||
|
(Success (Pair.init
|
||||||
|
(Str (String.slice s data-start (+ data-start len)))
|
||||||
|
(+ data-start (+ len 2)))))))))
|
||||||
|
\* (let [rest-start (+ pos 1)
|
||||||
|
idx (String.index-of-from s \return rest-start)]
|
||||||
|
(if (= idx -1)
|
||||||
|
(Error @"Malformed array")
|
||||||
|
(match (Int.from-string &(String.slice s rest-start idx))
|
||||||
|
(Maybe.Nothing) (Error @"Error decoding array: does not start with length!")
|
||||||
|
(Maybe.Just len)
|
||||||
|
(if (= len -1)
|
||||||
|
(Success (Pair.init (Null) (+ idx 2)))
|
||||||
|
(if (= len 0)
|
||||||
|
(Success (Pair.init (Arr []) (+ idx 2)))
|
||||||
|
(let-do [cur (+ idx 2)
|
||||||
|
a (Array.allocate len)
|
||||||
err @""]
|
err @""]
|
||||||
(for [i 0 (- l 1)]
|
(for [i 0 len]
|
||||||
(match (from-string- &s)
|
(match (decode-one s cur)
|
||||||
(Error msg)
|
(Result.Error e) (do (set! err e) (break))
|
||||||
|
(Result.Success p)
|
||||||
(do
|
(do
|
||||||
(set! err msg)
|
(aset-uninitialized! &a i (Box.init @(Pair.a &p)))
|
||||||
(break))
|
(set! cur @(Pair.b &p)))))
|
||||||
(Success p)
|
|
||||||
(do
|
|
||||||
(+= consumed @(Pair.a &p))
|
|
||||||
(aset-uninitialized! &a i (Box @(Pair.b &p)))
|
|
||||||
(set! s (String.suffix &s @(Pair.a &p))))))
|
|
||||||
(if (= &err "")
|
(if (= &err "")
|
||||||
(Success (Pair consumed (Arr a)))
|
(Success (Pair.init (Arr a) cur))
|
||||||
(Error err)))))))
|
(Error err))))))))
|
||||||
|
(Error (fmt "Malformed RESP data: got %s" &(String.slice s pos (+ pos 1)))))))
|
||||||
(defn from-string- [s]
|
|
||||||
(if (empty? s)
|
|
||||||
(Success (Pair 0 (Null)))
|
|
||||||
(case (head s)
|
|
||||||
\+
|
|
||||||
(let [f (unsafe-first &(split #"\r\n" &(tail s)))]
|
|
||||||
(Success (Pair (String.length f) (Str @f))))
|
|
||||||
\-
|
|
||||||
(let [f (unsafe-first &(split #"\r\n" &(tail s)))]
|
|
||||||
(Success (Pair (String.length f) (Err @f))))
|
|
||||||
\:
|
|
||||||
(let [f (unsafe-first &(split #"\r\n" &(tail s)))]
|
|
||||||
(match (from-string f)
|
|
||||||
(Maybe.Nothing)
|
|
||||||
(Error @"Could not parse integer in result.")
|
|
||||||
(Maybe.Just i)
|
|
||||||
(Success (Pair (String.length f) (Integer i)))))
|
|
||||||
\$ (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 "Couldn’t connect to %s:%d: %s" host port &e))))
|
||||||
(Error (fmt "Couldn’t 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, you’ll have to implement
|
If you want your types to be supported when encoding, you’ll 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))`.")
|
||||||
|
|||||||
Reference in New Issue
Block a user