File Coverage

blib/lib/Document/TriPart.pm
Criterion Covered Total %
statement 84 199 42.2
branch 21 110 19.0
condition 1 16 6.2
subroutine 16 30 53.3
pod 0 13 0.0
total 122 368 33.1


line stmt bran cond sub pod time code
1             package Document::TriPart;
2             BEGIN {
3 1     1   109956 $Document::TriPart::VERSION = '0.024';
4             }
5             # ABSTRACT: Read, write & edit a tri-part document (preamble, YAML::Tiny header, and body)
6              
7              
8 1     1   11 use warnings;
  1         3  
  1         31  
9 1     1   4 use strict;
  1         2  
  1         2367  
10              
11              
12 1     1   4161 use Any::Moose;
  1         42291  
  1         6  
13              
14 1     1   1705 use File::AtomicWrite;
  1         27856  
  1         40  
15 1     1   12 use File::Temp qw/tempfile/;
  1         2  
  1         47  
16 1     1   1042 use IO::Scalar;
  1         4756  
  1         45  
17 1     1   878 use Carp::Clan;
  1         2091  
  1         8  
18 1     1   1117 use Path::Class();
  1         53913  
  1         28  
19 1     1   13352 use YAML::Tiny();
  1         6680  
  1         2455  
20              
21             our $TriPart = 1;
22              
23             has file => qw/is rw/;
24             has atomic => qw/is rw/, default => 1;
25              
26             has separator => qw/is rw required 1 isa Str/, default => '---';
27              
28             has _preamble_content => qw/is rw isa Maybe[ScalarRef]/;
29             sub preamble_as_string {
30 0     0 0 0 my $self = shift;
31 0 0       0 return undef unless $self->_preamble_content;
32 0         0 return ${ $self->_preamble_content };
  0         0  
33             }
34              
35             has _header_content => qw/is rw isa Maybe[ScalarRef]/;
36             sub header_as_string {
37 0     0 0 0 my $self = shift;
38 0 0       0 return undef unless $self->_header_content;
39 0         0 return ${ $self->_header_content };
  0         0  
40             }
41              
42             has _body_content => qw/is rw isa Maybe[ScalarRef]/;
43             sub body_as_string {
44 0     0 0 0 my $self = shift;
45 0 0       0 return undef unless $self->_body_content;
46 0         0 return ${ $self->_body_content };
  0         0  
47             }
48              
49             has _header => qw/is ro lazy_build 1 isa Maybe[HashRef]/;
50             sub _build__header {
51 1     1   2 my $self = shift;
52 1 50       6 return {} unless defined $self->_header_content;
53 1         5 return $self->_parse_header($self->_header_content);
54             }
55              
56             sub preamble {
57 0     0 0 0 my $self = shift;
58 0 0       0 if (@_) {
59 0         0 my $value = shift;
60 0 0       0 if (defined $value) {
61 0 0       0 $value = $$value if ref $value eq "SCALAR";
62 0         0 $value = \"$value";
63             }
64 0         0 $self->_preamble_content($value);
65             }
66 0 0       0 return undef unless defined $self->_preamble_content;
67 0         0 return ${ $self->_preamble_content };
  0         0  
68             }
69              
70             sub header {
71 1     1 0 484 my $self = shift;
72 1 50       5 if (@_) {
73 0         0 my $value = shift;
74 0 0       0 if (defined $value) {
75 0 0       0 $value = $$value if ref $value eq "SCALAR";
76 0 0       0 $value = $self->_format_header($value) if ref $value eq "HASH";
77 0         0 $value = \"$value";
78             }
79 0         0 $self->_header_content($value);
80 0         0 $self->_clear_header;
81             }
82 1         10 return $self->_header;
83             }
84              
85             sub body {
86 0     0 0 0 my $self = shift;
87 0 0       0 if (@_) {
88 0         0 my $value = shift;
89 0 0       0 $value = "" unless defined $value;
90 0 0       0 $value = $$value if ref $value eq "SCALAR";
91 0         0 $value = \"$value";
92 0         0 $self->_body_content( $value );
93             }
94 0 0       0 return "" unless defined $self->_body_content;
95 0         0 return ${ $self->_body_content };
  0         0  
96             }
97              
98             sub write {
99 0     0 0 0 my $self = shift;
100 0         0 my $file;
101 0 0       0 $file = shift if @_ % 2;
102 0         0 my %given = @_;
103 0 0       0 $file = $given{file} unless defined $file;
104 0 0       0 $file = $self->file unless defined $file;
105 0 0       0 return $self->write_file( $file, @_ ) if defined $file;
106 0         0 croak "Can't write without having a file to write to";
107             }
108              
109             sub write_file {
110 0     0 0 0 my $self = shift;
111 0         0 my $file;
112 0 0       0 $file = shift if @_ % 2;
113 0         0 my %given = @_;
114 0 0       0 $file = $given{file} unless defined $file;
115              
116 0 0 0     0 croak "Wasn't given file to write to" unless defined $file && length $file;
117              
118 0         0 $file = Path::Class::File->new( "$file" );
119 0 0       0 $file->parent->mkpath unless -d $file->parent; # TODO Should we automatically make?
120              
121 0         0 my $content = \$self->write_string( @_ );
122 0 0       0 if (my $atomic = $self->atomic) {
123 0         0 my %atomic;
124 0 0       0 %atomic = %$atomic if ref $atomic eq 'HASH';
125 0         0 File::AtomicWrite->write_file({
126             file => $file.'',
127             input => $content,
128             %atomic,
129             });
130             }
131             else {
132 0 0       0 $file->openw->print( $$content ) or croak "Unable to write to $file since; $!";
133             }
134            
135             # my $handle = IO::AtomicFile->open("$file", 'w') or croak "Unable to write to $file since: $!";
136             # $handle->print( $self->write_string( @_ ) );
137             # $handle->close or die "Couldn't atomically write $file since: $!";
138              
139             }
140              
141             sub write_string {
142 0     0 0 0 my $self = shift;
143 0         0 my %given = @_;
144              
145 0 0 0     0 return $self->body || '' if $given{body_only};
146              
147 0         0 my @part = map { chomp; $_ } grep { defined }
  0         0  
  0         0  
  0         0  
148             $self->preamble,
149             $self->_format_header($self->_header),
150             ;
151              
152 0         0 my $separator = $self->separator;
153              
154 0   0     0 return join "\n$separator\n", @part, ( $self->body || '' );
155             }
156              
157             # TODO ($header, $body) = ->parse( \ ... )
158             sub read_string {
159 0     0 0 0 return shift->read(\shift());
160             }
161              
162             sub read {
163 2     2 0 168 my $self = shift;
164 2 100       41 return $self->new->read( @_ ) unless blessed $self;
165 1         3 my $file;
166 1 50       6 $file = shift if @_ % 2;
167 1         3 my %given = @_;
168 1 50       6 $file = $given{file} unless defined $file;
169 1 50       4 $file = $self->file unless defined $file;
170 1 50       9 return $self->read_file( $file, @_ ) if defined $file;
171 0         0 croak "Can't read without having a file to read from";
172             }
173              
174             sub read_file {
175 1     1 0 2 my $self = shift;
176 1 50       5 return $self->new->read_file( @_ ) unless blessed $self;
177 1         2 my $file;
178 1 50       5 $file = shift if @_ % 2;
179 1         3 my %given = @_;
180 1 50       7 $file = $given{file} unless defined $file;
181              
182 1         2 my $read = $file;
183              
184 1 50       4 croak "Wasn't given something to read" unless defined $read;
185              
186 1 50       6 if (ref $read eq "SCALAR") {
    0          
    0          
187 1         11 $read = IO::Scalar->new($read);
188             }
189             elsif (UNIVERSAL::isa($read => 'IO::Handle')) {
190             }
191             elsif (ref $read eq "GLOB") {
192 0         0 my $io = IO::Handle->new;
193 0         0 $io->fdopen( fileno($read), "r" );
194 0         0 $read = $io;
195             }
196             else {
197 0         0 $read = Path::Class::File->new( "$read" )->openr;
198             }
199              
200             # croak "Don't know how to read $read" unless UNIVERSAL::isa($read => 'IO::Handle');
201              
202 1 50       182 if ( $given{body_only} ) {
203 0         0 $self->{_body_content} = $self->_read( $read );
204             }
205             else {
206 1         2 my @part;
207 1 50       5 my $part_limit = $TriPart ? 2 : 1;
208 1         1 while (1) {
209 2         3 my ($more, $content);
210 2 50       5 if ( $part_limit > @part ) {
211 2         7 ($more, $content) = $self->_read_until_separator( $read );
212             }
213             else {
214 0         0 $content = $self->_read( $read );
215             }
216            
217 2         5 push @part, $content;
218 2 100       6 last unless $more;
219             }
220              
221 1         3 my $body = pop @part;
222 1         2 my $header = pop @part;
223              
224 1         3 my $preamble = pop @part;
225              
226 1         4 $self->_clear_header;
227              
228 1         7 $self->{_body_content} = $body;
229 1         2 $self->{_header_content} = $header;
230 1         3 $self->{_preamble_content} = $preamble;
231             }
232              
233 1         7 return $self;
234             }
235              
236             sub _read_until_separator {
237 2     2   3 my $self = shift;
238 2         3 my $handle = shift;
239 2         3 my $separator = shift;
240              
241 2         2 my $content;
242 2         10 $separator = $self->separator;
243 2         25 my $match = qr/^$separator\s*$/;
244 2         5 my $got_separator;
245 2         9 while (<$handle>) {
246 2 100       83 last if $got_separator = $_ =~ $match;
247 1         22 $content .= $_;
248             }
249 2         24 return ($got_separator => \$content);
250             }
251              
252             sub _read {
253 0     0   0 my $self = shift;
254 0         0 my $handle = shift;
255              
256 0         0 local $/ = undef;
257 0         0 my $content;
258 0         0 $content = <$handle>;
259 0         0 return \$content;
260             }
261              
262             sub _parse_header {
263 1     1   2 my $self = shift;
264 1         2 my $content = shift;
265              
266             # TODO Parsing of: { "a": "1" } does not work
267 1 50 33     14 chomp $$content if defined $$content && $$content =~ m/^\s*\{/;
268              
269 1 0       9 return {} unless my $header = YAML::Tiny->read_string($$content);
270 0           return $header->[0];
271             }
272              
273             sub _format_header {
274 0     0     my $self = shift;
275 0           my $header = shift;
276              
277 0 0         return undef unless defined $header;
278              
279 0 0         croak "Header given is not a hash ($header)" unless ref $header eq 'HASH';
280              
281 0           my $string = YAML::Tiny::Dump($header);
282 0           $string =~ s/^---\s*//;
283 0           return $string;
284             }
285              
286             sub _editor {
287 0   0 0     return [ split m/\s+/, ($ENV{VISUAL} || $ENV{EDITOR}) ];
288             }
289              
290             sub _edit_file {
291 0     0     my $file = shift;
292 0 0         die "Don't know what editor" unless my $editor = _editor;
293 0           my $rc = system @$editor, $file;
294 0 0         unless ($rc == 0) {
295 0           my ($exit_value, $signal, $core_dump);
296 0           $exit_value = $? >> 8;
297 0           $signal = $? & 127;
298 0           $core_dump = $? & 128;
299 0           die "Error during edit (@$editor): exit value ($exit_value), signal ($signal), core_dump($core_dump): $!";
300             }
301             }
302              
303             sub edit {
304 0     0 0   my $self = shift;
305              
306 0           my $file;
307 0 0         $file = shift if @_ % 2;
308 0           my %given = @_;
309 0 0         $file = $given{file} unless defined $file;
310 0 0 0       $file = $self->file unless defined $file || $given{tmp};
311              
312 0           my ($tmp_fh, $tmp_filename);
313 0 0         unless (defined $file) {
314 0           ($tmp_fh, $tmp_filename) = tempfile;
315 0           $file = $tmp_filename;
316              
317             # Only write out the file first if we're using a temporary file
318 0           $self->write( $file, @_ );
319             }
320              
321 0           _edit_file $file;
322              
323 0           $self->read( $file, @_ );
324             }
325              
326             1;
327              
328             __END__