The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package WWW::3Taps::API;

use Moose;
use MooseX::Params::Validate;
use URI;
use LWP::UserAgent;
use JSON::Any;
use WWW::3Taps::API::Types qw( Source Category Location Timestamp JSONMap
  JSONBoolean Retvals List Dimension ReferenceType NotificationFormat);
use MooseX::Types::Moose qw(Str Int Num HashRef ArrayRef);
use MooseX::Types::Structured qw(Dict Tuple Optional);
use MooseX::Types::Locale::Language qw(LanguageCode);

=head1 NAME

WWW::3Taps::API

=head1 VERSION

Version 0.05

=cut

our $VERSION = '0.06';

has agent_id => (
  is        => 'rw',
  isa       => 'Str',
  predicate => '_has_agent_id'
);

has auth_id => (
  is        => 'rw',
  isa       => 'Str',
  predicate => '_has_auth_id'
);

has _server => (
  is      => 'rw',
  isa     => 'Str',
  default => 'http://3taps.net'
);

has _ua => (
  is      => 'ro',
  isa     => 'LWP::UserAgent',
  default => sub { LWP::UserAgent->new() }
);

has _json_handler => (
  is      => 'rw',
  default => sub { JSON::Any->new( utf8 => 1, allow_nonref => 1 ) },
  handles => {
    _from_json => 'from_json',
    _to_json   => 'to_json'
  },
);

=head1 SYNOPSIS


  use WWW::3Taps::API;

  my $api = WWW::3Taps::API->new();
  my $results = $api->search( location => 'LAX', category => 'VAUT' );

  # $results = {
  #   execTimeMs => 325,
  #   numResults => 141087,
  #   success => bless( do { \( my $o = 1 ) }, 'JSON::XS::Boolean' )
  #   results    => [
  #     {
  #       category => "VAUT",
  #       externalURL =>
  #         "http://cgi.ebay.com/Ferrari-360-/8181818foo881818bar",
  #       heading =>
  # "Ferrari : 360 Coupe 2000 Ferrari 360 F1 Modena Coupe 20k Fresh Timing Belts",
  #       location  => "LAX",
  #       source    => "EBAYM",
  #       timestamp => "2011/03/08 01:13:05 UTC"
  #     },
  #    ...


  if ( $results->{success} ){
    foreach my $result (@{$results->{results}}) {
      print qq|<a href="$result->{externalURL}">$result->{heading}</a>\n|;
    }
  }


=head1 DESCRIPTION

