rackdis 0.12.pre.beta
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/CHANGELOG.md +33 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +198 -0
- data/Rakefile +6 -0
- data/bin/rackdis +44 -0
- data/etc/config.yml +11 -0
- data/lib/rackdis/api.rb +62 -0
- data/lib/rackdis/argument_parser.rb +29 -0
- data/lib/rackdis/config.rb +88 -0
- data/lib/rackdis/redis_facade.rb +118 -0
- data/lib/rackdis/response_builder.rb +22 -0
- data/lib/rackdis/version.rb +3 -0
- data/lib/rackdis.rb +76 -0
- data/rackdis.gemspec +36 -0
- data/spec/api_spec.rb +91 -0
- data/spec/lib/rackdis/argument_parser_spec.rb +23 -0
- data/spec/spec_helper.rb +27 -0
- metadata +263 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 75c6e66a488ac41277d59ce05e77de10be798f4e
|
4
|
+
data.tar.gz: 492d897a8e9593501e80b27d2d149be707b104ed
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 60a14fb0a27ba95a2e006b540d4e58590be7650729b849e33c3ca12763edef12d1785e537f3671095b226fe4aaae512cdd094e2d99001c84fe6139ed4d873927
|
7
|
+
data.tar.gz: 921643b6b898c0e72b4000233eeaa1312227201745c9bbf3042d90135394670aef9d329daed3717582ae17fba8d0aa7dd20bc7267a48350b23082b267a932da2
|
data/.gitignore
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## 0.12-beta
|
4
|
+
|
5
|
+
* refactored the API routes to dedup code, support POST commands
|
6
|
+
* added arg parser and response builder
|
7
|
+
* added a batch mode through a post request
|
8
|
+
* validated commands that come through the redis facade
|
9
|
+
* allowed running unsafe commands
|
10
|
+
* allowed force enabling commands
|
11
|
+
* made the config be setup better
|
12
|
+
* added some basic specs
|
13
|
+
* rescued all errors to return 500 (temporary)
|
14
|
+
* added logging with support for multiple locations/levels
|
15
|
+
* fixed option merging bugs
|
16
|
+
* added CORS support
|
17
|
+
* successfully tested chunked encoded subscribe manually
|
18
|
+
|
19
|
+
## 0.6-beta
|
20
|
+
|
21
|
+
* used the proper rack commands in the executable script
|
22
|
+
* allowed options to be loaded from a config file
|
23
|
+
* allowed options to be specified a the command line
|
24
|
+
* added support for most set commands (missing `SSCAN`)
|
25
|
+
* added support for most list commands (missing blocking commands)
|
26
|
+
* added support for `MGET`
|
27
|
+
* added support for `PUBLISH`
|
28
|
+
* added support for `SUBSCRIBE` over a streaming connection
|
29
|
+
|
30
|
+
## 0.0.1
|
31
|
+
|
32
|
+
* got the server running
|
33
|
+
* added GET and SET commands
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Robert McLeod
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
# Rackdis
|
2
|
+
|
3
|
+
![](http://i.imgur.com/CDKmIl1.png)
|
4
|
+
|
5
|
+
A Ruby clone of the awesome [Webdis](http://webd.is) server, allowing HTTP access to a Redis server through an API.
|
6
|
+
|
7
|
+
Webdis is awesome, but it hasn't had much action lately and there are lots of feature requests and bug reports that haven't been addressed. I would love to contribute but I have no desire in learning C at this stage. I also think Ruby is awesome so I wanted to try cloning Webdis in Ruby. Most of the stuff is already written for me, so this project is mostly glue code:
|
8
|
+
|
9
|
+
* [Grape](https://github.com/intridea/grape) provides the micro framework necessary to build an API
|
10
|
+
* [Slop](https://github.com/leejarvis/slop) awesome command line options
|
11
|
+
* [Rack::Stream](https://github.com/intridea/rack-stream) provides the websockets and chunked encoding support
|
12
|
+
* [Rack::Cors](https://github.com/cyu/rack-cors) provides cross origin resource sharing
|
13
|
+
* [Redis-rb](https://github.com/redis/redis-rb) provides the interface to a redis server
|
14
|
+
* [Thin](https://github.com/macournoyer/thin/) serves it all through rack
|
15
|
+
|
16
|
+
Most commands should work, there are a few left unsupported. However this code is mostly untested, with specs forthcoming so stuff might not work quite right yet (hence the `-beta`). I still have work to do in the Argument Parser and Response Builder area so commands that differ from the classic `COMMAND key [arg]` pattern may not work yet. In this situation you would see a `NotImplementError` reported.
|
17
|
+
|
18
|
+
**Please see the issues for feature planning and a vauge roadmap.**
|
19
|
+
|
20
|
+
## Features
|
21
|
+
|
22
|
+
* Most commands supported
|
23
|
+
* Unsafe commands disabled by default
|
24
|
+
* Pub/sub over streaming connections (chunked encoding/websockets)
|
25
|
+
* Batch processing of multiple redis commands (MULTI/EXEC anyone?)
|
26
|
+
* Logging to multiple locations and levels
|
27
|
+
* Cross Origin Resource Sharing
|
28
|
+
* Flexible command line operation
|
29
|
+
* Configuration priority: command line > config file > hard coded defaults
|
30
|
+
|
31
|
+
## Todo
|
32
|
+
|
33
|
+
* more relevant HTTP codes
|
34
|
+
* more specs
|
35
|
+
* figure out what MULTI/EXEC is and how to support it
|
36
|
+
* a lower level batch mode
|
37
|
+
|
38
|
+
## Installation
|
39
|
+
|
40
|
+
**Sorry I haven't actually pushed it to rubygems yet**
|
41
|
+
|
42
|
+
Add this line to your application's Gemfile:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
gem 'rackdis'
|
46
|
+
```
|
47
|
+
|
48
|
+
And then execute:
|
49
|
+
|
50
|
+
$ bundle
|
51
|
+
|
52
|
+
Or install it yourself as:
|
53
|
+
|
54
|
+
$ gem install rackdis
|
55
|
+
|
56
|
+
## Usage
|
57
|
+
|
58
|
+
The help screen is a good place to start:
|
59
|
+
|
60
|
+
```sh
|
61
|
+
$ rackdis --help
|
62
|
+
Usage: rackdis [options]
|
63
|
+
-r, --redis The redis server location (127.0.0.1, :6379, 127.0.0.1:6379)
|
64
|
+
-c, --config Config file to load options from
|
65
|
+
-p, --port Port for rackdis to bind to
|
66
|
+
-a, --address Address for rackdis to bind to
|
67
|
+
-d, --daemonize Put rackdis in the background
|
68
|
+
--log_level Log level (shutup, error, warn, debug, info)
|
69
|
+
-l, --log Log location (- or stdout for stdout, stderr)
|
70
|
+
--db The redis db to use (a number 0 through 16)
|
71
|
+
--allow_unsafe Enable unsafe commands (things like flushdb) !CAREFUL!
|
72
|
+
--force_enable Comma separated list of commands to enable !CAREFUL!
|
73
|
+
--allow_batching Allows batching of commands through a POST request
|
74
|
+
-h, --help Display this help message.
|
75
|
+
```
|
76
|
+
|
77
|
+
All of these commands are optional. The hard coded configuration has sane defaults. Specifying a configuration file would override those defaults. Specifying a command line options from above overrides everything. Speaking of configuration files, here's what one looks like:
|
78
|
+
|
79
|
+
```yml
|
80
|
+
---
|
81
|
+
:port: 7380
|
82
|
+
:address: 127.0.0.1
|
83
|
+
:daemonize: false
|
84
|
+
:db: 0
|
85
|
+
:log: stdout
|
86
|
+
:log_level: info
|
87
|
+
:allow_unsafe: false
|
88
|
+
:force_enable: false
|
89
|
+
:allow_batching: false
|
90
|
+
:redis: 127.0.0.1:6379
|
91
|
+
```
|
92
|
+
|
93
|
+
### Running it
|
94
|
+
|
95
|
+
Fully fledged example:
|
96
|
+
|
97
|
+
```sh
|
98
|
+
rackdis -p 7379 -a 127.0.0.1 -d --allow_batching --allow_unsafe -l stderr --log_level debug --redis :6379 --db 4 --force_enable flushdb,move,migrate,select
|
99
|
+
```
|
100
|
+
|
101
|
+
Simply load from a config file:
|
102
|
+
|
103
|
+
```sh
|
104
|
+
rackdis -c config.yml
|
105
|
+
```
|
106
|
+
|
107
|
+
### Call and response
|
108
|
+
|
109
|
+
The pattern looks like this: `http://localhost:7379/:version/:command/*args`
|
110
|
+
|
111
|
+
* **version** is just the API version (v1)
|
112
|
+
* **command** is the redis command to run
|
113
|
+
* everything after that is arguments to the redis command
|
114
|
+
|
115
|
+
The response is in JSON and provides some basic information about the request and including the result (of course).
|
116
|
+
|
117
|
+
So if I want to get a range from a list: `http://localhost:7380/v1/lrange/shopping_list/4/10`
|
118
|
+
|
119
|
+
```json
|
120
|
+
{
|
121
|
+
"success":true,
|
122
|
+
"command":"LRANGE",
|
123
|
+
"key":"shopping_list",
|
124
|
+
"result":[ "milk", "chicken abortions", "prophylactics"]
|
125
|
+
}
|
126
|
+
```
|
127
|
+
|
128
|
+
Add some members to a set: `http://localhost:7380/v1/sadd/cars/toyota/nissan/dodge`
|
129
|
+
|
130
|
+
```json
|
131
|
+
{
|
132
|
+
"success":true,
|
133
|
+
"command":"SADD",
|
134
|
+
"key":"cars",
|
135
|
+
"result":"OK"
|
136
|
+
}
|
137
|
+
```
|
138
|
+
|
139
|
+
Or just get a value: `http://localhost:7380/v1/get/readings:ph`
|
140
|
+
|
141
|
+
```json
|
142
|
+
{
|
143
|
+
"success":true,
|
144
|
+
"command":"GET",
|
145
|
+
"key":"readings:ph",
|
146
|
+
"result":"7.4"
|
147
|
+
}
|
148
|
+
```
|
149
|
+
|
150
|
+
You get the gist.
|
151
|
+
|
152
|
+
### Batching
|
153
|
+
|
154
|
+
Batching allows you to get redis to execute a bunch of commands one after the other
|
155
|
+
|
156
|
+
```javascript
|
157
|
+
$.post(
|
158
|
+
"http://localhost:7380/v1/batch",
|
159
|
+
{
|
160
|
+
commands: [
|
161
|
+
"SET this that",
|
162
|
+
"INCR rickrolls",
|
163
|
+
"SUNIONSTORE anothermans:treasure onemans:trash anothermans:treasure"
|
164
|
+
]
|
165
|
+
}
|
166
|
+
)
|
167
|
+
```
|
168
|
+
|
169
|
+
That will return this JSON:
|
170
|
+
|
171
|
+
```json
|
172
|
+
["OK", 45, "OK"]
|
173
|
+
```
|
174
|
+
|
175
|
+
You will need to enable batching explicitly at command line or in the config file.
|
176
|
+
|
177
|
+
### Pub/Sub
|
178
|
+
|
179
|
+
Publish/Subscribe is supported over chunked encoding and websockets.
|
180
|
+
|
181
|
+
More details forthcoming.
|
182
|
+
|
183
|
+
Subscribe: `http://localhost:7380/v1/subscribe/messages`
|
184
|
+
|
185
|
+
Publish: `http://localhost:7380/v1/publish/messages/hi`
|
186
|
+
|
187
|
+
## Changelog
|
188
|
+
|
189
|
+
Please see the file `CHANGELOG.md`
|
190
|
+
|
191
|
+
## Contributing
|
192
|
+
|
193
|
+
1. Fork it ( https://github.com/[my-github-username]/rackdis/fork )
|
194
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
195
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
196
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
197
|
+
5. Create a new Pull Request
|
198
|
+
6. If it's all good I'll merge it
|
data/Rakefile
ADDED
data/bin/rackdis
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rackdis'
|
4
|
+
require 'rack'
|
5
|
+
require 'slop'
|
6
|
+
|
7
|
+
opts = Slop.new help: true, strict: true, indent: 2 do
|
8
|
+
banner "Usage: rackdis [options]"
|
9
|
+
|
10
|
+
on :r, :redis=, "The redis server location (127.0.0.1, :6379, 127.0.0.1:6379)"
|
11
|
+
on :c, :config=, "Config file to load options from"
|
12
|
+
on :p, :port=, "Port for rackdis to bind to"
|
13
|
+
on :a, :address=, "Address for rackdis to bind to"
|
14
|
+
on :d, :daemonize, "Put rackdis in the background"
|
15
|
+
on :log_level=, "Log level (shutup, error, warn, debug, info)"
|
16
|
+
on :l, :log=, "Log location (- or stdout for stdout, stderr)"
|
17
|
+
on :db=, "The redis db to use (a number 0 through 16)"
|
18
|
+
on :allow_unsafe, "Enable unsafe commands (things like flushdb) !CAREFUL!"
|
19
|
+
on :force_enable=, "Comma separated list of commands to enable !CAREFUL!", as: Array
|
20
|
+
on :allow_batching, "Allows batching of commands through a POST request"
|
21
|
+
end
|
22
|
+
|
23
|
+
# Show any errors with the given options
|
24
|
+
begin
|
25
|
+
opts.parse!
|
26
|
+
Rackdis.configure(opts.to_hash)
|
27
|
+
rescue Slop::Error, ArgumentError => e
|
28
|
+
puts "ERROR: #{e.message}"
|
29
|
+
puts opts
|
30
|
+
abort
|
31
|
+
end
|
32
|
+
|
33
|
+
app = Rack::Builder.new do
|
34
|
+
use Rack::Stream
|
35
|
+
use Rack::Cors do
|
36
|
+
allow do
|
37
|
+
origins '*'
|
38
|
+
resource '*', headers: :any, methods: :get
|
39
|
+
end
|
40
|
+
end
|
41
|
+
run Rackdis::API.new
|
42
|
+
end
|
43
|
+
|
44
|
+
Rack::Handler::Thin.run(app, Rackdis.rack_options)
|
data/etc/config.yml
ADDED
data/lib/rackdis/api.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
module Rackdis
|
2
|
+
class API < Grape::API
|
3
|
+
|
4
|
+
|
5
|
+
rescue_from :all do |e|
|
6
|
+
Rackdis.logger.error("#{e.class.name}: #{e.message}")
|
7
|
+
Rack::Response.new({ success: false, error: e.message }.to_json, 500)
|
8
|
+
end
|
9
|
+
|
10
|
+
version 'v1'
|
11
|
+
format :json
|
12
|
+
|
13
|
+
helpers do
|
14
|
+
include Rack::Stream::DSL
|
15
|
+
|
16
|
+
def redis
|
17
|
+
@redis ||= RedisFacade.new(Rackdis.redis_client, Rackdis.config, Rackdis.logger)
|
18
|
+
end
|
19
|
+
|
20
|
+
def do_subscribe
|
21
|
+
after_open do
|
22
|
+
Rackdis.logger.debug "Someone subscribed to #{params[:channel]}"
|
23
|
+
redis.redis.subscribe params[:channel] do |on|
|
24
|
+
on.message do |channel, msg|
|
25
|
+
Rackdis.logger.debug "Pushing: #{msg}"
|
26
|
+
chunk msg
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
status 200
|
32
|
+
header 'Content-Type', 'application/json'
|
33
|
+
""
|
34
|
+
end
|
35
|
+
|
36
|
+
def args
|
37
|
+
params[:args].split("/")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Subscribe
|
42
|
+
get 'subscribe/:channel' do
|
43
|
+
do_subscribe
|
44
|
+
end
|
45
|
+
|
46
|
+
get 'SUBSCRIBE/:channel' do
|
47
|
+
do_subscribe
|
48
|
+
end
|
49
|
+
|
50
|
+
post 'batch' do
|
51
|
+
redis.batch params[:commands]
|
52
|
+
end
|
53
|
+
|
54
|
+
get ':command/*args' do
|
55
|
+
redis.call params[:command], args
|
56
|
+
end
|
57
|
+
|
58
|
+
post ':command/*args' do
|
59
|
+
redis.call params[:command], args
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Rackdis
|
2
|
+
class ArgumentParser
|
3
|
+
def initialize(command)
|
4
|
+
@command = command.to_sym
|
5
|
+
end
|
6
|
+
|
7
|
+
def process(args)
|
8
|
+
case @command
|
9
|
+
when :lpush, :rpush, :sadd, :srem, :sunionstore, :sinterstore, :sdiffstore
|
10
|
+
[
|
11
|
+
args[0],
|
12
|
+
args[1..-1]
|
13
|
+
]
|
14
|
+
when :mget, :sunion, :sinter, :sdiff
|
15
|
+
[
|
16
|
+
args[0..-1]
|
17
|
+
]
|
18
|
+
when :publish
|
19
|
+
[
|
20
|
+
args[0],
|
21
|
+
args[1..-1].join("/")
|
22
|
+
]
|
23
|
+
else
|
24
|
+
args
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Rackdis
|
2
|
+
class Config
|
3
|
+
|
4
|
+
def self.defaults
|
5
|
+
{
|
6
|
+
port: 7380,
|
7
|
+
address: "0.0.0.0",
|
8
|
+
daemonize: false,
|
9
|
+
log: STDOUT,
|
10
|
+
log_level: "info",
|
11
|
+
db: 0,
|
12
|
+
allow_unsafe: false,
|
13
|
+
force_enable: [],
|
14
|
+
allow_batching: false,
|
15
|
+
redis: "127.0.0.1:6379"
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(opts)
|
20
|
+
@config = Config.defaults
|
21
|
+
process_options opts
|
22
|
+
post_process
|
23
|
+
end
|
24
|
+
|
25
|
+
def [](key)
|
26
|
+
@config[key]
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def process_options(opts)
|
32
|
+
load_from_file opts[:config]
|
33
|
+
|
34
|
+
# We want to merge the command line opts
|
35
|
+
# overtop of the config file options but
|
36
|
+
# only if they are set because when they
|
37
|
+
# are not set they are nil which ends up
|
38
|
+
# wiping out all of the config
|
39
|
+
opts.each do |key, value|
|
40
|
+
next if value.nil?
|
41
|
+
@config[key] = value
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def load_from_file(file)
|
46
|
+
return false if file.nil?
|
47
|
+
raise ArgumentError, "Invalid config file: #{file}" unless File.exist? file
|
48
|
+
@config.merge!(YAML.load_file(file))
|
49
|
+
end
|
50
|
+
|
51
|
+
def post_process
|
52
|
+
process_log
|
53
|
+
process_redis
|
54
|
+
end
|
55
|
+
|
56
|
+
def process_log
|
57
|
+
@config[:log] = case @config[:log]
|
58
|
+
when nil, "", " ", "no", "none"
|
59
|
+
"/dev/null"
|
60
|
+
when "stdout", "-", "STDOUT"
|
61
|
+
STDOUT
|
62
|
+
when "stderr", "STDERR"
|
63
|
+
STDERR
|
64
|
+
else
|
65
|
+
@config[:log]
|
66
|
+
end
|
67
|
+
|
68
|
+
@config[:log] = "/dev/null" if @config[:log_level] == "shutup"
|
69
|
+
end
|
70
|
+
|
71
|
+
def process_redis
|
72
|
+
if @config[:redis].include? ":"
|
73
|
+
parts = @config[:redis].split(":")
|
74
|
+
port = parts.last
|
75
|
+
host = parts.first
|
76
|
+
else
|
77
|
+
host = @config[:redis]
|
78
|
+
end
|
79
|
+
|
80
|
+
host = "127.0.0.1" if host.nil? or host.empty?
|
81
|
+
port = 6379 if port.nil? or port.empty?
|
82
|
+
|
83
|
+
@config[:redis_port] = port
|
84
|
+
@config[:redis_host] = host
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module Rackdis
|
2
|
+
class RedisFacade
|
3
|
+
attr_reader :redis
|
4
|
+
|
5
|
+
def initialize(redis, config, log)
|
6
|
+
@redis = redis
|
7
|
+
@config = config
|
8
|
+
@log = log
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(command, args)
|
12
|
+
command.downcase!
|
13
|
+
valid_command! command
|
14
|
+
|
15
|
+
@log.info("redis> #{command} #{args.join(' ')}")
|
16
|
+
args = Rackdis::ArgumentParser.new(command).process(args)
|
17
|
+
|
18
|
+
@log.debug("API => REDIS: "+{command: command, args: args}.inspect)
|
19
|
+
|
20
|
+
begin
|
21
|
+
result = @redis.send(command, *args)
|
22
|
+
rescue ArgumentError
|
23
|
+
raise NotImplementedError, "Oops sorry - too beta - this needs fixing. A bug report at https://github.com/penguinpowernz/rackdis/issues would be nice :)"
|
24
|
+
end
|
25
|
+
|
26
|
+
return Rackdis::ResponseBuilder.new(command).process(args, result)
|
27
|
+
end
|
28
|
+
|
29
|
+
def batch(commands)
|
30
|
+
raise ArgumentError, "Batching is disabled" unless @config[:allow_batching]
|
31
|
+
|
32
|
+
# Try to extract individual commands
|
33
|
+
begin
|
34
|
+
commands = JSON.parse(commands)
|
35
|
+
rescue JSON::ParserError
|
36
|
+
raise ArgumentError, "Invalid batch commands" unless commands.is_a? String
|
37
|
+
commands = commands.split("\n")
|
38
|
+
end
|
39
|
+
|
40
|
+
raise ArgumentError, "Invalid batch commands" unless commands.is_a? Array
|
41
|
+
|
42
|
+
# Check each command and their arguments
|
43
|
+
calls = []
|
44
|
+
commands.each do |line|
|
45
|
+
args = line.split(" ")
|
46
|
+
cmd = args.shift.downcase.to_sym
|
47
|
+
|
48
|
+
# bail out if any of the commands or args are bad
|
49
|
+
valid_command!(cmd)
|
50
|
+
args = Rackdis::ArgumentParser.new(cmd).process(args)
|
51
|
+
|
52
|
+
calls << [cmd, args]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Everything passed to run all the commands now
|
56
|
+
calls.collect do |call|
|
57
|
+
result = @redis.send(*call)
|
58
|
+
Rackdis::ResponseBuilder.new(cmd).process(args, result)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Pub/Sub
|
63
|
+
|
64
|
+
def publish(channel, message)
|
65
|
+
@redis.publish(channel, message)
|
66
|
+
{ success: true, command: :PUBLISH, channel: channel }
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def valid_command!(cmd)
|
72
|
+
raise ArgumentError, "Unsupported command: #{command}" unless valid_command?
|
73
|
+
end
|
74
|
+
|
75
|
+
def valid_command?(cmd)
|
76
|
+
safe_commands.include?(cmd) or
|
77
|
+
(@config[:allow_unsafe] and unsafe_commands.include?(cmd)) or
|
78
|
+
(@config[:force_enable] and @config[:force_enable].include?(cmd))
|
79
|
+
end
|
80
|
+
|
81
|
+
def safe_commands
|
82
|
+
[
|
83
|
+
:ping, :echo, :dbsize, :time,
|
84
|
+
:persist, :expire, :expireat, :ttl, :pexpire, :pexpireat, :pttl, :dump, :restore,
|
85
|
+
:set, :setex, :psetex, :setnx, :mset, :msetnx, :setrange, :append,
|
86
|
+
:get, :getset, :mget, :getrange,
|
87
|
+
:getbit, :setbit, :bitcount, :bitop, :bitpos,
|
88
|
+
:incr, :incrby, :incrbyfloat, :decr, :decrby, :del, :exists, :keys, :randomkey, :rename, :renamex, :sort, :type, :strlen, :scan,
|
89
|
+
:lpush, :lpushx, :rpush, :rpushx, :lrange, :lindex, :linsert, :llen, :lpop, :rpop, :rpoplpush, :lrem, :lset, :ltrim,
|
90
|
+
:sadd, :scard, :smembers, :sismember, :srem, :sinter, :sinterstore, :sdiff, :sdiffstore, :sunion, :sunionstore, :spop, :srandmember, :smove, :sscan,
|
91
|
+
:zcard, :zadd, :zincrby, :zrem, :zscore, :zrange, :zrevrange, :zrank, :zrevrank, :zremrangebyrank, :zrangebylex, :zrangebyscore, :zrevrangebyscore, :zremrangebyscore, :zcount, :zinterstore, :zunionstore, :zsan,
|
92
|
+
:hlen, :hset, :hsetnx, :hmset, :hget, :hmget, :hdel, :hexists, :hincrby, :hincrbyfloat, :hkeys, :hvals, :hgetall, :hscan,
|
93
|
+
:publish,
|
94
|
+
:pfadd, :pfcount, :pfmerge
|
95
|
+
]
|
96
|
+
end
|
97
|
+
|
98
|
+
def unsafe_commands
|
99
|
+
[
|
100
|
+
:auth, :info, :slowlog,
|
101
|
+
:bgrewriteaof, :bgsave, :lastsave, :save,
|
102
|
+
:config, :flushall, :flushdb,
|
103
|
+
:move, :migrate, :select, :slaveof,
|
104
|
+
:script, :eval, :evalsha
|
105
|
+
]
|
106
|
+
end
|
107
|
+
|
108
|
+
# TODO: investigate and support these
|
109
|
+
# def unsupported_commmands
|
110
|
+
# [
|
111
|
+
# :monitor,
|
112
|
+
# :unsubscribe, :psubscribe, :punsubscribe,
|
113
|
+
# :brpop, :blpop, :brpoplpush
|
114
|
+
# ]
|
115
|
+
# end
|
116
|
+
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Rackdis
|
2
|
+
class ResponseBuilder
|
3
|
+
def initialize(command)
|
4
|
+
@command = command.to_sym
|
5
|
+
end
|
6
|
+
|
7
|
+
def process(args, result)
|
8
|
+
response_hash = {
|
9
|
+
command: @command.to_s.upcase,
|
10
|
+
result: result
|
11
|
+
}
|
12
|
+
|
13
|
+
case @command
|
14
|
+
when :sinterstore
|
15
|
+
else
|
16
|
+
response_hash[:key] = args[0]
|
17
|
+
end
|
18
|
+
|
19
|
+
response_hash
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/rackdis.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "yaml"
|
3
|
+
require "json"
|
4
|
+
require "redis"
|
5
|
+
require "grape"
|
6
|
+
require "redis/connection/synchrony"
|
7
|
+
require "rack/stream"
|
8
|
+
require "rack/cors"
|
9
|
+
|
10
|
+
require "rackdis/version"
|
11
|
+
require "rackdis/api"
|
12
|
+
require "rackdis/redis_facade"
|
13
|
+
require "rackdis/config"
|
14
|
+
require "rackdis/argument_parser"
|
15
|
+
require "rackdis/response_builder"
|
16
|
+
|
17
|
+
module Rackdis
|
18
|
+
class << self
|
19
|
+
attr_reader :config
|
20
|
+
|
21
|
+
def configure(**opts)
|
22
|
+
@config = Rackdis::Config.new(opts)
|
23
|
+
end
|
24
|
+
|
25
|
+
def rack_options
|
26
|
+
{
|
27
|
+
Port: @config[:port],
|
28
|
+
Host: @config[:address],
|
29
|
+
daemonize: @config[:daemonize]
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def redis_options
|
34
|
+
{
|
35
|
+
host: @config[:redis_host],
|
36
|
+
port: @config[:redis_port],
|
37
|
+
db: @config[:db] || 0
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def redis_client
|
42
|
+
Redis.new redis_options
|
43
|
+
end
|
44
|
+
|
45
|
+
def allow_unsafe_commands?
|
46
|
+
@config[:unsafe] || false
|
47
|
+
end
|
48
|
+
|
49
|
+
def logger
|
50
|
+
@logger ||= create_logger
|
51
|
+
end
|
52
|
+
|
53
|
+
def create_logger
|
54
|
+
logger = Logger.new @config[:log] || STDOUT
|
55
|
+
|
56
|
+
if @config
|
57
|
+
logger.level = case @config[:log_level]
|
58
|
+
when "debug"
|
59
|
+
Logger::DEBUG
|
60
|
+
when "info"
|
61
|
+
Logger::INFO
|
62
|
+
when "error"
|
63
|
+
Logger::ERROR
|
64
|
+
when "warn"
|
65
|
+
Logger::WARN
|
66
|
+
else
|
67
|
+
Logger::UNKNOWN # shutup!
|
68
|
+
end
|
69
|
+
else
|
70
|
+
logger.log_level = Logger::UNKNOWN
|
71
|
+
end
|
72
|
+
|
73
|
+
logger
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/rackdis.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'rackdis/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "rackdis"
|
8
|
+
spec.version = Rackdis::VERSION
|
9
|
+
spec.authors = ["Robert McLeod"]
|
10
|
+
spec.email = ["robert@penguinpower.co.nz"]
|
11
|
+
spec.summary = %q{Ruby clone of Webdis Redis web API}
|
12
|
+
spec.description = %q{A Ruby clone of the awesome Webdis Redis web API}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "rack"
|
22
|
+
spec.add_dependency "hiredis"
|
23
|
+
spec.add_dependency "redis"
|
24
|
+
spec.add_dependency "grape"
|
25
|
+
spec.add_dependency "rack-stream"
|
26
|
+
spec.add_dependency "em-synchrony"
|
27
|
+
spec.add_dependency "slop"
|
28
|
+
spec.add_dependency "thin"
|
29
|
+
spec.add_dependency "rack-cors"
|
30
|
+
|
31
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
32
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
33
|
+
spec.add_development_dependency "rspec"
|
34
|
+
spec.add_development_dependency "rack-test"
|
35
|
+
spec.add_development_dependency "pry"
|
36
|
+
end
|
data/spec/api_spec.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Rackdis::API do
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
subject(:response) { j2o last_response.body }
|
7
|
+
let(:r) { redis_connection }
|
8
|
+
let(:key) { "test:hello" }
|
9
|
+
after(:each) { r.del key }
|
10
|
+
|
11
|
+
describe "Key operations" do
|
12
|
+
|
13
|
+
describe "GET" do
|
14
|
+
it "should give null for unset values" do
|
15
|
+
get "/v1/get/#{key}"
|
16
|
+
expect(response.result).to eq nil
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should get a value that is set" do
|
20
|
+
r.set(key, "world")
|
21
|
+
# pry
|
22
|
+
get "/v1/get/#{key}"
|
23
|
+
expect(response.result).to eq "world"
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "SET" do
|
29
|
+
it "should set a value" do
|
30
|
+
get "/v1/set/#{key}/world"
|
31
|
+
expect(r.get(key)).to eq "world"
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should override a key" do
|
35
|
+
get "/v1/set/#{key}/world"
|
36
|
+
get "/v1/set/#{key}/bob"
|
37
|
+
expect(r.get(key)).to eq "bob"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "INCR" do
|
42
|
+
it "should increment a value" do
|
43
|
+
get "/v1/incr/#{key}"
|
44
|
+
get "/v1/incr/#{key}"
|
45
|
+
get "/v1/incr/#{key}"
|
46
|
+
expect(r.get key).to eq "3"
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should return the incremented value" do
|
50
|
+
get "/v1/incr/#{key}"
|
51
|
+
get "/v1/incr/#{key}"
|
52
|
+
get "/v1/incr/#{key}"
|
53
|
+
expect(response.result).to eq 3
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "Sets operations" do
|
59
|
+
|
60
|
+
describe "SMEMBERS" do
|
61
|
+
|
62
|
+
context "when there are no memebers" do
|
63
|
+
it "should return an empty array" do
|
64
|
+
get "/v1/smembers/#{key}"
|
65
|
+
expect(response.result).to eq []
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context "when there are members" do
|
70
|
+
it "should return them" do
|
71
|
+
r.sadd key, "world"
|
72
|
+
get "/v1/smembers/#{key}"
|
73
|
+
expect(response.result).to eq ["world"]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "SADD" do
|
80
|
+
|
81
|
+
it "should add single member" do
|
82
|
+
expect { get "/v1/sadd/#{key}/world" }.to change { r.scard key }.by(1)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should add multiple members" do
|
86
|
+
expect { get "/v1/sadd/#{key}/tom/dick/harry" }.to change { r.scard key }.by(3)
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Rackdis::ArgumentParser do
|
4
|
+
subject(:parser) { Rackdis::ArgumentParser.new(command) }
|
5
|
+
|
6
|
+
|
7
|
+
describe "PUBLISH" do
|
8
|
+
let(:command) { "publish" }
|
9
|
+
|
10
|
+
it "should not modify the correct number of args" do
|
11
|
+
inn = ["channel", "hi there"]
|
12
|
+
out = parser.process(inn)
|
13
|
+
expect(out).to eq inn
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should join the message args together" do
|
17
|
+
inn = ["channel", "she was about 18", "19"]
|
18
|
+
out = parser.process(inn)
|
19
|
+
expect(out.size).to eq 2
|
20
|
+
expect(out[1]).to eq "she was about 18/19"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rack/test'
|
2
|
+
require 'pry'
|
3
|
+
require 'ostruct'
|
4
|
+
require 'bundler/setup'
|
5
|
+
Bundler.setup
|
6
|
+
|
7
|
+
require 'rackdis'
|
8
|
+
|
9
|
+
# Set test options
|
10
|
+
Rackdis.configure(db: 13)
|
11
|
+
|
12
|
+
# give a seperate redis connection to the same
|
13
|
+
# database but use the hiredis driver to stop
|
14
|
+
# errors about the evma server
|
15
|
+
def redis_connection
|
16
|
+
Redis.new driver: :hiredis, db: 13
|
17
|
+
end
|
18
|
+
|
19
|
+
# Helpers for testing the action api
|
20
|
+
|
21
|
+
def app
|
22
|
+
Rackdis::API
|
23
|
+
end
|
24
|
+
|
25
|
+
def j2o(json)
|
26
|
+
OpenStruct.new(JSON.parse(json))
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,263 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rackdis
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.12.pre.beta
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Robert McLeod
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-11-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: hiredis
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: redis
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: grape
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rack-stream
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: em-synchrony
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: slop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: thin
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rack-cors
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: bundler
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '1.7'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '1.7'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rake
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '10.0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '10.0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: rspec
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: rack-test
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - ">="
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '0'
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: pry
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - ">="
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '0'
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - ">="
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: '0'
|
209
|
+
description: A Ruby clone of the awesome Webdis Redis web API
|
210
|
+
email:
|
211
|
+
- robert@penguinpower.co.nz
|
212
|
+
executables:
|
213
|
+
- rackdis
|
214
|
+
extensions: []
|
215
|
+
extra_rdoc_files: []
|
216
|
+
files:
|
217
|
+
- ".gitignore"
|
218
|
+
- CHANGELOG.md
|
219
|
+
- Gemfile
|
220
|
+
- LICENSE.txt
|
221
|
+
- README.md
|
222
|
+
- Rakefile
|
223
|
+
- bin/rackdis
|
224
|
+
- etc/config.yml
|
225
|
+
- lib/rackdis.rb
|
226
|
+
- lib/rackdis/api.rb
|
227
|
+
- lib/rackdis/argument_parser.rb
|
228
|
+
- lib/rackdis/config.rb
|
229
|
+
- lib/rackdis/redis_facade.rb
|
230
|
+
- lib/rackdis/response_builder.rb
|
231
|
+
- lib/rackdis/version.rb
|
232
|
+
- rackdis.gemspec
|
233
|
+
- spec/api_spec.rb
|
234
|
+
- spec/lib/rackdis/argument_parser_spec.rb
|
235
|
+
- spec/spec_helper.rb
|
236
|
+
homepage: ''
|
237
|
+
licenses:
|
238
|
+
- MIT
|
239
|
+
metadata: {}
|
240
|
+
post_install_message:
|
241
|
+
rdoc_options: []
|
242
|
+
require_paths:
|
243
|
+
- lib
|
244
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
245
|
+
requirements:
|
246
|
+
- - ">="
|
247
|
+
- !ruby/object:Gem::Version
|
248
|
+
version: '0'
|
249
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
250
|
+
requirements:
|
251
|
+
- - ">"
|
252
|
+
- !ruby/object:Gem::Version
|
253
|
+
version: 1.3.1
|
254
|
+
requirements: []
|
255
|
+
rubyforge_project:
|
256
|
+
rubygems_version: 2.2.2
|
257
|
+
signing_key:
|
258
|
+
specification_version: 4
|
259
|
+
summary: Ruby clone of Webdis Redis web API
|
260
|
+
test_files:
|
261
|
+
- spec/api_spec.rb
|
262
|
+
- spec/lib/rackdis/argument_parser_spec.rb
|
263
|
+
- spec/spec_helper.rb
|