File Coverage

blib/lib/App/DBCritic.pm
Criterion Covered Total %
statement 61 71 85.9
branch 4 12 33.3
condition 2 3 66.6
subroutine 17 19 89.4
pod 1 1 100.0
total 85 106 80.1


line stmt bran cond sub pod time code
1             package App::DBCritic;
2              
3             # ABSTRACT: Critique a database schema for best practices
4              
5             #pod =head1 SYNOPSIS
6             #pod
7             #pod use App::DBCritic;
8             #pod
9             #pod my $critic = App::DBCritic->new(
10             #pod dsn => 'dbi:Oracle:HR', username => 'scott', password => 'tiger');
11             #pod $critic->critique();
12             #pod
13             #pod =head1 DESCRIPTION
14             #pod
15             #pod This package is used to scan a database schema and catalog any violations
16             #pod of best practices as defined by a set of policy plugins. It takes conceptual
17             #pod and API inspiration from L.
18             #pod
19             #pod B is the command line interface.
20             #pod
21             #pod This is a work in progress - please see the L section below for
22             #pod information on how to contribute. It especially needs ideas (and
23             #pod implementations!) of new policies!
24             #pod
25             #pod =cut
26              
27 7     7   1568282 use strict;
  7         51  
  7         209  
28 7     7   42 use utf8;
  7         13  
  7         108  
29 7     7   198 use Modern::Perl '2011'; ## no critic (Modules::ProhibitUseQuotedVersion)
  7         14  
  7         61  
30              
31             our $VERSION = '0.023'; # VERSION
32 7     7   1305 use Carp;
  7         22  
  7         491  
33 7     7   1770 use English '-no_match_vars';
  7         7098  
  7         69  
34 7     7   2603 use List::Util 1.33 'any';
  7         156  
  7         555  
35             use Module::Pluggable
36 7         56 search_path => [ __PACKAGE__ . '::Policy' ],
37             sub_name => 'policies',
38 7     7   3979 instantiate => 'new';
  7         76416  
39              
40             #pod =method policies
41             #pod
42             #pod Returns an array of loaded policy names that will be applied during
43             #pod L. By default all modules under the
44             #pod C namespace are loaded.
45             #pod
46             #pod =cut
47              
48 7     7   5368 use Moo;
  7         64468  
  7         39  
49 7     7   11405 use Scalar::Util 'blessed';
  7         16  
  7         363  
50 7     7   3340 use App::DBCritic::Loader;
  7         32  
  7         43  
51              
52             for (qw(username password class_name)) { has $_ => ( is => 'ro' ) }
53              
54             #pod =attr username
55             #pod
56             #pod The optional username used to connect to the database.
57             #pod
58             #pod =attr password
59             #pod
60             #pod The optional password used to connect to the database.
61             #pod
62             #pod =attr class_name
63             #pod
64             #pod The name of a L class you wish to
65             #pod L.
66             #pod Only settable at construction time.
67             #pod
68             #pod =cut
69              
70             has dsn => ( is => 'ro', lazy => 1, default => \&_build_dsn );
71              
72             sub _build_dsn {
73 0     0   0 my $self = shift;
74              
75             ## no critic (ErrorHandling::RequireUseOfExceptions)
76 0 0       0 croak 'No schema defined' if not $self->has_schema;
77 0         0 my $dbh = $self->schema->storage->dbh;
78              
79             ## no critic (ValuesAndExpressions::ProhibitAccessOfPrivateData)
80 0         0 return join q{:} => 'dbi', $dbh->{Driver}{Name}, $dbh->{Name};
81             }
82              
83             #pod =attr dsn
84             #pod
85             #pod The L data source name (required) used to connect to the database.
86             #pod If no L or L is provided, L will then
87             #pod construct schema classes dynamically to be critiqued.
88             #pod
89             #pod =cut
90              
91             has schema => (
92             is => 'ro',
93             coerce => 1,
94             lazy => 1,
95             default => \&_build_schema,
96             coerce => \&_coerce_schema,
97             predicate => 1,
98             );
99              
100             sub _build_schema {
101 1     1   19 my $self = shift;
102              
103 1         3 my @connect_info = map { $self->$_ } qw(dsn username password);
  3         34  
104              
105 1 50       6 if ( my $class_name = $self->class_name ) {
106 0 0       0 return $class_name->connect(@connect_info)
107             if eval "require $class_name";
108             }
109              
110 1         4 return _coerce_schema( \@connect_info );
111             }
112              
113             sub _coerce_schema {
114 7     7   2968863 my $schema = shift;
115              
116 7 100 66     262 return $schema if blessed $schema and $schema->isa('DBIx::Class::Schema');
117              
118             local $SIG{__WARN__} = sub {
119 0 0   0   0 if ( $_[0] !~ / has no primary key at /ms ) {
120 0         0 print {*STDERR} $_[0];
  0         0  
121             }
122 1         10 };
123 1 50       5 return App::DBCritic::Loader->connect( @{$schema} )
  1         17  
124             if 'ARRAY' eq ref $schema;
125             ## no critic (ErrorHandling::RequireUseOfExceptions)
126 0         0 croak q{don't know how to make a schema from a } . ref $schema;
127             }
128              
129             #pod =attr schema
130             #pod
131             #pod A L object you wish to L.
132             #pod Only settable at construction time.
133             #pod
134             #pod =attr has_schema
135             #pod
136             #pod An attribute predicates that is true or false, depending on whether L
137             #pod has been defined.
138             #pod
139             #pod =cut
140              
141             has _elements => ( is => 'ro', lazy => 1, default => \&_build__elements );
142              
143             sub _build__elements {
144 5     5   64 my $self = shift;
145 5         91 my $schema = $self->schema;
146             return {
147             Schema => [$schema],
148 7         292 ResultSource => [ map { $schema->source($_) } $schema->sources ],
149 5         135 ResultSet => [ map { $schema->resultset($_) } $schema->sources ],
  7         2692  
150             };
151             }
152              
153             sub critique {
154 1     1 1 2 for ( @{ shift->violations } ) {say}
  1         21  
  0         0  
155 1         32 return;
156             }
157              
158             #pod =method critique
159             #pod
160             #pod Runs the L through the C engine using all
161             #pod the policies that have been loaded and dumps a string representation of
162             #pod L to C.
163             #pod
164             #pod =cut
165              
166             has violations => (
167             is => 'ro',
168             lazy => 1,
169             default => sub {
170             my $self = shift;
171             [ map { $self->_policy_loop( $_, $self->_elements->{$_} ) }
172             keys %{ $self->_elements },
173             ];
174             },
175             );
176              
177             #pod =method violations
178             #pod
179             #pod Returns an array reference of all
180             #pod Ls
181             #pod picked up by the various policies.
182             #pod
183             #pod =cut
184              
185             sub _policy_loop {
186 15     15   180 my ( $self, $policy_type, $elements_ref ) = @_;
187 15         30 my @violations;
188 15         87 for my $policy ( grep { _policy_applies_to( $_, $policy_type ) }
  60         104813  
189             $self->policies )
190             {
191 4         7038 push @violations, grep {$_}
192 20         41 map { $policy->violates( $_, $self->schema ) } @{$elements_ref};
  28         2564  
  20         43  
193             }
194 15         195 return @violations;
195             }
196              
197             sub _policy_applies_to {
198 60     60   132 my ( $policy, $type ) = @_;
199 60     60   199 return any { $_ eq $type } @{ $policy->applies_to };
  60         11035  
  60         1028  
200             }
201              
202             1;
203              
204             __END__