divan 0.1.2 → 0.1.3

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