File Coverage

blib/lib/GraphQL/Plugin/Convert/DBIC.pm
Criterion Covered Total %
statement 200 201 99.5
branch 77 92 83.7
condition 30 38 78.9
subroutine 25 25 100.0
pod 1 2 50.0
total 333 358 93.0


line stmt bran cond sub pod time code
1             package GraphQL::Plugin::Convert::DBIC;
2 5     5   4829 use strict;
  5         13  
  5         153  
3 5     5   29 use warnings;
  5         11  
  5         176  
4 5     5   2397 use GraphQL::Schema;
  5         8337676  
  5         317  
5 5     5   45 use GraphQL::Debug qw(_debug);
  5         11  
  5         249  
6 5     5   3420 use Lingua::EN::Inflect::Number qw(to_S to_PL);
  5         148746  
  5         45  
7 5     5   1369 use Carp qw(confess);
  5         14  
  5         372  
8              
9             our $VERSION = "0.15";
10 5     5   34 use constant DEBUG => $ENV{GRAPHQL_DEBUG};
  5         13  
  5         9106  
11              
12             my %GRAPHQL_TYPE2SQLS = (
13             String => [
14             'wlongvarchar',
15             'guid',
16             'uuid',
17             'wvarchar',
18             'wchar',
19             'longvarbinary',
20             'varbinary',
21             'binary',
22             'longvarchar',
23             'unknown_type',
24             'all_types',
25             'char',
26             'varchar',
27             'udt',
28             'udt_locator',
29             'row',
30             'ref',
31             'blob',
32             'blob_locator',
33             'clob',
34             'clob_locator',
35             'array',
36             'array_locator',
37             'multiset',
38             'multiset_locator',
39             # mysql
40             'text',
41             'tinytext',
42             'mediumtext',
43             'longtext',
44             # pgsql
45             'cidr',
46             'inet',
47             ],
48             Int => [
49             'bigint',
50             'bit',
51             'tinyint',
52             'integer',
53             'smallint',
54             'interval',
55             'interval_year',
56             'interval_month',
57             'interval_day',
58             'interval_hour',
59             'interval_minute',
60             'interval_second',
61             'interval_year_to_month',
62             'interval_day_to_hour',
63             'interval_day_to_minute',
64             'interval_day_to_second',
65             'interval_hour_to_minute',
66             'interval_hour_to_second',
67             'interval_minute_to_second',
68             # not DBI SQL_* types
69             'int',
70             ],
71             Float => [
72             'numeric',
73             'decimal',
74             'float',
75             'real',
76             'double',
77             ],
78             DateTime => [
79             'datetime',
80             'date',
81             'time',
82             'timestamp',
83             'type_date',
84             'type_time',
85             'type_timestamp',
86             'type_time_with_timezone',
87             'type_timestamp_with_timezone',
88             # pgsql
89             'timestamp with time zone',
90             'timestamp without time zone',
91             ],
92             Boolean => [
93             'boolean',
94             ],
95             ID => [
96             'wvarchar',
97             ],
98             );
99             my %TYPEMAP = (
100             (map {
101             my $gql_type = $_;
102             map {
103             ($_ => $gql_type)
104             } @{ $GRAPHQL_TYPE2SQLS{$gql_type} }
105             } keys %GRAPHQL_TYPE2SQLS),
106             enum => sub {
107             my ($source, $column, $info) = @_;
108             my $extra = $info->{extra};
109             return {
110             kind => 'enum',
111             name => _dbicsource2pretty(
112             $extra->{custom_type_name} || "${source}_$column"
113             ),
114             values => { map { _trim_name($_) => { value => $_ } } @{ $extra->{list} } },
115             }
116             },
117             );
118             my %TYPE2SCALAR = map { ($_ => 1) } qw(ID String Int Float Boolean);
119              
120             sub _dbicsource2pretty {
121 83     83   157 my ($source) = @_;
122 83 50       175 confess "_dbicsource2pretty given undef" if !defined $source;
123 83   66     124 $source = eval { $source->source_name } || $source;
124 83         5678 $source =~ s#.*::##;
125 83         265 $source = to_S $source;
126 83         69234 join '', map ucfirst, split /_+/, $source;
127             }
128              
129             sub _trim_name {
130 38     38   63 my ($name) = @_;
131 38 50       75 return if !defined $name;
132 38         82 $name =~ s#[^a-zA-Z0-9_]+#_#g;
133 38         143 $name;
134             }
135              
136             sub _apply_modifier {
137 854     854   1356 my ($modifier, $typespec) = @_;
138 854 100       1399 return $typespec if !$modifier;
139 741 100 100     2059 return $typespec if $modifier eq 'non_null'
      100        
140             and ref $typespec eq 'ARRAY'
141             and $typespec->[0] eq 'non_null'; # no double-non_null
142 678         1988 [ $modifier, { type => $typespec } ];
143             }
144              
145             sub _remove_modifiers {
146 68     68   117 my ($typespec) = @_;
147 68 100       149 return _remove_modifiers($typespec->{type}) if ref $typespec eq 'HASH';
148 50 100       147 return $typespec if ref $typespec ne 'ARRAY';
149 18         42 _remove_modifiers($typespec->[1]);
150             }
151              
152             sub _type2createinput {
153 28     28   81 my ($name, $fields, $pk21, $fk21, $column21, $name2type) = @_;
154             +{
155             kind => 'input',
156             name => "${name}CreateInput",
157             fields => {
158 162         260 (map { ($_ => $fields->{$_}) }
159 28   100     311 grep !$pk21->{$_} && !$fk21->{$_}, keys %$column21),
160             _make_fk_fields($name, $fk21, $name2type),
161             },
162             };
163             }
164              
165             sub _type2idinput {
166 28     28   61 my ($name, $fields, $pk21) = @_;
167             +{
168             kind => 'input',
169             name => "${name}IDInput",
170             fields => {
171 28         82 (map { ($_ => $fields->{$_}) }
  27         154  
172             keys %$pk21),
173             },
174             };
175             }
176              
177             sub _type2searchinput {
178 30     30   64 my ($name, $column2rawtype, $pk21, $column21) = @_;
179             +{
180             kind => 'input',
181             name => "${name}SearchInput",
182             fields => {
183 171         452 (map { ($_ => { type => $column2rawtype->{$_} }) }
184 30         119 grep !$pk21->{$_}, keys %$column21),
185             },
186             };
187             }
188              
189             sub _type2updateinput {
190 28     28   57 my ($name) = @_;
191             +{
192 28         83 kind => 'input',
193             name => "${name}UpdateInput",
194             fields => {
195             id => { type => _apply_modifier('non_null', "${name}IDInput") },
196             payload => { type => _apply_modifier('non_null', "${name}SearchInput") },
197             },
198             };
199             }
200              
201             sub _make_fk_fields {
202 28     28   55 my ($name, $fk21, $name2type) = @_;
203 28         49 my $type = $name2type->{$name};
204             (map {
205 28         127 my $field_type = $type->{fields}{$_}{type};
  17         38  
206 17 100       38 if (!$TYPE2SCALAR{_remove_modifiers($field_type)}) {
207 15   66     54 my $non_null =
208             ref($field_type) eq 'ARRAY' && $field_type->[0] eq 'non_null';
209 15   100     42 $field_type = _apply_modifier(
210             $non_null && 'non_null', _remove_modifiers($field_type)."IDInput"
211             );
212             }
213 17         149 ($_ => { type => $field_type })
214             } keys %$fk21);
215             }
216              
217             sub field_resolver {
218 64     64 1 1001371 my ($root_value, $args, $context, $info) = @_;
219 64         138 my $field_name = $info->{field_name};
220 64         84 DEBUG and _debug('DBIC.resolver', $field_name, $args, $info);
221 64         203 my $parent_name = $info->{parent_type}->name;
222 64 100       223 if ($parent_name eq 'Mutation') {
    100          
223 3         16 goto &_mutation_resolver;
224             } elsif ($parent_name eq 'Query') {
225 8         41 goto &_query_resolver;
226             }
227             my $property = ref($root_value) eq 'HASH'
228 53 100       137 ? $root_value->{$field_name}
229             : $root_value;
230 53 50       140 return $property->($args, $context, $info) if ref $property eq 'CODE';
231 53 100 50     337 return $property // die "DBIC.resolver could not resolve '$field_name'\n"
      66        
232             if ref $root_value eq 'HASH' or !$root_value->can($field_name);
233 51 50       174 return $root_value->$field_name($args, $context, $info)
234             if !UNIVERSAL::isa($root_value, 'DBIx::Class::Core');
235             # dbic search
236 51         1003 my $rs = $root_value->$field_name;
237 51 100       22339 $rs = [ $rs->all ] if $info->{return_type}->isa('GraphQL::Type::List');
238 51         21914 return $rs;
239             }
240              
241             sub _subfieldrels {
242 13     13   29 my ($field_node) = @_;
243 13 50       40 die "_subfieldrels called on non-field" if $field_node->{kind} ne 'field';
244 13 50       21 return {} unless my @sels = @{ $field_node->{selections} || [] };
  13 50       54  
245 13 100       27 return {} unless my @withsels = grep @{ $_->{selections} || [] }, @sels;
  27 100       136  
246 4         10 +{ map { $_->{name} => _subfieldrels($_) } @withsels };
  5         24  
247             }
248              
249             sub _query_resolver {
250 8     8   26 my ($dbic_schema, $args, $context, $info) = @_;
251 8         200 my $name = $info->{return_type}->name;
252 8 100       259 my $method = $info->{return_type}->isa('GraphQL::Type::List')
253             ? 'search' : 'find';
254 8         17 my @subfieldrels = map _subfieldrels($_), @{$info->{field_nodes}};
  8         34  
255 8 100       32 $args = $args->{input} if ref $args->{input} eq 'HASH';
256 8         29 $args = +{ map { ("me.$_" => $args->{$_}) } keys %$args };
  8         39  
257 8         19 DEBUG and _debug('DBIC.root_value', $name, $method, $args, \@subfieldrels, $info);
258 8         81 my $rs = $dbic_schema->resultset($name);
259 8         3971 $rs->result_class('DBIx::Class::ResultClass::HashRefInflator');
260 8         1651 my $result = $rs->$method(
261             $args,
262             { prefetch => { map %$, @subfieldrels } },
263             );
264 8 100       5535 $result = [ $result->all ] if $method eq 'search';
265 8         81164 $result;
266             }
267              
268             sub _make_query_pk_field {
269 52     52   106 my ($typename, $type, $name2pk21, $is_list) = @_;
270 52         75 my $return_type = $typename;
271 52 100       107 $return_type = _apply_modifier('list', $return_type) if $is_list;
272             +{
273             type => $return_type,
274             args => {
275             map {
276 54         118 my $field_type = _apply_modifier('non_null', $type->{fields}{$_}{type});
277 54 100       141 $field_type = _apply_modifier('non_null', _apply_modifier('list',
278             $field_type
279             )) if $is_list;
280 54         299 $_ => { type => $field_type }
281 52         83 } keys %{ $name2pk21->{$typename} }
  52         126  
282             },
283             };
284             }
285              
286             sub _make_input_field {
287 114     114   220 my ($typename, $return_type, $mutation_kind, $list_in, $list_out) = @_;
288 114 50       267 $return_type = _apply_modifier('list', $return_type) if $list_out;
289 114         273 my $input_type = $typename . ucfirst($mutation_kind) . 'Input';
290 114         178 $input_type = _apply_modifier('non_null', $input_type);
291 114 100       245 $input_type = _apply_modifier('non_null', _apply_modifier('list',
292             $input_type
293             )) if $list_in;
294             +{
295 114         594 type => $return_type,
296             args => { input => { type => $input_type } },
297             };
298             }
299              
300             use constant MUTATE_ARGSPROCESS => {
301 2         32 update => sub { $_[0]->{payload} },
302             delete => sub { },
303 5     5   71 };
  5         12  
  5         742  
