Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
131 changes: 131 additions & 0 deletions src/PostProcessor/FirestoreRequestParamProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare(strict_types=1);

namespace Google\PostProcessor;

use Microsoft\PhpParser\Node\Statement\ClassDeclaration;
use Microsoft\PhpParser\Node\MethodDeclaration;
use Microsoft\PhpParser\Parser;
use Microsoft\PhpParser\PositionUtilities;
use Microsoft\PhpParser\DiagnosticsProvider;
use LogicException;
use ParseError;

class FirestoreRequestParamProcessor implements ProcessorInterface
{
private ClassDeclaration $classNode;

public static function run(string $inputDir): void
{
$firestoreClientFile = $inputDir . '/src/V1/Client/FirestoreClient.php';
self::inject($firestoreClientFile);
}

private static function inject(string $classFile): void
{
// The class to update
$classContent = file_get_contents($classFile);
$processor = new FirestoreRequestParamProcessor($classContent);

$processor->addDatabaseRequestParamToListenMethod();

// Write the new contents to the class file.
file_put_contents($classFile, $processor->getContents());
print("Fragment written to $classFile\n");
}

private static function fromCode(string $contents): ClassDeclaration
{
$parser = new Parser();
$astNode = $parser->parseSourceFile($contents);
if ($errors = DiagnosticsProvider::getDiagnostics($astNode)) {
throw new ParseError('Provided contents contains a PHP syntax error');
}

foreach ($astNode->getDescendantNodes() as $childNode) {
if ($childNode instanceof ClassDeclaration) {
return $childNode;
}
}

throw new LogicException('Provided contents does not contain a PHP class');
}

public function __construct(string $contents)
{
$this->classNode = self::fromCode($contents);
}

/**
* @throws LogicException
* @throws ParseError
*/
public function addDatabaseRequestParamToListenMethod(): void
{
$listenMethod = $this->getMethodDeclaration('listen');

// update PHPDoc
$lineToReplace = ' * @type int $timeoutMillis';
$newLines = [
' * @type string $datbase',
' * Set the database of the call, to be added as a routing header',
$lineToReplace,
];

$phpdoc = $listenMethod->getDocCommentText();
$newPhpdoc = str_replace($lineToReplace, implode(PHP_EOL, $newLines), $phpdoc);
$newContents = str_replace($phpdoc, $newPhpdoc, $this->classNode->getFileContents());

// update listen method
$lineToReplace = ' return $this->startApiCall(\'Listen\', null, $callOptions);';
$newLines = [
' $requestParamHeaders = [];',
' if (isset($callOptions[\'database\'])) {',
' $requestParamHeaders[\'database\'] = $callOptions[\'database\'];',
' }',
' $requestParams = new \Google\ApiCore\RequestParamsHeaderDescriptor($requestParamHeaders);',
' $callOptions[\'headers\'] = isset($optionalArgs[\'headers\']) ? array_merge($requestParams->getHeader(), $callOptions[\'headers\']) : $requestParams->getHeader();',
$lineToReplace,
];
$methodText = $listenMethod->compoundStatementOrSemicolon->getText();
$newMethodText = str_replace($lineToReplace, implode(PHP_EOL, $newLines), $methodText);
$newContents = str_replace($methodText, $newMethodText, $newContents);

$this->classNode = self::fromCode($newContents);
}

public function getContents(): string
{
return $this->classNode->getFileContents();
}

private function getMethodDeclaration(string $insertBeforeMethod): MethodDeclaration
{
foreach ($this->classNode->getDescendantNodes() as $childNode) {
if ($childNode instanceof MethodDeclaration) {
if ($childNode->getName() === $insertBeforeMethod) {
return $childNode;
}
}
}

throw new LogicException(
'Provided contents does not contain method ' . $insertBeforeMethod
);
}
}
104 changes: 104 additions & 0 deletions tests/Unit/PostProcessor/FirestoreRequestParamProcessorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare(strict_types=1);

namespace Google\Generator\Tests\Unit\PostProcessor;

use PHPUnit\Framework\TestCase;
use Google\PostProcessor\FirestoreRequestParamProcessor;
use ParseError;

final class FirestoreRequestParamProcessorTest extends TestCase
{
private static $classContents = <<<EOL
<?php
namespace Google\Cloud\Firestore\V1\Client;

final class FirestoreClient
{
use GapicClientTrait;

/** The name of the service. */
private const SERVICE_NAME = 'google.firestore.v1.Firestore';

private static function getClientDefaults()
{
return [
'serviceName' => self::SERVICE_NAME,
];
}

public function __construct(array \$options = [])
{
\$clientOptions = \$this->buildClientOptions(\$options);
\$this->setClientOptions(\$clientOptions);
}

/**
* Listens to changes. This method is only available via gRPC or WebChannel
* (not REST).
*
* @example samples/V1/FirestoreClient/listen.php
*
* @param array \$callOptions {
* Optional.
*
* @type int \$timeoutMillis
* Timeout to use for this call.
* }
*
* @return BidiStream
*
* @throws ApiException Thrown if the API call fails.
*/
public function listen(array \$callOptions = []): BidiStream
{
return \$this->startApiCall('Listen', null, \$callOptions);
}
}
EOL;

public function testFirestoreRequestParamProcessor()
{
$firestorePostProcessor = new FirestoreRequestParamProcessor(self::$classContents);

// Insert the function before this one
$firestorePostProcessor->addDatabaseRequestParamToListenMethod();
$newClassContents = $firestorePostProcessor->getContents();

$this->assertStringContainsString('@type string $datbase', $newClassContents);
$this->assertStringContainsString('new \Google\ApiCore\RequestParamsHeaderDescriptor', $newClassContents);
}

public function testFirestoreRequestParamDoesNotContainSyntaxErrors()
{
$firestorePostProcessor = new FirestoreRequestParamProcessor(self::$classContents);

$codeString = $firestorePostProcessor->getContents();
$tempFile = tempnam(sys_get_temp_dir(), 'phpunit_check_syntax_');
file_put_contents($tempFile, $codeString);

$command = 'php -l ' . escapeshellarg($tempFile);
$output = [];
$returnVar = 0;
exec($command, $output, $returnVar);

$this->assertEquals(0, $returnVar, 'The code output contains a syntax error');

unlink($tempFile); // Clean up the temporary file
}
}