careo-makura 0.1

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/COPYING ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2008 Michael Fellinger <m.fellinger@gmail.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # Makura
2
+
3
+ Makura is a Ruby wrapper around the CouchDB REST API.
4
+
5
+ It doesn't provide a lot of bells and whistles, but aims to be as close to the
6
+ original API as possible, while taking advantage of Ruby's expressive power.
7
+
8
+ Most ideas for this have been gathered while trying other libraries such as
9
+ CouchObject, CouchRest, and RelaxDB.
10
+
11
+ It does so with almost no modification of ruby libraries and makes it simple
12
+ to switch the HTTP library used by changing one method.
13
+ Eventually Makura will be using an evented http library to provide better
14
+ performance.
15
+
16
+ We are using the json library, which adds following methods:
17
+ To Kernel: #j, #jj, #JSON
18
+ To Object and most core classes: #to_json
19
+ To String: #to_json_raw_object, #to_json, #to_json_raw
20
+
21
+ ## Dependencies
22
+
23
+ * CouchDB - 0.9 trunk (rev 725909 and higher)
24
+ * rest-client
25
+ * rack
26
+ * json
27
+
28
+ ## Features
29
+
30
+ * Simple Models, the CouchDB way and without magic.
31
+ * Free choice of inheritance, just include the Makura::Model module.
32
+ * Smart interpretation of returned JSON.
33
+ * Direct mapping of javascript files to map/reduce functions for views.
34
+ * CouchDB specific error reporting, no meaningless HTTP status code.
35
+ * Live update of views during runtime.
36
+ * Easy configuration, possibility to use different servers and databases each
37
+ model.
38
+
39
+ ## Usage
40
+
41
+ See the /example/blog.rb
data/bin/makura ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'makura'
5
+
6
+ Makura::Model.database = ARGV.shift
7
+ DB = Makura::Model.database
8
+
9
+ puts "Your database is assigned to DB"
10
+
11
+ require 'irb'
12
+ IRB.start
data/example/blog.rb ADDED
@@ -0,0 +1,53 @@
1
+ require 'makura'
2
+
3
+ # Setting up everything
4
+
5
+ # Makura::Model.server = 'http://localhost:5984'
6
+ Makura::Model.database = 'mydb'
7
+
8
+ class Post
9
+ include Makura::Model
10
+
11
+ properties :title, :text, :tags
12
+ belongs_to :author
13
+
14
+ layout :all
15
+
16
+ validates(:title){ presence and length :within => (3..100) }
17
+ validates(:text){ presence }
18
+
19
+ save # submit design docs to CouchDB
20
+ end
21
+
22
+ class Author
23
+ include Makura::Model
24
+
25
+ property :name
26
+
27
+ layout :posts, :reduce => :sum_length
28
+ layout :all
29
+
30
+ save
31
+ end
32
+
33
+ class Comment
34
+ include Makura::Model
35
+
36
+ property :text
37
+ end
38
+
39
+ # And here it goes.
40
+
41
+ author = Author.new('name' => 'Michael Fellinger')
42
+ author.save
43
+
44
+ post = Post.new(
45
+ :title => 'Hello, World!',
46
+ :text => 'This is my first post',
47
+ :author => author)
48
+ post.save
49
+
50
+ Post.view(:all).each do |post|
51
+ p post
52
+ p post.author
53
+ end
@@ -0,0 +1,5 @@
1
+ function(doc){
2
+ if(doc.type == 'Author'){
3
+ emit(doc._id, doc);
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ function(doc){
2
+ if(doc.type == 'Post' && doc.user){
3
+ emit(doc.user, doc);
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ function(doc){
2
+ if(doc.type == 'Post'){
3
+ emit(doc._id, doc);
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ function(doc){
2
+ if(doc.type == 'Comment'){
3
+ emit(doc._id, doc);
4
+ }
5
+ }
@@ -0,0 +1,7 @@
1
+ function(keys, values, rereduce){
2
+ if(rereduce){
3
+ return sum(values);
4
+ } else {
5
+ return values.length;
6
+ }
7
+ }
data/lib/makura.rb ADDED
@@ -0,0 +1,62 @@
1
+ require 'pp'
2
+ require 'uri'
3
+
4
+ begin
5
+ require 'rubygems'
6
+ rescue LoadError
7
+ end
8
+
9
+ require 'rest_client'
10
+ require 'json'
11
+
12
+ module Makura
13
+ VERSION = '2008.01.15'
14
+ ROOT = File.expand_path(File.dirname(__FILE__))
15
+ end
16
+
17
+ unless $LOAD_PATH.any?{|lp| File.expand_path(lp) == Makura::ROOT }
18
+ $LOAD_PATH.unshift(Makura::ROOT)
19
+ end
20
+
21
+ require 'makura/error'
22
+ require 'makura/http_methods'
23
+ require 'makura/server'
24
+ require 'makura/database'
25
+ require 'makura/uuid_cache'
26
+ require 'makura/model'
27
+ require 'makura/design'
28
+ require 'makura/layout'
29
+
30
+ module Makura
31
+ CHARS = (48..128).map{|c| c.chr}.grep(/[[:alnum:]]/)
32
+ MOD = CHARS.size
33
+
34
+ module_function
35
+
36
+ # From Rack
37
+ def escape(s)
38
+ s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
39
+ '%'+$1.unpack('H2'*$1.size).join('%').upcase
40
+ }.tr(' ', '+')
41
+ end
42
+
43
+ def pretty_from_md5(md5)
44
+ id = md5.to_i(16)
45
+ o = []
46
+ while id > 0
47
+ id, r = id.divmod(MOD)
48
+ o.unshift CHARS[r]
49
+ end
50
+ o.join
51
+ end
52
+
53
+ def pretty_to_md5(id)
54
+ i = 0
55
+ id.scan(/./) do |c|
56
+ i = i * MOD + CHARS.index(c)
57
+ end
58
+ i.to_s(16)
59
+ end
60
+ end
61
+
62
+ Sofa = Makura # be backwards compatible
@@ -0,0 +1,216 @@
1
+ module Makura
2
+ class Database
3
+ include HTTPMethods
4
+ attr_accessor :server, :name
5
+
6
+ # Initialize instance of Database and create if it doesn't exist yet.
7
+ # To prevent automatic creation, pass false as 3rd parameter
8
+ #
9
+ # Usage:
10
+ # server = Makura::Server.new
11
+ # # #<URI::HTTP:0xb7788234 URL:http://localhost:5984>
12
+ # database = Makura::Database.new(server, 'foo')
13
+ # # #<Makura::Database 'http://localhost:5984/foo'>
14
+
15
+ def initialize(server, name, auto_create = true)
16
+ @server, @name = server, name
17
+ create if auto_create
18
+ end
19
+
20
+ # Create the database if it doesn't exist already.
21
+ #
22
+ # Usage:
23
+ # server = Makura::Server.new
24
+ # # #<URI::HTTP:0xb76a4a98 URL:http://localhost:5984>
25
+ #
26
+ # database = Makura::Database.new(server, 'foo', false)
27
+ # # #<Makura::Database 'http://localhost:5984/foo'>
28
+ #
29
+ # database.create
30
+ # # {"update_seq"=>0, "doc_count"=>0, "purge_seq"=>0, "disk_size"=>4096,
31
+ # # "compact_running"=>false, "db_name"=>"foo", "doc_del_count"=>0}
32
+
33
+ def create
34
+ info
35
+ rescue Error::ResourceNotFound
36
+ put("/", :payload => '')
37
+ end
38
+
39
+ # Will delete document in the CouchDB corresponding to given +doc+.
40
+ # Use #destroy to delete the database itself.
41
+ # Use #delete! to automatically rescue exceptions on conflicts.
42
+ #
43
+ # Possible variations (User is a Makura::Model) are:
44
+ #
45
+ # # deleting based on explicit _id and :rev option.
46
+ # database.delete('manveru', :rev => 123134)
47
+ #
48
+ # # deleting based on a Hash
49
+ # database.delete('_id' => 'manveru', '_rev' => 123134)
50
+ #
51
+ # user = User.new(:name => 'manveru')
52
+ # user.save
53
+ # database.delete(user)
54
+ #
55
+ # Usage when deleting document:
56
+ # doc = database.save('name' => 'manveru', 'time' => Time.now)
57
+ # # {"rev"=>"484030692", "id"=>"67e086087d5b7e7196b5c99174b0b66c", "ok"=>true}
58
+ #
59
+ # database[doc['id']]
60
+ # # {"name"=>"manveru", "_rev"=>"484030692",
61
+ # "time"=>"Sat Nov 22 16:37:50 +0900 2008",
62
+ # "_id"=>"67e086087d5b7e7196b5c99174b0b66c"}
63
+ #
64
+ # database.delete(doc['id'], :rev => doc['rev'])
65
+ # # {"rev"=>"2034883605", "id"=>"67e086087d5b7e7196b5c99174b0b66c", "ok"=>true}
66
+ #
67
+ # database[doc['id']]
68
+ # RestClient::ResourceNotFound: RestClient::ResourceNotFound
69
+ #
70
+ # database.delete(doc['id'], :rev => doc['rev'])
71
+ # Makura::RequestFailed: {"reason"=>"Document update conflict.", "error"=>"conflict"}
72
+
73
+ def delete(doc, opts = {})
74
+ case doc
75
+ when Makura::Model
76
+ doc_id, doc_rev = doc._id, doc._rev
77
+ when Hash
78
+ doc_id = doc['_id'] || doc['id'] || doc[:_id] || doc[:id]
79
+ doc_rev = doc['_rev'] || doc['rev'] || doc[:_rev] || doc[:rev]
80
+ else
81
+ doc_id = doc
82
+ end
83
+
84
+ raise(ArgumentError, "document _id wasn't passed") unless doc_id
85
+
86
+ doc_id = Makura.escape(doc_id)
87
+ opts[:rev] ||= doc_rev if doc_rev
88
+
89
+ request(:delete, doc_id.to_s, opts)
90
+ end
91
+
92
+ def delete!(doc, opts = {})
93
+ delete(doc, opts)
94
+ rescue Error::Conflict, Error::ResourceNotFound
95
+ end
96
+
97
+ # Delete the database itself.
98
+ #
99
+ # Usage:
100
+ # database.destroy
101
+ # # {"ok"=>true}
102
+ # database.info
103
+ # # RestClient::ResourceNotFound: RestClient::ResourceNotFound
104
+
105
+ def destroy(opts = {})
106
+ request(:delete, '/', opts)
107
+ end
108
+
109
+ def destroy!(opts = {})
110
+ destroy(opts)
111
+ rescue Error::ResourceNotFound
112
+ end
113
+
114
+ def info
115
+ get('/')
116
+ end
117
+
118
+ def all_docs(params = {})
119
+ get('_all_docs')
120
+ end
121
+ alias documents all_docs
122
+
123
+ def [](id, rev = nil)
124
+ id = Makura.escape(id)
125
+ if rev
126
+ get(id, :rev => rev)
127
+ else
128
+ get(id)
129
+ end
130
+ end
131
+
132
+ def []=(id, doc)
133
+ id = Makura.escape(id)
134
+ put(id, :payload => prepare_doc(doc))
135
+ end
136
+
137
+ def temp_view(params = {})
138
+ params[:payload] = functions = {}
139
+ functions[:map] = params.delete(:map) if params[:map]
140
+ functions[:reduce] = params.delete(:reduce) if params[:reduce]
141
+ params['Content-Type'] = 'application/json'
142
+
143
+ post('_temp_view', params)
144
+ end
145
+
146
+ def view(layout, params = {})
147
+ get("_view/#{layout}", params)
148
+ end
149
+
150
+ def save(doc)
151
+ if id = doc['_id']
152
+ id = Makura.escape(id)
153
+ put(id, :payload => prepare_doc(doc))
154
+ else
155
+ id = doc['_id'] = @server.next_uuid
156
+ id = Makura.escape(id)
157
+ put(id, :payload => prepare_doc(doc))
158
+ end
159
+ end
160
+
161
+ # NOTE:
162
+ # * Seems like we don't even need to check _id, CouchDB will assign it.
163
+ # But in order to use our own uuids we still do it.
164
+ def bulk_docs(docs)
165
+ docs.each{|doc| doc['_id'] ||= @server.next_uuid }
166
+ post("_bulk_docs", :payload => {:docs => docs})
167
+ end
168
+ alias bulk_save bulk_docs
169
+
170
+ def get_attachment(doc, file_id)
171
+ doc_id = doc.respond_to?(:_id) ? doc._id : doc.to_str
172
+ file_id, doc_id = Makura.escape(file_id), Makura.escape(doc_id)
173
+
174
+ get("#{doc_id}/#{file_id}", :raw => true)
175
+ end
176
+
177
+ # PUT an attachment directly to CouchDB
178
+ def put_attachment(doc, file_id, file, options = {})
179
+ doc_id, file_id = Makura.escape(doc._id), Makura.escape(file_id)
180
+
181
+ options[:payload] = file
182
+ options[:raw] = true
183
+ options[:rev] = doc._rev if doc._rev
184
+
185
+ put("#{doc_id}/#{file_id}", options)
186
+ end
187
+
188
+ def prepare_doc(doc)
189
+ if attachments = doc['_attachments']
190
+ doc['_attachments'] = encode_attachments(attachments)
191
+ end
192
+
193
+ return doc
194
+ end
195
+
196
+ def request(method, path, params = {})
197
+ @server.send(:request, method, "/#{name}/#{path}", params)
198
+ end
199
+
200
+ def encode_attachments(attachments)
201
+ attachments.each do |key, value|
202
+ next if value['stub']
203
+ value['data'] = base64(value['data'])
204
+ end
205
+ attachments
206
+ end
207
+
208
+ def base64(data)
209
+ [data.to_s].pack('m').delete("\n")
210
+ end
211
+
212
+ def inspect
213
+ "#<Makura::Database '#{@server.uri(name)}'>"
214
+ end
215
+ end
216
+ end