[WIP] Refine JsonLdNode serialization

This commit is contained in:
Jeremy Dormitzer 2019-05-16 22:01:12 -04:00
parent 1e1ff24178
commit 113a26f130
10 changed files with 537 additions and 21 deletions

View File

@ -16,6 +16,7 @@
"docs": "phpdoc -d ./src -t ./docs"
},
"require": {
"php": "^5.5 || ^7.0",
"ext-json": "*",
"cache/apc-adapter": "0.3.2",
"cache/apcu-adapter": "0.2.2",
@ -31,6 +32,7 @@
"monolog/monolog": "^1.0",
"phpseclib/phpseclib": "^2.0",
"psr/http-message": "^1.0",
"ramsey/uuid": "3.8.0",
"symfony/dependency-injection": "^3.4",
"symfony/event-dispatcher": "^3.4",
"symfony/http-foundation": "^3.4",

View File

@ -2,7 +2,9 @@
namespace ActivityPub\JsonLd;
use ActivityPub\Utils\UuidProvider;
use InvalidArgumentException;
use Ramsey\Uuid\Uuid;
/**
* A view of a JSON-LD graph. Maps ids to JsonLdNode instances.
@ -11,27 +13,27 @@ use InvalidArgumentException;
*/
class JsonLdGraph
{
/**
* @var int
*/
private $nextBlankId;
/**
* @var array
*/
private $graph;
public function __construct()
/**
* @var UuidProvider
*/
private $uuidProvider;
public function __construct( UuidProvider $uuidProvider )
{
$this->nextBlankId = 0;
$this->graph = array();
$this->uuidProvider = $uuidProvider;
}
public function addNode( JsonLdNode $node )
{
$id = $node->getId();
if ( is_null( $id ) ) {
$id = $this->getNextBlankId();
$id = $this->uuidProvider->uuid();
$node->setId( $id );
}
$this->graph[$id] = $node;
@ -54,11 +56,4 @@ class JsonLdGraph
$this->graph[$newNodeName] = $this->graph[$blankNodeName];
unset( $this->graph[$blankNodeName] );
}
private function getNextBlankId()
{
$nextId = $this->nextBlankId;
$this->nextBlankId += 1;
return "_:b$nextId";
}
}

View File

@ -5,10 +5,14 @@ namespace ActivityPub\JsonLd;
use ActivityPub\JsonLd\Dereferencer\DereferencerInterface;
use ActivityPub\JsonLd\Exceptions\NodeNotFoundException;
use ActivityPub\JsonLd\Exceptions\PropertyNotDefinedException;
use ActivityPub\JsonLd\TripleStore\TypedRdfTriple;
use ArrayAccess;
use BadMethodCallException;
use InvalidArgumentException;
use ML\JsonLD\JsonLD;
use ML\JsonLD\TypedValue;
use ML\JsonLD\Value;
use Psr\Log\LoggerInterface;
use stdClass;
class JsonLdNode implements ArrayAccess
@ -42,6 +46,11 @@ class JsonLdNode implements ArrayAccess
*/
private $dereferencer;
/**
* @var LoggerInterface
*/
private $logger;
/**
* This node's view of the JSON-LD graph.
* @var JsonLdGraph
@ -63,17 +72,21 @@ class JsonLdNode implements ArrayAccess
* @param string|array|stdClass $context The JSON-LD context.
* @param JsonLdNodeFactory $factory The factory used to construct this instance.
* @param DereferencerInterface $dereferencer
* @param LoggerInterface $logger
* @param JsonLdGraph $graph The JSON-LD graph this node is a part of.
* @param array $backreferences
*/
public function __construct( $jsonLd,
$context,
JsonLdNodeFactory $factory,
DereferencerInterface $dereferencer,
LoggerInterface $logger,
JsonLdGraph $graph,
$backreferences = array() )
{
$this->factory = $factory;
$this->dereferencer = $dereferencer;
$this->logger = $logger;
if ( $jsonLd == new stdClass() ) {
$this->expanded = new stdClass();
} else {
@ -90,8 +103,8 @@ class JsonLdNode implements ArrayAccess
}
/**
* Gets this node's id, if it has one. Could be null or a temporary id if this is a blank node.
* @return string|null
* Gets this node's id. Could be a temporary id if this is a blank node.
* @return string
*/
public function getId()
{
@ -338,6 +351,52 @@ class JsonLdNode implements ArrayAccess
$this->backreferences[$expandedName][] = $referencingNode;
}
/**
* Returns the node expressed as an array of RdfTriples.
* @return TypedRdfTriple[]
*/
public function toRdfTriples()
{
$cloned = clone $this->expanded;
// First serialize this node
$quads = JsonLD::toRdf( $cloned );
$triples = array();
foreach ( $quads as $quad ) {
if ( (string)$quad->getSubject() === $this->getId() ) {
$objectType = null;
if ( $quad->getObject() instanceof Value ) {
$object = $quad->getObject()->getValue();
if ( $quad->getObject() instanceof TypedValue ) {
$objectType = $quad->getObject()->getType();
}
} else {
$objectIri = $quad->getObject();
if ( $objectIri->getScheme() === '_' ) {
// TODO resolve the associated value to force the generation of a UUID, then set $object to the uuid
} else {
$object = (string)$quad->getObject();
}
$objectType = '@id';
}
$triples[] = TypedRdfTriple::create(
(string)$quad->getSubject(), (string)$quad->getProperty(), $object, $objectType
);
}
}
// Then serialize any sub-nodes
foreach ( $this->expanded as $name => $values ) {
if ( $name === '@id' ) {
continue;
}
foreach ( $this->getMany( $name ) as $subNode ) {
if ( $subNode instanceof JsonLdNode ) {
$triples = array_merge( $triples, $subNode->toRdfTriples() );
}
}
}
return $triples;
}
/**
* Whether a offset exists
* @link https://php.net/manual/en/arrayaccess.offsetexists.php
@ -412,9 +471,11 @@ class JsonLdNode implements ArrayAccess
// TODO memoize this function
$dummyObj = (object) array(
'@context' => $this->context,
'_dummyKey' => '_dummyValue', // Set a dummy key to ensure that @id gets properly expanded
$name => '_dummyValue',
);
$expanded = (array) JsonLD::expand( $dummyObj )[0];
unset( $expanded['_:_dummyKey'] );
return array_keys( $expanded )[0];
}
}

View File

@ -3,6 +3,11 @@
namespace ActivityPub\JsonLd;
use ActivityPub\JsonLd\Dereferencer\DereferencerInterface;
use ActivityPub\JsonLd\TripleStore\TypedRdfTriple;
use ActivityPub\Utils\UuidProvider;
use InvalidArgumentException;
use Psr\Log\LoggerInterface;
use stdClass;
/**
* A factory class for constructing JsonLdNode instances
@ -23,10 +28,25 @@ class JsonLdNodeFactory
*/
private $dereferencer;
public function __construct( $context, DereferencerInterface $dereferencer )
/**
* @var LoggerInterface
*/
private $logger;
/**
* @var UuidProvider
*/
private $uuidProvider;
public function __construct( $context,
DereferencerInterface $dereferencer,
LoggerInterface $logger,
UuidProvider $uuidProvider )
{
$this->context = $context;
$this->dereferencer = $dereferencer;
$this->logger = $logger;
$this->uuidProvider = $uuidProvider;
}
/**
@ -39,8 +59,63 @@ class JsonLdNodeFactory
public function newNode( $jsonLd, $graph = null, $backreferences = array() )
{
if ( is_null( $graph ) ) {
$graph = new JsonLdGraph();
$graph = new JsonLdGraph( $this->uuidProvider );
}
return new JsonLdNode( $jsonLd, $this->context, $this, $this->dereferencer, $graph, $backreferences );
return new JsonLdNode(
$jsonLd, $this->context, $this, $this->dereferencer, $this->logger, $graph, $backreferences
);
}
/**
* Constructs a JsonLdNode from a collection of RdfTriples, properly setting up the graph traversals based
* on relationships between the passed-in triples.
*
* @param TypedRdfTriple[] $triples The triples.
* @param string $rootNodeId The ID of the root node - that is, the node returned from this function. This is
* necessary because the RDF triples array can contain triples from multiple nodes.
* @param JsonLdGraph|null $graph An existing JsonLdGraph to add this node to.
*/
public function nodeFromRdf( $triples, $rootNodeId, $graph = null )
{
if ( is_null( $graph ) ) {
$graph = new JsonLdGraph( $this->uuidProvider );
}
$buckets = array();
$backreferences = array();
foreach ( $triples as $triple ) {
$buckets[$triple->getSubject()][] = $triple;
}
if ( ! array_key_exists( $rootNodeId, $buckets ) ) {
throw new InvalidArgumentException("No triple with subject $rootNodeId was found");
}
$nodes = array();
foreach ( $buckets as $id => $triples ) {
$obj = new stdClass();
foreach( $triples as $triple ) {
if ( $triple->getObjectType() && $triple->getObjectType() === '@id' ) {
$obj->$triple->getPredicate()[] = (object) array( '@id' => $triple->getObject() );
$backreferences[$triple->getObject()][] = (object) array(
'predicate' => $triple->getPredicate(),
'referer' => $triple->getSubject(),
);
} else if ( $triple->getObjectType() ) {
$obj->$triple->getPredicate()[] = (object) array(
'@type' => $triple->getObjectType(),
'@value' => $triple->getObject(),
);
} else {
$obj->$triple->getPredicate()[] = (object) array( '@value' => $triple->getObject() );
}
}
$node = $this->newNode( $obj, $graph );
$nodes[$node->getId()] = $node;
}
foreach ( $backreferences as $referencedId => $references ) {
$referencedNode = $nodes[$referencedId];
foreach ( $references as $reference ) {
$referencedNode->addBackReference( $reference->predicate, $nodes[$reference->referer] );
}
}
return $nodes[$rootNodeId];
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace ActivityPub\JsonLd\TripleStore\Doctrine;
class DoctrineTriplestore
{
}

View File

@ -0,0 +1,55 @@
<?php
namespace ActivityPub\JsonLd\TripleStore;
/**
* A triplestore is a type of graph database that stores RDF triples. This interface defines the TripleStore API
* that ActivityPub-PHP relies on to persist JSON-LD nodes.
* Interface TripleStoreInterface
* @package ActivityPub\JsonLd\TripleStore
*/
interface TriplestoreInterface
{
/**
* Persists a triple. If the specified triple already exists in the store, this is a no-op.
* The triple must be fully specified, i.e. none of the subject, the predicate, or the object can be null.
* @param TypedRdfTriple $triple
*/
public function storeTriple( TypedRdfTriple $triple );
/**
* Persists multiple triples. If any of the specified triples already exist in the store, this is a no-op for those triples.
* All the triples must be fully specified, i.e. none of the subject, the predicate, or the object can be null.
* @param TypedRdfTriple[] $triples
*/
public function storeTriples( $triples );
/**
* Deletes a triple. If the specified triple doesn't exist, this is a no-op.
* The triple must be fully specified, i.e. none of the subject, the predicate, or the object can be null.
* @param TypedRdfTriple $triple
*/
public function deleteTriple( TypedRdfTriple $triple );
/**
* Deletes multiple triples. If any of the specified triples don't exist, this is a no-op for those triples.
* All the triples must be fully specified, i.e. none of the subject, the predicate, or the object can be null.
* @param TypedRdfTriple[] $triples
*/
public function deleteTriples( $triples );
/**
* Selects triples that match the selection from the store.
*
* The selection is a triple where any of the subject, predicate, or object can be null. This method will
* return the set of triples that match the terms that are specified in the selection. For example, given
* the selection ('https://example.com', null, null), this method will return the set of all triples whose
* subject is 'https://example.com', with any predicate and any object. The triple (null, null, null) will select
* the set of all triples.
* @param TypedRdfTriple $selection
* @return TypedRdfTriple[]
*/
public function select( TypedRdfTriple $selection );
}

View File

@ -0,0 +1,131 @@
<?php
namespace ActivityPub\JsonLd\TripleStore;
/**
* A triple represents a single fact in an RDF graph. A triple is made up a subject, a predicate, and an object.
* The object can also have a type, e.g. @id for references to other resources or
* http://www.w3.org/2001/XMLSchema#dateTime for date-time values.
*
* See https://www.w3.org/TR/rdf11-concepts/#data-model.
*
* Class Triple
* @package ActivityPub\JsonLd\TripleStore
*/
class TypedRdfTriple
{
/**
* @var string|null
*/
private $subject;
/**
* @var string|null
*/
private $predicate;
/**
* @var string|null
*/
private $object;
/**
* @var string|null
*/
private $objectType;
private function __construct( $subject = null, $predicate = null, $object = null, $objectType = null )
{
$this->subject = $subject;
$this->predicate = $predicate;
$this->object = $object;
$this->objectType = $objectType;
}
public static function create( $subject = null, $predicate = null, $object = null, $objectType = null )
{
return new TypedRdfTriple( $subject, $predicate, $object, $objectType );
}
/**
* @return string|null
*/
public function getSubject()
{
return $this->subject;
}
/**
* @param string $subject
* @return TypedRdfTriple
*/
public function setSubject( $subject )
{
$this->subject = $subject;
return $this;
}
/**
* @return string|null
*/
public function getPredicate()
{
return $this->predicate;
}
/**
* @param string $predicate
* @return TypedRdfTriple
*/
public function setPredicate( $predicate )
{
$this->predicate = $predicate;
return $this;
}
/**
* @return string|null
*/
public function getObject()
{
return $this->object;
}
/**
* @param string $object
* @return TypedRdfTriple
*/
public function setObject( $object )
{
$this->object = $object;
return $this;
}
/**
* @return string|null
*/
public function getObjectType()
{
return $this->objectType;
}
/**
* @param string $objectType
* @return TypedRdfTriple
*/
public function setObjectType( $objectType )
{
$this->objectType = $objectType;
return $this;
}
/**
* True if this triple has a subject, a predicate, and an object.
* @return bool
*/
public function isFullySpecified()
{
return $this->getSubject() && $this->getPredicate() && $this->getObject();
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace ActivityPub\Utils;
use Ramsey\Uuid\Uuid;
class UuidProvider
{
/**
* @return \Ramsey\Uuid\UuidInterface
* @throws \Exception
*/
public function uuid()
{
return Uuid::uuid4();
}
}

View File

@ -6,11 +6,27 @@ use ActivityPub\JsonLd\Exceptions\NodeNotFoundException;
use ActivityPub\JsonLd\Exceptions\PropertyNotDefinedException;
use ActivityPub\JsonLd\JsonLdNode;
use ActivityPub\JsonLd\JsonLdNodeFactory;
use ActivityPub\JsonLd\TripleStore\TypedRdfTriple;
use ActivityPub\Test\TestConfig\APTestCase;
use ActivityPub\Test\TestUtils\TestUuidProvider;
use ActivityPub\Utils\Logger;
use stdClass;
class JsonLdNodeTest extends APTestCase
{
private $uuids = array(
'ae699da1-2d11-4b60-91f9-e3e594fa0df9',
'5fb2dd08-be6f-4008-be9e-879ce072d308',
'5390f3ff-9ec4-40f2-9583-5d03a4782016',
'e560ea21-6d95-4dec-8646-6b3180544287',
'ee8a5dc0-e53b-4397-9f38-6f70551a4a2d',
'7e74832d-1ff7-46e7-94ad-77e0e69f6c8a',
'ff46dbe9-99f2-4378-bec1-99b9ceb09d2f',
'27e81733-e587-4b8e-9991-305e56df426e',
'aedfa3dc-cc55-45ca-ad37-156a3c45b7bb',
'8cd764f7-4463-463a-8271-697a42b6e7a7',
);
private $asContext = array(
'https://www.w3.org/ns/activitystreams',
);
@ -474,9 +490,130 @@ class JsonLdNodeTest extends APTestCase
}
}
public function provideToRdfTriple()
{
return array(
array(
(object) array(
'@context' => array( 'https://www.w3.org/ns/activitystreams' ),
'id' => 'https://example.org/collections/1',
'type' => 'Collection',
'name' => 'MyCollection',
'published' => '2019-05-01',
'items' => array(
'https://example.org/collections/1/items/1',
'https://example.org/collections/1/items/2',
)
),
$this->asContext,
array(
TypedRdfTriple::create(
'https://example.org/collections/1',
'http://www.w3.org/1999/02/22-rdf-syntax-ns#type',
'https://www.w3.org/ns/activitystreams#Collection',
'@id'
),
TypedRdfTriple::create(
'https://example.org/collections/1',
'https://www.w3.org/ns/activitystreams#items',
'https://example.org/collections/1/items/1',
'@id'
),
TypedRdfTriple::create(
'https://example.org/collections/1',
'https://www.w3.org/ns/activitystreams#items',
'https://example.org/collections/1/items/2',
'@id'
),
TypedRdfTriple::create(
'https://example.org/collections/1',
'https://www.w3.org/ns/activitystreams#name',
'MyCollection',
'http://www.w3.org/2001/XMLSchema#string'
),
TypedRdfTriple::create(
'https://example.org/collections/1',
'https://www.w3.org/ns/activitystreams#published',
'2019-05-01',
'http://www.w3.org/2001/XMLSchema#dateTime'
),
TypedRdfTriple::create(
'https://example.org/collections/1/items/1',
'http://www.w3.org/1999/02/22-rdf-syntax-ns#type',
'https://www.w3.org/ns/activitystreams#Note',
'@id'
),
TypedRdfTriple::create(
'https://example.org/collections/1/items/2',
'http://www.w3.org/1999/02/22-rdf-syntax-ns#type',
'https://www.w3.org/ns/activitystreams#Note',
'@id'
),
),
array(
'https://example.org/collections/1/items/1' => (object) array(
'@context' => array( 'https://www.w3.org/ns/activitystreams' ),
'id' => 'https://example.org/collections/1/items/1',
'type' => 'Note',
),
'https://example.org/collections/1/items/2' => (object) array(
'@context' => array( 'https://www.w3.org/ns/activitystreams' ),
'id' => 'https://example.org/collections/1/items/2',
'type' => 'Note',
),
),
),
array(
(object) array(
'@context' => array(
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
),
'id' => 'https://example.org/sally',
'type' => 'Actor',
'publicKey' => (object) array(
'publicKeyPem' => 'the_public_key',
)
),
$this->asContext,
array(
TypedRdfTriple::create(
'https://example.org/sally',
'http://www.w3.org/1999/02/22-rdf-syntax-ns#type',
'https://www.w3.org/ns/activitystreams#Actor',
'@id'
),
TypedRdfTriple::create(
'https://example.org/sally',
'https://w3id.org/security/v1#publicKey',
$this->uuids[0]
),
TypedRdfTriple::create(
$this->uuids[0],
'https://w3id.org/security/v1#publicKeyPem',
'the_public_key',
'@id'
),
),
),
);
}
/**
* @dataProvider provideToRdfTriple
*/
public function testToRdfTriple( $inputObj, $context, $expectedTriples, $nodeGraph = array() )
{
$node = $this->makeJsonLdNode( $inputObj, $context, $nodeGraph );
$triples = $node->toRdfTriples();
$this->assertEquals( $expectedTriples, $triples );
}
private function makeJsonLdNode( $inputObj, $context, $nodeGraph = array() )
{
$factory = new JsonLdNodeFactory( $context, new TestDereferencer( $nodeGraph ) );
$factory = new JsonLdNodeFactory(
$context, new TestDereferencer( $nodeGraph ), new Logger(), new TestUuidProvider( $this->uuids )
);
return $factory->newNode( $inputObj );
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace ActivityPub\Test\TestUtils;
use ActivityPub\Utils\UuidProvider;
class TestUuidProvider extends UuidProvider
{
/**
* @var array
*/
private $uuids;
/**
* @var int
*/
private $uuidIdx;
/**
* TestUuidProvider constructor.
* @param $uuids array
*/
public function __construct( $uuids )
{
$this->uuids = $uuids;
$this->uuidIdx = 0;
}
public function uuid()
{
$uuid = $this->uuids[$this->uuidIdx];
$this->uuidIdx = ( $this->uuidIdx + 1 ) % count( $this->uuids );
return $uuid;
}
}