The code which offended me is expunged
[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
53a42ae0 117 $meta->make_immutable;
118
62680454 119 my $instance = $app->setup_component($component_name);
120 $app->components->{ $component_name } = $instance;
53a42ae0 121 };
122};
59fc9d16 123
1241;
125
dc7781e3 126__END__
127
128=head1 NAME
129
130CatalystX::DynamicComponent - Parameterised Moose role providing functionality to build Catalyst components at runtime.
131
132=head1 SYNOPSIS
133
134 package My::DynamicComponentType;
135 use Moose::Role;
136 use namespace::autoclean;
137
138 with 'CatalystX::DynamicComponent' => {
139 name => '_setup_one_of_my_components', # Name of injected method
140 };
141
142 after setup_components => sub { shift->_setup_all_my_components(@_); };
143
144 sub _setup_all_my_components {
145 my ($self, $c) = @_;
f29c6cde 146 my $app = ref($self) || $self;
147 foreach my $component_name ('Controller::Foo') {
148 my %component_config = %{ $c->config->{$component_name} };
149 # Shallow copy so we avoid stuffing methods back in the config, as that's lame!
150 $component_config{methods} = {
151 some_method => sub { 'foo' },
152 };
153
dc7781e3 154 # Calling this method creates a component, and registers it in your application
f29c6cde 155 # This component will subclass 'MyApp::ControllerBase', do 'MyApp::ControllerRole'
156 # and have a method called 'some_method' which will return the value 'foo'..
157 $self->_setup_one_of_my_components($app . '::' . $component_name, \%component_config);
dc7781e3 158 }
159 }
160
f29c6cde 161 package MyApp;
162 use Moose;
163 use namespace::autoclean;
164 use Catalyst qw/
165 +My::DynameComponentType
166 /;
167 __PACKAGE__->config(
168 name => 'MyApp',
169 'Controller::Foo' => {
170 superclasses => [qw/MyApp::ControllerBase/],
171 roles => [qw/MyApp::ControllerRole/],
172 },
173 );
174 __PACKAGE__->setup;
175
dc7781e3 176=head1 DESCRIPTION
177
f29c6cde 178CatalystX::DynamicComponent aims to provide a flexible and reuseable method of building L<Roles|Moose::Role>
179which can be added to L<Catalyst> applications, which generate components dynamically at application
180startup using the L<Moose> meta model.
dc7781e3 181
f29c6cde 182Thi is implemented as a parametrised role which curries a
183component builder method into your current package at application time.
dc7781e3 184
f29c6cde 185Authors of specific dynamic component builders are expected implement an application class
186roles which composes this role, and their own advice after the C<< setup_compontents >>
187method, which will call the component generation method provided by using this role once
188for each component you wish to create.
dc7781e3 189
190=head1 PARAMETERS
191
192=head2 name
193
194B<Required> - The name of the component generator method to curry.
195
f29c6cde 196=head2 methods
dc7781e3 197
f29c6cde 198Optional, a hash reference with keys being method names, and values being a L<Class::MOP::Method>,
199or a plain code ref of a method to apply to
dc7781e3 200the dynamically generated package before making it immutable.
201
f29c6cde 202=head2 roles
203
204Optional, an array reference of roles to apply to the generated component
205
206=head2 superclasses
207
208Optional, an array reference of superclasses to give the generated component.
209
210If this is not defined, and not passed in as an argument to the generation method,
211then Catalyst::(Model|View|Controller) will used as the base class (as appropriate given
212the requested namespace of the generated class, otherwise Catalyst::Component will be used.
213
214FIXME - Need tests for this.
215
dc7781e3 216=head2 pre_immutable_hook
217
f29c6cde 218Optional, either a coderef, which will be called with the component $meta and the merged $config,
219or a string name of a method to call on the application class, with the same parameters.
220
221This hook is called after a component has been generated and methods added, but before it is made
222immutable, constructed, and added to your component registry.
dc7781e3 223
224=head1 CURRIED COMPONENT GENERATOR
225
226=head2 ARGUMENTS
227
228=over
229
6ba37fba 230=item *
231
232$component_name (E.g. C<< MyApp::Controller::Foo >>)
233
234=item *
235
236$config (E.g. C<< $c->config->{$component_name} >>)
dc7781e3 237
6ba37fba 238=back
dc7781e3 239
f29c6cde 240=head3 config
241
242It is possible to set each of the roles, methods and superclasses parameters for each generated package
243individually by defining those keys in the C< $config > parameter to your curried component generation method.
244
245By default, roles and methods supplied from the curried role, and those passed as config will be merged.
246
247Superclasses, no the other hand, will replace those from the curried configuration if passed as options.
248This is to discourage accidental use of multiple inheritence, if you need this feature enabled, you should
249probably be using Roles instead!
250
251It is possible to change the default behavior of each parameter by passing a
252C< $param_name.'_resolve_strategy' > parameter when currying a class generator, with values of either
253C<merge> or C<replace>.
254
255Example:
256
257 package My::ComponentGenerator;
258 use Moose;
259
260 with 'CatalystX::DynamicComponent' => {
261 name => 'generate_magic_component',
262 roles => ['My::Role'],
263 roles_resolve_strategy => 'replace',
264 };
265
266 package MyApp;
267 use Moose;
268 use Catalyst qw/
269 My::ComponentGenerator
270 /;
271 extends 'Catalyst';
272 after 'setup_components' => sub {
273 my ($app) = @_;
274 # Component generated has no roles
275 $app->generate_magic_component('MyApp::Controller::Foo', { roles => [] });
276 # Component generated does My::Role
277 $app->generate_magic_component('MyApp::Controller::Foo', {} );
278 };
279 __PACKAGE__->setup;
280
dc7781e3 281=head2 OPERATION
282
283FIXME
284
285=head1 TODO
286
287=over
288
289=item *
290
f29c6cde 291Test pre_immutable hook in tests
dc7781e3 292
293=item *
294
f29c6cde 295More tests fixme?
dc7781e3 296
297=item *
298
f29c6cde 299Unlame needing to pass fully qualified component name in, that's retarded...
92c28c42 300
f29c6cde 301Remember to fix the docs and clients too ;)
dc7781e3 302
741991f9 303=item *
304
305Tests for roles giving advice to methods which have just been added..
306
dc7781e3 307=back
308
f29c6cde 309=head1 LINKS
310
311L<Catalyst>, L<MooseX::MethodAttributes>, L<CatalystX::ModelsFromConfig>.
312
dc7781e3 313=head1 BUGS
314
315Probably plenty, test suite certainly isn't comprehensive.. Patches welcome.
316
317=head1 AUTHOR
318
319Tomas Doran (t0m) <bobtfish@bobtfish.net>
320
321=head1 LICENSE
322
323This code is copyright (c) 2009 Tomas Doran. This code is licensed on the same terms as perl
324itself.
325
326=cut
327