Browse Source

More JSON Feed functionality

master
J. King 4 years ago
parent
commit
8081f19747
  1. 16
      lib/Exception.php
  2. 14
      lib/Feed.php
  3. 2
      lib/Parser/Construct.php
  4. 20
      lib/Parser/Feed.php
  5. 39
      lib/Parser/JSON/Construct.php
  6. 51
      lib/Parser/JSON/Feed.php
  7. 8
      lib/Parser/XML/Feed.php
  8. 19
      lib/Text.php
  9. 33
      tests/cases/JSON/TestJSONFeed.php
  10. 81
      tests/cases/JSON/main.json

16
lib/Exception.php

@ -7,4 +7,20 @@ declare(strict_types=1);
namespace JKingWeb\Lax;
abstract class Exception extends \Exception {
const SYMBOLS = [
// Parsing: 0x1100
"notJSONType" => [0x1111, "Document Content-Type is not either that of JSON Feed or generic JSON"],
"notJSON" => [0x1112, "Document is not valid JSON"],
"notJSONFeed" => [0x1113, "Document is not a JSON Feed document"],
"unsupportedJSONFeedVersion" => [0x1114, "Document specifies an unsupported JSON Feed version"]
];
public function __construct(string $symbol, \Exception $e = null) {
$data = self::SYMBOLS[$symbol] ?? null;
if (!$data) {
throw new \Exception("Exception symbol \"$symbol\" does not exist");
}
list($code, $msg) = $data;
parent::__construct($msg, $code, $e);
}
}

14
lib/Feed.php

