Group
Extension

WWW-Challonge/lib/WWW/Challonge/Tournament.pm

package WWW::Challonge::Tournament;

use 5.010;
use strict;
use warnings;
use WWW::Challonge::Participant;
use WWW::Challonge::Match;
use Carp qw/carp croak/;
use JSON qw/to_json from_json/;

sub __is_kill;
sub __args_are_valid;

=head1 NAME

WWW::Challonge::Tournament - A class representing a single Challonge tournament.

=head1 VERSION

Version 1.01

=cut

our $VERSION = '1.01';

=head1 SUBROUTINES/METHODS

=head2 new

Takes a hashref representing the tournament, the API key and the REST client
and turns it into an object. This is mostly used by the module itself, to
create a new tournament see L<WWW::Challonge/new_tournament>.

	my $t = WWW::Challonge::Tournament->new($tournament, $key, $client);

=cut

sub new
{
	my $class = shift;
	my $tournament = shift;
	my $key = shift;
	my $client = shift;

	my $t =
	{
		alive => 1,
		client => $client,
		tournament => $tournament->{tournament},
		key => $key,
	};
	bless $t, $class;
}

=head2 update

Updates specific attributes of a tournament. For a full list, see
L<WWW::Challonge/new_tournament>. Unlike that method, however, all of the arguments
are optional.

	$t->update({
		name => "sample_tournament_2",
		type => "swiss",
	});

=cut

sub update
{
	my $self = shift;
	my $args = shift;

	# Do not operate on a dead tournament:
	return __is_kill unless($self->{alive});

	# Die on no errors:
	croak "No arguments given" unless(defined $args);

	# Get the key, REST client and tournament url:
	my $key = $self->{key};
	my $client = $self->{client};
	my $url = $self->{tournament}->{url};
	my $HOST = $WWW::Challonge::HOST;

	# Check the arguments and values are valid:
	return undef unless(WWW::Challonge::Tournament::__args_are_valid($args));

	# Add the API key and put everything else in a 'tournament' hash:
	my $params = { api_key => $key, tournament => $args };

	# Make the PUT request:
	my $response = $client->request(WWW::Challonge::__json_request(
		"$HOST/tournaments/$url.json", "PUT", $params));

	# Check for any errors:
	WWW::Challonge::__handle_error $response if($response->is_error);

	return 1;
}

=head2 destroy

Deletes the tournament from the user's account. There is no undo, so use with
care!

	$t->destroy;

	# $t still contains the tournament, but any future operations will fail:
	$t->update({ name => "sample_tournament_2" }); # ERROR!

=cut

sub destroy
{
	my $self = shift;

	# Do not operate on a dead tournament:
	return __is_kill unless($self->{alive});

	# Get the key, REST client and tournament URL:
	my $key = $self->{key};
	my $client = $self->{client};
	my $url = $self->{tournament}->{url};
	my $HOST = $WWW::Challonge::HOST;

	# Make the DELETE call:
	my $response = $client->delete("$HOST/tournaments/$url.json?api_key=$key");

	# Check for any errors:
	WWW::Challonge::__handle_error $response if($response->is_error);

	# Set the tournament to dead:
	$self->{alive} = 0;

	return 1;
}

=head2 process_check_ins

This should be invoked after a tournament's check-in window closes, but before
the tournament is started. It then does the following:

=over 4

=item 1

Marks participants who have not checked in as inactive.

=item 2

Moves inactive participants to the bottom seeds.

=item 3

Transitions the tournament state from "checking_in" to "checked_in".

=back

	$t->process_check_ins;

=cut

sub process_check_ins
{
	my $self = shift;

	# Do not operate on a dead tournament:
	return __is_kill unless($self->{alive});

	# Get the key, REST client and tournament URL:
	my $key = $self->{key};
	my $client = $self->{client};
	my $url = $self->{tournament}->{url};
	my $HOST = $WWW::Challonge::HOST;

	# Send the API key:
	my $params = { api_key => $key };

	# Make the POST call:
	my $response = $client->request(WWW::Challonge::__json_request(
		"$HOST/tournaments/$url/process_check_ins.json", "POST", $params));

	WWW::Challonge::__handle_error $response if($response->is_error);

	return 1;
}

