divan 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ # /doc/
2
+ /doc/*
3
+
4
+ # /drafts/
5
+ /drafts/*
data/README.rdoc ADDED
@@ -0,0 +1,60 @@
1
+ == Divan, are you insane?
2
+
3
+ * Do you need to have CouchDB <b>easy access to revisions</b>?
4
+ * *Sometimes* do you need to use just <b>one kind of document per database</b>? And sometimes don't?
5
+ * Do you need to use CouchDB <b>without care about activesupport</b> dependencies?
6
+ * Do you need to access <b>HTTP headers</b> of the request that returned your document object?
7
+
8
+ So you are *insane*! Divan is a comfortable place for you to rest.
9
+
10
+ == Look how it's easy!
11
+
12
+ # Model class
13
+ class InsanePerson < Divan::Models::InsanePerson
14
+ view_by :name
15
+ view_by :doctor
16
+ end
17
+
18
+ # Simple Usage
19
+ patient = InsanePeople.new :name => 'Hannibal', :doctor => 'House'
20
+ patient.problems = ['Sleepness', 'Headache', 'Alucinations']
21
+ patient.save
22
+
23
+ # Acessing HTTP header
24
+ patient.last_request.headers[:content_type]
25
+
26
+ # Easy accesso to revisions
27
+ patient.alive = true
28
+ patient.ttl = 7
29
+ 10.times.do
30
+ if patient.ttl > 0
31
+ patient.ttl -= 1
32
+ else
33
+ patient.alive = false
34
+ break
35
+ end
36
+ patient.save
37
+ end
38
+ patient.revision(2).rollback # Be carefull, he's back!
39
+
40
+ # Configuration file
41
+ insane_person:
42
+ host: http://127.0.0.1
43
+ port: 5984
44
+ database: insane_person
45
+
46
+ == FAQ
47
+
48
+ 1. Why are you not using active_model?
49
+ Because ActiveModel depends on active_support, and sometimes we want a model library that could
50
+ be used with Rails 2, or any other project could have conflicted dependencies.
51
+ ActiveModel is a great library, but it'snt a silver bullet for model libraries.
52
+ 2. Who needs access to HTTP headers?
53
+ Sometimes it's interesting to know exactly the time that each document is retuned by database,
54
+ you can do it calling method last_request in each document.
55
+ 3. Why dont you use CouchRest?
56
+ With CouchRest isn't easy to have access to HTTP headers
57
+
58
+ == TODO
59
+
60
+ A better README, documentation and other details
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.2
1
+ 0.1.3
data/init.rb CHANGED
@@ -1,8 +1,11 @@
1
- require 'divan.rb'
1
+ require "#{File.dirname(__FILE__)}/lib/divan.rb"
2
2
 
3
3
  #Lines below are used for debug purposes only
4
4
  Divan.load_database_configuration 'config/divan_config.yml'
5
5
 
6
+ class ProofOfConcept < Divan::Models::ProofOfConcept
7
+ end
8
+
6
9
  class POC < Divan::Models::ProofOfConcept
7
10
  view_by :mod
8
11
  view_by :value
data/lib/divan.rb CHANGED
@@ -1,4 +1,4 @@
1
- divan_path = File.expand_path('../lib', __FILE__)
1
+ divan_path = File.dirname(__FILE__)
2
2
  $:.unshift(divan_path) if File.directory?(divan_path) && !$:.include?(divan_path)
3
3
 
4
4
  require 'restclient'
@@ -64,7 +64,10 @@ module Divan
64
64
  attr_reader :new_document, :current_document
65
65
  def initialize(new_document)
66
66
  @new_document = new_document
67
- @current_document = new_document.class.find new_document.id
67
+ end
68
+
69
+ def current_document
70
+ @current_document ||= new_document.class.find new_document.id
68
71
  end
69
72
  end
70
73
  end
data/lib/divan/base.rb CHANGED
@@ -1,11 +1,15 @@
1
1
  module Divan
2
2
  class Base < Models::Base
3
- attr_accessor :id, :rev, :attributes, :last_request
3
+ attr_accessor :id, :rev, :attributes, :meta_attributes, :last_request
4
4
 
5
5
  def initialize(opts = {})
6
6
  opts = opts.clone
7
7
  @id = opts.delete(:id) || opts.delete(:_id) || Divan::Utils.uuid
8
8
  @rev = opts.delete(:rev) || opts.delete(:_rev)
9
+ @meta_attributes = opts.find_all{ |k,v| k.to_s[0..0] == '_' }.inject({}) do |hash, (key, value)|
10
+ hash[key.to_s[1..-1].to_sym] = opts.delete key
11
+ hash
12
+ end
9
13
  @attributes = opts
10
14
  @attributes[self.class.type_field.to_sym] = self.class.type_name unless self.class.top_level_model?
11
15
  self.class.properties.each{ |property| @attributes[property] ||= nil }
@@ -66,8 +70,9 @@ module Divan
66
70
  strs.delete ''
67
71
  subclass.model_name = strs.map{ |x| x.downcase }.join('_')
68
72
  if database
73
+ subclass.top_level_model! if database.name == subclass.model_name
74
+ subclass.define_view_all
69
75
  subclass.database = database
70
- subclass.top_level_model! if subclass.database.name == subclass.model_name
71
76
  end
72
77
  end
73
78
 
@@ -87,6 +92,10 @@ module Divan
87
92
  @top_level_model ||= false
88
93
  end
89
94
 
95
+ def views
96
+ @views ||= {}
97
+ end
98
+
90
99
  def properties
91
100
  @properties ||= ( superclass.methods.include? :properties ) ? superclass.properties.clone : []
92
101
  end
@@ -98,7 +107,6 @@ module Divan
98
107
  def database=(database)
99
108
  undefine_views if( !@database.nil? && @database != database )
100
109
  @database = database
101
- define_view_all
102
110
  define_views
103
111
  @database
104
112
  end
@@ -129,31 +137,31 @@ module Divan
129
137
  @views[param.to_sym] = functions
130
138
  end
131
139
 
132
- def define_view!(param, functions)
133
- database.views[model_name] ||= {}
134
- database.views[model_name][param.to_sym] = functions
135
- end
136
-
137
140
  def define_view_all
138
- if database && model_name == database.name
139
- define_view :all, :map => "function(doc){ if(doc._id.slice(0, 7) != \"_design\"){ emit(null, doc) } }"
141
+ if top_level_model?
142
+ define_view :all, :map => "function(doc){ if(doc._id[0] != \"_\"){ emit(null, doc) } }"
140
143
  else
141
144
  define_view :all, :map => "function(doc){ if(doc.#{type_field} == \"#{type_name}\"){ emit(null, doc) } }"
142
- end
145
+ end
143
146
  end
144
147
 
145
- def query_view(view, key=nil, args={}, special={})
146
- if key.is_a? Hash
147
- special = args
148
- args = key
148
+ def query_view(view, args={})
149
+ args ||= {}
150
+ args = args.clone
151
+ [:key, :startkey, :endkey].each do |k|
152
+ args[k] = args[k].to_json if args[k]
153
+ end
154
+
155
+ if args[:keys]
156
+ keys = args.delete(:keys).to_json
157
+ view_path = Divan::Utils.formatted_path "_design/#{model_name}/_view/#{view}", args
158
+ last_request = database.client[view_path].post keys
149
159
  else
150
- special = args
151
- args = {:key => key}
160
+ args[:key] = nil.to_json if args[:key].nil? && args[:startkey].nil? && args[:endkey].nil?
161
+ view_path = Divan::Utils.formatted_path "_design/#{model_name}/_view/#{view}", args
162
+ last_request = database.client[view_path].get
152
163
  end
153
164
 
154
- args = args.inject({}){ |hash,(k,v)| hash[k] = v.to_json; hash }
155
- view_path = Divan::Utils.formatted_path "_design/#{model_name}/_view/#{view}", args.merge(special)
156
- last_request = database.client[view_path].get
157
165
  results = JSON.parse last_request, :symbolize_names => true
158
166
  results[:rows].map do |row|
159
167
  obj = self.new row[:value]
@@ -167,11 +175,22 @@ module Divan
167
175
  def view_by(param)
168
176
  @view_by_params ||= []
169
177
  @view_by_params << param
170
- define_view! "by_#{param}", :map => "function(doc) { emit(doc.#{param}, doc) }"
178
+ if top_level_model?
179
+ define_view "by_#{param}", :map => "function(doc) { emit(doc.#{param}, doc) }"
180
+ else
181
+ define_view "by_#{param}", :map => "function(doc) { if(doc.#{type_field} == \"#{type_name}\"){ emit(doc.#{param}, doc) } }"
182
+ end
171
183
  eval <<-end_txt
172
184
  class << self
173
- def all_by_#{param}(key, args={}, special={})
174
- query_view :by_#{param}, key, args, special
185
+ def all_by_#{param}(args=nil)
186
+ unless args.is_a? Hash
187
+ if args.is_a? Array
188
+ args = { :keys => args }
189
+ else
190
+ args = { :key => args }
191
+ end
192
+ end
193
+ query_view :by_#{param}, args
175
194
  end
176
195
 
177
196
  def by_#{param}(key)
@@ -179,11 +198,11 @@ module Divan
179
198
  end
180
199
  end
181
200
  end_txt
201
+ define_views
182
202
  end
183
203
 
184
204
  def define_views
185
- database.views[model_name] ||= {}
186
- database.views[model_name].merge! @views if @views
205
+ database.views[model_name] = views
187
206
  end
188
207
 
189
208
  def undefine_views
@@ -30,6 +30,14 @@ module Divan
30
30
  end
31
31
  end
32
32
 
33
+ def compact
34
+ begin
35
+ client['_compact'].post Hash.new, :'content-type' => 'application/json'
36
+ rescue RestClient::ResourceNotFound
37
+ raise Divan::DatabaseNotFound.new(self), "Database was not found"
38
+ end
39
+ end
40
+
33
41
  def create
34
42
  begin
35
43
  client.put Hash.new
@@ -2,7 +2,7 @@ module Divan
2
2
  module Models
3
3
  class Base
4
4
 
5
- def save
5
+ def save(strategy=nil, &block)
6
6
  self.class.execute_before_validate_callback(self) or return false
7
7
 
8
8
  validate or return false
@@ -10,7 +10,7 @@ module Divan
10
10
  self.class.execute_after_validate_callback(self) or return false
11
11
  self.class.execute_before_save_callback(self) or return false
12
12
 
13
- execute_save
13
+ execute_save strategy, &block
14
14
 
15
15
  self.class.execute_after_save_callback(self)
16
16
  @last_request
@@ -38,27 +38,58 @@ module Divan
38
38
 
39
39
  def revision(index)
40
40
  revision!(index)
41
- rescue Divan::Divan::DocumentRevisionNotAvailable
41
+ rescue Divan::DocumentRevisionMissing
42
42
  nil
43
43
  end
44
44
 
45
45
  def revision!(index)
46
- r = revision_ids.find{ |rev| rev[0..1].to_i == index}
47
- r.nil? and raise Divan::Divan::DocumentRevisionNotAvailable.new(self), "Revision with index #{index} missing"
46
+ r = revision_ids.find{ |rev| rev[0..1].to_i == index }
47
+ r.nil? and raise Divan::DocumentRevisionMissing.new(self), "Revision with index #{index} missing"
48
48
  return self if r == @rev
49
49
  self.class.find @id, :rev => r
50
50
  end
51
51
 
52
52
  protected
53
53
 
54
- def execute_save
54
+ def execute_save(strategy=nil, &block)
55
+ previous_request = @last_request
55
56
  begin
56
57
  save_attrs = @attributes.clone
57
58
  save_attrs[:"_rev"] = @rev if @rev
59
+ save_attrs.delete(:id)
60
+ save_attrs.delete(:_id)
58
61
  @last_request = database.client[current_document_path].put save_attrs.to_json
59
62
  @rev = JSON.parse(@last_request, :symbolize_names => true )[:rev]
60
63
  rescue RestClient::Conflict
61
- raise Divan::DocumentConflict.new(self), "Update race conflict"
64
+ if strategy && self.class.strategies[strategy.to_sym]
65
+ run_strategy_in_block &self.class.strategies[strategy.to_sym]
66
+ elsif block_given?
67
+ run_strategy_in_block &block
68
+ else
69
+ raise Divan::DocumentConflict.new(self), "Update race conflict"
70
+ end
71
+ end
72
+ end
73
+
74
+ def run_strategy_in_function(strategy)
75
+ conflict_doc = self.class.find(id)
76
+ @rev = conflict_doc.rev
77
+ if send( "#{strategy}_strategy", self.class.find(id) )
78
+ execute_save strategy
79
+ else
80
+ @attributes = conflict_doc.attributes
81
+ @last_request = conflict_doc.last_request
82
+ end
83
+ end
84
+
85
+ def run_strategy_in_block(&block)
86
+ conflict_doc = self.class.find(id)
87
+ @rev = conflict_doc.rev
88
+ if yield(self, conflict_doc)
89
+ self.execute_save(nil, &block)
90
+ else
91
+ @attributes = conflict_doc.attributes
92
+ @last_request = conflict_doc.last_request
62
93
  end
63
94
  end
64
95
 
@@ -101,8 +132,16 @@ module Divan
101
132
  end
102
133
  end
103
134
 
135
+ def strategies
136
+ @strategies ||= superclass.respond_to?(:strategies) ? superclass.strategies.clone : {}
137
+ end
138
+
104
139
  protected
105
140
 
141
+ def strategy(name, &block)
142
+ strategies[name.to_sym] = block
143
+ end
144
+
106
145
  def single_create(opts = {})
107
146
  obj = self.new(opts)
108
147
  obj.save
@@ -118,9 +157,12 @@ module Divan
118
157
  end }
119
158
  last_request = database.client['_bulk_docs'].post( payload.to_json, :content_type => :json, :accept => :json )
120
159
  end
121
-
122
160
  end
123
161
 
162
+ strategy(:first_wins) { |here, in_database| false }
163
+ strategy(:last_wins) { |here, in_database| true }
164
+ strategy(:merge) { |here, in_database| here.attributes = in_database.attributes.merge here.attributes }
165
+
124
166
  end
125
167
  end
126
168
  end
@@ -0,0 +1,62 @@
1
+ module Divan
2
+ module Models
3
+ class Revision < Models::Base
4
+ attr_reader :revisioned_doc, :revisioned_at, :revisioned_by
5
+
6
+ def initialize(*args)
7
+ super
8
+ @revisioned_doc = @meta_attributes.delete :revision_of
9
+ @revisioned_at = @meta_attributes.delete :revisioned_at
10
+ @revisioned_by = @meta_attributes.delete :revisioned_by
11
+ end
12
+
13
+ def rollback
14
+ revisioned_doc.attributes = @attributes.clone
15
+ revisioned_doc.save :last_wins
16
+ end
17
+
18
+ class << self
19
+ attr_reader :revisioned_class
20
+
21
+ def create_by_revisioned_doc(rev_doc, rev_by = nil)
22
+ self.new rev_doc.params
23
+ self.id = URI.encode "_revision/#{rev_doc.rev}"
24
+ @revisioned_doc = rev_doc
25
+ # parsed_time = Date._parse rev_doc.last_request.headers[:date]
26
+ # @revisioned_at = Time.gm *[:year, :mon, :mday, :hour, :min, :sec].collect{ |k| parsed_time[k] }
27
+ @revisioned_at = Divan::Utils.parse_time rev_doc.last_request.headers[:date]
28
+ @revisioned_by = rev_by
29
+ self.save
30
+ end
31
+
32
+ def revisioned_class=(rev_class)
33
+ @revisioned_class = rev_class
34
+ self.database = rev_class.database
35
+ self.model_name = rev_class.model_name + '_revision'
36
+ end
37
+ alias :revision_of :'revisioned_class='
38
+
39
+ def type_name
40
+ revisioned_class.type_name
41
+ end
42
+
43
+ def type_field
44
+ revisioned_class.type_field
45
+ end
46
+
47
+ def top_level_model!(*args)
48
+ raise "Can't modify revision top level"
49
+ end
50
+
51
+ def top_level_model?
52
+ revisioned_class.top_level_model?
53
+ end
54
+
55
+ def inherithed(subclass)
56
+ nil
57
+ end
58
+ end
59
+
60
+ end
61
+ end
62
+ end
data/lib/divan/utils.rb CHANGED
@@ -6,6 +6,11 @@ module Divan
6
6
  "%04x%04x%04x%04x%04x%06x%06x" % values
7
7
  end
8
8
 
9
+ def self.parse_time(string)
10
+ parsed_time = Date._parse string
11
+ Time.gm *[:year, :mon, :mday, :hour, :min, :sec].collect{ |k| parsed_time[k] }
12
+ end
13
+
9
14
  def self.formatted_path(path = nil, opts = {})
10
15
  if opts.empty?
11
16
  CGI.escape path.to_s
data/test/unit/divan.rb CHANGED
@@ -20,9 +20,14 @@ class ViewedModel < Divan::Models::ProofOfConcept
20
20
  end
21
21
 
22
22
  class ProofOfConcept < Divan::Models::ProofOfConcept
23
+ strategy(:add_conflicted_field_and_keep_this) { |here, in_db| here.conflict = "conflicted!"}
24
+
23
25
  property :first_name
24
26
  end
25
27
 
28
+ Divan::Models::ProofOfConcept.database.create unless Divan::Models::ProofOfConcept.database.exists?
29
+ Divan::Models::ProofOfConcept.database.create_views
30
+
26
31
  class TestDivan < Test::Unit::TestCase
27
32
  def test_dynamic_model
28
33
  m = Divan::Model(:teste)
@@ -132,7 +137,6 @@ class TestDivan < Test::Unit::TestCase
132
137
  10.times do |n|
133
138
  assert ProofOfConcept.new( :value => n ).save
134
139
  end
135
- # assert_equal Divan[:proof_of_concept].views.count, 5
136
140
  assert Divan[:proof_of_concept].create_views
137
141
  assert_equal ProofOfConcept.delete_all(:limit => 6), 6
138
142
  assert_equal ProofOfConcept.all.first.class, ProofOfConcept
@@ -142,6 +146,7 @@ class TestDivan < Test::Unit::TestCase
142
146
  end
143
147
 
144
148
  def test_bulk_create
149
+ assert ProofOfConcept.top_level_model?
145
150
  assert ProofOfConcept.delete_all
146
151
  params = 10.times.map do |n|
147
152
  {:number => n, :double => 2*n}
@@ -195,4 +200,62 @@ class TestDivan < Test::Unit::TestCase
195
200
  assert_equal ViewedModel.delete_all, 1
196
201
  assert_equal ProofOfConcept.delete_all, 2
197
202
  end
203
+
204
+ def test_first_wins_save_strategy
205
+ first = ProofOfConcept.create :test => 123
206
+ last = ProofOfConcept.new :id => first.id, :test => 321
207
+ assert last.save(:first_wins)
208
+ assert_equal ProofOfConcept.find(first.id).test, 123
209
+ assert_equal last.test, 123
210
+ end
211
+
212
+ def test_last_wins_save_strategy
213
+ first = ProofOfConcept.create :test => 123
214
+ last = ProofOfConcept.new :id => first.id, :test => 321
215
+ assert last.save(:last_wins)
216
+ assert_equal ProofOfConcept.find(first.id).test, 321
217
+ assert_equal last.test, 321
218
+ end
219
+
220
+ def test_merge_save_strategy
221
+ first = ProofOfConcept.create :test_one => 1, :test => 'Working'
222
+ last = ProofOfConcept.new :id => first.id, :test_one => 123, :test_two => 321
223
+ expected_attributes = first.attributes.merge last.attributes
224
+ assert last.save(:merge)
225
+ assert_equal ProofOfConcept.find(first.id).attributes, expected_attributes
226
+ assert_equal last.attributes, expected_attributes
227
+ end
228
+
229
+ def test_custom_save_strategy
230
+ first = ProofOfConcept.create :amount => 100
231
+ last = ProofOfConcept.new :id => first.id, :amount => 200
232
+ expected_attributes = first.attributes.merge last.attributes
233
+ assert last.save(){ |here, in_database| here.amount += in_database.amount }
234
+ assert_equal ProofOfConcept.find(first.id).amount, 300
235
+ assert_equal last.amount, 300
236
+ assert first.save(){ |here, in_database| here.amount > in_database.amount }
237
+ assert_equal ProofOfConcept.find(first.id).amount, 300
238
+ assert_equal first.amount, 300
239
+ end
240
+
241
+ def test_finding_an_older_revision
242
+ older = ProofOfConcept.create :save_counts => 0
243
+ newer = ProofOfConcept.find older.id
244
+ newer.save_counts += 1
245
+ assert newer.save
246
+ older_back = ProofOfConcept.find older.id, :rev => older.rev
247
+ assert older_back
248
+ assert_equal older_back.rev, older.rev
249
+ end
250
+
251
+ def test_named_custom_strategy
252
+ first = ProofOfConcept.create :test => 123
253
+ last = ProofOfConcept.new :id => first.id, :test => 321
254
+ assert last.save(:add_conflicted_field_and_keep_this)
255
+ assert_equal ProofOfConcept.find(first.id).test, 321
256
+ assert_equal last.test, 321
257
+ assert_equal ProofOfConcept.find(first.id).conflict, 'conflicted!'
258
+ viewed = ViewedModel.new :id => first.id, :will => 'raise errors'
259
+ assert_raise(Divan::DocumentConflict){ viewed.save }
260
+ end
198
261
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: divan
3
3
  version: !ruby/object:Gem::Version
4
- hash: 31
4
+ hash: 29
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 2
10
- version: 0.1.2
9
+ - 3
10
+ version: 0.1.3
11
11
  platform: ruby
12
12
  authors:
13
13
  - Dalton Pinto
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-10-18 00:00:00 -02:00
18
+ date: 2010-10-25 00:00:00 -02:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -56,9 +56,10 @@ executables: []
56
56
  extensions: []
57
57
 
58
58
  extra_rdoc_files:
59
- - README
59
+ - README.rdoc
60
60
  files:
61
- - README
61
+ - .gitignore
62
+ - README.rdoc
62
63
  - Rakefile.rb
63
64
  - VERSION
64
65
  - init.rb
@@ -66,6 +67,7 @@ files:
66
67
  - lib/divan/base.rb
67
68
  - lib/divan/database.rb
68
69
  - lib/divan/models/base.rb
70
+ - lib/divan/models/revision.rb
69
71
  - lib/divan/utils.rb
70
72
  - test/test_helper.rb
71
73
  - test/unit/divan.rb
data/README DELETED
@@ -1,3 +0,0 @@
1
- Divan, a Ruby CouchDB client for insane people
2
-
3
- TODO: README, documentation and other details