better-ripple 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 +17 -0
- data/README.md +182 -0
- data/RELEASE_NOTES.md +284 -0
- data/better-ripple.gemspec +55 -0
- data/lib/rails/generators/ripple/configuration/configuration_generator.rb +13 -0
- data/lib/rails/generators/ripple/configuration/templates/ripple.yml +25 -0
- data/lib/rails/generators/ripple/js/js_generator.rb +13 -0
- data/lib/rails/generators/ripple/js/templates/js/contrib.js +63 -0
- data/lib/rails/generators/ripple/js/templates/js/iso8601.js +76 -0
- data/lib/rails/generators/ripple/js/templates/js/ripple.js +132 -0
- data/lib/rails/generators/ripple/model/model_generator.rb +20 -0
- data/lib/rails/generators/ripple/model/templates/model.rb.erb +10 -0
- data/lib/rails/generators/ripple/observer/observer_generator.rb +16 -0
- data/lib/rails/generators/ripple/observer/templates/observer.rb.erb +2 -0
- data/lib/rails/generators/ripple/test/templates/cucumber.rb.erb +7 -0
- data/lib/rails/generators/ripple/test/test_generator.rb +44 -0
- data/lib/rails/generators/ripple_generator.rb +79 -0
- data/lib/ripple.rb +86 -0
- data/lib/ripple/associations.rb +380 -0
- data/lib/ripple/associations/embedded.rb +35 -0
- data/lib/ripple/associations/instantiators.rb +26 -0
- data/lib/ripple/associations/linked.rb +65 -0
- data/lib/ripple/associations/many.rb +38 -0
- data/lib/ripple/associations/many_embedded_proxy.rb +39 -0
- data/lib/ripple/associations/many_linked_proxy.rb +66 -0
- data/lib/ripple/associations/many_reference_proxy.rb +95 -0
- data/lib/ripple/associations/many_stored_key_proxy.rb +76 -0
- data/lib/ripple/associations/one.rb +20 -0
- data/lib/ripple/associations/one_embedded_proxy.rb +35 -0
- data/lib/ripple/associations/one_key_proxy.rb +58 -0
- data/lib/ripple/associations/one_linked_proxy.rb +26 -0
- data/lib/ripple/associations/one_stored_key_proxy.rb +43 -0
- data/lib/ripple/associations/proxy.rb +118 -0
- data/lib/ripple/attribute_methods.rb +132 -0
- data/lib/ripple/attribute_methods/dirty.rb +59 -0
- data/lib/ripple/attribute_methods/query.rb +34 -0
- data/lib/ripple/attribute_methods/read.rb +28 -0
- data/lib/ripple/attribute_methods/write.rb +25 -0
- data/lib/ripple/callbacks.rb +71 -0
- data/lib/ripple/conflict/basic_resolver.rb +86 -0
- data/lib/ripple/conflict/document_hooks.rb +46 -0
- data/lib/ripple/conflict/resolver.rb +79 -0
- data/lib/ripple/conflict/test_helper.rb +34 -0
- data/lib/ripple/conversion.rb +29 -0
- data/lib/ripple/core_ext.rb +3 -0
- data/lib/ripple/core_ext/casting.rb +151 -0
- data/lib/ripple/core_ext/indexes.rb +89 -0
- data/lib/ripple/core_ext/object.rb +8 -0
- data/lib/ripple/document.rb +105 -0
- data/lib/ripple/document/bucket_access.rb +25 -0
- data/lib/ripple/document/finders.rb +131 -0
- data/lib/ripple/document/key.rb +35 -0
- data/lib/ripple/document/link.rb +30 -0
- data/lib/ripple/document/persistence.rb +130 -0
- data/lib/ripple/embedded_document.rb +63 -0
- data/lib/ripple/embedded_document/around_callbacks.rb +18 -0
- data/lib/ripple/embedded_document/finders.rb +26 -0
- data/lib/ripple/embedded_document/persistence.rb +75 -0
- data/lib/ripple/i18n.rb +5 -0
- data/lib/ripple/indexes.rb +151 -0
- data/lib/ripple/inspection.rb +32 -0
- data/lib/ripple/locale/en.yml +26 -0
- data/lib/ripple/locale/fr.yml +24 -0
- data/lib/ripple/nested_attributes.rb +275 -0
- data/lib/ripple/observable.rb +28 -0
- data/lib/ripple/properties.rb +74 -0
- data/lib/ripple/property_type_mismatch.rb +12 -0
- data/lib/ripple/railtie.rb +26 -0
- data/lib/ripple/railties/ripple.rake +103 -0
- data/lib/ripple/serialization.rb +82 -0
- data/lib/ripple/test_server.rb +35 -0
- data/lib/ripple/timestamps.rb +25 -0
- data/lib/ripple/translation.rb +18 -0
- data/lib/ripple/validations.rb +65 -0
- data/lib/ripple/validations/associated_validator.rb +43 -0
- data/lib/ripple/version.rb +3 -0
- metadata +310 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'ripple'
|
2
|
+
|
3
|
+
module Ripple
|
4
|
+
# Makes IRB and other inspect output a bit friendlier
|
5
|
+
module Inspection
|
6
|
+
|
7
|
+
# A human-readable version of the {Ripple::Document} or {Ripple::EmbeddedDocument}
|
8
|
+
# plus attributes
|
9
|
+
def inspect
|
10
|
+
attribute_list = attributes_for_persistence.except("_type").map {|k,v| "#{k}=#{v.inspect}" }.join(' ')
|
11
|
+
inspection_string(attribute_list)
|
12
|
+
end
|
13
|
+
|
14
|
+
# A string representation of the {Ripple::Document} or {Ripple::EmbeddedDocument}
|
15
|
+
def to_s
|
16
|
+
inspection_string
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def inspection_string(extra = nil)
|
22
|
+
body = self.class.name + persistance_identifier
|
23
|
+
body += " #{extra}" if extra
|
24
|
+
"<#{body}>"
|
25
|
+
end
|
26
|
+
|
27
|
+
def persistance_identifier
|
28
|
+
self.class.embeddable? ? "" : ":#{key || '[new]'}"
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
en:
|
2
|
+
ripple:
|
3
|
+
around_callbacks_not_supported: "around_%{type} callbacks are not supported on embedded documents. See https://github.com/seancribbs/ripple/issues/188 for more detail."
|
4
|
+
associated_document_error_summary: "for %{doc_type} %{doc_id}: %{errors}"
|
5
|
+
attribute_hash: "value of attributes must be a Hash"
|
6
|
+
cluster_not_created: "The cluster for the current environment is not created yet. Run `rake riak:create` first."
|
7
|
+
conflict_handler_not_implemented: "No on_conflict handler has been implemented for %{document}"
|
8
|
+
document_invalid: "Validation failed: %{errors}"
|
9
|
+
document_not_found:
|
10
|
+
no_key: "Couldn't find document without a key"
|
11
|
+
one_key: "Couldn't find document with key: %{key}"
|
12
|
+
many_keys: "Couldn't find documents with keys: %{keys}"
|
13
|
+
index_type_unknown: "Cannot determine index type for property '%{property}' of type '%{type}'."
|
14
|
+
index_undefined: "No index has been defined for property '%{property}' of type '%{type}'."
|
15
|
+
invalid_association_value: "Invalid value %{value} for association %{name} of type %{klass} on %{owner}"
|
16
|
+
many_association_validation_error: "are invalid (%{association_errors})"
|
17
|
+
many_key_association: "You cannot use a many association using :key"
|
18
|
+
mass_assignment_roles_unsupported: "Roles for mass assignment are not supported for Rails 3.0"
|
19
|
+
missing_configuration: "You are missing your ripple configuration file that should be at %{file}"
|
20
|
+
no_root_document: "You cannot call %{method} on %{doc} without a root document"
|
21
|
+
one_association_validation_error: "is invalid (%{association_errors})"
|
22
|
+
property_type_mismatch: "Cannot cast %{value} into a %{class}"
|
23
|
+
stored_key_requires_property: "Missing property: %{name}. Stored-key associations require a property on the owner document."
|
24
|
+
undefined_property: "Undefined property :%{prop} for class '%{class}'"
|
25
|
+
unexpected_conflicts: "Conflict cannot be resolved for %{conflicts} (for %{document})"
|
26
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
fr:
|
2
|
+
ripple:
|
3
|
+
around_callbacks_not_supported: "Les callbacks around_%{type} ne sont pas utilisables sur les documents embarqués. Voir https://github.com/seancribbs/ripple/issues/188 pour plus d'information."
|
4
|
+
associated_document_error_summary: "Pour %{doc_type} %{doc_id}: %{errors}"
|
5
|
+
attribute_hash: "La valeur des attributs doit être un Hash"
|
6
|
+
cluster_not_created: "Le cluster pour l'environnement actuel n'a pas été créé. Executez d'abord `rake riak:create`"
|
7
|
+
conflict_handler_not_implemented: "Aucun handler on_conflict n'a été implémenté pour %{document}"
|
8
|
+
document_invalid: "La validation a échoué : %{errors}"
|
9
|
+
document_not_found:
|
10
|
+
no_key: "Document introuvable sans clé."
|
11
|
+
one_key: "Document introuvable avec la clé %{key}"
|
12
|
+
many_keys: "Documents introuvables avec les clés : %{keys}"
|
13
|
+
index_type_unknown: "Impossible de déterminer le type d'index pour la propriété '%{property}' of type '%{type}'."
|
14
|
+
invalid_association_value: "Valeur invalide %{value} pour l'association %{name} de type %{klass} sur %{owner}"
|
15
|
+
many_association_validation_error: "sont invalides (%{association_errors})"
|
16
|
+
many_key_association: "Vous ne pouvez pas utiliser une association `many` avec :key"
|
17
|
+
mass_assignment_roles_unsupported: "Les rôles pour les assignations de masse ne sont pas supportés avec Rails 3.0"
|
18
|
+
missing_configuration: "Votre fichier de configuration est introuvable. Il devrait être ici : %{file}"
|
19
|
+
no_root_document: "Vous ne pouvez appeler %{method} sur %{doc} sans document racine."
|
20
|
+
one_association_validation_error: "est invalide (%{association_errors})"
|
21
|
+
property_type_mismatch: "Ne peut caster %{value} en %{class}"
|
22
|
+
undefined_property: "Propriété indéfinie :%{prop} pour la classe '%{class}'"
|
23
|
+
unexpected_conflicts: "Le conflit ne peut être résolu : %{conflicts} (pour %{document})"
|
24
|
+
|
@@ -0,0 +1,275 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
if ActiveSupport::VERSION::STRING < '3.2'
|
3
|
+
require 'active_support/core_ext/class/inheritable_attributes'
|
4
|
+
else
|
5
|
+
require 'active_support/core_ext/class/attribute'
|
6
|
+
end
|
7
|
+
|
8
|
+
module Ripple
|
9
|
+
module NestedAttributes #:nodoc:
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
UNASSIGNABLE_KEYS = %w{ _destroy }
|
13
|
+
TRUE_VALUES = [ true, "true", 1, "1", "yes", "ok", "y" ]
|
14
|
+
|
15
|
+
included do
|
16
|
+
if respond_to?(:class_attribute) # ActiveSupport 3.1
|
17
|
+
class_attribute :nested_attributes_options, :instance_writer => false
|
18
|
+
else
|
19
|
+
class_inheritable_accessor :nested_attributes_options, :instance_writer => false
|
20
|
+
end
|
21
|
+
self.nested_attributes_options = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
# = Nested Attributes
|
25
|
+
#
|
26
|
+
# This is similar to the `accepts_nested_attributes` functionality
|
27
|
+
# as found in AR. This allows the use update attributes and create
|
28
|
+
# new child records through the parent. It also allows the use of
|
29
|
+
# the `fields_for` form view helper, using a presenter pattern.
|
30
|
+
#
|
31
|
+
# To enable in the model, call the class method, using the same
|
32
|
+
# relationship as defined in the `one` or `many`.
|
33
|
+
#
|
34
|
+
# class Shipment
|
35
|
+
# include Ripple::Document
|
36
|
+
# one :box
|
37
|
+
# many :addresses
|
38
|
+
# accepts_nested_attributes_for :box, :addresses
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# == One
|
42
|
+
#
|
43
|
+
# Given this model:
|
44
|
+
#
|
45
|
+
# class Shipment
|
46
|
+
# include Ripple::Document
|
47
|
+
# one :box
|
48
|
+
# accepts_nested_attributes_for :box
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# This allows creating a box child during creation:
|
52
|
+
#
|
53
|
+
# shipment = Shipment.create(:box_attributes => { :shape => 'square' })
|
54
|
+
# shipment.box.shape # => 'square'
|
55
|
+
#
|
56
|
+
# This also allows updating box attributes:
|
57
|
+
#
|
58
|
+
# shipment.update_attributes(:box_attributes => { :key => 'xxx', :shape => 'triangle' })
|
59
|
+
# shipment.box.shape # => 'triangle'
|
60
|
+
#
|
61
|
+
# == Many
|
62
|
+
#
|
63
|
+
# Given this model
|
64
|
+
#
|
65
|
+
# class Manifest
|
66
|
+
# include Ripple::Document
|
67
|
+
# many :shipments
|
68
|
+
# accepts_nested_attributes_for :shipments
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# This allows creating several shipments during manifest creation:
|
72
|
+
#
|
73
|
+
# manifest = Manifest.create(:shipments_attributes => [ { :reference => "foo1" }, { :reference => "foo2" } ])
|
74
|
+
# manifest.shipments.size # => 2
|
75
|
+
# manifest.shipments.first.reference # => foo1
|
76
|
+
# manifest.shipments.second.reference # => foo2
|
77
|
+
#
|
78
|
+
# And updating shipment attributes:
|
79
|
+
#
|
80
|
+
# manifest.update_attributes(:shipment_attributes => [ { :key => 'xxx', :reference => 'updated foo1' },
|
81
|
+
# { :key => 'yyy', :reference => 'updated foo2' } ])
|
82
|
+
# manifest.shipments.first.reference # => updated foo1
|
83
|
+
# manifest.shipments.second.reference # => updated foo2
|
84
|
+
#
|
85
|
+
# NOTE: On many embedded, then entire collection of embedded documents is replaced, as there
|
86
|
+
# is no key to specifically update.
|
87
|
+
#
|
88
|
+
# Given
|
89
|
+
#
|
90
|
+
# class Manifest
|
91
|
+
# include Ripple::Documnet
|
92
|
+
# many :signatures
|
93
|
+
# accepts_nested_attributes_for :signatures
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# class Signature
|
97
|
+
# include Ripple::EmbeddedDocument
|
98
|
+
# property :esignature, String
|
99
|
+
# end
|
100
|
+
#
|
101
|
+
# The assigning of attributes replaces existing:
|
102
|
+
#
|
103
|
+
# manifest = Manifest.create(:signature_attributes => [ { :esig => 'a00001' }, { :esig => 'b00001' } ]
|
104
|
+
# manifest.signatures # => [<Signature esig="a00001">, <Signature esig="b00001">]
|
105
|
+
#
|
106
|
+
# manifest.signature_attributes = [ { :esig => 'c00001' } ]
|
107
|
+
# manifest.signatures # => [<Signature esig="c00001">]
|
108
|
+
#
|
109
|
+
module ClassMethods
|
110
|
+
|
111
|
+
def accepts_nested_attributes_for(*attr_names)
|
112
|
+
options = { :allow_destroy => false }
|
113
|
+
options.update(attr_names.extract_options!)
|
114
|
+
|
115
|
+
attr_names.each do |association_name|
|
116
|
+
if association = self.associations[association_name]
|
117
|
+
nested_attributes_options[association_name.to_sym] = options
|
118
|
+
|
119
|
+
define_method "#{association_name}_attributes=" do |attributes|
|
120
|
+
send("assign_nested_attributes_for_#{association.type}_association",
|
121
|
+
association_name, attributes
|
122
|
+
)
|
123
|
+
end
|
124
|
+
|
125
|
+
before_save :"autosave_nested_attributes_for_#{association_name}"
|
126
|
+
before_save :destroy_marked_for_destruction
|
127
|
+
|
128
|
+
define_method "autosave_nested_attributes_for_#{association_name}" do
|
129
|
+
if self.autosave[association_name]
|
130
|
+
send("save_nested_attributes_for_#{association.type}_association",
|
131
|
+
association_name
|
132
|
+
)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
private :"autosave_nested_attributes_for_#{association_name}"
|
136
|
+
|
137
|
+
else
|
138
|
+
raise ArgumentError, "Association #{association_name} not found!"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
protected
|
146
|
+
|
147
|
+
def autosave
|
148
|
+
@autosave_nested_attributes_for ||= {}
|
149
|
+
end
|
150
|
+
|
151
|
+
def marked_for_destruction
|
152
|
+
@marked_for_destruction ||= {}
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
def save_nested_attributes_for_one_association(association_name)
|
158
|
+
send(association_name).save
|
159
|
+
end
|
160
|
+
|
161
|
+
def save_nested_attributes_for_many_association(association_name)
|
162
|
+
send(association_name).map(&:save)
|
163
|
+
end
|
164
|
+
|
165
|
+
def destroy_marked_for_destruction
|
166
|
+
self.marked_for_destruction.each_pair do |association_name, resources|
|
167
|
+
resources.map(&:destroy)
|
168
|
+
send(association_name).reload
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def destroy_nested_many_association(association_name)
|
173
|
+
send(association_name).map(&:destroy)
|
174
|
+
end
|
175
|
+
|
176
|
+
def assign_nested_attributes_for_one_association(association_name, attributes)
|
177
|
+
association = self.class.associations[association_name]
|
178
|
+
if association.embedded?
|
179
|
+
assign_nested_attributes_for_one_embedded_association(association_name, attributes)
|
180
|
+
else
|
181
|
+
self.autosave[association_name] = true
|
182
|
+
assign_nested_attributes_for_one_linked_association(association_name, attributes)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def assign_nested_attributes_for_one_embedded_association(association_name, attributes)
|
187
|
+
send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
|
188
|
+
end
|
189
|
+
|
190
|
+
def assign_nested_attributes_for_one_linked_association(association_name, attributes)
|
191
|
+
attributes = attributes.stringify_keys
|
192
|
+
options = nested_attributes_options[association_name]
|
193
|
+
|
194
|
+
if attributes[key_attr.to_s].blank? && !reject_new_record?(association_name, attributes)
|
195
|
+
send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
|
196
|
+
else
|
197
|
+
if ((existing_record = send(association_name)).key.to_s == attributes[key_attr.to_s].to_s)
|
198
|
+
assign_to_or_mark_for_destruction(existing_record, attributes, association_name, options[:allow_destroy])
|
199
|
+
else
|
200
|
+
raise ArgumentError, "Attempting to update a child that isn't already associated to the parent."
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def assign_nested_attributes_for_many_association(association_name, attributes_collection)
|
206
|
+
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
|
207
|
+
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
|
208
|
+
end
|
209
|
+
|
210
|
+
if attributes_collection.is_a? Hash
|
211
|
+
attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
|
212
|
+
end
|
213
|
+
|
214
|
+
association = self.class.associations[association_name]
|
215
|
+
if association.embedded?
|
216
|
+
assign_nested_attributes_for_many_embedded_association(association_name, attributes_collection)
|
217
|
+
else
|
218
|
+
self.autosave[association_name] = true
|
219
|
+
assign_nested_attributes_for_many_linked_association(association_name, attributes_collection)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def assign_nested_attributes_for_many_embedded_association(association_name, attributes_collection)
|
224
|
+
options = nested_attributes_options[association_name]
|
225
|
+
send(:"#{association_name}=", []) # Clobber existing
|
226
|
+
attributes_collection.each do |attributes|
|
227
|
+
attributes = attributes.stringify_keys
|
228
|
+
if !reject_new_record?(association_name, attributes)
|
229
|
+
send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def assign_nested_attributes_for_many_linked_association(association_name, attributes_collection)
|
235
|
+
options = nested_attributes_options[association_name]
|
236
|
+
attributes_collection.each do |attributes|
|
237
|
+
attributes = attributes.stringify_keys
|
238
|
+
|
239
|
+
if attributes[key_attr.to_s].blank? && !reject_new_record?(association_name, attributes)
|
240
|
+
send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
|
241
|
+
elsif existing_record = send(association_name).detect { |record| record.key.to_s == attributes[key_attr.to_s].to_s }
|
242
|
+
assign_to_or_mark_for_destruction(existing_record, attributes, association_name, options[:allow_destroy])
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def assign_to_or_mark_for_destruction(record, attributes, association_name, allow_destroy)
|
248
|
+
if has_destroy_flag?(attributes) && allow_destroy
|
249
|
+
(self.marked_for_destruction[association_name] ||= []) << record
|
250
|
+
else
|
251
|
+
record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def has_destroy_flag?(hash)
|
256
|
+
TRUE_VALUES.include?(hash.stringify_keys['_destroy'])
|
257
|
+
end
|
258
|
+
|
259
|
+
def reject_new_record?(association_name, attributes)
|
260
|
+
has_destroy_flag?(attributes) || call_reject_if(association_name, attributes)
|
261
|
+
end
|
262
|
+
|
263
|
+
def call_reject_if(association_name, attributes)
|
264
|
+
attributes = attributes.stringify_keys
|
265
|
+
case callback = nested_attributes_options[association_name][:reject_if]
|
266
|
+
when Symbol
|
267
|
+
method(callback).arity == 0 ? send(callback) : send(callback, attributes)
|
268
|
+
when Proc
|
269
|
+
callback.call(attributes)
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
end
|
274
|
+
|
275
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
|
2
|
+
require 'active_support/concern'
|
3
|
+
require 'active_model/observing'
|
4
|
+
|
5
|
+
module Ripple
|
6
|
+
module Observable
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
include ActiveModel::Observing
|
9
|
+
|
10
|
+
included do
|
11
|
+
set_callback(:create, :before) { notify_observers :before_create }
|
12
|
+
set_callback(:create, :after) { notify_observers :after_create }
|
13
|
+
|
14
|
+
set_callback(:update, :before) { notify_observers :before_update }
|
15
|
+
set_callback(:update, :after) { notify_observers :after_update }
|
16
|
+
|
17
|
+
set_callback(:save, :before) { notify_observers :before_save }
|
18
|
+
set_callback(:save, :after) { notify_observers :after_save }
|
19
|
+
|
20
|
+
set_callback(:destroy, :before) { notify_observers :before_destroy }
|
21
|
+
set_callback(:destroy, :after) { notify_observers :after_destroy }
|
22
|
+
|
23
|
+
set_callback(:validation, :before) { notify_observers :before_validation }
|
24
|
+
set_callback(:validation, :after) { notify_observers :after_validation }
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'ripple/core_ext/casting'
|
2
|
+
require 'active_support/core_ext/hash/except'
|
3
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
4
|
+
|
5
|
+
module Ripple
|
6
|
+
# Adds the ability to declare properties on your Ripple::Document class.
|
7
|
+
# Properties will automatically generate accessor (get/set/query) methods and
|
8
|
+
# handle type-casting between your Ruby type and JSON-compatible types.
|
9
|
+
module Properties
|
10
|
+
# @private
|
11
|
+
def inherited(subclass)
|
12
|
+
super
|
13
|
+
subclass.properties.merge!(properties)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [Hash] the properties defined on the document
|
17
|
+
def properties
|
18
|
+
@properties ||= {}.with_indifferent_access
|
19
|
+
end
|
20
|
+
|
21
|
+
def property(key, type, options={})
|
22
|
+
prop = Property.new(key, type, options)
|
23
|
+
properties[prop.key] = prop
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Encapsulates a single property on your Ripple::Document class.
|
28
|
+
class Property
|
29
|
+
# @return [Symbol] the key of this property in the Document
|
30
|
+
attr_reader :key
|
31
|
+
# @return [Class] the Ruby type of property.
|
32
|
+
attr_reader :type
|
33
|
+
# @return [Hash] configuration options
|
34
|
+
attr_reader :options
|
35
|
+
|
36
|
+
# Create a new document property.
|
37
|
+
# @param [String, Symbol] key the key of the property
|
38
|
+
# @param [Class] type the Ruby type of the property. Use {Boolean} for true or false types.
|
39
|
+
# @param [Hash] options configuration options
|
40
|
+
# @option options [Object, Proc] :default (nil) a default value
|
41
|
+
# for the property, or a lambda to evaluate when providing the default.
|
42
|
+
def initialize(key, type, options={})
|
43
|
+
@options = options.to_options
|
44
|
+
@key = key.to_sym
|
45
|
+
@type = type
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [Object] The default value for this property if defined, or nil.
|
49
|
+
def default
|
50
|
+
default = options[:default]
|
51
|
+
default = default.dup if default.duplicable?
|
52
|
+
|
53
|
+
return nil if default.nil?
|
54
|
+
type_cast(default.respond_to?(:call) ? default.call : default)
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [Hash] options appropriate for the validates class method
|
58
|
+
def validation_options
|
59
|
+
@options.dup.except(:default)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Attempt to coerce the passed value into this property's type
|
63
|
+
# @param [Object] value the value to coerce
|
64
|
+
# @return [Object] the value coerced into this property's type
|
65
|
+
# @raise [PropertyTypeMismatch] if the value cannot be coerced into the property's type
|
66
|
+
def type_cast(value)
|
67
|
+
if @type.respond_to?(:ripple_cast)
|
68
|
+
@type.ripple_cast(value)
|
69
|
+
else
|
70
|
+
value
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|