property 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,10 @@
1
+ == 0.8.0 2010-02-11
2
+
3
+ * 3 major enhancements
4
+ * enabled Behaviors that can be added on an instance
5
+ * enabled non-DB types
6
+ * 100% test coverage
7
+
1
8
  == 0.7.0 2010-02-11
2
9
 
3
10
  * 2 major enhancement
@@ -46,3 +46,25 @@ And set them with:
46
46
  @contact.prop['name'] = 'Gandhi'
47
47
  @contact.name = 'Gandhi'
48
48
 
49
+ == Behaviors
50
+
51
+ Properties would not be really fun if you could not add new properties to your instances depending
52
+ on some flags. First define the behaviors:
53
+
54
+ @a_picture = Property::Behavior.new do |p|
55
+ p.integer :width, :default => :get_width
56
+ p.integer :height, :default => :get_height
57
+ p.string 'camera'
58
+ p.string 'location'
59
+ end
60
+
61
+ And then, either when creating new pictures or updating them, you need to include the behavior:
62
+
63
+ @model.behave_like @a_picture
64
+
65
+ The model now has the picture's properties defined, with accessors like @model.camera and default
66
+ values will be fetched on save.
67
+
68
+ Note that you do not need to include a behavior just to read the data as long as you use the 'prop'
69
+ accessor.
70
+
@@ -2,16 +2,29 @@ require 'property/attribute'
2
2
  require 'property/dirty'
3
3
  require 'property/properties'
4
4
  require 'property/column'
5
+ require 'property/behavior'
6
+ require 'property/schema'
5
7
  require 'property/declaration'
6
8
  require 'property/serialization/json'
7
9
  require 'property/core_ext/time'
8
10
 
9
11
  module Property
10
- VERSION = '0.7.0'
12
+ VERSION = '0.8.0'
11
13
 
12
14
  def self.included(base)
13
15
  base.class_eval do
14
16
  include ::Property::Attribute
15
17
  end
16
18
  end
19
+
20
+ def self.validators
21
+ @@validators ||= []
22
+ end
23
+
24
+ def self.validate_property_class(type)
25
+ @@validators.each do |validator|
26
+ return false unless validator.validate(type)
27
+ end
28
+ true
29
+ end
17
30
  end
@@ -0,0 +1,167 @@
1
+ module Property
2
+ # This class holds a set of property definitions. This is like a Module in ruby:
3
+ # by 'including' this behavior in a class or in an instance, you augment the said
4
+ # object with the behavior's property definitions.
5
+ class Behavior
6
+ attr_accessor :name, :included, :accessor_module
7
+
8
+ def self.new(name)
9
+ obj = super
10
+ if block_given?
11
+ yield obj
12
+ end
13
+ obj
14
+ end
15
+
16
+ # Initialize a new behavior with the given name
17
+ def initialize(name)
18
+ @name = name
19
+ @included_in_schemas = []
20
+ @accessor_module = Module.new
21
+ end
22
+
23
+ # List all property definitiosn for the current behavior
24
+ def columns
25
+ @columns ||= {}
26
+ end
27
+
28
+ # Return the list of column names.
29
+ def column_names
30
+ columns.keys
31
+ end
32
+
33
+ # Use this method to declare properties into a Behavior.
34
+ # Example:
35
+ # @behavior.property.string 'phone', :default => ''
36
+ #
37
+ # You can also use a block:
38
+ # @behavior.property do |p|
39
+ # p.string 'phone', 'name', :default => ''
40
+ # end
41
+ def property
42
+ if block_given?
43
+ yield self
44
+ end
45
+ self
46
+ end
47
+
48
+ # def string(*args)
49
+ # options = args.extract_options!
50
+ # column_names = args
51
+ # default = options.delete(:default)
52
+ # column_names.each { |name| column(name, default, 'string', options) }
53
+ # end
54
+ %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
55
+ class_eval <<-EOV
56
+ def #{column_type}(*args)
57
+ options = args.extract_options!
58
+ column_names = args
59
+ default = options.delete(:default)
60
+ column_names.each { |name| add_column(Property::Column.new(name, default, '#{column_type}', options)) }
61
+ end
62
+ EOV
63
+ end
64
+
65
+ # This is used to serialize a non-native DB type. Use:
66
+ # p.serialize 'pet', Dog
67
+ def serialize(name, klass, options = {})
68
+ Property.validate_property_class(klass)
69
+ add_column(Property::Column.new(name, nil, klass, options))
70
+ end
71
+
72
+ # @internal
73
+ # This is called when the behavior is included in a schema
74
+ def included(schema)
75
+ @included_in_schemas << schema
76
+ end
77
+
78
+ private
79
+
80
+ def define_property_methods(column)
81
+ name = column.name
82
+
83
+ #if create_time_zone_conversion_attribute?(name, column)
84
+ # define_read_property_method_for_time_zone_conversion(name)
85
+ #else
86
+ define_read_property_method(name.to_sym, name, column)
87
+ #end
88
+
89
+ #if create_time_zone_conversion_attribute?(name, column)
90
+ # define_write_property_method_for_time_zone_conversion(name)
91
+ #else
92
+ define_write_property_method(name.to_sym)
93
+ #end
94
+
95
+ define_question_property_method(name)
96
+ end
97
+
98
+ # Define a property reader method. Cope with nil column.
99
+ def define_read_property_method(symbol, attr_name, column)
100
+ # Unlike rails, we do not cast on read
101
+ evaluate_attribute_property_method attr_name, "def #{symbol}; prop['#{attr_name}']; end"
102
+ end
103
+
104
+ # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
105
+ # This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone.
106
+ # def define_read_property_method_for_time_zone_conversion(attr_name)
107
+ # method_body = <<-EOV
108
+ # def #{attr_name}(reload = false)
109
+ # cached = @attributes_cache['#{attr_name}']
110
+ # return cached if cached && !reload
111
+ # time = properties['#{attr_name}']
112
+ # @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
113
+ # end
114
+ # EOV
115
+ # evaluate_attribute_property_method attr_name, method_body
116
+ # end
117
+
118
+ # Defines a predicate method <tt>attr_name?</tt>.
119
+ def define_question_property_method(attr_name)
120
+ evaluate_attribute_property_method attr_name, "def #{attr_name}?; prop['#{attr_name}']; end", "#{attr_name}?"
121
+ end
122
+
123
+ def define_write_property_method(attr_name)
124
+ evaluate_attribute_property_method attr_name, "def #{attr_name}=(new_value);prop['#{attr_name}'] = new_value; end", "#{attr_name}="
125
+ end
126
+
127
+ # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
128
+ # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
129
+ # def define_write_property_method_for_time_zone_conversion(attr_name)
130
+ # method_body = <<-EOV
131
+ # def #{attr_name}=(time)
132
+ # unless time.acts_like?(:time)
133
+ # time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
134
+ # end
135
+ # time = time.in_time_zone rescue nil if time
136
+ # prop['#{attr_name}'] = time
137
+ # end
138
+ # EOV
139
+ # evaluate_attribute_property_method attr_name, method_body, "#{attr_name}="
140
+ # end
141
+
142
+ # Evaluate the definition for an attribute related method
143
+ def evaluate_attribute_property_method(attr_name, method_definition, method_name=attr_name)
144
+ accessor_module.class_eval(method_definition, __FILE__, __LINE__)
145
+ end
146
+
147
+ def add_column(column)
148
+ name = column.name
149
+
150
+ if columns[name]
151
+ raise TypeError.new("Property '#{name}' is already defined.")
152
+ else
153
+ verify_not_defined_in_schemas_using_this_behavior(name)
154
+ define_property_methods(column) if column.should_create_accessors?
155
+ columns[column.name] = column
156
+ end
157
+ end
158
+
159
+ def verify_not_defined_in_schemas_using_this_behavior(name)
160
+ @included_in_schemas.each do |schema|
161
+ if schema.columns[name]
162
+ raise TypeError.new("Property '#{name}' is already defined in #{schema.name}.")
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -11,11 +11,16 @@ module Property
11
11
  def initialize(name, default, type, options={})
