[WIP] Begin implementing JsonLdNode API

This commit is contained in:
Jeremy Dormitzer 2019-04-27 16:04:06 -04:00
parent cb3abbdc3c
commit 2a58f0b5de
6 changed files with 623 additions and 2 deletions

View File

@ -31,6 +31,7 @@
"doctrine/orm": "2.5.14", "doctrine/orm": "2.5.14",
"friendica/json-ld": "^1.1", "friendica/json-ld": "^1.1",
"guzzlehttp/guzzle": "^6.3", "guzzlehttp/guzzle": "^6.3",
"ml/json-ld": "1.1.0",
"monolog/monolog": "^1.0", "monolog/monolog": "^1.0",
"phpseclib/phpseclib": "^2.0", "phpseclib/phpseclib": "^2.0",
"psr/http-message": "^1.0", "psr/http-message": "^1.0",
@ -42,9 +43,9 @@
"zendframework/zend-diactoros": "1.4.1" "zendframework/zend-diactoros": "1.4.1"
}, },
"require-dev": { "require-dev": {
"ext-pdo": "*",
"phpunit/dbunit": "^2.0", "phpunit/dbunit": "^2.0",
"phpunit/phpunit": "^4.0", "phpunit/phpunit": "^4.0"
"ext-pdo": "*"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

View File

@ -0,0 +1,18 @@
<?php
namespace ActivityPub\JsonLd\Dereferencer;
use ActivityPub\JsonLd\Exceptions\NodeNotFoundException;
use ActivityPub\JsonLd\JsonLdNode;
interface DereferencerInterface
{
/**
* @param string $iri The IRI to dereference.
* @return JsonLdNode The dereferenced node.
* @throws NodeNotFoundException If a node with the IRI could not be found.
*/
public function dereference( $iri );
}

View File

@ -0,0 +1,14 @@
<?php
namespace ActivityPub\JsonLd\Exceptions;
use Exception;
class NodeNotFoundException extends Exception
{
public function __construct( $iri, $previous = null )
{
$message = "Node $iri not found.";
parent::__construct( $message, 0, $previous );
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace ActivityPub\JsonLd\Exceptions;
use Exception;
use Throwable;
/**
* This exception is thrown on an attempt to access an undefined property on a JsonLdNode.
* Class PropertyNotDefinedException
* @package ActivityPub\JsonLd\Exceptions
*/
class PropertyNotDefinedException extends Exception
{
/**
* PropertyNotDefinedException constructor.
* @param string $name The name of the undefined property.
* @param Throwable|null $previous
*/
public function __construct( $name, Throwable $previous = null )
{
$message = "Property $name is not defined.";
parent::__construct( $message, 0, $previous );
}
}

265
src/JsonLd/JsonLdNode.php Normal file
View File

@ -0,0 +1,265 @@
<?php
namespace ActivityPub\JsonLd;
use ActivityPub\JsonLd\Exceptions\PropertyNotDefinedException;
use ArrayAccess;
use ML\JsonLD\JsonLD;
use ML\JsonLD\Node;
use ML\JsonLD\Value;
/**
* Class JsonLdNode
* @package ActivityPub\JsonLd
*
* A representation of a node in a JSON-LD graph. Supports lazy-loading linked nodes and persisting RDF triples to
* a storage backend.
*/
class JsonLdNode implements ArrayAccess
{
/**
* The internal representation of the node.
* @var Node
*/
private $node;
/**
* The JSON-LD context that should be used when getting/setting properties on this node.
* @var array|\stdClass|string
*/
private $context;
/**
* JsonLdNode constructor.
* @param \stdClass $jsonLd The JSON-LD input as a stdClass.
* @param string $context This node's JSON-LD context.
*/
public function __construct( $jsonLd, $context )
{
$doc = JsonLD::getDocument( $jsonLd );
$graph = $doc->getGraph();
$id = empty( $doc->getIri() ) ? '_:b0' : $doc->getIri();
$this->node = $graph->getNode( $id );
if ( is_null( $this->node ) ) {
$this->node = $graph->createNode();
}
$this->context = $context;
}
/**
* Cardinality-one get. Gets the single value for the property named $name.
* If there are multiple values defined for the property, only the first value is returned.
* @param string $name The property name to get.
* @return mixed A single property value.
* @throws PropertyNotDefinedException If no property named $name exists.
*/
public function get( $name )
{
$property = $this->getNodeProperty( $name );
if ( is_array( $property ) ) {
$property = $property[0];
}
return $this->resolveProperty( $property );
}
/**
* Cardinality-many get. Gets all the values for the property named $name.
* If there is only one value defined for the property, it is returned as a length-1 array.
* @param string $name The property name to get.
* @return mixed A single property value.
* @throws PropertyNotDefinedException If no property named $name exists.
*/
public function getMany( $name )
{
$property = $this->getNodeProperty( $name );
if ( ! is_array( $property ) ) {
$property = array( $property );
}
return $this->resolveProperty( $property );
}
/**
* A convenience wrapper around $this->get( $name ). Cardinality-one.
* @param string $name
* @return mixed
* @throws PropertyNotDefinedException
*/
public function __get( $name )
{
return $this->get( $name );
}
/**
* Gets the value of the property named $name.
* @param string $name
* @return mixed
* @throws PropertyNotDefinedException if no property named $name exists.
*/
private function getNodeProperty( $name )
{
$expandedName = $this->expand_name( $name );
$property = $this->node->getProperty( $expandedName );
if ( is_null( $property ) ) {
throw new PropertyNotDefinedException( $name );
}
return $property;
}
/**
* Resolves the result of $this->node->getProperty() to something the application can use.
* @param mixed $property
* @return array|string
*/
private function resolveProperty( $property )
{
if ( $property instanceof Value ) {
return $property->getValue();
} else if ( is_array( $property ) ) {
return array_map( array( $this, 'resolveProperty' ), $property );
}
// TODO handle lazy-loading linked nodes here
// also, figure out what to do about as:items -- the vocab says it should be a node but the JsonLD lib
// seems to resolve it to an array if it comes in as an array of string values...
}
/**
* Sets the value for a new or existing property on the node.
* If the property already exists, the new value overwrites the old value(s).
* @param string $name
* @param string|\stdClass|array $value
*/
public function setProperty( $name, $value )
{
$expandedName = $this->expand_name( $name );
if ( $value instanceof \stdClass || is_array( $value ) ) {
// TODO handle adding a new linked node here
// should instantiate a new JsonLdNode and recursively call __set
} else if ( $value instanceof JsonLdNode ) {
// TODO handle adding a new linked node here
// by getting the \ML\JsonLD\Node instance from the $value and calling $this->node->addPropertyValue()
} else {
$this->node->setProperty( $expandedName, $value );
}
}
/**
* Convenience wrapper around $this->setProperty().
* If the property already exists, the new value overwrites the old value(s).
* @param string $name
* @param string|\stdClass|array $value
*/
public function __set( $name, $value )
{
return $this->setProperty( $name, $value );
}
/**
* Adds a new value to a new or existing property on the node.
* If the property already exists, the new value is added onto the existing values rather than
* overwriting them.
* @param string $name
* @param string|\stdClass|array $value
*/
public function addPropertyValue( $name, $value )
{
$expandedName = $this->expand_name( $name );
if ( $value instanceof \stdClass || is_array( $value ) ) {
// TODO handle adding a new linked node here
// should instantiate a new JsonLdNode and recursively call __set
} else if ( $value instanceof JsonLdNode ) {
// TODO handle adding a new linked node here
// by getting the \ML\JsonLD\Node instance from the $value and calling $this->node->addPropertyValue()
} else {
$this->node->addPropertyValue( $expandedName, $value );
}
}
/**
* Clears the property named $name, if it exists.
* @param string $name
*/
public function clearProperty( $name )
{
return $this->setProperty( $name, null );
}
/**
* Resolves $name to a full IRI given the JSON-LD context of this node.
* @param string $name The name of the property to resolve.
* @return string The expanded name.
*/
private function expand_name( $name )
{
$dummyObj = (object) array(
'@context' => $this->context,
$name => '_dummyValue',
);
$expanded = (array) JsonLD::expand( $dummyObj )[0];
return array_keys( $expanded )[0];
}
/**
* Whether a offset exists
* @link https://php.net/manual/en/arrayaccess.offsetexists.php
* @param mixed $offset <p>
* An offset to check for.
* </p>
* @return boolean true on success or false on failure.
* </p>
* <p>
* The return value will be casted to boolean if non-boolean was returned.
* @since 5.0.0
*/
public function offsetExists( $offset )
{
$expandedName = $this->expand_name( (string) $offset );
return !is_null( $this->node->getProperty( $expandedName ) );
}
/**
* Offset to retrieve
* @link https://php.net/manual/en/arrayaccess.offsetget.php
* @param mixed $offset <p>
* The offset to retrieve.
* </p>
* @return mixed Can return all value types.
* @since 5.0.0
* @throws PropertyNotDefinedException
*/
public function offsetGet( $offset )
{
return $this->get( (string) $offset );
}
/**
* Offset to set
* @link https://php.net/manual/en/arrayaccess.offsetset.php
* @param mixed $offset <p>
* The offset to assign the value to.
* </p>
* @param mixed $value <p>
* The value to set.
* </p>
* @return void
* @since 5.0.0
*/
public function offsetSet( $offset, $value )
{
return $this->setProperty( (string) $offset, $value );
}
/**
* Offset to unset
* @link https://php.net/manual/en/arrayaccess.offsetunset.php
* @param mixed $offset <p>
* The offset to unset.
* </p>
* @return void
* @since 5.0.0
*/
public function offsetUnset( $offset )
{
return $this->clearProperty( (string) $offset );
}
}

View File

@ -0,0 +1,298 @@
<?php
namespace ActivityPub\Test\JsonLd;
use ActivityPub\JsonLd\Exceptions\PropertyNotDefinedException;
use ActivityPub\JsonLd\JsonLdNode;
use ActivityPub\Test\TestConfig\APTestCase;
use stdClass;
class JsonLdNodeTest extends APTestCase
{
private $asContext = array(
'https://www.w3.org/ns/activitystreams',
);
public function provideForBasicGetProperty()
{
return array(
array(
(object) array(
'https://www.w3.org/ns/activitystreams#name' => 'Object1',
),
$this->asContext,
'name',
'Object1',
),
array(
(object) array(
'@context' => array(
'https://www.w3.org/ns/activitystreams',
),
'name' => 'Object2',
),
$this->asContext,
'name',
'Object2',
),
array(
(object) array(
'https://www.w3.org/ns/activitystreams#name' => 'Object1',
),
$this->asContext,
'https://www.w3.org/ns/activitystreams#name',
'Object1',
),
array(
(object) array(
'https://www.w3.org/ns/activitystreams#name' => 'Object1',
),
$this->asContext,
'foo',
null,
PropertyNotDefinedException::class,
),
array(
(object) array(
'https://www.w3.org/ns/activitystreams#name' => 'Object1',
),
(object) array(
'as' => 'https://www.w3.org/ns/activitystreams#'
),
'as:name',
'Object1',
),
array(
(object) array(
'https://www.w3.org/ns/activitystreams#subject' => array(
'https://example.org/item/1',
'https://example.org/item/2',
),
),
$this->asContext,
'subject',
'https://example.org/item/1',
),
);
}
/**
* @dataProvider provideForBasicGetProperty
*/
public function testBasicGetProperty( $inputObj, $context, $propertyName, $expectedValue, $expectedException = null )
{
$node = $this->makeJsonLdNode( $inputObj, $context );
if ( $expectedException ) {
$this->setExpectedException( $expectedException );
}
$propertyValue = $node->get( $propertyName );
$this->assertEquals( $expectedValue, $propertyValue );
}
/**
* @dataProvider provideForBasicGetProperty
*/
public function testBasicMagicGetProperty( $inputObj, $context, $propertyName, $expectedValue, $expectedException = null )
{
$node = $this->makeJsonLdNode( $inputObj, $context );
if ( $expectedException ) {
$this->setExpectedException( $expectedException );
}
$propertyValue = $node->$propertyName;
$this->assertEquals( $expectedValue, $propertyValue );
}
/**
* @dataProvider provideForBasicGetProperty
*/
public function testBasicArrayAccessProperty( $inputObj, $context, $propertyName, $expectedValue, $expectedException = null )
{
$node = $this->makeJsonLdNode( $inputObj, $context );
if ( $expectedException ) {
$this->setExpectedException( $expectedException );
}
$propertyValue = $node[$propertyName];
$this->assertEquals( $expectedValue, $propertyValue );
}
public function provideForBasicGetMany()
{
return array(
array(
(object) array(
'https://www.w3.org/ns/activitystreams#name' => 'Object1',
),
$this->asContext,
'name',
array( 'Object1' ),
),
array(
(object) array(
'@context' => array(
'https://www.w3.org/ns/activitystreams',
),
'name' => 'Object2',
),
$this->asContext,
'name',
array( 'Object2' ),
),
array(
(object) array(
'https://www.w3.org/ns/activitystreams#name' => 'Object1',
),
$this->asContext,
'https://www.w3.org/ns/activitystreams#name',
array( 'Object1' ),
),
array(
(object) array(
'https://www.w3.org/ns/activitystreams#name' => 'Object1',
),
$this->asContext,
'foo',
null,
PropertyNotDefinedException::class,
),
array(
(object) array(
'https://www.w3.org/ns/activitystreams#name' => 'Object1',
),
(object) array(
'as' => 'https://www.w3.org/ns/activitystreams#'
),
'as:name',
array( 'Object1' ),
),
array(
(object) array(
'https://www.w3.org/ns/activitystreams#subject' => array(
'https://example.org/item/1',
'https://example.org/item/2',
),
),
$this->asContext,
'subject',
array( 'https://example.org/item/1', 'https://example.org/item/2' ),
),
);
}
/**
* @dataProvider provideForBasicGetMany
*/
public function testBasicGetMany( $inputObj, $context, $propertyName, $expectedValue, $expectedException = null )
{
$node = $this->makeJsonLdNode( $inputObj, $context );
if ( $expectedException ) {
$this->setExpectedException( $expectedException );
}
$propertyValue = $node->getMany( $propertyName );
$this->assertEquals( $expectedValue, $propertyValue );
}
public function provideForBasicSetProperty()
{
return array(
array(
new stdClass(),
$this->asContext,
'name',
'NewName',
'https://www.w3.org/ns/activitystreams#name',
'NewName'
),
array(
(object) array(
'https://www.w3.org/ns/activitystreams#name' => 'OldName',
),
$this->asContext,
'name',
'NewName',
'https://www.w3.org/ns/activitystreams#name',
'NewName'
),
);
}
/**
* @dataProvider provideForBasicSetProperty
*/
public function testBasicSetProperty( $inputObj, $context, $propertyName, $newValue, $getPropertyName, $expectedValue, $expectedException = null )
{
$node = $this->makeJsonLdNode( $inputObj, $context );
if ( $expectedException ) {
$this->setExpectedException( $expectedException );
}
$node->setProperty( $propertyName, $newValue );
$this->assertEquals( $expectedValue, $node->$getPropertyName );
}
/**
* @dataProvider provideForBasicSetProperty
*/
public function testBasicMagicSetProperty( $inputObj, $context, $propertyName, $newValue, $getPropertyName, $expectedValue, $expectedException = null )
{
$node = $this->makeJsonLdNode( $inputObj, $context );
if ( $expectedException ) {
$this->setExpectedException( $expectedException );
}
$node->$propertyName = $newValue;
$this->assertEquals( $expectedValue, $node->$getPropertyName );
}
/**
* @dataProvider provideForBasicSetProperty
*/
public function testBasicArraySetProperty( $inputObj, $context, $propertyName, $newValue, $getPropertyName, $expectedValue, $expectedException = null )
{
$node = $this->makeJsonLdNode( $inputObj, $context );
if ( $expectedException ) {
$this->setExpectedException( $expectedException );
}
$node[$propertyName] = $newValue;
$this->assertEquals( $expectedValue, $node[$getPropertyName] );
}
public function provideForBasicAddPropertyValue()
{
return array(
array(
new stdClass(),
$this->asContext,
'name',
'NewName',
'name',
array( 'NewName' ),
),
array(
(object) array(
'https://www.w3.org/ns/activitystreams#name' => 'OldName',
),
$this->asContext,
'name',
'NewName',
'name',
array( 'OldName', 'NewName' ),
),
);
}
/**
* @dataProvider provideForBasicAddPropertyValue
*/
public function testBasicAddPropertyValue( $inputObj, $context, $propertyName, $newValue, $getPropertyName, $expectedValue, $expectedException = null )
{
$node = $this->makeJsonLdNode( $inputObj, $context );
if ( $expectedException ) {
$this->setExpectedException( $expectedException );
}
$node->addPropertyValue( $propertyName, $newValue );
$this->assertEquals( $expectedValue, $node->getMany( $getPropertyName ) );
}
private function makeJsonLdNode( $inputObj, $context )
{
return new JsonLdNode( $inputObj, $context );
}
}