Add myself as a contributor to C:M:D:Schema. Yay!
[catagits/Catalyst-Model-DBIC-Schema.git] / lib / Catalyst / Model / DBIC / Schema.pm
CommitLineData
ad91060a 1package Catalyst::Model::DBIC::Schema;
2
0fbbc8d5 3use Moose;
0f3de2c4 4use mro 'c3';
0fbbc8d5 5extends 'Catalyst::Model';
fb691af9 6with 'CatalystX::Component::Traits';
0fbbc8d5 7
2fa0a1f1 8our $VERSION = '0.29';
7dfd616a 9$VERSION = eval $VERSION;
f090a149 10
73f72d28 11use namespace::autoclean;
bd309c0c 12use Carp::Clan '^Catalyst::Model::DBIC::Schema';
bfcd6e3d 13use Data::Dumper;
2201c2e4 14use DBIx::Class ();
ad91060a 15
61ed82a5 16use Catalyst::Model::DBIC::Schema::Types
2fa0a1f1 17 qw/ConnectInfo LoadedClass SchemaClass/;
61ed82a5 18
41bcf32f 19use MooseX::Types::Moose qw/ArrayRef Str ClassName Undef/;
0fbbc8d5 20
ad91060a 21=head1 NAME
22
23Catalyst::Model::DBIC::Schema - DBIx::Class::Schema Model Class
24
25=head1 SYNOPSIS
26
aabc1d75 27Manual creation of a DBIx::Class::Schema and a Catalyst::Model::DBIC::Schema:
28
29=over
30
31=item 1.
32
33Create the DBIx:Class schema in MyApp/Schema/FilmDB.pm:
34
35 package MyApp::Schema::FilmDB;
36 use base qw/DBIx::Class::Schema/;
37
38 __PACKAGE__->load_classes(qw/Actor Role/);
39
40=item 2.
41
42Create some classes for the tables in the database, for example an
43Actor in MyApp/Schema/FilmDB/Actor.pm:
44
45 package MyApp::Schema::FilmDB::Actor;
46 use base qw/DBIx::Class/
07edc53e 47
aabc1d75 48 __PACKAGE__->load_components(qw/Core/);
49 __PACKAGE__->table('actor');
07edc53e 50
aabc1d75 51 ...
52
c8ae74f8 53and a Role in MyApp/Schema/FilmDB/Role.pm:
aabc1d75 54
55 package MyApp::Schema::FilmDB::Role;
56 use base qw/DBIx::Class/
07edc53e 57
aabc1d75 58 __PACKAGE__->load_components(qw/Core/);
4a3e80e9 59 __PACKAGE__->table('role');
07edc53e 60
aabc1d75 61 ...
62
63Notice that the schema is in MyApp::Schema, not in MyApp::Model. This way it's
64usable as a standalone module and you can test/run it without Catalyst.
65
66=item 3.
67
68To expose it to Catalyst as a model, you should create a DBIC Model in
69MyApp/Model/FilmDB.pm:
70
71 package MyApp::Model::FilmDB;
72 use base qw/Catalyst::Model::DBIC::Schema/;
07edc53e 73
aabc1d75 74 __PACKAGE__->config(
75 schema_class => 'MyApp::Schema::FilmDB',
2201c2e4 76 connect_info => {
77 dsn => "DBI:...",
78 user => "username",
79 password => "password",
80 }
aabc1d75 81 );
82
83See below for a full list of the possible config parameters.
84
85=back
86
6dec11b7 87Now you have a working Model which accesses your separate DBIC Schema. This can
aabc1d75 88be used/accessed in the normal Catalyst manner, via $c->model():
89
90 my $actor = $c->model('FilmDB::Actor')->find(1);
91
92You can also use it to set up DBIC authentication with
6f6b9c2d 93L<Catalyst::Authentication::Store::DBIx::Class> in MyApp.pm:
aabc1d75 94
95 package MyApp;
07edc53e 96
6f6b9c2d 97 use Catalyst qw/... Authentication .../;
07edc53e 98
aabc1d75 99 ...
07edc53e 100
6f6b9c2d 101 __PACKAGE__->config->{authentication} =
102 {
103 default_realm => 'members',
104 realms => {
105 members => {
106 credential => {
107 class => 'Password',
108 password_field => 'password',
109 password_type => 'hashed'
110 password_hash_type => 'SHA-256'
111 },
112 store => {
113 class => 'DBIx::Class',
114 user_model => 'DB::User',
115 role_relation => 'roles',
116 role_field => 'rolename',
117 }
118 }
119 }
120 };
aabc1d75 121
d52bc376 122C<< $c->model('Schema::Source') >> returns a L<DBIx::Class::ResultSet> for
123the source name parameter passed. To find out more about which methods can
124be called on a ResultSet, or how to add your own methods to it, please see
125the ResultSet documentation in the L<DBIx::Class> distribution.
aabc1d75 126
127Some examples are given below:
128
f1613faa 129 # to access schema methods directly:
130 $c->model('FilmDB')->schema->source(...);
131
132 # to access the source object, resultset, and class:
07edc53e 133 $c->model('FilmDB')->source(...);
134 $c->model('FilmDB')->resultset(...);
135 $c->model('FilmDB')->class(...);
c12b7310 136
07edc53e 137 # For resultsets, there's an even quicker shortcut:
138 $c->model('FilmDB::Actor')
139 # is the same as $c->model('FilmDB')->resultset('Actor')
ad91060a 140
f1613faa 141 # To get the composed schema for making new connections:
142 my $newconn = $c->model('FilmDB')->composed_schema->connect(...);
143
144 # Or the same thing via a convenience shortcut:
145 my $newconn = $c->model('FilmDB')->connect(...);
146
147 # or, if your schema works on different storage drivers:
148 my $newconn = $c->model('FilmDB')->composed_schema->clone();
149 $newconn->storage_type('::LDAP');
150 $newconn->connection(...);
151
152 # and again, a convenience shortcut
153 my $newconn = $c->model('FilmDB')->clone();
154 $newconn->storage_type('::LDAP');
155 $newconn->connection(...);
156
ad91060a 157=head1 DESCRIPTION
158
7b39f3f0 159This is a Catalyst Model for L<DBIx::Class::Schema>-based Models. See
ef91bcf9 160the documentation for L<Catalyst::Helper::Model::DBIC::Schema> for
161information on generating these Models via Helper scripts.
ad91060a 162
d52bc376 163When your Catalyst app starts up, a thin Model layer is created as an
164interface to your DBIC Schema. It should be clearly noted that the model
165object returned by C<< $c->model('FilmDB') >> is NOT itself a DBIC schema or
166resultset object, but merely a wrapper proving L<methods|/METHODS> to access
167the underlying schema.
168
169In addition to this model class, a shortcut class is generated for each
170source in the schema, allowing easy and direct access to a resultset of the
171corresponding type. These generated classes are even thinner than the model
172class, providing no public methods but simply hooking into Catalyst's
173model() accessor via the
174L<ACCEPT_CONTEXT|Catalyst::Component/ACCEPT_CONTEXT> mechanism. The complete
175contents of each generated class is roughly equivalent to the following:
176
177 package MyApp::Model::FilmDB::Actor
178 sub ACCEPT_CONTEXT {
179 my ($self, $c) = @_;
180 $c->model('FilmDB')->resultset('Actor');
181 }
182
183In short, there are three techniques available for obtaining a DBIC
184resultset object:
185
186 # the long way
187 my $rs = $c->model('FilmDB')->schema->resultset('Actor');
188
189 # using the shortcut method on the model object
190 my $rs = $c->model('FilmDB')->resultset('Actor');
191
192 # using the generated class directly
193 my $rs = $c->model('FilmDB::Actor');
194
c082639a 195In order to add methods to a DBIC resultset, you cannot simply add them to
196the source (row, table) definition class; you must define a separate custom
197resultset class. See L<DBIx::Class::Manual::Cookbook/"Predefined searches">
198for more info.
199
ad91060a 200=head1 CONFIG PARAMETERS
201
b9cc2f76 202Any options in your config not listed here are passed to your schema.
203
c4fee9b8 204=head2 schema_class
ad91060a 205
206This is the classname of your L<DBIx::Class::Schema> Schema. It needs
aabc1d75 207to be findable in C<@INC>, but it does not need to be inside the
208C<Catalyst::Model::> namespace. This parameter is required.
ad91060a 209
c4fee9b8 210=head2 connect_info
ad91060a 211
212This is an arrayref of connection parameters, which are specific to your
b9a72351 213C<storage_type> (see your storage type documentation for more details).
214If you only need one parameter (e.g. the DSN), you can just pass a string
215instead of an arrayref.
ad91060a 216
0f2fd2c0 217This is not required if C<schema_class> already has connection information
d89e6c8a 218defined inside itself (which isn't highly recommended, but can be done)
0f2fd2c0 219
7db6da78 220For L<DBIx::Class::Storage::DBI>, which is the only supported
221C<storage_type> in L<DBIx::Class> at the time of this writing, the
222parameters are your dsn, username, password, and connect options hashref.
223
018eb0e2 224See L<DBIx::Class::Storage::DBI/connect_info> for a detailed explanation
225of the arguments supported.
7db6da78 226
227Examples:
228
2201c2e4 229 connect_info => {
230 dsn => 'dbi:Pg:dbname=mypgdb',
231 user => 'postgres',
232 password => ''
233 }
07edc53e 234
2201c2e4 235 connect_info => {
236 dsn => 'dbi:SQLite:dbname=foo.db',
237 on_connect_do => [
238 'PRAGMA synchronous = OFF',
239 ]
240 }
07edc53e 241
2201c2e4 242 connect_info => {
243 dsn => 'dbi:Pg:dbname=mypgdb',
244 user => 'postgres',
245 password => '',
246 pg_enable_utf8 => 1,
247 on_connect_do => [
248 'some SQL statement',
249 'another SQL statement',
250 ],
251 }
7db6da78 252
8281c933 253Or using L<Config::General>:
254
255 <Model::FilmDB>
256 schema_class MyApp::Schema::FilmDB
c34bcab6 257 traits Caching
8281c933 258 <connect_info>
2201c2e4 259 dsn dbi:Pg:dbname=mypgdb
260 user postgres
42e14c31 261 password ""
2201c2e4 262 auto_savepoint 1
a75b6e58 263 quote_char """
8281c933 264 on_connect_do some SQL statement
265 on_connect_do another SQL statement
266 </connect_info>
b9cc2f76 267 user_defined_schema_accessor foo
8281c933 268 </Model::FilmDB>
269
270or
271
272 <Model::FilmDB>
273 schema_class MyApp::Schema::FilmDB
274 connect_info dbi:SQLite:dbname=foo.db
275 </Model::FilmDB>
276
2201c2e4 277Or using L<YAML>:
278
279 Model::MyDB:
280 schema_class: MyDB
b9cc2f76 281 traits: Caching
2201c2e4 282 connect_info:
283 dsn: dbi:Oracle:mydb
284 user: mtfnpy
285 password: mypass
286 LongReadLen: 1000000
287 LongTruncOk: 1
b9cc2f76 288 on_connect_call: 'datetime_setup'
a75b6e58 289 quote_char: '"'
2201c2e4 290
291The old arrayref style with hashrefs for L<DBI> then L<DBIx::Class> options is also
292supported:
293
294 connect_info => [
295 'dbi:Pg:dbname=mypgdb',
296 'postgres',
297 '',
298 {
299 pg_enable_utf8 => 1,
300 },
301 {
0fbbc8d5 302 auto_savepoint => 1,
2201c2e4 303 on_connect_do => [
304 'some SQL statement',
305 'another SQL statement',
306 ],
307 }
308 ]
309
c34bcab6 310=head2 traits
0fbbc8d5 311
41bcf32f 312Array of Traits to apply to the instance. Traits are L<Moose::Role>s.
313
fb691af9 314They are relative to the C<< MyApp::TraitFor::Model::DBIC::Schema:: >>, then the C<<
315Catalyst::TraitFor::Model::DBIC::Schema:: >> namespaces, unless prefixed with C<+>
41bcf32f 316in which case they are taken to be a fully qualified name. E.g.:
2201c2e4 317
c34bcab6 318 traits Caching
fb691af9 319 traits +MyApp::TraitFor::Model::Foo
2201c2e4 320
0fbbc8d5 321A new instance is created at application time, so any consumed required
322attributes, coercions and modifiers will work.
2201c2e4 323
fb691af9 324Traits are applied at L<Catalyst::Component/COMPONENT> time using
325L<CatalystX::Component::Traits>.
0fbbc8d5 326
41bcf32f 327C<ref $self> will be an anon class if any traits are applied, C<<
328$self->_original_class_name >> will be the original class.
f090a149 329
c7d7b849 330When writing a Trait, interesting points to modify are C<BUILD>, L</setup> and
331L</ACCEPT_CONTEXT>.
0fbbc8d5 332
c34bcab6 333Traits that come with the distribution:
0fbbc8d5 334
335=over 4
2201c2e4 336
fb691af9 337=item L<Catalyst::TraitFor::Model::DBIC::Schema::Caching>
0fbbc8d5 338
fb691af9 339=item L<Catalyst::TraitFor::Model::DBIC::Schema::Replicated>
c4fee9b8 340
0fbbc8d5 341=back
8281c933 342
c4fee9b8 343=head2 storage_type
ad91060a 344
345Allows the use of a different C<storage_type> than what is set in your
346C<schema_class> (which in turn defaults to C<::DBI> if not set in current
f1613faa 347L<DBIx::Class>). Completely optional, and probably unnecessary for most
348people until other storage backends become available for L<DBIx::Class>.
ad91060a 349
c7d7b849 350=head1 ATTRIBUTES
351
352The keys you pass in the model configuration are available as attributes.
353
354Other attributes available:
355
356=head2 connect_info
357
358Your connect_info args normalized to hashref form (with dsn/user/password.) See
359L<DBIx::Class::Storage::DBI/connect_info> for more info on the hashref form of
360L</connect_info>.
361
362=head2 model_name
363
364The model name L<Catalyst> uses to resolve this model, the part after
365C<::Model::> or C<::M::> in your class name. E.g. if your class name is
366C<MyApp::Model::DB> the L</model_name> will be C<DB>.
367
c7d7b849 368=head2 _default_cursor_class
369
6f6b9c2d 370What to reset your L<DBIx::Class::Storage::DBI/cursor_class> to if a custom one
c7d7b849 371doesn't work out. Defaults to L<DBIx::Class::Storage::DBI::Cursor>.
372
fb691af9 373=head1 ATTRIBUTES FROM L<MooseX::Traits::Pluggable>
374
375=head2 _original_class_name
376
377The class name of your model before any L</traits> are applied. E.g.
378C<MyApp::Model::DB>.
379
c7d7b849 380=head2 _traits
381
6f6b9c2d 382Unresolved arrayref of traits passed in the config.
c7d7b849 383
384=head2 _resolved_traits
385
386Traits you used resolved to full class names.
387
ad91060a 388=head1 METHODS
389
46a2eb0c 390Methods not listed here are delegated to the connected schema used by the model
391instance, so the following are equivalent:
392
393 $c->model('DB')->schema->my_accessor('foo');
394 # or
395 $c->model('DB')->my_accessor('foo');
396
397Methods on the model take precedence over schema methods.
398
c4fee9b8 399=head2 new
ad91060a 400
401Instantiates the Model based on the above-documented ->config parameters.
0f2fd2c0 402The only required parameter is C<schema_class>. C<connect_info> is
403required in the case that C<schema_class> does not already have connection
404information defined for it.
ad91060a 405
c4fee9b8 406=head2 schema
f1613faa 407
408Accessor which returns the connected schema being used by the this model.
409There are direct shortcuts on the model class itself for
410schema->resultset, schema->source, and schema->class.
411
c4fee9b8 412=head2 composed_schema
f1613faa 413
414Accessor which returns the composed schema, which has no connection info,
415which was used in constructing the C<schema> above. Useful for creating
416new connections based on the same schema/model. There are direct shortcuts
417from the model object for composed_schema->clone and composed_schema->connect
418
c4fee9b8 419=head2 clone
f1613faa 420
421Shortcut for ->composed_schema->clone
422
c4fee9b8 423=head2 connect
f1613faa 424
425Shortcut for ->composed_schema->connect
426
c4fee9b8 427=head2 source
c12b7310 428
f1613faa 429Shortcut for ->schema->source
430
c4fee9b8 431=head2 class
f1613faa 432
433Shortcut for ->schema->class
434
c4fee9b8 435=head2 resultset
f1613faa 436
437Shortcut for ->schema->resultset
438
c4fee9b8 439=head2 storage
f1613faa 440
441Provides an accessor for the connected schema's storage object.
442Used often for debugging and controlling transactions.
b8427e0b 443
ad91060a 444=cut
445
c34bcab6 446has schema_class => (
0fbbc8d5 447 is => 'ro',
2fa0a1f1 448 isa => SchemaClass,
0fbbc8d5 449 coerce => 1,
450 required => 1
451);
452
c34bcab6 453has storage_type => (is => 'rw', isa => Str);
0fbbc8d5 454
18b829f0 455has connect_info => (is => 'rw', isa => ConnectInfo, coerce => 1);
0fbbc8d5 456
c7d7b849 457has model_name => (
458 is => 'ro',
459 isa => Str,
460 required => 1,
461 lazy_build => 1,
462);
ad91060a 463
c34bcab6 464has _default_cursor_class => (
61ed82a5 465 is => 'ro',
7314403a 466 isa => LoadedClass,
61ed82a5 467 default => 'DBIx::Class::Storage::DBI::Cursor',
468 coerce => 1
469);
470
0fbbc8d5 471sub BUILD {
f27a05ea 472 my ($self, $args) = @_;
f2488839 473 my $class = $self->_original_class_name;
2201c2e4 474 my $schema_class = $self->schema_class;
ad91060a 475
2201c2e4 476 if( !$self->connect_info ) {
f1613faa 477 if($schema_class->storage && $schema_class->storage->connect_info) {
2201c2e4 478 $self->connect_info($schema_class->storage->connect_info);
f1613faa 479 }
480 else {
39f5f008 481 die "Either ->config->{connect_info} must be defined for $class"
460e3ac8 482 . " or $schema_class must have connect info defined on it."
483 . " Here's what we got:\n"
f1613faa 484 . Dumper($self);
485 }
7db6da78 486 }
487
0fbbc8d5 488 if (exists $self->connect_info->{cursor_class}) {
489 eval { Class::MOP::load_class($self->connect_info->{cursor_class}) }
490 or croak "invalid connect_info: Cannot load your cursor_class"
491 . " ".$self->connect_info->{cursor_class}.": $@";
492 }
493
0fbbc8d5 494 $self->setup;
495
f1613faa 496 $self->composed_schema($schema_class->compose_namespace($class));
2201c2e4 497
7bd33abf 498 $self->meta->make_mutable;
46a2eb0c 499 $self->meta->add_attribute('schema',
500 is => 'rw',
180c1a1a 501 isa => 'DBIx::Class::Schema',
46a2eb0c 502 handles => $self->_delegates
503 );
7bd33abf 504 $self->meta->make_immutable;
46a2eb0c 505
f1613faa 506 $self->schema($self->composed_schema->clone);
507
f27a05ea 508 $self->_pass_options_to_schema($args);
7bd33abf 509
2201c2e4 510 $self->schema->storage_type($self->storage_type)
511 if $self->storage_type;
7db6da78 512
2201c2e4 513 $self->schema->connection($self->connect_info);
514
515 $self->_install_rs_models;
2201c2e4 516}
517
518sub clone { shift->composed_schema->clone(@_); }
519
520sub connect { shift->composed_schema->connect(@_); }
521
c4fee9b8 522=head2 setup
2201c2e4 523
e203cd42 524Called at C<BUILD> time before configuration, but after L</connect_info> is
c7d7b849 525set. To do something after configuuration use C<< after BUILD => >>.
2201c2e4 526
527=cut
528
0fbbc8d5 529sub setup { 1 }
2201c2e4 530
c4fee9b8 531=head2 ACCEPT_CONTEXT
2201c2e4 532
73f72d28 533Point of extension for doing things at C<< $c->model >> time with context,
534returns the model instance, see L<Catalyst::Manual::Intro/ACCEPT_CONTEXT> for
535more information.
2201c2e4 536
0fbbc8d5 537=cut
2201c2e4 538
0fbbc8d5 539sub ACCEPT_CONTEXT { shift }
2201c2e4 540
541sub _install_rs_models {
542 my $self = shift;
7b1fe8c2 543 my $class = $self->_original_class_name;
2201c2e4 544
ad91060a 545 no strict 'refs';
39f5f008 546
547 my @sources = $self->schema->sources;
548
549 die "No sources found (did you forget to define your tables?)"
550 unless @sources;
551
552 foreach my $moniker (@sources) {
0b2a7108 553 my $classname = "${class}::$moniker";
7db6da78 554 *{"${classname}::ACCEPT_CONTEXT"} = sub {
ad91060a 555 shift;
2201c2e4 556 shift->model($self->model_name)->resultset($moniker);
ad91060a 557 }
558 }
2201c2e4 559}
ad91060a 560
61ed82a5 561sub _reset_cursor_class {
562 my $self = shift;
563
564 if ($self->storage->can('cursor_class')) {
565 $self->storage->cursor_class($self->_default_cursor_class)
566 if $self->storage->cursor_class ne $self->_default_cursor_class;
567 }
568}
569
50f488ec 570{
571 my %COMPOSED_CACHE;
572
573 sub composed_schema {
574 my $self = shift;
575 my $class = $self->_original_class_name;
576 my $store = \$COMPOSED_CACHE{$class}{$self->schema_class};
577
578 $$store = shift if @_;
579
580 return $$store
581 }
582}
583
c7d7b849 584sub _build_model_name {
585 my $self = shift;
586 my $class = $self->_original_class_name;
587 (my $model_name = $class) =~ s/^[\w:]+::(?:Model|M):://;
588
589 return $model_name;
41bcf32f 590}
591
180c1a1a 592sub _delegates {
46a2eb0c 593 my $self = shift;
594
b9cc2f76 595 my $schema_meta = Class::MOP::Class->initialize($self->schema_class);
596 my @schema_methods = $schema_meta->get_all_method_names;
46a2eb0c 597
180c1a1a 598# combine with any already added by other schemas
599 my @handles = eval {
600 @{ $self->meta->find_attribute_by_name('schema')->handles }
601 };
602
603# now kill the attribute, otherwise add_attribute in BUILD will not do the right
b9cc2f76 604# thing (it clears the handles for some reason.) May be a Moose bug.
180c1a1a 605 eval { $self->meta->remove_attribute('schema') };
606
607 my %schema_methods;
608 @schema_methods{ @schema_methods, @handles } = ();
609 @schema_methods = keys %schema_methods;
610
611 my @my_methods = $self->meta->get_all_method_names;
46a2eb0c 612 my %my_methods;
613 @my_methods{@my_methods} = ();
614
615 my @delegates;
616 for my $method (@schema_methods) {
617 push @delegates, $method unless exists $my_methods{$method};
618 }
619
620 return \@delegates;
621}
622
7bd33abf 623sub _pass_options_to_schema {
f27a05ea 624 my ($self, $args) = @_;
625
626 my @attributes = map {
627 $_->init_arg || ()
628 } $self->meta->get_all_attributes;
7bd33abf 629
7bd33abf 630 my %attributes;
631 @attributes{@attributes} = ();
632
f27a05ea 633 for my $opt (keys %$args) {
7bd33abf 634 if (not exists $attributes{$opt}) {
294245b0 635 next unless $self->schema->can($opt);
7bd33abf 636 $self->schema->$opt($self->{$opt});
637 }
638 }
639}
640
0fbbc8d5 641__PACKAGE__->meta->make_immutable;
2201c2e4 642
ad91060a 643=head1 SEE ALSO
644
7b39f3f0 645General Catalyst Stuff:
646
647L<Catalyst::Manual>, L<Catalyst::Test>, L<Catalyst::Request>,
648L<Catalyst::Response>, L<Catalyst::Helper>, L<Catalyst>,
649
650Stuff related to DBIC and this Model style:
651
652L<DBIx::Class>, L<DBIx::Class::Schema>,
f090a149 653L<DBIx::Class::Schema::Loader>, L<Catalyst::Helper::Model::DBIC::Schema>,
e203cd42 654L<CatalystX::Component::Traits>, L<MooseX::Traits::Pluggable>
ad91060a 655
c34bcab6 656Traits:
c4fee9b8 657
fb691af9 658L<Catalyst::TraitFor::Model::DBIC::Schema::Caching>,
659L<Catalyst::TraitFor::Model::DBIC::Schema::Replicated>
c4fee9b8 660
ad91060a 661=head1 AUTHOR
662
e203cd42 663Brandon L Black C<blblack at gmail.com>
ad91060a 664
e203cd42 665=head1 CONTRIBUTORS
2ff00e2b 666
e203cd42 667caelum: Rafael Kitover C<rkitover at cpan.org>
2ff00e2b 668
6d9e2623 669Dan Dascalescu C<dandv at cpan.org>
670
49c75c04 671Aran Deltac C<bluefeet@cpan.org>
672
ad91060a 673=head1 COPYRIGHT
674
6d9e2623 675This program is free software. You can redistribute it and/or modify it
ad91060a 676under the same terms as Perl itself.
677
678=cut
679
6801;
c7d7b849 681# vim:sts=4 sw=4 et: