Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for modules with attributes #90

Merged
merged 23 commits into from Jun 8, 2012
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5600671
Initial work on Virtus modules
solnic May 18, 2012
fb30a55
No idea why I added reverse there
solnic May 18, 2012
e9ed8d8
Make Virtus work with classes, instances and modules (messy spike com…
solnic Jun 4, 2012
6f5924e
Merge branch 'master' into modules-spike
solnic Jun 4, 2012
8f3c2a6
Add more use cases to the using modules integration spec
solnic Jun 4, 2012
6f6e96e
Rename ClassExtensions to ClassInclusions
dkubb Jun 4, 2012
7e30658
Remove AllowedWriterMethods
solnic Jun 4, 2012
76e6a35
Move allowed_writer_methods down a bit
solnic Jun 4, 2012
e256f84
Remove unecessary line from protected method
dkubb Jun 4, 2012
d81ac7d
Change methods to be private
dkubb Jun 4, 2012
e01b2d1
Add @api private doc to public_method_list
dkubb Jun 4, 2012
342cb26
Fix whitespace
dkubb Jun 4, 2012
371c2c9
Rename _attributes to attribute_set
solnic Jun 4, 2012
6d03269
Merge branch 'master' into modules-spike
solnic Jun 6, 2012
fe492ed
be kind and call super in ModuleExtensions#extended and #included to …
apotonick Jun 8, 2012
b5f27a5
Merge pull request #92 from apotonick/modules-spike
solnic Jun 8, 2012
04f8567
Fix Virtus.extended visibility
solnic Jun 8, 2012
023fdee
Rename InstanceExtensions to just Extensions
solnic Jun 8, 2012
76acc2d
Fix visibility of Extensions.extended
solnic Jun 8, 2012
57706fa
Adjust flay threshold
solnic Jun 8, 2012
7ce6f99
Raise argument error if Virtus is being included into an unsupported …
solnic Jun 8, 2012
3615ab4
Go back to 100% doc coverage
solnic Jun 8, 2012
15ad0b5
Simplify Virtus.included (that case statement made no sense)
solnic Jun 8, 2012
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 14 additions & 3 deletions lib/virtus.rb
Expand Up @@ -23,10 +23,17 @@ module Virtus
# @return [undefined]
#
# @api private
def self.included(descendant)
def self.included(object)
super
descendant.extend(ClassMethods)
descendant.send(:include, InstanceMethods)
case object
when Class then object.send(:include, ClassInclusions)
when Module then object.extend(ModuleExtensions)
end
end

def self.extended(object)
object.extend(InstanceExtensions)
object.extend(InstanceMethods)
end

private_class_method :included
Expand All @@ -38,6 +45,10 @@ def self.included(descendant)
require 'virtus/support/options'
require 'virtus/support/equalizer'

require 'virtus/instance_extensions'
require 'virtus/class_inclusions'
require 'virtus/module_extensions'

require 'virtus/attributes_accessor'
require 'virtus/class_methods'
require 'virtus/instance_methods'
Expand Down
20 changes: 20 additions & 0 deletions lib/virtus/class_inclusions.rb
@@ -0,0 +1,20 @@
module Virtus
module ClassInclusions

def self.included(descendant)
super
descendant.extend(ClassMethods)
descendant.extend(InstanceExtensions::AllowedWriterMethods)
descendant.send(:include, InstanceMethods)
end

def _attributes
self.class.attributes
end

def allowed_writer_methods
self.class.allowed_writer_methods
end

end # module ClassInclusions
end # module Virtus
67 changes: 8 additions & 59 deletions lib/virtus/class_methods.rb
Expand Up @@ -2,8 +2,7 @@ module Virtus

# Class methods that are added when you include Virtus
module ClassMethods
WRITER_METHOD_REGEXP = /=\z/.freeze
INVALID_WRITER_METHODS = %w[ == != === []= attributes= ].to_set.freeze
include InstanceExtensions

# Hook called when module is extended
#
Expand All @@ -22,39 +21,6 @@ def self.extended(descendant)

private_class_method :extended

# Defines an attribute on an object's class
#
# @example
# class Book
# include Virtus
#
# attribute :title, String
# attribute :author, String
# attribute :published_at, DateTime
# attribute :page_count, Integer
# end
#
# @param [Symbol] name
# the name of an attribute
#
# @param [Class] type
# the type class of an attribute
#
# @param [#to_hash] options
# the extra options hash
#
# @return [self]
#
# @see Attribute.build
#
# @api public
def attribute(*args)
attribute = Attribute.build(*args)
attribute.define_accessor_methods(virtus_attributes_accessor_module)
virtus_add_attribute(attribute)
self
end

# Returns all the attributes defined on a Class
#
# @example
Expand All @@ -79,21 +45,7 @@ def attributes
parent = superclass.public_send(method) if superclass.respond_to?(method)
@attributes = AttributeSet.new(parent)
end

# The list of writer methods that can be mass-assigned to in #attributes=
#
# @return [Set]
#
# @api private
def allowed_writer_methods
@allowed_writer_methods ||=
begin
allowed_writer_methods = public_instance_methods.map(&:to_s)
allowed_writer_methods = allowed_writer_methods.grep(WRITER_METHOD_REGEXP).to_set
allowed_writer_methods -= INVALID_WRITER_METHODS
allowed_writer_methods.freeze
end
end
alias _attributes attributes

protected

Expand All @@ -104,7 +56,7 @@ def allowed_writer_methods
# @api private
def virtus_setup_attributes_accessor_module
@virtus_attributes_accessor_module = AttributesAccessor.new(inspect)
include virtus_attributes_accessor_module
include @virtus_attributes_accessor_module
self
end

Expand All @@ -128,13 +80,6 @@ def inherited(descendant)
descendant.virtus_setup_attributes_accessor_module
end

# Holds the anonymous module which hosts this class's Attribute accessors
#
# @return [Module]
#
# @api private
attr_reader :virtus_attributes_accessor_module

# Hooks into const missing process to determine types of attributes
#
# @param [String] name
Expand All @@ -154,9 +99,13 @@ def const_missing(name)
#
# @api private
def virtus_add_attribute(attribute)
attributes << attribute
super
descendants.each { |descendant| descendant.attributes.reset }
end

def public_method_list
public_instance_methods
end

end # module ClassMethods
end # module Virtus
75 changes: 75 additions & 0 deletions lib/virtus/instance_extensions.rb
@@ -0,0 +1,75 @@
module Virtus

# Instance-level extensions
module InstanceExtensions
module AllowedWriterMethods
WRITER_METHOD_REGEXP = /=\z/.freeze
INVALID_WRITER_METHODS = %w[ == != === []= attributes= ].to_set.freeze

# The list of writer methods that can be mass-assigned to in #attributes=
#
# @return [Set]
#
# @api private
def allowed_writer_methods
@allowed_writer_methods ||=
begin
allowed_writer_methods = public_method_list.map(&:to_s)
allowed_writer_methods = allowed_writer_methods.grep(WRITER_METHOD_REGEXP).to_set
allowed_writer_methods -= INVALID_WRITER_METHODS
allowed_writer_methods.freeze
end
end
end

def self.extended(object)
object.extend(AllowedWriterMethods)
object.extend(InstanceMethods)
object.instance_eval do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@solnic do you think we can just do this here:

@virtus_attributes_accessor_module = AttributesAccessor.new(object.class.inspect)
object.extend @virtus_attributes_accessor_module

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dkubb we need that ivar, it's used in #attribute. so maybe object.instance_variable_set?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@solnic ahh yeah, after I mentioned it, I realized that the object needs the ivar set internally. I think #instance_eval the way we're doing it is probably nicer then.

@virtus_attributes_accessor_module = AttributesAccessor.new(object.class.inspect)
extend @virtus_attributes_accessor_module
end
end

# Defines an attribute on an object's class
#
# @example
# class Book
# include Virtus
#
# attribute :title, String
# attribute :author, String
# attribute :published_at, DateTime
# attribute :page_count, Integer
# end
#
# @param [Symbol] name
# the name of an attribute
#
# @param [Class] type
# the type class of an attribute
#
# @param [#to_hash] options
# the extra options hash
#
# @return [self]
#
# @see Attribute.build
#
# @api public
def attribute(*args)
attribute = Attribute.build(*args)
attribute.define_accessor_methods(@virtus_attributes_accessor_module)
virtus_add_attribute(attribute)
self
end

def _attributes
@_attributes ||= AttributeSet.new
end

def virtus_add_attribute(attribute)
_attributes << attribute
end
end # module InstanceExtensions
end # module Virtus
9 changes: 7 additions & 2 deletions lib/virtus/instance_methods.rb
Expand Up @@ -140,7 +140,7 @@ def to_hash
#
# @api private
def get_attributes
self.class.attributes.each_with_object({}) do |attribute, attributes|
_attributes.each_with_object({}) do |attribute, attributes|
name = attribute.name
attributes[name] = get_attribute(name) if yield(attribute)
end
Expand All @@ -155,7 +155,7 @@ def get_attributes
# @api private
def set_attributes(attributes)
::Hash.try_convert(attributes).each do |name, value|
set_attribute(name, value) if self.class.allowed_writer_methods.include?("#{name}=")
set_attribute(name, value) if allowed_writer_methods.include?("#{name}=")
end
end

Expand All @@ -181,5 +181,10 @@ def set_attribute(name, value)
__send__("#{name}=", value)
end

# @api private
def public_method_list
public_methods
end

end # module InstanceMethods
end # module Virtus
34 changes: 34 additions & 0 deletions lib/virtus/module_extensions.rb
@@ -0,0 +1,34 @@
module Virtus

# Virtus module class that can define attributes for later inclusion
#
module ModuleExtensions

def extended(object)
object.extend(Virtus)
define_attributes(object)
end

def included(object)
object.send(:include, ClassInclusions)
define_attributes(object)
end

def attribute(*args)
attribute_definitions << args
end

private

def attribute_definitions
@_attribute_definitions ||= []
end

def define_attributes(object)
attribute_definitions.each do |attribute_args|
object.attribute(*attribute_args)
end
end

end # class Module
end # module Virtus
3 changes: 2 additions & 1 deletion lib/virtus/value_object.rb
Expand Up @@ -63,6 +63,7 @@ def clone
self
end
alias dup clone

end

module ClassMethods
Expand Down Expand Up @@ -122,7 +123,7 @@ def allowed_writer_methods
@allowed_writer_methods ||=
begin
allowed_writer_methods = super
allowed_writer_methods += attributes.map{|attr| "#{attr.name}="}
allowed_writer_methods += _attributes.map{|attr| "#{attr.name}="}
allowed_writer_methods.to_set.freeze
end
end
Expand Down
35 changes: 35 additions & 0 deletions spec/integration/extending_objects_spec.rb
@@ -0,0 +1,35 @@
require 'spec_helper'

describe 'I can extend objects' do
before do
module Examples
class User; end

class Admin; end
end
end

specify 'defining attributes on an object' do
attributes = { :name => 'John', :age => 29 }

admin = Examples::Admin.new
admin.extend(Virtus)

admin.attribute :name, String
admin.attribute :age, Integer

admin.name = 'John'
admin.age = 29

admin.name.should eql('John')
admin.age.should eql(29)

admin.attributes.should eql(attributes)

new_attributes = { :name => 'Jane', :age => 28 }
admin.attributes = new_attributes

admin.name.should eql('Jane')
admin.age.should eql(28)
end
end