dpla-analysand 3.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +8 -0
  5. data/CHANGELOG +67 -0
  6. data/Gemfile +8 -0
  7. data/LICENSE +22 -0
  8. data/README +48 -0
  9. data/Rakefile +22 -0
  10. data/analysand.gemspec +33 -0
  11. data/bin/analysand +27 -0
  12. data/lib/analysand.rb +3 -0
  13. data/lib/analysand/bulk_response.rb +14 -0
  14. data/lib/analysand/change_watcher.rb +280 -0
  15. data/lib/analysand/config_response.rb +25 -0
  16. data/lib/analysand/connection_testing.rb +52 -0
  17. data/lib/analysand/database.rb +322 -0
  18. data/lib/analysand/errors.rb +60 -0
  19. data/lib/analysand/http.rb +90 -0
  20. data/lib/analysand/instance.rb +255 -0
  21. data/lib/analysand/reading.rb +26 -0
  22. data/lib/analysand/response.rb +35 -0
  23. data/lib/analysand/response_headers.rb +18 -0
  24. data/lib/analysand/session_response.rb +16 -0
  25. data/lib/analysand/status_code_predicates.rb +25 -0
  26. data/lib/analysand/streaming_view_response.rb +90 -0
  27. data/lib/analysand/version.rb +3 -0
  28. data/lib/analysand/view_response.rb +24 -0
  29. data/lib/analysand/view_streaming/builder.rb +142 -0
  30. data/lib/analysand/viewing.rb +95 -0
  31. data/lib/analysand/writing.rb +71 -0
  32. data/script/setup_database.rb +45 -0
  33. data/spec/analysand/a_response.rb +70 -0
  34. data/spec/analysand/change_watcher_spec.rb +102 -0
  35. data/spec/analysand/database_spec.rb +243 -0
  36. data/spec/analysand/database_writing_spec.rb +488 -0
  37. data/spec/analysand/instance_spec.rb +205 -0
  38. data/spec/analysand/response_spec.rb +26 -0
  39. data/spec/analysand/view_response_spec.rb +44 -0
  40. data/spec/analysand/view_streaming/builder_spec.rb +73 -0
  41. data/spec/analysand/view_streaming_spec.rb +122 -0
  42. data/spec/fixtures/vcr_cassettes/get_config.yml +40 -0
  43. data/spec/fixtures/vcr_cassettes/get_many_config.yml +40 -0
  44. data/spec/fixtures/vcr_cassettes/head_request_with_etag.yml +40 -0
  45. data/spec/fixtures/vcr_cassettes/reload_config.yml +114 -0
  46. data/spec/fixtures/vcr_cassettes/unauthorized_put_config.yml +43 -0
  47. data/spec/fixtures/vcr_cassettes/view.yml +40 -0
  48. data/spec/smoke/database_thread_spec.rb +59 -0
  49. data/spec/spec_helper.rb +30 -0
  50. data/spec/support/database_access.rb +40 -0
  51. data/spec/support/example_isolation.rb +86 -0
  52. data/spec/support/test_parameters.rb +39 -0
  53. metadata +283 -0
