Can mess with dirs just fine. Files need more work.
[teddybear.git] / couch.rb
blob27cd8e58a38d06eeae0a813cb3a7414b3897cdf6
1 # Ruby layer for accessing CouchDB
3 # Copyright (c) Stuart Glaser <StuGlaser@gmail.com>
5 require 'net/http'
6 require 'json'
8 # The JSON parser stupidly does not accept just strings.
9 def hacked_json_parse(json)
10   boxed = "[ " + json + " ]"
11   boxed_parsed = JSON.parse boxed
12   boxed_parsed[0]
13 end
15 class CouchException < StandardError
16   attr_reader :couch, :response
18   def initialize(couch, response)
19     @couch = couch
20     @response = response
21   end
23   def couch_backtrace
24     #json = JSON.parse @response.body
25     json = hacked_json_parse @response.body
26     json['error']['id'] + "\n" + json['error']['reason']
27   end
29   def to_s
30     "#{@response.class}\n" +
31       "Couch Backtrace:" + couch_backtrace +
32       "\nRuby Backtrace:"
33   end
34 end
36 module Couch
37   
38   class Server
39     attr_accessor :host, :port
41     def initialize(host, port)
42       @host = host
43       @port = port
44     end
45     
46     def get(path)
47       request Net::HTTP::Get.new(path)
48     end
50     def put(path, data)  # TODO: support text/javascript
51       req = Net::HTTP::Put.new(path)
52       req['content-type'] = 'text/javascript'
53       req['content-type'] = 'application/json'
54       req.body = data
55       request req
56     end
58     def post(path, data)  # TODO: support text/javascript
59       req = Net::HTTP::Post.new(path)
60       req.body = data
61       request req
62     end
64     def delete(path)
65       request Net::HTTP::Delete.new(path)
66     end
68     def request(req)
69       resp = Net::HTTP.start(@host, @port) do |http|
70         http.request req
71       end
72       if not resp.kind_of?(Net::HTTPSuccess)
73         raise CouchException.new(self, resp)
74       end
75       resp
76     end
78   end
81   class Db
82     attr_accessor :server
83     attr_accessor :name
85     def initialize(server, name)
86       @server = server
87       @name = name
88     end
90     def get(doc_id)
91       resp = @server.get doc_path(doc_id)
92       #JSON.parse resp.body
93       hacked_json_parse resp.body
94     end
96     def put(doc_id, body)
97       resp = $server.put doc_path(doc_id), body.to_json
98       #JSON.parse resp.body
99       hacked_json_parse resp.body
100     end
102     def post(body)
103       resp = $server.post doc_path(''), body.to_json
104       #JSON.parse resp.body
105       hacked_json_parse resp.body
106     end
108     def delete(doc_id)
109       resp = $server.delete doc_path(doc_id)
110       #JSON.parse resp.body
111       hacked_json_parse resp.body
112     end
114     def temp_view(data)
115       path = doc_path('_temp_view')
116       req = Net::HTTP::Post.new(path)
117       req['content-type'] = 'text/javascript'
118       req.body = data
119       request req
120     end
121     
122     def temp_view(body)  # TODO: content-type
123       resp = $server.post doc_path('_temp_view'), body
124       #JSON.parse resp.body
125       hacked_json_parse resp.body
126     end
128     def doc_path(doc_id)
129       '/' + @name + '/' + doc_id
130     end
131   end
134   # Represents a document in a Couch database
135   class Doc
136     attr_reader :db
137     attr_accessor :id
138     attr_reader :_rev
139     attr_accessor :_attachments
141     def _attachments
142       @_attachments = [] if @_attachments.nil?
143       @_attachments
144     end
146     class << self
147       attr_reader :field_names
148       
149       def field(*syms)
150         attr_accessor(*syms)
151         @field_names = [] if @field_names.nil?
152         @field_names |= syms
153         nil
154       end
155     end
157     def Doc.find(db, id)
158       begin
159         doc = new $db
160         doc.from_couch! $db.get(id)
161         doc
162       rescue CouchException=>ex
163         return nil if ex.response.instance_of? Net::HTTPNotFound
164         raise
165       end
166     end
167     
169     def Doc.find_or_create(db, id)
170       doc = find db, id
171       return doc unless doc.nil?
173       doc = new db
174       doc.id = id
175       doc.save
176       doc
177     end
179     
180     def initialize(db)
181       @db = db
182     end
184     
185     def save
186       print "SAVE #{id}\n"
187       fields = to_couch
188       results = if @id.nil?  # POST
189                   @db.post fields
190                 else         # PUT
191                   @db.put @id, fields
192                 end
193       @_rev = results['rev']
194       @id = results['id']
195       results
196     end
198     def refresh!
199       raise "Never saved" if @id.nil?
200       self.from_couch! @db.get(@id)
201       nil
202     end
204     def delete!
205       results = @db.delete(@id + "?rev=#{@_rev}")
206       @_rev = nil
207       results
208     end
210     def new_attachment
211       a = Attachment.new self
212       _attachments << a
213       a
214     end
216     # Converts the object to (unparsed) JSON format for couch
217     def to_couch
218       # Fills in the fields for saving to the db
219       fields = {}
220       fields['_rev'] = @_rev unless @_rev.nil?
221       self.class.field_names.each do |field|
222         fields[field] = instance_variable_get '@' + field.to_s
223       end
224       unless _attachments.empty?
225         # Let ye of functional natures rejoice!
226         fields['_attachments'] = _attachments.inject({}) do |all, a|
227           all.merge a.to_couch
228         end
229       end
230       fields
231     end
233     # Takes (parsed) JSON from couch and places it into the fields
234     def from_couch!(couch)
235       @id = couch['_id']
236       @_rev = couch['_rev']
237       self.class.field_names.each do |field|
238         instance_variable_set('@' + field.to_s, couch[field.to_s])
239       end
240       unless couch['_attachments'].nil?
241         @_attachments = []
242         couch['_attachments'].each do |name,value|
243           a = Attachment.new self
244           a.from_couch! name, value
245           @_attachments << a
246         end
247       end
248       nil
249     end
250   end
252   
253   class Attachment
254     attr_accessor :doc
255     attr_accessor :name, :type, :data
256     attr_reader :length, :stub
258     def data=(data)
259       @stub = data.nil?
260       @data = data
261     end
263     def data
264       if not @data.nil?
265         @data
266       else
267         path = @doc.id + '?attachment=' + @name
268         @doc.db.get path
269       end
270     end
272     def initialize(doc)
273       @doc = doc
274       @stub = true
275     end
277     def to_couch
278       a = { 'type' => type, 'stub' => stub }
279       a['data'] = data unless stub
280       return { 'asdf' => { 'data' => 123123 } } if stub
281       { name => a }
282     end
284     def from_couch!(name, couch)
285       @name = name
286       @stub = couch['stub'] or false
287       @type = couch['type']
288       @length = couch['length']
289       @data = nil
290     end
291   end
292   
297 # TODO: delete this test code
298 $server = Couch::Server.new 'localhost', 8888
299 $db = Couch::Db.new $server, 'teddybear'
301 class D < Couch::Doc
302   field :dd
305 def dd
306   d = D.new $db
307   d.id = "dud"
308   a = d.new_attachment
309   a.name = 'affs'
310   a.data = "something suspicious"
311   d.save
312   d
316 # class Foo < Couch::Doc
317 #   field :aa
318 #   field :bb
319 # end
322 true