couchbase 0.9.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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