analysand 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,3 @@
1
1
  module Analysand
2
- VERSION = "1.0.1"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -0,0 +1,142 @@
1
+ require 'analysand/errors'
2
+ require 'json/stream'
3
+
4
+ module Analysand
5
+ module ViewStreaming
6
+ # Private: A wrapper around JSON::Stream::Parser that extracts data from a
7
+ # CouchDB view document.
8
+ class Builder
9
+ attr_reader :offset
10
+ attr_reader :total_rows
11
+
12
+ # Rows constructed by the JSON parser that are ready for
13
+ # StreamingViewResponse#each.
14
+ attr_reader :staged_rows
15
+
16
+ # JSON::Stream::Parser callback methods.
17
+ CALLBACK_METHODS = %w(
18
+ start_object end_object start_array end_array key value
19
+ )
20
+
21
+ # If we find a key in the toplevel view object that isn't one of these,
22
+ # we raise UnexpectedViewKey.
23
+ KNOWN_KEYS = %w(
24
+ total_rows offset rows
25
+ )
26
+
27
+ def initialize
28
+ @in_rows = false
29
+ @parser = JSON::Stream::Parser.new
30
+ @stack = []
31
+ @staged_rows = []
32
+
33
+ CALLBACK_METHODS.each { |name| @parser.send(name, &method(name)) }
34
+ end
35
+
36
+ def <<(data)
37
+ @parser << data
38
+ end
39
+
40
+ def start_object
41
+ # We don't need to do anything for the toplevel view object, so just
42
+ # focus on the objects in rows.
43
+ if @in_rows
44
+ @stack.push ObjectNode.new
45
+ end
46
+ end
47
+
48
+ def end_object
49
+ if @in_rows
50
+ # If the stack's empty and we've come to the end of an object, assume
51
+ # we've exited the rows key. Trailing keys are handled by
52
+ # check_toplevel_key_validity.
53
+ if @stack.empty?
54
+ @in_rows = false
55
+ else
56
+ obj = @stack.pop.to_object
57
+
58
+ # If obj was the only thing on the stack and we're processing rows,
59
+ # then we've completed an object and need to stage it for the
60
+ # object stream.
61
+ if @stack.empty?
62
+ staged_rows << obj
63
+ else
64
+ @stack.last << obj
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ def start_array
71
+ # If we're not in the rows array but "rows" was the last key we've
72
+ # seen, then we're entering the rows array. Otherwise, we're building
73
+ # an array in a row.
74
+ if !@in_rows && @stack.pop == 'rows'
75
+ @in_rows = true
76
+ elsif @in_rows
77
+ @stack.push []
78
+ end
79
+ end
80
+
81
+ def end_array
82
+ if @in_rows
83
+ obj = @stack.pop
84
+
85
+ # If there's nothing on the row stack, it means that we've hit the
86
+ # end of the rows array.
87
+ if @stack.empty?
88
+ @in_rows = false
89
+ else
90
+ @stack.last << obj
91
+ end
92
+ end
93
+ end
94
+
95
+ def key(k)
96
+ if !@in_rows
97
+ check_toplevel_key_validity(k)
98
+ @stack.push k
99
+ else
100
+ @stack.last << k
101
+ end
102
+ end
103
+
104
+ def value(v)
105
+ if !@in_rows
106
+ case @stack.pop
107
+ when 'total_rows'; @total_rows = v
108
+ when 'offset'; @offset = v
109
+ end
110
+ else
111
+ @stack.last << v
112
+ end
113
+ end
114
+
115
+ def check_toplevel_key_validity(k)
116
+ if !KNOWN_KEYS.include?(k)
117
+ raise UnexpectedViewKey, "Unexpected key #{k} in top-level view object"
118
+ end
119
+ end
120
+
121
+ # Simplifies key/value pair construction.
122
+ class ObjectNode
123
+ def initialize
124
+ @obj = {}
125
+ end
126
+
127
+ def to_object
128
+ @obj
129
+ end
130
+
131
+ def <<(term)
132
+ if !@key
133
+ @key = term
134
+ elsif @key
135
+ @obj[@key] = term
136
+ @key = nil
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,95 @@
1
+ require 'analysand/errors'
2
+ require 'analysand/streaming_view_response'
3
+ require 'analysand/view_response'
4
+ require 'fiber'
5
+
6
+ module Analysand
7
+ module Viewing
8
+ JSON_VALUE_PARAMETERS = %w(key keys startkey endkey).map(&:to_sym)
9
+
10
+ def all_docs(parameters = {}, credentials = nil)
11
+ view('_all_docs', parameters, credentials)
12
+ end
13
+
14
+ def all_docs!(parameters = {}, credentials = nil)
15
+ view!('_all_docs', parameters, credentials)
16
+ end
17
+
18
+ def view(view_name, parameters = {}, credentials = nil)
19
+ stream = parameters.delete(:stream)
20
+ view_path = expand_view_path(view_name)
21
+
22
+ if stream
23
+ stream_view(view_path, parameters, credentials)
24
+ else
25
+ return_view(view_path, parameters, credentials)
26
+ end
27
+ end
28
+
29
+ def view!(view_name, parameters = {}, credentials = nil)
30
+ view(view_name, parameters, credentials).tap do |resp|
31
+ raise ex(CannotAccessView, resp) unless resp.success?
32
+ end
33
+ end
34
+
35
+ def stream_view(view_path, parameters, credentials)
36
+ StreamingViewResponse.new do |sresp|
37
+ do_view_query(view_path, parameters, credentials) do |resp|
38
+ sresp.http_response = resp
39
+ Fiber.yield
40
+ resp.read_body { |data| Fiber.yield(data) }
41
+ end
42
+ end
43
+ end
44
+
45
+ def return_view(view_path, parameters, credentials)
46
+ resp = do_view_query(view_path, parameters, credentials)
47
+
48
+ ViewResponse.new resp
49
+ end
50
+
51
+ def do_view_query(view_path, parameters, credentials, &block)
52
+ use_post = parameters.delete(:post)
53
+
54
+ if use_post
55
+ post_view(view_path, parameters, credentials, block)
56
+ else
57
+ get_view(view_path, parameters, credentials, block)
58
+ end
59
+ end
60
+
61
+ def get_view(view_path, parameters, credentials, block)
62
+ encode_parameters(parameters)
63
+ _get(view_path, credentials, parameters, {}, nil, block)
64
+ end
65
+
66
+ def post_view(view_path, parameters, credentials, block)
67
+ body = {
68
+ 'keys' => parameters.delete(:keys)
69
+ }.reject { |_, v| v.nil? }
70
+
71
+ encode_parameters(parameters)
72
+
73
+ _post(view_path, credentials, parameters, json_headers, body.to_json, block)
74
+ end
75
+
76
+ def encode_parameters(parameters)
77
+ JSON_VALUE_PARAMETERS.each do |p|
78
+ if parameters.has_key?(p)
79
+ parameters[p] = parameters[p].to_json
80
+ end
81
+ end
82
+ end
83
+
84
+ def expand_view_path(view_name)
85
+ if view_name.include?('/')
86
+ design_doc, view_name = view_name.split('/', 2)
87
+ "_design/#{design_doc}/_view/#{view_name}"
88
+ else
89
+ view_name
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ # vim:ts=2:sw=2:et:tw=78
@@ -0,0 +1,71 @@
1
+ require 'analysand/bulk_response'
2
+ require 'analysand/errors'
3
+ require 'analysand/response'
4
+
5
+ module Analysand
6
+ module Writing
7
+ def put(doc_id, doc, credentials = nil, options = {})
8
+ query = options
9
+
10
+ Response.new _put(doc_id, credentials, options, json_headers, doc.to_json)
11
+ end
12
+
13
+ def put!(doc_id, doc, credentials = nil, options = {})
14
+ put(doc_id, doc, credentials, options).tap do |resp|
15
+ raise ex(DocumentNotSaved, resp) unless resp.success?
16
+ end
17
+ end
18
+
19
+ def ensure_full_commit(credentials = nil, options = {})
20
+ Response.new _post('_ensure_full_commit', credentials, options, json_headers, {}.to_json)
21
+ end
22
+
23
+ def bulk_docs(docs, credentials = nil, options = {})
24
+ body = { 'docs' => docs }
25
+ body['all_or_nothing'] = true if options[:all_or_nothing]
26
+
27
+ BulkResponse.new _post('_bulk_docs', credentials, {}, json_headers, body.to_json)
28
+ end
29
+
30
+ def bulk_docs!(docs, credentials = nil, options = {})
31
+ bulk_docs(docs, credentials, options).tap do |resp|
32
+ raise ex(BulkOperationFailed, resp) unless resp.success?
33
+ end
34
+ end
35
+
36
+ def copy(source, destination, credentials = nil)
37
+ headers = { 'Destination' => destination }
38
+
39
+ Response.new _copy(source, credentials, {}, headers, nil)
40
+ end
41
+
42
+ def put_attachment(loc, io, credentials = nil, options = {})
43
+ query = {}
44
+ headers = {}
45
+
46
+ if options[:rev]
47
+ query['rev'] = options[:rev]
48
+ end
49
+
50
+ if options[:content_type]
51
+ headers['Content-Type'] = options[:content_type]
52
+ end
53
+
54
+ Response.new _put(loc, credentials, query, headers, io.read)
55
+ end
56
+
57
+ def delete(doc_id, rev, credentials = nil)
58
+ headers = { 'If-Match' => rev }
59
+
60
+ Response.new _delete(doc_id, credentials, {}, headers, nil)
61
+ end
62
+
63
+ def delete!(doc_id, rev, credentials = nil)
64
+ delete(doc_id, rev, credentials).tap do |resp|
65
+ raise ex(DocumentNotDeleted, resp) unless resp.success?
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ # vim:ts=2:sw=2:et:tw=78
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples_for 'a response' do
4
+ %w(etag success? code).each do |m|
5
+ it "responds to ##{m}" do
6
+ response.should respond_to(m)
7
+ end
8
+ end
9
+
10
+ describe '#etag' do
11
+ it 'returns a string' do
12
+ response.etag.should be_instance_of(String)
13
+ end
14
+
15
+ it 'returns ETags without quotes' do
16
+ response.etag.should_not include('"')
17
+ end
18
+ end
19
+ end
@@ -39,6 +39,24 @@ module Analysand
39
39
  drop_databases!
