| 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; |