File Coverage

blib/lib/Setup/File/TextFragment.pm
Criterion Covered Total %
statement 64 68 94.1
branch 23 38 60.5
condition 8 10 80.0
subroutine 7 7 100.0
pod 1 1 100.0
total 103 124 83.0


line stmt bran cond sub pod time code
1             package Setup::File::TextFragment;
2              
3             our $DATE = '2017-07-10'; # DATE
4             our $VERSION = '0.06'; # VERSION
5              
6 1     1   45541 use 5.010001;
  1         4  
7 1     1   6 use strict;
  1         3  
  1         21  
8 1     1   6 use warnings;
  1         3  
  1         30  
9 1     1   3566 use Log::ger;
  1         110  
  1         7  
10              
11 1     1   2002 use File::Trash::Undoable;
  1         16637  
  1         50  
12 1     1   766 use Text::Fragment;
  1         9670  
  1         1170  
13              
14             require Exporter;
15             our @ISA = qw(Exporter);
16             our @EXPORT_OK = qw(setup_text_fragment);
17              
18             our %SPEC;
19              
20             $SPEC{setup_text_fragment} = {
21             v => 1.1,
22             summary => 'Insert/delete text fragment in a file (with undo support)',
23             description => <<'_',
24              
25             On do, will insert fragment to file (or delete, if `should_exist` is set to
26             false). On undo, will restore old file.
27              
28             Unfixable state: file does not exist or not a regular file (directory and
29             symlink included).
30              
31             Fixed state: file exists, fragment already exists and with the same content (if
32             `should_exist` is true) or fragment already does not exist (if `should_exist` is
33             false).
34              
35             Fixable state: file exists, fragment doesn't exist or payload is not the same
36             (if `should_exist` is true) or fragment still exists (if `should_exist` is
37             false).
38              
39             _
40             args => {
41             path => {
42             summary => 'Path to file',
43             schema => 'str*',
44             req => 1,
45             pos => 0,
46             },
47             id => {
48             summary => 'Fragment ID',
49             schema => 'str*',
50             req => 1,
51             pos => 1,
52             },
53             payload => {
54             summary => 'Fragment content',
55             schema => 'str*',
56             req => 1,
57             pos => 2,
58             },
59             attrs => {
60             summary => 'Fragment attributes (only for inserting new fragment)'.
61             ', passed to Text::Fragment',
62             schema => 'hash',
63             },
64             top_style => {
65             summary => 'Will be passed to Text::Fragment',
66             schema => 'bool',
67             },
68             comment_style => {
69             summary => 'Will be passed to Text::Fragment',
70             schema => 'bool',
71             },
72             label => {
73             summary => 'Will be passed to Text::Fragment',
74             schema => 'str',
75             },
76             replace_pattern => {
77             summary => 'Will be passed to Text::Fragment',
78             schema => 'str',
79             },
80             good_pattern => {
81             summary => 'Will be passed to Text::Fragment',
82             schema => 'str',
83             },
84             should_exist => {
85             summary => 'Whether fragment should exist',
86             schema => [bool => {default=>1}],
87             },
88             },
89             features => {
90             tx => {v=>2},
91             idempotent => 1,
92             },
93             };
94             sub setup_text_fragment {
95 28     28 1 2694768 my %args = @_;
96              
97             # TMP, schema
98 28   50     236 my $tx_action = $args{-tx_action} // '';
99             my $taid = $args{-tx_action_id}
100 28 100       210 or return [400, "Please specify -tx_action_id"];
101 23         106 my $dry_run = $args{-dry_run};
102 23         112 my $path = $args{path};
103 23 50       121 defined($path) or return [400, "Please specify path"];
104 23         102 my $id = $args{id};
105 23 50       110 defined($id) or return [400, "Please specify id"];
106 23         266 my $payload = $args{payload};
107 23 50       116 defined($payload) or return [400, "Please specify payload"];
108 23         101 my $attrs = $args{attrs};
109 23         79 my $comment_style = $args{comment_style};
110 23         64 my $top_style = $args{top_style};
111 23         119 my $label = $args{label};
112 23         72 my $replace_pattern = $args{replace_pattern};
113 23         75 my $good_pattern = $args{good_pattern};
114 23   50     143 my $should_exist = $args{should_exist} // 1;
115              
116 23         2192 my $is_sym = (-l $path);
117 23         343 my @st = stat($path);
118 23   100     241 my $exists = $is_sym || (-e _);
119 23         94 my $is_file = (-f _);
120              
121 23         91 my @cmd;
122              
123 23 100       125 return [412, "$path does not exist"] unless $exists;
124 22 100 100     257 return [412, "$path is not a regular file"] if $is_sym||!$is_file;
125              
126 20 50       954 open my($fh), "<", $path or return [500, "Can't open $path: $!"];
127 20         80 my $text = do { local $/; scalar <$fh> };
  20         159  
  20         459  
128              
129 20         78 my $res;
130 20 50       99 if ($should_exist) {
131 20         256 $res = Text::Fragment::insert_fragment(
132             text=>$text, id=>$id, payload=>$payload,
133             comment_style=>$comment_style, label=>$label, attrs=>$attrs,
134             good_pattern=>$good_pattern, replace_pattern=>$replace_pattern,
135             top_style=>$top_style,
136             );
137             } else {
138 0         0 $res = Text::Fragment::delete_fragment(
139             text=>$text, id=>$id,
140             comment_style=>$comment_style, label=>$label,
141             );
142             }
143              
144 20 100       10280 return $res if $res->[0] == 304;
145 18 50       112 return $res if $res->[0] != 200;
146              
147 18 100       121 if ($tx_action eq 'check_state') {
    50          
148 9 50       42 if ($should_exist) {
149 9 50       44 log_info("(DRY) Inserting fragment $id to $path ...")
150             if $dry_run;
151             } else {
152 0 0       0 log_info("(DRY) Deleting fragment $id from $path ...")
153             if $dry_run;
154             }
155 9         688 return [200, "Fragment $id needs to be inserted to $path", undef,
156             {undo_actions=>[
157             ['File::Trash::Undoable::untrash', # restore old file
158             {path=>$path, suffix=>substr($taid,0,8)}],
159             ['File::Trash::Undoable::trash', # trash new file
160             {path=>$path, suffix=>substr($taid,0,8)."n"}],
161             ]}];
162             } elsif ($tx_action eq 'fix_state') {
163 9 50       44 if ($should_exist) {
164 9         98 log_info("Inserting fragment $id to $path ...");
165             } else {
166 0         0 log_info("Deleting fragment $id from $path ...");
167             }
168              
169 9         143 File::Trash::Undoable::trash(
170             path=>$path, suffix=>substr($taid,0,8), -tx_action=>'fix_state');
171 9 50       54300 open my($fh), ">", $path or return [500, "Can't open: $!"];
172 9         146 print $fh $res->[2]{text};
173 9 50       793 close $fh or return [500, "Can't write: $!"];
174 9         234 chmod $st[2] & 07777, $path; # XXX ignore error?
175 9 50       99 unless ($>) { chown $st[4], $st[5], $path } # XXX ignore error?
  9         159  
176 9         693 return [200, "OK"];
177             }
178 0           [400, "Invalid -tx_action"];
179             }
180              
181             1;
182             # ABSTRACT: Insert/delete text fragment in a file (with undo support)
183              
184             __END__
185              
186             =pod
187              
188             =encoding UTF-8
189              
190             =head1 NAME
191              
192             Setup::File::TextFragment - Insert/delete text fragment in a file (with undo support)
193              
194             =head1 VERSION
195              
196             This document describes version 0.06 of Setup::File::TextFragment (from Perl distribution Setup-File-TextFragment), released on 2017-07-10.
197              
198             =head1 FUNCTIONS
199              
200              
201             =head2 setup_text_fragment
202              
203             Usage:
204              
205             setup_text_fragment(%args) -> [status, msg, result, meta]
206              
207             Insert/delete text fragment in a file (with undo support).
208              
209             On do, will insert fragment to file (or delete, if C<should_exist> is set to
210             false). On undo, will restore old file.
211              
212             Unfixable state: file does not exist or not a regular file (directory and
213             symlink included).
214              
215             Fixed state: file exists, fragment already exists and with the same content (if
216             C<should_exist> is true) or fragment already does not exist (if C<should_exist> is
217             false).
218              
219             Fixable state: file exists, fragment doesn't exist or payload is not the same
220             (if C<should_exist> is true) or fragment still exists (if C<should_exist> is
221             false).
222              
223             This function is not exported by default, but exportable.
224              
225             This function is idempotent (repeated invocations with same arguments has the same effect as single invocation). This function supports transactions.
226              
227              
228             Arguments ('*' denotes required arguments):
229              
230             =over 4
231              
232             =item * B<attrs> => I<hash>
233              
234             Fragment attributes (only for inserting new fragment), passed to Text::Fragment.
235              
236             =item * B<comment_style> => I<bool>
237              
238             Will be passed to Text::Fragment.
239              
240             =item * B<good_pattern> => I<str>
241              
242             Will be passed to Text::Fragment.
243              
244             =item * B<id>* => I<str>
245              
246             Fragment ID.
247              
248             =item * B<label> => I<str>
249              
250             Will be passed to Text::Fragment.
251              
252             =item * B<path>* => I<str>
253              
254             Path to file.
255              
256             =item * B<payload>* => I<str>
257              
258             Fragment content.
259              
260             =item * B<replace_pattern> => I<str>
261              
262             Will be passed to Text::Fragment.
263              
264             =item * B<should_exist> => I<bool> (default: 1)
265              
266             Whether fragment should exist.
267              
268             =item * B<top_style> => I<bool>
269              
270             Will be passed to Text::Fragment.
271              
272             =back
273              
274             Special arguments:
275              
276             =over 4
277              
278             =item * B<-tx_action> => I<str>
279              
280             For more information on transaction, see L<Rinci::Transaction>.
281              
282             =item * B<-tx_action_id> => I<str>
283              
284             For more information on transaction, see L<Rinci::Transaction>.
285              
286             =item * B<-tx_recovery> => I<str>
287              
288             For more information on transaction, see L<Rinci::Transaction>.
289              
290             =item * B<-tx_rollback> => I<str>
291              
292             For more information on transaction, see L<Rinci::Transaction>.
293              
294             =item * B<-tx_v> => I<str>
295              
296             For more information on transaction, see L<Rinci::Transaction>.
297              
298             =back
299              
300             Returns an enveloped result (an array).
301              
302             First element (status) is an integer containing HTTP status code
303             (200 means OK, 4xx caller error, 5xx function error). Second element
304             (msg) is a string containing error message, or 'OK' if status is
305             200. Third element (result) is optional, the actual result. Fourth
306             element (meta) is called result metadata and is optional, a hash
307             that contains extra information.
308              
309             Return value: (any)
310              
311             =head1 HOMEPAGE
312              
313             Please visit the project's homepage at L<https://metacpan.org/release/Setup-File-TextFragment>.
314              
315             =head1 SOURCE
316              
317             Source repository is at L<https://github.com/perlancar/perl-Setup-File-TextFragment>.
318              
319             =head1 BUGS
320              
321             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Setup-File-TextFragment>
322              
323             When submitting a bug or request, please include a test-file or a
324             patch to an existing test-file that illustrates the bug or desired
325             feature.
326              
327             =head1 SEE ALSO
328              
329             L<Text::Fragment>
330              
331             L<Setup>
332              
333             =head1 AUTHOR
334              
335             perlancar <perlancar@cpan.org>
336              
337             =head1 COPYRIGHT AND LICENSE
338              
339             This software is copyright (c) 2017, 2015, 2014, 2013, 2012 by perlancar@cpan.org.
340              
341             This is free software; you can redistribute it and/or modify it under
342             the same terms as the Perl 5 programming language system itself.
343              
344             =cut