304             use constant MUTATE_POSTPROCESS => {
305 2 50       46 update => sub { ref($_[0]) eq 'GraphQL::Error' ? $_[0] : $_[0]->discard_changes },
306 1 50 50     11 delete => sub { ref($_[0]) eq 'GraphQL::Error' ? $_[0] : $_[0] && 1 },
307 5     5   42 };
  5         10  
  5         6637  
308             sub _mutation_resolver {
309 3     3   11 my ($dbic_schema, $args, $context, $info) = @_;
310 3         10 my $name = $info->{field_name};
311 3 50       29 die "Couldn't understand field '$name'"
312             unless $name =~ s/^(create|update|delete)//;
313 3         34 my $method = $1;
314 3         9 my $find_first = $method ne 'create';
315 3         18 my ($args_process, $result_process) = map $_->{$method},
316             MUTATE_ARGSPROCESS, MUTATE_POSTPROCESS;
317 3 50       13 $args = $args->{input} if $args->{input};
318 3         9 my $is_list = ref $args eq 'ARRAY';
319 3 50       11 $args = [ $args ] if !$is_list; # so can just deal as list below
320 3         6 DEBUG and _debug("DBIC.root_value", $args);
321 3         22 my $rs = $dbic_schema->resultset($name);
322             my $all_result = [
323             map {
324 3         1209 my $operand = $rs;
  5         14  
325 5 100       32 $operand = $operand->find($_->{id}) if $find_first;
326 5 100       14383 my $result = $operand
    100          
327             ? $operand->$method($args_process ? $args_process->($_) : $_)
328             : GraphQL::Error->coerce("$name not found");
329 5 100 100     55228 $result = $result_process->($result)
330             if $result_process and ref($result) ne 'GraphQL::Error';
331 5         8831 $result;
332             } @$args
333             ];
334 3 50       31 $all_result = $all_result->[0] if !$is_list;
335 3         14 $all_result
336             }
337              
338             sub to_graphql {
339 6     6 0 233693 my ($class, $dbic_schema) = @_;
340 6 50 50     49 $dbic_schema = $dbic_schema->() if ((ref($dbic_schema)||'') eq 'CODE');
341 6         13 my @ast;
342             my (
343 6         16 %name2type, %name2column21, %name2pk21, %name2fk21,
344             %name2column2rawtype, %seentype, %name2isview,
345             );
346 6         30 for my $source (map $dbic_schema->source($_), $dbic_schema->sources) {
347 30         1222 my $name = _dbicsource2pretty($source);
348 30         59 DEBUG and _debug("schema_dbic2graphql($name)", $source);
349 30 100       228 $name2isview{$name} = 1 if $source->can('view_definition');
350 30         53 my %fields;
351 30         119 my $columns_info = $source->columns_info;
352 30         1168 $name2pk21{$name} = +{ map { ($_ => 1) } $source->primary_columns };
  33         262  
353             my %rel2info = map {
354 30         341 ($_ => $source->relationship_info($_))
  40         264  
355             } $source->relationships;
356 30         432 for my $column (keys %$columns_info) {
357 216         331 my $info = $columns_info->{$column};
358 216         242 DEBUG and _debug("schema_dbic2graphql($name.col)", $column, $info);
359 216         429 my $rawtype = $TYPEMAP{ lc $info->{data_type} };
360 216 100       390 if ( 'CODE' eq ref $rawtype ) {
361 13         28 my $col_spec = $rawtype->($name, $column, $info);
362 13 100       47 push @ast, $col_spec unless $seentype{$col_spec->{name}};
363 13         24 $rawtype = $col_spec->{name};
364 13         34 $seentype{$col_spec->{name}} = 1;
365             }
366 216         353 $name2column2rawtype{$name}->{$column} = $rawtype;
367             my $fulltype = _apply_modifier(
368 216   100     691 !$info->{is_nullable} && 'non_null',
      50        
369             $rawtype
370 0         0 // die "'$column' unknown data type: @{[lc $info->{data_type}]}\n",
371             );
372 216         459 $fields{$column} = +{ type => $fulltype };
373 216 100       400 $name2fk21{$name}->{$column} = 1 if $info->{is_foreign_key};
374 216         403 $name2column21{$name}->{$column} = 1;
375             }
376 30         82 for my $rel (keys %rel2info) {
377 40         71 my $info = $rel2info{$rel};
378 40         57 DEBUG and _debug("schema_dbic2graphql($name.rel)", $rel, $info);
379 40         102 my $type = _dbicsource2pretty($info->{source});
380 40         109 $rel =~ s/_id$//; # dumb heuristic
381 40         91 delete $name2column21{$name}->{$rel}; # so it's not a "column" now
382 40         64 delete $name2pk21{$name}{$rel}; # it's not a PK either
383             # if it WAS a column, capture its non-null-ness
384 40   100     179 my $non_null = ref(($fields{$rel} || {})->{type}) eq 'ARRAY';
385 40 100       108 $type = _apply_modifier('non_null', $type) if $non_null;
386 40 100       150 $type = _apply_modifier('list', $type) if $info->{attrs}{accessor} eq 'multi';
387 40 100       92 $type = _apply_modifier('non_null', $type) if $non_null; # in case list
388 40         158 $fields{$rel} = +{ type => $type };
389             }
390 30         111 my $spec = +{
391             kind => 'type',
392             name => $name,
393             fields => \%fields,
394             };
395 30         66 $name2type{$name} = $spec;
396 30         141 push @ast, $spec;
397             }
398             push @ast, map _type2idinput(
399             $_, $name2type{$_}->{fields}, $name2pk21{$_},
400             $name2column21{$_},
401 6   66     81 ), grep !$name2isview{$_} || keys %{ $name2pk21{$_} }, keys %name2type;
402             push @ast, map _type2createinput(
403             $_, $name2type{$_}->{fields}, $name2pk21{$_}, $name2fk21{$_},
404             $name2column21{$_}, \%name2type,
405 6         55 ), grep !$name2isview{$_}, keys %name2type;
406             push @ast, map _type2searchinput(
407             $_, $name2column2rawtype{$_}, $name2pk21{$_},
408 6         39 $name2column21{$_},
409             ), keys %name2type;
410 6         39 push @ast, map _type2updateinput($_), grep !$name2isview{$_}, keys %name2type;
411             push @ast, {
412             kind => 'type',
413             name => 'Query',
414             fields => {
415             map {
416 6         26 my $name = $_;
  30         49  
417 30         53 my $type = $name2type{$name};
418 30         56 my $pksearch_name = lcfirst $name;
419 30         91 my $pksearch_name_plural = to_PL($pksearch_name);
420 30         24995 my $input_search_name = "search$name";
421 30         97 my @fields = (
422             $input_search_name => _make_input_field($name, $name, 'search', 0, 1),
423             );
424             push @fields, map((
425             ($_ ? $pksearch_name_plural : $pksearch_name),
426             _make_query_pk_field($name, $type, \%name2pk21, $_),
427 30 100       50 ), (0, 1)) if keys %{ $name2pk21{$name} };
  30 100       160  
428 30         168 @fields;
429             } keys %name2type
430             },
431             };
432             push @ast, {
433             kind => 'type',
434             name => 'Mutation',
435             fields => {
436             map {
437 28         47 my $name = $_;
438 28         55 my $create_name = "create$name";
439 28         97 my $update_name = "update$name";
440 28         67 my $delete_name = "delete$name";
441             (
442 28         53 $create_name => _make_input_field($name, $name, 'create', 1, 1),
443             $update_name => _make_input_field($name, $name, 'update', 1, 1),
444             $delete_name => _make_input_field($name, 'Boolean', 'ID', 1, 1),
445             )
446 6         48 } grep !$name2isview{$_}, keys %name2type
447             },
448             };
449             +{
450 6         118 schema => GraphQL::Schema->from_ast(\@ast),
451             root_value => $dbic_schema,
452             resolver => \&field_resolver,
453             };
454             }
455              
456             =encoding utf-8
457              
458             =head1 NAME
459              
460             GraphQL::Plugin::Convert::DBIC - convert DBIx::Class schema to GraphQL schema
461              
462             =begin markdown
463              
464             # PROJECT STATUS
465              
466             | OS | Build status |
467             |:-------:|--------------:|
468             | Linux | [![Build Status](https://travis-ci.org/graphql-perl/GraphQL-Plugin-Convert-DBIC.svg?branch=master)](https://travis-ci.org/graphql-perl/GraphQL-Plugin-Convert-DBIC) |
469              
470             [![CPAN version](https://badge.fury.io/pl/GraphQL-Plugin-Convert-DBIC.svg)](https://metacpan.org/pod/GraphQL::Plugin::Convert::DBIC)
471              
472             =end markdown
473              
474             =head1 SYNOPSIS
475              
476             use GraphQL::Plugin::Convert::DBIC;
477             use Schema;
478             my $converted = GraphQL::Plugin::Convert::DBIC->to_graphql(Schema->connect);
479             print $converted->{schema}->to_doc;
480              
481             =head1 DESCRIPTION
482              
483             This module implements the L<GraphQL::Plugin::Convert> API to convert
484             a L<DBIx::Class::Schema> to L<GraphQL::Schema> etc.
485              
486             Its C<Query> type represents a guess at what fields are suitable, based
487             on providing a lookup for each type (a L<DBIx::Class::ResultSource>).
488              
489             =head2 Example
490              
491             Consider this minimal data model:
492              
493             blog:
494             id # primary key
495             articles # has_many
496             title # non null
497             language # nullable
498             article:
499             id # primary key
500             blog # foreign key to Blog
501             title # non null
502             content # nullable
503              
504             =head2 Generated Output Types
505              
506             These L<GraphQL::Type::Object> types will be generated:
507              
508             type Blog {
509             id: Int!
510             articles: [Article]
511             title: String!
512             language: String
513             }
514              
515             type Article {
516             id: Int!
517             blog: Blog
518             title: String!
519             content: String
520             }
521              
522             type Query {
523             blog(id: [Int!]!): [Blog]
524             article(id: [Int!]!): [Blog]
525             }
526              
527             Note that while the queries take a list, the return order is
528             undefined. This also applies to the mutations. If this matters, request
529             the primary key fields and use those to sort.
530              
531             =head2 Generated Input Types
532              
533             Different input types are needed for each of CRUD (Create, Read, Update,
534             Delete).
535              
536             The create one needs to have non-null fields be non-null, for idiomatic
537             GraphQL-level error-catching. The read one needs all fields nullable,
538             since this will be how searches are implemented, allowing fields to be
539             left un-searched-for. Both need to omit primary key fields. The read
540             one also needs to omit foreign key fields, since the idiomatic GraphQL
541             way for this is to request the other object, with this as a field on it,
542             then request any required fields of this.
543              
544             Meanwhile, the update and delete ones need to include the primary key
545             fields, to indicate what to mutate, and also all non-primary key fields
546             as nullable, which for update will mean leaving them unchanged, and for
547             delete is to be ignored. These input types are split into one input
548             for the primary keys, which is a full input type to allow for multiple
549             primary keys, then a wrapper input for updates, that takes one ID input,
550             and a payload that due to the same requirements, is just the search input.
551              
552             Therefore, for the above, these input types (and an updated Query,
553             and Mutation) are created:
554              
555             input BlogCreateInput {
556             title: String!
557             language: String
558             }
559              
560             input BlogSearchInput {
561             title: String
562             language: String
563             }
564              
565             input BlogIDInput {
566             id: Int!
567             }
568              
569             input BlogUpdateInput {
570             id: BlogIDInput!
571             payload: BlogSearchInput!
572             }
573              
574             input ArticleCreateInput {
575             blog_id: Int!
576             title: String!
577             content: String
578             }
579              
580             input ArticleSearchInput {
581             title: String
582             content: String
583             }
584              
585             input ArticleIDInput {
586             id: Int!
587             }
588              
589             input ArticleUpdateInput {
590             id: ArticleIDInput!
591             payload: ArticleSearchInput!
592             }
593              
594             type Mutation {
595             createBlog(input: [BlogCreateInput!]!): [Blog]
596             createArticle(input: [ArticleCreateInput!]!): [Article]
597             deleteBlog(input: [BlogIDInput!]!): [Boolean]
598             deleteArticle(input: [ArticleIDInput!]!): [Boolean]
599             updateBlog(input: [BlogUpdateInput!]!): [Blog]
600             updateArticle(input: [ArticleUpdateInput!]!): [Article]
601             }
602              
603             extends type Query {
604             searchBlog(input: BlogSearchInput!): [Blog]
605             searchArticle(input: ArticleSearchInput!): [Article]
606             }
607              
608             =head1 ARGUMENTS
609              
610             To the C<to_graphql> method: a L<DBIx::Class::Schema> object.
611              
612             =head1 PACKAGE FUNCTIONS
613              
614             =head2 field_resolver
615              
616             This is available as C<\&GraphQL::Plugin::Convert::DBIC::field_resolver>
617             in case it is wanted for use outside of the "bundle" of the C<to_graphql>
618             method.
619              
620             =head1 DEBUGGING
621              
622             To debug, set environment variable C<GRAPHQL_DEBUG> to a true value.
623              
624             =head1 AUTHOR
625              
626             Ed J, C<< <etj at cpan.org> >>
627              
628             =head1 LICENSE
629              
630             Copyright (C) Ed J
631              
632             This library is free software; you can redistribute it and/or modify
633             it under the same terms as Perl itself.
634              
635             =cut
636              
637             1;