File Coverage

blib/lib/RDF/vCard/Entity.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package RDF::vCard::Entity;
2              
3 1     1   16 use 5.008;
  1         4  
  1         44  
4 1     1   4 use strict;
  1         2  
  1         26  
5 1     1   4 use warnings;
  1         2  
  1         31  
6 1     1   5 no warnings qw(uninitialized);
  1         1  
  1         28  
7              
8 1     1   522 use JSON qw[];
  0            
  0            
9             use RDF::TrineX::Functions
10             -shortcuts,
11             statement => { -as => 'rdf_statement' },
12             iri => { -as => 'rdf_resource' };
13              
14             sub V { return 'http://www.w3.org/2006/vcard/ns#' . shift; }
15             sub VX { return 'http://buzzword.org.uk/rdf/vcardx#' . shift; }
16             sub RDF { return 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' . shift; }
17             sub XSD { return 'http://www.w3.org/2001/XMLSchema#' . shift; }
18              
19             use namespace::clean;
20              
21             use overload '""' => \&to_string;
22             our $VERSION = '0.012';
23              
24             sub new
25             {
26             my ($class, %options) = @_;
27             $options{profile} ||= 'VCARD';
28             $options{lines} ||= [];
29             $options{components} ||= [];
30             $options{node} ||= $class->_node;
31             bless { %options }, $class;
32             }
33              
34             sub _node
35             {
36             my ($class) = @_;
37             return RDF::Trine::Node::Blank->new;
38             }
39              
40             sub profile
41             {
42             my ($self) = @_;
43             return $self->{profile};
44             }
45              
46             sub lines
47             {
48             my ($self) = @_;
49             return $self->{lines};
50             }
51              
52             sub components
53             {
54             my ($self) = @_;
55             return $self->{components};
56             }
57              
58             sub add
59             {
60             my ($self, $line) = @_;
61             push @{ $self->lines }, $line;
62             $self->_entity_order_fu($line);
63             return $self;
64             }
65              
66             sub add_component
67             {
68             my ($self, $c) = @_;
69             push @{ $self->components }, $c;
70             return $self;
71             }
72              
73             sub get
74             {
75             my ($self, $property) = @_;
76             return grep {
77             lc $_->property eq lc $property
78             } @{ $self->lines };
79             }
80              
81             sub matches
82             {
83             my ($self, $property, $regexp) = @_;
84             return grep {
85             $_->value_to_string =~ $regexp;
86             } $self->get($property);
87             }
88              
89             sub entity_order
90             {
91             my ($self) = @_;
92            
93             return $self->{property}{'sort-string'}
94             || $self->{property}{'n'}
95             || $self->{property}{'n-faked'}
96             || $self->{property}{'fn'}
97             || $self->{property}{'nickname'};
98             }
99              
100             sub _entity_order_fu
101             {
102             my ($self, $line) = @_;
103            
104             if ($line->property =~ /^(sort.string|n|fn|nickname)$/i)
105             {
106             my $x = $line->value_to_string;
107             $self->{property}{ lc $line->property } = $x if length $x;
108            
109             if (lc $line->property eq 'fn')
110             {
111             my @parts = split /\s+/, $x;
112             my $last = pop @parts;
113             unshift @parts, $last;
114             $self->{property}{'n-faked'} = join ';', @parts;
115             }
116             }
117             return $self;
118             }
119              
120             sub to_string
121             {
122             my ($self) = @_;
123            
124             my @lines = sort {
125             $a->property_order cmp $b->property_order;
126             } @{$self->lines};
127              
128             my @components = sort {
129             $a->entity_order cmp $b->entity_order;
130             } @{$self->components};
131              
132             my $str = sprintf("BEGIN:%s\r\n", $self->profile);
133             foreach my $line (@lines)
134             {
135             $str .= $line . "\r\n";
136             }
137             foreach my $component (@components)
138             {
139             $str .= $component;
140             }
141             $str .= sprintf("END:%s\r\n", $self->profile);
142            
143             return $str;
144             }
145              
146             sub node
147             {
148             my ($self) = @_;
149             return $self->{node};
150             }
151              
152             sub add_to_model
153             {
154             my ($self, $model) = @_;
155            
156             $model->add_statement(rdf_statement(
157             $self->node,
158             rdf_resource( RDF('type') ),
159             rdf_resource( V('VCard') ),
160             ));
161              
162             foreach my $line (@{ $self->lines })
163             {
164             $line->add_to_model($model, $self->node);
165             }
166            
167             return $self;
168             }
169              
170             sub to_jcard
171             {
172             my ($self, $hashref) = @_;
173             return ($hashref ? $self->TO_JSON : JSON::to_json($self));
174             }
175              
176             {
177             my @singular = qw(fn n bday tz geo sort-string uid class rev
178             anniversary birth dday death gender kind prodid sex version);
179             my @typed = qw(email tel adr label impp);
180            
181             sub TO_JSON
182             {
183             my ($self) = @_;
184             my $object = {};
185            
186             foreach my $line (@{ $self->lines })
187             {
188             my $p = lc $line->property;
189            
190             if ($p eq 'n')
191             {
192             my $o;
193             my @sp = qw(family-name given-name additional-name
194             honorific-prefix honorific-suffix);
195             for my $i (0..4)
196             {
197             if ($line->nvalue->[$i] and @{$line->nvalue->[$i]})
198             {
199             $o->{ $sp[$i] } = [ @{$line->nvalue->[$i]} ];
200             }
201             }
202             push @{$object->{n}}, $o;
203             }
204             elsif ($p eq 'org')
205             {
206             my @components = map { $_->[0] } @{$line->nvalue};
207             my $o = { 'organization-name' => shift @components };
208             $o->{'organization-unit'} = \@components;
209             push @{$object->{n}}, $o;
210             }
211             elsif ($p eq 'adr')
212             {
213             my $o;
214             while (my ($k, $v) = each %{$line->type_parameters})
215             {
216             push @{$o->{$k}}, (ref $v eq 'ARRAY' ? @$v : $v);
217             }
218             if ($o->{type})
219             {
220             $o->{type} = [ sort map {lc $_} @{ $o->{type} } ]
221             }
222             my @sp = qw(post-office-box extended-address street-address
223             locality region country-name postal-code);
224             for my $i (0..6)
225             {
226             if ($line->nvalue->[$i] and @{$line->nvalue->[$i]})
227             {
228             $o->{ $sp[$i] } = [ @{$line->nvalue->[$i]} ];
229             }
230             }
231             push @{$object->{adr}}, $o;
232             }
233             elsif ($p eq 'categories')
234             {
235             push @{$object->{categories}}, '@@TODO';
236             }
237             elsif ($p eq 'geo')
238             {
239             $object->{geo} = {
240             latitude => $line->nvalue->[0][0],
241             longitude => $line->nvalue->[1][0],
242             };
243             }
244             elsif (grep { $_ eq $p } @typed)
245             {
246             my $o = {};
247             while (my ($k, $v) = each %{$line->type_parameters})
248             {
249             push @{$o->{$k}}, (ref $v eq 'ARRAY' ? @$v : $v);
250             }
251             $o->{value} = $line->nvalue->[0][0];
252             if ($o->{type})
253             {
254             $o->{type} = [ sort map {lc $_} @{ $o->{type} } ]
255             }
256            
257             push @{ $object->{$p} }, $o;
258             }
259             elsif (grep { $_ eq $p } @singular)
260             {
261             $object->{$p} ||= $line->nvalue->[0][0];
262             }
263             else
264             {
265             push @{ $object->{$p} }, $line->nvalue->[0][0];
266             }
267             }
268            
269             return $object;
270             }
271             }
272              
273             1;
274              
275             __END__
276              
277             =head1 NAME
278              
279             RDF::vCard::Entity - represents a single vCard
280              
281             =head1 DESCRIPTION
282              
283             Instances of this class correspond to individual vCard objects, though
284             it could potentially be used as basis for other RFC 2425-based formats
285             such as iCalendar.
286              
287             =head2 Constructor
288              
289             =over
290              
291             =item * C<< new(%options) >>
292              
293             Returns a new RDF::vCard::Entity object.
294              
295             The only option worth worrying about is B<profile> which sets the
296             profile for the entity. This defaults to "VCARD".
297              
298             RDF::vCard::Entity overloads stringification, so you can do the following:
299              
300             my $vcard = RDF::vCard::Entity->new;
301             print $vcard if $vcard =~ /VCARD/i;
302              
303             =back
304              
305             =head2 Methods
306              
307             =over
308              
309             =item * C<< to_string() >>
310              
311             Formats the object according to RFC 2425 and RFC 2426.
312              
313             =item * C<< to_jcard() >>
314              
315             Formats the object according to L<http://microformats.org/wiki/jcard>.
316              
317             C<< to_jcard(1) >> will return the same data but without the JSON stringification.
318              
319             =item * C<< add_to_model($model) >>
320              
321             Given an RDF::Trine::Model, adds triples to the model for this entity.
322              
323             =item * C<< node() >>
324              
325             Returns an RDF::Trine::Node::Blank identifying this entity.
326              
327             =item * C<< entity_order() >>
328              
329             Returns a string along the lines of "Surname;Forename" useful for
330             sorting a list of entities.
331              
332             =item * C<< profile() >>
333              
334             Returns the entity type - e.g. "VCARD".
335              
336             =item * C<< lines() >>
337              
338             Returns an arrayref of L<RDF::vCard::Line> objects in the order they
339             were originally added.
340              
341             This excludes the "BEGIN:VCARD" and "END:VCARD" lines.
342              
343             =item * C<< add($line) >>
344              
345             Add a L<RDF::vCard::Line>.
346              
347             =item * C<< get($property) >>
348              
349             Returns a list of L<RDF::vCard::Line> objects for the given property.
350              
351             e.g.
352              
353             print "It has an address!\n" if ($vcard->get('ADR'));
354              
355             =item * C<< matches($property, $regexp) >>
356              
357             Checks to see if a property's value matches a regular expression.
358              
359             print "In London\n" if $vcard->matches(ADR => /London/);
360              
361             =item * C<< add_component($thing) >>
362              
363             Adds a nested entity within this one. This method is unused for vCard, but
364             is a hook for the benefit of L<RDF::iCalendar>.
365              
366             =item * C<< components >>
367              
368             Lists nested entities within this one.
369              
370             =back
371              
372             =begin private
373              
374             =item TO_JSON
375              
376             =end private
377              
378             =head1 SEE ALSO
379              
380             L<RDF::vCard>.
381              
382             =head1 AUTHOR
383              
384             Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
385              
386             =head1 COPYRIGHT
387              
388             Copyright 2011 Toby Inkster
389              
390             This library is free software; you can redistribute it and/or modify it
391             under the same terms as Perl itself.
392