couchrest_model-radiant 1.0.0

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 (73) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +19 -0
  3. data/Rakefile +74 -0
  4. data/THANKS.md +21 -0
  5. data/history.txt +207 -0
  6. data/lib/couchrest/model.rb +10 -0
  7. data/lib/couchrest/model/associations.rb +223 -0
  8. data/lib/couchrest/model/base.rb +111 -0
  9. data/lib/couchrest/model/callbacks.rb +27 -0
  10. data/lib/couchrest/model/casted_array.rb +39 -0
  11. data/lib/couchrest/model/casted_model.rb +68 -0
  12. data/lib/couchrest/model/class_proxy.rb +122 -0
  13. data/lib/couchrest/model/collection.rb +263 -0
  14. data/lib/couchrest/model/configuration.rb +51 -0
  15. data/lib/couchrest/model/design_doc.rb +123 -0
  16. data/lib/couchrest/model/document_queries.rb +83 -0
  17. data/lib/couchrest/model/errors.rb +23 -0
  18. data/lib/couchrest/model/extended_attachments.rb +77 -0
  19. data/lib/couchrest/model/persistence.rb +155 -0
  20. data/lib/couchrest/model/properties.rb +208 -0
  21. data/lib/couchrest/model/property.rb +97 -0
  22. data/lib/couchrest/model/property_protection.rb +71 -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 +175 -0
  26. data/lib/couchrest/model/validations.rb +68 -0
  27. data/lib/couchrest/model/validations/casted_model.rb +14 -0
  28. data/lib/couchrest/model/validations/locale/en.yml +5 -0
  29. data/lib/couchrest/model/validations/uniqueness.rb +44 -0
  30. data/lib/couchrest/model/views.rb +160 -0
  31. data/lib/couchrest/railtie.rb +12 -0
  32. data/lib/couchrest_model.rb +62 -0
  33. data/lib/rails/generators/couchrest_model.rb +16 -0
  34. data/lib/rails/generators/couchrest_model/model/model_generator.rb +27 -0
  35. data/lib/rails/generators/couchrest_model/model/templates/model.rb +2 -0
  36. data/spec/couchrest/assocations_spec.rb +196 -0
  37. data/spec/couchrest/attachment_spec.rb +176 -0
  38. data/spec/couchrest/base_spec.rb +463 -0
  39. data/spec/couchrest/casted_model_spec.rb +438 -0
  40. data/spec/couchrest/casted_spec.rb +75 -0
  41. data/spec/couchrest/class_proxy_spec.rb +132 -0
  42. data/spec/couchrest/configuration_spec.rb +78 -0
  43. data/spec/couchrest/inherited_spec.rb +40 -0
  44. data/spec/couchrest/persistence_spec.rb +415 -0
  45. data/spec/couchrest/property_protection_spec.rb +192 -0
  46. data/spec/couchrest/property_spec.rb +871 -0
  47. data/spec/couchrest/subclass_spec.rb +99 -0
  48. data/spec/couchrest/validations.rb +85 -0
  49. data/spec/couchrest/view_spec.rb +463 -0
  50. data/spec/fixtures/attachments/README +3 -0
  51. data/spec/fixtures/attachments/couchdb.png +0 -0
  52. data/spec/fixtures/attachments/test.html +11 -0
  53. data/spec/fixtures/base.rb +139 -0
  54. data/spec/fixtures/more/article.rb +35 -0
  55. data/spec/fixtures/more/card.rb +17 -0
  56. data/spec/fixtures/more/cat.rb +19 -0
  57. data/spec/fixtures/more/client.rb +6 -0
  58. data/spec/fixtures/more/course.rb +25 -0
  59. data/spec/fixtures/more/event.rb +8 -0
  60. data/spec/fixtures/more/invoice.rb +14 -0
  61. data/spec/fixtures/more/person.rb +9 -0
  62. data/spec/fixtures/more/question.rb +7 -0
  63. data/spec/fixtures/more/sale_entry.rb +9 -0
  64. data/spec/fixtures/more/sale_invoice.rb +13 -0
  65. data/spec/fixtures/more/service.rb +10 -0
  66. data/spec/fixtures/more/user.rb +22 -0
  67. data/spec/fixtures/views/lib.js +3 -0
  68. data/spec/fixtures/views/test_view/lib.js +3 -0
  69. data/spec/fixtures/views/test_view/only-map.js +4 -0
  70. data/spec/fixtures/views/test_view/test-map.js +3 -0
  71. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  72. data/spec/spec_helper.rb +48 -0
  73. metadata +263 -0
