release 0.49
[catagits/Catalyst-Model-DBIC-Schema.git] / lib / Catalyst / Helper / Model / DBIC / Schema.pm
1 package Catalyst::Helper::Model::DBIC::Schema;
2
3 use namespace::autoclean;
4 use Moose;
5 no warnings 'uninitialized';
6
7 our $VERSION = '0.49';
8 $VERSION = eval $VERSION;
9
10 use Carp;
11 use Tie::IxHash ();
12 use Data::Dumper ();
13 use List::Util 'first';
14 use MooseX::Types::Moose qw/Str HashRef Bool ArrayRef/;
15 use Catalyst::Model::DBIC::Schema::Types 'CreateOption';
16 use List::MoreUtils 'firstidx';
17 use Scalar::Util 'looks_like_number';
18 use File::Find 'finddepth';
19 use Try::Tiny;
20 use Cwd 'getcwd';
21
22 =head1 NAME
23
24 Catalyst::Helper::Model::DBIC::Schema - Helper for DBIC Schema Models
25
26 =head1 SYNOPSIS
27
28   script/create.pl model CatalystModelName DBIC::Schema MyApp::SchemaClass \
29     [ create=dynamic | create=static ] [ traits=trait1,trait2... ] \
30     [ Schema::Loader opts ] [ dsn user pass ] \
31     [ other connect_info args ]
32
33 =head1 DESCRIPTION
34
35 Helper for the DBIC Schema Models.
36
37 =head2 Arguments:
38
39 C<CatalystModelName> is the short name for the Catalyst Model class
40 being generated (i.e. callable with C<$c-E<gt>model('CatalystModelName')>).
41
42 C<MyApp::SchemaClass> is the fully qualified classname of your Schema,
43 which might or might not yet exist.  Note that you should have a good
44 reason to create this under a new global namespace, otherwise use an
45 existing top level namespace for your schema class.
46
47 C<create=dynamic> instructs this Helper to generate the named Schema
48 class for you, basing it on L<DBIx::Class::Schema::Loader> (which
49 means the table information will always be dynamically loaded at
50 runtime from the database).
51
52 C<create=static> instructs this Helper to generate the named Schema
53 class for you, using L<DBIx::Class::Schema::Loader> in "one shot"
54 mode to create a standard, manually-defined L<DBIx::Class::Schema>
55 setup, based on what the Loader sees in your database at this moment.
56 A Schema/Model pair generated this way will not require
57 L<DBIx::Class::Schema::Loader> at runtime, and will not automatically
58 adapt itself to changes in your database structure.  You can edit
59 the generated classes by hand to refine them.
60
61 C<traits> is the list of traits to apply to the model, see
62 L<Catalyst::Model::DBIC::Schema> for details.
63
64 C<Schema::Loader opts> are described in L</TYPICAL EXAMPLES> below.
65
66 C<connect_info> arguments are the same as what
67 DBIx::Class::Schema::connect expects, and are storage_type-specific.
68 For DBI-based storage, these arguments are the dsn, username,
69 password, and connect options, respectively.  These are optional for
70 existing Schemas, but required if you use either of the C<create=>
71 options.
72
73 username and password can be omitted for C<SQLite> dsns.
74
75 Use of either of the C<create=> options requires L<DBIx::Class::Schema::Loader>.
76
77 =head1 TYPICAL EXAMPLES
78
79 Use DBIx::Class::Schema::Loader to create a static DBIx::Class::Schema,
80 and a Model which references it:
81
82   script/myapp_create.pl model CatalystModelName DBIC::Schema \
83     MyApp::SchemaClass create=static dbi:mysql:foodb myuname mypass
84
85 Same, with extra connect_info args
86 user and pass can be omitted for sqlite, since they are always empty
87
88   script/myapp_create.pl model CatalystModelName DBIC::Schema \
89     MyApp::SchemaClass create=static dbi:SQLite:foo.db \
90     AutoCommit=1 cursor_class=DBIx::Class::Cursor::Cached \
91     on_connect_do='["select 1", "select 2"]' quote_names=1
92
93 B<ON WINDOWS COMMAND LINES QUOTING RULES ARE DIFFERENT>
94
95 In C<cmd.exe> the above example would be:
96
97   script/myapp_create.pl model CatalystModelName DBIC::Schema \
98     MyApp::SchemaClass create=static dbi:SQLite:foo.db \
99     AutoCommit=1 cursor_class=DBIx::Class::Cursor::Cached \
100     on_connect_do="[\"select 1\", \"select 2\"]" quote_names=1
101
102 Same, but with extra Schema::Loader args (separate multiple values by commas):
103
104   script/myapp_create.pl model CatalystModelName DBIC::Schema \
105     MyApp::SchemaClass create=static db_schema=foodb components=Foo,Bar \
106     exclude='^(wibble|wobble)$' moniker_map='{ foo => "FOO" }' \
107     dbi:Pg:dbname=foodb myuname mypass
108
109 Coderefs are also supported:
110
111   script/myapp_create.pl model CatalystModelName DBIC::Schema \
112     MyApp::SchemaClass create=static \
113     inflect_singular='sub { $_[0] =~ /\A(.+?)(_id)?\z/; $1 }' \
114     moniker_map='sub { join(q{}, map ucfirst, split(/[\W_]+/, lc $_[0])); }' \
115     dbi:mysql:foodb myuname mypass
116
117 See L<DBIx::Class::Schema::Loader::Base> for a list of options
118
119 Create a dynamic DBIx::Class::Schema::Loader-based Schema,
120 and a Model which references it (B<DEPRECATED>):
121
122   script/myapp_create.pl model CatalystModelName DBIC::Schema \
123     MyApp::SchemaClass create=dynamic dbi:mysql:foodb myuname mypass
124
125 Reference an existing Schema of any kind, and provide some connection information for ->config:
126
127   script/myapp_create.pl model CatalystModelName DBIC::Schema \
128     MyApp::SchemaClass dbi:mysql:foodb myuname mypass
129
130 Same, but don't supply connect information yet (you'll need to do this
131 in your app config, or [not recommended] in the schema itself).
132
133   script/myapp_create.pl model ModelName DBIC::Schema My::SchemaClass
134
135 =cut
136
137 has helper => (is => 'ro', isa => 'Catalyst::Helper', required => 1);
138 has create => (is => 'rw', isa => CreateOption);
139 has args => (is => 'ro', isa => ArrayRef);
140 has traits => (is => 'rw', isa => ArrayRef);
141 has schema_class => (is => 'ro', isa => Str, required => 1);
142 has loader_args => (is => 'rw', isa => HashRef);
143 has connect_info => (is => 'rw', isa => HashRef);
144 has old_schema => (is => 'rw', isa => Bool, lazy_build => 1);
145 has is_moose_schema => (is => 'rw', isa => Bool, lazy_build => 1);
146 has result_namespace => (is => 'rw', isa => Str, lazy_build => 1);
147 has components => (is => 'rw', isa => ArrayRef);
148
149 =head1 METHODS
150
151 =head2 mk_compclass
152
153 This is called by L<Catalyst::Helper> with the commandline args to generate the
154 files.
155
156 =cut
157
158 sub mk_compclass {
159     my ($package, $helper, $schema_class, @args) = @_;
160
161     my $self = $package->new(
162         helper => $helper,
163         schema_class => $schema_class,
164         args => \@args
165     );
166
167     $self->run;
168 }
169
170 sub BUILD {
171     my $self   = shift;
172     my $helper = $self->helper;
173     my @args   = @{ $self->args || [] };
174
175     $helper->{schema_class} = $self->schema_class;
176
177     @args = $self->_cleanup_args(\@args);
178
179     my ($traits_idx, $traits);
180     if (($traits_idx = firstidx { ($traits) = /^traits=(\S*)\z/ } @args) != -1) {
181         my @traits = split /,/ => $traits;
182
183         $self->traits(\@traits);
184
185         $helper->{traits} = '['
186             .(join ',' => map { qq{'$_'} } @traits)
187             .']';
188
189         splice @args, $traits_idx, 1, ();
190     }
191
192     if ($args[0] && $args[0] =~ /^create=(\S*)\z/) {
193         $self->create($1);
194         shift @args;
195
196         if (@args) {
197             $self->_parse_loader_args(\@args);
198
199             $helper->{loader_args} = $self->_build_helper_loader_args;
200         }
201     }
202
203     my $dbi_dsn_part;
204     if (first { ($dbi_dsn_part) = /^(dbi):/i } @args) {
205         die
206 qq{DSN must start with 'dbi:' not '$dbi_dsn_part' (case matters!)}
207             if $dbi_dsn_part ne 'dbi';
208
209         $helper->{setup_connect_info} = 1;
210
211         $helper->{connect_info} =
212             $self->_build_helper_connect_info(\@args);
213
214         $self->_parse_connect_info(\@args);
215     }
216
217     $helper->{generator} = ref $self;
218     $helper->{generator_version} = $VERSION;
219 }
220
221 =head2 run
222
223 Can be called on an instance to generate the files.
224
225 =cut
226
227 sub run {
228     my $self = shift;
229
230     if ($self->create eq 'dynamic') {
231         $self->_print_dynamic_deprecation_warning;
232         $self->_gen_dynamic_schema;
233     } elsif ($self->create eq 'static') {
234         $self->_gen_static_schema;
235     }
236
237     $self->_gen_model;
238 }
239
240 sub _parse_loader_args {
241     my ($self, $args) = @_;
242
243     my %loader_args = $self->_read_loader_args($args);
244
245     while (my ($key, $val) = each %loader_args) {
246         next if $key =~ /^(?:components|constraint|exclude)\z/;
247
248         $loader_args{$key} = $self->_eval($val);
249         die "syntax error for loader args key '$key' with value '$val': $@"
250             if $@;
251     }
252
253     my @components = $self->_build_loader_components(
254         delete $loader_args{components},
255         $loader_args{use_namespaces},
256     );
257
258     $self->components(\@components);
259
260     for my $re_opt (qw/constraint exclude/) {
261         $loader_args{$re_opt} = qr/$loader_args{$re_opt}/
262         if exists $loader_args{$re_opt};
263     }
264
265     tie my %result, 'Tie::IxHash';
266
267     %result = (
268         relationships => 1,
269         use_moose => $self->is_moose_schema ? 1 : 0,
270         col_collision_map => 'column_%s',
271         (!$self->old_schema ? (
272                 use_namespaces => 1
273             ) : ()),
274         (@components ? (
275                 components => \@components
276             ) : ()),
277         (%loader_args ? %loader_args : ()),
278     );
279
280     $self->loader_args(\%result);
281
282     wantarray ? %result : \%result;
283 }
284
285 sub _read_loader_args {
286     my ($self, $args) = @_;
287
288     my %loader_args;
289
290     while (@$args && $args->[0] !~ /^dbi:/i) {
291         my ($key, $val) = split /=/, shift(@$args), 2;
292
293         if ($self->_is_struct($val)) {
294             $loader_args{$key} = $val;
295         } elsif ((my @vals = split /,/ => $val) > 1) {
296             $loader_args{$key} = \@vals;
297         } else {
298             $loader_args{$key} = $val;
299         }
300     }
301
302     wantarray ? %loader_args : \%loader_args;
303 }
304
305 sub _build_helper_loader_args {
306     my $self = shift;
307
308     my $args = $self->loader_args;
309
310     tie my %loader_args, 'Tie::IxHash';
311
312     while (my ($arg, $val) = each %$args) {
313         if (ref $val) {
314             $loader_args{$arg} = $self->_data_struct_to_string($val);
315         } else {
316             $loader_args{$arg} = qq{'$val'};
317         }
318     }
319
320     \%loader_args
321 }
322
323 sub _build_loader_components {
324     my ($self, $components, $use_namespaces) = @_;
325
326     my @components = $self->old_schema && (not $use_namespaces) ? ()
327         : ('InflateColumn::DateTime');
328
329     if ($components) {
330         $components = [ $components ] if !ref $components;
331         push @components, @$components;
332     }
333
334     wantarray ? @components : \@components;
335 }
336
337 sub _build_helper_connect_info {
338     my ($self, $connect_info) = @_;
339
340     my @connect_info = @$connect_info;
341
342     my ($dsn, $user, $password) = $self->_get_dsn_user_pass(\@connect_info);
343
344     tie my %helper_connect_info, 'Tie::IxHash';
345
346     %helper_connect_info = (
347         dsn => qq{'$dsn'},
348         user => qq{'$user'},
349         password => qq{'$password'}
350     );
351
352     for (@connect_info) {
353         if (/^\s*{.*}\s*\z/) {
354             my $hash = $self->_eval($_);
355             die "Syntax errorr in connect_info hash: $_: $@" if $@;
356             my %hash = %$hash;
357
358             for my $key (keys %hash) {
359                 my $val = $hash{$key};
360
361                 if (ref $val) {
362                     $val = $self->_data_struct_to_string($val);
363                 } else {
364                     $val = $self->_quote($val);
365                 }
366
367                 $helper_connect_info{$key} = $val;
368             }
369
370             next;
371         }
372
373         my ($key, $val) = split /=/, $_, 2;
374
375         if ($key eq 'quote_char') {
376             $helper_connect_info{$key} = length($val) == 1 ?
377                 $self->_quote($val) :
378                 $self->_data_struct_to_string([split //, $val]);
379         } else {
380             $helper_connect_info{$key} = $self->_quote_unless_struct($val);
381         }
382     }
383
384     \%helper_connect_info
385 }
386
387 sub _build_old_schema {
388     my $self = shift;
389
390     return $self->result_namespace eq '' ? 1 : 0;
391 }
392
393 sub _build_is_moose_schema {
394     my $self = shift;
395
396     my @schema_parts = split '::', $self->schema_class;
397
398     my $result_dir = File::Spec->catfile(
399         $self->helper->{base}, 'lib', @schema_parts, $self->result_namespace
400     );
401
402     # assume yes for new schemas
403     return 1 if not -d $result_dir;
404
405     my $uses_moose = 1;
406
407     my $cwd = getcwd;
408
409     try {
410         finddepth(sub {
411             return if $File::Find::name !~ /\.pm\z/;
412
413             open my $fh, '<', $File::Find::name
414                 or die "Could not open $File::Find::name: $!";
415
416             my $code = do { local $/; <$fh> };
417             close $fh;
418
419             $uses_moose = 0 if $code !~ /\nuse Moose;\n/;
420
421             die;
422         }, $result_dir);
423     };
424
425     chdir $cwd;
426
427     return $uses_moose;
428 }
429
430 sub _build_result_namespace {
431     my $self = shift;
432
433     my @schema_parts = split '::', $self->schema_class;
434     my $schema_pm =
435         File::Spec->catfile($self->helper->{base}, 'lib', @schema_parts) . '.pm';
436
437     if (not -f $schema_pm) {
438         eval { Class::MOP::load_class('DBIx::Class::Schema::Loader') };
439
440         return 'Result' if $@;
441
442         return (try { DBIx::Class::Schema::Loader->VERSION('0.05') }) ? 'Result' : '';
443     }
444
445     open my $fh, '<', $schema_pm or die "Could not open $schema_pm: $!";
446     my $code = do { local $/; <$fh> };
447     close $fh;
448
449     my ($result_namespace) = $code =~ /result_namespace => '([^']+)'/;
450
451     return $result_namespace if $result_namespace;
452
453     return '' if $code =~ /->load_classes/;
454
455     return 'Result';
456 }
457
458 sub _data_struct_to_string {
459     my ($self, $data) = @_;
460
461     local $Data::Dumper::Terse = 1;
462     local $Data::Dumper::Quotekeys = 0;
463     local $Data::Dumper::Indent = 0;
464     local $Data::Dumper::Useqq = 1;
465
466     return Data::Dumper->Dump([$data]);
467 }
468
469 sub _get_dsn_user_pass {
470     my ($self, $connect_info) = @_;
471
472     my $dsn = shift @$connect_info;
473     my ($user, $password);
474
475     if ($dsn =~ /sqlite/i) {
476         ($user, $password) = ('', '');
477         shift @$connect_info while @$connect_info and $connect_info->[0] eq '';
478     } else {
479         ($user, $password) = splice @$connect_info, 0, 2;
480     }
481     
482     ($dsn, $user, $password)
483 }
484
485 sub _parse_connect_info {
486     my ($self, $connect_info) = @_;
487
488     my @connect_info = @$connect_info;
489
490     my ($dsn, $user, $password) = $self->_get_dsn_user_pass(\@connect_info);
491
492     tie my %connect_info, 'Tie::IxHash';
493     @connect_info{qw/dsn user password/} = ($dsn, $user, $password);
494
495     for (@connect_info) {
496         if (/^\s*{.*}\s*\z/) {
497             my $hash = $self->_eval($_);
498             die "Syntax errorr in connect_info hash: $_: $@" if $@;
499
500             %connect_info = (%connect_info, %$hash);
501
502             next;
503         }
504
505         my ($key, $val) = split /=/, $_, 2;
506
507         if ($key eq 'quote_char') {
508             $connect_info{$key} = length($val) == 1 ? $val : [split //, $val];
509         } elsif ($key =~ /^(?:name_sep|limit_dialect)\z/) {
510             $connect_info{$key} = $val;
511         } else {
512             $connect_info{$key} = $self->_eval($val);
513         }
514
515         die "syntax error for connect_info key '$key' with value '$val': $@"
516             if $@;
517     }
518
519     $self->connect_info(\%connect_info);
520
521     \%connect_info
522 }
523
524 sub _is_struct {
525     my ($self, $val) = @_;
526
527     return $val =~ /^\s*(?:sub|[[{])/;
528 }
529
530 sub _quote {
531     my ($self, $val) = @_;
532
533     return 'q{'.$val.'}';
534 }
535
536 sub _quote_unless_struct {
537     my ($self, $val) = @_;
538
539     $val = $self->_quote($val) if not $self->_is_struct($val);
540
541     return $val;
542 }
543
544 sub _eval {
545     my ($self, $code) = @_;
546
547     return $code if looks_like_number $code;
548
549     return $code if not $self->_is_struct($code);
550
551     return eval "{no strict; $code}";
552 }
553
554 sub _gen_dynamic_schema {
555     my $self = shift;
556
557     my $helper = $self->helper;
558
559     my @schema_parts = split(/\:\:/, $self->schema_class);
560     my $schema_file_part = pop @schema_parts;
561
562     my $schema_dir  = File::Spec->catfile(
563         $helper->{base}, 'lib', @schema_parts
564     );
565     my $schema_file = File::Spec->catfile(
566         $schema_dir, $schema_file_part . '.pm'
567     );
568
569     $helper->mk_dir($schema_dir);
570     $helper->render_file('schemaclass', $schema_file);
571 }
572
573 sub _gen_static_schema {
574     my $self = shift;
575
576     die "cannot load schema without connect info" unless $self->connect_info;
577
578     my $helper = $self->helper;
579
580     my $schema_dir = File::Spec->catfile($helper->{base}, 'lib');
581
582     try {
583         Class::MOP::load_class('DBIx::Class::Schema::Loader')
584     }
585     catch {
586         die "Cannot load DBIx::Class::Schema::Loader: $_";
587     };
588
589     DBIx::Class::Schema::Loader->import(
590         "dump_to_dir:$schema_dir", 'make_schema_at'
591     );
592
593     make_schema_at(
594         $self->schema_class,
595         $self->loader_args,
596         [$self->connect_info]
597     );
598 }
599
600 sub _gen_model {
601     my $self = shift;
602     my $helper = $self->helper;
603
604     $helper->render_file('compclass', $helper->{file} );
605 }
606
607 sub _print_dynamic_deprecation_warning {
608     warn <<EOF;
609 ************************************ WARNING **********************************
610 * create=dynamic is DEPRECATED, please use create=static instead.             *
611 *******************************************************************************
612 EOF
613     print "Continue? [y/n]: ";
614     chomp(my $response = <STDIN>);
615     exit 0 if $response =~ /^n(o)?\z/;
616 }
617
618 sub _cleanup_args {
619     my ($self, $args) = @_;
620
621 # remove blanks, ie. someoned doing foo \  bar
622     my @res = grep !/^\s+\z/, @$args;
623
624 # remove leading whitespace, ie. foo \ bar
625     s/^\s*// for @res;
626
627     @res
628 }
629
630 =head1 SEE ALSO
631
632 General Catalyst Stuff:
633
634 L<Catalyst::Manual>, L<Catalyst::Test>, L<Catalyst::Request>,
635 L<Catalyst::Response>, L<Catalyst::Helper>, L<Catalyst>,
636
637 Stuff related to DBIC and this Model style:
638
639 L<DBIx::Class>, L<DBIx::Class::Schema>,
640 L<DBIx::Class::Schema::Loader>, L<Catalyst::Model::DBIC::Schema>
641
642 =head1 AUTHOR
643
644 See L<Catalyst::Model::DBIC::Schema/AUTHOR> and
645 L<Catalyst::Model::DBIC::Schema/CONTRIBUTORS>.
646
647 =head1 COPYRIGHT
648
649 See L<Catalyst::Model::DBIC::Schema/COPYRIGHT>.
650
651 =head1 LICENSE
652
653 This library is free software, you can redistribute it and/or modify
654 it under the same terms as Perl itself.
655
656 =cut
657
658 1;
659
660 __DATA__
661
662 =begin pod_to_ignore
663
664 __schemaclass__
665 package [% schema_class %];
666
667 use strict;
668 use base qw/DBIx::Class::Schema::Loader/;
669
670 __PACKAGE__->loader_options(
671     [%- FOREACH key = loader_args.keys %]
672     [% key %] => [% loader_args.${key} %],
673     [%- END -%]
674
675 );
676
677 =head1 NAME
678
679 [% schema_class %] - L<DBIx::Class::Schema::Loader> class
680
681 =head1 SYNOPSIS
682
683 See L<[% app %]>
684
685 =head1 DESCRIPTION
686
687 Dynamic L<DBIx::Class::Schema::Loader> schema for use in L<[% class %]>
688
689 =head1 GENERATED BY
690
691 [% generator %] - [% generator_version %]
692
693 =head1 AUTHOR
694
695 [% author.replace(',+$', '') %]
696
697 =head1 LICENSE
698
699 This library is free software, you can redistribute it and/or modify
700 it under the same terms as Perl itself.
701
702 =cut
703
704 1;
705
706 __compclass__
707 package [% class %];
708
709 use strict;
710 use base 'Catalyst::Model::DBIC::Schema';
711
712 __PACKAGE__->config(
713     schema_class => '[% schema_class %]',
714     [% IF traits %]traits => [% traits %],[% END %]
715     [% IF setup_connect_info %]connect_info => {
716         [%- FOREACH key = connect_info.keys %]
717         [% key %] => [% connect_info.${key} %],
718         [%- END -%]
719
720     }[% END %]
721 );
722
723 =head1 NAME
724
725 [% class %] - Catalyst DBIC Schema Model
726
727 =head1 SYNOPSIS
728
729 See L<[% app %]>
730
731 =head1 DESCRIPTION
732
733 L<Catalyst::Model::DBIC::Schema> Model using schema L<[% schema_class %]>
734
735 =head1 GENERATED BY
736
737 [% generator %] - [% generator_version %]
738
739 =head1 AUTHOR
740
741 [% author.replace(',+$', '') %]
742
743 =head1 LICENSE
744
745 This library is free software, you can redistribute it and/or modify
746 it under the same terms as Perl itself.
747
748 =cut
749
750 1;
751 __END__
752 # vim:sts=4 sw=4: