| File: | blib/lib/TimeZone/TimeZoneDB.pm |
| Coverage: | 95.5% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package TimeZone::TimeZoneDB; | |||||
| 2 | ||||||
| 3 | 10 10 10 | 1049853 6 130 | use strict; | |||
| 4 | 10 10 10 | 12 6 201 | use warnings; | |||
| 5 | 10 10 10 | 1782 56373 20 | use autodie qw(:all); | |||
| 6 | ||||||
| 7 | 10 10 10 | 72825 11 208 | use Carp; | |||
| 8 | 10 10 10 | 2053 254290 126 | use CHI; | |||
| 9 | 10 10 10 | 24 7 214 | use JSON::MaybeXS; | |||
| 10 | 10 10 10 | 2961 116325 176 | use LWP::UserAgent; | |||
| 11 | 10 10 10 | 2829 389706 182 | use Object::Configure; | |||
| 12 | 10 10 10 | 33 73 158 | use Params::Get 0.13; | |||
| 13 | 10 10 10 | 14 57 111 | use Params::Validate::Strict 0.10; | |||
| 14 | 10 10 10 | 14 7 142 | use Readonly; | |||
| 15 | 10 10 10 | 11 8 83 | use Return::Set; | |||
| 16 | 10 10 10 | 13 5 81 | use Scalar::Util; | |||
| 17 | 10 10 10 | 19 9 24 | use Time::HiRes; | |||
| 18 | 10 10 10 | 323 10 6062 | use URI; | |||
| 19 | ||||||
| 20 - 28 | =head1 NAME TimeZone::TimeZoneDB - Interface to L<https://timezonedb.com> for looking up Timezone data =head1 VERSION Version 0.05 =cut | |||||
| 29 | ||||||
| 30 | our $VERSION = '0.05'; | |||||
| 31 | ||||||
| 32 | # --------------------------------------------------------------------------- | |||||
| 33 | # Compile-time constants for the timezonedb.com REST API. | |||||
| 34 | # These never vary and are inlined by the compiler. | |||||
| 35 | # --------------------------------------------------------------------------- | |||||
| 36 | Readonly::Scalar my $API_FORMAT => 'json'; | |||||
| 37 | Readonly::Scalar my $API_BY => 'position'; # query by geographic position | |||||
| 38 | ||||||
| 39 | # printf-style format for the cache key; 6 dp normalises 0.1 and 0.1000000 | |||||
| 40 | Readonly::Scalar my $CACHE_KEY_FMT => 'tz:%.6f:%.6f'; | |||||
| 41 | ||||||
| 42 | # Valid coordinate ranges as defined by the WGS-84 standard | |||||
| 43 | Readonly::Scalar my $LAT_MIN => -90; | |||||
| 44 | Readonly::Scalar my $LAT_MAX => 90; | |||||
| 45 | Readonly::Scalar my $LNG_MIN => -180; | |||||
| 46 | Readonly::Scalar my $LNG_MAX => 180; | |||||
| 47 | ||||||
| 48 | # --------------------------------------------------------------------------- | |||||
| 49 | # Runtime defaults. Every key may be overridden via Object::Configure, | |||||
| 50 | # which reads from a per-class configuration file or environment variables. | |||||
| 51 | # --------------------------------------------------------------------------- | |||||
| 52 | my %config = ( | |||||
| 53 | host => 'api.timezonedb.com', # remote API hostname | |||||
| 54 | api_version => 'v2.1', # path component for the API version | |||||
| 55 | api_endpoint => 'get-time-zone', # path component for the lookup method | |||||
| 56 | cache_expires => '1 day', # CHI expiry string for cached responses | |||||
| 57 | min_interval => 0, # minimum seconds between outbound requests | |||||
| 58 | ); | |||||
| 59 | ||||||
| 60 - 186 | =head1 SYNOPSIS
use TimeZone::TimeZoneDB;
my $tzdb = TimeZone::TimeZoneDB->new(key => 'XXXXXXXX');
my $tz = $tzdb->get_time_zone({ latitude => 0.1, longitude => 0.2 });
=head1 DESCRIPTION
The C<TimeZone::TimeZoneDB> Perl module provides an interface to the
L<https://timezonedb.com> API, enabling users to retrieve timezone data
based on geographic coordinates.
It supports configurable HTTP user agents, allowing for proxy settings
and request throttling.
The module includes robust error handling, ensuring proper validation of
input parameters and secure API interactions.
JSON responses are safely parsed with error handling to prevent crashes.
Designed for flexibility, it allows users to override default configurations
while maintaining a lightweight and efficient structure for querying timezone
information.
=over 4
=item * Caching
Identical requests are cached (using L<CHI> or a user-supplied caching object),
reducing the number of HTTP requests to the API and speeding up repeated queries.
A cache key is constructed from the normalised coordinates (6 decimal places)
so that C<0.1> and C<0.1000000> share the same cache entry.
=item * Rate-Limiting
A minimum interval between successive API calls can be enforced to ensure that
the API is not overwhelmed and to comply with any request throttling requirements.
Rate-limiting is implemented using L<Time::HiRes>.
A minimum interval between API calls can be specified via the C<min_interval>
parameter in the constructor.
Before making an API call, the module checks how much time has elapsed since
the last request and, if necessary, sleeps for the remaining time.
=back
=head1 METHODS
=head2 new
my $tzdb = TimeZone::TimeZoneDB->new(key => 'XXXXX');
# With a throttled user-agent that respects free-tier rate limits
use LWP::UserAgent::Throttled;
my $ua = LWP::UserAgent::Throttled->new();
$ua->env_proxy(1);
$tzdb = TimeZone::TimeZoneDB->new(ua => $ua, key => 'XXXXX');
# Retrieve the timezone for Ramsgate, UK
my $tz = $tzdb->get_time_zone({ latitude => 51.34, longitude => 1.42 })->{'zoneName'};
print "Ramsgate timezone: $tz\n";
Creates and returns a new C<TimeZone::TimeZoneDB> instance.
When invoked on an existing object rather than a class name, it returns a
shallow clone of that object with any supplied parameters merged in.
Passing C<ua =E<gt> undef> in a clone call is silently ignored so that the
original user-agent is inherited unchanged.
=head3 ARGUMENTS
=over 4
=item C<key> (required)
API key for timezonedb.com. Free keys are available at
L<https://timezonedb.com/register>.
=item C<ua> (optional)
An HTTP user-agent object. Must respond to C<get()>. Defaults to a plain
L<LWP::UserAgent> with C<gzip,deflate> accepted.
=item C<host> (optional)
Override the API hostname. Defaults to C<api.timezonedb.com>.
=item C<cache> (optional)
A L<CHI>-compatible caching object. Defaults to a private in-memory cache
with a one-day expiry.
=item C<min_interval> (optional)
Minimum number of seconds to wait between successive API calls.
Defaults to C<0> (no enforced delay).
=back
=head3 RETURNS
A blessed C<TimeZone::TimeZoneDB> reference.
Croaks if C<key> is absent.
=head3 SIDE EFFECTS
None.
=head3 NOTES
An optional C<logger> key may be passed; if present it must be an object
implementing C<warn()> and C<error()> (e.g. L<Log::Log4perl>).
=head3 API SPECIFICATION
=head4 INPUT
{
'key' => { type => 'string' },
'ua' => { type => 'object', can => 'get', optional => 1 },
'host' => { type => 'string', optional => 1 },
'cache' => { type => 'object', optional => 1 },
'min_interval' => { type => 'number', min => 0, optional => 1 },
}
=head4 OUTPUT
{ type => 'object' } # a blessed TimeZone::TimeZoneDB reference
=cut | |||||
| 187 | ||||||
| 188 | sub new | |||||
| 189 | { | |||||
| 190 | 243 | 1045511 | my $class = shift; | |||
| 191 | # Normalise both positional and named calling conventions | |||||
| 192 | 243 | 376 | my $params = Params::Get::get_params(undef, \@_) || {}; | |||
| 193 | ||||||
| 194 | # Support function-style call: TimeZone::TimeZoneDB::new() without -> | |||||
| 195 | 243 | 3215 | if(!defined($class)) { | |||
| 196 | 2 | 1 | $class = __PACKAGE__; | |||
| 197 | } elsif(Scalar::Util::blessed($class)) { | |||||
| 198 | # If $class is an object, clone it with new arguments | |||||
| 199 | # Clone path: merge new params over the existing object's fields. | |||||
| 200 | 22 | 25 | if(exists($params->{ua})) { | |||
| 201 | 11 | 26 | if(!defined($params->{ua})) { | |||
| 202 | # ua=>undef means "keep the original" -- silently drop it | |||||
| 203 | 5 | 4 | delete $params->{ua}; | |||
| 204 | } elsif(!Scalar::Util::blessed($params->{ua}) || !$params->{ua}->can('get')) { | |||||
| 205 | # A defined ua must be a proper object with a get() method | |||||
| 206 | 4 | 19 | Carp::croak("'ua' argument must be an object with a get() method"); | |||
| 207 | } | |||||
| 208 | } | |||||
| 209 | 18 18 18 | 10 22 50 | return bless { %{$class}, %{$params} }, ref($class); | |||
| 210 | } | |||||
| 211 | ||||||
| 212 | # Merge any file- or environment-based configuration into $params | |||||
| 213 | 221 | 249 | $params = Object::Configure::configure($class, $params); | |||
| 214 | ||||||
| 215 | # The API key is the only mandatory argument | |||||
| 216 | 221 | 49351 | my $key = $params->{'key'} or Carp::croak("'key' argument is required"); | |||
| 217 | ||||||
| 218 | # Build a default user-agent if the caller did not supply one | |||||
| 219 | 214 | 136 | my $ua = $params->{ua}; | |||
| 220 | 214 | 170 | if(!defined($ua)) { | |||
| 221 | 88 | 193 | $ua = LWP::UserAgent->new(agent => __PACKAGE__ . "/$VERSION"); | |||
| 222 | 88 | 18696 | $ua->default_header(accept_encoding => 'gzip,deflate'); | |||
| 223 | } | |||||
| 224 | ||||||
| 225 | # Prefer an explicit host override, then the package-level default | |||||
| 226 | 214 | 2137 | my $host = $params->{host} || $config{host}; | |||
| 227 | ||||||
| 228 | # Fall back to a private in-memory cache if none was supplied | |||||
| 229 | my $cache = $params->{cache} || CHI->new( | |||||
| 230 | driver => 'Memory', | |||||
| 231 | global => 0, | |||||
| 232 | expires_in => $config{cache_expires}, | |||||
| 233 | 214 | 392 | ); | |||
| 234 | ||||||
| 235 | # Use // so that an explicit 0 (disabled) is not replaced by the default | |||||
| 236 | 214 | 364087 | my $min_interval = $params->{min_interval} // $config{min_interval}; | |||
| 237 | ||||||
| 238 | return bless { | |||||
| 239 | key => $key, | |||||
| 240 | min_interval => $min_interval, | |||||
| 241 | last_request => 0, # epoch zero: no request has been made yet | |||||
| 242 | 214 214 | 145 676 | %{$params}, # pass any extra keys (e.g. logger) through | |||
| 243 | cache => $cache, # computed values override %{$params} copies | |||||
| 244 | host => $host, | |||||
| 245 | ua => $ua, | |||||
| 246 | }, $class; | |||||
| 247 | } | |||||
| 248 | ||||||
| 249 - 314 | =head2 get_time_zone
my $result = $tzdb->get_time_zone({ latitude => 51.34, longitude => 1.42 });
print $result->{'zoneName'}, "\n";
# Also accepts a Geo::Location::Point-compatible object
use Geo::Location::Point;
my $ramsgate = Geo::Location::Point->new({ latitude => 51.34, longitude => 1.42 });
my $tz = $tzdb->get_time_zone($ramsgate)->{'zoneName'};
Queries the timezonedb.com API for the IANA timezone name and associated
metadata at the supplied geographic coordinates.
Identical queries are served from cache without making a network request.
=head3 ARGUMENTS
=over 4
=item C<latitude> (required)
Decimal degrees, range C<-90> to C<+90>.
=item C<longitude> (required)
Decimal degrees, range C<-180> to C<+180>.
Alternatively, a single L<Geo::Location::Point>-compatible object (any
object implementing C<latitude()> and C<longitude()> methods) may be passed
instead of a hash or hashref.
=back
=head3 RETURNS
A hashref containing at least C<zoneName> on success.
Returns C<undef> when the API responds with a non-C<OK> status.
Croaks on HTTP errors or invalid arguments.
=head3 SIDE EFFECTS
Updates the internal response cache and the C<last_request> timestamp.
=head3 NOTES
The API key is transmitted as a URL query parameter because the
timezonedb.com API does not support an C<Authorization> header.
The key is redacted from all error and warning messages to prevent
accidental secret leakage into log aggregators or crash reporters.
=head3 API SPECIFICATION
=head4 INPUT
{
'latitude' => { type => 'number', min => -90, max => 90 },
'longitude' => { type => 'number', min => -180, max => 180 },
}
=head4 OUTPUT
Argument error : croak
HTTP error : croak
Non-OK status : undef
Success : { type => 'hashref', min => 1 }
=cut | |||||
| 315 | ||||||
| 316 | sub get_time_zone | |||||
| 317 | { | |||||
| 318 | 155 | 2012767 | my $self = shift; | |||
| 319 | 155 | 73 | my $params; | |||
| 320 | ||||||
| 321 | # Accept a Geo::Location::Point-compatible object or a plain hash/hashref | |||||
| 322 | 155 | 278 | if((@_ == 1) && Scalar::Util::blessed($_[0]) && $_[0]->can('latitude')) { | |||
| 323 | 9 | 9 | my $location = $_[0]; | |||
| 324 | 9 | 11 | $params->{latitude} = $location->latitude(); | |||
| 325 | 9 | 50 | $params->{longitude} = $location->longitude(); | |||
| 326 | } else { | |||||
| 327 | 146 | 170 | $params = Params::Get::get_params(undef, \@_); | |||
| 328 | } | |||||
| 329 | ||||||
| 330 | # Validate coordinate ranges; croaks on out-of-range or missing values | |||||
| 331 | 154 | 1884 | $params = Params::Validate::Strict::validate_strict( | |||
| 332 | args => $params, | |||||
| 333 | schema => { | |||||
| 334 | 'latitude' => { type => 'number', min => $LAT_MIN, max => $LAT_MAX }, | |||||
| 335 | 'longitude' => { type => 'number', min => $LNG_MIN, max => $LNG_MAX }, | |||||
| 336 | } | |||||
| 337 | ); | |||||
| 338 | ||||||
| 339 | 132 | 14377 | my $latitude = $params->{latitude}; | |||
| 340 | 132 | 81 | my $longitude = $params->{longitude}; | |||
| 341 | ||||||
| 342 | # Params::Validate::Strict silently skips type/range checks for undef values, | |||||
| 343 | # so guard explicitly to avoid sprintf warnings and silent URL corruption | |||||
| 344 | 132 | 125 | Carp::croak("Required parameter 'latitude' must be defined") unless defined($latitude); | |||
| 345 | 130 | 101 | Carp::croak("Required parameter 'longitude' must be defined") unless defined($longitude); | |||
| 346 | ||||||
| 347 | # Build the full API URL; key must go in the query string (API requirement) | |||||
| 348 | my $uri = URI->new( | |||||
| 349 | sprintf('https://%s/%s/%s', | |||||
| 350 | $self->{host}, | |||||
| 351 | $config{api_version}, | |||||
| 352 | $config{api_endpoint} | |||||
| 353 | ) | |||||
| 354 | 129 | 275 | ); | |||
| 355 | $uri->query_form( | |||||
| 356 | by => $API_BY, | |||||
| 357 | lat => $latitude, | |||||
| 358 | lng => $longitude, | |||||
| 359 | format => $API_FORMAT, | |||||
| 360 | 129 | 32413 | key => $self->{'key'}, | |||
| 361 | ); | |||||
| 362 | 129 | 7925 | my $url = $uri->as_string(); | |||
| 363 | ||||||
| 364 | # Normalise to 6 dp so that 0.1 and 0.1000000 share the same cache slot | |||||
| 365 | 129 | 581 | my $cache_key = sprintf($CACHE_KEY_FMT, $latitude, $longitude); | |||
| 366 | 129 | 172 | if(my $cached = $self->{cache}->get($cache_key)) { | |||
| 367 | 14 | 1058 | return $cached; | |||
| 368 | } | |||||
| 369 | ||||||
| 370 | # Sleep if needed to honour the caller's minimum inter-request interval | |||||
| 371 | 115 | 3679 | my $now = time(); | |||
| 372 | 115 | 87 | my $elapsed = $now - $self->{last_request}; | |||
| 373 | 115 | 145 | if($elapsed < $self->{min_interval}) { | |||
| 374 | 5 | 1000192 | Time::HiRes::sleep($self->{min_interval} - $elapsed); | |||
| 375 | } | |||||
| 376 | ||||||
| 377 | # Perform the HTTP GET; all transport details are the UA's responsibility | |||||
| 378 | 115 | 172 | my $res = $self->{ua}->get($url); | |||
| 379 | ||||||
| 380 | # Stamp the request time before any early return so rate-limiting is correct | |||||
| 381 | 115 | 2059 | $self->{last_request} = time(); | |||
| 382 | ||||||
| 383 | # Redact the API key before including the URL in any error message | |||||
| 384 | 115 | 141 | if($res->is_error()) { | |||
| 385 | 8 | 44 | (my $safe_url = $url) =~ s/key=[^&]*/key=REDACTED/; | |||
| 386 | 8 | 13 | if(my $logger = $self->{logger}) { | |||
| 387 | 0 | 0 | $logger->error($safe_url . ' API returned error: ' . $res->status_line()); | |||
| 388 | } | |||||
| 389 | 8 | 18 | Carp::croak($safe_url . ' API returned error: ' . $res->status_line()); | |||
| 390 | } | |||||
| 391 | ||||||
| 392 | # Safely decode the JSON body; a malformed response is a soft failure | |||||
| 393 | 105 | 343 | my $rc; | |||
| 394 | 105 105 | 51 598 | eval { $rc = JSON::MaybeXS->new()->utf8()->decode($res->decoded_content()) }; | |||
| 395 | 105 | 9908 | if($@) { | |||
| 396 | 6 | 10 | if(my $logger = $self->{logger}) { | |||
| 397 | 0 | 0 | $logger->warn("Failed to parse JSON response: $@"); | |||
| 398 | } | |||||
| 399 | 6 | 26 | Carp::carp("Failed to parse JSON response: $@"); | |||
| 400 | 6 | 1840 | return; | |||
| 401 | } | |||||
| 402 | ||||||
| 403 | # Cache the decoded response before returning so the next caller is served | |||||
| 404 | 99 | 133 | $self->{'cache'}->set($cache_key, $rc); | |||
| 405 | ||||||
| 406 | # A non-OK API status means the coordinates returned no result | |||||
| 407 | 99 | 13361 | if($rc && defined($rc->{'status'}) && ($rc->{'status'} ne 'OK')) { | |||
| 408 | 12 | 25 | if(my $logger = $self->{'logger'}) { | |||
| 409 | 5 | 14 | (my $safe_url = $url) =~ s/key=[^&]*/key=REDACTED/; | |||
| 410 | 5 | 15 | $logger->warn(__PACKAGE__ . ": $safe_url returns $rc->{status}"); | |||
| 411 | } | |||||
| 412 | 12 | 41 | return; | |||
| 413 | } | |||||
| 414 | ||||||
| 415 | # Assert the output contract: a non-empty hashref | |||||
| 416 | 86 | 154 | return Return::Set::set_return($rc, { 'type' => 'hashref', 'min' => 1 }); | |||
| 417 | } | |||||
| 418 | ||||||
| 419 - 482 | =head2 ua
# Getter: retrieve the current user-agent
my $ua = $tzdb->ua();
$ua->env_proxy(1);
# Setter: swap in a throttled agent (returns the new agent for compatibility)
use LWP::UserAgent::Throttled;
my $new_ua = LWP::UserAgent::Throttled->new();
$new_ua->throttle('timezonedb.com' => 1);
$tzdb->ua($new_ua);
Gets or sets the HTTP user-agent object used for API requests.
The return value is always the current user-agent (after any update),
consistent with the convention used by L<LWP::UserAgent> and related
packages that expose a C<ua()> accessor.
=head3 ARGUMENTS
=over 4
=item C<ua> (optional)
Replacement user-agent object. Must implement a C<get($url)> method.
Omit to use this method as a getter.
=back
=head3 RETURNS
The user-agent object stored on the instance -- the supplied value when
called as a setter, the existing value when called as a getter.
Croaks if a defined but invalid object (no C<get()> method) is supplied,
or if C<undef> is explicitly passed.
=head3 SIDE EFFECTS
When used as a setter, all subsequent API calls on this object use the new
user-agent.
=head3 NOTES
Free timezonedb.com accounts are rate-limited to one request per second.
Use L<LWP::UserAgent::Throttled> to enforce this transparently.
The accessor always returns the user-agent rather than C<$self> so that
callers can do C<$tzdb-E<gt>ua()-E<gt>env_proxy(1)> in a single expression
without ambiguity about what was returned.
=head3 API SPECIFICATION
=head4 INPUT
# Getter (no argument)
{}
# Setter
{ 'ua' => { type => 'object', can => 'get' } }
=head4 OUTPUT
{ type => 'object' } # the stored user-agent (getter or setter)
=cut | |||||
| 483 | ||||||
| 484 | sub ua { | |||||
| 485 | 77 | 10804 | my $self = shift; | |||
| 486 | ||||||
| 487 | # Getter path: no arguments, return the stored agent immediately | |||||
| 488 | 77 | 107 | return $self->{ua} unless @_; | |||
| 489 | ||||||
| 490 | # Params::Get::get_params('ua', \@_) mis-routes the named form ua(ua => $ref) | |||||
| 491 | # into { ua => [$key, $ref] } because its $array_ref path fires before the | |||||
| 492 | # even-count hash path when the value is a reference (Params::Get line 258). | |||||
| 493 | # Detect and fix the named-pair form manually before passing to validate_strict. | |||||
| 494 | 50 | 30 | my $args; | |||
| 495 | 50 | 97 | if(@_ == 2 && defined($_[0]) && !ref($_[0]) && $_[0] eq 'ua') { | |||
| 496 | 5 | 6 | $args = { ua => $_[1] }; # named: ua(ua => $obj) | |||
| 497 | } else { | |||||
| 498 | 45 | 52 | $args = Params::Get::get_params('ua', \@_); # positional: ua($obj) | |||
| 499 | } | |||||
| 500 | ||||||
| 501 | # Validate that the supplied object implements the interface we depend on | |||||
| 502 | 49 | 654 | my $params = Params::Validate::Strict::validate_strict( | |||
| 503 | args => $args, | |||||
| 504 | schema => { | |||||
| 505 | ua => { | |||||
| 506 | type => 'object', | |||||
| 507 | can => 'get', | |||||
| 508 | } | |||||
| 509 | } | |||||
| 510 | ); | |||||
| 511 | ||||||
| 512 | # Params::Validate::Strict skips the type check for undef 'object' params, | |||||
| 513 | # so we must guard explicitly to prevent silent corruption of $self->{ua} | |||||
| 514 | 31 | 2160 | if(!defined($params->{ua})) { | |||
| 515 | 11 | 15 | if(my $logger = $self->{'logger'}) { | |||
| 516 | 4 | 8 | $logger->error('ua() requires a defined value'); | |||
| 517 | } | |||||
| 518 | 11 | 76 | Carp::croak('ua() requires a defined value'); | |||
| 519 | } | |||||
| 520 | ||||||
| 521 | # Store the new agent and return it, consistent with LWP::UserAgent convention | |||||
| 522 | 20 | 20 | $self->{ua} = $params->{ua}; | |||
| 523 | 20 | 66 | return $self->{ua}; | |||
| 524 | } | |||||
| 525 | ||||||
| 526 - 632 | =head1 AUTHOR
Nigel Horne, C<< <njh@nigelhorne.com> >>
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.
Lots of thanks to the folks at L<https://timezonedb.com>.
=head1 BUGS
This module is provided as-is without any warranty.
Please report any bugs or feature requests to C<bug-timezone-timezonedb at rt.cpan.org>,
or through the web interface at
L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=TimeZone-TimeZoneDB>.
I will be notified, and then you'll automatically be notified of progress
on your bug as I make changes.
=head1 SEE ALSO
=over 4
=item * TimezoneDB API: L<https://timezonedb.com/api>
=item * L<Test Dashboard|https://nigelhorne.github.io/TimeZone-TimeZoneDB/coverage/>
=back
=encoding utf-8
=head2 FORMAL SPECIFICATION
=head3 new
TimeZoneDB-State ::= [
key : STRING ;
ua : USERAGENT ;
host : STRING ;
cache : CACHE ;
min_interval : â ;
last_request : â
]
Init
key? : STRING
ua? : USERAGENT ⪠{â¥}
host? : STRING ⪠{â¥}
cache? : CACHE ⪠{â¥}
min_interval? : â ⪠{â¥}
result! : TimeZoneDB-State
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
key? â "" â§
result!.key = key? â§
result!.ua = (if ua? â ⥠then ua? else DefaultUA) â§
result!.host = (if host? â ⥠then host? else config.host) â§
result!.cache = (if cache? â ⥠then cache? else NewCache) â§
result!.min_interval = (if min_interval? â ⥠then min_interval? else 0) â§
result!.last_request = 0
=head3 get_time_zone
GetTimeZone
Î TimeZoneDB-State (writes cache and last_request)
lat? : {n : â | -90 ⤠n ⤠90}
lng? : {n : â | -180 ⤠n ⤠180}
result! : HASHREF ⪠{â¥}
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
let k == sprintf(CACHE_KEY_FMT, lat?, lng?)
â§ cache.has(k) â
result! = cache.get(k)
â§ last_request' = last_request
â§ cache' = cache
⧠¬cache.has(k) â
let r == ua.get(ApiUrl(lat?, lng?, key))
⧠¬r.ok â â¥
â§ r.ok â§ r.json.status = "OK" â
result! = r.json
â§ cache' = cache â {k ⦠r.json}
â§ last_request' = now
â§ r.ok â§ r.json.status â "OK" â
result! = â¥
â§ cache' = cache
â§ last_request' = now
=head2 ua
UA
Delta TimeZoneDB-State
ua? : USERAGENT ⪠{â¥} (⥠= not supplied)
ua! : USERAGENT
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
(ua? = ⥠⧠ua' = ua) â¨
(ua? â ⥠⧠defined(ua?) â§ ua? can 'get'
â§ ua' = ua?
â§ â x : {key, host, cache, min_interval, last_request} ⢠x' = x)
â§ ua! = ua'
=head1 LICENSE AND COPYRIGHT
Copyright 2023-2026 Nigel Horne.
Usage is subject to the GPL2 licence terms.
If you use it,
please let me know.
=cut | |||||
| 633 | ||||||
| 634 | 1; | |||||