POEx-Weather-OpenWeatherMap/lib/POEx/Weather/OpenWeatherMap.pm
package POEx::Weather::OpenWeatherMap;
$POEx::Weather::OpenWeatherMap::VERSION = '0.003002';
use Defaults::Modern;
use POE 'Component::Client::HTTP';
use Weather::OpenWeatherMap::Cache;
use Weather::OpenWeatherMap::Error;
use Weather::OpenWeatherMap::Request;
use Weather::OpenWeatherMap::Request::Current;
use Weather::OpenWeatherMap::Request::Forecast;
use Weather::OpenWeatherMap::Request::Find;
use Weather::OpenWeatherMap::Result;
use Weather::OpenWeatherMap::Result::Current;
use Weather::OpenWeatherMap::Result::Forecast;
use Weather::OpenWeatherMap::Request::Find;
use Moo;
with 'MooX::Role::POE::Emitter';
has api_key => (
is => 'ro',
isa => Str,
predicate => 1,
writer => 'set_api_key',
builder => sub {
carp "No api_key specified, requests will likely fail!";
''
},
);
has _ua_alias => (
lazy => 1,
init_arg => 'ua_alias',
is => 'ro',
isa => Str,
builder => sub {
my ($self) = @_;
$self->alias ? $self->alias . 'UA'
: confess "Cannot build ua_alias; emitter not running"
},
);
has cache => (
lazy => 1,
is => 'ro',
isa => Bool,
builder => sub { 0 },
);
has cache_dir => (
lazy => 1,
is => 'ro',
isa => Maybe[Str],
builder => sub { undef },
);
has cache_expiry => (
lazy => 1,
is => 'ro',
isa => Maybe[StrictNum],
builder => sub { undef },
);
has _cache => (
lazy => 1,
is => 'ro',
isa => InstanceOf['Weather::OpenWeatherMap::Cache'],
builder => sub {
my ($self) = @_;
Weather::OpenWeatherMap::Cache->new(
maybe dir => $self->cache_dir,
maybe expiry => $self->cache_expiry,
)
},
);
method start {
$self->set_object_states(
[
$self => +{
emitter_started => 'mxrp_emitter_started',
emitter_stopped => 'mxrp_emitter_stopped',
get_weather => 'ext_get_weather',
ext_http_response => 'ext_http_response',
},
( $self->has_object_states ? $self->object_states->all : () ),
]
);
$self->_start_emitter
}
method stop { $self->_shutdown_emitter }
method _emit_error (@params) {
my $err = Weather::OpenWeatherMap::Error->new(@params);
$self->emit( error => $err );
$err
}
sub mxrp_emitter_started {
# my ($kernel, $self) = @_[KERNEL, OBJECT];
}
sub mxrp_emitter_stopped {
my ($kernel, $self) = @_[KERNEL, OBJECT];
# expire stale cache items before we stop if we have a persistent cache_dir;
# elsewise clear cache entirely:
if (defined $self->cache_dir) {
$self->_cache->expire_all
} else {
$self->_cache->clear
}
$kernel->call( $self->_ua_alias => 'shutdown' )
if $kernel->alias_resolve( $self->_ua_alias );
}
method _result_type (Object $my_request) {
state $base = 'Weather::OpenWeatherMap::Request::';
my ($type, $event);
CLASS: {
if ($my_request->isa($base.'Current')) {
$type = 'Current';
$event = 'weather';
last CLASS
}
if ($my_request->isa($base.'Forecast')) {
$type = 'Forecast';
$event = 'forecast';
last CLASS
}
confess "Unknown request type: $my_request"
}
wantarray ? ($type, $event) : $type
}
method get_weather (@params) { $self->yield(get_weather => @params) }
sub ext_get_weather {
my $self = $_[OBJECT];
my %args = @_[ARG0 .. $#_];
my $location = $args{location};
unless ($location) {
warn "Missing 'location =>' in query\n";
my $fake_req = Weather::OpenWeatherMap::Request->new_for(
Current =>
tag => $args{tag},
location => '',
);
$self->_emit_error(
source => 'internal',
request => $fake_req,
status => "Missing 'location =>' in query",
);
return
}
my $type =
delete $args{forecast} ? 'Forecast'
: delete $args{find} ? 'Find'
: 'Current';
my $my_request = Weather::OpenWeatherMap::Request->new_for(
$type =>
(
$self->has_api_key && length $self->api_key ?
(api_key => $self->api_key) : ()
),
%args
);
if ($self->cache && (my $cached = $self->_cache->retrieve($my_request)) ) {
my $obj = $cached->object;
my (undef, $event) = $self->_result_type($my_request);
$self->emit( $event => $obj );
return $obj
}
$self->_issue_http_request($my_request);
()
}
method _issue_http_request (Object $my_request) {
unless ( $poe_kernel->alias_resolve($self->_ua_alias) ) {
POE::Component::Client::HTTP->spawn(
Alias => $self->_ua_alias,
FollowRedirects => 2,
)
}
$poe_kernel->post( $self->_ua_alias => request => ext_http_response =>
$my_request->http_request,
$my_request
);
}
sub ext_http_response {
my $self = $_[OBJECT];
my (undef, $my_request) = @{ $_[ARG0] };
my ($http_response) = @{ $_[ARG1] };
unless ($http_response->is_success) {
$self->_emit_error(
source => 'http',
request => $my_request,
status => $http_response->status_line,
);
return
}
my ($type, $event) = $self->_result_type($my_request);
my $content = $http_response->content;
my $my_response = Weather::OpenWeatherMap::Result->new_for(
$type =>
request => $my_request,
json => $content,
);
unless ($my_response->is_success) {
my $code = $my_response->response_code;
$self->_emit_error(
source => 'api',
request => $my_request,
status => "$code: ".$my_response->error,
);
return
}
$self->_cache->cache($my_response) if $self->cache;
$self->emit(
$event => $my_response
);
}
1;
=pod
=for Pod::Coverage ext_\w+ mxrp_\w+ has_\w+ set_api_key
=head1 NAME
POEx::Weather::OpenWeatherMap - POE-enabled OpenWeatherMap client
=head1 SYNOPSIS
use POE;
use POEx::Weather::OpenWeatherMap;
# An API key can be obtained (free) at http://www.openweathermap.org:
my $api_key = 'foo';
POE::Session->create(
package_states => [
main => [qw/
_start
pwx_error
pwx_weather
pwx_forecast
/],
],
);
sub _start {
my ($kernel, $heap) = @_[KERNEL, HEAP];
# Create, store, and start emitter:
my $wx = POEx::Weather::OpenWeatherMap->new(
api_key => $api_key,
event_prefix => 'pwx_',
);
$heap->{wx} = $wx;
$wx->start;
## An example request:
$wx->get_weather(
location => 'Manchester, NH',
tag => 'mytag',
);
}
sub pwx_error {
my $err = $_[ARG0];
my $status = $err->status;
my $request = $err->request;
# ... do something with error ...
warn "Error! ($status)";
}
sub pwx_weather {
my $result = $_[ARG0];
my $tag = $result->request->tag;
my $place = $result->name;
my $tempf = $result->temp_f;
my $conditions = $result->conditions_verbose;
# (see Weather::OpenWeatherMap::Result::Current for a method list)
# ...
}
sub pwx_forecast {
my $result = $_[ARG0];
my $place = $result->name;
my $itr = $result->iter;
while (my $day = $itr->()) {
my $date = $day->dt->mdy;
my $temp_hi = $day->temp_max_f;
my $temp_lo = $day->temp_min_f;
# (see Weather::OpenWeatherMap::Result::Forecast)
# ...
}
}
POE::Kernel->run;
=head1 DESCRIPTION
A POE-enabled interface to OpenWeatherMap (L<http://www.openweathermap.org>),
providing an object-oriented asynchronous interface to current & forecast
weather conditions for a given city, latitude/longitude, or OpenWeatherMap
city code.
This is really just an asynchronous counterpart to L<Weather::OpenWeatherMap>;
look there for documentation regarding Request & Result objects.
This an event emitter that consumes L<MooX::Role::POE::Emitter>; look there
for documentation on composed methods. See L<http://www.openweathermap.org>
for more on OpenWeatherMap itself.
=head2 ATTRIBUTES
=head3 api_key
Your L<OpenWeatherMap|http://www.openweathermap.org> API key.
(See L<http://www.openweathermap.org/api> to register for free.)
C<api_key> can be set after object construction via B<set_api_key>; if the key
is not valid, requests will likely fail with C<< 401 Unauthorized >> errors.
=head3 cache
A boolean value indicating whether successful results should be cached to disk
via L<Weather::OpenWeatherMap::Cache>.
Defaults to false. This may change in a future release.
=head3 cache_dir
The directory in which cache files are saved. The default may be fine; see
L<Weather::OpenWeatherMap::Cache>.
=head3 cache_expiry
The duration (in seconds) for which cache files are considered valid. The
default may be fine; see L<Weather::OpenWeatherMap::Cache>.
=head2 METHODS
=head3 start
Start our session.
Must be called before events will be received or emitted.
=head3 stop
Stop our session, shutting down the emitter and user agent (which will cancel
pending requests).
=head3 get_weather
$wx->get_weather(
# 'location =>' is mandatory.
# These are all valid location strings:
# By name:
# 'Manchester, NH'
# 'London, UK'
# By OpenWeatherMap city code:
# 5089178
# By latitude/longitude:
# 'lat 42, long -71'
location => 'Manchester, NH',
# Set 'forecast => 1' to get the forecast,
# omit or set to false for current weather:
forecast => 1,
# If 'forecast' is true, you can specify the number of days to fetch
# (up to 14):
days => 3,
# Optional tag for identifying the response to this request:
tag => 'foo',
);
Request a weather report for the given C<< location => >>.
The location can be a 'City, State' or 'City, Country' string, an
L<OpenWeatherMap|http://www.openweathermap.org/> city code, or a 'lat X, long
Y' string.
Requests the current weather by default (see
L<Weather::OpenWeatherMap::Request::Current>).
If passed C<< forecast => 1 >>, requests a weather forecast (see
L<Weather::OpenWeatherMap::Request::Forecast>), in which case C<< days
=> $count >> can be specified (up to 14). If C<< hourly => 1 >> is also
specified, an hourly (rather than daily) report is returned; see
the documentation for L<Weather::OpenWeatherMap> for details.
If passed C<< find => 1 >>, requests search results for a given location name
or latitude & longitude; see L<Weather::OpenWeatherMap::Request::Find>.
An optional C<< tag => >> can be specified to identify the response when it
comes in.
Any extra arguments are passed to the constructor for the appropriate Request
subclass; see L<Weather::OpenWeatherMap::Request>.
The request is made asynchronously and a response (or error) emitted when it
is available; see L</EMITTED EVENTS>. There is no useful return value.
=head2 RECEIVED EVENTS
=head3 get_weather
$poe_kernel->post( $wx->session_id =>
get_weather =>
location => 'Manchester, NH',
tag => 'foo',
);
POE interface to the L</get_weather> method; see L</METHODS> for available
options.
=head2 EMITTED EVENTS
=head3 error
Emitted when an error occurs; this may be an internal error, an HTTP error,
or an error reported by the OpenWeatherMap API.
C<$_[ARG0]> is a L<Weather::OpenWeatherMap::Error> object.
=head3 weather
Emitted when a request for the current weather has been successfully processed.
C<$_[ARG0]> is a L<Weather::OpenWeatherMap::Result::Current> object; see
that module's documentation for details on retrieving weather information.
=head3 forecast
Emitted when a request for a weather forecast has been successfully processed.
C<$_[ARG0]> is a L<Weather::OpenWeatherMap::Result::Forecast> object;
see that module's documentation for details on retrieving daily or hourly forecasts
(L<Weather::OpenWeatherMap::Result::Forecast::Day> &
L<Weather::OpenWeatherMap::Result::Forecast::Hour> objects) from the retrieved
forecast.
=head1 SEE ALSO
L<Weather::OpenWeatherMap>
L<Weather::OpenWeatherMap::Error>
L<Weather::OpenWeatherMap::Result>
L<Weather::OpenWeatherMap::Result::Current>
L<Weather::OpenWeatherMap::Result::Forecast>
L<Weather::OpenWeatherMap::Request>
L<Weather::OpenWeatherMap::Request::Current>
L<Weather::OpenWeatherMap::Request::Forecast>
The C<examples/> directory of this distribution.
=head1 AUTHOR
Jon Portnoy <avenj@cobaltirc.org>
=cut
# vim: ts=2 sw=2 et sts=2 ft=perl