# ABSTRACT: Search Complex Data Structures
package Data::Seek;

use Data::Seek::Data;
use Data::Seek::Search;

use Data::Object::Class;
use Data::Object qw(:core);

our $VERSION = '0.08'; # VERSION

has 'cache' => (
    is      => 'rw',
    default => 0
);

has 'data' => (
    is      => 'ro',
    default => sub {{}}
);

has 'ignore' => (
    is      => 'rw',
    default => 0
);

sub search {
    my ($self, @criteria) = @_;
    my $cache  = $self->cache;
    my $data   = $self->data;
    my $ignore = $self->ignore;

    Data::Seek::Exception->throw('Not Searchable')
        unless UNIVERSAL::isa($data, 'HASH');

    my $search;

    if ($self->cache) {
        if ($search = $self->{cached_search}) {
            $search->criteria({});
        }
    }

    $search //= Data::Seek::Search->new(
        cache  => $cache,
        ignore => $ignore,
        data   => Data::Seek::Data->new($data),
    );

    $search->criterion($_) for @criteria;

    if ($self->cache) {
        $self->{cached_search} = $search;
    }
    else {
        $self->{cached_search} = undef;
    }

    return $search->result;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Data::Seek - Search Complex Data Structures

=head1 VERSION

version 0.08

=head1 SYNOPSIS

    use Data::Seek;

    my $hash   = {...};
    my $seeker = Data::Seek->new(data => $hash);
    my $result = $seeker->search('*');
    my $data   = $result->data;

=head1 DESCRIPTION

Data::Seek is a module for traversing complex data structures. This module
allows you to select specific node(s) in a hierarchical data structure using a
criteria. A criteria is an expression consisting of one or more criterion. A
criterion is the part of the criteria that is used to select node(s) to be
returned in the result. Data::Seek is akin to L<Data::Dpath> but with fewer
features and a simple node selection syntax. Data::Seek node traversal is
extremely fast, and it can throw exception objects which explain, in detail,
why a search failed. This is very useful internally, and externally when
processing foreign data structures where you need to provide detailed errors
explaining how to resolve the missing or malformed data nodes.

=head1 CONCEPTS

The follow is a short and simple overview of the strategy and syntax used by
Data::Seek to query complex data strictures. The overall idea behind Data::Seek
is to flatten/fold the data structure, preferably once, reduce it by applying a
series patterns, then unflatten/unfold and operate on the new data structure.
The introspection strategy is to flatten the data structure producing a
non-hierarchical data structure where its keys represent endpoints (using
dot-notation and colons to separate nested hash keys and array indices
respectively) within the structure.

=head1 ENCODING

During the processing of flattening a data structure with nested data, the
following data structure would be converted into a collection of endpoint/value
pairs.

    {
        'id' => 12345,
        'patient' => {
            'name' => {
                'first' => 'Bob',
                'last'  => 'Bee'
            }
        },
        'medications' => [{
            'aceInhibitors' => [{
                'name'      => 'lisinopril',
                'strength'  => '10 mg Tab',
                'dose'      => '1 tab',
                'route'     => 'PO',
                'sig'       => 'daily',
                'pillCount' => '#90',
                'refills'   => 'Refill 3'
            }],
            'antianginal' => [{
                'name'      => 'nitroglycerin',
                'strength'  => '0.4 mg Sublingual Tab',
                'dose'      => '1 tab',
                'route'     => 'SL',
                'sig'       => 'q15min PRN',
                'pillCount' => '#30',
                'refills'   => 'Refill 1'
            }],
        }]
    }

Given the aforementioned data structure, the following would be the resulting
flattened structure comprised of endpoint/value pairs.

    {
        'id'                                      => 12345,
        'medications:0.aceInhibitors:0.dose'      => '1 tab',
        'medications:0.aceInhibitors:0.name'      => 'lisinopril',
        'medications:0.aceInhibitors:0.pillCount' => '#90',
        'medications:0.aceInhibitors:0.refills'   => 'Refill 3',
        'medications:0.aceInhibitors:0.route'     => 'PO',
        'medications:0.aceInhibitors:0.sig'       => 'daily',
        'medications:0.aceInhibitors:0.strength'  => '10 mg Tab',
        'medications:0.antianginal:0.dose'        => '1 tab',
        'medications:0.antianginal:0.name'        => 'nitroglycerin',
        'medications:0.antianginal:0.pillCount'   => '#30',
        'medications:0.antianginal:0.refills'     => 'Refill 1',
        'medications:0.antianginal:0.route'       => 'SL',
        'medications:0.antianginal:0.sig'         => 'q15min PRN',
        'medications:0.antianginal:0.strength'    => '0.4 mg Sublingual Tab',
        'patient.name.first'                      => 'Bob'
        'patient.name.last'                       => 'Bee',
    }

This structure provides the endpoint strings which will be matched against using
the querying strategy.

=head1 QUERYING

