File Coverage

blib/lib/ExtJS/Generator/DBIC/Model.pm
Criterion Covered Total %
statement 157 176 89.2
branch 39 76 51.3
condition 24 57 42.1
subroutine 20 21 95.2
pod 8 8 100.0
total 248 338 73.3


line stmt bran cond sub pod time code
1             package ExtJS::Generator::DBIC::Model;
2             $ExtJS::Generator::DBIC::Model::VERSION = '0.004';
3             #ABSTRACT: ExtJS model producer
4              
5              
6 1     1   117479 use Moo;
  1         11748  
  1         6  
7 1     1   2232 use Types::Standard qw( Str HashRef InstanceOf HasMethods CodeRef );
  1         78527  
  1         11  
8 1     1   1858 use Data::Dump::JavaScript qw( dump_javascript false true );
  1         11830  
  1         68  
9 1     1   555 use Try::Tiny;
  1         1305  
  1         56  
10             #use Text::Xslate;
11             #use List::Util qw( none );
12 1     1   466 use Path::Class;
  1         19979  
  1         64  
13 1     1   9 use Fcntl qw( O_CREAT O_WRONLY O_EXCL O_TRUNC );
  1         3  
  1         48  
14 1     1   548 use Module::Load;
  1         1103  
  1         7  
15 1     1   581 use namespace::clean;
  1         10088  
  1         7  
