analysand 1.0.1 → 1.1.0
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/CHANGELOG +16 -0
- data/README +3 -2
- data/analysand.gemspec +4 -1
- data/bin/analysand +2 -3
- data/lib/analysand.rb +3 -5
- data/lib/analysand/change_watcher.rb +3 -1
- data/lib/analysand/connection_testing.rb +7 -1
- data/lib/analysand/database.rb +38 -147
- data/lib/analysand/errors.rb +3 -0
- data/lib/analysand/reading.rb +26 -0
- data/lib/analysand/streaming_view_response.rb +100 -0
- data/lib/analysand/version.rb +1 -1
- data/lib/analysand/view_streaming/builder.rb +142 -0
- data/lib/analysand/viewing.rb +95 -0
- data/lib/analysand/writing.rb +71 -0
- data/spec/analysand/a_response.rb +19 -0
- data/spec/analysand/change_watcher_spec.rb +18 -0
- data/spec/analysand/database_spec.rb +7 -0
- data/spec/analysand/response_spec.rb +9 -5
- data/spec/analysand/view_response_spec.rb +17 -6
- data/spec/analysand/view_streaming/builder_spec.rb +73 -0
- data/spec/analysand/view_streaming_spec.rb +122 -0
- data/spec/fixtures/vcr_cassettes/view.yml +40 -0
- data/spec/support/database_access.rb +2 -2
- metadata +90 -63
    
        data/CHANGELOG
    ADDED
    
    | @@ -0,0 +1,16 @@ | |
| 1 | 
            +
            Issue numbers refer to issues on Analysand's Github tracker:
         | 
| 2 | 
            +
            https://github.com/yipdw/analysand/issues
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            1.1.0 (2012-11-03)
         | 
