couchrest 0.12.4 → 0.23

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/README.md +33 -8
  2. data/Rakefile +11 -2
  3. data/examples/model/example.rb +19 -13
  4. data/lib/couchrest.rb +70 -11
  5. data/lib/couchrest/core/database.rb +121 -62
  6. data/lib/couchrest/core/design.rb +7 -17
  7. data/lib/couchrest/core/document.rb +42 -30
  8. data/lib/couchrest/core/response.rb +16 -0
  9. data/lib/couchrest/core/server.rb +47 -10
  10. data/lib/couchrest/helper/upgrade.rb +51 -0
  11. data/lib/couchrest/mixins.rb +4 -0
  12. data/lib/couchrest/mixins/attachments.rb +31 -0
  13. data/lib/couchrest/mixins/callbacks.rb +483 -0
  14. data/lib/couchrest/mixins/class_proxy.rb +108 -0
  15. data/lib/couchrest/mixins/design_doc.rb +90 -0
  16. data/lib/couchrest/mixins/document_queries.rb +44 -0
  17. data/lib/couchrest/mixins/extended_attachments.rb +68 -0
  18. data/lib/couchrest/mixins/extended_document_mixins.rb +7 -0
  19. data/lib/couchrest/mixins/properties.rb +129 -0
  20. data/lib/couchrest/mixins/validation.rb +242 -0
  21. data/lib/couchrest/mixins/views.rb +169 -0
  22. data/lib/couchrest/monkeypatches.rb +81 -6
  23. data/lib/couchrest/more/casted_model.rb +28 -0
  24. data/lib/couchrest/more/extended_document.rb +215 -0
  25. data/lib/couchrest/more/property.rb +40 -0
  26. data/lib/couchrest/support/blank.rb +42 -0
  27. data/lib/couchrest/support/class.rb +176 -0
  28. data/lib/couchrest/validation/auto_validate.rb +163 -0
  29. data/lib/couchrest/validation/contextual_validators.rb +78 -0
  30. data/lib/couchrest/validation/validation_errors.rb +118 -0
  31. data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
  32. data/lib/couchrest/validation/validators/confirmation_validator.rb +99 -0
  33. data/lib/couchrest/validation/validators/format_validator.rb +117 -0
  34. data/lib/couchrest/validation/validators/formats/email.rb +66 -0
  35. data/lib/couchrest/validation/validators/formats/url.rb +43 -0
  36. data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
  37. data/lib/couchrest/validation/validators/length_validator.rb +134 -0
  38. data/lib/couchrest/validation/validators/method_validator.rb +89 -0
  39. data/lib/couchrest/validation/validators/numeric_validator.rb +104 -0
  40. data/lib/couchrest/validation/validators/required_field_validator.rb +109 -0
  41. data/spec/couchrest/core/database_spec.rb +189 -124
  42. data/spec/couchrest/core/design_spec.rb +13 -6
  43. data/spec/couchrest/core/document_spec.rb +231 -177
  44. data/spec/couchrest/core/server_spec.rb +35 -0
  45. data/spec/couchrest/helpers/pager_spec.rb +1 -1
  46. data/spec/couchrest/more/casted_extended_doc_spec.rb +40 -0
  47. data/spec/couchrest/more/casted_model_spec.rb +98 -0
  48. data/spec/couchrest/more/extended_doc_attachment_spec.rb +130 -0
  49. data/spec/couchrest/more/extended_doc_spec.rb +509 -0
  50. data/spec/couchrest/more/extended_doc_subclass_spec.rb +98 -0
  51. data/spec/couchrest/more/extended_doc_view_spec.rb +355 -0
  52. data/spec/couchrest/more/property_spec.rb +136 -0
  53. data/spec/fixtures/more/article.rb +34 -0
  54. data/spec/fixtures/more/card.rb +20 -0
  55. data/spec/fixtures/more/course.rb +14 -0
  56. data/spec/fixtures/more/event.rb +6 -0
  57. data/spec/fixtures/more/invoice.rb +17 -0
  58. data/spec/fixtures/more/person.rb +8 -0
  59. data/spec/fixtures/more/question.rb +6 -0
  60. data/spec/fixtures/more/service.rb +12 -0
  61. data/spec/spec_helper.rb +13 -7
  62. metadata +58 -4
  63. data/lib/couchrest/core/model.rb +0 -613
  64. data/spec/couchrest/core/model_spec.rb +0 -855
