couch-shell 0.0.3 → 0.0.4
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/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
|