Implement collection normalizing

This commit is contained in:
Jeremy Dormitzer 2019-01-28 21:24:51 -05:00
parent 3fc30b786f
commit 23b993f05d
4 changed files with 209 additions and 7 deletions

View File

@ -82,7 +82,8 @@ class ActivityPubModule
$this->injector->register( CollectionsService::class, CollectionsService::class )
->addArgument( self::COLLECTION_PAGE_SIZE )
->addArgument( new Reference( AuthService::class ) )
->addArgument( new Reference( ContextProvider::class ) );
->addArgument( new Reference( ContextProvider::class ) )
->addArgument( new Reference( Client::class ) );
$this->injector->register( RandomProvider::class, RandomProvider::class );

View File

@ -4,7 +4,10 @@ namespace ActivityPub\Objects;
use ActivityPub\Auth\AuthService;
use ActivityPub\Entities\ActivityPubObject;
use ActivityPub\Objects\ContextProvider;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request as Psr7Request;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class CollectionsService
@ -24,12 +27,20 @@ class CollectionsService
*/
private $contextProvider;
public function __construct( int $pageSize, AuthService $authService,
ContextProvider $contextProvider )
/**
* @var Client
*/
private $httpClient;
public function __construct( int $pageSize,
AuthService $authService,
ContextProvider $contextProvider,
Client $httpClient )
{
$this->pageSize = $pageSize;
$this->authService = $authService;
$this->contextProvider = $contextProvider;
$this->httpClient = $httpClient;
}
/**
@ -63,6 +74,73 @@ class CollectionsService
return $colArr;
}
/**
* Given a collection as an array, normalize the collection by collapsing
* collection pages into a single `items` or `orderedItems` array
*
* @param array $collection The collection to normalize
* @return array The normalized collection
*/
public function normalizeCollection( array $collection )
{
if ( $collection['type'] !== 'Collection' &&
$collection['type'] !== 'OrderedCollection' ) {
return $collection;
}
if ( ! array_key_exists( 'first', $collection ) ) {
return $collection;
}
$first = $collection['first'];
if ( is_string( $first ) ) {
$first = $this->fetchPage( $first );
if ( ! $first ) {
throw new BadRequestHttpException(
"Unable to retrieve collection page '$first'"
);
}
}
$items = $this->getPageItems( $collection['first'] );
$itemsField = $collection['type'] === 'Collection' ? 'items' : 'orderedItems';
$collection[$itemsField] = $items;
unset( $collection['first'] );
if ( array_key_exists( 'last', $collection ) ) {
unset( $collection['last'] );
}
return $collection;
}
private function getPageItems( array $collectionPage )
{
$items = array();
if ( array_key_exists( 'items', $collectionPage ) ) {
$items = array_merge( $items, $collectionPage['items'] );
} else if ( array_key_exists( 'orderedItems', $collectionPage ) ) {
$items = array_merge( $items, $collectionPage['orderedItems'] );
}
if ( array_key_exists( 'next', $collectionPage ) ) {
$nextPage = $collectionPage['next'];
if ( is_string( $nextPage ) ) {
$nextPage = $this->fetchPage( $nextPage );
}
if ( $nextPage ) {
$items = array_merge( $items, $this->getPageItems( $nextPage ) );
}
}
return $items;
}
private function fetchPage( string $pageId )
{
$request = new Psr7Request( 'GET', $pageId, array(
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
) );
$response = $this->httpClient->send( $request );
if ( $response->getStatusCode() !== 200 || empty( $response->getBody() ) ) {
return;
}
return json_decode( $response->getBody(), true );
}
private function getCollectionPage( ActivityPubObject $collection,
Request $request,
int $offset,
@ -77,7 +155,7 @@ class CollectionsService
}
if ( ! $collection->hasField( $itemsKey ) ) {
throw new InvalidArgumentException(
"Collection does not have an \"$field\" key"
"Collection does not have an \"$itemsKey\" key"
);
}
$collectionItems = $collection->getFieldValue( $itemsKey );
@ -108,6 +186,7 @@ class CollectionsService
$itemsKey => $pageItems,
'partOf' => $collection['id'],
);
// TODO set 'first' and 'last' on the page
$nextIdx = $this->hasNextItem( $request, $collectionItems, $idx );
if ( $nextIdx ) {
$page['next'] = $collection['id'] . "?offset=$nextIdx";

View File

@ -9,6 +9,7 @@ use ActivityPub\Objects\ContextProvider;
use ActivityPub\Objects\CollectionsService;
use ActivityPub\Objects\ObjectsService;
use ActivityPub\Test\TestUtils\TestActivityPubObject;
use GuzzleHttp\Client;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@ -65,7 +66,8 @@ class GetControllerTest extends TestCase
);
$authService = new AuthService();
$contextProvider = new ContextProvider();
$collectionsService = new CollectionsService( 4, $authService, $contextProvider );
$httpClient = $this->createMock( Client::class );
$collectionsService = new CollectionsService( 4, $authService, $contextProvider, $httpClient );
$this->getController = new GetController(
$objectsService, $collectionsService, $authService
);

