=================================================================== RCS file: /cvs/todotxt/Text-Todo-REST-API/lib/Text/Todo/REST/API.pm,v retrieving revision 1.1.1.1 retrieving revision 1.10 diff -u -r1.1.1.1 -r1.10 --- todotxt/Text-Todo-REST-API/lib/Text/Todo/REST/API.pm 2010/01/16 16:01:07 1.1.1.1 +++ todotxt/Text-Todo-REST-API/lib/Text/Todo/REST/API.pm 2010/01/24 04:17:39 1.10 @@ -1,24 +1,310 @@ package Text::Todo::REST::API; -# $RedRiver$ +# $AFresh1: API.pm,v 1.9 2010/01/23 07:15:40 andrew Exp $ use warnings; use strict; use Carp; +use Text::Todo; +use Text::Todo::REST::API::Response; + +use Class::Std::Utils; +use Digest::MD5 qw/ md5_hex /; + use version; our $VERSION = qv('0.0.1'); -# Other recommended modules (uncomment to use): -# use IO::Prompt; -# use Perl6::Export; -# use Perl6::Slurp; -# use Perl6::Say; +{ + my @attr_refs = \( + my %todo_of, + my %suffix_of, + my %file_regex_of, -# Module implementation here + my %format_of, + my %action_handlers, + ); -1; # Magic true value required at end of module + sub new { + my ( $class, $options ) = @_; + + my $self = bless anon_scalar(), $class; + my $ident = ident($self); + + $format_of{$ident} = $options->{default_format}; + if ( $options->{format} ) { + $format_of{$ident} = $options->{format}; + } + + $suffix_of{$ident} = $options->{suffix} || '.txt'; + + $file_regex_of{$ident} = $options->{file_regex} || qr{ + .* + todo + .* + \Q$suffix_of{$ident}\E + $ + }ixms; + + eval { + $todo_of{$ident} = Text::Todo->new( + { todo_dir => $options->{todo_dir}, + todo_file => $options->{todo_file}, + } + ); + }; + if ($@) { + $self->fail( 'Unable to create Text::Todo object' . $@ ); + } + + return $self; + } + + sub _parse_options { + my ( $self, $method, @args ) = @_; + + my %options = ( + method => lc $method, + list => '', + action => 'files', + args => [], + ); + + if (@args) { + if ( !ref $args[0] ) { + $options{path} = shift @args; + } + + if ( ref $args[0] eq 'HASH' ) { + my $opts = shift @args; + foreach my $o ( keys %{$opts} ) { + $options{$o} = $opts->{$o}; + } + } + } + + if ( exists $options{path} ) { + my %opts = $self->_split_path( $options{path} ); + delete $options{path}; + + foreach my $o ( keys %opts ) { + if ( defined $opts{$o} ) { + $options{$o} = $opts{$o}; + } + } + } + + if ( $options{action} eq 'entry' && @{ $options{args} } ) { + $options{entry} = shift @{ $options{args} }; + if ( @{ $options{args} } ) { + $options{action} = lc shift @{ $options{args} }; + } + } + + push @{ $options{args} }, @args; + + $options{list} + = defined $options{list} ? $options{list} : 'todo_file'; + + if ( $options{format} ) { + $format_of{ ident $self } = $options{format}; + delete $options{format}; + } + + my @method; + foreach my $o qw( method action ) { + if ( $options{$o} ) { + push @method, $options{$o}; + } + } + $method = join q{_}, @method; + + return $method, %options; + } + + sub _handle_action { + my ( $self, @args ) = @_; + + my ( $method, %options ) = $self->_parse_options(@args); + + my $todo = $self->_todo; + $todo->load( $options{list} ); + + foreach my $class ($self) { + if ( $class->can($method) ) { + my @data = $class->$method( $todo, \%options ); + + return Text::Todo::REST::API::Response->new({ + type => $options{action}, + format => $self->_format, + data => \@data, + }); + } + } + + return $self->fail( 'Unable to handle [' . $method . ']' ); + } + + sub _split_path { + my ( $self, $path ) = @_; + + my %options = ( + list => undef, + action => undef, + args => [], + ); + + $path = defined $path ? $path : q{}; + $path =~ s{^/}{}xms; + + if ( $path =~ s/\.(\w+)$//xms ) { + $options{format} = $1; + } + + ( $options{list}, $options{action}, @{ $options{args} } ) = split '/', + $path; + + if ( $options{list} ) { + $options{action} ||= 'list'; + + my $suffix = $self->_suffix; + + if ( ( lc $options{list} ) eq 'files' ) { + $options{action} = lc $options{list}; + $options{list} = q{}; + } + elsif ( $self->_todo->file( $options{list} ) ) { + $options{list} = $self->_todo->file( $options{list} ); + } + } + + if ( @{ $options{args} } && ( lc $options{args}[0] ) eq 'entry' ) { + $options{action} = lc shift @{ $options{args} }; + } + + return %options; + } + + sub GET { + my ( $self, @args ) = @_; + return $self->_handle_action( 'GET', @args ); + } + + sub get_entry { + my ( $self, $todo, $key ) = @_; + + if ( !$key ) { + return $self->fail("get_entry requires arguments"); + } + elsif ( ref $key eq 'ARRAY' ) { + my @entries; + foreach ( @{$key} ) { + push @entries, $self->get_entry($_); + } + return @entries; + } + elsif ( ref $key eq 'HASH' ) { + if ( exists $key->{entry} ) { + $key = $key->{entry}; + } + else { + return $self->fail('get_entry requires key [entry]'); + } + } + + my @list = $self->get_list($todo); + + my $entry; + if ( $key =~ /^[[:xdigit:]]{32}$/xms ) { + my $search = lc $key; + + ENTRY: foreach my $e (@list) { + if ( $search eq $e->{md5} ) { + $entry = $e; + last ENTRY; + } + } + } + elsif ( $key =~ /^\d+$/xms ) { + $entry = $list[ $key - 1 ]; + } + + if ( !$entry ) { + return $self->fail("Unable to find entry!"); + } + + return $entry; + } + + sub get_list { + my ( $self, $todo ) = @_; + + my $line = 1; + return map ( { + line => $line++, + md5 => md5_hex( $_->text ), + text => $_->text, + }, + $todo->list ); + } + + sub get_files { + my ( $self, $todo ) = @_; + my $dir = $todo->file('todo_dir'); + + if ( !$dir ) { + return $self->fail('Unable to find todo_dir'); + } + + my $file_regex = $self->_file_regex; + + opendir my $dh, $dir or croak "Couldn't opendir: $!"; + my @files = grep {m/$file_regex/xms} readdir $dh; + closedir $dh; + + return @files; + } + + sub get_tags { + my ( $self, $todo, $tag ) = @_; + return $todo->listtag($tag); + } + + sub POST { + my ( $self, @args ) = @_; + return $self->_handle_action( 'POST', @args ); + } + + sub PUT { + my ( $self, @args ) = @_; + return $self->_handle_action( 'PUT', @args ); + } + + sub DELETE { + my ( $self, @args ) = @_; + return $self->_handle_action( 'DELETE', @args ); + } + + sub fail { + my ( $self, @message ) = @_; + croak(@message); + } + + sub _todo { my ($self) = @_; return $todo_of{ ident $self }; } + sub _suffix { my ($self) = @_; return $suffix_of{ ident $self}; } + sub _file_regex { my ($self) = @_; return $file_regex_of{ ident $self}; } + sub _format { my ($self) = @_; return $format_of{ ident $self}; } + + sub DESTROY { + my ($self) = @_; + my $ident = ident $self; + foreach my $attr_ref (@attr_refs) { + delete $attr_ref->{$ident}; + } + } +} +1; # Magic true value required at end of module __END__ =head1 NAME