# This file is part of Config::AST -*- perl -*- # Copyright (C) 2017-2019 Sergey Poznyakoff <gray@gnu.org> # # Config::AST is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3, or (at your option) # any later version. # # Config::AST is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Config::AST. If not, see <http://www.gnu.org/licenses/>. package Config::AST::Node; use strict; use warnings; use parent 'Exporter'; use Text::Locus; use Clone 'clone'; use Carp; our %EXPORT_TAGS = ( 'sort' => [ qw(NO_SORT SORT_NATURAL SORT_PATH) ] ); our @EXPORT_OK = qw(NO_SORT SORT_NATURAL SORT_PATH); =head1 NAME Config::AST::Node - generic configuration syntax tree node =head1 SYNOPSIS use parent 'Config::AST::Node'; =head1 DESCRIPTION This is an abstract class representing a node in the configuration parse tree. A node can be either a non-leaf node, representing a I<section>, or a leaf node, representing a I<simple statement>. =head1 METHODS =head2 new(ARG => VAL, ...) Creates new object. Recognized arguments are: =over 4 =item B<clone =E<gt>> I<OBJ> Clone object I<OBJ>, which must be an instance of B<Config::AST::Node> or its derived class. =item B<default =E<gt>> I<VAL> Sets default value. =item B<locus =E<gt>> I<LOC> Sets the locus - an object of class B<Text::Locus>, which see. =item B<file =E<gt>> I<NAME> Sets the file name. =item B<order =E<gt>> I<N> Sets ordinal number. =back =cut sub new { my $class = shift; local %_ = @_; my $v; my $self; if ($v = delete $_{clone}) { $self = Clone::clone($v); } else { $self = bless { }, $class; } if (defined($v = delete $_{default})) { $self->default($v); } if (defined($v = delete $_{locus})) { $self->locus($v); } if (defined($v = delete $_{file})) { $self->locus($v, delete $_{line} // 0); } if (defined($v = delete $_{order})) { $self->order($v); } croak "unrecognized arguments" if keys(%_); return $self; } =head2 $x = $node->locus; Returns a locus associated with the node. =head2 $node->locus($LOC) =head2 $node->locus($FILE, $LINE) Associates a locus with the node. In the second form, a new locus object is created for location I<$FILE>:I<$LINE>. =cut sub locus { my $self = shift; if (@_ == 1) { croak "bad argument type" unless ref($_[0]) eq 'Text::Locus'; $self->{_locus} = $_[0]; } elsif (@_ == 2) { $self->{_locus} = new Text::Locus(@_); } elsif (@_) { croak "bad number of arguments"; } return $self->{_locus} ||= new Text::Locus; } =head2 $x = $node->order =head2 $node->order(I<$N>) Returns or sets and returns ordinal number for the node. =cut sub order { my ($self, $val) = @_; if (defined($val)) { $self->{_order} = $val; } return $self->{_order} // 0; } =head2 $x = $node->default =head2 $node->default(I<$N>) Returns or sets and returns default value for the node. =cut sub default { my ($self, $val) = @_; if (defined($val)) { $self->{_default} = $val; } return $self->{_default}; } =head2 $node->is_leaf Returns true if node is a leaf node =cut sub is_leaf { 0 } =head2 $node->is_null Returns true if node is a null node =cut sub is_null { 0 } =head2 $node->is_section Returns true if node represents a section. =cut sub is_section { ! shift->is_leaf } =head2 $node->is_value Returns true if node represents a value (or statement). =cut sub is_value { shift->is_leaf } use constant { NO_SORT => sub { @_ }, SORT_NATURAL => sub { sort { $a->[1]->order <=> $b->[1]->order } @_ }, SORT_PATH => sub { sort { join('.',@{$a->[0]}) cmp join('.', @{$b->[0]}) } @_ } }; =head2 @array = $cfg->flatten() =head2 @array = $cfg->flatten(sort => $sort) Returns a I<flattened> representation of the configuration, as a list of pairs B<[ $path, $value ]>, where B<$path> is a reference to the variable pathname, and B<$value> is a B<Config::AST::Node::Value> object. The I<$sort> argument controls the ordering of the entries in the returned B<@array>. It is either a code reference suitable to pass to the Perl B<sort> function, or one of the following constants: =over 4 =item NO_SORT Don't sort the array. Statements will be placed in an apparently random order. =item SORT_NATURAL Preserve relative positions of the statements. Entries in the array will be in the same order as they appeared in the configuration file. This is the default. =item SORT_PATH Sort by pathname. =back These constants are not exported by default. You can either import the ones you need, or use the B<:sort> keyword to import them all, e.g.: use Config::AST::Node qw(:sort); @array = $node->flatten(sort => SORT_PATH); =cut sub flatten { my $self = shift; local %_ = @_; my $sort = delete($_{sort}) || SORT_NATURAL; my @ar; my $i; croak "unrecognized keyword arguments: ". join(',', keys %_) if keys %_; push @ar, [ [], $self ]; foreach my $elt (@ar) { next if $elt->[1]->is_value; while (my ($kw, $val) = each %{$elt->[1]->subtree}) { push @ar, [ [@{$elt->[0]}, $kw], $val ]; } } croak "sort must be a coderef" unless ref($sort) eq 'CODE'; shift @ar; # toss off first entry return &{$sort}(grep { $_->[1]->is_value } @ar); } =head2 $cfg->canonical(%args) Returns the canonical string representation of the configuration node. For value nodes, canonical representation is: QVAR=VALUE where QVAR is fully qualified variable name, and VALUE is the corresponding value. For sections, canonical representation is a list of canonical representations of the underlying nodes, delimited by newlines (or another character - see the description of the B<delim> argument, below). The list is sorted by QVAR in ascending lexicographical order. B<%args> are zero or more of the following keywords: =over 4 =item B<delim =E<gt> >I<STR> Use I<STR> to delimit statements, instead of the newline. =item B<locus =E<gt> 1> Prefix each statement with its location. =back =cut sub canonical { my $self = shift; local %_ = @_; my $delim; unless (defined($delim = delete $_{delim})) { $delim = "\n"; } my $prloc = delete $_{locus}; carp "unrecognized parameters: " . join(', ', keys(%_)) if (keys(%_)); return join $delim, map { ($prloc ? '[' . $_->[1]->locus . ']: ' : '') . join('.', map { if (/[\.="]/) { s/\"/\\"/; '"'.$_.'"' } else { $_ } } @{$_->[0]}) . "=" . Data::Dumper->new([scalar $_->[1]->value]) ->Useqq(1) ->Terse(1) ->Indent(0) ->Dump } $self->flatten(sort => SORT_PATH); } use overload bool => sub { 1 }, '""' => sub { shift->as_string }, eq => sub { my ($self,$other) = @_; return $self->as_string eq $other }; =head1 SEE ALSO B<Config::AST>, B<Config::AST::Node::Null>, B<Config::AST::Node::Value>, B<Config::AST::Node::Section>. =cut 1;