diff --git a/src/Internal/functions.php b/src/Internal/functions.php index f51e643..35320a5 100644 --- a/src/Internal/functions.php +++ b/src/Internal/functions.php @@ -2,8 +2,6 @@ namespace Amp\Postgres\Internal; -use function Amp\Postgres\cast; - /** @internal */ const STATEMENT_PARAM_REGEX = <<<'REGEX' [ @@ -92,3 +90,53 @@ function replaceNamedParams(array $params, array $names): array return $values; } + +/** + * @internal + * + * Casts a PHP value to a representation that is understood by Postgres, including encoding arrays. + * + * @throws \Error If $value is an object which is not a BackedEnum or Stringable, a resource, or an unknown type. + */ +function cast(mixed $value): string|int|float|null +{ + return match (\gettype($value)) { + "NULL", "integer", "double", "string" => $value, + "boolean" => $value ? 't' : 'f', + "array" => '{' . \implode(',', \array_map(encodeArrayItem(...), $value)) . '}', + "object" => match (true) { + $value instanceof \BackedEnum => $value->value, + $value instanceof \Stringable => (string) $value, + default => throw new \ValueError( + "An object in parameter values must be a BackedEnum or implement Stringable; got instance of " + . \get_debug_type($value) + ), + }, + default => throw new \ValueError(\sprintf( + "Invalid value type '%s' in parameter values", + \get_debug_type($value), + )), + }; +} + +/** + * @internal + * + * Wraps string in double-quotes for inclusion in an array. + */ +function encodeArrayItem(mixed $value): mixed +{ + return match (\gettype($value)) { + "NULL" => "NULL", + "string" => '"' . \str_replace(['\\', '"'], ['\\\\', '\\"'], $value) . '"', + "object" => match (true) { + $value instanceof \BackedEnum => encodeArrayItem($value->value), + $value instanceof \Stringable => encodeArrayItem((string) $value), + default => throw new \ValueError( + "An object in parameter arrays must be a BackedEnum or implement Stringable; " + . "got instance of " . \get_debug_type($value) + ), + }, + default => cast($value), + }; +} diff --git a/src/functions.php b/src/functions.php index 5f4a803..c1c7886 100644 --- a/src/functions.php +++ b/src/functions.php @@ -41,68 +41,3 @@ function connect(PostgresConfig $config, ?Cancellation $cancellation = null): Po { return postgresConnector()->connect($config, $cancellation); } - -/** - * Casts a PHP value to a representation that is understood by Postgres, including encoding arrays. - * - * @throws \Error If $value is an object without a __toString() method, a resource, or an unknown type. - */ -function cast(mixed $value): string|int|float|null -{ - switch ($type = \gettype($value)) { - case "NULL": - case "integer": - case "double": - case "string": - return $value; // No casting necessary for numerics, strings, and null. - - case "boolean": - return $value ? 't' : 'f'; - - case "array": - return encode($value); - - case "object": - if (!\method_exists($value, "__toString")) { - throw new \Error("Object without a __toString() method included in parameter values"); - } - - return (string) $value; - - default: - throw new \Error("Invalid value type '$type' in parameter values"); - } -} - -/** - * Encodes an array into a PostgreSQL representation of the array. - * - * @return string The serialized representation of the array. - * - * @throws \Error If $array contains an object without a __toString() method, a resource, or an unknown type. - */ -function encode(array $array): string -{ - $array = \array_map(function ($value) { - switch (\gettype($value)) { - case "NULL": - return "NULL"; - - case "object": - if (!\method_exists($value, "__toString")) { - throw new \Error("Object without a __toString() method in array"); - } - - $value = (string) $value; - // no break - - case "string": - return '"' . \str_replace(['\\', '"'], ['\\\\', '\\"'], $value) . '"'; - - default: - return cast($value); // Recursively encodes arrays and errors on invalid values. - } - }, $array); - - return '{' . \implode(',', $array) . '}'; -} diff --git a/test/CastTest.php b/test/CastTest.php new file mode 100644 index 0000000..dca1d0a --- /dev/null +++ b/test/CastTest.php @@ -0,0 +1,149 @@ +assertSame($string, cast($array)); + } + + public function testMultiDimensionalStringArray(): void + { + $array = ["one", "two", ["three", "four"], "five"]; + $string = '{"one","two",{"three","four"},"five"}'; + + $this->assertSame($string, cast($array)); + } + + public function testQuotedStrings(): void + { + $array = ["one", "two", ["three", "four"], "five"]; + $string = '{"one","two",{"three","four"},"five"}'; + + $this->assertSame($string, cast($array)); + } + + public function testEscapedQuoteDelimiter(): void + { + $array = ['va"lue1', 'value"2']; + $string = '{"va\\"lue1","value\\"2"}'; + + $this->assertSame($string, cast($array)); + } + + public function testNullValue(): void + { + $array = ["one", null, "three"]; + $string = '{"one",NULL,"three"}'; + + $this->assertSame($string, cast($array)); + } + + public function testSingleDimensionalIntegerArray(): void + { + $array = [1, 2, 3]; + $string = '{' . \implode(',', $array) . '}'; + + $this->assertSame($string, cast($array)); + } + + public function testIntegerArrayWithNull(): void + { + $array = [1, 2, null, 3]; + $string = '{1,2,NULL,3}'; + + $this->assertSame($string, cast($array)); + } + + public function testMultidimensionalIntegerArray(): void + { + $array = [1, 2, [3, 4], [5], 6, 7, [[8, 9], 10]]; + $string = '{1,2,{3,4},{5},6,7,{{8,9},10}}'; + + $this->assertSame($string, cast($array)); + } + + public function testEscapedBackslashesInQuotedValue(): void + { + $array = ["test\\ing", "esca\\ped\\"]; + $string = '{"test\\\\ing","esca\\\\ped\\\\"}'; + + $this->assertSame($string, cast($array)); + } + + public function testBackedEnum(): void + { + $this->assertSame(3, cast(IntegerEnum::Three)); + $this->assertSame('three', cast(StringEnum::Three)); + } + + public function testBackedEnumInArray(): void + { + $array = [ + [IntegerEnum::One, IntegerEnum::Two, IntegerEnum::Three], + [StringEnum::One, StringEnum::Two, StringEnum::Three], + ]; + $string = '{{1,2,3},{"one","two","three"}}'; + + $this->assertSame($string, cast($array)); + } + + public function testUnitEnum(): void + { + $this->expectException(\ValueError::class); + $this->expectExceptionMessage('An object in parameter values must be'); + + cast(UnitEnum::Case); + } + + public function testUnitEnumInArray(): void + { + $this->expectException(\ValueError::class); + $this->expectExceptionMessage('An object in parameter arrays must be'); + + cast([UnitEnum::Case]); + } + + public function testObjectWithoutToStringMethod(): void + { + $this->expectException(\ValueError::class); + $this->expectExceptionMessage('An object in parameter values must be'); + + cast(new \stdClass); + } + + public function testObjectWithoutToStringMethodInArray(): void + { + $this->expectException(\ValueError::class); + $this->expectExceptionMessage('An object in parameter arrays must be'); + + cast([new \stdClass]); + } +} diff --git a/test/EncodeTest.php b/test/EncodeTest.php deleted file mode 100644 index 16dd442..0000000 --- a/test/EncodeTest.php +++ /dev/null @@ -1,89 +0,0 @@ -assertSame($string, encode($array)); - } - - public function testMultiDimensionalStringArray(): void - { - $array = ["one", "two", ["three", "four"], "five"]; - $string = '{"one","two",{"three","four"},"five"}'; - - $this->assertSame($string, encode($array)); - } - - public function testQuotedStrings(): void - { - $array = ["one", "two", ["three", "four"], "five"]; - $string = '{"one","two",{"three","four"},"five"}'; - - $this->assertSame($string, encode($array)); - } - - public function testEscapedQuoteDelimiter(): void - { - $array = ['va"lue1', 'value"2']; - $string = '{"va\\"lue1","value\\"2"}'; - - $this->assertSame($string, encode($array)); - } - - public function testNullValue(): void - { - $array = ["one", null, "three"]; - $string = '{"one",NULL,"three"}'; - - $this->assertSame($string, encode($array)); - } - - public function testSingleDimensionalIntegerArray(): void - { - $array = [1, 2, 3]; - $string = '{' . \implode(',', $array) . '}'; - - $this->assertSame($string, encode($array)); - } - - public function testIntegerArrayWithNull(): void - { - $array = [1, 2, null, 3]; - $string = '{1,2,NULL,3}'; - - $this->assertSame($string, encode($array)); - } - - public function testMultidimensionalIntegerArray(): void - { - $array = [1, 2, [3, 4], [5], 6, 7, [[8, 9], 10]]; - $string = '{1,2,{3,4},{5},6,7,{{8,9},10}}'; - - $this->assertSame($string, encode($array)); - } - - public function testEscapedBackslashesInQuotedValue(): void - { - $array = ["test\\ing", "esca\\ped\\"]; - $string = '{"test\\\\ing","esca\\\\ped\\\\"}'; - - $this->assertSame($string, encode($array)); - } - - public function testObjectWithoutToStringMethod(): void - { - $this->expectException(\Error::class); - $this->expectExceptionMessage('Object without a __toString() method in array'); - - encode([new \stdClass]); - } -} diff --git a/test/PgSqlConnectionTest.php b/test/PgSqlConnectionTest.php index d27dfe5..a01c5e4 100644 --- a/test/PgSqlConnectionTest.php +++ b/test/PgSqlConnectionTest.php @@ -5,7 +5,7 @@ use Amp\Postgres\PgSqlConnection; use Amp\Postgres\PostgresLink; use Revolt\EventLoop; -use function Amp\Postgres\cast; +use function Amp\Postgres\Internal\cast; /** * @requires extension pgsql diff --git a/test/PgSqlPoolTest.php b/test/PgSqlPoolTest.php index f99dcd4..baa02dc 100644 --- a/test/PgSqlPoolTest.php +++ b/test/PgSqlPoolTest.php @@ -9,7 +9,7 @@ use Amp\Sql\Common\ConnectionPool; use Amp\Sql\SqlConnector; use Revolt\EventLoop; -use function Amp\Postgres\cast; +use function Amp\Postgres\Internal\cast; /** * @requires extension pgsql diff --git a/test/PqConnectionTest.php b/test/PqConnectionTest.php index 73a5525..0634f88 100644 --- a/test/PqConnectionTest.php +++ b/test/PqConnectionTest.php @@ -6,7 +6,7 @@ use Amp\Postgres\Internal\PqUnbufferedResultSet; use Amp\Postgres\PostgresLink; use Amp\Postgres\PqConnection; -use function Amp\Postgres\cast; +use function Amp\Postgres\Internal\cast; /** * @requires extension pq diff --git a/test/PqPoolTest.php b/test/PqPoolTest.php index 568d8f9..e25fa7c 100644 --- a/test/PqPoolTest.php +++ b/test/PqPoolTest.php @@ -8,7 +8,7 @@ use Amp\Postgres\PqConnection; use Amp\Sql\Common\ConnectionPool; use Amp\Sql\SqlConnector; -use function Amp\Postgres\cast; +use function Amp\Postgres\Internal\cast; /** * @requires extension pq