diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb542ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +blib* +Makefile +Makefile.old +Build +_build* +pm_to_blib* +*.tar.gz +.lwpcookies +Net-Server-Framework-* +cover_db +.vimbk +*.swp +*.swo +MANIFEST.SKIP diff --git a/.shipit b/.shipit new file mode 100644 index 0000000..b6d9d2a --- /dev/null +++ b/.shipit @@ -0,0 +1,11 @@ +# auto-generated shipit config file. +steps = FindVersion, ChangeVersion, CheckChangeLog, DistTest, Commit, Tag, MakeDist, UploadPuppet + +# svn.tagpattern = MyProj-%v +# svn.tagpattern = http://code.example.com/svn/tags/MyProj-%v +git.push_to = origin +git.tagpattern = %v + +puppet.dir = /Users/lenz/workspace/infra/puppet +puppet.file = modules/perl_modules/files/Net-Xero.tgz +# CheckChangeLog.files = ChangeLog, MyProj.CHANGES diff --git a/Changes b/Changes new file mode 100644 index 0000000..1e493b5 --- /dev/null +++ b/Changes @@ -0,0 +1,5 @@ +Revision history for Net-Xero + +0.01 Date/time + First version, released on an unsuspecting world. + diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..8a20d6b --- /dev/null +++ b/MANIFEST @@ -0,0 +1,8 @@ +Changes +MANIFEST +Makefile.PL +README +lib/Net/Xero.pm +t/00-load.t +t/pod-coverage.t +t/pod.t diff --git a/Makefile.PL b/Makefile.PL new file mode 100644 index 0000000..e3af6d7 --- /dev/null +++ b/Makefile.PL @@ -0,0 +1,13 @@ +use inc::Module::Install; + +name 'Net-Xero'; +all_from 'lib/Net/Xero.pm'; +author q{Lenz Gschwendtner }; +license 'perl'; + +build_requires 'Test::More'; + +auto_install; + +WriteAll; + diff --git a/README b/README new file mode 100644 index 0000000..ccff42e --- /dev/null +++ b/README @@ -0,0 +1,55 @@ +Net-Xero + +The README is used to introduce the module and provide instructions on +how to install the module, any machine dependencies it may have (for +example C compilers and installed libraries) and any other information +that should be provided before the module is installed. + +A README file is required for CPAN modules since CPAN extracts the README +file from a module distribution so that people browsing the archive +can use it to get an idea of the module's uses. It is usually a good idea +to provide version information here so that people can decide whether +fixes for the module are worth downloading. + + +INSTALLATION + +To install this module, run the following commands: + + perl Makefile.PL + make + make test + make install + +SUPPORT AND DOCUMENTATION + +After installing, you can find documentation for this module with the +perldoc command. + + perldoc Net::Xero + +You can also look for information at: + + RT, CPAN's request tracker + http://rt.cpan.org/NoAuth/Bugs.html?Dist=Net-Xero + + AnnoCPAN, Annotated CPAN documentation + http://annocpan.org/dist/Net-Xero + + CPAN Ratings + http://cpanratings.perl.org/d/Net-Xero + + Search CPAN + http://search.cpan.org/dist/Net-Xero/ + + +COPYRIGHT AND LICENCE + +Copyright (C) 2010 Lenz Gschwendtner + +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. + diff --git a/ignore.txt b/ignore.txt new file mode 100644 index 0000000..d707a95 --- /dev/null +++ b/ignore.txt @@ -0,0 +1,10 @@ +blib* +Makefile +Makefile.old +Build +_build* +pm_to_blib* +*.tar.gz +.lwpcookies +Net-Xero-* +cover_db diff --git a/lib/Net/Xero.pm b/lib/Net/Xero.pm new file mode 100644 index 0000000..992beb1 --- /dev/null +++ b/lib/Net/Xero.pm @@ -0,0 +1,268 @@ +package Net::Xero; + +use Mouse; +use Net::OAuth; +use Template::Alloy; +use LWP::UserAgent; +use HTTP::Request::Common; +use Data::Random qw(rand_chars); +use XML::LibXML::Simple qw(XMLin); + +=head1 NAME + +Net::Xero - The great new Net::Xero! + +=head1 VERSION + +Version 0.01 + +=cut + +our $VERSION = '0.01'; + +has 'debug' => (is => 'rw', isa => 'Bool', default => 0); +has 'error' => (is => 'rw', isa => 'Str', predicate => 'has_error'); +has 'key' => (is => 'rw', isa => 'Str'); +has 'secret' => (is => 'rw', isa => 'Str'); +has 'nonce' => (is => 'ro', isa => 'Str', default => join( '', rand_chars( size => 16, set => 'alphanumeric' ) )); +has 'login_link' => (is => 'rw', isa => 'Str'); +has 'callback_url' => (is => 'rw', isa => 'Str', default => 'http://localhost:3000/callback'); +has 'request_token' => (is => 'rw', isa => 'Str'); +has 'request_secret' => (is => 'rw', isa => 'Str'); +has 'access_token' => (is => 'rw', isa => 'Str'); +has 'access_secret' => (is => 'rw', isa => 'Str'); +#has 'context' => (is => 'rw', isa => 'Str', default => 'sandbox'); + +=head1 SYNOPSIS + +Quick summary of what the module does. + +Perhaps a little code snippet. + + use Net::Xero; + + my $foo = Net::Xero->new(); + ... + +=head1 EXPORT + +A list of functions that can be exported. You can delete this section +if you don't export anything, such as for a purely object-oriented module. + +=head1 FUNCTIONS + +=cut + +=head2 login + +This sets up the initial OAuth handshake and returns the login URL. This +URL has to be clicked by the user and the the user then has to accept +the application in xero. + +Dropbox then redirects back to the callback URL defined with +C<$self-Ecallback_url>. If the user already accepted the application the +redirect may happen without the user actually clicking anywhere. + +=cut + +sub login { + my $self = shift; + + my $ua = LWP::UserAgent->new; + + my $request = Net::OAuth->request("request token")->new( + consumer_key => $self->key, + consumer_secret => $self->secret, + request_url => 'http://api.xero.com/0/oauth/request_token', + request_method => 'POST', + signature_method => 'HMAC-SHA1', + timestamp => time, + nonce => $self->nonce, + callback => $self->callback_url, + ); + + $request->sign; + my $res = $ua->request(GET $request->to_url); + + if ($res->is_success) { + my $response = Net::OAuth->response('request token')->from_post_body($res->content); + $self->request_token($response->token); + $self->request_secret($response->token_secret); + print "Got Request Token ", $response->token, "\n" if $self->debug; + print "Got Request Token Secret ", $response->token_secret, "\n" if $self->debug; + return 'http://api.xero.com/0/oauth/authorize?oauth_token='.$response->token.'&oauth_callback='.$self->callback_url; + } + else { + $self->error($res->status_line); + warn "Something went wrong: ".$res->status_line; + } +} + +=head2 auth + +The auth method changes the initial request token into access token that we need +for subsequent access to the API. This method only has to be called once +after login. + +=cut + +sub auth { + my $self = shift; + + my $ua = LWP::UserAgent->new; + my $request = Net::OAuth->request("access token")->new( + consumer_key => $self->key, + consumer_secret => $self->secret, + request_url => 'http://api.xero.com/0/oauth/access_token', + request_method => 'POST', + signature_method => 'HMAC-SHA1', + timestamp => time, + nonce => $self->nonce, + callback => $self->callback_url, + token => $self->request_token, + token_secret => $self->request_secret, + ); + + $request->sign; + my $res = $ua->request(GET $request->to_url); + + if ($res->is_success) { + my $response = Net::OAuth->response('access token')->from_post_body($res->content); + $self->access_token($response->token); + $self->access_secret($response->token_secret); + print "Got Access Token ", $response->token, "\n" if $self->debug; + print "Got Access Token Secret ", $response->token_secret, "\n" if $self->debug; + } + else { + $self->error($res->status_line); + warn "Something went wrong: ".$res->status_line; + } +} + +=head2 accounts + +accounts polls the users accoutns from xero. + +=cut + +sub accounts { + my $self = shift; + + return $self->_talk('Accounts'); +} + +=head1 INTERNAL API + +=head2 _talk + +_talk handles the access to the restricted resources. You should +normally not need to access this directly. + +=cut + +sub _talk { + my $self = shift; + my $command = shift; + my $method = shift || 'GET'; + my $content = shift; + + my $ua = LWP::UserAgent->new; + + my %opts = ( + consumer_key => $self->key, + consumer_secret => $self->secret, + request_url => 'https://api.xero.com/api.xro/2.0/'.$command, + request_method => $method, + signature_method => 'HMAC-SHA1', + timestamp => time, + nonce => $self->nonce, + #callback => $self->callback_url, + token => $self->access_token, + token_secret => $self->access_secret, + ); + my $request = Net::OAuth->request("protected resource")->new( %opts ); + + $request->sign; + + my $res; + if($method =~ /get/i){ + $res = $ua->get($request->to_url); + } else { + $res = $ua->post($request->to_url, Content_Type => 'form-data', Content => $content ); + } + + if ($res->is_success) { + print "Got Content ", $res->content, "\n" if $self->debug; + return XMLin($res->content); + } + else { + $self->error($res->status_line); + warn "Something went wrong: ".$res->status_line; + } + return; +} + +=head2 talk + +=cut + +=head1 AUTHOR + +Lenz Gschwendtner, C<< >> + +=head1 BUGS + +Please report any bugs or feature requests to C, or through +the web interface at L. 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 Net::Xero + + +You can also look for information at: + +=over 4 + +=item * RT: CPAN's request tracker + +L + +=item * AnnoCPAN: Annotated CPAN documentation + +L + +=item * CPAN Ratings + +L + +=item * Search CPAN + +L + +=back + + +=head1 ACKNOWLEDGEMENTS + + +=head1 COPYRIGHT & LICENSE + +Copyright 2010 Lenz Gschwendtner. + +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; # End of Net::Xero diff --git a/t/00-load.t b/t/00-load.t new file mode 100644 index 0000000..2ed6e09 --- /dev/null +++ b/t/00-load.t @@ -0,0 +1,9 @@ +#!perl -T + +use Test::More tests => 1; + +BEGIN { + use_ok( 'Net::Xero' ); +} + +diag( "Testing Net::Xero $Net::Xero::VERSION, Perl $], $^X" ); diff --git a/t/boilerplate.t b/t/boilerplate.t new file mode 100644 index 0000000..ba43502 --- /dev/null +++ b/t/boilerplate.t @@ -0,0 +1,55 @@ +#!perl -T + +use strict; +use warnings; +use Test::More tests => 3; + +sub not_in_file_ok { + my ($filename, %regex) = @_; + open( my $fh, '<', $filename ) + or die "couldn't open $filename for reading: $!"; + + my %violated; + + while (my $line = <$fh>) { + while (my ($desc, $regex) = each %regex) { + if ($line =~ $regex) { + push @{$violated{$desc}||=[]}, $.; + } + } + } + + if (%violated) { + fail("$filename contains boilerplate text"); + diag "$_ appears on lines @{$violated{$_}}" for keys %violated; + } else { + pass("$filename contains no boilerplate text"); + } +} + +sub module_boilerplate_ok { + my ($module) = @_; + not_in_file_ok($module => + 'the great new $MODULENAME' => qr/ - The great new /, + 'boilerplate description' => qr/Quick summary of what the module/, + 'stub function definition' => qr/function[12]/, + ); +} + +TODO: { + local $TODO = "Need to replace the boilerplate text"; + + not_in_file_ok(README => + "The README is used..." => qr/The README is used/, + "'version information here'" => qr/to provide version information/, + ); + + not_in_file_ok(Changes => + "placeholder date/time" => qr(Date/time) + ); + + module_boilerplate_ok('lib/Net/Xero.pm'); + + +} + diff --git a/t/pod-coverage.t b/t/pod-coverage.t new file mode 100644 index 0000000..fc40a57 --- /dev/null +++ b/t/pod-coverage.t @@ -0,0 +1,18 @@ +use strict; +use warnings; +use Test::More; + +# Ensure a recent version of Test::Pod::Coverage +my $min_tpc = 1.08; +eval "use Test::Pod::Coverage $min_tpc"; +plan skip_all => "Test::Pod::Coverage $min_tpc required for testing POD coverage" + if $@; + +# Test::Pod::Coverage doesn't require a minimum Pod::Coverage version, +# but older versions don't recognize some common documentation styles +my $min_pc = 0.18; +eval "use Pod::Coverage $min_pc"; +plan skip_all => "Pod::Coverage $min_pc required for testing POD coverage" + if $@; + +all_pod_coverage_ok(); diff --git a/t/pod.t b/t/pod.t new file mode 100644 index 0000000..ee8b18a --- /dev/null +++ b/t/pod.t @@ -0,0 +1,12 @@ +#!perl -T + +use strict; +use warnings; +use Test::More; + +# Ensure a recent version of Test::Pod +my $min_tp = 1.22; +eval "use Test::Pod $min_tp"; +plan skip_all => "Test::Pod $min_tp required for testing POD" if $@; + +all_pod_files_ok();