File Coverage

blib/lib/MooseX/ClassCompositor.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package MooseX::ClassCompositor;
2             {
3             $MooseX::ClassCompositor::VERSION = '0.008';
4             }
5 4     4   96445 use Moose;
  0            
  0            
6             # ABSTRACT: a factory that builds classes from roles
7              
8             use namespace::autoclean;
9              
10             use Class::Load;
11             use Moose::Util qw(apply_all_roles);
12             use Moose::Util::MetaRole ();
13             use MooseX::StrictConstructor::Trait::Class;
14             use MooseX::Types::Perl qw(PackageName);
15             use Scalar::Util qw(refaddr);
16             use String::RewritePrefix;
17              
18              
19              
20             has class_basename => (
21             is => 'ro',
22             isa => PackageName,
23             required => 1,
24             );
25              
26              
27             has class_metaroles => (
28             reader => '_class_metaroles',
29             isa => 'HashRef',
30             default => sub { {} },
31             );
32              
33              
34             has known_classes => (
35             reader => '_known_classes',
36             isa => 'HashRef',
37             traits => [ 'Hash' ],
38             handles => {
39             _learn_class => 'set',
40             known_classes => 'elements',
41             },
42             init_arg => undef,
43             default => sub { {} },
44             );
45              
46              
47             has role_prefixes => (
48             reader => '_role_prefixes',
49             isa => 'HashRef',
50             default => sub { {} },
51             );
52              
53             sub _rewrite_roles {
54             my $self = shift;
55             String::RewritePrefix->rewrite($self->_role_prefixes, @_);
56             }
57              
58              
59             has fixed_roles => (
60             reader => '_fixed_roles',
61             isa => 'ArrayRef',
62             default => sub { [] },
63             );
64              
65             has serial_counter => (
66             reader => '_serial_counter',
67             isa => 'Str',
68             default => 'AA',
69             traits => [ 'String' ],
70             handles => { next_serial => 'inc' },
71             init_arg => undef,
72             );
73              
74             has memoization_table => (
75             isa => 'HashRef',
76             default => sub { {} },
77             traits => [ 'Hash' ],
78             handles => {
79             _class_for_key => 'get',
80             _set_class_for_key => 'set',
81             },
82             init_arg => undef,
83             );
84              
85              
86             has forbid_meta_role_objects => (
87             is => 'ro',
88             isa => 'Bool',
89             default => 0,
90             );
91              
92              
93             sub class_for {
94             my ($self, @args) = @_;
95              
96             # can't use memoize without losing subclassability, so we reimplemented
97             # -- rjbs, 2011-08-05
98             my $memo_key = $self->_memoization_key(\@args);
99             if (my $cached = $self->_class_for_key($memo_key)) {
100             return $cached;
101             }
102              
103             # Arguments here are role names, or role objects followed by nonce-names.
104             my @orig_args = @args;
105              
106             # $role_hash is a hash mapping nonce-names to role objects
107             # $role_names is an array of names of more roles to add
108             my (@roles, @role_class_names, @all_names);
109              
110             while (@args) {
111             my $name = shift @args;
112             if (ref $name eq 'ARRAY') {
113             my ($role_name, $moniker, $params) = @$name;
114              
115             my $full_name = $self->_rewrite_roles($role_name);
116             Class::Load::load_class($full_name);
117             my $role_object = $full_name->meta->generate_role(
118             parameters => $params,
119             );
120              
121             push @roles, $role_object;
122             $name = $moniker;
123             } elsif (blessed $name and $name->isa('Moose::Meta::Role')) {
124             confess "this class compositor does not allow role objects"
125             if $self->forbid_meta_role_objects;
126              
127             push @roles, $name;
128             $name = $name->name;
129             } else {
130             push @role_class_names, $name;
131             }
132              
133             $name =~ s/::/_/g if @all_names;
134             $name =~ s/^=//;
135              
136             push @all_names, $name;
137             }
138              
139             my $name = join q{::}, $self->class_basename, @all_names;
140              
141             for my $r (@{ $self->_fixed_roles }) {
142             if (blessed $r and $r->isa('Moose::Meta::Role')) {
143             push @roles, $r;
144             } else {
145             push @role_class_names, $r;
146             }
147             }
148              
149             @role_class_names = $self->_rewrite_roles(@role_class_names);
150              
151             Class::Load::load_class($_) for @role_class_names;
152              
153             if ($name->can('meta')) {
154             $name .= "_" . $self->next_serial;
155             }
156              
157             my $class = Moose::Meta::Class->create( $name => (
158             superclasses => [ 'Moose::Object' ],
159             ));
160              
161             $class = Moose::Util::MetaRole::apply_metaroles(
162             for => $class->name,
163             class_metaroles => $self->_class_metaroles,
164             );
165              
166             apply_all_roles($class, @role_class_names, map $_->name, @roles);
167              
168             $class->make_immutable;
169              
170             $self->_learn_class($name, \@orig_args);
171             $self->_set_class_for_key($memo_key, $name);
172              
173             return $class->name;
174             }
175              
176             sub _memoization_key {
177             my ($self, $args) = @_;
178             my @args = @$args;
179              
180             my @k;
181             while (@args) {
182             my $arg = shift @args;
183             if (ref $arg eq 'ARRAY') {
184             my ($role_name, $moniker, $params) = @$arg;
185             push @k, "$moniker : { " . __hash_to_string($params) . " }";
186             } elsif (blessed $arg and $arg->isa('Moose::Meta::Role')) {
187             push @k, $arg->name;
188             } else {
189             push @k, $arg;
190             }
191             }
192             my $key = join "; ", sort @k;
193             return $key;
194             }
195              
196             sub __hash_to_string {
197             my ($h) = @_;
198             my @k;
199             for my $k (sort keys %$h) {
200             my $v = ! defined($h->{$k}) ? "<undef>" :
201             ref($h->{$k}) ? join("-", @{$h->{$k}}) : $h->{$k};
202             push @k, "$k => $v";
203             }
204             join ", " => @k;
205             }
206              
207              
208             __PACKAGE__->meta->make_immutable;
209             1;
210              
211             __END__
212              
213             =pod
214              
215             =encoding UTF-8
216              
217             =head1 NAME
218              
219             MooseX::ClassCompositor - a factory that builds classes from roles
220              
221             =head1 VERSION
222              
223             version 0.008
224              
225             =head1 SYNOPSIS
226              
227             my $comp = MooseX::ClassCompositor->new({
228             class_basename => 'MyApp::Class',
229             class_metaroles => {
230             class => [ 'MooseX::StrictConstructor::Trait::Class' ],
231             },
232             role_prefixes => {
233             '' => 'MyApp::Role::',
234             '=' => '',
235             },
236             });
237              
238             my $class = $comp->class_for( qw( PieEater ContestWinner ) );
239              
240             my $object = $class->new({
241             pie_type => 'banana',
242             place => '2nd',
243             });
244              
245             =head1 OVERVIEW
246              
247             A MooseX::ClassCompositor is a class factory. If you think using a class
248             factory will make you feel like a filthy "enterprise" programmer, maybe you
249             should turn back now.
250              
251             The compositor has a C<L</class_for>> method that builds a class by combining a
252             list of roles with L<Moose::Object>, applying any supplied metaclass, and
253             producing an arbitrary-but-human-scannable name. The metaclass is then
254             made immutable, the operation is memoized, and the class name is returned.
255              
256             In the L</SYNOPSIS> above, you can see all the major features used:
257             C<class_metaroles> to enable strict constructors, C<role_prefixes> to use
258             L<String::RewritePrefix> to expand role name shorthand, and C<class_basename>
259             to pick a namespace under which to put constructed classes.
260              
261             Not shown is the C<L</known_classes>> method, which returns a list of pairs
262             describing all the classes that the factory has constructed. This method can
263             be useful for debugging and other somewhat esoteric purposes like
264             serialization.
265              
266             =head1 ATTRIBUTES
267              
268             =head2 class_basename
269              
270             This attribute must be given, and must be a valid Perl package name.
271             Constructed classes will all be under this namespace.
272              
273             =head2 class_metaroles
274              
275             This attribute, if given, must be a hashref of class metaroles that will be
276             applied to newly-constructed classes with
277             L<Moose::Util::MetaRole::apply_metaroles>.
278              
279             =head2 known_classes
280              
281             This attribute stores a mapping of class names to the parameters used to
282             construct them. The C<known_classes> method returns its contents as a list of
283             pairs.
284              
285             =head2 role_prefixes
286              
287             This attribute is used as the arguments to L<String::RewritePrefix> for
288             expanding role names passed to the compositor's L<class_for> method.
289              
290             =head2 fixed_roles
291              
292             This attribute may be initialized with an arrayref of role names and/or
293             L<Moose::Meta::Role> objects. These roles will I<always> be composed in
294             the classes built by the compositor.
295              
296             Role names (but not Moose::Meta::Role objects) I<will> be rewritten by
297             the role prefixes.
298              
299             =head2 forbid_meta_role_objects
300              
301             If true, an exception will be raised if a Moose::Meta::Role object is passed to
302             C<L</class_for>>. This is only rarely useful, such as if it's a strict
303             requirement that the memoization table of the compositor be serializable and
304             its contents reproduceable.
305              
306             Probably you don't need this.
307              
308             =head1 METHODS
309              
310             =head2 class_for
311              
312             my $class = $compositor->class_for(
313              
314             'Role::Name', # <-- will be expanded with role_prefixes
315             Other::Role->meta, # <-- will not be touched
316              
317             [
318             'Param::Role::Name', # <-- will be expanded with role_prefixes
319             'ApplicationName', # <-- will not be touched
320             { ...param... },
321             ],
322             );
323              
324             This method will return a class with the roles passed to it. They can be given
325             either as names (which will be expanded according to C<L</role_prefixes>>), as
326             L<Moose::Meta::Role> objects, or as arrayrefs containing a role name,
327             application name, and hashref of parameters. In the arrayref form, the
328             application name is just a name used to uniquely identify this application of
329             a parameterized role, so that they can be applied multiple times with each
330             application accounted for internally.
331              
332             =head1 THANKS
333              
334             Thanks to Pobox.com for sponsoring the development of this library.
335              
336             =head1 AUTHORS
337              
338             =over 4
339              
340             =item *
341              
342             Ricardo Signes <rjbs@cpan.org>
343              
344             =item *
345              
346             Mark Jason Dominus <mjd@cpan.org>
347              
348             =back
349              
350             =head1 COPYRIGHT AND LICENSE
351              
352             This software is copyright (c) 2011 by Ricardo Signes.
353              
354             This is free software; you can redistribute it and/or modify it under
355             the same terms as the Perl 5 programming language system itself.
356              
357             =cut