File Coverage

blib/lib/EntityModel/Model.pm
Criterion Covered Total %
statement 35 212 16.5
branch 1 20 5.0
condition 0 6 0.0
subroutine 8 39 20.5
pod 17 31 54.8
total 61 308 19.8


line stmt bran cond sub pod time code
1             package EntityModel::Model;
2             {
3             $EntityModel::Model::VERSION = '0.102';
4             }
5             use EntityModel::Class {
6 14         250 _isa => [qw{Mixin::Event::Dispatch}],
7             name => { type => 'string' },
8             handler => { type => 'hash' },
9             entity => { type => 'array', subclass => 'EntityModel::Entity' },
10             # Private mapping for entity name lookup
11             entity_map => { type => 'hash', scope => 'private', watch => {
12             entity => 'name'
13             } },
14 14     14   14296 };
  14         29  
15 14     14   6186 no if $] >= 5.017011, warnings => "experimental::smartmatch";
  14         30  
  14         76  
16              
17             =head1 NAME
18              
19             EntityModel::Model - base class for model definitions
20              
21             =head1 VERSION
22              
23             version 0.102
24              
25             =head1 SYNOPSIS
26              
27             see L.
28              
29             =head1 DESCRIPTION
30              
31             see L.
32              
33              
34              
35             =head1 METHODS
36              
37             =cut
38              
39 14     14   873 use List::Util qw(first);
  14         35  
  14         48355  
