# ABSTRACT: Construct data structure from Parser Events use strict; use warnings; package YAML::PP::Constructor; our $VERSION = 'v0.38.0'; # VERSION use YAML::PP; use YAML::PP::Common qw/ PRESERVE_ORDER PRESERVE_SCALAR_STYLE PRESERVE_FLOW_STYLE PRESERVE_ALIAS /; use Scalar::Util qw/ reftype /; use Carp qw/ croak /; use constant DEBUG => ($ENV{YAML_PP_LOAD_DEBUG} or $ENV{YAML_PP_LOAD_TRACE}) ? 1 : 0; use constant TRACE => $ENV{YAML_PP_LOAD_TRACE} ? 1 : 0; my %cyclic_refs = qw/ allow 1 ignore 1 warn 1 fatal 1 /; sub new { my ($class, %args) = @_; my $default_yaml_version = delete $args{default_yaml_version}; my $duplicate_keys = delete $args{duplicate_keys}; unless (defined $duplicate_keys) { $duplicate_keys = 0; } my $preserve = delete $args{preserve} || 0; if ($preserve == 1) { $preserve = PRESERVE_ORDER | PRESERVE_SCALAR_STYLE | PRESERVE_FLOW_STYLE | PRESERVE_ALIAS; } my $cyclic_refs = delete $args{cyclic_refs} || 'fatal'; die "Invalid value for cyclic_refs: $cyclic_refs" unless $cyclic_refs{ $cyclic_refs }; my $schemas = delete $args{schemas}; if (keys %args) { die "Unexpected arguments: " . join ', ', sort keys %args; } my $self = bless { default_yaml_version => $default_yaml_version, schemas => $schemas, cyclic_refs => $cyclic_refs, preserve => $preserve, duplicate_keys => $duplicate_keys, }, $class; $self->init; return $self; } sub clone { my ($self) = @_; my $clone = { schemas => $self->{schemas}, schema => $self->{schema}, default_yaml_version => $self->{default_yaml_version}, cyclic_refs => $self->cyclic_refs, preserve => $self->{preserve}, }; return bless $clone, ref $self; } sub init { my ($self) = @_; $self->set_docs([]); $self->set_stack([]); $self->set_anchors({}); $self->set_yaml_version($self->default_yaml_version); $self->set_schema($self->schemas->{ $self->yaml_version } ); } sub docs { return $_[0]->{docs} } sub stack { return $_[0]->{stack} } sub anchors { return $_[0]->{anchors} } sub set_docs { $_[0]->{docs} = $_[1] } sub set_stack { $_[0]->{stack} = $_[1] } sub set_anchors { $_[0]->{anchors} = $_[1] } sub schemas { return $_[0]->{schemas} } sub schema { return $_[0]->{schema} } sub set_schema { $_[0]->{schema} = $_[1] } sub cyclic_refs { return $_[0]->{cyclic_refs} } sub set_cyclic_refs { $_[0]->{cyclic_refs} = $_[1] } sub yaml_version { return $_[0]->{yaml_version} } sub set_yaml_version { $_[0]->{yaml_version} = $_[1] } sub default_yaml_version { return $_[0]->{default_yaml_version} } sub preserve_order { return $_[0]->{preserve} & PRESERVE_ORDER } sub preserve_scalar_style { return $_[0]->{preserve} & PRESERVE_SCALAR_STYLE } sub preserve_flow_style { return $_[0]->{preserve} & PRESERVE_FLOW_STYLE } sub preserve_alias { return $_[0]->{preserve} & PRESERVE_ALIAS } sub duplicate_keys { return $_[0]->{duplicate_keys} } sub document_start_event { my ($self, $event) = @_; my $stack = $self->stack; if ($event->{version_directive}) { my $version = $event->{version_directive}; $version = "$version->{major}.$version->{minor}"; if ($self->{schemas}->{ $version }) { $self->set_yaml_version($version); $self->set_schema($self->schemas->{ $version }); } else { $self->set_yaml_version($self->default_yaml_version); $self->set_schema($self->schemas->{ $self->default_yaml_version }); } } my $ref = []; push @$stack, { type => 'document', ref => $ref, data => $ref, event => $event }; } sub document_end_event { my ($self, $event) = @_; my $stack = $self->stack; my $last = pop @$stack; $last->{type} eq 'document' or die "Expected mapping, but got $last->{type}"; if (@$stack) { die "Got unexpected end of document"; } my $docs = $self->docs; push @$docs, $last->{ref}->[0]; $self->set_anchors({}); $self->set_stack([]); } sub mapping_start_event { my ($self, $event) = @_; my ($data, $on_data) = $self->schema->create_mapping($self, $event); my $ref = { type => 'mapping', ref => [], data => \$data, event => $event, on_data => $on_data, }; my $stack = $self->stack; my $preserve_order = $self->preserve_order; my $preserve_style = $self->preserve_flow_style; my $preserve_alias = $self->preserve_alias; if (($preserve_order or $preserve_style or $preserve_alias) and not tied(%$data)) { tie %$data, 'YAML::PP::Preserve::Hash', %$data; } if ($preserve_style) { my $t = tied %$data; $t->{style} = $event->{style}; } push @$stack, $ref; if (defined(my $anchor = $event->{anchor})) { if ($preserve_alias) { my $t = tied %$data; unless (exists $self->anchors->{ $anchor }) { # Repeated anchors cannot be preserved $t->{alias} = $anchor; } } $self->anchors->{ $anchor } = { data => $ref->{data} }; } } sub mapping_end_event { my ($self, $event) = @_; my $stack = $self->stack; my $last = pop @$stack; my ($ref, $data) = @{ $last }{qw/ ref data /}; $last->{type} eq 'mapping' or die "Expected mapping, but got $last->{type}"; my @merge_keys; my @ref; for (my $i = 0; $i < @$ref; $i += 2) { my $key = $ref->[ $i ]; if (ref $key eq 'YAML::PP::Type::MergeKey') { my $merge = $ref->[ $i + 1 ]; if ((reftype($merge) || '') eq 'HASH') { push @merge_keys, $merge; } elsif ((reftype($merge) || '') eq 'ARRAY') { for my $item (@$merge) { if ((reftype($item) || '') eq 'HASH') { push @merge_keys, $item; } else { die "Expected hash for merge key"; } } } else { die "Expected hash or array for merge key"; } } else { push @ref, $key, $ref->[ $i + 1 ]; } } for my $merge (@merge_keys) { for my $key (keys %$merge) { unless (exists $$data->{ $key }) { $$data->{ $key } = $merge->{ $key }; } } } my $on_data = $last->{on_data} || sub { my ($self, $hash, $items) = @_; my %seen; for (my $i = 0; $i < @$items; $i += 2) { my ($key, $value) = @$items[ $i, $i + 1 ]; $key = '' unless defined $key; if (ref $key) { $key = $self->stringify_complex($key); } if ($seen{ $key }++ and not $self->duplicate_keys) { croak "Duplicate key '$key'"; } $$hash->{ $key } = $value; } }; $on_data->($self, $data, \@ref); push @{ $stack->[-1]->{ref} }, $$data; if (defined(my $anchor = $last->{event}->{anchor})) { $self->anchors->{ $anchor }->{finished} = 1; } return; } sub sequence_start_event { my ($self, $event) = @_; my ($data, $on_data) = $self->schema->create_sequence($self, $event); my $ref = { type => 'sequence', ref => [], data => \$data, event => $event, on_data => $on_data, }; my $stack = $self->stack; my $preserve_style = $self->preserve_flow_style; my $preserve_alias = $self->preserve_alias; if ($preserve_style or $preserve_alias and not tied(@$data)) { tie @$data, 'YAML::PP::Preserve::Array', @$data; my $t = tied @$data; $t->{style} = $event->{style}; } push @$stack, $ref; if (defined(my $anchor = $event->{anchor})) { if ($preserve_alias) { my $t = tied @$data; unless (exists $self->anchors->{ $anchor }) { # Repeated anchors cannot be preserved $t->{alias} = $anchor; } } $self->anchors->{ $anchor } = { data => $ref->{data} }; } } sub sequence_end_event { my ($self, $event) = @_; my $stack = $self->stack; my $last = pop @$stack; $last->{type} eq 'sequence' or die "Expected mapping, but got $last->{type}"; my ($ref, $data) = @{ $last }{qw/ ref data /}; my $on_data = $last->{on_data} || sub { my ($self, $array, $items) = @_; push @$$array, @$items; }; $on_data->($self, $data, $ref); push @{ $stack->[-1]->{ref} }, $$data; if (defined(my $anchor = $last->{event}->{anchor})) { my $test = $self->anchors->{ $anchor }; $self->anchors->{ $anchor }->{finished} = 1; } return; } sub stream_start_event {} sub stream_end_event {} sub scalar_event { my ($self, $event) = @_; DEBUG and warn "CONTENT $event->{value} ($event->{style})\n"; my $value = $self->schema->load_scalar($self, $event); my $last = $self->stack->[-1]; my $preserve_alias = $self->preserve_alias; my $preserve_style = $self->preserve_scalar_style; if (($preserve_style or $preserve_alias) and not ref $value) { my %args = ( value => $value, tag => $event->{tag}, ); if ($preserve_style) { $args{style} = $event->{style}; } if ($preserve_alias and defined $event->{anchor}) { my $anchor = $event->{anchor}; unless (exists $self->anchors->{ $anchor }) { # Repeated anchors cannot be preserved $args{alias} = $event->{anchor}; } } $value = YAML::PP::Preserve::Scalar->new( %args ); } if (defined (my $name = $event->{anchor})) { $self->anchors->{ $name } = { data => \$value, finished => 1 }; } push @{ $last->{ref} }, $value; } sub alias_event { my ($self, $event) = @_; my $value; my $name = $event->{value}; if (my $anchor = $self->anchors->{ $name }) { # We know this is a cyclic ref since the node hasn't # been constructed completely yet unless ($anchor->{finished} ) { my $cyclic_refs = $self->cyclic_refs; if ($cyclic_refs ne 'allow') { if ($cyclic_refs eq 'fatal') { croak "Found cyclic ref for alias '$name'"; } if ($cyclic_refs eq 'warn') { $anchor = { data => \undef }; warn "Found cyclic ref for alias '$name'"; } elsif ($cyclic_refs eq 'ignore') { $anchor = { data => \undef }; } } } $value = $anchor->{data}; } else { croak "No anchor defined for alias '$name'"; } my $last = $self->stack->[-1]; push @{ $last->{ref} }, $$value; } sub stringify_complex { my ($self, $data) = @_; return $data if ( ref $data eq 'YAML::PP::Preserve::Scalar' and ($self->preserve_scalar_style or $self->preserve_alias) ); require Data::Dumper; local $Data::Dumper::Quotekeys = 0; local $Data::Dumper::Terse = 1; local $Data::Dumper::Indent = 0; local $Data::Dumper::Useqq = 0; local $Data::Dumper::Sortkeys = 1; my $string = Data::Dumper->Dump([$data], ['data']); $string =~ s/^\$data = //; return $string; } 1; __END__ =pod =encoding utf-8 =head1 NAME YAML::PP::Constructor - Constructing data structure from parsing events =head1 METHODS =over =item new The Constructor constructor my $constructor = YAML::PP::Constructor->new( schema => $schema, cyclic_refs => $cyclic_refs, ); =item init Resets any data being used during construction. $constructor->init; =item document_start_event, document_end_event, mapping_start_event, mapping_end_event, sequence_start_event, sequence_end_event, scalar_event, alias_event, stream_start_event, stream_end_event These methods are called from L: $constructor->document_start_event($event); =item anchors, set_anchors Helper for storing anchors during construction =item docs, set_docs Helper for storing resulting documents during construction =item stack, set_stack Helper for storing data during construction =item cyclic_refs, set_cyclic_refs Option for controlling the behaviour when finding circular references =item schema, set_schema Holds a L object =item stringify_complex When constructing a hash and getting a non-scalar key, this method is used to stringify the key. It uses a terse Data::Dumper output. Other modules, like L, use the default stringification, C for example. =back =cut