File Coverage

blib/lib/MooseX/ClassCompositor.pm
Criterion Covered Total %
statement 86 87 98.8
branch 20 22 90.9
condition 6 9 66.6
subroutine 13 13 100.0
pod 1 1 100.0
total 126 132 95.4


line stmt bran cond sub pod time code
1             package MooseX::ClassCompositor 0.010;
2             # ABSTRACT: a factory that builds classes from roles
3              
4 4     4   249998 use Moose;
  4         1613350  
  4         27  
5              
6 4     4   28658 use namespace::autoclean;
  4         27965  
  4         15  
7              
8 4     4   227 use Class::Load;
  4         8  
  4         134  
9 4     4   21 use Moose::Util qw(apply_all_roles);
  4         7  
  4         34  
10 4     4   910 use Moose::Util::MetaRole ();
  4         8  
  4         81  
11 4     4   1700 use MooseX::StrictConstructor::Trait::Class;
  4         47491  
  4         140  
12 4     4   2008 use MooseX::Types::Perl qw(PackageName);
  4         392661  
  4         47  
13 4     4   8547 use Scalar::Util qw(refaddr);
  4         11  
  4         280  
14 4     4   1872 use String::RewritePrefix;
  4         3295  
  4         20  
15              
16             #pod =head1 SYNOPSIS
17             #pod
18             #pod my $comp = MooseX::ClassCompositor->new({
19             #pod class_basename => 'MyApp::Class',
20             #pod class_metaroles => {
21             #pod class => [ 'MooseX::StrictConstructor::Trait::Class' ],
22             #pod },
23             #pod role_prefixes => {
24             #pod '' => 'MyApp::Role::',
25             #pod '=' => '',
26             #pod },
27             #pod });
28             #pod
29             #pod my $class = $comp->class_for( qw( PieEater ContestWinner ) );
30             #pod
31             #pod my $object = $class->new({
32             #pod pie_type => 'banana',
33             #pod place => '2nd',
34             #pod });
35             #pod
36             #pod =head1 OVERVIEW
37             #pod
38             #pod A MooseX::ClassCompositor is a class factory. If you think using a class
39             #pod factory will make you feel like a filthy "enterprise" programmer, maybe you
40             #pod should turn back now.
41             #pod
42             #pod The compositor has a C<L</class_for>> method that builds a class by combining a
43             #pod list of roles with L<Moose::Object>, applying any supplied metaclass, and
44             #pod producing an arbitrary-but-human-scannable name. The metaclass is then
45             #pod made immutable, the operation is memoized, and the class name is returned.
46             #pod
47             #pod In the L</SYNOPSIS> above, you can see all the major features used:
48             #pod C<class_metaroles> to enable strict constructors, C<role_prefixes> to use
49             #pod L<String::RewritePrefix> to expand role name shorthand, and C<class_basename>
50             #pod to pick a namespace under which to put constructed classes.
51             #pod
52             #pod Not shown is the C<L</known_classes>> method, which returns a list of pairs
53             #pod describing all the classes that the factory has constructed. This method can
54             #pod be useful for debugging and other somewhat esoteric purposes like
55             #pod serialization.
56             #pod
57             #pod =cut
58              
59             #pod =attr class_basename
60             #pod
61             #pod This attribute must be given, and must be a valid Perl package name.
62             #pod Constructed classes will all be under this namespace.
63             #pod
64             #pod =cut
65              
66             has class_basename => (
67             is => 'ro',
68             isa => PackageName,
69             required => 1,
70             );
71              
72             #pod =attr class_metaroles
73             #pod
74             #pod This attribute, if given, must be a hashref of class metaroles that will be
75             #pod applied to newly-constructed classes with
76             #pod L<Moose::Util::MetaRole/apply_metaroles>.
77             #pod
78             #pod =cut
79              
80             has class_metaroles => (
81             reader => '_class_metaroles',
82             isa => 'HashRef',
83             default => sub { {} },
84             );
85              
86             #pod =attr known_classes
87             #pod
88             #pod This attribute stores a mapping of class names to the parameters used to
89             #pod construct them. The C<known_classes> method returns its contents as a list of
90             #pod pairs.
91             #pod
92             #pod =cut
93              
94             has known_classes => (
95             reader => '_known_classes',
96             isa => 'HashRef',
97             traits => [ 'Hash' ],
98             handles => {
99             _learn_class => 'set',
100             known_classes => 'elements',
101             },
102             init_arg => undef,
103             default => sub { {} },
104             );
105              
106             #pod =attr role_prefixes
107             #pod
108             #pod This attribute is used as the arguments to L<String::RewritePrefix> for
109             #pod expanding role names passed to the compositor's L<class_for> method.
110             #pod
111             #pod =cut
112              
113             has role_prefixes => (
114             reader => '_role_prefixes',
115             isa => 'HashRef',
116             default => sub { {} },
117             );
118              
119             sub _rewrite_roles {
120 11     11   21 my $self = shift;
121 11         338 String::RewritePrefix->rewrite($self->_role_prefixes, @_);
122             }
123              
124             #pod =attr fixed_roles
125             #pod
126             #pod This attribute may be initialized with an arrayref of role names and/or
127             #pod L<Moose::Meta::Role> objects. These roles will I<always> be composed in
128             #pod the classes built by the compositor.
129             #pod
130             #pod Role names (but not Moose::Meta::Role objects) I<will> be rewritten by
131             #pod the role prefixes.
132             #pod
133             #pod =cut
134              
135             has fixed_roles => (
136             reader => '_fixed_roles',
137             isa => 'ArrayRef',
138             default => sub { [] },
139             );
140              
141             has serial_counter => (
142             reader => '_serial_counter',
143             isa => 'Str',
144             default => 'AA',
145             traits => [ 'String' ],
146             handles => { next_serial => 'inc' },
147             init_arg => undef,
148             );
149              
150             has memoization_table => (
151             isa => 'HashRef',
152             default => sub { {} },
153             traits => [ 'Hash' ],
154             handles => {
155             _class_for_key => 'get',
156             _set_class_for_key => 'set',
157             },
158             init_arg => undef,
159             );
160              
161             #pod =attr forbid_meta_role_objects
162             #pod
163             #pod If true, an exception will be raised if a Moose::Meta::Role object is passed to
164             #pod C<L</class_for>>. This is only rarely useful, such as if it's a strict
165             #pod requirement that the memoization table of the compositor be serializable and
166             #pod its contents reproduceable.
167             #pod
168             #pod Probably you don't need this.
169             #pod
170             #pod =cut
171              
172             has forbid_meta_role_objects => (
173             is => 'ro',
174             isa => 'Bool',
175             default => 0,
176             );
177              
178             #pod =method class_for
179             #pod
180             #pod my $class = $compositor->class_for(
181             #pod
182             #pod 'Role::Name', # <-- will be expanded with role_prefixes
183             #pod Other::Role->meta, # <-- will not be touched
184             #pod
185             #pod [
186             #pod 'Param::Role::Name', # <-- will be expanded with role_prefixes
187             #pod 'ApplicationName', # <-- will not be touched
188             #pod { ...param... },
189             #pod ],
190             #pod );
191             #pod
192             #pod This method will return a class with the roles passed to it. They can be given
193             #pod either as names (which will be expanded according to C<L</role_prefixes>>), as
194             #pod L<Moose::Meta::Role> objects, or as arrayrefs containing a role name,
195             #pod application name, and hashref of parameters. In the arrayref form, the
196             #pod application name is just a name used to uniquely identify this application of
197             #pod a parameterized role, so that they can be applied multiple times with each
198             #pod application accounted for internally.
199             #pod
200             #pod =cut
201              
202             sub class_for {
203 22     22 1 31498 my ($self, @args) = @_;
204              
205             # can't use memoize without losing subclassability, so we reimplemented
206             # -- rjbs, 2011-08-05
207 22         69 my $memo_key = $self->_memoization_key(\@args);
208 22 100       880 if (my $cached = $self->_class_for_key($memo_key)) {
209 12         47 return $cached;
210             }
211              
212             # Arguments here are role names, or role objects followed by nonce-names.
213 10         30 my @orig_args = @args;
214              
215             # $role_hash is a hash mapping nonce-names to role objects
216             # $role_names is an array of names of more roles to add
217 10         21 my (@roles, @role_class_names, @all_names);
218              
219 10         26 while (@args) {
220 14         26 my $name = shift @args;
221 14 100 66     81 if (ref $name eq 'ARRAY') {
    100          
222 2         4 my ($role_name, $moniker, $params) = @$name;
223              
224 2         7 my $full_name = $self->_rewrite_roles($role_name);
225 2         119 Class::Load::load_class($full_name);
226 2         76 my $role_object = $full_name->meta->generate_role(
227             parameters => $params,
228             );
229              
230 2         7550 push @roles, $role_object;
231 2         7 $name = $moniker;
232             } elsif (blessed $name and $name->isa('Moose::Meta::Role')) {
233 5 100       151 confess "this class compositor does not allow role objects"
234             if $self->forbid_meta_role_objects;
235              
236 4         8 push @roles, $name;
237 4         11 $name = $name->name;
238             } else {
239 7         13 push @role_class_names, $name;
240             }
241              
242 13 100       40 $name =~ s/::/_/g if @all_names;
243 13         31 $name =~ s/^=//;
244              
245 13         30 push @all_names, $name;
246             }
247              
248 9         256 my $name = join q{::}, $self->class_basename, @all_names;
249              
250 9         20 for my $r (@{ $self->_fixed_roles }) {
  9         263  
251 3 100 66     19 if (blessed $r and $r->isa('Moose::Meta::Role')) {
252 1         3 push @roles, $r;
253             } else {
254 2         6 push @role_class_names, $r;
255             }
256             }
257              
258 9         33 @role_class_names = $self->_rewrite_roles(@role_class_names);
259              
260             # We only _try_ to load because in use, some of these are embedded in other
261             # packages. While we'd like to stop relying on this, this is an expedient
262             # change. After all, it'll fail during composition, if the role package is
263             # not properly set up. -- rjbs, 2018-06-21
264 9         555 Class::Load::try_load_class($_) for @role_class_names;
265              
266 9 100       21519 if ($name->can('meta')) {
267 1         37 $name .= "_" . $self->next_serial;
268             }
269              
270 9         84 my $class = Moose::Meta::Class->create( $name => (
271             superclasses => [ 'Moose::Object' ],
272             ));
273              
274 9         21257 $class = Moose::Util::MetaRole::apply_metaroles(
275             for => $class->name,
276             class_metaroles => $self->_class_metaroles,
277             );
278              
279 9         30279 apply_all_roles($class, @role_class_names, map $_->name, @roles);
280              
281 9         119208 $class->make_immutable;
282              
283 9         57352 $self->_learn_class($name, \@orig_args);
284 9         433 $self->_set_class_for_key($memo_key, $name);
285              
286 9         103 return $class->name;
287             }
288              
289             sub _memoization_key {
290 22     22   44 my ($self, $args) = @_;
291 22         43 my @args = @$args;
292              
293 22         36 my @k;
294 22         60 while (@args) {
295 32         52 my $arg = shift @args;
296 32 100 66     144 if (ref $arg eq 'ARRAY') {
    100          
297 6         13 my ($role_name, $moniker, $params) = @$arg;
298 6         20 push @k, "$moniker : { " . __hash_to_string($params) . " }";
299             } elsif (blessed $arg and $arg->isa('Moose::Meta::Role')) {
300 5         29 push @k, $arg->name;
301             } else {
302 21         51 push @k, $arg;
303             }
304             }
305 22         79 my $key = join "; ", sort @k;
306 22         54 return $key;
307             }
308              
309             sub __hash_to_string {
310 6     6   12 my ($h) = @_;
311 6         9 my @k;
312 6         23 for my $k (sort keys %$h) {
313             my $v = ! defined($h->{$k}) ? "<undef>" :
314 6 50       24 ref($h->{$k}) ? join("-", @{$h->{$k}}) : $h->{$k};
  0 50       0  
315 6         19 push @k, "$k => $v";
316             }
317 6         25 join ", " => @k;
318             }
319              
320             #pod =head1 THANKS
321             #pod
322             #pod Thanks to Pobox.com for sponsoring the development of this library.
323             #pod
324             #pod =cut
325              
326             __PACKAGE__->meta->make_immutable;
327             1;
328              
329             __END__
330              
331             =pod
332              
333             =encoding UTF-8
334              
335             =head1 NAME
336              
337             MooseX::ClassCompositor - a factory that builds classes from roles
338              
339             =head1 VERSION
340              
341             version 0.010
342              
343             =head1 SYNOPSIS
344              
345             my $comp = MooseX::ClassCompositor->new({
346             class_basename => 'MyApp::Class',
347             class_metaroles => {
348             class => [ 'MooseX::StrictConstructor::Trait::Class' ],
349             },
350             role_prefixes => {
351             '' => 'MyApp::Role::',
352             '=' => '',
353             },
354             });
355              
356             my $class = $comp->class_for( qw( PieEater ContestWinner ) );
357              
358             my $object = $class->new({
359             pie_type => 'banana',
360             place => '2nd',
361             });
362              
363             =head1 OVERVIEW
364              
365             A MooseX::ClassCompositor is a class factory. If you think using a class
366             factory will make you feel like a filthy "enterprise" programmer, maybe you
367             should turn back now.
368              
369             The compositor has a C<L</class_for>> method that builds a class by combining a
370             list of roles with L<Moose::Object>, applying any supplied metaclass, and
371             producing an arbitrary-but-human-scannable name. The metaclass is then
372             made immutable, the operation is memoized, and the class name is returned.
373              
374             In the L</SYNOPSIS> above, you can see all the major features used:
375             C<class_metaroles> to enable strict constructors, C<role_prefixes> to use
376             L<String::RewritePrefix> to expand role name shorthand, and C<class_basename>
377             to pick a namespace under which to put constructed classes.
378              
379             Not shown is the C<L</known_classes>> method, which returns a list of pairs
380             describing all the classes that the factory has constructed. This method can
381             be useful for debugging and other somewhat esoteric purposes like
382             serialization.
383              
384             =head1 PERL VERSION
385              
386             This library should run on perls released even a long time ago. It should work
387             on any version of perl released in the last five years.
388              
389             Although it may work on older versions of perl, no guarantee is made that the
390             minimum required version will not be increased. The version may be increased
391             for any reason, and there is no promise that patches will be accepted to lower
392             the minimum required perl.
393              
394             =head1 ATTRIBUTES
395              
396             =head2 class_basename
397              
398             This attribute must be given, and must be a valid Perl package name.
399             Constructed classes will all be under this namespace.
400              
401             =head2 class_metaroles
402              
403             This attribute, if given, must be a hashref of class metaroles that will be
404             applied to newly-constructed classes with
405             L<Moose::Util::MetaRole/apply_metaroles>.
406              
407             =head2 known_classes
408              
409             This attribute stores a mapping of class names to the parameters used to
410             construct them. The C<known_classes> method returns its contents as a list of
411             pairs.
412              
413             =head2 role_prefixes
414              
415             This attribute is used as the arguments to L<String::RewritePrefix> for
416             expanding role names passed to the compositor's L<class_for> method.
417              
418             =head2 fixed_roles
419              
420             This attribute may be initialized with an arrayref of role names and/or
421             L<Moose::Meta::Role> objects. These roles will I<always> be composed in
422             the classes built by the compositor.
423              
424             Role names (but not Moose::Meta::Role objects) I<will> be rewritten by
425             the role prefixes.
426              
427             =head2 forbid_meta_role_objects
428              
429             If true, an exception will be raised if a Moose::Meta::Role object is passed to
430             C<L</class_for>>. This is only rarely useful, such as if it's a strict
431             requirement that the memoization table of the compositor be serializable and
432             its contents reproduceable.
433              
434             Probably you don't need this.
435              
436             =head1 METHODS
437              
438             =head2 class_for
439              
440             my $class = $compositor->class_for(
441              
442             'Role::Name', # <-- will be expanded with role_prefixes
443             Other::Role->meta, # <-- will not be touched
444              
445             [
446             'Param::Role::Name', # <-- will be expanded with role_prefixes
447             'ApplicationName', # <-- will not be touched
448             { ...param... },
449             ],
450             );
451              
452             This method will return a class with the roles passed to it. They can be given
453             either as names (which will be expanded according to C<L</role_prefixes>>), as
454             L<Moose::Meta::Role> objects, or as arrayrefs containing a role name,
455             application name, and hashref of parameters. In the arrayref form, the
456             application name is just a name used to uniquely identify this application of
457             a parameterized role, so that they can be applied multiple times with each
458             application accounted for internally.
459              
460             =head1 THANKS
461              
462             Thanks to Pobox.com for sponsoring the development of this library.
463              
464             =head1 AUTHORS
465              
466             =over 4
467              
468             =item *
469              
470             Ricardo Signes <cpan@semiotic.systems>
471              
472             =item *
473              
474             Mark Jason Dominus <mjd@cpan.org>
475              
476             =back
477              
478             =head1 CONTRIBUTORS
479              
480             =for stopwords Mohammad S Anwar Ricardo Signes Toby Inkster
481              
482             =over 4
483              
484             =item *
485              
486             Mohammad S Anwar <mohammad.anwar@yahoo.com>
487              
488             =item *
489              
490             Ricardo Signes <rjbs@semiotic.systems>
491              
492             =item *
493              
494             Toby Inkster <tobyink@cpan.org>
495              
496             =back
497              
498             =head1 COPYRIGHT AND LICENSE
499              
500             This software is copyright (c) 2011 by Ricardo Signes.
501              
502             This is free software; you can redistribute it and/or modify it under
503             the same terms as the Perl 5 programming language system itself.
504              
505             =cut