File Coverage

blib/lib/Project/Easy.pm
Criterion Covered Total %
statement 16 183 8.7
branch 0 58 0.0
condition 0 38 0.0
subroutine 5 22 22.7
pod 4 10 40.0
total 25 311 8.0


line stmt bran cond sub pod time code
1             package Project::Easy;
2              
3 2     2   2178 use Class::Easy::Base;
  2         1467  
  2         14  
4 2     2   303 use IO::Easy;
  2         4  
  2         16  
5              
6             unless ($^O eq 'MSWin32') {
7             try_to_use 'Sys::SigAction';
8             }
9              
10             # these constants must be available prior to the helper use
11             BEGIN {
12 2     2   419 our $VERSION = '0.30';
13              
14 2         9 has etc => 'etc'; # conf, config
15 2         84 has bin => 'bin'; # scripts, tools
16 2         67 has t => 't'; # test, tests
17             };
18              
19 2     2   103 use Project::Easy::Helper;
  2         4  
  2         43  
20 2     2   11 use Project::Easy::Config::File;
  2         5  
  2         14  
21              
22             # because singleton
23             our $singleton = {};
24              
25             sub singleton {
26 0     0 1   return $singleton;
27             }
28              
29             has daemons => {};
30              
31             has daemon_package => 'Project::Easy::Daemon';
32             has db_package => 'Project::Easy::DB';
33             has conf_package => 'Project::Easy::Config';
34              
35             sub import {
36 0     0     my $pack = shift;
37 0           my @params = @_;
38            
39 0           my $caller = caller;
40            
41 0 0 0       if (scalar grep {$_ eq 'script'} @params or $caller eq 'main') {
  0 0          
  0            
42 0           Project::Easy::Helper::_script_wrapper;
43              
44 0           Class::Easy->import;
45 0           IO::Easy->import (qw(project));
46            
47             } elsif (scalar grep {$_ eq 'project'} @params) {
48 0           Project::Easy::Helper::_script_wrapper (undef, 1);
49            
50             }
51             }
52              
53             sub init {
54 0     0 0   my $class = shift;
55            
56 0           my $conf_package = $class->conf_package;
57 0 0         try_to_use ($conf_package)
58             or die ('configuration package must exists');
59            
60 0           $class->attach_paths;
61            
62 0           my $root = dir ($class->lib_path)->parent;
63              
64 0           make_accessor ($class, root => $root);
65            
66 0           my $conf_path = $root->append ($class->etc, $class->id . '.' . $class->conf_format)->as_file;
67            
68 0 0         die "can't locate generic config file at '$conf_path'"
69             unless -f $conf_path;
70              
71             # blessing for functionality extension: serializer
72 0           $conf_path = bless ($conf_path, 'Project::Easy::Config::File');
73              
74 0           make_accessor ($class, conf_path => $conf_path);
75            
76             }
77              
78             sub instantiate {
79 0     0 0   my $class = shift;
80            
81 0 0         die "you cannot use Project::Easy in one project more than one time ($singleton->{_instantiated})"
82             if $singleton->{_instantiated};
83            
84 0           debug "here we try to detect package location, "
85             . "because this package isn't for public distribution "
86             . "and must live in /lib";
87            
88 0           $class->detect_environment;
89              
90 0           my $db_package = $class->db_package;
91 0           try_to_use ($db_package);
92            
93 0           bless $singleton, $class;
94            
95 0           $singleton->{_instantiated} = $class;
96              
97 0           my $config = $singleton->config;
98              
99 0           foreach my $datasource (keys %{$config->{db}}) {
  0            
100 0           my $datasource_package = $config->{db}->{$datasource}->{package};
101            
102 0 0         if (defined $datasource_package) {
103 0           try_to_use ($datasource_package);
104             }
105            
106             # now we must sure to entities packages available
107             # and create new ones, if unavailable
108              
109 0 0 0       if (defined $datasource_package and $datasource_package->can ('create_entity')) {
110 0           $datasource_package->can ('create_entity')->($class, $class->root, $datasource);
111             } else {
112 0           Project::Easy::Helper->can ('create_entity')->($class, $class->root, $datasource);
113             }
114            
115              
116             make_accessor ($class, "db_$datasource" => sub {
117 0     0     return $class->db ($datasource);
118 0           });
119             }
120            
121 0 0         if (exists $config->{daemons}) {
122            
123 0           my $d_pack = $class->daemon_package;
124 0           try_to_use ($d_pack);
125            
126 0           foreach my $d_name (keys %{$config->{daemons}}) {
  0            
127 0           my $d_conf = $config->{daemons}->{$d_name};
128            
129 0           my $d;
130            
131 0 0         if ($d_conf->{package}) {
132 0           try_to_use ($d_conf->{package});
133 0           $d = $d_conf->{package}->new ($singleton, $d_name, $d_conf);
134             } else {
135 0           $d = $d_pack->new ($singleton, $d_name, $d_conf);
136             }
137            
138 0           $d->create_script_file ($class->root);
139            
140 0           $singleton->daemons->{$d_name} = $d;
141             }
142             }
143            
144 0           return $singleton;
145             }
146              
147             sub detect_environment {
148 0     0 0   my $class = shift;
149            
150 0           my $root = $class->root;
151            
152 0           my $distro_path = $root->append ('var', 'distribution');
153 0           my $instance_path = $root->append ('var', 'instance');
154            
155 0 0 0       if (-f $distro_path and ! -f $instance_path) {
156 0           rename $distro_path, $instance_path
157             }
158            
159 0           my @fixups = ();
160             $root->dir_io ($class->etc)->scan_tree (sub {
161 0     0     my $f = shift;
162 0 0         push @fixups, $f->name
163             if -d $f;
164 0           });
165            
166 0           my $ending = ".\nprobably you want to set '"
167             . $instance_path->rel_path (dir->current) . '\' contents to fixup config dir name (available fixups: '
168             . join (', ', @fixups).")\n";
169            
170 0 0         die "instance file not found" . $ending
171             unless -f $instance_path;
172            
173 0           my $instance_string = $instance_path->as_file->contents;
174            
175 0           $instance_string =~ s/[\s\n\t\r]+$//;
176              
177 0 0         die "can't recognise instance name '$instance_string'" . $ending
178             unless $instance_string;
179            
180 0           my ($instance, $fixup_core) = split (/:/, $instance_string, 2); # windows workaround
181            
182 0           make_accessor ($class, 'instance', default => $instance);
183 0           make_accessor ($class, 'fixup_core', default => $fixup_core);
184            
185 0           my $fixup_path = $class->fixup_path_instance;
186              
187 0 0         die "can't locate fixup config file at '$fixup_path'" . $ending
188             unless -f $fixup_path;
189            
190 0           make_accessor ($class, 'fixup_path', default => $fixup_path);
191            
192             }
193              
194             sub fixup_path_instance {
195 0     0 0   my $self = shift;
196 0   0       my $instance = shift || $self->instance;
197            
198 0           my $fixup_core = $self->fixup_core;
199            
200 0           my $fixup_path;
201            
202 0 0 0       if (defined $fixup_core and $fixup_core) {
203 0           $fixup_path = IO::Easy->new ($fixup_core)->append ($instance);
204             } else {
205 0           $fixup_path = $self->root->append ($self->etc, $instance);
206             }
207            
208 0           $fixup_path = $fixup_path->append ($self->id . '.' . $self->conf_format)->as_file;
209            
210 0           bless ($fixup_path, 'Project::Easy::Config::File');
211             }
212              
213             sub config {
214 0     0 1   my $class = shift;
215            
216 0 0         if (@_ > 0) { # get config for another instance, do not cache
217 0           my $config = $class->conf_package->parse (
218             $singleton, @_
219             );
220            
221             # reparse config
222 0 0         if ($_[0] eq $class->instance) {
223 0           $singleton->{config} = $config
224             }
225            
226 0           return $config
227             }
228            
229 0 0         unless ($singleton->{config}) {
230 0           $singleton->{config} = $class->conf_package->parse (
231             $singleton
232             );
233             }
234            
235 0           return $singleton->{config};
236             }
237              
238             sub _detect_entity {
239 0     0     my $self = shift;
240 0           my $name = shift;
241            
242             # here we need to convert supplied name to outstanding format.
243             # name must be in datasource representation (oracle_article) instead of entity (OracleArticle)
244            
245 0           my $ds = Project::Easy::Helper::table_from_package ($name);
246            
247             # for example: $name = 'oracle_article'
248             # $ds_config_name - datasource name in configuration (oracle)
249             # $ds_path - for table name in RDBMS (article)
250             # $entity_name - entity name in perl package name (OracleArticle)
251             # $ds_entity_name - datasource name in perl package name (Oracle)
252 0           my ($ds_config_name, $ds_path, $entity_name, $ds_entity_name);
253            
254 0           $ds_config_name = 'default';
255 0           $ds_entity_name = '';
256 0           $ds_path = $ds;
257 0           $entity_name = Project::Easy::Helper::package_from_table ($ds);
258              
259             # next, we want to check table name against entity datasource prefixes
260 0           foreach my $k (grep {!/^default$/} keys %{$self->config->{db}}) {
  0            
  0            
261             # my $qk = Project::Easy::Helper::package_from_table ($k);
262            
263 0 0         if (index ($ds, $k) == 0) {
264 0           my $separator = substr ($ds, length($k), 1);
265 0 0 0       if ($separator eq '_' or $separator eq ':') {
266 0           $ds_config_name = $k;
267 0           $ds_entity_name = Project::Easy::Helper::package_from_table ($k);
268 0           $ds_path = substr ($ds, length ($k) + 1);
269 0           $entity_name = Project::Easy::Helper::package_from_table ($ds);
270 0           last;
271             }
272             }
273             }
274            
275 0           debug "datasource: $ds, entity name: $entity_name, datasource path: $ds_path, datasource entity name: $ds_entity_name, datasource config key: $ds_config_name";
276            
277 0           return ($entity_name, $ds_path, $ds_entity_name, $ds_config_name);
278             }
279              
280             sub entity {
281 0     0 1   my $self = shift;
282 0           my $name = shift;
283            
284             # TODO: make cache for entities
285            
286             # try to detect entity by prefix
287 0           my ($entity_name, $ds_path, $ds_entity_name, $ds_config_name) = $self->_detect_entity ($name);
288            
289 0           my $ds_config = $self->config->{db}->{$ds_config_name};
290            
291 0   0       my $ds_package = $ds_config->{package} || $self->db_package;
292            
293 0           debug "datasource package: $ds_package";
294            
295 0           $ds_package->entity ($name, $entity_name, $ds_path, $ds_entity_name, $ds_config_name);
296             }
297              
298             sub collection {
299 0     0 0   my $self = shift;
300 0           my $name = shift;
301            
302             # TODO: make cache for entities
303            
304             # try to detect entity by prefix
305 0           my ($entity_name, $ds_path, $ds_entity_name, $ds_config_name) = $self->_detect_entity ($name);
306            
307 0           my $ds_config = $self->config->{db}->{$ds_config_name};
308            
309 0   0       my $ds_package = $ds_config->{package} || $self->db_package;
310            
311 0           debug "datasource package: $ds_package";
312            
313 0           $ds_package->collection ($name, $entity_name, $ds_path, $ds_entity_name, $ds_config_name);
314             }
315              
316              
317             sub daemon {
318 0     0 0   my $core = shift;
319 0           my $code = shift;
320            
321 0           return $core->daemons->{$code};
322             }
323              
324             sub db { # TODO: rewrite using alert
325 0     0 1   my $class = shift;
326 0   0       my $type = shift || 'default';
327            
328 0           my $core = $class->singleton; # fetch current process singleton
329            
330 0 0         $core->{db}->{$type} = {ts => {}}
331             unless $core->{db}->{$type};
332            
333 0           my $current_db = $core->{db}->{$type};
334              
335 0   0       my $db_package = $core->config->{db}->{$type}->{package} || $core->db_package;
336              
337 0 0         unless ($current_db->{$$}) {
338            
339             $DBI::Easy::ERRHANDLER = sub {
340 0     0     debug '%%%%%%%%%%%%%%%%%%%%%% DBI ERROR: we relaunch connection %%%%%%%%%%%%%%%%%%%%%%%%';
341 0           debug 'ERROR: ', $@;
342            
343 0           $class->_connect_db ($current_db, $db_package, $type, 1);
344            
345 0           return $class->db ($type);
346 0           };
347            
348 0           my $t = timer ("database handle start");
349              
350 0           $class->_connect_db ($current_db, $db_package, $type);
351              
352 0           $t->end;
353            
354             }
355            
356             # we reconnect every hour for morning bug by default
357 0           my $force_reconnect = $core->config->{db}->{$type}->{force_reconnect};
358 0 0         $force_reconnect = 0
359             unless defined $force_reconnect;
360 0 0 0       $force_reconnect = 3600
361             if $force_reconnect != 0 and $force_reconnect < 3600;
362 0 0 0       if ($force_reconnect != 0 and (time - $current_db->{ts}->{$$}) > $force_reconnect) {
363 0           debug "forced reconnect";
364 0           $class->_connect_db ($current_db, $db_package, $type, 1);
365             }
366            
367 0           return $current_db->{$$};
368            
369             }
370              
371             sub _connect_db {
372 0     0     my $class = shift;
373 0           my ($current_db, $db_package, $type, $disconnect) = @_;
374            
375 0           my $core = $class->singleton;
376            
377 0           my $old_dbh = delete $current_db->{$$};
378              
379 0 0 0       if (defined $disconnect and $disconnect) {
380             # basic windows support
381 0           eval {
382 0 0         if ($^O ne 'MSWin32') {
383             my $h = Sys::SigAction::set_sig_handler('ALRM', sub {
384             # failed disconnect is safe solution
385 0     0     die;
386 0           });
387 0           alarm (2);
388             }
389            
390             $old_dbh->disconnect
391 0 0         if $old_dbh;
392            
393 0 0         alarm (0)
394             if $^O ne 'MSWin32';
395             };
396             }
397              
398 0           $current_db->{$$} = $db_package->new ($core, $type);
399 0           $current_db->{ts}->{$$} = time;
400              
401             }
402              
403              
404              
405             1;
406              
407             =head1 NAME
408              
409             Project::Easy - project deployment made easy.
410              
411             =head1 SYNOPSIS
412              
413             package Caramba;
414              
415             use Class::Easy;
416              
417             use Project::Easy;
418             use base qw(Project::Easy);
419              
420             has 'id', default => 'caramba';
421             has 'conf_format', default => 'json';
422              
423             my $class = __PACKAGE__;
424              
425             has 'entity_prefix', default => join '::', $class, 'Entity', '';
426              
427             $class->init;
428              
429             =head1 ACCESSORS
430              
431             =head2 singleton
432              
433             =over 4
434              
435             =item singleton
436              
437             return class instance
438              
439             =cut
440              
441             =head2 configurable options
442              
443             =over 4
444              
445             =item id
446              
447             project id
448              
449             =item conf_format
450              
451             default config file format
452              
453             =item daemon_package
454              
455             interface for daemon creation
456              
457             default => 'Project::Easy::Daemon'
458              
459             =item db_package
460              
461             interface for db connections creation
462              
463             default => 'Project::Easy::DB'
464              
465             =item conf_package
466              
467             configuration interface
468              
469             default => 'Project::Easy::Config';
470              
471             =item default configuration directory
472              
473             has 'etc', default => 'etc';
474              
475             =item default binary directory
476              
477             has 'bin', default => 'bin';
478              
479             =cut
480              
481             =head2 autodetect options
482              
483             =over 4
484              
485             =item root
486              
487             IO::Easy object for project root directory
488              
489             =item instance
490              
491             string contains current project instance name
492              
493             =item fixup_core
494              
495             path (string) to configuration fixup root
496              
497             =item conf_path
498              
499             path object to the global configuration file
500              
501             =item fixup_path
502              
503             path object to the local configuration file
504              
505             =cut
506              
507             =head1 METHODS
508              
509             =head2 config
510              
511             return configuration object
512              
513             =head2 db
514              
515             database pool
516              
517             =cut
518              
519             =head1 ENTITIES
520              
521             =over 4
522              
523             =item intro
524              
525             Project::Easy create default entity classes on initialization.
526             this entity based on default database connection. you can use
527             this connection (not recommended) within modules by mantra:
528              
529             my $core = ->singleton;
530             my $dbh = $core->db;
531              
532             method db return default $dbh. you can use non-default dbh named 'cache' by calling:
533              
534             my $dbh_cache = $core->db ('cache');
535              
536             or
537             my $dbh_cache = $core->db_cache;
538              
539             if DBI::Easy default API satisfy you, then you can use database entities
540             by calling
541              
542             my $account_record = $core->entity ('Account');
543             my $account_collection = $core->collection ('Account');
544            
545             my $all_accounts = $account_collection->new->list;
546              
547             in this case, virtual packages created for entity 'account'.
548              
549             or you can create these packages by hand:
550              
551             package ::Entity::Account;
552            
553             use Class::Easy;
554            
555             use base qw(::Entity::Record);
556            
557             1;
558              
559             and for collection:
560              
561             package ::Entity::Account::Collection;
562              
563             use Class::Easy;
564              
565             use base qw(::Entity::Collection);
566              
567             1;
568              
569             in this case
570              
571             my $account_record = $core->entity ('Account');
572             my $account_collection = $core->collection ('Account');
573            
574             also works for you
575              
576             =cut
577              
578             =item creation another database entity class
579              
580             TODO: creation by script
581              
582             =cut
583              
584             =item using entities from multiple databases
585              
586             TODO: read database tables and create entity mappings,
587             each entity subclass must contain converted database identifier:
588              
589             default entity, table account_settings => entity AccountSettings
590             'cache' entity, table account_settings => entity CacheAccountSettings
591            
592              
593             =cut
594              
595              
596             =head1 AUTHOR
597              
598             Ivan Baktsheev, C<< >>
599              
600             =head1 BUGS
601              
602             Please report any bugs or feature requests to my email address,
603             or through the web interface at L.
604             I will be notified, and then you'll automatically be notified
605             of progress on your bug as I make changes.
606              
607             =head1 SUPPORT
608              
609              
610              
611             =head1 ACKNOWLEDGEMENTS
612              
613              
614              
615             =head1 COPYRIGHT & LICENSE
616              
617             Copyright 2007-2009 Ivan Baktsheev
618              
619             This program is free software; you can redistribute it and/or modify it
620             under the same terms as Perl itself.
621              
622              
623             =cut