DBIO

 view release on metacpan or  search on metacpan

lib/DBIO/Storage/Composed.pm  view on Meta::CPAN



sub compose {
  my ($class, $base, $layers) = @_;
  $layers ||= [];
  my @layers = @$layers;

  return $base unless @layers;

  my $pkg = 'DBIO::Storage::Composed::'
    . join('__', map { (my $s = $_) =~ s/::/_/g; $s } @layers, $base);

  return $pkg if $COMPOSED{$pkg};

  Class::C3::Componentised->ensure_class_loaded($base);
  Class::C3::Componentised->ensure_class_loaded($_) for @layers;

  $class->_assert_no_layer_collisions(\@layers);

  {
    no strict 'refs';
    @{"${pkg}::ISA"} = (@layers, $base);
  }
  mro::set_mro($pkg, 'c3');

  $COMPOSED{$pkg} = { base => $base, layers => [@layers] };

  return $pkg;
}


sub composition_of {
  my ($class, $pkg) = @_;
  return undef unless defined $pkg;
  return $COMPOSED{$pkg};
}


sub layers_of {
  my ($class, $pkg) = @_;
  my $entry = $class->composition_of($pkg) or return;
  return @{ $entry->{layers} };
}


sub recompose {
  my ($class, $pkg, $new_base) = @_;
  my $entry = $class->composition_of($pkg)
    or DBIO::Exception->throw(
      "recompose: '$pkg' is not a composed storage class"
    );
  return $class->compose($new_base, $entry->{layers});
}

# The method names a package defines IN ITSELF -- CODE slots whose compiled body
# actually originates in $pkg, not aliased/imported subs and not inherited ones.
# Standard phasers and OO plumbing are never "layer methods" for collision
# purposes, so they are excluded (two layers each having a BEGIN block, or an
# imported croak, is not a method collision).
my %NOT_A_METHOD = map { $_ => 1 }
  qw(BEGIN END INIT CHECK UNITCHECK DESTROY AUTOLOAD import unimport);

sub _own_methods {
  my ($class, $pkg) = @_;

  my @names;
  no strict 'refs';
  my $stash = \%{"${pkg}::"};
  for my $name (keys %$stash) {
    next if $name =~ /::\z/;          # nested stash, not a sub
    next if $NOT_A_METHOD{$name};
    my $code = *{"${pkg}::${name}"}{CODE};
    next unless defined $code;
    # Only count a sub whose compiled body originates in $pkg -- skips imports
    # and re-exports that merely alias into the stash.
    my $origin = B::svref_2object($code)->GV->STASH->NAME;
    next unless $origin eq $pkg;
    push @names, $name;
  }
  return @names;
}

sub _assert_no_layer_collisions {
  my ($class, $layers) = @_;

  my %defined_in;   # method name => [ packages that define it ]
  for my $layer (@$layers) {
    push @{ $defined_in{$_} }, $layer for $class->_own_methods($layer);
  }

  my @collisions = grep { @{ $defined_in{$_} } > 1 } sort keys %defined_in;
  return unless @collisions;

  my $detail = join '; ', map {
    "$_ (defined in " . join(', ', @{ $defined_in{$_} }) . ')'
  } @collisions;

  DBIO::Exception->throw(
    "storage layer method collision: $detail -- two or more storage layers "
  . 'define the same method; rename or merge them so only one layer owns it, '
  . 'or override the base method in a single layer and let the others chain '
  . 'via next::method'
  );
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

DBIO::Storage::Composed - Runtime C3 composition of storage extension layers over a base storage class

=head1 VERSION

version 0.900002



( run in 1.311 second using v1.01-cache-2.11-cpan-3fabe0161c3 )