File Coverage

blib/lib/vCard.pm
Criterion Covered Total %
statement 122 127 96.0
branch 41 54 75.9
condition 18 35 51.4
subroutine 33 35 94.2
pod 19 20 95.0
total 233 271 85.9


line stmt bran cond sub pod time code
1             package vCard;
2             $vCard::VERSION = '3.09';
3 3     3   244690 use Moo;
  3         17917  
  3         16  
4              
5 3     3   2337 use Carp;
  3         4  
  3         140  
6 3     3   10 use Path::Tiny;
  3         4  
  3         97  
7 3     3   1111 use Text::vCard;
  3         10  
  3         46  
8 3     3   1109 use vCard::AddressBook;
  3         7  
  3         81  
9 3     3   1529 use URI;
  3         9407  
  3         3642  
10              
11             =head1 NAME
12              
13             vCard - Read, write, and edit vCards
14              
15             =head1 SYNOPSIS
16              
17             use vCard;
18              
19             # create the object
20             my $vcard = vCard->new;
21              
22             # these methods load vCard data
23             # (see method documentation for details)
24             $vcard->load_file($filename);
25             $vcard->load_string($string);
26             $vcard->load_hashref($hashref);
27              
28             # simple getters/setters
29             $vcard->full_name('Bruce Banner, PhD');
30             $vcard->title('Research Scientist');
31             $vcard->photo('http://example.com/bbanner.gif');
32              
33             # complex getters/setters
34             $vcard->phones([
35             { type => ['work', 'text'], number => '651-290-1234', preferred => 1 },
36             { type => ['home'], number => '651-290-1111' }
37             ]);
38             $vcard->email_addresses([
39             { type => ['work'], address => 'bbanner@ssh.secret.army.mil' },
40             { type => ['home'], address => 'bbanner@timewarner.com' },
41             ]);
42              
43             # these methods output data in vCard format
44             my $file = $vcard->as_file($filename); # writes to $filename
45             my $string = $vcard->as_string; # returns a string
46              
47              
48             =head1 DESCRIPTION
49              
50             A vCard is a digital business card. vCard and L provide an
51             API for parsing, editing, and creating vCards.
52              
53             This module is built on top of L. It provides a more intuitive user
54             interface.
55              
56             To handle an address book with several vCard entries in it, start with
57             L and then come back to this module.
58              
59             Note that the vCard RFC requires version() and full_name(). This module does
60             not check or warn if these conditions have not been met.
61              
62              
63             =head1 ENCODING AND UTF-8
64              
65             See the 'ENCODING AND UTF-8' section of L.
66              
67              
68             =head1 METHODS
69              
70             =cut
71              
72             has encoding_in => ( is => 'rw', default => sub {'UTF-8'} );
73             has encoding_out => ( is => 'rw', default => sub {'UTF-8'} );
74             has _data => ( is => 'rw', default => sub { { version => '4.0' } } );
75              
76             with 'vCard::Role::FileIO';
77              
78             =head2 load_hashref($hashref)
79              
80             $hashref should look like this:
81              
82             full_name => 'Bruce Banner, PhD',
83             given_names => ['Bruce'],
84             family_names => ['Banner'],
85             title => 'Research Scientist',
86             photo => 'http://example.com/bbanner.gif',
87             phones => [
88             { type => ['work'], number => '651-290-1234', preferred => 1 },
89             { type => ['cell'], number => '651-290-1111' },
90             },
91             addresses => [
92             { type => ['work'], ... },
93             { type => ['home'], ... },
94             ],
95             email_addresses => [
96             { type => ['work'], address => 'bbanner@shh.secret.army.mil' },
97             { type => ['home'], address => 'bbanner@timewarner.com' },
98             ],
99              
100             Returns $self in case you feel like chaining.
101              
102             =cut
103              
104             sub load_hashref {
105 1     1 1 7 my ( $self, $hashref ) = @_;
106 1         15 $self->_data($hashref);
107              
108             $self->_data->{version} = '4.0'
109 1 50       9 unless $self->_data->{version};
110              
111             $self->_data->{photo} = URI->new( $self->_data->{photo} )
112 1 50       13 unless ref $self->_data->{photo} =~ /^URI/;
113              
114 1         5092 return $self;
115             }
116              
117             =head2 load_file($filename)
118              
119             Returns $self in case you feel like chaining.
120              
121             =cut
122              
123             sub load_file {
124 2     2 1 13 my ( $self, $filename ) = @_;
125              
126 2         39 my $addressBook = vCard::AddressBook->new({
127             encoding_in => $self->encoding_in,
128             encoding_out => $self->encoding_out,
129             });
130 2         14 my $vcard = $addressBook->load_file($filename)->vcards->[0];
131              
132 2         7 $self->_data($vcard->_data);
133              
134 2         19 return $self;
135             }
136              
137             =head2 load_string($string)
138              
139             Returns $self in case you feel like chaining. This method assumes $string is
140             decoded (but not MIME decoded).
141              
142             =cut
143              
144             sub load_string {
145 3     3 1 15 my ( $self, $string ) = @_;
146              
147 3         62 my $addressBook = vCard::AddressBook->new({
148             encoding_in => $self->encoding_in,
149             encoding_out => $self->encoding_out,
150             });
151 3         19 my $vcard = $addressBook->load_string($string)->vcards->[0];
152              
153 2         34 $self->_data($vcard->_data);
154              
155 2         7 return $self;
156             }
157              
158             =head2 as_string()
159              
160             Returns the vCard as a string.
161              
162             =cut
163              
164             sub as_string {
165 7     7 1 988 my ($self) = @_;
166 7         76 my $vcard = Text::vCard->new( { encoding_out => $self->encoding_out } );
167              
168 7         23 my $phones = $self->_data->{phones};
169 7         15 my $addresses = $self->_data->{addresses};
170 7         14 my $email_addresses = $self->_data->{email_addresses};
171              
172 7         30 $self->_build_simple_nodes( $vcard, $self->_data );
173 7         25 $self->_build_name_node( $vcard, $self->_data );
174 7 50       28 $self->_build_org_node( $vcard, $self->_data->{org} ) if $self->_data->{org};
175 7 100       23 $self->_build_phone_nodes( $vcard, $phones ) if $phones;
176 7 100       24 $self->_build_address_nodes( $vcard, $addresses ) if $addresses;
177 7 100       20 $self->_build_email_address_nodes( $vcard, $email_addresses )
178             if $email_addresses;
179              
180 7         20 return $vcard->as_string;
181             }
182              
183             sub _simple_node_types {
184 18     18   6213 qw/full_name title photo birthday timezone version/;
185             #geo, too?
186             }
187              
188             sub _build_simple_nodes {
189 7     7   9 my ( $self, $vcard, $data ) = @_;
190              
191 7         16 foreach my $node_type ( $self->_simple_node_types ) {
192 42 100       52 if ( $node_type eq 'full_name' ) {
193 7 100       21 next unless $data->{full_name};
194 4         17 $vcard->fullname( $data->{full_name} );
195             } else {
196 35 100       113 next unless $data->{$node_type};
197 23         80 $vcard->$node_type( $data->{$node_type} );
198             }
199             }
200             }
201              
202             sub _build_complex_node {
203 28     28   29 my ( $self, $vcard, $node_type, $data ) = @_;
204 28 50       49 croak '$data must be HASHREF' unless ref $data eq 'HASH';
205 28         73 $vcard->add_node( { node_type => $node_type, data => [ $data ] } );
206             }
207              
208             sub _build_org_node {
209 0     0   0 my ( $self, $vcard, $data ) = @_;
210            
211 0 0       0 my $value = join ';', @{ $data || [] };
  0         0  
212 0         0 $self->_build_complex_node( $vcard, 'ORG', { value => $value } );
213             }
214              
215             sub _build_name_node {
216 7     7   10 my ( $self, $vcard, $data ) = @_;
217              
218 7 100       13 my $value = join ',', @{ $data->{family_names} || [] };
  7         29  
219 7 100       11 $value .= ';' . join ',', @{ $data->{given_names} || [] };
  7         26  
220 7 100       9 $value .= ';' . join ',', @{ $data->{other_names} || [] };
  7         33  
221 7 100       9 $value .= ';' . join ',', @{ $data->{honorific_prefixes} || [] };
  7         26  
222 7 100       8 $value .= ';' . join ',', @{ $data->{honorific_suffixes} || [] };
  7         30  
223              
224              
225 7 100       28 $self->_build_complex_node( $vcard, 'N', { value => $value } )
226             if $value ne ';;;;';
227             }
228              
229             sub _build_phone_nodes {
230 4     4   8 my ( $self, $vcard, $phones ) = @_;
231              
232 4         8 foreach my $phone (@$phones) {
233              
234             # TODO: better error handling
235 8 50       20 croak "'number' attr missing from 'phones'" unless $phone->{number};
236             croak "'type' attr in 'phones' should be an arrayref"
237 8 50 33     43 if ( $phone->{type} && ref( $phone->{type} ) ne 'ARRAY' );
238              
239 8   50     17 my $type = $phone->{type} || [];
240 8         9 my $preferred = $phone->{preferred};
241 8         10 my $number = $phone->{number};
242              
243 8         7 my $params = [];
244 8         36 push @$params, { type => $_ } foreach @$type;
245 8 100       37 push @$params, { pref => $preferred } if $preferred;
246              
247 8         24 $self->_build_complex_node( $vcard, 'TEL', { params => $params, value => $number } );
248             }
249             }
250              
251             sub _build_address_nodes {
252 4     4   8 my ( $self, $vcard, $addresses ) = @_;
253              
254 4         8 foreach my $address (@$addresses) {
255              
256             croak "'type' attr in 'addresses' should be an arrayref"
257 8 50 33     48 if ( $address->{type} && ref( $address->{type} ) ne 'ARRAY' );
258              
259 8   50     18 my $type = $address->{type} || [];
260 8         10 my $preferred = $address->{preferred};
261              
262 8         8 my $params = [];
263 8         28 push @$params, { type => $_ } foreach @$type;
264 8 50       15 push @$params, { pref => $preferred } if $preferred;
265              
266             my $value = join ';',
267             $address->{pobox} || '',
268             $address->{extended} || '',
269             $address->{street} || '',
270             $address->{city} || '',
271             $address->{region} || '',
272             $address->{post_code} || '',
273 8   50     93 $address->{country} || '';
      50        
      50        
      50        
      50        
      50        
      50        
274              
275 8         19 $self->_build_complex_node( $vcard, 'ADR', { params => $params, value => $value } );
276             }
277             }
278              
279             sub _build_email_address_nodes {
280 4     4   7 my ( $self, $vcard, $email_addresses ) = @_;
281              
282 4         7 foreach my $email_address (@$email_addresses) {
283              
284             # TODO: better error handling
285             croak "'address' attr missing from 'email_addresses'"
286 8 50       20 unless $email_address->{address};
287             croak "'type' attr in 'email_addresses' should be an arrayref"
288             if ( $email_address->{type}
289 8 50 33     35 && ref( $email_address->{type} ) ne 'ARRAY' );
290              
291 8   50     15 my $type = $email_address->{type} || [];
292 8         10 my $preferred = $email_address->{preferred};
293              
294 8         8 my $params = [];
295 8         23 push @$params, { type => $_ } foreach @$type;
296 8 100       18 push @$params, { pref => $preferred } if $preferred;
297              
298             # TODO: better error handling
299 8         9 my $value = $email_address->{address};
300              
301 8         20 $self->_build_complex_node( $vcard, 'EMAIL', { params => $params, value => $value } );
302             }
303             }
304              
305             =head2 as_file($filename)
306              
307             Write data in vCard format to $filename.
308              
309             Dies if not successful.
310              
311             =cut
312              
313             sub as_file {
314 1     1 1 687 my ( $self, $filename ) = @_;
315 1         7 my $file = $self->_path($filename);
316 1         64 $file->spew( $self->_iomode_out, $self->as_string );
317 1         1731 return $file;
318             }
319              
320             =head1 SIMPLE GETTERS/SETTERS
321              
322             These methods accept and return strings.
323              
324             =head2 version()
325              
326             Version number of the vcard. Defaults to '4.0'
327              
328             =head2 full_name()
329              
330             A person's entire name as they would like to see it displayed.
331              
332             =head2 title()
333              
334             A person's position or job.
335              
336             =head2 photo()
337              
338             This should be a link. Accepts a string or a URI object. This method
339             always returns a L object.
340              
341             TODO: handle binary images using the data uri schema
342              
343             =head2 birthday()
344              
345             =head2 timezone()
346              
347              
348             =head1 COMPLEX GETTERS/SETTERS
349              
350             These methods accept and return array references rather than simple strings.
351              
352             =head2 family_names()
353              
354             Accepts/returns an arrayref of family names (aka surnames).
355              
356             =head2 given_names()
357              
358             Accepts/returns an arrayref.
359              
360             =head2 other_names()
361              
362             Accepts/returns an arrayref of names which don't qualify as family_names or
363             given_names.
364              
365             =head2 honorific_prefixes()
366              
367             Accepts/returns an arrayref. eg C<[ 'Dr.' ]>
368              
369             =head2 honorific_suffixes()
370              
371             Accepts/returns an arrayref. eg C<[ 'Jr.', 'MD' ]>
372              
373             =head2 phones()
374              
375             Accepts/returns an arrayref that looks like:
376              
377             [
378             { type => ['work'], number => '651-290-1234', preferred => 1 },
379             { type => ['cell'], number => '651-290-1111' },
380             ]
381              
382             =head2 addresses()
383              
384             Accepts/returns an arrayref that looks like:
385              
386             [
387             { type => ['work'], street => 'Main St', preferred => 0 },
388             { type => ['home'],
389             pobox => 1234,
390             extended => 'asdf',
391             street => 'Army St',
392             city => 'Desert Base',
393             region => '',
394             post_code => '',
395             country => 'USA',
396             preferred => 1,
397             },
398             ]
399              
400             =head2 email_addresses()
401              
402             Accepts/returns an arrayref that looks like:
403              
404             [
405             { type => ['work'], address => 'bbanner@ssh.secret.army.mil' },
406             { type => ['home'], address => 'bbanner@timewarner.com', preferred => 1 },
407             ]
408              
409             =cut
410              
411 11     11 1 2276 sub version { shift->_setget( 'version', @_ ) }
412 8     8 1 540 sub full_name { shift->_setget( 'full_name', @_ ) }
413 8     8 1 1873 sub family_names { shift->_setget( 'family_names', @_ ) }
414 8     8 1 20 sub given_names { shift->_setget( 'given_names', @_ ) }
415 7     7 1 20 sub other_names { shift->_setget( 'other_names', @_ ) }
416 8     8 1 127 sub honorific_prefixes { shift->_setget( 'honorific_prefixes', @_ ) }
417 8     8 1 21 sub honorific_suffixes { shift->_setget( 'honorific_suffixes', @_ ) }
418 12     12 1 493 sub title { shift->_setget( 'title', @_ ) }
419 24     24 1 10322 sub photo { shift->_setget( 'photo', @_ ) }
420 12     12 1 2308 sub birthday { shift->_setget( 'birthday', @_ ) }
421 12     12 1 2255 sub timezone { shift->_setget( 'timezone', @_ ) }
422 8     8 1 18 sub phones { shift->_setget( 'phones', @_ ) }
423 8     8 1 1569 sub addresses { shift->_setget( 'addresses', @_ ) }
424 8     8 1 1826 sub email_addresses { shift->_setget( 'email_addresses', @_ ) }
425 0     0 0 0 sub organization { shift->_setget( 'organization', @_ ) }
426              
427             sub _setget {
428 142     142   163 my ( $self, $attr, $value ) = @_;
429              
430 142 50 100     488 $value = URI->new($value)
      66        
431             if $value && $attr eq 'photo' && ref $value =~ /^URI/;
432              
433 142 100       413 $self->_data->{$attr} = $value if $value;
434              
435 142         514 return $self->_data->{$attr};
436             }
437              
438             =head1 AUTHOR
439              
440             Eric Johnson (kablamo), github ~!at!~ iijo dot org
441              
442             =head1 ACKNOWLEDGEMENTS
443              
444             Thanks to L for making this module possible by
445             donating a significant amount of developer time.
446              
447             =cut
448              
449             1;