couch-shell 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/couch-shell/json_value.rb +48 -8
- data/lib/couch-shell/response.rb +8 -5
- data/lib/couch-shell/shell.rb +266 -13
- data/lib/couch-shell/version.rb +1 -1
- metadata +3 -3
@@ -25,8 +25,16 @@ module CouchShell
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
-
|
29
|
-
|
28
|
+
def self.parse(str)
|
29
|
+
if str.start_with?("[") || str.start_with?("{") # optimization
|
30
|
+
wrap(::JSON.parse(str))
|
31
|
+
else
|
32
|
+
# JSON parses only JSON documents, i.e. an object or an array. Thus we
|
33
|
+
# box the given json value in an array and unbox it after parsing to
|
34
|
+
# allow parsing of any json value.
|
35
|
+
wrap(::JSON.parse("[#{str}]")[0])
|
36
|
+
end
|
37
|
+
end
|
30
38
|
|
31
39
|
def initialize(value, ruby_value)
|
32
40
|
@value = value
|
@@ -47,15 +55,15 @@ module CouchShell
|
|
47
55
|
when nil
|
48
56
|
:null
|
49
57
|
else
|
50
|
-
raise "#{value
|
58
|
+
::Kernel.raise "#{value} is not of a valid json type"
|
51
59
|
end
|
52
60
|
end
|
53
61
|
|
54
62
|
def respond_to?(msg)
|
55
63
|
msg == :format || msg == :couch_shell_format_string ||
|
56
64
|
msg == :type || msg == :ruby_value || msg == :to_s ||
|
57
|
-
|
58
|
-
@value.
|
65
|
+
msg == :couch_shell_ruby_value! ||
|
66
|
+
(@type == :object && @value.has_key?(msg.to_s))
|
59
67
|
end
|
60
68
|
|
61
69
|
def method_missing(msg, *args)
|
@@ -65,11 +73,16 @@ module CouchShell
|
|
65
73
|
return @value[msg_str]
|
66
74
|
end
|
67
75
|
end
|
68
|
-
|
76
|
+
super
|
77
|
+
end
|
78
|
+
|
79
|
+
def [](i)
|
80
|
+
::Kernel.raise ::TypeError unless @type == :array
|
81
|
+
@value[i]
|
69
82
|
end
|
70
83
|
|
71
84
|
def to_s
|
72
|
-
case type
|
85
|
+
case @type
|
73
86
|
when :object, :array
|
74
87
|
::JSON.generate(@ruby_value)
|
75
88
|
when :null
|
@@ -80,7 +93,7 @@ module CouchShell
|
|
80
93
|
end
|
81
94
|
|
82
95
|
def format
|
83
|
-
case type
|
96
|
+
case @type
|
84
97
|
when :object, :array
|
85
98
|
::JSON.pretty_generate(@ruby_value)
|
86
99
|
when :null
|
@@ -94,6 +107,33 @@ module CouchShell
|
|
94
107
|
format
|
95
108
|
end
|
96
109
|
|
110
|
+
def delete_attr!(name)
|
111
|
+
::Kernel.raise ::TypeError unless @type == :object
|
112
|
+
@ruby_value.delete(name)
|
113
|
+
@value.delete(name)
|
114
|
+
end
|
115
|
+
|
116
|
+
def set_attr!(name, value)
|
117
|
+
::Kernel.raise ::TypeError unless @type == :object
|
118
|
+
v = value.respond_to?(:couch_shell_ruby_value!) ?
|
119
|
+
value.couch_shell_ruby_value! : value
|
120
|
+
@ruby_value[name] = v
|
121
|
+
@value[name] = JsonValue.wrap(v)
|
122
|
+
end
|
123
|
+
|
124
|
+
def couch_shell_ruby_value!
|
125
|
+
@ruby_value
|
126
|
+
end
|
127
|
+
|
128
|
+
def nil?
|
129
|
+
false
|
130
|
+
end
|
131
|
+
|
132
|
+
def attr_or_nil!(name)
|
133
|
+
return nil unless @type == :object
|
134
|
+
@value[name]
|
135
|
+
end
|
136
|
+
|
97
137
|
end
|
98
138
|
|
99
139
|
end
|
data/lib/couch-shell/response.rb
CHANGED
@@ -11,7 +11,8 @@ module CouchShell
|
|
11
11
|
"application/json", "text/plain"
|
12
12
|
].freeze
|
13
13
|
|
14
|
-
# +response+ is a HTTP::Message from httpclient library
|
14
|
+
# +response+ is a HTTP::Message from httpclient library, or a Net::HTTP
|
15
|
+
# response
|
15
16
|
def initialize(response)
|
16
17
|
@res = response
|
17
18
|
@json = nil
|
@@ -36,11 +37,11 @@ module CouchShell
|
|
36
37
|
end
|
37
38
|
|
38
39
|
def code
|
39
|
-
@res.status.to_s
|
40
|
+
@res.respond_to?(:status) ? @res.status.to_s : @res.code
|
40
41
|
end
|
41
42
|
|
42
43
|
def message
|
43
|
-
@res.reason
|
44
|
+
@res.respond_to?(:message) ? @res.message : @res.reason
|
44
45
|
end
|
45
46
|
|
46
47
|
def ok?
|
@@ -48,11 +49,13 @@ module CouchShell
|
|
48
49
|
end
|
49
50
|
|
50
51
|
def body
|
51
|
-
@res.content
|
52
|
+
@res.respond_to?(:content) ? @res.content : @res.body
|
52
53
|
end
|
53
54
|
|
54
55
|
def content_type
|
55
|
-
@res.
|
56
|
+
@res.respond_to?(:content_type) ?
|
57
|
+
@res.content_type :
|
58
|
+
@res.contenttype.sub(/;[^;]*\z/, '')
|
56
59
|
end
|
57
60
|
|
58
61
|
def attr(name, altname = nil)
|
data/lib/couch-shell/shell.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
2
|
|
3
|
+
require "tempfile"
|
3
4
|
require "uri"
|
5
|
+
require "net/http"
|
4
6
|
require "httpclient"
|
5
7
|
require "highline"
|
6
8
|
require "couch-shell/response"
|
@@ -59,7 +61,7 @@ module CouchShell
|
|
59
61
|
@highline = HighLine.new(@stdin, @stdout)
|
60
62
|
@responses = RingBuffer.new(10)
|
61
63
|
@eval_context = EvalContext.new(self)
|
62
|
-
@
|
64
|
+
@viewtext = nil
|
63
65
|
end
|
64
66
|
|
65
67
|
def normalize_server_url(url)
|
@@ -96,10 +98,15 @@ module CouchShell
|
|
96
98
|
else
|
97
99
|
@pathstack.pop
|
98
100
|
end
|
101
|
+
when %r{\A/\z}
|
102
|
+
@pathstack = []
|
99
103
|
when %r{\A/}
|
100
|
-
@pathstack =
|
104
|
+
@pathstack = []
|
105
|
+
cd path[1..-1], false
|
106
|
+
when %r{/}
|
107
|
+
path.split("/").each { |elem| cd elem, false }
|
101
108
|
else
|
102
|
-
@pathstack
|
109
|
+
@pathstack << path
|
103
110
|
end
|
104
111
|
if get
|
105
112
|
if request("GET", nil) != "200"
|
@@ -122,36 +129,74 @@ module CouchShell
|
|
122
129
|
end
|
123
130
|
|
124
131
|
|
125
|
-
def print_response(res, label = "")
|
132
|
+
def print_response(res, label = "", show_body = true)
|
126
133
|
@stdout.print @highline.color("#{res.code} #{res.message}", :cyan)
|
127
134
|
msg " #{label}"
|
128
|
-
if
|
129
|
-
|
135
|
+
if show_body
|
136
|
+
if res.json
|
137
|
+
@stdout.puts res.json.format
|
138
|
+
elsif res.body
|
139
|
+
@stdout.puts res.body
|
140
|
+
end
|
130
141
|
elsif res.body
|
131
|
-
|
142
|
+
msg "body has #{res.body.bytesize} bytes"
|
132
143
|
end
|
133
144
|
end
|
134
145
|
|
135
|
-
def request(method, path, body = nil)
|
146
|
+
def request(method, path, body = nil, show_body = true)
|
136
147
|
unless @server_url
|
137
148
|
errmsg "Server not set - can't perform request."
|
138
149
|
return
|
139
150
|
end
|
140
|
-
fpath = full_path(path)
|
151
|
+
fpath = URI.encode(full_path(path))
|
141
152
|
msg "#{method} #{fpath} ", false
|
142
153
|
if @server_url.scheme != "http"
|
143
154
|
errmsg "Protocol #{@server_url.scheme} not supported, use http."
|
144
155
|
return
|
145
156
|
end
|
146
|
-
|
157
|
+
# HTTPClient and CouchDB don't work together with simple put/post
|
158
|
+
# requests to due some Keep-alive mismatch.
|
159
|
+
#
|
160
|
+
# Net:HTTP doesn't support file upload streaming.
|
161
|
+
if body.kind_of?(FileToUpload) || method == "GET"
|
162
|
+
res = http_client_request(method, URI.encode(expand(path)), body)
|
163
|
+
else
|
164
|
+
res = net_http_request(method, fpath, body)
|
165
|
+
end
|
147
166
|
@responses << res
|
148
167
|
rescode = res.code
|
149
168
|
vars = ["r#{@responses.index}"]
|
150
169
|
vars << ["j#{@responses.index}"] if res.json
|
151
|
-
print_response res, " vars: #{vars.join(', ')}"
|
170
|
+
print_response res, " vars: #{vars.join(', ')}", show_body
|
152
171
|
res.code
|
153
172
|
end
|
154
173
|
|
174
|
+
def net_http_request(method, fpath, body)
|
175
|
+
res = nil
|
176
|
+
Net::HTTP.start(@server_url.host, @server_url.port) do |http|
|
177
|
+
req = (case method
|
178
|
+
when "GET"
|
179
|
+
Net::HTTP::Get
|
180
|
+
when "PUT"
|
181
|
+
Net::HTTP::Put
|
182
|
+
when "POST"
|
183
|
+
Net::HTTP::Post
|
184
|
+
when "DELETE"
|
185
|
+
Net::HTTP::Delete
|
186
|
+
else
|
187
|
+
raise "unsupported http method: `#{method}'"
|
188
|
+
end).new(fpath)
|
189
|
+
if body
|
190
|
+
req.body = body
|
191
|
+
if req.content_type.nil? && req.body =~ JSON_DOC_START_RX
|
192
|
+
req.content_type = "application/json"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
res = Response.new(http.request(req))
|
196
|
+
end
|
197
|
+
res
|
198
|
+
end
|
199
|
+
|
155
200
|
def http_client_request(method, absolute_url, body)
|
156
201
|
file = nil
|
157
202
|
headers = {}
|
@@ -163,7 +208,7 @@ module CouchShell
|
|
163
208
|
elsif body && body =~ JSON_DOC_START_RX
|
164
209
|
headers['Content-Type'] = "application/json"
|
165
210
|
end
|
166
|
-
res =
|
211
|
+
res = HTTPClient.new.request(method, absolute_url, body, headers)
|
167
212
|
Response.new(res)
|
168
213
|
ensure
|
169
214
|
file.close if file
|
@@ -193,6 +238,22 @@ module CouchShell
|
|
193
238
|
end
|
194
239
|
end
|
195
240
|
|
241
|
+
def prompt_msg(msg, newline = true)
|
242
|
+
@stdout.print @highline.color(msg, :yellow)
|
243
|
+
if newline
|
244
|
+
@stdout.puts
|
245
|
+
else
|
246
|
+
@stdout.flush
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def continue?(msg)
|
251
|
+
prompt_msg(msg, false)
|
252
|
+
unless @stdin.gets.chomp.empty?
|
253
|
+
raise ShellUserError, "cancelled"
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
196
257
|
def read
|
197
258
|
lead = @pathstack.empty? ? ">>" : @pathstack.join("/") + " >>"
|
198
259
|
begin
|
@@ -335,6 +396,9 @@ module CouchShell
|
|
335
396
|
else
|
336
397
|
raise ShellUserError, "no response index #{i}"
|
337
398
|
end
|
399
|
+
when "viewtext"
|
400
|
+
@viewtext or
|
401
|
+
raise ShellUserError, "viewtext not set"
|
338
402
|
else
|
339
403
|
raise UndefinedVariable.new(var)
|
340
404
|
end
|
@@ -346,7 +410,7 @@ module CouchShell
|
|
346
410
|
else
|
347
411
|
url, bodyarg= argstr.split(/\s+/, 2)
|
348
412
|
end
|
349
|
-
if bodyarg.start_with?("@")
|
413
|
+
if bodyarg && bodyarg.start_with?("@")
|
350
414
|
filename, content_type = bodyarg[1..-1].split(/\s+/, 2)
|
351
415
|
body = FileToUpload.new(filename, content_type)
|
352
416
|
else
|
@@ -355,6 +419,11 @@ module CouchShell
|
|
355
419
|
request method, interpolate(url), body
|
356
420
|
end
|
357
421
|
|
422
|
+
def editor_bin!
|
423
|
+
ENV["EDITOR"] or
|
424
|
+
raise ShellUserError, "EDITOR environment variable not set"
|
425
|
+
end
|
426
|
+
|
358
427
|
def command_get(argstr)
|
359
428
|
request "GET", interpolate(argstr)
|
360
429
|
end
|
@@ -427,6 +496,190 @@ module CouchShell
|
|
427
496
|
@stdout.puts expand(interpolate(argstr))
|
428
497
|
end
|
429
498
|
|
499
|
+
def command_sh(argstr)
|
500
|
+
unless argstr
|
501
|
+
errmsg "argument required"
|
502
|
+
return
|
503
|
+
end
|
504
|
+
unless system(argstr)
|
505
|
+
errmsg "command exited with status #{$?.exitstatus}"
|
506
|
+
end
|
507
|
+
end
|
508
|
+
|
509
|
+
def command_editview(argstr)
|
510
|
+
if @pathstack.size != 1
|
511
|
+
raise ShellUserError, "current directory must be database"
|
512
|
+
end
|
513
|
+
design_name, view_name = argstr.split(/\s+/, 2)
|
514
|
+
if design_name.nil? || view_name.nil?
|
515
|
+
raise ShellUserError, "design and view name required"
|
516
|
+
end
|
517
|
+
request "GET", "_design/#{design_name}", nil, false
|
518
|
+
return unless @responses.current(&:ok?)
|
519
|
+
design = @responses.current.json
|
520
|
+
view = nil
|
521
|
+
if design.respond_to?(:views) &&
|
522
|
+
design.views.respond_to?(view_name.to_sym)
|
523
|
+
view = design.views.__send__(view_name.to_sym)
|
524
|
+
end
|
525
|
+
mapval = view && view.respond_to?(:map) && view.map
|
526
|
+
reduceval = view && view.respond_to?(:reduce) && view.reduce
|
527
|
+
t = Tempfile.new(["view", ".js"])
|
528
|
+
t.puts("map")
|
529
|
+
if mapval
|
530
|
+
t.puts mapval
|
531
|
+
else
|
532
|
+
t.puts "function(doc) {\n emit(doc._id, doc);\n}"
|
533
|
+
end
|
534
|
+
if reduceval || view.nil?
|
535
|
+
t.puts
|
536
|
+
t.puts("reduce")
|
537
|
+
if reduceval
|
538
|
+
t.puts reduceval
|
539
|
+
else
|
540
|
+
t.puts "function(keys, values, rereduce) {\n\n}"
|
541
|
+
end
|
542
|
+
end
|
543
|
+
t.close
|
544
|
+
continue?(
|
545
|
+
"Press ENTER to edit #{view ? 'existing' : 'new'} view, " +
|
546
|
+
"CTRL+C to cancel ")
|
547
|
+
unless system(editor_bin!, t.path)
|
548
|
+
raise ShellUserError, "editing command failed with exit status #{$?.exitstatus}"
|
549
|
+
end
|
550
|
+
text = t.open.read
|
551
|
+
@viewtext = text
|
552
|
+
t.close
|
553
|
+
mapf = nil
|
554
|
+
reducef = nil
|
555
|
+
inmap = false
|
556
|
+
inreduce = false
|
557
|
+
i = 0
|
558
|
+
text.each_line { |line|
|
559
|
+
i += 1
|
560
|
+
case line
|
561
|
+
when /^map\s*(.*)$/
|
562
|
+
unless $1.empty?
|
563
|
+
msg "recover view text with `print viewtext'"
|
564
|
+
raise ShellUserError, "invalid map line at line #{i}"
|
565
|
+
end
|
566
|
+
unless mapf.nil?
|
567
|
+
msg "recover view text with `print viewtext'"
|
568
|
+
raise ShellUserError, "duplicate map line at line #{i}"
|
569
|
+
end
|
570
|
+
inreduce = false
|
571
|
+
inmap = true
|
572
|
+
mapf = ""
|
573
|
+
when /^reduce\s*(.*)$/
|
574
|
+
unless $1.empty?
|
575
|
+
msg "recover view text with `print viewtext'"
|
576
|
+
raise ShellUserError, "invalid reduce line at line #{i}"
|
577
|
+
end
|
578
|
+
unless reducef.nil?
|
579
|
+
msg "recover view text with `print viewtext'"
|
580
|
+
raise ShellUserError, "duplicate reduce line at line #{i}"
|
581
|
+
end
|
582
|
+
inmap = false
|
583
|
+
inreduce = true
|
584
|
+
reducef = ""
|
585
|
+
else
|
586
|
+
if inmap
|
587
|
+
mapf << line
|
588
|
+
elsif inreduce
|
589
|
+
reducef << line
|
590
|
+
elsif line =~ /^\s*$/
|
591
|
+
# ignore
|
592
|
+
else
|
593
|
+
msg "recover view text with `print viewtext'"
|
594
|
+
raise ShellUserError, "unexpected content at line #{i}"
|
595
|
+
end
|
596
|
+
end
|
597
|
+
}
|
598
|
+
mapf.strip! if mapf
|
599
|
+
reducef.strip! if reducef
|
600
|
+
mapf = nil if mapf && mapf.empty?
|
601
|
+
reducef = nil if reducef && reducef.empty?
|
602
|
+
prompt_msg "View parsed, following actions would be taken:"
|
603
|
+
if mapf && mapval.nil?
|
604
|
+
prompt_msg " Add map function."
|
605
|
+
elsif mapf.nil? && mapval
|
606
|
+
prompt_msg " Remove map function."
|
607
|
+
elsif mapf && mapval && mapf != mapval
|
608
|
+
prompt_msg " Update map function."
|
609
|
+
end
|
610
|
+
if reducef && reduceval.nil?
|
611
|
+
prompt_msg " Add reduce function."
|
612
|
+
elsif reducef.nil? && reduceval
|
613
|
+
prompt_msg " Remove reduce function."
|
614
|
+
elsif reducef && reduceval && reducef != reduceval
|
615
|
+
prompt_msg " Update reduce function."
|
616
|
+
end
|
617
|
+
continue? "Press ENTER to submit, CTRL+C to cancel "
|
618
|
+
if !design.respond_to?(:views)
|
619
|
+
design.set_attr!("views", {})
|
620
|
+
end
|
621
|
+
if view.nil?
|
622
|
+
design.views.set_attr!(view_name, {})
|
623
|
+
view = design.views.__send__(view_name.to_sym)
|
624
|
+
end
|
625
|
+
if mapf.nil?
|
626
|
+
view.delete_attr!("map")
|
627
|
+
else
|
628
|
+
view.set_attr!("map", mapf)
|
629
|
+
end
|
630
|
+
if reducef.nil?
|
631
|
+
view.delete_attr!("reduce")
|
632
|
+
else
|
633
|
+
view.set_attr!("reduce", reducef)
|
634
|
+
end
|
635
|
+
request "PUT", "_design/#{design_name}", design.to_s
|
636
|
+
unless @responses.current(&:ok?)
|
637
|
+
msg "recover view text with `print viewtext'"
|
638
|
+
end
|
639
|
+
ensure
|
640
|
+
if t
|
641
|
+
t.close
|
642
|
+
t.unlink
|
643
|
+
end
|
644
|
+
end
|
645
|
+
|
646
|
+
def command_view(argstr)
|
647
|
+
if @pathstack.size != 1
|
648
|
+
raise ShellUserError, "current directory must be database"
|
649
|
+
end
|
650
|
+
design_name, view_name = argstr.split("/", 2)
|
651
|
+
if design_name.nil? || view_name.nil?
|
652
|
+
raise ShellUserError, "argument in the form DESIGN/VIEW required"
|
653
|
+
end
|
654
|
+
request "GET", "_design/#{design_name}/_view/#{view_name}"
|
655
|
+
end
|
656
|
+
|
657
|
+
def command_member(argstr)
|
658
|
+
id, rev = nil, nil
|
659
|
+
json = @responses.current(&:json)
|
660
|
+
unless json && (id = json.attr_or_nil!("_id")) &&
|
661
|
+
(rev = json.attr_or_nil!("_rev")) &&
|
662
|
+
(@pathstack.size > 0) &&
|
663
|
+
(@pathstack.last == id.to_s)
|
664
|
+
raise ShellUserError,
|
665
|
+
"`cg' the desired document first, e.g.: `cg /my_db/my_doc_id'"
|
666
|
+
end
|
667
|
+
# TODO: read json string as attribute name if argstr starts with double
|
668
|
+
# quote
|
669
|
+
attr_name, new_valstr = argstr.split(/\s+/, 2)
|
670
|
+
unless attr_name && new_valstr
|
671
|
+
raise ShellUserError,
|
672
|
+
"attribute name and new value argument required"
|
673
|
+
end
|
674
|
+
if new_valstr == "remove"
|
675
|
+
json.delete_attr!(attr_name)
|
676
|
+
else
|
677
|
+
new_val = JsonValue.parse(new_valstr)
|
678
|
+
json.set_attr!(attr_name, new_val)
|
679
|
+
end
|
680
|
+
request "PUT", "?rev=#{rev}", json.to_s
|
681
|
+
end
|
682
|
+
|
430
683
|
end
|
431
684
|
|
432
685
|
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
|
+
- 4
|
9
|
+
version: 0.0.4
|
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-20 00:00:00 +01:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|