The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use strict;
use warnings;
package WebService::HealthGraph;
$WebService::HealthGraph::VERSION = '0.000004';
use Moo;

use LWP::UserAgent ();
use Types::Standard qw( Bool HashRef InstanceOf Int Str );
use Types::URI qw( Uri );
use URI ();
use URI::FromHash qw( uri );
use WebService::HealthGraph::Response ();

has auto_pagination => (
    is      => 'ro',
    isa     => Bool,
    default => 1,
);

has base_url => (
    is      => 'ro',
    isa     => Uri,
    lazy    => 1,
    default => 'https://api.runkeeper.com',
    coerce  => 1,
);

has debug => (
    is      => 'ro',
    isa     => Bool,
    default => 0,
);

has token => (
    is        => 'ro',
    isa       => Str,
    predicate => '_has_token',
);

has ua => (
    is      => 'ro',
    isa     => InstanceOf ['LWP::UserAgent'],
    lazy    => 1,
    builder => '_build_ua',
);

has url_map => (
    is      => 'ro',
    isa     => HashRef,
    lazy    => 1,
    builder => '_build_url_map',
);

has user => (
    is      => 'ro',
    isa     => InstanceOf ['WebService::HealthGraph::Response'],
    lazy    => 1,
    builder => '_build_user',
);

has user_id => (
    is      => 'ro',
    isa     => Int,
    lazy    => 1,
    default => sub { shift->user->content->{userID} },
);

sub _build_ua {
    my $self = shift;
    my $ua   = LWP::UserAgent->new;
    if ( $self->_has_token ) {
        $ua->default_header( Authorization => 'Bearer ' . $self->token );
    }

    return $ua unless $self->debug;
    require LWP::ConsoleLogger::Easy;
    LWP::ConsoleLogger::Easy::debug_ua($ua);
    return $ua;
}

sub _build_url_map {
    my $self = shift;
    my %map  = %{ $self->user->content };
    delete $map{userID};
    return \%map;
}

sub _build_user {
    my $self = shift;
    return $self->get('/user');
}

sub get {
    my $self    = shift;
    my $url     = shift;
    my $args    = shift;
    my $headers = $args->{headers} || {};
    my $feed    = $args->{feed} || 0;

    $url = URI->new($url) unless ref $url;

    my $path = $url->path;

    $url->scheme( $self->base_url->scheme );
    $url->host( $self->base_url->host );

    my @path_parts = $url->path_segments;
    shift @path_parts;    # first part is empty string with an absolute URL
    my $top_level = shift @path_parts;

    my %type = (
        backgroundActivities       => 'BackgroundActivitySet',
        changeLog                  => 'ChangeLog',
        diabetes                   => 'DiabetesMeasurementSet',
        fitnessActivities          => 'FitnessActivity',
        generalMeasurements        => 'GeneralMeasurementSet',
        nutrition                  => 'NutritionSet',
        profile                    => 'Profile',
        records                    => 'Records',
        settings                   => 'Settings',
        sleep                      => 'SleepSet',
        strengthTrainingActivities => 'StrengthTrainingActivity',
        team                       => 'Team',
        user                       => 'User',
        weight                     => 'WeightSet',
    );

    unless ( exists $headers->{Accept} ) {
        my $accept = $type{$top_level};

        # Weird exception to the rule
        if ( @path_parts and $top_level eq 'team' ) {
            $accept = 'Member';
        }

        $accept .= 'Feed' if $feed;

        $headers->{Accept}
            = sprintf( 'application/vnd.com.runkeeper.%s+json', $accept );
    }

    # Fix up URLs with a semicolon delimiter.
    if ( $url =~ m{;} ) {
        $url = $url->as_string;
        $url =~ s{;}{&}g;
    }

    my $res = $self->ua->get( $url, %{$headers} );
    return WebService::HealthGraph::Response->new(
        get => sub { $self->get( shift, $args ) }, raw => $res );
}