=head2 abort_check_in

Aborts the check-in process if the tournament's status is currently
"checking_in" or "checked_in". This is useful as you cannot edit the
tournament's start time during this state. It does the following:

=over 4

=item 1

Makes all participants active and clears their "checked_in_at" times.

=item 2

Sets the tournament state from "checking_in" or "checked_in" to "pending".

=back

	$t->abort_check_in;

=cut

sub abort_check_in
{
	my $self = shift;

	# Do not operate on a dead tournament:
	return __is_kill unless($self->{alive});

	# Get the key, REST client and tournament URL:
	my $key = $self->{key};
	my $client = $self->{client};
	my $url = $self->{tournament}->{url};
	my $HOST = $WWW::Challonge::HOST;

	# Send the API key:
	my $params = { api_key => $key };

	# Make the POST call:
	my $response = $client->request(WWW::Challonge::__json_request(
		"$HOST/tournaments/$url/abort_check_in.json", "POST", $params));

	# Check for any errors:
	WWW::Challonge::__handle_error $response if($response->is_error);

	return 1;
}

=head2 start

Starts a tournament, opening up matches for score reporting. The tournament
must have at least 2 participants. If successful, sets the state of the
tournament to "underway".

	$t->start;

=cut

sub start
{
	my $self = shift;

	# Do not operate on a dead tournament:
	return __is_kill unless($self->{alive});

	# Get the key, REST client and tournament URL:
	my $key = $self->{key};
	my $client = $self->{client};
	my $url = $self->{tournament}->{url};
	my $HOST = $WWW::Challonge::HOST;

	# Send the API key:
	my $params = { api_key => $key };

	# Make the POST call:
	my $response = $client->request(WWW::Challonge::__json_request(
		"$HOST/tournaments/$url/start.json", "POST", $params));

	# Check for any errors:
	WWW::Challonge::__handle_error $response if($response->is_error);

	return 1;
}

=head2 finalize

Finalises a tournament that has had all match scores submitted, rendering the
results permenant. If successful, it sets the state to "complete".

	$t->finalize;

=cut

sub finalize
{
	my $self = shift;

	# Do not operate on a dead tournament:
	return __is_kill unless($self->{alive});

	# Get the key, REST client and tournament URL:
	my $key = $self->{key};
	my $client = $self->{client};
	my $url = $self->{tournament}->{url};
	my $HOST = $WWW::Challonge::HOST;

	# Send the API key:
	my $params = { api_key => $key };

	# Make the POST call:
	my $response = $client->request(WWW::Challonge::__json_request(
		"$HOST/tournaments/$url/finalize.json", "POST", $params));

	# Check for any errors:
	WWW::Challonge::__handle_error $response if($response->is_error);

	return 1;
}

=head2 reset

Resets an "in_progress" tournament, deleting all match records. You can add,
remove or edit users before starting again. Sets the state to "pending".

	$t->reset;

=cut

sub reset
{
	my $self = shift;

	# Do not operate on a dead tournament:
	return __is_kill unless($self->{alive});

	# Get the key, REST client and tournament URL:
	my $key = $self->{key};
	my $client = $self->{client};
	my $url = $self->{tournament}->{url};
	my $HOST = $WWW::Challonge::HOST;

	# Send the API key:
	my $params = { api_key => $key };

	# Make the POST call:
	my $response = $client->request(WWW::Challonge::__json_request(
		"$HOST/tournaments/$url/reset.json", "POST", $params));

	# Check for any errors:
	WWW::Challonge::__handle_error $response if($response->is_error);

	return 1;
}

=head2 attributes

Gets all the attributes of the tournament in a hashref. Contains the following
fields.

=over 4

=item accepting_predictions

=item accept_attachments

=item allow_participant_match_reporting

=item anonymous_voting

=item category

