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.
@@ -25,8 +25,16 @@ module CouchShell
25
25
  end
26
26
  end
27
27
 
28
- attr_reader :type
29
- attr_reader :ruby_value
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.class} is not a valid json type"
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
- (@type == :object && @value.has_key?(msg.to_s)) ||
58
- @value.respond_to?(msg)
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
- @value.__send__(msg, *args)
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
@@ -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.contenttype.sub(/;[^;]*\z/, '')
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)
@@ -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
- @httpclient = HTTPClient.new
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 = path[1..-1].split("/")
104
+ @pathstack = []
105
+ cd path[1..-1], false
106
+ when %r{/}
107
+ path.split("/").each { |elem| cd elem, false }
101
108
  else
102
- @pathstack.concat path.split("/")
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 res.json
129
- @stdout.puts res.json.format
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
- @stdout.puts res.body
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
- res = http_client_request(method, expand(path), body)
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 = @httpclient.request(method, absolute_url, body, headers)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module CouchShell
4
4
 
5
- VERSION = "0.0.3"
5
+ VERSION = "0.0.4"
6
6
 
7
7
  end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 0
8
- - 3
9
- version: 0.0.3
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-19 00:00:00 +01:00
17
+ date: 2011-01-20 00:00:00 +01:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency