Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to compose Thor tasks in separate classes/modules/files?

Tags:

module

ruby

thor

I'm having some trouble getting Thor to do this, so hopefully someone can point out what I'm doing wrong.

I have a main class class MyApp < Thor that I want to break out into separate files for multiple namespaces, like thor create:app_type and thor update:app_type. I can't find any examples that show how one should break apart a Thor app into pieces, and what I've tried doesn't seem to work.

Take for instance, this class I'm trying to break out from the main Thor class:

module Things
  module Grouping

    desc "something", "Do something cool in this group"
    def something
      ....
    end
  end
end

When I try to include or require this in my main class:

class App < Thor
  ....
  require 'grouping_file'
  include Things::Grouping
  ....
end

I get an exception: '<module:Grouping>': undefined method 'desc' for Things::Grouping:Module (NoMethodError)

Is it possible to have multiple namespaces for Thor tasks, and if so, how does one break it out so that you don't have one monolithic class that takes several hundred lines?

like image 504
Nick Klauer Avatar asked Apr 20 '11 10:04

Nick Klauer


3 Answers

Why doesn't it work: when you use desc inside a Thor class, you are actually calling a class method Thor.desc. When you do that in the module, it calls YourModule.desc which obviously doesn't exist.

There are two ways I can suggest to fix this.

Fix one: using Module.included

Did you want to have those tasks reused in multiple Thor classes?

When a module is used as an include in Ruby, the included class method is called. http://www.ruby-doc.org/core/classes/Module.html#M000458

module MyModule
  def self.included(thor)
    thor.class_eval do

      desc "Something", "Something cool"
      def something
        # ...
      end

    end
  end
end

Fix two: separating your Thor classes into multiple files

Did you merely want to separately define tasks in another file?

If so, just reopen your App class in another file. Your Thorfile would look something like:

# Thorfile
Dir['./lib/thor/**/*.rb'].sort.each { |f| load f }

Then your lib/thor/app.rb would contain some tasks for App, while another file lib/thor/app-grouping.rb would contain some more tasks for the same App class.

like image 52
Rico Sta. Cruz Avatar answered Oct 18 '22 12:10

Rico Sta. Cruz


Use an over-arching module, let's say Foo, inside of which you will define all sub-modules and sub-classes.

Start the definition of this module in a single foo.thor file, which is in the directory from which you will run all Thor tasks. At the top of the Foo module in this foo.thor, define this method:

# Load all our thor files
module Foo
  def self.load_thorfiles(dir)
    Dir.chdir(dir) do
      thor_files = Dir.glob('**/*.thor').delete_if { |x| not File.file?(x) }
      thor_files.each do |f|
        Thor::Util.load_thorfile(f)
      end
    end
  end
end

Then at the bottom of your main foo.thor file, add:

Foo.load_thorfiles('directory_a')
Foo.load_thorfiles('directory_b')

That will recursively include all the *.thor files in those directories. Nest modules within your main Foo module to namespace your tasks. Doesn't matter where the files live or what they're called at that point, as long as you include all your Thor-related directories via the method described above.

like image 14
semperos Avatar answered Oct 18 '22 11:10

semperos


I had this same problem, and had almost given up but then I had an idea:

If you write your tasks into Thorfiles rather than as ruby classes, then you can simply require in Ruby files that contain Thor subclasses and they will appear in the list of available tasks when you run thor -T.

This is all managed by the Thor::Runner class. If you look through this you'll see a #thorfiles method which is responsible for looking for files named Thorfile under the current working directory.

All I had to do to a) break my Thor tasks into multiple files whilst b) not having to have a single Thorfile was to create a local subclass of Thor::Runner, overwrite its #thorfile method with one that returned my app specific list of Thor task files and then call its #start method and it all worked:

class MyApp::Runner < ::Thor::Runner
  private
  def thorfiles(*args)
    Dir['thortasks/**/*.rb']
  end
end

MyApp::Runner.start

So I can have any number of Ruby classes defining Thor tasks under thortasks e.g.

class MyApp::MyThorNamespace < ::Thor
  namespace :mynamespace

  # Unless you include the namespace in the task name the -T task list
  # will list everything under the top-level namespace
  # (which I think is a bug in Thor)
  desc "#{namespace}:task", "Does something"
  def task
    # do something
  end
end

I'd almost given up on Thor until I figured this out but there aren't many libraries that deal with creating generators as well as building namespaced tasks, so I'm glad I found a solution.

like image 5
magnetised Avatar answered Oct 18 '22 10:10

magnetised