File Coverage

blib/lib/notice.pm
Criterion Covered Total %
statement 37 41 90.2
branch 7 14 50.0
condition 2 3 66.6
subroutine 11 12 91.6
pod 1 6 16.6
total 58 76 76.3


line stmt bran cond sub pod time code
1             package notice;
2              
3 1     1   33873 use 5.014;
  1         4  
4              
5 1     1   7 use strict;
  1         3  
  1         23  
6 1     1   4 use warnings;
  1         2  
  1         24  
7              
8 1     1   4 use Carp;
  1         2  
  1         75  
9 1     1   613 use Time::Piece;
  1         10737  
  1         5  
10              
11             our $VERSION = '0.01'; # VERSION
12              
13             # FUNCTIONS
14              
15             sub import {
16 9     9   189242 my ($class, %args) = @_;
17              
18 9 50       41 return if exists $ENV{ACK_NOTICE};
19              
20 9         44 notice(scalar(caller), %args);
21              
22 9         750 return;
23             }
24              
25             sub check {
26 17     17 1 143 my ($class, %args) = @_;
27              
28 17         56 for my $name (sort keys %args) {
29 18         30 my %config = %{$args{$name}};
  18         63  
30 18 50       56 my $until = $config{until} or next;
31 18   66     63 my $varname = envvar($config{space} || $class, $name);
32 18 100       47 next if time > timepiece($until)->epoch;
33 8 100       496 next if exists $ENV{$varname};
34 5         71 return [$class, $name, $varname, $until, $config{notes}];
35             }
36              
37 12         918 return;
38             }
39              
40             sub envvar {
41 18     18 0 38 my ($class, $name) = @_;
42              
43 18         36 my $string = join '_', 'ack', 'notice', map {s/[^a-zA-Z0-9]+/_/gr} $class, $name;
  36         122  
44              
45 18         53 return uc($string);
46             }
47              
48             sub message {
49 0     0 0 0 my ($class, $name, $varname, $expiry, $notes) = @_;
50              
51 0 0       0 return "Unacknowledged notice for $class ($name):\n".
    0          
52             ($notes ? (ref($notes) ? (join("", map "- $_\n", @$notes)) : "- $notes\n") : "").
53             "- Notice can be supressed by setting the \"$varname\" environment variable\n".
54             "- Notice expires after $expiry\n"
55             }
56              
57             sub notice {
58 9     9 0 24 my ($class, %args) = @_;
59              
60 9 50       22 my $found = check($class, %args) or return;
61              
62 0         0 croak(message(@$found));
63              
64 0         0 return;
65             }
66              
67             sub timepiece {
68 18     18 0 27 my ($time) = @_;
69              
70 18         38 return Time::Piece->strptime($time, timeformat());
71             }
72              
73             sub timeformat {
74 18     18 0 68 return '%Y-%m-%d';
75             }
76              
77             1;
78             =encoding utf8
79              
80             =head1 NAME
81              
82             notice - Breaking-Change Acknowledgement
83              
84             =cut
85              
86             =head1 ABSTRACT
87              
88             Breaking-Change Acknowledgement Enforcement
89              
90             =cut
91              
92             =head1 SYNOPSIS
93              
94             package Example;
95              
96             BEGIN {
97             $ENV{ACK_NOTICE_EXAMPLE_UNSTABLE} = 1;
98             }
99              
100             use notice unstable => {
101             space => 'Example',
102             until => '2020-09-01',
103             notes => 'See https://example.com/latest/release-notes',
104             };
105              
106             1;
107              
108             =cut
109              
110             =head1 DESCRIPTION
111              
112             This package provides a mechanism for enforcing breaking-change
113             acknowledgements. When configured under a module namespace, a fatal error
114             (notice) will be thrown prompting the operator to acknowledge the notice
115             (unless the notice has already been ackowledged). Notices are acknowledged by
116             setting a predetermined environment variable. The environment variable always
117             takes the form of C<ACK_NOTICE_CLASS_NOTICENAME>. The fatal error (notice) is
118             thrown whenever, the encapsulating package is I<"used">, the notice criteria is
119             met, and the environment variable is missing. Multiple notices can be
120             configured and each can have a time-based expiry aftewhich the notice will
121             never be triggered.
122              
123             =cut
124              
125             =head1 FUNCTIONS
126              
127             This package implements the following functions:
128              
129             =cut
130              
131             =head2 check
132              
133             check(ClassName $name, Any %args) : Maybe[Tuple[Str, Str, Str, Str, Str | ArrayRef]]
134              
135             The check method returns truthy or falsy based upon whether the notice criteria
136             is met. When met, this function returns details about the trigger engaged.
137              
138             =over 4
139              
140             =item check example #1
141              
142             # given: synopsis
143              
144             delete $ENV{ACK_NOTICE_EXAMPLE_UNSTABLE};
145              
146             # notice: triggered (not acknowledged)
147              
148             notice::check('Example', (
149             unstable => {
150             until => '9999-09-01',
151             notes => 'see changelog',
152             },
153             ));
154              
155             =back
156              
157             =over 4
158              
159             =item check example #2
160              
161             # given: synopsis
162              
163             delete $ENV{ACK_NOTICE_EXAMPLE_UNSTABLE};
164              
165             # notice: not triggered (notice expired)
166              
167             notice::check('Example', (
168             unstable => {
169             until => '2000-09-01',
170             notes => 'see changelog',
171             },
172             ));
173              
174             =back
175              
176             =over 4
177              
178             =item check example #3
179              
180             # given: synopsis
181              
182             delete $ENV{ACK_NOTICE_EXAMPLE_UNSTABLE};
183              
184             # notice: triggered (not ackowledged)
185              
186             notice::check('Example::Agent', (
187             unstable => {
188             space => 'Example',
189             until => '9999-09-01',
190             notes => 'see changelog',
191             },
192             ));
193              
194             =back
195              
196             =over 4
197              
198             =item check example #4
199              
200             # given: synopsis
201              
202             $ENV{ACK_NOTICE_EXAMPLE_UNSTABLE} = 1;
203              
204             # notice: triggered (refactor not ackowledged)
205              
206             notice::check('Example::Agent', (
207             refactor => {
208             space => 'Example',
209             until => '9999-09-01',
210             notes => 'see refactor',
211             },
212             unstable => {
213             space => 'Example',
214             until => '9999-09-01',
215             notes => 'see changelog',
216             },
217             ));
218              
219             =back
220              
221             =over 4
222              
223             =item check example #5
224              
225             # given: synopsis
226              
227             $ENV{ACK_NOTICE_EXAMPLE_REFACTOR} = 1;
228             $ENV{ACK_NOTICE_EXAMPLE_UNSTABLE} = 1;
229              
230             # notice: not triggered (unstable and refactor ackowledged)
231              
232             notice::check('Example::Agent', (
233             refactor => {
234             space => 'Example',
235             until => '9999-09-01',
236             notes => 'see changelog',
237             },
238             unstable => {
239             space => 'Example',
240             until => '9999-09-01',
241             notes => 'see changelog',
242             },
243             ));
244              
245             =back
246              
247             =over 4
248              
249             =item check example #6
250              
251             # given: synopsis
252              
253             $ENV{ACK_NOTICE_EXAMPLE_UNSTABLE} = 1;
254              
255             # notice: triggered (wrong namespace ackowledged)
256              
257             notice::check('Example::Agent', (
258             unstable => {
259             until => '9999-09-01',
260             notes => 'see changelog',
261             },
262             ));
263              
264             =back
265              
266             =over 4
267              
268             =item check example #7
269              
270             # given: synopsis
271              
272             $ENV{ACK_NOTICE_EXAMPLE_AGENT_UNSTABLE} = 1;
273              
274             # notice: not triggered (notice ackowledged)
275              
276             notice::check('Example::Agent', (
277             unstable => {
278             until => '9999-09-01',
279             notes => 'see changelog',
280             },
281             ));
282              
283             =back
284              
285             =over 4
286              
287             =item check example #8
288              
289             # given: synopsis
290              
291             delete $ENV{ACK_NOTICE_EXAMPLE_UNSTABLE};
292              
293             # notice: triggered (not ackowledged)
294              
295             notice::check('Example', (
296             unstable => {
297             until => '9999-09-01',
298             notes => [
299             'see release notes for details',
300             'see https://example.com/latest/release-notes',
301             ],
302             },
303             ));
304              
305             =back
306              
307             =cut
308              
309             =head1 AUTHOR
310              
311             Al Newkirk, C<awncorp@cpan.org>
312              
313             =head1 LICENSE
314              
315             Copyright (C) 2011-2019, Al Newkirk, et al.
316              
317             This is free software; you can redistribute it and/or modify it under the terms
318             of the The Apache License, Version 2.0, as elucidated in the L<"license
319             file"|https://github.com/iamalnewkirk/notice/blob/master/LICENSE>.
320              
321             =head1 PROJECT
322              
323             L<Wiki|https://github.com/iamalnewkirk/notice/wiki>
324              
325             L<Project|https://github.com/iamalnewkirk/notice>
326              
327             L<Initiatives|https://github.com/iamalnewkirk/notice/projects>
328              
329             L<Milestones|https://github.com/iamalnewkirk/notice/milestones>
330              
331             L<Contributing|https://github.com/iamalnewkirk/notice/blob/master/CONTRIBUTE.md>
332              
333             L<Issues|https://github.com/iamalnewkirk/notice/issues>
334              
335             =cut