| 5 | 
            +
            ------------------
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            * View streaming (#3)
         | 
| 8 | 
            +
            * ChangeWatchers now pass credentials when checking CouchDB status (#4)
         | 
| 9 | 
            +
            * Some code organization cleanups
         | 
| 10 | 
            +
            * require "analysand" now loads the Database and Instance classes
         | 
| 11 | 
            +
             | 
| 12 | 
            +
             | 
| 13 | 
            +
            1.0.1 (2012-10-01)
         | 
| 14 | 
            +
            ------------------
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            * Initial release
         | 
    
        data/README
    CHANGED
    
    | @@ -12,9 +12,10 @@ Features: | |
| 12 12 | 
             
            * GET, PUT, DELETE on databases
         | 
| 13 13 | 
             
            * GET, PUT, DELETE, HEAD, COPY on documents
         | 
| 14 14 | 
             
            * GET, PUT on document attachments
         | 
| 15 | 
            -
            * GET on views
         | 
| 15 | 
            +
            * GET, POST on views
         | 
| 16 16 | 
             
            * POST /_session
         | 
| 17 17 | 
             
            * POST /_bulk_docs
         | 
| 18 | 
            +
            * View streaming
         | 
| 18 19 | 
             
            * Celluloid::IO-based change feed watchers
         | 
| 19 20 | 
             
            * Cookie and HTTP Basic authentication for all of the above
         | 
| 20 21 | 
             
            * Database objects can be safely shared across threads
         | 
| @@ -31,7 +32,7 @@ information. | |
| 31 32 |  | 
| 32 33 | 
             
            Naturally, we hang with all the cool kids:
         | 
| 33 34 |  | 
| 34 | 
            -
            * Travis CI:  | 
| 35 | 
            +
            * Travis CI: https://travis-ci.org/#!/yipdw/analysand
         | 
| 35 36 | 
             
            * Code Climate: https://codeclimate.com/github/yipdw/analysand
         | 
| 36 37 | 
             
            * Gemnasium: https://gemnasium.com/yipdw/analysand
         | 
| 37 38 |  | 
    
        data/analysand.gemspec
    CHANGED
    
    | @@ -6,7 +6,7 @@ Gem::Specification.new do |gem| | |
| 6 6 | 
             
              gem.email         = ["yipdw@member.fsf.org"]
         | 
| 7 7 | 
             
              gem.description   = %q{A terrible burden for a couch}
         | 
| 8 8 | 
             
              gem.summary       = %q{A CouchDB client of dubious worth}
         | 
| 9 | 
            -
              gem.homepage      = ""
         | 
| 9 | 
            +
              gem.homepage      = "https://github.com/yipdw/analysand"
         | 
| 10 10 |  | 
| 11 11 | 
             
              gem.files         = `git ls-files`.split($\)
         | 
| 12 12 | 
             
              gem.executables   = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
         | 
| @@ -15,10 +15,13 @@ Gem::Specification.new do |gem| | |
| 15 15 | 
             
              gem.require_paths = ["lib"]
         | 
| 16 16 | 
             
              gem.version       = Analysand::VERSION
         | 
| 17 17 |  | 
| 18 | 
            +
              gem.required_ruby_version = '>= 1.9'
         | 
| 19 | 
            +
             | 
| 18 20 | 
             
              gem.add_dependency 'celluloid', '>= 0.12'
         | 
| 19 21 | 
             
              gem.add_dependency 'celluloid-io'
         | 
| 20 22 | 
             
              gem.add_dependency 'http_parser.rb'
         | 
| 21 23 | 
             
              gem.add_dependency 'json'
         | 
| 24 | 
            +
              gem.add_dependency 'json-stream'
         | 
| 22 25 | 
             
              gem.add_dependency 'net-http-persistent'
         | 
| 23 26 | 
             
              gem.add_dependency 'rack'
         | 
| 24 27 | 
             
              gem.add_dependency 'yajl-ruby'
         | 
    
        data/bin/analysand
    CHANGED
    
    | @@ -2,13 +2,12 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
         | 
| 4 4 |  | 
| 5 | 
            -
            require 'analysand | 
| 5 | 
            +
            require 'analysand'
         | 
| 6 6 | 
             
            require 'irb'
         | 
| 7 7 | 
             
            require 'uri'
         | 
| 8 8 |  | 
| 9 9 | 
             
            $URI = URI('http://localhost:5984/analysand_test')
         | 
| 10 10 |  | 
| 11 | 
            -
             | 
| 12 11 | 
             
            def make_db(uri = $URI)
         | 
| 13 12 | 
             
              Analysand::Database.new(uri)
         | 
| 14 13 | 
             
            end
         | 
| @@ -21,7 +20,7 @@ Type make_db to make an Analysand::Database object.  The default URI is | |
| 21 20 |  | 
| 22 21 | 
             
            To point at different databases, supply a URI object to make_db, e.g.
         | 
| 23 22 |  | 
| 24 | 
            -
                make_db(URI('https://couchdb.example.org:6984/supersekrit')) | 
| 23 | 
            +
                make_db(URI('https://couchdb.example.org:6984/supersekrit'))
         | 
| 25 24 | 
             
            ------------------------------------------------------------------------------
         | 
| 26 25 | 
             
            END
         | 
| 27 26 |  | 
    
        data/lib/analysand.rb
    CHANGED
    
    
| @@ -7,9 +7,15 @@ module Analysand | |
| 7 7 | 
             
                ##
         | 
| 8 8 | 
             
                # Issues a HEAD request to the given URI.  If it responds with a success or
         | 
| 9 9 | 
             
                # redirection code, returns true; otherwise, returns false.
         | 
| 10 | 
            +
                #
         | 
| 11 | 
            +
                # If a block is given, yields the request object for customization.
         | 
| 10 12 | 
             
                def test_http_connection(uri)
         | 
| 11 13 | 
             
                  begin
         | 
| 12 | 
            -
                    resp = Net::HTTP.start(uri.host, uri.port)  | 
| 14 | 
            +
                    resp = Net::HTTP.start(uri.host, uri.port) do |h|
         | 
| 15 | 
            +
                      req = Net::HTTP::Head.new(uri.request_uri)
         | 
| 16 | 
            +
                      yield req if block_given?
         | 
| 17 | 
            +
                      h.request(req)
         | 
| 18 | 
            +
                    end
         | 
| 13 19 |  | 
| 14 20 | 
             
                    case resp
         | 
| 15 21 | 
             
                    when Net::HTTPSuccess then true
         | 
    
        data/lib/analysand/database.rb
    CHANGED
    
    | @@ -1,9 +1,11 @@ | |
| 1 | 
            -
            require 'analysand/bulk_response'
         | 
| 2 1 | 
             
            require 'analysand/errors'
         | 
| 2 | 
            +
            require 'analysand/reading'
         | 
| 3 3 | 
             
            require 'analysand/response'
         | 
| 4 | 
            -
            require 'analysand/ | 
| 4 | 
            +
            require 'analysand/viewing'
         | 
| 5 | 
            +
            require 'analysand/writing'
         | 
| 5 6 | 
             
            require 'net/http/persistent'
         | 
| 6 7 | 
             
            require 'rack/utils'
         | 
| 8 | 
            +
            require 'uri'
         | 
| 7 9 |  | 
| 8 10 | 
             
            module Analysand
         | 
| 9 11 | 
             
              ##
         | 
| @@ -141,15 +143,26 @@ module Analysand | |
| 141 143 | 
             
              #     vdb.view('video/recent', :key => ['member1'])
         | 
| 142 144 | 
             
              #     vdb.view('video/by_artist', :startkey => 'a', :endkey => 'b')
         | 
| 143 145 | 
             
              #
         | 
| 144 | 
            -
              # Keys are automatically JSON-encoded | 
| 145 | 
            -
              # | 
| 146 | 
            +
              # Keys are automatically JSON-encoded, as required by CouchDB.
         | 
| 147 | 
            +
              #
         | 
| 148 | 
            +
              # If you're running into problems with large key sets generating very long
         | 
| 149 | 
            +
              # query strings, you can use POST mode (CouchDB 0.9+):
         | 
| 150 | 
            +
              #
         | 
| 151 | 
            +
              #     vdb.view('video/by_artist', :keys => many_keys, :post => true)
         | 
| 152 | 
            +
              #
         | 
| 153 | 
            +
              # If you're reading many records from a view, you may want to stream them
         | 
| 154 | 
            +
              # in:
         | 
| 155 | 
            +
              #
         | 
| 156 | 
            +
              #     vdb.view('video/all', :stream => true)
         | 
| 157 | 
            +
              #
         | 
| 158 | 
            +
              # View data and metadata may be accessed as follows:
         | 
| 146 159 | 
             
              #
         | 
| 147 160 | 
             
              #     resp = vdb.view('video/recent', :limit => 10)
         | 
| 148 161 | 
             
              #     resp.total_rows   # => 16
         | 
| 149 162 | 
             
              #     resp.offset       # => 0
         | 
| 150 | 
            -
              #     resp.rows         # =>  | 
| 163 | 
            +
              #     resp.rows         # => an Enumerable
         | 
| 151 164 | 
             
              #
         | 
| 152 | 
            -
              # See ViewResponse for more details.
         | 
| 165 | 
            +
              # See ViewResponse and StreamingViewResponse for more details.
         | 
| 153 166 | 
             
              #
         | 
| 154 167 | 
             
              # You can also use view!, which will raise Analysand::CannotAccessView on a
         | 
| 155 168 | 
             
              # non-success response.
         | 
| @@ -251,8 +264,9 @@ module Analysand | |
| 251 264 | 
             
              # is also done.
         | 
| 252 265 | 
             
              class Database
         | 
| 253 266 | 
             
                include Rack::Utils
         | 
| 254 | 
            -
             | 
| 255 | 
            -
                 | 
| 267 | 
            +
                include Reading
         | 
| 268 | 
            +
                include Viewing
         | 
| 269 | 
            +
                include Writing
         | 
| 256 270 |  | 
| 257 271 | 
             
                attr_reader :http
         | 
| 258 272 | 
             
                attr_reader :uri
         | 
| @@ -268,21 +282,18 @@ module Analysand | |
| 268 282 | 
             
                def initialize(uri)
         | 
| 269 283 | 
             
                  raise InvalidURIError, 'You must supply an absolute URI' unless uri.absolute?
         | 
| 270 284 |  | 
| 271 | 
            -
                  @http = Net::HTTP::Persistent.new(' | 
| 285 | 
            +
                  @http = Net::HTTP::Persistent.new('analysand_database')
         | 
| 272 286 | 
             
                  @uri = uri
         | 
| 273 287 |  | 
| 274 | 
            -
                  #  | 
| 275 | 
            -
                  #  | 
| 288 | 
            +
                  # Document IDs and other database bits are appended to the URI path,
         | 
| 289 | 
            +
                  # so we need to make sure that it ends in a /.
         | 
| 276 290 | 
             
                  unless uri.path.end_with?('/')
         | 
| 277 291 | 
             
                    uri.path += '/'
         | 
| 278 292 | 
             
                  end
         | 
| 279 293 | 
             
                end
         | 
| 280 294 |  | 
| 281 295 | 
             
                def ping(credentials = nil)
         | 
| 282 | 
            -
                   | 
| 283 | 
            -
                  set_credentials(req, credentials)
         | 
| 284 | 
            -
             | 
| 285 | 
            -
                  Response.new(http.request(uri, req))
         | 
| 296 | 
            +
                  Response.new _get('', credentials)
         | 
| 286 297 | 
             
                end
         | 
| 287 298 |  | 
| 288 299 | 
             
                def status(credentials = nil)
         | 
| @@ -293,130 +304,8 @@ module Analysand | |
| 293 304 | 
             
                  http.shutdown
         | 
| 294 305 | 
             
                end
         | 
| 295 306 |  | 
| 296 | 
            -
                def put(doc_id, doc, credentials = nil, options = {})
         | 
| 297 | 
            -
                  query = options
         | 
| 298 | 
            -
                  headers = { 'Content-Type' => 'application/json' }
         | 
| 299 | 
            -
             | 
| 300 | 
            -
                  Response.new _put(doc_id, credentials, options, headers, doc.to_json)
         | 
| 301 | 
            -
                end
         | 
| 302 | 
            -
             | 
| 303 | 
            -
                def put!(doc_id, doc, credentials = nil, options = {})
         | 
| 304 | 
            -
                  put(doc_id, doc, credentials, options).tap do |resp|
         | 
| 305 | 
            -
                    raise ex(DocumentNotSaved, resp) unless resp.success?
         | 
| 306 | 
            -
                  end
         | 
| 307 | 
            -
                end
         | 
| 308 | 
            -
             | 
| 309 | 
            -
                def ensure_full_commit(credentials = nil, options = {})
         | 
| 310 | 
            -
                  headers = { 'Content-Type' => 'application/json' }
         | 
| 311 | 
            -
             | 
| 312 | 
            -
                  Response.new _post('_ensure_full_commit', credentials, options, headers, {}.to_json)
         | 
| 313 | 
            -
                end
         | 
| 314 | 
            -
             | 
| 315 | 
            -
                def bulk_docs(docs, credentials = nil, options = {})
         | 
| 316 | 
            -
                  headers = { 'Content-Type' => 'application/json' }
         | 
| 317 | 
            -
                  body = { 'docs' => docs }
         | 
| 318 | 
            -
                  body['all_or_nothing'] = true if options[:all_or_nothing]
         | 
| 319 | 
            -
             | 
| 320 | 
            -
                  BulkResponse.new _post('_bulk_docs', credentials, {}, headers, body.to_json)
         | 
| 321 | 
            -
                end
         | 
| 322 | 
            -
             | 
| 323 | 
            -
                def bulk_docs!(docs, credentials = nil, options = {})
         | 
| 324 | 
            -
                  bulk_docs(docs, credentials, options).tap do |resp|
         | 
| 325 | 
            -
                    raise ex(BulkOperationFailed, resp) unless resp.success?
         | 
| 326 | 
            -
                  end
         | 
| 327 | 
            -
                end
         | 
| 328 | 
            -
             | 
| 329 | 
            -
                def copy(source, destination, credentials = nil)
         | 
| 330 | 
            -
                  headers = { 'Destination' => destination }
         | 
| 331 | 
            -
             | 
| 332 | 
            -
                  Response.new _copy(source, credentials, {}, headers, nil)
         | 
| 333 | 
            -
                end
         | 
| 334 | 
            -
             | 
| 335 | 
            -
                def put_attachment(loc, io, credentials = nil, options = {})
         | 
| 336 | 
            -
                  query = {}
         | 
| 337 | 
            -
                  headers = {}
         | 
| 338 | 
            -
             | 
| 339 | 
            -
                  if options[:rev]
         | 
| 340 | 
            -
                    query['rev'] = options[:rev]
         | 
| 341 | 
            -
                  end
         | 
| 342 | 
            -
             | 
| 343 | 
            -
                  if options[:content_type]
         | 
| 344 | 
            -
                    headers['Content-Type'] = options[:content_type]
         | 
| 345 | 
            -
                  end
         | 
| 346 | 
            -
             | 
| 347 | 
            -
                  Response.new _put(loc, credentials, query, headers, io.read)
         | 
| 348 | 
            -
                end
         | 
| 349 | 
            -
             | 
| 350 | 
            -
                def delete(doc_id, rev, credentials = nil)
         | 
| 351 | 
            -
                  headers = { 'If-Match' => rev }
         | 
| 352 | 
            -
             | 
| 353 | 
            -
                  Response.new _delete(doc_id, credentials, {}, headers, nil)
         | 
| 354 | 
            -
                end
         | 
| 355 | 
            -
             | 
| 356 | 
            -
                def delete!(doc_id, rev, credentials = nil)
         | 
| 357 | 
            -
                  delete(doc_id, rev, credentials).tap do |resp|
         | 
| 358 | 
            -
                    raise ex(DocumentNotDeleted, resp) unless resp.success?
         | 
| 359 | 
            -
                  end
         | 
| 360 | 
            -
                end
         | 
| 361 | 
            -
             | 
| 362 | 
            -
                def get(doc_id, credentials = nil)
         | 
| 363 | 
            -
                  Response.new(_get(doc_id, credentials))
         | 
| 364 | 
            -
                end
         | 
| 365 | 
            -
             | 
| 366 | 
            -
                def get!(doc_id, credentials = nil)
         | 
| 367 | 
            -
                  get(doc_id, credentials).tap do |resp|
         | 
| 368 | 
            -
                    raise ex(CannotAccessDocument, resp) unless resp.success?
         | 
| 369 | 
            -
                  end
         | 
| 370 | 
            -
                end
         | 
| 371 | 
            -
             | 
| 372 | 
            -
                def head(doc_id, credentials = nil)
         | 
| 373 | 
            -
                  Response.new(_head(doc_id, credentials))
         | 
| 374 | 
            -
                end
         | 
| 375 | 
            -
             | 
| 376 | 
            -
                def get_attachment(loc, credentials = nil)
         | 
| 377 | 
            -
                  _get(loc, credentials)
         | 
| 378 | 
            -
                end
         | 
| 379 | 
            -
             | 
| 380 | 
            -
                def all_docs(parameters = {}, credentials = nil)
         | 
| 381 | 
            -
                  view('_all_docs', parameters, credentials)
         | 
| 382 | 
            -
                end
         | 
| 383 | 
            -
             | 
| 384 | 
            -
                def all_docs!(parameters = {}, credentials = nil)
         | 
| 385 | 
            -
                  view!('_all_docs', parameters, credentials)
         | 
| 386 | 
            -
                end
         | 
| 387 | 
            -
             | 
| 388 | 
            -
                def view(view_name, parameters = {}, credentials = nil)
         | 
| 389 | 
            -
                  view_path = expand_view_path(view_name)
         | 
| 390 | 
            -
             | 
| 391 | 
            -
                  JSON_VALUE_PARAMETERS.each do |p|
         | 
| 392 | 
            -
                    if parameters.has_key?(p)
         | 
| 393 | 
            -
                      parameters[p] = parameters[p].to_json
         | 
| 394 | 
            -
                    end
         | 
| 395 | 
            -
                  end
         | 
| 396 | 
            -
             | 
| 397 | 
            -
                  ViewResponse.new _get(view_path, credentials, parameters, {})
         | 
| 398 | 
            -
                end
         | 
| 399 | 
            -
             | 
| 400 | 
            -
                def expand_view_path(view_name)
         | 
| 401 | 
            -
                  if view_name.include?('/')
         | 
| 402 | 
            -
                    design_doc, view_name = view_name.split('/', 2)
         | 
| 403 | 
            -
                    "_design/#{design_doc}/_view/#{view_name}"
         | 
| 404 | 
            -
                  else
         | 
| 405 | 
            -
                    view_name
         | 
| 406 | 
            -
                  end
         | 
| 407 | 
            -
                end
         | 
| 408 | 
            -
             | 
| 409 | 
            -
                def view!(view_name, parameters = {}, credentials = nil)
         | 
| 410 | 
            -
                  view(view_name, parameters, credentials).tap do |resp|
         | 
| 411 | 
            -
                    raise ex(CannotAccessView, resp) unless resp.success?
         | 
| 412 | 
            -
                  end
         | 
| 413 | 
            -
                end
         | 
| 414 | 
            -
             | 
| 415 307 | 
             
                def create(credentials = nil)
         | 
| 416 | 
            -
                   | 
| 417 | 
            -
                  set_credentials(req, credentials)
         | 
| 418 | 
            -
             | 
| 419 | 
            -
                  Response.new(http.request(uri, req))
         | 
| 308 | 
            +
                  Response.new _put('', credentials)
         | 
| 420 309 | 
             
                end
         | 
| 421 310 |  | 
| 422 311 | 
             
                def create!(credentials = nil)
         | 
| @@ -426,10 +315,7 @@ module Analysand | |
| 426 315 | 
             
                end
         | 
| 427 316 |  | 
| 428 317 | 
             
                def drop(credentials = nil)
         | 
| 429 | 
            -
                   | 
| 430 | 
            -
                  set_credentials(req, credentials)
         | 
| 431 | 
            -
             | 
| 432 | 
            -
                  Response.new(http.request(uri, req))
         | 
| 318 | 
            +
                  Response.new _delete('', credentials)
         | 
| 433 319 | 
             
                end
         | 
| 434 320 |  | 
| 435 321 | 
             
                def drop!(credentials = nil)
         | 
| @@ -440,8 +326,8 @@ module Analysand | |
| 440 326 |  | 
| 441 327 | 
             
                %w(Head Get Put Post Delete Copy).each do |m|
         | 
| 442 328 | 
             
                  str = <<-END
         | 
| 443 | 
            -
                    def _#{m.downcase}(doc_id, credentials, query = {}, headers = {}, body = nil)
         | 
| 444 | 
            -
                      _req(Net::HTTP::#{m}, doc_id, credentials, query, headers, body)
         | 
| 329 | 
            +
                    def _#{m.downcase}(doc_id, credentials, query = {}, headers = {}, body = nil, block = nil)
         | 
| 330 | 
            +
                      _req(Net::HTTP::#{m}, doc_id, credentials, query, headers, body, block)
         | 
| 445 331 | 
             
                    end
         | 
| 446 332 | 
             
                  END
         | 
| 447 333 |  | 
| @@ -450,8 +336,9 @@ module Analysand | |
| 450 336 |  | 
| 451 337 | 
             
                ##
         | 
| 452 338 | 
             
                # @private
         | 
| 453 | 
            -
                def _req(klass, doc_id, credentials, query, headers, body)
         | 
| 454 | 
            -
                  uri =  | 
| 339 | 
            +
                def _req(klass, doc_id, credentials, query, headers, body, block)
         | 
| 340 | 
            +
                  uri = self.uri.dup
         | 
| 341 | 
            +
                  uri.path += URI.escape(doc_id)
         | 
| 455 342 | 
             
                  uri.query = build_query(query) unless query.empty?
         | 
| 456 343 |  | 
| 457 344 | 
             
                  req = klass.new(uri.request_uri)
         | 
| @@ -460,7 +347,7 @@ module Analysand | |
| 460 347 | 
             
                  req.body = body if body && req.request_body_permitted?
         | 
| 461 348 | 
             
                  set_credentials(req, credentials)
         | 
| 462 349 |  | 
| 463 | 
            -
                  http.request(uri, req)
         | 
| 350 | 
            +
                  http.request(uri, req, &block)
         | 
| 464 351 | 
             
                end
         | 
| 465 352 |  | 
| 466 353 | 
             
                ##
         | 
| @@ -479,6 +366,10 @@ module Analysand | |
| 479 366 | 
             
                  end
         | 
| 480 367 | 
             
                end
         | 
| 481 368 |  | 
| 369 | 
            +
                def json_headers
         | 
| 370 | 
            +
                  { 'Content-Type' => 'application/json' }
         | 
| 371 | 
            +
                end
         | 
| 372 | 
            +
             | 
| 482 373 | 
             
                ##
         | 
| 483 374 | 
             
                # @private
         | 
| 484 375 | 
             
                def ex(klass, response)
         | 
    
        data/lib/analysand/errors.rb
    CHANGED
    
    
| @@ -0,0 +1,26 @@ | |
| 1 | 
            +
            require 'analysand/errors'
         | 
| 2 | 
            +
            require 'analysand/response'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Analysand
         | 
| 5 | 
            +
              module Reading
         | 
| 6 | 
            +
                def get(doc_id, credentials = nil)
         | 
| 7 | 
            +
                  Response.new(_get(doc_id, credentials))
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                def get!(doc_id, credentials = nil)
         | 
| 11 | 
            +
                  get(doc_id, credentials).tap do |resp|
         | 
| 12 | 
            +
                    raise ex(CannotAccessDocument, resp) unless resp.success?
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def head(doc_id, credentials = nil)
         | 
| 17 | 
            +
                  Response.new(_head(doc_id, credentials))
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def get_attachment(loc, credentials = nil)
         | 
| 21 | 
            +
                  _get(loc, credentials)
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            # vim:ts=2:sw=2:et:tw=78
         | 
| @@ -0,0 +1,100 @@ | |
| 1 | 
            +
            require 'analysand/view_streaming/builder'
         | 
| 2 | 
            +
            require 'fiber'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Analysand
         | 
| 5 | 
            +
              # Public: Controls streaming of view data.
         | 
| 6 | 
            +
              #
         | 
| 7 | 
            +
              # This class is meant to be used by Analysand::Database#view.  It exports the
         | 
| 8 | 
            +
              # same interface as ViewResponse.
         | 
| 9 | 
            +
              #
         | 
| 10 | 
            +
              # Examples:
         | 
| 11 | 
            +
              #
         | 
| 12 | 
            +
              #     resp = db.view('view/something', :stream => true)
         | 
| 13 | 
            +
              #
         | 
| 14 | 
            +
              #     resp.total_rows       # => 1000000
         | 
| 15 | 
            +
              #     resp.offset           # => 0
         | 
| 16 | 
            +
              #     resp.rows.take(100)   # => first 100 rows
         | 
| 17 | 
            +
              class StreamingViewResponse
         | 
| 18 | 
            +
                include Enumerable
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                # Private: The HTTP response.
         | 
| 21 | 
            +
                #
         | 
| 22 | 
            +
                # This is set by Analysand::Database#stream_view.  The #etag and #code
         | 
| 23 | 
            +
                # methods use this for header information.
         | 
| 24 | 
            +
                attr_accessor :http_response
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def initialize
         | 
| 27 | 
            +
                  @reader = Fiber.new { yield self; "" }
         | 
| 28 | 
            +
                  @generator = ViewStreaming::Builder.new
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  # Analysand::Database#stream_view issues the request.  When the response
         | 
| 31 | 
            +
                  # arrives, it yields control back here.  Subsequent resumes read the
         | 
| 32 | 
            +
                  # body.
         | 
| 33 | 
            +
                  #
         | 
| 34 | 
            +
                  # We do this to provide the response headers as soon as possible.
         | 
| 35 | 
            +
                  @reader.resume
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                def etag
         | 
| 39 | 
            +
                  http_response.get_fields('ETag').first.gsub('"', '')
         | 
| 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 code
         | 
| 59 | 
            +
                  http_response.code
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                def success?
         | 
| 63 | 
            +
                  c = code.to_i
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  c >= 200 && c <= 299
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                def total_rows
         | 
| 69 | 
            +
                  read until @generator.total_rows
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  @generator.total_rows
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                def offset
         | 
| 75 | 
            +
                  read until @generator.offset
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  @generator.offset
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                def read
         | 
| 81 | 
            +
                  @generator << @reader.resume
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                def each
         | 
| 85 | 
            +
                  return to_enum unless block_given?
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                  while @reader.alive?
         | 
| 88 | 
            +
                    read while @reader.alive? && @generator.staged_rows.empty?
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                    until @generator.staged_rows.empty?
         | 
| 91 | 
            +
                      yield @generator.staged_rows.shift
         | 
| 92 | 
            +
                    end
         | 
| 93 | 
            +
                  end
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                def rows
         | 
| 97 | 
            +
                  self
         | 
| 98 | 
            +
                end
         | 
| 99 | 
            +
              end
         | 
| 100 | 
            +
            end
         |