File Coverage

blib/lib/Object/PadX/Enum.pm
Criterion Covered Total %
statement 155 157 98.7
branch 40 52 76.9
condition 5 8 62.5
subroutine 24 24 100.0
pod n/a
total 224 241 92.9


line stmt bran cond sub pod time code
1             package Object::PadX::Enum 0.02;
2              
3 14     14   1773134 use v5.22;
  14         81  
4 8     8   57 use warnings;
  8         17  
  8         509  
5              
6 8     8   50 use Carp;
  8         18  
  8         651  
7 8     8   5457 use Object::Pad 0.825 ();
  8         106752  
  8         1159  
8 8     8   68 use Object::Pad::MOP::Class qw( :experimental(mop) );
  8         17  
  8         59  
9              
10             # Loaded for its XS keyword registrations
11             require XSLoader;
12             XSLoader::load( __PACKAGE__, our $VERSION );
13              
14             =encoding UTF-8
15              
16             =for highlighter language=perl
17              
18             =head1 NAME
19              
20             C - syntactic sugar for enum-like singleton-bearing C classes
21              
22             =head1 SYNOPSIS
23              
24             use Object::PadX::Enum;
25              
26             enum Raptor {
27             item VELOCIRAPTOR ( max_speed_kmh => 60, max_weight_kg => 15, max_height_cm => 50 );
28             item DEINONYCHUS ( max_speed_kmh => 50, max_weight_kg => 80, max_height_cm => 87 );
29             item UTAHRAPTOR ( max_speed_kmh => 35, max_weight_kg => 500, max_height_cm => 150 );
30             item MICRORAPTOR ( max_speed_kmh => 40, max_weight_kg => 1, max_height_cm => 30 );
31             item DROMAEOSAURUS ( max_speed_kmh => 60, max_weight_kg => 15, max_height_cm => 50 );
32              
33             field $max_speed_kmh :param :reader;
34             field $max_weight_kg :param :reader;
35             field $max_height_cm :param :reader;
36              
37             method speed_per_kg { return $max_speed_kmh / $max_weight_kg }
38             method speed_per_cm { return $max_speed_kmh / $max_height_cm }
39              
40             method fastest :common {
41             my ( $top ) = sort { $b->max_speed_kmh <=> $a->max_speed_kmh } $class->values;
42             return $top;
43             }
44             }
45              
46             say Raptor->VELOCIRAPTOR->max_speed_kmh; # 60
47             say Raptor->DEINONYCHUS->speed_per_kg; # 0.625
48             say Raptor->from_ordinal(2)->name; # UTAHRAPTOR
49             say Raptor->from_name("MICRORAPTOR")->speed_per_cm; # 1.33333333333333
50             say 'Fastest in absolute terms: ', Raptor->fastest->name; # VELOCIRAPTOR or DROMAEOSAURUS (tie)
51              
52             =head1 DESCRIPTION
53              
54             C adds two keywords on top of L:
55              
56             =over 4
57              
58             =item * C
59              
60             Declares a class (using L's C machinery) and auto-injects
61             C<$ordinal :reader> and C fields. The C reader returns the
62             identifier under which the singleton was declared (e.g. C<"RED">). Inside the
63             block, all normal C constructs (C, C, C,
64             ...) are available, plus the C keyword.
65              
66             The following class-level attributes are accepted:
67              
68             =over 4
69              
70             =item C<:isa(CLASS)>, C<:isa(CLASS VERSION)>
71              
72             =item C<:extends(CLASS)>, C<:extends(CLASS VERSION)>
73              
74             Declares a superclass; equivalent to L's C<:isa>. The package is
75             loaded automatically. If a VERSION is given, C<< CLASS->VERSION(VERSION) >> is
76             called to enforce it.
77              
78             An C may inherit from another C. Fields, methods, roles and
79             C phasers from the parent are inherited normally. The parent's
80             B are I inherited: the child has its own ordinal-zero-based item
81             sequence, and accessing a parent item name on the child raises an error. The
82             child's C, C and C see only the child's
83             items. A parent enum must be finalized (i.e. its declaration must have
84             already executed at runtime) before a child enum that inherits from it; in
85             practice this is satisfied by normal source ordering and C ordering.
86              
87             =item C<:does(ROLE)>, C<:does(ROLE VERSION)>
88              
89             Composes a role into the enum class. May be repeated for multiple roles. The
90             role package is loaded automatically.
91              
92             =back
93              
94             The class attributes C<:abstract>, C<:strict>, C<:repr> and C<:lexical_new>
95             are not supported. C<:abstract> is semantically incompatible with C
96             (singletons cannot be constructed for an abstract class); the others have no
97             public L entry point and would require reaching into
98             private Object::Pad internals.
99              
100             =item * C
101              
102             Declares a named singleton instance of the enclosing C. C is the
103             key/value list passed to the auto-generated constructor; the parentheses (and
104             the arg list) are optional, so C is equivalent to C.
105              
106             =back
107              
108             After the C block closes, the following class-level methods are
109             installed on the enum class for each declared singleton C:
110              
111             $singleton = ClassName->NAME; # the named singleton
112             @all = ClassName->values; # all singletons in declaration order
113             $byord = ClassName->from_ordinal(0);
114             $byname = ClassName->from_name("RED");
115              
116             Direct construction via C<< ClassName->new(...) >> is blocked after the
117             C block closes; the only ways to obtain a singleton are the per-item
118             accessor, C, and C. Subclasses (whether plain
119             C or another C) may still call C on themselves; the block
120             applies only to direct invocation on the enum class itself.
121              
122             =head1 CAVEATS
123              
124             =over 4
125              
126             =item *
127              
128             User Cs require explicit C<:param> if you intend to set them via
129             C args. C does I inject C<:param> automatically.
130              
131             =item *
132              
133             Singletons are constructed at the runtime of the compilation unit that
134             contains the C declaration, after that unit's C phase. They
135             are therefore not visible from earlier C/C blocks of the
136             same unit. Normal runtime code (including code inside C and
137             C blocks executed during main runtime) sees them as expected.
138              
139             =item *
140              
141             C-level C<:abstract>, C<:strict>, C<:repr> and C<:lexical_new> are not
142             supported. See the description of the C keyword above for the rationale;
143             C<:isa> and C<:does> I supported.
144              
145             =item *
146              
147             The names C, C, C, C and C are
148             reserved and must not be used as C names.
149              
150             =back
151              
152             =cut
153              
154             # Per-class state captured during compilation.
155             # $Pending{$class} = { meta => $meta, items => [ [ $name, \@args, $line ], ... ], seen => { $name => 1 } }
156             my %Pending;
157              
158             # Permanent per-class registry of finalized enum item names, in declaration
159             # order. Populated by `_finalize_enum`. Queried by descendant enum finalizes
160             # (to shadow inherited item accessors) and could be useful for introspection
161             # in the future. Keys are class names; values are arrayrefs of item names.
162             my %EnumItems;
163              
164             my %RESERVED_ITEM_NAMES = map { $_ => 1 } qw(
165             values from_ordinal from_name ordinal name
166             new BUILD DOES META
167             );
168              
169             sub import {
170 20     20   555079 my $class = shift;
171 20         60 my $caller = caller;
172              
173 20         88 $^H{ 'Object::PadX::Enum/enum' } = 1;
174 20         67 $^H{ 'Object::PadX::Enum/item' } = 1;
175              
176 20         98 Object::Pad->import_into( $caller );
177             }
178              
179             # Attributes that have a documented public-MOP entry point.
180             my %ENUM_ATTR_HANDLERS = (
181             isa => \&_attr_isa,
182             extends => \&_attr_isa,
183             does => \&_attr_does,
184             );
185              
186             # Attributes that exist on Object::Pad's `class` keyword but are deliberately
187             # rejected here. The message explains why so users aren't left guessing.
188             my %ENUM_ATTR_REJECTED = (
189             abstract => "':abstract' is incompatible with enum: singleton values cannot be constructed for an abstract class",
190             strict => "':strict' is not supported on enum (no public Object::Pad MOP entry point); declare a plain 'class' instead",
191             repr => "':repr' is not supported on enum (no public Object::Pad MOP entry point); declare a plain 'class' instead",
192             lexical_new => "':lexical_new' is not supported on enum (no public Object::Pad MOP entry point); declare a plain 'class' instead",
193             );
194              
195             # Load $pkg via `require`, mirroring Object::Pad's :isa/:does autoload. Returns
196             # silently on success; croaks on failure.
197             sub _require_package {
198 14     14   26 my ( $pkg, $for ) = @_;
199              
200             # Skip require for packages already defined inline (no .pm needed).
201 8     8   4155 no strict 'refs';
  8         17  
  8         19414  
202 14 100       20 keys %{ "${pkg}::" } and return;
  14         75  
203              
204 1         8 ( my $file = "$pkg.pm" ) =~ s{::}{/}g;
205 1 50       3 eval { require $file; 1 }
  1         465  
  0         0  
206             or croak "Failed to load package '$pkg' for $for: $@";
207              
208 0         0 return;
209             }
210              
211             # Parse "Pkg" or "Pkg VER" into ($pkg, $ver). $ver is undef when absent.
212             sub _split_versioned_pkg {
213 14     14   30 my ( $raw, $attr_name ) = @_;
214              
215 14 50 33     71 defined $raw && length $raw
216             or croak "Attribute ':$attr_name' requires a value";
217              
218 14         50 my ( $pkg, $ver, $extra ) = split /\s+/, $raw, 3;
219 14 50       34 defined $extra
220             and croak "Attribute ':$attr_name($raw)' has too many parts; expected 'PACKAGE' or 'PACKAGE VERSION'";
221              
222 14         45 return ( $pkg, $ver );
223             }
224              
225             sub _attr_isa {
226 11     11   23 my ( $state, $value ) = @_;
227              
228             exists $state->{ isa }
229 11 100       181 and croak "Multiple ':isa' / ':extends' attributes on enum '$state->{name}'";
230              
231 10         28 my ( $pkg, $ver ) = _split_versioned_pkg( $value, 'isa' );
232 10         44 _require_package( $pkg, "':isa($pkg)' on enum '$state->{name}'" );
233 9 100       77 defined $ver and $pkg->VERSION( $ver );
234              
235 8         19 $state->{ isa } = $pkg;
236 8         22 return;
237             }
238              
239             sub _attr_does {
240 4     4   9 my ( $state, $value ) = @_;
241              
242 4         9 my ( $pkg, $ver ) = _split_versioned_pkg( $value, 'does' );
243 4         16 _require_package( $pkg, "':does($pkg)' on enum '$state->{name}'" );
244 4 50       8 defined $ver and $pkg->VERSION( $ver );
245              
246 4         6 push @{ $state->{ roles } }, $pkg;
  4         10  
247 4         10 return;
248             }
249              
250             # Called by XS at compile-time when `enum NAME ATTRS? {` is encountered.
251             sub _begin_enum {
252 28     28   3941 my ( $name, $attrs ) = @_;
253              
254 28 50       119 exists $Pending{ $name }
255             and croak "Cannot declare enum '$name'; already being defined";
256              
257 28         130 my $state = { name => $name, roles => [] };
258 28   50     72 for my $pair ( @{ $attrs // [] } ) {
  28         133  
259 19         52 my ( $attr, $value ) = @$pair;
260              
261 19 100       62 if ( my $msg = $ENUM_ATTR_REJECTED{ $attr } ) {
262 3         510 croak "$msg (enum '$name')";
263             }
264              
265 16 100       211 my $handler = $ENUM_ATTR_HANDLERS{ $attr }
266             or croak "Unrecognised attribute ':$attr' on enum '$name'";
267              
268 15         35 $handler->( $state, $value );
269             }
270              
271 21         64 my @begin_args = ( $name );
272             exists $state->{ isa }
273 21 100       83 and push @begin_args, ( isa => $state->{ isa } );
274              
275 21         91 my $meta = Object::Pad::MOP::Class->begin_class( @begin_args );
276              
277 21         1613 $meta->add_role( $_ ) for @{ $state->{ roles } };
  21         104  
278              
279             # $ordinal and $_name are reader-only (not :param) so user item args cannot
280             # override them; both are stamped after construction in _finalize_enum.
281 21     14   1191 $meta->add_field( '$ordinal', reader => 'ordinal' );
  14         64  
  14         84  
282 21     8   905 $meta->add_field( '$_name', reader => 'name' );
  8         96  
  8         78  
283              
284 21         126 $Pending{ $name } = { meta => $meta, items => [], seen => {} };
285              
286 21         22597 return;
287             }
288              
289             # Called at runtime, in source order, for each `item NAME(args)` statement.
290             sub _register_item {
291 34     34   1509789 my ( $class, $name, $line, @args ) = @_;
292              
293 34 50       151 my $entry = $Pending{ $class }
294             or croak "Internal error: item '$name' for unknown enum '$class' at line $line";
295              
296 33 100       368 $entry->{ seen }{ $name }
297             and croak "Duplicate item '$name' in enum '$class' at line $line";
298              
299 32 100       450 $RESERVED_ITEM_NAMES{ $name }
300             and croak "item name '$name' is reserved in enum '$class' at line $line";
301              
302 33         11543 push @{ $entry->{ items } }, [ $name, \@args, $line ];
  33         129  
303 30         101 $entry->{ seen }{ $name } = 1;
304              
305 30         102 return;
306             }
307              
308             # Called at runtime, once, after all item statements for the enum have run.
309             sub _finalize_enum {
310 18     18   90 my ( $class ) = @_;
311              
312 18 50       78 my $entry = delete $Pending{ $class }
313             or croak "Internal error: _finalize_enum on unknown enum '$class'";
314              
315 18         64 my $meta = $entry->{ meta };
316 18         261 my $ord_field = $meta->get_field( '$ordinal' );
317 18         74 my $name_field = $meta->get_field( '$_name' );
318 18         39 my @ordered;
319              
320 18         38 my $n = 0;
321 18         46 for my $item ( @{ $entry->{ items } } ) {
  18         60  
322 29         76 my ( $name, $args, $line ) = @$item;
323              
324 29         50 my $instance = eval { $class->new( @$args ) };
  29         299  
325 29 50       310 $@ and croak "Failed to construct enum value '$name' of '$class' at line $line: $@";
326              
327             # Stamp the ordinal and name after construction so they aren't user-facing :params.
328 29         107 $ord_field->value( $instance ) = $n;
329 29         87 $name_field->value( $instance ) = $name;
330              
331 29         74 push @ordered, [ $name, $instance ];
332 29         68 $n++;
333             }
334              
335 8     8   74 no strict 'refs';
  8         40  
  8         475  
336 8     8   64 no warnings 'redefine';
  8         20  
  8         11690  
337              
338 18         70 my %own_names;
339 18         45 for my $pair ( @ordered ) {
340 29         69 my ( $name, $instance ) = @$pair;
341 29         79 $own_names{ $name } = 1;
342 29     51   160 *{ "${class}::${name}" } = sub { $instance };
  29         217  
  48         9730  
343             }
344              
345 18         125 *{ "${class}::values" } = sub {
346 5     5   35 return map { $_->[1] } @ordered;
  11         28  
347 18         74 };
348              
349 18         93 *{ "${class}::from_ordinal" } = sub {
350 6     6   23 my ( undef, $idx ) = @_;
351 6 50       27 defined $idx or return undef;
352 6 100 100     53 $idx >= 0 && $idx < @ordered or return undef;
353 4         23 return $ordered[ $idx ][ 1 ];
354 18         124 };
355              
356 18         140 *{ "${class}::from_name" } = sub {
357 6     6   1424 my ( undef, $want ) = @_;
358 6 50       24 defined $want or return undef;
359 6         44 for my $pair ( @ordered ) {
360 13 100       78 return $pair->[1] if $pair->[0] eq $want;
361             }
362 2         11 return undef;
363 18         158 };
364              
365             # Shadow ancestor enum items not redefined locally. A child enum inherits
366             # fields/methods from a parent enum but loses the parent's items: accessing
367             # a parent item name on the child raises a clear error rather than
368             # returning the parent's singleton via MRO.
369 18         141 require mro;
370 18         95 my $linear = mro::get_linear_isa( $class );
371 18         36 my %shadowed;
372 18         47 for my $ancestor ( @$linear ) {
373 44 100       146 next if $ancestor eq $class;
374 26 100       97 my $ancestor_items = $EnumItems{ $ancestor } or next;
375 4         9 for my $aname ( @$ancestor_items ) {
376 8 100       53 next if $own_names{ $aname };
377 7 50       16 next if $shadowed{ $aname };
378 7         18 $shadowed{ $aname } = $ancestor;
379 7         19 my $msg = "'$aname' is not an item of '$class' (inherited from '$ancestor', shadowed)";
380 7     3   27 *{ "${class}::${aname}" } = sub { croak $msg };
  7         65  
  3         2559  
381             }
382             }
383              
384             # Register before installing the `new` override so any descendant enum
385             # whose finalize runs later (and which calls our `new` via MRO) sees us in
386             # the registry.
387 18         51 $EnumItems{ $class } = [ map { $_->[0] } @ordered ];
  29         100  
388              
389             # Block external construction. Capture the original Object::Pad-generated
390             # `new` so subclass enums (and plain subclasses) can pass through during
391             # their own construction; only direct calls on the enum class itself are
392             # rejected.
393 18         43 my @item_names = map { $_->[0] } @ordered;
  29         69  
394 18         34 my $orig_new = \&{ "${class}::new" };
  18         92  
395              
396 18         48 my $new_msg = "Cannot construct new instances of enum class '$class' directly";
397 18 100       62 if ( @item_names ) {
398 16         68 $new_msg .= '; use one of: ' . join( ', ', @item_names );
399 16         43 $new_msg .= " (or ${class}->from_name / ${class}->from_ordinal)";
400             }
401              
402 18         116 *{ "${class}::new" } = sub {
403 5     5   18642 my $invocant = shift;
404 5 50       29 $invocant ne $class
405             and return $invocant->$orig_new( @_ );
406 5         835 croak $new_msg;
407 18         127 };
408              
409 18         229 return;
410             }
411              
412             0x55AA;