diff --git a/src/RdfWriterBase.php b/src/RdfWriterBase.php index c3522e6..da2c7f6 100644 --- a/src/RdfWriterBase.php +++ b/src/RdfWriterBase.php @@ -1,648 +1,656 @@ role = $role; $this->labeler = $labeler ?: new BNodeLabeler(); $this->registerShorthand( 'a', 'rdf', 'type' ); $this->prefix( 'rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' ); $this->prefix( 'xsd', 'http://www.w3.org/2001/XMLSchema#' ); } /** * @param string $role * @param BNodeLabeler $labeler * * @return RdfWriterBase */ abstract protected function newSubWriter( $role, BNodeLabeler $labeler ); /** * Registers a shorthand that can be used instead of a qname, * like 'a' can be used instead of 'rdf:type'. * * @param string $shorthand * @param string $prefix * @param string $local */ protected function registerShorthand( $shorthand, $prefix, $local ) { $this->shorthands[$shorthand] = [ $prefix, $local ]; } /** * Registers a prefix * * @param string $prefix * @param string $iri The base IRI * * @throws LogicException */ public function prefix( $prefix, $iri ) { if ( $this->prefixesLocked ) { throw new LogicException( 'Prefixes can not be added after start()' ); } $this->prefixes[$prefix] = $iri; } /** * Determines whether $shorthand can be used as a shorthand. * * @param string $shorthand * * @return bool */ protected function isShorthand( $shorthand ) { return isset( $this->shorthands[$shorthand] ); } /** * Determines whether $shorthand can legally be used as a prefix. * * @param string $prefix * * @return bool */ protected function isPrefix( $prefix ) { return isset( $this->prefixes[$prefix] ); } /** * Returns the prefix map. * * @return string[] An associative array mapping prefixes to base IRIs. */ public function getPrefixes() { return $this->prefixes; } /** * @param string|null $languageCode * * @return bool */ protected function isValidLanguageCode( $languageCode ) { // preg_match is somewhat (12%) slower than strspn but more readable return $languageCode !== null && preg_match( '/^[\da-z-]{2,}$/i', $languageCode ); } /** * @return RdfWriter */ final public function sub() { $writer = $this->newSubWriter( self::SUBDOCUMENT_ROLE, $this->labeler ); $writer->state = self::STATE_DOCUMENT; // share registered prefixes $writer->prefixes =& $this->prefixes; $this->subs[] = $writer; return $writer; } /** * Returns the writers role. The role determines the behavior of the writer with respect * to which states and transitions are possible: a BNODE_ROLE writer would for instance * not accept a call to about(), since it can only process triples about a single subject * (the blank node it represents). * * @return string A string corresponding to one of the the XXX_ROLE constants. */ final public function getRole() { return $this->role; } /** * Appends string to the output buffer. * @param string $w */ final protected function write( $w ) { $this->buffer[] = $w; } /** * If $base is a shorthand, $base and $local are updated to hold whatever qname * the shorthand was associated with. * * Otherwise, $base and $local remain unchanged. * * @param string &$base * @param string|null &$local */ protected function expandShorthand( &$base, &$local ) { if ( $local === null && isset( $this->shorthands[$base] ) ) { list( $base, $local ) = $this->shorthands[$base]; } } /** * If $base is a registered prefix, $base will be replaced by the base IRI associated with * that prefix, with $local appended. $local will be set to null. * * Otherwise, $base and $local remain unchanged. * * @param string &$base * @param string|null &$local * * @throws LogicException */ protected function expandQName( &$base, &$local ) { if ( $local !== null && $base !== '_' ) { if ( isset( $this->prefixes[$base] ) ) { $base = $this->prefixes[$base] . $local; // XXX: can we avoid this concat? $local = null; } else { throw new LogicException( 'Unknown prefix: ' . $base ); } } } /** * @see RdfWriter::blank() * * @param string|null $label node label, will be generated if not given. * * @return string */ final public function blank( $label = null ) { return $this->labeler->getLabel( $label ); } /** * @see RdfWriter::start() */ final public function start() { $this->state( self::STATE_DOCUMENT ); $this->prefixesLocked = true; } /** * @see RdfWriter::finish() */ final public function finish() { // close all unclosed states $this->state( self::STATE_DOCUMENT ); // ...then insert output of sub-writers into the buffer, // so it gets placed before the footer... $this->drainSubs(); // and then finalize $this->state( self::STATE_FINISH ); // Detaches all subs. $this->subs = []; } /** * @see RdfWriter::drain() * * @return string RDF */ final public function drain() { // we can drain after finish, but finish state is sticky if ( $this->state !== self::STATE_FINISH ) { $this->state( self::STATE_DOCUMENT ); } $this->drainSubs(); $this->flattenBuffer(); $rdf = implode( '', $this->buffer ); $this->buffer = []; return $rdf; } /** * Calls drain() an any RdfWriter instances in $this->buffer, and replaces them * in $this->buffer with the string returned by the drain() call. Any closures * present in the $this->buffer will be called, and replaced by their return value. */ private function flattenBuffer() { foreach ( $this->buffer as &$b ) { if ( $b instanceof Closure ) { $b = $b(); } if ( $b instanceof RdfWriter ) { $b = $b->drain(); } } } /** * Drains all subwriters, and appends their output to this writer's buffer. * Subwriters remain usable. */ private function drainSubs() { foreach ( $this->subs as $sub ) { $rdf = $sub->drain(); $this->write( $rdf ); } } /** * @see RdfWriter::about() * * @param string $base A QName prefix if $local is given, or an IRI if $local is null. * @param string|null $local A QName suffix, or null if $base is an IRI. * * @return RdfWriter $this */ final public function about( $base, $local = null ) { $this->expandSubject( $base, $local ); if ( $this->state === self::STATE_OBJECT && $base === $this->currentSubject[0] && $local === $this->currentSubject[1] ) { return $this; // redundant about() call } $this->state( self::STATE_SUBJECT ); $this->currentSubject[0] = $base; $this->currentSubject[1] = $local; $this->currentPredicate[0] = null; $this->currentPredicate[1] = null; $this->writeSubject( $base, $local ); return $this; } /** * @see RdfWriter::a() * Shorthand for say( 'a' )->is( $type ). * * @param string $typeBase The data type's QName prefix if $typeLocal is given, * or an IRI or shorthand if $typeLocal is null. * @param string|null $typeLocal The data type's QName suffix, * or null if $typeBase is an IRI or shorthand. * * @return RdfWriter $this */ final public function a( $typeBase, $typeLocal = null ) { return $this->say( 'a' )->is( $typeBase, $typeLocal ); } /** * @see RdfWriter::say() * * @param string $base A QName prefix. * @param string|null $local A QName suffix. * * @return RdfWriter $this */ final public function say( $base, $local = null ) { $this->expandPredicate( $base, $local ); if ( $this->state === self::STATE_OBJECT && $base === $this->currentPredicate[0] && $local === $this->currentPredicate[1] ) { return $this; // redundant about() call } $this->state( self::STATE_PREDICATE ); $this->currentPredicate[0] = $base; $this->currentPredicate[1] = $local; $this->writePredicate( $base, $local ); return $this; } /** * @see RdfWriter::is() * * @param string $base A QName prefix if $local is given, or an IRI if $local is null. * @param string|null $local A QName suffix, or null if $base is an IRI. * * @return RdfWriter $this */ final public function is( $base, $local = null ) { $this->state( self::STATE_OBJECT ); $this->expandResource( $base, $local ); $this->writeResource( $base, $local ); return $this; } /** * @see RdfWriter::text() * * @param string $text the text to be placed in the output * @param string|null $language the language the text is in * * @return $this */ final public function text( $text, $language = null ) { $this->state( self::STATE_OBJECT ); $this->writeText( $text, $language ); return $this; } /** * @see RdfWriter::value() * * @param string $value the value encoded as a string * @param string|null $typeBase The data type's QName prefix if $typeLocal is given, * or an IRI or shorthand if $typeLocal is null. * @param string|null $typeLocal The data type's QName suffix, * or null if $typeBase is an IRI or shorthand. * * @return $this */ final public function value( $value, $typeBase = null, $typeLocal = null ) { $this->state( self::STATE_OBJECT ); if ( $typeBase === null && !is_string( $value ) ) { $vtype = gettype( $value ); switch ( $vtype ) { case 'integer': $typeBase = 'xsd'; $typeLocal = 'integer'; $value = "$value"; break; case 'double': $typeBase = 'xsd'; $typeLocal = 'double'; $value = "$value"; break; case 'boolean': $typeBase = 'xsd'; $typeLocal = 'boolean'; $value = $value ? 'true' : 'false'; break; } } $this->expandType( $typeBase, $typeLocal ); $this->writeValue( $value, $typeBase, $typeLocal ); return $this; } /** * State transition table * First state is "from", second is "to" * @var array */ protected $transitionTable = [ self::STATE_START => [ self::STATE_DOCUMENT => true, ], self::STATE_DOCUMENT => [ self::STATE_DOCUMENT => true, self::STATE_SUBJECT => true, self::STATE_FINISH => true, ], self::STATE_SUBJECT => [ self::STATE_PREDICATE => true, ], self::STATE_PREDICATE => [ self::STATE_OBJECT => true, ], self::STATE_OBJECT => [ self::STATE_DOCUMENT => true, self::STATE_SUBJECT => true, self::STATE_PREDICATE => true, self::STATE_OBJECT => true, ], ]; /** * Perform a state transition. Writer states roughly correspond to states in a naive * regular parser for the respective syntax. State transitions may generate output, * particularly of structural elements which correspond to terminals in a respective * parser. * * @param int $newState one of the self::STATE_... constants * * @throws LogicException */ final protected function state( $newState ) { if ( !isset( $this->transitionTable[$this->state][$newState] ) ) { throw new LogicException( 'Bad transition: ' . $this->state . ' -> ' . $newState ); } $action = $this->transitionTable[$this->state][$newState]; if ( $action !== true ) { if ( is_string( $action ) ) { $this->write( $action ); } else { $action(); } } $this->state = $newState; } /** * Must be implemented to generate output that starts a statement (or set of statements) * about a subject. Depending on the requirements of the output format, the implementation * may be empty. * * @note: $base and $local are given as passed to about() and processed by expandSubject(). * * @param string $base * @param string|null $local */ abstract protected function writeSubject( $base, $local = null ); /** * Must be implemented to generate output that represents the association of a predicate * with a subject that was previously defined by a call to writeSubject(). * * @note: $base and $local are given as passed to say() and processed by expandPredicate(). * * @param string $base * @param string|null $local */ abstract protected function writePredicate( $base, $local = null ); /** * Must be implemented to generate output that represents a resource used as the object * of a statement. * * @note: $base and $local are given as passed to is() and processed by expandObject(). * * @param string $base * @param string|null $local */ abstract protected function writeResource( $base, $local = null ); /** * Must be implemented to generate output that represents a text used as the object * of a statement. * * @param string $text the text to be placed in the output * @param string|null $language the language the text is in */ abstract protected function writeText( $text, $language ); /** * Must be implemented to generate output that represents a (typed) literal used as the object * of a statement. * * @note: $typeBase and $typeLocal are given as passed to value() and processed by expandType(). * * @param string $value the value encoded as a string * @param string|null $typeBase * @param string|null $typeLocal */ abstract protected function writeValue( $value, $typeBase, $typeLocal = null ); /** * Perform any expansion (shorthand to qname, qname to IRI) desired * for subject identifiers. * * @param string &$base * @param string|null &$local */ protected function expandSubject( &$base, &$local ) { } /** * Perform any expansion (shorthand to qname, qname to IRI) desired * for predicate identifiers. * * @param string &$base * @param string|null &$local */ protected function expandPredicate( &$base, &$local ) { } /** * Perform any expansion (shorthand to qname, qname to IRI) desired * for resource identifiers. * * @param string &$base * @param string|null &$local */ protected function expandResource( &$base, &$local ) { } /** * Perform any expansion (shorthand to qname, qname to IRI) desired * for type identifiers. * * @param string|null &$base * @param string|null &$local */ protected function expandType( &$base, &$local ) { } + /** + * Set BNode labeler. + * @param BNodeLabeler $labeler + */ + public function setLabeler( BNodeLabeler $labeler ) { + $this->labeler = $labeler; + } + } diff --git a/tests/phpunit/RdfWriterTestBase.php b/tests/phpunit/RdfWriterTestBase.php index 7393f19..34383c8 100644 --- a/tests/phpunit/RdfWriterTestBase.php +++ b/tests/phpunit/RdfWriterTestBase.php @@ -1,454 +1,464 @@ newWriter()->getMimeType(); $this->assertInternalType( 'string', $mimeType ); $this->assertRegExp( '/^\w+\/[\w-]+(\+(xml|json))?(; charset=UTF-8)?$/', $mimeType ); } public function testTriples() { $writer = $this->newWriter(); $writer->prefix( 'acme', 'http://acme.test/' ); $writer->start(); $writer->about( 'http://foobar.test/Bananas' ) ->say( 'a' )->is( 'http://foobar.test/Fruit' ); // shorthand name "a" $writer->about( 'acme', 'Nuts' ) ->say( 'acme', 'weight' )->value( '5.5', 'xsd', 'decimal' ); // redundant about( 'acme', 'Nuts' ) $writer->about( 'acme', 'Nuts' ) ->say( 'acme', 'color' )->value( 'brown' ); $writer->finish(); $rdf = $writer->drain(); $this->assertOutputLines( 'Triples', $rdf ); } public function testPredicates() { $writer = $this->newWriter(); $writer->prefix( '', 'http://acme.test/' ); // empty prefix $writer->start(); $writer->about( 'http://foobar.test/Bananas' ) ->a( 'http://foobar.test/Fruit' ) // shorthand function a() ->say( '', 'name' ) // empty prefix ->text( 'Banana' ) ->say( '', 'name' ) // redundant say( '', 'name' ) ->text( 'Banane', 'de' ); $writer->about( 'http://foobar.test/Apples' ) ->say( '', 'name' ) // subsequent call to say( '', 'name' ) for a different subject ->text( 'Apple' ); $writer->finish(); $rdf = $writer->drain(); $this->assertOutputLines( 'Predicates', $rdf ); } public function testPredicates_drain() { $writer = $this->newWriter(); $writer->prefix( '', 'http://acme.test/' ); // empty prefix $writer->start(); $writer->about( 'http://foobar.test/Bananas' ) ->a( 'http://foobar.test/Fruit' ) // shorthand function a() ->say( '', 'name' ) // empty prefix ->text( 'Banana' ) ->say( '', 'name' ) // redundant say( '', 'name' ) ->text( 'Banane', 'de' ); $rdf1 = $writer->drain(); $this->assertNotEmpty( $rdf1 ); $writer->about( 'http://foobar.test/Apples' ) ->say( '', 'name' ) // subsequent call to say( '', 'name' ) for a different subject ->text( 'Apple' ); $writer->finish(); $rdf2 = $writer->drain(); $this->assertNotEmpty( $rdf2 ); $this->assertOutputLines( 'Predicates', $rdf1 . $rdf2 ); } public function testPredicates_sub() { $writer = $this->newWriter(); $writer->prefix( '', 'http://acme.test/' ); // empty prefix $writer->start(); $sub = $writer->sub(); // output of the sub writer will appear after the output of the main writer. $sub->about( 'http://foobar.test/Apples' ) ->say( '', 'name' ) // subsequent call to say( '', 'name' ) for a different subject ->text( 'Apple' ); $writer->about( 'http://foobar.test/Bananas' ) ->a( 'http://foobar.test/Fruit' ) // shorthand function a() ->say( '', 'name' ) // empty prefix ->text( 'Banana' ) ->say( '', 'name' ) // redundant say( '', 'name' ) ->text( 'Banane', 'de' ); $writer->finish(); $rdf = $writer->drain(); $this->assertOutputLines( 'Predicates', $rdf ); } public function testPredicates_sub_drain() { $writer = $this->newWriter(); $writer->prefix( '', 'http://acme.test/' ); // empty prefix $writer->start(); $sub = $writer->sub(); $writer->about( 'http://foobar.test/Bananas' ) ->a( 'http://foobar.test/Fruit' ) // shorthand function a() ->say( '', 'name' ) // empty prefix ->text( 'Banana' ) ->say( '', 'name' ) // redundant say( '', 'name' ) ->text( 'Banane', 'de' ); $rdf1 = $writer->drain(); $this->assertNotEmpty( $rdf1 ); // sub-writer should still be usable after drain() $sub->about( 'http://foobar.test/Apples' ) ->say( '', 'name' ) // subsequent call to say( '', 'name' ) for a different subject ->text( 'Apple' ); $writer->finish(); $rdf2 = $writer->drain(); $this->assertNotEmpty( $rdf2 ); $this->assertOutputLines( 'Predicates', $rdf1 . $rdf2 ); } public function testValues() { $writer = $this->newWriter(); $writer->prefix( 'acme', 'http://acme.test/' ); $writer->start(); $writer->about( 'http://foobar.test/Bananas' ) ->say( 'acme', 'multi' ) ->value( 'A' ) ->value( 'B' ) ->value( 'C' ) ->say( 'acme', 'type' ) ->value( 'foo', 'acme', 'thing' ) ->value( '-5', 'xsd', 'integer' ) ->value( '-5', 'xsd', 'decimal' ) ->value( '-5', 'xsd', 'double' ) ->value( 'true', 'xsd', 'boolean' ) ->value( 'false', 'xsd', 'boolean' ) ->say( 'acme', 'autotype' ) ->value( -5 ) ->value( 3.14 ) ->value( true ) ->value( false ) ->say( 'acme', 'no-autotype' ) ->value( -5, 'xsd', 'decimal' ) ->value( 3.14, 'xsd', 'string' ) ->value( true, 'xsd', 'string' ) ->value( false, 'xsd', 'string' ) ->say( 'acme', 'shorthand' )->value( 'foo' ) ->say( 'acme', 'typed-shorthand' )->value( 'foo', 'acme', 'thing' ); $writer->finish(); $rdf = $writer->drain(); $this->assertOutputLines( 'Values', $rdf ); } public function testResources() { $writer = $this->newWriter(); $writer->prefix( 'acme', 'http://acme.test/' ); $writer->start(); $writer->about( 'acme', 'Bongos' ) ->say( 'acme', 'sounds' ) ->is( 'acme', 'Bing' ) ->is( 'http://foobar.test/sound/Bang' ); $writer->finish(); $rdf = $writer->drain(); $this->assertOutputLines( 'Resources', $rdf ); } public function testTexts() { $writer = $this->newWriter(); $writer->prefix( 'acme', 'http://acme.test/' ); $writer->start(); $writer->about( 'acme', 'Bongos' ) ->say( 'acme', 'sounds' ) ->text( 'Bom', 'de' ) ->text( 'Bam', 'en' ) ->text( 'Como estas', 'es-419' ) ->text( 'What?', 'bad tag' ); $writer->finish(); $rdf = $writer->drain(); $this->assertOutputLines( 'Texts', $rdf ); } public function testNumbers() { $writer = $this->newWriter(); $writer->prefix( 'acme', 'http://acme.test/' ); $writer->start(); $writer->about( 'acme', 'Bongos' ) ->say( 'acme', 'stock' )->value( 5, 'xsd', 'integer' ) ->value( 7 ) ->about( 'acme', 'Tablas' ) ->say( 'acme', 'stock' )->value( 6 ); $writer->finish(); $rdf = $writer->drain(); $this->assertOutputLines( 'Numbers', $rdf ); } public function testEricMiller() { // example taken from http://www.w3.org/2007/02/turtle/primer/ $writer = $this->newWriter(); $writer->prefix( 'contact', 'http://www.w3.org/2000/10/swap/pim/contact#' ); $writer->start(); $writer->about( 'http://www.w3.org/People/EM/contact#me' ) ->say( 'rdf', 'type' )->is( 'contact', 'Person' ) ->say( 'contact', 'fullName' )->text( 'Eric Miller' ) ->say( 'contact', 'mailbox' )->is( 'mailto:em@w3.org' ) ->say( 'contact', 'personalTitle' )->text( 'Dr.' ); $writer->finish(); $rdf = $writer->drain(); $this->assertOutputLines( 'EricMiller', $rdf ); } public function testLabeledBlankNode() { // example taken from http://www.w3.org/2007/02/turtle/primer/ $writer = $this->newWriter(); $writer->prefix( 'exterms', 'http://www.example.org/terms/' ); $writer->prefix( 'exstaff', 'http://www.example.org/staffid/' ); $writer->start(); $writer->about( 'exstaff', '85740' ) ->say( 'exterms', 'address' )->is( '_', $label = $writer->blank( 'johnaddress' ) ) ->about( '_', $label ) ->say( 'exterms', 'street' )->text( '1501 Grant Avenue' ) ->say( 'exterms', 'city' )->text( 'Bedfort' ) ->say( 'exterms', 'state' )->text( 'Massachusetts' ) ->say( 'exterms', 'postalCode' )->text( '01730' ); $writer->finish(); $rdf = $writer->drain(); $this->assertOutputLines( 'LabeledBlankNode', $rdf ); } public function testNumberedBlankNodes() { // example taken from http://www.w3.org/2007/02/turtle/primer/ $writer = $this->newWriter(); $writer->prefix( 'exterms', 'http://www.example.org/terms/' ); $writer->prefix( 'exstaff', 'http://www.example.org/staffid/' ); $writer->prefix( 'ex', 'http://example.org/packages/vocab#' ); $writer->start(); $writer->about( 'exstaff', 'Sue' ) ->say( 'exterms', 'publication' )->is( '_', $label1 = $writer->blank() ); $writer->about( '_', $label1 ) ->say( 'exterms', 'title' )->text( 'Antology of Time' ); $writer->about( 'exstaff', 'Jack' ) ->say( 'exterms', 'publication' )->is( '_', $label2 = $writer->blank() ); $writer->about( '_', $label2 ) ->say( 'exterms', 'title' )->text( 'Anthony of Time' ); $writer->finish(); $rdf = $writer->drain(); $this->assertOutputLines( 'NumberedBlankNode', $rdf ); } public function testQuotesAndSpecials() { $writer = $this->newWriter(); $writer->prefix( 'exterms', 'http://www.example.org/terms/' ); $writer->start(); $writer->about( 'exterms', 'Duck' )->say( 'exterms', 'says' ) ->text( 'Duck says: "Quack!"' ); $writer->about( 'exterms', 'Cow' )->say( 'exterms', 'says' ) ->text( "Cow says:\n\r 'Moo! \\Moo!'" ); $writer->about( 'exterms', 'Bear' )->say( 'exterms', 'says' ) ->text( 'Bear says: Превед!' ); $writer->finish(); $rdf = $writer->drain(); $this->assertOutputLines( 'TextWithSpecialChars', $rdf ); } public function testDumpHeader() { $writer = $this->newWriter(); $writer->prefix( 'wikibase', 'http://wikiba.se/ontology-beta#' ); $writer->prefix( 'schema', 'http://schema.org/' ); $writer->prefix( 'owl', 'http://www.w3.org/2002/07/owl#' ); $writer->prefix( 'cc', 'http://creativecommons.org/ns#' ); $writer->start(); $writer->about( 'wikibase', 'Dump' ) ->a( 'schema', 'Dataset' ) ->a( 'owl', 'Ontology' ) ->say( 'cc', 'license' )->is( 'http://creativecommons.org/publicdomain/zero/1.0/' ) ->say( 'schema', 'softwareVersion' )->value( '0.1.0' ) ->say( 'schema', 'dateModified' )->value( '2017-09-19T22:53:13-04:00', 'xsd', 'dateTime' ) ->say( 'owl', 'imports' )->is( 'http://wikiba.se/ontology-1.0.owl' ); $writer->finish(); $rdf = $writer->drain(); $this->assertOutputLines( 'DumpHeader', $rdf ); } public function testAlternatingValues() { $writer = $this->newWriter(); $writer->prefix( 'wikibase', 'http://wikiba.se/ontology-beta#' ); $writer->prefix( 'owl', 'http://www.w3.org/2002/07/owl#' ); $writer->start(); $writer->about( 'wikibase', 'Dump' ) ->say( 'owl', 'foo' )->is( 'owl', 'A' ) ->say( 'owl', 'bar' )->value( '5', 'xsd', 'decimal' ) ->say( 'owl', 'foo' )->is( 'owl', 'B' ) ->say( 'owl', 'bar' )->value( '6', 'xsd', 'decimal' ) ->say( 'owl', 'foo' )->is( 'owl', 'C' ) ->say( 'owl', 'bar' )->value( '7', 'xsd', 'decimal' ); $writer->finish(); $rdf = $writer->drain(); $this->assertOutputLines( 'AlternatingValues', $rdf ); } public function testTypeConflict() { $writer = $this->newWriter(); $writer->prefix( 'ex', 'http://example.com/' ); $writer->start(); $writer->about( 'ex', 'A' ) ->say( 'ex', 'foo' )->is( 'ex', 'Node' ) ->say( 'ex', 'foo' )->value( '5', 'xsd', 'decimal' ) ->say( 'ex', 'foo' )->value( 'string' ) ->say( 'ex', 'bar' )->value( 'string' ) ->say( 'ex', 'bar' )->value( '5', 'xsd', 'decimal' ) ->say( 'ex', 'bat' )->value( 'string' ); // A blank node is used in clients to indicate "any value" $writer->about( 'ex', 'B' ) ->say( 'ex', 'bat' )->is( '_', $writer->blank() ); $writer->finish(); $rdf = $writer->drain(); $this->assertOutputLines( 'TypeConflict', $rdf ); } + public function testSetLabeler() { + $writer = $this->newWriter(); + $bnode = $writer->blank(); + $this->assertEquals( 'genid1', $bnode ); + $writer->setLabeler( new BNodeLabeler( 'testme2-', 10 ) ); + $bnode = $writer->blank(); + $this->assertEquals( 'testme2-10', $bnode ); + } + /** * @param string $datasetName * @param string[]|string $actual */ private function assertOutputLines( $datasetName, $actual ) { $path = __DIR__ . '/../data/' . $datasetName . '.' . $this->getFileSuffix(); $this->assertNTriplesEquals( file_get_contents( $path ), $actual, "Result mismatches data in $path" ); } /** * @param string[]|string $nTriples * * @return string[] Sorted alphabetically. */ protected function normalizeNTriples( $nTriples ) { if ( is_string( $nTriples ) ) { // Trim and ignore newlines at the end of the file only. $nTriples = explode( "\n", rtrim( $nTriples, "\n" ) ); } if ( $this->sortLines() ) { sort( $nTriples ); } return $nTriples; } /** * @param string[]|string $expected * @param string[]|string $actual * @param string $message */ protected function assertNTriplesEquals( $expected, $actual, $message = '' ) { $expected = $this->normalizeNTriples( $expected ); $actual = $this->normalizeNTriples( $actual ); if ( $this->sortLines() ) { // Comparing $expected and $actual directly would show triples that are present in both but // shifted in position. That makes the output hard to read. Calculating the $missing and // $extra sets helps. $extra = array_diff( $actual, $expected ); $missing = array_diff( $expected, $actual ); // Cute: $missing and $extra can be equal only if they are empty. Comparing them here // directly looks a bit odd in code, but produces meaningful output, especially if the input // was sorted. $this->assertEquals( $missing, $extra, $message ); } else { $this->assertEquals( $expected, $actual, $message ); } } // FIXME: test non-ascii literals! // FIXME: test uerl-encoding // FIXME: test IRIs! }