Skip to content

Commit 8bdd763

Browse files
committed
Add a new HTMLType
1 parent b59fdb7 commit 8bdd763

7 files changed

Lines changed: 246 additions & 1 deletion

File tree

src/lib/types/src/Flow/Types/DSL/functions.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Flow\Types\DSL;
66

7+
use Dom\HTMLDocument;
78
use Flow\ETL\Attribute\{DocumentationDSL, Module, Type as DSLType};
89
use Flow\Types\Type;
910
use Flow\Types\Type\{Comparator, TypeDetector, TypeFactory, Types};
@@ -25,6 +26,7 @@
2526
UuidType,
2627
XMLElementType,
2728
XMLType};
29+
use Flow\Types\Type\Logical\HTMLType;
2830
use Flow\Types\Type\Native\{ArrayType,
2931
BooleanType,
3032
CallableType,
@@ -425,6 +427,15 @@ function type_literal(bool|float|int|string $value) : LiteralType
425427
return new LiteralType($value);
426428
}
427429

430+
/**
431+
* @return Type<HTMLDocument>
432+
*/
433+
#[DocumentationDSL(module: Module::TYPES, type: DSLType::TYPE)]
434+
function type_html() : Type
435+
{
436+
return new HTMLType();
437+
}
438+
428439
/**
429440
* @template T
430441
*
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\Types\Type\Logical;
6+
7+
use const Dom\HTML_NO_DEFAULT_NS;
8+
use function Flow\Types\DSL\type_string;
9+
use Dom\HTMLDocument;
10+
use Flow\Types\Exception\{CastingException, InvalidTypeException};
11+
use Flow\Types\Type;
12+
13+
/**
14+
* @implements Type<HTMLDocument>
15+
*/
16+
final readonly class HTMLType implements Type
17+
{
18+
public function assert(mixed $value) : HTMLDocument
19+
{
20+
if ($this->isValid($value)) {
21+
return $value;
22+
}
23+
24+
throw InvalidTypeException::value($value, $this);
25+
}
26+
27+
public function cast(mixed $value) : HTMLDocument
28+
{
29+
if ($this->isValid($value)) {
30+
return $value;
31+
}
32+
33+
if (\is_string($value)) {
34+
return HTMLDocument::createFromString($value, \LIBXML_COMPACT | \LIBXML_NOERROR | \LIBXML_HTML_NOIMPLIED | HTML_NO_DEFAULT_NS);
35+
}
36+
37+
try {
38+
$stringValue = type_string()->cast($value);
39+
40+
return HTMLDocument::createFromString($stringValue, \LIBXML_COMPACT | \LIBXML_NOERROR | \LIBXML_HTML_NOIMPLIED | HTML_NO_DEFAULT_NS);
41+
} catch (CastingException $e) {
42+
throw new CastingException($value, $this, $e);
43+
}
44+
}
45+
46+
public function isValid(mixed $value) : bool
47+
{
48+
return $value instanceof HTMLDocument;
49+
}
50+
51+
public function normalize() : array
52+
{
53+
return [
54+
'type' => 'html',
55+
];
56+
}
57+
58+
public function toString() : string
59+
{
60+
return 'html';
61+
}
62+
}

src/lib/types/src/Flow/Types/Type/Logical/NonEmptyStringType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Flow\Types\Type\Logical;
66

77
use function Flow\Types\DSL\dom_element_to_string;
8+
use DOM\HTMLDocument;
89
use Flow\Types\Exception\{CastingException, InvalidTypeException};
910
use Flow\Types\Type;
1011

@@ -49,6 +50,10 @@ public function cast(mixed $value) : string
4950
return $this->assert((string) $value);
5051
}
5152

53+
if ($value instanceof HTMLDocument) {
54+
return $this->assert($value->saveHtml($value->documentElement));
55+
}
56+
5257
if ($value instanceof \DOMDocument) {
5358
return $this->assert($value->saveXML($value->documentElement) ?: '');
5459
}

src/lib/types/src/Flow/Types/Type/Native/StringType.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Flow\Types\Type\Native;
66

77
use function Flow\Types\DSL\dom_element_to_string;
8+
use DOM\HTMLDocument;
89
use Flow\Types\Exception\{CastingException, InvalidTypeException};
910
use Flow\Types\Type;
1011

@@ -49,6 +50,14 @@ public function cast(mixed $value) : string
4950
return $value->getName();
5051
}
5152

53+
if ($value instanceof HTMLDocument) {
54+
return $value->saveHtml($value->documentElement);
55+
}
56+
57+
if ($value instanceof HTMLDocument) {
58+
return $value->saveHtml($value->documentElement);
59+
}
60+
5261
if ($value instanceof \DOMDocument) {
5362
return $value->saveXML($value->documentElement) ?: '';
5463
}

