File Coverage

blib/lib/Metabase/Report.pm
Criterion Covered Total %
statement 101 103 98.0
branch 25 34 73.5
condition 5 10 50.0
subroutine 17 18 94.4
pod 12 12 100.0
total 160 177 90.4


line stmt bran cond sub pod time code
1 4     4   2844 use 5.006;
  4         10  
2 4     4   13 use strict;
  4         5  
  4         65  
3 4     4   11 use warnings;
  4         4  
  4         160  
4              
5             package Metabase::Report;
6              
7             our $VERSION = '0.025';
8              
9 4     4   12 use Carp ();
  4         4  
  4         53  
10 4     4   1185 use JSON::MaybeXS ();
  4         11607  
  4         67  
11              
12 4     4   1636 use Metabase::Fact;
  4         8  
  4         3155  
13             our @ISA = qw/Metabase::Fact/;
14              
15             #--------------------------------------------------------------------------#
16             # abstract methods -- fatal
17             #--------------------------------------------------------------------------#
18              
19             sub report_spec {
20 0     0 1 0 my $self = shift;
21 0         0 Carp::confess "report_spec method not implemented by " . ref $self;
22             }
23              
24             sub set_creator {
25 4     4 1 281 my ( $self, $uri ) = @_;
26              
27 4         14 $self->SUPER::set_creator($uri);
28              
29 4         12 for my $fact ( $self->facts ) {
30 5 100       10 $fact->set_creator($uri)
31             unless $fact->creator;
32             }
33             }
34              
35             #--------------------------------------------------------------------------#
36             # alternate constructor methods
37             #--------------------------------------------------------------------------#
38              
39             # adapted from Fact::new() -- must keep in sync
40             # content field is optional -- should other fields be optional at this
41             # stage? Maybe we shouldn't let any fields be optional
42              
43             # XXX should probably refactor arg_spec for Fact->new so we can reuse it
44             # and just make the content one optional. -- dagolden, 2009-03-31
45              
46             sub open {
47 5     5 1 741 my ( $class, @args ) = @_;
48              
49 5         24 my $args = $class->__validate_args(
50             \@args,
51             {
52             resource => 1,
53             # still optional so we can manipulate anon facts -- dagolden, 2009-05-12
54             creator => 0,
55             # helpful for constructing facts with non-random guids
56             guid => 0,
57             }
58             );
59              
60 5   50     24 $args->{content} ||= [];
61              
62             # create and check
63 5         15 my $self = $class->_init_guts($args);
64              
65 5         15 return $self;
66             }
67              
68             sub add {
69 7     7 1 2958 my ( $self, @args ) = @_;
70 7 50       15 Carp::confess("report is already closed") if $self->{__closed};
71              
72 7         7 my ( $fact, $fact_class, $content );
73              
74 7 100 66     29 if ( @args == 1 && $args[0]->isa('Metabase::Fact') ) {
75 1         1 $fact = $args[0];
76             }
77             else {
78 6         6 ( $fact_class, $content ) = @args;
79 6         28 $fact = $fact_class->new(
80             resource => $self->resource->resource,
81             content => $content,
82             );
83             }
84              
85 7 100       21 $fact->set_creator( $self->creator->resource ) if $self->creator;
86              
87 7         6 push @{ $self->{content} }, $fact;
  7         12  
88 7         15 return $self;
89             }
90              
91             # close just validates -- otherwise unnecessary
92             sub close {
93 5     5 1 1265 my ($self) = @_;
94 5         7 my $class = ref $self;
95              
96 5         5 my $ok = eval { $self->validate_content; 1 };
  5         12  
  4         7  
97 5 100       9 unless ($ok) {
98 1   50     3 my $error = $@ || '(unknown error)';
99 1         139 Carp::confess("$class object content invalid: $error");
100             }
101              
102 4         9 $self->{__closed} = 1;
103              
104 4         8 return $self;
105             }
106              
107             # accessor for facts -- this must work regardless of __closed so
108             # that facts can be added using content_meta of facts already added
109             sub facts {
110 6     6 1 339 my ($self) = @_;
111 6         5 return @{ $self->content };
  6         14  
112             }
113              
114             #--------------------------------------------------------------------------#
115             # implement required abstract Fact methods
116             #--------------------------------------------------------------------------#
117              
118             sub from_struct {
119 3     3 1 179835 my ( $class, $struct ) = @_;
120 3         31 my $self = $class->SUPER::from_struct($struct);
121 3         7 $self->{__closed} = 1;
122 3         9 return $self;
123             }
124              
125             sub content_as_bytes {
126 2     2 1 4 my $self = shift;
127              
128 2 50       6 Carp::confess("can't serialize an open report") unless $self->{__closed};
129              
130 2         2 my $content = [ map { $_->as_struct } @{ $self->content } ];
  3         26  
  2         6  
131 2         8 my $encoded = eval { JSON::MaybeXS->new(ascii => 1)->encode($content) };
  2         12  
132 2 50       85 Carp::confess $@ if $@;
133 2         41 return $encoded;
134             }
135              
136             sub content_from_bytes {
137 3     3 1 5 my ( $self, $string ) = @_;
138 3 50       11 $string = $$string if ref $string;
139              
140 3         20 my $fact_structs = JSON::MaybeXS->new(ascii => 1)->decode($string);
141              
142 3         126 my @facts;
143 3         10 for my $struct (@$fact_structs) {
144 5         17 my $class = $self->class_from_type( $struct->{metadata}{core}{type} );
145 5 50       9 my $fact = eval { $class->from_struct($struct) }
  5         42  
146             or Carp::confess "Unable to create a '$class' object: $@";
147 5         12 push @facts, $fact;
148             }
149              
150 3         21 return \@facts;
151             }
152              
153             # XXX what if spec is '0' (not '0+')? -- dagolden, 2009-03-30
154             sub validate_content {
155 12     12 1 14 my ($self) = @_;
156              
157 12         48 my $spec = $self->report_spec;
158 12         64 my $content = $self->content;
159              
160 12 50       31 die ref $self . " content must be an array reference of Fact object"
161             unless ref $content eq 'ARRAY';
162              
163 12         14 my @fact_matched;
164             # check that each spec matches
165 12         23 for my $k ( keys %$spec ) {
166 15         60 $spec->{$k} =~ m{^(\d+)(\+)?$};
167 15 50       40 my $minimum = defined $1 ? $1 : 0;
168 15 100       28 my $exact = defined $2 ? 0 : 1; # exact unless "+"
169             # mark facts that match a spec
170 15         15 my $found = 0;
171 15         32 for my $i ( 0 .. @$content - 1 ) {
172 26 100       84 if ( $content->[$i]->isa($k) ) {
173 20         15 $found++;
174 20         28 $fact_matched[$i] = 1;
175             }
176             }
177              
178 15 100       23 if ($exact) {
179 13 100       63 die "expected $minimum of $k, but found $found\n"
180             if $found != $minimum;
181             }
182             else {
183 2 50       11 die "expected at least $minimum of $k, but found $found\n"
184             if $found < $minimum;
185             }
186             }
187              
188             # any facts that didn't match anything?
189 8         13 my $unmatched = grep { !$_ } @fact_matched;
  13         21  
190 8 50       17 die "$unmatched fact(s) not in the spec\n"
191             if $unmatched;
192              
193 8         23 return;
194             }
195              
196             #--------------------------------------------------------------------------#
197             # class methods
198             #--------------------------------------------------------------------------#
199              
200             sub fact_classes {
201 1     1 1 1 my ($self) = @_;
202 1   33     6 my $class = ref $self || $self;
203 1         1 return keys %{ $self->report_spec };
  1         2  
204             }
205              
206             sub load_fact_classes {
207 1     1 1 2 my ($self) = @_;
208 1         3 $self->_load_fact_class($_) for $self->fact_classes;
209 1         2 return;
210             }
211              
212             1;
213              
214             # ABSTRACT: a base class for collections of Metabase facts
215              
216             __END__