diff --git a/src/PostProcessor/FirestoreRequestParamProcessor.php b/src/PostProcessor/FirestoreRequestParamProcessor.php new file mode 100644 index 000000000..e7fcc527d --- /dev/null +++ b/src/PostProcessor/FirestoreRequestParamProcessor.php @@ -0,0 +1,129 @@ +startApiCall(\'Listen\', null, $callOptions);'; + + // Request param code to insert + private const DATABASE_REQUEST_PARAM_CODE = <<<'EOL' +$requestParamHeaders = []; +if (isset($callOptions['database'])) { + $requestParamHeaders['database'] = $callOptions['database']; +} +$requestParams = new \Google\ApiCore\RequestParamsHeaderDescriptor($requestParamHeaders); +$callOptions['headers'] = isset($callOptions['headers']) ? array_merge($requestParams->getHeader(), $callOptions['headers']) : $requestParams->getHeader(); +EOL; + + public static function run(string $inputDir): void + { + $firestoreClientFile = $inputDir . '/src/V1/Client/FirestoreClient.php'; + if (file_exists($firestoreClientFile)) { + 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"); + } + + /** + * @throws LogicException + * @throws ParseError + */ + public function addDatabaseRequestParamToListenMethod(): void + { + $contents = $this->classNode->getFileContents(); + $listenMethod = $this->getMethodDeclaration('listen'); + + // update PHPDoc + $phpdoc = $listenMethod->getDocCommentText(); + if (false === strpos($phpdoc, self::DATABASE_PHPDOC_PARAM)) { + $newLines = explode(PHP_EOL, self::DATABASE_PHPDOC_PARAM); + $newLines[] = self::DATABASE_PHPDOC_INSERT_AT; + + + $newPhpdoc = str_replace(self::DATABASE_PHPDOC_INSERT_AT, implode(PHP_EOL, $newLines), $phpdoc); + $contents = str_replace($phpdoc, $newPhpdoc, $contents); + } + + // Update param code + $methodText = $listenMethod->compoundStatementOrSemicolon->getText(); + // indent each line 8 spaces + $indent = str_repeat(' ', 8); + $newLines = array_map( + fn ($line) => $indent . $line, + explode(PHP_EOL, self::DATABASE_REQUEST_PARAM_CODE) + ); + if (false === strpos($methodText, $newLines[0])) { + $newLines[] = self::DATABASE_REQUEST_PARAM_INSERT_AT; + + $newMethodText = str_replace( + self::DATABASE_REQUEST_PARAM_INSERT_AT, + implode(PHP_EOL, $newLines), $methodText + ); + $contents = str_replace($methodText, $newMethodText, $contents); + } + + $this->classNode = self::fromCode($contents); + } + + 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 + ); + } +} diff --git a/src/PostProcessor/FragmentInjectionProcessor.php b/src/PostProcessor/FragmentInjectionProcessor.php index 3c1e1afcb..02da3a238 100644 --- a/src/PostProcessor/FragmentInjectionProcessor.php +++ b/src/PostProcessor/FragmentInjectionProcessor.php @@ -18,17 +18,14 @@ 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 FragmentInjectionProcessor implements ProcessorInterface { - private ClassDeclaration $classNode; + use PostProcessorTrait; public static function run(string $inputDir): void { @@ -62,28 +59,6 @@ private static function inject(string $fragmentFile, string $classFile): void 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 @@ -100,11 +75,6 @@ public function insert(string $newContent, ?string $insertBeforeMethod = null): $this->classNode = self::fromCode($contents); } - public function getContents(): string - { - return $this->classNode->getFileContents(); - } - private function getLineNumberFromPosition(int $startPosition): int { return PositionUtilities::getLineCharacterPositionFromPosition( diff --git a/src/PostProcessor/PostProcessor.php b/src/PostProcessor/PostProcessor.php index caa50d4a2..0610045b5 100644 --- a/src/PostProcessor/PostProcessor.php +++ b/src/PostProcessor/PostProcessor.php @@ -45,6 +45,9 @@ private function execute(): void private static function loadProcessors(array $opts): Vector { - return Vector::new([FragmentInjectionProcessor::class]); + return Vector::new([ + FragmentInjectionProcessor::class, + FirestoreRequestParamProcessor::class, + ]); } } diff --git a/src/PostProcessor/PostProcessorTrait.php b/src/PostProcessor/PostProcessorTrait.php new file mode 100644 index 000000000..9c8021662 --- /dev/null +++ b/src/PostProcessor/PostProcessorTrait.php @@ -0,0 +1,59 @@ +classNode = self::fromCode($contents); + } + + public function getContents(): string + { + return $this->classNode->getFileContents(); + } + + public static abstract function run(string $inputDir): void; + + 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'); + } +} \ No newline at end of file diff --git a/tests/Unit/PostProcessor/FirestoreRequestParamProcessorTest.php b/tests/Unit/PostProcessor/FirestoreRequestParamProcessorTest.php new file mode 100644 index 000000000..e45825c4c --- /dev/null +++ b/tests/Unit/PostProcessor/FirestoreRequestParamProcessorTest.php @@ -0,0 +1,103 @@ + 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 $database', $newClassContents); + $this->assertStringContainsString('new \Google\ApiCore\RequestParamsHeaderDescriptor', $newClassContents); + + // Insert again and ensure it's not added twice + $firestorePostProcessor->addDatabaseRequestParamToListenMethod(); + $newClassContents = $firestorePostProcessor->getContents(); + $this->assertEquals(1, substr_count($newClassContents, '@type string $database')); + $this->assertEquals(1, substr_count($newClassContents, 'new \Google\ApiCore\RequestParamsHeaderDescriptor')); + } + + public function testFirestoreRequestParamDoesNotContainSyntaxErrors() + { + $firestorePostProcessor = new FirestoreRequestParamProcessor(self::$classContents); + + $codeString = $firestorePostProcessor->getContents(); + + // This will throw a ParseError if the syntax is invalid + $tokens = token_get_all("assertNotNull($tokens); + } +}