Skip to content

Commit

Permalink
Fix directory import in stempler component (#1191)
Browse files Browse the repository at this point in the history
  • Loading branch information
spiralbot committed Jan 6, 2025
1 parent ae71b30 commit 5569e15
Show file tree
Hide file tree
Showing 7 changed files with 314 additions and 12 deletions.
10 changes: 2 additions & 8 deletions src/Node/Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,14 @@ final class Template implements NodeInterface, AttributedInterface
* @param list<TNode> $nodes
*/
public function __construct(
public array $nodes = []
) {
}
public array $nodes = [],
) {}

public function setContext(?Context $context = null): void
{
$this->context = $context;
}

public function getContext(): ?Context
{
return $this->context;
}

public function getIterator(): \Generator
{
yield 'nodes' => $this->nodes;
Expand Down
8 changes: 6 additions & 2 deletions src/Transform/Import/Bundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ final class Bundle implements ImportInterface
public function __construct(
private string $path,
private ?string $prefix = null,
?Context $context = null
?Context $context = null,
) {
$this->context = $context;
}
Expand All @@ -35,7 +35,11 @@ public function resolve(Builder $builder, string $name): ?Template

$path = $name;
if ($this->prefix !== null) {
$path = \substr($path, \strlen($this->prefix) + 1);
if (!TagHelper::hasPrefix($name, $this->prefix)) {
return null;
}

$path = TagHelper::stripPrefix($path, $this->prefix);
}

/** @var ImportInterface $import */
Expand Down
8 changes: 6 additions & 2 deletions src/Transform/Import/Directory.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,19 @@ final class Directory implements ImportInterface
public function __construct(
public string $path,
?string $prefix,
?Context $context = null
?Context $context = null,
) {
$this->prefix = $prefix ?? \substr($path, \strrpos($path, '/') + 1);
$this->context = $context;
}

public function resolve(Builder $builder, string $name): ?Template
{
$path = \substr($name, \strlen((string) $this->prefix) + 1);
if (!TagHelper::hasPrefix($name, $this->prefix)) {
return null;
}

$path = TagHelper::stripPrefix($name, $this->prefix);
$path = \str_replace('.', DIRECTORY_SEPARATOR, $path);

return $builder->load($this->path . DIRECTORY_SEPARATOR . $path);
Expand Down
49 changes: 49 additions & 0 deletions src/Transform/Import/TagHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Spiral\Stempler\Transform\Import;

final class TagHelper
{
private const SEPARATOR = [':', '.', '/'];

/**
* Validate tag against namespace.
*
* Example:
* - foo:bar
* - foo.bar
* - foo/bar
*/
public static function hasPrefix(string $tag, ?string $prefix): bool
{
// If no prefix is specified, allow everything
if ($prefix === null || $prefix === '') {
return true;
}

// The tag must be at least prefix + 2 chars:
// 1) The prefix itself
// 2) The separator
// 3) At least one more char after the separator
if (\strlen($tag) < \strlen($prefix) + 2) {
return false;
}

if (!\str_starts_with($tag, $prefix)) {
return false;
}

return \in_array($tag[\strlen($prefix)], self::SEPARATOR, true);
}

public static function stripPrefix(string $tag, ?string $prefix): string
{
if (!self::hasPrefix($tag, $prefix)) {
return $tag;
}

return \substr($tag, \strlen((string) $prefix) + 1);
}
}
131 changes: 131 additions & 0 deletions tests/Transform/Import/BundleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php

declare(strict_types=1);

namespace Spiral\Tests\Stempler\Transform\Import;

use Mockery as m;
use PHPUnit\Framework\Attributes\DataProvider;
use Spiral\Stempler\Builder;
use Spiral\Stempler\Loader\LoaderInterface;
use Spiral\Stempler\Loader\Source;
use Spiral\Stempler\Node\Template;
use Spiral\Stempler\Transform\Context\ImportContext;
use Spiral\Stempler\Transform\Import\Bundle;
use Spiral\Stempler\Transform\Import\ImportInterface;
use Spiral\Stempler\VisitorContext;
use Spiral\Stempler\VisitorInterface;
use Spiral\Tests\Stempler\Transform\BaseTestCase;

final class BundleTest extends BaseTestCase
{
use m\Adapter\Phpunit\MockeryPHPUnitIntegration;

public static function wrongNamespaceProvider(): iterable
{
yield ['span'];
yield ['abcd:span'];
yield ['test1:span'];
yield ['abc:span'];
yield ['tes:span'];
}

#[DataProvider('wrongNamespaceProvider')]
public function testResolveTagWithWrongNamespace(string $tag): void
{
$bundle = new Bundle('path/to/dir', 'test');
$loader = m::mock(LoaderInterface::class);

$loader
->shouldReceive('load')
->once()
->with('path/to/dir')
->andReturn(new Source('<span></span>'));

$builder = new Builder($loader);

$builder->addVisitor(
new class implements VisitorInterface {

public function enterNode(mixed $node, VisitorContext $ctx): mixed
{
$n = $ctx->getCurrentNode();
if ($n instanceof Template) {
$import = m::mock(ImportInterface::class);
$import->shouldNotReceive('resolve');
$n->setAttribute(ImportContext::class, [$import]);
}

return $node;
}

public function leaveNode(mixed $node, VisitorContext $ctx): mixed
{
return $node;
}
},
);

self::assertNull(
$bundle->resolve($builder, $tag),
);
}

public static function correctNamespaceProvider(): iterable
{
yield ['test.span'];
yield ['test:span'];
yield ['test/span'];
}

#[DataProvider('correctNamespaceProvider')]
public function testResolveTagWithCorrectNamespace(string $tag): void
{
$bundle = new Bundle('path/to/dir', 'test');
$loader = m::mock(LoaderInterface::class);

$loader
->shouldReceive('load')
->once()
->with('path/to/dir')
->andReturn(new Source('<span></span>'));

$builder = new Builder($loader);
$template = new Template();

$builder->addVisitor(
new class($builder, $template) implements VisitorInterface {
public function __construct(
private readonly Builder $builder,
private readonly Template $template,
) {}

public function enterNode(mixed $node, VisitorContext $ctx): mixed
{
$n = $ctx->getCurrentNode();
if ($n instanceof Template) {
$import = m::mock(ImportInterface::class);
$import
->shouldReceive('resolve')
->once()
->with($this->builder, 'span')
->andReturn($this->template);
$n->setAttribute(ImportContext::class, [$import]);
}

return $node;
}

public function leaveNode(mixed $node, VisitorContext $ctx): mixed
{
return $node;
}
},
);

self::assertSame(
$template,
$bundle->resolve($builder, $tag),
);
}
}
63 changes: 63 additions & 0 deletions tests/Transform/Import/DirectoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace Spiral\Tests\Stempler\Transform\Import;

use Mockery as m;
use PHPUnit\Framework\Attributes\DataProvider;
use Spiral\Stempler\Builder;
use Spiral\Stempler\Loader\LoaderInterface;
use Spiral\Stempler\Loader\Source;
use Spiral\Stempler\Transform\Import\Directory;
use Spiral\Tests\Stempler\Transform\BaseTestCase;

final class DirectoryTest extends BaseTestCase
{
use m\Adapter\Phpunit\MockeryPHPUnitIntegration;

public static function wrongNamespaceProvider(): iterable
{
yield ['span'];
yield ['abcd:span'];
yield ['test1:span'];
yield ['abc:span'];
yield ['tes:span'];
}

#[DataProvider('wrongNamespaceProvider')]
public function testResolveTagWithWrongNamespace(string $tag): void
{
$directory = new Directory('path/to/dir', 'test');

$loader = m::mock(LoaderInterface::class);
self::assertNull(
$directory->resolve(new Builder($loader), $tag),
);
}

public static function correctNamespaceProvider(): iterable
{
yield ['test.span'];
yield ['test:span'];
yield ['test/span'];
}

#[DataProvider('correctNamespaceProvider')]
public function testResolveTagWithCorrectNamespace(string $tag): void
{
$directory = new Directory('path/to/dir', 'test');

$loader = m::mock(LoaderInterface::class);

$loader
->shouldReceive('load')
->once()
->with($path = 'path/to/dir/span')
->andReturn(new Source('<span></span>'));

$template = $directory->resolve(new Builder($loader), $tag);

self::assertSame($path, $template->getContext()->getPath());
}
}
57 changes: 57 additions & 0 deletions tests/Transform/Import/TagHelperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace Spiral\Tests\Stempler\Transform\Import;

use Spiral\Stempler\Transform\Import\TagHelper;
use Spiral\Tests\Stempler\Transform\BaseTestCase;

final class TagHelperTest extends BaseTestCase
{
public function testHasPrefixWithNullPrefix(): void
{
self::assertTrue(TagHelper::hasPrefix('foo:bar', null));
self::assertTrue(TagHelper::hasPrefix('bar/foo', null));
}

public function testHasPrefixWithValidPrefix(): void
{
self::assertTrue(TagHelper::hasPrefix('foo:bar', 'foo'));
self::assertTrue(TagHelper::hasPrefix('foo.bar', 'foo'));
self::assertTrue(TagHelper::hasPrefix('foo/bar', 'foo'));
}

public function testHasPrefixWithInvalidPrefix(): void
{
self::assertFalse(TagHelper::hasPrefix('bar:foo', 'foo'));
self::assertFalse(TagHelper::hasPrefix('foobar', 'foo'));
self::assertFalse(TagHelper::hasPrefix('foo-bar', 'foo'));
}

public function testHasPrefixEdgeCases(): void
{
self::assertFalse(TagHelper::hasPrefix('foo', 'foo'));
self::assertFalse(TagHelper::hasPrefix('foo:', 'foo'));
}

public function testStripPrefixWithValidPrefix(): void
{
self::assertSame('bar', TagHelper::stripPrefix('foo:bar', 'foo'));
self::assertSame('bar', TagHelper::stripPrefix('foo.bar', 'foo'));
self::assertSame('bar', TagHelper::stripPrefix('foo/bar', 'foo'));
}

public function testStripPrefixWithInvalidPrefix(): void
{
self::assertSame('bar:foo', TagHelper::stripPrefix('bar:foo', 'foo'));
self::assertSame('foobar', TagHelper::stripPrefix('foobar', 'foo'));
self::assertSame('foo-bar', TagHelper::stripPrefix('foo-bar', 'foo'));
}

public function testStripPrefixEdgeCases(): void
{
self::assertSame('foo', TagHelper::stripPrefix('foo', 'foo'));
self::assertSame('foo:', TagHelper::stripPrefix('foo:', 'foo'));
}
}

0 comments on commit 5569e15

Please sign in to comment.