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