diff --git a/docs/developers/extensions/index.rst b/docs/developers/extensions/index.rst index 77ac63ff6..2001e12aa 100644 --- a/docs/developers/extensions/index.rst +++ b/docs/developers/extensions/index.rst @@ -36,3 +36,4 @@ Some ways to extend the guides: structure templates text-roles + interlinks diff --git a/docs/developers/extensions/interlinks.rst b/docs/developers/extensions/interlinks.rst new file mode 100644 index 000000000..ade00c68c --- /dev/null +++ b/docs/developers/extensions/interlinks.rst @@ -0,0 +1,72 @@ +.. include:: /include.rst.txt + +.. _custom_interlink_resolvers: + +=================== +Interlink Resolvers +=================== + +Interlinks are external references resolved from inventory files. +The format is inspired by Sphinx intersphinx inventories and lets guides resolve +links like ``project-id:target`` against a configured inventory source. + +Implement a custom resolver +=========================== + +Create a class implementing +:php:class:`phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryLinkResolver`. + +The key method is ``resolveInventoryLink()``. It receives the parsed cross-reference +node and should return a :php:class:`phpDocumentor\Guides\ReferenceResolvers\Interlink\ResolvedInventoryLink` +when the target can be resolved, otherwise ``null``. + +Register your resolver in DI +============================ + +Register your service with the tag ``phpdoc.guides.interlink_resolver``. +The chained resolver collects all services with this tag. + +.. code-block:: php + :caption: your-extension/resources/config/your-extension.php + + services() + ->defaults() + ->autowire() + ->autoconfigure() + ->set(MyInventoryResolver::class) + ->tag('phpdoc.guides.interlink_resolver'); + }; + +Your resolver is then considered together with the built-in default repository. + +Disable the default repository +============================== + +If your extension should fully control interlink resolution, disable the built-in +``DefaultInventoryRepository`` by setting the parameter +``phpdoc.guides.interlink.default_repository.enabled`` to ``false``. + +.. code-block:: php + :caption: your-extension/resources/config/your-extension.php + + parameters() + ->set('phpdoc.guides.interlink.default_repository.enabled', false); + }; + +When disabled, the default repository reports no available inventories, so only +custom tagged resolvers participate in interlink resolution. diff --git a/docs/reference/restructuredtext/interlinks.rst b/docs/reference/restructuredtext/interlinks.rst new file mode 100644 index 000000000..9716d2927 --- /dev/null +++ b/docs/reference/restructuredtext/interlinks.rst @@ -0,0 +1,60 @@ +.. include:: /include.rst.txt + +========= +Interlinks +========= + +Interlinks let you reference documentation in other projects. +The feature is inspired by Sphinx intersphinx links: guides reads inventory data +for configured external projects and resolves references by inventory id. + +Configure external inventories +============================== + +Define one or more inventories in ``guides.xml``: + +.. code-block:: xml + :caption: guides.xml + + + + + + +The ``id`` is the interlink domain used in references. + +Use interlinks in reStructuredText +================================== + +Interlinks are used in reference-oriented text roles by prefixing the target with +``:``. + +Examples with ``:ref:`` +----------------------- + +.. code-block:: + + :ref:`t3coreapi:assets` + :ref:`Working with assets ` + +Examples with ``:doc:`` +----------------------- + +.. code-block:: + + :doc:`t3coreapi:ApiOverview/Assets/Index` + :doc:`Assets chapter ` + +Resolution behavior +=================== + +- The resolver first selects a repository that reports the requested inventory id. +- It then resolves the target in that inventory. +- If no repository provides the inventory id, a warning is logged and the link is not resolved. + +See also +======== + +- :ref:`Text Roles ` +- :ref:`custom_interlink_resolvers` + diff --git a/docs/reference/restructuredtext/text-roles.rst b/docs/reference/restructuredtext/text-roles.rst index 043b11aae..d54d236b2 100644 --- a/docs/reference/restructuredtext/text-roles.rst +++ b/docs/reference/restructuredtext/text-roles.rst @@ -6,6 +6,8 @@ Text Roles Text roles can be used to style content inline. Some text roles have advanced processing such as reference resolving. +For external project references using inventory ids, see :doc:`interlinks`. + You can also :ref:`add your own custom text roles `. Currently the following text roles are implemented: diff --git a/packages/guides/resources/config/guides.php b/packages/guides/resources/config/guides.php index 02e9657a3..5196a65b6 100644 --- a/packages/guides/resources/config/guides.php +++ b/packages/guides/resources/config/guides.php @@ -30,6 +30,7 @@ use phpDocumentor\Guides\ReferenceResolvers\EmailReferenceResolver; use phpDocumentor\Guides\ReferenceResolvers\ExternalReferenceResolver; use phpDocumentor\Guides\ReferenceResolvers\ImageReferenceResolverPreRender; +use phpDocumentor\Guides\ReferenceResolvers\Interlink\ChainedInventoryLinkResolver; use phpDocumentor\Guides\ReferenceResolvers\Interlink\DefaultInventoryLoader; use phpDocumentor\Guides\ReferenceResolvers\Interlink\DefaultInventoryRepository; use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryLoader; @@ -74,7 +75,8 @@ return static function (ContainerConfigurator $container): void { $container->parameters() - ->set('phpdoc.guides.base_template_paths', [__DIR__ . '/../../../guides/resources/template/html']); + ->set('phpdoc.guides.base_template_paths', [__DIR__ . '/../../../guides/resources/template/html']) + ->set('phpdoc.guides.interlink.default_repository.enabled', true); $container->services() ->defaults() @@ -129,8 +131,13 @@ ->set(DocumentNodeTraverser::class) - ->set(InventoryRepository::class, DefaultInventoryRepository::class) + ->set(DefaultInventoryRepository::class) ->arg('$inventoryConfigs', param('phpdoc.guides.inventories')) + ->arg('$enabled', param('phpdoc.guides.interlink.default_repository.enabled')) + ->tag('phpdoc.guides.interlink_resolver') + + ->set(InventoryRepository::class, ChainedInventoryLinkResolver::class) + ->arg('$repositories', tagged_iterator('phpdoc.guides.interlink_resolver')) ->set(InventoryLoader::class, DefaultInventoryLoader::class) diff --git a/packages/guides/src/ReferenceResolvers/Interlink/ChainedInventoryLinkResolver.php b/packages/guides/src/ReferenceResolvers/Interlink/ChainedInventoryLinkResolver.php new file mode 100644 index 000000000..2f9ab8909 --- /dev/null +++ b/packages/guides/src/ReferenceResolvers/Interlink/ChainedInventoryLinkResolver.php @@ -0,0 +1,107 @@ + */ + private array $cachedRepositories = []; + + /** @param iterable $repositories */ + public function __construct( + private readonly iterable $repositories, + ) { + } + + public function getLink(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): InventoryLink|null + { + return $this->resolveInventoryLink($node, $renderContext, $messages)?->getLink(); + } + + public function hasInventory(string $key): bool + { + return $this->findInventoryRepository($key) !== null; + } + + public function getInventory(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): Inventory|null + { + Deprecation::trigger( + 'phpDocumentor/guides', + 'https://github.com/phpDocumentor/guides/issues', + 'InventoryRepository::getInventory() is deprecated. Implement ' + . 'InventoryLinkResolver::resolveInventoryLink() for one-call interlink resolution.', + ); + + return $this->findInventoryRepository($node->getInterlinkDomain())?->getInventory($node, $renderContext, $messages); + } + + public function resolveInventoryLink( + CrossReferenceNode $node, + RenderContext $renderContext, + Messages $messages, + ): ResolvedInventoryLink|null { + $repository = $this->findInventoryRepository($node->getInterlinkDomain()); + if ($repository === null) { + $messages->addWarning( + new Message( + sprintf('Inventory with key %s not found. ', $node->getInterlinkDomain()), + array_merge($renderContext->getLoggerInformation(), $node->getDebugInformation()), + ), + ); + + return null; + } + + if ($repository instanceof InventoryLinkResolver) { + return $repository->resolveInventoryLink($node, $renderContext, $messages); + } + + $inventory = $repository->getInventory($node, $renderContext, $messages); + $link = $repository->getLink($node, $renderContext, $messages); + if ($inventory === null || $link === null) { + return null; + } + + return new ResolvedInventoryLink($inventory->getBaseUrl(), $link); + } + + private function findInventoryRepository(string $key): InventoryRepository|null + { + if (array_key_exists($key, $this->cachedRepositories)) { + return $this->cachedRepositories[$key]; + } + + foreach ($this->repositories as $repository) { + if ($repository->hasInventory($key)) { + $this->cachedRepositories[$key] = $repository; + + return $repository; + } + } + + $this->cachedRepositories[$key] = null; + + return null; + } +} diff --git a/packages/guides/src/ReferenceResolvers/Interlink/DefaultInventoryRepository.php b/packages/guides/src/ReferenceResolvers/Interlink/DefaultInventoryRepository.php index a1192877a..7994ffde9 100644 --- a/packages/guides/src/ReferenceResolvers/Interlink/DefaultInventoryRepository.php +++ b/packages/guides/src/ReferenceResolvers/Interlink/DefaultInventoryRepository.php @@ -13,6 +13,7 @@ namespace phpDocumentor\Guides\ReferenceResolvers\Interlink; +use Doctrine\Deprecations\Deprecation; use phpDocumentor\Guides\Nodes\Inline\CrossReferenceNode; use phpDocumentor\Guides\ReferenceResolvers\AnchorNormalizer; use phpDocumentor\Guides\ReferenceResolvers\Message; @@ -23,7 +24,7 @@ use function array_merge; use function sprintf; -final class DefaultInventoryRepository implements InventoryRepository +final class DefaultInventoryRepository implements InventoryLinkResolver { /** @var array */ private array $inventories = []; @@ -33,28 +34,58 @@ public function __construct( private readonly AnchorNormalizer $anchorNormalizer, private readonly InventoryLoader $inventoryLoader, array $inventoryConfigs, + private readonly bool $enabled = true, ) { foreach ($inventoryConfigs as $inventory) { - $this->inventories[$this->anchorNormalizer->reduceAnchor($inventory['id'])] = new Inventory($inventory['url'], $anchorNormalizer); + $this->inventories[$this->anchorNormalizer->reduceAnchor($inventory['id'])] + = new Inventory($inventory['url'], $anchorNormalizer); } } public function getLink(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): InventoryLink|null { - $inventory = $this->getInventory($node, $renderContext, $messages); - $group = $inventory?->getGroup($node, $renderContext, $messages); - - return $group?->getLink($node, $renderContext, $messages); + return $this->resolveInventoryLink($node, $renderContext, $messages)?->getLink(); } public function hasInventory(string $key): bool { + if (!$this->enabled) { + return false; + } + $reducedKey = $this->anchorNormalizer->reduceAnchor($key); return array_key_exists($reducedKey, $this->inventories); } public function getInventory(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): Inventory|null + { + Deprecation::trigger( + 'phpDocumentor/guides', + 'https://github.com/phpDocumentor/guides/issues', + 'InventoryRepository::getInventory() is deprecated. Implement ' + . 'InventoryLinkResolver::resolveInventoryLink() for one-call interlink resolution.', + ); + + return $this->findInventory($node, $renderContext, $messages); + } + + public function resolveInventoryLink( + CrossReferenceNode $node, + RenderContext $renderContext, + Messages $messages, + ): ResolvedInventoryLink|null { + $inventory = $this->findInventory($node, $renderContext, $messages); + $group = $inventory?->getGroup($node, $renderContext, $messages); + $link = $group?->getLink($node, $renderContext, $messages); + if ($inventory === null || $link === null) { + return null; + } + + return new ResolvedInventoryLink($inventory->getBaseUrl(), $link); + } + + private function findInventory(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): Inventory|null { $reducedKey = $this->anchorNormalizer->reduceAnchor($node->getInterlinkDomain()); if (!$this->hasInventory($reducedKey)) { diff --git a/packages/guides/src/ReferenceResolvers/Interlink/InventoryLinkResolver.php b/packages/guides/src/ReferenceResolvers/Interlink/InventoryLinkResolver.php new file mode 100644 index 000000000..019701c7d --- /dev/null +++ b/packages/guides/src/ReferenceResolvers/Interlink/InventoryLinkResolver.php @@ -0,0 +1,27 @@ +baseUrl; + } + + public function getLink(): InventoryLink + { + return $this->link; + } +} diff --git a/packages/guides/src/ReferenceResolvers/InterlinkReferenceResolver.php b/packages/guides/src/ReferenceResolvers/InterlinkReferenceResolver.php index 3bef72104..7003001f7 100644 --- a/packages/guides/src/ReferenceResolvers/InterlinkReferenceResolver.php +++ b/packages/guides/src/ReferenceResolvers/InterlinkReferenceResolver.php @@ -17,6 +17,7 @@ use phpDocumentor\Guides\Nodes\Inline\CrossReferenceNode; use phpDocumentor\Guides\Nodes\Inline\LinkInlineNode; use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode; +use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryLinkResolver; use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryRepository; use phpDocumentor\Guides\RenderContext; @@ -37,17 +38,29 @@ public function resolve(LinkInlineNode $node, RenderContext $renderContext, Mess return false; } - $inventory = $this->inventoryRepository->getInventory($node, $renderContext, $messages); - if ($inventory === null) { - return false; - } + if ($this->inventoryRepository instanceof InventoryLinkResolver) { + $resolvedInventoryLink = $this->inventoryRepository->resolveInventoryLink($node, $renderContext, $messages); + if ($resolvedInventoryLink === null) { + return false; + } - $link = $this->inventoryRepository->getLink($node, $renderContext, $messages); - if ($link === null) { - return false; + $baseUrl = $resolvedInventoryLink->getBaseUrl(); + $link = $resolvedInventoryLink->getLink(); + } else { + $inventory = $this->inventoryRepository->getInventory($node, $renderContext, $messages); + if ($inventory === null) { + return false; + } + + $link = $this->inventoryRepository->getLink($node, $renderContext, $messages); + if ($link === null) { + return false; + } + + $baseUrl = $inventory->getBaseUrl(); } - $node->setUrl($inventory->getBaseUrl() . $link->getPath()); + $node->setUrl($baseUrl . $link->getPath()); if ($node instanceof CompoundNode) { if (count($node->getChildren()) === 0) { $node->addChildNode(new PlainTextInlineNode($link->getTitle())); diff --git a/packages/guides/tests/unit/Interlink/ChainedInventoryLinkResolverTest.php b/packages/guides/tests/unit/Interlink/ChainedInventoryLinkResolverTest.php new file mode 100644 index 000000000..1143c3dde --- /dev/null +++ b/packages/guides/tests/unit/Interlink/ChainedInventoryLinkResolverTest.php @@ -0,0 +1,121 @@ +renderContext = $this->createMock(RenderContext::class); + $this->renderContext->method('getLoggerInformation')->willReturn([]); + } + + public function testResolvesAndCachesRepositoryLookup(): void + { + $node = new ReferenceNode('modindex', [], 'some-key'); + $messages = new Messages(); + $link = new InventoryLink('project', '1.0', 'path.html', 'Some title'); + + $repository = $this->createMock(InventoryLinkResolver::class); + assert($repository instanceof InventoryLinkResolver); + $repository->expects(self::once())->method('hasInventory')->with('some-key')->willReturn(true); + $repository->expects(self::exactly(2)) + ->method('resolveInventoryLink') + ->willReturn(new ResolvedInventoryLink('https://example.com/', $link)); + + $resolver = new ChainedInventoryLinkResolver([$repository]); + + $resolved = $resolver->resolveInventoryLink($node, $this->renderContext, $messages); + self::assertNotNull($resolved); + self::assertEquals('https://example.com/', $resolved->getBaseUrl()); + + $resolved = $resolver->resolveInventoryLink($node, $this->renderContext, $messages); + self::assertNotNull($resolved); + self::assertEquals('path.html', $resolved->getLink()->getPath()); + self::assertCount(0, $messages->getWarnings()); + } + + public function testAddsWarningWhenNoRepositoryMatchesDomain(): void + { + $node = new ReferenceNode('modindex', [], 'missing-domain'); + $messages = new Messages(); + + $repository = $this->createMock(InventoryRepository::class); + assert($repository instanceof InventoryRepository); + $repository->expects(self::once())->method('hasInventory')->with('missing-domain')->willReturn(false); + + $resolver = new ChainedInventoryLinkResolver([$repository]); + self::assertNull($resolver->resolveInventoryLink($node, $this->renderContext, $messages)); + self::assertCount(1, $messages->getWarnings()); + } + + public function testFallsBackToLegacyRepositoryInterface(): void + { + $node = new ReferenceNode('modindex', [], 'legacy'); + $messages = new Messages(); + + $anchorNormalizer = new NullAnchorNormalizer(); + $inventory = new Inventory('https://legacy.example/', $anchorNormalizer); + $group = new InventoryGroup($anchorNormalizer); + $group->addLink('modindex', new InventoryLink('project', '1.0', 'legacy.html', 'Legacy')); + $inventory->addGroup('std:label', $group); + + $repository = new class ($inventory) implements InventoryRepository { + public function __construct(private readonly Inventory $inventory) + { + } + + public function getLink(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): InventoryLink|null + { + return $this->inventory->getGroup($node, $renderContext, $messages)?->getLink($node, $renderContext, $messages); + } + + public function hasInventory(string $key): bool + { + return $key === 'legacy'; + } + + public function getInventory(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): Inventory|null + { + return $this->inventory; + } + }; + + $resolver = new ChainedInventoryLinkResolver([$repository]); + $resolved = $resolver->resolveInventoryLink($node, $this->renderContext, $messages); + + self::assertNotNull($resolved); + self::assertEquals('https://legacy.example/', $resolved->getBaseUrl()); + self::assertEquals('legacy.html', $resolved->getLink()->getPath()); + } +} diff --git a/packages/guides/tests/unit/Interlink/InventoryLoaderTest.php b/packages/guides/tests/unit/Interlink/InventoryLoaderTest.php index bc1120ee0..4a3194b27 100644 --- a/packages/guides/tests/unit/Interlink/InventoryLoaderTest.php +++ b/packages/guides/tests/unit/Interlink/InventoryLoaderTest.php @@ -22,6 +22,7 @@ use phpDocumentor\Guides\ReferenceResolvers\Interlink\DefaultInventoryRepository; use phpDocumentor\Guides\ReferenceResolvers\Interlink\Inventory; use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryLink; +use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryLinkResolver; use phpDocumentor\Guides\ReferenceResolvers\Interlink\JsonLoader; use phpDocumentor\Guides\ReferenceResolvers\Messages; use phpDocumentor\Guides\ReferenceResolvers\SluggerAnchorNormalizer; @@ -73,10 +74,11 @@ public function loadObjectsJsonInv(string $filename): void public function testInventoryLoaderLoadsInventory(): void { - $node = new DocReferenceNode('SomeDocument', [], 'somekey'); - $inventory = $this->inventoryRepository->getInventory($node, $this->renderContext, new Messages()); - self::assertTrue($inventory instanceof Inventory); - self::assertGreaterThan(1, count($inventory->getGroups())); + $node = new ReferenceNode('modindex', [], 'somekey'); + self::assertInstanceOf(InventoryLinkResolver::class, $this->inventoryRepository); + $resolved = $this->inventoryRepository->resolveInventoryLink($node, $this->renderContext, new Messages()); + self::assertNotNull($resolved); + self::assertNotSame('', $resolved->getBaseUrl()); } public function testInventoryIsLoadedExactlyOnce(): void @@ -93,10 +95,11 @@ public function testInventoryIsLoadedExactlyOnce(): void public function testInventoryLoaderAcceptsNull(): void { $this->loadObjectsJsonInv(__DIR__ . '/fixtures/null-in-objects.inv.json'); - $node = new DocReferenceNode('SomeDocument', [], 'somekey'); - $inventory = $this->inventoryRepository->getInventory($node, $this->renderContext, new Messages()); - self::assertTrue($inventory instanceof Inventory); - self::assertGreaterThan(1, count($inventory->getGroups())); + $node = new ReferenceNode('modindex', [], 'somekey'); + self::assertInstanceOf(InventoryLinkResolver::class, $this->inventoryRepository); + $resolved = $this->inventoryRepository->resolveInventoryLink($node, $this->renderContext, new Messages()); + self::assertNotNull($resolved); + self::assertNotSame('', $resolved->getBaseUrl()); } #[DataProvider('rawAnchorProvider')] @@ -179,4 +182,17 @@ public static function notFoundInventoryProvider(): Generator new DocReferenceNode('Page1-Subpage1', [], 'somekey'), ]; } + + public function testDisabledRepositoryNeverClaimsInventory(): void + { + $disabledRepository = new DefaultInventoryRepository( + new SluggerAnchorNormalizer(), + $this->inventoryLoader, + [['id' => 'somekey', 'url' => 'https://example.com/']], + false, + ); + + self::assertFalse($disabledRepository->hasInventory('somekey')); + self::assertFalse($disabledRepository->hasInventory('some-key')); + } } diff --git a/packages/guides/tests/unit/ReferenceResolvers/InterlinkReferenceResolverTest.php b/packages/guides/tests/unit/ReferenceResolvers/InterlinkReferenceResolverTest.php index fe256b252..5f2316691 100644 --- a/packages/guides/tests/unit/ReferenceResolvers/InterlinkReferenceResolverTest.php +++ b/packages/guides/tests/unit/ReferenceResolvers/InterlinkReferenceResolverTest.php @@ -13,10 +13,13 @@ namespace phpDocumentor\Guides\ReferenceResolvers; +use phpDocumentor\Guides\Nodes\Inline\CrossReferenceNode; use phpDocumentor\Guides\Nodes\Inline\DocReferenceNode; use phpDocumentor\Guides\ReferenceResolvers\Interlink\Inventory; use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryLink; +use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryLinkResolver; use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryRepository; +use phpDocumentor\Guides\ReferenceResolvers\Interlink\ResolvedInventoryLink; use phpDocumentor\Guides\RenderContext; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; @@ -25,16 +28,12 @@ final class InterlinkReferenceResolverTest extends TestCase { private RenderContext&MockObject $renderContext; - private MockObject&InventoryRepository $inventoryRepository; - private InterlinkReferenceResolver $subject; private AnchorNormalizer $anchorNormalizer; protected function setUp(): void { $this->renderContext = $this->createMock(RenderContext::class); - $this->inventoryRepository = $this->createMock(InventoryRepository::class); $this->anchorNormalizer = new NullAnchorNormalizer(); - $this->subject = new InterlinkReferenceResolver($this->inventoryRepository); } #[DataProvider('pathProvider')] @@ -43,10 +42,50 @@ public function testDocumentReducer(string $expected, string $input, string $pat $input = new DocReferenceNode($input, [], 'interlink-target'); $inventoryLink = new InventoryLink('project', '1.0', $path, ''); $inventory = new Inventory('base-url/', $this->anchorNormalizer); - $this->inventoryRepository->expects(self::once())->method('getInventory')->willReturn($inventory); - $this->inventoryRepository->expects(self::once())->method('getLink')->willReturn($inventoryLink); + + $inventoryRepository = new class ($inventory, $inventoryLink) implements InventoryRepository { + public function __construct(private readonly Inventory $inventory, private readonly InventoryLink $inventoryLink) + { + } + + public function getLink(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): InventoryLink|null + { + return $this->inventoryLink; + } + + public function hasInventory(string $key): bool + { + return true; + } + + public function getInventory(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): Inventory|null + { + return $this->inventory; + } + }; + + $subject = new InterlinkReferenceResolver($inventoryRepository); $messages = new Messages(); - self::assertTrue($this->subject->resolve($input, $this->renderContext, $messages)); + self::assertTrue($subject->resolve($input, $this->renderContext, $messages)); + self::assertEmpty($messages->getWarnings()); + self::assertEquals($expected, $input->getUrl()); + } + + #[DataProvider('pathProvider')] + public function testDocumentReducerUsesOneCallResolver(string $expected, string $input, string $path): void + { + $input = new DocReferenceNode($input, [], 'interlink-target'); + $inventoryLink = new InventoryLink('project', '1.0', $path, ''); + + $inventoryRepository = $this->createMock(InventoryLinkResolver::class); + $inventoryRepository->expects(self::once()) + ->method('resolveInventoryLink') + ->willReturn(new ResolvedInventoryLink('base-url/', $inventoryLink)); + + $subject = new InterlinkReferenceResolver($inventoryRepository); + $messages = new Messages(); + + self::assertTrue($subject->resolve($input, $this->renderContext, $messages)); self::assertEmpty($messages->getWarnings()); self::assertEquals($expected, $input->getUrl()); }