couch-shell 0.0.1 → 0.0.2
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.
- data/README.txt +6 -3
- data/lib/couch-shell/eval_context.rb +21 -0
- data/lib/couch-shell/json_value.rb +99 -0
- data/lib/couch-shell/response.rb +72 -0
- data/lib/couch-shell/ring_buffer.rb +87 -0
- data/lib/couch-shell/shell.rb +116 -71
- data/lib/couch-shell/version.rb +1 -1
- metadata +7 -3
data/README.txt
CHANGED
@@ -1,11 +1,14 @@
|
|
1
1
|
couch-shell is a basic shell to interact with a CouchDB server.
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
Install with rubygems:
|
4
|
+
|
5
|
+
$ sudo gem install couch-shell
|
6
|
+
|
7
|
+
Only tested with Ruby 1.9!
|
5
8
|
|
6
9
|
Example session:
|
7
10
|
|
8
|
-
$
|
11
|
+
$ couch-shell http://mambofulani.couchone.com/
|
9
12
|
couch-shell 0.0.1
|
10
13
|
Set server to http://mambofulani.couchone.com:80/
|
11
14
|
GET / 200 OK
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module CouchShell
|
4
|
+
|
5
|
+
class EvalContext < BasicObject
|
6
|
+
|
7
|
+
def initialize(vardict)
|
8
|
+
@vardict = vardict
|
9
|
+
end
|
10
|
+
|
11
|
+
def method_missing(msg, *args)
|
12
|
+
if args.empty?
|
13
|
+
@vardict.lookup_var msg.to_s
|
14
|
+
else
|
15
|
+
super
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module CouchShell
|
6
|
+
|
7
|
+
class JsonValue < BasicObject
|
8
|
+
|
9
|
+
def self.wrap(ruby_value)
|
10
|
+
case ruby_value
|
11
|
+
when ::Hash
|
12
|
+
h = ::Hash.new(ruby_value.size)
|
13
|
+
ruby_value.each { |k, v|
|
14
|
+
h[k] = wrap(v)
|
15
|
+
}
|
16
|
+
JsonValue.new(h, ruby_value)
|
17
|
+
when ::Array
|
18
|
+
a = ::Array.new(ruby_value.size)
|
19
|
+
ruby_value.each_with_index { |v, i|
|
20
|
+
a[i] = wrap(v)
|
21
|
+
}
|
22
|
+
JsonValue.new(a, ruby_value)
|
23
|
+
else
|
24
|
+
JsonValue.new(ruby_value, ruby_value)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :type
|
29
|
+
attr_reader :ruby_value
|
30
|
+
|
31
|
+
def initialize(value, ruby_value)
|
32
|
+
@value = value
|
33
|
+
@ruby_value = ruby_value
|
34
|
+
@type = case @value
|
35
|
+
when ::Hash
|
36
|
+
:object
|
37
|
+
when ::Array
|
38
|
+
:array
|
39
|
+
when ::String
|
40
|
+
:string
|
41
|
+
when ::Numeric
|
42
|
+
:number
|
43
|
+
when ::TrueClass
|
44
|
+
:boolean
|
45
|
+
when ::FalseClass
|
46
|
+
:boolean
|
47
|
+
when nil
|
48
|
+
:null
|
49
|
+
else
|
50
|
+
raise "#{value.class} is not a valid json type"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def respond_to?(msg)
|
55
|
+
msg == :format || msg == :couch_shell_format_string ||
|
56
|
+
msg == :type || msg == :ruby_value || msg == :to_s ||
|
57
|
+
(@type == :object && @value.has_key?(msg.to_s)) ||
|
58
|
+
@value.respond_to?(msg)
|
59
|
+
end
|
60
|
+
|
61
|
+
def method_missing(msg, *args)
|
62
|
+
if args.empty? && @type == :object
|
63
|
+
msg_str = msg.to_s
|
64
|
+
if @value.has_key?(msg_str)
|
65
|
+
return @value[msg_str]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
@value.__send__(msg, *args)
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_s
|
72
|
+
case type
|
73
|
+
when :object, :array
|
74
|
+
::JSON.generate(@ruby_value)
|
75
|
+
when :null
|
76
|
+
"null"
|
77
|
+
else
|
78
|
+
@ruby_value.to_s
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def format
|
83
|
+
case type
|
84
|
+
when :object, :array
|
85
|
+
::JSON.pretty_generate(@ruby_value)
|
86
|
+
when :null
|
87
|
+
"null"
|
88
|
+
when
|
89
|
+
@ruby_value.to_s
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def couch_shell_format_string
|
94
|
+
format
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "couch-shell/json_value"
|
5
|
+
|
6
|
+
module CouchShell
|
7
|
+
|
8
|
+
class Response
|
9
|
+
|
10
|
+
JSON_CONTENT_TYPES = [
|
11
|
+
"application/json", "text/plain"
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
# +response+ is a Net::HTTP response
|
15
|
+
def initialize(response)
|
16
|
+
@res = response
|
17
|
+
@json = nil
|
18
|
+
@computed_json = false
|
19
|
+
end
|
20
|
+
|
21
|
+
# Response body parsed as json. nil if body is empty, false if
|
22
|
+
# parsing failed.
|
23
|
+
def json
|
24
|
+
unless @computed_json
|
25
|
+
if JSON_CONTENT_TYPES.include?(content_type) &&
|
26
|
+
!@res.body.nil? && !@res.body.empty?
|
27
|
+
begin
|
28
|
+
@json = JsonValue.wrap(JSON.parse(@res.body))
|
29
|
+
rescue JSON::ParserError
|
30
|
+
@json = false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
@computed_json = true
|
34
|
+
end
|
35
|
+
@json
|
36
|
+
end
|
37
|
+
|
38
|
+
def code
|
39
|
+
@res.code
|
40
|
+
end
|
41
|
+
|
42
|
+
def message
|
43
|
+
@res.message
|
44
|
+
end
|
45
|
+
|
46
|
+
def ok?
|
47
|
+
code.start_with?("2")
|
48
|
+
end
|
49
|
+
|
50
|
+
def body
|
51
|
+
@res.body
|
52
|
+
end
|
53
|
+
|
54
|
+
def content_type
|
55
|
+
@res.content_type
|
56
|
+
end
|
57
|
+
|
58
|
+
def attr(name, altname = nil)
|
59
|
+
name = name.to_sym
|
60
|
+
altname = altname ? altname.to_sym : nil
|
61
|
+
if json
|
62
|
+
if json.respond_to?(name)
|
63
|
+
json.__send__ name
|
64
|
+
elsif altname && json.respond_to?(altname)
|
65
|
+
json.__send__ altname
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module CouchShell
|
4
|
+
|
5
|
+
# Not threadsafe!
|
6
|
+
class RingBuffer
|
7
|
+
|
8
|
+
class UninitializedAccess < StandardError
|
9
|
+
|
10
|
+
attr_reader :index
|
11
|
+
|
12
|
+
def initialize(index)
|
13
|
+
@index = index
|
14
|
+
end
|
15
|
+
|
16
|
+
def message
|
17
|
+
"uninitalized RingBuffer access at index #@index"
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(size)
|
23
|
+
@ary = Array.new(size, nil)
|
24
|
+
@index = nil
|
25
|
+
@written = 0
|
26
|
+
end
|
27
|
+
|
28
|
+
def size
|
29
|
+
@ary.size
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialized_size
|
33
|
+
@written
|
34
|
+
end
|
35
|
+
|
36
|
+
def empty?
|
37
|
+
@written == 0
|
38
|
+
end
|
39
|
+
|
40
|
+
def current
|
41
|
+
if @index.nil?
|
42
|
+
if block_given?
|
43
|
+
nil
|
44
|
+
else
|
45
|
+
raise UninitializedAccess.new(0)
|
46
|
+
end
|
47
|
+
else
|
48
|
+
if block_given?
|
49
|
+
yield @ary[index]
|
50
|
+
else
|
51
|
+
@ary[index]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def readable_index?(i)
|
57
|
+
i >= 0 && i < @written
|
58
|
+
end
|
59
|
+
|
60
|
+
# index of current (last written) element, or nil if empty
|
61
|
+
def index
|
62
|
+
@index
|
63
|
+
end
|
64
|
+
|
65
|
+
def [](i)
|
66
|
+
i = i.to_int
|
67
|
+
if i >= @written
|
68
|
+
raise UninitializedAccess.new(i)
|
69
|
+
end
|
70
|
+
@ary[i]
|
71
|
+
end
|
72
|
+
|
73
|
+
def <<(elem)
|
74
|
+
if @index.nil? || @index == size - 1
|
75
|
+
@index = 0
|
76
|
+
else
|
77
|
+
@index += 1
|
78
|
+
end
|
79
|
+
@ary[@index] = elem
|
80
|
+
if @written < @index + 1
|
81
|
+
@written = @index + 1
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
data/lib/couch-shell/shell.rb
CHANGED
@@ -2,8 +2,10 @@
|
|
2
2
|
|
3
3
|
require "uri"
|
4
4
|
require "net/http"
|
5
|
-
require "json"
|
6
5
|
require "highline"
|
6
|
+
require "couch-shell/response"
|
7
|
+
require "couch-shell/ring_buffer"
|
8
|
+
require "couch-shell/eval_context"
|
7
9
|
|
8
10
|
module CouchShell
|
9
11
|
|
@@ -25,8 +27,12 @@ module CouchShell
|
|
25
27
|
|
26
28
|
end
|
27
29
|
|
28
|
-
PREDEFINED_VARS = [
|
29
|
-
|
30
|
+
PREDEFINED_VARS = [
|
31
|
+
"uuid", "id", "rev", "idr",
|
32
|
+
"content-type", "server"
|
33
|
+
].freeze
|
34
|
+
|
35
|
+
JSON_DOC_START_RX = /\A[ \t\n\r]*[\(\{]/
|
30
36
|
|
31
37
|
def initialize(stdin, stdout, stderr)
|
32
38
|
@stdin = stdin
|
@@ -35,15 +41,26 @@ module CouchShell
|
|
35
41
|
@server_url = nil
|
36
42
|
@pathstack = []
|
37
43
|
@highline = HighLine.new(@stdin, @stdout)
|
38
|
-
@
|
39
|
-
@
|
44
|
+
@responses = RingBuffer.new(10)
|
45
|
+
@eval_context = EvalContext.new(self)
|
46
|
+
end
|
47
|
+
|
48
|
+
def normalize_server_url(url)
|
49
|
+
return nil if url.nil?
|
50
|
+
# remove trailing slash
|
51
|
+
url = url.sub(%r{/\z}, '')
|
52
|
+
# prepend http:// if scheme is omitted
|
53
|
+
if url =~ /\A\p{Alpha}(?:\p{Alpha}|\p{Digit}|\+|\-|\.)*:/
|
54
|
+
url
|
55
|
+
else
|
56
|
+
"http://#{url}"
|
57
|
+
end
|
40
58
|
end
|
41
59
|
|
42
60
|
def server=(url)
|
43
61
|
if url
|
44
|
-
@server_url = URI.parse(url
|
45
|
-
msg "Set server to #{
|
46
|
-
"#{@server_url.host}:#{@server_url.port}/#{@server_url.path}"
|
62
|
+
@server_url = URI.parse(normalize_server_url(url))
|
63
|
+
msg "Set server to #{lookup_var 'server'}"
|
47
64
|
request("GET", nil)
|
48
65
|
else
|
49
66
|
@server_url = nil
|
@@ -87,49 +104,14 @@ module CouchShell
|
|
87
104
|
@stderr.puts @highline.color(str, :red)
|
88
105
|
end
|
89
106
|
|
90
|
-
def response_info(str)
|
91
|
-
@stdout.puts @highline.color(str, :cyan)
|
92
|
-
end
|
93
|
-
|
94
|
-
def print_last_response
|
95
|
-
res = @last_response
|
96
|
-
response_info "#{res.code} #{res.message}"
|
97
|
-
if res.body
|
98
|
-
if JSON_CONTENT_TYPES.include?(res.content_type)
|
99
|
-
@stdout.puts JSON.pretty_generate(last_response_json)
|
100
|
-
else
|
101
|
-
@stdout.puts res.body if res.body
|
102
|
-
end
|
103
|
-
end
|
104
|
-
end
|
105
107
|
|
106
|
-
def
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
end
|
114
|
-
|
115
|
-
def last_response_json
|
116
|
-
if @last_response_json
|
117
|
-
return @last_response_json
|
118
|
-
end
|
119
|
-
if @last_response
|
120
|
-
@last_response_json = response_json(@last_response)
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
def last_response_ok?
|
125
|
-
@last_response && @last_response.code == "200"
|
126
|
-
end
|
127
|
-
|
128
|
-
def last_response_attr(name, altname = nil)
|
129
|
-
json = last_response_json
|
130
|
-
if json && json.kind_of?(Hash) &&
|
131
|
-
(json.has_key?(name) || json.has_key?(altname))
|
132
|
-
json.has_key?(name) ? json[name] : json[altname]
|
108
|
+
def print_response(res, label = "")
|
109
|
+
@stdout.print @highline.color("#{res.code} #{res.message}", :cyan)
|
110
|
+
msg " #{label}"
|
111
|
+
if res.json
|
112
|
+
@stdout.puts res.json.format
|
113
|
+
elsif res.body
|
114
|
+
@stdout.puts res.body
|
133
115
|
end
|
134
116
|
end
|
135
117
|
|
@@ -151,17 +133,25 @@ module CouchShell
|
|
151
133
|
Net::HTTP::Get
|
152
134
|
when "PUT"
|
153
135
|
Net::HTTP::Put
|
136
|
+
when "POST"
|
137
|
+
Net::HTTP::Post
|
154
138
|
when "DELETE"
|
155
139
|
Net::HTTP::Delete
|
156
140
|
else
|
157
141
|
raise "unsupported http method: `#{method}'"
|
158
142
|
end).new(fpath)
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
143
|
+
if body
|
144
|
+
req.body = body
|
145
|
+
if req.content_type.nil? && req.body =~ JSON_DOC_START_RX
|
146
|
+
req.content_type = "application/json"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
res = Response.new(http.request(req))
|
150
|
+
@responses << res
|
163
151
|
rescode = res.code
|
164
|
-
|
152
|
+
vars = ["r#{@responses.index}"]
|
153
|
+
vars << ["j#{@responses.index}"] if res.json
|
154
|
+
print_response res, " vars: #{vars.join(', ')}"
|
165
155
|
end
|
166
156
|
rescode
|
167
157
|
end
|
@@ -237,7 +227,7 @@ module CouchShell
|
|
237
227
|
String.new.force_encoding(str.encoding).tap { |res|
|
238
228
|
escape = false
|
239
229
|
dollar = false
|
240
|
-
|
230
|
+
expr = nil
|
241
231
|
str.each_char { |c|
|
242
232
|
if escape
|
243
233
|
res << c
|
@@ -250,21 +240,21 @@ module CouchShell
|
|
250
240
|
next
|
251
241
|
elsif c == '('
|
252
242
|
if dollar
|
253
|
-
|
243
|
+
expr = ""
|
254
244
|
else
|
255
245
|
res << c
|
256
246
|
end
|
257
247
|
elsif c == ')'
|
258
|
-
if
|
259
|
-
res <<
|
260
|
-
|
248
|
+
if expr
|
249
|
+
res << shell_eval(expr)
|
250
|
+
expr = nil
|
261
251
|
else
|
262
252
|
res << c
|
263
253
|
end
|
264
254
|
elsif dollar
|
265
255
|
res << "$"
|
266
|
-
elsif
|
267
|
-
|
256
|
+
elsif expr
|
257
|
+
expr << c
|
268
258
|
else
|
269
259
|
res << c
|
270
260
|
end
|
@@ -273,12 +263,16 @@ module CouchShell
|
|
273
263
|
}
|
274
264
|
end
|
275
265
|
|
276
|
-
def
|
266
|
+
def shell_eval(expr)
|
267
|
+
@eval_context.instance_eval(expr)
|
268
|
+
end
|
269
|
+
|
270
|
+
def lookup_var(var)
|
277
271
|
case var
|
278
272
|
when "uuid"
|
279
273
|
command_uuids nil
|
280
|
-
if
|
281
|
-
json =
|
274
|
+
if @responses.current(&:ok?)
|
275
|
+
json = @responses.current.json
|
282
276
|
if json && (uuids = json["uuids"]) && uuids.kind_of?(Array) && uuids.size > 0
|
283
277
|
uuids[0]
|
284
278
|
else
|
@@ -289,15 +283,40 @@ module CouchShell
|
|
289
283
|
raise ShellUserError, "interpolation failed"
|
290
284
|
end
|
291
285
|
when "id"
|
292
|
-
|
286
|
+
@responses.current { |r| r.attr "id", "_id" } or
|
293
287
|
raise ShellUserError, "variable `id' not set"
|
294
288
|
when "rev"
|
295
|
-
|
289
|
+
@responses.current { |r| r.attr "rev", "_rev" } or
|
296
290
|
raise ShellUserError, "variable `rev' not set"
|
297
291
|
when "idr"
|
298
|
-
"#{
|
292
|
+
"#{lookup_var 'id'}?rev=#{lookup_var 'rev'}"
|
299
293
|
when "content-type"
|
300
|
-
@
|
294
|
+
@responses.current(&:content_type)
|
295
|
+
when "server"
|
296
|
+
if @server_url
|
297
|
+
u = @server_url
|
298
|
+
"#{u.scheme}://#{u.host}:#{u.port}#{u.path}"
|
299
|
+
else
|
300
|
+
raise ShellUserError, "variable `server' not set"
|
301
|
+
end
|
302
|
+
when /\Ar(\d)\z/
|
303
|
+
i = $1.to_i
|
304
|
+
if @responses.readable_index?(i)
|
305
|
+
@responses[i]
|
306
|
+
else
|
307
|
+
raise ShellUserError, "no response index #{i}"
|
308
|
+
end
|
309
|
+
when /\Aj(\d)\z/
|
310
|
+
i = $1.to_i
|
311
|
+
if @responses.readable_index?(i)
|
312
|
+
if @responses[i].json
|
313
|
+
@responses[i].json
|
314
|
+
else
|
315
|
+
raise ShellUserError, "no json in response #{i}"
|
316
|
+
end
|
317
|
+
else
|
318
|
+
raise ShellUserError, "no response index #{i}"
|
319
|
+
end
|
301
320
|
else
|
302
321
|
raise UndefinedVariable.new(var)
|
303
322
|
end
|
@@ -312,6 +331,15 @@ module CouchShell
|
|
312
331
|
request "PUT", interpolate(url), body
|
313
332
|
end
|
314
333
|
|
334
|
+
def command_post(argstr)
|
335
|
+
if argstr =~ JSON_DOC_START_RX
|
336
|
+
url, body = nil, argstr
|
337
|
+
else
|
338
|
+
url, body = argstr.split(/\s+/, 2)
|
339
|
+
end
|
340
|
+
request "POST", interpolate(url), body
|
341
|
+
end
|
342
|
+
|
315
343
|
def command_delete(argstr)
|
316
344
|
request "DELETE", interpolate(argstr)
|
317
345
|
end
|
@@ -345,10 +373,27 @@ module CouchShell
|
|
345
373
|
|
346
374
|
def command_print(argstr)
|
347
375
|
unless argstr
|
348
|
-
errmsg "
|
376
|
+
errmsg "expression required"
|
349
377
|
return
|
350
378
|
end
|
351
|
-
@stdout.puts
|
379
|
+
@stdout.puts shell_eval(argstr)
|
380
|
+
end
|
381
|
+
|
382
|
+
def command_format(argstr)
|
383
|
+
unless argstr
|
384
|
+
errmsg "expression required"
|
385
|
+
return
|
386
|
+
end
|
387
|
+
val = shell_eval(argstr)
|
388
|
+
if val.respond_to?(:couch_shell_format_string)
|
389
|
+
@stdout.puts val.couch_shell_format_string
|
390
|
+
else
|
391
|
+
@stdout.puts val
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
def command_server(argstr)
|
396
|
+
self.server = argstr
|
352
397
|
end
|
353
398
|
|
354
399
|
end
|
data/lib/couch-shell/version.rb
CHANGED
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version: 0.0.
|
8
|
+
- 2
|
9
|
+
version: 0.0.2
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Stefan Lang
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2011-01-
|
17
|
+
date: 2011-01-19 00:00:00 +01:00
|
18
18
|
default_executable:
|
19
19
|
dependencies: []
|
20
20
|
|
@@ -27,6 +27,10 @@ extensions: []
|
|
27
27
|
extra_rdoc_files: []
|
28
28
|
|
29
29
|
files:
|
30
|
+
- lib/couch-shell/eval_context.rb
|
31
|
+
- lib/couch-shell/response.rb
|
32
|
+
- lib/couch-shell/ring_buffer.rb
|
33
|
+
- lib/couch-shell/json_value.rb
|
30
34
|
- lib/couch-shell/version.rb
|
31
35
|
- lib/couch-shell/shell.rb
|
32
36
|
- lib/couch-shell.rb
|