From b7a0e6652570123b349ffa78dd7392cb58882013 Mon Sep 17 00:00:00 2001 From: Maddin Date: Sun, 8 Apr 2018 22:09:02 +0200 Subject: [PATCH] Add JSON Parser (#919) Closes #825 --- README.md | 1 + doc/complete-reference.md | 13 ++ fixtures/Integration/dummy.json | 5 + fixtures/Parser/FileListProviderTrait.php | 5 + fixtures/Parser/files/json/basic.json | 7 + fixtures/Parser/files/json/empty.json | 1 + fixtures/Parser/files/json/invalid.json | 1 + .../Symfony/Resources/config/parser.xml | 4 + src/Loader/NativeLoader.php | 2 + src/Parser/Chainable/JsonParser.php | 69 ++++++++ .../Parser/ParseExceptionFactory.php | 12 ++ tests/Loader/LoaderIntegrationTest.php | 15 ++ tests/Parser/Chainable/JsonParserTest.php | 154 ++++++++++++++++++ tests/Parser/Chainable/PhpParserTest.php | 10 ++ tests/Parser/Chainable/YamlParserTest.php | 10 ++ tests/Parser/FilesReference.php | 32 ++++ 16 files changed, 341 insertions(+) create mode 100644 fixtures/Integration/dummy.json create mode 100644 fixtures/Parser/files/json/basic.json create mode 100644 fixtures/Parser/files/json/empty.json create mode 100644 fixtures/Parser/files/json/invalid.json create mode 100644 src/Parser/Chainable/JsonParser.php create mode 100644 tests/Parser/Chainable/JsonParserTest.php diff --git a/README.md b/README.md index 83886bc89..766d4f4d3 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ for 2.x, head [here](https://github.com/nelmio/alice/tree/2.x)**. 1. [Creating Fixtures](doc/complete-reference.md#creating-fixtures) 1. [YAML](doc/complete-reference.md#yaml) 1. [PHP](doc/complete-reference.md#php) + 1. [JSON](doc/complete-reference.md#json) 1. [Fixture Ranges](doc/complete-reference.md#fixture-ranges) 1. [Fixture Lists](doc/complete-reference.md#fixture-lists) 1. [Fixture Reference](doc/complete-reference.md#fixture-reference) diff --git a/doc/complete-reference.md b/doc/complete-reference.md index a4c4a591e..164bd98a6 100644 --- a/doc/complete-reference.md +++ b/doc/complete-reference.md @@ -3,6 +3,7 @@ 1. [Creating Fixtures](#creating-fixtures) 1. [YAML](#yaml) 1. [PHP](#php) + 1. [JSON](#json) 1. [Fixture Ranges](#fixture-ranges) 1. [Fixture Reference](#fixture-reference) 1. [Fixture Lists](#fixture-lists) @@ -70,7 +71,19 @@ return [ ]; ``` +### JSON +You can also specify fixtures in a JSON file: + +```json +{ + "Nelmio\\Alice\\support\\models\\User": { + "user0": { + "fullname": "John Doe" + } + } +} +``` ## Fixture Ranges diff --git a/fixtures/Integration/dummy.json b/fixtures/Integration/dummy.json new file mode 100644 index 000000000..3bd474702 --- /dev/null +++ b/fixtures/Integration/dummy.json @@ -0,0 +1,5 @@ +{ + "\\stdClass": { + "dummy{1..2}": {} + } +} diff --git a/fixtures/Parser/FileListProviderTrait.php b/fixtures/Parser/FileListProviderTrait.php index fb5b14a78..58d53f4ea 100644 --- a/fixtures/Parser/FileListProviderTrait.php +++ b/fixtures/Parser/FileListProviderTrait.php @@ -25,6 +25,11 @@ public function provideYamlList() return FilesReference::getYamlList(); } + public function provideJsonList() + { + return FilesReference::getJsonList(); + } + public function provideUnsupportedList() { return FilesReference::getUnsupportedList(); diff --git a/fixtures/Parser/files/json/basic.json b/fixtures/Parser/files/json/basic.json new file mode 100644 index 000000000..1212f1e56 --- /dev/null +++ b/fixtures/Parser/files/json/basic.json @@ -0,0 +1,7 @@ +{ + "Nelmio\\Alice\\support\\models\\User": { + "user0": { + "fullname": "John Doe" + } + } +} diff --git a/fixtures/Parser/files/json/empty.json b/fixtures/Parser/files/json/empty.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/fixtures/Parser/files/json/empty.json @@ -0,0 +1 @@ +{} diff --git a/fixtures/Parser/files/json/invalid.json b/fixtures/Parser/files/json/invalid.json new file mode 100644 index 000000000..98232c64f --- /dev/null +++ b/fixtures/Parser/files/json/invalid.json @@ -0,0 +1 @@ +{ diff --git a/src/Bridge/Symfony/Resources/config/parser.xml b/src/Bridge/Symfony/Resources/config/parser.xml index 2a08319a9..939b4f30b 100644 --- a/src/Bridge/Symfony/Resources/config/parser.xml +++ b/src/Bridge/Symfony/Resources/config/parser.xml @@ -44,6 +44,10 @@ + + + + diff --git a/src/Loader/NativeLoader.php b/src/Loader/NativeLoader.php index 198ba81e1..cd0254c55 100644 --- a/src/Loader/NativeLoader.php +++ b/src/Loader/NativeLoader.php @@ -153,6 +153,7 @@ use Nelmio\Alice\GeneratorInterface; use Nelmio\Alice\IsAServiceTrait; use Nelmio\Alice\ObjectSet; +use Nelmio\Alice\Parser\Chainable\JsonParser; use Nelmio\Alice\Parser\Chainable\PhpParser; use Nelmio\Alice\Parser\Chainable\YamlParser; use Nelmio\Alice\Parser\IncludeProcessor\DefaultIncludeProcessor; @@ -313,6 +314,7 @@ protected function createParser(): ParserInterface $registry = new ParserRegistry([ new YamlParser(new SymfonyYamlParser()), new PhpParser(), + new JsonParser(), ]); return new RuntimeCacheParser( diff --git a/src/Parser/Chainable/JsonParser.php b/src/Parser/Chainable/JsonParser.php new file mode 100644 index 000000000..2f17a073b --- /dev/null +++ b/src/Parser/Chainable/JsonParser.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Nelmio\Alice\Parser\Chainable; + +use Nelmio\Alice\IsAServiceTrait; +use Nelmio\Alice\Parser\ChainableParserInterface; +use Nelmio\Alice\Throwable\Exception\InvalidArgumentExceptionFactory; +use Nelmio\Alice\Throwable\Exception\Parser\ParseExceptionFactory; +use Nelmio\Alice\Throwable\Exception\Parser\UnparsableFileException; + +final class JsonParser implements ChainableParserInterface +{ + use IsAServiceTrait; + + private const REGEX = '/.{1,}\.json$/i'; + + /** + * @inheritdoc + */ + public function canParse(string $file): bool + { + if (false === stream_is_local($file)) { + return false; + } + + return 1 === preg_match(self::REGEX, $file); + } + + /** + * {@inheritDoc} + * + * @param string $file Local JSON file + * + * @throws UnparsableFileException + */ + public function parse(string $file): array + { + if (false === file_exists($file)) { + throw InvalidArgumentExceptionFactory::createForFileCouldNotBeFound($file); + } + + try { + $data = json_decode(file_get_contents($file), true); + + if (null === $data) { + throw ParseExceptionFactory::createForInvalidJson($file); + } + + return $data; + } catch (\Exception $exception) { + if ($exception instanceof UnparsableFileException) { + throw $exception; + } + + throw ParseExceptionFactory::createForUnparsableFile($file, 0, $exception); + } + } +} diff --git a/src/Throwable/Exception/Parser/ParseExceptionFactory.php b/src/Throwable/Exception/Parser/ParseExceptionFactory.php index e8a40db0c..98d91081f 100644 --- a/src/Throwable/Exception/Parser/ParseExceptionFactory.php +++ b/src/Throwable/Exception/Parser/ParseExceptionFactory.php @@ -52,6 +52,18 @@ public static function createForInvalidYaml(string $file, int $code = 0, \Throwa ); } + public static function createForInvalidJson(string $file, int $code = 0, \Throwable $previous = null): UnparsableFileException + { + return new UnparsableFileException( + sprintf( + 'The file "%s" does not contain valid JSON.', + $file + ), + $code, + $previous + ); + } + private function __construct() { } diff --git a/tests/Loader/LoaderIntegrationTest.php b/tests/Loader/LoaderIntegrationTest.php index 3d022a492..1af3499f8 100644 --- a/tests/Loader/LoaderIntegrationTest.php +++ b/tests/Loader/LoaderIntegrationTest.php @@ -88,6 +88,21 @@ public function testLoadFiles() ); } + public function testLoadJsonFile() + { + $objects = $this->loader->loadFiles([ + self::FIXTURES_FILES_DIR.'/dummy.json', + ])->getObjects(); + + $this->assertEquals( + [ + 'dummy1' => new stdClass(), + 'dummy2' => new stdClass(), + ], + $objects + ); + } + public function testLoadRecursiveFiles() { $objects = $this->loader->loadFiles([ diff --git a/tests/Parser/Chainable/JsonParserTest.php b/tests/Parser/Chainable/JsonParserTest.php new file mode 100644 index 000000000..9b6776284 --- /dev/null +++ b/tests/Parser/Chainable/JsonParserTest.php @@ -0,0 +1,154 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Nelmio\Alice\Parser\Chainable; + +use Nelmio\Alice\Parser\ChainableParserInterface; +use Nelmio\Alice\Parser\FileListProviderTrait; +use PHPUnit\Framework\TestCase; +use ReflectionClass; + +/** + * @covers \Nelmio\Alice\Parser\Chainable\PhpParser + */ +class JsonParserTest extends TestCase +{ + use FileListProviderTrait; + + private static $dir; + + /** + * @var JsonParser + */ + private $parser; + + /** + * @inheritdoc + */ + public static function setUpBeforeClass() + { + parent::setUpBeforeClass(); + + self::$dir = __DIR__.'/../../../fixtures/Parser/files/json'; + } + + /** + * @inheritdoc + */ + public static function tearDownAfterClass() + { + self::$dir = null; + + parent::tearDownAfterClass(); + } + + /** + * @inheritdoc + */ + public function setUp() + { + $this->parser = new JsonParser(); + } + + public function testIsAChainableParser() + { + $this->assertTrue(is_a(JsonParser::class, ChainableParserInterface::class, true)); + } + + public function testIsNotClonable() + { + $this->assertFalse((new ReflectionClass(JsonParser::class))->isCloneable()); + } + + /** + * @dataProvider provideJsonList + */ + public function testCanParseJsonFiles(string $file, array $expectedParsers) + { + $actual = $this->parser->canParse($file); + $expected = (in_array(get_class($this->parser), $expectedParsers)); + + $this->assertEquals($expected, $actual); + } + + /** + * @dataProvider providePhpList + */ + public function testCanNotParsePhpFiles(string $file) + { + $actual = $this->parser->canParse($file); + + $this->assertFalse($actual); + } + + /** + * @dataProvider provideYamlList + */ + public function testCannotParseYamlFiles(string $file) + { + $actual = $this->parser->canParse($file); + + $this->assertFalse($actual); + } + + /** + * @dataProvider provideUnsupportedList + */ + public function testCannotParseUnsupportedFiles(string $file) + { + $actual = $this->parser->canParse($file); + + $this->assertFalse($actual); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage The file "/nowhere.json" could not be found. + */ + public function testThrowsAnExceptionIfFileDoesNotExist() + { + $this->parser->parse('/nowhere.json'); + } + + public function testReturnsParsedFileContent() + { + $actual = $this->parser->parse(self::$dir.'/basic.json'); + + $this->assertSame( + [ + 'Nelmio\Alice\support\models\User' => [ + 'user0' => [ + 'fullname' => 'John Doe', + ], + ], + ], + $actual + ); + } + + public function testParsingEmptyFileResultsInEmptySet() + { + $actual = $this->parser->parse(self::$dir.'/empty.json'); + + $this->assertSame([], $actual); + } + + /** + * @expectedException \Nelmio\Alice\Throwable\Exception\Parser\UnparsableFileException + * @expectedExceptionMessageRegExp /^The file ".+\/invalid\.json" does not contain valid JSON\.$/ + */ + public function testThrowsAnExceptionIfInvalidJson() + { + $this->parser->parse(self::$dir.'/invalid.json'); + } +} diff --git a/tests/Parser/Chainable/PhpParserTest.php b/tests/Parser/Chainable/PhpParserTest.php index 5765b08f5..55186471d 100644 --- a/tests/Parser/Chainable/PhpParserTest.php +++ b/tests/Parser/Chainable/PhpParserTest.php @@ -91,6 +91,16 @@ public function testCannotParseYamlFiles(string $file) $this->assertFalse($actual); } + /** + * @dataProvider provideJsonList + */ + public function testCannotParseJsonFiles(string $file) + { + $actual = $this->parser->canParse($file); + + $this->assertFalse($actual); + } + /** * @dataProvider provideUnsupportedList */ diff --git a/tests/Parser/Chainable/YamlParserTest.php b/tests/Parser/Chainable/YamlParserTest.php index 159f7b71f..d7fd13e5f 100644 --- a/tests/Parser/Chainable/YamlParserTest.php +++ b/tests/Parser/Chainable/YamlParserTest.php @@ -101,6 +101,16 @@ public function testCanParseYamlFiles(string $file, array $expectedParsers) $this->assertEquals($expected, $actual); } + /** + * @dataProvider provideJsonList + */ + public function testCannotParseJsonFiles(string $file) + { + $actual = $this->parser->canParse($file); + + $this->assertFalse($actual); + } + /** * @dataProvider provideUnsupportedList */ diff --git a/tests/Parser/FilesReference.php b/tests/Parser/FilesReference.php index b56e07563..536478df9 100644 --- a/tests/Parser/FilesReference.php +++ b/tests/Parser/FilesReference.php @@ -13,6 +13,7 @@ namespace Nelmio\Alice\Parser; +use Nelmio\Alice\Parser\Chainable\JsonParser; use Nelmio\Alice\Parser\Chainable\PhpParser; use Nelmio\Alice\Parser\Chainable\YamlParser; @@ -110,6 +111,32 @@ public function __construct() [], ], ], + 'json' => [ + 'regular JSON file' => [ + 'dummy.json', + [JsonParser::class], + ], + 'JSON file with uppercase extension' => [ + 'dummy.JSON', + [JsonParser::class], + ], + 'remote JSON file with HTTP' => [ + 'http://example.com/dummy.json', + [], + ], + 'remote JSON file with HTTPS' => [ + 'https://example.com/dummy.json', + [], + ], + 'remote JSON file with FTP' => [ + 'ftp://user:password@example.com/dummy.json', + [], + ], + 'remote JSON file with FTPS' => [ + 'ftps://user:password@example.com/dummy.json', + [], + ], + ], 'unsupported' => [ 'XML file' => ['dummy.xml'], 'CSV file' => ['dummy.csv'], @@ -127,6 +154,11 @@ public static function getYamlList(): array return self::getList('yaml'); } + public static function getJsonList(): array + { + return self::getList('json'); + } + public static function getUnsupportedList(): array { return self::getList('unsupported');