Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Rewrite of the core engine without accessor modules. This means that …

…accessor methods are not added to the object on 'include_role' (using method_missing instead).
  • Loading branch information...
commit 0d7ac430570c5c4160e56f4653d0bae57259bd5d 1 parent 38834f6
@gaspard gaspard authored
View
10 History.txt
@@ -1,4 +1,12 @@
-== 1.3.0 2010-11-9
+== 2.0.0
+
+* Major enhancements
+ * Rewrite of the core engine to remove all the metaclass and anonymous module codes (generated memory leaks).
+ * Removed "actions" to create methods in a Role (you have to define them in the host class instead).
+ * Not defining accessor methods in Roles (using method missing instead). Accessors in Schema are defined directly in the class.
+ * Not checking for redefined methods anymore.
+
+== 1.3.0 2010-11-9 (not released)
* Major enhancements
* Removed 'included_in' check (the same property can now be redefined)
View
2  lib/property.rb
@@ -3,7 +3,6 @@
require 'property/properties'
require 'property/column'
require 'property/role'
-require 'property/stored_role'
require 'property/schema'
require 'property/declaration'
require 'property/db'
@@ -11,6 +10,7 @@
require 'property/serialization/json'
require 'property/core_ext/time'
require 'property/base'
+require 'property/stored_role'
module Property
def self.included(base)
View
2  lib/property/attribute.rb
@@ -19,7 +19,7 @@ def self.included(base)
end
end
- # This is just a helper module that includes necessary code for property access, but without
+ # This is just a helper module that includes the necessary code for property access, but without
# the validation/save hooks.
module Base
def self.included(base)
View
82 lib/property/declaration.rb
@@ -1,7 +1,8 @@
module Property
- # Property::Declaration module is used to declare property definitions in a Class. The module
- # also manages property inheritence in sub-classes.
+ # This module is used to manage property definitions (the schema) in a Class. The module
+ # also manages property inheritence in sub-classes by linking the schema in the sub-class with
+ # the schema in the superclass.
module Declaration
def self.included(base)
base.class_eval do
@@ -10,6 +11,8 @@ def self.included(base)
end
end
+ # This is just a helper module that includes the necessary code for property definition, but without
+ # the validation/save hooks.
module Base
def self.included(base)
base.class_eval do
@@ -17,6 +20,7 @@ def self.included(base)
include InstanceMethods
class << self
+ # Every class has it's own schema.
attr_accessor :schema
def schema
@@ -24,12 +28,9 @@ def schema
end
private
+ # Build schema and manage inheritance.
def make_schema
- schema = Property::Schema.new(self.to_s, self)
- if superclass.respond_to?(:schema)
- schema.has_role superclass
- end
- schema
+ Property::Schema.new(self.to_s, :class => self)
end
end
end
@@ -40,11 +41,11 @@ module ClassMethods
# Include a new set of property definitions (Role) into the current class schema.
# You can also provide a class to simulate multiple inheritance.
- def has_role(role)
- schema.has_role role
+ def include_role(role)
+ schema.include_role role
end
- # Return true if the current object has all the roles of the given object, class or role.
+ # Return true if the current object has all the roles of the given schema or role.
def has_role?(role)
schema.has_role? role
end
@@ -63,7 +64,27 @@ def has_role?(role)
# end
# end
def property(&block)
- schema.role.property(&block)
+ schema.property(&block)
+ end
+
+ # Define property methods in a class. This is only triggered when properties are declared directly in the
+ # class and not through Role inclusion.
+ def define_property_methods(column)
+ attr_name = column.name
+
+ class_eval(%Q{
+ def #{attr_name} # def title
+ prop['#{attr_name}'] # prop['title']
+ end # end
+ #
+ def #{attr_name}? # def title?
+ prop['#{attr_name}'] # prop['title']
+ end # end
+ #
+ def #{attr_name}=(new_value) # def title=(new_value)
+ prop['#{attr_name}'] = new_value # prop['title'] = new_value
+ end # end
+ }, __FILE__, __LINE__)
end
end # ClassMethods
@@ -76,8 +97,8 @@ def schema
# Include a new set of property definitions (Role) into the current instance's schema.
# You can also provide a class to simulate multiple inheritance.
- def has_role(role)
- own_schema.has_role role
+ def include_role(role)
+ own_schema.include_role role
end
# Return the list of active roles. The active roles are all the Roles included
@@ -99,12 +120,39 @@ def properties_validation
def own_schema
@own_schema ||= make_own_schema
end
+
+ # When roles are dynamically added to a model, we use method_missing to mimic property
+ # accessors. Since this has a cost, it is better to use 'prop' based accessors in production
+ # code (this is mostly helpful for testing/debugging).
+ def method_missing(method, *args)
+ method = method.to_s
+ if args.empty?
+ if method[-1..-1] == '?'
+ # predicate
+ key = method[0..-2]
+ else
+ # reader
+ key = method
+ end
+
+ if schema.has_column?(key)
+ return prop[key]
+ end
+ elsif args.size == 1 && method[-1..-1] == '='
+ # writer
+ key = method[0..-2]
+ if schema.has_column?(key)
+ return prop[key] = args.first
+ end
+ end
+ # Not a property method
+ super
+ end
+
private
+ # Create a schema for the instance and inherit from the class
def make_own_schema
- this = class << self; self; end
- schema = Property::Schema.new(nil, this)
- schema.has_role self.class
- schema
+ Property::Schema.new(nil, :superschema => self.class.schema)
end
end # InsanceMethods
end # Declaration
View
147 lib/property/role.rb
@@ -1,18 +1,24 @@
-require 'property/role_module'
+require 'property/redefined_property_error'
+require 'property/redefined_method_error'
module Property
- # This class holds a set of property definitions. This is like a Module in ruby:
- # by 'including' this role in a class or in an instance, you augment the said
- # object with the role's property definitions.
+ # The Role holds information on a group of property columns. The "Role" is used
+ # in the same way as the ruby Module: as a mixin. The Schema class "includes" roles.
class Role
- attr_accessor :name
- include RoleModule
+ attr_accessor :name, :actions_module
- def self.new(name, &block)
+ # Create a new role. If a block is provided, this block can be used
+ # to define properties:
+ #
+ # Example:
+ # @role = Role.new('Poet') do |p|
+ # p.string :muse
+ # end
+ def self.new(name, klass = nil, &block)
if name.kind_of?(Hash)
- obj = super(name[:name] || name['name'])
+ obj = super(name[:name] || name['name'], klass)
else
- obj = super(name)
+ obj = super(name, klass)
end
if block_given?
@@ -22,9 +28,126 @@ def self.new(name, &block)
end
# Initialize a new role with the given name
- def initialize(name)
- self.name = name
- initialize_role_module
+ def initialize(name, opts = nil)
+ @name = name
+ @group_indices = []
end
+
+ # Return true if the role contains the given column (property).
+ def has_column?(name)
+ column_names.include?(name)
+ end
+
+ # Return the list of column names.
+ def column_names
+ columns.keys
+ end
+
+ # List all property columns defined for this role
+ def columns
+ defined_columns
+ end
+
+ # Use this method to declare properties into a Role or Schema.
+ #
+ # Example:
+ # @role.property.string 'phone', :default => ''
+ #
+ # You can also use the "property" method in the class to access the schema:
+ #
+ # Example:
+ # Page.property.string 'phone', :default => ''
+ #
+ # You can also use a block:
+ # Page.property do |p|
+ # p.string 'phone', 'name', :default => ''
+ # end
+ def property
+ if block_given?
+ yield self
+ end
+ self
+ end
+
+ %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
+ class_eval <<-EOV
+ def #{column_type}(*args)
+ options = args.extract_options!
+ column_names = args.flatten
+ default = options.delete(:default)
+ column_names.each { |name| add_column(Property::Column.new(name, default, '#{column_type}', options.merge(:role => self))) }
+ end
+ EOV
+ end
+
+ # This is used to serialize a non-native DB type. Use:
+ # p.serialize 'pet', Dog
+ def serialize(name, klass, options = {})
+ Property.validate_property_class(klass)
+ add_column(Property::Column.new(name, nil, klass, options.merge(:role => self)))
+ end
+
+ # This is used to create complex indices with the following syntax:
+ #
+ # p.index(:text) do |r| # r = record
+ # {
+ # "high" => "gender:#{r.gender} age:#{r.age} name:#{r.name}",
+ # "name_#{r.lang}" => r.name, # multi-lingual index
+ # }
+ # end
+ #
+ # The first argument is the type (used to locate the table where the data will be stored) and the block
+ # will be yielded with the record and should return a hash of key => value pairs.
+ def index(type, &block)
+ # type, key, proc
+ @group_indices << [type, nil, block]
+ end
+
+ # Returns true if the role is used by the given object. A role is
+ # considered to be used if any of it's defined columns is not blank in the object's
+ # properties.
+ def used_in(object)
+ object.properties.keys & defined_columns.keys != []
+ end
+
+ # Returns the list of column names in the current role that are used by the
+ # given object (value not blank).
+ def used_keys_in(object)
+ object.properties.keys & column_names
+ end
+
+ # Return a list of index definitions in the form [type, key, proc_or_nil]
+ def indices
+ columns.values.select do |c|
+ c.indexed?
+ end.map do |c|
+ [c.index, c.name, c.index_proc]
+ end + @group_indices
+ end
+
+ def inspect
+ # "#<#{self.class}:#{sprintf("0x%x", object_id)} #{@name.inspect} @klass = #{@klass.inspect} @defined_columns = #{@defined_columns.inspect}>"
+ "#<#{self.class}:#{sprintf("0x%x", object_id)} #{column_names.inspect}>"
+ end
+
+ protected
+ # List all property columns defined for this role
+ def defined_columns
+ @role_columns ||= {}
+ end
+
+ # @internal
+ def add_column(column)
+ name = column.name
+
+ if defined_columns[name]
+ raise RedefinedPropertyError.new("Property '#{name}' is already defined.")
+ else
+ defined_columns[column.name] = column
+ if @klass && column.should_create_accessors?
+ @klass.define_property_methods(column)
+ end
+ end
+ end
end
end
View
221 lib/property/role_module.rb
@@ -1,221 +0,0 @@
-require 'property/redefined_property_error'
-require 'property/redefined_method_error'
-
-module Property
- # This class holds a set of property definitions. This is like a Module in ruby:
- # by 'including' this role in a class or in an instance, you augment the said
- # object with the role's property definitions.
- module RoleModule
- attr_accessor :included, :accessor_module
-
- # We cannot use attr_accessor to define these because we are in a module
- # when the module is included in an ActiveRecord class.
- #%W{name included accessor_module}.each do |name|
- # class_eval %Q{
- # def #{name}
- # @#{name}
- # end
- #
- # def #{name}=(value)
- # @#{name} = value
- # end
- # }
- #end
-
- # Initialize module (should be called from within including class's initialize method).
- def initialize_role_module
- @group_indices = []
- @accessor_module = build_accessor_module
- end
-
- # List all property definitiosn for the current role
- def columns
- @columns ||= {}
- end
-
- # Return a list of index definitions in the form [type, key, proc_or_nil]
- def indices
- columns.values.select do |c|
- c.indexed?
- end.map do |c|
- [c.index, c.name, c.index_proc]
- end + @group_indices
- end
-
- # Return true if the Role contains the given column (property).
- def has_column?(name)
- column_names.include?(name)
- end
-
- # Return the list of column names.
- def column_names
- columns.keys
- end
-
- # Use this method to declare properties into a Role.
- # Example:
- # @role.property.string 'phone', :default => ''
- #
- # You can also use a block:
- # @role.property do |p|
- # p.string 'phone', 'name', :default => ''
- # end
- def property
- if block_given?
- yield accessor_module
- end
- accessor_module
- end
-
- # @internal
- def add_column(column)
- name = column.name
-
- if columns[name]
- raise RedefinedPropertyError.new("Property '#{name}' is already defined.")
- else
- define_property_methods(column) if column.should_create_accessors?
- columns[column.name] = column
- end
- end
-
- # @internal
- def add_index(type, proc)
- # type, key, proc
- @group_indices << [type, nil, proc]
- end
-
- # Returns true if the current role is used by the given object. A Role is
- # considered to be used if any of it's attributes is not blank in the object's
- # properties.
- def used_in(object)
- used_keys_in(object) != []
- end
-
- # Returns the list of column names in the current role that are used by the
- # given object (value not blank).
- def used_keys_in(object)
- object.properties.keys & column_names
- end
-
- private
- def build_accessor_module
- accessor_module = Module.new
- accessor_module.class_eval do
- class << self
- attr_accessor :role
-
- # def string(*args)
- # options = args.extract_options!
- # column_names = args
- # default = options.delete(:default)
- # column_names.each { |name| column(name, default, 'string', options) }
- # end
- %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
- class_eval <<-EOV
- def #{column_type}(*args)
- options = args.extract_options!
- column_names = args.flatten
- default = options.delete(:default)
- column_names.each { |name| role.add_column(Property::Column.new(name, default, '#{column_type}', options.merge(:role => role))) }
- end
- EOV
- end
-
- # This is used to serialize a non-native DB type. Use:
- # p.serialize 'pet', Dog
- def serialize(name, klass, options = {})
- Property.validate_property_class(klass)
- role.add_column(Property::Column.new(name, nil, klass, options.merge(:role => role)))
- end
-
- # This is used to create complex indices with the following syntax:
- #
- # p.index(:text) do |r| # r = record
- # {
- # "high" => "gender:#{r.gender} age:#{r.age} name:#{r.name}",
- # "name_#{r.lang}" => r.name, # multi-lingual index
- # }
- # end
- #
- # The first argument is the type (used to locate the table where the data will be stored) and the block
- # will be yielded with the record and should return a hash of key => value pairs.
- def index(type, &block)
- role.add_index(type, block)
- end
-
- alias actions class_eval
- end
- end
- accessor_module.role = self
- accessor_module
- end
-
- def define_property_methods(column)
- name = column.name
-
- #if create_time_zone_conversion_attribute?(name, column)
- # define_read_property_method_for_time_zone_conversion(name)
- #else
- define_read_property_method(name.to_sym, name, column)
- #end
-
- #if create_time_zone_conversion_attribute?(name, column)
- # define_write_property_method_for_time_zone_conversion(name)
- #else
- define_write_property_method(name.to_sym)
- #end
-
- define_question_property_method(name)
- end
-
- # Define a property reader method. Cope with nil column.
- def define_read_property_method(symbol, attr_name, column)
- # Unlike rails, we do not cast on read
- evaluate_attribute_property_method attr_name, "def #{symbol}; prop['#{attr_name}']; end"
- end
-
- # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
- # This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone.
- # def define_read_property_method_for_time_zone_conversion(attr_name)
- # method_body = <<-EOV
- # def #{attr_name}(reload = false)
- # cached = @attributes_cache['#{attr_name}']
- # return cached if cached && !reload
- # time = properties['#{attr_name}']
- # @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
- # end
- # EOV
- # evaluate_attribute_property_method attr_name, method_body
- # end
-
- # Defines a predicate method <tt>attr_name?</tt>.
- def define_question_property_method(attr_name)
- evaluate_attribute_property_method attr_name, "def #{attr_name}?; prop['#{attr_name}']; end", "#{attr_name}?"
- end
-
- def define_write_property_method(attr_name)
- evaluate_attribute_property_method attr_name, "def #{attr_name}=(new_value);prop['#{attr_name}'] = new_value; end", "#{attr_name}="
- end
-
- # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
- # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
- # def define_write_property_method_for_time_zone_conversion(attr_name)
- # method_body = <<-EOV
- # def #{attr_name}=(time)
- # unless time.acts_like?(:time)
- # time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
- # end
- # time = time.in_time_zone rescue nil if time
- # prop['#{attr_name}'] = time
- # end
- # EOV
- # evaluate_attribute_property_method attr_name, method_body, "#{attr_name}="
- # end
-
- # Evaluate the definition for an attribute related method
- def evaluate_attribute_property_method(attr_name, method_definition, method_name=attr_name)
- accessor_module.class_eval(method_definition, __FILE__, __LINE__)
- end
- end
-end
View
182 lib/property/schema.rb
@@ -1,100 +1,40 @@
-
module Property
- # This class holds all the properties of a given class or instance. It is used
- # to validate content and type_cast during write operations.
- #
- # The properties are not directly defined in the schema. They are stored in a
- # Role instance which checks that the database is in sync with the properties
- # defined.
- class Schema
- attr_reader :roles, :role, :binding
-
- # Create a new Schema. If a class_name is provided, the schema automatically
- # creates a default Role to store definitions.
- def initialize(class_name, binding)
- @binding = binding
- @roles = []
- if class_name
- @role = Role.new(class_name)
- include_role @role
- @roles << @role
- end
- end
-
- # Return an identifier for the schema to help locate property redefinition errors.
- def name
- @role ? @role.name : @binding.to_s
- end
-
- # Return true if the current schema has all the roles of the given object, class or role.
- def has_role?(thing)
- roles = self.roles.flatten
- test_roles = thing.class < RoleModule ? [thing] : thing.schema.roles.flatten
- test_roles.each do |role|
- return false unless roles.include?(role)
- end
- true
- end
-
- # If the parameter is a class, the schema will inherit the property definitions
- # from the class. If the parameter is a Role, the properties from that
- # role will be included. Any new columns added to a role or any new
- # roles included in a class will be dynamically added to the sub-classes (just like
- # Ruby class inheritance, module inclusion works).
- # If you ...
- def has_role(thing)
- if thing.kind_of?(Class)
- if thing.respond_to?(:schema) && thing.schema.kind_of?(Schema)
- schema_class = thing.schema.binding
- if @binding.ancestors.include?(schema_class)
- check_super_methods = false
- else
- check_super_methods = true
- end
- thing.schema.roles.flatten.each do |role|
- include_role role, check_super_methods
- end
- self.roles << thing.schema.roles
- else
- raise TypeError.new("expected Role or class with schema, found #{thing}")
+ # A schema contains all the property definitions for a given class. If Role is a module,
+ # then schema is a Class.
+ class Schema < Role
+ attr_accessor :roles, :klass
+
+ # Initialize a new schema with a name and the klass linked to the schema.
+ def initialize(name, opts = {})
+ super
+ @klass = opts[:class]
+
+ @roles = [self]
+
+ # Schema inheritance
+ unless superschema = opts[:superschema]
+ if @klass && @klass.superclass.respond_to?(:schema)
+ superschema = @klass.superclass.schema
end
- elsif thing.kind_of?(RoleModule)
- include_role thing
- self.roles << thing
- else
- raise TypeError.new("expected Role or class with schema, found #{thing.class}")
end
- end
-
- # Return the list of active roles. The active roles are all the Roles included
- # in the current object for which properties have been defined (not blank).
- def used_roles_in(object)
- roles.flatten.uniq.reject do |role|
- !role.used_in(object)
- end
- end
- # Return the list of column names.
- def column_names
- columns.keys
- end
-
- # Return true if the schema has a property with the given name.
- def has_column?(name)
- name = name.to_s
- [@roles].flatten.each do |role|
- return true if role.has_column?(name)
+ if superschema
+ include_role superschema
end
- false
end
- # Return column definitions from all included roles.
- def columns
- columns = {}
- @roles.flatten.uniq.each do |b|
- columns.merge!(b.columns)
+ # Add a set of property definitions to the schema.
+ def include_role(role)
+ if role.kind_of?(Schema)
+ # Superclass inheritance
+ @roles << role.roles
+ elsif role.kind_of?(Role)
+ @roles << role
+ elsif role.respond_to?(:schema) && role.schema.kind_of?(Role)
+ @roles << role.schema.roles
+ else
+ raise TypeError.new("Cannot include role #{role} (invalid type).")
end
- columns
end
# Return a hash with indexed types as keys and index definitions as values.
@@ -108,37 +48,47 @@ def index_groups
index_groups
end
- private
- def include_role(role, check_methods = true)
- return if roles.flatten.include?(role)
-
- stored_column_names = role.column_names
-
- check_duplicate_property_definitions(role, stored_column_names)
- check_duplicate_method_definitions(role, stored_column_names) if check_methods
-
- @binding.send(:include, role.accessor_module)
- end
-
- def check_duplicate_property_definitions(role, keys)
- common_keys = keys & self.columns.keys
- if !common_keys.empty?
- raise RedefinedPropertyError.new("Cannot include role '#{role.name}' in '#{name}'. Duplicate definitions: #{common_keys.join(', ')}")
+ # Return a hash with the column definitions defined in the schema and in the included
+ # roles.
+ def columns
+ # @columns ||=
+ begin
+ res = {}
+ @roles.flatten.uniq.each do |role|
+ # TODO: we could check for property redefinitions.
+ res.merge!(role.defined_columns)
end
+ res
end
+ end
- def check_duplicate_method_definitions(role, keys)
- common_keys = []
- # we are in an instance's metaclass, find the class
- superclass = @binding.ancestors.detect {|a| a.kind_of?(Class)}
- keys.each do |k|
- common_keys << k if superclass.method_defined?(k)
- end
+ # Return the list of active roles. The active roles are all the Roles included
+ # in the current object for which properties have been defined (not blank).
+ def used_roles_in(object)
+ roles.flatten.uniq.select do |role|
+ role.used_in(object)
+ end
+ end
- if !common_keys.empty?
- raise RedefinedMethodError.new("Cannot include role '#{role.name}' in '#{@binding}'. Would hide methods in superclass (#{superclass}): #{common_keys.join(', ')}")
- end
+ # Return true if the role has been included or is included in any superclass.
+ def has_role?(role)
+ if role.kind_of?(Schema)
+ role.roles.flatten - @roles.flatten == []
+ elsif role.kind_of?(Role)
+ @roles.flatten.include?(role)
+ elsif role.respond_to?(:schema) && role.schema.kind_of?(Role)
+ has_role?(role.schema)
+ else
+ false
end
+ end
+ # When a column is added in a Schema: define accessors in related class
+ def add_column(column)
+ super
+ if @klass
+ @klass.define_property_methods(column) if column.should_create_accessors?
+ end
+ end
end
-end
+end # Property
View
2  lib/property/stored_role.rb
@@ -8,7 +8,7 @@ module Property
# Once this module is included, you need to set the has_many association to the class that
# contains the columns definitions with something like:
#
- # has_many :stored_columns, :class_name => NameOfColumnsClass
+ # stored_columns_class NameOfColumnsClass
module StoredRole
include RoleModule
View
9 test/shoulda_macros/index.rb
@@ -3,7 +3,10 @@ module IndexMacros
class Client < ActiveRecord::Base
set_table_name :employees
include Property
-
+
+ def muse
+ 'I am your muse'
+ end
def index_reader(group_name)
if group_name.to_s == 'ml_string'
super.merge(:with => {'lang' => ['en', 'fr'], 'site_id' => '123'})
@@ -21,7 +24,7 @@ def self.should_maintain_indices
context "assigned to an instance of Dummy" do
subject do
dummy = IndexMacros::Client.new
- dummy.has_role @poet
+ dummy.include_role @poet
dummy
end
@@ -58,7 +61,7 @@ def self.should_not_maintain_indices
context "assigned to an instance of Dummy" do
subject do
dummy = IndexMacros::Client.new
- dummy.has_role @poet
+ dummy.include_role @poet
dummy
end
View
78 test/shoulda_macros/role.rb
@@ -77,7 +77,7 @@ def self.should_store_property_definitions(klass)
end
end # should_store_property_definitions
- def self.should_insert_properties_on_has_role_poet
+ def self.should_insert_properties_on_include_role_poet
context 'added' do
context 'to a parent class' do
@@ -86,35 +86,34 @@ def self.should_insert_properties_on_has_role_poet
set_table_name :dummies
include Property
property.string 'name'
+
+ def muse
+ 'I am your muse'
+ end
end
@klass = Class.new(@parent)
end
should 'propagate definitions to child' do
- @parent.has_role @poet
+ @parent.include_role @poet
assert_equal %w{name poem year}, @klass.schema.column_names.sort
end
should 'return true on has_role?' do
- @parent.has_role @poet
+ @parent.include_role @poet
assert @klass.has_role?(@poet)
end
- should 'raise an exception if class contains same definitions' do
- @parent.property.string 'poem'
- assert_raise(Property::RedefinedPropertyError) { @parent.has_role @poet }
- end
-
should 'not raise an exception on double inclusion' do
- @parent.has_role @poet
- assert_nothing_raised { @parent.has_role @poet }
+ @parent.include_role @poet
+ assert_nothing_raised { @parent.include_role @poet }
end
should 'add accessor methods to child' do
subject = @klass.new
- assert_raises(NoMethodError) { subject.poem = 'Poe'}
- @parent.has_role @poet
+ assert_raises(ArgumentError) { subject.poem = 'Poe'} # rails changes NoMethodError to ArgumentError
+ @parent.include_role @poet
assert_nothing_raised { subject.poem = 'Poe'}
end
@@ -126,22 +125,26 @@ def self.should_insert_properties_on_has_role_poet
set_table_name :dummies
include Property
property.string 'name'
+
+ def muse
+ 'I am your muse'
+ end
end
end
should 'insert definitions' do
- @klass.has_role @poet
+ @klass.include_role @poet
assert_equal %w{name poem year}, @klass.schema.column_names.sort
end
should 'return true on class has_role?' do
- @klass.has_role @poet
+ @klass.include_role @poet
assert @klass.has_role?(@poet)
end
should 'return role from column' do
- @klass.has_role @poet
- assert_equal (@poet.kind_of?(Class) ? @poet.schema.role : @poet), @klass.schema.columns['poem'].role
+ @klass.include_role @poet
+ assert_equal (@poet.kind_of?(Class) ? @poet.schema : @poet), @klass.schema.columns['poem'].role
end
end
@@ -149,7 +152,7 @@ def self.should_insert_properties_on_has_role_poet
subject { Developer.new }
setup do
- subject.has_role @poet
+ subject.include_role @poet
end
should 'merge property definitions' do
@@ -157,38 +160,7 @@ def self.should_insert_properties_on_has_role_poet
end
end
end
- end # should_insert_properties_on_has_role
-
- def self.should_add_role_methods
- context 'added' do
- context 'to a parent class' do
- setup do
- @parent = Class.new(ActiveRecord::Base) do
- set_table_name :dummies
- include Property
- property.string 'name'
- end
-
- @klass = Class.new(@parent)
- end
-
- should 'add role methods to child' do
- subject = @klass.new
- assert_raises(NoMethodError) { subject.muse }
- @parent.has_role @poet
-
- assert_nothing_raised { subject.muse }
- end
-
- should 'use role methods for defaults' do
- subject = @klass.new
- @parent.has_role @poet
- assert subject.save
- assert_equal 'I am your muse', subject.poem
- end
- end
- end
- end # should_add_role_methods
+ end # should_insert_properties_on_include_role
def self.should_take_part_in_used_list(has_defaults = true)
@@ -199,9 +171,13 @@ def self.should_take_part_in_used_list(has_defaults = true)
set_table_name :dummies
include Property
property.string 'name'
+
+ def muse
+ 'I am your muse'
+ end
end
- @klass.has_role @poet
+ @klass.include_role @poet
end
subject do
@@ -214,7 +190,7 @@ def self.should_take_part_in_used_list(has_defaults = true)
should 'not return role without corresponding attributes' do
subject.attributes = {'name' => 'hello'}
- assert_equal [@klass.schema.role], subject.used_roles
+ assert_equal [@klass.schema], subject.used_roles
end
should 'return role with corresponding attributes' do
View
65 test/unit/property/declaration_test.rb
@@ -41,7 +41,7 @@ def method_in_self
assert_equal %w{age first_name name}, subject.schema.column_names.sort
end
-
+
# This is allowed: it's the user's responsability to make sure such a thing does not happen
# or cause problems.
should 'be allowed to overwrite a property from the parent class' do
@@ -80,7 +80,7 @@ def method_in_self
end
end
end
-
+
context 'An instance' do
subject do
Class.new(ActiveRecord::Base) do
@@ -88,15 +88,15 @@ def method_in_self
include Property
end.new
end
-
+
should 'be able to include a role with _name_ property' do
role_with_name = Property::Role.new('foo')
role_with_name.property do |p|
p.string :name
end
-
+
assert_nothing_raised do
- subject.has_role role_with_name
+ subject.include_role role_with_name
end
end
end # An instance
@@ -178,12 +178,12 @@ def method_in_self
assert_equal Time, column.klass
assert_equal :datetime, column.type
end
-
+
should 'allow multiple declarations in one go' do
subject.property.string 'foo', 'bar', 'baz'
assert_equal %w{bar baz foo}, subject.schema.column_names.sort
end
-
+
should 'allow multiple declarations in an Array' do
subject.property.string ['foo', 'bar', 'baz']
assert_equal %w{bar baz foo}, subject.schema.column_names.sort
@@ -228,7 +228,7 @@ def to_json(*args)
p.string 'poem'
end
- @instance.has_role @poet
+ @instance.include_role @poet
end
should 'behave like any other property column' do
@@ -239,10 +239,19 @@ def to_json(*args)
assert_equal Hash['poem' => 'shazam'], @instance.prop
end
+ should 'use method_missing for property methods' do
+ assert !@instance.respond_to?(:poem=)
+ assert_nothing_raised do
+ @instance.poem = 'shazam'
+ assert_equal 'shazam', @instance.poem
+ end
+ end
+
should 'not affect instance class' do
assert !subject.schema.column_names.include?('poem')
- assert_raise(NoMethodError) do
- subject.new.poem = 'not a poet'
+ assert_raise(ArgumentError) do # rails transforms the NoMethodError into an obscure ArgumentError...
+ instance = subject.new
+ instance.poem = 'not a poet'
end
end
end
@@ -268,29 +277,13 @@ def to_json(*args)
subject { Class.new(Developer) }
should 'raise an exception if we ask to behave like a class without schema' do
- assert_raise(TypeError) { subject.has_role String }
+ assert_raise(TypeError) { subject.include_role String }
end
should 'raise an exception if we ask to behave like an object' do
- assert_raise(TypeError) { subject.has_role 'me' }
- end
-
- should 'raise an exception if the role redefines properties' do
- @emp = Property::Role.new('empi')
- @emp.property.string 'first_name'
- assert_raise(Property::RedefinedPropertyError) do
- subject.has_role @emp
- end
+ assert_raise(TypeError) { subject.include_role 'me' }
end
-
- should 'raise an exception if the role contains superclass methods' do
- @emp = Property::Role.new('empi')
- @emp.property.string 'method_in_parent'
- assert_raise(Property::RedefinedMethodError) do
- subject.has_role @emp
- end
- end
-
+
should 'inherit properties when asking to behave like a class' do
@class = Class.new(ActiveRecord::Base) do
include Property
@@ -298,10 +291,10 @@ def to_json(*args)
p.string 'hop'
end
end
-
- subject.has_role @class
+
+ subject.include_role @class
assert_equal %w{language last_name hop age first_name}, subject.schema.column_names
- assert subject.has_role?(@class)
+ assert subject.has_role?(@class.schema)
end
end
@@ -324,12 +317,10 @@ class Contact < ActiveRecord::Base
p.string 'first_name'
p.string 'famous', :default => :get_is_famous
p.integer 'age'
+ end
- p.actions do
- def get_is_famous
- 'no'
- end
- end
+ def get_is_famous
+ 'no'
end
def version
View
22 test/unit/property/role_test.rb
@@ -10,17 +10,10 @@ class RoleTest < ActiveSupport::TestCase
@poet = Property::Role.new('Poet') do |p|
p.string 'poem', :default => :muse
p.integer 'year'
-
- p.actions do
- def muse
- 'I am your muse'
- end
- end
end
end
- should_insert_properties_on_has_role_poet
- should_add_role_methods
+ should_insert_properties_on_include_role_poet
should_take_part_in_used_list
should_not_maintain_indices # no indexed column defined
@@ -48,12 +41,10 @@ class Foo < ActiveRecord::Base
property do |p|
p.string 'poem', :default => :muse
p.integer 'year'
+ end
- p.actions do
- def muse
- 'I am your muse'
- end
- end
+ def muse
+ 'I am your muse'
end
end
@@ -61,8 +52,7 @@ def muse
@poet = Foo
end
- should_insert_properties_on_has_role_poet
- should_add_role_methods
+ should_insert_properties_on_include_role_poet
should_take_part_in_used_list
context 'set on a sub-class instance' do
@@ -71,7 +61,7 @@ def muse
end
should 'not raise an exception' do
- assert_nothing_raised { subject.has_role Developer }
+ assert_nothing_raised { subject.include_role Developer }
end
end # set on a sub-class instance
end # A class used as role
View
2  test/unit/property/stored_role_test.rb
@@ -24,7 +24,7 @@ class Role < ActiveRecord::Base
@poet = Role.find(role.id)
end
- should_insert_properties_on_has_role_poet
+ should_insert_properties_on_include_role_poet
should_take_part_in_used_list(false)
should_not_maintain_indices # no indexed column defined
Please sign in to comment.
Something went wrong with that request. Please try again.