couchrest_extended_document 1.0.0.beta6 → 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/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
|