Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/AcsfApi/AcsfConnectorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Acquia\Cli\AcsfApi;

use Acquia\Cli\ConnectorFactoryInterface;
use AcquiaCloudApi\Connector\ConnectorInterface;

class AcsfConnectorFactory implements ConnectorFactoryInterface
{
Expand All @@ -15,7 +16,7 @@ public function __construct(protected array $config, protected ?string $baseUri
{
}

public function createConnector(): AcsfConnector
public function createConnector(): ConnectorInterface
{
return new AcsfConnector($this->config, $this->baseUri);
}
Expand Down
21 changes: 17 additions & 4 deletions src/CloudApi/ConnectorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Acquia\Cli\ConnectorFactoryInterface;
use AcquiaCloudApi\Connector\Connector;
use AcquiaCloudApi\Connector\ConnectorInterface;
use League\OAuth2\Client\Token\AccessToken;

class ConnectorFactory implements ConnectorFactoryInterface
Expand All @@ -17,10 +18,22 @@ public function __construct(protected array $config, protected ?string $baseUri
{
}

/**
* @return \Acquia\Cli\CloudApi\AccessTokenConnector|\AcquiaCloudApi\Connector\Connector
*/
public function createConnector(): Connector|AccessTokenConnector
public function createConnector(): ConnectorInterface
{
$connector = $this->buildConnector();

// If the AH_CODEBASE_UUID environment variable is set, that means
// it's a MEO subscription. For MEO, we need to rewrite the API request
// path so that MEO-specific endpoints are used and the correct
// endpoint can be selected based on the codebase.
if (getenv('AH_CODEBASE_UUID')) {
return new PathRewriteConnector($connector);
}

return $connector;
}

private function buildConnector(): ConnectorInterface
{
// A defined key & secret takes priority.
if ($this->config['key'] && $this->config['secret']) {
Expand Down
131 changes: 131 additions & 0 deletions src/CloudApi/PathRewriteConnector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php

declare(strict_types=1);

namespace Acquia\Cli\CloudApi;

use AcquiaCloudApi\Connector\ConnectorInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
* Decorates a ConnectorInterface to rewrite specific API paths before sending
* requests. Useful for redirecting legacy or alternative API endpoints to new
* ones transparently.
*/
final class PathRewriteConnector implements ConnectorInterface
{
/**
* The underlying connector to which requests are delegated after path rewriting.
*/
private ConnectorInterface $inner;

/**
* PathRewriteConnector constructor.
*
* @param ConnectorInterface $inner The connector to decorate.
*/
public function __construct(
ConnectorInterface $inner,
) {
$this->inner = $inner;
}

/**
* Creates a PSR-7 request, rewriting the path if it matches a rewrite rule.
*
* @param string $verb HTTP method (e.g., 'GET', 'POST').
* @param string $path The original API path.
* @return RequestInterface The PSR-7 request with possibly rewritten path.
*/
public function createRequest(string $verb, string $path): RequestInterface
{
return $this->inner->createRequest($verb, $this->rewritePath($path));
}

/**
* Sends an HTTP request, rewriting the path if it matches a rewrite rule.
*
* @param string $verb HTTP method (e.g., 'GET', 'POST').
* @param string $path The original API path.
* @param array<string, mixed> $options Additional request options.
* @return ResponseInterface The HTTP response.
*/
public function sendRequest(string $verb, string $path, array $options): ResponseInterface
{
return $this->inner->sendRequest($verb, $this->rewritePath($path), $options);
}

/**
* Returns the base URI for the API.
*
* @return string The base URI.
*/
public function getBaseUri(): string
{
return $this->inner->getBaseUri();
}

/**
* Returns the access token for URL authentication.
*
* @return string The access token.
*/
public function getUrlAccessToken(): string
{
return $this->inner->getUrlAccessToken();
}

/**
* Rewrites the API path using preg_replace if it matches any rewrite rule.
*
* @param string $path The original API path.
* @return string The rewritten path, or the original if no rule matches.
*/
private function rewritePath(string $path): string
{
foreach ($this->getPathsToRewrite() as $pattern => $replacement) {
if (preg_match($pattern, $path) === 1) {
return (string) preg_replace($pattern, $replacement, $path);
}
}

// Return the original path if no rewrite rule matches.
return $path;
}

/**
* Returns an array of regex patterns and their corresponding replacement paths for rewriting API request paths.
*
* Two rules cover all cases:
* - Paths with a trailing segment: /applications/{uuid}/foo/bar → /translation/codebases/{codebaseUuid}/foo/bar
* - Bare application UUID paths: /applications/{uuid} → /translation/codebases/{codebaseUuid}
*
* The first rule uses a capture group ($1) so any trailing path is preserved automatically,
* avoiding the need to enumerate every possible sub-path.
*
* @return array<string, string> Regex pattern => preg_replace replacement string.
*/
private function getPathsToRewrite(): array
{
$codebaseUuid = $this->getCodeBaseUuid();
return [
// Matches bare /applications/{uuid} with no trailing segment.
'#^/applications/[0-9a-f\-]+$#i' => '/translation/codebases/' . $codebaseUuid,
// Matches /applications/{uuid}/{anything} and preserves the trailing segment via $1.
'#^/applications/[0-9a-f\-]+/(.+)$#i' => '/translation/codebases/' . $codebaseUuid . '/$1',
];
}

/**
* Retrieves the codebase UUID.
*/
private function getCodeBaseUuid(): string
{
$codebaseUuid = getenv('AH_CODEBASE_UUID');
if (!$codebaseUuid) {
throw new \RuntimeException('Environment variable AH_CODEBASE_UUID is not set.');
}
return $codebaseUuid;
}
}
5 changes: 2 additions & 3 deletions src/ConnectorFactoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@

namespace Acquia\Cli;

use Acquia\Cli\CloudApi\AccessTokenConnector;
use AcquiaCloudApi\Connector\Connector;
use AcquiaCloudApi\Connector\ConnectorInterface;

interface ConnectorFactoryInterface
{
public function createConnector(): Connector|AccessTokenConnector;
public function createConnector(): ConnectorInterface;
}
77 changes: 77 additions & 0 deletions tests/phpunit/src/CloudApi/ConnectorFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace Acquia\Cli\Tests\CloudApi;

use Acquia\Cli\CloudApi\ConnectorFactory;
use Acquia\Cli\CloudApi\PathRewriteConnector;
use AcquiaCloudApi\Connector\Connector;
use PHPUnit\Framework\TestCase;

/**
* @covers \Acquia\Cli\CloudApi\ConnectorFactory
*
* Unit tests for the ConnectorFactory. Ensures that the factory returns the correct
* connector type depending on the presence of the AH_CODEBASE_UUID environment variable.
*/
class ConnectorFactoryTest extends TestCase
{
/**
* Stores the original value of AH_CODEBASE_UUID to restore after each test.
*/
private string|false $originalEnv;

/**
* Saves the original environment variable before each test.
*/
protected function setUp(): void
{
parent::setUp();
$this->originalEnv = getenv('AH_CODEBASE_UUID');
}


/**
* @dataProvider connectorFactoryProvider
*/
public function testCreateConnectorFactoryBehavior(?string $envValue, string $expectedClass): void
{
if ($envValue !== null) {
putenv("AH_CODEBASE_UUID=$envValue");
} else {
putenv('AH_CODEBASE_UUID');
}
$factory = new ConnectorFactory(['key' => 'k', 'secret' => 's'], 'https://api.example.com');
$connector = $factory->createConnector();
$this->assertInstanceOf($expectedClass, $connector);
}

/**
* Data provider for testCreateConnectorFactoryBehavior() test.
*
* @return array<int, array{0: string|null, 1: class-string}>
*/
public static function connectorFactoryProvider(): array
{
return [
// Env set: should return PathRewriteConnector.
['1234-5678-uuid', PathRewriteConnector::class],
// Env not set: should return Connector.
[null, Connector::class],
];
}

/**
* Restores the original environment variable after each test.
*/
protected function tearDown(): void
{
if ($this->originalEnv === false) {
putenv('AH_CODEBASE_UUID');
} else {
putenv('AH_CODEBASE_UUID=' . $this->originalEnv);
}
parent::tearDown();
}
}
Loading
Loading