File Coverage

blib/lib/Data/Header/Fields.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Data::Header::Fields;
2              
3 3     3   77799 use warnings;
  3         8  
  3         92  
4 3     3   13 use strict;
  3         6  
  3         91  
5              
6 3     3   1399 use IO::Any;
  0            
  0            
7             use Carp 'croak';
8             use String::Escape ();
9             use List::MoreUtils 'uniq', 'all';
10              
11             use overload
12             '""' => \&as_string,
13             'cmp' => \&cmp,
14             ;
15              
16             our $VERSION = '0.04';
17              
18             sub new {
19             my $class = shift;
20             return bless {
21             tight_folding => 0,
22             key_cmp => sub { $_[0] cmp $_[1] },
23             @_
24             }, $class;
25             }
26              
27             sub _lines {
28             my $self = shift;
29            
30             $self->{_lines} = shift
31             if (@_);
32            
33             $self->{_lines} = []
34             if (not $self->{_lines});
35            
36             return $self->{_lines};
37             }
38              
39             sub key_cmp {
40             my $self = shift;
41             $self->{'key_cmp'} = shift
42             if @_;
43            
44             return $self->{'key_cmp'};
45             }
46              
47             sub tight_folding {
48             my $self = shift;
49            
50             $self->{tight_folding} = shift
51             if (@_);
52            
53             return 0
54             if not ref $self;
55            
56             return $self->{tight_folding};
57             }
58              
59             sub decode {
60             my $self = shift;
61             my $any = shift;
62            
63             my @lines = $self->_read_lines($any);
64              
65             my $line_ending = (
66             ($lines[0] || '') =~ m/\r\n \Z/xms
67             ? "\r\n"
68             : "\n"
69             );
70             $self->line_ending($line_ending);
71            
72             @lines = (
73             map {
74             Data::Header::Fields::Line->new(
75             'line' => $_,
76             'parent' => $self,
77             );
78             } @lines
79             );
80            
81             if (ref $self) {
82             $self->_lines(\@lines);
83             return $self;
84             }
85            
86             return \@lines;
87             }
88              
89             sub _read_lines {
90             my $self = shift;
91             my $any = shift;
92            
93             my $fh = IO::Any->read($any);
94            
95             # put folded lines to an array http://tools.ietf.org/html/rfc2822#section-2.2.3
96             my @lines;
97             while (my $line = <$fh>) {
98             # folded line
99             if (($line =~ m/^\s/xms)) {
100             # ignore if the first line starts with white space
101             next if not @lines;
102            
103             $lines[-1] .= $line;
104             next;
105             }
106             push @lines, $line;
107             }
108            
109             close $fh;
110              
111             return @lines;
112             }
113              
114             *as_string = *encode;
115             sub encode {
116             my $self = shift;
117             my $lines = shift || (ref $self ? $self->_lines : undef);
118            
119             # no additional arguments
120             if (@_ == 0) {
121             my $text = '';
122             $self->encode($lines, \$text);
123             return $text;
124             }
125            
126             my $any = shift;
127            
128             my $fh = IO::Any->write($any);
129             foreach my $line (@{$lines}) {
130             print $fh $line->as_string;
131             }
132            
133             close $fh;
134            
135             return $self;
136             }
137              
138             sub get_fields {
139             my $self = shift;
140             my $field_name = shift or croak 'field_name argument is mandatory';
141            
142             my $key_cmp = $self->key_cmp;
143             return (
144             grep {
145             $key_cmp->($field_name, $_->key) == 0
146             } @{$self->_lines}
147             );
148             }
149              
150             sub get_field {
151             my $self = shift;
152             my $field_name = shift or croak 'field_name argument is mandatory';
153             my @extra_args = @_;
154            
155             my @fields = $self->get_fields($field_name, @extra_args);
156             croak 'more then one header field with name "'.$field_name.'"'
157             if @fields > 1;
158            
159             return $fields[0];
160             }
161              
162             sub get_value {
163             my $self = shift;
164             my $key = shift or croak 'key argument is mandatory';
165             my @extra_args = @_;
166              
167             my $field = $self->get_field($key, @extra_args);
168             return undef if not defined $field;
169             return $field->value;
170             }
171              
172             sub update_values {
173             my $self = shift;
174             my $key = shift or croak 'key argument is mandatory';
175             my $value = shift;
176              
177             my $key_cmp = $self->key_cmp;
178             my @lines = (
179             map {
180             ($key_cmp->($_->key, $key) == 0 ? $_->value($value) : ());
181             $_;
182             } @{$self->_lines}
183             );
184            
185             return $self;
186             }
187              
188             sub rm_fields {
189             my $self = shift;
190             my (@field_names) = (@_) or croak 'field_names argument is mandatory';
191              
192             my $key_cmp = $self->key_cmp;
193             my @lines = (
194             grep {
195             my $key = $_->key;
196             all { $key_cmp->($key, $_) != 0 } @field_names;
197             } @{$self->_lines}
198             );
199            
200             $self->_lines(\@lines);
201            
202             return $self;
203             }
204              
205             sub set_value {
206             my $self = shift;
207             my $key = shift or croak 'key argument is mandatory';
208             my $value = shift;
209              
210             my @fields = $self->get_fields($key);
211             if (@fields == 1) {
212             $self->update_values($key, $value);
213             }
214             elsif (@fields == 0) {
215             push @{$self->_lines}, Data::Header::Fields::Line->new(
216             'key' => $key,
217             'value' => $value,
218             'parent' => $self,
219             );
220             }
221             else {
222             croak 'more then one header field with name "'.$key.'"';
223             }
224            
225            
226             return $self;
227             }
228              
229             sub cmp {
230             my $a = shift;
231             my $b = shift;
232            
233             $a = $a->encode if ref $a and $a->can('encode');
234             $b = $b->encode if ref $b and $b->can('encode');
235            
236             return $a cmp $b;
237             }
238              
239             sub keys {
240             my $self = shift;
241             my $lines = shift || (ref $self ? $self->_lines : []);
242            
243             return
244             uniq
245             map {
246             $_->key
247             } @{$lines}
248             ;
249             }
250              
251             sub line_ending {
252             my $self = shift;
253            
254             return "\n"
255             if not ref $self;
256            
257             if (@_) {
258             $self->{line_ending} = shift;
259             }
260             $self->{line_ending} = "\n"
261             if (not $self->{line_ending});
262            
263             return $self->{line_ending};
264             }
265              
266              
267             1;
268              
269             package Data::Header::Fields::Value;
270              
271             use Scalar::Util 'weaken', 'isweak';
272              
273             use overload
274             '""' => \&as_string,
275             'cmp' => \&cmp,
276             ;
277              
278             sub new {
279             my $class = shift;
280             my $value = shift;
281            
282             if (@_ == 0) {
283             if (not ref $value) {
284             $value = { 'value' => $value };
285             }
286             }
287             else {
288             $value = { $value, @_ };
289             }
290            
291             my $self = bless { 'parent' => $class->_default_parent, %{$value} }, $class;
292            
293             weaken($self->{'parent'})
294             if (ref($self->{'parent'}) && !isweak($self->{'parent'}));
295            
296             return $self;
297             }
298              
299             sub as_string {
300             my $self = shift;
301              
302             # remove folding
303             my $line = $self->{value};
304             if ($self->parent->parent->tight_folding) {
305             $line =~ s/\n\s//xmsg;
306             }
307             else {
308             $line =~ s/\n(\s)/$1/xmsg;
309             }
310             $line =~ s/\r?\n$//;
311             $line = String::Escape::unprintable($line);
312            
313             return $line;
314             }
315              
316             sub cmp {
317             my $a = shift;
318             my $b = shift;
319            
320             $a = $a->as_string if ref $a and $a->can('as_string');
321             $b = $b->as_string if ref $b and $b->can('as_string');
322            
323             return $a cmp $b;
324             }
325              
326             sub _default_parent {
327             return 'Data::Header::Fields::Line';
328             }
329              
330             sub parent {
331             my $self = shift;
332             $self->{'parent'} = shift
333             if @_;
334            
335             return (ref $self->{'parent'} ? $self->{'parent'} : $self->_default_parent);
336             }
337              
338             sub value {
339             my $self = shift;
340            
341             if (@_) {
342             $self->{'value'} = shift;
343             $self->parent->line_changed;
344             }
345            
346             return $self->{'value'};
347             }
348             1;
349              
350             package Data::Header::Fields::Line;
351              
352             use Scalar::Util 'blessed', 'weaken', 'isweak';
353              
354             use overload
355             '""' => \&as_string,
356             'cmp' => \&cmp,
357             ;
358              
359             sub new {
360             my $class = shift;
361             my $line = shift;
362             my @args = @_;
363            
364             if (@args > 0) {
365             $line = { $line, @args };
366             }
367            
368             if (not ref $line) {
369             $line = { 'line' => $line };
370             }
371            
372             $line = { 'parent' => $class->_default_parent, %{$line} };
373            
374             if (exists $line->{'line'}) {
375             # reblessing the line object
376             if (blessed $line->{'line'}) {
377             my $self = delete $line->{'line'};
378             foreach my $key (keys %{$line}) {
379             $self->{$key} = $line->{$key};
380             }
381             return bless $self, $class;
382             }
383             else {
384             my $line_string = delete $line->{'line'};
385             $line->{'original_line'} = $line_string;
386             my ($key, $value) = split(/:/, $line_string, 2);
387             $line->{'key'} = $key;
388             $line->{'value'} = Data::Header::Fields::Value->new(
389             'value' => $value,
390             'parent' => $line,
391             );
392             }
393             }
394            
395             weaken($line->{'parent'})
396             if (ref($line->{'parent'}) && !isweak($line->{'parent'}));
397            
398             return bless $line, $class;
399             }
400              
401             sub key {
402             my $self = shift;
403             $self->{'key'} = shift
404             if @_;
405            
406             return $self->{'key'};
407             }
408             sub value {
409             my $self = shift;
410             $self->line_changed->{'value'} = shift
411             if @_;
412            
413             return $self->{'value'};
414             }
415              
416             sub line_changed {
417             my $self = shift;
418             delete $self->{'original_line'}
419             if ref $self;
420             return $self;
421             }
422              
423             sub as_string {
424             my $self = shift;
425            
426             if (exists $self->{'original_line'}) {
427             my $original_line = $self->{'original_line'};
428            
429             # make sure the line has line_ending, even the original one could be created without using ->new()
430             $original_line .= $self->parent->line_ending
431             if $original_line !~ m/ \n \Z /xms;
432            
433             return $original_line;
434             }
435              
436             my ($key, $value) = ($self->key, $self->value);
437             $value = String::Escape::printable($value);
438              
439             my $line = join(':', $key, $value);
440            
441             $line .= $self->parent->line_ending
442             if $line !~ m/\n$/xms;
443            
444             return $line;
445             }
446              
447             sub cmp {
448             my $a = shift;
449             my $b = shift;
450            
451             $a = $a->as_string if ref $a and $a->can('as_string');
452             $b = $b->as_string if ref $b and $b->can('as_string');
453            
454             return $a cmp $b;
455             }
456              
457             sub _default_parent {
458             return 'Data::Header::Fields';
459             }
460              
461             sub parent {
462             my $self = shift;
463             $self->{'parent'} = shift
464             if @_;
465            
466             return (ref $self ? $self->{'parent'} : $self->_default_parent);
467             }
468              
469              
470             1;
471              
472              
473             __END__