sub url_for { return shift->uri_for(@_) }

sub uri_for {
    my $self  = shift;
    my $type  = shift;
    my $query = shift;

    die "type $type not found" unless exists $self->url_map->{$type};

    return uri(
        path => $self->url_map->{$type},
        $query
        ? (
            query           => $query,
            query_separator => '&',
            )
        : (),
    );
}

1;

=pod

=encoding UTF-8

=head1 NAME

WebService::HealthGraph - A thin wrapper around the Runkeeper (Health Graph) API

=head1 VERSION

version 0.000004

=head1 SYNOPSIS

    my $runkeeper = WebService::HealthGraph->new(
        debug => 1,
        token => 'foo',
    );

    my $user = $runkeeper->user;

    use Data::Printer;
    p $user->content;

    # Fetch a weight feed

    use DateTime ();
    use URI::FromHash qw( uri );

    my $cutoff = DateTime->now->subtract( days => 7 );

    my $uri = uri(
        path  => '/weight',
        query => { noEarlierThan => $cutoff->ymd },
    );

    my $feed = $runkeeper->get($uri, { feed => 1 });
    p $feed->content;

=head1 DESCRIPTION

BETA BETA BETA.  The interface is subject to change.

This is a very thin wrapper around the Runkeeper (Health Graph) API.  At this
point it assumes that you already have an OAuth token to connect with.  You can
use L<Mojolicious::Plugin::Web::Auth::Site::Runkeeper> to create a token.  If
that doesn't suit you, patches to add OAuth token retrieval to this module will
be happily accepted.

=head1 CONSTRUCTOR ARGUMENTS

=head2 auto_pagination

Boolean.  If enabled, response objects will continue to fetch new result pages
as the iterator requires them.  Defaults to true.

=head2 base_url

The URL of the API.  Defaults to L<https://api.runkeeper.com>.  This is
settable in case you'd need this for mocking.

=head2 debug( $bool )

Turns on debugging via L<LWP::ConsoleLogger>.  Off by default.

=head2 token

OAuth token. Optional, but you'll need to to get any URLs.

=head2 ua

A user agent object of the L<LWP::UserAgent> family.  If you provide your own,
be sure you set the correct default headers required for authentication.

=head2 url_map

Returns a map of keys to URLs, as provided by the C<user> endpoint.  Runkeeper
wants you to use these URLs rather than constructing your own.

=head2 uri_for

Gives you the corresponding url (in the form of an L<URI> object) for any key
which exists in C<url_map>.  You can optionally pass a HashRef of query params
to this method.

    my $team_uri =  $runkeeper->uri_for( 'team', { pageSize => 10 } );

    my $friends = $runkeeper->get(
        $runkeeper->uri_for( 'team', { pageSize => 10 } ),
        { feed => 1 }
    );

=head2 url_for

Convenience method which points to C<url_for>.  Will be removed in a later
release.

=head2 user

The L<WebService::HealthGraph::Response> object for the C<user> endpoint.

=head2 user_id

The id of the user as provided by the C<user> endpoint.

=head1 METHODS

=head2 get( $url, $optional_args )

This module will try to do the right thing with the minimum amount of
information:

    my $weight_response = $runkeeper->get( 'weight', { feed => 1 } );
    if ( $weight_response->success ) {
        ...
    }

Optionally, you can provide your own Accept (or other) headers:

    my $record_response = $runkeeper->get(
        'records',
        {
            headers =>
                { Accept => 'application/vnd.com.runkeeper.Records+json' }
        );

Returns a L<WebService::HealthGraph::Response> object.

=head1 CAVEATS

Most response content will contain a C<HashRef>, but the C<records> endpoint
returns a response with an C<ArrayRef> in the content.

=head1 AUTHOR

Olaf Alders <olaf@wundercounter.com>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2016 by Olaf Alders.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut

__END__
# ABSTRACT: A thin wrapper around the Runkeeper (Health Graph) API