1
0
mirror of https://github.com/Chouchen/ShikiryuRSS.git synced 2024-05-19 06:11:32 +02:00

🦺 Add validator

This commit is contained in:
Shikiryu 2023-04-07 00:35:20 +02:00
parent bc0e818bbc
commit fec8c122e3
9 changed files with 276 additions and 55 deletions

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
/vendor/ /vendor/
/.idea /.idea
/tests/error.log

View File

@ -2,39 +2,103 @@
namespace Shikiryu\SRSS\Entity; namespace Shikiryu\SRSS\Entity;
use ReflectionException;
use Shikiryu\SRSS\Entity\Channel\Image; use Shikiryu\SRSS\Entity\Channel\Image;
use Shikiryu\SRSS\Validator\HasValidator;
use Shikiryu\SRSS\Validator\Validator;
class Channel implements SRSSElement /**
* https://cyber.harvard.edu/rss/rss.html#requiredChannelElements
*/
class Channel extends HasValidator implements SRSSElement
{ {
/**
* @required
* @nohtml
*/
public string $title; public string $title;
/**
* @required
* @url
*/
public string $link; public string $link;
/**
* @required
*/
public string $description; public string $description;
public ?string $language; /**
public ?string $copyright; * @lang
public ?string $managingEditor; */
public ?string $webMaster; public ?string $language = null;
public ?string $pubDate; /**
public ?string $lastBuildDate; * @nohtml
public ?string $category; */
public ?string $generator; public ?string $copyright = null;
public ?string $docs; /**
public ?string $cloud; * @nohtml
public ?string $ttl; */
public ?Image $image; public ?string $managingEditor = null;
public ?string $rating; /**
public ?string $textInput; * @nohtml
public ?string $skipHours; */
public ?string $skipDays; public ?string $webMaster = null;
/**
public array $required = ['title', 'link', 'description']; * @date
*/
public ?string $pubDate = null;
/**
* @date
*/
public ?string $lastBuildDate = null;
/**
* TODO should be an array
*/
public ?string $category = null;
/**
* @nohtml
*/
public ?string $generator = null;
/**
* @url
*/
public ?string $docs = null;
/**
* @var string|null
* TODO validator
*/
public ?string $cloud = null;
/**
* @int
*/
public ?string $ttl = null;
public ?Image $image = null;
public ?string $rating = null;
/**
* @var string|null
* The purpose of the <textInput> element is something of a mystery. You can use it to specify a search engine box. Or to allow a reader to provide feedback. Most aggregators ignore it.
*/
public ?string $textInput = null;
/**
* @hour
*/
public ?string $skipHours = null;
/**
* @day
*/
public ?string $skipDays = null;
/** /**
* @return bool * @return bool
* @throws ReflectionException
*/ */
public function isValid(): bool public function isValid(): bool
{ {
return count(array_filter($this->required, fn($field) => !empty($this->{$field}))) === 0; $annotation_validation = new Validator();
return $annotation_validation->isObjectValid($this);
} }
/** /**

View File

@ -8,11 +8,10 @@ use Shikiryu\SRSS\Entity\Item;
class SRSS implements Iterator class SRSS implements Iterator
{ {
public array $items; // array of SRSSItems
protected $attr; // array of RSS attributes
private $position; // Iterator position
public Channel $channel; public Channel $channel;
public array $items; // array of SRSSItems
private int $position; // Iterator position
// lists of possible attributes for RSS // lists of possible attributes for RSS
protected $possibleAttr = [ protected $possibleAttr = [
@ -43,7 +42,6 @@ class SRSS implements Iterator
*/ */
public function __construct() public function __construct()
{ {
$this->attr = [];
$this->items = []; $this->items = [];
$this->position = 0; $this->position = 0;
} }
@ -76,9 +74,8 @@ class SRSS implements Iterator
/** /**
* check if current RSS is a valid one (based on specifications) * check if current RSS is a valid one (based on specifications)
* @return bool * @return bool
* TODO use required
*/ */
public function isValid() public function isValid(): bool
{ {
$valid = true; $valid = true;
$items = $this->getItems(); $items = $this->getItems();
@ -115,7 +112,7 @@ class SRSS implements Iterator
*/ */
public function __set($name, $val) public function __set($name, $val)
{ {
if (!array_key_exists($name, $this->possibleAttr)) { if (!property_exists(Channel::class, $name)) {
throw new SRSSException($name . ' is not a possible item'); throw new SRSSException($name . ' is not a possible item');
} }
$flag = $this->possibleAttr[$name]; $flag = $this->possibleAttr[$name];
@ -148,7 +145,7 @@ class SRSS implements Iterator
/** /**
* current from Iterator * current from Iterator
*/ */
public function current() public function current(): mixed
{ {
return $this->items[$this->position]; return $this->items[$this->position];
} }
@ -179,7 +176,7 @@ class SRSS implements Iterator
/** /**
* getter of 1st item * getter of 1st item
* @return Item * @return Item|null
*/ */
public function getFirst(): ?Item public function getFirst(): ?Item
{ {
@ -214,7 +211,6 @@ class SRSS implements Iterator
/** /**
* getter of all items * getter of all items
* @return Item[] * @return Item[]
* @throws SRSSException
*/ */
public function getItems(): array public function getItems(): array
{ {
@ -224,7 +220,6 @@ class SRSS implements Iterator
/** /**
* transform current object into an array * transform current object into an array
* @return array * @return array
* @throws SRSSException
*/ */
public function toArray(): array public function toArray(): array
{ {

View File

@ -7,6 +7,7 @@ use DOMNodeList;
use DOMXPath; use DOMXPath;
use Shikiryu\SRSS\Entity\Channel; use Shikiryu\SRSS\Entity\Channel;
use Shikiryu\SRSS\Entity\Channel\Image; use Shikiryu\SRSS\Entity\Channel\Image;
use Shikiryu\SRSS\Entity\Item;
class SRSSParser extends DomDocument class SRSSParser extends DomDocument
{ {
@ -57,10 +58,10 @@ class SRSSParser extends DomDocument
} }
/** /**
* @return array|mixed * @return Item[]
* @throws \Shikiryu\SRSS\SRSSException * @throws \Shikiryu\SRSS\SRSSException
*/ */
private function getItems() private function getItems(): mixed
{ {
$channel = $this->_getChannel(); $channel = $this->_getChannel();
/** @var DOMNodeList $items */ /** @var DOMNodeList $items */

View File

@ -6,26 +6,23 @@ class SRSSTools
{ {
public static function check($check, $flag) public static function check($check, $flag)
{ {
switch($flag){ return match ($flag) {
case 'nohtml': return self::noHTML($check); 'nohtml' => self::noHTML($check),
case 'link': return self::checkLink($check); 'link' => self::checkLink($check),
case 'html': return self::HTML4XML($check); 'html' => self::HTML4XML($check),
/*case 'lang': 'date' => self::getRSSDate($check),
return self::noHTML($check); 'email' => self::checkEmail($check),
*/ 'int' => self::checkInt($check),
case 'date': return self::getRSSDate($check); 'hour' => self::checkHour($check),
case 'email': return self::checkEmail($check); 'day' => self::checkDay($check),
case 'int': return self::checkInt($check); 'folder' => [],
case 'hour': return self::checkHour($check); 'media_type' => self::checkMediaType($check),
case 'day': return self::checkDay($check); 'media_medium' => self::checkMediaMedium($check),
case 'folder': return []; 'bool' => self::checkBool($check),
case 'media_type': return self::checkMediaType($check); 'medium_expression' => self::checkMediumExpression($check),
case 'media_medium': return self::checkMediaMedium($check); '' => $check,
case 'bool': return self::checkBool($check); default => throw new SRSSException('flag ' . $flag . ' does not exist.'),
case 'medium_expression': return self::checkMediumExpression($check); };
case '': return $check;
default: throw new SRSSException('flag '.$flag.' does not exist.');
}
} }
/** /**

View File

@ -0,0 +1,11 @@
<?php
namespace Shikiryu\SRSS\Validator;
abstract class HasValidator
{
/**
* @var bool[]
*/
public array $validated = [];
}

146
src/Validator/Validator.php Normal file
View File

@ -0,0 +1,146 @@
<?php
namespace Shikiryu\SRSS\Validator;
use DateTimeInterface;
use ReflectionClass;
use ReflectionException;
class Validator
{
/**
* @throws ReflectionException
*/
public function isObjectValid($object): bool
{
if (!$object->validated) {
$object = $this->validateObject($object);
}
return !in_array(false, $object->validated);
}
/**
* @throws ReflectionException
*/
public function validateObject($object)
{
$properties = $this->_getClassProperties(get_class($object));
foreach ($properties as $property) {
$propertyValue = $object->{$property->name};
$propertyAnnotations = $this->_getPropertyAnnotations($property, get_class($object));
if (!in_array('required', $propertyAnnotations) && empty($propertyValue)) {
continue;
}
foreach ($propertyAnnotations as $propertyAnnotation) {
$annotation = explode(' ', $propertyAnnotation);
$object->validated[$property->name] = $this->_validateProperty($annotation, $propertyValue);
}
}
return $object;
}
private function _validateProperty(array $annotation, $property): bool
{
if (count($annotation) === 1) {
return call_user_func([$this, sprintf('_validate%s', ucfirst($annotation[0]))], $property);
}
return true; // TODO check
}
/**
* @throws ReflectionException
*/
private function _getClassProperties($class): array
{
$ReflectionClass = new ReflectionClass($class);
return $ReflectionClass->getProperties();
}
private function _getPropertyAnnotations($property, $className): array
{
preg_match_all('#@(.*?)\n#s', $property->getDocComment(), $annotations);
return array_map(fn($annotation) => trim($annotation), $annotations[1]);
}
private function _validateString($value): bool
{
return is_string($value);
}
private function _validateInt($value): bool
{
return is_numeric($value);
}
private function _validateRequired($value): bool
{
return !empty(trim($value));
}
/**
* @param $value
* @return bool
* https://cyber.harvard.edu/rss/languages.html
*/
private function _validateLang($value): bool
{
return in_array(strtolower($value), [
'af','sq','eu','be','bg','ca','zh-cn','zh-tw','hr','cs','da','nl','nl-be','nl-nl','en','en-au','en-bz',
'en-ca','en-ie','en-jm','en-nz','en-ph','en-za','en-tt','en-gb','en-us','en-zw','et','fo','fi','fr','fr-be',
'fr-ca','fr-fr','fr-lu','fr-mc','fr-ch','gl','gd','de','de-at','de-de','de-li','de-lu','de-ch','el','haw',
'hu','is','in','ga','it','it-it','it-ch','ja','ko','mk','no','pl','pt','pt-br','pt-pt','ro','ro-mo','ro-ro',
'ru','ru-mo','ru-ru','sr','sk','sl','es','es-ar','es-bo','es-cl','es-co','es-cr','es-do','es-ec','es-sv',
'es-gt','es-hn','es-mx','es-ni','es-pa','es-py','es-pe','es-pr','es-es','es-uy','es-ve','sv','sv-fi','sv-se',
'tr','uk',
]);
}
/**
* @param $value
* @return bool
*/
private function _validateNoHtml($value): bool
{
return strip_tags($value) === $value;
}
private function _validateUrl($value): bool
{
return filter_var($value, FILTER_VALIDATE_URL) !== false;
}
private function _validateDate($value): bool
{
return \DateTime::createFromFormat(DateTimeInterface::RSS, $value) !== false;
}
private function _validateHour($value): bool
{
$options = [
'options' => [
'default' => 0,
'min_range' => 0,
'max_range' => 23
]
];
return filter_var($value, FILTER_VALIDATE_INT, $options) !== false;
}
private function _validateDay($value): bool
{
return in_array(
strtolower($value),
['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
);
}
}

View File

@ -13,6 +13,8 @@ class BasicReader extends TestCase
$first_item = $rss->getFirst(); $first_item = $rss->getFirst();
self::assertNotNull($first_item); self::assertNotNull($first_item);
self::assertEquals('RSS Tutorial', $first_item->title); self::assertEquals('RSS Tutorial', $first_item->title);
self::assertTrue($rss->channel->isValid());
} }
public function testRssNotFound() public function testRssNotFound()
@ -38,5 +40,7 @@ class BasicReader extends TestCase
self::assertEquals('Star City', $rss->getFirst()->title); self::assertEquals('Star City', $rss->getFirst()->title);
self::assertEquals('http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp', $rss->getLast()->link); self::assertEquals('http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp', $rss->getLast()->link);
self::assertEquals('Fri, 30 May 2003 11:06:42 GMT', $rss->getItem(2)->pubDate); self::assertEquals('Fri, 30 May 2003 11:06:42 GMT', $rss->getItem(2)->pubDate);
self::assertTrue($rss->channel->isValid());
} }
} }

View File

@ -14,6 +14,7 @@ class MediaTest extends TestCase
self::assertEquals('Kirstie Alley, \'Cheers\' and \'Veronica\'s Closet\' star, dead at 71', $first_item->title); self::assertEquals('Kirstie Alley, \'Cheers\' and \'Veronica\'s Closet\' star, dead at 71', $first_item->title);
self::assertEquals('https://cdn.cnn.com/cnnnext/dam/assets/221205172141-kirstie-alley-2005-super-169.jpg', $first_item->medias[0]->url); self::assertEquals('https://cdn.cnn.com/cnnnext/dam/assets/221205172141-kirstie-alley-2005-super-169.jpg', $first_item->medias[0]->url);
self::assertTrue($rss->channel->isValid(), var_export($rss->channel->validated, true));
} }
public function testMusicVideo() public function testMusicVideo()
@ -25,5 +26,6 @@ class MediaTest extends TestCase
$first_item = $rss->getFirst(); $first_item = $rss->getFirst();
self::assertEquals('http://www.foo.com/movie.mov', $first_item->medias[0]->url); self::assertEquals('http://www.foo.com/movie.mov', $first_item->medias[0]->url);
self::assertTrue($rss->channel->isValid());
} }
} }