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.
@@ -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