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.
- data/LICENSE +176 -0
- data/README.md +19 -0
- data/Rakefile +74 -0
- data/THANKS.md +21 -0
- data/history.txt +207 -0
- data/lib/couchrest/model.rb +10 -0
- data/lib/couchrest/model/associations.rb +223 -0
- data/lib/couchrest/model/base.rb +111 -0
- data/lib/couchrest/model/callbacks.rb +27 -0
- data/lib/couchrest/model/casted_array.rb +39 -0
- data/lib/couchrest/model/casted_model.rb +68 -0
- data/lib/couchrest/model/class_proxy.rb +122 -0
- data/lib/couchrest/model/collection.rb +263 -0
- data/lib/couchrest/model/configuration.rb +51 -0
- data/lib/couchrest/model/design_doc.rb +123 -0
- data/lib/couchrest/model/document_queries.rb +83 -0
- data/lib/couchrest/model/errors.rb +23 -0
- data/lib/couchrest/model/extended_attachments.rb +77 -0
- data/lib/couchrest/model/persistence.rb +155 -0
- data/lib/couchrest/model/properties.rb +208 -0
- data/lib/couchrest/model/property.rb +97 -0
- data/lib/couchrest/model/property_protection.rb +71 -0
- data/lib/couchrest/model/support/couchrest.rb +19 -0
- data/lib/couchrest/model/support/hash.rb +9 -0
- data/lib/couchrest/model/typecast.rb +175 -0
- data/lib/couchrest/model/validations.rb +68 -0
- data/lib/couchrest/model/validations/casted_model.rb +14 -0
- data/lib/couchrest/model/validations/locale/en.yml +5 -0
- data/lib/couchrest/model/validations/uniqueness.rb +44 -0
- data/lib/couchrest/model/views.rb +160 -0
- data/lib/couchrest/railtie.rb +12 -0
- data/lib/couchrest_model.rb +62 -0
- data/lib/rails/generators/couchrest_model.rb +16 -0
- data/lib/rails/generators/couchrest_model/model/model_generator.rb +27 -0
- data/lib/rails/generators/couchrest_model/model/templates/model.rb +2 -0
- data/spec/couchrest/assocations_spec.rb +196 -0
- data/spec/couchrest/attachment_spec.rb +176 -0
- data/spec/couchrest/base_spec.rb +463 -0
- data/spec/couchrest/casted_model_spec.rb +438 -0
- data/spec/couchrest/casted_spec.rb +75 -0
- data/spec/couchrest/class_proxy_spec.rb +132 -0
- data/spec/couchrest/configuration_spec.rb +78 -0
- data/spec/couchrest/inherited_spec.rb +40 -0
- data/spec/couchrest/persistence_spec.rb +415 -0
- data/spec/couchrest/property_protection_spec.rb +192 -0
- data/spec/couchrest/property_spec.rb +871 -0
- data/spec/couchrest/subclass_spec.rb +99 -0
- data/spec/couchrest/validations.rb +85 -0
- data/spec/couchrest/view_spec.rb +463 -0
- data/spec/fixtures/attachments/README +3 -0
- data/spec/fixtures/attachments/couchdb.png +0 -0
- data/spec/fixtures/attachments/test.html +11 -0
- data/spec/fixtures/base.rb +139 -0
- data/spec/fixtures/more/article.rb +35 -0
- data/spec/fixtures/more/card.rb +17 -0
- data/spec/fixtures/more/cat.rb +19 -0
- data/spec/fixtures/more/client.rb +6 -0
- data/spec/fixtures/more/course.rb +25 -0
- data/spec/fixtures/more/event.rb +8 -0
- data/spec/fixtures/more/invoice.rb +14 -0
- data/spec/fixtures/more/person.rb +9 -0
- data/spec/fixtures/more/question.rb +7 -0
- data/spec/fixtures/more/sale_entry.rb +9 -0
- data/spec/fixtures/more/sale_invoice.rb +13 -0
- data/spec/fixtures/more/service.rb +10 -0
- data/spec/fixtures/more/user.rb +22 -0
- data/spec/fixtures/views/lib.js +3 -0
- data/spec/fixtures/views/test_view/lib.js +3 -0
- data/spec/fixtures/views/test_view/only-map.js +4 -0
- data/spec/fixtures/views/test_view/test-map.js +3 -0
- data/spec/fixtures/views/test_view/test-reduce.js +3 -0
- data/spec/spec_helper.rb +48 -0
- metadata +263 -0
@@ -0,0 +1,223 @@
|
|
1
|
+
module CouchRest
|
2
|
+
module Model
|
3
|
+
module Associations
|
4
|
+
|
5
|
+
# Basic support for relationships between CouchRest::Model::Base
|
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
|
+
check_obj(obj)
|
175
|
+
casted_by[property.to_s] << obj.id
|
176
|
+
end
|
177
|
+
super(array)
|
178
|
+
end
|
179
|
+
|
180
|
+
def << obj
|
181
|
+
check_obj(obj)
|
182
|
+
casted_by[property.to_s] << obj.id
|
183
|
+
super(obj)
|
184
|
+
end
|
185
|
+
|
186
|
+
def push(obj)
|
187
|
+
check_obj(obj)
|
188
|
+
casted_by[property.to_s].push obj.id
|
189
|
+
super(obj)
|
190
|
+
end
|
191
|
+
|
192
|
+
def unshift(obj)
|
193
|
+
check_obj(obj)
|
194
|
+
casted_by[property.to_s].unshift obj.id
|
195
|
+
super(obj)
|
196
|
+
end
|
197
|
+
|
198
|
+
def []= index, obj
|
199
|
+
check_obj(obj)
|
200
|
+
casted_by[property.to_s][index] = obj.id
|
201
|
+
super(index, obj)
|
202
|
+
end
|
203
|
+
|
204
|
+
def pop
|
205
|
+
casted_by[property.to_s].pop
|
206
|
+
super
|
207
|
+
end
|
208
|
+
|
209
|
+
def shift
|
210
|
+
casted_by[property.to_s].shift
|
211
|
+
super
|
212
|
+
end
|
213
|
+
|
214
|
+
protected
|
215
|
+
|
216
|
+
def check_obj(obj)
|
217
|
+
raise "Object cannot be added to #{casted_by.class.to_s}##{property.to_s} collection unless saved" if obj.new?
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module CouchRest
|
2
|
+
module Model
|
3
|
+
class Base < Document
|
4
|
+
|
5
|
+
extend ActiveModel::Naming
|
6
|
+
|
7
|
+
include CouchRest::Model::Configuration
|
8
|
+
include CouchRest::Model::Persistence
|
9
|
+
include CouchRest::Model::Callbacks
|
10
|
+
include CouchRest::Model::DocumentQueries
|
11
|
+
include CouchRest::Model::Views
|
12
|
+
include CouchRest::Model::DesignDoc
|
13
|
+
include CouchRest::Model::ExtendedAttachments
|
14
|
+
include CouchRest::Model::ClassProxy
|
15
|
+
include CouchRest::Model::Collection
|
16
|
+
include CouchRest::Model::PropertyProtection
|
17
|
+
include CouchRest::Model::Associations
|
18
|
+
include CouchRest::Model::Validations
|
19
|
+
|
20
|
+
def self.subclasses
|
21
|
+
@subclasses ||= []
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.inherited(subklass)
|
25
|
+
super
|
26
|
+
subklass.send(:include, CouchRest::Model::Properties)
|
27
|
+
subklass.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
28
|
+
def self.inherited(subklass)
|
29
|
+
super
|
30
|
+
subklass.properties = self.properties.dup
|
31
|
+
# This is nasty:
|
32
|
+
subklass._validators = self._validators.dup
|
33
|
+
end
|
34
|
+
EOS
|
35
|
+
subclasses << subklass
|
36
|
+
end
|
37
|
+
|
38
|
+
# Accessors
|
39
|
+
attr_accessor :casted_by
|
40
|
+
|
41
|
+
|
42
|
+
# Instantiate a new CouchRest::Model::Base by preparing all properties
|
43
|
+
# using the provided document hash.
|
44
|
+
#
|
45
|
+
# Options supported:
|
46
|
+
#
|
47
|
+
# * :directly_set_attributes: true when data comes directly from database
|
48
|
+
#
|
49
|
+
def initialize(doc = {}, options = {})
|
50
|
+
doc = prepare_all_attributes(doc, options)
|
51
|
+
super(doc)
|
52
|
+
unless self['_id'] && self['_rev']
|
53
|
+
self[self.model_type_key] = self.class.to_s
|
54
|
+
end
|
55
|
+
after_initialize if respond_to?(:after_initialize)
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
# Temp solution to make the view_by methods available
|
60
|
+
def self.method_missing(m, *args, &block)
|
61
|
+
if has_view?(m)
|
62
|
+
query = args.shift || {}
|
63
|
+
return view(m, query, *args, &block)
|
64
|
+
elsif m.to_s =~ /^find_(by_.+)/
|
65
|
+
view_name = $1
|
66
|
+
if has_view?(view_name)
|
67
|
+
return first_from_view(view_name, *args)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
super
|
71
|
+
end
|
72
|
+
|
73
|
+
### instance methods
|
74
|
+
|
75
|
+
# Gets a reference to the actual document in the DB
|
76
|
+
# Calls up to the next document if there is one,
|
77
|
+
# Otherwise we're at the top and we return self
|
78
|
+
def base_doc
|
79
|
+
return self if base_doc?
|
80
|
+
@casted_by.base_doc
|
81
|
+
end
|
82
|
+
|
83
|
+
# Checks if we're the top document
|
84
|
+
def base_doc?
|
85
|
+
!@casted_by
|
86
|
+
end
|
87
|
+
|
88
|
+
## Compatibility with ActiveSupport and older frameworks
|
89
|
+
|
90
|
+
# Hack so that CouchRest::Document, which descends from Hash,
|
91
|
+
# doesn't appear to Rails routing as a Hash of options
|
92
|
+
def is_a?(klass)
|
93
|
+
return false if klass == Hash
|
94
|
+
super
|
95
|
+
end
|
96
|
+
alias :kind_of? :is_a?
|
97
|
+
|
98
|
+
def persisted?
|
99
|
+
!new?
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_key
|
103
|
+
new? ? nil : [id]
|
104
|
+
end
|
105
|
+
|
106
|
+
alias :to_param :id
|
107
|
+
alias :new_record? :new?
|
108
|
+
alias :new_document? :new?
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module CouchRest #:nodoc:
|
4
|
+
module Model #:nodoc:
|
5
|
+
|
6
|
+
module Callbacks
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
included do
|
9
|
+
extend ActiveModel::Callbacks
|
10
|
+
|
11
|
+
define_model_callbacks \
|
12
|
+
:create,
|
13
|
+
:destroy,
|
14
|
+
:save,
|
15
|
+
:update,
|
16
|
+
:validate
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid?(*) #nodoc
|
21
|
+
_run_validation_callbacks { super }
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
#
|
2
|
+
# Wrapper around Array so that the casted_by attribute is set in all
|
3
|
+
# elements of the array.
|
4
|
+
#
|
5
|
+
|
6
|
+
module CouchRest::Model
|
7
|
+
class CastedArray < Array
|
8
|
+
attr_accessor :casted_by
|
9
|
+
attr_accessor :property
|
10
|
+
|
11
|
+
def initialize(array, property)
|
12
|
+
self.property = property
|
13
|
+
super(array)
|
14
|
+
end
|
15
|
+
|
16
|
+
def << obj
|
17
|
+
super(instantiate_and_cast(obj))
|
18
|
+
end
|
19
|
+
|
20
|
+
def push(obj)
|
21
|
+
super(instantiate_and_cast(obj))
|
22
|
+
end
|
23
|
+
|
24
|
+
def []= index, obj
|
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
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module CouchRest::Model
|
2
|
+
module CastedModel
|
3
|
+
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
include CouchRest::Model::Configuration
|
8
|
+
include CouchRest::Model::Callbacks
|
9
|
+
include CouchRest::Model::Properties
|
10
|
+
include CouchRest::Model::PropertyProtection
|
11
|
+
include CouchRest::Model::Associations
|
12
|
+
include CouchRest::Model::Validations
|
13
|
+
attr_accessor :casted_by
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(keys = {})
|
17
|
+
raise StandardError unless self.is_a? Hash
|
18
|
+
prepare_all_attributes(keys)
|
19
|
+
super()
|
20
|
+
end
|
21
|
+
|
22
|
+
def []= key, value
|
23
|
+
super(key.to_s, value)
|
24
|
+
end
|
25
|
+
|
26
|
+
def [] key
|
27
|
+
super(key.to_s)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Gets a reference to the top level extended
|
31
|
+
# document that a model is saved inside of
|
32
|
+
def base_doc
|
33
|
+
return nil unless @casted_by
|
34
|
+
@casted_by.base_doc
|
35
|
+
end
|
36
|
+
|
37
|
+
# False if the casted model has already
|
38
|
+
# been saved in the containing document
|
39
|
+
def new?
|
40
|
+
@casted_by.nil? ? true : @casted_by.new?
|
41
|
+
end
|
42
|
+
alias :new_record? :new?
|
43
|
+
|
44
|
+
def persisted?
|
45
|
+
!new?
|
46
|
+
end
|
47
|
+
|
48
|
+
# The to_param method is needed for rails to generate resourceful routes.
|
49
|
+
# In your controller, remember that it's actually the id of the document.
|
50
|
+
def id
|
51
|
+
return nil if base_doc.nil?
|
52
|
+
base_doc.id
|
53
|
+
end
|
54
|
+
alias :to_key :id
|
55
|
+
alias :to_param :id
|
56
|
+
|
57
|
+
# Sets the attributes from a hash
|
58
|
+
def update_attributes_without_saving(hash)
|
59
|
+
hash.each do |k, v|
|
60
|
+
raise NoMethodError, "#{k}= method not available, use property :#{k}" unless self.respond_to?("#{k}=")
|
61
|
+
end
|
62
|
+
hash.each do |k, v|
|
63
|
+
self.send("#{k}=",v)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
alias :attributes= :update_attributes_without_saving
|
67
|
+
end
|
68
|
+
end
|