The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use 5.12.0;
use warnings;

package Data::TagHive;
BEGIN {
  $Data::TagHive::VERSION = '0.001';
}
# ABSTRACT: hierarchical tags with values

use Carp;


sub new {
  my ($class) = @_;

  return bless { state => {} } => $class;
}

my $tagname_re  = qr{ [a-z] [-a-z0-9_]* }x;
my $tagvalue_re = qr{ [-a-z0-9_]+ }x;
my $tagpair_re  = qr{ $tagname_re (?::$tagvalue_re)? }x;
my $tagstr_re   = qr{ \A $tagpair_re (?:\.$tagpair_re)* \z }x;

sub _assert_tagstr {
  my ($self, $tagstr) = @_;
  croak "invalid tagstr <$tagstr>" unless $tagstr =~ $tagstr_re;
}

sub _tag_pairs {
  my ($self, $tagstr) = @_;

  $self->_assert_tagstr($tagstr);

  my @tags = map { my @pair = split /:/, $_; $#pair = 1; \@pair }
             split /\./, $tagstr;

  return @tags;
}

sub __differ {
  my ($x, $y) = @_;

  return 1 if defined $x xor defined $y;
  return unless defined $x;

  return $x ne $y;
}


sub add_tag {
  my ($self, $tagstr) = @_;

  my $state = $self->{state};

  my @tags  = $self->all_tags;
  my @pairs = $self->_tag_pairs($tagstr);

  my $stem = '';

  while (my $pair = shift @pairs) {
    $stem .= '.' if length $stem;

    my $key   = $stem . $pair->[0];
    my $value = length($pair->[1]) ? $pair->[1] : undef;

    CONFLICT: {
      if (exists $state->{ $key }) {
        my $existing = $state->{ $key };

        # Easiest cases: if they're both undef, or are eq, no conflict.
        last CONFLICT unless __differ($value, $existing);

        # Easist conflict case: we want to set tag:value1 but tag:value2 is
        # already set.  No matter whether there are descendants on either side,
        # this is a
        # conflict.
        croak "can't add <$tagstr> to taghive; conflict at $key"
          if defined $value and defined $existing and $value ne $existing;

        my $more_to_set = defined($value)         || @pairs;
        my $more_exists = defined($state->{$key}) || grep { /\A\Q$key./ } @tags;

        croak "can't add <$tagstr> to taghive; conflict at $key"
          if $more_to_set and $more_exists;
      }
    }


    $state->{ $key } = $value;

    $stem = defined $value ? "$key:$value" : $key;

    $state->{$stem} = undef;
  }
}


sub has_tag {
  my ($self, $tagstr) = @_;

  my $state = $self->{state};

  $self->_assert_tagstr($tagstr);
  return 1 if exists $state->{$tagstr};
  return;
}


sub delete_tag {
  my ($self, $tagstr) = @_;

  $self->_assert_tagstr($tagstr);

  my $state = $self->{state};
  my @keys  = grep { /\A$tagstr(?:$|[.:])/ } keys %$state;
  delete @$state{ @keys };

  if ($tagstr =~ s/:($tagvalue_re)\z//) {
    delete $state->{ $tagstr } if $state->{$tagstr} // '' eq $1;
  }
}


sub all_tags {
  my ($self) = @_;
  return keys %{ $self->{state} };
}

1;

__END__
=pod

=head1 NAME

Data::TagHive - hierarchical tags with values

=head1 VERSION

version 0.001

=head1 SYNOPSIS

  use Data::TagHive;

  my $taghive = Data::TagHive->new;

  $taghive->add_tag('book.topic:programming');

  $taghive->has_tag('book'); # TRUE

=head1 OVERVIEW

Data::TagHive is the bizarre, corrupted union of L<String::TagString> and
L<Data::Hive>.  It combines the "simple list of strings" of the former with the
"hierarchical key-value/value pairs" of the latter, using a different interface
from either.

It's probably better than that sounds, though.

A Data::TagHive object represents a set of tags.  Each tag is a string that
represents a structure of nested key-value pairs.  For example, a library book
might be tagged:

  book.pages.size:letter
  book.pages.count:180
  book.type:hardcover
  book.topic:programming.perl.cpan

Each tag is a set of key-value pairs.  Later pairs are qualified by earlier
pairs.  Values are optional.  Keys and values are separated by colons.
Key-value pairs are separated by dots.

A tag is considered present if it was set explicitly or if any more-specific
subtag of it was set.  For example, if we had explicitly added all the tags
shown above, a tag hive would then report true if asked whether each of the
following tags were set:

  book
  book.pages
  book.pages.size
  book.pages.size:letter
  book.pages.count
  book.pages.count:180
  book.type
  book.type:hardcover
  book.topic
  book.topic:programming
  book.topic:programming.perl
  book.topic:programming.perl.cpan

=head1 METHODS

=head2 add_tag

  $taghive->add_tag( $tagstr );

This method adds the given tag (given as a string) to the hive.  It will fail
if there are conflicts.  For example, if "foo:bar" is already set, "foo:xyz"
cannot be set.  Each tag can only have one value.

Tags without values may be given values through C<add_tag>, but only if they
have no tags beneath them.  For example, given a tag hive with "foo.bar"
tagged, "foo.bar:baz" could be added, but not "foo:baz"

=head2 has_tag

  if ($taghive->has_tag( $tagstr )) { ... }

This method returns true if the tag hive has the tag.

=head2 delete_tag

  $taghive->delete_tag( $tagstr );

This method deletes the tag from the hive, along with any tags below it.

If your hive has "foo.bar:xyz.abc" and you C<delete_tag> "foo.bar" it will be
left with nothing but the tag "foo"

=head2 all_tags

This method returns, as a list of strings, all the tags set on the hive either
explicitly or implicitly.

=head1 AUTHOR

Ricardo Signes <rjbs@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2011 by Ricardo Signes.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut