rulebow 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,118 @@
1
+ module Rulebow
2
+
3
+ ##
4
+ # Rulebow'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
+ # Rulebow 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 Fact
11
+ #
12
+ def initialize(&procedure)
13
+ @procedure = procedure
14
+ end
15
+
16
+ #
17
+ def call(digest)
18
+ set @procedure.call(digest)
19
+ end
20
+
21
+ # set or
22
+ def |(other)
23
+ Fact.new{ |d| set(self.call(d)) | set(other.call(d)) }
24
+ end
25
+
26
+ # set and
27
+ def &(other)
28
+ Fact.new{ |d| set(self.call(d)) & set(other.call(d)) }
29
+ end
30
+
31
+ private
32
+
33
+ #
34
+ def set(value)
35
+ case value
36
+ when Array
37
+ value.compact
38
+ when Boolean
39
+ value ? true : []
40
+ else
41
+ [value]
42
+ end
43
+ end
44
+
45
+ end
46
+
47
+ ##
48
+ # This subclass of Fact is specialized for file change conditions.
49
+ #
50
+ class FileFact < Fact
51
+ # Initialize new instance of FileFact.
52
+ #
53
+ # pattern - File glob or regular expression. [String,Regexp]
54
+ # digest - The system digest. [Digest]
55
+ #
56
+ def initialize(pattern, &coerce)
57
+ @pattern = pattern
58
+ @coerce = coerce
59
+ end
60
+
61
+ # File glob or regular expression.
62
+ attr :pattern
63
+
64
+ #
65
+ attr :coerce
66
+
67
+ # Process logic.
68
+ def call(digest)
69
+ result = []
70
+
71
+ case pattern
72
+ when Regexp
73
+ list = Dir.glob('**/*', File::FNM_PATHNAME)
74
+ list = digest.filter(list) # excludes ignored files (odd way to do it but if should work)
75
+ list.each do |fname|
76
+ if md = pattern.match(fname)
77
+ if digest.current[fname] != digest.saved[fname]
78
+ result << Match.new(fname, md)
79
+ end
80
+ end
81
+ end
82
+ # NOTE: The problem with using the digest list, is that if a rule
83
+ # adds a new file to the project, then a subsequent rule needs
84
+ # to be able to see it.
85
+ #@digest.current.keys.each do |fname|
86
+ # if md = pattern.match(fname)
87
+ # if @digest.current[fname] != @digest.saved[fname]
88
+ # result << Match.new(fname, md)
89
+ # end
90
+ # end
91
+ #end
92
+ else
93
+ list = Dir.glob(pattern, File::FNM_PATHNAME)
94
+ list = digest.filter(list) # excludes ignored files (odd way to do it but if should work)
95
+ list.each do |fname|
96
+ if digest.current[fname] != digest.saved[fname]
97
+ result << fname
98
+ end
99
+ end
100
+ #@digest.current.keys.each do |fname|
101
+ # if md = File.fnmatch?(pattern, fname, File::FNM_PATHNAME | File::FNM_EXTGLOB)
102
+ # if @digest.current[fname] != @digest.saved[fname]
103
+ # result << Match.new(fname, md)
104
+ # end
105
+ # end
106
+ #end
107
+ end
108
+
109
+ if coerce
110
+ return [coerce.call(result)].flatten.compact
111
+ else
112
+ return result
113
+ end
114
+ end
115
+
116
+ end
117
+
118
+ end
@@ -0,0 +1,136 @@
1
+ module Rulebow
2
+
3
+ ##
4
+ # Deprecated: Encapsulates list of file globs to be ignored.
5
+ #
6
+ class Ignore
7
+ include Enumerable
8
+
9
+ # Initialize new instance of Ignore.
10
+ #
11
+ # Returns nothing.
12
+ def initialize(ignore)
13
+ @ignore = ignore.to_a
14
+ end
15
+
16
+ # Filter a list of files in accordance with the
17
+ # ignore list.
18
+ #
19
+ # files - The list of files. [Array<String>]
20
+ #
21
+ # Returns [Array<String>]
22
+ def filter(files)
23
+ list = []
24
+ files.each do |file|
25
+ hit = any? do |pattern|
26
+ match?(pattern, file)
27
+ end
28
+ list << file unless hit
29
+ end
30
+ list
31
+ end
32
+
33
+ # Ignore file.
34
+ #def file
35
+ # @file ||= (
36
+ # Dir["{.gitignore,.hgignore}"].first
37
+ # )
38
+ #end
39
+
40
+ #
41
+ def each
42
+ to_a.each{ |g| yield g }
43
+ end
44
+
45
+ #
46
+ def size
47
+ to_a.size
48
+ end
49
+
50
+ #
51
+ def to_a
52
+ @ignore #||= load_ignore
53
+ end
54
+
55
+ #
56
+ def replace(*globs)
57
+ @ignore = globs.flatten
58
+ end
59
+
60
+ #
61
+ def concat(*globs)
62
+ @ignore.concat(globs.flatten)
63
+ end
64
+
65
+ #private
66
+
67
+ #def all_ignored_files
68
+ # list = []
69
+ # ignore.each do |glob|
70
+ # if glob.start_with?('/')
71
+ # list.concat Dir[File.join(@root, glob)]
72
+ # else
73
+ # list.concat Dir[File.join(@root, '**', glob)]
74
+ # end
75
+ # end
76
+ # list
77
+ #end
78
+
79
+ =begin
80
+ # Load ignore file. Removes blank lines and line starting with `#`.
81
+ #
82
+ # Returns [Array<String>]
83
+ def load_ignore
84
+ f = file
85
+ i = []
86
+ if f && File.exist?(f)
87
+ File.read(f).lines.each do |line|
88
+ glob = line.strip
89
+ next if glob.empty?
90
+ next if glob.start_with?('#')
91
+ i << glob
92
+ end
93
+ end
94
+ i
95
+ end
96
+ =end
97
+
98
+ # Given a pattern and a file, does the file match the
99
+ # pattern? This code is based on the rules used by
100
+ # git's .gitignore file.
101
+ #
102
+ # TODO: The code is probably not quite right.
103
+ #
104
+ # Returns [Boolean]
105
+ def match?(pattern, file)
106
+ if pattern.start_with?('!')
107
+ return !match?(pattern.sub('!','').strip)
108
+ end
109
+
110
+ dir = pattern.end_with?('/')
111
+ pattern = pattern.chomp('/') if dir
112
+
113
+ if pattern.start_with?('/')
114
+ fnmatch?(pattern.sub('/',''), file)
115
+ else
116
+ if dir
117
+ fnmatch?(File.join(pattern, '**', '*'), file) ||
118
+ fnmatch?(pattern, file) && File.directory?(file)
119
+ elsif pattern.include?('/')
120
+ fnmatch?(pattern, file)
121
+ else
122
+ fnmatch?(File.join('**',pattern), file)
123
+ end
124
+ end
125
+ end
126
+
127
+ # Shortcut to `File.fnmatch?` method.
128
+ #
129
+ # Returns [Boolean]
130
+ def fnmatch?(pattern, file, mode=File::FNM_PATHNAME)
131
+ File.fnmatch?(pattern, file, File::FNM_PATHNAME)
132
+ end
133
+
134
+ end
135
+
136
+ end
@@ -0,0 +1,26 @@
1
+ module Rulebow
2
+
3
+ # Match is a subclass of a string that also stores the
4
+ # MatchData then matched against it in a Regexp comparison.
5
+ #
6
+ class Match < String
7
+ # Initialize a new instance of Match.
8
+ #
9
+ # string - The string. [String]
10
+ # matchdata - The match data. [MatchData]
11
+ #
12
+ def initialize(string, matchdata)
13
+ replace(string)
14
+ @matchdata = matchdata
15
+ end
16
+
17
+ # The match data that resulted from
18
+ # a successful Regexp against the string.
19
+ #
20
+ # Returns [MatchData]
21
+ def matchdata
22
+ @matchdata
23
+ end
24
+ end
25
+
26
+ end
@@ -0,0 +1,63 @@
1
+ module Rulebow
2
+
3
+ # Rule class encapsulates a *rule* definition.
4
+ #
5
+ class Rule
6
+ # Initialize new instanance of Rule.
7
+ #
8
+ # fact - Conditional fact. [Fact,Boolean]
9
+ # action - Procedure to run if logic condition is met. [Proc]
10
+ #
11
+ def initialize(fact, options={}, &action)
12
+ @fact = fact
13
+ @action = action
14
+ end
15
+
16
+ # Access to the rule's logic condition. [Fact]
17
+ attr :fact
18
+
19
+ # Access to the rule's action procedure.
20
+ #
21
+ # Returns [Proc]
22
+ def to_proc
23
+ @action
24
+ end
25
+
26
+ # Apply rule, running the rule's procedure if the fact is true.
27
+ #
28
+ # Returns nothing.
29
+ def apply(digest)
30
+ case fact
31
+ when false, nil
32
+ when true
33
+ call()
34
+ else
35
+ result_set = fact.call(digest)
36
+ if result_set && !result_set.empty?
37
+ call(result_set)
38
+ end
39
+ end
40
+ end
41
+
42
+ # Alias for #apply.
43
+ alias :invoke :apply
44
+
45
+ protected
46
+
47
+ # Run rule procedure.
48
+ #
49
+ # result_set - The result set returned by the logic condition.
50
+ #
51
+ # Returns whatever the procedure returns. [Object]
52
+ def call(*result_set)
53
+ if @action.arity == 0
54
+ @action.call
55
+ else
56
+ #@action.call(session, *result_set)
57
+ @action.call(*result_set)
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ end
@@ -0,0 +1,308 @@
1
+ module Rulebow
2
+
3
+ ##
4
+ # Rulesets provides namespace isolation for facts, rules and methods.
5
+ #
6
+ class Ruleset < Module
7
+
8
+ # Instantiate new ruleset.
9
+ #
10
+ # Arguments
11
+ # system - The system to which this ruleset belongs. [System]
12
+ # name - Name of the ruleset.
13
+ #
14
+ # Yields the script defining the ruleset rules.
15
+ def initialize(system, name, &block)
16
+ extend ShellUtils
17
+ extend system
18
+ extend self
19
+
20
+ @scripts = []
21
+ @rules = []
22
+ @docs = []
23
+ @requires = []
24
+
25
+ @name, @chain = parse_ruleset_name(name)
26
+
27
+ @session = system.session
28
+
29
+ @watchlist = WatchList.new(:ignore=>system.ignore)
30
+
31
+ module_eval(&block) if block
32
+ end
33
+
34
+ # Ruleset name
35
+ attr :name
36
+
37
+ # Description of ruleset.
38
+ attr :docs
39
+
40
+ # Chain or dependencies.
41
+ attr :chain
42
+
43
+ # Session object can be used to passing information around between rulesets.
44
+ attr :session
45
+
46
+ # Rule scripts.
47
+ attr :scripts
48
+
49
+ # Array of defined facts.
50
+ #attr :facts
51
+
52
+ # Array of defined rules.
53
+ attr :rules
54
+
55
+ # Files to watch for this ruleset.
56
+ attr :watchlist
57
+
58
+ # Import from another file, or glob of files, relative to project root.
59
+ #
60
+ # TODO: Should importing be relative to the importing file? Currently
61
+ # it is relative to the project root.
62
+ #
63
+ # Returns nothing.
64
+ def import(*globs)
65
+ globs.each do |glob|
66
+ #if File.relative?(glob)
67
+ # dir = Dir.pwd #session.root #File.dirname(caller[0])
68
+ # glob = File.join(dir, glob)
69
+ #end
70
+ Dir[glob].each do |file|
71
+ next unless File.file?(file) # add warning
72
+ next if @scripts.include?(file)
73
+ @scripts << file
74
+ module_eval(File.read(file), file)
75
+ end
76
+ end
77
+ end
78
+
79
+ # Add paths to be watched.
80
+ #
81
+ # globs - List of file globs. [Array<String>]
82
+ #
83
+ # Returns [Array<String>]
84
+ def watch(*globs)
85
+ @watchlist.accept(globs) unless globs.empty?
86
+ @watchlist
87
+ end
88
+
89
+ # Replace paths to be watched.
90
+ #
91
+ # globs - List of file globs. [Array<String>]
92
+ #
93
+ # Returns [Array<String>]
94
+ def watch!(*globs)
95
+ @watchlist.accept!(globs)
96
+ end
97
+
98
+ # Add paths to be ignored in file rules.
99
+ #
100
+ # globs - List of file globs. [Array<String>]
101
+ #
102
+ # Returns [Array<String>]
103
+ def ignore(*globs)
104
+ @watchlist.ignore(globs) unless globs.empty?
105
+ end
106
+
107
+ # Replace globs in ignore list.
108
+ #
109
+ # globs - List of file globs. [Array<String>]
110
+ #
111
+ # Returns [Array<String>]
112
+ def ignore!(*globs)
113
+ @watchlist.ignore!(globs)
114
+ end
115
+
116
+ # Define a dependency chain.
117
+ #
118
+ # Returns [Array<Symbol>]
119
+ def chain=(*rulesets)
120
+ @chain = rulesets.map{ |b| b.to_sym }
121
+ end
122
+
123
+ # Provide a ruleset description.
124
+ #
125
+ # Returns [String]
126
+ def desc(description)
127
+ @docs << description
128
+ end
129
+
130
+ # Define a rule. Rules are procedures that are tiggered
131
+ # by logical facts.
132
+ #
133
+ # Examples
134
+ # rule :rdocs? do |files|
135
+ # sh "rdoc --output doc/rdoc " + files.join(" ")
136
+ # end
137
+ #
138
+ # TODO: Allow for an expression array that conjoins them with AND logic.
139
+ #
140
+ # Returns [Rule]
141
+ def rule(expression, &block)
142
+ case expression
143
+ when Hash
144
+ expression.each do |fact, task|
145
+ fact = define_fact(fact)
146
+ task = define_task(task)
147
+ @rules << Rule.new(fact, &task)
148
+ end
149
+ else
150
+ fact = define_fact(expression)
151
+ @rules << Rule.new(fact, &block)
152
+ end
153
+
154
+ #rule = Rule.new(@_facts, get_rule_options, &procedure)
155
+ #@rules << rule
156
+ #clear_rule_options
157
+
158
+ return @rules
159
+ end
160
+
161
+ # Defines a fact. Facts define conditions that are used to trigger
162
+ # rules. Named facts are defined as methods to ensure that only one fact
163
+ # is ever defined for a given name. Calling fact again with the same name
164
+ # as a previously defined fact will redefine the condition of that fact.
165
+ #
166
+ # Examples
167
+ # fact :no_doc? do
168
+ # File.directory?('doc')
169
+ # end
170
+ #
171
+ # Returns the name if name and condition is given. [Symbol]
172
+ # Returns a fact in only name or condition is given. [Fact]
173
+ def fact(name=nil, &condition)
174
+ if name && conditon
175
+ define_method(name) do
176
+ Fact.new(&condition) # TODO: maybe we don't really need the cache after all
177
+ end
178
+ name
179
+ else
180
+ define_fact(name || condition)
181
+ end
182
+ end
183
+
184
+ # Will probably be deprecated.
185
+ alias :state :fact
186
+
187
+ # Define a file fact.
188
+ #
189
+ # TODO: Probably will add this back & limit `fact` so it can't be used for file facts.
190
+ #
191
+ # Returns [FileFact]
192
+ #def file(patterns, &coerce)
193
+ # FileFact.new(patterns, &coerce)
194
+ #end
195
+
196
+ # Convenince method for defining environment variable facts.
197
+ #
198
+ # Examples
199
+ # rule env('PATH'=>/foo/) => :dosomething
200
+ #
201
+ # Returns [Fact]
202
+ def env(name_to_pattern)
203
+ Fact.new do
204
+ name_to_pattern.any? do |name, re|
205
+ re === ENV[name.to_s] # or `all?` instead?
206
+ end
207
+ end
208
+ end
209
+
210
+ # TODO: Private rulesets that can't be run from the CLI?
211
+ # Hmmm... maybe instead it can work like rake, if docs is empty
212
+ # it can't be run from the command line.
213
+ #
214
+ #def privatize
215
+ # @privatized = true
216
+ #end
217
+
218
+ # Issue notification.
219
+ #
220
+ # Returns nothing.
221
+ def notify(message, options={})
222
+ title = options.delete(:title) || 'Rulebow Notification'
223
+ Notify.notify(title, message.to_s, options)
224
+ end
225
+
226
+ # Any requires made within a ruleset will not be actually
227
+ # required until a rule is run.
228
+ #
229
+ # TODO: This feature has yet to be implemented.
230
+ #
231
+ # Returns nothing.
232
+ def require(feature=nil)
233
+ @requires << feature if feature
234
+ @requires
235
+ end
236
+
237
+ # Better inspection string.
238
+ def inspect
239
+ if chain.empty?
240
+ "#<Ruleset #{name}>"
241
+ else
242
+ "#<Ruleset #{name} " + chain.join(' ') + ">"
243
+ end
244
+ end
245
+
246
+ # TODO: Good idea?
247
+ def to_s
248
+ name.to_s
249
+ end
250
+
251
+ private
252
+
253
+ # Define a fact.
254
+ #
255
+ # Returns [Fact]
256
+ def define_fact(fact)
257
+ case fact
258
+ when Fact
259
+ fact
260
+ when String, Regexp
261
+ @watchlist.accept(fact)
262
+ FileFact.new(fact)
263
+ when Symbol
264
+ Fact.new{ send(fact) }
265
+ when true, false, nil
266
+ Fact.new{ fact }
267
+ else #when Proc
268
+ Fact.new(&fact)
269
+ end
270
+ end
271
+
272
+ #
273
+ def define_task(task)
274
+ case task
275
+ when Symbol
276
+ Proc.new do |*a|
277
+ meth = method(task)
278
+ if meth.arity == 0
279
+ meth.call
280
+ else
281
+ meth.call(*a)
282
+ end
283
+ end
284
+ else
285
+ task.to_proc
286
+ end
287
+ end
288
+
289
+ # Parse out a ruleset's name from it's ruleset dependencies.
290
+ #
291
+ # name - ruleset name
292
+ #
293
+ # Returns [Array]
294
+ def parse_ruleset_name(name)
295
+ if Hash === name
296
+ raise ArgumentError if name.size > 1
297
+ list = [name.values].flatten.map{ |b| b.to_sym }
298
+ name = name.keys.first
299
+ else
300
+ list = []
301
+ end
302
+ return name.to_sym, list
303
+ end
304
+ end
305
+
306
+ end
307
+
308
+