couchrest_model 1.0.0.beta7

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 (69) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +320 -0
  3. data/Rakefile +71 -0
  4. data/THANKS.md +19 -0
  5. data/examples/model/example.rb +144 -0
  6. data/history.txt +180 -0
  7. data/lib/couchrest/model.rb +10 -0
  8. data/lib/couchrest/model/associations.rb +207 -0
  9. data/lib/couchrest/model/attribute_protection.rb +74 -0
  10. data/lib/couchrest/model/attributes.rb +75 -0
  11. data/lib/couchrest/model/base.rb +111 -0
  12. data/lib/couchrest/model/callbacks.rb +27 -0
  13. data/lib/couchrest/model/casted_array.rb +39 -0
  14. data/lib/couchrest/model/casted_model.rb +68 -0
  15. data/lib/couchrest/model/class_proxy.rb +122 -0
  16. data/lib/couchrest/model/collection.rb +260 -0
  17. data/lib/couchrest/model/design_doc.rb +126 -0
  18. data/lib/couchrest/model/document_queries.rb +82 -0
  19. data/lib/couchrest/model/errors.rb +23 -0
  20. data/lib/couchrest/model/extended_attachments.rb +73 -0
  21. data/lib/couchrest/model/persistence.rb +141 -0
  22. data/lib/couchrest/model/properties.rb +144 -0
  23. data/lib/couchrest/model/property.rb +96 -0
  24. data/lib/couchrest/model/support/couchrest.rb +19 -0
  25. data/lib/couchrest/model/support/hash.rb +9 -0
  26. data/lib/couchrest/model/typecast.rb +170 -0
  27. data/lib/couchrest/model/validations.rb +68 -0
  28. data/lib/couchrest/model/validations/casted_model.rb +14 -0
  29. data/lib/couchrest/model/validations/locale/en.yml +5 -0
  30. data/lib/couchrest/model/validations/uniqueness.rb +45 -0
  31. data/lib/couchrest/model/views.rb +167 -0
  32. data/lib/couchrest_model.rb +56 -0
  33. data/spec/couchrest/assocations_spec.rb +213 -0
  34. data/spec/couchrest/attachment_spec.rb +148 -0
  35. data/spec/couchrest/attribute_protection_spec.rb +153 -0
  36. data/spec/couchrest/base_spec.rb +463 -0
  37. data/spec/couchrest/casted_model_spec.rb +424 -0
  38. data/spec/couchrest/casted_spec.rb +75 -0
  39. data/spec/couchrest/class_proxy_spec.rb +132 -0
  40. data/spec/couchrest/inherited_spec.rb +40 -0
  41. data/spec/couchrest/persistence_spec.rb +409 -0
  42. data/spec/couchrest/property_spec.rb +804 -0
  43. data/spec/couchrest/subclass_spec.rb +99 -0
  44. data/spec/couchrest/validations.rb +73 -0
  45. data/spec/couchrest/view_spec.rb +463 -0
  46. data/spec/fixtures/attachments/README +3 -0
  47. data/spec/fixtures/attachments/couchdb.png +0 -0
  48. data/spec/fixtures/attachments/test.html +11 -0
  49. data/spec/fixtures/base.rb +139 -0
  50. data/spec/fixtures/more/article.rb +35 -0
  51. data/spec/fixtures/more/card.rb +17 -0
  52. data/spec/fixtures/more/cat.rb +19 -0
  53. data/spec/fixtures/more/course.rb +25 -0
  54. data/spec/fixtures/more/event.rb +8 -0
  55. data/spec/fixtures/more/invoice.rb +14 -0
  56. data/spec/fixtures/more/person.rb +9 -0
  57. data/spec/fixtures/more/question.rb +7 -0
  58. data/spec/fixtures/more/service.rb +10 -0
  59. data/spec/fixtures/more/user.rb +22 -0
  60. data/spec/fixtures/views/lib.js +3 -0
  61. data/spec/fixtures/views/test_view/lib.js +3 -0
  62. data/spec/fixtures/views/test_view/only-map.js +4 -0
  63. data/spec/fixtures/views/test_view/test-map.js +3 -0
  64. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  65. data/spec/spec.opts +5 -0
  66. data/spec/spec_helper.rb +48 -0
  67. data/utils/remap.rb +27 -0
  68. data/utils/subset.rb +30 -0
  69. metadata +232 -0
