Able to use strings rather than having to use hash refs
[catagits/CatalystX-DynamicComponent.git] / lib / CatalystX / DynamicComponent.pm
CommitLineData
59fc9d16 1package CatalystX::DynamicComponent;
53a42ae0 2use MooseX::Role::Parameterized;
ac8aab7d 3use MooseX::Types::Moose qw/Str CodeRef HashRef ArrayRef/;
4use Catalyst::Utils;
5use Moose::Util::TypeConstraints;
6use List::MoreUtils qw/uniq/;
72b52242 7use Moose::Autobox;
046d763d 8use namespace::autoclean;
59fc9d16 9
ac8aab7d 10enum __PACKAGE__ . '::ResolveStrategy' => qw/
11 merge
12 replace
13/;
14
104abdae 15our $VERSION = 0.000001;
16
53a42ae0 17parameter 'name' => (
0b07685c 18 isa => Str,
53a42ae0 19 required => 1,
20);
21
22parameter 'pre_immutable_hook' => (
92c28c42 23 isa => CodeRef|Str,
53a42ae0 24 predicate => 'has_pre_immutable_hook',
25);
26
72b52242 27my $coerceablearray = subtype ArrayRef;
28coerce $coerceablearray, from Str, via { [ $_ ] };
29
ac8aab7d 30my %parameters = (
31 methods => {
32 isa =>HashRef,
33 default => sub { {} },
34 resolve_strategy => 'merge',
35 },
36 roles => {
72b52242 37 isa => $coerceablearray, coerce => 1,
ac8aab7d 38 default => sub { [] },
39 resolve_strategy => 'merge',
40 },
41 superclasses => {
72b52242 42 isa => $coerceablearray, coerce => 1,
ac8aab7d 43 default => sub { [] },
44 resolve_strategy => 'replace',
45 },
46);
47
48# Shameless metaprogramming.
49foreach my $name (keys %parameters) {
50 my $resolve_strategy = delete $parameters{$name}->{resolve_strategy};
51
52 parameter $name, %{ $parameters{$name} };
53
54 parameter $name . '_resolve_strategy' => (
55 isa => __PACKAGE__ . '::ResolveStrategy',
56 default => $resolve_strategy,
57 );
58}
59
60# Code refs to implement the strategy types
61my %strategies = ( # Right hand precedence where appropriate
72b52242 62 replace => sub {
63 $_[0] = [ $_[0] ] if $_[0] && !ref $_[0];
64 $_[1] = [ $_[1] ] if $_[1] && !ref $_[1];
65 $_[1] ? $_[1] : $_[0];
66 },
ac8aab7d 67 merge => sub {
72b52242 68 $_[0] = [ $_[0] ] if $_[0] && !ref $_[0];
69 $_[1] = [ $_[1] ] if $_[1] && !ref $_[1];
70 if (ref($_[0]) eq 'ARRAY' || ref($_[1]) eq 'ARRAY') {
ac8aab7d 71 [ uniq( @{ $_[0] }, @{ $_[1] } ) ];
72 }
73 else {
74 Catalyst::Utils::merge_hashes(shift, shift);
75 }
76 },
cd6bd40d 77);
78
ac8aab7d 79# Wrap all the crazy up in a method to generically merge configs.
80my $get_resolved_config = sub {
81 my ($name, $p, $config) = @_;
82 my $get_strategy_method_name = $name . '_resolve_strategy';
83 my $strategy = $strategies{$p->$get_strategy_method_name()};
84 $strategy->($p->$name, $config->{$name})
85 || $parameters{$name}->{default}->();
86};
87
53a42ae0 88role {
89 my $p = shift;
90 my $name = $p->name;
cd6bd40d 91 my $pre_immutable_hook = $p->pre_immutable_hook;
0b07685c 92
53a42ae0 93 method $name => sub {
c52d8688 94 my ($app, $name, $config) = @_;
62680454 95 my $appclass = blessed($app) || $app;
c52d8688 96
97 $config ||= {};
53a42ae0 98
53a42ae0 99 my $type = $name;
53a42ae0 100 $type =~ s/::.*$//;
101
62680454 102 my $component_name = $appclass . '::' . $name;
103 my $meta = Moose->init_meta( for_class => $component_name );
279c014c 104
ac8aab7d 105 my @superclasses = @{ $get_resolved_config->('superclasses', $p, $config) };
0b07685c 106 push(@superclasses, 'Catalyst::' . $type) unless @superclasses;
107 $meta->superclasses(@superclasses);
96e54fc3 108
ac8aab7d 109 my $methods = $get_resolved_config->('methods', $p, $config);
62680454 110 foreach my $method_name (keys %$methods) {
111 $meta->add_method($method_name => $methods->{$method_name});
549d6abc 112 }
741991f9 113
114 if (my @roles = @{ $get_resolved_config->('roles', $p, $config) }) {
62680454 115 Moose::Util::apply_all_roles( $component_name, @roles);
741991f9 116 }
96e54fc3 117
92c28c42 118 if ($p->has_pre_immutable_hook) {
119 if (!ref($pre_immutable_hook)) {
120 $app->$pre_immutable_hook($meta, $config);
121 }
122 else {
123 $pre_immutable_hook->($meta, $config);
124 }
125 }
126
53a42ae0 127 $meta->make_immutable;
128
62680454 129 my $instance = $app->setup_component($component_name);
130 $app->components->{ $component_name } = $instance;
53a42ae0 131 };
132};
59fc9d16 133
1341;
135
dc7781e3 136__END__
137
138=head1 NAME
139
140CatalystX::DynamicComponent - Parameterised Moose role providing functionality to build Catalyst components at runtime.
141
142=head1 SYNOPSIS
143
144 package My::DynamicComponentType;
145 use Moose::Role;
146 use namespace::autoclean;
147
148 with 'CatalystX::DynamicComponent' => {
149 name => '_setup_one_of_my_components', # Name of injected method
150 };
151
152 after setup_components => sub { shift->_setup_all_my_components(@_); };
153
154 sub _setup_all_my_components {
155 my ($self, $c) = @_;
f29c6cde 156 my $app = ref($self) || $self;
157 foreach my $component_name ('Controller::Foo') {
158 my %component_config = %{ $c->config->{$component_name} };
159 # Shallow copy so we avoid stuffing methods back in the config, as that's lame!
160 $component_config{methods} = {
161 some_method => sub { 'foo' },
162 };
163
dc7781e3 164 # Calling this method creates a component, and registers it in your application
f29c6cde 165 # This component will subclass 'MyApp::ControllerBase', do 'MyApp::ControllerRole'
166 # and have a method called 'some_method' which will return the value 'foo'..
167 $self->_setup_one_of_my_components($app . '::' . $component_name, \%component_config);
dc7781e3 168 }
169 }
170
f29c6cde 171 package MyApp;
172 use Moose;
173 use namespace::autoclean;
174 use Catalyst qw/
175 +My::DynameComponentType
176 /;
177 __PACKAGE__->config(
178 name => 'MyApp',
179 'Controller::Foo' => {
180 superclasses => [qw/MyApp::ControllerBase/],
181 roles => [qw/MyApp::ControllerRole/],
182 },
183 );
184 __PACKAGE__->setup;
185
dc7781e3 186=head1 DESCRIPTION
187
f29c6cde 188CatalystX::DynamicComponent aims to provide a flexible and reuseable method of building L<Roles|Moose::Role>
189which can be added to L<Catalyst> applications, which generate components dynamically at application
190startup using the L<Moose> meta model.
dc7781e3 191
f29c6cde 192Thi is implemented as a parametrised role which curries a
193component builder method into your current package at application time.
dc7781e3 194
f29c6cde 195Authors of specific dynamic component builders are expected implement an application class
196roles which composes this role, and their own advice after the C<< setup_compontents >>
197method, which will call the component generation method provided by using this role once
198for each component you wish to create.
dc7781e3 199
200=head1 PARAMETERS
201
202=head2 name
203
204B<Required> - The name of the component generator method to curry.
205
f29c6cde 206=head2 methods
dc7781e3 207
f29c6cde 208Optional, a hash reference with keys being method names, and values being a L<Class::MOP::Method>,
209or a plain code ref of a method to apply to
dc7781e3 210the dynamically generated package before making it immutable.
211
f29c6cde 212=head2 roles
213
214Optional, an array reference of roles to apply to the generated component
215
216=head2 superclasses
217
218Optional, an array reference of superclasses to give the generated component.
219
220If this is not defined, and not passed in as an argument to the generation method,
221then Catalyst::(Model|View|Controller) will used as the base class (as appropriate given
222the requested namespace of the generated class, otherwise Catalyst::Component will be used.
223
224FIXME - Need tests for this.
225
dc7781e3 226=head2 pre_immutable_hook
227
f29c6cde 228Optional, either a coderef, which will be called with the component $meta and the merged $config,
229or a string name of a method to call on the application class, with the same parameters.
230
231This hook is called after a component has been generated and methods added, but before it is made
232immutable, constructed, and added to your component registry.
dc7781e3 233
234=head1 CURRIED COMPONENT GENERATOR
235
236=head2 ARGUMENTS
237
238=over
239
6ba37fba 240=item *
241
242$component_name (E.g. C<< MyApp::Controller::Foo >>)
243
244=item *
245
246$config (E.g. C<< $c->config->{$component_name} >>)
dc7781e3 247
6ba37fba 248=back
dc7781e3 249
f29c6cde 250=head3 config
251
252It is possible to set each of the roles, methods and superclasses parameters for each generated package
253individually by defining those keys in the C< $config > parameter to your curried component generation method.
254
255By default, roles and methods supplied from the curried role, and those passed as config will be merged.
256
257Superclasses, no the other hand, will replace those from the curried configuration if passed as options.
258This is to discourage accidental use of multiple inheritence, if you need this feature enabled, you should
259probably be using Roles instead!
260
261It is possible to change the default behavior of each parameter by passing a
262C< $param_name.'_resolve_strategy' > parameter when currying a class generator, with values of either
263C<merge> or C<replace>.
264
265Example:
266
267 package My::ComponentGenerator;
268 use Moose;
269
270 with 'CatalystX::DynamicComponent' => {
271 name => 'generate_magic_component',
272 roles => ['My::Role'],
273 roles_resolve_strategy => 'replace',
274 };
275
276 package MyApp;
277 use Moose;
278 use Catalyst qw/
279 My::ComponentGenerator
280 /;
281 extends 'Catalyst';
282 after 'setup_components' => sub {
283 my ($app) = @_;
284 # Component generated has no roles
285 $app->generate_magic_component('MyApp::Controller::Foo', { roles => [] });
286 # Component generated does My::Role
287 $app->generate_magic_component('MyApp::Controller::Foo', {} );
288 };
289 __PACKAGE__->setup;
290
dc7781e3 291=head2 OPERATION
292
293FIXME
294
295=head1 TODO
296
297=over
298
299=item *
300
f29c6cde 301Test pre_immutable hook in tests
dc7781e3 302
303=item *
304
f29c6cde 305More tests fixme?
dc7781e3 306
307=item *
308
f29c6cde 309Unlame needing to pass fully qualified component name in, that's retarded...
92c28c42 310
f29c6cde 311Remember to fix the docs and clients too ;)
dc7781e3 312
741991f9 313=item *
314
315Tests for roles giving advice to methods which have just been added..
316
dc7781e3 317=back
318
f29c6cde 319=head1 LINKS
320
321L<Catalyst>, L<MooseX::MethodAttributes>, L<CatalystX::ModelsFromConfig>.
322
dc7781e3 323=head1 BUGS
324
325Probably plenty, test suite certainly isn't comprehensive.. Patches welcome.
326
327=head1 AUTHOR
328
329Tomas Doran (t0m) <bobtfish@bobtfish.net>
330
331=head1 LICENSE
332
333This code is copyright (c) 2009 Tomas Doran. This code is licensed on the same terms as perl
334itself.
335
336=cut
337