40
40
  end
41
41
 
42
+ describe '#connection_ok' do
43
+ describe 'with a non-public change feed' do
44
+ before do
45
+ set_security({ 'names' => [admin_username] })
46
+ end
47
+
48
+ after do
49
+ clear_security
50
+ end
51
+
52
+ it 'passes credentials' do
53
+ watcher = TestWatcher.new(db, admin_credentials)
54
+
55
+ watcher.connection_ok.should be_true
56
+ end
57
+ end
58
+ end
59
+
42
60
  describe '#changes_feed_uri' do
43
61
  let!(:watcher) { TestWatcher.new(db, admin_credentials) }
44
62
 
@@ -185,6 +185,13 @@ module Analysand
185
185
  resp.rows.length.should == 1
186
186
  end
187
187
 
188
+ it 'can issue POSTs' do
189
+ resp = db.view('doc/a_view', :keys => ['abc123', 'abc456'], :post => true)
190
+
191
+ resp.code.should == '200'
192
+ resp.rows.length.should == 2
193
+ end
194
+
188
195
  it 'passes credentials' do
189
196
  security = {
190
197
  'members' => {
@@ -3,17 +3,21 @@ require 'spec_helper'
3
3
  require 'analysand/database'
4
4
  require 'analysand/response'
5
5
 
6
+ require File.expand_path('../a_response', __FILE__)
7
+
6
8
  module Analysand
7
9
  describe Response do
8
10
  let(:db) { Database.new(database_uri) }
9
11
 
10
- describe '#etag' do
11
- let(:response) do
12
- VCR.use_cassette('head_request_with_etag') do
13
- db.head('abc123', admin_credentials)
14
- end
12
+ let(:response) do
13
+ VCR.use_cassette('head_request_with_etag') do
14
+ db.head('abc123', admin_credentials)
15
15
  end
16
+ end
16
17
 
18
+ it_should_behave_like 'a response'
19
+
20
+ describe '#etag' do
17
21
  it 'removes quotes from ETags' do
18
22
  response.etag.should == '1-967a00dff5e02add41819138abb3284d'
19
23
  end
@@ -1,18 +1,29 @@
1
1
  require 'spec_helper'
2
2
 
3
+ require 'analysand/database'
3
4
  require 'analysand/view_response'
4
5
 
6
+ require File.expand_path('../a_response', __FILE__)
7
+
5
8
  module Analysand
6
9
  describe ViewResponse do
7
- describe '#docs' do
8
- let(:resp_with_docs) do
9
- '{"rows": [{"id":"foo","key":"foo","value":{},"doc":{"foo":"bar"}}]}'
10
- end
10
+ let(:resp_with_docs) do
11
+ '{"rows": [{"id":"foo","key":"foo","value":{},"doc":{"foo":"bar"}}]}'
12
+ end
11
13
 
12
- let(:resp_without_docs) do
13
- '{"rows": [{"id":"foo","key":"foo","value":{}}]}'
14
+ let(:resp_without_docs) do
15
+ '{"rows": [{"id":"foo","key":"foo","value":{}}]}'
16
+ end
17
+
18
+ let(:db) { Database.new(database_uri) }
19
+
20
+ it_should_behave_like 'a response' do
21
+ let(:response) do
22
+ VCR.use_cassette('view') { db.view('doc/a_view') }
14
23
  end
24
+ end
15
25
 
26
+ describe '#docs' do
16
27
  describe 'if the view includes docs' do
17
28
  subject { ViewResponse.new(stub(:body => resp_with_docs)) }
18
29
 
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+
3
+ require 'analysand/view_streaming/builder'
4
+
5
+ module Analysand
6
+ module ViewStreaming
7
+ describe Builder do
8
+ let(:builder) { Builder.new }
9
+
10
+ it 'recognizes the "total_rows" key' do
11
+ builder << '{"total_rows":10000}'
12
+
13
+ builder.total_rows.should == 10000
14
+ end
15
+
16
+ it 'recognizes the "offset" key' do
17
+ builder << '{"offset":20}'
18
+
19
+ builder.offset.should == 20
20
+ end
21
+
22
+ describe 'for rows' do
23
+ before do
24
+ builder << '{"total_rows":10000,"offset":20,"rows":'
25
+ end
26
+
27
+ it 'builds objects' do
28
+ builder << '[{"id":"foo","key":"bar","value":1}]'
29
+
30
+ builder.staged_rows.should == [
31
+ { 'id' => 'foo', 'key' => 'bar', 'value' => 1 }
32
+ ]
33
+ end
34
+
35
+ it 'builds objects containing other objects' do
36
+ builder << '[{"id":"foo","key":"bar","value":{"total_rows":1}}]'
37
+
38
+ builder.staged_rows.should == [
39
+ { 'id' => 'foo',
40
+ 'key' => 'bar',
41
+ 'value' => {
42
+ 'total_rows' => 1
43
+ }
44
+ }
45
+ ]
46
+ end
47
+
48
+ it 'builds objects containing arrays' do
49
+ builder << '[{"id":"foo","key":"bar","value":{"things":[1,2,3]}}]'
50
+
51
+ builder.staged_rows.should == [
52
+ { 'id' => 'foo',
53
+ 'key' => 'bar',
54
+ 'value' => {
55
+ 'things' => [1, 2, 3]
56
+ }
57
+ }
58
+ ]
59
+ end
60
+ end
61
+
62
+ describe 'given unexpected top-level keys' do
63
+ before do
64
+ builder << '{"total_rows":10000,"offset":20,"rows":[],'
65
+ end
66
+
67
+ it 'raises Analysand::UnexpectedViewKey' do
68
+ lambda { builder << '"abc":"def"}' }.should raise_error(Analysand::UnexpectedViewKey)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end