File Coverage

blib/lib/Metabase/Report.pm
Criterion Covered Total %
statement 102 104 98.0
branch 25 34 73.5
condition 5 10 50.0
subroutine 17 18 94.4
pod 12 12 100.0
total 161 178 90.4


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