File Coverage

blib/lib/EntityModel/Class.pm
Criterion Covered Total %
statement 160 202 79.2
branch 27 62 43.5
condition 9 30 30.0
subroutine 32 41 78.0
pod 12 12 100.0
total 240 347 69.1


line stmt bran cond sub pod time code
1             package EntityModel::Class;
2             # ABSTRACT: Helper module for generating class definitions
3 1     1   28800 use strict;
  1         3  
  1         46  
4 1     1   5 use warnings;
  1         2  
  1         27  
5              
6 1     1   5 use feature ();
  1         3  
  1         23  
7              
8 1     1   1471 use IO::Handle;
  1         8300  
  1         93  
9              
10             our $VERSION = '0.016';
11              
12             =head1 NAME
13              
14             EntityModel::Class - define class definition
15              
16             =head1 VERSION
17              
18             Version 0.016
19              
20             =head1 SYNOPSIS
21              
22             package Thing;
23             use EntityModel::Class {
24             name => 'string',
25             items => { type => 'array', subclass => 'string' }
26             };
27              
28             package main;
29             my $thing = Thing->new;
30             $thing->name('A thing');
31             $thing->items->push('an entry');
32             $thing->items->push('another entry');
33             print "Have " . $thing->items->count . " items\n";
34              
35             =head1 DESCRIPTION
36              
37             Applies a class definition to a package. Automatically includes strict, warnings, error handling and other
38             standard features without needing to copy and paste boilerplate code.
39              
40             =head1 USAGE
41              
42             NOTE: This is mainly intended for use with L only, please consider L or similar for other
43             projects.
44              
45             Add EntityModel::Class near the top of the target package:
46              
47             package Test;
48             use EntityModel::Class { };
49              
50             The hashref parameter contains the class definition. Each key is the name of an attribute for the class,
51             with the exception of the following underscore-prefixed keys:
52              
53             =over 4
54              
55             =item * C<_vcs> - version control system information, a plain string containing information about the last
56             changed revision and author for this file.
57              
58             use EntityModel::Class { _vcs => '$Id$' };
59              
60             =item * C<_isa> - set up the parents for this class, similar to C.
61              
62             use EntityModel::Class { _isa => 'DateTime' };
63              
64             =back
65              
66             An attribute definition will typically create an accessor with the same name, and depending on type may
67             also include some additional helper methods.
68              
69             Available types include:
70              
71             =over 4
72              
73             =item * C - simple string scalar value.
74              
75             use EntityModel::Class { name => { type => 'string' } };
76              
77             =item * C - an array of objects, provide the object type as the subclass parameter
78              
79             use EntityModel::Class { childNodes => { type => 'array', subclass => 'Node' } };
80              
81             =item * C - hash of objects of subclass type
82              
83             use EntityModel::Class { authorMap => { type => 'hash', subclass => 'Author' } };
84              
85             =back
86              
87             If the type (or subclass) contains '::', or starts with a Capitalised letter, then it will be treated
88             as a class. All internal type names are lowercase.
89              
90             You can also set the scope on a variable, which defines whether it should be include when exporting or
91             importing:
92              
93             =over 4
94              
95             =item * C - private attributes are not exported or visible in attribute lists
96              
97             use EntityModel::Class { authorMap => { type => 'hash', subclass => 'Author', scope => 'private' } };
98              
99             =item * C (default) - public attributes are included in export/import, and will be visible when listing attributes for the class
100              
101             use EntityModel::Class { name => { type => 'string', scope => 'public' } };
102              
103             =back
104              
105             You can also specify actions to take when a variable is changed, to support internal attribute observers,
106             by specifying the C parameter. This takes a hashref with key corresponding to the attribute to watch,
107             and value indicating the method on that object. For example, C 'path'> would update whenever the
108             C mutator is called on the C attribute. This is intended for use with hash and array containers,
109             rather than classes or simple types.
110              
111             package Compendium;
112             use EntityModel::Class {
113             authors => { type => 'array', subclass => 'Author' },
114             authorByName => { type => 'hash', subclass => 'Author', scope => 'private', watch => { authors => 'name' } }
115             };
116              
117             package main;
118             my $c = Compendium->new;
119             $c->authors->push(Author->new("Adams"));
120             $c->authors->push(Author->new("Brecht"));
121             print $c->authorByName->{'Adams'}->id;
122              
123             =cut
124              
125 1     1   9 use Scalar::Util qw(refaddr);
  1         3  
  1         115  
126 1     1   1234 use Check::UnitCheck;
  1         4211  
  1         5  