@@ -0,0 +1,5 @@
1
+ en:
2
+ errors:
3
+ messages:
4
+ taken: "is already taken"
5
+
@@ -0,0 +1,44 @@
1
+ # encoding: utf-8
2
+
3
+ module CouchRest
4
+ module Model
5
+ module Validations
6
+
7
+ # Validates if a field is unique
8
+ class UniquenessValidator < ActiveModel::EachValidator
9
+
10
+ # Ensure we have a class available so we can check for a usable view
11
+ # or add one if necessary.
12
+ def setup(klass)
13
+ @klass = klass
14
+ end
15
+
16
+
17
+ def validate_each(document, attribute, value)
18
+ view_name = options[:view].nil? ? "by_#{attribute}" : options[:view]
19
+ # Determine the base of the search
20
+ base = options[:proxy].nil? ? @klass : document.instance_eval(options[:proxy])
21
+
22
+ if base.respond_to?(:has_view?) && !base.has_view?(view_name)
23
+ raise "View #{document.class.name}.#{options[:view]} does not exist!" unless options[:view].nil?
24
+ @klass.view_by attribute
25
+ end
26
+
27
+ docs = base.view(view_name, :key => value, :limit => 2, :include_docs => false)['rows']
28
+ return if docs.empty?
29
+
30
+ unless document.new?
31
+ return if docs.find{|doc| doc['id'] == document.id}
32
+ end
33
+
34
+ if docs.length > 0
35
+ document.errors.add(attribute, :taken, :default => options[:message], :value => value)
36
+ end
37
+ end
38
+
39
+
40
+ end
41
+
42
+ end
43
+ end
44
+ 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['model'] == '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['model'] == '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['#{model_type_key}'] == '#{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,12 @@
1
+ require "rails"
2
+ require "active_model/railtie"
3
+
4
+ module CouchrestModel
5
+ # = Active Record Railtie
6
+ class Railtie < Rails::Railtie
7
+ config.generators.orm :couchrest_model
8
+ config.generators.test_framework :test_unit, :fixture => false
9
+ end
10
+
11
+ end
12
+
@@ -0,0 +1,62 @@
1
+ gem 'couchrest', ">= 1.0.0"
2
+ require 'couchrest'
3
+
4
+ gem "tzinfo", ">= 0.3.22"
5
+
6
+ gem 'railties', ">= 3.0.0.rc"
7
+ gem "activesupport", ">= 3.0.0.rc"
8
+
9
+ require 'active_support/core_ext'
10
+ require 'active_support/json'
11
+
12
+ gem "activemodel", ">= 3.0.0.rc"
13
+ require 'active_model'
14
+ require "active_model/callbacks"
15
+ require "active_model/conversion"
16
+ require "active_model/deprecated_error_methods"
17
+ require "active_model/errors"
18
+ require "active_model/naming"
19
+ require "active_model/serialization"
20
+ require "active_model/translation"
21
+ require "active_model/validator"
22
+ require "active_model/validations"
23
+
24
+ gem "mime-types", ">= 1.15"
25
+ require 'mime/types'
26
+ require "enumerator"
27
+ require "time"
28
+ require 'digest/md5'
29
+
30
+ require 'bigdecimal' # used in typecast
31
+ require 'bigdecimal/util' # used in typecast
32
+
33
+ require 'couchrest/model'
34
+ require 'couchrest/model/errors'
35
+ require "couchrest/model/persistence"
36
+ require "couchrest/model/typecast"
37
+ require "couchrest/model/property"
38
+ require "couchrest/model/property_protection"
39
+ require "couchrest/model/casted_array"
40
+ require "couchrest/model/properties"
41
+ require "couchrest/model/validations"
42
+ require "couchrest/model/callbacks"
43
+ require "couchrest/model/document_queries"
44
+ require "couchrest/model/views"
45
+ require "couchrest/model/design_doc"
46
+ require "couchrest/model/extended_attachments"
47
+ require "couchrest/model/class_proxy"
48
+ require "couchrest/model/collection"
49
+ require "couchrest/model/associations"
50
+ require "couchrest/model/configuration"
51
+
52
+ # Monkey patches applied to couchrest
53
+ require "couchrest/model/support/couchrest"
54
+ require "couchrest/model/support/hash"
55
+
56
+ # Base libraries
57
+ require "couchrest/model/casted_model"
58
+ require "couchrest/model/base"
59
+
60
+ # Add rails support *after* everything has loaded
61
+
62
+ require "couchrest/railtie"
@@ -0,0 +1,16 @@
1
+ require 'rails/generators/named_base'
2
+ require 'rails/generators/active_model'
3
+ require 'couchrest_model'
4
+
5
+ module CouchrestModel
6
+ module Generators
7
+ class Base < Rails::Generators::NamedBase #:nodoc:
8
+
9
+ # Set the current directory as base for the inherited generators.
10
+ def self.base_root
11
+ File.dirname(__FILE__)
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ require 'rails/generators/couchrest_model'
2
+
3
+ module CouchrestModel
4
+ module Generators
5
+ class ModelGenerator < Base
6
+ check_class_collision
7
+
8
+ def create_model_file
9
+ template 'model.rb', File.join('app/models', class_path, "#{file_name}.rb")
10
+ end
11
+
12
+ def create_module_file
13
+ return if class_path.empty?
14
+ template 'module.rb', File.join('app/models', "#{class_path.join('/')}.rb") if behavior == :invoke
15
+ end
16
+
17
+ hook_for :test_framework
18
+
19
+ protected
20
+
21
+ def parent_class_name
22
+ "CouchRest::Model::Base"
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,2 @@
1
+ class <%= class_name %> < <%= parent_class_name.classify %>
2
+ end
@@ -0,0 +1,196 @@
1
+ # encoding: utf-8
2
+ require File.expand_path('../../spec_helper', __FILE__)
3
+ require File.join(FIXTURE_PATH, 'more', 'sale_invoice')
4
+
5
+
6
+ describe "Assocations" do
7
+
8
+ describe "of type belongs to" do
9
+
10
+ before :each do
11
+ @invoice = SaleInvoice.create(:price => 2000)
12
+ @client = Client.create(:name => "Sam Lown")
13
+ end
14
+
15
+ it "should create a foreign key property with setter and getter" do
16
+ @invoice.properties.find{|p| p.name == 'client_id'}.should_not be_nil
17
+ @invoice.respond_to?(:client_id).should be_true
18
+ @invoice.respond_to?("client_id=").should be_true
19
+ end
20
+
21
+ it "should set the property and provide object when set" do
22
+ @invoice.client = @client
23
+ @invoice.client_id.should eql(@client.id)
24
+ @invoice.client.should eql(@client)
25
+ end
26
+
27
+ it "should set the attribute, save and return" do
28
+ @invoice.client = @client
29
+ @invoice.save
30
+ @invoice = SaleInvoice.get(@invoice.id)
31
+ @invoice.client.id.should eql(@client.id)
32
+ end
33
+
34
+ it "should remove the association if nil is provided" do
35
+ @invoice.client = @client
36
+ @invoice.client = nil
37
+ @invoice.client_id.should be_nil
38
+ end
39
+
40
+ it "should not try to search for association if foreign_key is nil" do
41
+ @invoice.client_id = nil
42
+ Client.should_not_receive(:get)
43
+ @invoice.client
44
+ end
45
+
46
+ it "should raise error if class name does not exist" do
47
+ lambda {
48
+ class TestBadAssoc < CouchRest::Model::Base
49
+ belongs_to :test_bad_item
50
+ end
51
+ }.should raise_error
52
+ end
53
+
54
+ it "should allow override of foreign key" do
55
+ @invoice.respond_to?(:alternate_client).should be_true
56
+ @invoice.respond_to?("alternate_client=").should be_true
57
+ @invoice.properties.find{|p| p.name == 'alt_client_id'}.should_not be_nil
58
+ end
59
+
60
+ it "should allow override of foreign key and save" do
61
+ @invoice.alternate_client = @client
62
+ @invoice.save
63
+ @invoice = SaleInvoice.get(@invoice.id)
64
+ @invoice.alternate_client.id.should eql(@client.id)
65
+ end
66
+
67
+ end
68
+
69
+ describe "of type collection_of" do
70
+
71
+ before(:each) do
72
+ @invoice = SaleInvoice.create(:price => 2000)
73
+ @entries = [
74
+ SaleEntry.create(:description => 'test line 1', :price => 500),
75
+ SaleEntry.create(:description => 'test line 2', :price => 500),
76
+ SaleEntry.create(:description => 'test line 3', :price => 1000)
77
+ ]
78
+ end
79
+
80
+ it "should create an associated property and collection proxy" do
81
+ @invoice.respond_to?('entry_ids')
82
+ @invoice.respond_to?('entry_ids=')
83
+ @invoice.entries.class.should eql(::CouchRest::CollectionOfProxy)
84
+ end
85
+
86
+ it "should allow replacement of objects" do
87
+ @invoice.entries = @entries
88
+ @invoice.entries.length.should eql(3)
89
+ @invoice.entry_ids.length.should eql(3)
90
+ @invoice.entries.first.should eql(@entries.first)
91
+ @invoice.entry_ids.first.should eql(@entries.first.id)
92
+ end
93
+
94
+ it "should allow ids to be set directly and load entries" do
95
+ @invoice.entry_ids = @entries.collect{|i| i.id}
96
+ @invoice.entries.length.should eql(3)
97
+ @invoice.entries.first.should eql(@entries.first)
98
+ end
99
+
100
+ it "should replace collection if ids replaced" do
101
+ @invoice.entry_ids = @entries.collect{|i| i.id}
102
+ @invoice.entries.length.should eql(3) # load once
103
+ @invoice.entry_ids = @entries[0..1].collect{|i| i.id}
104
+ @invoice.entries.length.should eql(2)
105
+ end
106
+
107
+ it "should allow forced collection update if ids changed" do
108
+ @invoice.entry_ids = @entries[0..1].collect{|i| i.id}
109
+ @invoice.entries.length.should eql(2) # load once
110
+ @invoice.entry_ids << @entries[2].id
111
+ @invoice.entry_ids.length.should eql(3)
112
+ @invoice.entries.length.should eql(2) # cached!
113
+ @invoice.entries(true).length.should eql(3)
114
+ end
115
+
116
+ it "should empty arrays when nil collection provided" do
117
+ @invoice.entries = @entries
118
+ @invoice.entries = nil
119
+ @invoice.entry_ids.should be_empty
120
+ @invoice.entries.should be_empty
121
+ end
122
+
123
+ it "should empty arrays when nil ids array provided" do
124
+ @invoice.entries = @entries
125
+ @invoice.entry_ids = nil
126
+ @invoice.entry_ids.should be_empty
127
+ @invoice.entries.should be_empty
128
+ end
129
+
130
+ it "should ignore nil entries" do
131
+ @invoice.entries = [ nil ]
132
+ @invoice.entry_ids.should be_empty
133
+ @invoice.entries.should be_empty
134
+ end
135
+
136
+ describe "proxy" do
137
+
138
+ it "should ensure new entries to proxy are matched" do
139
+ @invoice.entries << @entries.first
140
+ @invoice.entry_ids.first.should eql(@entries.first.id)
141
+ @invoice.entries.first.should eql(@entries.first)
142
+ @invoice.entries << @entries[1]
143
+ @invoice.entries.count.should eql(2)
144
+ @invoice.entry_ids.count.should eql(2)
145
+ @invoice.entry_ids.last.should eql(@entries[1].id)
146
+ @invoice.entries.last.should eql(@entries[1])
147
+ end
148
+
149
+ it "should support push method" do
150
+ @invoice.entries.push(@entries.first)
151
+ @invoice.entry_ids.first.should eql(@entries.first.id)
152
+ end
153
+
154
+ it "should support []= method" do
155
+ @invoice.entries[0] = @entries.first
156
+ @invoice.entry_ids.first.should eql(@entries.first.id)
157
+ end
158
+
159
+ it "should support unshift method" do
160
+ @invoice.entries.unshift(@entries.first)
161
+ @invoice.entry_ids.first.should eql(@entries.first.id)
162
+ @invoice.entries.unshift(@entries[1])
163
+ @invoice.entry_ids.first.should eql(@entries[1].id)
164
+ end
165
+
166
+ it "should support pop method" do
167
+ @invoice.entries.push(@entries.first)
168
+ @invoice.entries.pop.should eql(@entries.first)
169
+ @invoice.entries.empty?.should be_true
170
+ @invoice.entry_ids.empty?.should be_true
171
+ end
172
+
173
+ it "should support shift method" do
174
+ @invoice.entries.push(@entries[0])
175
+ @invoice.entries.push(@entries[1])
176
+ @invoice.entries.shift.should eql(@entries[0])
177
+ @invoice.entries.first.should eql(@entries[1])
178
+ @invoice.entry_ids.first.should eql(@entries[1].id)
179
+ end
180
+
181
+ it "should raise error when adding un-persisted entries" do
182
+ SaleEntry.find_by_description('test entry').should be_nil
183
+ entry = SaleEntry.new(:description => 'test entry', :price => 500)
184
+ lambda {
185
+ @invoice.entries << entry
186
+ }.should raise_error
187
+ # In the future maybe?
188
+ # @invoice.save.should be_true
189
+ # SaleEntry.find_by_description('test entry').should_not be_nil
190
+ end
191
+
192
+ end
193
+
194
+ end
195
+
196
+ end