Reputation: 20560
Update: I've simplified my question; you can see the full history by checking out my editing revisions. Thanks to iain and bernardk for getting me this far.
I want to load carrierwave functionality into an instance of my User < ActiveRecord::Base
model.
require 'uploaders/avatar_uploader'
module HasAnAvatar
def self.extended(host)
if host.class == Class
mount_uploader :avatar, AvatarUploader
else
class << host
mount_uploader :avatar, AvatarUploader
end
end
end
end
Executing:
(user = User.first).extend(HasAnAvatar).avatar
Results in:
NoMethodError: undefined method
new' for nil:NilClass from /Users/evan/.rbenv/versions/1.9.3-p286/lib/ruby/gems/1.9.1/gems/carrierwave-0.6.2/lib/carrierwave/mount.rb:306:in
uploader'
I suspect the problem is that mount_uploader
in HasAnAvatar
is not being invoked properly on the eigenclass for user
, such that the uploaders
hash isn't populated.
Any thoughts on how to get this working?
Here is an example Rails app for this issue: https://github.com/neezer/extend_with_avatar_example
Upvotes: 1
Views: 645
Reputation: 21742
Firstly there's something odd about your contexts or at least the naming. Context do not return RolePlayers. Roles exist only inside a context. The role methods are not and should not be accessible outside the context. That said the standard way of dealing with DCI in ruby is method injection. This approach is not flawless but is so far the closest to pure DCI anyone have come in Ruby. There's an experimental library called alias_dci that might help.
EDIT There now is a gem that makes injectionless DCI possible in Ruby. It's based on the work gone into Marvin, the first language to support injectionless DCI. The gem is called Moby and can be installed with
gem install Moby
it's currently still somewhat experimental but the smoke test of being able to implement the DCI examples from fullOO has been passed
Upvotes: 0
Reputation: 20560
Ok, I think I found out what was causing my issues...
In https://github.com/jnicklas/carrierwave/blob/master/lib/carrierwave/mount.rb, in the definition of CarrierWave::Mount::Mounter
, there are three references to record.class
. This rightly references back to the main User
class, which doesn't have the extended methods I loaded into the user's metaclass. So, I changed those to this: https://gist.github.com/4465172, and it seemed to work.
Also seems to continue to work if used normally like in the CarrierWave docs, so that's good too. Will continue testing it, though.
Upvotes: 0
Reputation: 3734
From what I know of Ruby classes, once I include a module into a class, ... but not retroactively alter any existing instances of User.
On the contrary, an include/extend immediately affects all existing instances, because it is a question of pointer between the class and its superclass. See How does Inheritance work in Ruby? and also the links inside.
module HasAnAvatar
def m
puts 'in HasAnAvatar#m'
end
end
class AvatarUploader; end
class User
end
user = User.new
print 'user.respond_to?(:m) ? '; puts user.respond_to?(:m) ? 'yes' : 'no'
print '1) user.respond_to?(:avatar) ? '; puts user.respond_to?(:avatar) ? 'yes' : 'no'
class User
include HasAnAvatar
end
print 'user.respond_to?(:m) ? '; puts user.respond_to?(:m) ? 'yes' : 'no'
print '2) user.respond_to?(:avatar) ? '; puts user.respond_to?(:avatar) ? 'yes' : 'no'
module HasAnAvatar
def self.included(base)
puts "#{self} included into #{base}"
# base.mount_uploader :avatar, AvatarUploader
base.send(:attr_reader, :avatar)
end
end
class User
include HasAnAvatar
end
print '3) user.respond_to?(:avatar) ? '; puts user.respond_to?(:avatar) ? 'yes' : 'no'
print 'user.avatar : '; p user.avatar
print 'user.avatar.class : '; p user.avatar.class
user.instance_variable_set(:@avatar, AvatarUploader.new)
print 'after set, user.avatar : '; p user.avatar
print 'user.avatar.class : '; p user.avatar.class
Execution (Ruby 1.9.2) :
$ ruby -w t.rb
user.respond_to?(:m) ? no
1) user.respond_to?(:avatar) ? no
user.respond_to?(:m) ? yes
2) user.respond_to?(:avatar) ? no
HasAnAvatar included into User
3) user.respond_to?(:avatar) ? yes
user.avatar : nil
user.avatar.class : NilClass
after set, user.avatar : #<AvatarUploader:0x007fcc2b047cf8>
user.avatar.class : AvatarUploader
So included methods immediately become available to all existing instances.
Why does user.avatar
answer nil ? Because instance variables belong to ... single instances. See Why are symbols in Ruby not thought of as a type of variable?
In this case, the old user was not assigned an avatar.
But why do you get user2.avatar.class #=> AvatarUploader
. I suppose that, behind the scene, base.mount_uploader :avatar, AvatarUploader
does something such that :avatar is not an accessor of a corresponding instance variable, or defines an initialize method which starts to set this variable into new instances.
Here is a solution for the second example :
module Pacifiable
def self.extended(host)
puts "#{host} extended by #{self}"
def host.pacified_with(mechanism)
@@method_name = "pacified_with_#{mechanism}?"
puts "about to define '#{@@method_name}' into #{self} of class #{self.class }"
if self.class == Class
then # define an instance method in a class
define_method(@@method_name) { true }
else # define a singleton method for an object
class << self
define_method(@@method_name) { true }
end
end
end
end
end
class JellyFish
define_method(:is_squishy?) { true }
end
class Lobster
extend Pacifiable
pacified_with :polar_bear
define_method(:is_squishy?) { false }
end
a_lobster = Lobster.new
print 'a_lobster.pacified_with_polar_bear? '; p a_lobster.pacified_with_polar_bear?
print 'a_lobster.is_squishy? '; p a_lobster.is_squishy?
jelly = JellyFish.new
### Add functionality to instance
#
## what I want:
#
jelly.extend(Pacifiable)
jelly.pacified_with(:polar_bear)
print 'jelly.pacified_with_polar_bear? '; p jelly.pacified_with_polar_bear? #=> true
Execution :
Lobster extended by Pacifiable
about to define 'pacified_with_polar_bear?' into Lobster of class Class
a_lobster.pacified_with_polar_bear? true
a_lobster.is_squishy? false
#<JellyFish:0x007fcc2b047640> extended by Pacifiable
about to define 'pacified_with_polar_bear?' into #<JellyFish:0x007fcc2b047640> of class JellyFish
jelly.pacified_with_polar_bear? true
About jelly.class.pacified_with(:polar_bear)
: the mistake was to call class
, which answers the class JellyFish
; what you want is the singleton class of the instance object jelly. Once a method is defined in the singleton class of an object, you can send it directly to the object. The same applies to classes, which are instances of Class. Once a method is defined in the singleton class of a class, you can send it directly to the class. We call them class methods, they are actually instance methods of the singleton class of the class. Ouf !
Last OR : as explained, if you extend the class, it is valid for all existing instances. Thus you have to extend individual instances :
class JellyFromTheBigBlueSea
def find
puts 'in JellyFromTheBigBlueSea#find'
jelly = JellyFish.new
jelly.extend(Pacifiable)
jelly.pacified_with :polar_bear
jelly
end
end
class JellyFromAnIsolatedCove
def find
puts 'in JellyFromAnIsolatedCove#find'
JellyFish.new
end
end
normal_jelly = JellyFromTheBigBlueSea.new.find
ignorant_jelly = JellyFromAnIsolatedCove.new.find
## what I want:
#
print 'normal_jelly.pacified_with_polar_bear? '; p normal_jelly.pacified_with_polar_bear?
print 'ignorant_jelly.pacified_with_polar_bear?' ; p ignorant_jelly.pacified_with_polar_bear?
Execution :
in JellyFromTheBigBlueSea#find
#<JellyFish:0x007fb5d9045060> extended by Pacifiable
about to define 'pacified_with_polar_bear?' into #<JellyFish:0x007fb5d9045060> of class JellyFish
in JellyFromAnIsolatedCove#find
normal_jelly.pacified_with_polar_bear? true
ignorant_jelly.pacified_with_polar_bear?t.rb:109:in `<main>': undefined method `pacified_with_polar_bear?' for #<JellyFish:0x007fb5d9044d18> (NoMethodError)
Upvotes: 1
Reputation: 12251
Here are two ways you can can include a module into an instance (which is to basically extend the instance's eigenclass). I don't think this is the best answer to your problem though, even if it may answer the question (in part).
class A
end
# => nil
module B
def blah
"Blah!"
end
end
# => nil
a = A.new
=> #<A:0x0000010086cdf0>
a.blah
# NoMethodError: undefined method `blah' for #<A:0x0000010086cdf0>
class << a
include B
end
a.blah
# => "Blah!"
b = A.new
# => #<A:0x0000010083b818>
b.blah
# NoMethodError: undefined method `blah' for #<A:0x0000010083b818>
b.extend B
# => #<A:0x0000010083b818>
b.blah
# => "Blah!"
c.blah
# NoMethodError: undefined method `blah' for #<A:0x0000010085eed0>
From your edit:
module Pacifiable
def pacified_with(mechanism)
class_eval do
define_method(:"pacified_with_#{mechanism}?") { true }
end
end
end
# => nil
class JellyFish
define_method(:is_squishy?) { true }
end
# => #<Proc:0x00000100850448@(irb):10 (lambda)>
class Lobster
extend Pacifiable
pacified_with :polar_bear
define_method(:is_squishy?) { false }
end
# => #<Proc:0x00000100960540@(irb):16 (lambda)>
lobster = Lobster.new
# => #<Lobster:0x0000010095aa50>
lobster.pacified_with_polar_bear?
# => true
jelly = JellyFish.new
# => #<JellyFish:0x00000100951108>
jelly.pacified_with_polar_bear?
# NoMethodError: undefined method `pacified_with_polar_bear?' for #<JellyFish:0x00000100951108>
class << jelly
extend Pacifiable
pacified_with :polar_bear
end
# => #<Proc:0x0000010093ddd8@(irb):4 (lambda)>
jelly.pacified_with_polar_bear?
# => true
big_jelly = JellyFish.new
# => #<JellyFish:0x0000010091ad38>
big_jelly.pacified_with_polar_bear?
# NoMethodError: undefined method `pacified_with_polar_bear?' for #<JellyFish:0x0000010091ad38>
Upvotes: 2
Reputation: 2380
First of all, the way with instance extend
calls is working (See end of the post).
user.extend
part. For example, see if mount_uploader
uses Carrierwave::Mount#mount_uploader
method, as there are defined 'Uploader' everrides.1.9.3p327 :001 > class A
1.9.3p327 :002?> def foo
1.9.3p327 :003?> '42'
1.9.3p327 :004?> end
1.9.3p327 :005?> end
=> nil
1.9.3p327 :006 > A.new.foo
=> "42"
1.9.3p327 :011 > module Ext
1.9.3p327 :012?> def foo
1.9.3p327 :013?> 'ext'
1.9.3p327 :014?> end
1.9.3p327 :015?> end
=> nil
1.9.3p327 :016 > class AFancy
1.9.3p327 :017?> def call
1.9.3p327 :018?> a = A.new
1.9.3p327 :019?> a.extend Ext
1.9.3p327 :020?> a
1.9.3p327 :021?> end
1.9.3p327 :022?> end
=> nil
1.9.3p327 :023 > a1 = A.new
=> #<A:0x00000000e09b10>
1.9.3p327 :024 > a2 = AFancy.new.call
=> #<A:0x00000000e17210>
1.9.3p327 :025 > a3 = A.new
=> #<A:0x00000000e1bd38>
1.9.3p327 :026 > [a1, a2, a3].map(&:foo)
=> ["42", "ext", "42"]
Upvotes: 0