Skip to content

Commit 7470b80

Browse files
authored
Merge pull request #97 from Bouncey/feature/driver-parity-tests
Reworked the HTTP formatter to correctly interpret maps and lists.
2 parents 8100251 + a765fe5 commit 7470b80

15 files changed

+1302
-625
lines changed

src/Contracts/FormatterInterface.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Laudis\Neo4j\Types\CypherList;
2020
use Psr\Http\Message\RequestInterface;
2121
use Psr\Http\Message\ResponseInterface;
22+
use stdClass;
2223

2324
/**
2425
* A formatter (aka Hydrator) is reponsible for formatting the incoming results of the driver.
@@ -82,14 +83,13 @@ public function formatBoltResult(array $meta, array $results, ConnectionInterfac
8283
/**
8384
* Formats the results of the HTTP protocol to the unified format.
8485
*
85-
* @param CypherResponseSet $body
8686
* @param iterable<Statement> $statements
8787
*
8888
* @throws JsonException
8989
*
9090
* @return CypherList<ResultFormat>
9191
*/
92-
public function formatHttpResult(ResponseInterface $response, array $body, ConnectionInterface $connection, float $resultsAvailableAfter, float $resultsConsumedAfter, iterable $statements): CypherList;
92+
public function formatHttpResult(ResponseInterface $response, stdClass $body, ConnectionInterface $connection, float $resultsAvailableAfter, float $resultsConsumedAfter, iterable $statements): CypherList;
9393

9494
/**
9595
* Decorates a request to make make sure it requests the correct format.

src/Formatter/BasicFormatter.php

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,12 @@
2727
use Laudis\Neo4j\Types\CypherMap;
2828
use Psr\Http\Message\RequestInterface;
2929
use Psr\Http\Message\ResponseInterface;
30+
use stdClass;
3031
use UnexpectedValueException;
3132

3233
/**
3334
* Formats the result in basic CypherLists and CypherMaps. All cypher types are erased so that the map only contains scalar, null or array values.
3435
*
35-
* @psalm-import-type CypherError from \Laudis\Neo4j\Contracts\FormatterInterface
36-
* @psalm-import-type CypherRowResponse from \Laudis\Neo4j\Contracts\FormatterInterface
37-
* @psalm-import-type CypherResponse from \Laudis\Neo4j\Contracts\FormatterInterface
38-
* @psalm-import-type CypherResponseSet from \Laudis\Neo4j\Contracts\FormatterInterface
39-
*
4036
* @psalm-type BasicResults = CypherList<CypherMap<scalar|array|null>>
4137
*
4238
* @implements FormatterInterface<BasicResults>
@@ -74,36 +70,40 @@ public function formatBoltResult(array $meta, array $results, ?ConnectionInterfa
7470
return new CypherList($tbr);
7571
}
7672

77-
public function formatHttpResult(ResponseInterface $response, array $body, ?ConnectionInterface $connection = null, ?float $resultsAvailableAfter = null, ?float $resultsConsumedAfter = null, ?iterable $statements = null): CypherList
73+
public function formatHttpResult(ResponseInterface $response, stdClass $body, ?ConnectionInterface $connection = null, ?float $resultsAvailableAfter = null, ?float $resultsConsumedAfter = null, ?iterable $statements = null): CypherList
7874
{
7975
/** @var list<CypherList<CypherMap<scalar|array|null>>> */
8076
$tbr = [];
8177

82-
foreach ($body['results'] as $results) {
78+
/** @var stdClass $results */
79+
foreach ($body->results as $results) {
8380
$tbr[] = $this->buildResult($results);
8481
}
8582

8683
return new CypherList($tbr);
8784
}
8885