@@ -0,0 +1,136 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
2
+ require File.join(FIXTURE_PATH, 'more', 'card')
3
+ require File.join(FIXTURE_PATH, 'more', 'invoice')
4
+ require File.join(FIXTURE_PATH, 'more', 'service')
5
+ require File.join(FIXTURE_PATH, 'more', 'event')
6
+
7
+
8
+ describe "ExtendedDocument properties" do
9
+
10
+ before(:each) do
11
+ reset_test_db!
12
+ @card = Card.new(:first_name => "matt")
13
+ end
14
+
15
+ it "should be accessible from the object" do
16
+ @card.properties.should be_an_instance_of(Array)
17
+ @card.properties.map{|p| p.name}.should include("first_name")
18
+ end
19
+
20
+ it "should let you access a property value (getter)" do
21
+ @card.first_name.should == "matt"
22
+ end
23
+
24
+ it "should let you set a property value (setter)" do
25
+ @card.last_name = "Aimonetti"
26
+ @card.last_name.should == "Aimonetti"
27
+ end
28
+
29
+ it "should not let you set a property value if it's read only" do
30
+ lambda{@card.read_only_value = "test"}.should raise_error
31
+ end
32
+
33
+ it "should let you use an alias for an attribute" do
34
+ @card.last_name = "Aimonetti"
35
+ @card.family_name.should == "Aimonetti"
36
+ @card.family_name.should == @card.last_name
37
+ end
38
+
39
+ it "should be auto timestamped" do
40
+ @card.created_at.should be_nil
41
+ @card.updated_at.should be_nil
42
+ @card.save.should be_true
43
+ @card.created_at.should_not be_nil
44
+ @card.updated_at.should_not be_nil
45
+ end
46
+
47
+ describe "validation" do
48
+ before(:each) do
49
+ @invoice = Invoice.new(:client_name => "matt", :employee_name => "Chris", :location => "San Diego, CA")
50
+ end
51
+
52
+ it "should be able to be validated" do
53
+ @card.valid?.should == true
54
+ end
55
+
56
+ it "should let you validate the presence of an attribute" do
57
+ @card.first_name = nil
58
+ @card.should_not be_valid
59
+ @card.errors.should_not be_empty
60
+ @card.errors.on(:first_name).should == ["First name must not be blank"]
61
+ end
62
+
63
+ it "should let you look up errors for a field by a string name" do
64
+ @card.first_name = nil
65
+ @card.should_not be_valid
66
+ @card.errors.on('first_name').should == ["First name must not be blank"]
67
+ end
68
+
69
+ it "should validate the presence of 2 attributes" do
70
+ @invoice.clear
71
+ @invoice.should_not be_valid
72
+ @invoice.errors.should_not be_empty
73
+ @invoice.errors.on(:client_name).first.should == "Client name must not be blank"
74
+ @invoice.errors.on(:employee_name).should_not be_empty
75
+ end
76
+
77
+ it "should let you set an error message" do
78
+ @invoice.location = nil
79
+ @invoice.valid?
80
+ @invoice.errors.on(:location).should == ["Hey stupid!, you forgot the location"]
81
+ end
82
+
83
+ it "should validate before saving" do
84
+ @invoice.location = nil
85
+ @invoice.should_not be_valid
86
+ @invoice.save.should be_false
87
+ @invoice.should be_new_document
88
+ end
89
+ end
90
+
91
+ describe "autovalidation" do
92
+ before(:each) do
93
+ @service = Service.new(:name => "Coumpound analysis", :price => 3_000)
94
+ end
95
+
96
+ it "should be valid" do
97
+ @service.should be_valid
98
+ end
99
+
100
+ it "should not respond to properties not setup" do
101
+ @service.respond_to?(:client_name).should be_false
102
+ end
103
+
104
+ describe "property :name, :length => 4...20" do
105
+
106
+ it "should autovalidate the presence when length is set" do
107
+ @service.name = nil
108
+ @service.should_not be_valid
109
+ @service.errors.should_not be_nil
110
+ @service.errors.on(:name).first.should == "Name must be between 4 and 19 characters long"
111
+ end
112
+
113
+ it "should autovalidate the correct length" do
114
+ @service.name = "a"
115
+ @service.should_not be_valid
116
+ @service.errors.should_not be_nil
117
+ @service.errors.on(:name).first.should == "Name must be between 4 and 19 characters long"
118
+ end
119
+ end
120
+ end
121
+
122
+ describe "casting" do
123
+ describe "cast keys to any type" do
124
+ before(:all) do
125
+ event_doc = { :subject => "Some event", :occurs_at => Time.now }
126
+ e = Event.database.save_doc event_doc
127
+
128
+ @event = Event.get e['id']
129
+ end
130
+ it "should cast created_at to Time" do
131
+ @event['occurs_at'].should be_an_instance_of(Time)
132
+ end
133
+ end
134
+ end
135
+
136
+ end
@@ -0,0 +1,34 @@
1
+ class Article < CouchRest::ExtendedDocument
2
+ use_database TEST_SERVER.default_database
3
+ unique_id :slug
4
+
5
+ view_by :date, :descending => true
6
+ view_by :user_id, :date
7
+
8
+ view_by :tags,
9
+ :map =>
10
+ "function(doc) {
11
+ if (doc['couchrest-type'] == 'Article' && doc.tags) {
12
+ doc.tags.forEach(function(tag){
13
+ emit(tag, 1);
14
+ });
15
+ }
16
+ }",
17
+ :reduce =>
18
+ "function(keys, values, rereduce) {
19
+ return sum(values);
20
+ }"
21
+
22
+ property :date
23
+ property :slug, :read_only => true
24
+ property :title
25
+ property :tags
26
+
27
+ timestamps!
28
+
29
+ save_callback :before, :generate_slug_from_title
30
+
31
+ def generate_slug_from_title
32
+ self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'') if new_document?
33
+ end
34
+ end
@@ -0,0 +1,20 @@
1
+ class Card < CouchRest::ExtendedDocument
2
+ # Include the validation module to get access to the validation methods
3
+ include CouchRest::Validation
4
+ # set the auto_validation before defining the properties
5
+ auto_validate!
6
+
7
+ # Set the default database to use
8
+ use_database TEST_SERVER.default_database
9
+
10
+ # Official Schema
11
+ property :first_name
12
+ property :last_name, :alias => :family_name
13
+ property :read_only_value, :read_only => true
14
+
15
+ timestamps!
16
+
17
+ # Validation
18
+ validates_present :first_name
19
+
20
+ end
@@ -0,0 +1,14 @@
1
+ require File.join(FIXTURE_PATH, 'more', 'question')
2
+ require File.join(FIXTURE_PATH, 'more', 'person')
3
+
4
+ class Course < CouchRest::ExtendedDocument
5
+ use_database TEST_SERVER.default_database
6
+
7
+ property :title
8
+ property :questions, :cast_as => ['Question']
9
+ property :professor, :cast_as => 'Person'
10
+ property :final_test_at, :cast_as => 'Time'
11
+
12
+ view_by :title
13
+ view_by :dept, :ducktype => true
14
+ end
@@ -0,0 +1,6 @@
1
+ class Event < CouchRest::ExtendedDocument
2
+ use_database TEST_SERVER.default_database
3
+
4
+ property :subject
5
+ property :occurs_at, :cast_as => 'Time', :send => 'parse'
6
+ end
@@ -0,0 +1,17 @@
1
+ class Invoice < CouchRest::ExtendedDocument
2
+ # Include the validation module to get access to the validation methods
3
+ include CouchRest::Validation
4
+
5
+ # Set the default database to use
6
+ use_database TEST_SERVER.default_database
7
+
8
+ # Official Schema
9
+ property :client_name
10
+ property :employee_name
11
+ property :location
12
+
13
+ # Validation
14
+ validates_present :client_name, :employee_name
15
+ validates_present :location, :message => "Hey stupid!, you forgot the location"
16
+
17
+ end
@@ -0,0 +1,8 @@
1
+ class Person < Hash
2
+ include ::CouchRest::CastedModel
3
+ property :name
4
+
5
+ def last_name
6
+ name.last
7
+ end
8
+ end
@@ -0,0 +1,6 @@
1
+ class Question < Hash
2
+ include ::CouchRest::CastedModel
3
+
4
+ property :q
5
+ property :a
6
+ end
@@ -0,0 +1,12 @@
1
+ class Service < CouchRest::ExtendedDocument
2
+ # Include the validation module to get access to the validation methods
3
+ include CouchRest::Validation
4
+ auto_validate!
5
+ # Set the default database to use
6
+ use_database TEST_SERVER.default_database
7
+
8
+ # Official Schema
9
+ property :name, :length => 4...20
10
+ property :price, :type => Integer
11
+
12
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,20 +1,26 @@
1
1
  require "rubygems"