12
12
  name = name.to_s
13
13
  extract_property_options(options)
14
+ if type.kind_of?(Class)
15
+ @klass = type
16
+ end
14
17
  super(name, default, type, options)
15
18
  end
16
19
 
17
20
  def validate(value, errors)
18
- # Do nothing for the moment
21
+ if @klass && !value.kind_of?(@klass)
22
+ errors.add(name, "cannot cast #{value.class} to #{@klass}")
23
+ end
19
24
  end
20
25
 
21
26
  def should_create_accessors?
@@ -36,6 +41,21 @@ module Property
36
41
  end
37
42
  end
38
43
 
44
+ def klass
45
+ @klass || super
46
+ end
47
+
48
+ def type_cast(value)
49
+ if type == :string
50
+ value = value.to_s
51
+ value.blank? ? nil : value
52
+ elsif @klass
53
+ value
54
+ else
55
+ super
56
+ end
57
+ end
58
+
39
59
  private
40
60
  def extract_property_options(options)
41
61
  @indexed = options.delete(:indexed)
@@ -10,90 +10,34 @@ module Property
10
10
  include InstanceMethods
11
11
 
12
12
  class << self
13
- attr_accessor :own_property_columns
14
- attr_accessor :property_definition_proxy
15
- end
16
-
17
- validate :properties_validation, :if => :properties
18
- end
19
- end
20
-
21
- class DefinitionProxy
22
- def initialize(klass)
23
- @klass = klass
24
- end
25
-
26
- def column(name, default, type, options)
27
- if columns[name.to_s]
28
- raise TypeError.new("Property '#{name}' is already defined.")
29
- else
30
- add_column(Property::Column.new(name, default, type, options))
31
- end
32
- end
13
+ attr_accessor :schema
33
14
 
34
- def add_column(column)
35
- own_columns[column.name] = column
36
- @klass.define_property_methods(column) if column.should_create_accessors?
37
- end
38
-
39
- # If someday we find the need to insert other native classes directly in the DB, we
40
- # could use this:
41
- # p.serialize MyClass, xxx, xxx
42
- # def serialize(klass, name, options={})
43
- # if @klass.super_property_columns[name.to_s]
44
- # raise TypeError.new("Property '#{name}' is already defined in a superclass.")
45
- # elsif !@klass.validate_property_class(type)
46
- # raise TypeError.new("Custom type '#{type}' cannot be serialized.")
47
- # else
48
- # # Find a way to insert the type (maybe with 'serialize'...)
49
- # # (@klass.own_property_columns ||= {})[name] = Property::Column.new(name, type, options)
50
- # end
51
- # end
52
-
53
- # def string(*args)
54
- # options = args.extract_options!
55
- # column_names = args
56
- # default = options.delete(:default)
57
- # column_names.each { |name| column(name, default, 'string', options) }
58
- # end
59
- %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
60
- class_eval <<-EOV
61
- def #{column_type}(*args)
62
- options = args.extract_options!
63
- column_names = args
64
- default = options.delete(:default)
65
- column_names.each { |name| column(name, default, '#{column_type}', options) }
15
+ def schema
16
+ @schema ||= make_schema
66
17
  end