8986
/**
90-
* @psalm-param CypherResponse $result
91-
*
9287
* @return CypherList<CypherMap<scalar|array|null>>
9388
*/
94-
private function buildResult(array $result): CypherList
89+
private function buildResult(stdClass $result): CypherList
9590
{
9691
/** @var list<CypherMap<scalar|array|null>> */
9792
$tbr = [];
9893

99-
$columns = $result['columns'];
100-
foreach ($result['data'] as $dataRow) {
101-
$row = $dataRow['row'];
94+
/** @var list<string> $columns */
95+
$columns = (array) $result->columns;
96+
/** @var stdClass $dataRow */
97+
foreach ($result->data as $dataRow) {
10298
/** @var array<string, scalar|array|null> $map */
10399
$map = [];
104-
$vector = $row;
100+
/** @var list<stdClass|scalar|array|null> */
101+
$vector = $dataRow->row;
105102
foreach ($columns as $index => $key) {
106-
$map[$key] = $vector[$index];
103+
// Removes the stdClasses from the json objects
104+
/** @var scalar|array|null */
105+
$decoded = json_decode(json_encode($vector[$index], JSON_THROW_ON_ERROR), true, 512, JSON_THROW_ON_ERROR);
106+
$map[$key] = $decoded;
107107
}
108108
$tbr[] = new CypherMap($map);
109109
}

src/Formatter/OGMFormatter.php

Lines changed: 11 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,16 @@
1515

1616
use function array_slice;
1717
use function count;
18-
use Exception;
19-
use function is_array;
20-
use function is_string;
2118
use Laudis\Neo4j\Contracts\ConnectionInterface;
2219
use Laudis\Neo4j\Contracts\FormatterInterface;
2320
use Laudis\Neo4j\Databags\Statement;
2421
use Laudis\Neo4j\Formatter\Specialised\BoltOGMTranslator;
25-
use Laudis\Neo4j\Formatter\Specialised\HttpOGMArrayTranslator;
26-
use Laudis\Neo4j\Formatter\Specialised\HttpOGMStringTranslator;
22+
use Laudis\Neo4j\Formatter\Specialised\HttpOGMTranslator;
2723
use Laudis\Neo4j\Types\CypherList;
2824
use Laudis\Neo4j\Types\CypherMap;
2925
use Psr\Http\Message\RequestInterface;
3026
use Psr\Http\Message\ResponseInterface;
27+
use stdClass;
3128

3229
/**
3330
* Formats the result in a basic OGM (Object Graph Mapping) format by mapping al cypher types to types found in the \Laudis\Neo4j\Types namespace.
@@ -38,13 +35,6 @@
3835
*
3936
* @psalm-type OGMResults = CypherList<CypherMap<OGMTypes>>
4037
*
41-
* @psalm-import-type NodeArray from \Laudis\Neo4j\Formatter\Specialised\HttpOGMArrayTranslator
42-
* @psalm-import-type MetaArray from \Laudis\Neo4j\Formatter\Specialised\HttpOGMArrayTranslator
43-
* @psalm-import-type RelationshipArray from \Laudis\Neo4j\Formatter\Specialised\HttpOGMArrayTranslator
44-
*
45-
* @psalm-type CypherResultDataRow = list<array{row: list<scalar|array|null>, meta: MetaArray, graph: array{nodes: list<NodeArray>, relationships: list<RelationshipArray>}}>
46-
* @psalm-type CypherResult = array{columns: list<string>, data: CypherResultDataRow}
47-
*
4838
* @psalm-import-type BoltMeta from \Laudis\Neo4j\Contracts\FormatterInterface
4939
*
5040
* @implements FormatterInterface<CypherList<CypherMap<OGMTypes>>>
@@ -54,14 +44,12 @@
5444
final class OGMFormatter implements FormatterInterface
5545
{
5646
private BoltOGMTranslator $boltTranslator;
57-
private HttpOGMArrayTranslator $arrayTranslator;
58-
private HttpOGMStringTranslator $stringTranslator;
47+
private HttpOGMTranslator $httpTranslator;
5948

60-
public function __construct(BoltOGMTranslator $boltTranslator, HttpOGMArrayTranslator $arrayTranslator, HttpOGMStringTranslator $stringTranslator)
49+
public function __construct(BoltOGMTranslator $boltTranslator, HttpOGMTranslator $httpTranslator)
6150
{
6251
$this->boltTranslator = $boltTranslator;
63-
$this->arrayTranslator = $arrayTranslator;
64-
$this->stringTranslator = $stringTranslator;
52+
$this->httpTranslator = $httpTranslator;
6553
}
6654

6755
/**
@@ -71,11 +59,7 @@ public function __construct(BoltOGMTranslator $boltTranslator, HttpOGMArrayTrans
7159
*/
7260
public static function create(): OGMFormatter
7361
{
74-
return new self(
75-
new BoltOGMTranslator(),
76-
new HttpOGMArrayTranslator(),
77-
new HttpOGMStringTranslator()
78-
);
62+
return new self(new BoltOGMTranslator(), new HttpOGMTranslator());
7963
}
8064

8165
/**
@@ -98,59 +82,15 @@ public function formatBoltResult(array $meta, array $results, ConnectionInterfac
9882
return new CypherList($tbr);
9983
}
10084

101-
public function formatHttpResult(ResponseInterface $response, array $body, ConnectionInterface $connection, float $resultsAvailableAfter, float $resultsConsumedAfter, iterable $statements): CypherList
85+
public function formatHttpResult(ResponseInterface $response, stdClass $body, ConnectionInterface $connection, float $resultsAvailableAfter, float $resultsConsumedAfter, iterable $statements): CypherList
10286
{
10387
/** @var list<CypherList<CypherMap<OGMTypes>>> $tbr */
10488
$tbr = [];
10589

106-
foreach ($body['results'] as $results) {
107-
/** @var CypherResult $results */
108-
$tbr[] = $this->buildResult($results);
109-
}
110-
111-
return new CypherList($tbr);
112-
}
113-
114-
/**
115-
* @param CypherResult $result
116-
*
117-
* @throws Exception
118-
*
119-
* @return CypherList<CypherMap<OGMTypes>>
120-
*/
121-
private function buildResult(array $result): CypherList
122-
{
123-
/** @var list<CypherMap<OGMTypes>> $tbr */
124-
$tbr = [];
125-
126-
$columns = $result['columns'];
127-
foreach ($result['data'] as $data) {
128-
$meta = $data['meta'];
129-
$nodes = $data['graph']['nodes'];
130-
$relationship = $data['graph']['relationships'];
131-
$metaIndex = 0;
132-
$relationshipIndex = 0;
133-
134-
/** @var array<string, OGMTypes> $record */
135-
$record = [];
136-
foreach ($data['row'] as $i => $value) {
137-
if (is_array($value)) {
138-
$translation = $this->arrayTranslator->translate($meta, $relationship, $metaIndex, $relationshipIndex, $nodes, $value);
139-
140-
$relationshipIndex += $translation[1];
141-
$metaIndex += $translation[0];
142-
$record[$columns[$i]] = $translation[2];
143-
} elseif (is_string($value)) {
144-
[$metaIncrement, $translation] = $this->stringTranslator->translate($metaIndex, $meta, $value);
145-
$metaIndex += $metaIncrement;
146-
$record[$columns[$i]] = $translation;
147-
} else {
148-
$record[$columns[$i]] = $value;
149-
++$metaIndex;
150-
}
151-
}
152-
153-
$tbr[] = new CypherMap($record);
90+
/** @var list<stdClass> $results */
91+
$results = $body->results;
92+
foreach ($results as $result) {
93+
$tbr[] = $this->httpTranslator->translateResult($result);
15494
}
15595

15696
return new CypherList($tbr);
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Laudis Neo4j package.
5+
*
6+
* (c) Laudis technologies <http://laudis.tech>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Laudis\Neo4j\Formatter\Specialised;
13+
14+
use function is_array;
15+
use stdClass;
16+
17+
/**
18+
* @psalm-immutable
19+
*/
20+
final class HttpMetaInfo
21+
{
22+
/** @var list<stdClass|list<stdClass>> */
23+
private array $meta;
24+
/** @var list<stdClass> */
25+
private array $nodes;
26+
/** @var list<stdClass> */
27+
private array $relationships;
28+
private int $currentMeta;
29+
30+
/**
31+
* @param list<stdClass> $relationships
32+
* @param list<stdClass> $meta
33+
* @param list<stdClass> $nodes
34+
*/
35+
public function __construct(
36+
array $meta,
37+
array $nodes,
38+
array $relationships,
39+
int $currentMeta = 0
40+
) {
41+
$this->meta = $meta;
42+
$this->nodes = $nodes;
43+
$this->relationships = $relationships;
44+
$this->currentMeta = $currentMeta;
45+
}
46+
47+
/**
48+
* @pure
49+
*/
50+
public static function createFromData(stdClass $data): self
51+
{
52+
/** @var stdClass */
53+
$graph = $data->graph;
54+
55+
/** @psalm-suppress MixedArgument */
56+
return new self($data->meta, $graph->nodes, $graph->relationships);
57+
}
58+
59+
/**
60+
* @return stdClass|list<stdClass>|null
61+
*/
62+
public function currentMeta()
63+
{
64+
return $this->meta[$this->currentMeta] ?? null;
65+
}
66+
67+
public function currentNode(): ?stdClass
68+
{
69+
$meta = $this->currentMeta();
70+
if ($meta === null || is_array($meta)) {
71+
return null;
72+
}
73+
74+
foreach ($this->nodes as $node) {
75+
if ((int) $node->id === $meta->id) {
76+
return $node;
77+
}
78+
}
79+
80+
return null;
81+
}
82+
83+
public function getCurrentRelationship(): ?stdClass
84+
{
85+
$meta = $this->currentMeta();
86+
if ($meta === null || is_array($meta)) {
87+
return null;
88+
}
89+
90+
foreach ($this->relationships as $relationship) {
91+
if ((int) $relationship->id === $meta->id) {
92+
return $relationship;
93+
}
94+
}
95+
96+
return null;
97+
}
98+
99+
public function getCurrentType(): ?string
100+
{
101+
$currentMeta = $this->currentMeta();
102+
if (is_array($currentMeta)) {
103+
return 'path';
104+
}
105+
106+
if ($currentMeta === null) {
107+
return null;
108+
}
109+
110+
/** @var string */
111+
return $currentMeta->type;
112+
}
113+
114+
public function withNestedMeta(): self
115+
{
116+
$tbr = clone $this;
117+
118+
$currentMeta = $this->currentMeta();
119+
if (is_array($currentMeta)) {
120+
$tbr->meta = $currentMeta;
121+
$tbr->currentMeta = 0;
122+
}
123+
124+
return $tbr;
125+
}
126+
127+
public function incrementMeta(): self
128+
{
129+
$tbr = clone $this;
130+
++$tbr->currentMeta;
131+
132+
return $tbr;
133+
}
134+
}

0 commit comments

Comments
 (0)