3

I am trying to understand the shrine gem source code that is a toolkit for file attachments. You can define an uploader on your model in rails like so:

class Picture < ApplicationRecord
  include ImageUploader::Attachment.new(:image) 
end

The class definition for Attachment can be found at this link.

This is all it is:

class Attachment < Module
  @shrine_class = ::Shrine
end

My assumption is that this allows you to instantiate the class in the include so that the methods are now available where you included it, similar to a mixin. Is Module a ruby class? How exactly does this work?

Edit:

For clarity, ImageUploader is defined in my app like so:

class ImageUploader < Shrine
  plugin :remove_attachment
end

So ImageUploader::Attachment.new(:image) is using that Attachment class defined in Shrine.

trueinViso
  • 1,354
  • 3
  • 18
  • 30

3 Answers3

2

Module is indeed a Ruby class. An instance of the class Module is a Ruby module. To illustrate, these two ways of defining a module are equivalent:

module MyModule
  # ...
end

# is equivalent to

MyModule = Module.new do
  # ...
end

If an instance of Module is a Ruby module, that means that an instance of any subclass of Module is also a Ruby module, including Shrine::Attachment. This makes sense, because we know that we can include only modules, so an instance of Shrine::Attachment has to be a module.

Because of Shrine's plugin system design, this:

class Attachment < Module
  @shrine_class = ::Shrine
end

isn't the whole implementation of Shrine::Attachment; the actual implementation is defined in the Shrine::Plugins::Base::AttachmentMethods module, which gets included into Shrine::Attachment.

If we look at the implementation of Shrine::Attachment.new, we can see that it dynamically defines methods on itself based on the given attribute name. For example, Shrine::Attachment.new(:image) will generate a module with the following methods defined: #image_attacher, #image=, #image, and #image_url. These methods will then get added to the model that includes that Shrine::Attachment instance.


Why didn't I just have a method that creates a new module via Module.new (like Refile does), instead of creating a whole subclass of Module? Well, two main reasons:

First, this gives better introspection, because instead of seeing #<Module:0x007f8183d27ab0> in your model's ancestors list, you now see an actual Shrine::Attachment instance that points to its definition. You could still manually override #to_s and #inspect, but this is better.

Second, since Shrine::Attachment is now a class, other Shrine plugins can extend it with more behaviour. So remote_url plugin adds the #<attachment>_remote_url accessor, data_uri plugin adds the #<attachment>_data_uri accessor etc.

Janko
  • 8,985
  • 7
  • 34
  • 51
  • 1
    Thanks for the gem, I really like the coding style you use. The only thing I'm hazy on, is how the `initialize` method inside of `AttachmentMethods` gets called when I instantiate `ImageUploader::Attachment.new(:image)`? – trueinViso Sep 18 '17 at 15:35
  • 1
    At the end of the file `Shrine.plugin Shrine::Plugins::Base` is called, which includes the `Shrine::Plugins::Base::AttachmentMethods` into `Shrine::Attachment` (see the implementation of `Shrine::Plugins::Base::ClassMethods#plugin`), so `Shrine::Plugins::Base::AttachmentMethods#initialize` and all other methods from that module get added to `Shrine::Attachment`, and that's how `Shrine::Attachment#initialize` is defined. – Janko Sep 18 '17 at 19:10
  • 1
    Thanks, though I owe the whole plugin system implementation to Jeremy Evans, it's practically a copy-paste from the Roda gem: https://github.com/jeremyevans/roda – Janko Sep 18 '17 at 19:12
  • I noticed on your blogs that your gem was heavily influenced by his style, I plan on giving Sequel and Roda a try soon. I will definitely be integrating some of these techniques into my own work :D. – trueinViso Sep 18 '17 at 20:35
1

Modules are a way of grouping together methods, classes, and constants. here is first sample grouping a class

app/services/purchase_service.rb

module PurchaseService

  class PurchaseRequest
    def initialize
      # init value
    end

    def request_item
      # action
    end
  end

  class PurchaseOrder
    def initialize
      # init value
    end

    def order_item
      # action
    end
  end
end

in constroller file, you can call class using Module_name::Class_name.new as follow

@purchase_svc = PurchaseService::PurchaseRequest.new
@purchase_svc.request_item
@purchase_svc = PurchaseService::PurchaseOrder.new
@purchase_svc.order_item

but there are times when you want to group things together that don't naturally form a class. here is second example for grouping but not in class form

module_collection.rb, (one file have two modules stack and queue)

module Stacklike
  def stack
    @stack ||= []
  end

  def add_to_stack(obj)
    @stack.push(obj)
  end

  def take_from_stack
    @stack.pop
  end
