Skip to content

Commit

Permalink
feat (core): Things Validations!
Browse files Browse the repository at this point in the history
  • Loading branch information
vorburger committed Jun 26, 2024
1 parent d57ded1 commit 36e5012
Show file tree
Hide file tree
Showing 16 changed files with 504 additions and 11 deletions.
29 changes: 27 additions & 2 deletions java/dev/enola/cli/CommandWithModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,12 @@
import dev.enola.thing.proto.Thing;
import dev.enola.thing.repo.ThingMemoryRepositoryROBuilder;
import dev.enola.thing.template.TemplateThingRepository;
import dev.enola.thing.validation.LoggingCollector;
import dev.enola.thing.validation.Validators;

import org.jspecify.annotations.Nullable;

import picocli.CommandLine;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Option;
Expand All @@ -61,6 +64,15 @@ public abstract class CommandWithModel extends CommandWithResourceProvider {
@ArgGroup(multiplicity = "1")
ModelOrServer group;

@CommandLine.Option(
names = {"--validate"},
negatable = true,
required = true,
defaultValue = "false", // TODO Enable Model validation by default
showDefaultValue = CommandLine.Help.Visibility.ALWAYS,
description = "Whether validation errors in loaded models should stop & exit")
boolean validate;

private EnolaServiceBlockingStub gRPCService;

// TODO Turn remote service encapsulation upside down (as-is this "exception" is strange)
Expand Down Expand Up @@ -88,8 +100,21 @@ public final void run() throws Exception {
loader.convertIntoOrThrow(stream, store);
}
}
TemplateThingRepository templateThingRepository =
new TemplateThingRepository(store.build());

var repo = store.build();
var c = new LoggingCollector();
var v = new Validators(repo);
v.validate(repo, c);
if (c.hasMessages()) {
System.err.println("Loaded models have validation errors; use -v to show them");
if (validate) {
System.exit(7);
} else {
System.err.println("Use --no-validate to continue anyway");
}
}

