rulebow 0.4.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,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
+