Skip to content

Commit 48c4a05

Browse files
committed
Add Email and StringScalar
1 parent d127002 commit 48c4a05

File tree

6 files changed

+260
-63
lines changed

6 files changed

+260
-63
lines changed

README.md

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ A collection of custom scalar types for usage with https://github.com/webonyx/gr
1818
You can use the provided Scalars just like any other type in your schema definition.
1919
Check [SchemaUsageTest](tests/SchemaUsageTest.php) for an example.
2020

21+
## Simple Scalars
22+
23+
This package comes with a bunch of scalars that are ready-to-use and just work out of the box.
24+
25+
### Email
26+
27+
The Email scalar validates that a given value is a RFC 5321 compliant email.
28+
29+
## Advanced Scalars
30+
2131
### The Regex Scalar
2232

2333
The `Regex` class allows you to define a custom scalar that validates that the given
@@ -31,9 +41,11 @@ a name and a regular expression and you will receive a ready-to-use custom regex
3141

3242
use MLL\GraphQLScalars\Regex;
3343

34-
$hexValue = Regex::make('HexValue', '/^#?([a-f0-9]{6}|[a-f0-9]{3})$/');
35-
36-
$hexValue instanceof \GraphQL\Type\Definition\ScalarType; // true
44+
$hexValue = Regex::make(
45+
'HexValue',
46+
'A hexadecimal color is specified with: #RRGGBB, where RR (red), GG (green) and BB (blue) are hexadecimal integers between 00 and FF specifying the intensity of the color.',
47+
'/^#?([a-f0-9]{6}|[a-f0-9]{3})$/'
48+
);
3749
```
3850

3951
You may also define your regex scalar as a class.
@@ -46,9 +58,39 @@ use MLL\GraphQLScalars\Regex;
4658
// The name is implicitly set through the class name here
4759
class HexValue extends Regex
4860
{
61+
public $description = 'A hexadecimal color is specified with: #RRGGBB, where RR (red), GG (green) and BB (blue) are hexadecimal integers between 00 and FF specifying the intensity of the color.';
62+
4963
protected function regex() : string
5064
{
5165
return '/^#?([a-f0-9]{6}|[a-f0-9]{3})$/';
5266
}
5367
}
5468
```
69+
70+
### The StringBase Scalar
71+
72+
The `StringScalar` encapsulate all the boilerplate associated with creating a string-based Scalar type.
73+
It does the proper string checking for you and let's you focus on the minimal logic that is specific to your use case.
74+
75+
All you have to specify is a function that checks if the given string is valid. Use the factory method
76+
to generate an instance on the fly.
77+
78+
```php
79+
<?php
80+
81+
use MLL\GraphQLScalars\StringScalar;
82+
83+
$coolName = StringScalar::make(
84+
'CoolName',
85+
'A name that is most definitely cool.',
86+
function(string $name): bool {
87+
return in_array($name, [
88+
'Vladar',
89+
'Benedikt',
90+
'Christopher',
91+
]);
92+
}
93+
);
94+
```
95+
96+
Or you may simply extend the class, check out the implementation of the [Email](src/Email.php) scalar to see how.

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"require": {
1414
"php": ">=7.0",
1515
"webonyx/graphql-php": "^0.12.6",
16-
"spatie/regex": "^1.3"
16+
"spatie/regex": "^1.3",
17+
"egulias/email-validator": "^2.1"
1718
},
1819
"require-dev": {
1920
"phpunit/phpunit": "^6|^7.3"

src/Email.php

Lines changed: 14 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,67 +4,26 @@
44

55
namespace MLL\GraphQLScalars;
66

7-
use GraphQL\Error\InvariantViolation;
8-
use GraphQL\Language\AST\Node;
9-
use GraphQL\Type\Definition\ScalarType;
7+
use Egulias\EmailValidator\EmailValidator;
8+
use Egulias\EmailValidator\Validation\RFCValidation;
109

11-
class Email extends ScalarType
10+
class Email extends StringScalar
1211
{
12+
/** @var string */
13+
public $description = 'A valid RFC 5321 compliant email.';
14+
1315
/**
14-
* Serializes an internal value to include in a response.
16+
* Check if the given string is a valid email.
1517
*
16-
* @param string $value
18+
* @param string $stringValue
1719
*
18-
* @return string
20+
* @return bool
1921
*/
20-
public function serialize($value): string
22+
protected function isValid(string $stringValue): bool
2123
{
22-
if (!canBeString($value)) {
23-
$valueClass = get_class($value);
24-
25-
throw new InvariantViolation("The given value of class $valueClass can not be serialized.");
26-
}
27-
28-
$stringValue = strval($value);
29-
30-
if (!$this->matchesRegex($stringValue)) {
31-
throw new InvariantViolation("The given string $stringValue did not match the regex {$this->regex()}");
32-
}
33-
34-
return $stringValue;
35-
}
36-
37-
/**
38-
* Parses an externally provided value (query variable) to use as an input.
39-
*
40-
* @param mixed $value
41-
*
42-
* @return mixed
43-
*/
44-
public function parseValue($value)
45-
{
46-
// TODO implement validation
47-
48-
return $value;
49-
}
50-
51-
/**
52-
* Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input.
53-
*
54-
* E.g.
55-
* {
56-
* user(email: "[email protected]")
57-
* }
58-
*
59-
* @param Node $valueNode
60-
* @param array $variables
61-
*
62-
* @return mixed
63-
*/
64-
public function parseLiteral($valueNode, array $variables = null)
65-
{
66-
// TODO implement validation
67-
68-
return $valueNode->value;
24+
return (new EmailValidator)->isValid(
25+
$stringValue,
26+
new RFCValidation
27+
);
6928
}
7029
}

src/Regex.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,17 @@ abstract class Regex extends ScalarType
1919
* @return string
2020
*/
2121
abstract protected function regex(): string;
22-
22+
2323
/**
2424
* This factory method allows you to create a Regex scalar in a one-liner.
2525
*
26-
* @param string $name
27-
* @param string $regex
26+
* @param string $name The name that the scalar type will have in the schema.
27+
* @param string|null $description A description for the type.
28+
* @param string $regex The regular expression that is validated against.
2829
*
2930
* @return Regex
3031
*/
31-
public static function make(string $name, string $regex): self
32+
public static function make(string $name, string $description = null, string $regex): self
3233
{
3334
$regexClass = new class() extends Regex {
3435
/**
@@ -45,6 +46,7 @@ protected function regex(): string
4546
};
4647

4748
$regexClass->name = $name;
49+
$regexClass->description = $description;
4850
$regexClass->regex = $regex;
4951

5052
return $regexClass;

src/StringScalar.php

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MLL\GraphQLScalars;
6+
7+
use GraphQL\Error\Error;
8+
use GraphQL\Error\InvariantViolation;
9+
use GraphQL\Language\AST\Node;
10+
use GraphQL\Type\Definition\ScalarType;
11+
use GraphQL\Utils\Utils;
12+
13+
abstract class StringScalar extends ScalarType
14+
{
15+
/**
16+
* Check if the given string is a valid email.
17+
*
18+
* @param string $stringValue
19+
*
20+
* @return bool
21+
*/
22+
abstract protected function isValid(string $stringValue): bool;
23+
24+
/**
25+
* @param string $name The name that the scalar type will have in the schema.
26+
* @param string|null $description A description for the type.
27+
* @param callable $isValid A function that returns a boolean whether a given string is valid.
28+
*
29+
* @return StringScalar
30+
*/
31+
public static function make(string $name, string $description = null, callable $isValid): StringScalar
32+
{
33+
$instance = new class() extends StringScalar {
34+
/**
35+
* Check if the given string is a valid email.
36+
*
37+
* @param string $stringValue
38+
*
39+
* @return bool
40+
*/
41+
protected function isValid(string $stringValue): bool
42+
{
43+
return call_user_func($this->isValid, $stringValue);
44+
}
45+
};
46+
47+
$instance->name = $name;
48+
$instance->description = $description;
49+
$instance->isValid = $isValid;
50+
51+
return $instance;
52+
}
53+
54+
/**
55+
* Serializes an internal value to include in a response.
56+
*
57+
* @param string $value
58+
*
59+
* @return string
60+
*/
61+
public function serialize($value): string
62+
{
63+
$stringValue = assertString($value, InvariantViolation::class);
64+
65+
if (!$this->isValid($stringValue)) {
66+
throw new InvariantViolation("The given string $stringValue is not a valid {$this->tryInferName()}.");
67+
}
68+
69+
return $stringValue;
70+
}
71+
72+
/**
73+
* Parses an externally provided value (query variable) to use as an input.
74+
*
75+
* @param mixed $value
76+
*
77+
* @throws Error
78+
*
79+
* @return string
80+
*/
81+
public function parseValue($value): string
82+
{
83+
$stringValue = assertString($value, Error::class);
84+
85+
if(!$this->isValid($stringValue)) {
86+
$safeValue = Utils::printSafeJson($stringValue);
87+
throw new Error("The given string {$safeValue} is not a valid {$this->tryInferName()}.");
88+
}
89+
90+
return $stringValue;
91+
}
92+
93+
/**
94+
* Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input.
95+
*
96+
* E.g.
97+
* {
98+
* user(email: "[email protected]")
99+
* }
100+
*
101+
* @param Node $valueNode
102+
* @param array $variables
103+
*
104+
* @throws Error
105+
*
106+
* @return string
107+
*/
108+
public function parseLiteral($valueNode, array $variables = null): string
109+
{
110+
$stringValue = assertStringLiteral($valueNode);
111+
112+
if(!$this->isValid($stringValue)) {
113+
$safeValue = Utils::printSafeJson($stringValue);
114+
throw new Error("The given string {$safeValue} is not a valid {$this->tryInferName()}.", $valueNode);
115+
}
116+
117+
return $stringValue;
118+
}
119+
}

tests/EmailTest.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests;
6+
7+
use GraphQL\Error\Error;
8+
use GraphQL\Error\InvariantViolation;
9+
use GraphQL\Language\AST\IntValueNode;
10+
use GraphQL\Language\AST\NodeKind;
11+
use GraphQL\Language\AST\StringValueNode;
12+
use MLL\GraphQLScalars\Email;
13+
use MLL\GraphQLScalars\Regex;
14+
15+
class EmailTest extends \PHPUnit\Framework\TestCase
16+
{
17+
public function testSerializeThrowsIfUnserializableValueIsGiven()
18+
{
19+
$this->expectException(InvariantViolation::class);
20+
$this->expectExceptionMessageRegExp('/^The given value .* can not be serialized\./');
21+
22+
(new Email)->serialize(
23+
new class() {
24+
}
25+
);
26+
}
27+
28+
public function testSerializeThrowsIfEmailIsInvalid()
29+
{
30+
$this->expectException(InvariantViolation::class);
31+
$this->expectExceptionMessage('The given string foo is not a valid Email.');
32+
33+
(new Email)->serialize('foo');
34+
}
35+
36+
public function testSerializePassesWhenEmailIsInvalid()
37+
{
38+
$serializedResult = (new Email)->serialize('foo@bar');
39+
40+
$this->assertSame('foo@bar', $serializedResult);
41+
}
42+
43+
public function testParseValueThrowsIfEmailIsInvalid()
44+
{
45+
$this->expectException(Error::class);
46+
$this->expectExceptionMessage('The given string "foo" is not a valid Email.');
47+
48+
(new Email)->parseValue('foo');
49+
}
50+
51+
public function testParseValuePassesIfEmailIsValid()
52+
{
53+
$this->assertSame(
54+
'foo@bar',
55+
(new Email)->parseValue('foo@bar')
56+
);
57+
}
58+
59+
public function testParseLiteralThrowsIfNotValidEmail()
60+
{
61+
$this->expectException(Error::class);
62+
$this->expectExceptionMessage('The given string "foo" is not a valid Email.');
63+
64+
(new Email)->parseLiteral(new StringValueNode(['value' => 'foo']));
65+
}
66+
67+
public function testParseLiteralPassesIfEmailIsValid()
68+
{
69+
$this->assertSame(
70+
'foo@bar',
71+
(new Email)->parseLiteral(new StringValueNode(['value' => 'foo@bar']))
72+
);
73+
}
74+
}

0 commit comments

Comments
 (0)