127 1     1   5163 use Module::Load;
  1         3819  
  1         11  
128 1     1   4282 use overload;
  1         1690  
  1         7  
129              
130 1     1   1075 use EntityModel::Log ':all';
  1         55308  
  1         562  
131 1     1   1301 use EntityModel::Array;
  1         3  
  1         98  
132 1     1   710 use EntityModel::Hash;
  1         2  
  1         21  
133 1     1   553 use EntityModel::Error;
  1         3  
  1         28  
134 1     1   551 use EntityModel::Class::Accessor;
  1         3  
  1         35  
135 1     1   723 use EntityModel::Class::Accessor::Array;
  1         4  
  1         29  
136 1     1   808 use EntityModel::Class::Accessor::Hash;
  1         2  
  1         857  
137              
138             my %classInfo;
139              
140             my %CLASS_DEFAULTS;
141              
142             =head2 import
143              
144             Apply supplied attributes, and load in the following modules:
145              
146             =over 4
147              
148             =item use strict;
149              
150             =item use warnings;
151              
152             =item use feature;
153              
154             =item use 5.010;
155              
156             =back
157              
158             =cut
159              
160             sub import {
161 3     3   486 my $class = __PACKAGE__;
162 3         6 my $called_on = shift;
163 3         7 my $pkg = caller(0);
164              
165 3 100 66     24 my $info = (ref($_[0]) && ref($_[0]) eq 'HASH') ? $_[0] : { @_ }; # support list of args or hashref
166              
167             # Expand 'string' to { type => 'string' }
168 3 50       12 $_ = { type => $_ } foreach grep { !ref($_) && /^[a-z]/i } values %$info;
  7         28  
169              
170             # Bail out early if we already have inheritance or have recorded this entry in the master list
171 3 50 33     56 return if $classInfo{$pkg} || $pkg->isa('EntityModel::BaseClass');
172              
173             # Basic setup, including strict and other pragmas
174 3         10 $class->setup($pkg);
175 3         9 $class->apply_inheritance($pkg, $info);
176 3         15 $class->load_dependencies($pkg, $info);
177 3         10 $class->apply_logging($pkg, $info);
178 3         8 $class->apply_version($pkg, $info);
179 3         7 $class->apply_attributes($pkg, $info);
180 3         9 $class->record_class($pkg, $info);
181 3         1576 1;
182             }
183              
184             =head2 record_class
185              
186             Add an entry for this class in the central class info hash.
187              
188             =cut
189              
190             sub record_class {
191 3     3 1 6 my ($class, $pkg, $info) = @_;
192 3         7 my @attribs = grep { !/^_/ } keys %$info;
  10         26  
193 1     1   8 { no strict 'refs'; *{$pkg . '::ATTRIBS'} = sub () { @attribs }; }
  1     0   3  
  1         200  
  3         5  
  3         11  
  3         24  
  0         0  
194 3         8 $classInfo{$pkg} = $info;
195             }
196              
197             =head2 apply_inheritance
198              
199             Set up inheritance as required for this class.
200              
201             =cut
202              
203             sub apply_inheritance {
204 3     3 1 6 my ($class, $pkg, $info) = @_;
205             # Inheritance
206 3   50     5 my @inheritFrom = @{ $info->{_isa} // [] };
  3         17  
207 3         6 push @inheritFrom, 'EntityModel::BaseClass';
208             # TODO we want to skip loading if the module has already been loaded or defined
209             # earlier, but there is probably a cleaner way to do this?
210 3         29 Module::Load::load($_) for grep !$pkg->isa($_), @inheritFrom;
211 1     1   5 { no strict 'refs'; push @{$pkg . '::ISA'}, @inheritFrom; }
  1         2  
  1         14641  
  3         124  
  3         4  
  3         41  
212 3         12 delete $info->{_isa};
213             }
214              
215             =head2 load_dependencies
216              
217             Load all modules required for classes
218              
219             =cut
220              
221             sub load_dependencies {
222 3     3 1 7 my ($class, $pkg, $info) = @_;
223 3   33     8 my @attribs = grep { !/^_/ && !/~~/ } keys %$info;
  7         40  
224 3 50 66     6 my @classList = grep { $_ && /:/ } map { $info->{$_}->{subclass} // $info->{$_}->{type} } grep { !$info->{$_}->{defer} } @attribs;
  7         53  
  7         33  
  7         16  
225             CLASS:
226 3         8 foreach my $c (@classList) {
227 1         2 my $file = $c;
228 1         8 $file =~ s{::|'}{/}g;
229 1         3 $file .= '.pm';
230 1 50       4 if($INC{$file}) {
231 0         0 logDebug("Already in INC: $file");
232 0         0 next CLASS;
233             }
234             eval {
235 1         11 Module::Load::load($c);
236 0         0 1
237 1 50       2 } or do {
238 1 50       727 logError($@) unless $@ =~ /^Can't locate /;
239             };
240             }
241             }
242              
243             =head2 apply_logging
244              
245             =cut
246              
247             sub apply_logging {
248 3     3 1 5 my ($class, $pkg, $info) = @_;
249             # Support logging methods by default, unless explicitly disabled
250 3 50 33     344 EntityModel::Log->export_to_level(2, $pkg, ':all')
251             if $info->{_log} || !exists $info->{_log};
252             # Apply any log-level overrides first at package level
253 3 50       14 if(exists $info->{_logMask}->{default}) {
254 0         0 $EntityModel::Log::LogMask{$pkg}->{level} = EntityModel::Log::levelFromString($info->{_logMask}->{default});
255             }
256              
257             # ... then at method level
258 3 50       12 if(exists $info->{_logMask}->{methods}) {
259 0         0 my %meth = %{$info->{_logMask}->{methods}};
  0         0  
260 0         0 foreach my $k (keys %meth) {
261 0         0 $EntityModel::Log::LogMask{$pkg . '::' . $k}->{level} = EntityModel::Log::levelFromString($meth{$k});
262             }
263             }
264             }
265              
266             =head2 apply_version
267              
268             Record the VCS revision information from C<_vcs> attribute.
269              
270             =cut
271              
272             sub apply_version {
273 3     3 1 6 my ($class, $pkg, $info) = @_;
274             # Typically version is provided as an SVN Rev property wrapped in $ signs.
275 3 50       11 if(exists $info->{_vcs}) {
276 0         0 my $v = delete $info->{_vcs};
277 0         0 $class->vcs($pkg, $v);
278             }
279             }
280              
281             =head2 apply_attributes
282              
283             =cut
284              
285             sub apply_attributes {
286 3     3 1 4 my ($class, $pkg, $info) = @_;
287 3         3 my %methodList;
288 3         7 my @attribs = grep { !/^_/ } keys %$info;
  10         25  
289              
290             # Smart match support - 1 to use a default refaddr-based system, coderef for anything else
291 3 50       9 if(my $match = delete $info->{'~~'}) {
292 0     0   0 $class->add_method($pkg, '()', sub () { });
  0         0  
293 0 0       0 if(ref $match) {
294 0         0 $class->add_method($pkg, '(~~', $match);
295             } else {
296             $class->add_method($pkg, '(~~', sub {
297 0     0   0 my ($self, $target) = @_;
298 0 0 0     0 return 0 unless defined($self) && defined($target);
299 0 0 0     0 return 0 unless ref($self) && ref($target);
300 0 0       0 return 0 unless $self->isa($pkg);
301 0 0       0 return 0 unless $target->isa($pkg);
302 0 0       0 return 0 unless refaddr($self) == refaddr($target);
303 0         0 return 1;
304 0         0 });
305             }
306              
307             # Update overload cache if we previously invalidated (for smartmatch or other operators),
308             # possibly required if calling L at runtime.
309 0         0 bless {}, $pkg;
310             }
311              
312             # Anything else is an accessor, set it up
313 3         6 foreach my $attr (@attribs) {
314 7         13 my $type = $info->{$attr}->{type};
315 7 100       22 if($type eq 'array') {
    100          
316 1         18 %methodList = (%methodList, EntityModel::Class::Accessor::Array->add_to_class($pkg, $attr => $info->{$attr}))
317             } elsif($type eq 'hash') {
318 2         15 %methodList = (%methodList, EntityModel::Class::Accessor::Hash->add_to_class($pkg, $attr => $info->{$attr}))
319             } else {
320 4         23 %methodList = (%methodList, EntityModel::Class::Accessor->add_to_class($pkg, $attr => $info->{$attr}))
321             }
322             }
323              
324 3         6 $CLASS_DEFAULTS{$pkg} = [ grep { exists $info->{$_}->{default} } @attribs ];
  7         18  
325              
326             # Apply watchers after we've defined the fields - each watcher is field => method
327 3         5 foreach my $watcher (grep { exists $info->{$_}->{watch} } @attribs) {
  7         17  
328 1         3 my $w = $info->{$watcher}->{watch};
329 1         2 foreach my $watched (keys %$w) {
330 1         8 $class->add_watcher($pkg, $watcher, $watched, $info->{$watched}, $w->{$watched});
331             }
332             }
333              
334             # Thanks to Check::UnitCheck
335             Check::UnitCheck::unitcheckify(sub {
336             # FIXME Can't call any log functions within UNITCHECK
337 2     2   5 local $::DISABLE_LOG = 1;
338 2         9 my %ml = %methodList;
339 2         15 $class->add_method($pkg, $_, $ml{$_}) foreach keys %ml;
340 2 50   0   40 $class->add_method($pkg, 'import', sub { }) unless $pkg->can('import');
  0         0  
341 3 100       24 }) if %methodList;
342             }
343              
344             =head2 add_method
345              
346             =cut
347              
348             sub add_method {
349 9     9 1 11 my $class = shift;
350 9         16 my ($pkg, $name, $method) = @_;
351 9         15 my $sym = $pkg . '::' . $name;
352 9         30 logDebug("Add method $sym");
353 1 50   1   15 { no strict 'refs'; *$sym = $method unless *$sym{CODE}; }
  1         2  
  1         141  
  9         246  
  9         52  
354 9         1182 return $sym;
355             }
356              
357             =head2 vcs
358              
359             Add a version control system tag to the class.
360              
361             =cut
362              
363             sub vcs {
364 0     0 1 0 my $class = shift;
365 0         0 my $pkg = shift;
366 0         0 my $v = shift;
367              
368             # Define with empty prototype, which should mean we compile to a constant
369 0     0   0 my $versionSub = sub () { $v };
  0         0  
370 0         0 my $sym = $pkg . '::VCS_INFO';
371 1 0   1   7 { no strict 'refs'; *$sym = $versionSub unless *$sym{CODE}; }
  1         2  
  1         607  
  0         0  
  0         0  
372             }
373              
374             =head2 setup
375              
376             Standard module setup - enable strict and warnings, and disable 'import' fallthrough.
377              
378             =cut
379              
380             sub setup {
381 3     3 1 5 my ($class, $pkg) = @_;
382              
383 3         37 strict->import;
384 3         42 warnings->import();
385 3         183 feature->import(':5.10');
386             }
387              
388              
389             =head2 validator
390              
391             Basic validation function.
392              
393             =cut
394              
395             sub validator {
396 0     0 1 0 my $v = shift;
397 0         0 my $allowed = $v->{valid};
398             return defined $allowed
399             ? ref $allowed eq 'CODE'
400 0     0   0 ? $allowed : sub { $_[0] eq $allowed }
401 0 0       0 : undef;
    0          
402             }
403              
404             =head2 _attrib_info
405              
406             Returns attribute information for a given package's attribute.
407              
408             =cut
409              
410             sub _attrib_info {
411 0     0   0 my $class = shift;
412 0         0 my $attr = shift;
413             # return unless ref $self;
414 0   0     0 return $classInfo{ref $class || $class}->{$attr};
415             }
416              
417             =head2 has_defaults
418              
419             Returns any defaults defined for this class.
420              
421             =cut
422              
423             sub has_defaults {
424 1     1 1 2 my $class = shift;
425 1   50     1 return @{ $CLASS_DEFAULTS{$class} // [] };
  1         9  
426             }
427              
428             =head2 add_watcher
429              
430             Add watchers as required for all package definitions.
431              
432             Call this after all the class definitions have been loaded.
433              
434             =cut
435              
436             sub add_watcher {
437 1     1 1 2 my $class = shift;
438 1         2 my ($pkg, $obj, $target, $attrDef, $meth) = @_;
439              
440             # The watcher is called with the new value as add|drop => $v
441             my $sub = sub {
442 4     4   5 my $self = shift;
443 4         9 my ($action, $v) = @_;
444 4 50       9 return unless $v;
445 4 50       32 my $k = $meth ? $v->$meth : $v;
446 4         13 logDebug("%s for %s with %s", $action, $k, $v);
447 4 100       124 if($action eq 'add') {
    50          
448 3         13 $self->$obj->set($k, $v);
449             } elsif($action eq 'drop') {
450 1         6 $self->$obj->erase($k);
451             } else {
452 0         0 logError("Don't know %s", $_);
453             }
454 4         27 return $self;
455 1         4 };
456              
457 1 50       4 if($attrDef->{type} eq 'array') {
458 1         12 EntityModel::Class::Accessor::Array->add_watcher($pkg, $target, $sub);
459             } else {
460 0   0       die "Unknown type " . ($_ // 'undef');
461             }
462             }
463              
464             1;
465              
466             __END__