2
2
  require "spec" # Satisfies Autotest and anyone else not using the Rake tasks
3
3
 
4
- require File.dirname(__FILE__) + '/../lib/couchrest'
4
+ require File.join(File.dirname(__FILE__), '..','lib','couchrest')
5
+ # check the following file to see how to use the spec'd features.
5
6
 
6
7
  unless defined?(FIXTURE_PATH)
7
- FIXTURE_PATH = File.dirname(__FILE__) + '/fixtures'
8
- SCRATCH_PATH = File.dirname(__FILE__) + '/tmp'
8
+ FIXTURE_PATH = File.join(File.dirname(__FILE__), '/fixtures')
9
+ SCRATCH_PATH = File.join(File.dirname(__FILE__), '/tmp')
9
10
 
10
11
  COUCHHOST = "http://127.0.0.1:5984"
11
- TESTDB = 'couchrest-test'
12
+ TESTDB = 'couchrest-test'
13
+ TEST_SERVER = CouchRest.new
14
+ TEST_SERVER.default_database = TESTDB
15
+ end
16
+
17
+ class Basic < CouchRest::ExtendedDocument
18
+ use_database TEST_SERVER.default_database
12
19
  end
13
20
 
14
21
  def reset_test_db!
15
- cr = CouchRest.new(COUCHHOST)
22
+ cr = TEST_SERVER
16
23
  db = cr.database(TESTDB)
17
- db.delete! rescue nil
18
- db = cr.create_db(TESTDB) rescue nin
24
+ db.recreate! rescue nil
19
25
  db
20
26
  end
metadata CHANGED
@@ -1,10 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: couchrest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.4
4
+ version: "0.23"
5
5
  platform: ruby
6
6
  authors:
7
7
  - J. Chris Anderson
8
+ - Matt Aimonetti
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
@@ -82,13 +83,49 @@ files:
82
83
  - lib/couchrest/core/database.rb
83
84
  - lib/couchrest/core/design.rb
84
85
  - lib/couchrest/core/document.rb
85
- - lib/couchrest/core/model.rb
86
+ - lib/couchrest/core/response.rb
86
87
  - lib/couchrest/core/server.rb