67
- EOV
68
- end
69
18
 
70
- private
71
- def own_columns
72
- @klass.own_property_columns ||= {}
73
- end
74
-
75
- def columns
76
- @klass.property_columns
19
+ private
20
+ def make_schema
21
+ schema = Property::Schema.new(self.to_s, self)
22
+ if superclass.respond_to?(:schema)
23
+ schema.behave_like superclass
24
+ end
25
+ schema
26
+ end
77
27
  end
78
28
 
79
- end
80
-
81
- class InstanceDefinitionProxy < DefinitionProxy
82
- def initialize(instance)
83
- @properties = instance.prop
84
- end
85
-
86
- def add_column(column)
87
- columns[column.name] = column
88
- end
89
-
90
- def columns
91
- @properties.columns
29
+ validate :properties_validation, :if => :properties
92
30
  end
93
31
  end
94
32
 
95
33
  module ClassMethods
96
34
 
35
+ # Include a new set of property definitions (Behavior) into the current class schema.
36
+ # You can also provide a class to simulate multiple inheritance.
37
+ def behave_like(behavior)
38
+ schema.behave_like behavior
39
+ end
40
+
97
41
  # Use this class method to declare properties that will be used in your models.
98
42
  # Example:
99
43
  # property.string 'phone', :default => ''
@@ -103,131 +47,44 @@ module Property
103
47
  # p.string 'phone', 'name', :default => ''
104
48
  # end
105
49
  def property
106
- proxy = self.property_definition_proxy ||= DefinitionProxy.new(self)
107
- if block_given?
108
- yield proxy
109
- end
110
- proxy
111
- end
112
-
113
- # @internal.
114
- # If you need the list of columns (including instance columns), you should use
115
- # properties.columns
116
- #
117
- # Return the list of all properties defined for the current class, including the properties
118
- # defined in the parent class.
119
- def property_columns
120
- super_property_columns.merge(self.own_property_columns || {})
121
- end
122
-
123
- def property_column_names
124
- property_columns.keys
125
- end
126
-
127
- def super_property_columns
128
- if superclass.respond_to?(:property_columns)
129
- superclass.property_columns
130
- else
131
- {}
132
- end
133
- end
134
-
135
- def define_property_methods(column)
136
- name = column.name
137
- unless instance_method_already_implemented?(name)
138
- if create_time_zone_conversion_attribute?(name, column)
139
- define_read_property_method_for_time_zone_conversion(name)
140
- else
141
- define_read_property_method(name.to_sym, name, column)
142
- end
143
- end
50
+ setter = schema.behavior
144
51
 
145
- unless instance_method_already_implemented?("#{name}=")
146
- if create_time_zone_conversion_attribute?(name, column)
147
- define_write_property_method_for_time_zone_conversion(name)
148
- else
149
- define_write_property_method(name.to_sym)
150
- end
52
+ if block_given?
53
+ yield setter
151
54
  end
152
55
 
153
- unless instance_method_already_implemented?("#{name}?")
154
- define_question_property_method(name)
155
- end
56
+ setter
156
57
  end
157
-
158
- private
159
- # Define a property reader method. Cope with nil column.
160
- def define_read_property_method(symbol, attr_name, column)
161
- # Unlike rails, we do not cast on read
162
- evaluate_attribute_property_method attr_name, "def #{symbol}; prop['#{attr_name}']; end"
163
- end
164
-
165
- # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
166
- # This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone.
167
- def define_read_property_method_for_time_zone_conversion(attr_name)
168
- method_body = <<-EOV
169
- def #{attr_name}(reload = false)
170
- cached = @attributes_cache['#{attr_name}']
171
- return cached if cached && !reload
172
- time = properties['#{attr_name}']
173
- @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
174
- end
175
- EOV
176
- evaluate_attribute_property_method attr_name, method_body
177
- end
178
-
179
- # Defines a predicate method <tt>attr_name?</tt>.
180
- def define_question_property_method(attr_name)
181
- evaluate_attribute_property_method attr_name, "def #{attr_name}?; prop['#{attr_name}']; end", "#{attr_name}?"
182
- end
183
-
184
- def define_write_property_method(attr_name)
185
- evaluate_attribute_property_method attr_name, "def #{attr_name}=(new_value);prop['#{attr_name}'] = new_value; end", "#{attr_name}="
186
- end
187
-
188
- # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
189
- # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
190
- def define_write_property_method_for_time_zone_conversion(attr_name)
191
- method_body = <<-EOV
192
- def #{attr_name}=(time)
193
- unless time.acts_like?(:time)
194
- time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
195
- end
196
- time = time.in_time_zone rescue nil if time
197
- prop['#{attr_name}'] = time
198
- end
199
- EOV
200
- evaluate_attribute_property_method attr_name, method_body, "#{attr_name}="
201
- end
202
-
203
- # Evaluate the definition for an attribute related method
204
- def evaluate_attribute_property_method(attr_name, method_definition, method_name=attr_name)
205
- class_eval(method_definition, __FILE__, __LINE__)
206
- end
207
58
  end # ClassMethods
208
59
 
209
60
  module InstanceMethods
61
+ # Instance's schema (can be different from the instance's class schema if behaviors have been
62
+ # added to the instance.
63
+ def schema
64
+ @own_schema || self.class.schema
65
+ end
210
66
 
