Created
February 23, 2023 15:00
-
-
Save thekid/ca535649f521cda2408948a7137223e9 to your computer and use it in GitHub Desktop.
xp-forge/address: JSON iteration
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php namespace util\address; | |
use IteratorAggregate, Traversable; | |
use io\streams\InputStream; | |
use text\StreamTokenizer; | |
use lang\FormatException; | |
class JsonIterator implements IteratorAggregate { | |
const SEPARATOR= '/'; | |
private $input; | |
private $path= null; | |
/** | |
* Creates a new JSON iterator on a given stream | |
* | |
* @param io.streams.InputStream $input If seekable, this iterator will be rewindable. | |
*/ | |
public function __construct(InputStream $input) { | |
$this->input= new StreamTokenizer($input, ":{}[]\"\r\n\t ", true); | |
} | |
public function rewind() { | |
$this->input->reset(); | |
} | |
protected function nextToken($delimiters= null) { | |
do { | |
$token= $this->input->nextToken($delimiters ?? $this->input->delimiters); | |
} while (0 === strcspn($token, "\r\n\t ")); | |
return $token; | |
} | |
protected function nextString() { | |
$string= $this->nextToken('"'); // TBI: Escaping | |
$this->nextToken('"'); | |
return $string; | |
} | |
protected function nextValue($token) { | |
if ('{' === $token) { | |
yield $this->path.self::SEPARATOR => null; | |
$next= $this->nextToken(); | |
while ('}' !== $next && $this->input->hasMoreTokens()) { | |
$this->path.= self::SEPARATOR.$this->nextString(); | |
$this->nextToken(':'); | |
foreach ($this->nextValue($this->nextToken()) as $value) { | |
yield $this->path => $value; | |
} | |
$this->path= substr($this->path, 0, strrpos($this->path, self::SEPARATOR)); | |
if (',' === ($next= $this->nextToken(',}'))) { | |
$next= $this->nextToken(); | |
} | |
} | |
} else if ('[' === $token) { | |
yield $this->path.self::SEPARATOR => null; | |
$next= $this->nextToken(); | |
$i= 0; | |
while (']' !== $next && $this->input->hasMoreTokens()) { | |
$this->path.= self::SEPARATOR.'['.($i++).']'; | |
foreach ($this->nextValue($next) as $value) { | |
yield $this->path => $value; | |
} | |
$this->path= substr($this->path, 0, strrpos($this->path, self::SEPARATOR)); | |
if (',' === ($next= $this->nextToken(',]'))) { | |
$next= $this->nextToken(); | |
} | |
} | |
} else if ('"' === $token) { | |
yield $this->nextString(); | |
} else if ('null' === $token) { | |
return null; | |
} else if ('false' === $token) { | |
return false; | |
} else if ('true' === $token) { | |
return true; | |
} else if (0 === strcspn($token, '.0123456789eE+-')) { | |
yield false === strpos($token, '.') ? (int)$token : (float)$token; | |
} else { | |
throw new FormatException('Unexpected token `'.$token.'`'); | |
} | |
} | |
public function getIterator(): Traversable { | |
return $this->nextValue($this->nextToken()); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php namespace util\address\unittest; | |
use io\streams\{InputStream, MemoryInputStream}; | |
use lang\{IllegalStateException, FormatException}; | |
use unittest\Assert; | |
use unittest\{Test, Values}; | |
use util\address\JsonIterator; | |
class JsonIteratorTest { | |
/** | |
* Assert iteration result | |
* | |
* @param [:var][] $expected | |
* @param util.data.JsonIterator $fixture | |
*/ | |
protected function assertIterated($expected, JsonIterator $fixture) { | |
$actual= []; | |
foreach ($fixture as $key => $value) { | |
$actual[]= [$key => $value]; | |
} | |
Assert::equals($expected, $actual); | |
} | |
#[Test] | |
public function can_create() { | |
new JsonIterator(new MemoryInputStream('{}')); | |
} | |
#[Test] | |
public function string() { | |
$this->assertIterated( | |
[['Test']], | |
new JsonIterator(new MemoryInputStream('"Test"')) | |
); | |
} | |
#[Test] | |
public function empty_map() { | |
$this->assertIterated( | |
[['/' => null]], | |
new JsonIterator(new MemoryInputStream('{}')) | |
); | |
} | |
#[Test] | |
public function single_pair() { | |
$this->assertIterated( | |
[['/' => null], ['/test' => 'Test']], | |
new JsonIterator(new MemoryInputStream('{"test":"Test"}')) | |
); | |
} | |
#[Test, Values(['{"color":"Green","price":"$12.99"}', '{"color": "Green", "price": "$12.99"}'])] | |
public function two_pairs($input) { | |
$this->assertIterated( | |
[['/' => null], ['/color' => 'Green'], ['/price' => '$12.99']], | |
new JsonIterator(new MemoryInputStream($input)) | |
); | |
} | |
#[Test] | |
public function empty_list() { | |
$this->assertIterated( | |
[['/' => null]], | |
new JsonIterator(new MemoryInputStream('[]')) | |
); | |
} | |
#[Test] | |
public function single_element() { | |
$this->assertIterated( | |
[['/' => null], ['/[0]' => 'Test']], | |
new JsonIterator(new MemoryInputStream('["Test"]')) | |
); | |
} | |
#[Test, Values(['["Color","Price"]', '["Color", "Price"]'])] | |
public function two_elements($input) { | |
$this->assertIterated( | |
[['/' => null], ['/[0]' => 'Color'], ['/[1]' => 'Price']], | |
new JsonIterator(new MemoryInputStream($input)) | |
); | |
} | |
#[Test] | |
public function map_containing_list() { | |
$this->assertIterated( | |
[['/' => null], ['/items' => null], ['/items/[0]' => 'One'], ['/items/[1]' => 'Two']], | |
new JsonIterator(new MemoryInputStream('{"items":["One","Two"]}')) | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment