diff --git a/composer.json b/composer.json index 5ad832cf..6c9c3bb2 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "require": { "php": "^7.1", "justinrainbow/json-schema": "^4.0.0 || ^5.0.0", - "localheinz/json-printer": "^1.0.0" + "localheinz/json-printer": "^2.0.0" }, "require-dev": { "infection/infection": "~0.8.1", diff --git a/composer.lock b/composer.lock index a34f45b4..3f800747 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "ec1fccbe241dc60dca215392db4c4e4a", + "content-hash": "b23c8a997a4ba79e5851e89571f5e278", "packages": [ { "name": "justinrainbow/json-schema", @@ -74,27 +74,27 @@ }, { "name": "localheinz/json-printer", - "version": "1.1.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/localheinz/json-printer.git", - "reference": "49459a160c551b5504d1edcbb10692039071d08a" + "reference": "1a350fd94544df716d1c75ef107d34057f80ac7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/localheinz/json-printer/zipball/49459a160c551b5504d1edcbb10692039071d08a", - "reference": "49459a160c551b5504d1edcbb10692039071d08a", + "url": "https://api.github.com/repos/localheinz/json-printer/zipball/1a350fd94544df716d1c75ef107d34057f80ac7a", + "reference": "1a350fd94544df716d1c75ef107d34057f80ac7a", "shasum": "" }, "require": { "php": "^7.0" }, "require-dev": { - "infection/infection": "~0.7.0", - "localheinz/php-cs-fixer-config": "~1.9.0", + "infection/infection": "~0.8.1", + "localheinz/php-cs-fixer-config": "~1.13.1", "localheinz/test-util": "0.6.1", "phpbench/phpbench": "~0.14.0", - "phpunit/phpunit": "^6.5.5" + "phpunit/phpunit": "^6.5.7" }, "type": "library", "autoload": { @@ -119,7 +119,7 @@ "json", "printer" ], - "time": "2018-01-27T21:05:05+00:00" + "time": "2018-04-06T22:10:47+00:00" } ], "packages-dev": [ diff --git a/src/Format/Format.php b/src/Format/Format.php index caeadd2a..bef513b7 100644 --- a/src/Format/Format.php +++ b/src/Format/Format.php @@ -20,6 +20,11 @@ final class Format implements FormatInterface */ private const PATTERN_INDENT = '/^[ \t]+$/'; + /** + * Constant for a regular expression matching valid new-line character sequence. + */ + private const PATTERN_NEW_LINE = '/^(?>\r\n|\n|\r)$/'; + /** * @var int */ @@ -35,14 +40,20 @@ final class Format implements FormatInterface */ private $hasFinalNewLine; + /** + * @var string + */ + private $newLine; + /** * @param int $jsonEncodeOptions * @param string $indent + * @param string $newLine * @param bool $hasFinalNewLine * * @throws \InvalidArgumentException */ - public function __construct(int $jsonEncodeOptions, string $indent, bool $hasFinalNewLine) + public function __construct(int $jsonEncodeOptions, string $indent, string $newLine, bool $hasFinalNewLine) { if (0 > $jsonEncodeOptions) { throw new \InvalidArgumentException(\sprintf( @@ -58,8 +69,16 @@ public function __construct(int $jsonEncodeOptions, string $indent, bool $hasFin )); } + if (1 !== \preg_match(self::PATTERN_NEW_LINE, $newLine)) { + throw new \InvalidArgumentException(\sprintf( + '"%s" is not a valid new-line character sequence.', + $newLine + )); + } + $this->jsonEncodeOptions = $jsonEncodeOptions; $this->indent = $indent; + $this->newLine = $newLine; $this->hasFinalNewLine = $hasFinalNewLine; } @@ -73,6 +92,11 @@ public function indent(): string return $this->indent; } + public function newLine(): string + { + return $this->newLine; + } + public function hasFinalNewLine(): bool { return $this->hasFinalNewLine; @@ -110,6 +134,22 @@ public function withIndent(string $indent): FormatInterface return $mutated; } + public function withNewLine(string $newLine): FormatInterface + { + if (1 !== \preg_match(self::PATTERN_NEW_LINE, $newLine)) { + throw new \InvalidArgumentException(\sprintf( + '"%s" is not a valid new-line character sequence.', + $newLine + )); + } + + $mutated = clone $this; + + $mutated->newLine = $newLine; + + return $mutated; + } + public function withHasFinalNewLine(bool $hasFinalNewLine): FormatInterface { $mutated = clone $this; diff --git a/src/Format/FormatInterface.php b/src/Format/FormatInterface.php index 0815074a..92ca623e 100644 --- a/src/Format/FormatInterface.php +++ b/src/Format/FormatInterface.php @@ -19,8 +19,19 @@ public function jsonEncodeOptions(): int; public function indent(): string; + public function newLine(): string; + public function hasFinalNewLine(): bool; + /** + * @param int $jsonEncodeOptions + * + * @throws \InvalidArgumentException + * + * @return FormatInterface + */ + public function withJsonEncodeOptions(int $jsonEncodeOptions): self; + /** * @param string $indent * @@ -31,13 +42,13 @@ public function hasFinalNewLine(): bool; public function withIndent(string $indent): self; /** - * @param int $jsonEncodeOptions + * @param string $newLine * * @throws \InvalidArgumentException * * @return FormatInterface */ - public function withJsonEncodeOptions(int $jsonEncodeOptions): self; + public function withNewLine(string $newLine): self; public function withHasFinalNewLine(bool $hasFinalNewLine): self; } diff --git a/src/Format/Formatter.php b/src/Format/Formatter.php index f6d4bb72..8a3057cd 100644 --- a/src/Format/Formatter.php +++ b/src/Format/Formatter.php @@ -45,13 +45,14 @@ public function format(string $json, FormatInterface $format): string $printed = $this->printer->print( $encoded, - $format->indent() + $format->indent(), + $format->newLine() ); if (!$format->hasFinalNewLine()) { return $printed; } - return $printed . PHP_EOL; + return $printed . $format->newLine(); } } diff --git a/src/Format/Sniffer.php b/src/Format/Sniffer.php index 5050d615..57c862eb 100644 --- a/src/Format/Sniffer.php +++ b/src/Format/Sniffer.php @@ -27,6 +27,7 @@ public function sniff(string $json): FormatInterface return new Format( $this->jsonEncodeOptions($json), $this->indent($json), + $this->newLine($json), $this->hasFinalNewLine($json) ); } @@ -55,6 +56,15 @@ private function indent(string $json): string return ' '; } + private function newLine(string $json): string + { + if (1 === \preg_match('/(?P\r\n|\n|\r)/', $json, $match)) { + return $match['newLine']; + } + + return PHP_EOL; + } + private function hasFinalNewLine(string $json): bool { if (\rtrim($json, " \t") === \rtrim($json)) { diff --git a/test/Unit/Format/FormatTest.php b/test/Unit/Format/FormatTest.php index c7782a88..0d796283 100644 --- a/test/Unit/Format/FormatTest.php +++ b/test/Unit/Format/FormatTest.php @@ -31,6 +31,7 @@ public function testConstructorRejectsInvalidEncodeOptions(): void { $jsonEncodeOptions = -1; $indent = ' '; + $newLine = PHP_EOL; $hasFinalNewLine = true; $this->expectException(\InvalidArgumentException::class); @@ -42,6 +43,7 @@ public function testConstructorRejectsInvalidEncodeOptions(): void new Format( $jsonEncodeOptions, $indent, + $newLine, $hasFinalNewLine ); } @@ -54,6 +56,7 @@ public function testConstructorRejectsInvalidEncodeOptions(): void public function testConstructorRejectsInvalidIndent(string $indent): void { $jsonEncodeOptions = 0; + $newLine = PHP_EOL; $hasFinalNewLine = true; $this->expectException(\InvalidArgumentException::class); @@ -65,6 +68,7 @@ public function testConstructorRejectsInvalidIndent(string $indent): void new Format( $jsonEncodeOptions, $indent, + $newLine, $hasFinalNewLine ); } @@ -84,34 +88,85 @@ public function providerInvalidIndent(): \Generator } /** - * @dataProvider providerJsonIndentAndFinalNewLine + * @dataProvider providerInvalidNewLine + * + * @param string $newLine + */ + public function testConstructorRejectsInvalidNewLine(string $newLine): void + { + $jsonEncodeOptions = 0; + $indent = ' '; + $hasFinalNewLine = true; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage(\sprintf( + '"%s" is not a valid new-line character sequence.', + $newLine + )); + + new Format( + $jsonEncodeOptions, + $indent, + $newLine, + $hasFinalNewLine + ); + } + + public function providerInvalidNewLine(): \Generator + { + $values = [ + "\t", + " \r ", + " \r\n ", + " \n ", + ' ', + "\f", + "\x0b", + "\x85", + ]; + + foreach ($values as $value) { + yield [ + $value, + ]; + } + } + + /** + * @dataProvider providerJsonIndentNewLineAndFinalNewLine * * @param string $indent + * @param string $newLine * @param bool $hasFinalNewLine */ - public function testConstructorSetsValues(string $indent, bool $hasFinalNewLine): void + public function testConstructorSetsValues(string $indent, string $newLine, bool $hasFinalNewLine): void { $jsonEncodeOptions = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES; $format = new Format( $jsonEncodeOptions, $indent, + $newLine, $hasFinalNewLine ); $this->assertSame($jsonEncodeOptions, $format->jsonEncodeOptions()); $this->assertSame($indent, $format->indent()); + $this->assertSame($newLine, $format->newLine()); $this->assertSame($hasFinalNewLine, $format->hasFinalNewLine()); } - public function providerJsonIndentAndFinalNewLine(): \Generator + public function providerJsonIndentNewLineAndFinalNewLine(): \Generator { foreach ($this->indents() as $indent) { - foreach ($this->hasFinalNewLines() as $hasFinalNewLine) { - yield [ - $indent, - $hasFinalNewLine, - ]; + foreach ($this->newLines() as $newLine) { + foreach ($this->hasFinalNewLines() as $hasFinalNewLine) { + yield [ + $indent, + $newLine, + $hasFinalNewLine, + ]; + } } } } @@ -123,6 +178,7 @@ public function testWithJsonEncodeOptionsRejectsInvalidJsonEncodeOptions(): void $format = new Format( JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES, ' ', + PHP_EOL, true ); @@ -140,6 +196,7 @@ public function testWithJsonEncodeOptionsClonesFormatAndSetsJsonEncodeOptions(): $format = new Format( JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES, ' ', + PHP_EOL, true ); @@ -162,6 +219,7 @@ public function testWithIndentRejectsInvalidIndent(string $indent): void $format = new Format( JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES, ' ', + PHP_EOL, true ); @@ -184,6 +242,7 @@ public function testWithIndentClonesFormatAndSetsIndent(string $indent): void $format = new Format( JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES, ' ', + PHP_EOL, true ); @@ -203,6 +262,59 @@ public function providerIndent(): \Generator } } + /** + * @dataProvider providerInvalidNewLine + * + * @param string $newLine + */ + public function testWithNewLineRejectsInvalidNewLine(string $newLine): void + { + $format = new Format( + JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES, + ' ', + PHP_EOL, + true + ); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage(\sprintf( + '"%s" is not a valid new-line character sequence.', + $newLine + )); + + $format->withNewLine($newLine); + } + + /** + * @dataProvider providerNewLine + * + * @param string $newLine + */ + public function testWithNewLineClonesFormatAndSetsNewLine(string $newLine): void + { + $format = new Format( + JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES, + ' ', + PHP_EOL, + true + ); + + $mutated = $format->withNewLine($newLine); + + $this->assertInstanceOf(FormatInterface::class, $mutated); + $this->assertNotSame($format, $mutated); + $this->assertSame($newLine, $mutated->newLine()); + } + + public function providerNewLine(): \Generator + { + foreach ($this->newLines() as $newLine) { + yield [ + $newLine, + ]; + } + } + /** * @dataProvider providerHasFinalNewLine * @@ -213,6 +325,7 @@ public function testWithHasFinalNewLineClonesFormatAndSetsFinalNewLine(bool $has $format = new Format( JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES, ' ', + PHP_EOL, false ); @@ -243,6 +356,18 @@ private function indents(): array ]; } + /** + * @return string[] + */ + private function newLines(): array + { + return [ + "\r\n", + "\n", + "\r", + ]; + } + /** * @return bool[] */ diff --git a/test/Unit/Format/FormatterTest.php b/test/Unit/Format/FormatterTest.php index a2096fc0..5d637724 100644 --- a/test/Unit/Format/FormatterTest.php +++ b/test/Unit/Format/FormatterTest.php @@ -51,15 +51,19 @@ public function testFormatRejectsInvalidJson(): void /** * @dataProvider providerFinalNewLine * - * @param bool $hasFinalNewLine - * @param string $suffix + * @param bool $hasFinalNewLine */ - public function testFormatEncodesWithJsonEncodeOptionsIndentsAndPossiblySuffixesWithFinalNewLine(bool $hasFinalNewLine, string $suffix): void + public function testFormatEncodesWithJsonEncodeOptionsIndentsAndPossiblySuffixesWithFinalNewLine(bool $hasFinalNewLine): void { $faker = $this->faker(); $jsonEncodeOptions = $faker->numberBetween(1); $indent = \str_repeat(' ', $faker->numberBetween(1, 5)); + $newLine = $faker->randomElement([ + "\r\n", + "\n", + "\r", + ]); $json = <<<'JSON' { @@ -92,6 +96,11 @@ public function testFormatEncodesWithJsonEncodeOptionsIndentsAndPossiblySuffixes ->shouldBeCalled() ->willReturn($indent); + $format + ->newLine() + ->shouldBeCalled() + ->willReturn($newLine); + $format ->hasFinalNewLine() ->shouldBeCalled() @@ -102,7 +111,8 @@ public function testFormatEncodesWithJsonEncodeOptionsIndentsAndPossiblySuffixes $printer ->print( Argument::is($encoded), - Argument::is($indent) + Argument::is($indent), + Argument::is($newLine) ) ->shouldBeCalled() ->willReturn($printed); @@ -114,6 +124,8 @@ public function testFormatEncodesWithJsonEncodeOptionsIndentsAndPossiblySuffixes $format->reveal() ); + $suffix = $hasFinalNewLine ? $newLine : ''; + $this->assertSame($printed . $suffix, $formatted); } diff --git a/test/Unit/Format/SnifferTest.php b/test/Unit/Format/SnifferTest.php index 8f798a90..c2b5d015 100644 --- a/test/Unit/Format/SnifferTest.php +++ b/test/Unit/Format/SnifferTest.php @@ -194,6 +194,74 @@ public function providerIndent(): \Generator } } + /** + * @dataProvider providerJsonWithoutWhitespace + * + * @param string $json + */ + public function testSniffReturnsFormatWithDefaultNewLineIfUnableToSniff(string $json): void + { + $sniffer = new Sniffer(); + + $format = $sniffer->sniff($json); + + $this->assertInstanceOf(FormatInterface::class, $format); + $this->assertSame(PHP_EOL, $format->newLine()); + } + + /** + * @dataProvider providerNewLine + * + * @param string $newLine + */ + public function testSniffReturnsFormatWithNewLineSniffedFromArray(string $newLine): void + { + $json = <<sniff($json); + + $this->assertInstanceOf(FormatInterface::class, $format); + $this->assertSame($newLine, $format->newLine()); + } + + /** + * @dataProvider providerNewLine + * + * @param string $newLine + */ + public function testSniffReturnsFormatWithNewLineNewLineSniffedFromObject(string $newLine): void + { + $json = <<sniff($json); + + $this->assertInstanceOf(FormatInterface::class, $format); + $this->assertSame($newLine, $format->newLine()); + } + + public function providerNewLine(): \Generator + { + $values = [ + "\r\n", + "\n", + "\r", + ]; + + foreach ($values as $newLine) { + yield [ + $newLine, + ]; + } + } + /** * @dataProvider providerWhitespaceWithoutNewLine *