rackdis 0.12.pre.beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+

|
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
|