end

module Queuelike
  #
end

now if an object for example cargo need to have stack facility, then I will include the module.

cargo.rb,

  require './module_collection.rb'
  include Stacklike
    # as cargo needs stack
  class
    def initialize
      stack
      # this will call stack method inside module_collection.rb
    end
  end
widjajayd
  • 6,090
  • 4
  • 30
  • 41
1

Note : preparing this answer took several hours. In the meantime janko-m has answered well.

The guys at Rails or Shrine have pushed the knowledge of what one can do in Ruby to a level far beyond what one can imagine by reading Ruby books, and I have read a dozen.

99 % of the time include is in the form

include SomeModule

and SomeModule is defined in a separate file some_module.rb which is incorporated into the current source file with a require 'some_module'.

This

include ImageUploader::Attachment.new(:image)

is tricky for many reasons.

=== inner class ===

98 % of the time, a class is an outer object which contains mainly def methods, some includes and a pinch of class instance variables. I haven't written tons of Ruby code, but only once an inner class in a special circumstance. From the outside, it can be used only by giving the full acces path, such as Shrine::Attachment or Shrine::Plugins::Base::AttacherMethods.

I didn't know that a subclass "inherits" inner classes so that one can write

ImageUploader::Attachment

=== Module.new ===

If you read enough Ruby documentation, you'll find 1'000 times that the difference between a class and a module is that we cannot instanciate a module. Modules serve only to create a namespace around one's code or (mainly) mixin methods in a class (more exactly, include SomeModule creates an anonymous superclass so that the search path for methods goes from the class to SomeModule, then to the superclass (Object if not explicitly defined)).

Thus I would have sworn under torture that there is no new method for Module, because there is no need. But there is one, which returns an anonymous module.

Well, having said that, here we instantiate the classImageUploader::Attachment, not a module, and even Module.new instantiates the class Module.

=== include expression ===

For the 1 % of includes which don't use a constant but an expression, the expression must return a Module. And you have your answer to why Attachment inherits from Module. Whitout such inheritance, include will complain. Run the following code, it works. But if you uncomment

#    include ImageUploader::Attachment_O.new(:image) 

in class Picture, there is an error :

t.rb:28:in `include': wrong argument type Shrine::Attachment_O (expected Module) (TypeError)
    from t.rb:28:in `<class:Picture>'
    from t.rb:27:in `<main>'

file t.rb :

class Shrine
    class Attachment_O
        def initialize(parm=nil)
            puts "creating an instance of #{self.class.name}"
        end
    end

    class Attachment_M < Module
        def initialize(parm=nil)
            puts "creating an instance of #{self.class.name}"
        end
    end
end

print 'Attachment_O ancestors '; p Shrine::Attachment_O.ancestors
print 'Attachment_M ancestors '; p Shrine::Attachment_M.ancestors

class ImageUploader < Shrine
end

imupO = ImageUploader::Attachment_O.new
imupM = ImageUploader::Attachment_M.new

print 'imupO is a Module ? '; p imupO.is_a?(Module)
print 'imupM is a Module ? '; p imupM.is_a?(Module)

class Picture
#    include ImageUploader::Attachment_O.new(:image) 
    include ImageUploader::Attachment_M.new(:image) 
end

Execution :

$ ruby -w t.rb 
Attachment_O ancestors [Shrine::Attachment_O, Object, Kernel, BasicObject]
Attachment_M ancestors [Shrine::Attachment_M, Module, Object, Kernel, BasicObject]
creating an instance of Shrine::Attachment_O
creating an instance of Shrine::Attachment_M
imupO is a Module ? false
imupM is a Module ? true
creating an instance of Shrine::Attachment_M

This is all it is:

At first glance, the definition of Attachment seems curious because it is empty. I haven't studied shrine.rb in detail, but I've seen this :

# Load a new plugin into the current class ...
def plugin(plugin, *args, &block)
...
    self::Attachment.include(plugin::AttachmentMethods) if defined?(plugin::AttachmentMethods)

Obviously Attachment is later populated with methods by inclusion of a module, or more exactly, include creates an anonymous superclass to Attachment, which points to AttachmentMethods, so that the method search mechanism finds the methods in the included module. See also How does Inheritance work in Ruby? .

BernardK
  • 3,674
  • 2
  • 15
  • 10
  • Thank you for the explanation! `Thus I would have sworn under torture that there is no new method for Module, because there is no need. But there is one, which returns an anonymous module.` That was funny haha. – trueinViso Sep 18 '17 at 15:47