File Coverage

blib/lib/Dist/Zilla/Role/File/ChangeNotification.pm
Criterion Covered Total %
statement 31 31 100.0
branch 3 4 75.0
condition 2 2 100.0
subroutine 11 11 100.0
pod 2 2 100.0
total 49 50 98.0


line stmt bran cond sub pod time code
1 6     6   10190168 use strict;
  6         12  
  6         192  
2 6     6   24 use warnings;
  6         6  
  6         279  
3             package Dist::Zilla::Role::File::ChangeNotification;
4             # git description: v0.004-1-g3993219
5             $Dist::Zilla::Role::File::ChangeNotification::VERSION = '0.005';
6             # ABSTRACT: Receive notification when something changes a file's contents
7             # vim: set ts=8 sw=4 tw=78 et :
8              
9 6     6   28 use Moose::Role;
  6         8  
  6         54  
10 6     6   24484 use Digest::MD5 'md5_hex';
  6         12  
  6         385  
11 6     6   30 use Encode 'encode_utf8';
  6         8  
  6         224  
12 6     6   31 use namespace::autoclean;
  6         6  
  6         52  
13              
14             has _content_checksum => ( is => 'rw', isa => 'Str' );
15              
16             has on_changed => (
17             isa => 'ArrayRef[CodeRef]',
18             traits => ['Array'],
19             handles => {
20             _add_on_changed => 'push',
21             _on_changed_subs => 'elements',
22             },
23             lazy => 1,
24             default => sub { [] },
25             );
26              
27             sub on_changed
28             {
29 7     7 1 9453 my ($self, $watch_sub) = @_;
30             $self->_add_on_changed($watch_sub || sub {
31 1     1   2 my ($file, $new_content) = @_;
32 1         25 die 'content of ', $file->name, ' has changed!';
33 7   100     466 });
34             }
35              
36             sub watch_file
37             {
38 7     7 1 4177 my $self = shift;
39              
40 7 100       380 $self->on_changed if not $self->_on_changed_subs;
41 7 50       343 return if $self->_content_checksum;
42              
43             # Storing a checksum initiates the "watch" process
44 7         80 $self->_content_checksum($self->__calculate_checksum);
45 7         41 return;
46             }
47              
48             sub __calculate_checksum
49             {
50 18     18   25 my $self = shift;
51             # this may not be the correct encoding, but things should work out okay
52             # anyway - all we care about is deterministically getting bytes back
53 18         62 md5_hex(encode_utf8($self->content))
54             }
55              
56             around content => sub {
57             my $orig = shift;
58             my $self = shift;
59              
60             # pass through if getter
61             return $self->$orig if @_ < 1;
62              
63             # store the new content
64             # XXX possible TODO: do not set the new content until after the callback
65             # is invoked. Talk to me if you care about this in either direction!
66             my $content = shift;
67             $self->$orig($content);
68              
69             my $old_checksum = $self->_content_checksum;
70              
71             # do nothing extra if we haven't got a checksum yet
72             return $content if not $old_checksum;
73              
74             # ...or if the content hasn't actually changed
75             my $new_checksum = $self->__calculate_checksum;
76             return $content if $old_checksum eq $new_checksum;
77              
78             # update the checksum to reflect the new content
79             $self->_content_checksum($new_checksum);
80              
81             # invoke the callback
82             $self->_has_changed($content);
83              
84             return $self->content;
85             };
86              
87             sub _has_changed
88             {
89 11     11   22 my ($self, @args) = @_;
90              
91 11         602 $self->$_(@args) for $self->_on_changed_subs;
92             }
93              
94             1;
95              
96             __END__
97              
98             =pod
99              
100             =encoding UTF-8
101              
102             =head1 NAME
103              
104             Dist::Zilla::Role::File::ChangeNotification - Receive notification when something changes a file's contents
105              
106             =head1 VERSION
107              
108             version 0.005
109              
110             =head1 SYNOPSIS
111              
112             package Dist::Zilla::Plugin::MyPlugin;
113             sub some_phase
114             {
115             my $self = shift;
116              
117             my ($source_file) = grep { $_->name eq 'some_name' } @{$self->zilla->files};
118             # ... do something with this file ...
119              
120             Dist::Zilla::Role::File::ChangeNotification->meta->apply($source_file);
121             my $plugin = $self;
122             $file->on_changed(sub {
123             $plugin->log_fatal('someone tried to munge ', shift->name,
124             ' after we read from it. You need to adjust the load order of your plugins.');
125             });
126             $file->watch_file;
127             }
128              
129             =head1 DESCRIPTION
130              
131             This is a role for L<Dist::Zilla::Role::File> objects which gives you a
132             mechanism for detecting and acting on files changing their content. This is
133             useful if your plugin performs an action based on a file's content (perhaps
134             copying that content to another file), and then later in the build process,
135             that source file's content is later modified.
136              
137             =head1 METHODS
138              
139             =head2 C<on_changed($subref)>
140              
141             Provide a method to be invoked against the file when the file's
142             content has changed. The new file content is passed as an argument. If you
143             need to do something in your plugin at this point, define the sub as a closure
144             over your plugin object, as demonstrated in the L</SYNOPSIS>.
145              
146             B<Be careful> of infinite loops, which can result if your sub changes the same
147             file's content again! Add a mechanism to return without altering content if
148             particular conditions are met (say that the needed content is already present,
149             or even the value of a particular suitably-scoped variable.
150              
151             =head1 METHODS
152              
153             =head2 C<watch_file>
154              
155             Once this method is called, every subsequent change to
156             the file's content will result in your C<on_changed> sub being invoked against
157             the file. The new content is passed as the argument to the sub; the return
158             value is ignored.
159              
160             =head1 SUPPORT
161              
162             =for stopwords irc
163              
164             Bugs may be submitted through L<the RT bug tracker|https://rt.cpan.org/Public/Dist/Display.html?Name=Dist-Zilla-Role-File-ChangeNotification>
165             (or L<bug-Dist-Zilla-Role-File-ChangeNotification@rt.cpan.org|mailto:bug-Dist-Zilla-Role-File-ChangeNotification@rt.cpan.org>).
166             I am also usually active on irc, as 'ether' at C<irc.perl.org>.
167              
168             =head1 SEE ALSO
169              
170             =over 4
171              
172             =item *
173              
174             L<Dist::Zilla::Role::FileWatcher> - in this distribution, for providing an interface for a plugin to watch a file
175              
176             =item *
177              
178             L<Dist::Zilla::File::OnDisk>
179              
180             =item *
181              
182             L<Dist::Zilla::File::InMemory>
183              
184             =back
185              
186             =head1 AUTHOR
187              
188             Karen Etheridge <ether@cpan.org>
189              
190             =head1 COPYRIGHT AND LICENSE
191              
192             This software is copyright (c) 2013 by Karen Etheridge.
193              
194             This is free software; you can redistribute it and/or modify it under
195             the same terms as the Perl 5 programming language system itself.
196              
197             =head1 CONTRIBUTOR
198              
199             =for stopwords Yanick Champoux
200              
201             Yanick Champoux <yanick@babyl.dyndns.org>
202              
203             =cut