data/Rakefile ADDED
@@ -0,0 +1,71 @@
1
+ require 'rake'
2
+ require "rake/rdoctask"
3
+
4
+ $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
5
+ require 'couchrest_model'
6
+
7
+ begin
8
+ require 'spec/rake/spectask'
9
+ rescue LoadError
10
+ puts <<-EOS
11
+ To use rspec for testing you must install rspec gem:
12
+ gem install rspec
13
+ EOS
14
+ exit(0)
15
+ end
16
+
17
+ begin
18
+ require 'jeweler'
19
+ Jeweler::Tasks.new do |gemspec|
20
+ gemspec.name = "couchrest_model"
21
+ gemspec.summary = "Extends the CouchRest Document for advanced modelling."
22
+ gemspec.description = "CouchRest Model provides aditional features to the standard CouchRest Document class such as properties, view designs, associations, callbacks, typecasting and validations."
23
+ gemspec.email = "jchris@apache.org"
24
+ gemspec.homepage = "http://github.com/couchrest/couchrest_model"
25
+ gemspec.authors = ["J. Chris Anderson", "Matt Aimonetti", "Marcos Tapajos", "Will Leinweber", "Sam Lown"]
26
+ gemspec.extra_rdoc_files = %w( README.md LICENSE THANKS.md )
27
+ gemspec.files = %w( LICENSE README.md Rakefile THANKS.md history.txt couchrest.gemspec) + Dir["{examples,lib,spec,utils}/**/*"] - Dir["spec/tmp"]
28
+ gemspec.has_rdoc = true
29
+ gemspec.add_dependency("couchrest", ">= 1.0.0.beta")
30
+ gemspec.add_dependency("mime-types", ">= 1.15")
31
+ gemspec.add_dependency("activesupport", ">= 2.3.5")
32
+ gemspec.add_dependency("activemodel", ">= 3.0.0.beta4")
33
+ gemspec.version = CouchRest::Model::VERSION
34
+ gemspec.date = "2008-11-22"
35
+ gemspec.require_path = "lib"
36
+ end
37
+ rescue LoadError
38
+ puts "Jeweler not available. Install it with: gem install jeweler"
39
+ end
40
+
41
+ desc "Run all specs"
42
+ Spec::Rake::SpecTask.new('spec') do |t|
43
+ t.spec_opts = ["--color"]
44
+ t.spec_files = FileList['spec/**/*_spec.rb']
45
+ end
46
+
47
+ desc "Print specdocs"
48
+ Spec::Rake::SpecTask.new(:doc) do |t|
49
+ t.spec_opts = ["--format", "specdoc"]
50
+ t.spec_files = FileList['spec/*_spec.rb']
51
+ end
52
+
53
+ desc "Generate the rdoc"
54
+ Rake::RDocTask.new do |rdoc|
55
+ files = ["README.rdoc", "LICENSE", "lib/**/*.rb"]
56
+ rdoc.rdoc_files.add(files)
57
+ rdoc.main = "README.rdoc"
58
+ rdoc.title = "CouchRest: Ruby CouchDB, close to the metal"
59
+ end
60
+
61
+ desc "Run the rspec"
62
+ task :default => :spec
63
+
64
+ module Rake
65
+ def self.remove_task(task_name)
66
+ Rake.application.instance_variable_get('@tasks').delete(task_name.to_s)
67
+ end
68
+ end
69
+
70
+ Rake.remove_task("github:release")
71
+ Rake.remove_task("release")
data/THANKS.md ADDED
@@ -0,0 +1,19 @@
1
+ CouchRest THANKS
2
+ =====================
3
+
4
+ CouchRest was originally developed by J. Chris Anderson <jchris@grabb.it>
5
+ and a number of other contributors. Many people further contributed to
6
+ CouchRest by reporting problems, suggesting various improvements or submitting
7
+ changes. A list of these people is included below.
8
+
9
+ * [Matt Aimonetti](http://merbist.com/about/)
10
+ * [Greg Borenstein](http://ideasfordozens.com)
11
+ * [Geoffrey Grosenbach](http://nubyonrails.com/)
12
+ * [Jonathan S. Katz](http://github.com/jkatz)
13
+ * [Matt Lyon](http://mattly.tumblr.com/)
14
+ * Simon Rozet (simon /at/ rozet /dot/ name)
15
+ * [Marcos Tapajós](http://tapajos.me)
16
+
17
+ Patches are welcome. The primary source for this software project is [on Github](http://github.com/couchrest/couchrest)
18
+
19
+ A lot of people have active forks - thank you all - even the patches I don't end up using are helpful.
@@ -0,0 +1,144 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'couchrest')
2
+
3
+ def show obj
4
+ puts obj.inspect
5
+ puts
6
+ end
7
+
8
+ SERVER = CouchRest.new
9
+ SERVER.default_database = 'couchrest-extendeddoc-example'
10
+
11
+ class Author < CouchRest::ExtendedDocument
12
+ use_database SERVER.default_database
13
+ property :name
14
+
15
+ def drink_scotch
16
+ puts "... glug type glug ... I'm #{name} ... type glug glug ..."
17
+ end
18
+ end
19
+
20
+ class Post < CouchRest::ExtendedDocument
21
+ use_database SERVER.default_database
22
+
23
+ property :title
24
+ property :body
25
+ property :author, :cast_as => 'Author'
26
+
27
+ timestamps!
28
+ end
29
+
30
+ class Comment < CouchRest::ExtendedDocument
31
+ use_database SERVER.default_database
32
+
33
+ property :commenter, :cast_as => 'Author'
34
+ timestamps!
35
+
36
+ def post= post
37
+ self["post_id"] = post.id
38
+ end
39
+ def post
40
+ Post.get(self['post_id']) if self['post_id']
41
+ end
42
+
43
+ end
44
+
45
+ puts "Act I: CRUD"
46
+ puts
47
+ puts "(pause for dramatic effect)"
48
+ puts
49
+ sleep 2
50
+
51
+ puts "Create an author."
52
+ quentin = Author.new("name" => "Quentin Hazel")
53
+ show quentin
54
+
55
+ puts "Create a new post."
56
+ post = Post.new(:title => "First Post", :body => "Lorem ipsum dolor sit amet, consectetur adipisicing elit...")
57
+ show post
58
+
59
+ puts "Add the author to the post."
60
+ post.author = quentin
61
+ show post
62
+
63
+ puts "Save the post."
64
+ post.save
65
+ show post
66
+
67
+ puts "Load the post."
68
+ reloaded = Post.get(post.id)
69
+ show reloaded
70
+
71
+ puts "The author of the post is an instance of Author."
72
+ reloaded.author.drink_scotch
73
+
74
+ puts "\nAdd some comments to the post."
75
+ comment_one = Comment.new :text => "Blah blah blah", :commenter => {:name => "Joe Sixpack"}
76
+ comment_two = Comment.new :text => "Yeah yeah yeah", :commenter => {:name => "Jane Doe"}
77
+ comment_three = Comment.new :text => "Whatever...", :commenter => {:name => "John Stewart"}
78
+
79
+ # TODO - maybe add some magic here?
80
+ comment_one.post = post
81
+ comment_two.post = post
82
+ comment_three.post = post
83
+ comment_one.save
84
+ comment_two.save
85
+ comment_three.save
86
+
87
+ show comment_one
88
+ show comment_two
89
+ show comment_three
90
+
91
+ puts "We can load a post through its comment (no magic here)."
92
+ show post = comment_one.post
93
+
94
+ puts "Commenters are also authors."
95
+ comment_two['commenter'].drink_scotch
96
+ comment_one['commenter'].drink_scotch
97
+ comment_three['commenter'].drink_scotch
98
+
99
+ puts "\nLet's save an author to her own document."
100
+ jane = comment_two['commenter']
101
+ jane.save
102
+ show jane
103
+
104
+ puts "Oh, that's neat! Because Ruby passes hash valuee by reference, Jane's new id has been added to the comment she left."
105
+ show comment_two
106
+
107
+ puts "Of course, we'd better remember to save it."
108
+ comment_two.save
109
+ show comment_two
110
+
111
+ puts "Oooh, denormalized... feel the burn!"
112
+ puts
113
+ puts
114
+ puts
115
+ puts "Act II: Views"
116
+ puts
117
+ puts
118
+ sleep 2
119
+
120
+ puts "Let's find all the comments that go with our post."
121
+ puts "Our post has id #{post.id}, so lets find all the comments with that post_id."
122
+ puts
123
+
124
+ class Comment
125
+ view_by :post_id
126
+ end
127
+
128
+ comments = Comment.by_post_id :key => post.id
129
+ show comments
130
+
131
+ puts "That was too easy."
132
+ puts "We can even wrap it up in a finder on the Post class."
133
+ puts
134
+
135
+ class Post
136
+ def comments
137
+ Comment.by_post_id :key => id
138
+ end
139
+ end
140
+
141
+ show post.comments
142
+ puts "Gimme 5 minutes and I'll roll this into the framework. ;)"
143
+ puts
144
+ puts "There is a lot more that can be done with views, but a lot of the interesting stuff is joins, which of course range across types. We'll pick up where we left off, next time."
data/history.txt ADDED
@@ -0,0 +1,180 @@
1
+ == Next Version
2
+
3
+ * Major enhancements
4
+
5
+ * Minor enhancements
6
+
7
+ == CouchRest Model 1.0.0.beta7
8
+
9
+ * Major enhancements
10
+ * Renamed ExtendedDocument to CouchRest::Model
11
+ * Added initial support for simple belongs_to associations
12
+ * Added support for basic collection_of association (unique to document databases!)
13
+ * Moved Validation to ActiveModel
14
+ * Moved Callbacks to ActiveModel
15
+ * Removed support for properties defined using a string for the type instead of a class
16
+ * Validation always included
17
+ * Uniqueness validation now available
18
+
19
+
20
+ == 1.0.0.beta6
21
+
22
+ * Major enhancements
23
+ * Added support for anonymous CastedModels defined in Documents
24
+
25
+ * Minor enhancements
26
+ * Added 'find_by_*' alias for finding first item in view with matching key.
27
+ * Fixed issue with active_support in Rails3 and text in README for JSON.
28
+ * Refactoring of properties, added read_attribute and write_attribute methods.
29
+ * Now possible to send anything to update_attribtues method. Invalid or readonly attributes will be ignored.
30
+ * Attributes with arrays are *always* instantiated as a CastedArray.
31
+ * Setting a property of type Array (or keyed hash) must be an array or an error will be raised.
32
+ * Now possible to set Array attribute from hash where keys determine order.
33
+
34
+ == 1.0.0.beta5
35
+
36
+ * Minor enhancements
37
+ * Added 'find' alias for 'get' for easier rails transition
38
+
39
+ == 1.0.0.beta3
40
+
41
+ * Minor enhancements
42
+ * Removed Validation by default, requires too many structure changes (FAIL)
43
+ * Added support for instantiation of documents read from database as couchrest-type provided (Sam Lown)
44
+ * Improved attachment handling for detecting file type (Sam Lown)
45
+ * Removing some monkey patches and relying on active_support for constantize and humanize (Sam Lown)
46
+ * Added support for setting type directly on property (Sam Lown)
47
+
48
+
49
+ == 1.0.0.beta2
50
+
51
+ * Minor enhancements
52
+ * Enable Validation by default and refactored location (Sam Lown)
53
+
54
+ == 1.0.0.beta
55
+
56
+ * Major enhancements
57
+ * Separated ExtendedDocument from main CouchRest gem (Sam Lown)
58
+
59
+ * Minor enhancements
60
+ * active_support included by default
61
+
62
+ == 0.37
63
+
64
+ * Minor enhancements
65
+ * Added gemspec (needed for Bundler install) (Tapajós)
66
+
67
+ == 0.36
68
+
69
+ * Major enhancements
70
+ * Adds support for continuous replication (sauy7)
71
+ * Automatic Type Casting (Alexander Uvarov, Sam Lown, Tim Heighes, Will Leinweber)
72
+ * Added a search method to CouchRest:Database to search the documents in a given database. (Dave Farkas, Arnaud Berthomier, John Wood)
73
+
74
+ * Minor enhancements
75
+ * Provide a description of the timeout error (John Wood)
76
+
77
+ == 0.35
78
+
79
+ * Major enhancements
80
+ * CouchRest::ExtendedDocument allow chaining the inherit class callback (Kenneth Kalmer) - http://github.com/couchrest/couchrest/issues#issue/8
81
+
82
+ * Minor enhancements
83
+ * Fix attachment bug (Johannes Jörg Schmidt)
84
+ * Fix create database exception bug (Damien Mathieu)
85
+ * Compatible with restclient >= 1.4.0 new responses (Julien Kirch)
86
+ * Bug fix: Attribute protection no longer strips attributes coming from the database (Will Leinweber)
87
+ * Bug fix: Remove double CGI escape when PUTting an attachment (nzoschke)
88
+ * Bug fix: Changing Class proxy to set database on result sets (Peter Gumeson)
89
+ * Bug fix: Updated time regexp (Nolan Darilek)
90
+ * Added an update_doc method to database to handle conflicts during atomic updates. (Pierre Larochelle)
91
+ * Bug fix: http://github.com/couchrest/couchrest/issues/#issue/2 (Luke Burton)
92
+
93
+ == 0.34
94
+
95
+ * Major enhancements
96
+
97
+ * Added support for https database URIs. (Mathias Meyer)
98
+ * Changing some validations to be compatible with activemodel. (Marcos Tapajós)
99
+ * Adds attribute protection to properties. (Will Leinweber)
100
+ * Improved CouchRest::Database#save_doc, added "batch" mode to significantly speed up saves at cost of lower durability gurantees. (Igal Koshevoy)
101
+ * Added CouchRest::Database#bulk_save_doc and #batch_save_doc as human-friendlier wrappers around #save_doc. (Igal Koshevoy)
102
+
103
+ * Minor enhancements
104
+
105
+ * Fix content_type handling for attachments
106
+ * Fixed a bug in the pagination code that caused it to paginate over records outside of the scope of the view parameters.(John Wood)
107
+ * Removed amount_pages calculation for the pagination collection, since it cannot be reliably calculated without a view.(John Wood)
108
+ * Bug fix: http://github.com/couchrest/couchrest/issues/#issue/2 (Luke Burton)
109
+ * Bug fix: http://github.com/couchrest/couchrest/issues/#issue/1 (Marcos Tapajós)
110
+ * Removed the Database class deprecation notices (Matt Aimonetti)
111
+ * Adding support to :cast_as => 'Date'. (Marcos Tapajós)
112
+ * Improve documentation (Marcos Tapajós)
113
+ * Streamer fixes (Julien Sanchez)
114
+ * Fix Save on Document & ExtendedDocument crashed if bulk (Julien Sanchez)
115
+ * Fix Initialization of ExtendentDocument model shouldn't failed on a nil value in argument (deepj)
116
+ * Change to use Jeweler and Gemcutter (Marcos Tapajós)
117
+
118
+ == 0.33
119
+
120
+ * Major enhancements
121
+
122
+ * Added a new Rack logger middleware letting you log/save requests/queries (Matt Aimonetti)
123
+
124
+ * Minor enhancements
125
+
126
+ * Added #amount_pages to a paginated result array (Matt Aimonetti)
127
+ * Ruby 1.9.2 compatible (Matt Aimonetti)
128
+ * Added a property? method for property cast as :boolean (John Wood)
129
+ * Added an option to force the deletion of a attachments (bypass 409s) (Matt Aimonetti)
130
+ * Created a new abstraction layer for the REST API (Matt Aimonetti)
131
+ * Bug fix: made ExtendedDocument#all compatible with Couch 0.10 (tc)
132
+
133
+ == 0.32
134
+
135
+ * Major enhancements
136
+
137
+ * ExtendedDocument.get doesn't raise an exception anymore. If no documents are found nil is returned.
138
+ * ExtendedDocument.get! works the say #get used to work and will raise an exception if a document isn't found.
139
+
140
+ * Minor enhancements
141
+
142
+ * Bug fix: Model.all(:keys => [1,2]) was not working (Matt Aimonetti)
143
+ * Added ValidationErrors#count in order to play nicely with Rails (Peter Wagenet)
144
+ * Bug fix: class proxy design doc refresh (Daniel Kirsh)
145
+ * Bug fix: the count method on the proxy collection was missing (Daniel Kirsch)
146
+ * Added #amount_pages to a paginated collection. (Matt Aimonetti)
147
+
148
+ == 0.31
149
+
150
+ * Major enhancements
151
+
152
+ * Created an abstraction HTTP layer to support different http adapters (Matt Aimonetti)
153
+ * Added ExtendedDocument.create({}) and #create!({}) so you don't have to do Model.new.create (Matt Aimonetti)
154
+
155
+ * Minor enhancements
156
+
157
+ * Added an init.rb file for easy usage as a Rails plugin (Aaron Quint)
158
+ * Bug fix: pagination shouldn't die on empty results (Arnaud Berthomier)
159
+ * Optimized ExtendedDocument.count to run about 3x faster (Matt Aimonetti)
160
+ * Added Float casting (Ryan Felton & Matt Aimonetti)
161
+
162
+ == 0.30
163
+
164
+ * Major enhancements
165
+
166
+ * Added support for pagination (John Wood)
167
+ * Improved performance when initializing documents with timestamps (Matt Aimonetti)
168
+
169
+ * Minor enhancements
170
+
171
+ * Extended the API to retrieve an attachment URI (Matt Aimonetti)
172
+ * Bug fix: default value should be able to be set as false (Alexander Uvarov)
173
+ * Bug fix: validates_is_numeric should be able to properly validate a Float instance (Rob Kaufman)
174
+ * Bug fix: fixed the Timeout implementation (Seth Falcon)
175
+
176
+
177
+ ---
178
+
179
+ Unfortunately, before 0.30 we did not keep a track of the modifications made to CouchRest.
180
+ You can see the full commit history on GitHub: http://github.com/couchrest/couchrest/commits/master/
@@ -0,0 +1,10 @@
1
+
2
+ module CouchRest
3
+
4
+ module Model
5
+
6
+ VERSION = "1.0.0.beta7"
7
+
8
+ end
9
+
10
+ end
@@ -0,0 +1,207 @@
1
+ module CouchRest
2
+ module Model
3
+ module Associations
4
+
5
+ # Basic support for relationships between ExtendedDocuments
6
+
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+
13
+ # Define an association that this object belongs to.
14
+ #
15
+ def belongs_to(attrib, *options)
16
+ opts = {
17
+ :foreign_key => attrib.to_s + '_id',
18
+ :class_name => attrib.to_s.camelcase,
19
+ :proxy => nil
20
+ }
21
+ case options.first
22
+ when Hash
23
+ opts.merge!(options.first)
24
+ end
25
+
26
+ begin
27
+ opts[:class] = opts[:class_name].constantize
28
+ rescue
29
+ raise "Unable to convert class name into Constant for #{self.name}##{attrib}"
30
+ end
31
+
32
+ prop = property(opts[:foreign_key], opts)
33
+
34
+ create_belongs_to_getter(attrib, prop, opts)
35
+ create_belongs_to_setter(attrib, prop, opts)
36
+
37
+ prop
38
+ end
39
+
40
+ # Provide access to a collection of objects where the associated
41
+ # property contains a list of the collection item ids.
42
+ #
43
+ # The following:
44
+ #
45
+ # collection_of :groups
46
+ #
47
+ # creates a pseudo property called "groups" which allows access
48
+ # to a CollectionOfProxy object. Adding, replacing or removing entries in this
49
+ # proxy will cause the matching property array, in this case "group_ids", to
50
+ # be kept in sync.
51
+ #
52
+ # Any manual changes made to the collection ids property (group_ids), unless replaced, will require
53
+ # a reload of the CollectionOfProxy for the two sets of data to be in sync:
54
+ #
55
+ # group_ids = ['123']
56
+ # groups == [Group.get('123')]
57
+ # group_ids << '321'
58
+ # groups == [Group.get('123')]
59
+ # groups(true) == [Group.get('123'), Group.get('321')]
60
+ #
61
+ # Of course, saving the parent record will store the collection ids as they are
62
+ # found.
63
+ #
64
+ # The CollectionOfProxy supports the following array functions, anything else will cause
65
+ # a mismatch between the collection objects and collection ids:
66
+ #
67
+ # groups << obj
68
+ # groups.push obj
69
+ # groups.unshift obj
70
+ # groups[0] = obj
71
+ # groups.pop == obj
72
+ # groups.shift == obj
73
+ #
74
+ # Addtional options match those of the the belongs_to method.
75
+ #
76
+ # NOTE: This method is *not* recommended for large collections or collections that change
77
+ # frequently! Use with prudence.
78
+ #
79
+ def collection_of(attrib, *options)
80
+ opts = {
81
+ :foreign_key => attrib.to_s.singularize + '_ids',
82
+ :class_name => attrib.to_s.singularize.camelcase,
83
+ :proxy => nil
84
+ }
85
+ case options.first
86
+ when Hash
87
+ opts.merge!(options.first)
88
+ end
89
+ begin
90
+ opts[:class] = opts[:class_name].constantize
91
+ rescue
92
+ raise "Unable to convert class name into Constant for #{self.name}##{attrib}"
93
+ end
94
+ opts[:readonly] = true
95
+
96
+ prop = property(opts[:foreign_key], [], opts)
97
+
98
+ create_collection_of_property_setter(attrib, prop, opts)
99
+ create_collection_of_getter(attrib, prop, opts)
100
+ create_collection_of_setter(attrib, prop, opts)
101
+
102
+ prop
103
+ end
104
+
105
+
106
+ private
107
+
108
+ def create_belongs_to_getter(attrib, property, options)
109
+ base = options[:proxy] || options[:class_name]
110
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
111
+ def #{attrib}
112
+ @#{attrib} ||= #{options[:foreign_key]}.nil? ? nil : #{base}.get(self.#{options[:foreign_key]})
113
+ end
114
+ EOS
115
+ end
116
+
117
+ def create_belongs_to_setter(attrib, property, options)
118
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
119
+ def #{attrib}=(value)
120
+ self.#{options[:foreign_key]} = value.nil? ? nil : value.id
121
+ @#{attrib} = value
122
+ end
123
+ EOS
124
+ end
125
+
126
+ ### collection_of support methods
127
+
128
+ def create_collection_of_property_setter(attrib, property, options)
129
+ # ensure CollectionOfProxy is nil, ready to be reloaded on request
130
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
131
+ def #{options[:foreign_key]}=(value)
132
+ @#{attrib} = nil
133
+ write_attribute("#{options[:foreign_key]}", value)
134
+ end
135
+ EOS
136
+ end
137
+
138
+ def create_collection_of_getter(attrib, property, options)
139
+ base = options[:proxy] || options[:class_name]
140
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
141
+ def #{attrib}(reload = false)
142
+ return @#{attrib} unless @#{attrib}.nil? or reload
143
+ ary = self.#{options[:foreign_key]}.collect{|i| #{base}.get(i)}
144
+ @#{attrib} = ::CouchRest::CollectionOfProxy.new(ary, self, '#{options[:foreign_key]}')
145
+ end
146
+ EOS
147
+ end
148
+
149
+ def create_collection_of_setter(attrib, property, options)
150
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
151
+ def #{attrib}=(value)
152
+ @#{attrib} = ::CouchRest::CollectionOfProxy.new(value, self, '#{options[:foreign_key]}')
153
+ end
154
+ EOS
155
+ end
156
+
157
+ end
158
+
159
+ end
160
+ end
161
+
162
+ # Special proxy for a collection of items so that adding and removing
163
+ # to the list automatically updates the associated property.
164
+ class CollectionOfProxy < Array
165
+ attr_accessor :property
166
+ attr_accessor :casted_by
167
+
168
+ def initialize(array, casted_by, property)
169
+ self.property = property
170
+ self.casted_by = casted_by
171
+ (array ||= []).compact!
172
+ casted_by[property.to_s] = [] # replace the original array!
173
+ array.compact.each do |obj|
174
+ casted_by[property.to_s] << obj.id
175
+ end
176
+ super(array)
177
+ end
178
+ def << obj
179
+ casted_by[property.to_s] << obj.id
180
+ super(obj)
181
+ end
182
+ def push(obj)
183
+ casted_by[property.to_s].push obj.id
184
+ super(obj)
185
+ end
186
+ def unshift(obj)
187
+ casted_by[property.to_s].unshift obj.id
188
+ super(obj)
189
+ end
190
+
191
+ def []= index, obj
192
+ casted_by[property.to_s][index] = obj.id
193
+ super(index, obj)
194
+ end
195
+
196
+ def pop
197
+ casted_by[property.to_s].pop
198
+ super
199
+ end
200
+ def shift
201
+ casted_by[property.to_s].shift
202
+ super
203
+ end
204
+ end
205
+
206
+
207
+ end