Skip to content

Commit

Permalink
Improve JSONConverter codebase
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Oct 12, 2024
1 parent b4583da commit 3b0c402
Showing 1 changed file with 88 additions and 78 deletions.
166 changes: 88 additions & 78 deletions src/JsonConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,21 +111,21 @@ final class JsonConverter
private readonly bool $isForceObject;
/** @var non-empty-string */
private readonly string $indentation;
/** @var Closure(string, array-key): string */
private readonly Closure $internalFormatter;
/** @var Closure(array<int, T>): string */
private readonly Closure $bufferEncoder;
/** @var int<1, max> */
public readonly int $chunkSize;

public static function create(): self
{
return new self(flags: 0, depth: 512, indentSize: 4, formatter: null, chunkSize: 500);
return new self(flags: 0, depth: 512, indentSize: 4, formatter: fn (mixed $value, int|string $offset) => $value, chunkSize: 500);
}

/**
* @param int<1, max> $depth
* @param int<1, max> $indentSize
*/
private function __construct(int $flags, int $depth, int $indentSize, ?Closure $formatter, int $chunkSize)
private function __construct(int $flags, int $depth, int $indentSize, Closure $formatter, int $chunkSize)
{
json_encode([], $flags & ~JSON_THROW_ON_ERROR, $depth);

Expand All @@ -136,28 +136,85 @@ private function __construct(int $flags, int $depth, int $indentSize, ?Closure $
$this->flags = $flags;
$this->depth = $depth;
$this->indentSize = $indentSize;
$this->indentation = str_repeat(' ', $indentSize);
$this->formatter = $formatter ?? fn (mixed $value) => $value;
$this->isPrettyPrint = ($this->flags & JSON_PRETTY_PRINT) === JSON_PRETTY_PRINT;
$this->isForceObject = ($this->flags & JSON_FORCE_OBJECT) === JSON_FORCE_OBJECT;
$this->internalFormatter = $this->setInternalFormatter();
$this->formatter = $formatter;
$this->chunkSize = $chunkSize;

// Initialize private properties used for conversion.
// To optimize encoding everything is calculated in
// the constructor to reduce conditions
$this->indentation = str_repeat(' ', $this->indentSize);
$this->isPrettyPrint = $this->useFlags(JSON_PRETTY_PRINT);
$this->isForceObject = $this->useFlags(JSON_FORCE_OBJECT);
$this->bufferEncoder = $this->setBufferEncoder();
}

/**
* @return Closure(string, array-key): string
* Returns the effective encoder for the iterable structure.
*
* @return Closure(array<int, T>): string
*/
private function setInternalFormatter(): Closure
private function setBufferEncoder(): Closure
{
$callback = match ($this->isForceObject) {
false => fn (string $json, int|string $offset): string => $json,
default => fn (string $json, int|string $offset): string => '"'.json_encode($offset).'":'.$json,
$internalFormatter = $this->isPrettyPrint ? $this->prettyPrint(...) : fn (string $json): string => $json;
$bufferFormatter = match ($this->useFlags(JSON_FORCE_OBJECT)) {
true => function (array $value): array {
$data = [];
foreach ($value as $offset => $item) {
$data[$offset] = ($this->formatter)($item, $offset);
}

return $data;
},
false => function (array $value): array {
$data = [];
foreach ($value as $offset => $item) {
$data[] = ($this->formatter)($item, $offset);
}

return $data;
},
};

return match ($this->isPrettyPrint) {
false => $callback,
default => fn (string $json, int|string $offset): string => $this->prettyPrint($callback($json, $offset)),
};
return fn (array $buffer) => ($internalFormatter)(substr(
string: json_encode(
value: ($bufferFormatter)($buffer),
flags: ($this->flags & ~JSON_PRETTY_PRINT) | JSON_THROW_ON_ERROR,
depth: $this->depth
),
offset: 1,
length: -1
));
}

/**
* Pretty Print the JSON string without using JSON_PRETTY_PRINT
* The method also allow using an arbitrary length for the indentation.
*/
private function prettyPrint(string $json): string
{
$level = 1;
$inQuotes = false;
$escape = false;
$length = strlen($json);

$str = $this->indentation;
for ($i = 0; $i < $length; $i++) {
$char = $json[$i];
if ('"' === $char && !$escape) {
$inQuotes = !$inQuotes;
}

$escape = '\\' === $char && !$escape;
$str .= $inQuotes ? $char : match ($char) {
'{', '[' => $char."\n".str_repeat($this->indentation, ++$level),
'}', ']' => "\n".str_repeat($this->indentation, --$level).$char,
',' => $char."\n".str_repeat($this->indentation, $level),
':' => $char.' ',
default => $char,
};
}

return $str;
}

/**
Expand Down Expand Up @@ -239,6 +296,9 @@ public function useFlags(int ...$flags): bool
return [] !== $flags;
}

/**
* Sets the json_encode flags.
*/
private function setFlags(int $flags): self
{
return match ($flags) {
Expand Down Expand Up @@ -291,6 +351,8 @@ public function chunkSize(int $chunkSize): self
*/
public function formatter(?Closure $formatter): self
{
$formatter ??= fn (mixed $value, int|string $offset) => $value;

return new self($this->flags, $this->depth, $this->indentSize, $formatter, $this->chunkSize);
}

Expand Down Expand Up @@ -407,84 +469,32 @@ public function convert(iterable $records): Iterator
}

$offset = 0;
$incr = 0;
$buffer = [];
$current = $records->current();
$records->next();

yield $start;

$incr = 0;
$buffer = [];
while ($records->valid()) {
if ($incr === $this->chunkSize) {
yield $this->format($buffer, $offset).$separator;
yield ($this->bufferEncoder)($buffer).$separator;

$incr = 0;
$buffer = [];
}
$incr++;
$buffer[] = $current;

$offset++;
$incr++;
$buffer[$offset] = $current;
$current = $records->current();
$records->next();
}

$last = $this->format($buffer, $offset);
if ('' !== $last) {
yield $last.$separator;
}

yield $this->format([$current], $offset++).$end;
}

/**
* @throws JsonException
*/
private function format(array $value, int $offset): string
{
$data = [];
foreach ($value as $item) {
$data[] = ($this->formatter)($item, $offset);
++$offset;
}

$json = json_encode(
value: $data,
flags: ($this->flags & ~JSON_PRETTY_PRINT) | JSON_THROW_ON_ERROR,
depth: $this->depth
);

return ($this->internalFormatter)(substr($json, 1, -1), $offset);
}

/**
* Pretty Print the JSON string without using JSON_PRETTY_PRINT
* The method also allow using an arbitrary length for the indentation.
*/
private function prettyPrint(string $json): string
{
$level = 1;
$inQuotes = false;
$escape = false;
$length = strlen($json);

$str = $this->indentation;
for ($i = 0; $i < $length; $i++) {
$char = $json[$i];
if ('"' === $char && !$escape) {
$inQuotes = !$inQuotes;
}

$escape = '\\' === $char && !$escape;
$str .= $inQuotes ? $char : match ($char) {
'{', '[' => $char."\n".str_repeat($this->indentation, ++$level),
'}', ']' => "\n".str_repeat($this->indentation, --$level).$char,
',' => $char."\n".str_repeat($this->indentation, $level),
':' => $char.' ',
default => $char,
};
if ([] !== $buffer) {
yield ($this->bufferEncoder)($buffer).$separator;
}

return $str;
yield ($this->bufferEncoder)([$offset => $current]).$end;
}
}

0 comments on commit 3b0c402

Please sign in to comment.