From: Robert 'phaylon' Sedlacek Date: Mon, 23 Jul 2012 18:38:27 +0000 (+0000) Subject: initial version with mediawiki produce/parse, three reports, missing update-merger X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=499ebcdd07b89484627e8148cbd84a134b0da144;p=scpubgit%2FSystem-Introspector-Report.git initial version with mediawiki produce/parse, three reports, missing update-merger --- 499ebcdd07b89484627e8148cbd84a134b0da144 diff --git a/Makefile.PL b/Makefile.PL new file mode 100644 index 0000000..1c30f73 --- /dev/null +++ b/Makefile.PL @@ -0,0 +1,28 @@ +use strict; +use warnings FATAL => 'all'; +use ExtUtils::MakeMaker; + +(do 'maint/Makefile.PL.include' or die $@) unless -f 'META.yml'; + +WriteMakefile( + NAME => 'System-Introspector-Report', + VERSION_FROM => 'lib/System/Introspector/Report.pm', + PREREQ_PM => { + 'Moo' => '1.000000', + 'HTML::Zoom' => '0.009006', + 'Config::General' => '2.51', + 'Module::Runtime' => '0.013', + 'IO::All' => '0.45', + 'strictures' => '1', + 'JSON::PP' => '2.53', + 'Object::Remote' => '0.001001', + 'MRO::Compat' => 0, + 'Class::C3' => 0, + 'Getopt::Long' => 0, + 'Pod::Usage' => 0, + 'aliased' => 0, + }, + EXE_FILES => [ + 'bin/system-introspector-report', + ], +); diff --git a/bin/system-introspector-report b/bin/system-introspector-report new file mode 100755 index 0000000..deb074a --- /dev/null +++ b/bin/system-introspector-report @@ -0,0 +1,44 @@ +#!/usr/bin/env perl +use strictures; +use Getopt::Long; +use Pod::Usage; +use System::Introspector::Report::Source; +use System::Introspector::Report::Config; + +GetOptions( + 's|storage=s' => \my $storage_dir, + 'c|config=s' => \my $config_file, + 'r|report=s' => \my @report_types, + 'p|publish=s' => \my @publish_types, + 'a|all' => \my $all_reports, + 'h|help' => sub { pod2usage(0) }, +) or pod2usage(2); + +die "$0 requires --storage (-s) to be specified\n" + unless defined $storage_dir; + +die "$0 requires --config (-c) to be specified\n" + unless defined $storage_dir; + +my $config = System::Introspector::Report::Config + ->new(config_file => $config_file); + +my $source = System::Introspector::Report::Source + ->new_from_root($storage_dir); + +my @types = $all_reports + ? $config->report_types + : map [$_, {}], @report_types; +my @reports = $source->generate(@types); + +for my $publisher ($config->publishers(@publish_types)) { + $publisher->publish(\@reports); +} + +__END__ + +=head1 NAME + +system-introspector-report - Generate System::Introspector reports + +=cut diff --git a/lib/System/Introspector/Report.pm b/lib/System/Introspector/Report.pm new file mode 100644 index 0000000..6dc0610 --- /dev/null +++ b/lib/System/Introspector/Report.pm @@ -0,0 +1,33 @@ +package System::Introspector::Report; + +our $VERSION = '0.000001'; # 0.0.1 + +$VERSION = eval $VERSION; + +1; + +=head1 NAME + +System::Introspector::Report - Description goes here + +=head1 SYNOPSIS + +=head1 DESCRIPTION + +=head1 AUTHOR + + Robert Sedlacek + +=head1 CONTRIBUTORS + +None yet - maybe this software is perfect! (ahahahahahahahahaha) + +=head1 COPYRIGHT + +Copyright (c) 2012 the System::Introspector::Report L and L +as listed above. + +=head1 LICENSE + +This library is free software and may be distributed under the same terms +as perl itself. diff --git a/lib/System/Introspector/Report/Builder.pm b/lib/System/Introspector/Report/Builder.pm new file mode 100644 index 0000000..4f69ab4 --- /dev/null +++ b/lib/System/Introspector/Report/Builder.pm @@ -0,0 +1,14 @@ +package System::Introspector::Report::Builder; +use Moo; + +sub required_data { () } + +sub collect_from { + die sprintf "Instance '%s' does not provide 'collect_from'\n", shift; +} + +sub render_reports { + die sprintf "Instance '%s' does not provide 'render_reports'\n", shift; +} + +1; diff --git a/lib/System/Introspector/Report/Builder/Packages/Apt/API.pm b/lib/System/Introspector/Report/Builder/Packages/Apt/API.pm new file mode 100644 index 0000000..3b498a5 --- /dev/null +++ b/lib/System/Introspector/Report/Builder/Packages/Apt/API.pm @@ -0,0 +1,24 @@ +package System::Introspector::Report::Builder::Packages::Apt::API; +use Moo::Role; + +requires qw( + _collect_from_data +); + +sub collect_from { + my ($self, $id, $data) = @_; + my $host = $data->{host}{hostname}; + my $package = $data->{'packages/apt'}{installed}{packages} || {}; + my $upgrade = $data->{'packages/apt'}{upgradable}{actions}{inst} || {}; + return $self->_collect_from_data($id, $host, $package, $upgrade, $data); +} + +sub _clear_upgrade { + my ($self, $string, $version) = @_; + return undef + unless defined $string; + $string =~ s{^\[\Q$version\E\]\s*\((.+)\)$}{$1}; + return $string; +} + +1; diff --git a/lib/System/Introspector/Report/Builder/Packages/Apt/ByHost.pm b/lib/System/Introspector/Report/Builder/Packages/Apt/ByHost.pm new file mode 100644 index 0000000..5c9a088 --- /dev/null +++ b/lib/System/Introspector/Report/Builder/Packages/Apt/ByHost.pm @@ -0,0 +1,65 @@ +package System::Introspector::Report::Builder::Packages::Apt::ByHost; +use Moo; + +extends 'System::Introspector::Report::Builder'; + +has _known_package => (is => 'ro', default => sub { {} }); +has _host_package => (is => 'ro', default => sub { {} }); +has _host_upgrade => (is => 'ro', default => sub { {} }); + +sub required_data { + return qw( + packages/apt + host + ); +} + +sub _collect_from_data { + my ($self, $id, $hostname, $packages, $upgrade, $data) = @_; + for my $package (keys %$packages) { + $self->_known_package->{$package}++; + } + $self->_host_package->{$id} = $packages; + $self->_host_upgrade->{$id} = $upgrade; + return 1; +} + +sub render_reports { + my ($self) = @_; + my @columns = ( + { key => 'package', label => 'Package' }, + { key => 'version', label => 'Version' }, + { key => 'upgrade', label => 'Upgrade' }, + ); + my $host_package = $self->_host_package; + my $host_upgrade = $self->_host_upgrade; + my @known_packages = sort keys %{$self->_known_package}; + return map { + my $remote = $_; + +{ + columns => \@columns, + title => "Packages on $remote", + id => ['packages-apt-by-remote', $remote], + meta => { remote => $remote }, + rows => [map { + my $package = $_; + my $version = $host_package->{$remote}{$package}{version}; + my $upgrade = $self->_clear_upgrade( + $host_upgrade->{$remote}{$package}, + $version, + ); + +{ + package => $package, + defined($version) ? (version => $version) : (), + defined($upgrade) ? (upgrade => $upgrade) : (), + }; + } @known_packages], + }; + } sort keys %{$self->_host_package}; +} + +with $_ for qw( + System::Introspector::Report::Builder::Packages::Apt::API +); + +1; diff --git a/lib/System/Introspector/Report/Builder/Packages/Apt/ByPackage.pm b/lib/System/Introspector/Report/Builder/Packages/Apt/ByPackage.pm new file mode 100644 index 0000000..12c8b80 --- /dev/null +++ b/lib/System/Introspector/Report/Builder/Packages/Apt/ByPackage.pm @@ -0,0 +1,63 @@ +package System::Introspector::Report::Builder::Packages::Apt::ByPackage; +use Moo; + +extends 'System::Introspector::Report::Builder'; + +has _host_by_remote => (is => 'ro', default => sub { {} }); +has _host_data => (is => 'ro', default => sub { {} }); + +sub required_data { + return qw( + packages/apt + host + ); +} + +sub _collect_from_data { + my ($self, $id, $hostname, $packages, $upgrades) = @_; + $self->_host_by_remote->{$id} = $hostname; + for my $package (keys %$packages) { + my $version = $packages->{$package}{version}; + my $upgrade = $self->_clear_upgrade($upgrades->{$package}, $version); + $self->_host_data->{$package}{$id} = { + version => $version, + defined($upgrade) ? (upgrade => $upgrade) : (), + }; + } +} + +sub render_reports { + my ($self) = @_; + my @columns = ( + { key => 'hostname', label => 'Hostname' }, + { key => 'remote', label => 'Remote' }, + { key => 'version', label => 'Version' }, + { key => 'upgrade', label => 'Upgrade' }, + ); + my $data = $self->_host_data; + my $hbr = $self->_host_by_remote; + return map { + my $package = $_; + my $hosts = $data->{$package}; + +{ + columns => \@columns, + title => "$package Installations", + id => ['packages-apt-by-package', $package], + meta => { package => $package }, + rows => [ map { + my $remote = $_; + +{ + remote => $remote, + hostname => $hbr->{$remote}, + %{$hosts->{$remote}}, + }; + } sort keys %$hosts ], + }; + } sort keys %$data; +} + +with $_ for qw( + System::Introspector::Report::Builder::Packages::Apt::API +); + +1; diff --git a/lib/System/Introspector/Report/Builder/Perls.pm b/lib/System/Introspector/Report/Builder/Perls.pm new file mode 100644 index 0000000..041e33a --- /dev/null +++ b/lib/System/Introspector/Report/Builder/Perls.pm @@ -0,0 +1,60 @@ +package System::Introspector::Report::Builder::Perls; +use Moo; + +extends 'System::Introspector::Report::Builder'; + +has skip_errors => (is => 'ro', default => sub { 1 }); + +has _rows => (is => 'ro', default => sub { [] }); + +sub required_data { + return qw( + perls + host + ); +} + +sub collect_from { + my ($self, $id, $data) = @_; + my $perls = $data->{perls}{perls} || {}; + my $hostname = $data->{host}{hostname}; + if (my @perl_keys = sort keys %$perls) { + for my $perl_key (@perl_keys) { + my $perl = $perls->{$perl_key}; + next if defined($perl->{__error__}) and $self->skip_errors; + push @{$self->_rows}, { + hostname => $hostname, + remote => $id, + location => $perl->{executable}, + version => $perl->{config}{version}, + defined($perl->{__error__}) + ? (__error__ => $perl->{__error__}) + : (), + }; + } + } + else { + push @{$self->_rows}, { + hostname => $hostname, + remote => $id, + }; + } + return 1; +} + +sub render_reports { + my ($self) = @_; + return +{ + title => 'Perl Installations', + id => 'perls', + columns => [ + { key => 'remote', label => 'Remote' }, + { key => 'hostname', label => 'Hostname' }, + { key => 'location', label => 'Location' }, + { key => 'version', label => 'Version' }, + ], + rows => $self->_rows, + }; +} + +1; diff --git a/lib/System/Introspector/Report/Config.pm b/lib/System/Introspector/Report/Config.pm new file mode 100644 index 0000000..251cb45 --- /dev/null +++ b/lib/System/Introspector/Report/Config.pm @@ -0,0 +1,33 @@ +package System::Introspector::Report::Config; +use Moo; +use Config::General; +use Module::Runtime qw( use_module ); + +has config_file => (is => 'ro', required => 1); +has config => (is => 'lazy'); + +sub _build_config { + my ($self) = @_; + return +{ + Config::General->new($self->config_file)->getall, + }; +} + +sub report_types { + my ($self) = @_; + my $reports = $self->config->{report} || {}; + return map { + [$_, $reports->{$_}]; + } sort keys %$reports; +} + +sub publishers { + my ($self, @override) = @_; + my $publish = $self->config->{publish} || {}; + return map { + use_module("System::Introspector::Report::Publish::$_") + ->new($publish->{$_} || {}); + } @override ? @override : sort keys %$publish; +} + +1; diff --git a/lib/System/Introspector/Report/Publish/API.pm b/lib/System/Introspector/Report/Publish/API.pm new file mode 100644 index 0000000..12bef42 --- /dev/null +++ b/lib/System/Introspector/Report/Publish/API.pm @@ -0,0 +1,29 @@ +package System::Introspector::Report::Publish::API; +use Moo::Role; + +requires qw( + publish +); + +sub _prepare_matcher_from { + my ($self, $matchers) = @_; + my $pattern = join '|', map { + my @elements = split m{:}, $_; + join qr{\0}, map { + ($_ eq '*') ? qr{[^\0]+} : qr{\Q$_\E}; + } split m{:}, $_; + } ref($matchers) ? @$matchers : $matchers; + return qr{^(?:$pattern)$}; +} + +sub _match_id { + my ($self, $report, $match) = @_; + $match = $self->_prepare_matcher_from($match) + unless ref($match) eq 'Regexp'; + my $id = join "\0", ref($report->{id}) + ? @{$report->{id}} + : ($report->{id}); + return $id =~ $match; +} + +1; diff --git a/lib/System/Introspector/Report/Publish/MediaWiki.pm b/lib/System/Introspector/Report/Publish/MediaWiki.pm new file mode 100644 index 0000000..839a2e3 --- /dev/null +++ b/lib/System/Introspector/Report/Publish/MediaWiki.pm @@ -0,0 +1,63 @@ +package System::Introspector::Report::Publish::MediaWiki; +use Moo; +use aliased 'System::Introspector::Report::Publish::MediaWiki::Producer'; +use aliased 'System::Introspector::Report::Publish::MediaWiki::Connection'; + +has page_options => ( + is => 'ro', + default => sub { {} }, + init_arg => 'page', +); + +has producer => (is => 'lazy', handles => { + _render_page => 'render', +}); + +has connection => (is => 'ro', lazy => 1, builder => 1, handles => { + get_page => 'get', + put_page => 'put', +}); + +sub _build_producer { Producer->new } +sub _build_connection { Connection->new } + +sub publish { + my ($self, $reports) = @_; + my $pages = $self->page_options; + for my $page (sort keys %$pages) { + $self->_publish_page($reports, $page, $pages->{$page}); + } + return 1; +} + +sub _sort_reports { + my ($self, $reports, $included) = @_; + my @matchers = map { + $self->_prepare_matcher_from($_); + } ref($included) ? @{$included} : $included; + my @grouped; + for my $report (@$reports) { + my ($group_idx) = grep { + $self->_match_id($report, $matchers[$_]); + } 0 .. $#matchers; + if (defined $group_idx) { + push @{$grouped[$group_idx]}, $report; + } + } + return [ map { (@$_) } @grouped ]; +} + +sub _publish_page { + my ($self, $reports, $page, $options) = @_; + my $sorted = $self->_sort_reports($reports, $options->{report} || []); + print $self->_render_page($sorted); +# print $self->render_report($_) +# for @$sorted; + return 1; +} + +with $_ for qw( + System::Introspector::Report::Publish::API +); + +1; diff --git a/lib/System/Introspector/Report/Publish/MediaWiki/Connection.pm b/lib/System/Introspector/Report/Publish/MediaWiki/Connection.pm new file mode 100644 index 0000000..e0f19d6 --- /dev/null +++ b/lib/System/Introspector/Report/Publish/MediaWiki/Connection.pm @@ -0,0 +1,8 @@ +package System::Introspector::Report::Publish::MediaWiki::Connection; +use Moo; + +sub get { } + +sub put { } + +1; diff --git a/lib/System/Introspector/Report/Publish/MediaWiki/Page.pm b/lib/System/Introspector/Report/Publish/MediaWiki/Page.pm new file mode 100644 index 0000000..0244eef --- /dev/null +++ b/lib/System/Introspector/Report/Publish/MediaWiki/Page.pm @@ -0,0 +1,11 @@ +package System::Introspector::Report::Publish::MediaWiki::Page; +use Moo; + +has timestamp => (is => 'ro', required => 1); +has content => (is => 'ro', required => 1); + +sub update { + my ($self, $update_stream) = @_; +} + +1; diff --git a/lib/System/Introspector/Report/Publish/MediaWiki/Parser.pm b/lib/System/Introspector/Report/Publish/MediaWiki/Parser.pm new file mode 100644 index 0000000..4932353 --- /dev/null +++ b/lib/System/Introspector/Report/Publish/MediaWiki/Parser.pm @@ -0,0 +1,173 @@ +package System::Introspector::Report::Publish::MediaWiki::Parser; +use Moo; +use HTML::Zoom; + +my $_rx_front = qr{\A\Z}; +my $_rx_table_begin = qr{ $_rx_front TABLE \s+ begin \s+ }x; +my $_rx_table_end = qr{ $_rx_front TABLE \s+ end \s+ }x; +my $_rx_title_begin = qr{ $_rx_front TITLE \s+ begin \s+ }x; +my $_rx_title_end = qr{ $_rx_front TITLE \s+ end \s+ }x; +my $_rx_any_begin = qr{ $_rx_front (TABLE|TITLE) \s+ begin \s }x; +my $_rx_id = qr{(\S+)}; + +sub parse { + my ($self, $body) = @_; + my $lines = [split m{\n}, $body]; + my $parts = []; + my $state = '_parse_text'; + while (@$lines) { + $state = $self->$state($lines, $parts); + } + return $parts; +} + +sub _parse_table { + my ($self, $lines, $done) = @_; + my ($id, $title) = $self->_read_title($lines); + my $description = []; + $self->_parse_text($lines, $description); + my $report = $self->_read_report($lines, $id); + $report->{title} = $title; + $report->{description} = $description; + push @$done, $report; + return '_parse_text'; +} + +sub _read_report { + my ($self, $lines, $id) = @_; + shift(@$lines) =~ m{ $_rx_table_begin \Q$id\E $_rx_end }x + or die "Expected table for '$id' after title\n"; + my @report_lines; + while (@$lines) { + my $line = shift @$lines; + if ($line =~ m{ $_rx_table_end $_rx_id $_rx_end }x) { + return $self->_inflate_report(join('', @report_lines), $id); + } + push @report_lines, $line; + } + die "Missing end of '$id' report\n"; +} + +my $_is_type = sub { + my ($markup, $type, $tag) = @_; + my $next = $markup->peek; + return $markup->next + if not($next) or ( + ($next->{name} || '') eq $tag + and ($next->{type} || '') eq uc($type) + ); + return undef; +}; + +my $_collect_content = sub { + my ($stream, $type) = @_; + my @events; + until ($stream->$_is_type(close => $type)) { + push @events, $stream->next; + } + return HTML::Zoom->from_events(\@events)->to_html; +}; + +my $_get_class = sub { + my ($event, $rx) = @_; + my $class = $event->{attrs}{class} || ''; + if ($class =~ $rx) { + return $1; + } + return undef; +}; + +sub _inflate_columns { + my ($self, $stream) = @_; + my $user_col_idx = 0; + my @columns; + while (my $next = $stream->next) { + if (my $ev = $stream->$_is_type(open => 'th')) { + my $key = $ev->$_get_class(qr{si-colhead-(\S+)}); + my $label = $stream->$_collect_content('th'); + push @columns, { + key => defined($key) ? $key : '__usercol_' . $user_col_idx++, + label => $label, + defined($key) ? () : (user => 1), + }; + } + elsif ($stream->$_is_type(close => 'tr')) { + last; + } + } + return @columns; +} + +sub _inflate_rows { + my ($self, $stream, $cols) = @_; + my @rows; + my $user_col_orph_idx = 0; + while (my $next = $stream->next) { + if ($stream->$_is_type(open => 'tr')) { + push @rows, {}; + next; + } + if (my $ev = $stream->$_is_type(open => 'td')) { + my $key; + if ($#$cols >= scalar keys %{$rows[-1]}) { + $key = $cols->[scalar keys %{$rows[-1]}]{key}; + } + else { + my $newcol = { + key => "__usercol_orph_" . $user_col_orph_idx++, + user => 1, + }; + push @$cols, $newcol; + $key = $newcol->{key}; + } + my $value = $stream->$_collect_content('td'); + $rows[-1]{$key} = $value; + } + } + return \@rows; +} + +sub _inflate_report { + my ($self, $table, $id) = @_; + my $markup = HTML::Zoom->from_html($table); + my $stream = $markup->to_stream; + my @columns = $self->_inflate_columns($stream); + my $rows = $self->_inflate_rows($stream, \@columns); + return { + id => [split m{:}, $id], + columns => [@columns], + rows => $rows, + }; +} + +sub _read_title { + my ($self, $lines) = @_; + my $start_line = shift @$lines; + $start_line =~ m{ $_rx_title_begin ($_rx_id) $_rx_end }x + or die "Unable to parse table start: $start_line\n"; + my $id = $1; + my $title_line = shift @$lines; + $title_line =~ m{ \A == \s+ (.+) \s+ == \Z }x + or die "Unable to parse title line: $title_line\n"; + my $title = $1; + my $end_line = shift(@$lines); + $end_line =~ m{ $_rx_title_end \Q$id\E $_rx_end }x + or die "Missing title end marker, found: $end_line\n"; + return $id, $title; +} + +sub _parse_text { + my ($self, $lines, $done) = @_; + while (@$lines) { + if ($lines->[0] =~ m{ $_rx_any_begin $_rx_id $_rx_end }x) { + return '_parse_table'; + } + my $line = shift @$lines; + chomp $line; + push @$done, $line; + } + return undef; +} + +1; diff --git a/lib/System/Introspector/Report/Publish/MediaWiki/Producer.pm b/lib/System/Introspector/Report/Publish/MediaWiki/Producer.pm new file mode 100644 index 0000000..fa43a43 --- /dev/null +++ b/lib/System/Introspector/Report/Publish/MediaWiki/Producer.pm @@ -0,0 +1,119 @@ +package System::Introspector::Report::Publish::MediaWiki::Producer; +use Moo; +use HTML::Zoom; + +sub render { + my ($self, $stream) = @_; + return join "\n", map { + ref($_) ? $self->_render_table($_) : $_; + } @$stream; +} + +sub _wrap { + my ($self, $type, $id, $body) = @_; + chomp $body; + return join "\n", + sprintf('', uc($type), $id), + $body, + sprintf('', uc($type), $id), +} + +sub _render_table { + my ($self, $report) = @_; + my $id = $report->{id}; + my $str_id = ref($id) ? join(':', @$id) : $id; + my $markup = $self->_load_markup; + $markup = $self->_apply_identity($markup, $report); + $markup = $self->_apply_table_head($markup, $report); + $markup = $self->_apply_table_body($markup, $report); + my $description = $report->{description} || ['']; + return join "\n", + $self->_wrap('title', $str_id, $self->_render_title($report)), + @$description, + $self->_wrap('table', $str_id, $markup->to_html), + ''; +} + +sub _render_title { + my ($self, $report) = @_; + return sprintf '== %s ==', $report->{title}; +} + +sub _apply_identity { + my ($self, $markup, $report) = @_; + my $id = $report->{id}; + return $markup + ->select('table') + ->set_attribute(id => 'si-id-' . join '__', + ref($id) ? @$id : $id, + ); +} + +sub _apply_table_body { + my ($self, $markup, $report) = @_; + my $index = 0; + my $rows = $report->{rows}; + my @cols = map $_->{key}, @{$report->{columns}}; + return $markup->repeat('.data-row', sub { + return HTML::Zoom::CodeStream->new({ + code => sub { + return if $index > $#$rows; + my $row = $rows->[$index++]; + return sub { + $_->repeat('td', [ map { + my $col_idx = $_; + my $col = $cols[$_]; + my $value = $row->{$col}; + sub { + $_->apply_if($col !~ m{^__}, sub { + $_->add_to_attribute('td', class => "si-column-$col"); + }) + ->replace_content('td', defined($value) ? $value : '') + ->apply_if($col_idx != $#cols, sub { + $_->add_after('td', "\n "); + }); + }; + } 0 .. $#cols ]); + }; + }, + }); + }); +} + +sub _apply_table_head { + my ($self, $markup, $report) = @_; + my @cols = @{$report->{columns}}; + return $markup + ->repeat('th', [map { + my $col_idx = $_; + my $col = $cols[$_]; + sub { + $_->apply_if($col->{key} !~ m{^__}, sub { + $_->add_to_attribute('th', class => 'si-colhead-' . $col->{key}); + }) + ->replace_content('th', $col->{label}) + ->apply_if($col_idx != $#cols, sub { + $_->add_after('th', "\n "); + }); + }; + } 0 .. $#cols ]) +} + +sub _load_markup { + return HTML::Zoom->from_html(do { + local $/; + scalar ; + }); +} + +1; + +__DATA__ + + + + + + + +
Header
Value
diff --git a/lib/System/Introspector/Report/Publish/StdOut.pm b/lib/System/Introspector/Report/Publish/StdOut.pm new file mode 100644 index 0000000..b9c09da --- /dev/null +++ b/lib/System/Introspector/Report/Publish/StdOut.pm @@ -0,0 +1,28 @@ +package System::Introspector::Report::Publish::StdOut; +use Moo; +use JSON::PP; + +has report => (is => 'ro'); + +my $_json = JSON::PP->new->utf8->pretty; + +sub publish { + my ($self, $reports) = @_; + my $match = $self->report; + return unless defined $match; + $match = $self->_prepare_matcher_from($match); + for my $idx (0 .. $#$reports) { + my $report = $reports->[$idx]; + next unless $self->_match_id($report, $match); + print $_json->encode($report), "\n"; + print "---\n" + if $idx < $#$reports; + } + return 1; +} + +with $_ for qw( + System::Introspector::Report::Publish::API +); + +1; diff --git a/lib/System/Introspector/Report/Source.pm b/lib/System/Introspector/Report/Source.pm new file mode 100644 index 0000000..e71d075 --- /dev/null +++ b/lib/System/Introspector/Report/Source.pm @@ -0,0 +1,73 @@ +package System::Introspector::Report::Source; +use Moo; +use JSON::PP; +use IO::All; +use Module::Runtime qw( use_module ); + +my $_json = JSON::PP->new->utf8->relaxed->allow_nonref; + +has root => (is => 'ro', required => 1); + +sub new_from_root { + my ($class, $root, @args) = @_; + if ($root =~ m{^(.+):(.+)$}) { + my ($remote, $remote_root) = ($1, $2); + require Object::Remote; + return $class->new::on($remote, @args, root => $remote_root); + } + else { + return $class->new(@args, root => $root); + } +} + +sub generate { + my ($self, @types) = @_; + my @generators = map { $self->_make_generator($_) } @types; + my $required = +{ map { ($_ => 1) } map $_->required_data, @generators }; + my @dirs = $self->_find_source_dirs; + for my $dir_spec (@dirs) { + my ($id, $path) = @$dir_spec; + my $data = $self->_load_dataset($path, $required) + or next; + $_->collect_from($id, $data) + for @generators; + } + return map { ($_->render_reports) } @generators; +} + +sub _load_dataset { + my ($self, $path, $required) = @_; + my $root = $self->root; + return +{ + map { + ($_->[0], $_json->decode(scalar $_->[1]->slurp) || {}); + } grep { + $required->{$_->[0]}; + } map { + my $key = $_; + $key =~ s!^\Q$root\E/*(?:[^/]+/+){2}!!; + $key =~ s{\.json$}{}; + [$key, $_]; + } grep { + not(m{^\.}) and m{\.json$}; + } $path->deep->all_files + }; +} + +sub _find_source_dirs { + my ($self) = @_; + my $root = $self->root; + return map { + (my $id = $_) =~ s{^\Q$root\E/*}{}; + [$id, $_]; + } io($root)->all_dirs; +} + +sub _make_generator { + my ($self, $spec) = @_; + my ($type, $args) = @$spec; + return use_module("System::Introspector::Report::Builder::$type") + ->new($args || {}); +} + +1; diff --git a/maint/Makefile.PL.include b/maint/Makefile.PL.include new file mode 100644 index 0000000..05af156 --- /dev/null +++ b/maint/Makefile.PL.include @@ -0,0 +1,8 @@ +BEGIN { + -e 'Distar' + or system("git clone git://git.shadowcat.co.uk/p5sagit/Distar.git") +} +use lib 'Distar/lib'; +use Distar; + +author 'Robert Sedlacek '; diff --git a/t/publish_mediawiki_parser.t b/t/publish_mediawiki_parser.t new file mode 100644 index 0000000..e02c6b2 --- /dev/null +++ b/t/publish_mediawiki_parser.t @@ -0,0 +1,84 @@ +use strictures 1; +use Test::More; + +use aliased 'System::Introspector::Report::Publish::MediaWiki::Parser'; + +my $parser = Parser->new; +my $stream = $parser->parse(<<'EOR'); + += Heading = + + +== Foo == + + +User description + + + + + + + + + + + + + + + + + + + +
BarBaz + Qux +
231421171
232422172173
+ + +End Text + +EOR + +is shift(@$stream), '', 'empty line'; +is shift(@$stream), '= Heading =', 'heading'; +is shift(@$stream), '', 'empty line'; + +do { + my $report = shift @$stream; + is ref($report), 'HASH', 'report structure'; + + is_deeply $report->{id}, ['foo'], 'report id'; + is $report->{title}, 'Foo', 'report title'; + + is $report->{columns}[0]{key}, 'bar', 'first column key'; + is $report->{columns}[0]{label}, 'Bar', 'first column label'; + ok not($report->{columns}[0]{user}), 'first column not user supplied'; + + is $report->{columns}[1]{key}, 'baz', 'second column key'; + is $report->{columns}[1]{label}, 'Baz', 'second column label'; + ok not($report->{columns}[1]{user}), 'second column not user supplied'; + + is $report->{columns}[2]{key}, '__usercol_0', 'first user supplied column key'; + like $report->{columns}[2]{label}, qr{^\s+Qux\s+$}, 'first user supplied column label'; + ok $report->{columns}[2]{user}, 'third column is user supplied'; + + is $report->{columns}[3]{key}, '__usercol_orph_0', 'second user supplied column key'; + is $report->{columns}[3]{label}, undef, 'second user supplied column label is empty'; + ok $report->{columns}[3]{user}, 'fourth column is user supplied'; + + is_deeply $report->{rows}[0], { + bar => 231, baz => 421, __usercol_0 => 171, + }, 'first row'; + is_deeply $report->{rows}[1], { + bar => 232, baz => 422, __usercol_0 => 172, __usercol_orph_0 => 173, + }, 'second row'; +}; + +is shift(@$stream), '', 'empty line'; +is shift(@$stream), 'End Text', 'end text'; +is shift(@$stream), '', 'empty line' + while @$stream; + +done_testing;