Skip to content

RubyTapas

Josh Schairbaum edited this page Nov 18, 2013 · 18 revisions

148 Rake Invoke

You can call a Rake application from an external ruby application.

require 'rake'
Rake.application.init
Rake.application.load_rakefile # looks in cwd
Rake::Task["hello"].invoke # will not run again if ctime hasn't changed.

# To run a task a 2nd time
Rake::Task["hello"].invoke
Rake::Task["hello"].reenable
Rake::Task["hello"].invoke

# To load a Rakefile from somewhere else
Rake.load_rakefile("./Rakefile")
Rake::Task["ns:hello"].invoke

135 Rake Multitask

Rake has a simple mechanism for running items in parallel that is dead simple. Change task to multitask. rake -j allows you to set the number of threads you want to use. rake -m allows you to run items as multitask without changing the tasks.

require 'rake/clean'

task :default => :highlights

LISTINGS   = FileList["listings/*"]
HIGHLIGHTS = LISTINGS.ext(".html")
CLEAN.include(HIGHLIGHTS)

multitask :highlights => HIGHLIGHTS

rule ".html" => ->(f){ FileList[f.ext(".*")].first } do |t|
  sh "pygmentize -o ${t.name} #{t.source}"
end

For example I think you could do this to start up processes.

134 Rake Clean

Rake has a library to specifically clean generated files and directories.

require 'rake/clean'

CLEAN # FileList of generated files
CLEAN.include(SOURCE_FILES.ext(".html"))

CLOBBER # FileList of final product files
# CLOBBER also runs CLEAN
CLOBBER.include("book.mobi")

133 Rake File Operations

directory task in rake will ensure a directory exists before running a task. Add the directory task as a dependency for another task. You can also ensure subdirectories are created by using mkdir_p.

directory "output"

rule ".html" => [".md", "output"] do |t|
  mkdir_p t.name.pathmap("%d")
  "md_to_html #{t.name} #{t.source}"
end

Cleaning up after a run is also important. This task will remove the directory before creating it on the next run.

task :clean do 
  rm_rf "output"
end

Ruby::FileUtils RDoc

132 Rake Pathmap

Using pathmap, we can modify lists of paths to new paths. For example, we could change the load path for ruby.

load_paths = FileList["mylibs", "yourlibs", "ourlibs"]
ruby_args  = load_paths.pathmap("-I%p")
# => ["-Imylibs", "-Iyourlibs", "-Iourlibs"]

Assume all source files are under a src directory and we want to generate the files to the output directory.

source_files = Rake::fileList.new("sources/**/*.md")
output_files = source_files.pathmap("%{^src/,output/}X.html")
# ensure all the directories are created
sh "mkdir -p #{f.pathmap('%d')}"

Rake#pathmap docs

131 Rake Rules

The Rake rule for building files is that if the target file is newer than all it's dependencies, it is not built.

Invoke rake -P will cause Rake to spit out it's list of prerequisites. You can get further diagnostic information by turning on Rake's trace-rules option. You can also set it via rake --rules.

Rake.application.options.trace_rules = true

In order to prevent ourselves from having to redefine rules for each type of mapping, we can make a general rule.

SOURCE_FILES = Rake::FileList.new("**/*.md", "**/.markdown") do |files|
  files.exclude("~*")
  files.exclude(/^tmp\//)
  files.exclude do |f|
    `git ls-files #{f}`.empty?
  end
end

rule ".html" => ->(f) { source_for_html(f) } do |t|
  sh "md_to_html #{t.name} #{t.source}"
end

def source_for_html(html_file)
  SOURCE_FILES.detect { |f| f.ext == html_file.ext }
end

Jim Weirich mentioned in the comments that he would normally just loop over the rule generation, which is something I think I prefer.

%w(.md .markdown).each do |ext|
  rule ".html" => ext do |t|
    "md_to_html #{t.name} #{t.source}"
  end
end

130 Rake File Lists

File Lists are flexible because you can pass patterns of files to include and files to exclude.

markdown_files = Rake::FileList.new("**/*.md", "**/.markdown") do |files|
  files.exclude("~*")
  files.exclude(/^tmp\//)
  files.exclude do |f|
    `git ls-files #{f}`.empty?
  end
end

This can also be invoke using the shorthand method Rake::FileList["*.md"]. If you want to change the extension of the file list, use the #ext method.

markdown_files.ext(".html")

To add this to the previous example:

source_files = Rake::FileList.new("**/*.md", "**/*.markdown") do |files|
  files.exclude("~*")
  files.exclude(/^tmp\//)
  files.exclude do |f|
    `git ls-files #{f}`.empty
  end
end

task :html => source_files.ext(".html")

# Also duplicate the "html" => "md" rule for "html" => "markdown"

129 Rake

Rake is super useful for declaring file dependencies. In the example below, the file method declares that the html_file is dependent upon the md_file. Provide a block that acts as "make" instructions.

%w(file1.md file2.md).each do |md_file|
  html_file = File.basename(md_file, ".md") + ".html"
  file html_file => md_file do
      sh "md_to_html #{md_file} #{html_file}
  end
end

This can now be invoked via rake file1.html. If the modification time hasn't changed, no action is taken. We can take this a step further by making a default task.

task :default => :html
task :html => %w(file1.html file2.html)

We can remove a lot of duplication by teaching rake how to generate the HTML file.

rule ".html" => ".md" do |t|
  sh "md_to_html #{t.name} #{t.source}"
end

There is some filename matching logic that makes Rake intelligent enough to know if it's given an HTML file it should look for the corresponding Markdown file.

122 Testing Blocks With RSpec

There are some special matchers for yielding blocks in RSpec. The most simple:

class Operation
  def perform(device, &block)
    yield
  end
end

it "yields control to a block" do 
  expect { |probe| 
    operation.perform(device, &probe)
  }.to yield_control
end

You can specify what is yielded by using #yield_with_args. Additionally, if you want to ensure that multiple items are yielded you can use #yield_successive_args.

121 Testing Blocks

Given the implementation for Episode 38.

it "yields an error" do 
  yielded = :unyielded
  do_request(method, uri, nil) do |error|
    yielded = error
  end
  expect(yielded).to be_a(CustomError)
end

This also works if it yields multiple items

let(:my_hash) { { foo: :bar } }
it "yields the key and value" do 
  yielded = []
  my_hash.each do |key, value|
    yielded << [key, value]
  end
  expect(yielded).to include([:foo, :bar])
end

106 Class Accessors

class Operation
  class << self
    attr_accessor :logger
  end
end

Operation.logger = Logger.new(STDOUT)

or

class Operation
  def self.logger
    @logger
  end

  def self.logger=(logger)
    @logger = logger
  end
end

Operation.logger = Logger.new(STDOUT)

038 Caller-Specified Callback

A technique for allowing the caller to specify behavior within a method.

DEFAULT_STRATEGY = ->(error) { raise }

def do_request(method, uri, data = nil, &strategy)
  strategy ||= DEFAULT_STRATEGY
  response = connection.send(method, uri, data)
rescue => error
  strategy.call(error)
end

do_request(:get, uri, nil) { "N/A" }

do_request(:get, uri, nil) do |error|
  raise CustomError, error.message
end

This implementation can be found in #fetch.

006 Forwardable

A technique for composing

class Operation
  extend Forwardable
  def_delegators :runner, :run
end

The target for delegation can be anything that could be eval'd such as '@runner.connection'.

class Operation
  extend Forwardable
  # The singular version of #def_delegator sets up an alias for the method.
  def_delegator :runner, :configuration, :config
end

Clone this wiki locally