couchrest_model_thought 1.0.0.beta8

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.
Files changed (70) hide show
  1. data/Gemfile +9 -0
  2. data/LICENSE +176 -0
  3. data/README.md +320 -0
  4. data/Rakefile +69 -0
  5. data/THANKS.md +19 -0
  6. data/examples/model/example.rb +144 -0
  7. data/lib/couchrest/model/associations.rb +207 -0
  8. data/lib/couchrest/model/attribute_protection.rb +74 -0
  9. data/lib/couchrest/model/attributes.rb +91 -0
  10. data/lib/couchrest/model/base.rb +111 -0
  11. data/lib/couchrest/model/callbacks.rb +27 -0
  12. data/lib/couchrest/model/casted_array.rb +39 -0
  13. data/lib/couchrest/model/casted_model.rb +68 -0
  14. data/lib/couchrest/model/class_proxy.rb +122 -0
  15. data/lib/couchrest/model/collection.rb +260 -0
  16. data/lib/couchrest/model/design_doc.rb +126 -0
  17. data/lib/couchrest/model/document_queries.rb +82 -0
  18. data/lib/couchrest/model/errors.rb +23 -0
  19. data/lib/couchrest/model/extended_attachments.rb +73 -0
  20. data/lib/couchrest/model/persistence.rb +141 -0
  21. data/lib/couchrest/model/properties.rb +144 -0
  22. data/lib/couchrest/model/property.rb +96 -0
  23. data/lib/couchrest/model/support/couchrest.rb +19 -0
  24. data/lib/couchrest/model/support/hash.rb +9 -0
  25. data/lib/couchrest/model/typecast.rb +170 -0
  26. data/lib/couchrest/model/validations/casted_model.rb +14 -0
  27. data/lib/couchrest/model/validations/locale/en.yml +5 -0
  28. data/lib/couchrest/model/validations/uniqueness.rb +42 -0
  29. data/lib/couchrest/model/validations.rb +68 -0
  30. data/lib/couchrest/model/version.rb +5 -0
  31. data/lib/couchrest/model/views.rb +160 -0
  32. data/lib/couchrest/model.rb +5 -0
  33. data/lib/couchrest_model.rb +53 -0
  34. data/spec/couchrest/assocations_spec.rb +213 -0
  35. data/spec/couchrest/attachment_spec.rb +148 -0
  36. data/spec/couchrest/attribute_protection_spec.rb +153 -0
  37. data/spec/couchrest/base_spec.rb +463 -0
  38. data/spec/couchrest/casted_model_spec.rb +424 -0
  39. data/spec/couchrest/casted_spec.rb +75 -0
  40. data/spec/couchrest/class_proxy_spec.rb +132 -0
  41. data/spec/couchrest/inherited_spec.rb +40 -0
  42. data/spec/couchrest/persistence_spec.rb +409 -0
  43. data/spec/couchrest/property_spec.rb +804 -0
  44. data/spec/couchrest/subclass_spec.rb +99 -0
  45. data/spec/couchrest/validations.rb +85 -0
  46. data/spec/couchrest/view_spec.rb +463 -0
  47. data/spec/fixtures/attachments/README +3 -0
  48. data/spec/fixtures/attachments/couchdb.png +0 -0
  49. data/spec/fixtures/attachments/test.html +11 -0
  50. data/spec/fixtures/base.rb +139 -0
  51. data/spec/fixtures/more/article.rb +35 -0
  52. data/spec/fixtures/more/card.rb +17 -0
  53. data/spec/fixtures/more/cat.rb +19 -0
  54. data/spec/fixtures/more/course.rb +25 -0
  55. data/spec/fixtures/more/event.rb +8 -0
  56. data/spec/fixtures/more/invoice.rb +14 -0
  57. data/spec/fixtures/more/person.rb +9 -0
  58. data/spec/fixtures/more/question.rb +7 -0
  59. data/spec/fixtures/more/service.rb +10 -0
  60. data/spec/fixtures/more/user.rb +22 -0
  61. data/spec/fixtures/views/lib.js +3 -0
  62. data/spec/fixtures/views/test_view/lib.js +3 -0
  63. data/spec/fixtures/views/test_view/only-map.js +4 -0
  64. data/spec/fixtures/views/test_view/test-map.js +3 -0
  65. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  66. data/spec/spec.opts +5 -0
  67. data/spec/spec_helper.rb +48 -0
  68. data/utils/remap.rb +27 -0
  69. data/utils/subset.rb +30 -0
  70. metadata +215 -0