87
88
  - lib/couchrest/core/view.rb
88
89
  - lib/couchrest/helper
89
90
  - lib/couchrest/helper/pager.rb
90
91
  - lib/couchrest/helper/streamer.rb
92
+ - lib/couchrest/helper/upgrade.rb
93
+ - lib/couchrest/mixins
94
+ - lib/couchrest/mixins/attachments.rb
95
+ - lib/couchrest/mixins/callbacks.rb
96
+ - lib/couchrest/mixins/class_proxy.rb
97
+ - lib/couchrest/mixins/design_doc.rb
98
+ - lib/couchrest/mixins/document_queries.rb
99
+ - lib/couchrest/mixins/extended_attachments.rb
100
+ - lib/couchrest/mixins/extended_document_mixins.rb
101
+ - lib/couchrest/mixins/properties.rb
102
+ - lib/couchrest/mixins/validation.rb
103
+ - lib/couchrest/mixins/views.rb
104
+ - lib/couchrest/mixins.rb
91
105
  - lib/couchrest/monkeypatches.rb
106
+ - lib/couchrest/more
107
+ - lib/couchrest/more/casted_model.rb
108
+ - lib/couchrest/more/extended_document.rb
109
+ - lib/couchrest/more/property.rb
110
+ - lib/couchrest/support
111
+ - lib/couchrest/support/blank.rb
112
+ - lib/couchrest/support/class.rb
113
+ - lib/couchrest/validation
114
+ - lib/couchrest/validation/auto_validate.rb
115
+ - lib/couchrest/validation/contextual_validators.rb
116
+ - lib/couchrest/validation/validation_errors.rb
117
+ - lib/couchrest/validation/validators
118
+ - lib/couchrest/validation/validators/absent_field_validator.rb
119
+ - lib/couchrest/validation/validators/confirmation_validator.rb
120
+ - lib/couchrest/validation/validators/format_validator.rb
121
+ - lib/couchrest/validation/validators/formats
122
+ - lib/couchrest/validation/validators/formats/email.rb
123
+ - lib/couchrest/validation/validators/formats/url.rb
124
+ - lib/couchrest/validation/validators/generic_validator.rb
125
+ - lib/couchrest/validation/validators/length_validator.rb
126
+ - lib/couchrest/validation/validators/method_validator.rb
127
+ - lib/couchrest/validation/validators/numeric_validator.rb
128
+ - lib/couchrest/validation/validators/required_field_validator.rb
92
129
  - lib/couchrest.rb
93
130
  - spec/couchrest
94
131
  - spec/couchrest/core
@@ -96,15 +133,32 @@ files:
96
133
  - spec/couchrest/core/database_spec.rb
97
134
  - spec/couchrest/core/design_spec.rb
98
135
  - spec/couchrest/core/document_spec.rb
99
- - spec/couchrest/core/model_spec.rb
136
+ - spec/couchrest/core/server_spec.rb
100
137
  - spec/couchrest/helpers
101
138
  - spec/couchrest/helpers/pager_spec.rb
102
139
  - spec/couchrest/helpers/streamer_spec.rb
140
+ - spec/couchrest/more
141
+ - spec/couchrest/more/casted_extended_doc_spec.rb
142
+ - spec/couchrest/more/casted_model_spec.rb
143
+ - spec/couchrest/more/extended_doc_attachment_spec.rb
144
+ - spec/couchrest/more/extended_doc_spec.rb
145
+ - spec/couchrest/more/extended_doc_subclass_spec.rb
146
+ - spec/couchrest/more/extended_doc_view_spec.rb
147
+ - spec/couchrest/more/property_spec.rb
103
148
  - spec/fixtures
104
149
  - spec/fixtures/attachments
105
150
  - spec/fixtures/attachments/couchdb.png
106
151
  - spec/fixtures/attachments/README
107
152
  - spec/fixtures/attachments/test.html
153
+ - spec/fixtures/more
154
+ - spec/fixtures/more/article.rb
155
+ - spec/fixtures/more/card.rb
156
+ - spec/fixtures/more/course.rb
157
+ - spec/fixtures/more/event.rb
158
+ - spec/fixtures/more/invoice.rb
159
+ - spec/fixtures/more/person.rb
160
+ - spec/fixtures/more/question.rb
161
+ - spec/fixtures/more/service.rb
108
162
  - spec/fixtures/views
109
163
  - spec/fixtures/views/lib.js
110
164
  - spec/fixtures/views/test_view
@@ -138,7 +192,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
138
192
  requirements: []
139
193
 
140
194
  rubyforge_project:
141
- rubygems_version: 1.2.0
195
+ rubygems_version: 1.3.1
142
196
  signing_key:
143
197
  specification_version: 2
144
198
  summary: Lean and RESTful interface to CouchDB.
