App-karr

 view release on metacpan or  search on metacpan

lib/App/karr/Task.pm  view on Meta::CPAN

# ABSTRACT: Task object representing a single kanban card

package App::karr::Task;
our $VERSION = '0.302';
use Moo;
use YAML::XS qw( Load Dump );
use Path::Tiny;
use Time::Piece;
use JSON::MaybeXS qw( encode_json );


has id         => ( is => 'ro', required => 1 );
has title      => ( is => 'rw', required => 1 );
has status     => ( is => 'rw', default => sub { 'backlog' } );
has priority   => ( is => 'rw', default => sub { 'medium' } );
has assignee   => ( is => 'rw', predicate => 1, clearer => 1 );
has tags       => ( is => 'rw', default => sub { [] } );
has due        => ( is => 'rw', predicate => 1, clearer => 1 );
has estimate   => ( is => 'rw', predicate => 1, clearer => 1 );
has class      => ( is => 'rw', default => sub { 'standard' } );
has parent     => ( is => 'rw', predicate => 1, clearer => 1 );
has depends_on => ( is => 'rw', default => sub { [] } );
has body       => ( is => 'rw', default => sub { '' } );
has created    => ( is => 'ro', default => sub { gmtime->datetime . 'Z' } );
has updated    => ( is => 'rw', default => sub { gmtime->datetime . 'Z' } );
has claimed_by => ( is => 'rw', predicate => 1, clearer => 1 );
has claimed_at => ( is => 'rw', predicate => 1, clearer => 1 );
has blocked    => ( is => 'rw', predicate => 1, clearer => 1 );
has started    => ( is => 'rw', predicate => 1, clearer => 1 );
has completed  => ( is => 'rw', predicate => 1, clearer => 1 );
has file_path  => ( is => 'rw', predicate => 1 );

# Optional fields are addressed through their predicate everywhere (pick,
# board, list, show, handoff all treat has_X as "is this set"). Clearing one
# by assigning undef would leave the predicate true, so callers must use the
# generated clear_X. This guards the load path: a file that carries an
# explicit null (our own older writes, or an external kanban-md edit) is
# normalized back to "unset" instead of lingering as has_X-true-but-undef.
sub BUILD {
  my ($self) = @_;
  for my $attr (qw( assignee due estimate parent claimed_by claimed_at blocked started completed )) {
    my $clearer = "clear_$attr";
    my $has     = "has_$attr";
    $self->$clearer if $self->$has && !defined $self->$attr;
  }
}

sub slug {
  my ($self) = @_;
  my $slug = lc($self->title);
  $slug =~ s/[^a-z0-9]+/-/g;
  $slug =~ s/^-|-$//g;
  $slug = substr($slug, 0, 50);
  return $slug;
}

sub filename {
  my ($self) = @_;
  return sprintf('%03d-%s.md', $self->id, $self->slug);
}

sub to_frontmatter {
  my ($self) = @_;
  my %fm = (
    id       => $self->id,
    title    => $self->title,
    status   => $self->status,
    priority => $self->priority,
    created  => $self->created,
    updated  => $self->updated,
    class    => $self->class,
  );
  $fm{assignee}   = $self->assignee   if $self->has_assignee;
  $fm{tags}       = $self->tags       if @{$self->tags};
  $fm{due}        = $self->due        if $self->has_due;
  $fm{estimate}   = $self->estimate   if $self->has_estimate;
  $fm{parent}     = $self->parent     if $self->has_parent;
  $fm{depends_on} = $self->depends_on if @{$self->depends_on};
  $fm{claimed_by} = $self->claimed_by if $self->has_claimed_by;
  $fm{claimed_at} = $self->claimed_at if $self->has_claimed_at;
  $fm{blocked}    = $self->blocked    if $self->has_blocked;
  $fm{started}    = $self->started    if $self->has_started;
  $fm{completed}  = $self->completed  if $self->has_completed;
  return \%fm;
}

sub to_markdown {
  my ($self) = @_;
  my $yaml = Dump($self->to_frontmatter);
  $yaml =~ s/\A---\n//;
  my $md = "---\n${yaml}---\n";
  $md .= "\n" . $self->body . "\n" if $self->body;
  return $md;
}

sub _parse_content {
  my ($class, $content) = @_;
  my ($yaml, $body) = $content =~ m{\A---\n(.+?)---(?:\n(.*))?\z}s
    or die "Invalid task format\n";
  $body //= '';
  $body =~ s/^\n//;
  $body =~ s/\n$//;
  return (Load($yaml), $body);
}

sub from_string {
  my ($class, $content) = @_;
  my ($fm, $body) = $class->_parse_content($content);
  return $class->new(%$fm, body => $body);
}

sub from_file {
  my ($class, $file) = @_;
  $file = path($file);
  my ($fm, $body) = $class->_parse_content($file->slurp_utf8);
  return $class->new(%$fm, body => $body, file_path => $file);
}

sub save {
  my ($self, $dir) = @_;
  $self->updated(gmtime->datetime . 'Z');
  my $file = $dir ? path($dir)->child($self->filename) : path($self->file_path);
  $file->spew_utf8($self->to_markdown);
  $self->file_path($file);
  return $file;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

App::karr::Task - Task object representing a single kanban card

=head1 VERSION



( run in 2.229 seconds using v1.01-cache-2.11-cpan-2398b32b56e )