TemplateThingRepository templateThingRepository = new TemplateThingRepository(repo);
templateService = templateThingRepository;
// NB: Copy/pasted below...
ekr = new EntityKindRepository();
Expand Down
3 changes: 2 additions & 1 deletion java/dev/enola/cli/EnolaCLI.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
ServerCommand.class,
ExecMdCommand.class,
LoggingTestCommand.class,
InfoCommand.class
InfoCommand.class,
ValidateCommand.class
})
public class EnolaCLI {

Expand Down
31 changes: 31 additions & 0 deletions java/dev/enola/cli/ValidateCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2024 The Enola <https://enola.dev> 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.
*/
package dev.enola.cli;

import dev.enola.core.meta.EntityKindRepository;
import dev.enola.core.proto.EnolaServiceGrpc;

import picocli.CommandLine;

@CommandLine.Command(name = "validate", description = "Validate Models (and exit)")
public class ValidateCommand extends CommandWithModel {

@Override
protected void run(EntityKindRepository ekr, EnolaServiceGrpc.EnolaServiceBlockingStub service)
throws Exception {}
}
28 changes: 21 additions & 7 deletions java/dev/enola/common/URILineColumnMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,33 @@ public URILineColumnMessage(String message, URI uri, long line) {
}

public String format() {
// TODO Have some way of configuring output format
return forGCC();
// TODO Have some way of globally configuring default output format
return format(Format.GCC);
}

protected String forGCC() {
return uri().toString() + ":" + line + optionalColumn(":") + ": " + message;
public String format(Format format) {
return switch (format) {
case GCC -> forGCC();
case VS -> forVS();
case JSON -> asJSON();
};
}

protected String forVS() {
return uri().toString() + "(" + line + optionalColumn(",") + "): " + message;
public enum Format {
GCC,
VS,
JSON
}

protected String asJSON() {
private String forGCC() {
return uri() + ":" + line + optionalColumn(":") + ": " + message;
}

private String forVS() {
return uri() + "(" + line + optionalColumn(",") + "): " + message;
}

private String asJSON() {
var sb = new StringBuilder("{ \"file\": \"").append(uri);
sb.append("\", \"line\": ").append(line);
if (column > -1) {
Expand Down
6 changes: 6 additions & 0 deletions java/dev/enola/data/Store.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,10 @@ public interface Store<B, T> {
/** Store multiple Ts; see {@link #store(Object)}. */
@CanIgnoreReturnValue
B store(Iterable<T> items);

default void store(T... items) {
for (T item : items) {
store(item);
}
}
}
3 changes: 2 additions & 1 deletion java/dev/enola/thing/repo/ThingMemoryRepositoryRW.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
* <p>{@link ThingMemoryRepositoryROBuilder} is one of possibly several other alternatives for this.
*/
@ThreadSafe
public class ThingMemoryRepositoryRW extends MemoryRepositoryRW<Thing> implements ThingRepository {
public class ThingMemoryRepositoryRW extends MemoryRepositoryRW<Thing>
implements ThingRepositoryRW {

@Override
protected String getIRI(Thing value) {
Expand Down
23 changes: 23 additions & 0 deletions java/dev/enola/thing/repo/ThingRepositoryRW.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2024 The Enola <https://enola.dev> 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.
*/
package dev.enola.thing.repo;

import dev.enola.data.RepositoryRW;
import dev.enola.thing.Thing;

public interface ThingRepositoryRW extends ThingRepository, RepositoryRW<Thing> {}
23 changes: 23 additions & 0 deletions java/dev/enola/thing/validation/Collector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2024 The Enola <https://enola.dev> 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.
*/
package dev.enola.thing.validation;

public interface Collector {

void add(String predicateIRI, String message);
}
26 changes: 26 additions & 0 deletions java/dev/enola/thing/validation/Collector2.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2024 The Enola <https://enola.dev> 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.
*/
package dev.enola.thing.validation;

import dev.enola.thing.Thing;

// TODO Find a better name / design... I'm too tired right now! ;)
public interface Collector2 {

void add(Thing thing, String predicateIRI, String message);
}
67 changes: 67 additions & 0 deletions java/dev/enola/thing/validation/LinksValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2024 The Enola <https://enola.dev> 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.
*/
package dev.enola.thing.validation;

import dev.enola.thing.Link;
import dev.enola.thing.PredicatesObjects;
import dev.enola.thing.repo.ThingRepository;

import java.net.URI;

public class LinksValidator implements Validator<PredicatesObjects> {

private final ThingRepository repo;

public LinksValidator(ThingRepository repo) {
this.repo = repo;
}

@Override
public void validate(PredicatesObjects thing, Collector collector) {
thing.datatypes()
.forEach((predicateIRI, datatypeIRI) -> c(predicateIRI, datatypeIRI, collector));
thing.properties().forEach((predicateIRI, object) -> c(predicateIRI, object, collector));
}

private void c(String predicateIRI, String linkedIRI, Collector collector) {
if (linkedIRI.startsWith("file:"))
// TODO Remove this workaround again eventually
// file: shouldn't be Links in the first place (but :URL)
return;

if (repo.get(linkedIRI) == null) {
collector.add(predicateIRI, "Unknown thing: " + linkedIRI);
}
}

private void c(String predicateIRI, Object object, Collector collector) {
if (object instanceof Link link) {
c(predicateIRI, link.iri(), collector);
} else if (object instanceof URI uri) {
c(predicateIRI, uri.toString(), collector);
} else if (object instanceof PredicatesObjects predicatesObjects) {
validate(predicatesObjects, collector);
} else if (object instanceof Iterable<?> iterable) {
for (var element : iterable) {
if (element instanceof PredicatesObjects thing) {
validate(thing, collector);
}
}
} // else skip it
}
}
75 changes: 75 additions & 0 deletions java/dev/enola/thing/validation/LinksValidatorTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2024 The Enola <https://enola.dev> 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.
*/
package dev.enola.thing.validation;

import static com.google.common.truth.Truth.assertThat;

import dev.enola.thing.Link;
import dev.enola.thing.Thing;
import dev.enola.thing.impl.ImmutableThing;
import dev.enola.thing.repo.ThingMemoryRepositoryRW;
import dev.enola.thing.repo.ThingRepositoryRW;

import org.junit.Test;

public class LinksValidatorTest {

// TODO Add missing test coverage for (working) blank nodes, Iterables, URI instead Link

Thing one = ImmutableThing.builder().iri("http://example.com/one").build();
Thing two =
ImmutableThing.builder()
.iri("http://example.com/two")
.set("http://example.com/property1", new Link(one.iri()))
.build();
Thing bad1 =
ImmutableThing.builder()
.iri("http://example.com/three")
.set("http://example.com/property1", new Link("http://example.com/MISSING"))
.build();
Thing bad2 =
ImmutableThing.builder()
.iri("http://example.com/four")
.set("http://example.com/property1", "hi", "http://example.com/MISSING")
.build();

ThingRepositoryRW repo = new ThingMemoryRepositoryRW();
TestCollector collector = new TestCollector();
Validators v = new Validators(new LinksValidator(repo));

@Test
public void empty() {
v.validate(repo, collector);
repo.store(one);
assertThat(collector.getDiagnostics()).isEmpty();
}

@Test
public void aok() {
repo.store(one, two);
v.validate(repo, collector);
assertThat(collector.getDiagnostics()).isEmpty();
}

@Test
public void bad() {
repo.store(one, two, bad1, bad2);
v.validate(repo, collector);
assertThat(collector.getDiagnostics()).hasSize(2);
}
}
Loading

0 comments on commit 36e5012

Please sign in to comment.