dm-couchdb-adapter 0.10.2

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1 @@
1
+ source "http://rubygems.org"
data/History.txt ADDED
@@ -0,0 +1,33 @@
1
+ === 0.9.10 / 2009-01-19
2
+
3
+ * 2 minor enhancements:
4
+
5
+ * Confirmed upload feature working
6
+ * Implemented multi-key fetch
7
+ * Return a Hash instead of Struct increasing performance
8
+
9
+ * 4 bug fixes:
10
+
11
+ * Fixed conflict with to_json from dm-serializer
12
+ * Stop escaping the slash in auto_migrate to match new CouchDB
13
+ behavior
14
+ * Fixed lazy evaluation of views
15
+ * Internal fixes for CouchDB r731863
16
+
17
+ === 0.9.9 / 2009-01-04
18
+
19
+ * 1 bug fix:
20
+
21
+ * Escape the slash in destroy_model_storage
22
+
23
+ === 0.9.8 / 2008-12-07
24
+
25
+ * 1 minor enhancement:
26
+
27
+ * Correct parsing of a params hash
28
+
29
+ * 3 bug fixes:
30
+
31
+ * Correct escaping of view and attachment URLs
32
+ * couch adapter name no longer mandated
33
+ * Correct content type now sent
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Kabari Hendrick
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Manifest.txt ADDED
@@ -0,0 +1,21 @@
1
+ .gitignore
2
+ History.txt
3
+ LICENSE
4
+ Manifest.txt
5
+ README.txt
6
+ Rakefile
7
+ TODO
8
+ lib/couchdb_adapter.rb
9
+ lib/couchdb_adapter/attachments.rb
10
+ lib/couchdb_adapter/couch_resource.rb
11
+ lib/couchdb_adapter/json_object.rb
12
+ lib/couchdb_adapter/version.rb
13
+ lib/couchdb_adapter/view.rb
14
+ spec/couchdb_adapter_spec.rb
15
+ spec/couchdb_attachments_spec.rb
16
+ spec/couchdb_view_spec.rb
17
+ spec/spec.opts
18
+ spec/spec_helper.rb
19
+ spec/testfile.txt
20
+ tasks/install.rb
21
+ tasks/spec.rb
data/README.rdoc ADDED
@@ -0,0 +1,68 @@
1
+ This is a datamapper adapter to couchdb.
2
+
3
+ NOTE: some functionality and their specs are based on functionality that is in
4
+ edge couch but not in stable. If you want everything to work, use edge.
5
+ Otherwise, your milage may vary. Good luck and let me know about any bugs.
6
+
7
+ == Setup
8
+ Install with the rest of the dm-more package, using:
9
+ gem install dm-more
10
+
11
+ Setting up:
12
+ The easiest way is to pass a full url, here is an example:
13
+ "couchdb://localhost:5984/my_app_development"
14
+
15
+ You can break it out like this:
16
+ "#{adapter}://#{host}:#{port}/#{database}"
17
+ - adapter should be :couchdb
18
+ - database (should be the name of your database)
19
+ - host (probably localhost)
20
+ - port should be specified (couchdb defaults to port 5984)
21
+
22
+ If you haven't you'll need to create this database.
23
+ The easiest way is with curl in the terminal, like so:
24
+ 'curl -X PUT localhost:5984/my_app_development'
25
+ You should use the same address here as you did to connect (just leave out the 'couchdb://' part)
26
+
27
+ Now, if you want to have a model stored in couch you can just use:
28
+ include DataMapper::CouchResource
29
+ instead of the normal:
30
+ include DataMapper::Resource
31
+
32
+ This adds the following reserved properties (which have special meaning in Couch, so don't overwrite them):
33
+ property :id, String, :key => true, :field => '_id'
34
+ property :rev, String, :field => '_rev'
35
+ property :attachments, DataMapper::Types::JsonObject, :field => '_attachments'
36
+
37
+ If you want the model to use your couch repository by default, be sure to also add the following(replacing :couch with your repository name):
38
+ def self.default_repository_name
39
+ :couch
40
+ end
41
+
42
+ You should now be able to use resources and their properties and have them stored to couchdb.
43
+ NOTE: 'couchdb_type' is a reserved property, used to map documents to their ruby models.
44
+
45
+ == Views
46
+ Special consideration has been made to work with CouchDB views.
47
+ You should do ALL queries you'll be repeating this way, doing 'User.all(:something => 'this)' will work, but it is much slower and more inefficient than running views you already created.
48
+ You define them in the model with the view function and use Model.auto_migrate! to add the views for that Model to the database, or DataMapper.auto_migrate! to add the views for all models to the database.
49
+
50
+ An example class with views:
51
+
52
+ class User
53
+ include DataMapper::Resource
54
+
55
+ property :name, String
56
+ view(:by_name_only_this_model) {{ "map" => "function(doc) { if (doc.couchdb_type == 'User') { emit(doc.name, doc); } }" }}
57
+ view(:by_name_with_descendants) {{ "map" => "function(doc) { if (#{couchdb_types_condition}) { emit(doc.name, doc); } }" }}
58
+ end
59
+
60
+ couchdb_types_condition builds a condition for you if you want a view that checks to see if the couchdb_type of the record is that of the current model or any of its descendants, just load your models and run Model.couchdb_types_condition and copy/paste the output as the condition in the models view. I will be making this smoother/cleaner, as I need to reimplement view handling.
61
+
62
+ You could then call User.by_name to get a listing of users ordered by name, or pass a key to try and find a specific user by their name, ie User.by_name(:key => 'username').
63
+
64
+ # TODO: add details about other view options
65
+
66
+ == Example
67
+ For a working example of this functionality checkout muddle, my merb based tumblelog, which uses this adapter to save its posts, at:
68
+ http://github.com/geemus/muddle
data/Rakefile ADDED
@@ -0,0 +1,65 @@
1
+ require 'pathname'
2
+ require 'rubygems'
3
+ require "rake"
4
+
5
+ ROOT = Pathname(__FILE__).dirname.expand_path
6
+ JRUBY = RUBY_PLATFORM =~ /java/
7
+ WINDOWS = Gem.win_platform?
8
+ SUDO = (WINDOWS || JRUBY) ? '' : ('sudo' unless ENV['SUDOLESS'])
9
+
10
+ require ROOT + 'lib/couchdb_adapter/version'
11
+
12
+ GEM_NAME = 'dm-couchdb-adapter'
13
+ GEM_VERSION = DataMapper::CouchDBAdapter::VERSION
14
+ GEM_DEPENDENCIES = [['dm-core', "~>#{GEM_VERSION}"], ['mime-types', '~>1.15']]
15
+ GEM_CLEAN = %w[ log pkg coverage ]
16
+ GEM_EXTRAS = { :has_rdoc => true, :extra_rdoc_files => %w[ README.txt LICENSE TODO History.txt ] }
17
+
18
+
19
+
20
+ begin
21
+ require 'jeweler'
22
+ Jeweler::Tasks.new do |gem|
23
+ gem.name = GEM_NAME
24
+ gem.summary = %Q{CouchDB Adapter for DataMapper}
25
+ gem.email = 'kabari [a] gmail [d] com'
26
+ gem.homepage = "http://github.com/kabari/#{GEM_NAME}/tree/master"
27
+ gem.authors = ["Kabari Hendrick"]
28
+ # gem is a Gem::Specification... see
29
+ # for additional settings
30
+ gem.required_ruby_version = '>= 1.8.6'
31
+ gem.add_dependency("extlib", ">= 0.9.11")
32
+ gem.add_dependency('mime-types', '~>1.15')
33
+ end
34
+ Jeweler::GemcutterTasks.new
35
+ rescue LoadError
36
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
37
+ end
38
+
39
+ require 'spec/rake/spectask'
40
+ Spec::Rake::SpecTask.new(:spec) do |spec|
41
+ spec.libs << 'lib' << 'spec'
42
+ spec.spec_files = FileList['spec/**/*_spec.rb']
43
+ spec.fail_on_error = false
44
+ end
45
+
46
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
47
+ spec.libs << 'lib' << 'spec'
48
+ spec.pattern = 'spec/**/*_spec.rb'
49
+ spec.rcov = true
50
+ end
51
+
52
+
53
+ task :default => :spec
54
+
55
+ require 'rake/rdoctask'
56
+ Rake::RDocTask.new do |rdoc|
57
+ rdoc.rdoc_dir = 'rdoc'
58
+ rdoc.title = "#{GEM_NAME} #{GEM_VERSION}"
59
+ rdoc.rdoc_files.include('README*')
60
+ rdoc.rdoc_files.include("LICENSE")
61
+ rdoc.rdoc_files.include("TODO")
62
+ rdoc.rdoc_files.include("History.txt")
63
+ rdoc.rdoc_files.include('lib/**/*.rb')
64
+ end
65
+
data/TODO ADDED
File without changes
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.10.2
@@ -0,0 +1,165 @@
1
+ module DataMapper
2
+ module Adapters
3
+ class CouchDBAdapter < AbstractAdapter
4
+ ConnectionError = Class.new(StandardError)
5
+
6
+ # Persists one or many new resources
7
+ #
8
+ # @example
9
+ # adapter.create(collection) # => 1
10
+ #
11
+ # Adapters provide specific implementation of this method
12
+ #
13
+ # @param [Enumerable<Resource>] resources
14
+ # The list of resources (model instances) to create
15
+ #
16
+ # @return [Integer]
17
+ # The number of records that were actually saved into the data-store
18
+ #
19
+ # @api semipublic
20
+ def create(resources)
21
+ raise NotImplementedError, "#{self.class}#create not implemented"
22
+ end
23
+
24
+ # Reads one or many resources from a datastore
25
+ #
26
+ # @example
27
+ # adapter.read(query) # => [ { 'name' => 'Dan Kubb' } ]
28
+ #
29
+ # Adapters provide specific implementation of this method
30
+ #
31
+ # @param [Query] query
32
+ # the query to match resources in the datastore
33
+ #
34
+ # @return [Enumerable<Hash>]
35
+ # an array of hashes to become resources
36
+ #
37
+ # @api semipublic
38
+ def read(query)
39
+ with_connection do |connection|
40
+
41
+ end
42
+ end
43
+
44
+ # Updates one or many existing resources
45
+ #
46
+ # @example
47
+ # adapter.update(attributes, collection) # => 1
48
+ #
49
+ # Adapters provide specific implementation of this method
50
+ #
51
+ # @param [Hash(Property => Object)] attributes
52
+ # hash of attribute values to set, keyed by Property
53
+ # @param [Collection] collection
54
+ # collection of records to be updated
55
+ #
56
+ # @return [Integer]
57
+ # the number of records updated
58
+ #
59
+ # @api semipublic
60
+ def update(attributes, collection)
61
+ raise NotImplementedError, "#{self.class}#update not implemented"
62
+ end
63
+
64
+ # Deletes one or many existing resources
65
+ #
66
+ # @example
67
+ # adapter.delete(collection) # => 1
68
+ #
69
+ # Adapters provide specific implementation of this method
70
+ #
71
+ # @param [Collection] collection
72
+ # collection of records to be deleted
73
+ #
74
+ # @return [Integer]
75
+ # the number of records deleted
76
+ #
77
+ # @api semipublic
78
+ def delete(collection)
79
+ raise NotImplementedError, "#{self.class}#delete not implemented"
80
+ end
81
+
82
+ # Returns the name of the CouchDB database.
83
+ #
84
+ # @raise [RuntimeError] if the CouchDB database name is invalid.
85
+ def db_name
86
+ result = options[:path].scan(/^\/?([-_+%()$a-z0-9]+?)\/?$/).flatten[0]
87
+ if result != nil
88
+ return Addressable::URI.unencode_component(result)
89
+ else
90
+ raise StandardError, "Invalid database path: '#{options[:path]}'"
91
+ end
92
+ end
93
+
94
+ # Returns the name of the CouchDB database after being escaped.
95
+ def escaped_db_name
96
+ return Addressable::URI.encode_component(
97
+ self.db_name, Addressable::URI::CharacterClasses::UNRESERVED)
98
+ end
99
+
100
+
101
+ private
102
+
103
+ def initialize(repo_name, options = {})
104
+ super
105
+
106
+ # When giving a repository URI rather than a hash, the database name
107
+ # is :path, with a leading slash.
108
+ if options[:path] && options[:database].nil?
109
+ options[:database] = db_name
110
+ end
111
+
112
+ @resource_naming_convention = NamingConventions::Resource::Underscored
113
+ @uri = Addressable::URI.new(options.only(:scheme, :host, :path, :port))
114
+ end
115
+
116
+
117
+ # Returns the CouchRest::Database instance for this process.
118
+ #
119
+ # @return [CouchRest::Database]
120
+ #
121
+ # @raise [ConnectionError]
122
+ # If the database requires you to authenticate, and the given username
123
+ # or password was not correct, a ConnectionError exception will be
124
+ # raised.
125
+ #
126
+ # @api semipublic
127
+ def database
128
+ unless defined?(@database)
129
+ @database = connection.database!(@options[:database])
130
+ end
131
+ @database
132
+ rescue Errno::ECONNREFUSED
133
+ DataMapper.logger.error("Could Not Connect to Database!")
134
+ raise(ConnectionError, "The adapter could not connect to Couchdb running at '#{@uri}'")
135
+ end
136
+
137
+ def with_connection
138
+ begin
139
+ yield connection
140
+ rescue => e
141
+ DataMapper.logger.error(exception.to_s)
142
+ raise e
143
+ end
144
+ end
145
+
146
+ # @see #connection
147
+ def connection
148
+ @connection ||= open_connection
149
+ end
150
+
151
+ # Returns CouchRest::Server instance
152
+ # @return [CouchRest::Server]
153
+ # @todo reset! connection and allow #uuid_batch_count to change
154
+ # also....do I need to use #chainable for this?
155
+ # @api semipublic
156
+ def open_connection
157
+ CouchRest::Server.new(@uri)
158
+ end
159
+ end # CouchDBAdapter
160
+
161
+ # Required naming scheme.
162
+ CouchdbAdapter = CouchDBAdapter
163
+ const_added(:CouchdbAdapter)
164
+ end
165
+ end
@@ -0,0 +1,121 @@
1
+ require 'base64'
2
+ require 'net/http'
3
+ require 'rubygems'
4
+
5
+ gem 'mime-types', '~>1.15'
6
+ require 'mime/types'
7
+
8
+ module DataMapper
9
+ module CouchResource
10
+ module Attachments
11
+
12
+ def self.included(mod)
13
+ mod.class_eval do
14
+
15
+ def add_attachment(file, options = {})
16
+ assert_attachments_property
17
+
18
+ filename = File.basename(file.path)
19
+
20
+ content_type = options[:content_type] || begin
21
+ mime_types = MIME::Types.of(filename)
22
+ mime_types.empty? ? 'application/octet-stream' : mime_types.first.content_type
23
+ end
24
+
25
+ name = options[:name] || filename
26
+ data = file.read
27
+
28
+ if new_record? || !model.properties.has_property?(:rev)
29
+ self.attachments ||= {}
30
+ self.attachments[name] = {
31
+ 'content_type' => content_type,
32
+ 'data' => Base64.encode64(data).chomp,
33
+ }
34
+ else
35
+ adapter = repository.adapter
36
+ http = Net::HTTP.new(adapter.uri.host, adapter.uri.port)
37
+ uri = Addressable::URI.encode_component("#{attachment_path(name)}?rev=#{self.rev}")
38
+ headers = {
39
+ 'Content-Length' => data.size.to_s,
40
+ 'Content-Type' => content_type,
41
+ }
42
+ http.put(uri, data, headers)
43
+ self.reload
44
+ end
45
+
46
+ end
47
+
48
+ def delete_attachment(name)
49
+ assert_attachments_property
50
+
51
+ attachment = self.attachments[name] if self.attachments
52
+
53
+ unless attachment
54
+ return false
55
+ end
56
+
57
+ response = unless new_record?
58
+ adapter = repository.adapter
59
+ http = Net::HTTP.new(adapter.uri.host, adapter.uri.port)
60
+ uri = Addressable::URI.encode_component("#{attachment_path(name)}?rev=#{self.rev}")
61
+ http.delete(uri, 'Content-Type' => attachment['content_type'])
62
+ end
63
+
64
+ if response && !response.kind_of?(Net::HTTPSuccess)
65
+ false
66
+ else
67
+ self.attachments.delete(name)
68
+ self.attachments = nil if self.attachments.empty?
69
+ true
70
+ end
71
+ end
72
+
73
+ # TODO: cache data on model? (don't want to make resource dirty though...)
74
+ def get_attachment(name)
75
+ assert_attachments_property
76
+
77
+ attachment = self.attachments[name] if self.attachments
78
+
79
+ unless self.id && attachment
80
+ nil
81
+ else
82
+ adapter = repository.adapter
83
+ http = Net::HTTP.new(adapter.uri.host, adapter.uri.port)
84
+ uri = Addressable::URI.encode_component(attachment_path(name))
85
+ response, data = http.get(uri, 'Content-Type' => attachment['content_type'])
86
+
87
+ unless response.kind_of?(Net::HTTPSuccess)
88
+ nil
89
+ else
90
+ data
91
+ end
92
+ end
93
+
94
+ end
95
+
96
+ private
97
+
98
+ def attachment_path(name)
99
+ if new_record?
100
+ nil
101
+ else
102
+ "/#{repository.adapter.escaped_db_name}/#{self.id}/#{name}"
103
+ end
104
+ end
105
+
106
+ def assert_attachments_property
107
+ property = model.properties[:attachments]
108
+
109
+ unless property &&
110
+ property.type == DataMapper::Types::JsonObject &&
111
+ property.field == '_attachments'
112
+ raise ArgumentError, "Attachments require property :attachments, JsonObject, :field => '_attachments'"
113
+ end
114
+ end
115
+
116
+ end
117
+
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,7 @@
1
+ module DataMapper
2
+ module Couch
3
+ class Collection < Collection
4
+ attr_accessor :total_rows, :offset
5
+ end
6
+ end
7
+ end