File Coverage

blib/lib/VisualCrossing/API.pm
Criterion Covered Total %
statement 56 77 72.7
branch 31 56 55.3
condition 10 18 55.5
subroutine 8 9 88.8
pod 0 2 0.0
total 105 162 64.8


line stmt bran cond sub pod time code
1             # ABSTRACT: provides perl API to VisualCrossing
2             package VisualCrossing::API;
3              
4 4     4   142843 use JSON;
  4         35326  
  4         20  
5 4     4   3636 use HTTP::Tiny;
  4         201935  
  4         161  
6 4     4   2289 use Moo;
  4         46800  
  4         27  
7 4     4   8235 use strictures 2;
  4         6784  
  4         175  
8 4     4   2699 use namespace::clean;
  4         46166  
  4         28  
9              
10             our $VERSION = '1.0.0';
11              
12             my $DEBUG = 0;
13              
14             my $api = "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline";
15             my $docs = "https://www.visualcrossing.com/resources/documentation/weather-api/timeline-weather-api/";
16             my %unitGroups = (
17             us => 1,
18             base => 1,
19             metric => 1,
20             uk => 1,
21             );
22             my %includes = (
23             days => 1,
24             hours => 1,
25             current => 1,
26             events => 1,
27             obs => 1,
28             remote => 1,
29             fcst => 1,
30             stats => 1,
31             statsfcst => 1,
32             );
33              
34             has url => (
35             is => 'lazy',
36             'default' => sub {
37             my $self = shift;
38             return $self->_getUrl();
39             },
40             );
41             # key must be specified
42             has key => (
43             is => 'ro',
44             'isa' => sub {
45             die "Invalid key specified: see $docs\n"
46             unless defined($_[0]);
47             },
48             required => 1,
49             );
50              
51             # location or latitude/longitude must be specified
52             has location => (is => 'ro');
53             has latitude => (is => 'ro');
54             has longitude => (is => 'ro');
55              
56             # date is optional
57             has date => (is => 'ro');
58             has date2 => (is => 'ro');
59              
60             # uncommon options
61             has include => (
62             is => 'ro',
63             'isa' => sub {
64             die "Invalid include specified: see $docs\n"
65             unless exists($includes{ $_[0] });
66             },
67             );
68             has unitGroup => (
69             is => 'ro',
70             'isa' => sub {
71             die "Invalid unitGroup specified: see $docs\n"
72             unless exists($unitGroups{ $_[0] });
73             },
74             );
75             has lang => (is => 'ro');
76             has options => (is => 'ro');
77             has nonulls => (is => 'ro');
78             has noheaders => (is => 'ro');
79             has contentType => (is => 'ro');
80             has timezone => (is => 'ro');
81             has maxDistance => (is => 'ro');
82             has maxStations => (is => 'ro');
83             has elevationDifference => (is => 'ro');
84             has locationNames => (is => 'ro');
85             has forecastBasisDate => (is => 'ro');
86             has forecastBasisDay => (is => 'ro');
87             has degreeDayTempBase => (is => 'ro');
88             has degreeDayTempMaxThreshold => (is => 'ro');
89              
90             sub getWeather {
91 1     1   6 my $self = shift;
92 1         7 my $http = HTTP::Tiny->new;
93 1         186 my $url = $self->url;
94 1         4 my $response = $http->get($url);
95              
96             die "Request to '$url' failed: $response->{status} $response->{reason}\n"
97 1 50       15 unless $response->{success};
98              
99 1         17 my $coder = JSON->new->utf8;
100 1         340 my $result = $coder->decode($response->{content});
101 1         24 return $result;
102             }
103              
104             sub BUILD {
105 7     7 0 102 my ($self, $args) = @_;
106             # validate the resulting object
107              
108 7 100 66     33 if (defined($self->{location})) {
    50          
109             # ok
110             } elsif (defined($self->{latitude}) && defined($self->{longitude})) {
111             # ok
112             } else {
113 3         32 die "Invalid request either location or latitude/longitude must be specified: see $docs\n";
114             }
115              
116 4 100 100     58 if (defined($self->{date}) && defined($self->{date2})) {
    100 66        
117             # ok
118             } elsif (!defined($self->{date}) && defined($self->{date2})) {
119 1         9 die "Invalid request date must exist if date2 is specified: see $docs\n";
120             }
121             }
122              
123             sub _getUrl {
124 2     2   4 my ($self) = @_;
125 2         4 my $url = $api;
126              
127 2 50 0     8 if (defined($self->{location})) {
    0          
128 2         9 $url = $url . '/' . $self->{location};
129             } elsif (defined($self->{latitude}) && defined($self->{longitude})) {
130 0         0 $url = $url . '/' . $self->{latitude} . ',' . $self->{longitude};
131             } else {
132 0         0 die "Invalid request either location or latitude/longitude must be specified: see $docs\n";
133             }
134              
135 2 100 66     30 if (defined($self->{date}) && defined($self->{date2})) {
    50 33        
    50          
136 1         4 $url = $url . '/' . $self->{date} . '/' . $self->{date2};
137             } elsif (!defined($self->{date}) && defined($self->{date2})) {
138 0         0 die "Invalid request date must exist if date2 is specified: see $docs\n";
139             } elsif (defined($self->{date})) {
140 1         5 $url = $url . '/' . $self->{date} ;
141             }
142              
143 2 50       8 if (!defined($self->{key})) {
144 0         0 die "Invalid request key must be specified: see $docs\n";
145             }
146 2         6 $url = $url . "?key=" . $self->{key};
147              
148 2 50       8 if (defined($self->{include})) {
149 2         6 $url = $url . '&include=' . $self->{include};
150             }
151 2 50       9 if (defined($self->{unitGroup})) {
152 0         0 $url = $url . '&unitGroup=' . $self->{unitGroup};
153             }
154 2 50       6 if (defined($self->{lang})) {
155 0         0 $url = $url . '&lang=' . $self->{lang};
156             }
157 2 50       7 if (defined($self->{options})) {
158 0         0 $url = $url . '&options=' . $self->{options};
159             }
160 2 50       6 if (defined($self->{nonulls})) {
161 0         0 $url = $url . '&nonulls=' . $self->{nonulls};
162             }
163 2 50       5 if (defined($self->{noheaders})) {
164 0         0 $url = $url . '&noheaders=' . $self->{noheaders};
165             }
166 2 50       5 if (defined($self->{contentType})) {
167 0         0 $url = $url . '&contentType=' . $self->{contentType};
168             }
169 2 50       5 if (defined($self->{timezone})) {
170 0         0 $url = $url . '&timezone=' . $self->{timezone};
171             }
172 2 50       6 if (defined($self->{maxDistance})) {
173 0         0 $url = $url . '&maxDistance=' . $self->{maxDistance};
174             }
175 2 50       4 if (defined($self->{maxStations})) {
176 0         0 $url = $url . '&maxStations=' . $self->{maxStations};
177             }
178 2 50       5 if (defined($self->{elevationDifference})) {
179 0         0 $url = $url . '&elevationDifference=' . $self->{elevationDifference};
180             }
181 2 50       4 if (defined($self->{locationNames})) {
182 0         0 $url = $url . '&locationNames=' . $self->{locationNames};
183             }
184 2 50       6 if (defined($self->{forecastBasisDate})) {
185 0         0 $url = $url . '&forecastBasisDate=' . $self->{forecastBasisDate};
186             }
187 2 50       7 if (defined($self->{forecastBasisDay})) {
188 0         0 $url = $url . '&forecastBasisDay=' . $self->{forecastBasisDay};
189             }
190 2 50       16 if (defined($self->{degreeDayTempBase})) {
191 0         0 $url = $url . '°reeDayTempBase=' . $self->{degreeDayTempBase};
192             }
193 2 50       6 if (defined($self->{degreeDayTempMaxThreshold})) {
194 0         0 $url = $url . '°reeDayTempMaxThreshold=' . $self->{degreeDayTempMaxThreshold};
195             }
196              
197 2 50       6 $DEBUG && print "DEBUG: ddURL=" . $url . "\n";
198 2         13 return $url;
199             }
200              
201 0     0 0   sub TO_JSON {return { %{shift()} };}
  0            
