# Build::Hopen::Scope - a nested key-value store
use Exporter 'import';
our $VERSION = '0.000006'; # TRIAL
# Class definition
outer => undef,
local => false,
name => 'anonymous scope',
# Internal
_first_set => undef, # name of the first set
};
# Static exports
our @EXPORT; BEGIN { @EXPORT=qw(FIRST_ONLY); }
my $_first_only = {};
sub FIRST_ONLY { $_first_only }
use constant _LOCAL => 'local';
# What we use
use Config;
use POSIX ();
use Build::Hopen::Util::Data qw(forward_opts);
use Sub::ScopeFinalizer qw(scope_finalizer);
# Docs {{{1
=head1 NAME
Build::Hopen::Scope - a nested key-value store.
=head1 SYNOPSIS
A Scope represents a set of data available to operations. It is a
key-value store that falls back to an outer C<Scope> if a requested key
isn't found.
This class is the abstract base of Scopes. See L<Build::Hopen::Scope::Hash>
for an example of a concrete implementation using a hash under the
hood. Different subclasses use different representations.
See L</"FUNCTIONS TO BE OVERRIDDEN IN SUBCLASSES"> for more on that topic.
=head1 STATIC EXPORTS
=head2 FIRST_ONLY
A flag used as a L</$set> (q.v.).
=head1 ATTRIBUTES
=head2 outer
The fallback C<Scope> for looking up names not found in this C<Scope>.
If non is provided, it is C<undef>, and no fallback will happen.
=head2 local
(Default falsy.) If truthy, do not go past this scope when doing local
lookups (see L</$levels> below).
=head2 name
Not used, but provided so you can use L<Build::Hopen/hnew> to make Scopes.
=head1 PARAMETERS
The methods generally receive the same parameters. They are as follows.
=head2 $name
The name of an item to be looked up. Names must be truthy. That means,
among other things, that C<'0'> is not a valid key.
=head2 $set
A Scope can have multiple sets of data. C<$set> specifies which one to
look in.
=over
=item *
If specified as a number or a name, look only in that set.
=item *
If C<'*'>, look in every available set at this level, and return a
hashref of C<< { set_name => value } >>.
Note that this is not recursive --- it won't collect all instances
of the given name from all sets in all the levels. (TODO? change this?)
=item *
If L</FIRST_ONLY>, look in only the first set (usually named C<0>).
=item *
If unspecified or undefined, look in every available set at this level, and
return the first one found, regardless of which set it comes from.
=back
=head2 $levels
How many levels up (L</outer>) to go when performing an operation. Note:
chains more than C<POSIX::INT_MAX> (L<POSIX/LIMITS>) Scopes long may fail in
unexpected ways, depending on your platform! For 32- or 64-bit platforms,
that number is at least 2,000,000,000, so you're probably OK :) .
=over
=item *
If numeric and non-negative, go up that many more levels
(i.e., C<$levels==0> means only return this scope's local names).
=item *
If C<'local'>, go up until reaching a scope with L</local> set.
If the current scope has L</local> set, don't go up at all.
=item *
If not provided or not defined, go all the way to the outermost Scope.
=back
=head1 METHODS
See also L</add>, below, which is part of the public API.
=cut
# }}}1
# Handle $levels and invoke a function on the outer scope if appropriate.
# Usage:
# $self->_invoke(coderef, $levels, [other args to be passed, starting with
# invocant, if any]
# A new levels value will be added to the end of the args as -levels=>$val.
# Returns undef if there's no more traversing to be done.
sub _invoke {
my $self = shift or croak 'Need an instance';
my $coderef = shift or croak 'Need a coderef';
my $levels = shift;
# Handle 'local'-scoped searches by terminating when $self->local is set.
$levels = 0 if ( ($levels//'') eq _LOCAL) && $self->local;
# Search the outer scopes
if($self->outer && # Search the outer scopes
(!defined($levels) || ($levels eq _LOCAL) || ($levels>0) )
) {
my $newlevels =
!defined($levels) ? undef :
( ($levels eq _LOCAL) ? _LOCAL : ($levels-1) );
unshift @_, $self->outer;
push @_, -levels => $newlevels;
goto &$coderef;
}
return undef;
} #_invoke()
=head2 find
Find a named data item in the scope and return it. Looks up the scope chain
to the outermost scope if necessary. Returns undef on
failure. Usage:
$scope->find($name[, $set[, $levels]]);
$scope->find($name[, -set => $set][, -levels => $levels]);
# Alternative using named arguments
Dies if given a falsy name, notably, C<'0'>.
=cut
sub find {
my ($self, %args) = parameters('self', [qw(name ; set levels)], @_);
croak 'Need a name' unless $args{name};
# Therefore, '0' is not a valid name
my $levels = $args{levels};
my $here = $self->_find_here($args{name}, $args{set});
return $here if defined $here;
return $self->_invoke(\&find, $args{levels},
forward_opts(\%args, {'-'=>1}, qw(name set))
);
} #find()
=head2 names
Returns a L<Set::Scalar> of the names of the items available through this
Scope, optionally including all its parent Scopes (if any). Usage
and example:
my $set = $scope->names([$levels]);
say "Name $_ is available" foreach @$set; # Set::Scalar supports @$set
TODO? Support a C<$set> parameter?
=cut
sub names {
my ($self, %args) = parameters('self', [qw(; levels)], @_);
my $retval = Set::Scalar->new;
$self->_fill_names($retval, $args{levels});
return $retval;
} #names()
# Implementation of names()
sub _fill_names {
#say Dumper(\@_);
my ($self, %args) = parameters('self', [qw(retval levels)], @_);
$self->_names_here($args{retval}); # Insert this scope's names
return $self->_invoke(\&_fill_names, $args{levels}, -retval=>$args{retval});
} #_fill_names()
=head2 as_hashref
Returns a hash of the items available through this Scope, optionally
including all its parent Scopes (if any). Usage:
my $hashref = $scope->as_hashref([-levels => $levels][, -deep => $deep])
If C<$levels> is provided and nonzero, go up that many more levels
(i.e., C<$levels==0> means only return this scope's local names).
If C<$levels> is not provided, go all the way to the outermost Scope.
If C<$deep> is provided and truthy, make a deep copy of each value (using
L<Build::Hopen/clone>. Otherwise, just copy.
TODO? Support a C<$set> parameter?
=cut
sub as_hashref {
my ($self, %args) = parameters('self', [qw(; levels deep)], @_);
my $hrRetval = {};
$self->_fill_hashref($hrRetval, $args{deep}, $args{levels});
return $hrRetval;
} #as_hashref()
# Implementation of as_hashref. Mutates the provided $hrRetval.
# TODO move this to subclasses.
sub _fill_hashref {
my ($self, %args) = parameters('self', [qw(retval levels deep)], @_);
my $hrRetval = $args{retval};
# Innermost wins, so copy ours first.
foreach my $k (keys %{$self->_content}) {
unless(exists($hrRetval->{$k})) { # An inner scope might have set it
$hrRetval->{$k} =
($args{deep} ? clone($self->_content->{$k}) : $self->_content->{$k});
}
}
return $self->_invoke(\&_fill_hashref, $args{levels},
forward_opts(\%args, {'-'=>1}, qw(retval deep)));
} #_fill_hashref()
=head2 outerize
Set L</outer>, and return a scalar that will restore L</outer> when it
goes out of scope. Usage:
my $saver = $scope->outerize($new_outer);
C<$new_outer> may be C<undef> or a valid C<Scope>.
=cut
sub outerize {
my ($self, %args) = parameters('self', [qw(outer)], @_);
croak 'Need a Scope' unless
(!defined($args{outer})) or
(ref $args{outer} && eval { $args{outer}->DOES('Build::Hopen::Scope') });
# Protect the author of this function from himself
croak 'Sorry, but I must insist that you save my return value'
unless defined wantarray;
my $old_outer = $self->outer;
my $saver = scope_finalizer { $self->outer($old_outer) };
$self->outer($args{outer});
return $saver;
} #outerize()
=head1 FUNCTIONS TO BE OVERRIDDEN IN SUBCLASSES
To implement a Scope with a different data-storage model than the hash
this class uses, subclass Scope and override these functions. Only L</add>
is part of the public API.
=head2 add
Add key-value pairs to this scope. Returns the scope so you can
chain. Example usage:
my $scope = Build::Hopen::Scope->new()->add(foo => 1);
C<add> is responsible for handling any conflicts that may occur. In this
particular implementation, the last-added value for a particular key wins.
TODO add $set option.
=cut
sub add {
...
} #add()
=head2 _names_here
Populates a L<Set::Scalar> with the names of the items stored in this Scope,
but B<not> any outer Scope. Called as:
$scope->_names_here($retval[, $set])
C<$retval> is the C<Set::Scalar> instance. C<$set> is as
defined L<above|/$set>.
No return value.
=cut
sub _names_here {
...
} #_names_here()
=head2 _find_here
Looks for a given item in this scope, but B<not> any outer scope. Called as:
$scope->_find_here($name[, $set])
Returns the value, or C<undef> if not found.
=cut
sub _find_here {
...
} #_find_here()
1;
__END__
# vi: set fdm=marker: #