File Coverage

blib/lib/Devel/Deprecations/Environmental.pm
Criterion Covered Total %
statement 55 55 100.0
branch 26 26 100.0
condition 16 16 100.0
subroutine 9 9 100.0
pod n/a
total 106 106 100.0


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