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/lib/couchrest/mixins.rb
CHANGED
@@ -0,0 +1,75 @@
|
|
1
|
+
module CouchRest
|
2
|
+
module Mixins
|
3
|
+
module Attributes
|
4
|
+
|
5
|
+
## Support for handling attributes
|
6
|
+
#
|
7
|
+
# This would be better in the properties file, but due to scoping issues
|
8
|
+
# this is not yet possible.
|
9
|
+
#
|
10
|
+
|
11
|
+
def prepare_all_attributes(doc = {}, options = {})
|
12
|
+
apply_all_property_defaults
|
13
|
+
if options[:directly_set_attributes]
|
14
|
+
directly_set_read_only_attributes(doc)
|
15
|
+
else
|
16
|
+
remove_protected_attributes(doc)
|
17
|
+
end
|
18
|
+
directly_set_attributes(doc) unless doc.nil?
|
19
|
+
end
|
20
|
+
|
21
|
+
# Takes a hash as argument, and applies the values by using writer methods
|
22
|
+
# for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
|
23
|
+
# missing. In case of error, no attributes are changed.
|
24
|
+
def update_attributes_without_saving(hash)
|
25
|
+
# Remove any protected and update all the rest. Any attributes
|
26
|
+
# which do not have a property will simply be ignored.
|
27
|
+
attrs = remove_protected_attributes(hash)
|
28
|
+
directly_set_attributes(attrs)
|
29
|
+
end
|
30
|
+
alias :attributes= :update_attributes_without_saving
|
31
|
+
|
32
|
+
# Takes a hash as argument, and applies the values by using writer methods
|
33
|
+
# for each key. Raises a NoMethodError if the corresponding methods are
|
34
|
+
# missing. In case of error, no attributes are changed.
|
35
|
+
def update_attributes(hash)
|
36
|
+
update_attributes_without_saving hash
|
37
|
+
save
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def directly_set_attributes(hash)
|
43
|
+
hash.each do |attribute_name, attribute_value|
|
44
|
+
if self.respond_to?("#{attribute_name}=")
|
45
|
+
self.send("#{attribute_name}=", hash.delete(attribute_name))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def directly_set_read_only_attributes(hash)
|
51
|
+
property_list = self.properties.map{|p| p.name}
|
52
|
+
hash.each do |attribute_name, attribute_value|
|
53
|
+
next if self.respond_to?("#{attribute_name}=")
|
54
|
+
if property_list.include?(attribute_name)
|
55
|
+
write_attribute(attribute_name, hash.delete(attribute_name))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def set_attributes(hash)
|
61
|
+
attrs = remove_protected_attributes(hash)
|
62
|
+
directly_set_attributes(attrs)
|
63
|
+
end
|
64
|
+
|
65
|
+
def check_properties_exist(attrs)
|
66
|
+
property_list = self.properties.map{|p| p.name}
|
67
|
+
attrs.each do |attribute_name, attribute_value|
|
68
|
+
raise NoMethodError, "Property #{attribute_name} not created" unless respond_to?("#{attribute_name}=") or property_list.include?(attribute_name)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
@@ -370,6 +370,8 @@ module CouchRest
|
|
370
370
|
end
|
371
371
|
|
372
372
|
module ClassMethods
|
373
|
+
extend CouchRest::InheritableAttributes
|
374
|
+
|
373
375
|
#CHAINS = {:before => :before, :around => :before, :after => :after}
|
374
376
|
|
375
377
|
# Make the _run_save_callbacks method. The generated method takes
|
@@ -497,9 +499,9 @@ module CouchRest
|
|
497
499
|
def define_callbacks(*symbols)
|
498
500
|
terminator = symbols.pop if symbols.last.is_a?(String)
|
499
501
|
symbols.each do |symbol|
|
500
|
-
|
502
|
+
couchrest_inheritable_accessor("_#{symbol}_terminator") { terminator }
|
501
503
|
|
502
|
-
|
504
|
+
couchrest_inheritable_accessor("_#{symbol}_callback") do
|
503
505
|
CallbackChain.new(symbol)
|
504
506
|
end
|
505
507
|
|
@@ -1,7 +1,6 @@
|
|
1
1
|
require 'time'
|
2
2
|
require File.join(File.dirname(__FILE__), '..', 'property')
|
3
3
|
require File.join(File.dirname(__FILE__), '..', 'casted_array')
|
4
|
-
require File.join(File.dirname(__FILE__), '..', 'typecast')
|
5
4
|
|
6
5
|
module CouchRest
|
7
6
|
module Mixins
|
@@ -9,80 +8,45 @@ module CouchRest
|
|
9
8
|
|
10
9
|
class IncludeError < StandardError; end
|
11
10
|
|
12
|
-
include ::CouchRest::More::Typecast
|
13
|
-
|
14
11
|
def self.included(base)
|
15
12
|
base.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
16
|
-
|
13
|
+
extend CouchRest::InheritableAttributes
|
14
|
+
couchrest_inheritable_accessor(:properties) unless self.respond_to?(:properties)
|
17
15
|
self.properties ||= []
|
18
16
|
EOS
|
19
17
|
base.extend(ClassMethods)
|
20
18
|
raise CouchRest::Mixins::Properties::IncludeError, "You can only mixin Properties in a class responding to [] and []=, if you tried to mixin CastedModel, make sure your class inherits from Hash or responds to the proper methods" unless (base.new.respond_to?(:[]) && base.new.respond_to?(:[]=))
|
21
19
|
end
|
22
|
-
|
23
|
-
|
20
|
+
|
21
|
+
# Returns the Class properties
|
22
|
+
#
|
23
|
+
# ==== Returns
|
24
|
+
# Array:: the list of properties for model's class
|
25
|
+
def properties
|
26
|
+
self.class.properties
|
27
|
+
end
|
28
|
+
|
29
|
+
def read_attribute(property)
|
30
|
+
self[property.to_s]
|
31
|
+
end
|
32
|
+
|
33
|
+
def write_attribute(property, value)
|
34
|
+
prop = property.is_a?(::CouchRest::Property) ? property : self.class.properties.detect {|p| p.to_s == property.to_s}
|
35
|
+
raise "Missing property definition for #{property.to_s}" unless prop
|
36
|
+
self[prop.to_s] = prop.cast(self, value)
|
37
|
+
end
|
38
|
+
|
39
|
+
def apply_all_property_defaults
|
24
40
|
return if self.respond_to?(:new?) && (new? == false)
|
25
|
-
return unless self.class.respond_to?(:properties)
|
26
|
-
return if self.class.properties.empty?
|
27
41
|
# TODO: cache the default object
|
28
42
|
self.class.properties.each do |property|
|
29
|
-
|
30
|
-
# let's make sure we have a default
|
31
|
-
unless property.default.nil?
|
32
|
-
if property.default.class == Proc
|
33
|
-
self[key] = property.default.call
|
34
|
-
else
|
35
|
-
self[key] = Marshal.load(Marshal.dump(property.default))
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def cast_keys
|
42
|
-
return unless self.class.properties
|
43
|
-
self.class.properties.each do |property|
|
44
|
-
cast_property(property)
|
43
|
+
write_attribute(property, property.default_value)
|
45
44
|
end
|
46
45
|
end
|
47
|
-
|
48
|
-
def cast_property(property, assigned=false)
|
49
|
-
return unless property.casted
|
50
|
-
key = self.has_key?(property.name) ? property.name : property.name.to_sym
|
51
|
-
# Don't cast the property unless it has a value
|
52
|
-
return unless self[key]
|
53
|
-
if property.type.is_a?(Array)
|
54
|
-
klass = property.type[0]
|
55
|
-
self[key] = [self[key]] unless self[key].is_a?(Array)
|
56
|
-
arr = self[key].collect do |value|
|
57
|
-
value = typecast_value(value, klass, property.init_method)
|
58
|
-
associate_casted_to_parent(value, assigned)
|
59
|
-
value
|
60
|
-
end
|
61
|
-
# allow casted_by calls to be passed up chain by wrapping in CastedArray
|
62
|
-
self[key] = klass != String ? ::CouchRest::CastedArray.new(arr) : arr
|
63
|
-
self[key].casted_by = self if self[key].respond_to?(:casted_by)
|
64
|
-
else
|
65
|
-
self[key] = typecast_value(self[key], property.type, property.init_method)
|
66
|
-
associate_casted_to_parent(self[key], assigned)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def associate_casted_to_parent(casted, assigned)
|
71
|
-
casted.casted_by = self if casted.respond_to?(:casted_by)
|
72
|
-
casted.document_saved = true if !assigned && casted.respond_to?(:document_saved)
|
73
|
-
end
|
74
|
-
|
75
|
-
def cast_property_by_name(property_name)
|
76
|
-
return unless self.class.properties
|
77
|
-
property = self.class.properties.detect{|property| property.name == property_name}
|
78
|
-
return unless property
|
79
|
-
cast_property(property, true)
|
80
|
-
end
|
81
|
-
|
82
46
|
|
83
47
|
module ClassMethods
|
84
48
|
|
85
|
-
def property(name, *options)
|
49
|
+
def property(name, *options, &block)
|
86
50
|
opts = { }
|
87
51
|
type = options.shift
|
88
52
|
if type.class != Hash
|
@@ -93,21 +57,29 @@ module CouchRest
|
|
93
57
|
end
|
94
58
|
existing_property = self.properties.find{|p| p.name == name.to_s}
|
95
59
|
if existing_property.nil? || (existing_property.default != opts[:default])
|
96
|
-
define_property(name, opts)
|
60
|
+
define_property(name, opts, &block)
|
97
61
|
end
|
98
62
|
end
|
99
63
|
|
100
64
|
protected
|
101
65
|
|
102
66
|
# This is not a thread safe operation, if you have to set new properties at runtime
|
103
|
-
# make sure
|
104
|
-
def define_property(name, options={})
|
67
|
+
# make sure a mutex is used.
|
68
|
+
def define_property(name, options={}, &block)
|
105
69
|
# check if this property is going to casted
|
106
|
-
|
107
|
-
|
70
|
+
type = options.delete(:type) || options.delete(:cast_as)
|
71
|
+
if block_given?
|
72
|
+
type = Class.new(Hash) do
|
73
|
+
include CastedModel
|
74
|
+
end
|
75
|
+
type.class_eval { yield type }
|
76
|
+
type = [type] # inject as an array
|
77
|
+
end
|
78
|
+
property = CouchRest::Property.new(name, type, options)
|
108
79
|
create_property_getter(property)
|
109
80
|
create_property_setter(property) unless property.read_only == true
|
110
81
|
properties << property
|
82
|
+
property
|
111
83
|
end
|
112
84
|
|
113
85
|
# defines the getter for the property (and optional aliases)
|
@@ -115,18 +87,15 @@ module CouchRest
|
|
115
87
|
# meth = property.name
|
116
88
|
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
117
89
|
def #{property.name}
|
118
|
-
|
90
|
+
read_attribute('#{property.name}')
|
119
91
|
end
|
120
92
|
EOS
|
121
93
|
|
122
94
|
if ['boolean', TrueClass.to_s.downcase].include?(property.type.to_s.downcase)
|
123
95
|
class_eval <<-EOS, __FILE__, __LINE__
|
124
96
|
def #{property.name}?
|
125
|
-
|
126
|
-
|
127
|
-
else
|
128
|
-
true
|
129
|
-
end
|
97
|
+
value = read_attribute('#{property.name}')
|
98
|
+
!(value.nil? || value == false)
|
130
99
|
end
|
131
100
|
EOS
|
132
101
|
end
|
@@ -143,8 +112,7 @@ module CouchRest
|
|
143
112
|
property_name = property.name
|
144
113
|
class_eval <<-EOS
|
145
114
|
def #{property_name}=(value)
|
146
|
-
|
147
|
-
cast_property_by_name('#{property_name}')
|
115
|
+
write_attribute('#{property_name}', value)
|
148
116
|
end
|
149
117
|
EOS
|
150
118
|
|
@@ -1,7 +1,6 @@
|
|
1
1
|
require 'time'
|
2
2
|
require 'bigdecimal'
|
3
3
|
require 'bigdecimal/util'
|
4
|
-
require File.join(File.dirname(__FILE__), 'property')
|
5
4
|
|
6
5
|
class Time
|
7
6
|
# returns a local time value much faster than Time.parse
|
@@ -23,19 +22,19 @@ class Time
|
|
23
22
|
end
|
24
23
|
|
25
24
|
module CouchRest
|
26
|
-
module
|
25
|
+
module Mixins
|
27
26
|
module Typecast
|
28
27
|
|
29
|
-
def typecast_value(value, klass, init_method)
|
28
|
+
def typecast_value(value, property) # klass, init_method)
|
30
29
|
return nil if value.nil?
|
31
|
-
klass =
|
30
|
+
klass = property.type_class
|
32
31
|
if value.instance_of?(klass) || klass == Object
|
33
32
|
value
|
34
33
|
elsif [String, TrueClass, Integer, Float, BigDecimal, DateTime, Time, Date, Class].include?(klass)
|
35
34
|
send('typecast_to_'+klass.to_s.downcase, value)
|
36
35
|
else
|
37
36
|
# Allow the init_method to be defined as a Proc for advanced conversion
|
38
|
-
init_method.is_a?(Proc) ? init_method.call(value) : klass.send(init_method, value)
|
37
|
+
property.init_method.is_a?(Proc) ? property.init_method.call(value) : klass.send(property.init_method, value)
|
39
38
|
end
|
40
39
|
end
|
41
40
|
|
data/lib/couchrest/property.rb
CHANGED
@@ -1,47 +1,93 @@
|
|
1
|
+
|
2
|
+
require File.join(File.dirname(__FILE__), 'mixins', 'typecast')
|
3
|
+
|
1
4
|
module CouchRest
|
2
5
|
|
3
6
|
# Basic attribute support for adding getter/setter + validation
|
4
7
|
class Property
|
8
|
+
|
9
|
+
include ::CouchRest::Mixins::Typecast
|
10
|
+
|
5
11
|
attr_reader :name, :type, :read_only, :alias, :default, :casted, :init_method, :options
|
6
12
|
|
7
|
-
#
|
13
|
+
# Attribute to define.
|
14
|
+
# All Properties are assumed casted unless the type is nil.
|
8
15
|
def initialize(name, type = nil, options = {})
|
9
16
|
@name = name.to_s
|
17
|
+
@casted = true
|
10
18
|
parse_type(type)
|
11
19
|
parse_options(options)
|
12
20
|
self
|
13
21
|
end
|
14
22
|
|
23
|
+
def to_s
|
24
|
+
name
|
25
|
+
end
|
26
|
+
|
27
|
+
# Cast the provided value using the properties details.
|
28
|
+
def cast(parent, value)
|
29
|
+
return value unless casted
|
30
|
+
if type.is_a?(Array)
|
31
|
+
# Convert to array if it is not already
|
32
|
+
value = [value].compact unless value.is_a?(Array)
|
33
|
+
arr = value.collect { |data| cast_value(parent, data) }
|
34
|
+
# allow casted_by calls to be passed up chain by wrapping in CastedArray
|
35
|
+
value = type_class != String ? ::CouchRest::CastedArray.new(arr, self) : arr
|
36
|
+
value.casted_by = parent if value.respond_to?(:casted_by)
|
37
|
+
elsif !value.nil?
|
38
|
+
value = cast_value(parent, value)
|
39
|
+
end
|
40
|
+
value
|
41
|
+
end
|
42
|
+
|
43
|
+
# Cast an individual value, not an array
|
44
|
+
def cast_value(parent, value)
|
45
|
+
raise "An array inside an array cannot be casted, use CastedModel" if value.is_a?(Array)
|
46
|
+
value = typecast_value(value, self)
|
47
|
+
associate_casted_value_to_parent(parent, value)
|
48
|
+
end
|
49
|
+
|
50
|
+
def default_value
|
51
|
+
return if default.nil?
|
52
|
+
if default.class == Proc
|
53
|
+
default.call
|
54
|
+
else
|
55
|
+
Marshal.load(Marshal.dump(default))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Always provide the basic type as a class. If the type
|
60
|
+
# is an array, the class will be extracted.
|
61
|
+
def type_class
|
62
|
+
return String unless casted # This is rubbish, to handle validations
|
63
|
+
return @type_class unless @type_class.nil?
|
64
|
+
base = @type.is_a?(Array) ? @type.first : @type
|
65
|
+
base = String if base.nil?
|
66
|
+
base = TrueClass if base.is_a?(String) && base.downcase == 'boolean'
|
67
|
+
@type_class = base.is_a?(Class) ? base : base.constantize
|
68
|
+
end
|
69
|
+
|
15
70
|
private
|
16
71
|
|
72
|
+
def associate_casted_value_to_parent(parent, value)
|
73
|
+
value.casted_by = parent if value.respond_to?(:casted_by)
|
74
|
+
value
|
75
|
+
end
|
76
|
+
|
17
77
|
def parse_type(type)
|
18
78
|
if type.nil?
|
19
|
-
@
|
20
|
-
|
21
|
-
@type = [Object]
|
79
|
+
@casted = false
|
80
|
+
@type = nil
|
22
81
|
else
|
23
|
-
|
24
|
-
if base_type.is_a?(String)
|
25
|
-
if base_type.downcase == 'boolean'
|
26
|
-
base_type = TrueClass
|
27
|
-
else
|
28
|
-
begin
|
29
|
-
base_type = base_type.constantize
|
30
|
-
rescue # leave base type as a string and convert in more/typecast
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
@type = type.is_a?(Array) ? [base_type] : base_type
|
82
|
+
@type = type
|
35
83
|
end
|
36
84
|
end
|
37
85
|
|
38
86
|
def parse_options(options)
|
39
|
-
return if options.empty?
|
40
87
|
@validation_format = options.delete(:format) if options[:format]
|
41
88
|
@read_only = options.delete(:read_only) if options[:read_only]
|
42
89
|
@alias = options.delete(:alias) if options[:alias]
|
43
90
|
@default = options.delete(:default) unless options[:default].nil?
|
44
|
-
@casted = options[:casted] ? true : false
|
45
91
|
@init_method = options[:init_method] ? options.delete(:init_method) : 'new'
|
46
92
|
@options = options
|
47
93
|
end
|
data/lib/couchrest/validation.rb
CHANGED
@@ -48,8 +48,10 @@ module CouchRest
|
|
48
48
|
module Validation
|
49
49
|
|
50
50
|
def self.included(base)
|
51
|
-
base.extlib_inheritable_accessor(:auto_validation)
|
52
51
|
base.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
52
|
+
extend CouchRest::InheritableAttributes
|
53
|
+
couchrest_inheritable_accessor(:auto_validation)
|
54
|
+
|
53
55
|
# Callbacks
|
54
56
|
define_callbacks :validate
|
55
57
|
|
@@ -80,10 +82,9 @@ module CouchRest
|
|
80
82
|
end
|
81
83
|
EOS
|
82
84
|
base.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
83
|
-
def self.define_property(name, options={})
|
84
|
-
super
|
85
|
-
auto_generate_validations(
|
86
|
-
autovalidation_check = true
|
85
|
+
def self.define_property(name, options={}, &block)
|
86
|
+
property = super
|
87
|
+
auto_generate_validations(property) unless property.nil?
|
87
88
|
end
|
88
89
|
RUBY_EVAL
|
89
90
|
end
|