File Coverage

blib/lib/Metabase/Fact.pm
Criterion Covered Total %
statement 141 162 87.0
branch 41 54 75.9
condition 10 27 37.0
subroutine 34 45 75.5
pod 32 32 100.0
total 258 320 80.6


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