Initial commit
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Behat\Gherkin\Loader;
|
||||
|
||||
use Behat\Gherkin\Exception\NodeException;
|
||||
use Behat\Gherkin\Node\ArgumentInterface;
|
||||
use Behat\Gherkin\Node\BackgroundNode;
|
||||
use Behat\Gherkin\Node\ExampleTableNode;
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
use Behat\Gherkin\Node\OutlineNode;
|
||||
use Behat\Gherkin\Node\PyStringNode;
|
||||
use Behat\Gherkin\Node\ScenarioInterface;
|
||||
use Behat\Gherkin\Node\ScenarioNode;
|
||||
use Behat\Gherkin\Node\StepNode;
|
||||
use Behat\Gherkin\Node\TableNode;
|
||||
|
||||
/**
|
||||
* Loads a feature from cucumber's messages JSON format.
|
||||
*
|
||||
* Lines in the ndjson file are expected to match the Cucumber Messages JSON schema defined at https://github.com/cucumber/messages/tree/main/jsonschema
|
||||
*
|
||||
* @phpstan-type TLocation array{line: int, column?: int}
|
||||
* @phpstan-type TBackground array{location: TLocation, keyword: string, name: string, description: string, steps: list<TStep>, id: string}
|
||||
* @phpstan-type TComment array{location: TLocation, text: string}
|
||||
* @phpstan-type TDataTable array{location: TLocation, rows: list<TTableRow>}
|
||||
* @phpstan-type TDocString array{location: TLocation, content: string, delimiter: string, mediaType?: string}
|
||||
* @phpstan-type TExamples array{location: TLocation, tags: list<TTag>, keyword: string, name: string, description: string, tableHeader?: TTableRow, tableBody: list<TTableRow>, id: string}
|
||||
* @phpstan-type TFeature array{location: TLocation, tags: list<TTag>, language: string, keyword: string, name: string, description: string, children: list<TFeatureChild>}
|
||||
* @phpstan-type TFeatureChild array{background?: TBackground, scenario?: TScenario, rule?: TRule}
|
||||
* @phpstan-type TRule array{location: TLocation, tags: list<TTag>, keyword: string, name: string, description: string, children: list<TRuleChild>, id: string}
|
||||
* @phpstan-type TRuleChild array{background?: TBackground, scenario?: TScenario}
|
||||
* @phpstan-type TScenario array{location: TLocation, tags: list<TTag>, keyword: string, name: string, description: string, steps: list<TStep>, examples: list<TExamples>, id: string}
|
||||
* @phpstan-type TStep array{location: TLocation, keyword: string, keywordType?: 'Unknown'|'Context'|'Action'|'Outcome'|'Conjunction', text: string, docString?: TDocString, dataTable?: TDataTable, id: string}
|
||||
* @phpstan-type TTableCell array{location: TLocation, value: string}
|
||||
* @phpstan-type TTableRow array{location: TLocation, cells: list<TTableCell>, id: string}
|
||||
* @phpstan-type TTag array{location: TLocation, name: string, id: string}
|
||||
* @phpstan-type TGherkinDocument array{uri?: string, feature?: TFeature, comments: list<TComment>}
|
||||
* // We only care about the gherkinDocument messages for our use case, so this does not describe the envelope fully
|
||||
* @phpstan-type TEnvelope array{gherkinDocument?: TGherkinDocument, ...}
|
||||
*/
|
||||
class CucumberNDJsonAstLoader implements LoaderInterface
|
||||
{
|
||||
public function supports($resource)
|
||||
{
|
||||
return is_string($resource);
|
||||
}
|
||||
|
||||
public function load($resource)
|
||||
{
|
||||
return array_values(
|
||||
array_filter(
|
||||
array_map(
|
||||
static function ($line) use ($resource) {
|
||||
// As we load data from the official Cucumber project, we assume the data matches the JSON schema.
|
||||
return self::getFeature(json_decode($line, true, 512, \JSON_THROW_ON_ERROR), $resource);
|
||||
},
|
||||
file($resource)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param TEnvelope $json
|
||||
*/
|
||||
private static function getFeature(array $json, string $filePath): ?FeatureNode
|
||||
{
|
||||
if (!isset($json['gherkinDocument']['feature'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$featureJson = $json['gherkinDocument']['feature'];
|
||||
|
||||
return new FeatureNode(
|
||||
$featureJson['name'],
|
||||
$featureJson['description'],
|
||||
self::getTags($featureJson),
|
||||
self::getBackground($featureJson),
|
||||
self::getScenarios($featureJson),
|
||||
$featureJson['keyword'],
|
||||
$featureJson['language'],
|
||||
preg_replace('/(?<=\\.feature).*$/', '', $filePath),
|
||||
$featureJson['location']['line']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param array{tags: list<TTag>, ...} $json
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
private static function getTags(array $json): array
|
||||
{
|
||||
return array_map(
|
||||
static fn (array $tag) => preg_replace('/^@/', '', $tag['name']),
|
||||
$json['tags']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param TFeature $json
|
||||
*
|
||||
* @return list<ScenarioInterface>
|
||||
*/
|
||||
private static function getScenarios(array $json): array
|
||||
{
|
||||
return array_values(
|
||||
array_map(
|
||||
static function ($child) {
|
||||
$tables = self::getTables($child['scenario']['examples']);
|
||||
|
||||
if ($tables) {
|
||||
return new OutlineNode(
|
||||
$child['scenario']['name'],
|
||||
self::getTags($child['scenario']),
|
||||
self::getSteps($child['scenario']['steps']),
|
||||
$tables,
|
||||
$child['scenario']['keyword'],
|
||||
$child['scenario']['location']['line']
|
||||
);
|
||||
}
|
||||
|
||||
return new ScenarioNode(
|
||||
$child['scenario']['name'],
|
||||
self::getTags($child['scenario']),
|
||||
self::getSteps($child['scenario']['steps']),
|
||||
$child['scenario']['keyword'],
|
||||
$child['scenario']['location']['line']
|
||||
);
|
||||
},
|
||||
array_filter(
|
||||
$json['children'],
|
||||
static function ($child) {
|
||||
return isset($child['scenario']);
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param TFeature $json
|
||||
*/
|
||||
private static function getBackground(array $json): ?BackgroundNode
|
||||
{
|
||||
$backgrounds = array_filter(
|
||||
$json['children'],
|
||||
static fn ($child) => isset($child['background']),
|
||||
);
|
||||
|
||||
if (count($backgrounds) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$background = array_shift($backgrounds);
|
||||
|
||||
return new BackgroundNode(
|
||||
$background['background']['name'],
|
||||
self::getSteps($background['background']['steps']),
|
||||
$background['background']['keyword'],
|
||||
$background['background']['location']['line']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param list<TStep> $items
|
||||
*
|
||||
* @return list<StepNode>
|
||||
*/
|
||||
private static function getSteps(array $items): array
|
||||
{
|
||||
return array_map(
|
||||
static fn (array $item) => new StepNode(
|
||||
trim($item['keyword']),
|
||||
$item['text'],
|
||||
self::getStepArguments($item),
|
||||
$item['location']['line'],
|
||||
trim($item['keyword'])
|
||||
),
|
||||
$items
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param TStep $step
|
||||
*
|
||||
* @return list<ArgumentInterface>
|
||||
*/
|
||||
private static function getStepArguments(array $step): array
|
||||
{
|
||||
$args = [];
|
||||
|
||||
if (isset($step['docString'])) {
|
||||
$args[] = new PyStringNode(
|
||||
explode("\n", $step['docString']['content']),
|
||||
$step['docString']['location']['line'],
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($step['dataTable'])) {
|
||||
$table = [];
|
||||
foreach ($step['dataTable']['rows'] as $row) {
|
||||
$table[$row['location']['line']] = array_column($row['cells'], 'value');
|
||||
}
|
||||
$args[] = new TableNode($table);
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param list<TExamples> $items
|
||||
*
|
||||
* @return list<ExampleTableNode>
|
||||
*/
|
||||
private static function getTables(array $items): array
|
||||
{
|
||||
return array_map(
|
||||
static function ($tableJson): ExampleTableNode {
|
||||
$headerRow = $tableJson['tableHeader'] ?? null;
|
||||
$tableBody = $tableJson['tableBody'];
|
||||
|
||||
if ($headerRow === null && ($tableBody !== [])) {
|
||||
throw new NodeException(
|
||||
sprintf(
|
||||
'Table header is required when a table body is provided for the example on line %s.',
|
||||
$tableJson['location']['line'],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$table = [];
|
||||
if ($headerRow !== null) {
|
||||
$table[$headerRow['location']['line']] = array_column($headerRow['cells'], 'value');
|
||||
}
|
||||
|
||||
foreach ($tableBody as $bodyRow) {
|
||||
$table[$bodyRow['location']['line']] = array_column($bodyRow['cells'], 'value');
|
||||
}
|
||||
|
||||
return new ExampleTableNode(
|
||||
$table,
|
||||
$tableJson['keyword'],
|
||||
self::getTags($tableJson)
|
||||
);
|
||||
},
|
||||
$items
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user