Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 59 additions & 36 deletions src/main/php/text/json/Format.class.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php namespace text\json;

use StdClass;
use StdClass, Traversable;
use lang\{IllegalArgumentException, Value};

/**
Expand Down Expand Up @@ -81,52 +81,75 @@ public function close($token) {
* @return string
*/
public function representationOf($value) {
$t= gettype($value);
if ('string' === $t) {
return json_encode($value, $this->options);
} else if ('integer' === $t) {
return (string)$value;
} else if ('double' === $t) {
$r= '';
foreach ($this->tokensOf($value) as $bytes) {
$r.= $bytes;
}
return $r;
}

/**
* Yields tokens for a given value
*
* @param var $value
* @return iterable
*/
public function tokensOf($value) {
if (is_string($value)) {
yield json_encode($value, $this->options);
} else if (is_int($value)) {
yield (string)$value;
} else if (is_float($value)) {
$cast= (string)$value;
return strpos($cast, '.') ? $cast : $cast.'.0';
} else if ('array' === $t) {
yield strpos($cast, '.') ? $cast : $cast.'.0';
} else if (is_array($value)) {
if (empty($value)) {
return '[]';
yield '[]';
} else if (0 === key($value)) {
$r= $this->open('[');
$next= false;
yield $this->open('[');
$i= 0;
foreach ($value as $element) {
if ($next) {
$r.= $this->comma;
} else {
$next= true;
}
$r.= $this->representationOf($element);
if ($i++) yield $this->comma;
yield from $this->tokensOf($element);
}
return $r.$this->close(']');
} else { map:
$r= $this->open('{');
$next= false;
foreach ($value as $key => $mapped) {
if ($next) {
$r.= $this->comma;
} else {
$next= true;
}
$r.= $this->representationOf($key).$this->colon.$this->representationOf($mapped);
yield $this->close(']');
} else {
map: yield $this->open('{');
$i= 0;
foreach ($value as $key => $element) {
if ($i++) yield $this->comma;
yield from $this->tokensOf((string)$key);
yield $this->colon;
yield from $this->tokensOf($element);
}
return $r.$this->close('}');
yield $this->close('}');
}
} else if (null === $value) {
return 'null';
yield 'null';
} else if (true === $value) {
return 'true';
yield 'true';
} else if (false === $value) {
return 'false';
} else if ($value instanceof StdClass) {
$value= (array)$value;
if (empty($value)) return '{}';
yield 'false';
} else if ($value instanceof JsonObject || $value instanceof StdClass) {
goto map;
} else if ($value instanceof Traversable) {
$i= 0;
$map= null;
foreach ($value as $key => $element) {
if (0 === $i++) {
$map= 0 !== $key;
yield $this->open($map ? '{' : '[');
} else {
yield $this->comma;
}

if ($map) {
yield from $this->tokensOf((string)$key);
yield $this->colon;
}
yield from $this->tokensOf($element);
}
yield null === $map ? '[]' : $this->close($map ? '}' : ']');
} else {
throw new IllegalArgumentException('Cannot represent instances of '.typeof($value));
}
Expand Down
7 changes: 4 additions & 3 deletions src/main/php/text/json/Input.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,13 @@ protected function escaped($pos, $len, &$offset) {
protected function readObject($nesting) {
$token= $this->nextToken();
if ('}' === $token) {
return [];
return new JsonObject();
} else if (null !== $token) {
$result= [];
if (++$nesting > $this->maximumNesting) {
throw new FormatException('Nesting level too deep');
}

$result= [];
do {
$key= $this->valueOf($token, $nesting);
if (!is_string($key)) {
Expand All @@ -113,7 +114,7 @@ protected function readObject($nesting) {
if (',' === $delim) {
continue;
} else if ('}' === $delim) {
return $result;
return new JsonObject($result);
} else {
throw new FormatException('Unexpected '.Objects::stringOf($delim).', expecting "," or "}"');
}
Expand Down
53 changes: 53 additions & 0 deletions src/main/php/text/json/JsonObject.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php namespace text\json;

use ArrayAccess, ArrayIterator, Countable, IteratorAggregate, ReturnTypeWillChange;
use lang\Value;
use util\Objects;

/** @test text.json.JsonObjectTest */
class JsonObject implements ArrayAccess, Countable, IteratorAggregate, Value {
private $backing;

/** @param [:var] $backing */
public function __construct($backing= []) {
$this->backing= $backing;
}

#[ReturnTypeWillChange]
public function count() { return sizeof($this->backing); }

#[ReturnTypeWillChange]
public function offsetGet($key) { return $this->backing[$key]; }

#[ReturnTypeWillChange]
public function offsetSet($key, $value) { $this->backing[$key]= $value; }

#[ReturnTypeWillChange]
public function offsetExists($key) { return isset($this->backing[$key]); }

#[ReturnTypeWillChange]
public function offsetUnset($key) { unset($this->backing[$key]); }

#[ReturnTypeWillChange]
public function getIterator() { return new ArrayIterator($this->backing); }

/** @return string */
public function toString() {
return '(object)'.Objects::stringOf($this->backing);
}

/** @return string */
public function hashCode() {
return 'J'.Objects::hashOf($this->backing);
}

/**
* Comparison
*
* @param var $value
* @return int
*/
public function compareTo($value) {
return $value instanceof self ? Objects::compare($this->backing, $value->backing) : 1;
}
}
26 changes: 2 additions & 24 deletions src/main/php/text/json/Output.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,30 +22,8 @@ public function __construct($format= null) {
* @return self
*/
public function write($value) {
$f= $this->format;
if ($value instanceof Traversable || is_array($value)) {
$i= 0;
$map= null;
foreach ($value as $key => $element) {
if (0 === $i++) {
$map= 0 !== $key;
$this->appendToken($f->open($map ? '{' : '['));
} else {
$this->appendToken($f->comma);
}

if ($map) {
$this->appendToken($f->representationOf((string)$key).$f->colon);
}
$this->write($element);
}
if (null === $map) {
$this->appendToken('[]');
} else {
$this->appendToken($f->close($map ? '}' : ']'));
}
} else {
$this->appendToken($f->representationOf($value));
foreach ($this->format->tokensOf($value) as $token) {
$this->appendToken($token);
}
return $this;
}
Expand Down
19 changes: 17 additions & 2 deletions src/test/php/text/json/unittest/FormatTest.class.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
<?php namespace text\json\unittest;

use test\{Assert, Before, Test, Values};
use text\json\Format;
use test\Assert;
use test\Test;

abstract class FormatTest {
protected $format;

/** @return iterable */
protected function singleTokens() {
yield [true, ['true']];
yield [false, ['false']];
yield [null, ['null']];
yield [6100, ['6100']];
yield [0.5, ['0.5']];
yield ['Test', ['"Test"']];
yield [[], ['[]']];
}

/**
* Returns a `Format` instance
Expand Down Expand Up @@ -96,4 +107,8 @@ public abstract function object_with_one_pair();
#[Test]
public abstract function object_with_multiple_pairs();

#[Test, Values(from: 'singleTokens')]
public function iterate_single_tokens($value, $expected) {
Assert::equals($expected, iterator_to_array($this->format()->tokensOf($value)));
}
}
23 changes: 14 additions & 9 deletions src/test/php/text/json/unittest/JsonInputTest.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use io\streams\MemoryInputStream;
use lang\FormatException;
use test\{Assert, Expect, Test, Values};
use text\json\Types;
use text\json\{Types, JsonObject};
use util\collections\Pair;

/**
Expand Down Expand Up @@ -118,32 +118,37 @@ public function read_keyword($expected, $source) {

#[Test, Values(['{}', '{ }'])]
public function read_empty_object($source) {
Assert::equals([], $this->read($source));
Assert::equals(new JsonObject([]), $this->read($source));
}

#[Test, Values(['{"key": "value"}', '{"key" : "value"}', '{ "key" : "value" }'])]
public function read_key_value_pair($source) {
Assert::equals(['key' => 'value'], $this->read($source));
Assert::equals(new JsonObject(['key' => 'value']), $this->read($source));
}

#[Test, Values(['{"a": "v1", "b": "v2"}', '{"a" : "v1", "b" : "v2"}', '{ "a" : "v1" , "b" : "v2" }'])]
public function read_key_value_pairs($source) {
Assert::equals(['a' => 'v1', 'b' => 'v2'], $this->read($source));
Assert::equals(new JsonObject(['a' => 'v1', 'b' => 'v2']), $this->read($source));
}

#[Test, Values(['{"": "value"}', '{"" : "value"}', '{ "" : "value" }'])]
public function empty_key($source) {
Assert::equals(['' => 'value'], $this->read($source));
Assert::equals(new JsonObject(['' => 'value']), $this->read($source));
}

#[Test]
public function keys_overwrite_each_other() {
Assert::equals(['key' => 'v2'], $this->read('{"key": "v1", "key": "v2"}'));
Assert::equals(new JsonObject(['key' => 'v2']), $this->read('{"key": "v1", "key": "v2"}'));
}

#[Test]
public function object_ending_with_zero() {
Assert::equals(['key' => 0], $this->read('{"key": 0}'));
Assert::equals(new JsonObject(['key' => 0]), $this->read('{"key": 0}'));
}

#[Test]
public function array_access() {
Assert::equals('value', $this->read('{"key" : "value"}')['key']);
}

#[Test, Expect(FormatException::class), Values(['{', '{{', '{{}', '}', '}}'])]
Expand Down Expand Up @@ -279,11 +284,11 @@ public function files_typically_end_with_trailing_newline() {
#[Test]
public function indented_json() {
Assert::equals(
[
new JsonObject([
'color' => 'green',
'sizes' => ['S', 'M', 'L', 'XL'],
'price' => 12.99
],
]),
$this->read('{
"color" : "green",
"sizes" : [ "S", "M", "L", "XL" ],
Expand Down
Loading
Loading