detroit 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.ruby +36 -38
- data/COPYING.rdoc +15 -11
- data/HISTORY.rdoc +20 -0
- data/QED.rdoc +87 -0
- data/README.rdoc +5 -5
- data/lib/detroit.rb +6 -9
- data/lib/detroit.yml +36 -38
- data/lib/detroit/application.rb +120 -89
- data/lib/detroit/assembly.rb +194 -55
- data/lib/detroit/assembly_system.rb +80 -0
- data/lib/detroit/config.rb +35 -29
- data/lib/detroit/control.rb +17 -12
- data/lib/detroit/core_ext.rb +0 -2
- data/lib/detroit/custom.rb +21 -6
- data/lib/detroit/service.rb +14 -7
- data/lib/detroit/standard_assembly.rb +5 -6
- data/lib/detroit/tool.rb +51 -36
- data/lib/detroit/tool/email_utils.rb +13 -11
- data/lib/detroit/tool/shell_utils.rb +88 -47
- data/qed/{01_schedule → 01_assembly}/02_initialize.md +0 -0
- metadata +62 -63
- data/lib/detroit/schedule.rb +0 -187
data/lib/detroit/assembly.rb
CHANGED
@@ -1,80 +1,219 @@
|
|
1
1
|
module Detroit
|
2
2
|
|
3
|
-
#
|
4
|
-
#
|
5
|
-
|
6
|
-
# in defining custom lines.
|
7
|
-
MAINTENANCE_TRACK = [:reset, :clean, :purge]
|
8
|
-
|
9
|
-
# Returns Hash of name and Circuit instance pairs.
|
10
|
-
def self.assemblies
|
11
|
-
@assemblies ||= {}
|
12
|
-
end
|
3
|
+
# Assembly encapsulates a `Assembly` file and it's service instance
|
4
|
+
# configurations.
|
5
|
+
class Assembly
|
13
6
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
7
|
+
# Load Scedule file.
|
8
|
+
def self.load(input)
|
9
|
+
new(input)
|
10
|
+
end
|
18
11
|
|
19
|
-
|
20
|
-
|
21
|
-
class Assembly
|
12
|
+
# Hash table of services.
|
13
|
+
attr :services
|
22
14
|
|
23
|
-
|
24
|
-
attr :name
|
15
|
+
private
|
25
16
|
|
26
|
-
#
|
27
|
-
|
17
|
+
# Initialize new Assembly instance.
|
18
|
+
def initialize(file, options={})
|
19
|
+
@project = options[:project]
|
28
20
|
|
29
|
-
|
30
|
-
alias_method :tracks, :lines
|
21
|
+
@services = {}
|
31
22
|
|
32
|
-
|
33
|
-
def initialize(name, &block)
|
34
|
-
@name = name.to_sym
|
35
|
-
@lines = {:maintenance => MAINTENANCE_TRACK}
|
36
|
-
instance_eval(&block) if block
|
37
|
-
end
|
23
|
+
@file = (String === file ? File.new(file) : file)
|
38
24
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
25
|
+
case File.extname(@file.path)
|
26
|
+
when '.rb'
|
27
|
+
instance_eval(@file.read, @file.path)
|
28
|
+
when '.yml', '.yaml'
|
29
|
+
@services = YAML.load(erb(@file.read))
|
43
30
|
else
|
44
|
-
|
31
|
+
text = @file.read
|
32
|
+
if /^---/ =~ text
|
33
|
+
@services = YAML.load(erb(text))
|
34
|
+
else
|
35
|
+
instance_eval(text, @file.path)
|
36
|
+
end
|
45
37
|
end
|
46
38
|
end
|
47
39
|
|
48
|
-
#
|
49
|
-
|
40
|
+
# Define a service.
|
41
|
+
def service(name, settings={}, &block)
|
42
|
+
if block
|
43
|
+
block_context = BlockContext.new(&block)
|
44
|
+
settings.update(block_context.settings)
|
45
|
+
end
|
46
|
+
@services[name.to_s] = settings.rekey(&:to_s)
|
47
|
+
end
|
50
48
|
|
51
|
-
|
52
|
-
|
53
|
-
#
|
49
|
+
alias_method :tool, :service
|
50
|
+
|
51
|
+
#
|
54
52
|
#
|
55
|
-
#
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
53
|
+
#
|
54
|
+
#
|
55
|
+
def custom(name, &block)
|
56
|
+
context = CustomContext.new(&block)
|
57
|
+
settings = context.settings
|
58
|
+
@services[name.to_s] = settings.rekey(&:to_s)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Access to project data.
|
62
|
+
#
|
63
|
+
# NOTE: Thinking that the project should be relative
|
64
|
+
# to the Routine file itself, unless a `project` is passed
|
65
|
+
# in manually through the initializer. In the mean time,
|
66
|
+
# the project is just relative to the current working directory.
|
67
|
+
#
|
68
|
+
# TODO: Make configurable and use .ruby by default ?
|
69
|
+
def project
|
70
|
+
@project ||= POM::Project.find #(file_directory)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Capitalized service names called as methods
|
74
|
+
# can also define a service.
|
75
|
+
def method_missing(sym, *args, &block)
|
76
|
+
service_class = sym.to_s
|
77
|
+
case service_class
|
78
|
+
when /^[A-Z]/
|
79
|
+
if Hash === args.last
|
80
|
+
args.last[:service] = service_class
|
62
81
|
else
|
63
|
-
|
64
|
-
raise "Unknown track `#{name}'." unless track
|
65
|
-
unless track.include?(stop)
|
66
|
-
raise "Unknown stop `#{stop}` for track `#{name}'."
|
67
|
-
end
|
82
|
+
args << {:service=>service_class}
|
68
83
|
end
|
84
|
+
case args.first
|
85
|
+
when String, Symbol
|
86
|
+
name = args.first
|
87
|
+
else
|
88
|
+
name = service_class.to_s.downcase
|
89
|
+
end
|
90
|
+
service(name, *args, &block)
|
69
91
|
else
|
70
|
-
|
92
|
+
super(sym, *args, &block)
|
71
93
|
end
|
72
|
-
track
|
73
94
|
end
|
74
95
|
|
75
|
-
|
76
|
-
|
96
|
+
private
|
97
|
+
|
98
|
+
# Process Routine document via ERB.
|
99
|
+
def erb(text)
|
100
|
+
context = ERBContext.new(project)
|
101
|
+
ERB.new(text).result(context.__binding__)
|
102
|
+
end
|
103
|
+
|
104
|
+
# ERBContext provides the clean context to process a Routine
|
105
|
+
# as an ERB template.
|
106
|
+
class ERBContext
|
107
|
+
#
|
108
|
+
def initialize(project)
|
109
|
+
@project = project
|
110
|
+
end
|
111
|
+
|
112
|
+
# Access to a clean binding.
|
113
|
+
def __binding__
|
114
|
+
binding
|
115
|
+
end
|
116
|
+
|
117
|
+
# Provide access to project data.
|
118
|
+
def project
|
119
|
+
@project
|
120
|
+
end
|
121
|
+
|
122
|
+
#
|
123
|
+
def method_missing(name, *args)
|
124
|
+
if project.respond_to?(name)
|
125
|
+
project.__send__(name, *args)
|
126
|
+
elsif project.metadata.respond_to?(name)
|
127
|
+
project.metadata.__send__(name, *args)
|
128
|
+
else
|
129
|
+
super(name, *args)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
#
|
135
|
+
class BlockContext
|
136
|
+
#
|
137
|
+
attr :settings
|
138
|
+
|
139
|
+
#
|
140
|
+
def initialize(&b)
|
141
|
+
@settings = {}
|
142
|
+
b.arity == 1 ? b.call(self) : instance_eval(&b)
|
143
|
+
end
|
144
|
+
|
145
|
+
#
|
146
|
+
def set(name, value=nil, &block)
|
147
|
+
if block
|
148
|
+
block_context = BlockContext.new
|
149
|
+
block.call(block_context)
|
150
|
+
@settings[name.to_s] = block_context.settings
|
151
|
+
else
|
152
|
+
@settings[name.to_s] = value
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
#
|
157
|
+
def method_missing(symbol, value=nil, *args)
|
158
|
+
case name = symbol.to_s
|
159
|
+
when /=$/
|
160
|
+
@settings[name.chomp('=')] = value
|
161
|
+
else
|
162
|
+
super(symbol, value=nil, *args)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
#
|
168
|
+
class CustomContext
|
169
|
+
#
|
170
|
+
attr :settings
|
171
|
+
#
|
172
|
+
def initialize(&b)
|
173
|
+
@settings = {}
|
174
|
+
b.arity == 0 ? instance_eval(&b) : b.call(self)
|
175
|
+
end
|
176
|
+
#
|
177
|
+
def method_missing(s,a=nil,*x,&b)
|
178
|
+
case s.to_s
|
179
|
+
when /=$/
|
180
|
+
@settings[s.to_s.chomp('=').to_sym] = b ? b : a
|
181
|
+
else
|
182
|
+
return @settings[s] unless a
|
183
|
+
@settings[s] = b ? b : a
|
184
|
+
end
|
185
|
+
end
|
186
|
+
def respond_to?(s)
|
187
|
+
@settings.key?(s.to_sym)
|
188
|
+
end
|
189
|
+
end
|
77
190
|
|
78
191
|
end
|
79
192
|
|
193
|
+
# NOTE: This is problematic, because a Scheudle file should really know from
|
194
|
+
# what file it was derived.
|
195
|
+
|
196
|
+
#
|
197
|
+
DOMAIN = "rubyworks.github.com/detroit,2011-05-27"
|
198
|
+
|
199
|
+
# TODO: If using Psych rather than Syck, then define a domain type.
|
200
|
+
|
201
|
+
#if defined?(Psych) #RUBY_VERSION >= '1.9'
|
202
|
+
# YAML::add_domain_type(DOMAIN, "assembly") do |type, hash|
|
203
|
+
# Assembly.load(hash)
|
204
|
+
# end
|
205
|
+
#else
|
206
|
+
YAML::add_builtin_type("assembly") do |type, value|
|
207
|
+
value
|
208
|
+
#case value
|
209
|
+
#when String
|
210
|
+
# Assembly.eval(value)
|
211
|
+
#when Hash
|
212
|
+
# Assembly.new(value)
|
213
|
+
#else
|
214
|
+
# raise "ERROR: Invalid Assembly"
|
215
|
+
#end
|
216
|
+
end
|
217
|
+
#end
|
218
|
+
|
80
219
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Detroit
|
2
|
+
|
3
|
+
# All assemblies and tracks have a maintenance sub-track.
|
4
|
+
# For this reason stop names `reset`, `clean` and `purge`
|
5
|
+
# are reserved names and MUST not be used as stop names
|
6
|
+
# in defining custom lines.
|
7
|
+
MAINTENANCE_TRACK = [:reset, :clean, :purge]
|
8
|
+
|
9
|
+
# Returns Hash of name and Circuit instance pairs.
|
10
|
+
def self.assembly_systems
|
11
|
+
@assembly_system ||= {}
|
12
|
+
end
|
13
|
+
|
14
|
+
# Define a new assembly system.
|
15
|
+
def self.assembly_system(name, &block)
|
16
|
+
assembly_systems[name.to_sym] = AssemblySystem.new(name, &block)
|
17
|
+
end
|
18
|
+
|
19
|
+
# The AssemblySystem class encapsulates a set of interrelated
|
20
|
+
# assembly lines, or _tracks_.
|
21
|
+
class AssemblySystem
|
22
|
+
|
23
|
+
# Name of the assembly system.
|
24
|
+
attr :name
|
25
|
+
|
26
|
+
# Returns a Hash of track names mapped to list of stops.
|
27
|
+
attr :lines
|
28
|
+
|
29
|
+
# Lines are also called `tracks`.
|
30
|
+
alias_method :tracks, :lines
|
31
|
+
|
32
|
+
# Create a new instance.
|
33
|
+
def initialize(name, &block)
|
34
|
+
@name = name.to_sym
|
35
|
+
@lines = {:maintenance => MAINTENANCE_TRACK}
|
36
|
+
instance_eval(&block) if block
|
37
|
+
end
|
38
|
+
|
39
|
+
# Define an assembly line.
|
40
|
+
def line(name, *stops)
|
41
|
+
if stops.empty?
|
42
|
+
@lines[name.to_sym]
|
43
|
+
else
|
44
|
+
@lines[name.to_sym] = stops.map{ |s| s.to_sym }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Lines are also called tracks.
|
49
|
+
alias_method :track, :line
|
50
|
+
|
51
|
+
# Lookup track by name and (optional) stop. If the stop belongs
|
52
|
+
# to the maintenance sub-track then the maintenance sub-track will
|
53
|
+
# be returned instead of the track itself.
|
54
|
+
#
|
55
|
+
# The Application class uses this to simplify track lookup.
|
56
|
+
def get_track(name, stop=nil)
|
57
|
+
name = name.to_sym
|
58
|
+
if stop
|
59
|
+
stop = stop.to_sym
|
60
|
+
if MAINTENANCE_TRACK.include?(stop.to_sym)
|
61
|
+
track = MAINTENANCE_TRACK
|
62
|
+
else
|
63
|
+
track = tracks[name]
|
64
|
+
raise "Unknown track `#{name}'." unless track
|
65
|
+
unless track.include?(stop)
|
66
|
+
raise "Unknown stop `#{stop}` for track `#{name}'."
|
67
|
+
end
|
68
|
+
end
|
69
|
+
else
|
70
|
+
track = tracks[name]
|
71
|
+
end
|
72
|
+
track
|
73
|
+
end
|
74
|
+
|
75
|
+
# Did I mention that `line` and `track` are synonyms?
|
76
|
+
alias_method :get_line, :get_track
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
data/lib/detroit/config.rb
CHANGED
@@ -8,18 +8,18 @@ module Detroit
|
|
8
8
|
# Configuration directory name (most likely a hidden "dot" directory).
|
9
9
|
DIRECTORY = "detroit"
|
10
10
|
|
11
|
-
# File identifier used to find a project's
|
12
|
-
FILE_EXTENSION = "
|
11
|
+
# File identifier used to find a project's Assembly(s).
|
12
|
+
FILE_EXTENSION = "assembly"
|
13
13
|
|
14
14
|
# Current POM::Project object.
|
15
15
|
#attr :project
|
16
16
|
|
17
|
-
# The list of a project's
|
17
|
+
# The list of a project's assembly files.
|
18
18
|
#
|
19
19
|
# @return [Array<String>] routine files
|
20
|
-
attr :
|
20
|
+
attr :assemblies
|
21
21
|
|
22
|
-
# Service configurations from
|
22
|
+
# Service configurations from Assembly or *.assembly files.
|
23
23
|
#
|
24
24
|
# @return [Hash] service settings
|
25
25
|
attr :services
|
@@ -32,22 +32,22 @@ module Detroit
|
|
32
32
|
attr :defaults
|
33
33
|
|
34
34
|
#
|
35
|
-
def initialize(
|
36
|
-
if
|
37
|
-
@
|
35
|
+
def initialize(assembly_files=nil)
|
36
|
+
if assembly_files && !assembly_files.empty?
|
37
|
+
@assembly_filenames = assembly_files
|
38
38
|
else
|
39
|
-
@
|
39
|
+
@assembly_filenames = nil
|
40
40
|
end
|
41
41
|
|
42
|
-
@
|
43
|
-
@services
|
44
|
-
@defaults
|
42
|
+
@assemblies = {}
|
43
|
+
@services = {}
|
44
|
+
@defaults = {}
|
45
45
|
|
46
46
|
@loaded_plugins = {}
|
47
47
|
|
48
48
|
load_plugins
|
49
49
|
load_defaults
|
50
|
-
|
50
|
+
load_assemblies
|
51
51
|
end
|
52
52
|
|
53
53
|
#--
|
@@ -60,8 +60,14 @@ module Detroit
|
|
60
60
|
# Load a plugin.
|
61
61
|
def load_plugin(name)
|
62
62
|
@loaded_plugins[name] ||= (
|
63
|
-
|
64
|
-
|
63
|
+
begin
|
64
|
+
require "detroit-#{name}"
|
65
|
+
rescue LoadError => e
|
66
|
+
$stderr.puts "ERROR: #{e.message.capitalize}"
|
67
|
+
$stderr.puts " Perhaps `gem install detroit-#{name}`?"
|
68
|
+
exit -1
|
69
|
+
end
|
70
|
+
name # true ?
|
65
71
|
)
|
66
72
|
end
|
67
73
|
|
@@ -84,16 +90,16 @@ module Detroit
|
|
84
90
|
end
|
85
91
|
|
86
92
|
#
|
87
|
-
def
|
88
|
-
|
89
|
-
|
93
|
+
def load_assemblies
|
94
|
+
assembly_filenames.each do |file|
|
95
|
+
load_assembly_file(file)
|
90
96
|
end
|
91
97
|
end
|
92
98
|
|
93
99
|
#
|
94
|
-
def
|
95
|
-
@
|
96
|
-
@services.merge!(
|
100
|
+
def load_assembly_file(file)
|
101
|
+
@assemblies[file] = Assembly.load(File.new(file))
|
102
|
+
@services.merge!(assemblies[file].services)
|
97
103
|
end
|
98
104
|
|
99
105
|
# Set defaults.
|
@@ -101,21 +107,21 @@ module Detroit
|
|
101
107
|
@defaults = hash.to_h
|
102
108
|
end
|
103
109
|
|
104
|
-
# If a `
|
105
|
-
# all `*.
|
110
|
+
# If a `Assembly` or `.assembly` file exists, then it is returned. Otherwise
|
111
|
+
# all `*.assembly` files are loaded. To load `*.assembly` files from another
|
106
112
|
# directory add the directory to config options file.
|
107
|
-
def
|
108
|
-
@
|
113
|
+
def assembly_filenames
|
114
|
+
@assembly_filenames ||= (
|
109
115
|
files = []
|
110
|
-
## match '
|
116
|
+
## match 'Assembly' or '.assembly' file
|
111
117
|
files = project.root.glob("{,.,*.}#{FILE_EXTENSION}{,.rb,.yml,.yaml}", :casefold)
|
112
118
|
## only files
|
113
119
|
files = files.select{ |f| File.file?(f) }
|
114
120
|
##
|
115
121
|
if files.empty?
|
116
|
-
## match '.detroit/*.
|
122
|
+
## match '.detroit/*.assembly' or 'detroit/*.assembly'
|
117
123
|
files += project.root.glob("{,.}#{DIRECTORY}/*.#{FILE_EXTENSION}", :casefold)
|
118
|
-
## match 'task/*.
|
124
|
+
## match 'task/*.assembly' (OLD SCHOOL)
|
119
125
|
files += project.root.glob("{task,tasks}/*.#{FILE_EXTENSION}", :casefold)
|
120
126
|
## only files
|
121
127
|
files = files.select{ |f| File.file?(f) }
|
@@ -140,7 +146,7 @@ module Detroit
|
|
140
146
|
def load_detroit_file(file)
|
141
147
|
#@dir = File.dirname(file)
|
142
148
|
|
143
|
-
|
149
|
+
assemblies[file] =
|
144
150
|
|
145
151
|
# TODO: can we just read the first line of the file and go from there?
|
146
152
|
#text = File.read(file).strip
|