8c8d0f60fcf66f87b88ceceec1df7d623ff7baf5
[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 username and password can be omitted for C<SQLite> dsns.
68
69 Use of either of the C<create=> options requires L<DBIx::Class::Schema::Loader>.
70
71 =head1 TYPICAL EXAMPLES
72
73 Use DBIx::Class::Schema::Loader to create a static DBIx::Class::Schema,
74 and a Model which references it:
75
76   script/myapp_create.pl model CatalystModelName DBIC::Schema \
77     MyApp::SchemaClass create=static dbi:mysql:foodb myuname mypass
78
79 Same, with extra connect_info args
80 user and pass can be omitted for sqlite, since they are always empty
81
82   script/myapp_create.pl model CatalystModelName DBIC::Schema \
83     MyApp::SchemaClass create=static dbi:SQLite:foo.db \
84     AutoCommit=1 cursor_class=DBIx::Class::Cursor::Cached \
85     on_connect_do='["select 1", "select 2"]' quote_char='"'
86
87 B<ON WINDOWS COMMAND LINES QUOTING RULES ARE DIFFERENT>
88
89 In C<cmd.exe> the above example would be:
90
91   script/myapp_create.pl model CatalystModelName DBIC::Schema \
92     MyApp::SchemaClass create=static dbi:SQLite:foo.db \
93     AutoCommit=1 cursor_class=DBIx::Class::Cursor::Cached \
94     on_connect_do="[\"select 1\", \"select 2\"]" quote_char="\""
95
96 Same, but with extra Schema::Loader args (separate multiple values by commas):
97
98   script/myapp_create.pl model CatalystModelName DBIC::Schema \
99     MyApp::SchemaClass create=static db_schema=foodb components=Foo,Bar \
100     exclude='^wibble|wobble$' moniker_map='{ foo => "FOO" }' \
101     dbi:Pg:dbname=foodb myuname mypass
102
103 See L<DBIx::Class::Schema::Loader::Base> for a list of options
104
105 Create a dynamic DBIx::Class::Schema::Loader-based Schema,
106 and a Model which references it (B<DEPRECATED>):
107
108   script/myapp_create.pl model CatalystModelName DBIC::Schema \
109     MyApp::SchemaClass create=dynamic dbi:mysql:foodb myuname mypass
110
111 Reference an existing Schema of any kind, and provide some connection information for ->config:
112
113   script/myapp_create.pl model CatalystModelName DBIC::Schema \
114     MyApp::SchemaClass dbi:mysql:foodb myuname mypass
115
116 Same, but don't supply connect information yet (you'll need to do this
117 in your app config, or [not recommended] in the schema itself).
118
119   script/myapp_create.pl model ModelName DBIC::Schema My::SchemaClass
120
121 =cut
122
123 has helper => (is => 'ro', isa => 'Catalyst::Helper', required => 1);
124
125 has schema_class => (is => 'ro', isa => Str, required => 1);
126
127 has loader_args => (is => 'rw', isa => HashRef);
128 has connect_info => (is => 'rw', isa => HashRef);
129
130 has old_schema => (is => 'rw', isa => Bool, lazy_build => 1);
131
132 =head1 METHODS
133
134 =head2 mk_compclass
135
136 This is called by L<Catalyst::Helper> with the commandline args to generate the
137 files.
138
139 =cut
140
141 sub mk_compclass {
142     my ($package, $helper, $schema_class, @args) = @_;
143
144     my $self = $package->new(helper => $helper, schema_class => $schema_class);
145
146     $helper->{schema_class} = $schema_class;
147
148     @args = $self->_cleanup_args(\@args);
149
150     my $create = '';
151     if ($args[0] && $args[0] =~ /^create=(dynamic|static)\z/) {
152         $create = $1;
153         shift @args;
154
155         if ($args[0] && $args[0] =~ /^roles=(.*)\z/) {
156             $helper->{roles} = '['
157                 .(join ',' => map { qq{'$_'} } (split /,/ => $1))
158                 .']';
159             shift @args;
160         }
161
162         if (@args) {
163             $self->_parse_loader_args(\@args);
164
165             if (first { /^dbi:/i } @args) {
166                 $helper->{setup_connect_info} = 1;
167
168                 $helper->{connect_info} =
169                     $self->_build_helper_connect_info(\@args);
170
171                 $self->_parse_connect_info(\@args) if $create eq 'static';
172             }
173         }
174     }
175
176     $helper->{generator} = ref $self;
177     $helper->{generator_version} = $VERSION;
178
179     if ($create eq 'dynamic') {
180         $self->_print_dynamic_deprecation_warning;
181         $self->helper->{loader_args} = $self->_build_helper_loader_args;
182         $self->_gen_dynamic_schema;
183     } elsif ($create eq 'static') {
184         $self->_gen_static_schema;
185     }
186
187     $self->_gen_model;
188 }
189
190 sub _parse_loader_args {
191     my ($self, $args) = @_;
192
193     my %loader_args = $self->_read_loader_args($args);
194
195     while (my ($key, $val) = each %loader_args) {
196         next if $key =~ /^(?:components|constraint|exclude)\z/;
197
198         $loader_args{$key} = eval $val;
199         die "syntax error for loader args key '$key' with value '$val': $@"
200             if $@;
201     }
202
203     my @components =
204     $self->_build_loader_components(delete $loader_args{components});
205
206     for my $re_opt (qw/constraint exclude/) {
207         $loader_args{$re_opt} = qr/$loader_args{$re_opt}/
208         if exists $loader_args{$re_opt};
209     }
210
211     tie my %result, 'Tie::IxHash';
212
213     %result = (
214         relationships => 1,
215         (%loader_args ? %loader_args : ()),
216         (!$self->old_schema ? (
217                 use_namespaces => 1
218             ) : ()),
219         (@components ? (
220                 components => \@components
221             ) : ())
222     );
223
224     $self->loader_args(\%result);
225
226     wantarray ? %result : \%result;
227 }
228
229 sub _read_loader_args {
230     my ($self, $args) = @_;
231
232     my %loader_args;
233
234     while (@$args && $args->[0] !~ /^dbi:/) {
235         my ($key, $val) = split /=/, shift(@$args), 2;
236
237         if ((my @vals = split /,/ => $val) > 1) {
238             $loader_args{$key} = \@vals;
239         } else {
240             $loader_args{$key} = $val;
241         }
242     }
243
244     wantarray ? %loader_args : \%loader_args;
245 }
246
247 sub _build_helper_loader_args {
248     my $self = shift;
249
250     my $args = $self->loader_args;
251
252     tie my %loader_args, 'Tie::IxHash';
253
254     while (my ($arg, $val) = each %$args) {
255         if (ref $val) {
256             $loader_args{$arg} = $self->_data_struct_to_string($val);
257         } else {
258             $loader_args{$arg} = qq{'$val'};
259         }
260     }
261
262     \%loader_args
263 }
264
265 sub _build_loader_components {
266     my ($self, $components) = @_;
267
268     my @components = $self->old_schema ? () : ('InflateColumn::DateTime');
269
270     if ($components) {
271         $components = [ $components ] if !ref $components;
272         push @components, @$components;
273     }
274
275     wantarray ? @components : \@components;
276 }
277
278 sub _build_helper_connect_info {
279     my ($self, $connect_info) = @_;
280
281     my @connect_info = @$connect_info;
282
283     my ($dsn, $user, $password) = $self->_get_dsn_user_pass(\@connect_info);
284
285     tie my %helper_connect_info, 'Tie::IxHash';
286
287     %helper_connect_info = (
288         dsn => qq{'$dsn'},
289         user => qq{'$user'},
290         password => qq{'$password'}
291     );
292
293     for (@connect_info) {
294         if (/^\s*{.*}\s*\z/) {
295             my $hash = eval $_;
296             die "Syntax errorr in connect_info hash: $_: $@" if $@;
297             my %hash = %$hash;
298
299             for my $key (keys %hash) {
300                 my $val = $hash{$key};
301
302                 if (ref $val) {
303                     $val = $self->_data_struct_to_string($val);
304                 } else {
305                     $val = qq{'$val'};
306                 }
307
308                 $helper_connect_info{$key} = $val;
309             }
310
311             next;
312         }
313
314         my ($key, $val) = split /=/, $_, 2;
315
316         $helper_connect_info{$key} = $self->_quote_unless_struct($val);
317     }
318
319     \%helper_connect_info
320 }
321
322 sub _build_old_schema {
323     my $self = shift;
324
325     my @schema_pm   = split '::', $self->schema_class;
326     $schema_pm[-1] .= '.pm';
327     my $schema_file =
328     File::Spec->catfile($self->helper->{base}, 'lib', @schema_pm);
329
330     if (-f $schema_file) {
331         my $schema_code = do { local (@ARGV, $/) = $schema_file; <> };
332         return 1 if $schema_code =~ /->load_classes/;
333     }
334
335     0;
336 }
337
338 sub _data_struct_to_string {
339     my ($self, $data) = @_;
340
341     local $Data::Dumper::Terse = 1;
342     local $Data::Dumper::Quotekeys = 0;
343     local $Data::Dumper::Indent = 0;
344     local $Data::Dumper::Useqq = 1;
345
346     return Data::Dumper->Dump([$data]);
347 }
348
349 sub _get_dsn_user_pass {
350     my ($self, $connect_info) = @_;
351
352     my $dsn = shift @$connect_info;
353     my ($user, $password);
354
355     if ($dsn =~ /sqlite/i) {
356         ($user, $password) = ('', '');
357         shift @$connect_info while $connect_info->[0] eq '';
358     } else {
359         ($user, $password) = splice @$connect_info, 0, 2;
360     }
361     
362     ($dsn, $user, $password)
363 }
364
365 sub _parse_connect_info {
366     my ($self, $connect_info) = @_;
367
368     my @connect_info = @$connect_info;
369
370     my ($dsn, $user, $password) = $self->_get_dsn_user_pass(\@connect_info);
371
372     tie my %connect_info, 'Tie::IxHash';
373     @connect_info{qw/dsn user password/} = ($dsn, $user, $password);
374
375     for (@connect_info) {
376         if (/^\s*{.*}\s*\z/) {
377             my $hash = eval $_;
378             die "Syntax errorr in connect_info hash: $_: $@" if $@;
379
380             %connect_info = (%connect_info, %$hash);
381
382             next;
383         }
384
385         my ($key, $val) = split /=/, $_, 2;
386
387         if ($key =~ /^(?:quote_char|name_sep)\z/) {
388             $connect_info{$key} = $val;
389         } else {
390             $connect_info{$key} = eval $val;
391         }
392
393         die "syntax error for connect_info key '$key' with value '$val': $@"
394             if $@;
395     }
396
397     $self->connect_info(\%connect_info);
398
399     \%connect_info
400 }
401
402 sub _quote_unless_struct {
403     my ($self, $val) = @_;
404
405     $val = qq{'$val'} if $val !~ /^\s*[[{]/;
406
407     $val;
408 }
409
410 sub _gen_dynamic_schema {
411     my $self = shift;
412
413     my $helper = $self->helper;
414
415     my @schema_parts = split(/\:\:/, $self->schema_class);
416     my $schema_file_part = pop @schema_parts;
417
418     my $schema_dir  = File::Spec->catfile(
419         $helper->{base}, 'lib', @schema_parts
420     );
421     my $schema_file = File::Spec->catfile(
422         $schema_dir, $schema_file_part . '.pm'
423     );
424
425     $helper->mk_dir($schema_dir);
426     $helper->render_file('schemaclass', $schema_file);
427 }
428
429 sub _gen_static_schema {
430     my $self = shift;
431
432     die "cannot load schema without connect info" unless $self->connect_info;
433
434     my $helper = $self->helper;
435
436     my $schema_dir = File::Spec->catfile($helper->{base}, 'lib');
437
438     eval { Class::MOP::load_class('DBIx::Class::Schema::Loader') };
439     die "Cannot load DBIx::Class::Schema::Loader: $@" if $@;
440
441     DBIx::Class::Schema::Loader->import(
442         "dump_to_dir:$schema_dir", 'make_schema_at'
443     );
444
445     make_schema_at(
446         $self->schema_class,
447         $self->loader_args,
448         [$self->connect_info]
449     );
450 }
451
452 sub _gen_model {
453     my $self = shift;
454     my $helper = $self->helper;
455
456     $helper->render_file('compclass', $helper->{file} );
457 }
458
459 sub _print_dynamic_deprecation_warning {
460     warn <<EOF;
461 ************************************ WARNING **********************************
462 * create=dynamic is DEPRECATED, please use create=static instead.             *
463 *******************************************************************************
464 EOF
465     print "Continue? [y/n]: ";
466     chomp(my $response = <STDIN>);
467     exit 0 if $response =~ /^n(o)?\z/;
468 }
469
470 sub _cleanup_args {
471     my ($self, $args) = @_;
472
473 # remove blanks, ie. someoned doing foo \  bar
474     my @res = grep !/^\s+\z/, @$args;
475
476 # remove leading whitespace, ie. foo \ bar
477     s/^\s*// for @res;
478
479     @res
480 }
481
482 =head1 SEE ALSO
483
484 General Catalyst Stuff:
485
486 L<Catalyst::Manual>, L<Catalyst::Test>, L<Catalyst::Request>,
487 L<Catalyst::Response>, L<Catalyst::Helper>, L<Catalyst>,
488
489 Stuff related to DBIC and this Model style:
490
491 L<DBIx::Class>, L<DBIx::Class::Schema>,
492 L<DBIx::Class::Schema::Loader>, L<Catalyst::Model::DBIC::Schema>
493
494 =head1 AUTHOR
495
496 Brandon L Black, C<blblack@gmail.com>
497
498 Contributors:
499
500 Rafael Kitover, C<<rkitover at cpan.org>>
501
502 =head1 LICENSE
503
504 This library is free software, you can redistribute it and/or modify
505 it under the same terms as Perl itself.
506
507 =cut
508
509 1;
510
511 __DATA__
512
513 =begin pod_to_ignore
514
515 __schemaclass__
516 package [% schema_class %];
517
518 use strict;
519 use base qw/DBIx::Class::Schema::Loader/;
520
521 __PACKAGE__->loader_options(
522     [%- FOREACH key = loader_args.keys %]
523     [% key %] => [% loader_args.${key} %],
524     [%- END -%]
525
526 );
527
528 =head1 NAME
529
530 [% schema_class %] - L<DBIx::Class::Schema::Loader> class
531
532 =head1 SYNOPSIS
533
534 See L<[% app %]>
535
536 =head1 DESCRIPTION
537
538 Dynamic L<DBIx::Class::Schema::Loader> schema for use in L<[% class %]>
539
540 =head1 GENERATED BY
541
542 [% generator %] - [% generator_version %]
543
544 =head1 AUTHOR
545
546 [% author.replace(',+$', '') %]
547
548 =head1 LICENSE
549
550 This library is free software, you can redistribute it and/or modify
551 it under the same terms as Perl itself.
552
553 =cut
554
555 1;
556
557 __compclass__
558 package [% class %];
559
560 use strict;
561 use base 'Catalyst::Model::DBIC::Schema';
562
563 __PACKAGE__->config(
564     schema_class => '[% schema_class %]',
565     [% IF roles %]roles => [% roles %],[% END %]
566     [% IF setup_connect_info %]connect_info => {
567         [%- FOREACH key = connect_info.keys %]
568         [% key %] => [% connect_info.${key} %],
569         [%- END -%]
570
571     }[% END %]
572 );
573
574 =head1 NAME
575
576 [% class %] - Catalyst DBIC Schema Model
577
578 =head1 SYNOPSIS
579
580 See L<[% app %]>
581
582 =head1 DESCRIPTION
583
584 L<Catalyst::Model::DBIC::Schema> Model using schema L<[% schema_class %]>
585
586 =head1 GENERATED BY
587
588 [% generator %] - [% generator_version %]
589
590 =head1 AUTHOR
591
592 [% author.replace(',+$', '') %]
593
594 =head1 LICENSE
595
596 This library is free software, you can redistribute it and/or modify
597 it under the same terms as Perl itself.
598
599 =cut
600
601 1;
602 __END__
603 # vim:sts=4 sw=4: