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