Dragonfly is an awesome image/asset management tool. It is great because of many reasons, but what I love the most, is how it handles thumbnails. You don't need to specify sizes, or anything else in the models in which you use it. Instead every type of thumbnail can be generated "on the fly", the first time you access it. For that reason, the original image (by default) is always stored. Unfortunately by default, it will not be auto-oriented based on the picture Exif data. Luckily there's an easy way out. In your Dragonfly model you need to include an after_assign block with an auto orient command and you are ready to go!
class Picture < ActiveRecord::Base
extend Dragonfly::Model
dragonfly_accessor :image do
after_assign do |attachment|
# Auto orient all the images - so they will look as they should
attachment.convert! '-auto-orient'
end
end
end
After that all of your images (and all the thumbnails - since you change the original file) will be auto-oriented.
It's not really often that you don't know what will be in a block. Especially when you plan to nest blocks multiple times. However things like this happen...
Typical nested blocks
Here's a "typical" example of nested blocks:
class TimeBenchmarkWrapper
def monitor
time = Time.now
yield
ms = (Time.now - time) * 1000
print "#{TimeBenchmarkWrapper}: Time taken: #{ms}ms\n"
end
end
class LoggerWrapper
def monitor
print "#{LoggerWrapper}: logging something\n"
yield
end
end
class AroundFilterWrapper
def monitor
return unless before_action
yield
after_action
end
def before_action
print "#{AroundFilterWrapper} before logic\n"
end
def after_action
print "#{AroundFilterWrapper} after logic\n"
end
end
class ExampleClass
def execute
# Here should the code go
print "#{ExampleClass} executing...\n"
end
end
time = TimeBenchmarkWrapper.new
logger = LoggerWrapper.new
around = AroundFilterWrapper.new
logic = ExampleClass.new
around.monitor do
logger.monitor do
time.monitor do
logic.execute
end
end
end
The code above is really simple. It just wraps around a business logic with some monitors (logger, benchmark, around filter). But...
Reordering, rearranging, dynamic number of nested blocks
Everything is awesome until we decide to to have a dynamic order and amount of nested blocks that perform some sort of logic around our business logic.
Let's say that I would like to do something like this:
wrappers = [ClosestWrapper, MiddleWrapper, OuterWrapper]
wrapped_block = wrap_with(wrappers) do
# The "proper" business logic
logic.execute
end
In the wrappers array we would like to have all the wrappers that we want to use in a given order. And then the magical method wrap_with should somehow nest all the blocks. So how can we achieve such a behaviour?
Multiple blocks injecting
The solution to this problem is quite simple: defining blocks that accept block parameter and them injecting one into another. If you don't understand the code, please read to comments in the code - they explain all the details:
def wrap_with(wrappers, &block)
# We define the most bottom block - that should evaluate the real code
# All the blocks are in array because we will use inject to inject one into another
# and since we will be injecting first into second, second into third, etc
# our "base" proc needs to be first (it will be the most inner block)
blocks = [-> { block.call } ]
# Each wrapper needs to be wrapped with a proc that accepts a inner block containing
# stuff that should be inside - this inside stuff is a proc as well and
# will be executed. That's why when we execute the most outer block if will execute
# the inner one (and it will happen as a cascade)
blocks += wrappers.map do |wrapper|
proc do |inner_block = nil|
wrapper.new.monitor do
inner_block.call
end
end
end
# In general it is equal to a code like this (but generated dynamically):
# We assume that we have following monitors: [Monitor1, Monitor2, Monitor3]
# Monitor3.new.monitor do
# Monitor2.new.monitor do
# Monitor1.new.monitor do
# proxy.call(*args, &block)
# end
# end
# end
blocks.inject do |inner, resource|
proc { resource.call(inner) }
end.call
end
# Example usage - now we can change order and amount of wrappers
# and everything will still work
wrappers = [TimeBenchmarkWrapper, LoggerWrapper, AroundFilterWrapper]
wrap_with(wrappers) do
ExampleClass.new.execute
end
If you still don't get it, here's an illustration of how the blocks are injected:
Performance impact
Below you can see, that from 1 to 100 nested blocks, "automated" nested blocks perform around 2.5-3 times slower than a standard Ruby code. The more nestings you have the slower it gets. The generated nestings are slower mostly because they use more resources for managing (storing, handling) more blocks.
Conclusion
If the performance is not your primary goal and you prefer visibility and flexibility over it, then the auto-generated block approach is definitely better. However it is still worth keeping in mind, that even with normal blocks, the more of them we have the slower it gets.