dm-couchdb-adapter 0.10.2

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/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