mynyml-watchr 0.3.0 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,79 @@
1
+ module Watchr
2
+
3
+ # The controller contains the app's core logic.
4
+ #
5
+ # ===== Examples
6
+ #
7
+ # script = Watchr::Script.new(file)
8
+ # contrl = Watchr::Controller.new(script)
9
+ # contrl.run
10
+ #
11
+ # Calling <tt>#run</tt> will enter the listening loop, and from then on every
12
+ # file event will trigger its corresponding action defined in <tt>script</tt>
13
+ #
14
+ # The controller also automatically adds the script's file itself to its list
15
+ # of monitored files and will detect any changes to it, providing on the fly
16
+ # updates of defined rules.
17
+ #
18
+ class Controller
19
+
20
+ # Creates a controller object around given <tt>script</tt>
21
+ #
22
+ # ===== Parameters
23
+ # script<Script>:: The script object
24
+ #
25
+ def initialize(script, handler)
26
+ @script = script
27
+ @handler = handler
28
+ @handler.add_observer(self)
29
+
30
+ Watchr.debug "using %s handler" % handler.class.name
31
+ end
32
+
33
+ # Enters listening loop.
34
+ #
35
+ # Will block control flow until application is explicitly stopped/killed.
36
+ #
37
+ def run
38
+ @handler.listen(monitored_paths)
39
+ end
40
+
41
+ # Callback for file events.
42
+ #
43
+ # Called while control flow in in listening loop. It will execute the
44
+ # file's corresponding action as defined in the script. If the file is the
45
+ # script itself, it will refresh its state to account for potential changes.
46
+ #
47
+ # ===== Parameters
48
+ # path<Pathname, String>:: path that triggered event
49
+ # event<Symbol>:: event type (ignored for now)
50
+ #
51
+ def update(path, event = nil)
52
+ path = Pathname(path).expand_path
53
+
54
+ if path == @script.path
55
+ @script.parse!
56
+ @handler.refresh(monitored_paths)
57
+ else
58
+ @script.action_for(path).call
59
+ end
60
+ end
61
+
62
+ # List of paths the script is monitoring.
63
+ #
64
+ # Basically this means all paths below current directoly recursivelly that
65
+ # match any of the rules' patterns, plus the script file.
66
+ #
67
+ # ===== Returns
68
+ # paths<Array[Pathname]>:: List of monitored paths
69
+ #
70
+ def monitored_paths
71
+ paths = Dir['**/*'].select do |path|
72
+ @script.patterns.any? {|p| path.match(p) }
73
+ end
74
+ paths.push(@script.path).compact!
75
+ paths.map {|path| Pathname(path).expand_path }
76
+ end
77
+ end
78
+ end
79
+
@@ -0,0 +1,48 @@
1
+ require 'observer'
2
+
3
+ module Watchr
4
+ module EventHandler
5
+ class AbstractMethod < Exception; end
6
+
7
+ # Base functionality mixin meant to be included in specific event handlers.
8
+ module Base
9
+ include Observable
10
+
11
+ # Notify that a file was modified.
12
+ #
13
+ # ===== Parameters
14
+ # path<Pathname, String>:: full path or path relative to current working directory
15
+ # event<Symbol>:: event type (not yet used).
16
+ #
17
+ #--
18
+ # #changed and #notify_observers are Observable methods
19
+ def notify(path, event = nil)
20
+ changed(true)
21
+ notify_observers(path, event)
22
+ end
23
+
24
+ # Begin watching given paths and enter listening loop. Called by the controller.
25
+ #
26
+ # Abstract method
27
+ #
28
+ # ===== Parameters
29
+ # monitored_paths<Array(Pathname)>:: list of paths the application is currently monitoring.
30
+ #
31
+ def listen(monitored_paths)
32
+ raise AbstractMethod
33
+ end
34
+
35
+ # Called by the controller when the list of paths monitored by wantchr
36
+ # has changed. It should refresh the list of paths being watched.
37
+ #
38
+ # Abstract method
39
+ #
40
+ # ===== Parameters
41
+ # monitored_paths<Array(Pathname)>:: list of paths the application is currently monitoring.
42
+ #
43
+ def refresh(monitored_paths)
44
+ raise AbstractMethod
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,55 @@
1
+ module Watchr
2
+ module EventHandler
3
+ class Portable
4
+ include Base
5
+
6
+ attr_accessor :monitored_paths
7
+ attr_accessor :reference_mtime
8
+
9
+ def initialize
10
+ @reference_mtime = Time.now
11
+ end
12
+
13
+ # Enters listening loop.
14
+ #
15
+ # Will block control flow until application is explicitly stopped/killed.
16
+ #
17
+ def listen(monitored_paths)
18
+ @monitored_paths = monitored_paths
19
+ loop { trigger; sleep(1) }
20
+ end
21
+
22
+ # See if an event occured, and if so notify observers.
23
+ def trigger #:nodoc:
24
+ path, type = detect_event
25
+ notify(path, type) unless path.nil?
26
+ end
27
+
28
+ # Update list of monitored paths.
29
+ def refresh(monitored_paths)
30
+ @monitored_paths = monitored_paths
31
+ end
32
+
33
+ private
34
+
35
+ # Verify mtimes of monitored files.
36
+ #
37
+ # If the latest mtime is more recent than the reference mtime, return
38
+ # that file's path.
39
+ #
40
+ # ===== Returns
41
+ # path and type of event if event occured, nil otherwise
42
+ #
43
+ def detect_event
44
+ path = @monitored_paths.max {|a,b| a.mtime <=> b.mtime }
45
+
46
+ if path.mtime > @reference_mtime
47
+ @reference_mtime = path.mtime
48
+ [path, :changed]
49
+ else
50
+ nil
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,62 @@
1
+ require 'rev'
2
+
3
+ module Watchr
4
+ module EventHandler
5
+ class Unix
6
+ include Base
7
+
8
+ # Used by Rev. Wraps a monitored path, and Rev::Loop will call its
9
+ # callback on file events.
10
+ class SingleFileWatcher < Rev::StatWatcher #:nodoc:
11
+ class << self
12
+ # Stores a reference back to handler so we can call its #nofity
13
+ # method with file event info
14
+ attr_accessor :handler
15
+ end
16
+
17
+ # Callback. Called on file change event
18
+ # Delegates to Controller#update, passing in path and event type
19
+ def on_change
20
+ self.class.handler.notify(path, :changed)
21
+ end
22
+ end
23
+
24
+ def initialize
25
+ SingleFileWatcher.handler = self
26
+ @loop = Rev::Loop.default
27
+ end
28
+
29
+ # Enters listening loop.
30
+ #
31
+ # Will block control flow until application is explicitly stopped/killed.
32
+ #
33
+ def listen(monitored_paths)
34
+ @monitored_paths = monitored_paths
35
+ attach
36
+ @loop.run
37
+ end
38
+
39
+ # Rebuilds file bindings.
40
+ #
41
+ # will detach all current bindings, and reattach the <tt>monitored_paths</tt>
42
+ #
43
+ def refresh(monitored_paths)
44
+ @monitored_paths = monitored_paths
45
+ detach
46
+ attach
47
+ end
48
+
49
+ private
50
+
51
+ # Binds all <tt>monitored_paths</tt> to the listening loop.
52
+ def attach
53
+ @monitored_paths.each {|path| SingleFileWatcher.new(path.to_s).attach(@loop) }
54
+ end
55
+
56
+ # Unbinds all paths currently attached to listening loop.
57
+ def detach
58
+ @loop.watchers.each {|watcher| watcher.detach }
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,192 @@
1
+ module Watchr
2
+
3
+ # A script object wraps a script file, and is used by a controller.
4
+ #
5
+ # ===== Examples
6
+ #
7
+ # path = Pathname.new('specs.watchr')
8
+ # script = Watchr::Script.new(path)
9
+ #
10
+ class Script
11
+
12
+ # Convenience type. Provides clearer and simpler access to rule properties.
13
+ #
14
+ # ===== Examples
15
+ #
16
+ # rule = script.watch('lib/.*\.rb') { 'ohaie' }
17
+ # rule.pattern #=> 'lib/.*\.rb'
18
+ # rule.action.call #=> 'ohaie'
19
+ #
20
+ Rule = Struct.new(:pattern, :action)
21
+
22
+ # TODO eval context
23
+ class API #:nodoc:
24
+ end
25
+
26
+ # Creates a script object for <tt>file</tt>.
27
+ #
28
+ # Will also immediatly parse the script so it is ready to be passed to a
29
+ # controller.
30
+ #
31
+ # ===== Parameters
32
+ # file<Pathname>:: the path to the script
33
+ #
34
+ # ===== TODO
35
+ # * only accept Pathname/String
36
+ #
37
+ #--
38
+ # see issue with #parse!
39
+ # (update class example when fixed)
40
+ #
41
+ def initialize(file = StringIO.new)
42
+ @file = file
43
+ @rules = []
44
+ @default_action = lambda {}
45
+ parse!
46
+ end
47
+
48
+ # Main script API method. Builds a new rule, binding a pattern to an action.
49
+ #
50
+ # Whenever a file is saved that matches a rule's <tt>pattern</tt>, its
51
+ # corresponding <tt>action</tt> is triggered.
52
+ #
53
+ # Patterns can be either a Regexp or a string. Because they always
54
+ # represent paths however, it's simpler to use strings. But remember to use
55
+ # single quotes (not double quotes), otherwise escape sequences will be
56
+ # parsed (for example "foo/bar\.rb" #=> "foo/bar.rb", notice "\." becomes
57
+ # "."), and won't be interpreted as the regexp you expect.
58
+ #
59
+ # Also note that patterns will be matched against relative paths (relative
60
+ # from current working directory).
61
+ #
62
+ # Actions, the blocks passed to <tt>watch</tt>, receive a MatchData object
63
+ # as argument. It will be populated with the whole matched string (md[0])
64
+ # as well as individual backreferences (md[1..n]). See MatchData#[]
65
+ # documentation for more details.
66
+ #
67
+ # ===== Examples
68
+ #
69
+ # # in script file
70
+ # watch( 'test/test_.*\.rb' ) {|md| system("ruby #{md[0]}") }
71
+ # watch( 'lib/(.*)\.rb' ) {|md| system("ruby test/test_#{md[1]}.rb") }
72
+ #
73
+ # With these two rules, watchr will run any test file whenever it is itself
74
+ # changed (first rule), and will also run a corresponding test file
75
+ # whenever a lib file is changed (second rule).
76
+ #
77
+ # ===== Parameters
78
+ # pattern<~#match>:: pattern to match targetted paths
79
+ # action<Block>:: action to trigger
80
+ #
81
+ # ===== Returns
82
+ # rule<Rule>:: rule created by the method
83
+ #
84
+ def watch(pattern, &action)
85
+ @rules << Rule.new(pattern, action || @default_action)
86
+ @rules.last
87
+ end
88
+
89
+ # Convenience method. Define a default action to be triggered when a rule
90
+ # has none specified.
91
+ #
92
+ # ===== Examples
93
+ #
94
+ # # in script file
95
+ #
96
+ # default_action { system('rake --silent rdoc') }
97
+ #
98
+ # watch( 'lib/.*\.rb' )
99
+ # watch( 'README.rdoc' )
100
+ # watch( 'TODO.txt' )
101
+ # watch( 'LICENSE' )
102
+ #
103
+ # # equivalent to:
104
+ #
105
+ # watch( 'lib/.*\.rb' ) { system('rake --silent rdoc') }
106
+ # watch( 'README.rdoc' ) { system('rake --silent rdoc') }
107
+ # watch( 'TODO.txt' ) { system('rake --silent rdoc') }
108
+ # watch( 'LICENSE' ) { system('rake --silent rdoc') }
109
+ #
110
+ def default_action(&action)
111
+ @default_action = action
112
+ end
113
+
114
+ # Eval content of script file.
115
+ #--
116
+ # TODO @file.read will only work with Pathname objects!
117
+ # TODO fix script file not found error
118
+ def parse!
119
+ Watchr.debug('loading script file %s' % @file.to_s.inspect)
120
+
121
+ @rules.clear
122
+ instance_eval(@file.read)
123
+
124
+ rescue Errno::ENOENT
125
+ # TODO figure out why this is happening. still can't reproduce
126
+ Watchr.debug('script file "not found". wth')
127
+ sleep(0.3) #enough?
128
+ instance_eval(@file.read)
129
+ end
130
+
131
+ # Find an action corresponding to a path. The returned action is actually a
132
+ # wrapper around the rule's action, with the match_data prepopulated.
133
+ #
134
+ # ===== Examples
135
+ #
136
+ # script.watch( 'test/test_.*\.rb' ) {|md| "ruby #{md[0]}" }
137
+ # script.action_for('test/test_watchr.rb').call #=> "ruby test/test_watchr.rb"
138
+ #
139
+ def action_for(path)
140
+ path = rel_path(path).to_s
141
+ rule = rule_for(path)
142
+ data = path.match(rule.pattern)
143
+ lambda { rule.action.call(data) }
144
+ end
145
+
146
+ # Collection of all patterns defined in script.
147
+ #
148
+ # ===== Returns
149
+ # patterns<String, Regexp>:: all patterns
150
+ #
151
+ def patterns
152
+ #@rules.every.pattern
153
+ @rules.map {|r| r.pattern }
154
+ end
155
+
156
+ # Path to the script file
157
+ #
158
+ # ===== Returns
159
+ # path<Pathname>:: path to script file
160
+ #
161
+ def path
162
+ Pathname(@file.respond_to?(:to_path) ? @file.to_path : @file.to_s).expand_path
163
+ end
164
+
165
+ private
166
+
167
+ # Rule corresponding to a given path. If more than one rule matches, then
168
+ # the last defined rule takes precedence.
169
+ #
170
+ # ===== Parameters
171
+ # path<Pathname, String>:: path to look up rule for
172
+ #
173
+ # ===== Returns
174
+ # rule<Rule>:: rule corresponding to <tt>path</tt>
175
+ #
176
+ def rule_for(path)
177
+ @rules.reverse.detect {|rule| path.match(rule.pattern) }
178
+ end
179
+
180
+ # Make a path relative to current working directory.
181
+ #
182
+ # ===== Parameters
183
+ # path<Pathname, String>:: absolute or relative path
184
+ #
185
+ # ===== Returns
186
+ # path<Pathname>:: relative path, from current working directory.
187
+ #
188
+ def rel_path(path)
189
+ Pathname(path).expand_path.relative_path_from(Pathname(Dir.pwd))
190
+ end
191
+ end
192
+ end
@@ -1,11 +1,11 @@
1
1
  module Watchr
2
- module VERSION
2
+ module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 3
5
- TINY = 0
4
+ MINOR = 5
5
+ TINY = 2
6
6
  end
7
7
 
8
- def self.version
8
+ def self.version #:nodoc:
9
9
  [VERSION::MAJOR, VERSION::MINOR, VERSION::TINY].join('.')
10
10
  end
11
11
  end