File Coverage

blib/lib/WWW/NOS/Open.pm
Criterion Covered Total %
statement 111 187 59.3
branch 7 16 43.7
condition 0 2 0.0
subroutine 34 46 73.9
pod 7 7 100.0
total 159 258 61.6


line stmt bran cond sub pod time code
1             # -*- cperl; cperl-indent-level: 4 -*-
2             # Copyright (C) 2011-2021, Roland van Ipenburg
3             package WWW::NOS::Open v1.0.4;
4 4     4   472535 use strict;
  4         42  
  4         117  
5 4     4   21 use warnings;
  4         9  
  4         101  
6              
7 4     4   2676 use utf8;
  4         56  
  4         24  
8 4     4   164 use 5.014000;
  4         13  
9              
10 4     4   1921 use Date::Calc qw(Add_Delta_Days Date_to_Days Delta_Days Today);
  4         27927  
  4         340  
11 4     4   2164 use Date::Format;
  4         34342  
  4         255  
12 4     4   2404 use HTTP::Headers;
  4         28621  
  4         160  
13 4     4   1982 use HTTP::Request;
  4         52250  
  4         164  
14             use HTTP::Status
15 4     4   2068 qw(HTTP_OK HTTP_BAD_REQUEST HTTP_UNAUTHORIZED HTTP_FORBIDDEN HTTP_INTERNAL_SERVER_ERROR);
  4         19623  
  4         508  
16 4     4   3425 use JSON;
  4         41627  
  4         30  
17 4     4   3861 use LWP::UserAgent;
  4         75509  
  4         164  
18 4     4   4171 use Log::Log4perl qw(:easy get_logger);
  4         206757  
  4         24  
19 4     4   5529 use Moose qw/around has with/;
  4         1925928  
  4         28  
20 4     4   25833 use Moose::Util::TypeConstraints qw/enum/;
  4         10  
  4         38  
21 4     4   1912 use URI::Escape qw(uri_escape);
  4         10  
  4         313  
22 4     4   29 use URI;
  4         11  
  4         159  
23 4     4   4330 use XML::Simple;
  4         38366  
  4         39  
24              
25 4     4   2514 use namespace::autoclean '-also' => qr/^__/sxm;
  4         34501  
  4         38  
26              
27 4     4   2331 use WWW::NOS::Open::Article;
  4         20  
  4         197  
28 4     4   2593 use WWW::NOS::Open::AudioFragment;
  4         14  
  4         195  
29 4     4   2474 use WWW::NOS::Open::Broadcast;
  4         29  
  4         183  
30 4     4   2489 use WWW::NOS::Open::DayGuide;
  4         15  
  4         185  
31 4     4   2363 use WWW::NOS::Open::Document;
  4         15  
  4         195  
32 4     4   2583 use WWW::NOS::Open::Exceptions;
  4         12  
  4         200  
33 4     4   1747 use WWW::NOS::Open::Result;
  4         13  
  4         160  
34 4     4   35 use WWW::NOS::Open::TypeDef;
  4         10  
  4         47  
35 4     4   6995 use WWW::NOS::Open::Version;
  4         13  
  4         161  
36 4     4   2176 use WWW::NOS::Open::Video;
  4         14  
  4         180  
37              
38 4     4   33 use Readonly;
  4         8  
  4         11119  
