/
namespaces.rb
197 lines (161 loc) · 6.38 KB
/
namespaces.rb
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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
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 <tt>:desc</tt> 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