File Coverage

lib/UR/Namespace/Command/Sys/ClassBrowser.pm
Criterion Covered Total %
statement 148 323 45.8
branch 22 78 28.2
condition 8 32 25.0
subroutine 31 58 53.4
pod 0 10 0.0
total 209 501 41.7


line stmt bran cond sub pod time code
1             package UR::Namespace::Command::Sys::ClassBrowser;
2              
3             # This turns on the perl stuff to insert data in the DB
4             # namespace so we can get line numbers and stuff about
5             # loaded modules
6             BEGIN {
7 2 50   2   34 unless ($^P) {
8 2     2   39 no strict 'refs';
  2         2  
  2         167  
9 0         0 *DB::DB = sub {};
10 0         0 $^P = 0x31f;
11             }
12             }
13              
14 2     2   7 use strict;
  2         5  
  2         31  
15 2     2   6 use warnings;
  2         4  
  2         60  
16 2     2   7 use UR;
  2         3  
  2         14  
17 2     2   7 use Data::Dumper;
  2         3  
  2         114  
18 2     2   8 use File::Spec;
  2         5  
  2         24  
19 2     2   33 use File::Basename;
  2         3  
  2         90  
20 2     2   8 use IO::File;
  2         2  
  2         263  
21 2     2   915 use Template;
  2         5496  
  2         81  
22 2     2   955 use Plack::Request;
  2         19825  
  2         17  
23 2     2   896 use Class::Inspector;
  2         4197  
  2         21  
