couchbase 0.9.7

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.
@@ -0,0 +1,57 @@
1
+ # Author:: Couchbase <info@couchbase.com>
2
+ # Copyright:: 2011 Couchbase, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ module Couchbase
19
+ autoload :Bucket, 'couchbase/bucket'
20
+ autoload :Couchdb, 'couchbase/couchdb'
21
+ autoload :Document, 'couchbase/document'
22
+ autoload :HttpStatus, 'couchbase/http_status'
23
+ autoload :Latch, 'couchbase/latch'
24
+ autoload :Memcached, 'couchbase/memcached'
25
+ autoload :Node, 'couchbase/node'
26
+ autoload :RestClient, 'couchbase/rest_client'
27
+ autoload :VERSION, 'couchbase/version'
28
+ autoload :View, 'couchbase/view'
29
+
30
+ # This error is raising when library detects that some operation
31
+ # doesn't implemented by the server. For example views API doesn't
32
+ # implemented by Membase 1.7.x
33
+ class NotImplemented < Exception; end
34
+
35
+ class ViewError < Exception
36
+ attr_reader :from, :reason
37
+
38
+ def initialize(from, reason)
39
+ @from = from
40
+ @reason = reason
41
+ super("#{from}: #{reason}")
42
+ end
43
+ end
44
+
45
+ class << self
46
+ # The method +new+ initializes new Bucket instance with all arguments passed.
47
+ #
48
+ # === Examples
49
+ # Couchbase.new("http://localhost:8091/pools/default") #=> establish connection with couchbase default pool and default bucket
50
+ # Couchbase.new("http://localhost:8091/pools/default", :bucket_name => 'blog') #=> select custom bucket
51
+ # Couchbase.new("http://localhost:8091/pools/default", :bucket_name => 'blog', :bucket_password => 'secret') #=> specify password for bucket (and SASL auth for memcached client)
52
+ def new(*args)
53
+ Bucket.new(*args)
54
+ end
55
+ end
56
+
57
+ end
@@ -0,0 +1,138 @@
1
+ # Author:: Couchbase <info@couchbase.com>
2
+ # Copyright:: 2011 Couchbase, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'uri'
19
+
20
+ module Couchbase
21
+
22
+ # This class in charge of all stuff connected to communication with
23
+ # Couchbase. It includes CouchDB and Memcached APIs. Also it includes
24
+ # methods for HTTP transport from RestClient.
25
+
26
+ class Bucket
27
+ include RestClient
28
+ include Couchdb
29
+ include Memcached
30
+
31
+ attr_accessor :pool_uri, :environment, :type, :nodes,
32
+ :streaming_uri, :name, :uri, :vbuckets
33
+
34
+ # Initializes connection using +pool_uri+ and optional
35
+ # +:bucket_name+ and +:bucket_password+ (for protected buckets). Bucket
36
+ # name will be used as a username for all authorizations (SASL for
37
+ # Memcached API and Basic for HTTP). It also accepts +:environment+
38
+ # parameter wich intended to let library know what mode it should
39
+ # use when it applicable (for example it skips/preserves design
40
+ # documents with 'dev_' prefix for CouchDB API). You can specify
41
+ # any string starting from 'dev' or 'test' to activate development
42
+ # mode.
43
+ #
44
+ # Also starts thread which will simultanuously listen for
45
+ # configuration update via +streaming_uri+. Server should push
46
+ # update notification about adding or removing nodes from cluster.
47
+ #
48
+ # Raises ArgumentError when it cannot find specified bucket in given
49
+ # pool.
50
+ def initialize(pool_uri, options = {})
51
+ @latch = Latch.new(:in_progress, :ready)
52
+ @name = options[:bucket_name] || "default"
53
+ @pool_uri = URI.parse(pool_uri)
54
+ @environment = if options[:environment].to_s =~ /^(dev|test)/
55
+ :development
56
+ else
57
+ :production
58
+ end
59
+ config = http_get("#{@pool_uri}/buckets").detect do |bucket|
60
+ bucket['name'] == @name
61
+ end
62
+ unless config
63
+ raise ArgumentError,
64
+ "There no such bucket with name '#{@name}' in pool #{pool_uri}"
65
+ end
66
+ @uri = @pool_uri.merge(config['uri'])
67
+ @streaming_uri = @pool_uri.merge(config['streamingUri'])
68
+ if password = options[:bucket_password]
69
+ @credentials = {:username => @name, :password => password}
70
+ end
71
+ super
72
+
73
+ # Considering all initialization stuff completed and now we can
74
+ # start config listener
75
+ listen_for_config_changes
76
+
77
+ @latch.wait
78
+ end
79
+
80
+ # Select next node for work with Couchbase. Currently it makes sense
81
+ # only for couchdb API, because memcached client works using moxi.
82
+ def next_node
83
+ nodes.shuffle.first
84
+ end
85
+
86
+ # Perform configuration using configuration cache. It turn all URIs
87
+ # into full form (with schema, host and port).
88
+ #
89
+ # You can override this method in included modules or descendants if
90
+ # you'd like to reconfigure them when new configuration arrives from
91
+ # server.
92
+ def setup(config)
93
+ @type = config['bucketType']
94
+ @nodes = config['nodes'].map do |node|
95
+ Node.new(node['status'],
96
+ node['hostname'].split(':').first,
97
+ node['ports'],
98
+ node['couchApiBase'])
99
+ end
100
+ if @type == 'membase'
101
+ @vbuckets = config['vBucketServerMap']['vBucketMap']
102
+ end
103
+ super
104
+ @latch.toggle
105
+ end
106
+
107
+ private
108
+
109
+ # Run background thread to listen for configuration changes.
110
+ # Rewrites configuration for each update. Curl::Multi uses select()
111
+ # call when waiting for data, so is should be efficient use ruby
112
+ # threads here.
113
+ def listen_for_config_changes
114
+ Thread.new do
115
+ multi = Curl::Multi.new
116
+ multi.add(mk_curl(@streaming_uri.to_s))
117
+ multi.perform
118
+ end
119
+ end
120
+
121
+ def mk_curl(url)
122
+ Curl::Easy.new(url) do |curl|
123
+ curl.useragent = "couchbase-ruby-client/#{Couchbase::VERSION}"
124
+ if @credentials
125
+ curl.http_auth_types = :basic
126
+ curl.username = @credentials[:username]
127
+ curl.password = @credentials[:password]
128
+ end
129
+ curl.verbose = true if Kernel.respond_to?(:debugger)
130
+ curl.on_body do |data|
131
+ config = Yajl::Parser.parse(data)
132
+ setup(config) if config
133
+ data.bytesize
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,107 @@
1
+ # Author:: Couchbase <info@couchbase.com>
2
+ # Copyright:: 2011 Couchbase, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ module Couchbase
19
+ module Couchdb
20
+
21
+ # Initializes CouchDB related part of connection.
22
+ #
23
+ # @param [ String ] pool_uri Couchbase pool URI.
24
+ #
25
+ # @param [ Hash ] options Connection options. This is the hash the client
26
+ # passed to Couchbase.new to start the session
27
+ def initialize(pool_uri, options = {})
28
+ super
29
+ end
30
+
31
+ # Fetch design docs stored in current bucket
32
+ #
33
+ # @return [ Hash ]
34
+ def design_docs
35
+ docs = http_get("#{next_node.couch_api_base}/_all_docs",
36
+ :params => {:startkey => "_design/", :endkey => "_design0", :include_docs => true})
37
+ result = {}
38
+ docs['rows'].each do |doc|
39
+ doc = Document.wrap(self, doc)
40
+ key = doc['_id'].sub(/^_design\//, '')
41
+ next if @environment == :production && key =~ /dev_/
42
+ result[key] = doc
43
+ end
44
+ result
45
+ end
46
+
47
+ # Update or create design doc with supplied views
48
+ #
49
+ # @param [ Hash, IO, String ] data The source object containing JSON
50
+ # encoded design document. It must have
51
+ # <tt>_id</tt> key set, this key should
52
+ # start with <tt>_design/</tt>.
53
+ #
54
+ # @return [ Couchbase::Document ] instance
55
+ def save_design_doc(data)
56
+ doc = parse_design_document(data)
57
+ rv = http_put("#{next_node.couch_api_base}/#{doc['_id']}", {}, doc)
58
+ doc['_rev'] = rv['rev']
59
+ doc
60
+ end
61
+
62
+ # Fetch all documents from the bucket.
63
+ #
64
+ # @param [ Hash ] params Params for CouchDB <tt>/_all_docs</tt> query
65
+ #
66
+ # @return [ Couchbase::View ] View object
67
+ def all_docs(params = {})
68
+ View.new(self, "#{next_node.couch_api_base}/_all_docs", params)
69
+ end
70
+
71
+ # Delete design doc with given id and revision.
72
+ #
73
+ # @param [ String ] id Design document id. It might have '_design/'
74
+ # prefix.
75
+ #
76
+ # @param [ String ] rev Document revision. It uses latest revision if
77
+ # <tt>rev</tt> parameter is nil.
78
+ #
79
+ def delete_design_doc(id, rev = nil)
80
+ ddoc = design_docs[id.sub(/^_design\//, '')]
81
+ return nil unless ddoc
82
+ http_delete("#{next_node.couch_api_base}/#{ddoc['_id']}",
83
+ :params => {:rev => rev || ddoc['_rev']})
84
+ end
85
+
86
+ protected
87
+
88
+ def parse_design_document(doc)
89
+ data = case doc
90
+ when String
91
+ Yajl::Parser.parse(doc)
92
+ when IO
93
+ Yajl::Parser.parse(doc.read)
94
+ when Hash
95
+ doc
96
+ else
97
+ raise ArgumentError, "Document should be Hash, String or IO instance"
98
+ end
99
+
100
+ if data['_id'].to_s !~ /^_design\//
101
+ raise ArgumentError, "'_id' key must be set and start with '_design/'."
102
+ end
103
+ data['language'] ||= 'javascript'
104
+ Document.wrap(self, data)
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,71 @@
1
+ # Author:: Couchbase <info@couchbase.com>
2
+ # Copyright:: 2011 Couchbase, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ module Couchbase
19
+ class Document
20
+ # Undefine as much methods as we can to free names for views
21
+ instance_methods.each do |m|
22
+ undef_method(m) if m.to_s !~ /(?:^__|^nil\?$|^send$|^object_id$|^class$|)/
23
+ end
24
+
25
+ attr_accessor :data, :views
26
+
27
+ def initialize(connection, data)
28
+ @data = data
29
+ @connection = connection
30
+ @views = []
31
+ begin
32
+ if design_doc?
33
+ data['views'].each do |name, funs|
34
+ @views << name
35
+ self.instance_eval <<-EOV, __FILE__, __LINE__ + 1
36
+ def #{name}(params = {})
37
+ endpoint = "\#{@connection.next_node.couch_api_base}/\#{@data['_id']}/_view/#{name}"
38
+ View.new(@connection, endpoint, params)
39
+ end
40
+ EOV
41
+ end
42
+ end
43
+ rescue NoMethodError
44
+ end
45
+ end
46
+
47
+ def self.wrap(connection, data)
48
+ Document.new(connection, data['doc'] || data)
49
+ end
50
+
51
+ def [](key)
52
+ @data[key]
53
+ end
54
+
55
+ def []=(key, value)
56
+ @data[key] = value
57
+ end
58
+
59
+ def design_doc?
60
+ !!(@data['_id'] =~ %r(_design/))
61
+ end
62
+
63
+ def has_views?
64
+ !!(design_doc? && !@views.empty?)
65
+ end
66
+
67
+ def inspect
68
+ %(#<#{self.class.name}:#{self.object_id} #{@data.inspect}>)
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,118 @@
1
+ # Author:: Couchbase <info@couchbase.com>
2
+ # Copyright:: 2011 Couchbase, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ module Couchbase
19
+
20
+ # This module contains definitions for exceptions which wrap HTTP
21
+ # errors. Also it collect them in useful structure
22
+ # +HttpStatus::Errors+ which is map with status code as a key and
23
+ # exception class as a value. See the usage example below.
24
+ #
25
+ # === Example
26
+ #
27
+ # data = Yajl::Parser.parse(curl.body_str)
28
+ # if error_class = HttpStatus::Errors[curl.response_code]
29
+ # raise error_class.new(data['error'], data['reason'])
30
+ # end
31
+ #
32
+
33
+ module HttpStatus
34
+
35
+ # Base class for all HTTP error codes. It povides handy methods for
36
+ # investigating what happened. For example, it stores +status_code+
37
+ # and human readable +status_message+ automatically and allows to
38
+ # provide context specific code and message (+error+ and +reason+
39
+ # correspondingly)
40
+
41
+ class Status < Exception
42
+ class << self
43
+ attr_accessor :status_code, :status_message
44
+ alias_method :to_i, :status_code
45
+
46
+ def status_line
47
+ "#{status_code} #{status_message}"
48
+ end
49
+ end
50
+
51
+ attr_reader :error, :reason
52
+ def initialize(error, reason)
53
+ @error = error
54
+ @reason = reason
55
+ super("#{error}: #{reason}")
56
+ end
57
+
58
+ def status_code
59
+ self.class.status_code
60
+ end
61
+
62
+ def status_message
63
+ self.class.status_message
64
+ end
65
+
66
+ def status_line
67
+ self.class.status_line
68
+ end
69
+
70
+ def to_i
71
+ self.class.to_i
72
+ end
73
+ end
74
+
75
+ StatusMessage = {
76
+ 400 => 'Bad Request',
77
+ 401 => 'Unauthorized',
78
+ 402 => 'Payment Required',
79
+ 403 => 'Forbidden',
80
+ 404 => 'Not Found',
81
+ 405 => 'Method Not Allowed',
82
+ 406 => 'Not Acceptable',
83
+ 407 => 'Proxy Authentication Required',
84
+ 408 => 'Request Timeout',
85
+ 409 => 'Conflict',
86
+ 410 => 'Gone',
87
+ 411 => 'Length Required',
88
+ 412 => 'Precondition Failed',
89
+ 413 => 'Request Entity Too Large',
90
+ 414 => 'Request-URI Too Large',
91
+ 415 => 'Unsupported Media Type',
92
+ 416 => 'Request Range Not Satisfiable',
93
+ 417 => 'Expectation Failed',
94
+ 422 => 'Unprocessable Entity',
95
+ 423 => 'Locked',
96
+ 424 => 'Failed Dependency',
97
+ 500 => 'Internal Server Error',
98
+ 501 => 'Not Implemented',
99
+ 502 => 'Bad Gateway',
100
+ 503 => 'Service Unavailable',
101
+ 504 => 'Gateway Timeout',
102
+ 505 => 'HTTP Version Not Supported',
103
+ 507 => 'Insufficient Storage'
104
+ }
105
+
106
+ # Hash with error code as a key and exceptin class as a value
107
+ Errors = {}
108
+
109
+ StatusMessage.each do |status_code, status_message|
110
+ klass = Class.new(Status)
111
+ klass.status_code = status_code
112
+ klass.status_message = status_message
113
+ klass_name = status_message.gsub(/[ \-]/,'')
114
+ const_set(klass_name, klass)
115
+ HttpStatus::Errors[status_code] = const_get(klass_name)
116
+ end
117
+ end
118
+ end