aboutsummaryrefslogtreecommitdiffstats
path: root/railties/lib/vendor/thor-0.11.6/lib/thor/invocation.rb
blob: 32e6a72454314da6df09c0721709e4ad64360dce (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
class Thor
  module Invocation
    def self.included(base) #:nodoc:
      base.extend ClassMethods
    end

    module ClassMethods
      # Prepare for class methods invocations. This method must return a klass to
      # have the invoked class options showed in help messages in generators.
      #
      def prepare_for_invocation(key, name) #:nodoc:
        case name
          when Symbol, String
            Thor::Util.namespace_to_thor_class_and_task(name.to_s, false)
          else
            name
        end
      end
    end

    # Make initializer aware of invocations and the initializer proc.
    #
    def initialize(args=[], options={}, config={}, &block) #:nodoc:
      @_invocations = config[:invocations] || Hash.new { |h,k| h[k] = [] }
      @_initializer = [ args, options, config ]
      super
    end

    # Receives a name and invokes it. The name can be a string (either "task" or
    # "namespace:task"), a Thor::Task, a Class or a Thor instance. If the task
    # cannot be guessed by name, it can also be supplied as second argument.
    #
    # You can also supply the arguments, options and configuration values for
    # the task to be invoked, if none is given, the same values used to
    # initialize the invoker are used to initialize the invoked.
    #
    # ==== Examples
    #
    #   class A < Thor
    #     def foo
    #       invoke :bar
    #       invoke "b:hello", ["José"]
    #     end
    #
    #     def bar
    #       invoke "b:hello", ["José"]
    #     end
    #   end
    #
    #   class B < Thor
    #     def hello(name)
    #       puts "hello #{name}"
    #     end
    #   end
    #
    # You can notice that the method "foo" above invokes two tasks: "bar",
    # which belongs to the same class and "hello" which belongs to the class B.
    #
    # By using an invocation system you ensure that a task is invoked only once.
    # In the example above, invoking "foo" will invoke "b:hello" just once, even
    # if it's invoked later by "bar" method.
    #
    # When class A invokes class B, all arguments used on A initialization are
    # supplied to B. This allows lazy parse of options. Let's suppose you have
    # some rspec tasks:
    #
    #   class Rspec < Thor::Group
    #     class_option :mock_framework, :type => :string, :default => :rr
    #
    #     def invoke_mock_framework
    #       invoke "rspec:#{options[:mock_framework]}"
    #     end
    #   end
    #
    # As you noticed, it invokes the given mock framework, which might have its
    # own options:
    #
    #   class Rspec::RR < Thor::Group
    #     class_option :style, :type => :string, :default => :mock
    #   end
    #
    # Since it's not rspec concern to parse mock framework options, when RR
    # is invoked all options are parsed again, so RR can extract only the options
    # that it's going to use.
    #
    # If you want Rspec::RR to be initialized with its own set of options, you
    # have to do that explicitely:
    #
    #   invoke "rspec:rr", [], :style => :foo
    #
    # Besides giving an instance, you can also give a class to invoke:
    #
    #   invoke Rspec::RR, [], :style => :foo
    #
    def invoke(name=nil, task=nil, args=nil, opts=nil, config=nil)
      task, args, opts, config = nil, task, args, opts if task.nil? || task.is_a?(Array)
      args, opts, config = nil, args, opts if args.is_a?(Hash)

      object, task    = _prepare_for_invocation(name, task)
      klass, instance = _initialize_klass_with_initializer(object, args, opts, config)

      method_args = []
      current = @_invocations[klass]

      iterator = proc do |_, task|
        unless current.include?(task.name)
          current << task.name
          task.run(instance, method_args)
        end
      end

      if task
        args ||= []
        method_args = args[Range.new(klass.arguments.size, -1)] || []
        iterator.call(nil, task)
      else
        klass.all_tasks.map(&iterator)
      end
    end

    protected

      # Configuration values that are shared between invocations.
      #
      def _shared_configuration #:nodoc:
        { :invocations => @_invocations }
      end

      # Prepare for invocation in the instance level. In this case, we have to
      # take into account that a just a task name from the current class was
      # given or even a Thor::Task object.
      #
      def _prepare_for_invocation(name, sent_task=nil) #:nodoc:
        if name.is_a?(Thor::Task)
          task = name
        elsif task = self.class.all_tasks[name.to_s]
          object = self
        else
          object, task = self.class.prepare_for_invocation(nil, name)
          task ||= sent_task
        end

        # If the object was not set, use self and use the name as task.
        object, task = self, name unless object
        return object, _validate_task(object, task)
      end

      # Check if the object given is a Thor class object and get a task object
      # for it.
      #
      def _validate_task(object, task) #:nodoc:
        klass = object.is_a?(Class) ? object : object.class
        raise "Expected Thor class, got #{klass}" unless klass <= Thor::Base

        task ||= klass.default_task if klass <= Thor
        task = klass.all_tasks[task.to_s] || Thor::Task::Dynamic.new(task) if task && !task.is_a?(Thor::Task)
        task
      end

      # Initialize klass using values stored in the @_initializer.
      #
      def _initialize_klass_with_initializer(object, args, opts, config) #:nodoc:
        if object.is_a?(Class)
          klass = object

          stored_args, stored_opts, stored_config = @_initializer
          args ||= stored_args.dup
          opts ||= stored_opts.dup

          config ||= {}
          config = stored_config.merge(_shared_configuration).merge!(config)
          [ klass, klass.new(args, opts, config) ]
        else
          [ object.class, object ]
        end
      end
  end
end