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