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