require 'capistrano/task_definition' module Capistrano class Configuration module Namespaces DEFAULT_TASK = :default def self.included(base) #:nodoc: base.send :alias_method, :initialize_without_namespaces, :initialize base.send :alias_method, :initialize, :initialize_with_namespaces end # The name of this namespace. Defaults to +nil+ for the top-level # namespace. attr_reader :name # The parent namespace of this namespace. Returns +nil+ for the top-level # namespace. attr_reader :parent # The hash of tasks defined for this namespace. attr_reader :tasks # The hash of namespaces defined for this namespace. attr_reader :namespaces def initialize_with_namespaces(*args) #:nodoc: @name = @parent = nil initialize_without_namespaces(*args) @tasks = {} @namespaces = {} end private :initialize_with_namespaces # Returns the top-level namespace (the one with no parent). def top return parent.top if parent return self end # Returns the fully-qualified name of this namespace, or nil if the # namespace is at the top-level. def fully_qualified_name return nil if name.nil? [parent.fully_qualified_name, name].compact.join(":") end # Describe the next task to be defined. The given text will be attached to # the next task that is defined and used as its description. def desc(text) @next_description = text end # Returns the value set by the last, pending "desc" call. If +reset+ is # not false, the value will be reset immediately afterwards. def next_description(reset=false) @next_description ensure @next_description = nil if reset end # Open a namespace in which to define new tasks. If the namespace was # defined previously, it will be reopened, otherwise a new namespace # will be created for the given name. def namespace(name, &block) name = name.to_sym raise ArgumentError, "expected a block" unless block_given? namespace_already_defined = namespaces.key?(name) if all_methods.any? { |m| m.to_sym == name } && !namespace_already_defined thing = tasks.key?(name) ? "task" : "method" raise ArgumentError, "defining a namespace named `#{name}' would shadow an existing #{thing} with that name" end namespaces[name] ||= Namespace.new(name, self) namespaces[name].instance_eval(&block) # make sure any open description gets terminated namespaces[name].desc(nil) if !namespace_already_defined metaclass = class << self; self; end metaclass.send(:define_method, name) { namespaces[name] } end end # Describe a new task. If a description is active (see #desc), it is added # to the options under the :desc key. The new task is added to # the namespace. def task(name, options={}, &block) name = name.to_sym raise ArgumentError, "expected a block" unless block_given? task_already_defined = tasks.key?(name) if all_methods.any? { |m| m.to_sym == name } && !task_already_defined thing = namespaces.key?(name) ? "namespace" : "method" raise ArgumentError, "defining a task named `#{name}' would shadow an existing #{thing} with that name" end tasks[name] = TaskDefinition.new(name, self, {:desc => next_description(:reset)}.merge(options), &block) if !task_already_defined metaclass = class << self; self; end metaclass.send(:define_method, name) { execute_task(tasks[name]) } end end # Find the task with the given name, where name is the fully-qualified # name of the task. This will search into the namespaces and return # the referenced task, or nil if no such task can be found. If the name # refers to a namespace, the task in that namespace named "default" # will be returned instead, if one exists. def find_task(name) parts = name.to_s.split(/:/) tail = parts.pop.to_sym ns = self until parts.empty? next_part = parts.shift ns = next_part.empty? ? nil : ns.namespaces[next_part.to_sym] return nil if ns.nil? end if ns.namespaces.key?(tail) ns = ns.namespaces[tail] tail = DEFAULT_TASK end ns.tasks[tail] end # Given a task name, this will search the current namespace, and all # parent namespaces, looking for a task that matches the name, exactly. # It returns the task, if found, or nil, if not. def search_task(name) name = name.to_sym ns = self until ns.nil? return ns.tasks[name] if ns.tasks.key?(name) ns = ns.parent end return nil end # Returns the default task for this namespace. This will be +nil+ if # the namespace is at the top-level, and will otherwise return the # task named "default". If no such task exists, +nil+ will be returned. def default_task return nil if parent.nil? return tasks[DEFAULT_TASK] end # Returns the tasks in this namespace as an array of TaskDefinition # objects. If a non-false parameter is given, all tasks in all # namespaces under this namespace will be returned as well. def task_list(all=false) list = tasks.values namespaces.each { |name,space| list.concat(space.task_list(:all)) } if all list end private def all_methods public_methods.concat(protected_methods).concat(private_methods) end class Namespace def initialize(name, parent) @parent = parent @name = name end def role(*args) raise NotImplementedError, "roles cannot be defined in a namespace" end def respond_to?(sym) super || parent.respond_to?(sym) end def method_missing(sym, *args, &block) if parent.respond_to?(sym) parent.send(sym, *args, &block) else super end end include Capistrano::Configuration::Namespaces undef :desc, :next_description end end end end