@@ -1,613 +0,0 @@
1
- require 'rubygems'
2
- begin
3
- require 'extlib'
4
- rescue
5
- puts "CouchRest::Model requires extlib. This is left out of the gemspec on purpose."
6
- raise
7
- end
8
- require 'digest/md5'
9
- require File.dirname(__FILE__) + '/document'
10
- require 'mime/types'
11
-
12
- # = CouchRest::Model - Document modeling, the CouchDB way
13
- module CouchRest
14
- # = CouchRest::Model - Document modeling, the CouchDB way
15
- #
16
- # CouchRest::Model provides an ORM-like interface for CouchDB documents. It
17
- # avoids all usage of <tt>method_missing</tt>, and tries to strike a balance
18
- # between usability and magic. See CouchRest::Model#view_by for
19
- # documentation about the view-generation system.
20
- #
21
- # ==== Example
22
- #
23
- # This is an example class using CouchRest::Model. It is taken from the
24
- # spec/couchrest/core/model_spec.rb file, which may be even more up to date
25
- # than this example.
26
- #
27
- # class Article < CouchRest::Model
28
- # use_database CouchRest.database!('http://127.0.0.1:5984/couchrest-model-test')
29
- # unique_id :slug
30
- #
31
- # view_by :date, :descending => true
32
- # view_by :user_id, :date
33
- #
34
- # view_by :tags,
35
- # :map =>
36
- # "function(doc) {
37
- # if (doc['couchrest-type'] == 'Article' && doc.tags) {
38
- # doc.tags.forEach(function(tag){
39
- # emit(tag, 1);
40
- # });
41
- # }
42
- # }",
43
- # :reduce =>
44
- # "function(keys, values, rereduce) {
45
- # return sum(values);
46
- # }"
47
- #
48
- # key_writer :date
49
- # key_reader :slug, :created_at, :updated_at
50
- # key_accessor :title, :tags
51
- #
52
- # timestamps!
53
- #
54
- # before(:create, :generate_slug_from_title)
55
- # def generate_slug_from_title
56
- # self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'')
57
- # end
58
- # end
59
- #
60
- # ==== Examples of finding articles with these views:
61
- #
62
- # * All the articles by Barney published in the last 24 hours. Note that we
63
- # use <tt>{}</tt> as a special value that sorts after all strings,
64
- # numbers, and arrays.
65
- #
66
- # Article.by_user_id_and_date :startkey => ["barney", Time.now - 24 * 3600], :endkey => ["barney", {}]
67
- #
68
- # * The most recent 20 articles. Remember that the <tt>view_by :date</tt>
69
- # has the default option <tt>:descending => true</tt>.
70
- #
71
- # Article.by_date :limit => 20
72
- #
73
- # * The raw CouchDB view reduce result for the custom <tt>:tags</tt> view.
74
- # In this case we'll get a count of the number of articles tagged "ruby".
75
- #
76
- # Article.by_tags :key => "ruby", :reduce => true
77
- #
78
- class Model < Document
79
-
80
- # instantiates the hash by converting all the keys to strings.
81
- def initialize keys = {}
82
- super(keys)
83
- apply_defaults
84
- cast_keys
85
- unless self['_id'] && self['_rev']
86
- self['couchrest-type'] = self.class.to_s
87
- end
88
- end
89
-
90
- # this is the CouchRest::Database that model classes will use unless
91
- # they override it with <tt>use_database</tt>
92
- cattr_accessor :default_database
93
-
94
- class_inheritable_accessor :casts
95
- class_inheritable_accessor :default_obj
96
- class_inheritable_accessor :class_database
97
- class_inheritable_accessor :design_doc
98
- class_inheritable_accessor :design_doc_slug_cache
99
- class_inheritable_accessor :design_doc_fresh
100
-
101
- class << self
102
- # override the CouchRest::Model-wide default_database
103
- def use_database db
104
- self.class_database = db
105
- end
106
-
107
- # returns the CouchRest::Database instance that this class uses
108
- def database
109
- self.class_database || CouchRest::Model.default_database
110
- end
111
-
112
- # Load a document from the database by id
113
- def get id
114
- doc = database.get id
115
- new(doc)
116
- end
117
-
118
- # Load all documents that have the "couchrest-type" field equal to the
119
- # name of the current class. Take the standard set of
120
- # CouchRest::Database#view options.
121
- def all opts = {}, &block
122
- self.design_doc ||= Design.new(default_design_doc)
123
- unless design_doc_fresh
124
- refresh_design_doc
125
- end
126
- view :all, opts, &block
127
- end
128
-
129
- # Load the first document that have the "couchrest-type" field equal to
130
- # the name of the current class.
131
- #
132
- # ==== Returns
133
- # Object:: The first object instance available
134
- # or
135
- # Nil:: if no instances available
136
- #
137
- # ==== Parameters
138
- # opts<Hash>::
139
- # View options, see <tt>CouchRest::Database#view</tt> options for more info.
140
- def first opts = {}
141
- first_instance = self.all(opts.merge!(:limit => 1))
142
- first_instance.empty? ? nil : first_instance.first
143
- end
144
-
145
- # Cast a field as another class. The class must be happy to have the
146
- # field's primitive type as the argument to it's constuctur. Classes
147
- # which inherit from CouchRest::Model are happy to act as sub-objects
148
- # for any fields that are stored in JSON as object (and therefore are
149
- # parsed from the JSON as Ruby Hashes).
150
- #
151
- # Example:
152
- #
153
- # class Post < CouchRest::Model
154
- #
155
- # key_accessor :title, :body, :author
156
- #
157
- # cast :author, :as => 'Author'
158
- #
159
- # end
160
- #
161
- # post.author.class #=> Author
162
- #
163
- # Using the same example, if a Post should have many Comments, we
164
- # would declare it like this:
165
- #
166
- # class Post < CouchRest::Model
167
- #
168
- # key_accessor :title, :body, :author, comments
169
- #
170
- # cast :author, :as => 'Author'
171
- # cast :comments, :as => ['Comment']
172
- #
173
- # end
174
- #
175
- # post.author.class #=> Author
176
- # post.comments.class #=> Array
177
- # post.comments.first #=> Comment
178
- #
179
- def cast field, opts = {}
180
- self.casts ||= {}
181
- self.casts[field.to_s] = opts
182
- end
183
-
184
- # Defines methods for reading and writing from fields in the document.
185
- # Uses key_writer and key_reader internally.
186
- def key_accessor *keys
187
- key_writer *keys
188
- key_reader *keys
189
- end
190
-
191
- # For each argument key, define a method <tt>key=</tt> that sets the
192
- # corresponding field on the CouchDB document.
193
- def key_writer *keys
194
- keys.each do |method|
195
- key = method.to_s
196
- define_method "#{method}=" do |value|
197
- self[key] = value
198
- end
199
- end
200
- end
201
-
202
- # For each argument key, define a method <tt>key</tt> that reads the
203
- # corresponding field on the CouchDB document.
204
- def key_reader *keys
205
- keys.each do |method|
206
- key = method.to_s
207
- define_method method do
208
- self[key]
209
- end
210
- end
211
- end
212
-
213
- def default
214
- self.default_obj
215
- end
216
-
217
- def set_default hash
218
- self.default_obj = hash
219
- end
220
-
221
- # Automatically set <tt>updated_at</tt> and <tt>created_at</tt> fields
222
- # on the document whenever saving occurs. CouchRest uses a pretty
223
- # decent time format by default. See Time#to_json
224
- def timestamps!
225
- before(:save) do
226
- self['updated_at'] = Time.now
227
- self['created_at'] = self['updated_at'] if new_document?
228
- end
229
- end
230
-
231
- # Name a method that will be called before the document is first saved,
232
- # which returns a string to be used for the document's <tt>_id</tt>.
233
- # Because CouchDB enforces a constraint that each id must be unique,
234
- # this can be used to enforce eg: uniq usernames. Note that this id
235
- # must be globally unique across all document types which share a
236
- # database, so if you'd like to scope uniqueness to this class, you
237
- # should use the class name as part of the unique id.
238
- def unique_id method = nil, &block
239
- if method
240
- define_method :set_unique_id do
241
- self['_id'] ||= self.send(method)
242
- end
243
- elsif block
244
- define_method :set_unique_id do
245
- uniqid = block.call(self)
246
- raise ArgumentError, "unique_id block must not return nil" if uniqid.nil?
247
- self['_id'] ||= uniqid
248
- end
249
- end
250
- end
251
-
252
- # Define a CouchDB view. The name of the view will be the concatenation
253
- # of <tt>by</tt> and the keys joined by <tt>_and_</tt>
254
- #
255
- # ==== Example views:
256
- #
257
- # class Post
258
- # # view with default options
259
- # # query with Post.by_date
260
- # view_by :date, :descending => true
261
- #
262
- # # view with compound sort-keys
263
- # # query with Post.by_user_id_and_date
264
- # view_by :user_id, :date
265
- #
266
- # # view with custom map/reduce functions
267
- # # query with Post.by_tags :reduce => true
268
- # view_by :tags,
269
- # :map =>
270
- # "function(doc) {
271
- # if (doc['couchrest-type'] == 'Post' && doc.tags) {
272
- # doc.tags.forEach(function(tag){
273
- # emit(doc.tag, 1);
274
- # });
275
- # }
276
- # }",
277
- # :reduce =>
278
- # "function(keys, values, rereduce) {
279
- # return sum(values);
280
- # }"
281
- # end
282
- #
283
- # <tt>view_by :date</tt> will create a view defined by this Javascript
284
- # function:
285
- #
286
- # function(doc) {
287
- # if (doc['couchrest-type'] == 'Post' && doc.date) {
288
- # emit(doc.date, null);
289
- # }
290
- # }
291
- #
292
- # It can be queried by calling <tt>Post.by_date</tt> which accepts all
293
- # valid options for CouchRest::Database#view. In addition, calling with
294
- # the <tt>:raw => true</tt> option will return the view rows
295
- # themselves. By default <tt>Post.by_date</tt> will return the
296
- # documents included in the generated view.
297
- #
298
- # CouchRest::Database#view options can be applied at view definition
299
- # time as defaults, and they will be curried and used at view query
300
- # time. Or they can be overridden at query time.
301
- #
302
- # Custom views can be queried with <tt>:reduce => true</tt> to return
303
- # reduce results. The default for custom views is to query with
304
- # <tt>:reduce => false</tt>.
305
- #
306
- # Views are generated (on a per-model basis) lazily on first-access.
307
- # This means that if you are deploying changes to a view, the views for
308
- # that model won't be available until generation is complete. This can
309
- # take some time with large databases. Strategies are in the works.
310
- #
311
- # To understand the capabilities of this view system more compeletly,
312
- # it is recommended that you read the RSpec file at
313
- # <tt>spec/core/model_spec.rb</tt>.
314
-
315
- def view_by *keys
316
- self.design_doc ||= Design.new(default_design_doc)
317
- opts = keys.pop if keys.last.is_a?(Hash)
318
- opts ||= {}
319
- ducktype = opts.delete(:ducktype)
320
- unless ducktype || opts[:map]
321
- opts[:guards] ||= []
322
- opts[:guards].push "(doc['couchrest-type'] == '#{self.to_s}')"
323
- end
324
- keys.push opts
325
- self.design_doc.view_by(*keys)
326
- self.design_doc_fresh = false
327
- end
328
-
329
- def method_missing m, *args
330
- if has_view?(m)
331
- query = args.shift || {}
332
- view(m, query, *args)
333
- else
334
- super
335
- end
336
- end
337
-
338
- # returns stored defaults if the there is a view named this in the design doc
339
- def has_view?(view)
340
- view = view.to_s
341
- design_doc && design_doc['views'] && design_doc['views'][view]
342
- end
343
-
344
- # Dispatches to any named view.
345
- def view name, query={}, &block
346
- unless design_doc_fresh
347
- refresh_design_doc
348
- end
349
- query[:raw] = true if query[:reduce]
350
- raw = query.delete(:raw)
351
- fetch_view_with_docs(name, query, raw, &block)
352
- end
353
-
354
- def all_design_doc_versions
355
- database.documents :startkey => "_design/#{self.to_s}-",
356
- :endkey => "_design/#{self.to_s}-\u9999"
357
- end
358
-
359
- # Deletes any non-current design docs that were created by this class.
360
- # Running this when you're deployed version of your application is steadily
361
- # and consistently using the latest code, is the way to clear out old design
362
- # docs. Running it to early could mean that live code has to regenerate
363
- # potentially large indexes.
364
- def cleanup_design_docs!
365
- ddocs = all_design_doc_versions
366
- ddocs["rows"].each do |row|
367
- if (row['id'] != design_doc_id)
368
- database.delete({
369
- "_id" => row['id'],
370
- "_rev" => row['value']['rev']
371
- })
372
- end
373
- end
374
- end
375
-
376
- private
377
-
378
- def fetch_view_with_docs name, opts, raw=false, &block
379
- if raw
380
- fetch_view name, opts, &block
381
- else
382
- begin
383
- view = fetch_view name, opts.merge({:include_docs => true}), &block
384
- view['rows'].collect{|r|new(r['doc'])} if view['rows']
385
- rescue
386
- # fallback for old versions of couchdb that don't
387
- # have include_docs support
388
- view = fetch_view name, opts, &block
389
- view['rows'].collect{|r|new(database.get(r['id']))} if view['rows']
390
- end
391
- end
392
- end
393
-
394
- def fetch_view view_name, opts, &block
395
- retryable = true
396
- begin
397
- design_doc.view(view_name, opts, &block)
398
- # the design doc could have been deleted by a rouge process
399
- rescue RestClient::ResourceNotFound => e
400
- if retryable
401
- refresh_design_doc
402
- retryable = false
403
- retry
404
- else
405
- raise e
406
- end
407
- end
408
- end
409
-
410
- def design_doc_id
411
- "_design/#{design_doc_slug}"
412
- end
413
-
414
- def design_doc_slug
415
- return design_doc_slug_cache if design_doc_slug_cache && design_doc_fresh
416
- funcs = []
417
- design_doc['views'].each do |name, view|
418
- funcs << "#{name}/#{view['map']}#{view['reduce']}"
419
- end
420
- md5 = Digest::MD5.hexdigest(funcs.sort.join(''))
421
- self.design_doc_slug_cache = "#{self.to_s}-#{md5}"
422
- end
423
-
424
- def default_design_doc
425
- {
426
- "language" => "javascript",
427
- "views" => {
428
- 'all' => {
429
- 'map' => "function(doc) {
430
- if (doc['couchrest-type'] == '#{self.to_s}') {
431
- emit(null,null);
432
- }
433
- }"
434
- }
435
- }
436
- }
437
- end
438
-
439
- def refresh_design_doc
440
- did = design_doc_id
441
- saved = database.get(did) rescue nil
442
- if saved
443
- design_doc['views'].each do |name, view|
444
- saved['views'][name] = view
445
- end
446
- database.save(saved)
447
- self.design_doc = saved
448
- else
449
- design_doc['_id'] = did
450
- design_doc.delete('_rev')
451
- design_doc.database = database
452
- design_doc.save
453
- end
454
- self.design_doc_fresh = true
455
- end
456
-
457
- end # class << self
458
-
459
- # returns the database used by this model's class
460
- def database
461
- self.class.database
462
- end
463
-
464
- # Takes a hash as argument, and applies the values by using writer methods
465
- # for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
466
- # missing. In case of error, no attributes are changed.
467
- def update_attributes_without_saving hash
468
- hash.each do |k, v|
469
- raise NoMethodError, "#{k}= method not available, use key_accessor or key_writer :#{k}" unless self.respond_to?("#{k}=")
470
- end
471
- hash.each do |k, v|
472
- self.send("#{k}=",v)
473
- end
474
- end
475
-
476
- # Takes a hash as argument, and applies the values by using writer methods
477
- # for each key. Raises a NoMethodError if the corresponding methods are
478
- # missing. In case of error, no attributes are changed.
479
- def update_attributes hash
480
- update_attributes_without_saving hash
481
- save
482
- end
483
-
484
- # for compatibility with old-school frameworks
485
- alias :new_record? :new_document?
486
-
487
- # Overridden to set the unique ID.
488
- def save bulk = false
489
- set_unique_id if new_document? && self.respond_to?(:set_unique_id)
490
- super(bulk)
491
- end
492
-
493
- # Saves the document to the db using create or update. Raises an exception
494
- # if the document is not saved properly.
495
- def save!
496
- raise "#{self.inspect} failed to save" unless self.save
497
- end
498
-
499
- # Deletes the document from the database. Runs the :destroy callbacks.
500
- # Removes the <tt>_id</tt> and <tt>_rev</tt> fields, preparing the
501
- # document to be saved to a new <tt>_id</tt>.
502
- def destroy
503
- result = database.delete self
504
- if result['ok']
505
- self['_rev'] = nil
506
- self['_id'] = nil
507
- end
508
- result['ok']
509
- end
510
-
511
- # creates a file attachment to the current doc
512
- def create_attachment(args={})
513
- raise ArgumentError unless args[:file] && args[:name]
514
- return if has_attachment?(args[:name])
515
- self['_attachments'] ||= {}
516
- set_attachment_attr(args)
517
- rescue ArgumentError => e
518
- raise ArgumentError, 'You must specify :file and :name'
519
- end
520
-
521
- # reads the data from an attachment
522
- def read_attachment(attachment_name)
523
- Base64.decode64(database.fetch_attachment(self.id, attachment_name))
524
- end
525
-
526
- # modifies a file attachment on the current doc
527
- def update_attachment(args={})
528
- raise ArgumentError unless args[:file] && args[:name]
529
- return unless has_attachment?(args[:name])
530
- delete_attachment(args[:name])
531
- set_attachment_attr(args)
532
- rescue ArgumentError => e
533
- raise ArgumentError, 'You must specify :file and :name'
534
- end
535
-
536
- # deletes a file attachment from the current doc
537
- def delete_attachment(attachment_name)
538
- return unless self['_attachments']
539
- self['_attachments'].delete attachment_name
540
- end
541
-
542
- # returns true if attachment_name exists
543
- def has_attachment?(attachment_name)
544
- !!(self['_attachments'] && self['_attachments'][attachment_name] && !self['_attachments'][attachment_name].empty?)
545
- end
546
-
547
- # returns URL to fetch the attachment from
548
- def attachment_url(attachment_name)
549
- return unless has_attachment?(attachment_name)
550
- "#{database.root}/#{self.id}/#{attachment_name}"
551
- end
552
-
553
- private
554
-
555
- def apply_defaults
556
- return unless new_document?
557
- if self.class.default
558
- self.class.default.each do |k,v|
559
- unless self.key?(k.to_s)
560
- if v.class == Proc
561
- self[k.to_s] = v.call
562
- else
563
- self[k.to_s] = Marshal.load(Marshal.dump(v))
564
- end
565
- end
566
- end
567
- end
568
- end
569
-
570
- def cast_keys
571
- return unless self.class.casts
572
- # TODO move the argument checking to the cast method for early crashes
573
- self.class.casts.each do |k,v|
574
- next unless self[k]
575
- target = v[:as]
576
- v[:send] || 'new'
577
- if target.is_a?(Array)
578
- klass = ::Extlib::Inflection.constantize(target[0])
579
- self[k] = self[k].collect do |value|
580
- (!v[:send] && klass == Time) ? Time.parse(value) : klass.send((v[:send] || 'new'), value)
581
- end
582
- else
583
- self[k] = if (!v[:send] && target == 'Time')
584
- Time.parse(self[k])
585
- else
586
- ::Extlib::Inflection.constantize(target).send((v[:send] || 'new'), self[k])
587
- end
588
- end
589
- end
590
- end
591
-
592
- def encode_attachment(data)
593
- Base64.encode64(data).gsub(/\r|\n/,'')
594
- end
595
-
596
- def get_mime_type(file)
597
- MIME::Types.type_for(file.path).empty? ?
598
- 'text\/plain' : MIME::Types.type_for(file.path).first.content_type.gsub(/\//,'\/')
599
- end
600
-
601
- def set_attachment_attr(args)
602
- content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file])
603
- self['_attachments'][args[:name]] = {
604
- 'content-type' => content_type,
605
- 'data' => encode_attachment(args[:file].read)
606
- }
607
- end
608
-
609
- include ::Extlib::Hook
610
- register_instance_hooks :save, :destroy
611
-
612
- end # class Model
613
- end # module CouchRest