diff --git a/common/rdf/src/main/java/dev/enola/rdf/RdfThingConverter.java b/common/rdf/src/main/java/dev/enola/rdf/RdfThingConverter.java index 3168fe14a..b84526028 100644 --- a/common/rdf/src/main/java/dev/enola/rdf/RdfThingConverter.java +++ b/common/rdf/src/main/java/dev/enola/rdf/RdfThingConverter.java @@ -27,7 +27,6 @@ import org.eclipse.rdf4j.model.BNode; import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.model.Model; -import org.eclipse.rdf4j.model.Statement; import org.eclipse.rdf4j.model.base.CoreDatatype; import java.util.ArrayList; @@ -81,7 +80,25 @@ public Stream convert(Model input) { } else throw new UnsupportedOperationException("TODO: " + subject); - convert(thing, statement, deferred); + // The goal of this is to turn an RDF Object List into a Thing List Value + var predicate = statement.getPredicate(); + var statements = input.filter(subject, predicate, null); + if (statements.size() == 1) { + convertAndPut(thing, predicate, statement.getObject(), deferred); + var value = convert(thing, predicate, statement.getObject(), deferred); + thing.putFields(predicate.stringValue(), value.build()); + } else { + var valueList = dev.enola.thing.Value.List.newBuilder(); + for (var subStatement : statements) { + var object = subStatement.getObject(); + var value = convert(thing, predicate, object, deferred); + valueList.addValues(value); + } + var value = Value.newBuilder().setList(valueList); + thing.putFields(statement.getPredicate().stringValue(), value.build()); + } + // TODO It's surprising that this this really works (test pass) as-is? + // What causes it not to repeat (duplicate) values? } for (var action : deferred) { @@ -91,9 +108,21 @@ public Stream convert(Model input) { return roots.values().stream(); } - private static void convert( - Builder thing, Statement statement, List>> deferred) { - org.eclipse.rdf4j.model.Value rdfValue = statement.getObject(); + // TODO Remove this again? + private static void convertAndPut( + Builder thing, + IRI predicate, + org.eclipse.rdf4j.model.Value object, + List>> deferred) { + var value = convert(thing, predicate, object, deferred); + thing.putFields(predicate.stringValue(), value.build()); + } + + private static dev.enola.thing.Value.Builder convert( + Builder thing, + IRI predicate, + org.eclipse.rdf4j.model.Value rdfValue, + List>> deferred) { var value = Value.newBuilder(); if (rdfValue.isIRI()) { value.setLink(Link.newBuilder().setIri(rdfValue.stringValue())); @@ -127,7 +156,7 @@ private static void convert( throw new IllegalStateException( bNodeID + " not found: " + map.keySet()); value.setStruct(containedThing); - thing.putFields(statement.getPredicate().stringValue(), value.build()); + thing.putFields(predicate.stringValue(), value.build()); }); } else if (rdfValue.isTriple()) { @@ -137,6 +166,6 @@ private static void convert( throw new UnsupportedOperationException("TODO: Resource"); } - thing.putFields(statement.getPredicate().stringValue(), value.build()); + return value; } } diff --git a/common/rdf/src/main/java/dev/enola/rdf/ThingRdfConverter.java b/common/rdf/src/main/java/dev/enola/rdf/ThingRdfConverter.java index df7641c9f..c3cfa0cf3 100644 --- a/common/rdf/src/main/java/dev/enola/rdf/ThingRdfConverter.java +++ b/common/rdf/src/main/java/dev/enola/rdf/ThingRdfConverter.java @@ -17,7 +17,10 @@ */ package dev.enola.rdf; +import static java.util.Collections.singleton; + import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; import dev.enola.common.convert.ConversionException; import dev.enola.common.convert.Converter; @@ -91,9 +94,10 @@ private void convertInto( } for (var field : from.getFieldsMap().entrySet()) { IRI predicate = vf.createIRI(field.getKey()); - var object = convert(field.getValue(), containedThings); - Statement statement = vf.createStatement(subject, predicate, object); - into.handleStatement(statement); + for (var object : convert(field.getValue(), containedThings)) { + Statement statement = vf.createStatement(subject, predicate, object); + into.handleStatement(statement); + } } for (var containedThing : containedThings.entrySet()) { @@ -106,21 +110,22 @@ private void convertInto( } } - private org.eclipse.rdf4j.model.Value convert( + private Iterable convert( dev.enola.thing.Value value, Map containedThings) { return switch (value.getKindCase()) { - case LINK -> vf.createIRI(value.getLink().getIri()); + case LINK -> singleton(vf.createIRI(value.getLink().getIri())); - case STRING -> vf.createLiteral(value.getString()); + case STRING -> singleton(vf.createLiteral(value.getString())); case LITERAL -> { var literal = value.getLiteral(); - yield vf.createLiteral(literal.getValue(), vf.createIRI(literal.getDatatype())); + yield singleton( + vf.createLiteral(literal.getValue(), vf.createIRI(literal.getDatatype()))); } case LANG_STRING -> { LangString langString = value.getLangString(); - yield vf.createLiteral(langString.getText(), langString.getLang()); + yield singleton(vf.createLiteral(langString.getText(), langString.getLang())); } case STRUCT -> { @@ -133,10 +138,22 @@ private org.eclipse.rdf4j.model.Value convert( bNode = vf.createBNode(); } containedThings.put(bNode, containedThing); - yield bNode; + yield singleton(bNode); } - // case LIST -> throw new UnsupportedOperationException("TODO"); + case LIST -> { + // TODO value.getList().getValuesList().stream().map(eValue -> convert(eValue,? + var enolaValues = value.getList().getValuesList(); + var rdfValues = + ImmutableList.builderWithExpectedSize( + enolaValues.size()); + for (var enolaValue : enolaValues) { + var rdfValue = convert(enolaValue, containedThings); + // Not 100% sure if addAll() is "correct" here... + rdfValues.addAll(rdfValue); + } + yield rdfValues.build(); + } case KIND_NOT_SET -> throw new IllegalArgumentException(value.toString()); }; diff --git a/common/rdf/src/test/java/dev/enola/rdf/RdfThingConverterTest.java b/common/rdf/src/test/java/dev/enola/rdf/RdfThingConverterTest.java index eb98e6933..6a3703d43 100644 --- a/common/rdf/src/test/java/dev/enola/rdf/RdfThingConverterTest.java +++ b/common/rdf/src/test/java/dev/enola/rdf/RdfThingConverterTest.java @@ -19,6 +19,7 @@ import static dev.enola.common.io.mediatype.YamlMediaType.YAML_UTF_8; +import com.google.common.truth.Truth; import com.google.common.truth.extensions.proto.ProtoTruth; import dev.enola.common.convert.ConversionException; @@ -38,7 +39,12 @@ public class RdfThingConverterTest { private final ReadableResource turtle = new ClasspathResource("picasso.turtle", RdfMediaType.TURTLE); - private final ReadableResource yaml = new ClasspathResource("picasso.thing.yaml", YAML_UTF_8); + + private final ReadableResource picassoYaml = + new ClasspathResource("picasso.thing.yaml", YAML_UTF_8); + + private final ReadableResource daliYaml = // + new ClasspathResource("dali.thing.yaml", YAML_UTF_8); private final ProtoIO protoReader = new ProtoIO(); private final RdfReaderConverter rdfReader = new RdfReaderConverter(); @@ -46,29 +52,46 @@ public class RdfThingConverterTest { private final ThingRdfConverter thingToRdfConverter = new ThingRdfConverter(); private Model rdf; - private Thing thing; + private Thing picassoThing; + private Thing daliThing; @Before public void before() throws ConversionException, IOException { rdf = rdfReader.convert(turtle); - thing = protoReader.read(yaml, Thing.newBuilder(), Thing.class); + picassoThing = protoReader.read(picassoYaml, Thing.newBuilder(), Thing.class); + daliThing = protoReader.read(daliYaml, Thing.newBuilder(), Thing.class); } @Test - public void rdfToThing() throws ConversionException, IOException { + public void rdfToPicassoThing() throws ConversionException, IOException { var actualThings = rdfToThingConverter.convertToList(rdf); - var expectedThing = thing; + var expectedThing = picassoThing; ProtoTruth.assertThat(actualThings.get(1).build()).isEqualTo(expectedThing); } @Test - public void thingToRDF() throws ConversionException { - var actualRDF = thingToRdfConverter.convert(thing); - rdf.remove(Values.iri("http://example.enola.dev/Dalí"), null, null); + public void rdfToDaliThing() throws ConversionException, IOException { + var actualThings = rdfToThingConverter.convertToList(rdf); + var expectedThing = daliThing; + ProtoTruth.assertThat(actualThings.get(0).build()).isEqualTo(expectedThing); + } + + @Test + public void picassoThingToRDF() throws ConversionException { + var actualRDF = thingToRdfConverter.convert(picassoThing); + Truth.assertThat(rdf.remove(Values.iri("http://example.enola.dev/Dalí"), null, null)) + .isTrue(); var expectedRDF = rdf; ModelSubject.assertThat(actualRDF).isEqualTo(expectedRDF); } + @Test + public void daliThingToRDF() throws ConversionException { + var actualRDF = thingToRdfConverter.convert(daliThing); + var expectedRDF = rdf.filter(Values.iri("http://example.enola.dev/Dalí"), null, null); + ModelSubject.assertThat(actualRDF).isEqualTo(expectedRDF); + } + @Test public void messageToRDF() { // TODO Implement messageToRDF(), via MessageToThingConverter diff --git a/common/rdf/src/test/resources/dali.thing.yaml b/common/rdf/src/test/resources/dali.thing.yaml new file mode 100644 index 000000000..eeb17b302 --- /dev/null +++ b/common/rdf/src/test/resources/dali.thing.yaml @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2024 The Enola Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +iri: http://example.enola.dev/Dalí +fields: + http://xmlns.com/foaf/0.1/firstName: + { list: { values: [{ string: "Salvador" }, { string: "Domingo" }, { string: "Felipe" }, { string: "Jacinto" }] } } + http://www.w3.org/1999/02/22-rdf-syntax-ns#type: { link: { iri: "http://example.enola.dev/Artist" } } diff --git a/common/thing/thing.proto b/common/thing/thing.proto index 7f02077b8..757c9df3d 100644 --- a/common/thing/thing.proto +++ b/common/thing/thing.proto @@ -56,6 +56,12 @@ message Value { // https://github.com/enola-dev/enola/pull/540... LangString lang_string = 4; + // Sub-structure (contained) Thing. + Thing struct = 18; + + // List of Values. + List list = 19; + // https://protobuf.dev/programming-guides/proto3/#scalar // TODO Reconsider if this is really needed?! By who, for what? // bytes bytes = 4; @@ -72,13 +78,6 @@ message Value { // double double = 15; // float float = 16; // bool bool = 17; - - Thing struct = 18; - - // TODO Do we actually need List? That's just a stream of Thing, no? - // TODO Inline? repeated Thing things = 18; - // TODO Never has an IRI? Maybe just repeated Value values = 18; - // List list = 19; } message Link { @@ -114,9 +113,9 @@ message Value { string lang = 2; } - // message List { - // repeated Thing entries = 1; - // } + message List { + repeated Value values = 1; + } } // TODO Allow "uint64 id" instatead string IRIs (or all Value?), similar to