During the processing of querying the data structure, the criteria (query
expressions) are converted into a series of regular expressions to be applied
sequentially, filtering/reducing the endpoints and producing a data set of
matching nodes or throwing an exception explaining the search failure.

=head2 Node Expression

    id
    patient
    medications

The node expression is a part of a criterion, which preforms an exact match
against a node in the data structure. It is a string which can contain letters,
numbers, and/or underscores.

=head2 Step Expression

    my $result = $seeker->search(...);

    # given "id"
    { id => 12345 }

    # given "patient.name.first"
    { patient => { name => { first => "Bob" } } }

    # given "patient.name.last"
    { patient => { name => { last => "Bee" } } }

The step expression is a criterion, or part of a criterion, made up of one or
more node expressions separated using the period character, which matches
against nodes in the data structure. It is a string which can contain letters,
numbers, and/or underscores, separated using periods.

=head2 Index Expression

    my $result = $seeker->search(...);

    # given "medications:0.aceInhibitors:0.dose"
    { medications => [{ aceInhibitors => [{ dose => "1 tab" }] }] }

    # given "medications:0.aceInhibitors:0.name"
    { medications => [{ aceInhibitors => [{ name => "lisinopril" }] }], }

    # given "medications:0.aceInhibitors:0.pillCount"
    { medications => [{ aceInhibitors => [{ pillCount => "#90" }] }] }

The index expression is a criterion, or part of a criterion, having a node
expressions suffixed with a colon followed by a number denoting that it should
only match an array which has an index corresponding to the numeric portion of
the suffix. It is a string which can contain letters, numbers, and/or
underscores, suffixed with a semi-colon followed by a number.

=head2 Iterator Expression

    my $result = $seeker->search(...);

    # given "@medications.@aceInhibitors.dose"
    { medications => [{ aceInhibitors => [{ dose => "1 tab" }] }] }

    # given "@medications.@aceInhibitors.name"
    { medications => [{ aceInhibitors => [{ name => "lisinopril" }] }], }

    # given "@medications.@aceInhibitors.pillCount"
    { medications => [{ aceInhibitors => [{ pillCount => "#90" }] }] }

The iteration expression is a criterion, or part of a criterion, having a node
expressions preceded by an "at" character (ampersand) denoting that the node
expression should match all nodes in the data structure which are mapped to
array objects. It is a string which can contain letters, numbers, and/or
underscores, preceded by a single ampersand character.

=head2 Wildcard Expression

    my $result = $seeker->search(...);

    # given "*"
    { id => 12345 }

    # given "*.*.first"
    { patient => { name => { first => "Bob" } } }

    # given "*.*.last"
    { patient => { name => { last => "Bee" } } }

    # given "patient.*.first"
    { patient => { name => { first => "Bob" } } }

    # given "patient.*.last"
    { patient => { name => { last => "Bee" } } }

    # given "@*.@*.pillCount"
    {
        medications => [{
            aceInhibitors => [{ pillCount => "#90" }],
            antianginal   => [{ pillCount => "#30" }],
        }],
    }

The wildcard expression is a criterion, or part of a criterion, which matches
against a single node having a single "star" character match and represent one
node expression. It is a string which can contain letters, numbers, underscores,
and/or a single star character.

=head2 Greedy-Wildcard Expression

    my $result = $seeker->search(...);

    # given "**.first"
    { patient => { name => { first => "Bob" } } }

    # given "**.last"
    { patient => { name => { last => "Bee" } } }

    # given "patient.**"
    { patient => { name => { first => "Bob", last => "Bee" } } }

    # given "medications**.pillCount"
    {
        medications => [{
            aceInhibitors => [{ pillCount => "#90" }],
            antianginal   => [{ pillCount => "#30" }],
        }],
    }

The greedy-wildcard expression is a criterion, or part of a criterion, which
matches against any multitude of nodes having a double "star" character match
and represent one or more of any character. It is a string which can contain
letters, numbers, underscores, and/or a double star character.

=head1 ATTRIBUTES

=head2 cache

    $seeker->cache;
    $seeker->cache(1);

Encode the data structure and cache the result. Allows multiple queries to
execute faster. Caching is disabled by default.

=head2 data

    $seeker->data;
    $seeker->data(Data::Seek::Data->new(...));

The data structure to be introspected, must be a hash reference, blessed or not,
which defaults to or becomes a L<Data::Seek::Data> object.

=head2 ignore

    $seeker->ignore;
    $seeker->ignore(1);

Bypass exceptions thrown when a criterion is invalid or no data matches can be
found.

=head1 METHODS

=head2 search

    my @criteria = ('id', 'person.name.*');
    my $result   = $seeker->search(@criteria);

Prepare a search object to use the supplied criteria and return a result
object. Introspection is triggered when the result object is enacted. See
L<Data::Seek::Search::Result> for usage information.

=head1 AUTHOR

Al Newkirk <anewkirk@ana.io>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2014 by Al Newkirk.

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
