File Coverage

blib/lib/App/SpamcupNG/Summary/Recorder.pm
Criterion Covered Total %
statement 72 72 100.0
branch 21 36 58.3
condition n/a
subroutine 13 13 100.0
pod 3 3 100.0
total 109 124 87.9


line stmt bran cond sub pod time code
1             package App::SpamcupNG::Summary::Recorder;
2 6     6   563 use strict;
  6         15  
  6         190  
3 6     6   41 use warnings;
  6         19  
  6         157  
4 6     6   34 use Carp qw(confess);
  6         15  
  6         319  
5 6     6   46 use Hash::Util qw(lock_hash);
  6         20  
  6         54  
6 6     6   9245 use DBI 1.643;
  6         90234  
  6         386  
7 6     6   6048 use DateTime 1.55;
  6         3359348  
  6         6439  
8              
9             our $VERSION = '0.017'; # VERSION
10              
11             =pod
12              
13             =head1 NAME
14              
15             App::SpamcupNG::Summary::Recorder - class to save Summary to SQLite3
16              
17             =head1 SYNOPSIS
18              
19             use App::SpamcupNG::Summary::Recorder;
20              
21             # just pretend that $summary is an existing App::SpamcupNG::Summary instance
22             my $recorder = App::SpamcupNG::Summary::Recorder->new(
23             '/some/path/database_file' );
24             $recorder->init;
25             $recorder->save($summary);
26              
27             =head1 DESCRIPTION
28              
29             This class is used to persist L<App::SpamcupNG::Summary> instances to a SQLite3
30             database.
31              
32             =head1 METHODS
33              
34             =head2 new
35              
36             Creates a new recorder instance.
37              
38             Expects as parameter the complete path to a existing (or to create) SQLite 3
39             file.
40              
41             =cut
42              
43             sub new {
44 2     2 1 1383 my ( $class, $file, $now ) = @_;
45 2         12 my $self = { db_file => $file, now => $now };
46              
47             # TODO: add tables names for DRY also replacing in _save_attrib
48 2 50       26 $self->{dbh} = DBI->connect( ( 'dbi:SQLite:dbname=' . $file ), '', '' )
49             or die $DBI::errstr;
50 2         26785 bless $self, $class;
51 2         14 return $self;
52             }
53              
54             =head2 init
55              
56             Initialize the database if it doesn't exist yet. This is idempotent.
57              
58             =cut
59              
60             sub init {
61 2     2 1 11 my $self = shift;
62             $self->{dbh}->do(
63             q{
64             CREATE TABLE IF NOT EXISTS email_content_type (
65             id INTEGER PRIMARY KEY AUTOINCREMENT,
66             name TEXT NOT NULL UNIQUE
67             )
68             }
69 2 50       22 ) or die $self->{dbh}->errstr;
70              
71             $self->{dbh}->do(
72             q{
73             CREATE TABLE IF NOT EXISTS spam_age_unit (
74             id INTEGER PRIMARY KEY AUTOINCREMENT,
75             name TEXT NOT NULL UNIQUE
76             )
77             }
78 2 50       38498 ) or die $self->{dbh}->errstr;
79              
80             $self->{dbh}->do(
81             q{
82             CREATE TABLE IF NOT EXISTS email_charset (
83             id INTEGER PRIMARY KEY AUTOINCREMENT,
84             name TEXT NOT NULL UNIQUE
85             )
86             }
87 2 50       34530 ) or die $self->{dbh}->errstr;
88              
89             $self->{dbh}->do(
90             q{
91             CREATE TABLE IF NOT EXISTS receiver (
92             id INTEGER PRIMARY KEY AUTOINCREMENT,
93             email TEXT NOT NULL UNIQUE
94             )
95             }
96 2 50       33583 ) or die $self->{dbh}->errstr;
97              
98             $self->{dbh}->do(
99             q{
100             CREATE TABLE IF NOT EXISTS mailer (
101             id INTEGER PRIMARY KEY AUTOINCREMENT,
102             name TEXT NOT NULL UNIQUE
103             )
104             }
105 2 50       31596 ) or die $self->{dbh}->errstr;
106              
107             $self->{dbh}->do(
108             q{
109             CREATE TABLE IF NOT EXISTS summary (
110             id INTEGER PRIMARY KEY AUTOINCREMENT,
111             tracking_id TEXT NOT NULL UNIQUE,
112             created INTEGER NOT NULL,
113             charset_id INTEGER REFERENCES email_charset ON DELETE SET NULL,
114             content_type_id INTEGER REFERENCES email_content_type ON DELETE SET NULL,
115             age INTEGER NOT NULL,
116             age_unit_id INTEGER REFERENCES spam_age_unit ON DELETE SET NULL,
117             mailer_id INTEGER REFERENCES mailer ON DELETE SET NULL
118             )
119             }
120 2 50       33039 ) or die $self->{dbh}->errstr;
121              
122             $self->{dbh}->do(
123             q{
124             CREATE TABLE IF NOT EXISTS summary_receiver (
125             id INTEGER PRIMARY KEY AUTOINCREMENT,
126             summary_id INTEGER REFERENCES summary ON DELETE CASCADE,
127             receiver_id INTEGER REFERENCES receiver ON DELETE CASCADE,
128             report_id TEXT UNIQUE
129             )
130             }
131 2 50       32889 ) or die $self->{dbh}->errstr;
132              
133             }
134              
135             =head2 save
136              
137             Persists a L<App::SpamcupNG::Summary> instance to the database.
138              
139             Returns "true" (in Perl terms) if everything goes fine.
140              
141             =cut
142              
143             sub save {
144 2     2 1 15 my ( $self, $summary ) = @_;
145 2         6807 my $summary_class = 'App::SpamcupNG::Summary';
146 2         17 my $ref = ref($summary);
147 2 50       28 confess "summary must be instance of $summary_class class, not '$ref'"
148             unless ( $ref eq $summary_class );
149              
150             # TODO: create a method for Summary to provide those names
151 2         15 my @fields = qw(content_type age_unit charset mailer);
152 2         7 my %fields;
153              
154 2         13 foreach my $field_name (@fields) {
155 8         48 my $method = "get_$field_name";
156 8         106 $fields{$field_name}
157             = $self->_save_attrib( $field_name, $summary->$method );
158             }
159              
160 2         59 lock_hash(%fields);
161              
162 2         173 my $summary_id = $self->_save_summary( $summary, \%fields );
163              
164 2         14 foreach my $receiver ( @{ $summary->get_receivers } ) {
  2         21  
165 4         29254 my $receiver_id = $self->_save_attrib( 'receiver', $receiver->email );
166 4         56 $self->_save_sum_rec( $summary_id, $receiver_id,
167             $receiver->report_id );
168             }
169              
170 2         35940 return 1;
171             }
172              
173             sub _save_sum_rec {
174 4     4   135 my ( $self, $sum_id, $rec_id, $report_id ) = @_;
175 4         23 my @values = ( $sum_id, $rec_id, $report_id );
176             $self->{dbh}->do(
177             q{
178             INSERT INTO summary_receiver (summary_id, receiver_id, report_id)
179             VALUES(?, ?, ?)
180             }, undef, @values
181 4 50       41 ) or confess $self->{dbh}->errstr;
182             }
183              
184             sub _save_summary {
185 2     2   15 my ( $self, $summary, $fields_ref ) = @_;
186 2 100       36 my $now = $self->{now} ? $self->{now} : DateTime->now->epoch;
187 2         1069 my $insert = q{
188             INSERT INTO summary
189             (tracking_id, created, charset_id, content_type_id, age, age_unit_id, mailer_id)
190             VALUES (?, ?, ?, ?, ?, ?, ?)
191             };
192             my @values = (
193             $summary->get_tracking_id, $now,
194             $fields_ref->{charset}, $fields_ref->{content_type},
195             $summary->get_age, $fields_ref->{age_unit},
196             $fields_ref->{mailer}
197 2         28 );
198             $self->{dbh}->do( $insert, undef, @values )
199 2 50       145 or confess $self->{dbh}->errstr;
200 2         32924 return $self->{dbh}->last_insert_id;
201             }
202              
203             sub _save_attrib {
204 12     12   452 my ( $self, $attrib, $value ) = @_;
205 12         114 my %attrib_to_table = (
206             content_type => 'email_content_type',
207             age_unit => 'spam_age_unit',
208             charset => 'email_charset',
209             mailer => 'mailer',
210             receiver => 'receiver'
211             );
212              
213 12 100       68 return undef unless ( defined($value) );
214             confess "'$attrib' is not a valid attribute"
215 11 50       63 unless ( exists( $attrib_to_table{$attrib} ) );
216 11         37 my $table = $attrib_to_table{$attrib};
217 11         31 my $column;
218              
219 11 100       59 if ( $attrib eq 'receiver' ) {
220 4         15 $column = 'email';
221             }
222             else {
223 7         36 $column = 'name';
224             }
225              
226             my $row_ref
227             = $self->{dbh}
228 11         191 ->selectrow_arrayref( "SELECT id FROM $table WHERE $column = ?",
229             undef, $value );
230 11 50       3236 return $row_ref->[0] if ( defined( $row_ref->[0] ) );
231              
232             $self->{dbh}->do(
233 11         196 qq{
234             INSERT INTO $table ($column) VALUES (?)
235             },
236             undef,
237             $value
238             );
239              
240 11         180315 return $self->{dbh}->last_insert_id;
241             }
242              
243             =head2 DESTROY
244              
245             Properly closes the SQLite 3 database file when the recorder instance goes out
246             of scope.
247              
248             =cut
249              
250             sub DESTROY {
251 2     2   10361 my $self = shift;
252              
253 2 50       17 if ( $self->{dbh} ) {
254 2 50       552 $self->{dbh}->disconnect or warn $self->{dbh}->errstr;
255             }
256             }
257              
258             =pod
259              
260             =head1 QUERYING RESULTS
261              
262             This is a sample query to checkout records in the database:
263              
264             SELECT A.id,
265             A.tracking_id,
266             DATETIME(A.created, 'unixepoch') AS CREATED,
267             B.name AS CHARSET,
268             C.name AS CONTENT_TYPE,
269             A.age,
270             D.name AS MAILER,
271             E.report_id,
272             F.email
273             FROM summary A outer left join email_charset B on A.charset_id = B.id
274             INNER JOIN email_content_type C ON A.content_type_id = C.id
275             OUTER LEFT JOIN mailer D ON A.mailer_id = D.id
276             INNER JOIN summary_receiver E ON A.id = E.summary_id
277             INNER JOIN receiver F ON E.receiver_id = F.id;
278              
279             =head1 SEE ALSO
280              
281             =over
282              
283             =item *
284              
285             L<https://www.sqlite.org/docs.html>
286              
287             =back
288              
289             =cut
290              
291             1;