src/lib/types/src/Flow/Types/Type/TypeFactory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
type_date,
1111
type_datetime,
1212
type_float,
13+
type_html,
1314
type_integer,
1415
type_json,
1516
type_mixed,
@@ -79,6 +80,7 @@ public static function fromArray(array $data) : Type
7980
'scalar' => type_scalar(),
8081
'mixed' => type_mixed(),
8182
'numeric-string' => type_numeric_string(),
83+
'html' => type_html(),
8284
default => throw new InvalidArgumentException("Unknown type '" . (\is_string($data['type']) ? $data['type'] : \gettype($data['type'])) . "'"),
8385
};
8486
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\Types\Tests\Unit\Type\Logical;
6+
7+
use function Flow\Types\DSL\{type_from_array, type_html};
8+
use DOM\HTMLDocument;
9+
use Flow\Types\Exception\{CastingException, InvalidTypeException};
10+
use PHPUnit\Framework\Attributes\{DataProvider, RequiresPhp};
11+
use PHPUnit\Framework\TestCase;
12+
13+
#[RequiresPhp('8.4')]
14+
final class HTMLTypeTest extends TestCase
15+
{
16+
public static function assert_data_provider() : \Generator
17+
{
18+
yield 'valid \DOM\HTMLDocument' => [
19+
'value' => HTMLDocument::createEmpty(),
20+
'exceptionClass' => null,
21+
];
22+
23+
yield 'invalid string' => [
24+
'value' => 'string',
25+
'exceptionClass' => InvalidTypeException::class,
26+
];
27+
28+
yield 'invalid UUID string' => [
29+
'value' => '49e952c8-80ec-4910-a1d6-a19bd46b163d',
30+
'exceptionClass' => InvalidTypeException::class,
31+
];
32+
33+
yield 'invalid boolean' => [
34+
'value' => false,
35+
'exceptionClass' => InvalidTypeException::class,
36+
];
37+
38+
yield 'invalid float' => [
39+
'value' => 124.25,
40+
'exceptionClass' => InvalidTypeException::class,
41+
];
42+
43+
yield 'invalid array' => [
44+
'value' => [1, 2],
45+
'exceptionClass' => InvalidTypeException::class,
46+
];
47+
48+
yield 'invalid object' => [
49+
'value' => new \stdClass(),
50+
'exceptionClass' => InvalidTypeException::class,
51+
];
52+
53+
yield 'invalid DateTimeZone' => [
54+
'value' => new \DateTimeZone('UTC'),
55+
'exceptionClass' => InvalidTypeException::class,
56+
];
57+
58+
yield 'invalid DateTimeImmutable' => [
59+
'value' => new \DateTimeImmutable(),
60+
'exceptionClass' => InvalidTypeException::class,
61+
];
62+
}
63+
64+
public static function cast_data_provider() : \Generator
65+
{
66+
yield 'string to HTML' => [
67+
'value' => '<!DOCTYPE html><html lang="en"><body><div><span>1</span></div></body></html>',
68+
'expected' => <<<'HTML'
69+
<!DOCTYPE html><html lang="en"><body><div><span>1</span></div></body></html>
70+
HTML,
71+
'exceptionClass' => null,
72+
];
73+
74+
yield 'incomplete string to HTML' => [
75+
'value' => '<div><span>1</span></div>',
76+
'expected' => <<<'HTML'
77+
<div><span>1</span></div>
78+
HTML,
79+
'exceptionClass' => null,
80+
];
81+
82+
yield 'object to HTML' => [
83+
'value' => new \stdClass(),
84+
'expected' => null,
85+
'exceptionClass' => CastingException::class,
86+
];
87+
}
88+
89+
public static function is_valid_data_provider() : \Generator
90+
{
91+
yield 'valid \DOM\HTMLDocument' => [
92+
'value' => HTMLDocument::createEmpty(),
93+
'expected' => true,
94+
];
95+
96+
yield 'invalid HTML string' => [
97+
'value' => '<html></html>',
98+
'expected' => false,
99+
];
100+
101+
yield 'invalid date string' => [
102+
'value' => '2020-01-01',
103+
'expected' => false,
104+
];
105+
106+
yield 'invalid datetime string' => [
107+
'value' => '2020-01-01 00:00:00',
108+
'expected' => false,
109+
];
110+
}
111+
112+
#[DataProvider('assert_data_provider')]
113+
public function test_assert(mixed $value, ?string $exceptionClass = null) : void
114+
{
115+
if ($exceptionClass !== null) {
116+
$this->expectException($exceptionClass);
117+
type_html()->assert($value);
118+
} else {
119+
self::assertInstanceOf(HTMLDocument::class, type_html()->assert($value));
120+
}
121+
}
122+
123+
#[DataProvider('cast_data_provider')]
124+
public function test_cast(mixed $value, mixed $expected, ?string $exceptionClass) : void
125+
{
126+
if ($exceptionClass !== null) {
127+
$this->expectException($exceptionClass);
128+
type_html()->cast($value);
129+
} else {
130+
$result = type_html()->cast($value);
131+
self::assertSame($expected, $result->saveHtml());
132+
}
133+
}
134+
135+
#[DataProvider('is_valid_data_provider')]
136+
public function test_is_valid(mixed $value, bool $expected) : void
137+
{
138+
self::assertSame($expected, type_html()->isValid($value));
139+
}
140+
141+
public function test_normalization() : void
142+
{
143+
$type = type_html();
144+
$normalized = $type->normalize();
145+
146+
self::assertEquals($type, type_from_array($normalized));
147+
}
148+
149+
public function test_to_string() : void
150+
{
151+
self::assertSame(
152+
'html',
153+
type_html()->toString()
154+
);
155+
}
156+
}

web/landing/resources/dsl.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)