39             Readonly::Scalar my $SERVER => $ENV{'NOSOPEN_SERVER'} || q{http://open.nos.nl};
40             Readonly::Scalar my $TIMEOUT => 15;
41             Readonly::Scalar my $AGENT => q{WWW::NOS::Open/} . $WWW::NOS::Open::VERSION;
42             Readonly::Scalar my $DATE_FORMAT => q{%04u-%02u-%02u};
43             Readonly::Scalar my $DEFAULT_START => -1; # Yesterday
44             Readonly::Scalar my $DEFAULT_END => 1; # Tomorrow
45             Readonly::Scalar my $MAX_RANGE => 14; # Two weeks
46             Readonly::Scalar my $GET => q{GET};
47             Readonly::Scalar my $DEFAULT_API_KEY => q{TEST};
48             Readonly::Scalar my $DEFAULT_OUTPUT => q{xml};
49             Readonly::Scalar my $DEFAULT_CATEGORY => q{nieuws};
50             Readonly::Scalar my $DASH => q{-};
51             Readonly::Scalar my $DOUBLE_COLON => q{::};
52             Readonly::Scalar my $FRAGMENT => q{Fragment};
53             Readonly::Scalar my $VERSION_PATH => q{%s/v1/index/version/key/%s/output/%s/};
54             Readonly::Scalar my $LATEST_PATH =>
55             q{%s/v1/latest/%s/key/%s/output/%s/category/%s/};
56             Readonly::Scalar my $SEARCH_PATH => q{%s/v1/search/query/key/%s/output/%s/q/%s};
57             Readonly::Scalar my $GUIDE_PATH =>
58             q{%s/v1/guide/%s/key/%s/output/%s/start/%s/end/%s/};
59             Readonly::Scalar my $STRIP_PRIVATE => qr{^_}smx;
60              
61             Readonly::Hash my %ERR => (
62             'INTERNAL_SERVER' => q{Internal server error or no response recieved},
63             'EXCEEDED_RANGE' => qq{Date range exceeds maximum of $MAX_RANGE days},
64             );
65             Readonly::Hash my %LOG => (
66             'REQUESTING' => q{Requesting %s},
67             'RESPONSE_CODE' => q{Response code %d},
68             );
69              
70             Log::Log4perl::easy_init($ERROR);
71              
72             my $log = Log::Log4perl->get_logger(__PACKAGE__);
73              
74             has '_ua' => (
75             'is' => 'ro',
76             'isa' => 'LWP::UserAgent',
77             'default' => sub {
78             LWP::UserAgent->new(
79             'timeout' => $TIMEOUT,
80             'agent' => $AGENT,
81             );
82             },
83             );
84              
85             has '_version' => (
86             'is' => 'ro',
87             'isa' => 'WWW::NOS::Open::Version',
88             );
89              
90             sub get_version {
91 1     1 1 8 my $self = shift;
92 1         34 my $url = sprintf $VERSION_PATH, $SERVER,
93             URI::Escape::uri_escape( $self->get_api_key ),
94             URI::Escape::uri_escape( $self->_get_default_output );
95 1         19 my $response = $self->_do_request($url);
96 0         0 my $version = $self->_parse_version( $response->decoded_content );
97 0         0 return $version;
98             }
99              
100             sub _parse_version {
101 0     0   0 my ( $self, $body ) = @_;
102 0         0 my ( $version, $build );
103 0         0 my $xml = XML::Simple->new( 'ForceArray' => 1 )->XMLin($body);
104 0         0 $version = $xml->{'item'}[0]->{'version'}[0];
105 0         0 $build = $xml->{'item'}[0]->{'build'}[0];
106 0         0 return WWW::NOS::Open::Version->new( $version, $build );
107             }
108              
109             has '_default_output' => (
110             'is' => 'ro',
111             'isa' => 'Str',
112             'default' => $DEFAULT_OUTPUT,
113             'reader' => '_get_default_output',
114             'init_arg' => 'default_output',
115             );
116              
117             has '_api_key' => (
118             'is' => 'rw',
119             'isa' => 'Str',
120             'default' => $DEFAULT_API_KEY,
121             'reader' => 'get_api_key',
122             'writer' => 'set_api_key',
123             'init_arg' => 'api_key',
124             );
125              
126             sub _get_latest_resources {
127 0     0   0 my ( $self, $type, $category ) = @_;
128 0 0       0 ( defined $category ) || ( $category = $DEFAULT_CATEGORY );
129 0         0 my $url = sprintf $LATEST_PATH,
130             $SERVER,
131             URI::Escape::uri_escape($type),
132             URI::Escape::uri_escape( $self->get_api_key ),
133             URI::Escape::uri_escape( $self->_get_default_output ),
134             URI::Escape::uri_escape($category);
135 0         0 my $response = $self->_do_request($url);
136 0         0 my @resources =
137             $self->_parse_resources( $type, $response->decoded_content );
138 0         0 return @resources;
139             }
140              
141             sub get_latest_articles {
142 0     0 1 0 my ( $self, @param ) = @_;
143 0         0 return $self->_get_latest_resources( q{article}, @param );
144             }
145              
146             sub __get_props {
147             my $meta = shift;
148             my @props = map { $_->name } $meta->get_all_attributes;
149             for (@props) {
150             s/$STRIP_PRIVATE//smx;
151             }
152             return @props;
153             }
154              
155             sub _parse_resource {
156 0     0   0 my ( $self, $type, $hr_resource ) = @_;
157 0         0 my %mapping = (
158             'article' => __PACKAGE__ . $DOUBLE_COLON . ucfirst $type,
159             'video' => __PACKAGE__ . $DOUBLE_COLON . ucfirst $type,
160             'audio' => __PACKAGE__ . $DOUBLE_COLON . ucfirst $type . $FRAGMENT,
161             'document' => __PACKAGE__ . $DOUBLE_COLON . ucfirst $type,
162             'broadcast' => __PACKAGE__ . $DOUBLE_COLON . ucfirst $type,
163             );
164              
165 0         0 my @props = __get_props( ( $mapping{$type} )->meta );
166 0         0 my %param;
167 0         0 while ( my $prop = shift @props ) {
168             $param{$prop} =
169             ( q{HASH} eq ref $hr_resource->{$prop}[0] )
170 0         0 ? %{ $hr_resource->{$prop}[0] }
171 0 0       0 : $hr_resource->{$prop}[0];
172             }
173 0   0     0 $param{'keywords'} = $hr_resource->{'keywords'}->[0]->{'keyword'} || [];
174 0         0 return ( $mapping{$type} )->new(%param);
175             }
176              
177             sub _parse_resources {
178 0     0   0 my ( $self, $type, $body ) = @_;
179 0         0 my @resources;
180 0         0 my $xml = XML::Simple->new( 'ForceArray' => 1 )->XMLin($body);
181 0         0 my @xml_resources = @{ $xml->{$type} };
  0         0  
182 0         0 while ( my $resource = shift @xml_resources ) {
183 0         0 push @resources, $self->_parse_resource( $type, $resource );
184             }
185 0         0 return @resources;
186             }
187              
188             sub get_latest_videos {
189 0     0 1 0 my ( $self, @param ) = @_;
190 0         0 return $self->_get_latest_resources( q{video}, @param );
191             }
192              
193             sub get_latest_audio_fragments {
194 0     0 1 0 my ( $self, @param ) = @_;
195 0         0 return $self->_get_latest_resources( q{audio}, @param );
196             }
197              
198             sub _parse_result {
199 0     0   0 my ( $self, $body ) = @_;
200 0         0 my @documents;
201 0         0 my $xml = XML::Simple->new( 'ForceArray' => 1 )->XMLin($body);
202 0         0 my @xml_documents = @{ $xml->{'documents'}->[0]->{'document'} };
  0         0  
203 0         0 while ( my $hr_document = shift @xml_documents ) {
204 0         0 push @documents, $self->_parse_resource( q{document}, $hr_document );
205             }
206             my $result = WWW::NOS::Open::Result->new(
207             'documents' => [@documents],
208 0         0 'related' => $xml->{'related'}->[0]->{'related'},
209             );
210 0         0 return $result;
211             }
212              
213             sub search {
214 0     0 1 0 my ( $self, $query ) = @_;
215 0         0 my $url = sprintf $SEARCH_PATH,
216             $SERVER,
217             URI::Escape::uri_escape( $self->get_api_key ),
218             URI::Escape::uri_escape( $self->_get_default_output ),
219             URI::Escape::uri_escape($query);
220 0         0 my $response = $self->_do_request($url);
221 0         0 my $result = $self->_parse_result( $response->decoded_content );
222 0         0 return $result;
223             }
224              
225             sub __get_date {
226             my ( $start_day, $end_day ) = @_;
227             my $today = Date_to_Days(Today);
228             return (
229             (
230             sprintf $DATE_FORMAT,
231             Add_Delta_Days( 1, 1, 1, $today + $start_day - 1 ),
232             ),
233             (
234             sprintf $DATE_FORMAT,
235             Add_Delta_Days( 1, 1, 1, $today + $end_day - 1 ),
236             ),
237             );
238             }
239              
240             sub _parse_dayguide {
241 0     0   0 my ( $self, $hr_dayguide ) = @_;
242              
243 0         0 my @props = __get_props( WWW::NOS::Open::DayGuide->meta );
244 0         0 my %param;
245 0         0 while ( my $prop = shift @props ) {
246 0         0 $param{$prop} = $hr_dayguide->{$prop};
247             }
248 0         0 $param{'broadcasts'} = [];
249 0         0 my @broadcasts = $hr_dayguide->{'item'};
250 0         0 while ( my $ar_broadcast = shift @broadcasts ) {
251 0         0 push @{ $param{'broadcasts'} },
  0         0  
252             $self->_parse_resource( q{broadcast}, $ar_broadcast->[0] );
253             }
254 0         0 return WWW::NOS::Open::DayGuide->new(%param);
255             }
256              
257             sub _parse_guide {
258 0     0   0 my ( $self, $body ) = @_;
259 0         0 my @dayguides;
260 0         0 my $xml = XML::Simple->new( 'ForceArray' => 1 )->XMLin($body);
261 0         0 my @xml_dayguides = @{ $xml->{'dayguide'} };
  0         0  
262 0         0 while ( my $hr_dayguide = shift @xml_dayguides ) {
263 0         0 push @dayguides, $self->_parse_dayguide($hr_dayguide);
264             }
265 0         0 return @dayguides;
266             }
267              
268             sub _get_broadcasts {
269 4     4   17 my ( $self, $type, $start, $end, $channel ) = @_;
270              
271 4         17 my ( $default_start, $default_end ) =
272             __get_date( $DEFAULT_START, $DEFAULT_END );
273 4 50       23 ( defined $start ) || ( $start = $default_start );
274 4 50       19 ( defined $end ) || ( $end = $default_end );
275              
276 4         15 foreach ( $start, $end ) {
277 8 100       91 ( q{DateTime} eq ref ) && ( $_ = $_->ymd );
278             }
279 4 100       100 if ( Delta_Days( split /$DASH/smx, qq{$start$DASH$end} ) > $MAX_RANGE ) {
280             ## no critic qw(RequireExplicitInclusion)
281             NOSOpenExceededRangeException->throw(
282             ## use critic
283 2         58 'error' => $ERR{'EXCEEDED_RANGE'},
284             );
285             }
286 2         37 my $url = sprintf $GUIDE_PATH,
287             $SERVER,
288             URI::Escape::uri_escape($type),
289             URI::Escape::uri_escape( $self->get_api_key ),
290             URI::Escape::uri_escape( $self->_get_default_output ),
291             URI::Escape::uri_escape($start),
292             URI::Escape::uri_escape($end);
293 2         66 my $response = $self->_do_request($url);
294 0         0 my @guide_days = $self->_parse_guide( $response->decoded_content );
295 0         0 return @guide_days;
296             }
297              
298             sub get_tv_broadcasts {
299 0     0 1 0 my ( $self, @param ) = @_;
300 0         0 return $self->_get_broadcasts( q{tv}, @param );
301             }
302              
303             sub get_radio_broadcasts {
304 4     4 1 8276 my ( $self, @param ) = @_;
305 4         20 return $self->_get_broadcasts( q{radio}, @param );
306             }
307              
308             sub __throw {
309             my $json = JSON->new;
310             my %map = (
311             HTTP::Status::HTTP_BAD_REQUEST => q{NOSOpenBadRequestException},
312             HTTP::Status::HTTP_UNAUTHORIZED => q{NOSOpenUnauthorizedException},
313             HTTP::Status::HTTP_FORBIDDEN => q{NOSOpenForbiddenException},
314             );
315             my $res = shift;
316             $map{ $res->code }
317             ->throw( 'error' => $json->decode( $res->decoded_content ), );
318             }
319              
320             sub _do_request {
321 3     3   11 my ( $self, $url ) = @_;
322 3         21 my $request = HTTP::Request->new(
323             $GET => $url,
324             HTTP::Headers->new(),
325             );
326 3         16607 $log->debug( sprintf $LOG{'REQUESTING'}, $url );
327 3         193 my $response = $self->_ua->request($request);
328 3         237204 $log->debug( sprintf $LOG{'RESPONSE_CODE'}, $response->code );
329 3 50       203 if ( $response->code == HTTP_INTERNAL_SERVER_ERROR ) {
    0          
330             ## no critic qw(RequireExplicitInclusion)
331             NOSOpenInternalServerErrorException->throw(
332             ## use critic
333 3         114 'error' => $ERR{'INTERNAL_SERVER'},
334             );
335             }
336             elsif ( $response->code > HTTP_OK ) {
337 0           __throw($response);
338             }
339 0           return $response;
340             }
341              
342             around 'BUILDARGS' => sub {
343             my $orig = shift;
344             my $class = shift;
345             my ( $api_key, $default_output ) = @_;
346              
347             return $class->$orig(
348             'api_key' => $api_key || $DEFAULT_API_KEY,
349             'default_output' => $default_output || $DEFAULT_OUTPUT,
350             );
351             };
352              
353             with 'WWW::NOS::Open::Interface';
354              
355 4     4   40 no Moose;
  4         15  
  4         29  
356              
357             __PACKAGE__->meta->make_immutable;
358              
359             1;
360              
361             __END__
362              
363             =encoding utf8
364              
365             =for stopwords Bitbucket DateTime perl JSON Readonly URI PHP Ipenburg
366             MERCHANTABILITY
367              
368             =head1 NAME
369              
370             WWW::NOS::Open - Perl framework for the Open NOS REST API
371              
372             =head1 VERSION
373              
374             This document describes WWW::NOS::Open version C<v1.0.4>.
375              
376             =head1 SYNOPSIS
377              
378             use WWW::NOS::Open;
379             my $nos = WWW::NOS::Open->new($API_KEY);
380             @latest_articles = $nos->get_latest_articles('nieuws');
381              
382             =head1 DESCRIPTION
383              
384             The L<Dutch public broadcasting foundation NOS|http:://www.nos.nl> provides a
385             REST API to their content. This module provides a wrapper around that API to
386             use data from the Open NOS platform with Perl.
387              
388             =head1 SUBROUTINES/METHODS
389              
390             =head2 C<new>
391              
392             Create a new WWW::NOS::Open object.
393              
394             =over
395              
396             =item 1. The API key to use in the connection to the Open NOS service. You
397             need to L<register at Open NOS|http://open.nos.nl/registratie/> to get an API
398             key, and link your IP address to that account to do authorized requests.
399              
400             =back
401              
402             =head2 C<get_version>
403              
404             Gets the version of the REST API as a
405             L<WWW::NOS::Open::Version|WWW::NOS::Open::Version> object.
406              
407             =head2 C<get_latest_articles>
408              
409             Returns the ten most recent articles as an array of
410             L<WWW::NOS::Open::Article|WWW::NOS::Open::Article> objects.
411              
412             =over
413              
414             =item 1. The optional category of the requested articles, C<nieuws> or
415             C<sport>. Defaults to the category C<nieuws>.
416              
417             =back
418              
419             =head2 C<get_latest_videos>
420              
421             Returns the ten most recent videos as an array of
422             L<WWW::NOS::Open::Video|WWW::NOS::Open::Video> objects.
423              
424             =over
425              
426             =item 1. The optional category of the requested videos, C<nieuws> or C<sport>.
427             Defaults to the category C<nieuws>.
428              
429             =back
430              
431             =head2 C<get_latest_audio_fragments>
432              
433             Returns the ten most recent audio fragments as an array of
434             L<WWW::NOS::Open::AudioFragment|WWW::NOS::Open::AudioFragment> objects.
435              
436             =over
437              
438             =item 1. The optional category of the requested audio fragments, C<nieuws> or
439             C<sport>. Defaults to the category C<nieuws>.
440              
441             =back
442              
443             =head2 C<search>
444              
445             Search the search engine from L<NOS|http://www.nos.nl> for keywords. Returns
446             a L<WWW::NOS::Open::Results|WWW::NOS::Open::Results> object with a maximum of
447             25 items.
448              
449             =over
450              
451             =item 1. The keyword or a combination of keywords, for example C<cricket>,
452             C<cricket AND engeland>, C<cricket OR curling>.
453              
454             =back
455              
456             =head2 C<get_tv_broadcasts>
457              
458             Gets a collection of television broadcasts between two optional dates. Returns
459             an array of L<WWW::NOS::Open::DayGuide|WWW::NOS::Open::DayGuide> objects. The
460             period defaults to starting yesterday and ending tomorrow. The period has an
461             upper limit of 14 days. An C<NOSOpenExceededRangeException> is thrown when
462             this limit is exceeded.
463              
464             =over
465              
466             =item 1. Start date in the format C<YYYY-MM-DD> or as L<DateTime|DateTime>
467             object.
468              
469             =item 2. End date in the format C<YYYY-MM-DD> or as L<DateTime|DateTime>
470             object.
471              
472             =back
473              
474             =head2 C<get_radio_broadcasts>
475              
476             Gets a collection of radio broadcasts between two optional dates. Returns an
477             array of L<WWW::NOS::Open::DayGuide|WWW::NOS::Open::DayGuide> objects. The
478             period defaults to starting yesterday and ending tomorrow. The period has an
479             upper limit of 14 days. An C<NOSOpenExceededRangeException> is thrown when this
480             limit is exceeded.
481              
482             =over
483              
484             =item 1. Start date in the format C<YYYY-MM-DD> or as L<DateTime|DateTime>
485             object.
486              
487             =item 2. End date in the format C<YYYY-MM-DD> or as L<DateTime|DateTime>
488             object.
489              
490             =back
491              
492             =head1 CONFIGURATION AND ENVIRONMENT
493              
494             To use this module with the live content of Open NOS you need an API key which
495             can be obtained by registering at L<Open NOS|http://open.nos.nl/registratie/>
496             and then configure your account there with the IP range you'll be accessing
497             the service from.
498              
499             This module can use the optional environment variable C<NOSOPEN_SERVER> to
500             specify a server URL that is not the default Open NOS live service at
501             L<http://open.nos.nl|http://open.nos.nl>.
502              
503             The user agent identifier used in the request to the REST API is
504             C<WWW::NOS::Open/v1.0.4>.
505              
506             =head1 DEPENDENCIES
507              
508             =over
509              
510             =item * perl 5.14
511              
512             =item * L<Date::Calc|Date::Calc>
513              
514             =item * L<Date::Format|Date::Format>
515              
516             =item * L<HTTP::Headers|HTTP::Headers>
517              
518             =item * L<HTTP::Request|HTTP::Request>
519              
520             =item * L<HTTP::Status|HTTP::Status>
521              
522             =item * L<JSON|JSON>
523              
524             =item * L<LWP::UserAgent|LWP::UserAgent>
525              
526             =item * L<Log::Log4perl|Log::Log4perl>
527              
528             =item * L<Moose|Moose>
529              
530             =item * L<Moose::Util::TypeConstraints|Moose::Util::TypeConstraints>
531              
532             =item * L<Readonly|Readonly>
533              
534             =item * L<URI|URI>, L<URI::Escape|URI::Escape>
535              
536             =item * L<WWW::NOS::Open::Article|WWW::NOS::Open::Article>
537              
538             =item * L<WWW::NOS::Open::AudioFragment|WWW::NOS::Open::AudioFragment>
539              
540             =item * L<WWW::NOS::Open::Broadcast|WWW::NOS::Open::Broadcast>
541              
542             =item * L<WWW::NOS::Open::DayGuide|WWW::NOS::Open::DayGuide>
543              
544             =item * L<WWW::NOS::Open::Document|WWW::NOS::Open::Document>
545              
546             =item * L<WWW::NOS::Open::Exceptions|WWW::NOS::Open::Exceptions>
547              
548             =item * L<WWW::NOS::Open::Result|WWW::NOS::Open::Result>
549              
550             =item * L<WWW::NOS::Open::TypeDef|WWW::NOS::Open::TypeDef>
551              
552             =item * L<WWW::NOS::Open::Version|WWW::NOS::Open::Version>
553              
554             =item * L<WWW::NOS::Open::Video|WWW::NOS::Open::Video>
555              
556             =item * L<XML::Simple|XML::Simple>
557              
558             =item * L<namespace::autoclean|namespace::autoclean>
559              
560             =back
561              
562             =head1 INCOMPATIBILITIES
563              
564             None.
565              
566             =head1 DIAGNOSTICS
567              
568             Exceptions in the form of an L<Exception::Class|Exception::Class> are thrown
569             when the Open NOS service reports an exception:
570              
571             =head2 C<NOSOpenUnauthorizedException>
572              
573             When a request was made without a valid API key, or from an IP address not
574             configured to be valid for that API key.
575              
576             =head2 C<NOSOpenExceededRangeException>
577              
578             When the time period for a guide request exceeds the supported range of 14
579             days.
580              
581             =head2 C<NOSOpenInternalServerErrorException>
582              
583             When an internal server error has occurred in the Open NOS service.
584              
585             =head2 C<NOSOpenBadRequestException>
586              
587             When the Open NOS service reports the request had bad syntax.
588              
589             =head1 BUGS AND LIMITATIONS
590              
591             Currently this module only uses the XML output of the Open NOS service and has
592             no option to use the JSON or serialized PHP formats. When the API matures the
593             other output options might be added and the content of the raw responses
594             exposed for further processing in an appropriate environment.
595              
596             Please report any bugs or feature requests at
597             L<Bitbucket|https://bitbucket.org/rolandvanipenburg/www-nos-open/issues>.
598              
599             =head1 AUTHOR
600              
601             Roland van Ipenburg, E<lt>roland@rolandvanipenburg.comE<gt>
602              
603             =head1 LICENSE AND COPYRIGHT
604              
605             Copyright 2011-2021 by Roland van Ipenburg
606              
607             This library is free software; you can redistribute it and/or modify
608             it under the same terms as Perl itself, either Perl version 5.14.0 or,
609             at your option, any later version of Perl 5 you may have available.
610              
611             =head1 DISCLAIMER OF WARRANTY
612              
613             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
614             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
615             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
616             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
617             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
618             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
619             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
620             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
621             NECESSARY SERVICING, REPAIR, OR CORRECTION.
622              
623             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
624             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
625             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENSE, BE
626             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
627             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
628             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
629             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
630             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
631             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
632             SUCH DAMAGES.
633              
634             =cut