@@ -0,0 +1,42 @@
1
+ module CouchRest
2
+ module Model
3
+ module Validations
4
+
5
+ # Validates if a field is unique
6
+ class UniquenessValidator < ActiveModel::EachValidator
7
+
8
+ # Ensure we have a class available so we can check for a usable view
9
+ # or add one if necessary.
10
+ def setup(klass)
11
+ @klass = klass
12
+ end
13
+
14
+
15
+ def validate_each(document, attribute, value)
16
+ view_name = options[:view].nil? ? "by_#{attribute}" : options[:view]
17
+ # Determine the base of the search
18
+ base = options[:proxy].nil? ? @klass : document.instance_eval(options[:proxy])
19
+
20
+ if base.respond_to?(:has_view?) && !base.has_view?(view_name)
21
+ raise "View #{document.class.name}.#{options[:view]} does not exist!" unless options[:view].nil?
22
+ @klass.view_by attribute
23
+ end
24
+
25
+ docs = base.view(view_name, :key => value, :limit => 2, :include_docs => false)['rows']
26
+ return if docs.empty?
27
+
28
+ unless document.new?
29
+ return if docs.find{|doc| doc['id'] == document.id}
30
+ end
31
+
32
+ if docs.length > 0
33
+ document.errors.add(attribute, :taken, :default => options[:message], :value => value)
34
+ end
35
+ end
36
+
37
+
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,68 @@
1
+ # encoding: utf-8
2
+
3
+ require "couchrest/model/validations/casted_model"
4
+ require "couchrest/model/validations/uniqueness"
5
+
6
+ I18n.load_path << File.join(
7
+ File.dirname(__FILE__), "validations", "locale", "en.yml"
8
+ )
9
+
10
+ module CouchRest
11
+ module Model
12
+
13
+ # Validations may be applied to both Model::Base and Model::CastedModel
14
+ module Validations
15
+ extend ActiveSupport::Concern
16
+ included do
17
+ include ActiveModel::Validations
18
+ end
19
+
20
+
21
+ module ClassMethods
22
+
23
+ # Validates the associated casted model. This method should not be
24
+ # used within your code as it is automatically included when a CastedModel
25
+ # is used inside the model.
26
+ #
27
+ def validates_casted_model(*args)
28
+ validates_with(CastedModelValidator, _merge_attributes(args))
29
+ end
30
+
31
+ # Validates if the field is unique for this type of document. Automatically creates
32
+ # a view if one does not already exist and performs a search for all matching
33
+ # documents.
34
+ #
35
+ # Example:
36
+ #
37
+ # class Person < CouchRest::Model::Base
38
+ # property :title, String
39
+ #
40
+ # validates_uniqueness_of :title
41
+ # end
42
+ #
43
+ # Asside from the standard options, you can specify the name of the view you'd like
44
+ # to use for the search inside the +:view+ option. The following example would search
45
+ # for the code in side the +all+ view, useful for when +unique_id+ is used and you'd
46
+ # like to check before receiving a RestClient Conflict error:
47
+ #
48
+ # validates_uniqueness_of :code, :view => 'all'
49
+ #
50
+ # A +:proxy+ parameter is also accepted if you would
51
+ # like to call a method on the document on which the view should be performed.
52
+ #
53
+ # For Example:
54
+ #
55
+ # # Same as not including proxy:
56
+ # validates_uniqueness_of :title, :proxy => 'class'
57
+ #
58
+ # # Person#company.people provides a proxy object for people
59
+ # validates_uniqueness_of :title, :proxy => 'company.people'
60
+ #
61
+ def validates_uniqueness_of(*args)
62
+ validates_with(UniquenessValidator, _merge_attributes(args))
63
+ end
64
+ end
65
+
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,5 @@
1
+ module CouchRest
2
+ module Model
3
+ VERSION = "1.0.0.beta8".freeze
4
+ end
5
+ end
@@ -0,0 +1,160 @@
1
+ module CouchRest
2
+ module Model
3
+ module Views
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ # Define a CouchDB view. The name of the view will be the concatenation
8
+ # of <tt>by</tt> and the keys joined by <tt>_and_</tt>
9
+ #
10
+ # ==== Example views:
11
+ #
12
+ # class Post
13
+ # # view with default options
14
+ # # query with Post.by_date
15
+ # view_by :date, :descending => true
16
+ #
17
+ # # view with compound sort-keys
18
+ # # query with Post.by_user_id_and_date
19
+ # view_by :user_id, :date
20
+ #
21
+ # # view with custom map/reduce functions
22
+ # # query with Post.by_tags :reduce => true
23
+ # view_by :tags,
24
+ # :map =>
25
+ # "function(doc) {
26
+ # if (doc['couchrest-type'] == 'Post' && doc.tags) {
27
+ # doc.tags.forEach(function(tag){
28
+ # emit(doc.tag, 1);
29
+ # });
30
+ # }
31
+ # }",
32
+ # :reduce =>
33
+ # "function(keys, values, rereduce) {
34
+ # return sum(values);
35
+ # }"
36
+ # end
37
+ #
38
+ # <tt>view_by :date</tt> will create a view defined by this Javascript
39
+ # function:
40
+ #
41
+ # function(doc) {
42
+ # if (doc['couchrest-type'] == 'Post' && doc.date) {
43
+ # emit(doc.date, null);
44
+ # }
45
+ # }
46
+ #
47
+ # It can be queried by calling <tt>Post.by_date</tt> which accepts all
48
+ # valid options for CouchRest::Database#view. In addition, calling with
49
+ # the <tt>:raw => true</tt> option will return the view rows
50
+ # themselves. By default <tt>Post.by_date</tt> will return the
51
+ # documents included in the generated view.
52
+ #
53
+ # Calling with :database => [instance of CouchRest::Database] will
54
+ # send the query to a specific database, otherwise it will go to
55
+ # the model's default database (use_database)
56
+ #
57
+ # CouchRest::Database#view options can be applied at view definition
58
+ # time as defaults, and they will be curried and used at view query
59
+ # time. Or they can be overridden at query time.
60
+ #
61
+ # Custom views can be queried with <tt>:reduce => true</tt> to return
62
+ # reduce results. The default for custom views is to query with
63
+ # <tt>:reduce => false</tt>.
64
+ #
65
+ # Views are generated (on a per-model basis) lazily on first-access.
66
+ # This means that if you are deploying changes to a view, the views for
67
+ # that model won't be available until generation is complete. This can
68
+ # take some time with large databases. Strategies are in the works.
69
+ #
70
+ # To understand the capabilities of this view system more completely,
71
+ # it is recommended that you read the RSpec file at
72
+ # <tt>spec/couchrest/more/extended_doc_spec.rb</tt>.
73
+
74
+ def view_by(*keys)
75
+ opts = keys.pop if keys.last.is_a?(Hash)
76
+ opts ||= {}
77
+ ducktype = opts.delete(:ducktype)
78
+ unless ducktype || opts[:map]
79
+ opts[:guards] ||= []
80
+ opts[:guards].push "(doc['couchrest-type'] == '#{self.to_s}')"
81
+ end
82
+ keys.push opts
83
+ design_doc.view_by(*keys)
84
+ req_design_doc_refresh
85
+ end
86
+
87
+ # returns stored defaults if there is a view named this in the design doc
88
+ def has_view?(view)
89
+ view = view.to_s
90
+ design_doc && design_doc['views'] && design_doc['views'][view]
91
+ end
92
+
93
+ # Dispatches to any named view.
94
+ def view(name, query={}, &block)
95
+ query = query.dup # Modifications made on copy!
96
+ db = query.delete(:database) || database
97
+ refresh_design_doc(db)
98
+ query[:raw] = true if query[:reduce]
99
+ raw = query.delete(:raw)
100
+ fetch_view_with_docs(db, name, query, raw, &block)
101
+ end
102
+
103
+ # Find the first entry in the view. If the second parameter is a string
104
+ # it will be used as the key for the request, for example:
105
+ #
106
+ # Course.first_from_view('by_teacher', 'Fred')
107
+ #
108
+ # More advanced requests can be performed by providing a hash:
109
+ #
110
+ # Course.first_from_view('by_teacher', :startkey => 'bbb', :endkey => 'eee')
111
+ #
112
+ def first_from_view(name, *args)
113
+ query = {:limit => 1}
114
+ case args.first
115
+ when String, Array
116
+ query.update(args[1]) unless args[1].nil?
117
+ query[:key] = args.first
118
+ when Hash
119
+ query.update(args.first)
120
+ end
121
+ view(name, query).first
122
+ end
123
+
124
+ private
125
+
126
+ def fetch_view_with_docs(db, name, opts, raw=false, &block)
127
+ if raw || (opts.has_key?(:include_docs) && opts[:include_docs] == false)
128
+ fetch_view(db, name, opts, &block)
129
+ else
130
+ if block.nil?
131
+ collection_proxy_for(design_doc, name, opts.merge({:database => db, :include_docs => true}))
132
+ else
133
+ view = fetch_view db, name, opts.merge({:include_docs => true}), &block
134
+ view['rows'].collect{|r|create_from_database(r['doc'])} if view['rows']
135
+ end
136
+ end
137
+ end
138
+
139
+ def fetch_view(db, view_name, opts, &block)
140
+ raise "A view needs a database to operate on (specify :database option, or use_database in the #{self.class} class)" unless db
141
+ retryable = true
142
+ begin
143
+ design_doc.view_on(db, view_name, opts, &block)
144
+ # the design doc may not have been saved yet on this database
145
+ rescue RestClient::ResourceNotFound => e
146
+ if retryable
147
+ save_design_doc(db)
148
+ retryable = false
149
+ retry
150
+ else
151
+ raise e
152
+ end
153
+ end
154
+ end
155
+
156
+ end # module ClassMethods
157
+
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,5 @@
1
+ module CouchRest
2
+ module Model
3
+
4
+ end
5
+ end
@@ -0,0 +1,53 @@
1
+ require 'couchrest'
2
+ require 'couchrest_extended_document'
3
+
4
+ require 'active_support/core_ext'
5
+ require 'active_support/json'
6
+
7
+ require 'active_model'
8
+ require "active_model/callbacks"
9
+ require "active_model/conversion"
10
+ require "active_model/deprecated_error_methods"
11
+ require "active_model/errors"
12
+ require "active_model/naming"
13
+ require "active_model/serialization"
14
+ require "active_model/translation"
15
+ require "active_model/validator"
16
+ require "active_model/validations"
17
+
18
+ require 'mime/types'
19
+ require "enumerator"
20
+ require "time"
21
+ require 'digest/md5'
22
+
23
+ require 'bigdecimal' # used in typecast
24
+ require 'bigdecimal/util' # used in typecast
25
+
26
+ require 'couchrest/model'
27
+ require 'couchrest/model/errors'
28
+ require "couchrest/model/persistence"
29
+ require "couchrest/model/typecast"
30
+ require "couchrest/model/property"
31
+ require "couchrest/model/casted_array"
32
+ require "couchrest/model/properties"
33
+ require "couchrest/model/validations"
34
+ require "couchrest/model/callbacks"
35
+ require "couchrest/model/document_queries"
36
+ require "couchrest/model/views"
37
+ require "couchrest/model/design_doc"
38
+ require "couchrest/model/extended_attachments"
39
+ require "couchrest/model/class_proxy"
40
+ require "couchrest/model/collection"
41
+ require "couchrest/model/attribute_protection"
42
+ require "couchrest/model/attributes"
43
+ require "couchrest/model/associations"
44
+
45
+ # Monkey patches applied to couchrest
46
+ require "couchrest/model/support/couchrest"
47
+ require "couchrest/model/support/hash"
48
+
49
+ # Base libraries
50
+ require "couchrest/model/casted_model"
51
+ require "couchrest/model/base"
52
+
53
+ # Add rails support *after* everything has loaded
@@ -0,0 +1,213 @@
1
+ # encoding: utf-8
2
+ require File.expand_path('../../spec_helper', __FILE__)
3
+
4
+ class Client < CouchRest::Model::Base
5
+ use_database DB
6
+
7
+ property :name
8
+ property :tax_code
9
+ end
10
+
11
+ class SaleEntry < CouchRest::Model::Base
12
+ use_database DB
13
+
14
+ property :description
15
+ property :price
16
+ end
17
+
18
+ class SaleInvoice < CouchRest::Model::Base
19
+ use_database DB
20
+
21
+ belongs_to :client
22
+ belongs_to :alternate_client, :class_name => 'Client', :foreign_key => 'alt_client_id'
23
+
24
+ collection_of :entries, :class_name => 'SaleEntry'
25
+
26
+ property :date, Date
27
+ property :price, Integer
28
+ end
29
+
30
+
31
+ describe "Assocations" do
32
+
33
+ describe "of type belongs to" do
34
+
35
+ before :each do
36
+ @invoice = SaleInvoice.create(:price => "sam", :price => 2000)
37
+ @client = Client.create(:name => "Sam Lown")
38
+ end
39
+
40
+ it "should create a foreign key property with setter and getter" do
41
+ @invoice.properties.find{|p| p.name == 'client_id'}.should_not be_nil
42
+ @invoice.respond_to?(:client_id).should be_true
43
+ @invoice.respond_to?("client_id=").should be_true
44
+ end
45
+
46
+ it "should set the property and provide object when set" do
47
+ @invoice.client = @client
48
+ @invoice.client_id.should eql(@client.id)
49
+ @invoice.client.should eql(@client)
50
+ end
51
+
52
+ it "should set the attribute, save and return" do
53
+ @invoice.client = @client
54
+ @invoice.save
55
+ @invoice = SaleInvoice.get(@invoice.id)
56
+ @invoice.client.id.should eql(@client.id)
57
+ end
58
+
59
+ it "should remove the association if nil is provided" do
60
+ @invoice.client = @client
61
+ @invoice.client = nil
62
+ @invoice.client_id.should be_nil
63
+ end
64
+
65
+ it "should not try to search for association if foreign_key is nil" do
66
+ @invoice.client_id = nil
67
+ Client.should_not_receive(:get)
68
+ @invoice.client
69
+ end
70
+
71
+ it "should raise error if class name does not exist" do
72
+ lambda {
73
+ class TestBadAssoc < CouchRest::Model::Base
74
+ belongs_to :test_bad_item
75
+ end
76
+ }.should raise_error
77
+ end
78
+
79
+ it "should allow override of foreign key" do
80
+ @invoice.respond_to?(:alternate_client).should be_true
81
+ @invoice.respond_to?("alternate_client=").should be_true
82
+ @invoice.properties.find{|p| p.name == 'alt_client_id'}.should_not be_nil
83
+ end
84
+
85
+ it "should allow override of foreign key and save" do
86
+ @invoice.alternate_client = @client
87
+ @invoice.save
88
+ @invoice = SaleInvoice.get(@invoice.id)
89
+ @invoice.alternate_client.id.should eql(@client.id)
90
+ end
91
+
92
+ end
93
+
94
+ describe "of type collection_of" do
95
+
96
+ before(:each) do
97
+ @invoice = SaleInvoice.create(:price => "sam", :price => 2000)
98
+ @entries = [
99
+ SaleEntry.create(:description => 'test line 1', :price => 500),
100
+ SaleEntry.create(:description => 'test line 2', :price => 500),
101
+ SaleEntry.create(:description => 'test line 3', :price => 1000)
102
+ ]
103
+ end
104
+
105
+ it "should create an associated property and collection proxy" do
106
+ @invoice.respond_to?('entry_ids')
107
+ @invoice.respond_to?('entry_ids=')
108
+ @invoice.entries.class.should eql(::CouchRest::CollectionOfProxy)
109
+ end
110
+
111
+ it "should allow replacement of objects" do
112
+ @invoice.entries = @entries
113
+ @invoice.entries.length.should eql(3)
114
+ @invoice.entry_ids.length.should eql(3)
115
+ @invoice.entries.first.should eql(@entries.first)
116
+ @invoice.entry_ids.first.should eql(@entries.first.id)
117
+ end
118
+
119
+ it "should allow ids to be set directly and load entries" do
120
+ @invoice.entry_ids = @entries.collect{|i| i.id}
121
+ @invoice.entries.length.should eql(3)
122
+ @invoice.entries.first.should eql(@entries.first)
123
+ end
124
+
125
+ it "should replace collection if ids replaced" do
126
+ @invoice.entry_ids = @entries.collect{|i| i.id}
127
+ @invoice.entries.length.should eql(3) # load once
128
+ @invoice.entry_ids = @entries[0..1].collect{|i| i.id}
129
+ @invoice.entries.length.should eql(2)
130
+ end
131
+
132
+ it "should allow forced collection update if ids changed" do
133
+ @invoice.entry_ids = @entries[0..1].collect{|i| i.id}
134
+ @invoice.entries.length.should eql(2) # load once
135
+ @invoice.entry_ids << @entries[2].id
136
+ @invoice.entry_ids.length.should eql(3)
137
+ @invoice.entries.length.should eql(2) # cached!
138
+ @invoice.entries(true).length.should eql(3)
139
+ end
140
+
141
+ it "should empty arrays when nil collection provided" do
142
+ @invoice.entries = @entries
143
+ @invoice.entries = nil
144
+ @invoice.entry_ids.should be_empty
145
+ @invoice.entries.should be_empty
146
+ end
147
+
148
+ it "should empty arrays when nil ids array provided" do
149
+ @invoice.entries = @entries
150
+ @invoice.entry_ids = nil
151
+ @invoice.entry_ids.should be_empty
152
+ @invoice.entries.should be_empty
153
+ end
154
+
155
+ it "should ignore nil entries" do
156
+ @invoice.entries = [ nil ]
157
+ @invoice.entry_ids.should be_empty
158
+ @invoice.entries.should be_empty
159
+ end
160
+
161
+ describe "proxy" do
162
+
163
+ it "should ensure new entries to proxy are matched" do
164
+ @invoice.entries << @entries.first
165
+ @invoice.entry_ids.first.should eql(@entries.first.id)
166
+ @invoice.entries.first.should eql(@entries.first)
167
+ @invoice.entries << @entries[1]
168
+ @invoice.entries.count.should eql(2)
169
+ @invoice.entry_ids.count.should eql(2)
170
+ @invoice.entry_ids.last.should eql(@entries[1].id)
171
+ @invoice.entries.last.should eql(@entries[1])
172
+ end
173
+
174
+ it "should support push method" do
175
+ @invoice.entries.push(@entries.first)
176
+ @invoice.entry_ids.first.should eql(@entries.first.id)
177
+ end
178
+
179
+ it "should support []= method" do
180
+ @invoice.entries[0] = @entries.first
181
+ @invoice.entry_ids.first.should eql(@entries.first.id)
182
+ end
183
+
184
+ it "should support unshift method" do
185
+ @invoice.entries.unshift(@entries.first)
186
+ @invoice.entry_ids.first.should eql(@entries.first.id)
187
+ @invoice.entries.unshift(@entries[1])
188
+ @invoice.entry_ids.first.should eql(@entries[1].id)
189
+ end
190
+
191
+ it "should support pop method" do
192
+ @invoice.entries.push(@entries.first)
193
+ @invoice.entries.pop.should eql(@entries.first)
194
+ @invoice.entries.empty?.should be_true
195
+ @invoice.entry_ids.empty?.should be_true
196
+ end
197
+
198
+ it "should support shift method" do
199
+ @invoice.entries.push(@entries[0])
200
+ @invoice.entries.push(@entries[1])
201
+ @invoice.entries.shift.should eql(@entries[0])
202
+ @invoice.entries.first.should eql(@entries[1])
203
+ @invoice.entry_ids.first.should eql(@entries[1].id)
204
+ end
205
+
206
+
207
+ end
208
+
209
+
210
+ end
211
+
212
+ end
213
+