File Coverage

blib/lib/UR/Object/Command/Crud.pm
Criterion Covered Total %
statement 240 250 96.0
branch 90 130 69.2
condition 10 21 47.6
subroutine 34 36 94.4
pod 0 4 0.0
total 374 441 84.8


line stmt bran cond sub pod time code
1             package UR::Object::Command::Crud;
2              
3 11     11   503733 use strict;
  11         32  
  11         439  
4 11     11   67 use warnings 'FATAL';
  11         27  
  11         538  
5              
6 11     11   2668 use UR::Object::Command::Create;
  11         42  
  11         190  
7 11     11   4180 use UR::Object::Command::Copy;
  11         120  
  11         139  
8 11     11   4569 use UR::Object::Command::Delete;
  11         54  
  11         153  
9 11     11   4608 use UR::Object::Command::Update;
  11         49  
  11         146  
10 11     11   4783 use UR::Object::Command::UpdateTree;
  11         55  
  11         171  
11 11     11   4948 use UR::Object::Command::UpdateIsMany;
  11         52  
  11         157  
12 11     11   5253 use UR::Object::Command::CrudUtil;
  11         58  
  11         127  
13 11     11   462 use Lingua::EN::Inflect;
  11         25  
  11         692  
14 11     11   75 use List::MoreUtils;
  11         26  
  11         110  
15 11     11   6187 use Sub::Install;
  11         26  
  11         92  
16 11     11   4664 use UR::Object::Command::List;
  11         2855108  
  11         137  
