Skip to content

Commit

Permalink
Improve SPI docs, implement source reference validation
Browse files Browse the repository at this point in the history
  • Loading branch information
fbiville committed Feb 29, 2024
1 parent c5ba701 commit 60ff3b3
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package org.neo4j.importer.v1.validation;

import java.io.Reader;
import java.util.Map;
import java.util.function.Consumer;
import org.neo4j.importer.v1.actions.Action;
Expand All @@ -24,6 +25,24 @@
import org.neo4j.importer.v1.targets.NodeTarget;
import org.neo4j.importer.v1.targets.RelationshipTarget;

/**
* This is the SPI for custom validators.
* Custom validators have the ability to validate elements of an {@link org.neo4j.importer.v1.ImportSpecification}.
* The import specification at this stage is guaranteed to comply to the official import specification JSON schema.
* Every custom validator is instantiated only once (per {@link org.neo4j.importer.v1.ImportSpecificationDeserializer#deserialize(Reader)} call.
* The validation order is as follows:
* 1. visitConfiguration
* 2. visitSource (as many times as there are sources)
* 3. visitNodeTarget (as many times as there are node targets)
* 4. visitRelationshipTarget (as many times as there are relationship targets)
* 5. visitCustomQueryTarget (as many times as there are custom query targets)
* 6. visitAction (as many times as there are actions)
* Then {@link SpecificationValidator#accept} is called with a {@link SpecificationValidationResult.Builder}, where
* errors are reported via {@link SpecificationValidationResult.Builder#addError(String, String, String)} and warnings
* via {@link SpecificationValidationResult.Builder#addWarning(String, String, String)}.
* Implementations are not expected to be thread-safe.
* Modifying the provided arguments via any of the visitXxx or accept calls is considered undefined behavior.
*/
public interface SpecificationValidator extends Consumer<SpecificationValidationResult.Builder> {

default void visitConfiguration(Map<String, Object> configuration) {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* 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
*
* http://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 org.neo4j.importer.v1.validation.plugin;

import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import org.neo4j.importer.v1.sources.Source;
import org.neo4j.importer.v1.targets.CustomQueryTarget;
import org.neo4j.importer.v1.targets.NodeTarget;
import org.neo4j.importer.v1.targets.RelationshipTarget;
import org.neo4j.importer.v1.targets.Target;
import org.neo4j.importer.v1.validation.SpecificationValidationResult.Builder;
import org.neo4j.importer.v1.validation.SpecificationValidator;

public class NoDanglingSourceValidator implements SpecificationValidator {

private static final String ERROR_CODE = "DANG-001";

private final Set<String> sourceNames;
private final Map<String, String> pathToSourceName;

public NoDanglingSourceValidator() {
sourceNames = new LinkedHashSet<>();
pathToSourceName = new LinkedHashMap<>();
}

@Override
public void visitSource(int index, Source source) {
sourceNames.add(source.getName());
}

@Override
public void visitNodeTarget(int index, NodeTarget target) {
checkSource(target, () -> String.format("$.targets.nodes[%d]", index));
}

@Override
public void visitRelationshipTarget(int index, RelationshipTarget target) {
checkSource(target, () -> String.format("$.targets.relationships[%d]", index));
}

@Override
public void visitCustomQueryTarget(int index, CustomQueryTarget target) {
checkSource(target, () -> String.format("$.targets.queries[%d]", index));
}

@Override
public void accept(Builder builder) {
pathToSourceName.forEach((path, source) -> {
builder.addError(
path,
ERROR_CODE,
String.format(
"%s refers to the non-existing source \"%s\". Possible names are: \"%s\"",
path, source, String.join("\", \"", sourceNames)));
});
}

private void checkSource(Target target, Supplier<String> path) {
String source = target.getSource();
if (!sourceNames.contains(source)) {
pathToSourceName.put(path.get(), source);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.neo4j.importer.v1.validation;
package org.neo4j.importer.v1.validation.plugin;

import java.util.ArrayList;
import java.util.LinkedHashMap;
Expand All @@ -26,6 +26,7 @@
import org.neo4j.importer.v1.targets.NodeTarget;
import org.neo4j.importer.v1.targets.RelationshipTarget;
import org.neo4j.importer.v1.validation.SpecificationValidationResult.Builder;
import org.neo4j.importer.v1.validation.SpecificationValidator;

public class NoDuplicatedNameValidator implements SpecificationValidator {

Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
org.neo4j.importer.v1.validation.NoDuplicatedNameValidator
org.neo4j.importer.v1.validation.plugin.NoDuplicatedNameValidator
org.neo4j.importer.v1.validation.plugin.NoDanglingSourceValidator
Original file line number Diff line number Diff line change
Expand Up @@ -719,4 +719,105 @@ void fails_if_source_name_is_duplicated_with_action() {
"0 warning(s)",
"Name \"duplicate\" is duplicated across the following paths: $.sources[0].name, $.actions[0].name");
}

@Test
void fails_if_node_target_does_not_refer_to_existing_source() {
assertThatThrownBy(() -> deserialize(new StringReader(
"""
{
"sources": [{
"type": "bigquery",
"name": "a-source",
"query": "SELECT id, name FROM my.table"
}],
"targets": {
"nodes": [{
"name": "a-target",
"source": "incorrect-source-name",
"labels": ["Label1", "Label2"],
"write_mode": "create",
"properties": [
{"source_field": "field_1", "target_property": "property1"},
{"source_field": "field_2", "target_property": "property2"}
]
}]
}
}
"""
.stripIndent())))
.isInstanceOf(InvalidSpecificationException.class)
.hasMessageContainingAll(
"1 error(s)",
"0 warning(s)",
"$.targets.nodes[0] refers to the non-existing source \"incorrect-source-name\". "
+ "Possible names are: \"a-source\"");
}

@Test
void fails_if_relationship_target_does_not_refer_to_existing_source() {
assertThatThrownBy(() -> deserialize(new StringReader(
"""
{
"sources": [{
"type": "bigquery",
"name": "a-source",
"query": "SELECT id, name FROM my.table"
}],
"targets": {
"relationships": [{
"name": "a-target",
"source": "incorrect-source-name",
"type": "TYPE",
"start_node": {
"label": "Label1",
"key_properties": [
{"source_field": "field_1", "target_property": "property1"}
]
},
"end_node": {
"label": "Label2",
"key_properties": [
{"source_field": "field_2", "target_property": "property2"}
]
}
}]
}
}
"""
.stripIndent())))
.isInstanceOf(InvalidSpecificationException.class)
.hasMessageContainingAll(
"1 error(s)",
"0 warning(s)",
"$.targets.relationships[0] refers to the non-existing source \"incorrect-source-name\". "
+ "Possible names are: \"a-source\"");
}

@Test
void fails_if_custom_query_target_does_not_refer_to_existing_source() {
assertThatThrownBy(() -> deserialize(new StringReader(
"""
{
"sources": [{
"type": "bigquery",
"name": "a-source",
"query": "SELECT id, name FROM my.table"
}],
"targets": {
"queries": [{
"name": "a-target",
"source": "incorrect-source-name",
"query": "UNWIND $rows AS row CREATE (n:ANode) SET n = row"
}]
}
}
"""
.stripIndent())))
.isInstanceOf(InvalidSpecificationException.class)
.hasMessageContainingAll(
"1 error(s)",
"0 warning(s)",
"$.targets.queries[0] refers to the non-existing source \"incorrect-source-name\". "
+ "Possible names are: \"a-source\"");
}
}

0 comments on commit 60ff3b3

Please sign in to comment.