fire 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.
@@ -0,0 +1,77 @@
1
+ module Fire
2
+
3
+ # TODO: Borrow code from Detroit for ShellUtils and beef her up!
4
+
5
+ # File system utility methods.
6
+ #
7
+ module ShellUtils
8
+ # Shell out via system call.
9
+ #
10
+ # Arguments
11
+ # args - Argument vector. [Array]
12
+ #
13
+ # Returns success of shell invocation.
14
+ def sh(*args)
15
+ puts args.join(' ')
16
+ system(*args)
17
+ end
18
+
19
+ def directory?(path)
20
+ File.directory?(path)
21
+ end
22
+
23
+ #
24
+ # Synchronize a destination directory with a source directory.
25
+ #
26
+ # TODO: Augment FileUtils instead.
27
+ # TODO: Not every action needs to be verbose.
28
+ #
29
+ def sync(src, dst, options={})
30
+ src_files = Dir[File.join(src, '**', '*')].map{ |f| f.sub(src+'/', '') }
31
+ dst_files = Dir[File.join(dst, '**', '*')].map{ |f| f.sub(dst+'/', '') }
32
+
33
+ removal = dst_files - src_files
34
+
35
+ rm_dirs, rm_files = [], []
36
+ removal.each do |f|
37
+ path = File.join(dst, f)
38
+ if File.directory?(path)
39
+ rm_dirs << path
40
+ else
41
+ rm_files << path
42
+ end
43
+ end
44
+
45
+ rm_files.each { |f| rm(f) }
46
+ rm_dirs.each { |d| rmdir(d) }
47
+
48
+ src_files.each do |f|
49
+ src_path = File.join(src, f)
50
+ dst_path = File.join(dst, f)
51
+ if File.directory?(src_path)
52
+ mkdir_p(dst_path)
53
+ else
54
+ parent = File.dirname(dst_path)
55
+ mkdir_p(parent) unless File.directory?(parent)
56
+ install(src_path, dst_path)
57
+ end
58
+ end
59
+ end
60
+
61
+ #
62
+ # If FileUtils responds to a missing method, then call it.
63
+ #
64
+ def method_missing(s, *a, &b)
65
+ if FileUtils.respond_to?(s)
66
+ if $DRYRUN
67
+ FileUtils::DryRun.__send__(s, *a, &b)
68
+ else
69
+ FileUtils::Verbose.__send__(s, *a, &b)
70
+ end
71
+ else
72
+ super(s, *a, &b)
73
+ end
74
+ end
75
+ end
76
+
77
+ end
@@ -0,0 +1,91 @@
1
+ module Fire
2
+ require 'fire/match'
3
+
4
+ # Fire's logic system is a *set logic* system. That means an empty set, `[]`
5
+ # is treated as `false` and a non-empty set is `true`.
6
+ #
7
+ # Fire handles complex logic by building-up lazy logic constructs. It's logical
8
+ # operators are defined using single charcter symbols, e.g. `&` and `|`.
9
+ #
10
+ class State
11
+ def initialize(&procedure)
12
+ @procedure = procedure
13
+ end
14
+
15
+ def call
16
+ set @procedure.call
17
+ end
18
+
19
+ # set or
20
+ def |(other)
21
+ State.new{ set(self.call) | set(other.call) }
22
+ end
23
+
24
+ # set and
25
+ def &(other)
26
+ State.new{ set(self.call) & set(other.call) }
27
+ end
28
+
29
+ private
30
+
31
+ #
32
+ def set(value)
33
+ case value
34
+ when Array
35
+ value.compact
36
+ when Boolean
37
+ value ? true : []
38
+ else
39
+ [value]
40
+ end
41
+ end
42
+
43
+ end
44
+
45
+ # File state.
46
+ #
47
+ class FileState < State
48
+ # Initialize new instance of Autologic.
49
+ #
50
+ # pattern - File glob or regular expression. [String,Regexp]
51
+ # digest -
52
+ # ignore -
53
+ #
54
+ def initialize(pattern, digest, ignore)
55
+ @pattern = pattern
56
+ @digest = digest
57
+ @ignore = ignore
58
+ end
59
+
60
+ # File glob or regular expression.
61
+ attr :pattern
62
+
63
+ # TODO: it would be nice if we could pass the regexp match too the procedure too
64
+
65
+ # Process logic.
66
+ def call
67
+ result = []
68
+ case pattern
69
+ when Regexp
70
+ @digest.current.keys.each do |fname|
71
+ if md = pattern.match(fname)
72
+ if @digest.current[fname] != @digest.saved[fname]
73
+ result << Match.new(fname, md)
74
+ end
75
+ end
76
+ end
77
+ else
78
+ # TODO: if fnmatch? worked like glob then we'd follow the same code as for regexp
79
+ list = Dir[pattern].reject{ |path| @ignore.any?{ |ig| /^#{ig}/ =~ path } }
80
+ list.each do |fname|
81
+ if @digest.current[fname] != @digest.saved[fname]
82
+ result << fname
83
+ end
84
+ end
85
+ end
86
+ result
87
+ end
88
+
89
+ end
90
+
91
+ end
@@ -0,0 +1,244 @@
1
+ require 'ostruct'
2
+ require 'notify'
3
+
4
+ require 'fire/shellutils'
5
+ require 'fire/rule'
6
+ require 'fire/state'
7
+ require 'fire/task'
8
+ require 'fire/digest'
9
+ #require 'fire/rulefile'
10
+
11
+ module Fire
12
+
13
+ #
14
+ # Master system instance.
15
+ #
16
+ def self.system
17
+ @system ||= System.new
18
+ end
19
+
20
+ # System stores states and rules.
21
+ class System < Module
22
+
23
+ # TODO: there are some namespace issues to deal with here.
24
+ # we don't necessarily want a rule block to be able to call #rule.
25
+
26
+ # Instantiate new system.
27
+ #
28
+ def initialize(options={})
29
+ extend self
30
+ extend ShellUtils
31
+
32
+ @ignore = Array(options[:ignore] || [])
33
+ @files = Array(options[:files] || [])
34
+
35
+ @rules = []
36
+ @states = {}
37
+ @tasks = {}
38
+
39
+ @digest = Digest.new
40
+ @session = OpenStruct.new
41
+
42
+ @files.each do |file|
43
+ module_eval(File.read(file), file)
44
+ end
45
+ end
46
+
47
+ # Current session.
48
+ attr :session
49
+
50
+ # Array of defined states.
51
+ attr :states
52
+
53
+ # Array of defined rules.
54
+ attr :rules
55
+
56
+ # Mapping of defined tasks.
57
+ attr :tasks
58
+
59
+ # File digest.
60
+ attr :digest
61
+
62
+ # Import from another file, or glob of files, relative to project root.
63
+ #
64
+ # @todo Should importing be relative the importing file?
65
+ # @return nothing
66
+ def import(*globs)
67
+ globs.each do |glob|
68
+ #if File.relative?(glob)
69
+ # dir = Dir.pwd #session.root #File.dirname(caller[0])
70
+ # glob = File.join(dir, glob)
71
+ #end
72
+ Dir[glob].each do |file|
73
+ next unless File.file?(file)
74
+ #instance_eval(File.read(file), file)
75
+ module_eval(File.read(file), file)
76
+ end
77
+ end
78
+ end
79
+
80
+ # Add paths to be ignored in file rules.
81
+ def ignore(*globs)
82
+ @ignore.concat(globs.flatten)
83
+ @ignore
84
+ end
85
+
86
+ # Define a named state. States define conditions that are used to trigger
87
+ # rules. Named states are kept in a hash table to ensure that only one state
88
+ # is ever defined for a given name. Calling state again with the same name
89
+ # as a previously defined state will redefine the condition of that state.
90
+ #
91
+ # @example
92
+ # state :no_rdocs? do
93
+ # files = Dir.glob('lib/**/*.rb')
94
+ # FileUtils.uptodate?('doc', files) ? files : false
95
+ # end
96
+ #
97
+ # Returns nil if state name is given. [nil]
98
+ # Returns State in no name is given. [State]
99
+ def state(name=nil, &condition)
100
+ if name
101
+ if condition
102
+ @states[name.to_sym] = condition
103
+ define_method(name) do |*args|
104
+ state = @states[name.to_sym]
105
+ State.new{ states[name.to_sym].call(*args) }
106
+ end
107
+ else
108
+ raise ArgumentError
109
+ end
110
+ else
111
+ State.new{ condition.call(*args) }
112
+ end
113
+ end
114
+
115
+ # Define a file state.
116
+ #
117
+ # Returns [FileState]
118
+ def file(pattern)
119
+ FileState.new(pattern, digest, ignore)
120
+ end
121
+
122
+ # Define an environment state.
123
+ #
124
+ # Examples
125
+ # env('PATH'=>/foo/)
126
+ #
127
+ # Returns [State]
128
+ def env(name_to_pattern)
129
+ State.new do
130
+ name_to_pattern.any? do |name, re|
131
+ re === ENV[name.to_s] # or `all?` instead?
132
+ end
133
+ end
134
+ end
135
+
136
+ # Define a rule. Rules are procedures that are tiggered
137
+ # by logical states.
138
+ #
139
+ # Examples
140
+ # rule no_rdocs do |files|
141
+ # sh "rdoc --output doc/rdoc " + files.join(" ")
142
+ # end
143
+ #
144
+ def rule(state, &procedure)
145
+ state, todo = parse_arrow(state)
146
+
147
+ case state
148
+ when String, Regexp
149
+ file_rule(state, :todo=>todo, &procedure)
150
+ when Symbol
151
+ # TODO: Is this really the best idea?
152
+ #@states[state.to_sym]
153
+ else
154
+ @rules << Rule.new(state, :todo=>todo, &procedure)
155
+ end
156
+ end
157
+
158
+ #
159
+ # Check a name state.
160
+ #
161
+ def state?(name, *args)
162
+ @states[name.to_sym].call(*args)
163
+ end
164
+
165
+ #
166
+ # Run a task.
167
+ #
168
+ def run(task_name) #, *args)
169
+ tasks[task_name.to_sym].invoke #call(*args)
170
+ end
171
+
172
+ # Set task description. The next task defined will get the most
173
+ # recently defined description attached to it.
174
+ def desc(description)
175
+ @_desc = description
176
+ end
177
+
178
+ # Define a command line task. A task is special type of rule that
179
+ # is triggered when the command line tool is invoked with
180
+ # the name of the task.
181
+ #
182
+ # Tasks are an isolated set of rules and suppress the activation of
183
+ # all other rules not specifically given as prerequisites.
184
+ #
185
+ # task :rdoc do
186
+ # trip no_rdocs
187
+ # end
188
+ #
189
+ # Returns [Task]
190
+ def task(name_and_state, &procedure)
191
+ name, todo = parse_arrow(name_and_state)
192
+ task = Task.new(name, :todo=>todo, :desc=>@_desc, &procedure)
193
+ @tasks[name.to_sym] = task
194
+ @_desc = nil
195
+ task
196
+ end
197
+
198
+ #
199
+ # Issue notification.
200
+ #
201
+ def notify(message, options={})
202
+ title = options.delete(:title) || 'Fire Notification'
203
+ Notify.notify(title, message.to_s, options)
204
+ end
205
+
206
+ private
207
+
208
+ # Split a hash argument into it's key and value pair.
209
+ # The hash is expected to have only one entry. If the argument
210
+ # is not a hash then returns the argument and an empty array.
211
+ #
212
+ # Raises an [ArgumetError] if the hash has more than one entry.
213
+ #
214
+ # Returns key and value. [Array]
215
+ def parse_arrow(argument)
216
+ case argument
217
+ when Hash
218
+ raise ArgumentError if argument.size > 1
219
+ head, tail = *argument.to_a.first
220
+ return head, Array(tail)
221
+ else
222
+ return argument, []
223
+ end
224
+ end
225
+
226
+ # TODO: pass `self` to FileState instead of digest and igonre ?
227
+
228
+ # Define a file rule. A file rule is a rule with a specific state
229
+ # based on changes in files.
230
+ #
231
+ # @example
232
+ # file_rule 'test/**/case_*.rb' do |files|
233
+ # sh "ruby-test " + files.join(" ")
234
+ # end
235
+ #
236
+ # Returns nothing.
237
+ def file_rule(pattern, options={}, &procedure)
238
+ state = FileState.new(pattern, digest, ignore)
239
+ @rules << Rule.new(state, options, &procedure)
240
+ end
241
+
242
+ end
243
+
244
+ end
@@ -0,0 +1,71 @@
1
+ module Fire
2
+
3
+ # The Task class encapsulates command-line dependent rules.
4
+ #
5
+ class Task
6
+ #
7
+ def initialize(name, options={}, &procedure)
8
+ @name = name
9
+ @description = options[:desc]
10
+ @requisite = options[:todo] || []
11
+ @procedure = procedure
12
+
13
+ #@_reducing = nil
14
+ end
15
+
16
+ # The tasks name.
17
+ attr :name
18
+
19
+ # Task description. This is need for a task to
20
+ # available via the command line.
21
+ attr :description
22
+
23
+ #
24
+ attr :requisite
25
+
26
+ #
27
+ alias :todo :requisite
28
+
29
+ # Run the task.
30
+ def invoke(&prepare)
31
+ prepare.call
32
+ call
33
+ end
34
+
35
+ # Alias for #invoke.
36
+ alias :apply :invoke
37
+
38
+ #def to_s
39
+ # @description.to_s
40
+ #end
41
+
42
+ protected
43
+
44
+ #
45
+ def call
46
+ @procedure.call
47
+ end
48
+
49
+ =begin
50
+ # Reduce task list.
51
+ #
52
+ # Returns [Array<Task>]
53
+ def reduce
54
+ return [] if @_reducing
55
+ list = []
56
+ begin
57
+ @_reducing = true
58
+ @requisite.each do |r|
59
+ list << @system.tasks[r.to_sym].reduce
60
+ end
61
+ list << self
62
+ ensure
63
+ @_reducing = false
64
+ end
65
+ list.flatten.uniq
66
+ end
67
+ =end
68
+
69
+ end
70
+
71
+ end