@@ -0,0 +1,90 @@
1
+ require 'analysand/response_headers'
2
+ require 'analysand/status_code_predicates'
3
+ require 'analysand/view_streaming/builder'
4
+ require 'fiber'
5
+
6
+ module Analysand
7
+ # Public: Controls streaming of view data.
8
+ #
9
+ # This class is meant to be used by Analysand::Database#view. It exports the
10
+ # same interface as ViewResponse.
11
+ #
12
+ # Examples:
13
+ #
14
+ # resp = db.view('view/something', :stream => true)
15
+ #
16
+ # resp.total_rows # => 1000000
17
+ # resp.offset # => 0
18
+ # resp.rows.take(100) # => first 100 rows
19
+ class StreamingViewResponse
20
+ include Enumerable
21
+ include ResponseHeaders
22
+ include StatusCodePredicates
23
+
24
+ # Internal: The HTTP response.
25
+ #
26
+ # This is set by Analysand::Database#stream_view. The #etag and #code
27
+ # methods use this for header information.
28
+ attr_accessor :response
29
+
30
+ def initialize
31
+ @reader = Fiber.new { yield self; "" }
32
+ @generator = ViewStreaming::Builder.new
33
+
34
+ # Analysand::Database#stream_view issues the request. When the response
35
+ # arrives, it yields control back here. Subsequent resumes read the
36
+ # body.
37
+ #
38
+ # We do this to provide the response headers as soon as possible.
39
+ @reader.resume
40
+ end
41
+
42
+ # Public: Yields documents in the view stream.
43
+ #
44
+ # Note that #docs and #rows advance the same stream, so expect to miss half
45
+ # your rows if you do something like
46
+ #
47
+ # resp.docs.zip(resp.rows)
48
+ #
49
+ # If this is a problem for you, let me know and we can work out a solution.
50
+ def docs
51
+ to_enum(:get_docs)
52
+ end
53
+
54
+ def get_docs
55
+ each { |r| yield r['doc'] if r.has_key?('doc') }
56
+ end
57
+
58
+ def total_rows
59
+ read until @generator.total_rows
60
+
61
+ @generator.total_rows
62
+ end
63
+
64
+ def offset
65
+ read until @generator.offset
66
+
67
+ @generator.offset
68
+ end
69
+
70
+ def read
71
+ @generator << @reader.resume
72
+ end
73
+
74
+ def each
75
+ return to_enum unless block_given?
76
+
77
+ while @reader.alive?
78
+ read while @reader.alive? && @generator.staged_rows.empty?
79
+
80
+ until @generator.staged_rows.empty?
81
+ yield @generator.staged_rows.shift
82
+ end
83
+ end
84
+ end
85
+
86
+ def rows
87
+ self
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,3 @@
1
+ module Analysand
2
+ VERSION = "3.0.2"
3
+ end
@@ -0,0 +1,24 @@
1
+ require 'analysand/response'
2
+
3
+ module Analysand
4
+ ##
5
+ # A subclass of Response with additional view-specific accessors: total_rows,
6
+ # offset, and rows.
7
+ class ViewResponse < Response
8
+ def total_rows
9
+ body['total_rows']
10
+ end
11
+
12
+ def offset
13
+ body['offset']
14
+ end
15
+
16
+ def rows
17
+ body['rows']
18
+ end
19
+
20
+ def docs
21
+ rows.map { |r| r['doc'] }.compact
22
+ end
23
+ end
24
+ 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.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.start_with?('_design') || !view_name.include?('/')
86
+ view_name
87
+ else
88
+ design_doc, view_name = view_name.split('/', 2)
89
+ "_design/#{design_doc}/_view/#{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 bulk_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,45 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path('../../spec/support/test_parameters', __FILE__)
4
+ require 'digest'
5
+ require 'json'
6
+
7
+ include TestParameters
8
+
9
+ admin_command = ['curl',
10
+ '-X PUT',
11
+ %Q{--data-binary '"#{admin_password}"'},
12
+ "#{instance_uri}/_config/admins/#{admin_username}"
13
+ ].join(' ')
14
+
15
+ salt = "ff518dabd59b04b527de7c55179059a46ac54976"
16
+
17
+ member_doc = {
18
+ "name" => member1_username,
19
+ "salt" => salt,
20
+ "password_sha" => Digest::SHA1.hexdigest("#{member1_password}#{salt}"),
21
+ "type" => "user",
22
+ "roles" => []
23
+ }
24
+
25
+ member_command = ['curl',
26
+ '-X PUT',
27
+ "--data-binary '#{member_doc.to_json}'",
28
+ "-u #{admin_username}:#{admin_password}",
29
+ "#{instance_uri}/_users/org.couchdb.user:#{member1_username}"
30
+ ].join(' ')
31
+
32
+ db_command = ['curl',
33
+ '-X PUT',
34
+ "-u #{admin_username}:#{admin_password}",
35
+ database_uri.to_s
36
+ ].join(' ')
37
+
38
+ [ admin_command,
39
+ member_command,
40
+ db_command
41
+ ].each do |cmd|
42
+ puts cmd
43
+ system cmd
44
+ puts
45
+ end