File Coverage

lib/UR/Namespace/Command/Update/Doc.pm
Criterion Covered Total %
statement 21 164 12.8
branch 0 66 0.0
condition 0 10 0.0
subroutine 7 17 41.1
pod 0 2 0.0
total 28 259 10.8


line stmt bran cond sub pod time code
1             package UR::Namespace::Command::Update::Doc;
2              
3 1     1   23 use strict;
  1         1  
  1         28  
4 1     1   3 use warnings;
  1         1  
  1         34  
5              
6 1     1   6 use UR;
  1         2  
  1         6  
7             our $VERSION = "0.46"; # UR $VERSION;
8              
9 1     1   4 use IO::File;
  1         2  
  1         137  
10 1     1   4 use File::Basename;
  1         2  
  1         46  
11 1     1   4 use File::Path;
  1         2  
  1         32  
12 1     1   5 use YAML;
  1         1  
  1         1282  
13              
14             class UR::Namespace::Command::Update::Doc {
15             is => 'Command::V2',
16             has => [
17             executable_name => {
18             is => 'Text',
19             shell_args_position => 1,
20             doc => 'the name of the executable to document'
21             },
22             class_name => {
23             is => 'Text',
24             shell_args_position => 2,
25             doc => 'the command class which maps to the executable'
26             },
27             targets => {
28             is => 'Text',
29             is_optional => 1,
30             shell_args_position => 3,
31             is_many => 1,
32             doc => 'specific classes to document (documents all unless specified)',
33             },
34             exclude_sections => {
35             is => 'Text',
36             is_many => 1,
37             is_optional => 1,
38             doc => 'if specified, sections matching these names will be omitted',
39             },
40             input_path => {
41             is => 'Path',
42             is_optional => 1,
43             doc => 'optional location of the modules to document',
44             },
45             restrict_to_input_path => {
46             is => 'Boolean',
47             default_value => 1,
48             doc => 'when set, only modules found under the input-path will be processed',
49             },
50             output_path => {
51             is => 'Text',
52             is_optional => 1,
53             doc => 'optional location to output documentation files',
54             },
55             output_format => {
56             is => 'Text',
57             default_value => 'pod',
58             valid_values => ['pod', 'html'],
59             doc => 'the output format to write'
60             },
61             generate_index => {
62             is => 'Boolean',
63             default_value => 1,
64             doc => "when true, an 'index' of all files generated is written (currently works for html only)",
65             },
66             suppress_errors => {
67             is => 'Boolean',
68             default_value => 1,
69             doc => 'when set, warnings about unloadable modules will not be printed',
70             },
71             ],
72             has_transient_optional => [
73             _writer_class => {
74             is => 'Text',
75             },
76             _index_filename => {
77             is => 'Text',
78             }
79             ],
80             doc => "generate documentation for commands"
81             };
82              
83             sub help_synopsis {
84             return <<"EOS"
85             ur update doc -i ./lib -o ./doc ur UR::Namespace::Command
86             EOS
87 0     0 0   }
88              
89             sub help_detail {
90 0     0 0   return join("\n",
91             'This tool generates documentation for each of the commands in a tree for a given executable.',
92             'This command must be run from within the namespace directory.');
93             }
94              
95             sub execute {
96 0     0     my $self = shift;
97              
98 0 0 0       die "--generate-index requires --output-dir to be specified" if $self->generate_index and !$self->output_path;
99              
100              
101             # scrub any trailing / from input/output_path
102 0 0         if ($self->output_path) {
103 0           my $output_path = $self->output_path;
104 0           $output_path =~ s/\/+$//m;
105 0           $self->output_path($output_path);
106             }
107              
108 0 0         if ($self->input_path) {
109 0           my $input_path = $self->input_path;
110 0           $input_path =~ s/\/+$//m;
111 0           $self->input_path($input_path);
112             }
113              
114 0           $self->_writer_class("UR::Doc::Writer::" . ucfirst($self->output_format));
115 0 0         die "Unable to create a writer for output format '" . $self->output_format . "'" unless($self->_writer_class->can("create"));
116              
117 0           local $ENV{ANSI_COLORS_DISABLED} = 1;
118 0           my $entry_point_bin = $self->executable_name;
119 0           my $entry_point_class = $self->class_name;
120              
121 0           my @targets = $self->targets;
122 0 0         unless (@targets) {
123 0           @targets = ($entry_point_class);
124             }
125              
126 0           local @INC = @INC;
127 0 0         if ($self->input_path) {
128 0           unshift @INC, $self->input_path;
129 0           $self->status_message("using modules at " . $self->input_path);
130             }
131              
132 0           my $errors = 0;
133 0           for my $target (@targets) {
134 0           eval "use $target";
135 0 0         if ($@) {
136 0           $self->error_message("Failed to use $target: $@");
137 0           $errors++;
138             }
139             }
140 0 0         return if $errors;
141              
142 0 0         if ($self->output_path) {
143 0 0         unless (-d $self->output_path) {
144 0 0         if (-e $self->output_path) {
145 0           $self->status_message("output path is not a directory!: " . $self->output_path);
146             }
147             else {
148 0           File::Path::make_path($self->output_path);
149 0 0         if (-d $self->output_path) {
150 0           $self->status_message("using output directory " . $self->output_path);
151             }
152             else {
153 0           $self->status_message("error creating directory: $! for " . $self->output_path);
154             }
155             }
156             }
157             }
158              
159 0           local $Command::entry_point_bin = $entry_point_bin;
160 0           local $Command::entry_point_class = $entry_point_class;
161              
162 0           my @command_trees = map( $self->_get_command_tree($_), @targets);
163 0           $self->_generate_index(@command_trees);
164 0           for my $tree (@command_trees) {
165 0           $self->_process_command_tree($tree);
166             }
167              
168 0           return 1;
169             }
170              
171             sub _generate_index {
172 0     0     my ($self, @command_trees) = @_;
173              
174 0 0         if ($self->generate_index) {
175 0           my $index = Dump({ command_tree => \@command_trees });
176 0 0 0       if ($index and $index ne '') {
177 0           my $index_filename = "index.yml";
178 0           my $index_path = join("/", $self->output_path, $index_filename);
179 0 0         if (-e $index_path) {
180 0           $self->warning_message("Index generation overwriting existing file at $index_path");
181             }
182              
183 0           my $fh = IO::File->new($index_path, 'w');
184 0 0         unless ($fh) {
185 0           Carp::croak("Can't open file $index_path for writing: $!");
186             }
187 0           $fh->print($index);
188 0           $fh->close();
189              
190 0 0         $self->_index_filename($index_filename) if -e $index_path;
191             } else {
192 0           $self->warning_message("Unable to generate index");
193             }
194             }
195 0           return;
196             }
197              
198             sub _generate_content {
199 0     0     my ($self, $command) = @_;
200              
201 0           my $doc;
202 0           eval {
203 0           my @all_sections = $command->doc_sections;
204 0           my @sections;
205 0           for my $s (@all_sections) {
206 0 0         push(@sections, $s) unless grep { $s->title =~ /$_/ } $self->exclude_sections;
  0            
207             }
208              
209 0           my $writer = $self->_writer_class->create(
210             sections => \@sections,
211             title => $command->command_name,
212             );
213 0           $doc = $writer->render;
214             };
215              
216 0 0         if($@) {
217 0           $self->warning_message('Could not generate docs for ' . $command . '. ' . $@);
218 0           return;
219             }
220              
221 0 0         unless($doc) {
222 0           $self->warning_message('No docs generated for ' . $command);
223 0           return;
224             }
225              
226 0           my $command_name = $command->command_name;
227 0           my $filename = $self->_make_filename($command_name);
228 0           my $dir = $self->_get_output_dir($command_name);
229 0           my $doc_path = join("/", $dir, $filename);
230 0           $self->status_message("Writing $doc_path");
231              
232 0           my $fh;
233 0   0       $fh = IO::File->new('>' . $doc_path) || die "Cannot create file at " . $doc_path . "\n";
234 0           print $fh $doc;
235 0           close($fh);
236             }
237              
238             sub _process_command_tree {
239 0     0     my ($self, $tree) = @_;
240              
241 0 0         $self->_generate_content($tree->{command}) unless $tree->{external};
242              
243 0           for my $subtree (@{$tree->{sub_commands}}) {
  0            
244 0           $self->_process_command_tree($subtree);
245             }
246             }
247              
248             sub _make_filename {
249 0     0     my ($self, $class_name) = @_;
250 0           $class_name =~ s/ /-/g;
251 0           return "$class_name." . $self->output_format;
252             }
253              
254             sub _get_output_dir {
255 0     0     my ($self, $class_name) = @_;
256              
257 0 0         return $self->output_path if defined $self->output_path;
258 0           return File::Basename::dirname($class_name->__meta__->module_path);
259             }
260              
261             sub _navigation_info {
262 0     0     my ($self, $cmd_class) = @_;
263              
264 0           my @navigation_info;
265 0 0         if ($cmd_class eq $self->class_name) {
266 0           push(@navigation_info, [$self->executable_name, undef]);
267             } else {
268 0           push(@navigation_info, [$cmd_class->command_name_brief, undef]);
269 0           my $parent_class = $cmd_class->parent_command_class;
270 0           while ($parent_class) {
271 0 0         if ($parent_class eq $self->class_name) {
272 0           my $uri = $self->_make_filename($self->executable_name);
273 0           my $name = $self->executable_name;
274 0           unshift(@navigation_info, [$name, $uri]);
275 0           last;
276             } else {
277 0           my $uri = $self->_make_filename($parent_class->command_name);
278 0           my $name = $parent_class->command_name_brief;
279 0           unshift(@navigation_info, [$name, $uri]);
280             }
281 0           $parent_class = $parent_class->parent_command_class;
282             }
283             }
284              
285 0 0         if ($self->_index_filename) {
286 0           unshift(@navigation_info, ["(Top)", $self->_index_filename]);
287             }
288              
289 0           return @navigation_info;
290             }
291              
292             sub _get_command_tree {
293 0     0     my ($self, $command) = @_;
294 0           my $src = "use $command";
295 0           eval $src;
296 0 0         if ($@) {
297 0 0         $self->error_message("Failed to load class $command: $@") unless $self->suppress_errors;
298 0           return;
299             }
300              
301 0 0         return if $command->_is_hidden_in_docs;
302              
303 0           my $module_name = $command;
304 0           $module_name =~ s|::|/|g;
305 0           $module_name .= '.pm';
306 0 0         my $input_path = $self->input_path ? $self->input_path : '';
307 0           my $module_path = $INC{$module_name};
308 0           $self->status_message("Loaded $command from $module_name at $module_path");
309              
310 0 0         my $external = $module_path !~ /^$input_path\// ? 1 : 0;
311 0   0       my $tree = {
312             command => $command,
313             sub_commands => [],
314             module_path => $module_path,
315             external => $external,
316             parent_class => $command->parent_command_class || undef,
317             description => $command->help_brief,
318             };
319              
320 0 0         if ($command eq $self->class_name) {
321 0           $tree->{command_name} = $tree->{command_name_brief} = $self->executable_name;
322             } else {
323 0           $tree->{command_name} = $command->command_name;
324 0           $tree->{command_name_brief} = $command->command_name_brief;
325             }
326 0           $tree->{uri} = $self->_make_filename($tree->{command_name});
327              
328 0 0         if ($command->can("sub_command_classes")) {
329 0           for my $cmd ($command->sub_command_classes) {
330 0           my $subtree = $self->_get_command_tree($cmd);
331 0 0         push(@{$tree->{sub_commands}}, $subtree) if $subtree;
  0            
332             }
333             }
334 0           return $tree;
335             }
336              
337             1;