This module provides an Object Oriented interface to 3taps(L<http://3taps.net>)
search API. See L<http://developers.3taps.net> for a full description of the
3taps API and L<https://github.com/3taps/3taps-Perl-Client> for the source
repository.

=head1 SUBROUTINES/METHODS

=head1 Search methods

=head2 search(%params)

  use WWW::3Taps::API;

  my $api    = WWW::3Taps::API->new;
  my $result = $api->search(
    location    => 'LAX+OR+NYC',
    category    => 'VAUT',
    annotations => '{"make":"porsche"}'
  );
  my $results = $api->search(location => 'LAX', category => 'VAUT');

  # {
  #   execTimeMs => 7,
  #   numResults => 0,
  #   results    => [ ... ],
  #   success    => 1
  # }



The search method creates a new search request.

=head3 Parameters

=over

=item rpp

The number of results to return for a synchonous search. If this is not specified, 
a maximum of ten postings will be returned at once. If this is set to -1, all matching
postings will be returned at once. 

=item page

The page number of the results to return for a synchronous search, where zero is the 
first page of results. If this is not specified, the most recent page of postings will
be returned.

=item source

The 5-character source code a posting must have if is to be included in the list of 
search results.

=item category

The 4-character category code a posting must have if it is to be included in the list 
of search results. Note that multiple categories can be searched by passing in multiple
category codes, separated by +OR+.


=item location

The 3-character location code a posting must have if it is to be included in the list 
of search results. Note that multiple locations can be searched by passing in multiple 
location codes, separated by +OR+.


=item heading

A string which must occur within the heading of the posting if it is to be included in 
the list of search results.


=item body

A string which must occur within the body of the posting if it is to be included in the
list of search results.

=item text

A string which must occur in either the heading or the body of the posting if it is to 
be included in the list of search results.

=item poster

The user ID of the person who created the posts. If this is specified, only postings 
created by the specified user will be included in the list of search results

=item externalID

A string which must match the "externalID" field for a posting if it is to be included
in the list of search results.

=item start

(YYYY-MM-DD HH:MM:SS) This defines the desired starting timeframe for the search query.
Only postings with a timestamp greater than or equal to the given value will be
included in the list of search results. Note: all times in 3taps are in UTC.

=item end

(YYYY-MM-DD HH:MM:SS) This defines the desired ending timeframe for the search query. 
Only postings with a timestamp less than or equal to the given value will be included 
in the list of search results. Note: all times in 3taps are in UTC.

=item annotations

A JSON encoded map of key/value pairs that a posting must have in annotations to be 
included in the list of search results


=item trustedAnnotations

A JSON encoded map of key/value pairs that a posting must have in trusted annotations
to be included in the list of search results



=item retvals

A comma-separated list of the fields to return for each posting that matches the desired
set of search criteria. The following field names are currently supported:

  source
  category
  location
  longitude
  latitude
  heading
  body
  images
  externalURL
  userID
  timestamp
  externalID
  annotations
  postKey

These fields match the fields with the same name as defined in the Posting API.  If no 
retvals argument is supplied, the following list of fields will be returned by default:

  category
  location
  heading
  externalURL
  timestamp

=back

=head3 Returns

A hashref containing a decoded JSON object with the following fields:

=over

=item success

If the search was a success, this will be true.

=item numResults

The total number of results found for this search.

=item execTimeMs

The amount of time it took 3taps to perform your search, in milliseconds.

=item error

If success is false, error will contain the error message

=item results

An array of posting objects, each containing the fields specified in retvals

=back

=cut

my @_search_params = (
  rpp                => { isa => 'Int',     optional => 1 },
  page               => { isa => 'Int',     optional => 1 },
  source             => { isa => Source,    optional => 1 },
  category           => { isa => Category,  optional => 1 },
  location           => { isa => Location,  optional => 1 },
  heading            => { isa => 'Str',     optional => 1 },
  body               => { isa => 'Str',     optional => 1 },
  text               => { isa => 'Str',     optional => 1 },
  poster             => { isa => 'Str',     optional => 1 },
  externalID         => { isa => 'Str',     optional => 1 },
  start              => { isa => Timestamp, optional => 1 },
  end                => { isa => Timestamp, optional => 1 },
  annotations        => { isa => JSONMap,   optional => 1 },
  trustedAnnotations => { isa => JSONMap,   optional => 1 },
  retvals            => { isa => Retvals,   optional => 1 }
);

sub search {
  my ( $self, %params ) = validated_hash( \@_, @_search_params );

  confess 'You need to provide at least a query parameter'
    unless scalar values %params;

  my $uri = URI->new( $self->_server );

  $uri->path('search');
  $uri->query_form(%params);

  return $self->_do_request( get => $uri );
}

=head2 count(%search_params)

  my $api = WWW::3Taps::API->new;
  my $result = $api->count( location => 'LAX', category => 'VAUT' );

  # { count => 146725 }


Returns the number of items matching a given search. Note that this method accepts the
same general parameters as the search method.


=head3 Parameters

Same as C<search> method

=head3 Returns

A hashref with a single field, "count", holding the number of matches found for the 
given parameters.

=cut

sub count {
  my ( $self, %params ) = validated_hash( \@_, @_search_params );

  confess 'You need to provide at least a query parameter'
    unless scalar values %params;

  my $uri = URI->new( $self->_server );

  $uri->path('search/count');
  $uri->query_form(%params);

  return $self->_do_request( get => $uri );

}

=head2 range(%search_params, fields => $fields)

  my $api = WWW::3Taps::API->new;
  my $result = $api->range( location => 'LAX', category => 'VAUT', fields => 'year,price');

  # {
  #   price => { max => 15000, min => 200 },
  #   year  => { max => 2011, min => 1967 },
  # }



Returns the minimum and maximum values currently in 3taps for the given fields, that 
match the given search parameters. The basic idea here is to provide developers with a
method of determining sensible values for range-based filters. Note that this method 
accepts the same query parameters as the search method.

=head3 Parameters

=over

=item fields

A comma-separated list of fields to retrieve the min and max values for. The Search API
will look for the min and max values in fields and annotations.

=back

=head3 Returns

A hashref with the min and max values for each field.

=cut

sub range {
  my ( $self, %params ) =
    validated_hash( \@_, @_search_params, fields => { isa => List } );

  confess 'You need to provide at least a query parameter'
    unless scalar values %params;

  my $uri = URI->new( $self->_server );

  $uri->path('search/range');
  $uri->query_form(%params);

  return $self->_do_request( get => $uri );
}

=head2 summary( %search_params, dimension => $dimension)


  my $api = WWW::3Taps::API->new;
  my $result = $api->summary( text => 'toyota', dimension => 'source');

  # {
  #   execTimeMs => 360,
  #   totals => {
  #     "37SIG" => 0,
  #     "3TAPS" => 0,
  #     "9-1-1" => 0,
  #     "AMZON" => 0,
  #     "CRAIG" => 184231,
  #     "E_BAY" => 5221,
  #      ...
  #   }
  # }

Returns the total number of postings found in 3taps, across the given dimension, that 
match the given search query parameters. For example, searching for "text=toyota" 
across "dimension=source" would return a list of all sources in 3taps, along with the 
number of postings matching the search "text=toyota" in that source. All search query 
parameters are supported. You may currently search across dimensions source, category, 
and location. At this time, category will only search across top level categories, and 
location is limited to our top 10 metro areas.

=head3 Parameters

=over

=item dimension

The dimension to summarize across: source, category, or location.

=back

=head3 Returns

A hashref with the following fields:

=over

=item totals

A decoded JSON object with one field for each member of the dimension, along with the 
total found (matching the search query) in that dimension.

=item execTimeMs

The number of milliseconds it took 3taps to retrieve this information for you. 

=back

=cut

sub summary {
  my ( $self, %params ) =
    validated_hash( \@_, @_search_params, dimension => { isa => Dimension } );

  confess 'You need to provide at least a query parameter'
    unless scalar values %params;

  my $uri = URI->new( $self->_server );

  $uri->path('search/summary');
  $uri->query_form(%params);

  return $self->_do_request( get => $uri );
}

=head1 Status methods

=head2 update_status

  my $api     = WWW::3Taps::API->new;
  my $results = $api->status_update(
    postings => [
      {
        source     => "E_BAY",
        externalID => "3434399120",
        status     => "sent",
        timestamp  => "2011/12/21 01:13:28",
        attributes => { postKey => "3JE8VFD" }
      },
      {
        source     => "E_BAY",
        externalID => "33334399121",
        status     => "sent",
        timestamp  => "2011/12/21 01:13:28",
        attributes => { postKey => "3JE8VFF" }
      }
    ]
  );

Send in status updates for postings

=head3 Parameters

=over

=item postings

An array containing a list of hashrefs representing the posting status updates.
Each entry in this array must contain a key representing the following:

=over

=item status (required)

The status of the posting

=item externalID (required)

The ID of the posting in the source system.

=item source (required)

The 5 letter code of the source of this posting. (ex: CRAIG, E_BAY)

=item timestamp (optional)

The time that this status occured, in format YYYY/MM/DD hh:mm:dd, in UTC.

=item attributes (optional)

A hashref containing name/value pairs of attributes to associate with this status. (ex: postKey, errors)

=back

=back

=head3 Returns

The body of the response will consist of a hashref with two fields, code and message.

=cut

sub update_status {
  my ( $self, %params ) = validated_hash(
    \@_,
    postings => {
      isa => ArrayRef [
        Dict [
          status     => Str,
          externalID => Str,
          source     => Str,
          timestamp  => Optional [Timestamp],
          attributes => Optional [HashRef]
        ]
      ]
    }
  );
  my $args = { data => $self->_to_json( \%params ) };

  my $uri = URI->new( $self->_server );
  $uri->path('status/update');
  $self->_do_request( post => $uri, request_args => $args );

}

=head2 get_status

  my $api     = WWW::3Taps::API->new;
  my $results = $api->get_status(
    ids => [
      { source => 'CRAIG', externalID => 3434399120 },
      { source => 'CRAIG', externalID => 33334399121 }
    ]
  );

  # [
  #   {
  #     exists => bless( do { \( my $o = 0 ) }, 'JSON::XS::Boolean' ),
  #     externalID => "3434399120",
  #     source     => "CRAIG"
  #   },
  #   {
  #     exists => bless( do { \( my $o = 1 ) }, 'JSON::XS::Boolean' ),
  #     externalID => "3434399121",
  #     history    => {
  #       saved => [
  #         {
  #           attributes => {
  #             batchKey => "BDBBTHF500",
  #             postKey  => "BDBBTXQ"
  #           },
  #           errors    => undef,
  #           timestamp => "2011-02-25T18:24:41Z"
  #         }
  #       ]
  #     },
  #     source => "CRAIG"
  #   }
  # ]

Get status history for postings

=head3 Parameters

=over

=item ids

An array of hashrefs containing a key/value pair of two fields: "externalID" and "source".
Each field will identify a posting to retrieve status for in this request.

=back

=head3 Returns

An array of hashrefs, each representing a requested posting, each with the following fields

=over

=item exists (boolean)

If false, the Status API has no history of the posting.

=item externalID (string)

The external ID of this requested posting.

=item source (string)

The 5 letter code of the source of this posting. (ex: E_BAY, CRAIG)

=item history (hashref)

The history hashref contains a number of fields, one for each "status" that has been
recorded for the posting. Within each status field, the value is an array of status
events for that status. For example, in the "found" status field, you would find a
status event object for each time the posting was found. Each status event object can
contain the following fields:

=over

=item timestamp

The date that this status event was recorded, in UTC.

=item errors

An array of error hashrefs, each with two fields: "code" and "message".

=item attributes

An hashref holding a number of key/value pairs associated with this status event
(ex: postKey)

=back

=back

=cut

sub get_status {
  my ( $self, %params ) = validated_hash(
    \@_,
    ids => {
      isa => ArrayRef [
        Dict [
          externalID => Str,
          source     => Str
        ]
      ]
    }
  );

  my $args = { ids => $self->_to_json( $params{ids} ) };

  my $uri = URI->new( $self->_server );
  $uri->path('status/get');

  $self->_do_request( post => $uri, request_args => $args );

}

=head2 system_status

  my $api     = WWW::3Taps::API->new;
  my $results = $api->system_status();

  # { code => 200, message => "3taps is up and running!" }

Get the current system status.

=head3 Returns

A hashref with two fields, code and message.

=cut

sub system_status {
  my $self = shift;

  my $uri = URI->new( $self->_server );
  $uri->path('status/system');

  $self->_do_request( get => $uri );
}

=head1 Reference API

The Reference API provides a mechanism for accessing the standard
"reference information" used by the 3taps system, including locations,

=head2 reference_location

  my $api     = WWW::3Taps::API->new;
  my $results = $api->reference_location();

  # $results = [
  #   {
  #     city        => "New York",
  #     cityRank    => 1,
  #     code        => "NYC",
  #     country     => "United States",
  #     countryRank => 1,
  #     latitude    => "40.6344",
  #     longitude   => "-74.2827",
  #     stateCode   => "NY",
  #     stateName   => "New York"
  #   },

  #   {
  #     city     => "Los Angeles",
  #     cityRank => 2,
  #     code     => "LAX",
  #     country  => "United
  #   States",
  #     countryRank => 1,
  #     latitude    => "33.9846",
  #     longitude   => "-118.112",
  #     stateCode   => "CA",
  #     stateName   => "California"
  #   }, # (...)
  # ]


Returns the 3taps locations. Note that you can request a single
location by passing in the location code.

  my $results = $api->reference_location('NYC');

  # $results = [
  #   {
  #     city        => "New York",
  #     cityRank    => 1,
  #     code        => "NYC",
  #     country     => "United States",
  #     countryRank => 1,
  #     latitude    => "40.6344",
  #     longitude   => "-74.2827",
  #     stateCode   => "NY",
  #     stateName   => "New York"
  #   }
  # ];

=head3 Returns

An array of hashrefs, each representing a requested location, each with
the following fields:

=over

=item code (string)

A unique 3-character code identifying this location within 3taps.

=item countryRank (integer)

A number that can optionally be used to sort the countries into a
useful order in the UI (ie, to place the most popular countries at the
top, and "Other" at the bottom).

=item country (string)

The name of the country this location is in.

=item cityRank (integer)

A number that can optionally be used to sort the cities within a
country into a useful order (eg, to place the most popular cities at
the top, and "Other" at the bottom).

=item city (string)

The name of the city this location is in.

=item stateCode (string)

A brief (usually two-letter) code for the state or region this
location is in.

=item stateName (string)

The name of the state or region this location is in. This will be
blank for countries which do not have states or regions.

=item hidden (boolean)

If true, this location should be hidden from the user-interface.

=item latitude (float)

The latitude of this location.

=item longitude (float)

The longitude of this location.

=back

=cut

sub reference_location {
  my ( $self, $location ) = @_;

  my $uri = URI->new( $self->_server );

  if ($location) {
    $uri->path("reference/location/$location");
  }
  else {
    $uri->path("reference/location");
  }

  $self->_do_request( get => $uri );
}

=head2 reference_category


  my $api     = WWW::3Taps::API->new;
  my $results = $api->reference_category( annotations => 0 );

  # $results = [
  #   { category => "Toys & Hobbies",   code => "STOY", group => "For Sale" },
  #   { category => "Tools",            code => "STOO", group => "For Sale" },
  #   { category => "Tickets",          code => "STIX", group => "For Sale" },
  #   { category => "Sports & Fitness", code => "SSNF", group => "For Sale" },
  #   { category => "Other Goods",      code => "SOTH", group => "For Sale" }
  # ];


Returns the 3taps categories. Note that you can request a single
category by passing in the category code:

  my $results = $api->reference_category( code => 'VAUT', annotations => 1);

=head3 Parameters

=over

=item code (string) (optional)

Code representing a category

=item annotations (boolean) (optional)

Set to C<false> if you'd prefer to get the category data without
annotations. Defaults to C<true>.

=back

=head3 Returns

An array of hashrefs representing categories containing the following
fields

=over

=item code (string)

A unique 4-character code identifying this category within 3taps.

=item group (string)

The name of the group of this category.

=item category (string)

The name of the category.

=item hidden (boolean)

If true, this category should be hidden from the user-interface.

=item annotations (arrayref)

An array of hashref representing annotations. Each annotation may have
the following fields:

=over 

=item name (string)

The name of this annotation.

=item type (string)

The type of the annotation. Possible types currently include "string",
"select" and "number".

=item options (arrayref)

Suggested values for the annotation. Each suggestion is an HASHREF
that can contain two fields: C<value> and C<subannotation>. C<value>
will contain the string value of this option, while C<subannotation>
will contain an annotation HASHREF that can be applied to a posting or
search if this option is selected.

=back

=back

=cut

sub reference_category {
  my ( $self, %params ) = validated_hash(
    \@_,
    code => { isa => 'Str', optional => 1 },
    annotations => { isa => JSONBoolean, optional => 1, coerce => 1 }
  );

  my $uri = URI->new( $self->_server );

  $uri->path('reference/category');
  $uri->path("reference/category/$params{code}")
    if exists $params{code};
  $uri->query_form( annotations => $params{annotations} )
    if exists $params{annotations};
  $self->_do_request( get => $uri );
}

=head2 reference_source

  my $api     = WWW::3Taps::API->new;
  my $results = $api->reference_source();

  # $results = [
  #   {
  #     code => "37SIG",
  #     logo_sm_url => "http://3taps.com/img/logos/37SIG37sig-fav.png",
  #     logo_url => "http://3taps.com/img/logos/37SIG37signals.png",
  #     name => "37signals"
  #   },
  #   {
  #     code => "3TAPS",
  #     logo_sm_url => "http://3taps.com/img/logos/3TAPS3taps-fav.png",
  #     logo_url => "http://3taps.com/img/logos/3TAPS3taps.png",
  #     name => "3taps"
  #   }, # (...)
  # ];

Returns the 3taps sources. Note that you can request a single source
by passing in the source code

  my $results = $api->reference_source('E_BAY');


=head2 Returns

An array of hashrefs representing source objects and containing the
following fields:

=over

=item code (string)

A unique 5-character code identifying this source within 3taps.

=item name (string)

The name of the source.

=item logo_url (string)

The URL of the logo to use for this source in the UI.

=item logo_sm_url (string)

The URL of a smaller, square logo to use for this source in the UI.

=back

=cut

sub reference_source {
  my ( $self, $source ) = @_;

  my $uri = URI->new( $self->_server );

  if ($source) {
    $uri->path("reference/source/$source");
  }
  else {
    $uri->path("reference/source");
  }

  $self->_do_request( get => $uri );
}

=head2 reference_modified


  my $api     = WWW::3Taps::API->new;
  my $results = $api->reference_modified('source');

Returns the time that the Reference API's data was updated for the
given reference type.

=head3 Parameters

=over

=item reference_type (string)

Can be "source", "category", or "location".

=back

=head3 Returns

The date that the Reference API's data was last updated for the given
reference type. Ex:

  2010-12-08 22:29:38 UTC


=cut

sub reference_modified {
  my $self = shift;
  my ($reference_type) = pos_validated_list( \@_, { isa => ReferenceType } );

  my $uri = URI->new( $self->_server );

  $uri->path("reference/modified/$reference_type");

  $self->_do_request( get => $uri, options => { no_decode => 1 } );
}

=head1 Posting API

The Posting API allows developers to store and retrieve postings in
the 3taps system.

=head2 Posting Model

Before diving into the methods of the Posting API, let's first define
the structure of the posting object. Note that fields marked REQUIRED
will always be present in postings received from 3taps, and are
required in all postings sent to 3taps.

=over

=item postKey (string)

The unique identifier for the posting in the 3taps system. REQUIRED

=item location (string)

A 3-character code identifying the location of this posting.

=item category (string)

A 4-character code identifying the category of this posting.

=item source (string)

A 5-character code identifying the source of this posting. REQUIRED

=item heading (string)

A short (max 255 character) piece of text that summarizes the
posting. Think of it as the "title" of the posting. REQUIRED

=item body (string)

The content of the posting.

=item latitude (float)

The latitude of the posting.

=item longitude (float)

The longitude of the posting.

=item language (string)

The language of the posting, represented by a 2-letter code conforming
to the ISO 639-1 standard (english is "en").

=item price (string)

The price of the posting, if any. Price may also be used for
compensation, or rent, in different contexts.

=item currency (string)

The currency of the price of the posting, represented by a 3-letter
code conforming to the ISO 4217 standard (US dollars is "USD").

=item images (arrayref)

An arrayref of strings, each containing the URL of an image associated
with the posting.

=item externalID (string)

The ID of this posting in its source system.

=item externalURL (string)

The URL of this posting on its source system.

=item accountName (string)

The name of the user that created the posting in the source system.

=item accountID (string)

The ID of the user that created the posting in the source system.

=item timestamp (date)

The time that the posting was created in the source system, in format
'YYYY/MM/DD hh:mm:dd', in UTC. REQUIRED

=item expiration (date)

The time that the posting should expire in the 3taps system, in format
'YYYY/MM/DD hh:mm:dd', in UTC. Note that if no expiration is
specified, 3taps will expire the posting after one week.

=item indexed (date)

The time that the posting was indexed in threetaps, in format
'YYYY/MM/DD hh:mm:dd', in UTC.

=item annotations (hashref)

A set of searchable key/value pairs associated with this posting.

=item trustedAnnotations (hashref)

A set of searchable key/value pairs associated with this posting,
limited to 3taps trusted annotations.

=item clickCount (integer)

The number of times a posting has been clicked on in the 3taps system.

=back

=head2 posting_get

  my $api     = WWW::3Taps::API->new;
  my $results = $api->posting_get('X7J67W');

  # $results = {
  #   accountId   => undef,
  #   accountName => "shopping.power2",
  #   annotations => {
  #     ship_to_locations => { 0 => "Worldwide" },
  #     tags => [ "#eBay", "#forsale", "#jewelry", "#HKG" ]
  #   },
  #   body     => "Thisisamensbluesportswatch.",
  #   category => "SGJE"
  #   # (...)
  #   }

Returns information about a single posting.

=head3 Parameters

=over

=item postKey (string)

The posting key for the desired posting.

=back

=head3 Returns

A hashref representing a posting object with one or more fields
outlined above in the L<posting model|/"Posting Model">. If the
posting is not found, a hashref containing two keys: "code" and
"message" representing the error, is returned instead.

=cut

sub posting_get {
  my $self = shift;
  my ($posting_key) = pos_validated_list( \@_, { isa => Str } );

  my $uri = URI->new( $self->_server );

  $uri->path("posting/get/$posting_key");

  $self->_do_request( get => $uri );
}

=head2 posting_create

  my $api     = WWW::3Taps::API->new;
  my $results = $api->posting_create(
    postings => [
      {
        annotations => {
          brand => "Specialized",
          color => "red"
        },
        body        => "Thisisatestpost.One.",
        category    => "SGBI",
        currency    => "USD",
        externalURL => "http://www.ebay.com",
        heading     => "TestPost1inBicyclesForSaleCategory",
        location    => "LAX",
        price       => "1.99",
        timestamp   => '20101130232514',
        source      => "E_BAY"
      }
    ]
  );

  # $result = [{postKey:"X73XFN"}]

Saves a new posting in 3taps.

=head3 Parameters

=over 

=item postings (arrayref)

=back

An arrayref of hashref representing objects to be saved in 3taps, each
with one or more fields outlined above in the L<posting model|/"Posting Model">.

=head3 Returns

An arrayref with one entry for each posting that was supplied. Each
entry will be an hashref with the following fields:

=over

=item postKey (string)

The postKey generated for this posting.

=item error (hashref)

If there was an error saving the posting, the error field will contain
a hashref with two keys: "code" and "message".

=back

=cut

sub posting_create {
  my ( $self, %params ) = validated_hash(
    \@_,
    postings => {
      isa => ArrayRef [
        Dict [
          location  => Optional [Location],
          category  => Optional [Category],
          source    => Source,
          heading   => Str,
          body      => Optional [Str],
          latitude  => Optional [Num],
          longitude => Optional [Num],
          language  => Optional [LanguageCode],
          price     => Optional [Str],
          currency  => Optional [Str],
          images => Optional [ ArrayRef [Str] ],
          externalID         => Optional [Str],
          externalURL        => Optional [Str],
          accountName        => Optional [Str],
          accountID          => Optional [Str],
          timestamp          => Timestamp,
          expiration         => Optional [Timestamp],
          indexed            => Optional [Timestamp],
          annotations        => Optional [HashRef],
          trustedAnnotations => Optional [HashRef],
          clickCount         => Optional [Int]
        ]
      ]
    }
  );

  my $args = { postings => $self->_to_json( $params{postings} ) };

  my $uri = URI->new( $self->_server );
  $uri->path('posting/create');

  $self->_do_request( post => $uri, request_args => $args );
}

=head2 posting_update

  my $api     = WWW::3Taps::API->new;
  my $results = $api->posting_update(
    postings => [
      [ 'X73XFP', { price       => '20.00' } ],
      [ 'X73XFN', { accountName => 'anonymous-X73XFN@mailserver.com' } ]
    ]
  );

  # $result = { success => 1 }

Update postings from 3taps

=head3 Parameters

=over

=item postings (arrayref)

An arrayref with one entry for each posting to be updated. Each posting's
entry in the arrayref should itself be an arrayref with two entries:
[postingKey, changes], where postingKey is the posting key identifying
the posting to update, and changes is an hashref, containing key/value
pairs mapping field names to their updated values.

=back

=head3 Returns

An hashref with one boolean field called "success".

=cut

sub posting_update {
  my ( $self, %params ) = validated_hash(
    \@_,
    postings => {
      isa => ArrayRef [
        Tuple [
          Str,
          Dict [
            location           => Optional [Location],
            category           => Optional [Category],
            source             => Optional [Source],
            heading            => Optional [Str],
            body               => Optional [Str],
            latitude           => Optional [Num],
            longitude          => Optional [Num],
            language           => Optional [LanguageCode],
            price              => Optional [Str],
            currency           => Optional [Str],
            images             => Optional [ ArrayRef [Str] ],
            externalID         => Optional [Str],
            externalURL        => Optional [Str],
            accountName        => Optional [Str],
            accountID          => Optional [Str],
            timestamp          => Optional [Timestamp],
            expiration         => Optional [Timestamp],
            indexed            => Optional [Timestamp],
            annotations        => Optional [HashRef],
            trustedAnnotations => Optional [HashRef],
            clickCount         => Optional [Int]
          ]
        ]
      ]
    }
  );

  my $args = { data => $self->_to_json( $params{postings} ) };

  my $uri = URI->new( $self->_server );
  $uri->path('posting/update');

  $self->_do_request( post => $uri, request_args => $args );
}

=head2 posting_delete

  my $api = WWW::3Taps::API->new;
  my $results = $api->posting_delete( postings => [ 'X73XFP', 'X73XFN' ] );

  # $result = { success => 1 }

Deletes postings from 3taps.

=head3 Parameters

=over

=item postings (arrayref)

An arrayref of postKeys whose postKeys are to be deleted

=back

=head3 Returns

An hashref with one boolean field called "success".

=cut

sub posting_delete {
  my ( $self, %params ) =
    validated_hash( \@_, postings => { isa => ArrayRef [Str] } );

  my $args = { postings => $self->_to_json( $params{postings} ) };

  my $uri = URI->new( $self->_server );
  $uri->path('posting/delete');

  $self->_do_request( post => $uri, request_args => $args );
}

=head2 posting_exists

  my $api     = WWW::3Taps::API->new;
  my $results = $api->posting_exists(
    postings => [
      { source => 'E_BAY', externalID => '220721553191' },
      { source => 'CRAIG', externalID => '191' },
      { source => 'AMZON', externalID => '370468535518' }
    ]
  );

  # $results = [
  #   {
  #     exists => bless( do { \( my $o = 1 ) }, 'JSON::XS::Boolean' ),
  #     postKey => "5NUURN",
  #     time    => "2011-01-0800:38:22UTC"
  #   },
  #   {
  #     error  => "Heading cannot be null",
  #     exists => bless( do { \( my $o = 0 ) }, 'JSON::XS::Boolean' ),
  #     time   => "2011-01-08 00:38:22 UTC"
  #   },
  #   { exists => $VAR1->[1]{exists} }
  # ];

Returns information on the existence of postings. Note that this
method is DEPRECATED and the Status API should be used instead.

=head3 Parameters

=over

=item ids (arrayref)

An arrayref of hashrefs representing request objects with two fields:
"source" and "externalID".

=back

=head3 Returns

An arrayref of hashrefs representing a response objects with the
following fields:

=over

=item exists (boolean)

Returns true if this posting exists.

=item postKey (string)

The postKey of the post.

=item indexed (string) 

The date that the posting was indexed by 3taps.

=item failures (arrayref)

An array of the failed attempts at saving this posting. Each failed
attempt is represented as a hashref with the following fields:

=over

=item postKey (string)

The postKey that was issued for this failed attempt

=item errors (arrayref)

An array of hashrefs representing the errors associated with this
attempt

=item timestamp (date)

The time that this failure occurred.

=back

=back

=cut

sub posting_exists {
  my ( $self, %params ) = validated_hash(
    \@_,
    postings => {
      isa => ArrayRef [
        Dict [
          source     => Source,
          externalID => Str
        ]
      ]
    }
  );

  my $args = { postings => $self->_to_json( $params{postings} ) };

  my $uri = URI->new( $self->_server );
  $uri->path('posting/exists');

  $self->_do_request( post => $uri, request_args => $args );
}

=head2 posting_error

  my $api     = WWW::3Taps::API->new;
  my $results = $api->posting_exists('X7J67W');

  # $results = {
  #   fields => [ 'source', 'category', 'location' ],
  #   post   => [ 'e_bay',  'SAPP',     'GBR' ],
  #   errors => ['externalID must be unique for this source']
  # };


Returns errors found when trying to process the given posting. Note
that this method is DEPRECATED and the Status API should be used
instead.

=head3 Parameters

=over

=item postKey (string)

The postKey for the desired posting.

=back

=head3 Returns

A hashref  with some or all of the following fields:

=over

=item fields (arrayref)

The fields submitted for this posting.

=item post (arrayref)

The values submitted for this posting.

=item errros (arrayref)

The errors found in the posting.

=back

=cut

sub posting_error {
  my $self = shift;
  my ($posting_key) = pos_validated_list( \@_, { isa => Str } );

  my $uri = URI->new( $self->_server );

  $uri->path("posting/error/$posting_key");

  $self->_do_request( get => $uri );
}

=head1 Geocoder API

The Geocoder API is a web service within the 3taps Network that allows
other programs (both external systems and other parts of the 3taps
Network) to calculate the location to use for a posting based on
location-specific details within the posting such as a street address
or a latitude and longitude value.  This process of calculating the
location for a posting is known as geocoding.


=head2 geocoder_geocode

  my $api     = WWW::3Taps::API->new;
  my $results = $api->geocoder_geocode(
    postings => [
      { text    => 'San Francisco, California' },
      { country => 'USA', state => 'CA', city => 'Los Angeles' }
    ]
  );

  # $results = [["SFO",37.77493,-122.41942],["LAX",34.05223,-118.24368]];

Calculate the location for a posting

=head3 Parameters

=over

=item postings (arrayref)

An arrayref of hasrefs representing postings to geocode. Each entry in
this array should contains one or more of the following fields:

=over

=item latitude (float)

The GPS latitude value as a decimal degree.

=item longitude (float)

The GPS longitude value as a decimal degree.

=item country (string)

The GPS longitude value as a decimal degree.

=item state (string)

The name or code of the state or region.

=item city (string)

The name of the city.

=item locality (string)

The name of a suburb, area or town within the specified city.

=item street (string)

The full street address for this location.

=item postal (string)

A zip or postal code.

=item text (string)

An unstructured location or address value.

=back

=back

=head3 Returns 

An arrayref with one entry for each posting. Each array entry will itself
be an array with three entries:

  (locationCode, latitude, longitude)

where locationCode is the three-character location code to use for
this posting, and latitude and longitude represent the calculated GPS
coordinate for this posting’s location, in the form of floating-point
numbers representing decimal degrees.

If the posting could not be geocoded at all, locationCode will be set
to undef. If the geocoder was unable to calculate a lat/long value for
the posting based on the supplied location details, latitude and
longitude will be set to undef.

=cut

sub geocoder_geocode {
  my ( $self, %params ) = validated_hash(
    \@_,
    postings => {
      isa => ArrayRef [
        Dict [
          latitude  => Optional [Num],
          longitude => Optional [Num],
          country   => Optional [Str],
          state     => Optional [Str],
          city      => Optional [Str],
          locality  => Optional [Str],
          street    => Optional [Str],
          postal    => Optional [Str],
          text      => Optional [Str],
        ]
      ]
    }
  );

  my $args = { data => $self->_to_json( $params{postings} ) };

  my $uri = URI->new( $self->_server );
  $uri->path('geocoder/geocode');

  $self->_do_request( post => $uri, request_args => $args );
}

=head1 Notifications API

The 3taps Notifier constantly monitors all incoming postings, and
sends out notifications via email, XMPP, Twitter, or iPhone Push as
postings that match certain criteria are received.  External users and
systems are able to send a request to the Notification API to have
notifications sent out to a given destination for all postings that
meet a given set of criteria.  These notifications will continue to be
sent out until the request is explicitly cancelled or the request
expires, usually after a period of seven days.

NOTE: Third party developers will need to contact us before they can
use the Notifications API before they use it, so they can register
their app with us. We're documenting this here to just let developers
know what is available.


=head2 notifications_firehose

  my $api     = WWW::3Taps::API->new;
  my $results = $api->notifications_firehose(
    text     => 'honda',
    category => 'VAUT',
    location => 'LAX',
    name     => 'Hondas in LA'
  );

Creates an XMPP firehose.  A variant of
L<create|/posting_create>. Supports the use of Common Search Criteria.

=head3 Parameters

=over

=item name (string)

The name to give this firehose (optional)

=back

=head3 Returns

A hashref with the following fields

=over

=item success (boolean)

true/false depending on if the subscription was successfully created.

=item jid (string)

The XMPP jid of the newly created firehose.

=item username (string)

The username of the jid account of the newly created firehose.

=item password (string)

The password of the jid account of the newly created firehose.

=item id (string)

The id of the subscription associated with the firehose (to be used
with L<delete|/notifications_delete>) 

=item secret (string)

The secret key to use when deleting this firehose (to be used with
L<delete|/notifications_delete>)

=item error (hashref)

If the firehose could not be created, error will be a hashref with two
fields: "code", "and "message".

=back

=cut

sub notifications_firehose {
  my ( $self, %params ) = validated_hash(
    \@_,
    location           => { optional => 1, isa => Location },
    category           => { optional => 1, isa => Category },
    source             => { optional => 1, isa => Source },
    heading            => { optional => 1, isa => Str },
    body               => { optional => 1, isa => Str },
    latitude           => { optional => 1, isa => Num },
    longitude          => { optional => 1, isa => Num },
    language           => { optional => 1, isa => LanguageCode },
    price              => { optional => 1, isa => Str },
    currency           => { optional => 1, isa => Str },
    images             => { optional => 1, isa => ArrayRef [Str] },
    externalID         => { optional => 1, isa => Str },
    externalURL        => { optional => 1, isa => Str },
    accountName        => { optional => 1, isa => Str },
    accountID          => { optional => 1, isa => Str },
    timestamp          => { optional => 1, isa => Timestamp },
    expiration         => { optional => 1, isa => Timestamp },
    indexed            => { optional => 1, isa => Timestamp },
    annotations        => { optional => 1, isa => HashRef },
    trustedAnnotations => { optional => 1, isa => HashRef },
    clickCount         => { optional => 1, isa => Int },
    name               => { optional => 1, isa => Str },
    text               => { optional => 1, isa => Str }
  );
  my $args = { data => $self->_to_json( \%params ) };

  my $uri = URI->new( $self->_server );
  $uri->path('notifications/firehose');

  $self->_do_request( post => $uri, request_args => $args );
}

=head2 notifications_delete

  my $api     = WWW::3Taps::API->new;
  my $results = $api->notifications_delete(
    id     => '1873',
    secret => "201d7288b4c18a679e48b31c72c30ded"
  );

Cancel a notification subscription.

=head3 Parameters

=over 

=item id (string)

The id of the notification subscription to delete.

=item secret (string)

The secret key that was returned to you when you created the
notification subscription. You kept it, right?

=back

=head3 Returns

A hashref with the following fields:

=over

=item success (boolean)

true/false depending on if the subscription was successfully deleted.

=item error (hashref)

If the delete was unsuccessful, error will contain a hashref with two
fields: code, and message.

=back

=cut

sub notifications_delete {
  my ( $self, %params ) = validated_hash(
    \@_,
    id     => { optional => 1, isa => Str },
    secret => { optional => 1, isa => Str },
  );

  my $args = { data => $self->_to_json( \%params ) };

  my $uri = URI->new( $self->_server );
  $uri->path('notifications/delete');

  $self->_do_request( post => $uri, request_args => $args );
}

=head2 notifications_get

  my $api     = WWW::3Taps::API->new;
  my $results = $api->notifications_get(
    id     => '1873',
    secret => "201d7288b4c18a679e48b31c72c30ded"
  );

Get information about a notification subscription.

=head3 Parameters

=over 

=item id (string)

The id of the notification subscription to delete.

=item secret (string)

The secret key that was returned to you when you created the
notification subscription. You kept it, right?

=back

=head3 Returns

A hashref with the following fields:

=over

=item subscription (hashref)

A hashref containing information about the notification subscription.

=item error (hashref)

If the delete was unsuccessful, error will contain a hashref with two
fields: code, and message.

=back

=cut

sub notifications_get {
  my ( $self, %params ) = validated_hash(
    \@_,
    id     => { optional => 1, isa => Str },
    secret => { optional => 1, isa => Str },
  );

  my $args = { data => $self->_to_json( \%params ) };

  my $uri = URI->new( $self->_server );
  $uri->path('notifications/get');

  $self->_do_request( post => $uri, request_args => $args );
}

=head2 notifications/create

  my $api     = WWW::3Taps::API->new;
  my $results = $api->notifications_create(
    text        => 'red',
    location    => 'LAX',
    source      => 'CRAIG',
    annotations => { price => "200", make => "honda" },
    email       => 'dfoley@3taps.com',
    name        => 'red things in los angeles'
  );

Ask the notifier to start sending out notifications by creating a new
"subscription".

Subscriptions need one delivery param (email, jid, token) and at least
one Common Search Criteria parameter.

In order to eliminate unwanted strain on both the notification server
and clients, the system will examine filter criteria before creating a
subscription to make sure that the criteria is not too broad.  If you
try to subscribe to "all of eBay" you will get an error telling you to
narrow your criteria.

Note that right now, only the following search params are available:
loc, src, cat, text, price, annotations.

=head3 Parameters

=over

=item name (string)

The name to give this subscription. This will be included in iPhone
Push notifications. (optional).

=item expiration (integer)

The number of days to keep this subscription around for (default 7 days)

=item format (string)

Defines how the notifications should be formatted. The following
formats are currently supported:

=over

=item push 

This format is intended for iPhone push notifications. The
notification includes the following information in a single line:
subscription name; the number of notifications received on this
subscription today; the heading of the post.

=item brief

This format is intended for short, human-readable messages such as
watching notifications on a chat client. The notification has two
lines for the post: the heading, followed by a line break and the URL
used to access the post within the 3taps system.

=item full

=item extended

These two formats are intended for sending notifications to external
systems for further use. The data is sent as a JSON-encoded array with
two entries: [fieldList, postings], where fieldList is an array of
field names, and postings is an array of postings, where each posting
is itself an array of field values, in the same order as the fieldList
list.

For the full format, the following fields will be included:

  postKey
  source 
  category
  location
  heading
  body
  workspaceURL
  created

The extended format includes all the fields from the full format,
plus: 

  externalURL
  externalID
  trustedAnnotations
  latitude
  longitude
  price
  currency
  language


=item html

This format is intended for human-readable notifications such as
emails. The following information is presented in HTML format:

  postKey
  source
  category
  location
  heading
  body
  workspaceURL
  created

=item text140

This format is intended to send notifications to Twitter; a minimal
set of fields are included, and limited to 140 characters so that the
notification can be sent out as a Twitter status update

=back 

=item email (string)

The email address to send this notification to.

=item jid (string)

The XMPP JID to send this notification to.

=item token (string)

The iPhone Device Token to send this notification to. (Note that you
should only supply one of email, jid, or token.)

=item app (string)

The name of the app this notification subscription is being created from.

=back

=head3 Returns

An arrayref with the following values:

=over

=item success

true or false, depending on if the notification subscription was
successfully created.

=item id

The id of the newly created subscription. This field is only returned
on success.

=item secret

The secret pass for the newly created subscription, required for
deleting subscriptions. This field is only returned on success.

=item error

If there was a problem with the API request, the error message will be
included here as a hashref with two fields: code, and message. This
field is only returned on failure.

=back

=cut

sub notifications_create {
  my ( $self, %params ) = validated_hash(
    \@_,
    location           => { optional => 1, isa => Location },
    category           => { optional => 1, isa => Category },
    source             => { optional => 1, isa => Source },
    heading            => { optional => 1, isa => Str },
    body               => { optional => 1, isa => Str },
    latitude           => { optional => 1, isa => Num },
    longitude          => { optional => 1, isa => Num },
    language           => { optional => 1, isa => LanguageCode },
    price              => { optional => 1, isa => Str },
    currency           => { optional => 1, isa => Str },
    images             => { optional => 1, isa => ArrayRef [Str] },
    externalID         => { optional => 1, isa => Str },
    externalURL        => { optional => 1, isa => Str },
    accountName        => { optional => 1, isa => Str },
    accountID          => { optional => 1, isa => Str },
    timestamp          => { optional => 1, isa => Timestamp },
    expiration         => { optional => 1, isa => Timestamp },
    indexed            => { optional => 1, isa => Timestamp },
    annotations        => { optional => 1, isa => HashRef },
    trustedAnnotations => { optional => 1, isa => HashRef },
    clickCount         => { optional => 1, isa => Int },
    name               => { optional => 1, isa => Str },
    text               => { optional => 1, isa => Str },
    email              => { optional => 1, isa => Str },
    jid                => { optional => 1, isa => Str },
    token              => { optional => 1, isa => Str },
    app                => { optional => 1, isa => Str },
    format             => { optional => 1, isa => NotificationFormat }
  );
  my $args = { data => $self->_to_json( \%params ) };

  my $uri = URI->new( $self->_server );
  $uri->path('notifications/create');

  $self->_do_request( post => $uri, request_args => $args );
}

sub _do_request {
  my ( $self, $method, $uri, %args ) = @_;
  my $response;
  my @auth;

  if ( $self->_has_auth_id && $self->_has_agent_id ) {
    @auth = (
      agentID => $self->agent_id,
      authID  => $self->auth_id
    );
  }

  $response = $self->_ua->get($uri) if $method eq 'get';
  $response = $self->_ua->post( $uri, $args{request_args}, @auth )
    if $method eq 'post';

  return (
      $args{options}->{no_decode}
    ? $response->content
    : $self->_from_json( $response->content )
  ) if $response->is_success;

  confess $response->status_line;
}

=head1 AUTHORS

  Eden Cardim, C << <edencardim at gmail.com> >>
  Gabriel Andrade,  << <gabiruh at gmail.com> >>

=head1 BUGS

Please report any bugs or feature requests to C<bug-www-3taps-api at
rt.cpan.org>, or through the web interface at
L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=WWW-3Taps-API>.  I
will be notified, and then you'll automatically be notified of
progress on your bug as I make changes.


=head1 SUPPORT

For detailed developer info, see http://developers.3taps.net.

You can find documentation for this module with the perldoc command.

    perldoc WWW::3Taps::API

You can also look for information at:

=over 4

=item * RT: CPAN's request tracker

L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=WWW-3Taps-API>

=item * AnnoCPAN: Annotated CPAN documentation

L<http://annocpan.org/dist/WWW-3Taps-API>

=item * CPAN Ratings

L<http://cpanratings.perl.org/d/WWW-3Taps-API>

=item * Search CPAN

L<http://search.cpan.org/dist/WWW-3Taps-API/>

=back


=head1 ACKNOWLEDGEMENTS


=head1 LICENSE AND COPYRIGHT

Copyright 2011 Eden Cardim

This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.

=cut

1;