C::M::DBIC::Schema - minor cleanups
[catagits/Catalyst-Model-DBIC-Schema.git] / lib / Catalyst / Helper / Model / DBIC / Schema.pm
1 package Catalyst::Helper::Model::DBIC::Schema;
2
3 use Moose;
4 no warnings 'uninitialized';
5
6 our $VERSION = '0.24';
7
8 use Carp;
9 use Tie::IxHash ();
10 use Data::Dumper ();
11 use List::Util 'first';
12 use MooseX::Types::Moose qw/Str HashRef Bool/;
13
14 use namespace::clean -except => 'meta';
15
16 =head1 NAME
17
18 Catalyst::Helper::Model::DBIC::Schema - Helper for DBIC Schema Models
19
20 =head1 SYNOPSIS
21
22   script/create.pl model CatalystModelName DBIC::Schema MyApp::SchemaClass \
23     [ create=dynamic | create=static ] [ roles=role1,role2... ] \
24     [ Schema::Loader opts ] [ dsn user pass ] \
25     [ other connect_info args ]
26
27 =head1 DESCRIPTION
28
29 Helper for the DBIC Schema Models.
30
31 =head2 Arguments:
32
33 C<CatalystModelName> is the short name for the Catalyst Model class
34 being generated (i.e. callable with C<$c-E<gt>model('CatalystModelName')>).
35
36 C<MyApp::SchemaClass> is the fully qualified classname of your Schema,
37 which might or might not yet exist.  Note that you should have a good
38 reason to create this under a new global namespace, otherwise use an
39 existing top level namespace for your schema class.
40
41 C<create=dynamic> instructs this Helper to generate the named Schema
42 class for you, basing it on L<DBIx::Class::Schema::Loader> (which
43 means the table information will always be dynamically loaded at
44 runtime from the database).
45
46 C<create=static> instructs this Helper to generate the named Schema
47 class for you, using L<DBIx::Class::Schema::Loader> in "one shot"
48 mode to create a standard, manually-defined L<DBIx::Class::Schema>
49 setup, based on what the Loader sees in your database at this moment.
50 A Schema/Model pair generated this way will not require
51 L<DBIx::Class::Schema::Loader> at runtime, and will not automatically
52 adapt itself to changes in your database structure.  You can edit
53 the generated classes by hand to refine them.
54
55 C<roles> is the list of roles to apply to the model, see
56 L<Catalyst::Model::DBIC::Schema> for details.
57
58 C<Schema::Loader opts> are described in L</TYPICAL EXAMPLES> below.
59
60 C<connect_info> arguments are the same as what
61 DBIx::Class::Schema::connect expects, and are storage_type-specific.
62 For DBI-based storage, these arguments are the dsn, username,
63 password, and connect options, respectively.  These are optional for
64 existing Schemas, but required if you use either of the C<create=>
65 options.
66
67 Use of either of the C<create=> options requires L<DBIx::Class::Schema::Loader>.
68
69 =head1 TYPICAL EXAMPLES
70
71   # Use DBIx::Class::Schema::Loader to create a static DBIx::Class::Schema,
72   #  and a Model which references it:
73   script/myapp_create.pl model CatalystModelName DBIC::Schema \
74     MyApp::SchemaClass create=static dbi:mysql:foodb myuname mypass
75
76   # Same, with extra connect_info args
77   script/myapp_create.pl model CatalystModelName DBIC::Schema \
78     MyApp::SchemaClass create=static dbi:SQLite:foo.db '' '' \
79     AutoCommit=1 cursor_class=DBIx::Class::Cursor::Cached \
80     on_connect_do='["select 1", "select 2"]'
81
82   # Same, but with extra Schema::Loader args (separate multiple values by commas):
83   script/myapp_create.pl model CatalystModelName DBIC::Schema \
84     MyApp::SchemaClass create=static db_schema=foodb components=Foo,Bar \
85     exclude='^wibble|wobble$' moniker_map='{ foo => "FFFFUUUU" }' \
86     dbi:Pg:dbname=foodb myuname mypass
87
88   # See DBIx::Class::Schema::Loader::Base for list of options
89
90   # Create a dynamic DBIx::Class::Schema::Loader-based Schema,
91   #  and a Model which references it:
92   script/myapp_create.pl model CatalystModelName DBIC::Schema \
93     MyApp::SchemaClass create=dynamic dbi:mysql:foodb myuname mypass
94
95   # Reference an existing Schema of any kind, and provide some connection information for ->config:
96   script/myapp_create.pl model CatalystModelName DBIC::Schema \
97     MyApp::SchemaClass dbi:mysql:foodb myuname mypass
98
99   # Same, but don't supply connect information yet (you'll need to do this
100   #  in your app config, or [not recommended] in the schema itself).
101   script/myapp_create.pl model ModelName DBIC::Schema My::SchemaClass
102
103 =cut
104
105 has helper => (is => 'ro', isa => 'Catalyst::Helper', required => 1);
106
107 has schema_class => (is => 'ro', isa => Str, required => 1);
108
109 has loader_args => (is => 'rw', isa => HashRef);
110 has connect_info => (is => 'rw', isa => HashRef);
111
112 has old_schema => (is => 'rw', isa => Bool, lazy_build => 1);
113
114 =head1 METHODS
115
116 =head2 mk_compclass
117
118 This is called by L<Catalyst::Helper> with the commandline args to generate the
119 files.
120
121 =cut
122
123 sub mk_compclass {
124     my ($package, $helper, $schema_class, @args) = @_;
125
126     my $self = $package->new(helper => $helper, schema_class => $schema_class);
127
128     $helper->{schema_class} = $schema_class;
129
130     @args = $self->_cleanup_args(\@args);
131
132     my $create = '';
133     if ($args[0] && $args[0] =~ /^create=(dynamic|static)\z/) {
134         $create = $1;
135         shift @args;
136
137         if ($args[0] && $args[0] =~ /^roles=(.*)\z/) {
138             $helper->{roles} = '['
139                 .(join ',' => map { qq{'$_'} } (split /,/ => $1))
140                 .']';
141             shift @args;
142         }
143
144         if (@args) {
145             $self->_parse_loader_args(\@args);
146
147             if (first { /^dbi:/i } @args) {
148                 $helper->{setup_connect_info} = 1;
149
150                 $helper->{connect_info} =
151                     $self->_build_helper_connect_info(\@args);
152
153                 $self->_parse_connect_info(\@args) if $create eq 'static';
154             }
155         }
156     }
157
158     $helper->{generator} = ref $self;
159     $helper->{generator_version} = $VERSION;
160
161     if ($create eq 'dynamic') {
162         $self->_print_dynamic_deprecation_warning;
163         $self->helper->{loader_args} = $self->_build_helper_loader_args;
164         $self->_gen_dynamic_schema;
165     } elsif ($create eq 'static') {
166         $self->_gen_static_schema;
167     }
168
169     $self->_gen_model;
170 }
171
172 sub _parse_loader_args {
173     my ($self, $args) = @_;
174
175     my %loader_args = $self->_read_loader_args($args);
176
177     while (my ($key, $val) = each %loader_args) {
178         next if $key =~ /^(?:components|constraint|exclude)\z/;
179
180         $loader_args{$key} = eval $val;
181         die "syntax error for loader args key '$key' with value '$val': $@"
182             if $@;
183     }
184
185     my @components =
186     $self->_build_loader_components(delete $loader_args{components});
187
188     for my $re_opt (qw/constraint exclude/) {
189         $loader_args{$re_opt} = qr/$loader_args{$re_opt}/
190         if exists $loader_args{$re_opt};
191     }
192
193     tie my %result, 'Tie::IxHash';
194
195     %result = (
196         relationships => 1,
197         (%loader_args ? %loader_args : ()),
198         (!$self->old_schema ? (
199                 use_namespaces => 1
200             ) : ()),
201         (@components ? (
202                 components => \@components
203             ) : ())
204     );
205
206     $self->loader_args(\%result);
207
208     wantarray ? %result : \%result;
209 }
210
211 sub _read_loader_args {
212     my ($self, $args) = @_;
213
214     my %loader_args;
215
216     while (@$args && $args->[0] !~ /^dbi:/) {
217         my ($key, $val) = split /=/, shift(@$args), 2;
218
219         if ((my @vals = split /,/ => $val) > 1) {
220             $loader_args{$key} = \@vals;
221         } else {
222             $loader_args{$key} = $val;
223         }
224     }
225
226     wantarray ? %loader_args : \%loader_args;
227 }
228
229 sub _build_helper_loader_args {
230     my $self = shift;
231
232     my $args = $self->loader_args;
233
234     tie my %loader_args, 'Tie::IxHash';
235
236     while (my ($arg, $val) = each %$args) {
237         if (ref $val) {
238             $loader_args{$arg} = $self->_data_struct_to_string($val);
239         } else {
240             $loader_args{$arg} = qq{'$val'};
241         }
242     }
243
244     \%loader_args
245 }
246
247 sub _build_loader_components {
248     my ($self, $components) = @_;
249
250     my @components = $self->old_schema ? () : ('InflateColumn::DateTime');
251
252     if ($components) {
253         $components = [ $components ] if !ref $components;
254         push @components, @$components;
255     }
256
257     wantarray ? @components : \@components;
258 }
259
260 sub _build_helper_connect_info {
261     my ($self, $connect_info) = @_;
262
263     my @connect_info = @$connect_info;
264
265     my ($dsn, $user, $password) = splice @connect_info, 0, 3;
266
267     tie my %helper_connect_info, 'Tie::IxHash';
268
269     %helper_connect_info = (
270         dsn => qq{'$dsn'},
271         user => qq{'$user'},
272         password => qq{'$password'}
273     );
274
275     for (@connect_info) {
276         if (/^\s*{.*}\s*\z/) {
277             my $hash = eval $_;
278             die "Syntax errorr in connect_info hash: $_: $@" if $@;
279             my %hash = %$hash;
280
281             for my $key (keys %hash) {
282                 my $val = $hash{$key};
283
284                 if (ref $val) {
285                     $val = $self->_data_struct_to_string($val);
286                 } else {
287                     $val = qq{'$val'};
288                 }
289
290                 $helper_connect_info{$key} = $val;
291             }
292
293             next;
294         }
295
296         my ($key, $val) = split /=/, $_, 2;
297
298         $helper_connect_info{$key} = $self->_quote_unless_struct($val);
299     }
300
301     \%helper_connect_info
302 }
303
304 sub _build_old_schema {
305     my $self = shift;
306
307     my @schema_pm   = split '::', $self->schema_class;
308     $schema_pm[-1] .= '.pm';
309     my $schema_file =
310     File::Spec->catfile($self->helper->{base}, 'lib', @schema_pm);
311
312     if (-f $schema_file) {
313         my $schema_code = do { local (@ARGV, $/) = $schema_file; <> };
314         return 1 if $schema_code =~ /->load_classes/;
315     }
316
317     0;
318 }
319
320 sub _data_struct_to_string {
321     my ($self, $data) = @_;
322
323     local $Data::Dumper::Terse = 1;
324     local $Data::Dumper::Quotekeys = 0;
325     local $Data::Dumper::Indent = 0;
326     local $Data::Dumper::Useqq = 1;
327
328     return Data::Dumper->Dump([$data]);
329 }
330
331 sub _parse_connect_info {
332     my ($self, $connect_info) = @_;
333
334     my @connect_info = @$connect_info;
335
336     my ($dsn, $user, $password) = splice @connect_info, 0, 3;
337
338     tie my %connect_info, 'Tie::IxHash';
339     @connect_info{qw/dsn user password/} = ($dsn, $user, $password);
340
341     for (@connect_info) {
342         if (/^\s*{.*}\s*\z/) {
343             my $hash = eval $_;
344             die "Syntax errorr in connect_info hash: $_: $@" if $@;
345
346             %connect_info = (%connect_info, %$hash);
347
348             next;
349         }
350
351         my ($key, $val) = split /=/, $_, 2;
352
353         if ($key =~ /^(?:quote_char|name_sep)\z/) {
354             $connect_info{$key} = $val;
355         } else {
356             $connect_info{$key} = eval $val;
357         }
358
359         die "syntax error for connect_info key '$key' with value '$val': $@"
360             if $@;
361     }
362
363     $self->connect_info(\%connect_info);
364
365     \%connect_info
366 }
367
368 sub _quote_unless_struct {
369     my ($self, $val) = @_;
370
371     $val = qq{'$val'} if $val !~ /^\s*[[{]/;
372
373     $val;
374 }
375
376 sub _gen_dynamic_schema {
377     my $self = shift;
378
379     my $helper = $self->helper;
380
381     my @schema_parts = split(/\:\:/, $self->schema_class);
382     my $schema_file_part = pop @schema_parts;
383
384     my $schema_dir  = File::Spec->catfile(
385         $helper->{base}, 'lib', @schema_parts
386     );
387     my $schema_file = File::Spec->catfile(
388         $schema_dir, $schema_file_part . '.pm'
389     );
390
391     $helper->mk_dir($schema_dir);
392     $helper->render_file('schemaclass', $schema_file);
393 }
394
395 sub _gen_static_schema {
396     my $self = shift;
397
398     die "cannot load schema without connect info" unless $self->connect_info;
399
400     my $helper = $self->helper;
401
402     my $schema_dir = File::Spec->catfile($helper->{base}, 'lib');
403
404     eval { Class::MOP::load_class('DBIx::Class::Schema::Loader') };
405     die "Cannot load DBIx::Class::Schema::Loader: $@" if $@;
406
407     DBIx::Class::Schema::Loader->import(
408         "dump_to_dir:$schema_dir", 'make_schema_at'
409     );
410
411     make_schema_at(
412         $self->schema_class,
413         $self->loader_args,
414         [$self->connect_info]
415     );
416 }
417
418 sub _gen_model {
419     my $self = shift;
420     my $helper = $self->helper;
421
422     $helper->render_file('compclass', $helper->{file} );
423 }
424
425 sub _print_dynamic_deprecation_warning {
426     warn <<EOF;
427 ************************************ WARNING **********************************
428 * create=dynamic is DEPRECATED, please use create=static instead.             *
429 *******************************************************************************
430 EOF
431     print "Continue? [y/n]: ";
432     chomp(my $response = <STDIN>);
433     exit 0 if $response =~ /^n(o)?\z/;
434 }
435
436 sub _cleanup_args {
437     my ($self, $args) = @_;
438
439 # remove blanks, ie. someoned doing foo \  bar
440     my @res = grep !/^\s+\z/, @$args;
441
442 # remove leading whitespace, ie. foo \ bar
443     s/^\s*// for @res;
444
445     @res
446 }
447
448 =head1 SEE ALSO
449
450 General Catalyst Stuff:
451
452 L<Catalyst::Manual>, L<Catalyst::Test>, L<Catalyst::Request>,
453 L<Catalyst::Response>, L<Catalyst::Helper>, L<Catalyst>,
454
455 Stuff related to DBIC and this Model style:
456
457 L<DBIx::Class>, L<DBIx::Class::Schema>,
458 L<DBIx::Class::Schema::Loader>, L<Catalyst::Model::DBIC::Schema>
459
460 =head1 AUTHOR
461
462 Brandon L Black, C<blblack@gmail.com>
463
464 Contributors:
465
466 Rafael Kitover, C<<rkitover at cpan.org>>
467
468 =head1 LICENSE
469
470 This library is free software, you can redistribute it and/or modify
471 it under the same terms as Perl itself.
472
473 =cut
474
475 1;
476
477 __DATA__
478
479 =begin pod_to_ignore
480
481 __schemaclass__
482 package [% schema_class %];
483
484 use strict;
485 use base qw/DBIx::Class::Schema::Loader/;
486
487 __PACKAGE__->loader_options(
488     [%- FOREACH key = loader_args.keys %]
489     [% key %] => [% loader_args.${key} %],
490     [%- END -%]
491
492 );
493
494 =head1 NAME
495
496 [% schema_class %] - L<DBIx::Class::Schema::Loader> class
497
498 =head1 SYNOPSIS
499
500 See L<[% app %]>
501
502 =head1 DESCRIPTION
503
504 Dynamic L<DBIx::Class::Schema::Loader> schema for use in L<[% class %]>
505
506 =head1 GENERATED BY
507
508 [% generator %] - [% generator_version %]
509
510 =head1 AUTHOR
511
512 [% author.replace(',+$', '') %]
513
514 =head1 LICENSE
515
516 This library is free software, you can redistribute it and/or modify
517 it under the same terms as Perl itself.
518
519 =cut
520
521 1;
522
523 __compclass__
524 package [% class %];
525
526 use strict;
527 use base 'Catalyst::Model::DBIC::Schema';
528
529 __PACKAGE__->config(
530     schema_class => '[% schema_class %]',
531     [% IF roles %]roles => [% roles %],[% END %]
532     [% IF setup_connect_info %]connect_info => {
533         [%- FOREACH key = connect_info.keys %]
534         [% key %] => [% connect_info.${key} %],
535         [%- END -%]
536
537     }[% END %]
538 );
539
540 =head1 NAME
541
542 [% class %] - Catalyst DBIC Schema Model
543
544 =head1 SYNOPSIS
545
546 See L<[% app %]>
547
548 =head1 DESCRIPTION
549
550 L<Catalyst::Model::DBIC::Schema> Model using schema L<[% schema_class %]>
551
552 =head1 GENERATED BY
553
554 [% generator %] - [% generator_version %]
555
556 =head1 AUTHOR
557
558 [% author.replace(',+$', '') %]
559
560 =head1 LICENSE
561
562 This library is free software, you can redistribute it and/or modify
563 it under the same terms as Perl itself.
564
565 =cut
566
567 1;