View File

@ -6,6 +6,8 @@ use ActivityPub\Auth\AuthService;
use ActivityPub\Objects\ContextProvider;
use ActivityPub\Objects\CollectionsService;
use ActivityPub\Test\TestUtils\TestActivityPubObject;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response as Psr7Response;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@ -18,12 +20,22 @@ class CollectionsServiceTest extends TestCase
{
$authService = new AuthService();
$contextProvider = new ContextProvider();
$httpClient = $this->createMock( Client::class );
$httpClient->method( 'send' )->willReturn(
new Psr7Response( 200, array(), json_encode( array(
'type' => 'OrderedCollectionPage',
'orderedItems' => array(
'item3',
'item4',
),
) ) )
);
$this->collectionsService = new CollectionsService(
4, $authService, $contextProvider
4, $authService, $contextProvider, $httpClient
);
}
public function testCollectionsService()
public function testCollectionPaging()
{
$testCases = array(
array(
@ -378,5 +390,113 @@ class CollectionsServiceTest extends TestCase
);
}
}
public function testCollectionNormalizing()
{
$testCases = array(
array(
'id' => 'basicNormalizingTest',
'collection' => array(
'type' => 'Collection',
'first' => array(
'type' => 'CollectionPage',
'items' => array(
'item1',
'item2',
),
),
),
'expectedResult' => array(
'type' => 'Collection',
'items' => array(
'item1',
'item2',
),
),
),
array(
'id' => 'orderedNormalizingTest',
'collection' => array(
'type' => 'OrderedCollection',
'first' => array(
'type' => 'OrderedCollectionPage',
'orderedItems' => array(
'item1',
'item2',
),
),
),
'expectedResult' => array(
'type' => 'OrderedCollection',
'orderedItems' => array(
'item1',
'item2',
),
),
),
array(
'id' => 'pageTraversal',
'collection' => array(
'type' => 'OrderedCollection',
'first' => array(
'type' => 'OrderedCollectionPage',
'orderedItems' => array(
'item1',
'item2',
),
'next' => array(
'type' => 'OrderedCollectionPage',
'orderedItems' => array(
'item3',
'item4',
),
),
),
),
'expectedResult' => array(
'type' => 'OrderedCollection',
'orderedItems' => array(
'item1',
'item2',
'item3',
'item4',
),
),
),
array(
'id' => 'pageTraversal',
'collection' => array(
'type' => 'OrderedCollection',
'first' => array(
'type' => 'OrderedCollectionPage',
'orderedItems' => array(
'item1',
'item2',
),
'next' => 'https://example.com/collection/1?page=2',
),
),
'expectedResult' => array(
'type' => 'OrderedCollection',
'orderedItems' => array(
'item1',
'item2',
'item3',
'item4',
),
),
),
);
foreach ( $testCases as $testCase ) {
$collection = $testCase['collection'];
if ( array_key_exists( 'expectedException', $testCase ) ) {
$this->expectException( $testCase['expectedException'] );
}
$actual = $this->collectionsService->normalizeCollection( $collection );
$this->assertEquals(
$testCase['expectedResult'], $actual, "Error on test $testCase[id]"
);
}
}
}
?>