detroit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|