diff --git a/CHANGELOG.md b/CHANGELOG.md index f0d0471..f185ef3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Duct Changelog +### 0.2.1 (2013-07-08) + +* **[NEW]** Added `error` event to `EventedParser` +* **[FIXED]** Parsers are now reset when an exception is thrown + ### 0.2.0 (2013-07-08) * **[NEW]** Added `EventedParser`, a SAX-JS/Clarinet-like event-based JSON parser diff --git a/README.md b/README.md index f12095e..7670ef9 100644 --- a/README.md +++ b/README.md @@ -92,8 +92,9 @@ the following events are emitted as the buffer is parsed. * **object-key** (string $key): emitted when an object key is encountered * **value** (mixed $value): emitted whenever a scalar or null is encountered, including inside objects and arrays * **document** (mixed $value): emitted after an entire JSON document has been parsed + * **error** (Exception $error): emitted when a syntax error is encountered [Build Status]: https://travis-ci.org/IcecaveStudios/duct.png?branch=develop [Test Coverage]: https://coveralls.io/repos/IcecaveStudios/duct/badge.png?branch=develop -[SemVer]: http://calm-shore-6115.herokuapp.com/?label=semver&value=0.2.0&color=yellow +[SemVer]: http://calm-shore-6115.herokuapp.com/?label=semver&value=0.2.1&color=yellow diff --git a/lib-typhoon/Icecave/Duct/TypeCheck/Validator/Icecave/Duct/Exception/LexerExceptionTypeCheck.php b/lib-typhoon/Icecave/Duct/TypeCheck/Validator/Icecave/Duct/Detail/Exception/LexerExceptionTypeCheck.php similarity index 55% rename from lib-typhoon/Icecave/Duct/TypeCheck/Validator/Icecave/Duct/Exception/LexerExceptionTypeCheck.php rename to lib-typhoon/Icecave/Duct/TypeCheck/Validator/Icecave/Duct/Detail/Exception/LexerExceptionTypeCheck.php index 83fcb27..aa9bcbe 100644 --- a/lib-typhoon/Icecave/Duct/TypeCheck/Validator/Icecave/Duct/Exception/LexerExceptionTypeCheck.php +++ b/lib-typhoon/Icecave/Duct/TypeCheck/Validator/Icecave/Duct/Detail/Exception/LexerExceptionTypeCheck.php @@ -1,5 +1,5 @@ 1) { - throw new \Icecave\Duct\TypeCheck\Exception\UnexpectedArgumentException(1, $arguments[1]); - } - $value = $arguments[0]; - if (!\is_string($value)) { - throw new \Icecave\Duct\TypeCheck\Exception\UnexpectedArgumentValueException( - 'buffer', - 0, - $arguments[0], - 'string' - ); - } - } - public function feed(array $arguments) { $argumentCount = \count($arguments); @@ -74,13 +55,6 @@ public function finalize(array $arguments) } } - public function tokens(array $arguments) - { - if (\count($arguments) > 0) { - throw new \Icecave\Duct\TypeCheck\Exception\UnexpectedArgumentException(0, $arguments[0]); - } - } - public function consume(array $arguments) { if (\count($arguments) > 0) { diff --git a/lib-typhoon/Icecave/Duct/TypeCheck/Validator/Icecave/Duct/EventedParserTypeCheck.php b/lib-typhoon/Icecave/Duct/TypeCheck/Validator/Icecave/Duct/EventedParserTypeCheck.php index bd235e5..b080e0d 100644 --- a/lib-typhoon/Icecave/Duct/TypeCheck/Validator/Icecave/Duct/EventedParserTypeCheck.php +++ b/lib-typhoon/Icecave/Duct/TypeCheck/Validator/Icecave/Duct/EventedParserTypeCheck.php @@ -11,6 +11,32 @@ public function validateConstruct(array $arguments) } } + public function feed(array $arguments) + { + $argumentCount = \count($arguments); + if ($argumentCount < 1) { + throw new \Icecave\Duct\TypeCheck\Exception\MissingArgumentException('buffer', 0, 'string'); + } elseif ($argumentCount > 1) { + throw new \Icecave\Duct\TypeCheck\Exception\UnexpectedArgumentException(1, $arguments[1]); + } + $value = $arguments[0]; + if (!\is_string($value)) { + throw new \Icecave\Duct\TypeCheck\Exception\UnexpectedArgumentValueException( + 'buffer', + 0, + $arguments[0], + 'string' + ); + } + } + + public function finalize(array $arguments) + { + if (\count($arguments) > 0) { + throw new \Icecave\Duct\TypeCheck\Exception\UnexpectedArgumentException(0, $arguments[0]); + } + } + public function on(array $arguments) { $argumentCount = \count($arguments); diff --git a/lib/Icecave/Duct/AbstractParser.php b/lib/Icecave/Duct/AbstractParser.php index 288bbb6..c1eaf42 100644 --- a/lib/Icecave/Duct/AbstractParser.php +++ b/lib/Icecave/Duct/AbstractParser.php @@ -1,6 +1,7 @@ lexer = $lexer; $this->parser = $parser; + + $this->lexer->on( + 'token', + array($this->parser, 'feedToken') + ); } /** @@ -37,8 +43,7 @@ public function __construct(Lexer $lexer = null, TokenStreamParser $parser = nul * * @param string $buffer The JSON data. * - * @return Vector The sequence of parsed JSON values. - * @throws Exception\ParserException Indicates that the JSON stream terminated midway through a JSON value. + * @throws Exception\SyntaxExceptionInterface */ public function parse($buffer) { @@ -63,28 +68,37 @@ public function reset() /** * Feed (potentially incomplete) JSON data to the parser. * - * @param string $buffer The JSON data. + * @param string $buffer The JSON data. + * @throws Exception\SyntaxExceptionInterface */ public function feed($buffer) { $this->typeCheck->feed(func_get_args()); - $this->lexer->feed($buffer); - $this->parser->feed($this->lexer->tokens()); + try { + $this->lexer->feed($buffer); + } catch (Exception $e) { + $this->reset(); + throw $e; + } } /** * Finalize parsing. * - * @throws Exception\ParserException Indicates that the JSON stream terminated midway through a JSON value. + * @throws Exception\SyntaxExceptionInterface */ public function finalize() { $this->typeCheck->finalize(func_get_args()); - $this->lexer->finalize(); - $this->parser->feed($this->lexer->tokens()); - $this->parser->finalize(); + try { + $this->lexer->finalize(); + $this->parser->finalize(); + } catch (Exception $e) { + $this->reset(); + throw $e; + } } private $typeCheck; diff --git a/lib/Icecave/Duct/Detail/Lexer.php b/lib/Icecave/Duct/Detail/Lexer.php index b184bf2..05b1572 100644 --- a/lib/Icecave/Duct/Detail/Lexer.php +++ b/lib/Icecave/Duct/Detail/Lexer.php @@ -1,7 +1,7 @@ inputBuffer = ''; $this->tokenBuffer = ''; $this->unicodeBuffer = ''; - $this->tokens = new Vector; - } - - /** - * Tokenize JSON data. - * - * @param string $buffer The JSON data. - * - * @return Vector The sequence of tokens representing the JSON data. - * @throws Exception\LexerException Indicates that the input terminated midway through a token. - */ - public function lex($buffer) - { - $this->typeCheck->lex(func_get_args()); - - $this->reset(); - $this->feed($buffer); - $this->finalize(); - - return $this->tokens(); } /** * Feed JSON data to the lexer. * * @param string $buffer The JSON data. + * + * @throws Exception\LexerException */ public function feed($buffer) { @@ -105,21 +87,6 @@ public function finalize() } } - /** - * Fetch the tokens produced by the lexer so far and remove them from the internal token sequence. - * - * @return Vector The sequence of tokens representing the JSON value. - */ - public function tokens() - { - $this->typeCheck->tokens(func_get_args()); - - $tokens = clone $this->tokens; - $this->tokens->clear(); - - return $tokens; - } - private function consume() { if (!mb_check_encoding($this->inputBuffer, $this->encoding)) { @@ -412,7 +379,7 @@ private function isWhitespace($char) */ private function emitSpecial($char) { - $this->tokens->pushBack(Token::createSpecial($char)); + $this->emit('token', array(Token::createSpecial($char))); $this->tokenBuffer = ''; $this->state = LexerState::BEGIN(); } @@ -422,7 +389,7 @@ private function emitSpecial($char) */ private function emitLiteral($value) { - $this->tokens->pushBack(Token::createLiteral($value)); + $this->emit('token', array(Token::createLiteral($value))); $this->tokenBuffer = ''; $this->state = LexerState::BEGIN(); } @@ -496,5 +463,4 @@ private function convertUnicodeCodepoint($codepoint) private $tokenBuffer; private $unicodeBuffer; private $unicodeHighSurrogate; - private $tokens; } diff --git a/lib/Icecave/Duct/Detail/TokenStreamParser.php b/lib/Icecave/Duct/Detail/TokenStreamParser.php index 7a8d64b..b928e33 100644 --- a/lib/Icecave/Duct/Detail/TokenStreamParser.php +++ b/lib/Icecave/Duct/Detail/TokenStreamParser.php @@ -34,6 +34,8 @@ public function reset() * Feed tokens to the parser. * * @param mixed $tokens The sequence of tokens. + * + * @throws Exception\ParserException */ public function feed($tokens) { @@ -61,7 +63,7 @@ public function finalize() /** * @param Token $token */ - private function feedToken(Token $token) + public function feedToken(Token $token) { if (!$this->stack->isEmpty()) { switch ($this->stack->next()->state) { diff --git a/lib/Icecave/Duct/EventedParser.php b/lib/Icecave/Duct/EventedParser.php index fa63059..f9cf74a 100644 --- a/lib/Icecave/Duct/EventedParser.php +++ b/lib/Icecave/Duct/EventedParser.php @@ -2,6 +2,7 @@ namespace Icecave\Duct; use Evenement\EventEmitterInterface; +use Exception; use Icecave\Duct\Detail\Lexer; use Icecave\Duct\Detail\TokenStreamParser; use Icecave\Duct\TypeCheck\TypeCheck; @@ -24,6 +25,39 @@ public function __construct(Lexer $lexer = null, TokenStreamParser $parser = nul parent::__construct($lexer, $parser); } + /** + * Feed (potentially incomplete) JSON data to the parser. + * + * @param string $buffer The JSON data. + * @throws Exception\SyntaxExceptionInterface + */ + public function feed($buffer) + { + $this->typeCheck->feed(func_get_args()); + + try { + parent::feed($buffer); + } catch (Exception $e) { + $this->emit('error', array($e)); + } + } + + /** + * Finalize parsing. + * + * @throws Exception\SyntaxExceptionInterface + */ + public function finalize() + { + $this->typeCheck->finalize(func_get_args()); + + try { + parent::finalize(); + } catch (Exception $e) { + $this->emit('error', array($e)); + } + } + /** * @param string $event * @param callable $listener diff --git a/lib/Icecave/Duct/Parser.php b/lib/Icecave/Duct/Parser.php index a601a5a..1f6aa47 100644 --- a/lib/Icecave/Duct/Parser.php +++ b/lib/Icecave/Duct/Parser.php @@ -36,8 +36,8 @@ public function __construct(Lexer $lexer = null, TokenStreamParser $parser = nul * * @param string $buffer The JSON data. * - * @return Vector The sequence of parsed JSON values. - * @throws Exception\ParserException Indicates that the JSON stream terminated midway through a JSON value. + * @return Vector The sequence of parsed JSON values. + * @throws Exception\SyntaxExceptionInterface */ public function parse($buffer) { diff --git a/test/suite/Icecave/Duct/Detail/LexerTest.php b/test/suite/Icecave/Duct/Detail/LexerTest.php index c35cef0..8e9d03d 100644 --- a/test/suite/Icecave/Duct/Detail/LexerTest.php +++ b/test/suite/Icecave/Duct/Detail/LexerTest.php @@ -1,6 +1,7 @@ lexer = new Lexer; + $this->tokens = new Vector; + + $this->lexer->on( + 'token', + array($this->tokens, 'pushBack') + ); } public function testFeedEmitsIntegerAfterNonDigit() { $this->lexer->feed(' 1 '); - $tokens = $this->lexer->tokens(); - $this->assertSame(TokenType::NUMBER_LITERAL(), $tokens->back()->type()); - $this->assertSame(1, $tokens->back()->value()); + $this->assertSame(TokenType::NUMBER_LITERAL(), $this->tokens->back()->type()); + $this->assertSame(1, $this->tokens->back()->value()); } public function testFeedEmitsMultipleIntegers() { $this->lexer->feed(' 1 2 '); - $tokens = $this->lexer->tokens(); - $this->assertSame(1, $tokens[0]->value()); - $this->assertSame(2, $tokens[1]->value()); + $this->assertSame(1, $this->tokens[0]->value()); + $this->assertSame(2, $this->tokens[1]->value()); } public function testFeedIntegerThenNonInteger() { $this->lexer->feed(' 1{ '); - $tokens = $this->lexer->tokens(); - $this->assertSame(1, $tokens[0]->value()); - $this->assertSame('{', $tokens[1]->value()); + $this->assertSame(1, $this->tokens[0]->value()); + $this->assertSame('{', $this->tokens[1]->value()); } public function testFeedZeroIntegerThenNonInteger() { $this->lexer->feed(' 0{ '); - $tokens = $this->lexer->tokens(); - $this->assertSame(0, $tokens[0]->value()); - $this->assertSame('{', $tokens[1]->value()); + $this->assertSame(0, $this->tokens[0]->value()); + $this->assertSame('{', $this->tokens[1]->value()); } public function testFeedFloatThenNonInteger() { $this->lexer->feed(' 1.1{ '); - $tokens = $this->lexer->tokens(); - $this->assertSame(1.1, $tokens[0]->value()); - $this->assertSame('{', $tokens[1]->value()); + $this->assertSame(1.1, $this->tokens[0]->value()); + $this->assertSame('{', $this->tokens[1]->value()); } public function testFeedExponentThenNonInteger() { $this->lexer->feed(' 1e5{ '); - $tokens = $this->lexer->tokens(); - $this->assertSame(1e5, $tokens[0]->value()); - $this->assertSame('{', $tokens[1]->value()); + $this->assertSame(1e5, $this->tokens[0]->value()); + $this->assertSame('{', $this->tokens[1]->value()); } public function testFeedFailsOnInvalidBeginningCharacter() @@ -156,11 +157,11 @@ public function testFeedPartialMultibyteCharacter() { $this->lexer->feed("\"\xc3"); - $this->assertTrue($this->lexer->tokens()->isEmpty()); + $this->assertTrue($this->tokens->isEmpty()); $this->lexer->feed("\xb6\""); - $this->assertSame("\xc3\xb6", $this->lexer->tokens()->back()->value()); + $this->assertSame("\xc3\xb6", $this->tokens->back()->value()); } /** @@ -193,11 +194,12 @@ public function partialLiterals() */ public function testLexWithSingleToken($json, $expectedToken) { - $tokens = $this->lexer->lex($json); - $this->assertInstanceOf('Icecave\Collections\Vector', $tokens); - $this->assertSame(1, $tokens->size()); - $this->assertEquals($expectedToken, $tokens->back()); - $this->assertSame($expectedToken->value(), $tokens->back()->value()); + $this->lexer->feed($json); + $this->lexer->finalize(); + + $this->assertSame(1, $this->tokens->size()); + $this->assertEquals($expectedToken, $this->tokens->back()); + $this->assertSame($expectedToken->value(), $this->tokens->back()->value()); } public function singleTokens() diff --git a/test/suite/Icecave/Duct/EventedParserTest.php b/test/suite/Icecave/Duct/EventedParserTest.php index c76fa5a..05ba434 100644 --- a/test/suite/Icecave/Duct/EventedParserTest.php +++ b/test/suite/Icecave/Duct/EventedParserTest.php @@ -1,6 +1,7 @@ parser = new EventedParser(null, $this->tokenStreamParser); } + public function testFeed() + { + $this->parser->feed('[]'); + + Phake::verify($this->tokenStreamParser)->emit('document', array(array())); + } + + public function testFeedFailure() + { + $this->parser->feed('{ 1 :'); + + $arguments = null; + Phake::verify($this->tokenStreamParser)->emit('error', Phake::capture($arguments)); + + $expected = array( + new ParserException('Unexpected token "NUMBER_LITERAL" in state "OBJECT_KEY".') + ); + + $this->assertEquals($expected, $arguments); + } + + public function testFinalize() + { + $this->parser->feed('10'); + + Phake::verify($this->tokenStreamParser, Phake::never())->emit(Phake::anyParameters()); + + $this->parser->finalize(); + + Phake::verify($this->tokenStreamParser)->emit('document', array(10)); + } + + public function testFinalizeFailure() + { + $this->parser->feed('{ 1'); + $this->parser->finalize(); + + $arguments = null; + Phake::verify($this->tokenStreamParser)->emit('error', Phake::capture($arguments)); + + $expected = array( + new ParserException('Unexpected token "NUMBER_LITERAL" in state "OBJECT_KEY".') + ); + + $this->assertEquals($expected, $arguments); + } + public function testOn() { $callback = function() {}; diff --git a/test/suite/Icecave/Duct/ParserTest.php b/test/suite/Icecave/Duct/ParserTest.php index 846e11e..6612834 100644 --- a/test/suite/Icecave/Duct/ParserTest.php +++ b/test/suite/Icecave/Duct/ParserTest.php @@ -1,6 +1,7 @@ assertSame(array(1, 2, 3), $result->front()); } + public function testFeedFailure() + { + $this->setExpectedException('Icecave\Duct\Detail\Exception\ParserException', 'Unexpected token "BRACKET_CLOSE".'); + + try { + $this->parser->feed(']'); + } catch (Exception $e) { + Phake::verify($this->parser)->reset(); + throw $e; + } + } + + public function testFinalizeFailure() + { + $this->setExpectedException('Icecave\Duct\Detail\Exception\ParserException', 'Unexpected token "NUMBER_LITERAL" in state "OBJECT_KEY".'); + + try { + $this->parser->feed('{ 1'); + $this->parser->finalize(); + } catch (Exception $e) { + Phake::verify($this->parser)->reset(); + throw $e; + } + } + /** * @dataProvider parseData */