File Coverage

blib/lib/Devel/Deprecations/Environmental.pm
Criterion Covered Total %
statement 59 62 95.1
branch 27 30 90.0
condition 16 16 100.0
subroutine 9 9 100.0
pod n/a
total 111 117 94.8


line stmt bran cond sub pod time code
1             package Devel::Deprecations::Environmental;
2              
3 7     7   498944 use strict;
  7         75  
  7         200  
4 7     7   40 use warnings;
  7         16  
  7         176  
5              
6 7     7   3247 use Devel::Deprecations::Environmental::MicroDateTime;
  7         18  
  7         188  
7 7     7   3516 use Module::Load ();
  7         7959  
  7         201  
8 7     7   48 use Scalar::Util qw(blessed);
  7         16  
  7         6060  
9              
10             our $VERSION = '1.100';
11              
12             =head1 NAME
13              
14             Devel::Deprecations::Environmental - deprecations for your code's surroundings
15              
16             =head1 DESCRIPTION
17              
18             A framework for managing deprecations of the environment in which your code runs
19              
20             =head1 SYNOPSIS
21              
22             This will load the Devel::Deprecations::Environmental::Plugin::Int32 plugin and emit a
23             warning if running on a 32 bit system:
24              
25             use Devel::Deprecations::Environmental qw(Int32);
26              
27             This will start warning about an impending deprecation on the 1st of February
28             2023, upgrade that to a warning about being unsupported on the 1st of February
29             2024, and upgrade that to a fatal error on the 1st of February 2025:
30              
31             use Devel::Deprecations::Environmental
32             Int32 => {
33             warn_from => '2023-02-01',
34             unsupported_from => '2024-02-01',
35             fatal_from => '2025-02-01',
36             };
37              
38             This will always warn about 32 bit perl or a really old perl:
39              
40             use Devel::Deprecations::Environmental
41             OldPerl => { older_than => '5.14.0', },
42             'Int32';
43              
44             =head1 INCOMPATIBLE CHANGES
45              
46             Version 1.000 used L for parsing times provided as
47             strings. This was removed in 1.100 as it had an unreasonable number of
48             dependencies. As a result we now only support a subset of ISO 8601 formats,
49             namely the sensible ones:
50              
51             =over
52              
53             =item YYYY-MM-DD
54              
55             =item YYYY-MM-DD HH:MM:SS
56              
57             =item YYYY-MM-DDTHH:MM:SS
58              
59             =back
60              
61             You can still pass in a DateTime object if you wish, and so can use
62             DateTime::Format::ISO8601 yourself to construct them from some crazy
63             involving week numbers if you really want.
64              
65             =head1 DEPRECATION ARGUMENTS
66              
67             Each deprecation has a name, which can be optionally followed by a hash-ref of
68             arguments. All deprecations automatically support:
69              
70             =over
71              
72             =item warn_from
73              
74             The time at which to start emitting warnings about an impending deprecation.
75             Defaults to the moment of creation, C<'1970-01-01'> (in general the accepted
76             date format is YYYY-MM-DD followed by an optional space or letter T and
77             HH:MM:SS). You can also provide this as a L object.
78              
79             This must be before any of C or C which are
80             specified.
81              
82             =item unsupported_from
83              
84             The time at which to start warning harder, when something is no longer
85             supported. Defaults to C, meaning "don't do this".
86              
87             This must be before C if that is specified.
88              
89             =item fatal_from
90              
91             The time after which the code should just C. Defaults to C,
92             meaning "don't do this".
93              
94             =back
95              
96             Of those three only the most severe will be emitted.
97              
98             Arguments with names beginning with an underscore are reserved for internal
99             use. Plugins can support any other arguments they wish.
100              
101             =head1 CONTENT OF WARNINGS / FATAL ERRORS
102              
103             The pseudo-variables C<$date>, C<$filename>, C<$line>, and C<$reason> will be
104             interpolated.
105              
106             C<$date> will be C or C (using
107             whichever is earlier) if one of those is configured.
108              
109             C<$filename> and C<$line> will tell you the file and line on which
110             C is loaded.
111              
112             C<$reason> is defined in the plugin's C method.
113              
114             =head2 Initial warning
115              
116             C
117              
118             =head2 "Unsupported" warning
119              
120             C
121              
122             =head2 Fatal error
123              
124             C
125              
126             =cut
127              
128             sub import {
129 51     51   41195 my $class = shift;
130 51         137 my @args = @_;
131 51 100       156 if($class eq __PACKAGE__) {
132             # when loading Devel::Deprecations::Environmental itself ...
133 25         82 while(@args) {
134 27         138 my $plugin = 'Devel::Deprecations::Environmental::Plugin::'.shift(@args);
135 27 100       95 my $plugin_args = ref($args[0]) ? shift(@args) : {};
136             $plugin_args->{_source} = {
137 27         176 filename => (caller(0))[1],
138             line => (caller(0))[2]
139             };
140              
141 27         1160 Module::Load::load($plugin);
142 27         31767 my @errors = ();
143 27 100       236 push @errors, "doesn't inherit from ".__PACKAGE__
144             unless($plugin->isa(__PACKAGE__));
145 27 100       196 push @errors, "doesn't implement 'reason()'"
146             unless($plugin->can('reason'));
147 27 100       139 push @errors, "doesn't implement 'is_deprecated()'"
148             unless($plugin->can('is_deprecated'));
149             die(join("\n",
150             __PACKAGE__.": plugin $plugin doesn't implement all it needs to",
151 27 100       85 map { " $_" } @errors
  3         21  
152             )."\n")
153             if(@errors);
154 26         121 $plugin->import($plugin_args);
155             }
156             } else {
157             # when called on a subclass ...
158 26         51 my $args = $args[0];
159 26   100     138 $args->{warn_from} ||= '1970-01-01';
160             my %_froms = (
161             map {
162             $_ => blessed($args->{$_})
163             ? $args->{$_}
164 40 100       245 : Devel::Deprecations::Environmental::MicroDateTime->parse_datetime($args->{$_})
165             } grep {
166 26         66 exists($args->{$_})
  78         204  
167             } qw(warn_from unsupported_from fatal_from)
168             );
169 25         151 delete($args->{$_}) foreach(qw(warn_from unsupported_from fatal_from));
170              
171 25         98 my $now = Devel::Deprecations::Environmental::MicroDateTime->now();
172              
173             # if any param is a DateTime we need to upgrade all of them so comparisons work
174 25 50       68 if(
175 39         165 grep { blessed($_) eq 'DateTime' }
176 39         85 map { $_froms{$_} }
177 75         177 grep { exists($_froms{$_}) }
178             qw(fatal_from unsupported_from warn_from)
179             ) {
180 0         0 $now = DateTime->from_epoch(epoch => $now->epoch());
181             $_froms{$_} = DateTime->from_epoch(epoch => $_froms{$_}->epoch())
182 0 0       0 foreach(grep { exists($_froms{$_}) && blessed($_froms{$_}) ne 'DateTime' } qw(fatal_from unsupported_from warn_from));
  0         0  
183             }
184              
185             # check that warn/unsupported/fatal are ordered correctly in time
186 25         102 foreach my $pair (
187             [qw(warn_from unsupported_from)],
188             [qw(warn_from fatal_from)],
189             [qw(unsupported_from fatal_from)],
190             ) {
191 72 100 100     330 if(
      100        
192             exists($_froms{$pair->[0]}) && exists($_froms{$pair->[1]}) &&
193             !($_froms{$pair->[0]} < $_froms{$pair->[1]})
194             ) {
195 3         12 die(sprintf("%s: %s must be before %s\n", __PACKAGE__, @{$pair}));
  3         36  
196             }
197             }
198              
199 22 100       96 if($class->is_deprecated($args)) {
200 16         36592 my $reason = $class->reason($args);
201              
202 16 100 100     272 if($_froms{fatal_from} && $_froms{fatal_from} < $now) {
    100 100        
    100          
203             die(_fatal_msg(
204 2         4 %{$args->{_source}},
  2         8  
205             reason => $reason
206             ));
207             } elsif($_froms{unsupported_from} && $_froms{unsupported_from} < $now) {
208             warn(_unsupported_msg(
209 2         3 %{$args->{_source}},
  2         9  
210             reason => $reason
211             ));
212             } elsif($_froms{warn_from} < $now) { # warn_from always exists!
213             warn(_warn_msg(
214 10         52 %{$args->{_source}},
215             reason => $reason,
216             date => (
217             sort { $a <=> $b }
218             map { $_froms{$_} }
219 10   100     26 grep { $_froms{$_} }
220             qw(unsupported_from fatal_from)
221             )[0] || undef
222             ));
223             }
224             }
225             }
226             }
227              
228             sub _fatal_msg {
229 4     4   13 my %args = @_;
230 4         52 return "Unsupported! In $args{filename} on line $args{line}: $args{reason}\n";
231             }
232              
233 2     2   9 sub _unsupported_msg { return _fatal_msg(@_); }
234              
235             sub _warn_msg {
236 10     10   46 my %args = @_;
237             return "Deprecation warning! ".
238 10 100       172 ($args{date} ? 'From '.$args{date}->iso8601().': ' : '').
239             "In $args{filename} on line $args{line}: $args{reason}\n";
240             }
241              
242             =head1 FUNCTIONS
243              
244             There are no public functions or methods, everything is done when the
245             module is loaded (specifically, when its C method is called)
246             with all specific deprecations handled by plugins.
247              
248             =head1 WRITING YOUR OWN PLUGINS
249              
250             The C namespace is yours to play in, except
251             for the C namespace.
252              
253             A plugin should inherit from C, and implement the following
254             methods, which will be called as class methods. Failure to define either of
255             them will result in fatal errors. They will be passed the arguments hash-ref
256             (with C, C, and C removed):
257              
258             =over
259              
260             =item reason
261              
262             Returns a brief string explaining the deprecation. For example "32 bit
263             integers" or "Perl too old".
264              
265             =item is_deprecated
266              
267             This should return true or false for whether the environment matches the
268             deprecation or not.
269              
270             =back
271              
272             =head1 FEEDBACK
273              
274             I welcome feedback about my code, including constructive criticism, bug
275             reports, documentation improvements, and feature requests. The best bug reports
276             include files that I can add to the test suite, which fail with the current
277             code in my git repo and will pass once I've fixed the bug
278              
279             Feature requests are far more likely to get implemented if you submit a patch
280             yourself, preferably with tests.
281              
282             =head1 SOURCE CODE REPOSITORY
283              
284             L
285              
286             =head1 SEE ALSO
287              
288             L - for deprecating parts of your own code as opposed
289             to parts of the environment your code is running in;
290              
291             =head1 AUTHOR, LICENCE and COPYRIGHT
292              
293             Copyright 2023 David Cantrell EFE
294              
295             This software is free-as-in-speech software, and may be used, distributed, and
296             modified under the terms of either the GNU General Public Licence version 2 or
297             the Artistic Licence. It's up to you which one you use. The full text of the
298             licences can be found in the files GPL2.txt and ARTISTIC.txt, respectively.
299              
300             =head1 CONSPIRACY
301              
302             This module is also free-as-in-mason software.
303              
304             =cut
305              
306             1;