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