property 0.7.0 → 0.8.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.
@@ -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