17              
18             class UR::Object::Command::Crud {
19             id_by => {
20             target_class => { is => 'Text' },
21             },
22             has => {
23             namespace => { is => 'Text', },
24             target_name => { is => 'Text', },
25             target_name_pl => { is => 'Text', },
26             sub_command_configs => { is => 'HASH', default_value => {}, },
27             },
28             has_calculated => {
29             target_name_ub => {
30             calculate_from => [qw/ target_name /],
31             calculate => q( $target_name =~ s/ /_/g; $target_name; ),
32             },
33             target_name_ub_pl => {
34             calculate_from => [qw/ target_name_pl /],
35             calculate => q( $target_name_pl =~ s/ /_/g; $target_name_pl; ),
36             },
37             },
38             has_transient_optional => {
39             namespace_sub_command_classes => {
40             is => 'Text',
41             is_many => 1,
42             },
43             namespace_sub_command_names => {
44             is => 'Text',
45             is_many => 1,
46             },
47             },
48             doc => 'Dynamically build CRUD commands',
49             };
50              
51 16     16 0 13818 sub buildable_sub_command_names { (qw/ copy create delete list update /) }
52             sub sub_command_class_name_for {
53 115     115 0 37868 join('::', $_[0]->namespace, join('', map { ucfirst } split(/\s+/, $_[1])));
  115         1708  
54             }
55              
56             sub sub_command_config_for {
57 69     69 0 232 my ($self, $name) = @_;
58              
59 69 50       219 $self->fatal_message('No sub command name given to get config!') if not $name;
60              
61 69         283 my $sub_command_configs = $self->sub_command_configs;
62 69 100       565 return if !exists $sub_command_configs->{$name};
63              
64 53 50       496 if ( ref($sub_command_configs->{$name}) ne 'HASH' ) {
65 0         0 $self->fatal_message('Invalid sub_command_config for %s: %s', $name, Data::Dumper::Dumper($sub_command_configs->{$name}));
66             }
67              
68 53         100 %{$sub_command_configs->{$name}}; # copy hash
  53         287  
69             }
70              
71             sub create_command_subclasses {
72 16     16 0 75151 my ($class, %params) = @_;
73              
74 16 100       94 $class->fatal_message('No target_class given!') if not $params{target_class};
75              
76 15         138 my $self = $class->create(%params);
77 15 50       1541141 return if not $self;
78              
79 15 50       83 $self->namespace( $self->target_class.'::Command' ) if not $self->namespace;
80 15         1318 $self->_resolve_target_names;
81              
82 15         6506 my @errors = $self->__errors__;
83 15 50       60169 $self->fatal_message( join("\n", map { $_->__display_name__ } @errors) ) if @errors;
  0         0  
84              
85 15         90 $self->_build_command_tree;
86 15         922 $self->_get_current_namespace_sub_commands_and_names;
87              
88 15         26553 $self->_build_copy_command;
89 15         5486 $self->_build_create_command;
90 15         7658 $self->_build_list_command;
91 12         6046 $self->_build_update_command;
92 12         10239 $self->_build_delete_command;
93 12         4557 $self->_set_namespace_sub_commands_and_names;
94              
95 12         827 $self;
96             }
97              
98             sub _get_current_namespace_sub_commands_and_names {
99 15     15   40 my $self = shift;
100 15         66 $self->namespace_sub_command_classes([ $self->namespace->sub_command_classes ]);
101 15         32998 $self->namespace_sub_command_names([ $self->namespace->sub_command_names ]);
102             }
103              
104             sub _add_to_namespace_sub_commands_and_names {
105 19     19   86 my ($self, $name) = @_;
106 19         124 $self->namespace_sub_command_names([ $self->namespace_sub_command_names, $name ]);
107 19         75034 $self->namespace_sub_command_classes([ $self->namespace_sub_command_classes, $self->sub_command_class_name_for($name) ]);
108             }
109              
110             sub _set_namespace_sub_commands_and_names {
111 12     12   37 my $self = shift;
112              
113 12         98 my @sub_command_classes = sort { $a cmp $b } List::MoreUtils::uniq $self->namespace_sub_command_classes;
  14         3127  
114             Sub::Install::reinstall_sub({
115 7     7   7436 code => sub{ @sub_command_classes },
116 12         17799 into => $self->namespace,
117             as => 'sub_command_classes',
118             });
119              
120 12         1240 my @sub_command_names = sort { $a cmp $b } List::MoreUtils::uniq $self->namespace_sub_command_names;
  14         3024  
121             Sub::Install::reinstall_sub({
122 2     2   759 code => sub{ @sub_command_names },
123 12         18452 into => $self->namespace,
124             as => 'sub_command_names',
125             });
126             }
127              
128             sub _resolve_target_names {
129 15     15   47 my $self = shift;
130              
131 15 50       74 if ( !$self->target_name ) {
132 15         133 $self->target_name( join(' ', map { lc(UR::Value::Text->get($_)->to_camel) } split('::', $self->target_class)) );
  27         2454182  
133             }
134              
135 15 50       102115 if ( !$self->target_name_pl ) {
136 15         195 Lingua::EN::Inflect::classical(persons => 1);
137 15         435 $self->target_name_pl( Lingua::EN::Inflect::PL($self->target_name) );
138             }
139             }
140              
141             sub _build_command_tree {
142 15     15   41 my $self = shift;
143              
144 15 100       64 return if UR::Object::Type->get($self->namespace);
145              
146 14         48271 UR::Object::Type->define(
147             class_name => $self->namespace,
148             is => 'Command::Tree',
149             doc => 'work with '.$self->target_name_pl,
150             );
151              
152 14         368459 for my $property (qw/ namespace target_class target_name target_name_pl target_name_ub target_name_ub_pl /) {
153             Sub::Install::install_sub({
154 24     24   338420 code => sub{ $self->$property },
155 84         3678 into => $self->namespace,
156             as => $property,
157             });
158             }
159             }
160              
161             sub _build_list_command {
162 15     15   39 my $self = shift;
163              
164 15         61 my $list_command_class_name = $self->sub_command_class_name_for('list');
165 15 50       86 return if UR::Object::Type->get($list_command_class_name); # Do not recreate...
166              
167 15         57012 my %config = $self->sub_command_config_for('list');
168 15 100       72 return if exists $config{skip}; # Do not create if told not too...
169              
170 6         32 my @has = (
171             subject_class_name => {
172             is_constant => 1,
173             value => $self->target_class,
174             },
175             );
176              
177 6         54 my $show = delete $config{show};
178 6 100       21 if ( $show ) {
179 1 50       10 $self->fatal_message('Invalid config for LIST `show` => %s', Data::Dumper::Dumper($show)) if ref $show;
180 0         0 push @has, show => { value => $show, };
181             }
182              
183 5         13 my $order_by = delete $config{order_by};
184 5 100       17 if ( $order_by ) {
185 1 50       8 $self->fatal_message('Invalid config for LIST `order_by` => %s', Data::Dumper::Dumper($order_by)) if ref $order_by;
186 0         0 push @has, order_by => { value => $order_by, };
187             }
188              
189 4 100       18 $self->fatal_message('Unknown config for LIST: %s', Data::Dumper::Dumper(\%config)) if %config;
190              
191 3         26 UR::Object::Type->define(
192             class_name => $list_command_class_name,
193             is => 'UR::Object::Command::List',
194             has => \@has,
195             );
196              
197             Sub::Install::install_sub({
198 0     0   0 code => sub{ $self->target_name_pl },
199 3         88865 into => $list_command_class_name,
200             as => 'help_brief',
201             });
202              
203 3         227 $self->_add_to_namespace_sub_commands_and_names('list');
204             }
205              
206             sub _build_create_command {
207 15     15   39 my $self = shift;
208              
209 15         64 my $create_command_class_name = $self->sub_command_class_name_for('create');
210 15 50       83 return if UR::Object::Type->get($create_command_class_name); # Do not recreate...
211              
212 15         54711 my %config = $self->sub_command_config_for('create');
213 15 100       76 return if exists $config{skip}; # Do not create if told not too...
214              
215 4         11 my @exclude;
216 4 100       16 if ( exists $config{exclude} ) {
217 1         2 @exclude = @{delete $config{exclude}};
  1         3  
218             }
219              
220 4 50       16 $self->fatal_message('Unknown config for CREATE: %s', Data::Dumper::Dumper(\%config)) if %config;
221              
222 4         20 my $target_meta = $self->target_class->__meta__;
223 4         38 my %properties;
224 4         37 for my $target_property ( $target_meta->property_metas ) {
225 24         1162 my $property_name = $target_property->property_name;
226              
227 24 100       140 next if grep { $property_name eq $_ } @exclude;
  6         16  
228 23 50       57 next if $target_property->class_name eq 'UR::Object';
229 23 50       146 next if $property_name =~ /^_/;
230 23 50       37 next if grep { $target_property->$_ } (qw/ is_calculated is_constant is_transient /);
  69         297  
231 23 0 0     138 next if $target_property->is_id and ($property_name eq 'id' or $property_name =~ /_id$/);
      33        
232 23 100       111 next if grep { not $target_property->$_ } (qw/ is_mutable /);
  23         60  
233 19 50 66     140 next if $target_property->is_many and $target_property->is_delegated and not $target_property->via; # direct relationship
      66        
234              
235 19         186 my %property = (
236             property_name => $property_name,
237             data_type => $target_property->data_type,
238             is_many => $target_property->is_many,
239             is_optional => $target_property->is_optional,
240             valid_values => $target_property->valid_values,
241             default_value => $target_property->default_value,
242             doc => $target_property->doc,
243             );
244              
245 19 100       450 if ( $property_name =~ s/_id(s)?$// ) {
246 4 50       18 $property_name .= $1 if $1;
247 4         16 my $object_meta = $target_meta->property_meta_for_name($property_name);
248 4 50 33     72 if ( $object_meta and not grep { $object_meta->$_ } (qw/ is_calculated is_constant is_transient id_class_by /) ) {
  16         81  
249 4         33 $property{property_name} = $property_name;
250 4         22 $property{data_type} = $object_meta->data_type;
251 4 50       26 $property{doc} = $object_meta->doc if $object_meta->doc;
252             }
253             }
254              
255 19         141 $properties{$property{property_name}} = \%property;
256             }
257              
258 4 50       17 $self->fatal_message('No properties found for target class %s', $self->target_class) if not %properties;
259              
260 4         22 my $create_meta = UR::Object::Type->define(
261             class_name => $create_command_class_name,
262             is => 'UR::Object::Command::Create',
263             has => \%properties,
264             has_constant_transient => {
265             namespace => { value => $self->namespace, },
266             target_class_properties => { value => [ keys %properties ], },
267             },
268             doc => 'create '.$self->target_name_pl,
269             );
270              
271 4         198304 $self->_add_to_namespace_sub_commands_and_names('create');
272             }
273              
274             sub _build_copy_command {
275 15     15   37 my $self = shift;
276              
277 15         77 my $copy_command_class_name = $self->sub_command_class_name_for('copy');
278 15 50       79 return if UR::Object::Type->get($copy_command_class_name);
279            
280 15         240175 my %config = $self->sub_command_config_for('copy');
281 15 100       99 return if exists $config{skip}; # Do not create if told not too...
282              
283 3         42 UR::Object::Type->define(
284             class_name => $copy_command_class_name,
285             is => 'UR::Object::Command::Copy',
286             doc => sprintf('copy a %s', $self->target_name),
287             has => {
288             source => {
289             is => $self->target_class,
290             shell_args_position => 1,
291             doc => sprintf('The source %s to copy.', $self->target_name),
292             },
293             },
294             );
295              
296 3         98145 $self->_add_to_namespace_sub_commands_and_names('copy');
297             }
298              
299             sub _build_update_command {
300 12     12   36 my $self = shift;
301              
302 12         49 my %config = $self->sub_command_config_for('update');
303 12 100       62 return if exists $config{skip}; # Do not create if told not too...
304              
305             # Config
306             # target meta and properties
307 6         27 my $target_meta = $self->target_class->__meta__;
308 6         102 my @properties = $target_meta->property_metas;
309              
310             # exclude these properties
311 6         1198 my @exclude;
312 6 100       28 if ( exists $config{exclude} ) {
313 1         2 @exclude = @{delete $config{exclude}};
  1         5  
314             }
315              
316             # only if null
317 6         16 my %only_if_null;
318 6 100       25 if ( my $only_if_null = delete $config{only_if_null} ) {
319 1         3 my $ref = ref $only_if_null;
320 1 50       6 if ( $only_if_null eq 1 ) {
    50          
321 0         0 %only_if_null = map { $_->property_name => 1 } @properties;
  0         0  
322             }
323             elsif ( not $ref ) {
324 0         0 Carp::confess("Unknown 'only_if_null' config: $only_if_null");
325             }
326             else {
327 1 50       33 %only_if_null = map { $_ => 1 } map { s/_id$//; $_; } ( $ref eq 'ARRAY' ? @$only_if_null : keys %$only_if_null )
  2         5  
  2         6  
  2         4  
328             }
329             }
330              
331 6 50       50 $self->fatal_message('Unknown config for UPDATE: %s', Data::Dumper::Dumper(\%config)) if %config;
332              
333             # Update Tree
334 6         26 my $update_command_class_name = $self->sub_command_class_name_for('update');
335 6         90 my $update_meta = UR::Object::Type->get($update_command_class_name);
336              
337 6         16904 my (@update_sub_commands, @update_sub_command_names);
338 6 100       27 if ( not $update_meta ) {
339 5         32 UR::Object::Type->define(
340             class_name => $update_command_class_name,
341             is => 'UR::Object::Command::UpdateTree',
342             doc => 'properties on '.$self->target_name_pl,
343             );
344             }
345             else { # update command tree exists
346 1         8 @update_sub_commands = $update_command_class_name->sub_command_classes;
347 1         248 @update_sub_command_names = $update_command_class_name->sub_command_names;
348             }
349              
350             # Properties: make a command for each
351 6         146063 my %properties_seen;
352 6         49 PROPERTY: for my $target_property ( $target_meta->property_metas ) {
353 36         651 my $property_name = $target_property->property_name;
354 36 50       323 next if grep { $property_name eq $_ } @update_sub_command_names;
  0         0  
355 36 100   11   364 next if List::MoreUtils::any { $property_name eq $_ } @exclude;
  11         31  
356              
357 34 50       186 next if $target_property->class_name eq 'UR::Object';
358 34 50       343 next if $property_name =~ /^_/;
359 34 50       94 next if grep { $target_property->$_ } (qw/ is_id is_calculated is_constant is_transient /);
  136         798  
360 34 100       246 next if grep { not $target_property->$_ } (qw/ is_mutable /);
  34         139  
361 28 50 66     259 next if $target_property->is_many and $target_property->is_delegated and not $target_property->via; # direct relationship
      66        
362              
363 28         364 my %property = (
364             name => $target_property->singular_name,
365             name_pl => $target_property->plural_name,
366             is_many => $target_property->is_many,
367             data_type => $target_property->data_type,
368             doc => $target_property->doc,
369             );
370 28 100       800 $property{valid_values} = $target_property->valid_values if defined $target_property->valid_values;
371 28 100       302 $property{only_if_null} = ( exists $only_if_null{$property_name} ) ? 1 : 0;
372              
373 28 100       156 if ( $property_name =~ s/_id(s)?$// ) {
374 6 50       42 $property_name .= $1 if $1;
375 6         65 my $object_meta = $target_meta->property_meta_for_name($property_name);
376 6 50       161 if ( $object_meta ) {
377 6 50       21 next if grep { $object_meta->$_ } (qw/ is_calculated is_constant is_transient id_class_by /);
  24         134  
378 6         57 $property{name} = $object_meta->singular_name;
379 6         48 $property{name_pl} = $object_meta->plural_name;
380 6         51 $property{is_optional} = $object_meta->is_optional;
381 6         44 $property{data_type} = $object_meta->data_type;
382             }
383             }
384 28 50       134 next if $properties_seen{$property_name};
385 28         78 $properties_seen{$property_name} = 1;
386              
387 28         55 my $update_sub_command;
388 28 100       91 if ( $property{is_many} ) {
389 5         30 $update_sub_command = $self->_build_update_is_many_property_sub_commands(\%property);
390             }
391             else {
392 23         101 $update_sub_command = $self->_build_update_property_sub_command(\%property);
393             }
394 28 50       310 push @update_sub_commands, $update_sub_command if $update_sub_command;
395             }
396              
397             Sub::Install::reinstall_sub({
398 1     1   1864 code => sub{ @update_sub_commands },
399 6         107 into => $update_command_class_name,
400             as => 'sub_command_classes',
401             });
402              
403 6         543 $self->_add_to_namespace_sub_commands_and_names('update');
404             }
405              
406             sub _build_update_property_sub_command {
407 23     23   70 my ($self, $property) = @_;
408              
409 23         124 my $update_property_class_name = join('::', $self->sub_command_class_name_for('update'), join('', map { ucfirst } split('_', $property->{name})));
  29         136  
410 23 50       132 return if UR::Object::Type->get($update_property_class_name);
411              
412             UR::Object::Type->define(
413             class_name => $update_property_class_name,
414             is => 'UR::Object::Command::Update',
415             has => {
416             $self->target_name_ub_pl => {
417             is => $self->target_class,
418             is_many => 1,
419             shell_args_position => 1,
420             doc => ucfirst($self->target_name_pl).' to update, resolved via query string.',
421             },
422             value => {
423             is => $property->{data_type},
424             valid_values => $property->{valid_values},
425             doc => sprintf('New `%s` (%s) of the %s.', $property->{name}, $property->{data_type}, $self->target_name_pl),
426             },
427             },
428             has_constant_transient => {
429             namespace => { value => $self->namespace, },
430             property_name => { value => $property->{name}, },
431             only_if_null => { value => $property->{only_if_null}, },
432             },
433 23 100       99239 doc => sprintf('update %s %s%s', $self->target_name_pl, $property->{name}, ( $property->{only_if_null} ? ' [only if null]' : '' )),
434             );
435              
436 23         1023140 $update_property_class_name;
437             }
438              
439             sub _build_update_is_many_property_sub_commands {
440 5     5   19 my ($self, $property) = @_;
441              
442 5         32 my $tree_class_name = join('::', $self->namespace, 'Update', join('', map { ucfirst } split('_', $property->{name_pl})));
  5         91  
443             UR::Object::Type->define(
444             class_name => $tree_class_name,
445             is => 'Command::Tree',
446             doc => 'add/remove '.$property->{name_pl},
447 5         53 );
448              
449 5         130845 my @update_sub_command_class_names;
450             Sub::Install::reinstall_sub({
451 0     0   0 code => sub{ @update_sub_command_class_names },
452 5         84 into => $tree_class_name,
453             as => 'sub_command_classes',
454             });
455              
456 5         393 for my $function (qw/ add remove /) {
457 10         212481 my $sub_command_class_name = join('::', $tree_class_name, ucfirst($function));
458 10         37 push @update_sub_command_class_names, $sub_command_class_name;
459             UR::Object::Type->define(
460             class_name => $sub_command_class_name,
461             is => 'UR::Object::Command::UpdateIsMany',
462             has => {
463             $self->target_name_ub_pl => {
464             is => $self->target_class,
465             is_many => 1,
466             shell_args_position => 1,
467             doc => sprintf('%s to update %s, resolved via query string.', ucfirst($self->target_name_pl), $property->{name_pl}),
468             },
469             'values' => => {
470             is => $property->{data_type},
471             is_many => 1,
472             valid_values => $property->{valid_values},
473             doc => sprintf('%s (%s) to %s %s %s.', ucfirst($property->{name_pl}), $property->{data_type}, $function, ( $function eq 'add' ? 'to' : 'from' ), $self->target_name_pl),
474             },
475             },
476             has_constant_transient => {
477             namespace => { value => $self->namespace, },
478             property_function => { value => join('_', $function, $property->{name}), },
479             },
480 10 100       323 doc => sprintf('%s to %s', $property->{name_pl}, $self->target_name_pl),
481             );
482             }
483              
484 5         204232 $tree_class_name;
485             }
486              
487             sub _build_delete_command {
488 12     12   33 my $self = shift;
489              
490 12         58 my $delete_command_class_name = $self->sub_command_class_name_for('delete');
491 12 50       131 return if UR::Object::Type->get($delete_command_class_name);
492              
493 12         44143 my %config = $self->sub_command_config_for('delete');
494 12 100       70 return if exists $config{skip}; # Do not create if told not too...
495              
496 3 50       12 $self->fatal_message('Unknown config for DELETE: %s', Data::Dumper::Dumper(\%config)) if %config;
497              
498 3         103 UR::Object::Type->define(
499             class_name => $delete_command_class_name,
500             is => 'UR::Object::Command::Delete',
501             has => {
502             $self->target_name_ub => {
503             is => $self->target_class,
504             shell_args_position => 1,
505             require_user_verify => 1,
506             doc => ucfirst($self->target_name).' to delete, resolved via query string.',
507             },
508             },
509             has_constant_transient => {
510             namespace => { value => $self->namespace, },
511             },
512             doc => sprintf('delete %s', Lingua::EN::Inflect::A($self->target_name)),
513             );
514              
515 3         105706 $self->_add_to_namespace_sub_commands_and_names('delete');
516             }
517              
518             1;