24              
25             our $VERSION = "0.46"; # UR $VERSION;
26              
27             UR::Object::Type->define(
28             class_name => __PACKAGE__,
29             is => 'UR::Namespace::Command::Base',
30             has_optional => [
31             generate_cache => { is => 'Boolean', default_value => 0, doc => 'Generate the class cache file' },
32             use_cache => { is => 'Boolean', default_value => 1, doc => 'Use the class cache instead of scanning for modules'},
33             port => { is => 'Integer', default_value => 8080, doc => 'TCP port to listen for connections' },
34             timeout => { is => 'Integer', doc => 'If specified, exit after this many minutes of inactivity' },
35             host => { is => 'String', default_value => 'localhost', doc => 'host to listen on for connections' },
36             ],
37             );
38              
39 2     2 0 8 sub is_sub_command_delegator { 0;}
40              
41             sub help_brief {
42 0     0 0 0 "Start a web server to browse through the class structure.";
43             }
44              
45             sub help_synopsis {
46 0     0 0 0 q(# Start the web server
47             # By default, only connections from localhost are accepted
48             ur sys class-browser
49              
50             # Start the server and accept connections from any
51             # address, not just localhost
52             ur sys class-browser --host 0
53              
54             # Create the cache file for the current namespace
55             ur sys class-browser --generate-cache);
56             }
57              
58             sub help_detail {
59 0     0 0 0 q(The class-browser command starts an embedded web server containing an app for
60             browsing through the class structure. After starting, it prints a URL on
61             STDOUT that can be copy-and-pasted into a browser to run the app.);
62             }
63              
64             sub _class_info_cache_file_name_for_namespace {
65 0     0   0 my($self, $namespace) = @_;
66 0 0       0 unless ($INC{$namespace.'.pm'}) {
67 0         0 eval "use $namespace";
68 0 0       0 die $@ if $@;
69             }
70 0         0 my $class_cache_file = sprintf('.%s-class-browser-cache', $namespace);
71 0         0 return File::Spec->catfile($namespace->get_base_directory_name, $class_cache_file);
72             }
73              
74              
75             sub load_class_info_for_namespace {
76 0     0 0 0 my($self, $namespace) = @_;
77              
78 0         0 my $class_cache_file = $self->_class_info_cache_file_name_for_namespace($namespace);
79 0 0 0     0 if ($self->use_cache and -f $class_cache_file) {
80 0         0 $self->_load_class_info_from_cache_file($namespace, $class_cache_file);
81             } else {
82 0         0 $self->status_message("Preloading class information for namespace $namespace...");
83 0         0 $self->_load_class_info_from_modules_on_filesystem($namespace);
84             }
85             }
86              
87             sub _write_class_info_to_cache_file {
88 0     0   0 my $self = shift;
89              
90 0         0 my $current_namespace = $self->namespace_name;
91 0 0       0 return unless ($self->{_cache}->{$current_namespace});
92              
93 0         0 my $cache_file = $self->_class_info_cache_file_name_for_namespace($current_namespace);
94 0   0     0 my $fh = IO::File->new($cache_file, 'w') || die "Can't open $cache_file for writing: $!";
95              
96 0         0 $fh->print( Data::Dumper->new([$self->{_cache}->{$current_namespace}], ['cache_data'])->Sortkeys(1)->Purity(1)->Dump );
97 0         0 $fh->close();
98 0         0 $self->status_message("Saved class info to cache file $cache_file");
99             }
100              
101              
102             sub _load_class_info_from_cache_file {
103 0     0   0 my($self, $namespace, $class_cache_file) = @_;
104              
105 0 0       0 return 1 if ($self->{_cache}->{$namespace}); # Don't load same namespace more than once
106              
107 0         0 $self->status_message("Loading class info cache file $class_cache_file\n");
108 0         0 my $fh = IO::File->new($class_cache_file, 'r');
109 0 0       0 unless ($fh) {
110 0         0 $self->error_message("Cannot load class cache file $class_cache_file: $!");
111 0         0 return;
112             }
113              
114 0         0 my $buf;
115 0         0 { local $/;
  0         0  
116 0         0 $buf = <$fh>;
117             }
118 0         0 my $cache_data;
119 0         0 eval $buf;
120 0         0 $self->{_cache}->{$namespace} = $cache_data;
121             }
122              
123             sub _load_class_info_from_modules_on_filesystem {
124 3     3   2352 my $self = shift;
125 3         6 my $namespace = shift;
126              
127 3 100       18 return 1 if ($self->{_cache}->{$namespace}); # Don't load same namespace more than once
128              
129             my $by_class_name = $self->{_cache}->{$namespace}->{by_class_name}
130 1   33     8 ||= $self->_generate_class_name_cache($namespace);
131              
132 1 50       5 unless ($self->name_tree_cache($namespace)) {
133 1         22 $self->name_tree_cache( $namespace,
134             UR::Namespace::Command::Sys::ClassBrowser::TreeItem->new(
135             name => $namespace,
136             relpath => $namespace.'.pm'));
137             }
138 1 50       3 unless ($self->inheritance_tree_cache($namespace)) {
139 1         2 $self->inheritance_tree_cache( $namespace,
140             UR::Namespace::Command::Sys::ClassBrowser::TreeItem->new(
141             name => 'UR::Object',
142             relpath => 'UR::Object'));
143             }
144 1 50       3 unless ($self->directory_tree_cache($namespace)) {
145 1         5 $self->directory_tree_cache($namespace,
146             UR::Namespace::Command::Sys::ClassBrowser::TreeItem->new(
147             name => $namespace,
148             relpath => $namespace.'.pm' ));
149             }
150 1         2 my $inh_inserter = $self->_class_inheritance_cache_inserter($by_class_name, $self->inheritance_tree_cache($namespace));
151 1         4 foreach my $data ( values %$by_class_name ) {
152 5         9 $self->_insert_cache_for_class_name_tree($data);
153 5         8 $self->_insert_cache_for_path($data);
154 5         9 $inh_inserter->($data->{name});
155             }
156 1         6 1;
157             }
158              
159             foreach my $cache ( [ 'by_class_name_tree', 'name_tree_cache'],
160             [ 'by_class_inh_tree', 'inheritance_tree_cache'],
161             [ 'by_directory_tree', 'directory_tree_cache'] ) {
162             my $key = $cache->[0];
163             my $subname = $cache->[1];
164             my $sub = sub {
165 20     20   19 my $self = shift;
166 20         15 my $namespace = shift;
167 20 50       28 unless (defined $namespace) {
168 0         0 Carp::croak "\$namespace is a required argument";
169             }
170 20 100       26 if (@_) {
171 3         6 $self->{_cache}->{$namespace}->{$key} = shift;
172             }
173 20         33 return $self->{_cache}->{$namespace}->{$key};
174             };
175             Sub::Install::install_sub({
176             into => __PACKAGE__,
177             as => $subname,
178             code => $sub,
179             });
180             }
181              
182              
183             sub _namespace_for_class_name {
184 10     10   9 my($self, $class_name) = @_;
185 10         33 return ($class_name =~ m/^(\w+)(::)?/)[0];
186             }
187              
188             sub _cached_data_for_class {
189 0     0   0 my($self, $class_name) = @_;
190              
191 0         0 my $namespace = $self->_namespace_for_class_name($class_name);
192 0         0 return $self->{_cache}->{$namespace}->{by_class_name}->{$class_name};
193             }
194              
195             # 1-level hash. Maps a class name to a hashref containing simple
196             # data about that class. relpath is relative to the namespace's module_path
197             sub _generate_class_name_cache {
198 2     2   380 my($self, $namespace) = @_;
199              
200 2         19 my $cwd = Cwd::getcwd . '/';
201 2         7 my $namespace_meta = $namespace->__meta__;
202 2         17 my $namespace_dir = $namespace_meta->module_directory;
203 2         6 (my $path = $namespace_meta->module_path) =~ s/^$cwd//;
204 2         14 my $by_class_name = { $namespace => {
205             name => $namespace,
206             is => $namespace_meta->is,
207             relpath => $namespace . '.pm',
208             id => $path,
209             file => File::Basename::basename($path),
210             }
211             };
212 2         11 foreach my $class_meta ( $namespace->get_material_classes ) {
213 8         25 my $class_name = $class_meta->class_name;
214 8         18 $by_class_name->{$class_name} = $self->_class_name_cache_data_for_class_name($class_name);
215             }
216 2         11 return $by_class_name;
217             }
218              
219             sub _class_name_cache_data_for_class_name {
220 11     11   11 my($self, $class_name) = @_;
221              
222 11         35 my $class_meta = $class_name->__meta__;
223 11 50       20 unless ($class_meta) {
224 0         0 Carp::carp("Can't get class metadata for $class_name... skipping.");
225 0         0 return;
226             }
227 11         41 my $namespace_dir = $class_meta->namespace->__meta__->module_directory;
228 11         38 my $module_path = $class_meta->module_path;
229 11         73 (my $relpath = $module_path) =~ s/^$namespace_dir//;
230             return {
231 11         24 name => $class_meta->class_name,
232             relpath => $relpath,
233             file => File::Basename::basename($relpath),
234             is => $class_meta->is,
235             };
236             }
237              
238             # Build the by-class-name tree data
239             sub _insert_cache_for_class_name_tree {
240 5     5   4 my($self, $data) = @_;
241              
242 5         10 my $namespace = $self->_namespace_for_class_name($data->{name});
243 5         10 my $tree = $self->name_tree_cache($namespace);
244 5         18 my @names = split('::', $data->{name});
245 5         5 my $relpath = shift @names; # Namespace is first part of the name
246 5         9 while(my $name = shift @names) {
247 6         11 $relpath = join('::', $relpath, $name);
248 6   66     9 $tree = $tree->get_child($name)
249             || $tree->add_child(
250             name => $name,
251             relpath => $relpath);
252             }
253 5         7 $tree->data($data);
254 5         6 return $tree;
255             }
256              
257             # Build the by_directory_tree data
258             sub _insert_cache_for_path {
259 5     5   5 my($self, $data) = @_;
260              
261 5         7 my $namespace = $self->_namespace_for_class_name($data->{name});
262 5         6 my $tree = $self->directory_tree_cache($namespace);
263              
264             # split up the path to the module relative to the namespace directory
265 5         35 my @path_parts = File::Spec->splitdir($data->{relpath});
266 5 50       12 shift @path_parts if $path_parts[0] eq '.'; # remove . at the start of the path
267              
268 5         3 my $partial_path = shift @path_parts;
269 5         10 while (my $subdir = shift @path_parts) {
270 6         9 $partial_path = join('/', $partial_path, $subdir);
271 6   66     8 $tree = $tree->get_child($subdir)
272             || $tree->add_child(
273             name => $subdir,
274             relpath => $partial_path);
275             }
276 5         8 $tree->data($data);
277 5         6 return $tree;
278             }
279              
280             sub _cache_has_data_for {
281 0     0   0 my($self, $namespace) = @_;
282 0         0 return exists($self->{_cache}->{$namespace});
283             }
284              
285              
286             # build the by_class_inh_tree data
287             sub _class_inheritance_cache_inserter {
288 1     1   1 my($self, $by_class_name, $tree) = @_;
289              
290 1 50       10 my $cache = $tree ? { $tree->name => $tree } : {};
291              
292 1         1 my $do_insert;
293             $do_insert = sub {
294 12     12   9 my $class_name = shift;
295              
296 12   66     30 $by_class_name->{$class_name} ||= $self->_class_name_cache_data_for_class_name($class_name);
297 12         19 my $data = $by_class_name->{$class_name};
298              
299 12 100       20 if ($cache->{$class_name}) {
300 5         9 return $cache->{$class_name};
301             }
302 7         12 my $node = UR::Namespace::Command::Sys::ClassBrowser::TreeItem->new(
303             name => $class_name, data => $data
304             );
305 7         11 $cache->{$class_name} = $node;
306              
307 7 50 33     18 if ((! $data->{is}) || (! @{ $data->{is}} )) {
  7         11  
308             # no parents?! This _is_ the root!
309 0         0 return $tree = $node;
310             }
311 7         6 foreach my $parent_class ( @{ $data->{is}} ) {
  7         11  
312 7         16 my $parent_class_tree = $do_insert->($parent_class);
313 7 50       9 unless ($parent_class_tree->has_child($class_name)) {
314 7         10 $parent_class_tree->add_child( $node );
315             }
316             }
317 7         6 return $node;
318 1         8 };
319              
320 1         4 return $do_insert;
321             }
322              
323              
324             sub execute {
325 0     0   0 my $self = shift;
326              
327 0 0       0 if ($self->generate_cache) {
328 0         0 $self->_load_class_info_from_modules_on_filesystem($self->namespace_name);
329 0         0 $self->_write_class_info_to_cache_file();
330 0         0 return 1;
331             }
332              
333 0         0 $self->load_class_info_for_namespace($self->namespace_name);
334              
335 0   0     0 my $tt = $self->{_tt} ||= Template->new({ INCLUDE_PATH => $self->_template_dir, RECURSION => 1 });
336              
337 0         0 my $server = UR::Service::WebServer->create(timeout => $self->timeout,
338             host => $self->host,
339             port => $self->port);
340              
341 0         0 my $router = UR::Service::UrlRouter->create( verbose => $self->verbose);
342 0         0 my $assets_dir = $self->__meta__->module_data_subdirectory.'/assets/';
343 0         0 $router->GET(qr(/assets/(.*)), $server->file_handler_for_directory( $assets_dir));
344 0     0   0 $router->GET('/', sub { $self->index(@_) });
  0         0  
345 0     0   0 $router->GET(qr(/detail-for-class/(.*)), sub { $self->detail_for_class(@_) });
  0         0  
346 0     0   0 $router->GET(qr(/search-for-class/(.*)), sub { $self->search_for_class(@_) });
  0         0  
347 0     0   0 $router->GET(qr(/render-perl-module/(.*)), sub { $self->render_perl_module(@_) });
  0         0  
348 0     0   0 $router->GET(qr(/property-metadata-list/(.*)/(\w+)), sub { $self->property_metadata_list(@_) });
  0         0  
349              
350 0         0 $server->cb($router);
351 0         0 $server->run();
352              
353 0         0 return 1;
354             }
355              
356             sub _template_dir {
357 0     0   0 my $self = shift;
358 0         0 return $self->__meta__->module_data_subdirectory();
359             }
360              
361             sub index {
362 0     0 0 0 my $self = shift;
363 0         0 my $env = shift;
364              
365 0         0 my $req = Plack::Request->new($env);
366 0   0     0 my $namespace = $req->param('namespace') || $self->namespace_name;
367              
368 0 0       0 unless ($self->_cache_has_data_for($namespace)) {
369 0         0 $self->load_class_info_for_namespace($namespace);
370             }
371             my $data = {
372             current_namespace => $namespace,
373 0         0 namespaces => [ map { $_->id } UR::Namespace->is_loaded() ],
  0         0  
374             classnames => $self->name_tree_cache($namespace),
375             inheritance => $self->inheritance_tree_cache($namespace),
376             paths => $self->directory_tree_cache($namespace),
377             };
378              
379 0         0 return $self->_process_template('class-browser.html', $data);
380             }
381              
382             sub _process_template {
383 0     0   0 my($self, $template_name, $template_data) = @_;
384              
385 0         0 my $out = '';
386 0         0 my $tmpl = $self->{_tt};
387 0 0       0 $tmpl->process($template_name, $template_data, \$out)
388             and return [ 200, [ 'Content-Type' => 'text/html' ], [ $out ]];
389              
390             # Template error :(
391 0         0 $self->error_message("Template failed: ".$tmpl->error);
392 0         0 return [ 500, [ 'Content-Type' => 'text/plain' ], [ 'Template failed', $tmpl->error ]];
393             }
394              
395             sub _fourohfour {
396 0     0   0 return [ 404, [ 'Content-Type' => 'text/plain' ], ['Not Found']];
397             }
398              
399             sub _line_for_function {
400 0     0   0 my($self, $name) = @_;
401 0         0 my $info = $DB::sub{$name};
402              
403 0 0       0 return () unless $info;
404 0         0 my ($file,$start);
405 0 0       0 if ($info =~ m/\[(.*?):(\d+)\]/) { # This should match eval's and __ANON__s
    0          
406 0         0 ($file,$start) = ($1,$2);
407              
408             } elsif ($info =~ m/(.*?):(\d+)-(\d+)$/) {
409 0         0 ($file,$start) = ($1,$2);
410              
411             }
412              
413 0 0       0 if ($start) {
414             # Convert $file into a package name
415 0         0 foreach my $inc ( keys %INC ) {
416 0 0       0 if ($INC{$inc} eq $file) {
417 0         0 (my $pkg = $inc) =~ s/\//::/g;
418 0         0 $pkg =~ s/\.pm$//;
419 0         0 return (package => $pkg, line => $start);
420             }
421             }
422             }
423 0         0 return;
424             }
425              
426             # Return a list of package names where $method is defined
427             sub _overrides_for_method {
428 0     0   0 my($self, $class, $method) = @_;
429              
430 0         0 my %seen;
431             my @results;
432 0         0 my @isa = ($class);
433 0         0 while (my $target_class = shift @isa) {
434 0 0       0 next if $seen{$target_class}++;
435 0 0       0 if (Class::Inspector->function_exists($target_class, $method)) {
436 0         0 push @results, $target_class;
437             }
438 2     2   3943 { no strict 'vars';
  2         3  
  2         1830  
  0         0  
439 0         0 push @isa, eval '@' . $target_class . '::ISA';
440             }
441             }
442 0         0 return \@results;
443             }
444              
445             sub detail_for_class {
446 0     0 0 0 my $self = shift;
447 0         0 my $env = shift;
448 0         0 my $class = shift;
449              
450 0         0 my $class_meta = eval { $class->__meta__};
  0         0  
451              
452 0         0 my $tree = UR::Namespace::Command::Sys::ClassBrowser::TreeItem->new(
453             name => 'UR::Object',
454             relpath => 'UR::Object');
455              
456 0         0 my $namespace = $class_meta->namespace;
457             my $treebuilder = $self->_class_inheritance_cache_inserter(
458             $self->{_cache}->{$namespace}->{by_class_name},
459 0         0 $tree,
460             );
461 0         0 $treebuilder->($class);
462              
463 0 0       0 unless ($class_meta) {
464 0         0 return $self->_fourohfour;
465             }
466              
467 0         0 my @public_methods = sort { $a->[2] cmp $b->[2] } # sort by function name
468 0         0 @{ Class::Inspector->methods($class, 'public', 'expanded') };
  0         0  
469 0         0 my @private_methods = sort { $a->[2] cmp $b->[2] } # sort by function name
470 0         0 @{ Class::Inspector->methods($class, 'private', 'expanded') };
  0         0  
471              
472             # Convert each of them to a hashref for easier access
473 0         0 foreach ( @public_methods, @private_methods ) {
474 0         0 my $class = $_->[1];
475 0         0 my $method = $_->[2];
476 0         0 my $function = $_->[0];
477 0         0 my $cache = $self->_cached_data_for_class($class);
478             $_ = {
479             class => $class,
480             method => $method,
481             file => $cache->{relpath},
482 0         0 overrides => $self->_overrides_for_method($class, $method),
483             $self->_line_for_function($function),
484             };
485             }
486              
487 0         0 my @sorted_properties = sort { $a->property_name cmp $b->property_name }
  0         0  
488             $class_meta->properties;
489              
490 0         0 my $tmpl_data = {
491             meta => $class_meta,
492             property_metas => \@sorted_properties,
493             class_inheritance_tree => $tree,
494             public_methods => \@public_methods,
495             private_methods => \@private_methods,
496             };
497 0         0 return $self->_process_template('class-detail.html', $tmpl_data);
498             }
499              
500             sub search_for_class {
501 0     0 0 0 my $self = shift;
502 0         0 my $env = shift;
503 0         0 my $search = shift;
504              
505 0         0 my $req = Plack::Request->new($env);
506 0   0     0 my $namespace = $req->param('namespace') || $self->namespace_name;
507              
508 0         0 my $class_cache = $self->{_cache}->{$namespace}->{by_class_name};
509             my @results = sort
510 0         0 grep { m/$search/i } keys %$class_cache;
  0         0  
511              
512 0 0       0 if (@results == 1) {
513 0         0 return $self->detail_for_class($env, $results[0]);
514             } else {
515 0         0 return $self->_process_template('search_results.html',
516             { search => $search, classes => \@results });
517             }
518             }
519              
520             sub render_perl_module {
521 0     0 0 0 my($self, $env, $module_name) = @_;
522              
523 0         0 my $module_path;
524 0 0       0 if (my $class_meta = eval { $module_name->__meta__ }) {
  0         0  
525 0         0 $module_path = $class_meta->module_path;
526              
527             } else {
528 0         0 ($module_path = $module_name) =~ s/::/\//g;
529 0         0 $module_path = $INC{$module_path.'.pm'};
530             }
531 0 0 0     0 unless ($module_path and -f $module_path) {
532 0         0 return $self->_fourohfour;
533             }
534              
535 0         0 my $fh = IO::File->new($module_path, 'r');
536 0         0 my @lines = <$fh>;
537 0         0 chomp(@lines);
538 0         0 return $self->_process_template('render-perl-module.html', { module_name => $module_name, lines => \@lines });
539             }
540              
541             # Render the popover content when hovering over a row in the
542             # class property table
543             sub property_metadata_list {
544 0     0 0 0 my($self, $env, $class_name, $property_name) = @_;
545              
546 0         0 my $class_meta = $class_name->__meta__;
547 0 0       0 unless ($class_meta) {
548 0         0 return $self->_fourohfour;
549             }
550 0         0 my $prop_meta = $class_meta->property_meta_for_name($property_name);
551 0 0       0 unless ($prop_meta) {
552 0         0 return $self->_fourohfour;
553             }
554              
555 0         0 return $self->_process_template('partials/property_metadata_list.html',
556             { meta => $prop_meta,
557             show => [qw( doc class_name column_name data_type data_length is_id
558             via to where reverse_as id_by
559             valid_values example_values is_optional is_transient is_constant
560             is_mutable is_delegated is_abstract is_many is_deprecated
561             is_calculated calculate_perl calculate_sql
562             )],
563             });
564             }
565              
566              
567             package UR::Namespace::Command::Sys::ClassBrowser::TreeItem;
568              
569             sub new {
570 19     19   14 my $class = shift;
571 19         35 my %node = @_;
572 19 50       26 die "new() requires a 'name' parameter" unless (exists $node{name});
573              
574 19         22 $node{children} = {};
575 19 50       24 unless (defined $node{id}) {
576 19         33 ($node{id} = $node{name}) =~ s/::/__/g;
577             }
578 19         22 my $self = bless \%node, __PACKAGE__;
579 19         26 return $self;
580             }
581              
582             sub id {
583 0     0   0 return shift->{id};
584             }
585              
586             sub name {
587 36     36   100 return shift->{name};
588             }
589              
590             sub relpath {
591 0     0   0 return shift->{relpath};
592             }
593              
594             sub data {
595 22     22   27 my $self = shift;
596 22 100       33 if (@_) {
597 10         9 $self->{data} = shift;
598             }
599 22         58 return $self->{data};
600             }
601              
602             sub has_children {
603 11     11   674 my $self = shift;
604 11         8 return %{$self->{children}};
  11         51  
605             }
606              
607             sub children {
608 11     11   17 my $self = shift;
609 11         7 return [ values(%{$self->{children}}) ];
  11         44  
610             }
611              
612             sub has_child {
613 7     7   5 my $self = shift;
614 7         7 my $child_name = shift;
615 7         14 return exists($self->{children}->{$child_name});
616             }
617              
618             sub get_child {
619 29     29   1467 my $self = shift;
620 29         32 my $child_name = shift;
621 29         89 return $self->{children}->{$child_name};
622             }
623              
624             sub add_child {
625 16     16   11 my $self = shift;
626 16 100       27 my $child = ref($_[0]) ? shift(@_) : $self->new(@_);
627 16         23 $self->{children}->{ $child->name } = $child;
628             }
629              
630              
631             1;
632              
633             =pod
634              
635             =head1 NAME
636              
637             UR::Namespace::Command::Sys::ClassBrowser - WebApp for browsing the class structure
638              
639             =head1 SYNOPSIS
640              
641             # Start the web server
642             ur sys class-browser
643              
644             # Create the cache file for the current namespace
645             ur sys class-browser --generate-cache
646              
647             =head1 DESCRIPTION
648              
649             The class-browser command starts an embedded web server containing an app for
650             browsing through the class structure. After starting, it prints a URL on
651             STDOUT that can be copy-and-pasted into a browser to run the app.
652              
653             =head1 COMMAND-LINE OPTIONS
654              
655             With no options, the command expects to be run within a Namespace directory.
656             It will auto-discover all the classes in the Namespace, either from a
657             previously created cache file, or by scanning all the perl modules within the
658             Namespace's subdirectory.
659              
660             =over 4
661              
662             =item --generate-cache
663              
664             Instead of starting a web server, the command will scan for all perl modules
665             within the Namespace's subdirectory and create a file called
666             .-class-browser-cache, then exit. This file will contain
667             information about all the classes it found, which will improve the start-up
668             time the next time the command is run.
669              
670             =item --port
671              
672             Change the TCP port the web server listens on. The default is 8080.
673              
674             =item --nouse-cache
675              
676             The command will use the cache file generated by the --generate-cache option
677             if it finds one. When --nouse-cache is used, it will always scan for perl
678             modules, and will ignore any cache that may be present.
679              
680             =item --verbose
681              
682             Causes the command to print the STDOUT the URLs loaded while it is running.
683              
684             =back
685              
686             =head1 SEE ALSO
687              
688             L, L, L
689              
690             =cut