Commit 54836f2a authored by yosymfony's avatar yosymfony
Browse files

Fixed the bug #23: "Unable to parse ArrayTables that contain Tables"

parent 8d67c2c9
CHANGELOG
=========
1.0.2
-----
* Fixed the bug #23: "Unable to parse ArrayTables that contain Tables".
* A new class `KeyStore` has been added to deal with the logic of the keys (keys, tables and array of tables).
1.0.1 (2018-02-05)
------------------
* Fixed a bug related to integer keys: now, it's possible to create keys using an integer. Reported by @betrixed.
......
......@@ -103,7 +103,7 @@ You can create a TOML string with TomlBuilder. TomlBuilder uses a *fluent interf
->addArrayTables('fruit') // Row
->addValue('name', 'banana')
->addArrayTables('fruit.variety')
->addValue('name', 'platain')
->addValue('name', 'plantain')
->getTomlString(); // Generate the TOML string
```
......@@ -154,7 +154,7 @@ The result:
name = "banana"
[[fruit.variety]]
name = "platain"
name = "plantain"
Contributing
------------
......@@ -164,7 +164,6 @@ the CS, you can use the CLI tool [PHP-CS-Fixer](https://github.com/FriendsOfPHP/
Unit tests
----------
This library requires [PHPUnit](https://phpunit.de/) >= 6.3.
You can run the unit tests with the following command:
```bash
......@@ -172,6 +171,13 @@ $ cd toml
$ phpunit
```
or
```bash
$ cd toml
$ composer test
```
## License
This library is open-sourced software licensed under the
......
......@@ -16,9 +16,15 @@
"php": ">=7.1",
"yosymfony/parser-utils": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^7.1"
},
"autoload": {
"psr-4": { "Yosymfony\\Toml\\": "src/" }
},
"scripts": {
"test": "vendor/bin/phpunit"
},
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
......
This diff is collapsed.
<?php
/*
* This file is part of the Yosymfony\Toml package.
*
* (c) YoSymfony <http://github.com/yosymfony>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Yosymfony\Toml;
/**
* Internal class for managing keys (key-values, tables and array of tables)
*
* @author Victor Puertas <vpgugr@vpgugr.com>
*/
class KeyStore
{
private $keys = [];
private $tables = [];
private $arrayOfTables = [];
private $implicitArrayOfTables = [];
private $currentTable = '';
private $currentArrayOfTable = '';
public function addKey(string $name) : void
{
if (!$this->isValidKey($name)) {
throw new \LogicException("The key \"{$name}\" is not valid.");
}
$this->keys[] = $this->composeKeyWithCurrentPrefix($name);
}
public function isValidKey(string $name) : bool
{
$composedKey = $this->composeKeyWithCurrentPrefix($name);
if (in_array($composedKey, $this->keys, true) === true) {
return false;
}
return true;
}
public function addTableKey(string $name) : void
{
if (!$this->isValidTableKey($name)) {
throw new \LogicException("The table key \"{$name}\" is not valid.");
}
$this->currentTable = '';
$this->currentArrayOfTable = $this->getArrayOfTableKeyFromTableKey($name);
$this->addkey($name);
$this->currentTable = $name;
$this->tables[] = $name;
}
public function isValidTableKey($name) : bool
{
$currentTable = $this->currentTable;
$currentArrayOfTable = $this->currentArrayOfTable;
$this->currentTable = '';
$this->currentArrayOfTable = $this->getArrayOfTableKeyFromTableKey($name);
if ($this->currentArrayOfTable == $name) {
return false;
}
$isValid = $this->isValidKey($name);
$this->currentTable = $currentTable;
$this->currentArrayOfTable = $currentArrayOfTable;
return $isValid;
}
public function isValidInlineTable(string $name): bool
{
return $this->isValidTableKey($name);
}
public function addInlineTableKey(string $name) : void
{
$this->addTableKey($name);
}
public function addArrayTableKey(string $name) : void
{
if (!$this->isValidArrayTableKey($name)) {
throw new \LogicException("The array table key \"{$name}\" is not valid.");
}
$this->currentTable = '';
$this->currentArrayOfTable = '';
if (isset($this->arrayOfTables[$name]) === false) {
$this->addkey($name);
$this->arrayOfTables[$name] = 0;
} else {
$this->arrayOfTables[$name]++;
}
$this->currentArrayOfTable = $name;
$this->processImplicitArrayTableNameIfNeeded($name);
}
public function isValidArrayTableKey(string $name) : bool
{
$isInArrayOfTables = isset($this->arrayOfTables[$name]);
$isInKeys = in_array($name, $this->keys, true);
if ((!$isInArrayOfTables && !$isInKeys) || ($isInArrayOfTables && $isInKeys)) {
return true;
}
return false;
}
public function isRegisteredAsTableKey(string $name) : bool
{
return in_array($name, $this->tables);
}
public function isRegisteredAsArrayTableKey(string $name) : bool
{
return isset($this->arrayOfTables[$name]);
}
public function isTableImplicitFromArryTable(string $name) : bool
{
$isInImplicitArrayOfTables = in_array($name, $this->implicitArrayOfTables);
$isInArrayOfTables = isset($this->arrayOfTables[$name]);
if ($isInImplicitArrayOfTables && !$isInArrayOfTables) {
return true;
}
return false;
}
private function composeKeyWithCurrentPrefix(string $name) : string
{
$currentArrayOfTableIndex = '';
if ($this->currentArrayOfTable != '') {
$currentArrayOfTableIndex = (string) $this->arrayOfTables[$this->currentArrayOfTable];
}
return \trim("{$this->currentArrayOfTable}{$currentArrayOfTableIndex}.{$this->currentTable}.{$name}", '.');
}
private function getArrayOfTableKeyFromTableKey(string $name) : string
{
if (isset($this->arrayOfTables[$name])) {
return $name;
}
$keyParts = explode('.', $name);
if (count($keyParts) === 1) {
return '';
}
array_pop($keyParts);
while (count($keyParts) > 0) {
$candidateKey = implode('.', $keyParts);
if (isset($this->arrayOfTables[$candidateKey])) {
return $candidateKey;
}
array_pop($keyParts);
}
return '';
}
private function processImplicitArrayTableNameIfNeeded(string $name) : void
{
$nameParts = explode('.', $name);
if (count($nameParts) < 2) {
return;
}
array_pop($nameParts);
while (count($nameParts) != 0) {
$this->implicitArrayOfTables[] = implode('.', $nameParts);
array_pop($nameParts);
}
}
}
......@@ -23,11 +23,8 @@ use Yosymfony\ParserUtils\SyntaxErrorException;
*/
class Parser extends AbstractParser
{
private $keys = [];
private $keyOfTables = [];
private $keysOfImplicitArrayOfTables = [];
private $arrayOfTablekeyCounters = [];
private $currentKeyPrefix = '';
/** @var KeyStore */
private $keyStore;
private $result = [];
private $workArray;
......@@ -62,6 +59,7 @@ class Parser extends AbstractParser
*/
protected function parseImplentation(TokenStream $ts) : array
{
$this->keyStore = new KeyStore();
$this->resetWorkArrayToResultArray();
while ($ts->hasPendingTokens()) {
......@@ -71,11 +69,6 @@ class Parser extends AbstractParser
return $this->result;
}
/**
* Process an expression
*
* @param TokenStream $ts The token stream
*/
private function processExpression(TokenStream $ts) : void
{
if ($ts->isNext('T_HASH')) {
......@@ -106,14 +99,29 @@ class Parser extends AbstractParser
private function parseKeyValue(TokenStream $ts, bool $isFromInlineTable = false) : void
{
$keyName = $this->parseKeyName($ts);
$this->addKeyToKeyStore($this->composeKeyWithCurrentKeyPrefix($keyName));
$this->parseSpaceIfExists($ts);
$this->matchNext('T_EQUAL', $ts);
$this->parseSpaceIfExists($ts);
$isInlineTable = $ts->isNext('T_LEFT_CURLY_BRACE');
if ($isInlineTable) {
if (!$this->keyStore->isValidInlineTable($keyName)) {
$this->syntaxError("The inline table key \"{$keyName}\" has already been defined previously.");
}
$this->keyStore->addInlineTableKey($keyName);
} else {
if (!$this->keyStore->isValidKey($keyName)) {
$this->syntaxError("The key \"{$keyName}\" has already been defined previously.");
}
$this->keyStore->addkey($keyName);
}
if ($ts->isNext('T_LEFT_SQUARE_BRAKET')) {
$this->workArray[$keyName] = $this->parseArray($ts);
} elseif ($ts->isNext('T_LEFT_CURLY_BRACE')) {
} elseif ($isInlineTable) {
$this->parseInlineTable($ts, $keyName);
} else {
$this->workArray[$keyName] = $this->parseSimpleValue($ts)->value;
......@@ -433,11 +441,9 @@ class Parser extends AbstractParser
private function parseInlineTable(TokenStream $ts, string $keyName) : void
{
$this->matchNext('T_LEFT_CURLY_BRACE', $ts);
$priorcurrentKeyPrefix = $this->currentKeyPrefix;
$priorWorkArray = &$this->workArray;
$this->addArrayKeyToWorkArray($keyName);
$this->currentKeyPrefix = $this->composeKeyWithCurrentKeyPrefix($keyName);
$this->parseSpaceIfExists($ts);
......@@ -455,7 +461,6 @@ class Parser extends AbstractParser
}
$this->matchNext('T_RIGHT_CURLY_BRACE', $ts);
$this->currentKeyPrefix = $priorcurrentKeyPrefix;
$this->workArray = &$priorWorkArray;
}
......@@ -477,8 +482,11 @@ class Parser extends AbstractParser
$this->addArrayKeyToWorkArray($key);
}
$this->addKeyToTableKeyStore($this->composeKeyWithCurrentKeyPrefix($fullTableName));
$this->currentKeyPrefix = $fullTableName;
if (!$this->keyStore->isValidTableKey($fullTableName)) {
$this->syntaxError("The key \"{$fullTableName}\" has already been defined previously.");
}
$this->keyStore->addTableKey($fullTableName);
$this->matchNext('T_RIGHT_SQUARE_BRAKET', $ts);
$this->parseSpaceIfExists($ts);
......@@ -505,8 +513,15 @@ class Parser extends AbstractParser
$this->addArrayOfTableKeyToWorkArray($key, !$ts->isNext('T_DOT'));
}
$this->addArrayOfTableKeyToKeyStore($fullTableName);
$this->currentKeyPrefix = $fullTableName. $this->getCounterArrayOfTableKey($fullTableName);
if (!$this->keyStore->isValidArrayTableKey($fullTableName)) {
$this->syntaxError("The key \"{$fullTableName}\" has already been defined previously.");
}
if ($this->keyStore->isTableImplicitFromArryTable($fullTableName)) {
$this->syntaxError("The array of tables \"{$fullTableName}\" has already been defined as previous table");
}
$this->keyStore->addArrayTableKey($fullTableName);
$this->matchNext('T_RIGHT_SQUARE_BRAKET', $ts);
$this->matchNext('T_RIGHT_SQUARE_BRAKET', $ts);
......@@ -550,89 +565,14 @@ class Parser extends AbstractParser
}
}
private function addKeyToKeyStore(string $keyName) : void
{
if (in_array($keyName, $this->keys, true) === true) {
$this->syntaxError(sprintf(
'The key "%s" has already been defined previously.',
$keyName
));
}
$this->keys[] = $keyName;
}
private function addKeyToTableKeyStore(string $keyName) : void
{
$this->addKeyToKeyStore($keyName);
$this->keyOfTables[] = $keyName;
}
private function addArrayOfTableKeyToKeyStore(string $keyName) : void
private function addArrayKeyToWorkArray(string $keyName) : void
{
if (isset($this->arrayOfTablekeyCounters[$keyName]) === false) {
$this->addKeyToKeyStore($keyName);
}
$keyNameParts = explode('.', $keyName);
if ($this->isNecesaryToProcessImplicitKeyNameParts($keyNameParts)) {
array_pop($keyNameParts);
foreach ($keyNameParts as $keyNamePart) {
$this->keysOfImplicitArrayOfTables[] = implode('.', $keyNameParts);
array_pop($keyNameParts);
}
if ($this->keyStore->isRegisteredAsArrayTableKey($keyName)) {
$this->addArrayOfTableKeyToWorkArray($keyName, false);
return;
}
if (in_array($keyName, $this->keysOfImplicitArrayOfTables) === true
&& isset($this->arrayOfTablekeyCounters[$keyName]) === false) {
$this->syntaxError(
sprintf('The array of tables "%s" has already been defined as previous table', $keyName)
);
}
}
private function isNecesaryToProcessImplicitKeyNameParts(array $keynameParts) : bool
{
if (count($keynameParts) > 1) {
array_pop($keynameParts);
$implicitArrayOfTablesName = implode('.', $keynameParts);
if (in_array($implicitArrayOfTablesName, $this->arrayOfTablekeyCounters) === false) {
return true;
}
}
return false;
}
private function getCounterArrayOfTableKey($keyName) : int
{
if (isset($this->arrayOfTablekeyCounters[$keyName]) === false) {
return $this->arrayOfTablekeyCounters[$keyName] = 0;
}
return $this->arrayOfTablekeyCounters[$keyName] = $this->arrayOfTablekeyCounters[$keyName] + 1;
}
private function composeKeyWithCurrentKeyPrefix(string $keyName) : string
{
$composedKey = $this->currentKeyPrefix;
if ($composedKey !== '') {
$composedKey .= '.';
}
$composedKey .= $keyName;
return $composedKey;
}
private function addArrayKeyToWorkArray(string $keyName) : void
{
if (isset($this->workArray[$keyName]) === false) {
$this->workArray[$keyName] = [];
}
......@@ -649,7 +589,7 @@ class Parser extends AbstractParser
$this->workArray[$keyName][] = [];
}
if (in_array($keyName, $this->keyOfTables) === false) {
if (!$this->keyStore->isRegisteredAsTableKey($keyName)) {
end($this->workArray[$keyName]);
$this->workArray = &$this->workArray[$keyName][key($this->workArray[$keyName])];
......@@ -661,7 +601,6 @@ class Parser extends AbstractParser
private function resetWorkArrayToResultArray() : void
{
$this->currentKeyPrefix = '';
$this->workArray = &$this->result;
}
......
<?php
/*
* This file is part of the Yosymfony\Toml package.
*
* (c) YoSymfony <http://github.com/yosymfony>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Yosymfony\Toml\tests;
use PHPUnit\Framework\TestCase;
use Yosymfony\Toml\KeyStore;
class KeyStoreTest extends TestCase
{
private $keyStore;
public function setUp()
{
$this->keyStore = new KeyStore();
}
public function testIsValidKeyMustReturnTrueWhenTheKeyDoesNotExist()
{
$this->assertTrue($this->keyStore->isValidKey('a'));
}
public function testIsValidKeyMustReturnFalseWhenDuplicateKeys()
{
$this->keyStore->addKey('a');
$this->assertFalse($this->keyStore->isValidKey('a'));
}
public function testIsValidTableKeyMustReturnTrueWhenTheTableKeyDoesNotExist()
{
$this->assertTrue($this->keyStore->isValidTableKey('a'));
}
public function testIsValidTableKeyMustReturnTrueWhenSuperTableIsNotDireclyDefined()
{
$this->keyStore->addTableKey('a.b');
$this->keyStore->addKey('c');
$this->assertTrue($this->keyStore->isValidTableKey('a'));
}
public function testIsValidTableKeyMustReturnFalseWhenDuplicateTableKeys()
{
$this->keyStore->addTableKey('a');
$this->assertFalse($this->keyStore->isValidTableKey('a'));
}
public function testIsValidTableKeyMustReturnFalseWhenThereIsAKeyWithTheSameName()
{
$this->keyStore->addTableKey('a');
$this->keyStore->addKey('b');
$this->assertFalse($this->keyStore->isValidTableKey('a.b'));
}
public function testIsValidArrayTableKeyMustReturnFalseWhenThereIsAPreviousKeyWithTheSameName()
{
$this->keyStore->addKey('a');
$this->assertFalse($this->keyStore->isValidArrayTableKey('a'));
}
public function testIsValidArrayTableKeyMustReturnFalseWhenThereIsAPreviousTableWithTheSameName()
{
$this->keyStore->addTableKey('a');
$this->assertFalse($this->keyStore->isValidArrayTableKey('a'));
}
public function testIsValidTableKeyMustReturnFalseWhenAttemptingToDefineATableKeyEqualToPreviousDefinedArrayTable()
{
$this->keyStore->addArrayTableKey('a');
$this->keyStore->addArrayTableKey('a.b');
$this->assertFalse($this->keyStore->isValidTableKey('a.b'));
}
public function testIsValidTableKeyMustReturnTrueWithTablesInsideArrayOfTables()
{
$this->keyStore->addArrayTableKey('a');
$this->keyStore->addTableKey('a.b');
$this->keyStore->addArrayTableKey('a');
$this->assertTrue($this->keyStore->isValidTableKey('a.b'));
}
}
......@@ -829,6 +829,49 @@ toml;
], $array);
}
/**
* @see https://github.com/yosymfony/toml/issues/23
*/
public function testParseMustParseTablesContainedWithinArrayTables()
{
$toml = <<<'toml'
[[tls]]
entrypoints = ["https"]
[tls.certificate]
certFile = "certs/foo.crt"
keyFile = "keys/foo.key"
[[tls]]
entrypoints = ["https"]
[tls.certificate]
certFile = "certs/bar.crt"
keyFile = "keys/bar.key"
toml;
$array = $this->parser->parse($toml);
$this->assertEquals([
'tls' => [
[
'entrypoints' => ['https'],
'certificate' => [
'certFile' => 'certs/foo.crt',
'keyFile' => 'keys/foo.key',
],
],
[
'entrypoints' => ['https'],
'certificate' => [
'certFile' => 'certs/bar.crt',
'keyFile' => 'keys/bar.key',
],
],