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