211
- # Use this method to declare properties *for the current* instance.
212
- # Example:
213
- # @obj.property.string 'phone', :default => ''
214
- #
215
- # You can also use a block:
216
- # @obj.property do |p|
217
- # p.string 'phone', 'name', :default => ''
218
- # end
219
- def property
220
- proxy = @instance_definition_proxy ||= InstanceDefinitionProxy.new(self)
221
- if block_given?
222
- yield proxy
223
- end
224
- proxy
67
+ # Include a new set of property definitions (Behavior) into the current instance's schema.
68
+ # You can also provide a class to simulate multiple inheritance.
69
+ def behave_like(behavior)
70
+ own_schema.behave_like behavior
225
71
  end
226
72
 
227
73
  protected
228
74
  def properties_validation
229
75
  properties.validate
230
76
  end
77
+
78
+ def own_schema
79
+ @own_schema ||= make_own_schema
80
+ end
81
+ private
82
+ def make_own_schema
83
+ this = class << self; self; end
84
+ schema = Property::Schema.new(nil, this)
85
+ schema.behave_like self.class
86
+ schema
87
+ end
231
88
  end # InsanceMethods
232
89
  end # Declaration
233
90
  end # Property
@@ -20,7 +20,7 @@ module Property
20
20
  delete(key)
21
21
  end
22
22
  else
23
- super(key, column.type_cast(value.to_s))
23
+ super(key, column.type_cast(value))
24
24
  end
25
25
  else
26
26
  super
@@ -56,24 +56,14 @@ module Property
56
56
  end
57
57
 
58
58
  keys_to_validate.each do |key|
59
- value = self[key]
60
- column = columns[key]
61
- if value.blank?
62
- if column.has_default?
63
- self[key] = column.default_for(@owner)
64
- else
65
- delete(key)
66
- end
67
- else
68
- columns[key].validate(self[key], errors)
69
- end
59
+ columns[key].validate(self[key], errors)
70
60
  end
71
61
 
72
62
  bad_keys.empty?
73
63
  end
74
64
 
75
65
  def columns
76
- @columns ||= @owner.class.property_columns
66
+ @columns ||= @owner.schema.columns
77
67
  end
78
68
  end
79
69
  end
@@ -0,0 +1,79 @@
1
+
2
+ module Property
3
+ # This class holds all the properties of a given class or instance. It is used
4
+ # to validate content and type_cast during write operations.
5
+ #
6
+ # The properties are not directly defined in the schema. They are stored in a
7
+ # Behavior instance which checks that the database is in sync with the properties
8
+ # defined.
9
+ class Schema
10
+ attr_reader :behaviors, :behavior, :binding
11
+
12
+ # Create a new Schema. If a class_name is provided, the schema automatically
13
+ # creates a default Behavior to store definitions.
14
+ def initialize(class_name, binding)
15
+ @binding = binding
16
+ @behaviors = []
17
+ if class_name
18
+ @behavior = Behavior.new(class_name)
19
+ include_behavior @behavior
20
+ @behaviors << @behavior
21
+ end
22
+ end
23
+
24
+ # Return an identifier for the schema to help locate property redefinition errors.
25
+ def name
26
+ @behavior ? @behavior.name : @binding.to_s
27
+ end
28
+
29
+ # If the parameter is a class, the schema will inherit the property definitions
30
+ # from the class. If the parameter is a Behavior, the properties from that
31
+ # behavior will be included. Any new columns added to a behavior or any new
32
+ # behaviors included in a class will be dynamically added to the sub-classes (just like
33
+ # Ruby class inheritance, module inclusion works).
34
+ # If you ...
35
+ def behave_like(thing)
36
+ if thing.kind_of?(Class)
37
+ if thing.respond_to?(:schema) && thing.schema.kind_of?(Schema)
38
+ thing.schema.behaviors.flatten.each do |behavior|
39
+ include_behavior behavior
40
+ end
41
+ self.behaviors << thing.schema.behaviors
42
+ else
43
+ raise TypeError.new("expected Behavior or class with schema, found #{thing}")
44
+ end
45
+ elsif thing.kind_of?(Behavior)
46
+ include_behavior thing
47
+ self.behaviors << thing
48
+ else
49
+ raise TypeError.new("expected Behavior or class with schema, found #{thing.class}")
50
+ end
51
+ end
52
+
53
+ # Return the list of column names.
54
+ def column_names
55
+ columns.keys
56
+ end
57
+
58
+ # Return column definitions from all included behaviors.
59
+ def columns
60
+ columns = {}
61
+ @behaviors.flatten.uniq.each do |b|
62
+ columns.merge!(b.columns)
63
+ end
64
+ columns
65
+ end
66
+
67
+ private
68
+ def include_behavior(behavior)
69
+ return if behaviors.include?(behavior)
70
+ columns = self.columns
71
+ common_keys = behavior.column_names & columns.keys
72
+ if !common_keys.empty?
73
+ raise TypeError.new("Cannot include behavior #{behavior.name}. Duplicate definitions: #{common_keys.join(', ')}")
74
+ end
75
+ behavior.included(self)
76
+ @binding.send(:include, behavior.accessor_module)
77
+ end
78
+ end
79
+ end
@@ -5,11 +5,11 @@ module Property
5
5
  # provide 'self.create_json' and 'to_json' methods for the classes you want
6
6
  # to serialize.
7
7
  module JSON
8
- module ClassMethods
8
+ module Validator
9
9
  NATIVE_TYPES = [Hash, Array, Integer, Float, String, TrueClass, FalseClass, NilClass]
10
10
 
11
- # Returns true if the given class can be serialized with JSON
12
- def validate_property_class(klass)
11
+ # Should raise an exception if the type is not serializable.
12
+ def self.validate(klass)
13
13
  if NATIVE_TYPES.include?(klass) ||
14
14
  (klass.respond_to?('json_create') && klass.instance_methods.include?('to_json'))
