File Coverage

blib/lib/Feed/Data.pm
Criterion Covered Total %
statement 85 110 77.2
branch 5 10 50.0
condition 1 8 12.5
subroutine 24 28 85.7
pod 4 4 100.0
total 119 160 74.3


line stmt bran cond sub pod time code
1             package Feed::Data;
2              
3 13     13   1334090 use Moo;
  13         129678  
  13         65  
4 13     13   25771 use MooX::HandlesVia;
  13         137477  
  13         79  
5 13     13   11066 use Types::Standard qw/Any Str ArrayRef HashRef Optional/;
  13         1322345  
  13         210  
6 13     13   18676 use Carp qw(carp croak);
  13         27  
  13         939  
7 13     13   5331 use Feed::Data::Parser;
  13         51  
  13         500  
8 13     13   6672 use Feed::Data::Stream;
  13         51  
  13         535  
9 13     13   5881 use Feed::Data::Object;
  13         63  
  13         497  
10 13     13   7658 use HTML::TableContent;
  13         1152380  
  13         514  
11 13     13   10076 use JSON;
  13         138805  
  13         92  
12 13     13   2117 use Compiled::Params::OO qw/cpo/;
  13         37  
  13         192  
13 13     13   7908 use XML::RSS::LibXML;
  13         953351  
  13         119  
14 13     13   13695 use Text::CSV_XS qw/csv/;
  13         161491  
  13         956  
15 13     13   5994 use YAML::XS qw//;
  13         36483  
  13         307  
16              
17 13     13   253 use 5.006;
  13         54  
