File Coverage

blib/lib/Metabase/Fact.pm
Criterion Covered Total %
statement 140 161 86.9
branch 41 54 75.9
condition 10 27 37.0
subroutine 34 45 75.5
pod 32 32 100.0
total 257 319 80.5


line stmt bran cond sub pod time code
1 7     7   90 use 5.006;
  7         14  
2 7     7   20 use strict;
  7         8  
  7         112  
3 7     7   19 use warnings;
  7         7  
  7         258  
4              
5             package Metabase::Fact;
6              
7             our $VERSION = '0.025';
8              
9 7     7   23 use Carp ();
  7         7  
  7         134  
10 7     7   1805 use Data::GUID guid_string => { -as => '_guid' };
  7         54109  
  7         57  
11 7     7   1893 use JSON::MaybeXS ();
  7         7457  
  7         104  
12 7     7   2454 use Metabase::Resource;
  7         48  
  7         11126  
13              
14             #--------------------------------------------------------------------------#
15             # main API methods -- shouldn't be overridden
16             #--------------------------------------------------------------------------#
17              
18             # We originally used Params::Validate, but only for
19             # required/optional/disallowed, and it was Yet Another Prereq for what
20             # needed to be a very small set of libraries. Sadly, we've rolled our
21             # own... -- rjbs, 2009-03-30
22             sub __validate_args {
23 47     47   56 my ( $self, $args, $spec ) = @_;
24             my $hash =
25 47 100 66     239 ( @$args == 1 and ref $args->[0] ) ? { %{ $args->[0] } }
  6 100       21  
26             : ( @$args == 0 ) ? {}
27             : {@$args};
28              
29 47         41 my @errors;
30              
31 47         98 for my $key ( keys %$hash ) {
32             push @errors, qq{unknown argument "$key" when constructing $self}
33 157 50       245 unless exists $spec->{$key};
34             }
35              
36 47         98 for my $key ( grep { $spec->{$_} } keys %$spec ) {
  231         223  
37             push @errors, qq{missing required argument "$key" when constructing $self}
38 139 100       214 unless defined $hash->{$key};
39             }
40              
41 47 100       259 Carp::confess( join qq{\n}, @errors ) if @errors;
42              
43 46         65 return $hash;
44             }
45              
46             my $hex = '[0-9a-f]';
47             my $guid_re = qr(\A$hex{8}-$hex{4}-$hex{4}-$hex{4}-$hex{12}\z)i;
48              
49             sub __validate_guid {
50 45     45   60 my ( $class, $string ) = @_;
51 45 50       294 if ( $string !~ $guid_re ) {
52 0         0 Carp::confess("'$string' is not formatted as a GUID string");
53             }
54 45         108 return lc $string;
55             }
56              
57             sub validate_resource {
58 45     45 1 45 my ( $self, $uri ) = @_;
59             # Metabase::Resource->new dies if invalid
60 45         116 my $obj = Metabase::Resource->new($uri);
61 45 50 33     258 if ( !( ref($obj) && $obj->isa("Metabase::Resource") ) ) {
62 0         0 Carp::confess("Could not validate '$uri' as a Metabase::Resource");
63             }
64 45         74 return $obj;
65             }
66              
67             sub new {
68 31     31 1 9202 my ( $class, @args ) = @_;
69 31         140 my $args = $class->__validate_args(
70             \@args,
71             {
72             content => 1,
73             resource => 1, # where to validate? -- dagolden, 2009-03-31
74             # still optional so we can manipulate anon facts -- dagolden, 2009-05-12
75             creator => 0,
76             # helpful for constructing facts with non-random guids
77             guid => 0,
78             },
79             );
80              
81             # create the object
82 30         104 my $self = $class->_init_guts($args);
83              
84             # validate content
85 30         34 eval { $self->validate_content };
  30         110  
86 30 100       107 if ($@) {
87 5         615 Carp::confess("$class object content invalid: $@");
88             }
89              
90 25         103 return $self;
91             }
92              
93             sub _zulu_datetime {
94 35     35   221 my ( $y, $mo, $d, $h, $mi, $s ) = (gmtime)[ reverse 0 .. 5 ];
95             return
96 35         254 sprintf( "%4d-%02d-%02dT%02d:%02d:%02dZ", 1900 + $y, 1 + $mo, $d, $h, $mi, $s );
97             }
98              
99 47 100   47   84 sub _bool { return $_[0] ? 1 : 0 }
100              
101             # used for both new() and from_struct() -- in the former case
102             # only content, resource, guid and creator could exist; in
103             # the latter case, all fields would exist
104             sub _init_guts {
105 45     45   48 my ( $class, $args ) = @_;
106              
107             # confirm type
108             $args->{type} = $class->type
109 45 100       134 unless defined $args->{type};
110              
111             Carp::confess("illegal type ($args->{type}) for $class")
112 45 50       71 if $args->{type} ne $class->type;
113              
114             # if restoring from_struct, we must cope with older schemas
115             $args->{schema_version} = $class->default_schema_version
116 45 100       142 unless defined $args->{schema_version};
117              
118             $class->upgrade_fact($args)
119 45 50       68 if $args->{schema_version} != $class->default_schema_version;
120              
121             # initialize guid if not provided
122 45 100       66 if ( !defined $args->{guid} ) {
123 32         71 $args->{guid} = lc _guid();
124             }
125              
126             # initialize the object
127 45         1806 my $self = bless {}, $class;
128              
129 45         106 $self->{content} = $args->{content};
130              
131 45         89 my $meta = $self->{metadata} = { core => {} };
132 45         122 $meta->{core}{guid} = $class->__validate_guid( $args->{guid} );
133 45   66     112 $meta->{core}{creation_time} = $args->{creation_time} || _zulu_datetime();
134 45         58 $meta->{core}{update_time} = $meta->{core}{creation_time};
135 45         49 $meta->{core}{schema_version} = $args->{schema_version};
136 45         59 $meta->{core}{type} = $self->type;
137 45 100       106 $meta->{core}{valid} = _bool( defined $args->{valid} ? $args->{valid} : 1 );
138              
139             # validate creator via mutator if given
140 45 100       100 $self->set_creator( $args->{creator} ) if defined $args->{creator};
141              
142             # validate resource field
143 45         122 $meta->{core}{resource} = $self->validate_resource( $args->{resource} );
144              
145 45         64 return $self;
146             }
147              
148             # Content accessor
149 80     80 1 338 sub content { $_[0]->{content} }
150              
151             # Accessors for core metadata
152              
153 0     0 1 0 sub creation_time { $_[0]->{metadata}{core}{creation_time} }
154 8     8 1 622 sub guid { $_[0]->{metadata}{core}{guid} }
155 10     10 1 649 sub resource { $_[0]->{metadata}{core}{resource} }
156 0     0 1 0 sub schema_version { $_[0]->{metadata}{core}{schema_version} }
157              
158             # Creator can be set once after the fact is created
159              
160 31     31 1 677 sub creator { $_[0]->{metadata}{core}{creator} }
161              
162             sub set_creator {
163 13     13 1 1053 my ( $self, $uri ) = @_;
164              
165 13 50       480 Carp::confess("can't set creator; it is already set")
166             if $self->creator;
167              
168             # validate $uri
169 13         43 my $obj = Metabase::Resource->new($uri);
170 13 50       23 unless ( $obj->type eq 'Metabase-Resource-metabase-user' ) {
171 0         0 Carp::confess( "creator must be a Metabase User Profile resource URI of\n"
172             . "the form 'metabase:user:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'" );
173             }
174              
175 13         33 $self->{metadata}{core}{creator} = $obj;
176             }
177              
178             # update_time can always be modified
179              
180 0     0 1 0 sub update_time { $_[0]->{metadata}{core}{update_time} }
181              
182             sub touch {
183 0     0 1 0 my ($self) = @_;
184 0         0 $self->{metadata}{core}{update_time} = _zulu_datetime();
185             }
186              
187             # valid can be modified
188              
189 0     0 1 0 sub valid { $_[0]->{metadata}{core}{valid} }
190              
191             sub set_valid {
192 2     2 1 480 my ( $self, $val ) = @_;
193 2         3 $self->{metadata}{core}{valid} = _bool($val);
194             }
195              
196             # metadata structure accessors
197              
198             sub core_metadata {
199 17     17 1 606 my $self = shift;
200 17         18 return { %{ $self->{metadata}{core} } };
  17         143  
201             }
202              
203             sub core_metadata_types {
204             return {
205 0     0 1 0 creation_time => '//str',
206             creator => '//str',
207             guid => '//str',
208             resource => '//str',
209             schema_version => '//num',
210             type => '//str',
211             update_time => '//str',
212             valid => '//bool',
213             };
214             }
215              
216             sub resource_metadata {
217 0     0 1 0 my $self = shift;
218 0   0     0 $self->{metadata}{resource} ||= $self->resource->metadata;
219 0         0 return { %{ $self->{metadata}{resource} } };
  0         0  
220             }
221              
222             sub resource_metadata_types {
223 0     0 1 0 my $self = shift;
224 0         0 return $self->resource->metadata_types;
225             }
226              
227             # persistence routines
228              
229             # Class might not be in its own file -- check if method can resolve
230             # or else try to load it
231             my $id_re = qr/[_a-z]+/i;
232             my $class_re = qr/^$id_re(?:::$id_re)*$/;
233              
234             sub _load_fact_class {
235 13     13   46 my ( $class, $fact_class ) = @_;
236 13 50       55 unless ( defined $fact_class ) {
237 0         0 Carp::confess "Can't load undef as a module";
238             }
239 13 100       108 unless ( $fact_class =~ $class_re ) {
240 1         95 Carp::confess "'$fact_class' does not look like a class name";
241             }
242 12 100       86 unless ( $fact_class->can('type') ) {
243 2 50       86 eval "require $fact_class; 1" ## no critic
244             or Carp::confess "Could not load fact class $fact_class\: $@";
245             }
246 12         26 return 1;
247             }
248              
249             sub as_struct {
250 15     15 1 1077 my ($self) = @_;
251              
252             # turn Metabase::Resources back into URI strings
253 15         14 my $core = { %{ $self->core_metadata } };
  15         36  
254 15         65 $core->{resource} = $core->{resource}->resource;
255 15 100       94 $core->{creator} = $core->{creator}->resource if exists $core->{creator};
256              
257             return {
258 15         46 content => $self->content_as_bytes,
259             metadata => {
260             # We only provide core metadata here, not resource or content metadata,
261             # because we use as_struct for serialized transmission. The remote that
262             # receives the transmission should reconstruct the metadata for itself,
263             # as it is more likely to have an improved metadata producer. -- rjbs,
264             # 2009-06-24
265             core => $core,
266             }
267             };
268             }
269              
270             sub from_struct {
271 10     10 1 386 my ( $class, $struct ) = @_;
272              
273             # Might be called as Metabase::Fact->from_struct($struct), so we
274             # need to find and load the actual fact class
275 10         47 my $fact_class = $class->class_from_type( $struct->{metadata}{core}{type} );
276 10         42 $class->_load_fact_class($fact_class);
277              
278 10         16 my $metadata = $struct->{metadata};
279 10         8 my $core_meta = $metadata->{core};
280              
281             # transform struct into content and core metadata arguments the way they
282             # would be given to new, then validate these and get an object from
283             # _init_guts
284             my @args = (
285 77         136 ( map { $_ => $core_meta->{$_} } keys %$core_meta ),
286 10         37 content => $fact_class->content_from_bytes( $struct->{content} ),
287             );
288              
289 10         92 my $args = $fact_class->__validate_args(
290             \@args,
291             {
292             # when thawing, all of these must be provided
293             content => 1,
294             creation_time => 1,
295             guid => 1,
296             resource => 1,
297             schema_version => 1,
298             type => 1,
299             valid => 1,
300             # still optional so we can manipulate anon facts -- dagolden, 2009-05-12
301             creator => 0,
302             update_time => 0,
303             },
304             );
305              
306 10         51 my $self = $fact_class->_init_guts($args);
307              
308 10         53 return $self;
309             }
310              
311             sub as_json {
312 1     1 1 8 my ($self) = @_;
313 1         10 return JSON::MaybeXS->new(ascii => 1)->encode( $self->as_struct );
314             }
315              
316             sub from_json {
317 1     1 1 2 my ( $class, $string ) = @_;
318 1 50       3 my $struct = eval { JSON::MaybeXS->new(ascii => 1)->decode($string) }
  1         6  
319             or Carp::confess "Error decoding JSON:\n$@";
320 1         53 return $class->from_struct($struct);
321             }
322              
323             sub save {
324 1     1 1 1425 my ( $self, $filename ) = @_;
325 1         3 my $class = ref($self);
326 1 50       82 open my $fh, ">", $filename
327             or Carp::confess "Error saving $class to '$filename'\: $!";
328 1         3 print {$fh} scalar $self->as_json;
  1         12  
329 1         46 close $fh;
330 1         6 return 1;
331             }
332              
333             sub load {
334 1     1 1 475 my ( $class, $filename ) = @_;
335 1 50       39 open my $fh, "<", $filename
336             or Carp::confess "Error loading fact from '$filename'\: $!";
337 1         3 my $string = do { local $/; <$fh> };
  1         4  
  1         17  
338 1         7 close $fh;
339 1         19 return $class->from_json($string);
340             }
341              
342             #--------------------------------------------------------------------------#
343             # utilities for all facts to do class/type conversions
344             #--------------------------------------------------------------------------#
345              
346             # type_from_class
347             sub type {
348 129     129 1 1117 my $self = shift;
349 129   66     269 my $type = ref $self || $self;
350              
351 129         184 $type =~ s{::}{-}g;
352 129         207 return $type;
353             }
354              
355             # XXX: I'm not really excited about having this in here. -- rjbs, 2009-03-28
356             # XXX: Need it ->type for symmetry. Make it private? -- dagolden, 2009-03-31
357             sub class_from_type {
358 15     15 1 24 my ( undef, $type ) = @_;
359 15 50       29 Carp::confess "can't get class from undef type" unless defined $type;
360 15         53 $type =~ s/-/::/g;
361 15         29 return $type;
362             }
363              
364             #--------------------------------------------------------------------------#
365             # class methods
366             #--------------------------------------------------------------------------#
367              
368             # schema_version recorded in 'version' attribution during ->new
369             # if format of content changes, class module should increment schema version
370             # to check: if ( $obj->version != $class->schema_version ) ...
371              
372             # XXX should this be a fatal abstract? Forcing classes to be
373             # explicit about schema versions? Annoying, but correct -- dagolden, 2009-03-31
374 81     81 1 1146 sub default_schema_version { 1 }
375              
376             #--------------------------------------------------------------------------#
377             # abstract methods -- mostly fatal
378             #--------------------------------------------------------------------------#
379              
380 0     0 1 0 sub content_metadata { return +{} }
381              
382 0     0 1 0 sub content_metadata_types { return +{} }
383              
384             sub upgrade_fact {
385 0     0 1 0 my ($self) = @_;
386 0   0     0 Carp::confess "Detected a schema mismatch, but upgrade_fact not implemented by "
387             . ( ref $self || $self );
388             }
389              
390             sub content_as_bytes {
391 1     1 1 27 my ( $self, $content ) = @_;
392 1   33     85 Carp::confess "content_as_bytes not implemented by " . ( ref $self || $self );
393             }
394              
395             sub content_from_bytes {
396 1     1 1 436 my ( $self, $bytes ) = @_;
397 1   33     80 Carp::confess "content_from_bytes not implemented by " . ( ref $self || $self );
398             }
399              
400             sub validate_content {
401 1     1 1 421 my ( $self, $content ) = @_;
402 1   33     76 Carp::confess "validate_content not implemented by " . ( ref $self || $self );
403             }
404              
405             1;
406              
407             # ABSTRACT: base class for Metabase Facts
408              
409             __END__