File Coverage

lib/XML/Feed/Aggregator.pm
Criterion Covered Total %
statement 20 22 90.9
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 28 30 93.3


line stmt bran cond sub pod time code
1             package XML::Feed::Aggregator;
2             BEGIN {
3 1     1   4048 $XML::Feed::Aggregator::VERSION = '0.0401';
4             }
5 1     1   16 use Moose;
  1         5  
  1         14  
6 1     1   8793 use MooseX::Types::Moose qw/ArrayRef Str/;
  1         3  
  1         16  
7 1         10 use MooseX::Types -declare => [qw/
8             Sources Feed AtomFeed AtomEntry
9             RSSFeed RSSEntry Feeds Entry
10 1     1   6752 /];
  1         3  
11 1     1   10649 use MooseX::Types::URI 'Uri';
  1         184755  
  1         8  
12 1     1   2075 use Moose::Util::TypeConstraints;
  1         4  
  1         7  
13 1     1   2057 use URI;
  1         3  
  1         21  
14 1     1   626 use XML::Feed;
  0            
  0            
15             use Try::Tiny;
16             use namespace::autoclean;
17              
18             class_type RSSEntry, {class => 'XML::Feed::Entry::Format::RSS'};
19             class_type AtomEntry, {class => 'XML::Feed::Entry::Format::Atom'};
20             class_type AtomFeed, {class => 'XML::Feed::Format::RSS'};
21             class_type RSSFeed, {class => 'XML::Feed::Format::Atom'};
22              
23             subtype Sources,
24             as ArrayRef[Uri];
25              
26             coerce Sources,
27             from ArrayRef[Str],
28             via {
29             [ map { Uri->coerce($_) } @{$_} ]
30             };
31              
32             subtype Feed,
33             as AtomFeed|RSSFeed,
34             message { "$_ is not a Feed!" };
35              
36             subtype Entry,
37             as AtomEntry|RSSEntry,
38             message { "$_ is not an Entry!" };
39              
40             has sources => (
41             is => 'rw',
42             isa => Sources,
43             traits => [qw/Array/],
44             default => sub { [] },
45             coerce => 1,
46             handles => {
47             all_sources => 'elements',
48             add_source => 'push',
49             },
50             );
51              
52             has feeds => (
53             is => 'rw',
54             isa => ArrayRef[Feed],
55             traits => [qw/Array/],
56             default => sub { [] },
57             handles => {
58             all_feeds => 'elements',
59             add_feed => 'push',
60             feed_count => 'count',
61             },
62             );
63              
64             has entries => (
65             is => 'rw',
66             isa => ArrayRef[Entry],
67             traits => [qw/Array/],
68             default => sub { [] },
69             handles => {
70             all_entries => 'elements',
71             add_entry => 'push',
72             sort_entries => 'sort_in_place',
73             map_entries => 'map',
74             entry_count => 'count',
75             }
76             );
77              
78             has _errors => (
79             is => 'rw',
80             isa => ArrayRef[Str],
81             traits => [qw/Array/],
82             default => sub { [] },
83             handles => {
84             errors => 'elements',
85             error_count => 'count',
86             add_error => 'push',
87             }
88             );
89              
90             with 'XML::Feed::Aggregator::Sort';
91             with 'XML::Feed::Aggregator::Deduper';
92              
93             sub fetch {
94             my ($self) = @_;
95              
96             for my $uri ($self->all_sources) {
97             try {
98             $self->add_feed(XML::Feed->parse($uri));
99             }
100             catch {
101             $self->add_error($uri->as_string." - failed: $_");
102             };
103             }
104              
105             return $self;
106             }
107              
108             sub aggregate {
109             my ($self) = @_;
110              
111             return $self if $self->entry_count > 0;
112              
113             for my $feed ($self->all_feeds) {
114             $self->add_entry($feed->entries);
115             }
116              
117             $self->grep_entries(sub { defined $_ });
118              
119             return $self;
120             }
121              
122             sub grep_entries {
123             my ($self, $filter) = @_;
124              
125             my @entries = grep { $filter->($_) } $self->all_entries;
126             $self->entries(\@entries);
127              
128             return $self;
129             }
130              
131             sub to_feed {
132             my ($self, @params) = @_;
133              
134             my $feed = XML::Feed->new(@params);
135              
136             for my $entry ($self->all_entries) {
137             $feed->add_entry($entry);
138             }
139              
140             return $feed;
141             }
142              
143             1;
144              
145              
146             =pod
147              
148             =head1 NAME
149              
150             XML::Feed::Aggregator
151              
152             =head1 VERSION
153              
154             version 0.0401
155              
156             =head1 SYNOPSIS
157              
158             use XML::Feed::Aggregator;
159              
160             my $syndicator = XML::Feed::Aggregator->new(
161             sources => [
162             "http://blogs.perl.org/atom.xml",
163             "http://news.ycombinator.com/"
164             ],
165             feeds => [ XML::Feed->parse('./slashdot.rss') ]
166            
167             )->fetch->aggregate->deduplicate->sort_by_date;
168              
169             $syndicator->grep_entries(sub {
170             $_->author ne 'James'
171             })->deduplicate;
172              
173             say $syndicator->map_entries(sub { $_->title } );
174              
175             =head1 DESCRIPTION
176              
177             This module aggregates feeds from different sources for easy filtering and sorting.
178              
179             =head1 NAME
180              
181             XML::Feed::Aggregator - Simple feed aggregator
182              
183             =head1 ATTRIBUTES
184              
185             =head2 sources
186              
187             Sources to be fetched and loaded into the feeds attribute.
188              
189             Coerces to an ArrayRef of URI objects.
190              
191             =head2 feeds
192              
193             An ArrayRef of XML::Feed objects.
194              
195             =head2 entries
196              
197             List of XML::Feed::Entry objects obtained from each feed
198              
199             =head1 METHODS
200              
201             =head2 fetch
202              
203             Convert each source into an XML::Feed object, via XML::Feed->parse()
204              
205             For a remote address this involves a http request.
206              
207             =head2 aggregate
208              
209             Combine all feed entries into a single 'entries' attribute
210              
211             =head2 to_feed
212              
213             Export aggregated feed to a single XML::Feed object.
214              
215             All parameters passed to L<XML::Feed> constructor.
216              
217             =head1 FEED METHODS
218              
219             Methods relating to the 'feeds' attribute
220              
221             =head2 add_feed
222              
223             Add a new feed to the 'feeds' attribute.
224              
225             =head2 all_feeds
226              
227             Return all feeds as an Array.
228              
229             =head2 feed_count
230              
231             Number of feeds.
232              
233             =head1 ENTRY METHODS
234              
235             Methods relating to the 'entries' attribute
236              
237             =head2 sort_entries
238              
239             See L<XML::Feed::Aggregator::Sort>
240              
241             =head2 map_entries
242              
243             Loop over all entries using $_ within a CodeRef.
244              
245             =head2 grep_entries
246              
247             Grep through entries using $_ within a CodeRef.
248              
249             =head2 add_entry
250              
251             Add a new entry to the aggregated feed.
252              
253             =head2 entry_count
254              
255             Number of entries.
256              
257             =head2 all_entries
258              
259             Returns all entries as an array
260              
261             =head1 ROLES
262              
263             This class consumes the following roles for sorting and deduplication.
264              
265             L<XML::Feed::Aggregator::Deduper>
266             L<XML::Feed::Aggregator::Sort>
267              
268             =head1 ERROR HANDLING
269              
270             =head2 error_count
271              
272             Number of errors occured.
273              
274             =head2 errors
275              
276             An ArrayRef of errors whilst fetching / parsing feeds.
277              
278             =head1 SEE ALSO
279              
280             L<XML::Feed::Aggregator::Deduper>
281              
282             L<XML::Feed::Aggregator::Sort>
283              
284             L<App::Syndicator> L<Perlanet> L<XML::Feed> L<Feed::Find>
285              
286             =head1 AUTHOR
287              
288             Robin Edwards <robin.ge@gmail.com>
289              
290             =head1 COPYRIGHT AND LICENSE
291              
292             This software is copyright (c) 2011 by Robin Edwards.
293              
294             This is free software; you can redistribute it and/or modify it under
295             the same terms as the Perl 5 programming language system itself.
296              
297             =cut
298              
299              
300             __END__
301