18             our $VERSION = '0.08';
19              
20             our $validate;
21             BEGIN {
22             $validate = cpo(
23             write => [Any, Str, Optional->of(Str)],
24 0         0 render => [Any, Optional->of(Str), {default => sub { 'text' }}],
25 13     13   169 generate => [Any, Optional->of(Str), {default => sub { 'text' }}],
  0         0  
26             rss => [Any, Optional->of(Str)],
27             raw => [Any, Optional->of(Str)],
28             text => [Any, Optional->of(Str)],
29             json => [Any, Optional->of(Str)],
30             yaml => [Any, Optional->of(Str)],
31             csv => [Any, Optional->of(Str)],
32             table => [Any, Optional->of(Str)],
33             convert_feed => [Any, Str, Str]
34             );
35             }
36              
37             has 'feed' => (
38             is => 'rw',
39             isa => ArrayRef
40             lazy => 1,
41             default => sub { [ ] },
42             handles_via => 'Array',
43             handles => {
44             all => 'elements',
45             count => 'count',
46             get => 'get',
47             pop => 'pop',
48             delete => 'delete',
49             insert => 'unshift',
50             is_empty => 'is_empty',
51             clear => 'clear',
52             }
53             );
54              
55             has title => (
56             is => 'rw',
57             isa => Str
58             );
59              
60             has description => (
61             is => 'rw',
62             isa => Str
63             );
64              
65             has link => (
66             is => 'rw',
67             isa => Str
68             );
69              
70             has rss_channel => (
71             is => 'rw',
72             isa => HashRef
73             default => sub { { } }
74             );
75              
76             sub parse {
77 13     13 1 447 my ($self, $stream) = @_;
78              
79 13 50       63 if (!$stream) {
80 0         0 croak "No stream was provided to parse().";
81             }
82              
83 13         150 my $parser = Feed::Data::Parser->new(
84             stream => Feed::Data::Stream->new(stream => $stream)->open_stream
85             )->parse;
86              
87 13         38706 my $parsed = $parser->parse;
88 13         643 my $feed = $parser->feed;
89 13 50       217 return carp 'parse failed' unless $feed;
90              
91 13 100       282 if ($self->count >= 1) {
92 1         67 $self->insert(@{ $parsed });
  1         20  
93             } else {
94 12         1131 $self->feed($parsed);
95             }
96              
97 13         786 return 1;
98             }
99              
100             sub write {
101 6     6 1 101 my ($self, $stream, $type) = $validate->write->(@_);
102 6   50     269 Feed::Data::Stream->new(stream => $stream)->write_file($self->render($type || 'text'));
103 6         76 return 1;
104             }
105              
106             sub render {
107 6     6 1 388 my ( $self, $format ) = $validate->render->(@_);
108 6         130 $format = '_' . $format;
109 6         68 return $self->$format('render');
110             }
111              
112             sub generate {
113 0     0 1 0 my ( $self, $format ) = $validate->generate->(@_);
114 0         0 $format = '_' . $format;
115 0         0 return $self->$format('generate');
116             }
117              
118             sub _rss {
119 0     0   0 my ($self, $type) = $validate->rss->(@_);
120 0         0 my $rss = XML::RSS::LibXML->new(version => '1.0');
121             $rss->channel(
122             title => $self->title || "Feed::Data",
123             link => $self->link || "Feed::Data",
124             description => $self->description || "Feed::Data",
125 0   0     0 %{$self->rss_channel}
  0   0     0  
      0        
126             );
127 0         0 my @render = $self->_convert_feed('generate', 'json');
128 0         0 for (@render) {
129             $rss->add_item(
130 0         0 %{$_}
  0         0  
131             );
132             }
133 0         0 return $rss->as_string;
134             }
135              
136             sub _raw {
137 0     0   0 my ( $self, $type ) = $validate->raw->(@_);
138 0         0 my @render = $self->_convert_feed($type, 'raw');
139 0 0       0 if ($type eq q{render}) {
140 0         0 return join "\n", @render;
141             } else {
142 0         0 return \@render;
143             }
144             }
145              
146             sub _text {
147 1     1   9 my ( $self, $type ) = $validate->text->(@_);
148 1         25 my @render = $self->_convert_feed($type, 'text');
149 1 50       5 if ($type eq q{render}) {
150 1         12 return join "\n", @render;
151             } else {
152 0         0 return \@render;
153             }
154             }
155              
156             sub _json {
157 1     1   9 my ( $self, $type ) = $validate->json->(@_);
158 1         18 my @render = $self->_convert_feed('generate', 'json');
159 1         29 my $json = JSON->new->allow_nonref;
160 1         37 return $json->pretty->encode( \@render );
161             }
162              
163             sub _yaml {
164 2     2   40 my ( $self, $type ) = $validate->yaml->(@_);
165 2         44 my @render = $self->_convert_feed('generate', 'json');
166 2         278 return YAML::XS::Dump \@render;
167             }
168              
169             sub _table {
170 1     1   8 my ( $self, $type ) = $validate->table->(@_);
171 1         21 my @render = $self->_convert_feed('generate', 'json');
172 1         26 my $table = HTML::TableContent->new();
173 1         42 $table->create_table({
174             aoh => \@render,
175             order => [
176             qw/author title description category comment content date image link permalink tagline/
177             ]
178             })->render;
179             }
180              
181             sub _styled_table {
182 0     0   0 my ( $self, $type ) = $validate->table->(@_);
183 0         0 my @render = $self->_convert_feed('generate', 'json');
184 0         0 my $table = HTML::TableContent->new();
185 0         0 $table->create_table({
186             aoh => \@render,
187             order => [
188             qw/author title description category comment content date image link permalink tagline/
189             ]
190             })->render . '<style>
191             table {
192             font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
193             border-collapse: collapse;
194             width: 100%;
195             }
196              
197             td, th {
198             word-break: break-word;
199             min-width: 100px;
200             border: 1px solid #ddd;
201             padding: 8px;
202             }
203              
204             tr:nth-child(even){background-color: #f2f2f2;}
205              
206             tr:hover {background-color: #ddd;}
207              
208             th {
209             padding-top: 12px;
210             padding-bottom: 12px;
211             text-align: left;
212             background-color: #4c65af;
213             color: white;
214             }
215             </style>'
216             }
217              
218             sub _csv {
219 1     1   13 my ( $self, $type ) = $validate->json->(@_);
220 1         20 my @render = $self->_convert_feed('generate', 'json');
221 1         3 my $string;
222 1         8 csv (in => \@render, out => \$string);
223 1         571 return $string;
224             }
225              
226             sub _convert_feed {
227 6     6   44 my ( $self, $type, $format ) = $validate->convert_feed->(@_);
228 6         88 my @render;
229 6         13 foreach my $object ( @{$self->feed} ) {
  6         124  
230 12         138 push @render, $object->$type($format);
231             }
232 6         45 return @render;
233             }
234              
235             __PACKAGE__->meta->make_immutable;
236              
237             1;
238              
239             __END__
240              
241             =head1 NAME
242              
243             Feed::Data - dynamic data feeds
244              
245             =head1 VERSION
246              
247             Version 0.08
248              
249             =head1 SYNOPSIS
250              
251             use Feed::Data;
252              
253             my $feed = Feed::Data->new();
254             $feed->parse( 'https://services.parliament.uk/calendar/lords_main_chamber.rss' );
255              
256             $feed->all;
257             $feed->count;
258             $feed->delete($index);
259             $feed->get($index);
260              
261             $feed->write( 'path/to/empty.rss', 'rss' );
262             my $feed_text = $feed->render('rss');
263              
264             foreach my $object ( $feed->all ) {
265             $object->render('text'); # text, html, xml..
266             $object->hash('text'); # text, html, xml...
267             $object->fields('title', 'description'); # returns title and description object
268             $object->edit(title => 'TTI', description => 'Should have been a PWA.'); # sets
269            
270             $object->title->text;
271             $object->link->raw;
272             $object->description->text;
273             $object->image->raw;
274             $object->date->text;
275            
276             $entry->title->as_text;
277             }
278              
279             ...
280              
281             use Feed::Data;
282              
283             my $feed = Feed::Data->new();
284            
285             $feed->parse( 'https://services.parliament.uk/calendar/commons_main_chamber.rss' );
286              
287             my $string = $feed->render('styled_table');
288              
289             $feed->clear;
290              
291             $feed->parse($string);
292              
293             =head1 DESCRIPTION
294              
295             Feed::Data is a frontend for building dynamic data feeds.
296              
297             =cut
298              
299             =head1 Methods
300              
301             =cut
302              
303             =head2 parse
304              
305             Populates the feed Attribute, this is an Array of Feed::Data::Object 's
306              
307             You can currently build Feeds by parsing xml (RSS, ATOM), JSON, CSV, plain text using key values seperated by a colon HTML via Meta Tags (twitter, opengraph) or table markup.
308              
309             =cut
310              
311             =over
312              
313             =item URI
314              
315             # any rss/atom feed or a web page that contains og or twitter markup
316             $feed->parse( 'http://examples.com/feed.xml' );
317              
318             =item File
319              
320             $feed->parse( 'path/to/feed.json' );
321              
322             =item Raw
323              
324             $feed->parse( 'qq{<?xml version="1.0"><feed> .... </feed>} );
325              
326             =back
327              
328             =head2 all
329              
330             returns all elements in the current feed
331              
332             $feed->all
333              
334             =cut
335              
336             =head2 count
337              
338             returns the count of the current data feed
339              
340             $feed->count
341              
342             =cut
343              
344             =head2 get
345              
346             accepts an index and returns an Feed::Data::Object from feed by its Array index
347              
348             $feed->get($index)
349              
350             =cut
351              
352             =head2 pop
353              
354             pop the last Feed::Data::Object from the current feed
355              
356             $feed->pop;
357              
358             =cut
359              
360             =head2 delete
361              
362             accepts an index and deletes the relevant Feed::Data::Object based on its Array index
363              
364             $feed->delete($index);
365              
366             =cut
367              
368             =head2 insert
369              
370             insert an 'Feed::Data::Object' into the feed
371              
372             $feed->insert($record)
373              
374             =cut
375              
376             =head2 is_empty
377              
378             returns true if Feed::Data is empty.
379              
380             $feed->is_empty
381            
382             =cut
383              
384             =head2 clear
385              
386             clear the current feed.
387              
388             $feed->clear
389              
390             =cut
391              
392             =head2 title
393              
394             Set the title of the rss feed the default is Feed::Data.
395              
396             $feed->title('Custom Title');
397              
398             =head2 link
399              
400             Set the link of the rss feed the default is Feed::Data.
401              
402             $feed->link('https://custom.link');
403              
404             =head2 description
405              
406             Set the description of the rss feed the default is Feed::Data.
407              
408             $feed->description('Custom Description');
409              
410             =head2 rss_channel
411              
412             Pass additional arguments into the rss feed channel section. See XML::RSS for more information.
413              
414             $feed->rss_channel({
415             dc => {
416             date => '2000-01-01T07:00+00:00',
417             subject => "LNATION",
418             creator => 'email@lnation.org',
419             publisher => 'email@lnation.org',
420             rights => 'Copyright 2000, lnation.org',
421             },
422             syn => {
423             updatePeriod => "hourly",
424             updateFrequency => "1",
425             updateBase => "1901-01-01T00:00+00:00",
426             },
427             });
428              
429             =head2 render
430              
431             render the feed using the passed in format, defaults to text.
432            
433             # raw - as taken from the feed
434             # text - stripped to plain text
435             # json
436             # rss
437             # csv
438             # yaml
439             # table
440             # styled_table
441              
442             $feed->render('raw');
443              
444             =cut
445              
446             =head2 generate
447              
448             returns the feed object as a Array of hashes but with the values rendered, key being the field. You can also pass in a format.
449            
450             $feed->hash('text');
451              
452             =cut
453              
454             =head2 write
455              
456             Writes the current stream to file.
457              
458             $feed->write($file_path, $type);
459              
460             =head1 AUTHOR
461              
462             lnation, C<< <email at lnation.org> >>
463              
464             =head1 BUGS
465              
466             Please report any bugs or feature requests to C<bug-feed-data at rt.cpan.org>, or through
467             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Feed-Data>. I will be notified, and then you'll
468             automatically be notified of progress on your bug as I make changes.
469              
470             =head1 SUPPORT
471              
472             You can find documentation for this module with the perldoc command.
473              
474              
475             You can also look for information at:
476              
477             =over 4
478              
479             =item * RT: CPAN's request tracker (report bugs here)
480              
481             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Feed-Data>
482              
483             =item * AnnoCPAN: Annotated CPAN documentation
484              
485             L<http://annocpan.org/dist/Feed-Data>
486              
487             =item * CPAN Ratings
488              
489             L<http://cpanratings.perl.org/d/Feed-Data>
490              
491             =item * Search CPAN
492              
493             L<http://search.cpan.org/dist/Feed-Data/>
494              
495             =back
496              
497             =head1 ACKNOWLEDGEMENTS
498              
499             =head1 LICENSE AND COPYRIGHT
500              
501             Copyright 2016 LNATION.
502              
503             This program is free software; you can redistribute it and/or modify it
504             under the terms of the the Artistic License (2.0). You may obtain a
505             copy of the full license at:
506              
507             L<http://www.perlfoundation.org/artistic_license_2_0>
508              
509             Any use, modification, and distribution of the Standard or Modified
510             Versions is governed by this Artistic License. By using, modifying or
511             distributing the Package, you accept this license. Do not use, modify,
512             or distribute the Package, if you do not accept this license.
513              
514             If your Modified Version has been derived from a Modified Version made
515             by someone other than you, you are nevertheless required to ensure that
516             your Modified Version complies with the requirements of this license.
517              
518             This license does not grant you the right to use any trademark, service
519             mark, tradename, or logo of the Copyright Holder.
520              
521             This license includes the non-exclusive, worldwide, free-of-charge
522             patent license to make, have made, use, offer to sell, sell, import and
523             otherwise transfer the Package with respect to any patent claims
524             licensable by the Copyright Holder that are necessarily infringed by the
525             Package. If you institute patent litigation (including a cross-claim or
526             counterclaim) against any party alleging that the Package constitutes
527             direct or contributory patent infringement, then this Artistic License
528             to you shall terminate on the date that such litigation is filed.
529              
530             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
531             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
532             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
533             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
534             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
535             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
536             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
537             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
538              
539              
540             =cut
541              
542             1; # End of Feed::Data