202              
203             1;
204              
205              
206             =pod
207              
208             =encoding utf-8
209              
210             =head1 NAME
211              
212             VisualCrossing::API - Provides Perl API to VisualCrossing
213              
214             =head1 SYNOPSIS
215              
216             use VisualCrossing::API;
217             use JSON::XS;
218             use feature 'say';
219              
220             my $location = "AU419";
221             my $date = "2023-05-25"; # example time (optional)
222             my $key = "ABCDEFGABCDEFGABCDEFGABCD"; # example VisualCrossing API key
223              
224             ## Current Data (limit to current, saves on API cost)
225             my $weatherApi = VisualCrossing::API->new(
226             key => $key,
227             location => $location,
228             include => "current",
229             );
230             my $current = $weatherApi->getWeather;
231              
232             say "current temperature: " . $current->{currentConditions}->{temp};
233             say "current conditions: " . $current->{currentConditions}->{conditions};
234              
235             ## Historical Data (limit to single day, saves on API cost)
236             my $weatherApi = VisualCrossing::API->new(
237             key => $key,
238             location => $location,
239             date => $date
240             date2 => $date
241             include => "days",
242             );
243             my $history = $weatherApi->getWeather;
244              
245             say "$date temperature: " . $history->{days}[0]->{temp};
246             say "$date conditions: " . $history->{days}[0]->{conditions};
247              
248             =head1 DESCRIPTION
249              
250             This module is a wrapper around the VisualCrossing API.
251              
252             =head1 REFERENCES
253              
254             Git repository: L
255              
256             VisualCrossing API docs: L
257              
258             Based on DarkSky-API: L
259              
260             =head1 COPYRIGHT
261              
262             Copyright (c) 2023 L
263              
264             =head1 LICENSE
265              
266             This library is free software and may be distributed under the APACHE LICENSE, VERSION 2.0 L.
267              
268             =cut