=item check_in_duration

=item completed_at

=item created_at

=item created_by_api

=item credit_capped

=item description

=item description_source

=item full_challonge_url

=item game_id

=item game_name

=item group_stages_enabled

=item group_stages_were_started

=item hide_forum

=item hide_seeds

=item hold_third_place_match

=item id

=item live_image_url

=item max_predictions_per_user

=item name

=item notify_users_when_match_opens

=item notify_users_when_the_tournament_ends

=item open_signup

=item participants_count

=item participants_locked

=item participants_swappable

=item prediction_method

=item predictions_opened_at

=item private

=item progress_meter

=item pts_for_bye

=item pts_for_game_tie

=item pts_for_game_win

=item pts_for_match_tie

=item pts_for_match_win

=item quick_advance

=item ranked_by

=item review_before_finalizing

=item require_score_agreement

=item rr_pts_for_game_tie

=item rr_pts_for_game_win

=item rr_pts_for_match_tie

=item rr_pts_for_match_win

=item sequential pairings

=item show_rounds

=item signup_cap

=item sign_up_url

=item start_at

=item started_at

=item started_checking_in_at

=item state

=item swiss_rounds

=item subdomain

=item teams

=item team_convertable

=item tie_breaks

=item tournament_type

=item updated_at

=item url

=back

	my $attr = $t->attributes;
	print $attr->{name}, "\n";

=cut

sub attributes
{
	my $self = shift;

	# Do not operate on a dead tournament:
	return __is_kill unless($self->{alive});

	# Get the key, REST client and url:
	my $key = $self->{key};
	my $client = $self->{client};
	my $url = $self->{tournament}->{url};
	my $HOST = $WWW::Challonge::HOST;

	# Get the most recent version:
	my $response = $client->get(
		"$HOST/tournaments/$url.json?api_key=$key");

	# Check for any errors:
	WWW::Challonge::__handle_error $response if($response->is_error);

	# Save the most recent version and return it:
	$self->{tournament} = from_json($response->decoded_content)->{tournament};

	return $self->{tournament};
}

=head2 participants

