mynyml-watchr 0.3.0 → 0.5.2

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,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