@ -7,17 +7,27 @@ declare(strict_types=1);
namespace JKingWeb\Lax;
class Feed {
/** @var string $type The type of feed, one of the following:
*
* - `rss` for RSS 0.9x or RSS 2.0.x
* - `rdf` for RSS 1.0
* - `atom` for Atom feeds
* - `json` for JSON Feed
* - `hfeed` for a microformat h-feed
*
* The feed type is largely advisory, but is used when converting between formats
*/
public $type;
public $version;
public $id;
public $url;
public $link;
public $title;
public $summary;
public $categories;
public $people;
public $author;
public $dateModified;
public $entries;
public $entries = [];
public static function parse(string $data, ?string $contentType = null, ?string $url = null): self {
$out = new self;

2
lib/Parser/Construct.php

@ -55,7 +55,7 @@ trait Construct {
return (bool) filter_var($addr, \FILTER_VALIDATE_EMAIL, $flags);
}
protected function parseDate(string $date) {
protected function parseDate(string $date): ?Date {
$out = null;
$date = $this->trimText($date);
if (!strlen($date)) {

20
lib/Parser/Feed.php

@ -8,32 +8,34 @@ namespace JKingWeb\Lax\Parser;
use JKingWeb\Lax\Person\Collection as PersonCollection;
use JKingWeb\Lax\Category\Collection as CategoryCollection;
use JKingWeb\Lax\Date;
use JKingWeb\Lax\Text;
interface Feed {
/** General function to fetch the canonical feed URL */
public function getUrl(): string;
public function getUrl(): ?string;
/** General function to fetch the feed title */
public function getTitle(): string;
public function getTitle(): ?Text;
/** General function to fetch the feed's Web-representation URL */
public function getLink(): string;
public function getLink(): ?string;
/** General function to fetch the description of a feed */
public function getSummary(): string;
public function getSummary(): ?Text;
/** General function to fetch the categories of a feed */
public function getCategories(): CategoryCollection;
public function getCategories(): ?CategoryCollection;
/** General function to fetch the feed identifier */
public function getId(): string;
public function getId(): ?string;
/** General function to fetch a collection of people associated with a feed */
public function getPeople(): PersonCollection;
public function getPeople(): ?PersonCollection;
/** General function to fetch the feed's modification date */
public function getDateModified();
public function getDateModified(): ?Date;
/** General function to fetch the feed's modification date */
public function getEntries() : array;
public function getEntries() : ?array;
}

39
lib/Parser/JSON/Construct.php

@ -6,37 +6,24 @@
declare(strict_types=1);
namespace JKingWeb\Lax\Parser\JSON;
use JKingWeb\Lax\Date;
use JKingWeb\Lax\Text;
trait Construct {
use \JKingWeb\Lax\Parser\Construct;
/** @var object */
protected $json;
/** Returns an object member if the member exists and is of the expected type
*
* Returns null otherwise
*/
protected function fetchMember(string $key, string $type, \stdClass $obj = null) {
$obj = $obj ?? $this->json;
$obj = $obj ?? $this->data;
if (!isset($obj->$key)) {
return null;
}
$type = strtolower($type);
switch ($type) {
case "bool":
$type = "boolean";
break;
case "int":
$type = "integer";
break;
case "float":
$type = "double";
break;
case "str":
$type = "string";
break;
}
if (strtolower(gettype($obj->$key))==$type) {
$type = ['bool' => "boolean", 'int' => "integer", 'float' => "double", 'str' => "string"][$type] ?? $type;
if (strtolower(gettype($obj->$key)) === $type) {
return $obj->$key;
} else {
return null;
@ -44,13 +31,21 @@ trait Construct {
}
/** Returns an object member as a resolved URL */
protected function fetchUrl(string $key, \stdClass $obj = null) {
protected function fetchUrl(string $key, \stdClass $obj = null): ?string {
$url = $this->fetchMember($key, "str", $obj);
return (!is_null($url)) ? $this->resolveUrl($url, $this->url) : $url;
return (!is_null($url)) ? $this->resolveUrl($url, $this->url) : null;
}
/** Returns an object member as a parsed date */
protected function fetchDate(string $key, \stdClass $obj = null) {
protected function fetchDate(string $key, \stdClass $obj = null): ?Date {
return $this->parseDate($this->fetchMember($key, "str", $obj) ?? "");
}
protected function fetchText(string $key, \stdClass $obj = null): ?Text {
$t = $this->fetchMember($key, "str", $obj);
if (!is_null($t)) {
return new Text($t);
}
return null;
}
}

51
lib/Parser/JSON/Feed.php

@ -6,10 +6,13 @@
declare(strict_types=1);
namespace JKingWeb\Lax\Parser\JSON;
use JKingWeb\Lax\Feed as FeedStruct;
use JKingWeb\Lax\Person\Person;
use JKingWeb\Lax\Person\Collection as PersonCollection;
use JKingWeb\Lax\Category\Collection as CategoryCollection;
use JKingWeb\Lax\Parser\Exception;
use JKingWeb\Lax\Text;
use JKingWeb\Lax\Date;
class Feed implements \JKingWeb\Lax\Parser\Feed {
use Construct;
@ -38,7 +41,7 @@ class Feed implements \JKingWeb\Lax\Parser\Feed {
}
/** Performs format-specific preparation and validation */
protected function init(): void {
protected function init(FeedStruct $feed): FeedStruct {
$type = preg_replace("/[\s;,].*/", "", trim(strtolower($this->contentType)));
if (strlen($type) && !in_array($type, self::MIME_TYPES)) {
throw new Exception("notJSONType");
@ -48,24 +51,26 @@ class Feed implements \JKingWeb\Lax\Parser\Feed {
throw new Exception("notJSON");
} elseif (!isset($data->version) || !preg_match("<^https://jsonfeed\.org/version/(\d+(?:\.\d+)?)$>", $data->version, $match)) {
throw new Exception("notJSONFeed");
} elseif (version_compare($match[1], "1.0", "<") || version_compare($match[1], "2", ">=")) {
} elseif (version_compare($match[1], "1", "<") || version_compare($match[1], "2", ">=")) {
throw new Exception("unsupportedJSONFeedVersion");
}
$this->data = $data;
$this->version = $match[1];
$feed->type = "json";
$feed->version = $this->version;
return $feed;
}
/** Parses the feed to extract sundry metadata */
public function parse(\JKingWeb\Lax\Feed $feed): \JKingWeb\Lax\Feed {
$this->init();
return $feed;
public function parse(FeedStruct $feed): FeedStruct {
$feed = $this->init($feed);
$feed->title = $this->getTitle();
$feed->id = $this->getId();
$feed->url = $this->getUrl();
$feed->link = $this->getLink();
$feed->title = $this->getTitle();
$feed->summary = $this->getSummary();
return $feed;
$feed->people = $this->getPeople();
$feed->author = $this->people->primary();
$feed->dateModified = $this->getDateModified();
$feed->entries = $this->getEntries();
// do a second pass on missing data we'd rather fill in
@ -80,51 +85,51 @@ class Feed implements \JKingWeb\Lax\Parser\Feed {
*
* If the feed does not include a canonical URL, the request URL is returned instead
*/
public function getUrl(): string {
return $this->fetchUrl("feed_url") ?? $this->reqUrl;
public function getUrl(): ?string {
return $this->fetchUrl("feed_url");
}
/** General function to fetch the feed title */
public function getTitle(): string {
return $this->fetchMember("title", "str") ?? "";
public function getTitle(): ?Text {
return $this->fetchText("title");
}
/** General function to fetch the feed's Web-representation URL */
public function getLink(): string {
return $this->fetchUrl("home_page_url") ?? "";
public function getLink(): ?string {
return $this->fetchUrl("home_page_url");
}
/** General function to fetch the description of a feed */
public function getSummary(): string {
return $this->fetchMember("description", "str") ?? "";
public function getSummary(): ?Text {
return $this->fetchText("description");
}
/** General function to fetch the categories of a feed
*
* JSON Feed does not have categories at the feed level, so this always returns an empty collection
* JSON Feed does not have categories at the feed level, so this always returns null
*/
public function getCategories(): CategoryCollection {
return new CategoryCollection;
public function getCategories(): ?CategoryCollection {
return null;
}
/** General function to fetch the feed identifier
*
* For JSON feeds this is always the feed URL specified in the feed
*/
public function getId(): string {
return $this->fetchUrl("feed_url") ?? "";
public function getId(): ?string {
return $this->fetchUrl("feed_url");
}
/** General function to fetch a collection of people associated with a feed */
public function getPeople(): PersonCollection {
return $this->getPeopleV1() ?? new PersonCollection;
public function getPeople(): ?PersonCollection {
return $this->getPeopleV1();
}
/** General function to fetch the modification date of a feed
*
* JSON feeds themselves don't have dates, so this always returns null
*/
public function getDateModified() {
public function getDateModified(): ?Date {
return null;
}

8
lib/Parser/XML/Feed.php

@ -8,6 +8,8 @@ namespace JKingWeb\Lax\Parser\XML;
use JKingWeb\Lax\Person\Collection as PersonCollection;
use JKingWeb\Lax\Category\Collection as CategoryCollection;
use JKingWeb\Lax\Date;
use JKingWeb\Lax\Text;
class Feed implements \JKingWeb\Lax\Parser\Feed {
use Construct;
@ -82,7 +84,7 @@ class Feed implements \JKingWeb\Lax\Parser\Feed {
}
/** General function to fetch the feed title */
public function getTitle(): string {
public function getTitle(): ?Text {
return $this->getTitleAtom() ?? $this->getTitleRss1() ?? $this->getTitleRss2() ?? $this->getTitleDC() ?? $this->getTitlePod() ?? "";
}
@ -92,7 +94,7 @@ class Feed implements \JKingWeb\Lax\Parser\Feed {
}
/** General function to fetch the description of a feed */
public function getSummary(): string {
public function getSummary(): ?Text {
// unlike most other data, Atom is not preferred, because Atom doesn't really have feed summaries
return $this->getSummaryDC() ?? $this->getSummaryRss1() ?? $this->getSummaryRss2() ?? $this->getSummaryPod() ?? $this->getSummaryAtom() ?? "";
}
@ -117,7 +119,7 @@ class Feed implements \JKingWeb\Lax\Parser\Feed {
}
/** General function to fetch the modification date of a feed */
public function getDateModified() {
public function getDateModified(): ?Date {
return $this->getDateModifiedAtom() ?? $this->getDateModifiedDC() ?? $this->getDateModifiedRss2();
}

19
lib/Text.php

@ -0,0 +1,19 @@
<?php
/** @license MIT
* Copyright 2018 J. King et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Lax;
class Text {
public $plain;
public $html;
public $xhtml;
public $loose;
public function __construct(string $data = null, string $type = "plain") {
assert(in_array($type, ["plain", "html", "xhtml", "loose"]), new \InvalidArgumentException);
$this->$type = $data;
}
}

33
tests/cases/JSON/TestJSONFeed.php

@ -9,6 +9,7 @@ namespace JKingWeb\Lax\TestCase\JSON;
use JKingWeb\Lax\Parser\Exception;
use JKingWeb\Lax\Feed;
use JKingWeb\Lax\Parser\JSON\Feed as Parser;
use JKingWeb\Lax\Text;
/** @covers JKingWeb\Lax\Parser\JSON\Feed<extended> */
class TestJSON extends \PHPUnit\Framework\TestCase {
@ -19,13 +20,14 @@ class TestJSON extends \PHPUnit\Framework\TestCase {
} elseif (!is_string($input)) {
throw new \Exception("Test input is invalid");
}
$f = new Feed;
$p = new Parser($input, $type);
if ($output instanceof \Exception) {
$this->expectExceptionObject($output);
$p->parse($f);
$p->parse(new Feed);
} else {
$this->assertTrue(false);
$act = $p->parse(new Feed);
$exp = $this->makeFeed($output);
$this->assertEquals($exp, $act);
}
}
@ -43,4 +45,29 @@ class TestJSON extends \PHPUnit\Framework\TestCase {
}
}
}
protected function makeFeed(array $output): Feed {
$f = new Feed;
foreach ($output as $k => $v) {
if (in_array($k, ["title", "summary"])) {
$f->$k = $this->makeText($v);
} else {
$f->$k = $v;
}
}
return $f;
}
protected function makeText($data): Text {
if (is_string($data)) {
return new Text($data);
}
$out = new Text;
foreach(["plain", "html", "xhtml", "loose"] as $k) {
if (isset($data[$k])) {
$out->$k = $data[$k];
}
}
return $out;
}
}

81
tests/cases/JSON/main.json

@ -0,0 +1,81 @@
[
{
"description": "Minimal example 1",
"input": {
"version": "https://jsonfeed.org/version/1"
},
"output": {
"type": "json",
"version": "1"
}
},
{
"description": "Minimal example 2",
"input": {
"version": "https://jsonfeed.org/version/1.1"
},
"output": {
"type": "json",
"version": "1.1"
}
},
{
"description": "Correct type of member",
"input": {
"version": "https://jsonfeed.org/version/1",
"title": "Example title"
},
"output": {
"type": "json",
"version": "1",
"title": "Example title"
}
},
{
"description": "Incorrect type of member",
"input": {
"version": "https://jsonfeed.org/version/1",
"title": 1001001
},
"output": {
"type": "json",
"version": "1"
}
},
{
"description": "URL -> ID equivalence",
"input": {
"version": "https://jsonfeed.org/version/1",
"title": "Example title",
"feed_url": "http://example.com/"
},
"output": {
"type": "json",
"version": "1",
"title": "Example title",
"id": "http://example.com/",
"url": "http://example.com/"
}
},
{
"description": "Basic example",
"input": {
"version": "https://jsonfeed.org/version/1",
"title": "Example title",
"feed_url": "http://example.com/",
"home_page_url": "http://example.net/",
"description": "Example description",
"user_comment": "Example comment",
"next_url": "http://example.com/next"
},
"output": {
"type": "json",
"version": "1",
"title": "Example title",
"id": "http://example.com/",
"url": "http://example.com/",
"link": "http://example.net/",
"summary": "Example description"
}
}
]
Loading…
Cancel
Save