16              
17              
18             has 'schemaname' => (
19             is => 'ro',
20             isa => Str,
21             required => 1,
22             );
23              
24              
25             has 'schema' => (
26             is => 'lazy',
27             isa => InstanceOf ['DBIx::Class::Schema'],
28             );
29              
30             sub _build_schema {
31 1     1   19 my $self = shift;
32 1         15 load $self->schemaname;
33 1         150100 my $schema = $self->schemaname->connect;
34 1         61650 return $schema;
35             }
36              
37              
38             has 'appname' => (
39             is => 'ro',
40             isa => Str,
41             required => 1,
42             );
43              
44              
45             has 'model_namespace' => (
46             is => 'lazy',
47             isa => Str,
48             );
49              
50             sub _build_model_namespace {
51 1     1   16 my $self = shift;
52              
53 1         20 return $self->appname . '.model';
54             }
55              
56              
57             has 'model_baseclass' => (
58             is => 'lazy',
59             isa => Str,
60             );
61              
62             sub _build_model_baseclass {
63 1     1   16 my $self = shift;
64              
65 1         20 return $self->appname . '.data.Model';
66             }
67              
68             #sub sort_field_attrs {
69             # my ($attrs, $fixed) = @_;
70             # return [ @$fixed, sort grep { my $attr = $_; none { $_ eq $attr } @$fixed } @$attrs ];
71             #}
72              
73             #has '_xslate' => (
74             # is => 'lazy',
75             # isa => InstanceOf['Text::Xslate'],
76             #);
77             #
78             #sub _build__xslate {
79             # my $self = shift;
80             # return Text::Xslate->new(
81             # function => {
82             # 'array::sort_field_attrs' => \&sort_field_attrs,
83             # },
84             # );
85             #}
86              
87             #has 'model_template' => (
88             # is => 'lazy',
89             # isa => Str,
90             #);
91              
92             #sub _build_model_template {
93             # return q/Ext.define('<: $classname :>', {
94             # : for $attributes.keys().sort_field_attrs(["extend", "requires", "idProperty", "fields"]) -> $attr {
95             # : if is_array_ref($attributes[$attr]) {
96             # <: $attr :>: [
97             # : for $attributes[$attr].sort() -> $subkey {
98             # : if $attr == 'fields' {
99             # {
100             # : for $subkey.keys().sort_field_attrs(["name", "type"]) -> $field {
101             # <: $field :>: '<: $subkey[$field] :>'<: if ! $~field.is_last { :>,<: } :>
102             # : }
103             # }<: if ! $~subkey.is_last { :>,<: } :>
104             # : }
105             # : else {
106             # '<: $subkey :>'<: if ! $~subkey.is_last { :>,<: } :>
107             # : }
108             # : }
109             # ]<: if ! $~attr.is_last { :>,<: } :>
110             # : }
111             # : else {
112             # <: $attr :>: '<: $attributes[$attr] :>'<: if ! $~attr.is_last { :>,<: } :>
113             # : }
114             # : }
115             #});
116             #/;
117             #}
118              
119              
120             has 'model_args' => (
121             is => 'ro',
122             isa => HashRef,
123             );
124              
125             my %translate = (
126              
127             #
128             # MySQL types
129             #
130             bigint => 'int',
131             double => 'float',
132             decimal => 'float',
133             float => 'float',
134             int => 'int',
135             integer => 'int',
136             mediumint => 'int',
137             smallint => 'int',
138             tinyint => 'int',
139             char => 'string',
140             varchar => 'string',
141             tinyblob => 'auto',
142             blob => 'auto',
143             mediumblob => 'auto',
144             longblob => 'auto',
145             tinytext => 'string',
146             text => 'string',
147             longtext => 'string',
148             mediumtext => 'string',
149             enum => 'string',
150             set => 'string',
151             date => 'date',
152             datetime => 'date',
153             time => 'date',
154             timestamp => 'date',
155             year => 'date',
156              
157             #
158             # PostgreSQL types
159             #
160             numeric => 'float',
161             'double precision' => 'float',
162             serial => 'int',
163             bigserial => 'int',
164             money => 'float',
165             character => 'string',
166             'character varying' => 'string',
167             bytea => 'auto',
168             interval => 'float',
169             boolean => 'boolean',
170             point => 'float',
171             line => 'float',
172             lseg => 'float',
173             box => 'float',
174             path => 'float',
175             polygon => 'float',
176             circle => 'float',
177             cidr => 'string',
178             inet => 'string',
179             macaddr => 'string',
180             bit => 'int',
181             'bit varying' => 'int',
182              
183             #
184             # Oracle types
185             #
186             number => 'float',
187             varchar2 => 'string',
188             long => 'float',
189             );
190              
191             my %extjs_class_for_datatype = (
192             boolean => 'Ext.data.field.Boolean',
193             date => 'Ext.data.field.Date',
194             int => 'Ext.data.field.Integer',
195             float => 'Ext.data.field.Number',
196             string => 'Ext.data.field.String',
197             );
198              
199              
200             sub extjs_model_name {
201 8     8 1 19 my ( $self, $tablename ) = @_;
202 8 50       50 $tablename = $tablename =~ m/^(?:\w+::)* (\w+)$/x ? $1 : $tablename;
203 8         140 return $self->model_namespace . '.' . ucfirst($tablename);
204             }
205              
206              
207             sub extjs_model_alias {
208 6     6 1 78 my ( $self, $modelname ) = @_;
209 6 50       50 $modelname = $modelname =~ m/^(?:\w+\.)* (\w+\.\w+)$/x ? $1 : $modelname;
210 6         40 return lc($modelname);
211             }
212              
213              
214             sub extjs_model_entityname {
215 2     2 1 21 my ( $self, $extjs_model_name ) = @_;
216 2 50       7 die 'ExtJS model name is required'
217             if not defined $extjs_model_name;
218 2         32 my $model_namespace = $self->model_namespace;
219 2         38 my ($entityname) = $extjs_model_name =~ /^$model_namespace\.(.+)$/;
220 2         26 return $entityname;
221             }
222              
223              
224             sub extjs_model {
225 6     6 1 4618 my ( $self, $rsrcname ) = @_;
226 6         151 my $schema = $self->schema;
227              
228 6         92 my $rsrc = $schema->source($rsrcname);
229 6         303 my $extjsname = $self->extjs_model_name($rsrcname);
230 6         99 my $columns_info = $rsrc->columns_info;
231 6         166 my %field_by_colname;
232             my %requires;
233             # same order the columns where added to the ResultSource
234 6         19 foreach my $colname ( $rsrc->columns ) {
235 28         109 my $field_params = { name => $colname };
236 28         47 my $column_info = $columns_info->{$colname};
237              
238             # views might not have column infos
239 28 50       57 if ( not %$column_info ) {
240 0         0 $field_params->{data_type} = 'auto';
241             }
242             else {
243 28         52 my $data_type = lc( $column_info->{data_type} );
244 28 50       66 if ( exists $translate{$data_type} ) {
245 28         44 my $extjs_data_type = $translate{$data_type};
246              
247             # determine if a numeric column is an int or a really a float
248 28 100       54 if ( $extjs_data_type eq 'float' ) {
249             $extjs_data_type = 'int'
250             if exists $column_info->{size}
251 4 50 33     64 && $column_info->{size} !~ /,/;
252             }
253 28         49 $field_params->{type} = $extjs_data_type;
254             # remember all ExtJS data types for requires
255 28         58 $requires{$extjs_class_for_datatype{$extjs_data_type}} = 1;
256             }
257              
258             $field_params->{defaultValue} = $column_info->{default_value}
259             if exists $column_info->{default_value}
260 28 100 100     71 && defined $column_info->{default_value};
261              
262 28 100       54 if ( exists $column_info->{is_nullable} ) {
263 26 100 100     80 if ( $column_info->{is_nullable} && $field_params->{type} ne 'date' ) {
264 16         39 $field_params->{allowNull} = true();
265             }
266             # only required for foreign key columns -> smaller JS
267             #else {
268             # $field_params->{allowBlank} = false();
269             #}
270             }
271             # is_nullable defaults to false in DBIC, allowNull defaults also
272             # to false in ExtJS 6, so we don't need to set it -> smaller JS
273             # else {
274             # $field_params->{allowNull} = false();
275             # }
276 28 50 66     98 if ( exists $column_info->{is_auto_increment}
277             && $column_info->{is_auto_increment} ) {
278 6         36 $field_params->{persist} = false();
279             }
280              
281             #use Data::Dumper::Concise;
282             #warn Dumper($column_info)
283             # if $rsrcname eq 'Customer';
284              
285             # support for DBIx::Class::DynamicDefault
286 28 0 0     146 if ( $rsrc->isa('DBIx::Class::DynamicDefault')
      33        
287             && (
288             ( exists $column_info->{dynamic_default_on_create}
289             && $column_info->{dynamic_default_on_create} eq 'get_timestamp' )
290             || (
291             exists $column_info->{dynamic_default_on_update}
292             && $column_info->{dynamic_default_on_update} eq 'get_timestamp'
293             )
294             ) ) {
295 0         0 $field_params->{persist} = false();
296             }
297              
298             # support for DBIx::Class::TimeStamp
299 28 0 33     100 if ( ( exists $column_info->{set_on_create}
      33        
      33        
300             && $column_info->{set_on_create} )
301             || (
302             exists $column_info->{set_on_update}
303             && $column_info->{set_on_update}
304             ) ) {
305 0         0 $field_params->{persist} = false();
306             }
307              
308             # support for DBIx::Class::UserStamp
309 28 0 33     83 if ( ( exists $column_info->{store_user_on_create}
      33        
      33        
310             && $column_info->{store_user_on_create} )
311             || (
312             exists $column_info->{store_user_on_update}
313             && $column_info->{store_user_on_update}
314             ) ) {
315 0         0 $field_params->{persist} = false();
316             }
317              
318             # support for DBIx::Class::InflateColumn::Boolean
319 28 0 33     55 if ( exists $column_info->{is_boolean}
320             && $column_info->{is_boolean} ) {
321 0         0 $field_params->{type} = 'bool';
322             }
323             }
324 28         58 $field_by_colname{$colname} = $field_params;
325             }
326              
327             #my @assocs;
328 6         28 foreach my $relname ( sort $rsrc->relationships ) {
329 6         50 my $relinfo = $rsrc->relationship_info($relname);
330              
331             # FIXME: handle complex relationship conditions, skip for now
332 6 50       34 if ( ! (ref $relinfo->{cond} eq 'HASH') ) {
333 0         0 warn "$extjsname:\t$relname: complex relationship condition, skipping";
334 0         0 next;
335             }
336              
337 6 50       8 if ( keys %{ $relinfo->{cond} } > 1 ) {
  6         20  
338 0         0 warn
339             "$extjsname:\t$relname: skipping because multi-cond rels aren't supported by ExtJS\n";
340 0         0 next;
341             }
342              
343 6 50       11 if ( keys %{ $relinfo->{cond} } > 1 ) {
  6         20  
344 0         0 warn
345             "$extjsname:\t$relname: multiple column relationship not supported by ExtJS\n";
346 0         0 next;
347             }
348              
349             #use Data::Dumper::Concise;
350             #print $rsrcname . Dumper($relinfo)
351             # if $rsrcname eq 'Raduser';
352              
353 6         10 my ($rel_col) = keys %{ $relinfo->{cond} };
  6         19  
354 6         13 my $our_col = $relinfo->{cond}->{$rel_col};
355 6         28 $rel_col =~ s/^foreign\.//;
356 6         20 $our_col =~ s/^self\.//;
357 6         13 my $column_info = $columns_info->{$our_col};
358              
359 6         20 my $remote_rsrc = $schema->source($relinfo->{source});
360 6         443 my $remote_relname;
361 6         17 foreach my $relname ( $remote_rsrc->relationships ) {
362 6         57 my $remote_relinfo = $remote_rsrc->relationship_info($relname);
363              
364             # FIXME: handle complex relationship conditions, skip for now
365 6 50       59 if ( ! (ref $remote_relinfo->{cond} eq 'HASH') ) {
366 0         0 warn "$extjsname:\t$relname: complex relationship condition, skipping";
367 0         0 next;
368             }
369 6         12 my ($remote_rel_col) = keys %{ $remote_relinfo->{cond} };
  6         19  
370 6         14 my $remote_our_col = $remote_relinfo->{cond}->{$remote_rel_col};
371 6         21 $remote_rel_col =~ s/^foreign\.//;
372 6         78 $remote_our_col =~ s/^self\.//;
373 6 50 33     138 if ( $remote_relinfo->{source} eq $rsrc->result_class
      33        
374             && $rel_col eq $remote_our_col
375             && $our_col eq $remote_rel_col ) {
376 6         178 $remote_relname = $relname;
377 6         12 last;
378             }
379             }
380 6 50       16 warn "$extjsname:\t$relname: can't find reverse relationship name\n"
381             if not defined $remote_relname;
382              
383 6         14 my $attrs = $relinfo->{attrs};
384              
385             #my $extjs_rel = {
386             # name => $relname,
387             # associationKey => $relname,
388             #
389             # # class instead of source?
390             # model => $self->extjs_model_name( $relinfo->{source} ),
391             # primaryKey => $rel_col,
392             # foreignKey => $our_col,
393             #};
394              
395             # belongs_to
396             #{
397             # attrs => {
398             # accessor => "filter",
399             # is_depends_on => 1,
400             # is_foreign_key_constraint => 1,
401             # undef_on_null_fk => 1
402             # },
403             # class => "My::Schema::Result::Another",
404             # cond => {
405             # "foreign.id" => "self.another_id"
406             # },
407             # source => "My::Schema::Result::Another"
408             #}
409              
410             # has_one
411             #{
412             # attrs => {
413             # accessor => "single",
414             # cascade_delete => 0,
415             # cascade_update => 1,
416             # is_depends_on => 0,
417             # proxy => [ "radusername_realm" ]
418             # },
419             # class => "NAC::Model::DBIC::Table::View_Raduser",
420             # cond => {
421             # "foreign.id_raduser" => "self.id_raduser"
422             # },
423             # source => "NAC::Model::DBIC::Table::View_Raduser"
424             #}
425 6 50 33     27 if (
      66        
426             $attrs->{is_foreign_key_constraint}
427             && ( $attrs->{accessor} eq 'single'
428             || $attrs->{accessor} eq 'filter' )
429             ) {
430 2 50       7 if ( exists $field_by_colname{$our_col}->{reference}) {
431             warn "$extjsname:\t$relname: relationship for column '$our_col' would overwrite '"
432             . $field_by_colname{$our_col}->{reference}->{role}
433 0         0 . "', skipping\n";
434 0         0 next;
435             }
436             # add reference to field definition
437             $field_by_colname{$our_col}->{reference} = {
438             type => $self->extjs_model_entityname(
439 2 50       7 $self->extjs_model_name( $relinfo->{source} ) ),
440             role => $relname,
441             (defined $remote_relname
442             ? (inverse => $remote_relname)
443             : ()
444             ),
445             };
446              
447             $field_by_colname{$our_col}->{allowBlank} = false()
448             if exists $column_info->{is_nullable}
449 2 50 33     16 and !$column_info->{is_nullable};
450              
451             $field_by_colname{$our_col}->{unique} = true()
452             if $attrs->{accessor} eq 'single'
453 2 50 33     11 && $attrs->{is_depends_on} == 0;
454              
455             #$extjs_rel->{type} = 'belongsTo';
456             }
457              
458             #{
459             # attrs => {
460             # accessor => "multi",
461             # cascade_copy => 1,
462             # cascade_delete => 1,
463             # is_depends_on => 0,
464             # join_type => "LEFT"
465             # },
466             # class => "My::Schema::Result::Basic",
467             # cond => {
468             # "foreign.another_id" => "self.id"
469             # },
470             # source => "My::Schema::Result::Basic"
471             #}
472             #elsif ( $attrs->{accessor} eq 'multi' ) {
473             # $extjs_rel->{type} = 'hasMany';
474             #}
475             #push @assocs, $extjs_rel;
476             }
477 6         148 my $model = {
478              
479             extend => $self->model_baseclass,
480             alias => $self->extjs_model_alias($extjsname),
481             requires => [ sort keys %requires ],
482             };
483 6         30 my @pk = $rsrc->primary_columns;
484 6 50       50 if ( @pk == 1 ) {
485 6         22 $model->{idProperty} = $pk[0];
486             }
487             else {
488 0         0 warn
489             "$extjsname:\tnot setting idProperty because number of primary key columns isn't one\n";
490             }
491 6         11 my @fields;
492              
493             # always keep the primary column as the first entry
494             push @fields, delete $field_by_colname{ $model->{idProperty} }
495 6 50       28 if exists $model->{idProperty};
496 6         23 push @fields, map { $field_by_colname{$_} } sort keys %field_by_colname;
  22         41  
497 6         16 $model->{fields} = \@fields;
498              
499             #$model->{associations} = \@assocs
500             # if @assocs;
501              
502             # override any generated config properties
503 6 50       29 if ( $self->model_args ) {
504 0         0 my %foo = ( %$model, %{ $self->model_args } );
  0         0  
505 0         0 $model = \%foo;
506             }
507              
508 6         39 return [ $extjsname, $model ];
509             }
510              
511              
512             sub extjs_models {
513 1     1 1 4763 my $self = shift;
514              
515 1         28 my $schema = $self->schema;
516              
517 1         11 my %output;
518 1         14 foreach my $rsrcname ( $schema->sources ) {
519 2         45 my $extjs_model = $self->extjs_model($rsrcname);
520              
521 2         9 $output{ $extjs_model->[0] } = $extjs_model;
522             }
523              
524 1         7 return \%output;
525             }
526              
527              
528             sub extjs_model_to_file {
529 4     4 1 16699 my ( $self, $rsrcname, $dirname ) = @_;
530              
531 4         31 my $dir = Path::Class::Dir->new($dirname);
532 4 100       257 $dir->open
533             or die "$!: " . $dirname;
534              
535             my ( $extjs_model_name, $extjs_model_code ) =
536 3         308 @{ $self->extjs_model($rsrcname) };
  3         97  
537              
538 3         14 my @namespaces = split( /\./, $extjs_model_name );
539 3 50       12 die "model class '"
540             . $namespaces[0]
541             . "' doesn't match appname '"
542             . $self->appname . "'"
543             if $namespaces[0] ne $self->appname;
544              
545 3         16 my $modeldir = $dir->subdir( @namespaces[ 1 .. $#namespaces - 1 ] );
546 3         169 $modeldir->mkpath;
547              
548 3         398 my $filename = $namespaces[-1] . '.js';
549 3         15 my $file = $modeldir->file($filename);
550 3 50       306 my $fh = $file->open( O_CREAT | O_WRONLY | O_EXCL )
551             #my $fh = $file->open( O_TRUNC | O_WRONLY | O_EXCL )
552             or die "$!: $file";
553              
554             #$extjs_model_code->{classname} = $extjs_model_name;
555             #my $template_vars = {
556             # classname => $extjs_model_name,
557             # attributes => $extjs_model_code,
558             #};
559 3         504 my $json =
560             #$self->_xslate->render_string($self->model_template, $template_vars);
561             "Ext.define('$extjs_model_name', "
562             . dump_javascript($extjs_model_code)
563             . ');';
564             #. $self->_json->encode($extjs_model_code)
565              
566 3         2333 $fh->write($json . "\n");
567             }
568              
569              
570             sub extjs_basemodel_to_file {
571 1     1 1 4 my ( $self, $dirname ) = @_;
572              
573 1         7 my $dir = Path::Class::Dir->new($dirname);
574 1 50       45 $dir->open
575             or die "$!: " . $dirname;
576              
577 1         95 my @namespaces = split( /\./, $self->model_baseclass );
578 1 50       69 die "model base class '"
579             . $namespaces[0]
580             . "' doesn't match appname '"
581             . $self->appname . "'"
582             if $namespaces[0] ne $self->appname;
583              
584 1         7 my $basemodeldir = $dir->subdir( @namespaces[ 1 .. $#namespaces - 1 ] );
585 1         52 $basemodeldir->mkpath;
586              
587 1         173 my $filename = $namespaces[-1] . '.js';
588 1         4 my $file = $basemodeldir->file($filename);
589 1 50       100 my $fh = $file->open( O_CREAT | O_WRONLY | O_EXCL )
590             #my $fh = $file->open( O_TRUNC | O_WRONLY | O_EXCL )
591             or die "$!: $file";
592              
593 1         174 my $extjs_basemodel_code = {
594             extend => 'Ext.data.Model',
595             schema => {
596             namespace => $self->model_namespace,
597             },
598             };
599              
600 1         28 my $json =
601             "Ext.define('" . $self->model_baseclass . "', "
602             . dump_javascript($extjs_basemodel_code) . ');';
603              
604 1         166 $fh->write($json . "\n");
605 1 50       60 $fh->close
606             or die "$!: " . $dirname;
607             }
608              
609              
610             sub extjs_all_to_file {
611 1     1 1 1685 my ( $self, $dirname ) = @_;
612              
613             # to check if the path exists and not fail for each source
614 1 50       7 my $dh = Path::Class::Dir->new($dirname)->open
615             or die "$!: $dirname";
616 1 50       139 $dh->close
617             or die "$!: $dirname";;
618              
619 1         49 my $schema = $self->schema;
620              
621             try {
622 1     1   135 $self->extjs_basemodel_to_file($dirname);
623             }
624       0     catch {
625             # ignore fails
626 1         18 };
627              
628 1         72 $self->extjs_model_to_file( $_, $dirname ) for sort $schema->sources;
629             }
630              
631              
632             1;
633              
634             __END__