couchrest_extended_document 1.0.0.beta6 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +173 -2
- data/Rakefile +3 -3
- data/THANKS.md +4 -1
- data/history.txt +7 -1
- data/lib/couchrest/casted_array.rb +20 -6
- data/lib/couchrest/casted_model.rb +3 -5
- data/lib/couchrest/extended_document.rb +16 -78
- data/lib/couchrest/mixins.rb +1 -0
- data/lib/couchrest/mixins/attributes.rb +75 -0
- data/lib/couchrest/mixins/callbacks.rb +4 -2
- data/lib/couchrest/mixins/properties.rb +41 -73
- data/lib/couchrest/{typecast.rb → mixins/typecast.rb} +4 -5
- data/lib/couchrest/property.rb +64 -18
- data/lib/couchrest/validation.rb +6 -5
- data/lib/couchrest/validation/auto_validate.rb +4 -4
- data/lib/couchrest_extended_document.rb +1 -0
- data/spec/couchrest/casted_model_spec.rb +20 -2
- data/spec/couchrest/extended_doc_spec.rb +10 -9
- data/spec/couchrest/property_spec.rb +143 -1
- data/spec/fixtures/more/card.rb +1 -1
- data/spec/fixtures/more/question.rb +3 -2
- metadata +13 -16
- data/lib/couchrest/mixins/validation.rb +0 -245
data/README.md
CHANGED
@@ -28,6 +28,177 @@ Note: CouchRest::ExtendedDocument only supports CouchDB 0.10.0 or newer.
|
|
28
28
|
|
29
29
|
end
|
30
30
|
|
31
|
+
@cat = Cat.new(:name => 'Felix', :nicknames => ['so cute', 'sweet kitty'])
|
32
|
+
|
33
|
+
@cat.new? # true
|
34
|
+
@cat.save
|
35
|
+
|
36
|
+
@cat['name'] # "Felix"
|
37
|
+
|
38
|
+
@cat.nicknames << 'getoffdamntable'
|
39
|
+
|
40
|
+
@cat = Cat.new
|
41
|
+
@cat.update_attributes(:name => 'Felix', :random_text => 'feline')
|
42
|
+
@cat.new? # false
|
43
|
+
@cat.random_text # Raises error!
|
44
|
+
|
45
|
+
|
46
|
+
### Properties
|
47
|
+
|
48
|
+
Only attributes with a property definition will be stored be ExtendedDocument (as opposed
|
49
|
+
to a normal CouchRest Document which will store everything). To help prevent confusion,
|
50
|
+
a property should be considered as the definition of an attribute. An attribute must be associated
|
51
|
+
with a property, but a property may not have any attributes associated if none have been set.
|
52
|
+
|
53
|
+
|
54
|
+
In its simplest form, a property
|
55
|
+
will only create a getter and setter passing all attribute data directly to the database. Assuming the attribute
|
56
|
+
provided responds to +to_json+, there will not be any problems saving it, but when loading the
|
57
|
+
data back it will either be a string, number, array, or hash:
|
58
|
+
|
59
|
+
class Cat < CouchRest::ExtendedDocument
|
60
|
+
property :name
|
61
|
+
property :birthday
|
62
|
+
end
|
63
|
+
|
64
|
+
@cat = Cat.new(:name => 'Felix', :birthday => 2.years.ago)
|
65
|
+
@cat.name # 'Felix'
|
66
|
+
@cat.birthday.is_a?(Time) # True!
|
67
|
+
@cat.save
|
68
|
+
@cat = Cat.find(@cat.id)
|
69
|
+
@cat.name # 'Felix'
|
70
|
+
@cat.birthday.is_a?(Time) # False!
|
71
|
+
|
72
|
+
Properties create getters and setters similar to the following:
|
73
|
+
|
74
|
+
def name
|
75
|
+
read_attribute('name')
|
76
|
+
end
|
77
|
+
|
78
|
+
def name=(value)
|
79
|
+
write_attribute('name', value)
|
80
|
+
end
|
81
|
+
|
82
|
+
Properties can also have a type which
|
83
|
+
will be used for casting data retrieved from CouchDB when the attribute is set:
|
84
|
+
|
85
|
+
class Cat < CouchRest::ExtendedDocument
|
86
|
+
property :name, String
|
87
|
+
property :last_fed_at, Time
|
88
|
+
end
|
89
|
+
|
90
|
+
@cat = Cat.new(:name => 'Felix', :last_fed_at => 10.minutes.ago)
|
91
|
+
@cat.last_fed_at.is_a?(Time) # True!
|
92
|
+
@cat.save
|
93
|
+
@cat = Cat.find(@cat.id)
|
94
|
+
@cat.last_fed_at < 20.minutes.ago # True!
|
95
|
+
|
96
|
+
|
97
|
+
Booleans or TrueClass will also create a getter with question mark at the end:
|
98
|
+
|
99
|
+
class Cat < CouchRest::ExtendedDocument
|
100
|
+
property :awake, TrueClass, :default => true
|
101
|
+
end
|
102
|
+
|
103
|
+
@cat.awake? # true
|
104
|
+
|
105
|
+
Adding the +:default+ option will ensure the attribute always has a value.
|
106
|
+
|
107
|
+
Defining a property as read-only will mean that its value is set only when read from the
|
108
|
+
database and that it will not have a setter method. You can however update a read-only
|
109
|
+
attribute using the +write_attribute+ method:
|
110
|
+
|
111
|
+
class Cat < CouchRest::ExtendedDocument
|
112
|
+
property :name, String
|
113
|
+
property :lives, Integer, :default => 9, :readonly => true
|
114
|
+
|
115
|
+
def fall_off_balcony!
|
116
|
+
write_attribute(:lives, lives - 1)
|
117
|
+
save
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
@cat = Cat.new(:name => "Felix")
|
122
|
+
@cat.fall_off_balcony!
|
123
|
+
@cat.lives # Now 8!
|
124
|
+
|
125
|
+
|
126
|
+
### Property Arrays
|
127
|
+
|
128
|
+
An attribute may also contain an array of data. ExtendedDocument handles this, along
|
129
|
+
with casting, by defining the class of the child attributes inside an Array:
|
130
|
+
|
131
|
+
class Cat < CouchRest::ExtendedDocument
|
132
|
+
property :name, String
|
133
|
+
property :nicknames, [String]
|
134
|
+
end
|
135
|
+
|
136
|
+
By default, the array will be ready to use from the moment the object as been instantiated:
|
137
|
+
|
138
|
+
@cat = Cat.new(:name => 'Fluffy')
|
139
|
+
@cat.nicknames << 'Buffy'
|
140
|
+
|
141
|
+
@cat.nicknames == ['Buffy']
|
142
|
+
|
143
|
+
When anything other than a string is set as the class of a property, the array will be converted
|
144
|
+
into special wrapper called a CastedArray. If the child objects respond to the 'casted_by' method
|
145
|
+
(such as those created with CastedModel, below) it will contain a reference to the parent.
|
146
|
+
|
147
|
+
### Casted Models
|
148
|
+
|
149
|
+
ExtendedDocument allows you to take full advantage of CouchDB's ability to store complex
|
150
|
+
documents and retrieve them using the CastedModel module. Simply include the module in
|
151
|
+
a Hash (or other model that responds to the [] and []= methods) and set any properties
|
152
|
+
you'd like to use. For example:
|
153
|
+
|
154
|
+
class CatToy << Hash
|
155
|
+
include CouchRest::CastedModel
|
156
|
+
|
157
|
+
property :name, String
|
158
|
+
property :purchased, Date
|
159
|
+
end
|
160
|
+
|
161
|
+
class Cat << CouchRest::ExtendedDocument
|
162
|
+
property :name, String
|
163
|
+
property :toys, [CatToy]
|
164
|
+
end
|
165
|
+
|
166
|
+
@cat = Cat.new(:name => 'Felix', :toys => [{:name => 'mouse', :purchases => 1.month.ago}])
|
167
|
+
@cat.toys.first.class == CatToy
|
168
|
+
@cat.toys.first.name == 'mouse'
|
169
|
+
|
170
|
+
Additionally, any hashes sent to the property will automatically be converted:
|
171
|
+
|
172
|
+
@cat.toys << {:name => 'catnip ball'}
|
173
|
+
@cat.toys.last.is_a?(CatToy) # True!
|
174
|
+
|
175
|
+
Of course, to use your own classes they *must* be defined before the parent uses them otherwise
|
176
|
+
Ruby will bring up a missing constant error. To avoid this, or if you have a really simple array of data
|
177
|
+
you'd like to model, the latest version of ExtendedDocument (> 1.0.0) supports creating
|
178
|
+
anonymous classes:
|
179
|
+
|
180
|
+
class Cat << CouchRest::ExtendedDocument
|
181
|
+
property :name, String
|
182
|
+
|
183
|
+
property :toys do |toy|
|
184
|
+
toy.property :name, String
|
185
|
+
toy.property :rating, Integer
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
@cat = Cat.new(:name => 'Felix', :toys => [{:name => 'mouse', :rating => 3}, {:name => 'catnip ball', :rating => 5}])
|
190
|
+
@cat.toys.last.rating == 5
|
191
|
+
@cat.toys.last.name == 'catnip ball'
|
192
|
+
|
193
|
+
Using this method of anonymous classes will *only* create arrays of objects.
|
194
|
+
|
195
|
+
### Notable Issues
|
196
|
+
|
197
|
+
ExtendedDocument uses active_support for some of its internals. Ensure you have a stable active support gem installed
|
198
|
+
or at least 3.0.0.beta4.
|
199
|
+
|
200
|
+
JSON gem versions 1.4.X are kown to cause problems with stack overflows and general badness. Version 1.2.4 appears to work fine.
|
201
|
+
|
31
202
|
### Ruby on Rails
|
32
203
|
|
33
204
|
CouchRest::ExtendedDocument is compatible with rails and provides some ActiveRecord-like methods.
|
@@ -54,13 +225,13 @@ CouchRest install, from the project root directory run `rake`, or `autotest`
|
|
54
225
|
|
55
226
|
API: [http://rdoc.info/projects/couchrest/couchrest_extended_document](http://rdoc.info/projects/couchrest/couchrest_extended_document)
|
56
227
|
|
57
|
-
Check the wiki for documentation and examples [http://wiki.github.com/couchrest/
|
228
|
+
Check the wiki for documentation and examples [http://wiki.github.com/couchrest/couchrest_extended_document](http://wiki.github.com/couchrest/couchrest_extended_document)
|
58
229
|
|
59
230
|
|
60
231
|
|
61
232
|
## Contact
|
62
233
|
|
63
|
-
Please post bugs, suggestions and patches to the bug tracker at [http://github.com/couchrest/
|
234
|
+
Please post bugs, suggestions and patches to the bug tracker at [http://github.com/couchrest/couchrest_extended_document/issues](http://github.com/couchrest/couchrest_extended_document/issues).
|
64
235
|
|
65
236
|
Follow us on Twitter: [http://twitter.com/couchrest](http://twitter.com/couchrest)
|
66
237
|
|
data/Rakefile
CHANGED
@@ -20,16 +20,16 @@ begin
|
|
20
20
|
gemspec.description = "CouchRest::ExtendedDocument provides aditional features to the standard CouchRest::Document class such as properties, view designs, callbacks, typecasting and validations."
|
21
21
|
gemspec.email = "jchris@apache.org"
|
22
22
|
gemspec.homepage = "http://github.com/couchrest/couchrest_extended_document"
|
23
|
-
gemspec.authors = ["J. Chris Anderson", "Matt Aimonetti", "Marcos Tapajos", "Will Leinweber"]
|
23
|
+
gemspec.authors = ["J. Chris Anderson", "Matt Aimonetti", "Marcos Tapajos", "Will Leinweber", "Sam Lown"]
|
24
24
|
gemspec.extra_rdoc_files = %w( README.md LICENSE THANKS.md )
|
25
25
|
gemspec.files = %w( LICENSE README.md Rakefile THANKS.md history.txt couchrest.gemspec) + Dir["{examples,lib,spec,utils}/**/*"] - Dir["spec/tmp"]
|
26
26
|
gemspec.has_rdoc = true
|
27
|
-
gemspec.add_dependency("couchrest", ">= 1.0.0
|
27
|
+
gemspec.add_dependency("couchrest", ">= 1.0.0")
|
28
28
|
gemspec.add_dependency("mime-types", ">= 1.15")
|
29
29
|
gemspec.add_dependency("activesupport", ">= 2.3.0")
|
30
30
|
gemspec.add_dependency("builder", ">=2.1.2")
|
31
31
|
gemspec.version = CouchRest::ExtendedDocument::VERSION
|
32
|
-
gemspec.date = "
|
32
|
+
gemspec.date = Time.now.strftime("%Y-%m-%d")
|
33
33
|
gemspec.require_path = "lib"
|
34
34
|
end
|
35
35
|
rescue LoadError
|
data/THANKS.md
CHANGED
@@ -13,7 +13,10 @@ changes. A list of these people is included below.
|
|
13
13
|
* [Matt Lyon](http://mattly.tumblr.com/)
|
14
14
|
* Simon Rozet (simon /at/ rozet /dot/ name)
|
15
15
|
* [Marcos Tapajós](http://tapajos.me)
|
16
|
+
* [Sam Lown](http://github.com/samlown)
|
17
|
+
* [Will Leinweber](http://github.com/will)
|
18
|
+
|
16
19
|
|
17
|
-
Patches are welcome. The primary source for this software project is [on Github](http://github.com/couchrest/
|
20
|
+
Patches are welcome. The primary source for this software project is [on Github](http://github.com/couchrest/couchrest_extended_document)
|
18
21
|
|
19
22
|
A lot of people have active forks - thank you all - even the patches I don't end up using are helpful.
|
data/history.txt
CHANGED
@@ -8,7 +8,13 @@
|
|
8
8
|
|
9
9
|
* Minor enhancements
|
10
10
|
* Added 'find_by_*' alias for finding first item in view with matching key.
|
11
|
-
* Fixed issue with active_support in Rails3.
|
11
|
+
* Fixed issue with active_support in Rails3 and text in README for JSON.
|
12
|
+
* Refactoring of properties, added read_attribute and write_attribute methods.
|
13
|
+
* Now possible to send anything to update_attribtues method. Invalid or readonly attributes will be ignored.
|
14
|
+
* Updating to use couchrest_inheritable_attributes to avoid possible Rails conflicts
|
15
|
+
|
16
|
+
* Major enhancements
|
17
|
+
* Added support for anonymous CastedModels defined in Documents
|
12
18
|
|
13
19
|
== 1.0.0.beta5
|
14
20
|
|
@@ -6,20 +6,34 @@
|
|
6
6
|
module CouchRest
|
7
7
|
class CastedArray < Array
|
8
8
|
attr_accessor :casted_by
|
9
|
+
attr_accessor :property
|
10
|
+
|
11
|
+
def initialize(array, property)
|
12
|
+
self.property = property
|
13
|
+
super(array)
|
14
|
+
end
|
9
15
|
|
10
16
|
def << obj
|
11
|
-
obj
|
12
|
-
super(obj)
|
17
|
+
super(instantiate_and_cast(obj))
|
13
18
|
end
|
14
19
|
|
15
20
|
def push(obj)
|
16
|
-
obj
|
17
|
-
super(obj)
|
21
|
+
super(instantiate_and_cast(obj))
|
18
22
|
end
|
19
23
|
|
20
24
|
def []= index, obj
|
21
|
-
|
22
|
-
|
25
|
+
super(index, instantiate_and_cast(obj))
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def instantiate_and_cast(obj)
|
31
|
+
if self.casted_by && self.property && obj.class != self.property.type_class
|
32
|
+
self.property.cast_value(self.casted_by, obj)
|
33
|
+
else
|
34
|
+
obj.casted_by = self.casted_by if obj.respond_to?(:casted_by)
|
35
|
+
obj
|
36
|
+
end
|
23
37
|
end
|
24
38
|
end
|
25
39
|
end
|
@@ -5,17 +5,15 @@ module CouchRest
|
|
5
5
|
base.send(:include, ::CouchRest::Mixins::Callbacks)
|
6
6
|
base.send(:include, ::CouchRest::Mixins::Properties)
|
7
7
|
base.send(:attr_accessor, :casted_by)
|
8
|
-
base.send(:attr_accessor, :document_saved)
|
9
8
|
end
|
10
9
|
|
11
10
|
def initialize(keys={})
|
12
11
|
raise StandardError unless self.is_a? Hash
|
13
|
-
|
12
|
+
apply_all_property_defaults # defined in CouchRest::Mixins::Properties
|
14
13
|
super()
|
15
14
|
keys.each do |k,v|
|
16
|
-
|
15
|
+
write_attribute(k.to_s, v)
|
17
16
|
end if keys
|
18
|
-
cast_keys # defined in CouchRest::Mixins::Properties
|
19
17
|
end
|
20
18
|
|
21
19
|
def []= key, value
|
@@ -36,7 +34,7 @@ module CouchRest
|
|
36
34
|
# False if the casted model has already
|
37
35
|
# been saved in the containing document
|
38
36
|
def new?
|
39
|
-
|
37
|
+
@casted_by.nil? ? true : @casted_by.new?
|
40
38
|
end
|
41
39
|
alias :new_record? :new?
|
42
40
|
|
@@ -8,7 +8,7 @@ module CouchRest
|
|
8
8
|
# Same as CouchRest::Document but with properties and validations
|
9
9
|
class ExtendedDocument < Document
|
10
10
|
|
11
|
-
VERSION = "1.0.0
|
11
|
+
VERSION = "1.0.0"
|
12
12
|
|
13
13
|
include CouchRest::Mixins::Callbacks
|
14
14
|
include CouchRest::Mixins::DocumentQueries
|
@@ -18,6 +18,7 @@ module CouchRest
|
|
18
18
|
include CouchRest::Mixins::ClassProxy
|
19
19
|
include CouchRest::Mixins::Collection
|
20
20
|
include CouchRest::Mixins::AttributeProtection
|
21
|
+
include CouchRest::Mixins::Attributes
|
21
22
|
|
22
23
|
# Including validation here does not work due to the way inheritance is handled.
|
23
24
|
#include CouchRest::Validation
|
@@ -57,12 +58,17 @@ module CouchRest
|
|
57
58
|
base.new(doc, :directly_set_attributes => true)
|
58
59
|
end
|
59
60
|
|
61
|
+
|
62
|
+
# Instantiate a new ExtendedDocument by preparing all properties
|
63
|
+
# using the provided document hash.
|
64
|
+
#
|
65
|
+
# Options supported:
|
66
|
+
#
|
67
|
+
# * :directly_set_attributes: true when data comes directly from database
|
68
|
+
#
|
60
69
|
def initialize(doc = {}, options = {})
|
61
|
-
|
62
|
-
remove_protected_attributes(doc) unless options[:directly_set_attributes]
|
63
|
-
directly_set_attributes(doc) unless doc.nil?
|
70
|
+
prepare_all_attributes(doc, options) # defined in CouchRest::Mixins::Attributes
|
64
71
|
super(doc)
|
65
|
-
cast_keys # defined in CouchRest::Mixins::Properties
|
66
72
|
unless self['_id'] && self['_rev']
|
67
73
|
self['couchrest-type'] = self.class.to_s
|
68
74
|
end
|
@@ -94,12 +100,12 @@ module CouchRest
|
|
94
100
|
# decent time format by default. See Time#to_json
|
95
101
|
def self.timestamps!
|
96
102
|
class_eval <<-EOS, __FILE__, __LINE__
|
97
|
-
property(:updated_at, :read_only => true, :
|
98
|
-
property(:created_at, :read_only => true, :
|
103
|
+
property(:updated_at, Time, :read_only => true, :protected => true, :auto_validation => false)
|
104
|
+
property(:created_at, Time, :read_only => true, :protected => true, :auto_validation => false)
|
99
105
|
|
100
106
|
set_callback :save, :before do |object|
|
101
|
-
|
102
|
-
|
107
|
+
write_attribute('updated_at', Time.now)
|
108
|
+
write_attribute('created_at', Time.now) if object.new?
|
103
109
|
end
|
104
110
|
EOS
|
105
111
|
end
|
@@ -142,14 +148,6 @@ module CouchRest
|
|
142
148
|
|
143
149
|
### instance methods
|
144
150
|
|
145
|
-
# Returns the Class properties
|
146
|
-
#
|
147
|
-
# ==== Returns
|
148
|
-
# Array:: the list of properties for the instance
|
149
|
-
def properties
|
150
|
-
self.class.properties
|
151
|
-
end
|
152
|
-
|
153
151
|
# Gets a reference to the actual document in the DB
|
154
152
|
# Calls up to the next document if there is one,
|
155
153
|
# Otherwise we're at the top and we return self
|
@@ -163,28 +161,6 @@ module CouchRest
|
|
163
161
|
!@casted_by
|
164
162
|
end
|
165
163
|
|
166
|
-
# Takes a hash as argument, and applies the values by using writer methods
|
167
|
-
# for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
|
168
|
-
# missing. In case of error, no attributes are changed.
|
169
|
-
def update_attributes_without_saving(hash)
|
170
|
-
# remove attributes that cannot be updated, silently ignoring them
|
171
|
-
# which matches Rails behavior when, for instance, setting created_at.
|
172
|
-
# make a copy, we don't want to change arguments
|
173
|
-
attrs = hash.dup
|
174
|
-
%w[_id _rev created_at updated_at].each {|attr| attrs.delete(attr)}
|
175
|
-
check_properties_exist(attrs)
|
176
|
-
set_attributes(attrs)
|
177
|
-
end
|
178
|
-
alias :attributes= :update_attributes_without_saving
|
179
|
-
|
180
|
-
# Takes a hash as argument, and applies the values by using writer methods
|
181
|
-
# for each key. Raises a NoMethodError if the corresponding methods are
|
182
|
-
# missing. In case of error, no attributes are changed.
|
183
|
-
def update_attributes(hash)
|
184
|
-
update_attributes_without_saving hash
|
185
|
-
save
|
186
|
-
end
|
187
|
-
|
188
164
|
# for compatibility with old-school frameworks
|
189
165
|
alias :new_record? :new?
|
190
166
|
alias :new_document? :new?
|
@@ -255,7 +231,6 @@ module CouchRest
|
|
255
231
|
raise ArgumentError, "a document requires a database to be saved to (The document or the #{self.class} default database were not set)" unless database
|
256
232
|
set_unique_id if new? && self.respond_to?(:set_unique_id)
|
257
233
|
result = database.save_doc(self, bulk)
|
258
|
-
mark_as_saved
|
259
234
|
result["ok"] == true
|
260
235
|
end
|
261
236
|
|
@@ -281,43 +256,6 @@ module CouchRest
|
|
281
256
|
end
|
282
257
|
end
|
283
258
|
end
|
284
|
-
|
285
|
-
protected
|
286
|
-
|
287
|
-
# Set document_saved flag on all casted models to true
|
288
|
-
def mark_as_saved
|
289
|
-
self.each do |key, prop|
|
290
|
-
if prop.is_a?(Array)
|
291
|
-
prop.each do |item|
|
292
|
-
if item.respond_to?(:document_saved)
|
293
|
-
item.send(:document_saved=, true)
|
294
|
-
end
|
295
|
-
end
|
296
|
-
elsif prop.respond_to?(:document_saved)
|
297
|
-
prop.send(:document_saved=, true)
|
298
|
-
end
|
299
|
-
end
|
300
|
-
end
|
301
|
-
|
302
|
-
private
|
303
|
-
|
304
|
-
def check_properties_exist(attrs)
|
305
|
-
attrs.each do |attribute_name, attribute_value|
|
306
|
-
raise NoMethodError, "#{attribute_name}= method not available, use property :#{attribute_name}" unless self.respond_to?("#{attribute_name}=")
|
307
|
-
end
|
308
|
-
end
|
309
|
-
|
310
|
-
def directly_set_attributes(hash)
|
311
|
-
hash.each do |attribute_name, attribute_value|
|
312
|
-
if self.respond_to?("#{attribute_name}=")
|
313
|
-
self.send("#{attribute_name}=", hash.delete(attribute_name))
|
314
|
-
end
|
315
|
-
end
|
316
|
-
end
|
317
|
-
|
318
|
-
def set_attributes(hash)
|
319
|
-
attrs = remove_protected_attributes(hash)
|
320
|
-
directly_set_attributes(attrs)
|
321
|
-
end
|
259
|
+
|
322
260
|
end
|
323
261
|
end
|