40              
41             =pod
42              
43             =cut
44              
45             sub table {
46 0     0 0 0 my $self = shift;
47 0         0 my $name = shift;
48 0     0   0 my ($e) = first { $_->name eq $name } $self->entity->list;
  0         0  
49 0         0 return $e;
50             }
51              
52             sub entity_by_name {
53 6     6 0 28948 my $self = shift;
54 6         14 my $name = shift;
55 6         29 return $self->entity_map->{$name};
56             }
57              
58             =head2 pending_entities
59              
60             Returns a list of all pending entities for this model.
61              
62             These will be applied on L, or cleared on L.
63              
64             =cut
65              
66             sub pending_entities {
67 0     0 1 0 my $self = shift;
68 0   0     0 my $type = shift || [ 'update', 'add', 'remove' ];
69 0 0       0 $type = [ $type ] unless ref $type eq 'ARRAY';
70 0         0 my @lst;
71 0         0 foreach my $t (@$type) {
72 0         0 push @lst, values %{$self->{pending}->{$t}};
  0         0  
73             }
74 0         0 return @lst;
75             }
76              
77             =head2 rollback
78              
79             Revert any pending changes for this model.
80              
81             =cut
82              
83             sub rollback {
84 0     0 1 0 my $self = shift;
85 0         0 undef $self->{pending};
86 0         0 return $self;
87             }
88              
89             =head2 apply_fields
90              
91             Apply the field definitions for an entity.
92              
93             =cut
94              
95             sub apply_fields {
96 0     0 1 0 my $self = shift;
97 0         0 my $entity = shift;
98             local $SIG{__DIE__} = sub {
99 0     0   0 logStack("FAILED HERE ===== $_[0]");
100 0         0 die @_;
101 0         0 };
102 0         0 my @fieldList = $self->read_fields($entity);
103 0         0 foreach my $details (@fieldList) {
104 0         0 my $field = $entity->new_field($details->{name});
105 0         0 foreach (sort keys %$details) {
106 0         0 $field->$_($details->{$_});
107             }
108 0         0 $entity->field->push($field);
109             }
110 0         0 return $self;
111             }
112              
113             =head2 load_model
114              
115             Populate the entity model from any information we can get from
116             the data source.
117              
118             =cut
119              
120             sub load_model {
121 0     0 1 0 my $self = shift;
122 0         0 $self->entity->clear;
123              
124 0         0 logDebug("Reading tables");
125 0         0 my @tableList = $self->read_tables;
126 0         0 logDebug("Import " . scalar(@tableList) . " tables");
127 0         0 foreach (@tableList) {
128 0         0 my $tbl = $self->add_table($_);
129 0         0 $self->apply_fields($tbl);
130             }
131 0         0 logDebug("Import complete");
132 0         0 return $self;
133             }
134              
135             =head2 update_from
136              
137             Update this entity model so that it matches the given model.
138              
139             =cut
140              
141             sub update_from {
142 0     0 1 0 my ($self, $src) = @_;
143 0         0 my %srcNames = map { $_->name => $_ } $src->entity->list;
  0         0  
144 0         0 foreach my $e ($self->entity->list) {
145 0 0       0 if(exists $srcNames{$e->name}) {
146 0         0 my $es = delete $srcNames{$e->name};
147             # Mark this for update unless it's the same as the one we have already
148 0 0       0 if($es->matches($e)) {
149 0         0 logDebug("Should keep [%s]", $e->name);
150             } else {
151 0         0 logDebug("Should update [%s]", $e->name);
152 0         0 $self->{pending}->{update}->{$es->name} = $es;
153             }
154             } else {
155 0         0 logDebug("Should remove [%s]", $e->name);
156 0         0 $self->{pending}->{remove}->{$e->name} = $e;
157             }
158             }
159 0         0 foreach my $name (sort keys %srcNames) {
160 0         0 logDebug("Should add [%s]", $name);
161 0         0 $self->{pending}->{add}->{$name} = $srcNames{$name};
162             }
163 0         0 return $self;
164             }
165              
166             =head2 matches
167              
168             Returns true if this entity model has identical content to another given model.
169              
170             =cut
171              
172             sub matches {
173 0     0 1 0 my ($self, $dst) = @_;
174 0         0 my @srcList = sort { $a->name cmp $b->name } $self->entity->list;
  0         0  
175 0         0 my @dstList = sort { $a->name cmp $b->name } $dst->entity->list;
  0         0  
176 0         0 logDebug("Check match: src " . scalar(@srcList) . ", dest " . scalar(@dstList));
177 0         0 return \@srcList ~~ \@dstList;
178             }
179              
180             =head2 read_tables
181              
182             Virtual method for reading table definitions.
183              
184             =cut
185              
186 0     0 1 0 sub read_tables { }
187              
188             sub dump {
189 0     0 1 0 my $self = shift;
190             my $out = shift || sub {
191 0     0   0 print join(' ', @_) . "\n";
192 0   0     0 };
193              
194 0         0 $out->('Entity list for ' . $self->name);
195 0         0 foreach (sort { $a->name cmp $b->name } $self->entity) {
  0         0  
196 0         0 $out->($_->name);
197 0         0 $_->dump($out);
198             }
199 0         0 $self;
200             }
201              
202 0     0 0 0 sub apply { }
203              
204             sub pending {
205 0     0 0 0 my $self = shift;
206 0 0       0 return @_ ? ($self->{pending} = shift) : $self->{pending};
207             }
208              
209             sub hasPending {
210 0     0 0 0 my $self = shift;
211 0 0       0 return $self->pending ? 1 : 0;
212             }
213              
214             =head2 new_entity
215              
216             Helper method to create a new entity.
217              
218             =cut
219              
220             sub new_entity {
221 0     0 1 0 my $self = shift;
222 0         0 my $name = shift;
223              
224 0         0 my $entity = EntityModel::Entity->new($name);
225 0         0 return $entity;
226             }
227              
228             =head2 create_entity
229              
230             Helper method to create a new entity and add it to our list.
231              
232             Takes the following parameters:
233              
234             =over 4
235              
236             =item *
237              
238             =back
239              
240             Returns
241              
242             =cut
243              
244             sub create_entity {
245 0     0 1 0 my $self = shift;
246 0         0 $self->add_entity(EntityModel::Entity->new(@_))
247             }
248              
249             =head2 add_table
250              
251             Generate an appropriate L for the given table name.
252              
253             =cut
254              
255             sub add_table {
256 0     0 1 0 my ($self, $tbl) = @_;
257 0         0 my $entity = $self->new_entity($tbl->{name});
258 0         0 my @fieldList = $self->read_fields($entity);
259 0         0 my @primaryList = $self->read_primary($entity);
260 0         0 logDebug("Import " . scalar(@fieldList) . " fields for " . $tbl->{name});
261 0         0 my %fieldName;
262 0         0 foreach (@fieldList) {
263 0         0 my $name = delete $_->{name};
264             # logDebug("Set $name");
265 0         0 $fieldName{ $name } = $entity->new_field($name, $_);
266             }
267 0 0       0 $entity->primary(join('-', @primaryList)) if @primaryList;
268             # logDebug("Primary for $_ is " . $_->name) foreach $entity->list_primary;
269 0         0 $self->entity->push($entity);
270 0         0 return $entity;
271             }
272              
273             =head2 add_entity
274              
275             Add an L to this model.
276              
277             =cut
278              
279             sub add_entity {
280 14     14 1 480 my $self = shift;
281 14         25 my $entity = shift;
282 14         73 $self->entity->push($entity);
283 14         1059 return $self;
284             }
285              
286             sub resolve_entity_dependencies {
287 6     6 0 13 my $self = shift;
288 6         130 foreach my $entity ($self->entity->list) {
289 13         141 my @deps = grep { $_->refer } $entity->field->list;
  34         235  
290 13         277 $_->refer->entity($self->entity_by_name($_->refer->table)) foreach @deps;
291             }
292 6         33 return $self;
293             }
294              
295             =head2 commit
296              
297             Apply the actions, starting with the longest names first for removal, and shortest
298             first for update and create.
299              
300             =cut
301              
302             sub commit {
303 0     0 1 0 my $self = shift;
304              
305 0         0 logDebug("Commit $self");
306 0         0 $self->commit_pending_remove;
307 0         0 $self->commit_pending_add;
308 0         0 $self->commit_pending_update;
309 0         0 delete $self->{pending};
310             # Don't really want to commit here, since having everything in an uncommitted transaction can be useful.
311             # $self->postCommit if $self->can('postCommit');
312 0         0 return $self->load_model;
313             }
314              
315             =head2 commit_pending_update
316              
317             =cut
318              
319             sub commit_pending_update {
320 0     0 1 0 my $self = shift;
321 0         0 logInfo("Update " . join(',', map { $_->name } $self->pending_entities('update')));
  0         0  
322 0         0 $self->update_table($_) foreach $self->pending_entities('update');
323 0         0 return $self;
324             }
325              
326             =head2 commit_pending_remove
327              
328             =cut
329              
330             sub commit_pending_remove {
331 0     0 1 0 my $self = shift;
332 0         0 logInfo("Remove " . join(',', map { $_->name } $self->pending_entities('remove')));
  0         0  
333 0         0 $self->remove_table($_) foreach $self->pending_entities('remove');
334 0         0 return $self;
335             }
336              
337             =head2 commit_pending_add
338              
339             Add all pending items, ordering to resolve dependencies as required.
340              
341             =cut
342              
343             sub commit_pending_add {
344 0     0 1 0 my $self = shift;
345 0         0 logInfo("Create " . join(',', map { $_->name } $self->pending_entities('add')));
  0         0  
346 0         0 my @pending = $self->pending_entities('add');
347             ITEM:
348 0         0 while(@pending) {
349 0         0 my $e = shift(@pending);
350             # TODO Not hugely efficient, perhaps could do with a profile run here?
351 0         0 my @deps = map { $_->name } $e->dependencies;
  0         0  
352 0         0 my @pendingNames = map { $_->name } @pending;
  0         0  
353 0         0 my @unsatisfied = grep { $_ ~~ @deps } @pendingNames;
  0         0  
354 0         0 my @existing = map { $_->name } $self->entity->list;
  0         0  
355             # Include current entity in list of available entries, so that we can allow self-reference
356 0         0 my @unresolved = grep { !($_ ~~ [@pendingNames, @existing, $e->name]) } @deps;
  0         0  
357 0 0       0 if(@unresolved) {
358 0         0 logError("%s unresolved (pending %s, deps %s for %s)", join(',', @unresolved), join(',', @pendingNames), join(',', @deps), $e->name);
359 0         0 die "Dependency error";
360             }
361 0 0       0 if(@unsatisfied) {
362 0         0 logInfo("%s has %d unsatisfied deps, postponing: %s", $e->name, scalar @unsatisfied, join(',',@unsatisfied));
363 0         0 push @pending, $e;
364 0         0 next ITEM;
365             }
366 0         0 $self->create_table($e);
367             }
368 0         0 return $self;
369             }
370              
371 0     0 0 0 sub remove_entity { shift->remove_table(@_) }
372              
373             sub remove_table {
374 0     0 0 0 my $self = shift;
375 0         0 my $tbl = shift;
376 0         0 logDebug("Remove table " . $tbl->name);
377 0     0   0 $self->entity->remove(sub { $_[0]->name ne $tbl->name });
  0         0  
378 0         0 return $self;
379             }
380              
381             sub create_table {
382 0     0 0 0 my $self = shift;
383 0         0 my $tbl = shift;
384 0         0 logDebug("Create table " . $tbl->name);
385 0         0 $self->entity->push($tbl);
386 0         0 return $self;
387             }
388              
389             sub update_table {
390 0     0 0 0 my $self = shift;
391 0         0 my $src = shift;
392 0         0 my ($e) = grep { $_->name eq $src->name } $self->entity->list;
  0         0  
393 0         0 logDebug("Found table [%s] for [%s]", $e->name, $src->name);
394 0         0 my $dst = $self->entity_map->get($src->name);
395 0   0     0 logDebug("Update table [%s], dest has fields: [%s]", $src->name, join(',', map { $_->name // "undef" } $dst->field->list));
  0         0  
396 0         0 my @add = grep { !$dst->field_map->get($_->name) } $src->field->list;
  0         0  
397 0         0 logDebug("Want to add [%s]", join(',', map { $_->name } @add));
  0         0  
398 0         0 $self->add_field_to_table($dst, $_) foreach @add;
399 0         0 return $self;
400             }
401              
402             =head2 add_field_to_table
403              
404             =cut
405              
406             sub add_field_to_table {
407 0     0 1 0 my $self = shift;
408 0         0 my $entity = shift;
409 0         0 my $field = shift;
410 0         0 $entity->field->push($field->clone);
411 0         0 return $self;
412             }
413              
414             sub handler_for {
415 0     0 0 0 my $self = shift;
416 0         0 my $name = shift;
417 0         0 logDebug("Check for handlers for [%s] node", $name);
418 0         0 return;
419             }
420              
421             sub provide_handler_for {
422 1     1 0 18 my $self = shift;
423 1         4 my @args = @_;
424 1         8 while(@args) {
425 1         4 my $k = shift(@args);
426 1         4 my $v = shift(@args);
427 1         13 $self->handler->set($k, $v);
428             }
429 1         81 return $self;
430             }
431              
432             sub handle_item {
433 1     1 0 2 my $self = shift;
434 1         5 my %args = @_;
435 1 50       5 if(my $code = $self->handler->get($args{item})) {
436 1         48 logDebug("Handling [%s] with plugin", $args{item});
437 1         38 $code->($self, item => $args{item}, data => $args{data});
438             } else {
439 0         0 logError("No handler for [%s]", $args{item});
440             }
441 1         18738 return $self;
442             }
443              
444             sub flush {
445 0     0 0   my $self = shift;
446 0           $self->commit;
447             }
448              
449             =head2 DESTROY
450              
451             Notify when there are pending uncommitted entries.
452              
453             =cut
454              
455             sub DESTROY {
456 0     0     my $self = shift;
457 0 0         if($self->hasPending) {
458 0           logError("Had pending commits for $self");
459             }
460             }
461              
462             1;
463              
464             __END__