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.
@@ -9,3 +9,4 @@ require File.join(mixins_dir, 'extended_attachments')
9
9
  require File.join(mixins_dir, 'class_proxy')
10
10
  require File.join(mixins_dir, 'collection')
11
11
  require File.join(mixins_dir, 'attribute_protection')
12
+ require File.join(mixins_dir, 'attributes')
@@ -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
- extlib_inheritable_accessor("_#{symbol}_terminator") { terminator }
502
+ couchrest_inheritable_accessor("_#{symbol}_terminator") { terminator }
501
503
 
502
- extlib_inheritable_accessor("_#{symbol}_callback") do
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
- extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
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
- def apply_defaults
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
- key = property.name.to_s
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 to use a mutex.
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
- options[:casted] = !!(options[:cast_as] || options[:type])
107
- property = CouchRest::Property.new(name, (options.delete(:cast_as) || options.delete(:type)), options)
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
- self['#{property.name}']
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
- if self['#{property.name}'].nil? || self['#{property.name}'] == false
126
- false
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
- self['#{property_name}'] = value
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 More
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 = klass.constantize unless klass.is_a?(Class)
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
 
@@ -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
- # attribute to define
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
- @type = String
20
- elsif type.is_a?(Array) && type.empty?
21
- @type = [Object]
79
+ @casted = false
80
+ @type = nil
22
81
  else
23
- base_type = type.is_a?(Array) ? type.first : type
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
@@ -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(properties.last) if properties && properties.size > 0
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