File Coverage

lib/UR/Object/Command/List.pm
Criterion Covered Total %
statement 111 234 47.4
branch 14 76 18.4
condition 4 33 12.1
subroutine 28 36 77.7
pod 1 6 16.6
total 158 385 41.0


line stmt bran cond sub pod time code
1             package UR::Object::Command::List;
2 2     2   58 use strict;
  2         2  
  2         51  
3 2     2   6 use warnings;
  2         3  
  2         53  
4              
5 2     2   7 use IO::File;
  2         3  
  2         317  
6 2     2   9 use Data::Dumper;
  2         2  
  2         95  
7             require Term::ANSIColor;
8 2     2   7 use UR;
  2         1  
  2         12  
9 2     2   1478 use UR::Object::Command::List::Style;
  2         3  
  2         20  
10 2     2   85 use List::Util qw(reduce);
  2         3  
  2         133  
11 2     2   445 use Command::V2;
  2         4  
  2         15  
12 2     2   57 use Carp qw();
  2         2  
  2         2380  
13              
14             our $VERSION = "0.46"; # UR $VERSION;
15              
16             class UR::Object::Command::List {
17             is => 'Command::V2',
18             has_input => [
19             subject_class_name => {
20             is => 'ClassName',
21             doc => 'the type of object to list',
22             },
23             filter => {
24             is => 'Text',
25             is_optional => 1,
26             doc => 'Filter results based on the parameters. See below for details.',
27             shell_args_position => 1,
28             },
29             show => {
30             is => 'Text',
31             is_optional => 1,
32             doc => 'Specify which columns to show, in order. Prefix with "+" or "^" to append/prepend to the default list.',
33             },
34             order_by => {
35             is => 'Text',
36             is_optional => 1,
37             doc => 'Output rows are listed sorted by these named columns in increasing order.',
38             },
39             ],
40             has_param => [
41             style => {
42             is => 'Text',
43             is_optional => 1,
44             valid_values => [qw/text csv tsv pretty html xml newtext/],
45             default_value => 'text',
46             doc => 'The output format.',
47             },
48             csv_delimiter => {
49             is => 'Text',
50             is_optional => 1,
51             default_value => ',',
52             doc => 'For the "csv" output style, specify the field delimiter for something besides a comma.',
53             },
54             noheaders => {
55             is => 'Boolean',
56             is_optional => 1,
57             default => 0,
58             doc => 'Include headers. Set --noheaders to turn headers off.',
59             },
60             ],
61             has_transient => [
62             output => {
63             is => 'IO::Handle',
64             is_optional =>1,
65             is_transient =>1,
66             default => \*STDOUT,
67             doc => 'output handle for list, defauls to STDOUT',
68             },
69             _fields => {
70             is_many => 1,
71             is_optional => 1,
72             doc => 'Methods which the caller intends to use on the fetched objects. May lead to pre-fetching the data.',
73             },
74             ],
75             doc => 'lists objects matching the specified expression',
76             };
77              
78 0     0 0 0 sub sub_command_sort_position { .2 };
79              
80             sub create {
81 6     6 1 221 my $class = shift;
82 6         38 my $self = $class->SUPER::create(@_);
83              
84 6 50 33     26 if (defined($self->csv_delimiter)
      33        
85             and ($self->csv_delimiter ne $self->__meta__->property_meta_for_name('csv_delimiter')->default_value)
86             and ($self->style ne 'csv')
87             ) {
88 0         0 $self->error_message('--csv-delimiter is only valid when used with --style csv');
89 0         0 return;
90             }
91              
92 6 50       21 unless ( ref $self->output ){
93 0         0 my $ofh = IO::File->new("> ".$self->output);
94 0 0 0     0 $self->error_message("Can't open file handle to output param ".$self->output) and die unless $ofh;
95 0         0 $self->output($ofh);
96             }
97              
98 6         18 return $self;
99             }
100              
101             sub _resolve_boolexpr {
102 6     6   6 my $self = shift;
103              
104 6         18 my ($bool_expr, %extra) = UR::BoolExpr->resolve_for_string(
105             $self->subject_class_name,
106             $self->_complete_filter,
107             $self->_hint_string,
108             $self->order_by,
109             );
110              
111 6 50       18 if (%extra) {
112 0         0 Carp::croak(
113             sprintf(
114             'Cannot list for class %s because some items in the filter or show were not properties of that class: %s',
115             $self->subject_class_name,
116             join(', ', keys %extra)
117             )
118             );
119             }
120              
121 6         17 return $bool_expr;
122             }
123              
124              
125             # Used by create() and execute() to distinguish whether an item from the show list
126             # is likely a property of the subject class or a more complicated expression that needs
127             # to be eval-ed later
128             sub _show_item_is_property_name {
129 42     42   44 my($self, $item) = @_;
130 42         152 return $item =~ m/^[\w\.]+$/;
131             }
132              
133             sub execute {
134 6     6   13 my $self = shift;
135              
136 6         18 my $subject_class_name = $self->subject_class_name;
137              
138             # ensure classes can be loaded from whatever namespace the subject class has
139             # TODO: make the UR command open the door for the type loading below to hit
140             # all namespaces when _it_ is running only. The ur commands are sw maint tools.
141 6         44 my ($ns) = ($subject_class_name =~ /^(.*?)::/);
142 6     1   548 eval "use $ns";
  1     1   6  
  1     1   1  
  1     1   22  
  1     1   7  
  1     1   1  
  1         9  
  1         7  
  1         1  
  1         7  
  1         6  
  1         1  
  1         7  
  1         7  
  1         2  
  1         8  
  1         8  
  1         2  
  1         8  
143 6         36 my $subject_class = UR::Object::Type->get($subject_class_name);
144              
145 6         22 my @fields = $self->resolve_show_column_names;
146              
147 6         20 my $bool_expr = $self->_resolve_boolexpr();
148 6 50       17 return unless (defined $bool_expr);
149              
150             # TODO: instead of using an iterator, get all the results back in a list and
151             # have the styler use the list, since it needs all the results to space the columns
152             # out properly anyway
153 6         19 my $iterator = $self->create_iterator_for_results_from_boolexpr($bool_expr);
154              
155 6         26 $self->display_styled_results($iterator, \@fields);
156              
157 6         114 return 1;
158             }
159              
160             sub resolve_show_column_names {
161 6     6 0 9 my $self = shift;
162 6         18 $self->_resolve_field_list;
163             }
164              
165             sub create_iterator_for_results_from_boolexpr {
166 6     6 0 9 my($self, $bx) = @_;
167 6         27 my $iterator = $self->subject_class_name->create_iterator($bx);
168 6 50       17 unless ($iterator) {
169 0         0 $self->fatal_message($self->subject_class_name->error_message);
170             }
171 6         14 return $iterator;
172             }
173              
174             sub display_styled_results {
175 6     6 0 8 my($self, $iterator, $fields) = @_;
176              
177 6         23 my $style_module_name = __PACKAGE__ . '::' . ucfirst $self->style;
178 6         29 my $style_module = $style_module_name->new(
179             iterator => $iterator,
180             show => $fields,
181             csv_delimiter => $self->csv_delimiter,
182             noheaders => $self->noheaders,
183             output => $self->output,
184             );
185 6         28 $style_module->format_and_print;
186             }
187              
188             sub _resolve_field_list {
189 12     12   11 my $self = shift;
190              
191 12 50       21 if ( my $show = $self->show ) {
192 12 50       37 if (substr($show,0,1) =~ /([\+\^\-])/) {
193             # if it starts with any of the special characters, combine with the default
194 0         0 my $default = $self->__meta__->property('show')->default_value;
195 0 0       0 unless ($default) {
196 0         0 $default = join(",", map { $_->property_name } $self->_properties_for_class_to_document($self->subject_class_name));
  0         0  
197             }
198 0         0 $show = join(',',$default,$show);
199             }
200              
201 12         14 my @show;
202             my $expr;
203 12         29 my @parts = (split(/,/, $show));
204 12         17 my $append_prepend_or_omit = '+';
205 12         11 my $prepend_count = 0;
206 12         16 for my $item (@parts) {
207 28 50       53 if ($item =~ /^([\+\^\-])/) {
208 0 0       0 if ($1 eq '^') {
209 0         0 $prepend_count = 0;
210             }
211 0         0 $append_prepend_or_omit = $1;
212 0         0 $item = substr($item,1);
213             }
214 28 100 66     43 if ($self->_show_item_is_property_name($item) and not defined $expr) {
215 26 50       36 if ($append_prepend_or_omit eq '+') {
    0          
    0          
216             # append
217 26         39 push @show, $item;
218             }
219             elsif ($append_prepend_or_omit eq '^') {
220             # prepend
221 0         0 splice(@show, $prepend_count, 0, $item);
222 0         0 $prepend_count++;
223             }
224             elsif ($append_prepend_or_omit eq '-') {
225             # omit
226 0         0 @show = grep { $_ ne $item } @show;
  0         0  
227             }
228             else {
229 0         0 die "unrecognized operator in show string: $append_prepend_or_omit";
230             }
231             }
232             else {
233 2 50       6 if ($expr) {
234 0         0 $expr .= ',' . $item;
235             }
236             else {
237 2         4 $expr = '(' . $item;
238             }
239 2         2 my $o;
240 2 50       111 if (eval('sub { ' . $expr . ')}')) {
241 2         4 push @show, $expr . ')';
242             #print "got: $expr<\n";
243 2         12 $expr = undef;
244             }
245             }
246             }
247 12 50       26 if ($expr) {
248 0         0 die "Bad expression: $expr\n$@\n";
249             }
250 12         41 return @show;
251             }
252             else {
253 0         0 return map { $_->property_name } $self->_properties_for_class_to_document($self->subject_class_name);
  0         0  
254             }
255             }
256              
257             sub _filter_doc {
258 0     0   0 my $class = shift;
259 0         0 my $doc = <
260             Filtering:
261             ----------
262             Restrict which items are listed by adding a filter.
263             job=Captain
264              
265             Quotes are needed only when spaces or special words are involved.
266             Sylistically, use " on the outer expression, and ' around field values:
267             "age>18" # > is a special character
268             name='Bob Jones' # spaces in a field value
269              
270             Standard and/or predicated logic is supported (like in SQL).
271             "name='Bob Jones' and job='Captain' and age>18"
272             "name='Betty Jones' and (score < 10 or score > 100)"
273              
274             The "like" operator uses "%" as a wildcard:
275             "name like '%Jones'"
276              
277             The "not" operator negates the condition:
278             "name not like '%Jones'"
279              
280             Use square brackets for "in" clauses.
281             "name like '%Jones' and job in [Captain,Ensign,'First Officer']"
282              
283             Use a dot (".") to indirectly access related data (joins):
284             "age<18 and father.address.city='St. Louis'"
285             "previous_order.items.price > 100"
286              
287             A shorthand filter form allows many queries to be written more concisely:
288             regular: "name = 'Jones' and age between 18-25 and happy in ['yes','no','maybe']"
289             shorthand: name~%Jones,age:18-25,happy:yes/no/maybe
290              
291             Shorthand Key:
292             --------------
293             , " and "
294             = exactly equal to
295             ~ "like" the value
296             : "between" two values, dash "-" separated
297             : "in" the list of several values, slash "/" separated
298             ! "not" operator can be combined with any of the above
299             EOS
300              
301 0 0       0 if (my $help_synopsis = $class->help_synopsis) {
302 0         0 $doc .= "\n Examples:\n ---------\n";
303 0         0 $doc .= " $help_synopsis\n";
304             }
305              
306             # Try to get the subject class name
307 0         0 my $self = $class->create;
308 0 0 0     0 if ( not $self->subject_class_name
309             and my $subject_class_name = $self->_resolved_params_from_get_options->{subject_class_name} ) {
310 0         0 $self = $class->create(subject_class_name => $subject_class_name);
311             }
312              
313 0         0 my @properties = $self->_properties_for_class_to_document($self->subject_class_name);
314 0 0       0 my @filterable_properties = grep { ! $_->data_type or index($_->data_type, '::') == -1 } @properties;
  0         0  
315 0 0       0 my @relational_properties = grep { $_->data_type and index($_->data_type, '::') >= 0 } @properties;
  0         0  
316              
317 0         0 my $longest_name = 0;
318 0         0 foreach my $property ( @properties ) {
319 0         0 my $name_len = length($property->property_name);
320 0 0       0 $longest_name = $name_len if ($name_len > $longest_name);
321             }
322              
323 0         0 my @data;
324 0 0       0 if ( ! $self->subject_class_name ) {
    0          
325 0         0 $doc .= " Can't determine the list of properties without a subject_class_name.\n";
326             } elsif ( ! @properties ) {
327 0         0 $doc .= sprintf(" %s\n", $self->error_message);
328             } else {
329 0 0       0 if (@filterable_properties) {
330 0         0 push @data, 'Simple Properties:';
331 0         0 for my $property ( @filterable_properties ) {
332 0         0 push @data, [$property->property_name, $self->_doc_for_property($property, $longest_name)];
333             }
334             }
335              
336 0 0       0 if (@relational_properties) {
337 0         0 push @data, 'Complex Properties (support dot-syntax):';
338 0         0 for my $property ( @relational_properties ) {
339 0         0 my $name = $property->property_name;
340 0         0 my @doc = $self->_doc_for_property($property,$longest_name);
341 0         0 push @data, [$name, $doc[0]];
342 0         0 for my $n (1..$#doc) {
343 0         0 push @data, ['', $doc[$n]];
344             }
345             }
346             }
347             }
348 0         0 my @lines = $class->_format_property_doc_data(@data);
349 2     2   11 { no warnings 'uninitialized';
  2         2  
  2         1021  
  0         0  
350 0         0 $doc .= join("\n ", @lines);
351             }
352              
353 0         0 $self->delete;
354 0         0 return $doc;
355             }
356              
357             sub _doc_for_property {
358 0     0   0 my $self = shift;
359 0         0 my $property = shift;
360 0         0 my $longest_name = shift;
361              
362 0         0 my $doc;
363              
364 0         0 my $property_doc = $property->doc;
365 0 0       0 unless ($property_doc) {
366 0         0 eval {
367 0         0 foreach my $ancestor_class_meta ( $property->class_meta->ancestry_class_metas ) {
368 0         0 my $ancestor_property_meta = $ancestor_class_meta->property_meta_for_name($property->property_name);
369 0 0 0     0 if ($ancestor_property_meta and $ancestor_property_meta->doc) {
370 0         0 $property_doc = $ancestor_property_meta->doc;
371 0         0 last;
372             }
373             }
374             };
375             }
376 0   0     0 $property_doc ||= '';
377 0         0 $property_doc =~ s/\n//gs; # Get rid of embeded newlines
378              
379 0         0 my $data_type = $property->data_type;
380 0         0 my $data_class = eval { $property->_data_type_as_class_name };
  0         0  
381              
382 0 0 0     0 if ($data_type and $data_class eq $data_type) {
383 0         0 my @has = $self->_properties_for_class_to_document($data_class);
384 0         0 my @labels;
385 0         0 for my $pmeta (@has) {
386 0         0 my $name = $pmeta->property_name;
387 0         0 my $type = $pmeta->data_type;
388 0 0 0     0 if ($type and $type =~ /::/) {
389 0         0 push @labels, "$name\[.*\]";
390             }
391             else {
392 0         0 push @labels, $name;
393             }
394             }
395             return (
396 0 0       0 ($property_doc ? $property_doc : ()),
397             " see for more details",
398             ' has: ' . join(", ", @labels),
399             '',
400             );
401             }
402             else {
403 0   0     0 $data_type ||= 'Text';
404 0 0       0 $data_type = (index($data_type, '::') == -1) ? ucfirst(lc $data_type) : $data_type;
405 0 0       0 if ($property_doc) {
406 0         0 $property_doc = '(' . $data_type . '): ' . $property_doc;
407             }
408             else {
409 0         0 $property_doc = '(' . $data_type . ')';
410             }
411 0         0 return $property_doc;
412             }
413             }
414              
415             sub _format_property_doc_data {
416 0     0   0 my ($class, @data) = @_;
417              
418 0         0 my @names = map { $_->[0] } grep { ref $_ } @data;
  0         0  
  0         0  
419 0 0   0   0 my $longest_name = reduce { length($a) > length($b) ? $a : $b } @names;
  0         0  
420 0         0 my $w = length($longest_name);
421              
422 0         0 my @lines;
423 0         0 for my $data (@data) {
424 0 0       0 if (ref $data) {
425 0         0 push @lines, sprintf(" %${w}s %s", $data->[0], $data->[1]);
426             } else {
427 0         0 push @lines, ' ', $data, '-' x length($data);
428             }
429             }
430            
431 0         0 return @lines;
432             }
433              
434             sub _properties_for_class_to_document {
435 0     0   0 my $self = shift;
436 0         0 my $target_class_name = shift;
437              
438 0         0 my $target_class_meta = $target_class_name->__meta__;
439 0         0 my @id_by = $target_class_meta->id_properties;
440              
441 0         0 my @props = $target_class_meta->properties;
442              
443 2     2   10 no warnings;
  2         3  
  2         544  
444             # These final maps are to get around a bug in perl 5.8 sort
445             # involving method calls inside the sort sub that may
446             # do sorts of their own
447             return
448 0         0 map { $_->[1] }
449 0 0       0 sort { $a->[1]->position_in_module_header <=> $b->[1]->position_in_module_header or $a->[0] cmp $b->[0] }
450 0         0 map { [ $_->property_name, $_ ] }
451             grep {
452 0 0 0     0 substr($_->property_name, 0, 1) ne '_'
  0   0     0  
453             and not $_->implied_by
454             and not $_->is_transient
455             and not $_->is_deprecated
456             }
457             @props;
458             }
459              
460             sub _base_filter {
461 6     6   20 return;
462             }
463              
464             sub _complete_filter {
465 6     6   8 my $self = shift;
466 6         18 return join(',', grep { defined $_ } $self->_base_filter,$self->filter);
  6         29  
467             }
468              
469             sub help_detail {
470 0     0 0 0 my $self = shift;
471 0         0 return join(
472             "\n",
473             $self->_style_doc,
474             $self->_filter_doc,
475             );
476             }
477              
478             sub _style_doc {
479 0     0   0 return <
480             Listing Styles:
481             ---------------
482             text - table like
483             pretty - objects listed singly with color enhancements
484             html - html table
485             xml - xml document using elements
486             tsv - tab separated values
487             csv - comma (or other character) separated values*
488              
489             --csv-delimiter can be used tospecify another delimiter besides a comma for "csv"
490             EOS
491             }
492              
493             sub _hint_string {
494 6     6   8 my $self = shift;
495 6         12 my @show_parts = grep { $self->_show_item_is_property_name($_) } $self->_resolve_field_list();
  14         27  
496 6         24 return join(',',@show_parts);
497             }
498              
499              
500             1;