Skip to content

Instantly share code, notes, and snippets.

@thekid
Created February 23, 2023 15:00
Show Gist options
  • Save thekid/ca535649f521cda2408948a7137223e9 to your computer and use it in GitHub Desktop.
Save thekid/ca535649f521cda2408948a7137223e9 to your computer and use it in GitHub Desktop.
xp-forge/address: JSON iteration
<?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());
}
}
<?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