makura 2009.05.27

Sign up to get free protection for your applications and to get access to all the features.
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,50 @@
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
+ save # submit design docs to CouchDB
17
+ end
18
+
19
+ class Author
20
+ include Makura::Model
21
+
22
+ property :name
23
+
24
+ layout :posts, :reduce => :sum_length
25
+ layout :all
26
+
27
+ save
28
+ end
29
+
30
+ class Comment
31
+ include Makura::Model
32
+
33
+ property :text
34
+ end
35
+
36
+ # And here it goes.
37
+
38
+ author = Author.new('name' => 'Michael Fellinger')
39
+ author.save
40
+
41
+ post = Post.new(
42
+ :title => 'Hello, World!',
43
+ :text => 'This is my first post',
44
+ :author => author)
45
+ post.save
46
+
47
+ Post.view(:all).each do |post|
48
+ p post
49
+ p post.author
50
+ 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,66 @@
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
+ ROOT = File.expand_path(File.dirname(__FILE__))
14
+ end
15
+
16
+ unless $LOAD_PATH.any?{|lp| File.expand_path(lp) == Makura::ROOT }
17
+ $LOAD_PATH.unshift(Makura::ROOT)
18
+ end
19
+
20
+ require 'makura/version'
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
+
61
+ def constant(name, root = Module)
62
+ name.split('::').inject(root){|s,v| s.const_get(v) }
63
+ end
64
+ end
65
+
66
+ 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("_design/#{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
@@ -0,0 +1,37 @@
1
+ module Makura
2
+ class Design
3
+ attr_accessor :database, :name, :language
4
+ attr_reader :layouts
5
+
6
+ def initialize(name, database = nil)
7
+ @name, @database = name, database
8
+ @language = 'javascript'
9
+ @layouts = {}
10
+ end
11
+
12
+ def save
13
+ hash = to_hash
14
+ doc = @database[hash['_id']]
15
+ doc['views'] = hash['views']
16
+ @database.save(doc)
17
+ rescue Makura::Error::ResourceNotFound
18
+ @database.save(to_hash)
19
+ end
20
+
21
+ def [](layout_name)
22
+ @layouts[layout_name.to_s]
23
+ end
24
+
25
+ def []=(layout_name, layout)
26
+ @layouts[layout_name.to_s] = layout
27
+ end
28
+
29
+ def to_hash
30
+ views = {}
31
+ @layouts.each{|name, layout| views[name] = layout.to_hash }
32
+ views.delete_if{|k,v| !(v[:map] || v['map']) }
33
+
34
+ {'language' => @language, '_id' => "_design/#{@name}", 'views' => views}
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ module Makura
2
+ # Mother and namespace of all exceptions
3
+ class Error < ::RuntimeError
4
+ class ConnectionRefused < Error; end
5
+ class RequestFailed < Error; end
6
+ class ResourceNotFound < RequestFailed; end
7
+ class Conflict < RequestFailed; end
8
+ class MissingRevision < RequestFailed; end
9
+ class BadRequest < RequestFailed; end
10
+ class Authorization < RequestFailed; end
11
+ class NotFound < RequestFailed; end
12
+ class FileExists < RequestFailed; end
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ module Makura
2
+ module HTTPMethods
3
+ def delete(path, params = {})
4
+ request(:delete, path, params)
5
+ end
6
+
7
+ def get(path, params = {})
8
+ request(:get, path, params)
9
+ end
10
+
11
+ def post(path, params = {})
12
+ request(:post, path, params)
13
+ end
14
+
15
+ def put(path, params = {})
16
+ request(:put, path, params)
17
+ end
18
+ end
19
+ end