Returns an arrayref of C<WWW::Challonge::Participant> objects for every
participant in the tourney.

	my $p = $t->participants;
	for my $participant(@{$p})
	{
		...

=cut

sub participants
{
	my $self = shift;

	# Do not operate on a dead tournament:
	return __is_kill unless($self->{alive});

	# Get the key, REST client and url:
	my $key = $self->{key};
	my $client = $self->{client};
	my $url = $self->{tournament}->{url};
	my $HOST = $WWW::Challonge::HOST;

	# Make the GET request:
	my $response = $client->get(
		"$HOST/tournaments/$url/participants.json?api_key=$key");

	# Check for any errors:
	WWW::Challonge::__handle_error $response if($response->is_error);

	# If not, make an object for every participant:
	my $participants = [];
	for my $participant(@{from_json($response->decoded_content)})
	{
		push @{$participants}, WWW::Challonge::Participant->new($participant,
			$key, $client);
	}
	return $participants;
}

=head2 participant

Returns a single C<WWW::Challonge::Participant> object representing the
participant with the given unique ID.

	my $p = $t->participant(24279875);

=cut

sub participant
{
	my $self = shift;
	my $participant = shift;

	# Do not operate on a dead tournament:
	return __is_kill unless($self->{alive});

	# Die on no arguments:
	croak "No arguments given" unless(defined $participant);

	# Get the key, REST client and url:
	my $key = $self->{key};
	my $client = $self->{client};
	my $url = $self->{tournament}->{url};
	my $HOST = $WWW::Challonge::HOST;

	# Make the GET request:
	my $response = $client->get(
		"$HOST/tournaments/$url/participants/$participant.json?api_key=$key");

	# Check for any errors:
	WWW::Challonge::__handle_error $response if($response->is_error);

	# If so, create an object and return it:
	my $p = WWW::Challonge::Participant->new(
		from_json($response->decoded_content), $key, $client);
	return $p;
}

=head2 new_participant

Adds a new participant to the tournament, and if successful returns the newly
created C<WWW::Challonge::Participant> object. The possible arguments are as
follows.

=over 4

=item name

The name of the participant. Required unless I<challonge_username> or I<email>
are provided. Must be unique within the tournament.

=item challonge_username

If the participant has a valid Challonge account, providing a name will send
them an invite to join the tournament.

=item email

If the email is attached to a valid Challonge account, it will invite them to
join the tournament. If not, the 'new-user-email' attribute will be set, and
an email will be sent to invite the person to join Challonge.

=item seed

Integer. The participant's new seed. Must be between 1 and the new number of
participants. Overwriting an existing seed will bump up the other participants.
If none is given, the participant will be given the lowest possible seed (the
bottom).

=item misc

Miscellaneous notes on a player only accessible via the API. Maximum 255
characters.

=back

	my $p = $t->new_participant({
		name => "test",
		seed => 4
	});

=cut

sub new_participant
{
	my $self = shift;
	my $args = shift;

	# Do not operate on a dead tournament:
	return __is_kill unless($self->{alive});

	# Get the key, REST client and url:
	my $key = $self->{key};
	my $client = $self->{client};
	my $url = $self->{tournament}->{url};
	my $HOST = $WWW::Challonge::HOST;

	# Fail if name or challonge_username or email is not provided:
	unless((defined $args->{name}) || (defined $args->{challonge_username}) ||
		(defined $args->{email}))
	{
		croak "Name, email or Challonge username are required to create a new ".
			"participant.\n";
		return undef;
	}

	# Check the arguments and values are valid:
	return undef unless(WWW::Challonge::Participant::__args_are_valid($args));

	# Add in the API key and convert to a POST request:
	my $params = { api_key => $key, participant => $args };

	# Now we have all the arguments validated, send the POST request:
	my $response = $client->request(WWW::Challonge::__json_request(
		"$HOST/tournaments/$url/participants.json", "POST", $params));

	# Check for any errors:
	WWW::Challonge::__handle_error $response if($response->is_error);

	# If not, create an object and return it:
	my $p = WWW::Challonge::Participant->new(
		from_json($response->decoded_content), $key, $client);
	return $p;
}

=head2 matches

Returns an arrayref of C<WWW::Challonge::Match> objects for every
match in the tourney. The tournament must be in progress before this will
return anything useful.

	my $m = $t->matches;
	for my $match(@{$m})
	{
		...

=cut

sub matches
{
	my $self = shift;

	# Do not operate on a dead tournament:
	return __is_kill unless($self->{alive});

	# Get the key, REST client and url:
	my $key = $self->{key};
	my $client = $self->{client};
	my $url = $self->{tournament}->{url};
	my $HOST = $WWW::Challonge::HOST;

	# Make the GET request:
	my $response = $client->get(
		"$HOST/tournaments/$url/matches.json?api_key=$key");

	# Check for any errors:
	WWW::Challonge::__handle_error $response if($response->is_error);

	# If so, make an object for every participant:
	my $matches = [];
	for my $match(@{from_json($response->decoded_content)})
	{
		push @{$matches}, WWW::Challonge::Match->new($match, $key,
			$client);
	}
	return $matches;
}

=head2 match

Returns a single C<WWW::Challonge::Match> object representing the match with
the given unique ID.

	my $m = $t->match(24279875);

=cut

sub match
{
	my $self = shift;
	my $match = shift;

	# Do not operate on a dead tournament:
	return __is_kill unless($self->{alive});

	# Die on no arguments:
	croak "No arguments given" unless(defined $match);

	# Get the key, REST client and url:
	my $key = $self->{key};
	my $client = $self->{client};
	my $url = $self->{tournament}->{url};
	my $HOST = $WWW::Challonge::HOST;

	# Make the GET request:
	my $response = $client->get(
		"$HOST/tournaments/$url/matches/$match.json?api_key=$key");

	# Check for any errors:
	WWW::Challonge::__handle_error $response if($response->is_error);

	# If so, create an object and return it:
	my $m = WWW::Challonge::Match->new(from_json($response->decoded_content),
		$key, $client);
	return $m;
}

=head2 __is_kill

Returns an error explaining that the current tournament has been destroyed and
returns undef, used so a function doesn't attempt to operate on a tournament
that has been successfully destroyed.

=cut

sub __is_kill
{
	croak "Tournament has been destroyed";
}

=head2 __args_are_valid

Checks if the passed arguments and values are valid for creating or updating
a tournament.

=cut

sub __args_are_valid
{
	my $args = shift;

	# The possible parameters, grouped together by the kind of input they take.
	my %valid_args = (
		string => [
			"name",
			"tournament_type",
			"url",
			"subdomain",
			"description",
			"game_name",
			"ranked_by",
		],
		integer => [
			"swiss_rounds",
			"signup_cap",
			"check_in_duration",
		],
		decimal => [
			"pts_for_match_win",
			"pts_for_match_tie",
			"pts_for_game_win",
			"pts_for_game_tie",
			"pts_for_bye",
			"rr_pts_for_match_win",
			"rr_pts_for_match_tie",
			"rr_pts_for_game_win",
			"rr_pts_for_game_tie",
		],
		bool => [
			"open_signup",
			"hold_third_place_match",
			"accept_attachments",
			"hide_forum",
			"show_rounds",
			"private",
			"notify_users_when_matches_open",
			"notify_users_when_the_tournament_ends",
			"sequential_pairings",
		],
		datetime => [
			"start_at"
		],
	);

	# Validate the inputs:
	for my $arg(@{$valid_args{string}})
	{
		next unless(defined $args->{$arg});
		# Most of the string-based arguments require individual validation
		# based on what they are:
		if($arg =~ /^name$/)
		{
			if(length $args->{$arg} > 60)
			{
				croak "Name '" . $args->{$arg} . " is longer than 60 characters";
			}
		}
		elsif($arg =~ /^tournament_type$/)
		{
			if($args->{$arg} !~ /^((single|double) elimination)|(round robin)|
				(swiss)$/i)
			{
				croak "Value '" . $args->{$arg} . "' is invalid for argument '".
					$arg . "'";
			}
		}
		elsif($arg =~ /^url$/)
		{
			if($args->{$arg} !~ /^[a-zA-Z0-9_]*$/)
			{
				croak "Value '" . $args->{$arg} . "' is not a valid URL";
			}
		}
		elsif($arg =~ /^ranked_by$/)
		{
			if($args->{$arg} !~ /^((match|game) wins)|
				(points (scored|difference))|custom/i)
			{
				croak "Value '" . $args->{$arg} . "' is invalid for argument '".
					$arg . "'";
			}
		}
	}
	for my $arg(@{$valid_args{integer}})
	{
		next unless(defined $args->{$arg});
		# Make sure the argument is an integer:
		if($args->{$arg} !~ /^\d*$/)
		{
			croak "Value '" . $args->{$arg} . "' is not a valid integer for " .
				"argument '" . $arg . "'";
		}
	}
	for my $arg(@{$valid_args{decimal}})
	{
		next unless(defined $args->{$arg});
		# Make sure the argument is an integer or decimal:
		if($args->{$arg} !~ /^\d*\.?\d*$/)
		{
			croak "Value '" . $args->{$arg} . "' is not a valid decimal for " .
				"argument '" . $arg . "'";
		}
		else
		{
			$args->{$arg} = sprintf("%.1f", $args->{$arg});
		}
	}
	for my $arg(@{$valid_args{boolean}})
	{
		next unless(defined $args->{$arg});
		# Make sure the argument is true or false:
		if($args->{$arg} !~ /^(true|false)$/i)
		{
			croak "Value '", $args->{$arg}, "' is not valid for argument '" .
				$arg . "'. It should be 'true' or 'false'";
		}
	}
	for my $arg(@{$valid_args{datetime}})
	{
		next unless(defined $args->{$arg});

		# Check if we have a DateTime object:
		my $is_datetime;
		eval { $is_datetime = $args->{$arg}->can("iso8601") };

		# If so, get the ISO8601 string:
		if($is_datetime)
		{
			$args->{$arg} = $args->{$arg}->iso8601;
		}
		# If not make sure the argument is a valid datetime:
		elsif($args->{$arg} !~ /
			^\d{4}- # The year, mandatory in all cases
				(?:
					(?:
						\d{2}-\d{2} # Month and day
							(?:
								T\d{2}:\d{2}:\d{2} # Hours, minutes, seconds
									(?:
										(?:
											\+\d{2}:\d{2} # Timezone
										)
										|
										(?:
											Z # UTC
										)
									)
							)?
					)
					|
					(?:
						W\d{2} # Week
							(?:
								-\d # Date with week number
							)?
					)
					|
					(?:
						\d{3} # Ordinal date
					)
				)
			$
		/x)
		{
			croak "Value '", $args->{$arg}, "' is not a valid datetime for " .
				"argument '" . $arg . "'";
		}
	}

	# Finally, check if there are any unrecognised arguments, but just ignore
	# them instead of erroring out:
	my @accepted_inputs = (
		@{$valid_args{string}},
		@{$valid_args{integer}},
		@{$valid_args{decimal}},
		@{$valid_args{bool}},
		@{$valid_args{datetime}}
	);
	my $is_valid = 0;
	for my $arg(keys %{$args})
	{
		for my $valid_arg(@accepted_inputs)
		{
			if($arg eq $valid_arg)
			{
				$is_valid = 1;
				last;
			}
		}
		carp "Ignoring unknown argument '" . $arg . "'" unless($is_valid);
		$is_valid = 0;
	}
	return 1;
}

=head1 AUTHOR

Alex Kerr, C<< <kirby at cpan.org> >>

=head1 BUGS

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

=head1 SUPPORT

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

    perldoc WWW::Challonge::Tournament

You can also look for information at:

=over 4

=item * RT: CPAN's request tracker (report bugs here)

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

=item * AnnoCPAN: Annotated CPAN documentation

L<http://annocpan.org/dist/WWW-Challonge>

=item * CPAN Ratings

L<http://cpanratings.perl.org/d/WWW-Challonge>

=item * Search CPAN

L<http://search.cpan.org/dist/WWW-Challonge>

=back

=head1 SEE ALSO

=over 4

=item L<WWW::Challonge>

=item L<WWW::Challonge::Participant>

=item L<WWW::Challonge::Match>

=item L<WWW::Challonge::Match::Attachment>

=back

=head1 ACKNOWLEDGEMENTS

Everyone on the L<Challonge|http://challonge.com> team for making such a great
service.

=head1 LICENSE AND COPYRIGHT

Copyright 2015 Alex Kerr.

This program is free software; you can redistribute it and/or modify it
under the terms of the the Artistic License (2.0). You may obtain a
copy of the full license at:

L<http://www.perlfoundation.org/artistic_license_2_0>

Any use, modification, and distribution of the Standard or Modified
Versions is governed by this Artistic License. By using, modifying or
distributing the Package, you accept this license. Do not use, modify,
or distribute the Package, if you do not accept this license.

If your Modified Version has been derived from a Modified Version made
by someone other than you, you are nevertheless required to ensure that
your Modified Version complies with the requirements of this license.

This license does not grant you the right to use any trademark, service
mark, tradename, or logo of the Copyright Holder.

This license includes the non-exclusive, worldwide, free-of-charge
patent license to make, have made, use, offer to sell, sell, import and
otherwise transfer the Package with respect to any patent claims
licensable by the Copyright Holder that are necessarily infringed by the
Package. If you institute patent litigation (including a cross-claim or
counterclaim) against any party alleging that the Package constitutes
direct or contributory patent infringement, then this Artistic License
to you shall terminate on the date that such litigation is filed.

Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

=cut

1; # End of WWW::Challonge::Tournament


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.