15
15
  true
@@ -20,10 +20,10 @@ module Property
20
20
  end
21
21
 
22
22
  def self.included(base)
23
- base.extend ClassMethods
23
+ Property.validators << Validator
24
24
  end
25
25
 
26
- # Encode properties with Marhsal
26
+ # Encode properties with JSON
27
27
  def encode_properties(properties)
28
28
  properties.to_json
29
29
  end
@@ -7,16 +7,6 @@ module Property
7
7
  # * no corruption risk if the version of Marshal changes
8
8
  # * it can be accessed by other languages then ruby
9
9
  module Marshal
10
- module ClassMethods
11
- # Returns true if the given class can be serialized with Marshal
12
- def validate_property_class(klass)
13
- true
14
- end
15
- end
16
-
17
- def self.included(base)
18
- base.extend ClassMethods
19
- end
20
10
 
21
11
  # Encode properties with Marhsal
22
12
  def encode_properties(properties)
@@ -3,17 +3,6 @@ module Property
3
3
  # Use YAML to encode properties. This method is the slowest of all
4
4
  # and you should use JSON if you haven't got good reasons not to.
5
5
  module YAML
6
- module ClassMethods
7
- # Returns true if the given class can be serialized with YAML
8
- def validate_property_class(klass)
9
- true
10
- end
11
- end
12
-
13
- def self.included(base)
14
- base.extend ClassMethods
15
- end
16
-
17
6
  # Encode properties with YAML
18
7
  def encode_properties(properties)
19
8
  ::YAML.dump(properties)
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{property}
8
- s.version = "0.7.0"
8
+ s.version = "0.8.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Renaud Kern", "Gaspard Bucher"]
12
- s.date = %q{2010-02-11}
12
+ s.date = %q{2010-02-12}
13
13
  s.description = %q{Wrap model properties into a single database column and declare properties from within the model.}
14
14
  s.email = %q{gaspard@teti.ch}
15
15
  s.extra_rdoc_files = [
@@ -24,11 +24,13 @@ Gem::Specification.new do |s|
24
24
  "generators/property/property_generator.rb",
25
25
  "lib/property.rb",
26
26
  "lib/property/attribute.rb",
27
+ "lib/property/behavior.rb",
27
28
  "lib/property/column.rb",
28
29
  "lib/property/core_ext/time.rb",
29
30
  "lib/property/declaration.rb",
30
31
  "lib/property/dirty.rb",
31
32
  "lib/property/properties.rb",
33
+ "lib/property/schema.rb",
32
34
  "lib/property/serialization/json.rb",
33
35
  "lib/property/serialization/marshal.rb",
34
36
  "lib/property/serialization/yaml.rb",
@@ -37,6 +39,7 @@ Gem::Specification.new do |s|
37
39
  "test/shoulda_macros/serialization.rb",
38
40
  "test/test_helper.rb",
39
41
  "test/unit/property/attribute_test.rb",
42
+ "test/unit/property/behavior_test.rb",
40
43
  "test/unit/property/declaration_test.rb",
41
44
  "test/unit/property/dirty_test.rb",
42
45
  "test/unit/property/validation_test.rb",
@@ -55,6 +58,7 @@ Gem::Specification.new do |s|
55
58
  "test/shoulda_macros/serialization.rb",
56
59
  "test/test_helper.rb",
57
60
  "test/unit/property/attribute_test.rb",
61
+ "test/unit/property/behavior_test.rb",
58
62
  "test/unit/property/declaration_test.rb",
59
63
  "test/unit/property/dirty_test.rb",
60
64
  "test/unit/property/validation_test.rb",
@@ -24,6 +24,25 @@ class Version < ActiveRecord::Base
24
24
  end
25
25
  end
26
26
 
27
+ # To test custom class serialization
28
+ class Dog
29
+ attr_accessor :name, :toy
30
+ def self.json_create(data)
31
+ Dog.new(data['name'], data['toy'])
32
+ end
33
+ def initialize(name, toy)
34
+ @name, @toy = name, toy
35
+ end
36
+ def to_json(*args)
37
+ { 'json_class' => self.class.to_s,
38
+ 'name' => @name, 'toy' => @toy
39
+ }.to_json(*args)
40
+ end
41
+ def ==(other)
42
+ other.kind_of?(Dog) && @name == other.name && @toy == other.toy
43
+ end
44
+ end
45
+
27
46
  begin
28
47
  class PropertyMigration < ActiveRecord::Migration
29
48
  def self.down
@@ -2,17 +2,6 @@ class Test::Unit::TestCase
2
2
 
3
3
  def self.should_encode_and_decode_properties
4
4
  klass = self.name.gsub(/Test$/,'').constantize
5
- context klass do
6
- should 'respond to validate_property_class' do
7
- assert klass.respond_to? :validate_property_class
8
- end
9
-
10
- [Property::Properties, String, Integer, Float].each do |a_class|
11
- should "accept to serialize #{a_class}" do
12
- assert klass.validate_property_class(a_class)
13
- end
14
- end
15
- end
16
5
 
17
6
  context "Instance of #{klass}" do
18
7
  setup do
@@ -29,7 +18,13 @@ class Test::Unit::TestCase
29
18
 
30
19
  context 'with Properties' do
31
20
  setup do
32
- @properties = Property::Properties['foo' => 'bar']
21
+ @properties = Property::Properties[
22
+ 'string' => 'bar',
23
+ 'serialized' => Dog.new('Pavlov', 'Freud'),
24
+ 'datetime' => Time.utc(2010, 02, 12, 21, 31, 25),
25
+ 'float' => 4.3432,
26
+ 'integer' => 4
27
+ ]
33
28
  end
34
29
 
35
30
  should 'encode Properties in string' do
@@ -6,6 +6,7 @@ require 'shoulda'
6
6
  require 'active_record'
7
7
  require 'property'
8
8
  require 'shoulda_macros/serialization'
9
+ require 'fixtures'
9
10
 
10
11
  class Test::Unit::TestCase
11
12
 
@@ -1,6 +1,4 @@
1
1
  require 'test_helper'
2
- require 'fixtures'
3
- require 'benchmark'
4
2
 
5
3
  class AttributeTest < Test::Unit::TestCase
6
4
 
@@ -215,6 +213,32 @@ class AttributeTest < Test::Unit::TestCase
215
213
  assert_equal @now, subject.prop['mydatetime']
216
214
  end
217
215
  end
216
+
217
+
218
+ context 'a saved serialized class' do
219
+ setup do
220
+ @dog = Dog.new('Pavlov', 'Freud')
221
+ end
222
+
223
+ subject do
224
+ klass = Class.new(ActiveRecord::Base) do
225
+ include Property
226
+ set_table_name :dummies
227
+ property.serialize 'myserialized', Dog
228
+ end
229
+
230
+ obj = klass.create('myserialized' => @dog)
231
+ klass.find(obj)
232
+ end
233
+
234
+ should 'find class back' do
235
+ assert_kind_of Dog, subject.prop['myserialized']
236
+ end
237
+
238
+ should 'find same value' do
239
+ assert_equal @dog, subject.prop['myserialized']
240
+ end
241
+ end
218
242
  end
219
243
 
220
244
  context 'Setting attributes' do
@@ -0,0 +1,119 @@
1
+ require 'test_helper'
2
+ require 'fixtures'
3
+
4
+ class BehaviorTest < Test::Unit::TestCase
5
+
6
+ context 'A Behavior' do
7
+ subject { Property::Behavior.new('Foobar') }
8
+
9
+ should 'allow string columns' do
10
+ subject.property.string('weapon')
11
+ column = subject.columns['weapon']
12
+ assert_equal 'weapon', column.name
13
+ assert_equal String, column.klass
14
+ assert_equal :string, column.type
15
+ end
16
+
17
+ should 'treat symbol keys as strings' do
18
+ subject.property.string(:weapon)
19
+ column = subject.columns['weapon']
20
+ assert_equal 'weapon', column.name
21
+ assert_equal String, column.klass
22
+ assert_equal :string, column.type
23
+ end
24
+
25
+ should 'allow integer columns' do
26
+ subject.property.integer('indestructible')
27
+ column = subject.columns['indestructible']
28
+ assert_equal 'indestructible', column.name
29
+ assert_equal Fixnum, column.klass
30
+ assert_equal :integer, column.type
31
+ end
32
+
33
+ should 'allow float columns' do
34
+ subject.property.float('boat')
35
+ column = subject.columns['boat']
36
+ assert_equal 'boat', column.name
37
+ assert_equal Float, column.klass
38
+ assert_equal :float, column.type
39
+ end
40
+
41
+ should 'allow datetime columns' do
42
+ subject.property.datetime('time_weapon')
43
+ column = subject.columns['time_weapon']
44
+ assert_equal 'time_weapon', column.name
45
+ assert_equal Time, column.klass
46
+ assert_equal :datetime, column.type
47
+ end
48
+
49
+ should 'allow default value option' do
50
+ subject.property.integer('force', :default => 10)
51
+ column = subject.columns['force']
52
+ assert_equal 10, column.default
53
+ end
54
+
55
+ should 'allow indexed option' do
56
+ subject.property.string('rolodex', :indexed => true)
57
+ column = subject.columns['rolodex']
58
+ assert column.indexed?
59
+ end
60
+ end # A Behavior
61
+
62
+ context 'Adding a behavior' do
63
+ setup do
64
+ @poet = Property::Behavior.new('Poet') do |p|
65
+ p.string 'poem'
66
+ end
67
+ end
68
+
69
+ context 'to a class' do
70
+ setup do
71
+ @parent = Class.new(ActiveRecord::Base) do
72
+ set_table_name :dummies
73
+ include Property
74
+ property.string 'name'
75
+ end
76
+
77
+ @klass = Class.new(@parent)
78
+ end
79
+
80
+ should 'propagate definitions to child' do
81
+ @parent.behave_like @poet
82
+ assert_equal %w{name poem}, @klass.schema.column_names.sort
83
+ end
84
+
85
+ should 'raise an exception if class contains same definitions' do
86
+ @parent.property.string 'poem'
87
+ assert_raise(TypeError) { @parent.behave_like @poet }
88
+ end
89
+
90
+ should 'not raise an exception on double inclusion' do
91
+ @parent.behave_like @poet
92
+ assert_nothing_raised { @parent.behave_like @poet }
93
+ end
94
+
95
+ should 'add accessor methods to child' do
96
+ subject = @klass.new
97
+ assert_raises(NoMethodError) { subject.poem = 'Poe'}
98
+ @parent.behave_like @poet
99
+
100
+ assert_nothing_raised { subject.poem = 'Poe'}
101
+ end
102
+ end
103
+
104
+ context 'to a parent class' do
105
+ end
106
+
107
+ context 'to an instance' do
108
+ subject { Developer.new }
109
+
110
+ setup do
111
+ subject.behave_like @poet
112
+ end
113
+
114
+ should 'merge property definitions' do
115
+ assert_equal %w{age first_name language last_name poem}, subject.schema.column_names.sort
116
+ end
117
+ end
118
+ end
119
+ end
@@ -9,29 +9,31 @@ class DeclarationTest < Test::Unit::TestCase
9
9
  @klass = Developer
10
10
  end
11
11
 
12
- should 'inherit property columsn from parent class' do
13
- assert_equal %w{age first_name language last_name}, @klass.property_column_names.sort
12
+ should 'inherit property columns from parent class' do
13
+ assert_equal %w{age first_name language last_name}, @klass.schema.column_names.sort
14
14
  end
15
15
 
16
16
  should 'not back-propagate definitions to parent' do
17
- assert !@klass.superclass.property_columns.include?('language')
17
+ assert !@klass.superclass.schema.columns.include?('language')
18
18
  end
19
19
 
20
- should 'inherit current definitions from parent' do
20
+ should 'inherit new definitions in parent' do
21
21
  class ParentClass < ActiveRecord::Base
22
22
  include Property
23
23
  property.string 'name'
24
24
  end
25
+
25
26
  @klass = Class.new(ParentClass) do
26
27
  property.integer 'age'
27
28
  end
28
- assert_equal %w{age name}, @klass.property_column_names.sort
29
+
30
+ assert_equal %w{age name}, @klass.schema.column_names.sort
29
31
 
30
32
  ParentClass.class_eval do
31
33
  property.string 'first_name'
32
34
  end
33
35
 
34
- assert_equal %w{age first_name name}, @klass.property_column_names.sort
36
+ assert_equal %w{age first_name name}, @klass.schema.column_names.sort
35
37
  end
36
38
 
37
39
  should 'not be allowed to overwrite a property from the parent class' do
@@ -62,7 +64,7 @@ class DeclarationTest < Test::Unit::TestCase
62
64
 
63
65
  should 'create Property::Column definitions' do
64
66
  subject.property.string('weapon')
65
- assert_kind_of Property::Column, subject.property_columns['weapon']
67
+ assert_kind_of Property::Column, subject.schema.columns['weapon']
66
68
  end
67
69
 
68
70
  should 'create ruby accessors' do
@@ -84,7 +86,7 @@ class DeclarationTest < Test::Unit::TestCase
84
86
 
85
87
  should 'allow string columns' do
86
88
  subject.property.string('weapon')
87
- column = subject.property_columns['weapon']
89
+ column = subject.schema.columns['weapon']
88
90
  assert_equal 'weapon', column.name
89
91
  assert_equal String, column.klass
90
92
  assert_equal :string, column.type
@@ -92,7 +94,7 @@ class DeclarationTest < Test::Unit::TestCase
92
94
 
93
95
  should 'treat symbol keys as strings' do
94
96
  subject.property.string(:weapon)
95
- column = subject.property_columns['weapon']
97
+ column = subject.schema.columns['weapon']
96
98
  assert_equal 'weapon', column.name
97
99
  assert_equal String, column.klass
98
100
  assert_equal :string, column.type
@@ -100,7 +102,7 @@ class DeclarationTest < Test::Unit::TestCase
100
102
 
101
103
  should 'allow integer columns' do
102
104
  subject.property.integer('indestructible')
103
- column = subject.property_columns['indestructible']
105
+ column = subject.schema.columns['indestructible']
104
106
  assert_equal 'indestructible', column.name
105
107
  assert_equal Fixnum, column.klass
106
108
  assert_equal :integer, column.type
@@ -108,7 +110,7 @@ class DeclarationTest < Test::Unit::TestCase
108
110
 
109
111
  should 'allow float columns' do
110
112
  subject.property.float('boat')
111
- column = subject.property_columns['boat']
113
+ column = subject.schema.columns['boat']
112
114
  assert_equal 'boat', column.name
113
115
  assert_equal Float, column.klass
114
116
  assert_equal :float, column.type
@@ -116,41 +118,67 @@ class DeclarationTest < Test::Unit::TestCase
116
118
 
117
119
  should 'allow datetime columns' do
118
120
  subject.property.datetime('time_weapon')
119
- column = subject.property_columns['time_weapon']
121
+ column = subject.schema.columns['time_weapon']
120
122
  assert_equal 'time_weapon', column.name
121
123
  assert_equal Time, column.klass
122
124
  assert_equal :datetime, column.type
123
125
  end
126
+
127
+ should 'allow serialized columns' do
128
+ Dog = Struct.new(:name, :toy) do
129
+ def self.json_create(data)
130
+ Dog.new(data['name'], data['toy'])
131
+ end
132
+ def to_json(*args)
133
+ { 'json_class' => self.class.to_s,
134
+ 'name' => @name, 'toy' => @toy
135
+ }.to_json(*args)
136
+ end
137
+ end
138
+
139
+ subject.property.serialize('pet', Dog)
140
+ column = subject.schema.columns['pet']
141
+ assert_equal 'pet', column.name
142
+ assert_equal Dog, column.klass
143
+ assert_equal nil, column.type
144
+ end
124
145
 
125
146
  should 'allow default value option' do
126
147
  subject.property.integer('force', :default => 10)
127
- column = subject.property_columns['force']
148
+ column = subject.schema.columns['force']
128
149
  assert_equal 10, column.default
129
150
  end
130
151
 
131
152
  should 'allow indexed option' do
132
153
  subject.property.string('rolodex', :indexed => true)
133
- column = subject.property_columns['rolodex']
154
+ column = subject.schema.columns['rolodex']
134
155
  assert column.indexed?
135
156
  end
136
157
 
137
- context 'in an instance singleton' do
158
+ context 'through a Behavior on an instance' do
138
159
  setup do
139
160
  @instance = subject.new
140
- @instance.property do |p|
141
- p.string 'instance_only'
161
+ @poet = Property::Behavior.new('Poet')
162
+ @poet.property do |p|
163
+ p.string 'poem'
142
164
  end
165
+
166
+ @instance.behave_like @poet
143
167
  end
144
168
 
145
169
  should 'behave like any other property column' do
146
- @instance.attributes = {'instance_only' => 'hello'}
170
+ @instance.attributes = {'poem' => 'hello'}
171
+ @instance.poem = 'shazam'
147
172
  assert @instance.save
148
173
  @instance = subject.find(@instance.id)
149
- assert_equal Hash['instance_only' => 'hello'], @instance.prop
174
+ assert_equal Hash['poem' => 'shazam'], @instance.prop
150
175
  end
151
176
 
152
177
  should 'not affect instance class' do
153
- assert !subject.property_column_names.include?('instance_only')
178
+ assert !subject.schema.column_names.include?('poem')
179
+ assert_raise(NoMethodError) do
180
+ subject.new.poem = 'not a poet'
181
+ end
154
182
  end
155
183
  end
156
184
  end
@@ -162,12 +190,24 @@ class DeclarationTest < Test::Unit::TestCase
162
190
  end
163
191
 
164
192
  should 'return empty Hash if no property columsn are declared' do
165
- assert_equal Hash[], Dummy.property_columns
193
+ assert_equal Hash[], Dummy.schema.columns
166
194
  end
167
195
 
168
196
  should 'return list of property columns from class' do
169
- assert_kind_of Hash, Employee.property_columns
170
- assert_kind_of Property::Column, Employee.property_columns['first_name']
197
+ assert_kind_of Hash, Employee.schema.columns
198
+ assert_kind_of Property::Column, Employee.schema.columns['first_name']
199
+ end
200
+ end
201
+
202
+ context 'On a class with a schema' do
203
+ subject { Developer }
204
+
205
+ should 'raise an exception if we ask to behave like a class without schema' do
206
+ assert_raise(TypeError) { subject.behave_like String }
207
+ end
208
+
209
+ should 'raise an exception if we ask to behave like an object' do
210
+ assert_raise(TypeError) { subject.behave_like 'me' }
171
211
  end
172
212
  end
173
213
  end
@@ -9,6 +9,7 @@ class ValidationTest < Test::Unit::TestCase
9
9
  include Property
10
10
  property.float 'boat'
11
11
  property.string 'bird_name'
12
+ property.serialize 'dog', Dog
12
13
  end
13
14
 
14
15
  subject { Pirate.create }
@@ -39,6 +40,12 @@ class ValidationTest < Test::Unit::TestCase
39
40
  assert_kind_of String, subject.prop['bird_name']
40
41
  assert_equal '1337', subject.prop['bird_name']
41
42
  end
43
+
44
+ should 'show an error for serialized types' do
45
+ subject.update_attributes('dog' => 'Medor')
46
+ assert !subject.valid?
47
+ assert_equal 'cannot cast String to Dog', subject.errors['dog']
48
+ end
42
49
  end
43
50
 
44
51
  context 'to a blank value' do
@@ -9,4 +9,29 @@ class MyJSONTest < Test::Unit::TestCase
9
9
 
10
10
  should_encode_and_decode_properties
11
11
 
12
+ context 'JSON validator' do
13
+ subject { Property::Serialization::JSON::Validator }
14
+
15
+ should 'respond to validate' do
16
+ assert subject.respond_to? :validate
17
+ end
18
+
19
+ [Property::Properties, String, Integer, Float].each do |a_class|
20
+ should "accept to serialize #{a_class}" do
21
+ assert subject.validate(a_class)
22
+ end
23
+ end
24
+ end
25
+
26
+ context 'on a class with properties as custom type' do
27
+ subject do
28
+ Class.new(ActiveRecord::Base) do
29
+ include Property
30
+ end
31
+ end
32
+
33
+ should 'raise an exception if we try to encode an invalid class' do
34
+ assert_raise(TypeError) { subject.property.serialize 'not_json', Regexp }
35
+ end
36
+ end
12
37
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: property
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Renaud Kern
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2010-02-11 00:00:00 +01:00
13
+ date: 2010-02-12 00:00:00 +01:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -50,11 +50,13 @@ files:
50
50
  - generators/property/property_generator.rb
51
51
  - lib/property.rb
52
52
  - lib/property/attribute.rb
53
+ - lib/property/behavior.rb
53
54
  - lib/property/column.rb
54
55
  - lib/property/core_ext/time.rb
55
56
  - lib/property/declaration.rb
56
57
  - lib/property/dirty.rb
57
58
  - lib/property/properties.rb
59
+ - lib/property/schema.rb
58
60
  - lib/property/serialization/json.rb
59
61
  - lib/property/serialization/marshal.rb
60
62
  - lib/property/serialization/yaml.rb
@@ -63,6 +65,7 @@ files:
63
65
  - test/shoulda_macros/serialization.rb
64
66
  - test/test_helper.rb
65
67
  - test/unit/property/attribute_test.rb
68
+ - test/unit/property/behavior_test.rb
66
69
  - test/unit/property/declaration_test.rb
67
70
  - test/unit/property/dirty_test.rb
68
71
  - test/unit/property/validation_test.rb
@@ -102,6 +105,7 @@ test_files:
102
105
  - test/shoulda_macros/serialization.rb
103
106
  - test/test_helper.rb
104
107
  - test/unit/property/attribute_test.rb
108
+ - test/unit/property/behavior_test.rb
105
109
  - test/unit/property/declaration_test.rb
106
110
  - test/unit/property/dirty_test.rb
107
111
  - test/unit/property/validation_test.rb