Skip to content

Commit

Permalink
feat(openapi): precondition exceptions return 412 (#12552)
Browse files Browse the repository at this point in the history
  • Loading branch information
david-leifker authored Feb 5, 2025
1 parent 23a86fd commit 7f88710
Show file tree
Hide file tree
Showing 8 changed files with 384 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,53 +18,47 @@ public static AspectValidationException forItem(BatchItem item, String msg) {
}

public static AspectValidationException forItem(BatchItem item, String msg, Exception e) {
return new AspectValidationException(item, msg, SubType.VALIDATION, e);
return new AspectValidationException(item, msg, ValidationSubType.VALIDATION, e);
}

public static AspectValidationException forPrecondition(BatchItem item, String msg) {
return forPrecondition(item, msg, null);
}

public static AspectValidationException forFilter(BatchItem item, String msg) {
return new AspectValidationException(item, msg, SubType.FILTER);
return new AspectValidationException(item, msg, ValidationSubType.FILTER);
}

public static AspectValidationException forPrecondition(BatchItem item, String msg, Exception e) {
return new AspectValidationException(item, msg, SubType.PRECONDITION, e);
return new AspectValidationException(item, msg, ValidationSubType.PRECONDITION, e);
}

@Nonnull BatchItem item;
@Nonnull ChangeType changeType;
@Nonnull Urn entityUrn;
@Nonnull String aspectName;
@Nonnull SubType subType;
@Nonnull ValidationSubType subType;
@Nullable String msg;

public AspectValidationException(@Nonnull BatchItem item, String msg, SubType subType) {
public AspectValidationException(@Nonnull BatchItem item, String msg, ValidationSubType subType) {
this(item, msg, subType, null);
}

public AspectValidationException(
@Nonnull BatchItem item, @Nonnull String msg, @Nullable SubType subType, Exception e) {
@Nonnull BatchItem item,
@Nonnull String msg,
@Nullable ValidationSubType subType,
Exception e) {
super(msg, e);
this.item = item;
this.changeType = item.getChangeType();
this.entityUrn = item.getUrn();
this.aspectName = item.getAspectName();
this.msg = msg;
this.subType = subType != null ? subType : SubType.VALIDATION;
this.subType = subType != null ? subType : ValidationSubType.VALIDATION;
}

public Pair<Urn, String> getAspectGroup() {
return Pair.of(entityUrn, aspectName);
}

public enum SubType {
// A validation exception is thrown
VALIDATION,
// A failed precondition is thrown if the header constraints are not met
PRECONDITION,
// Exclude from processing further
FILTER
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand All @@ -15,17 +16,20 @@
public class ValidationExceptionCollection
extends HashMap<Pair<Urn, String>, Set<AspectValidationException>> {

private final Set<Integer> failedHashCodes;
private final Set<Integer> filteredHashCodes;
private final Map<ValidationSubType, Set<Integer>> subTypeHashCodes;

public ValidationExceptionCollection() {
super();
this.failedHashCodes = new HashSet<>();
this.filteredHashCodes = new HashSet<>();
this.subTypeHashCodes = new HashMap<>();
}

public boolean hasFatalExceptions() {
return !failedHashCodes.isEmpty();
return subTypeHashCodes.keySet().stream()
.anyMatch(subType -> !ValidationSubType.FILTER.equals(subType));
}

public Set<ValidationSubType> getSubTypes() {
return subTypeHashCodes.keySet();
}

public static ValidationExceptionCollection newCollection() {
Expand All @@ -34,11 +38,9 @@ public static ValidationExceptionCollection newCollection() {

public void addException(AspectValidationException exception) {
super.computeIfAbsent(exception.getAspectGroup(), key -> new HashSet<>()).add(exception);
if (!AspectValidationException.SubType.FILTER.equals(exception.getSubType())) {
failedHashCodes.add(exception.getItem().hashCode());
} else {
filteredHashCodes.add(exception.getItem().hashCode());
}
subTypeHashCodes
.computeIfAbsent(exception.getSubType(), key -> new HashSet<>())
.add(exception.getItem().hashCode());
}

public void addException(BatchItem item, String message) {
Expand All @@ -58,16 +60,27 @@ public <T extends BatchItem> Collection<T> successful(Collection<T> items) {
}

public <T extends BatchItem> Stream<T> streamSuccessful(Stream<T> items) {
return items.filter(
i -> !failedHashCodes.contains(i.hashCode()) && !filteredHashCodes.contains(i.hashCode()));
return items.filter(i -> isSuccessful(i.hashCode()));
}

public <T extends BatchItem> Collection<T> exceptions(Collection<T> items) {
return streamExceptions(items.stream()).collect(Collectors.toList());
}

public <T extends BatchItem> Stream<T> streamExceptions(Stream<T> items) {
return items.filter(i -> failedHashCodes.contains(i.hashCode()));
return items.filter(i -> isException(i.hashCode()));
}

private boolean isException(int hashCode) {
return subTypeHashCodes.keySet().stream()
.filter(subType -> !ValidationSubType.FILTER.equals(subType))
.anyMatch(subType -> subTypeHashCodes.get(subType).contains(hashCode));
}

private boolean isSuccessful(int hashCode) {
return !isException(hashCode)
&& (!subTypeHashCodes.containsKey(ValidationSubType.FILTER)
|| !subTypeHashCodes.get(ValidationSubType.FILTER).contains(hashCode));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.linkedin.metadata.aspect.plugins.validation;

public enum ValidationSubType {
// A validation exception is thrown
VALIDATION,
// A failed precondition is thrown if the header constraints are not met
PRECONDITION,
// Exclude from processing further
FILTER
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package com.linkedin.metadata.aspect.plugins.validation;

import static org.mockito.Mockito.*;
import static org.testng.Assert.*;

import com.datahub.test.TestEntityProfile;
import com.linkedin.common.Status;
import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils;
import com.linkedin.data.schema.annotation.PathSpecBasedSchemaAnnotationVisitor;
import com.linkedin.metadata.aspect.batch.BatchItem;
import com.linkedin.metadata.models.registry.ConfigEntityRegistry;
import com.linkedin.metadata.models.registry.EntityRegistry;
import com.linkedin.test.metadata.aspect.batch.TestMCP;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;

public class ValidationExceptionCollectionTest {
private final Urn TEST_URN = UrnUtils.getUrn("urn:li:chart:123");

private ValidationExceptionCollection collection;
private EntityRegistry testEntityRegistry;

private static final String ERROR_MESSAGE = "Test error message";

@BeforeTest
public void disableAssert() {
PathSpecBasedSchemaAnnotationVisitor.class
.getClassLoader()
.setClassAssertionStatus(PathSpecBasedSchemaAnnotationVisitor.class.getName(), false);
}

@BeforeMethod
public void setUp() {
collection = ValidationExceptionCollection.newCollection();
testEntityRegistry =
new ConfigEntityRegistry(
TestEntityProfile.class
.getClassLoader()
.getResourceAsStream("test-entity-registry.yml"));
}

@Test
public void testNewCollection() {
assertNotNull(collection);
assertTrue(collection.isEmpty());
assertFalse(collection.hasFatalExceptions());
assertEquals(collection.getSubTypes().size(), 0);
}

@Test
public void testAddException() {
BatchItem testItem =
TestMCP.ofOneMCP(TEST_URN, new Status(), testEntityRegistry).stream().findFirst().get();
AspectValidationException exception =
new AspectValidationException(testItem, ERROR_MESSAGE, ValidationSubType.VALIDATION, null);

collection.addException(exception);

assertEquals(collection.size(), 1);
assertTrue(collection.containsKey(exception.getAspectGroup()));
assertTrue(collection.get(exception.getAspectGroup()).contains(exception));
}

@Test
public void testAddExceptionWithMessage() {
BatchItem testItem =
TestMCP.ofOneMCP(TEST_URN, new Status(), testEntityRegistry).stream().findFirst().get();
collection.addException(testItem, ERROR_MESSAGE);

assertEquals(collection.size(), 1);
assertTrue(collection.hasFatalExceptions());
}

@Test
public void testHasFatalExceptionsWithMultipleTypes() {
BatchItem testItem =
TestMCP.ofOneMCP(TEST_URN, new Status(), testEntityRegistry).stream().findFirst().get();

// Add FILTER exception
collection.addException(
new AspectValidationException(testItem, ERROR_MESSAGE, ValidationSubType.FILTER, null));
assertFalse(collection.hasFatalExceptions());

// Add VALIDATION exception
collection.addException(
new AspectValidationException(testItem, ERROR_MESSAGE, ValidationSubType.VALIDATION, null));
assertTrue(collection.hasFatalExceptions());
}

@Test
public void testGetSubTypesWithAllTypes() {
BatchItem testItem =
TestMCP.ofOneMCP(TEST_URN, new Status(), testEntityRegistry).stream().findFirst().get();

collection.addException(
new AspectValidationException(testItem, ERROR_MESSAGE, ValidationSubType.FILTER, null));
collection.addException(
new AspectValidationException(testItem, ERROR_MESSAGE, ValidationSubType.VALIDATION, null));
collection.addException(
new AspectValidationException(
testItem, ERROR_MESSAGE, ValidationSubType.PRECONDITION, null));

Set<ValidationSubType> subTypes = collection.getSubTypes();
assertEquals(subTypes.size(), 3);
assertTrue(
subTypes.containsAll(
Arrays.asList(
ValidationSubType.FILTER,
ValidationSubType.VALIDATION,
ValidationSubType.PRECONDITION)));
}

@Test
public void testSuccessfulAndExceptionItems() {
BatchItem validationItem =
TestMCP.ofOneMCP(UrnUtils.getUrn("urn:li:chart:111"), new Status(), testEntityRegistry)
.stream()
.findFirst()
.get();
BatchItem filterItem =
TestMCP.ofOneMCP(UrnUtils.getUrn("urn:li:chart:222"), new Status(), testEntityRegistry)
.stream()
.findFirst()
.get();
BatchItem successItem =
TestMCP.ofOneMCP(UrnUtils.getUrn("urn:li:chart:333"), new Status(), testEntityRegistry)
.stream()
.findFirst()
.get();

collection.addException(
new AspectValidationException(
validationItem, ERROR_MESSAGE, ValidationSubType.VALIDATION, null));
collection.addException(
new AspectValidationException(filterItem, ERROR_MESSAGE, ValidationSubType.FILTER, null));

Collection<BatchItem> items = Arrays.asList(validationItem, filterItem, successItem);

// Test successful items
Collection<BatchItem> successful = collection.successful(items);
assertEquals(successful.size(), 1);
assertTrue(successful.contains(successItem));

// Test exception items
Collection<BatchItem> exceptions = collection.exceptions(items);
assertEquals(exceptions.size(), 1);
assertTrue(exceptions.contains(validationItem));
assertFalse(exceptions.contains(filterItem)); // FILTER type should not be included
}

@Test
public void testStreamOperations() {
BatchItem validationItem =
TestMCP.ofOneMCP(UrnUtils.getUrn("urn:li:chart:111"), new Status(), testEntityRegistry)
.stream()
.findFirst()
.get();
BatchItem successItem =
TestMCP.ofOneMCP(UrnUtils.getUrn("urn:li:chart:222"), new Status(), testEntityRegistry)
.stream()
.findFirst()
.get();

collection.addException(
new AspectValidationException(
validationItem, ERROR_MESSAGE, ValidationSubType.VALIDATION, null));

List<BatchItem> items = Arrays.asList(validationItem, successItem);

// Test streamSuccessful
List<BatchItem> successful = collection.streamSuccessful(items.stream()).toList();
assertEquals(successful.size(), 1);
assertTrue(successful.contains(successItem));

// Test streamExceptions
List<BatchItem> exceptions = collection.streamExceptions(items.stream()).toList();
assertEquals(exceptions.size(), 1);
assertTrue(exceptions.contains(validationItem));
}

@Test
public void testMultipleExceptionsForSameEntityDifferentAspects() {
BatchItem item1 =
TestMCP.ofOneMCP(UrnUtils.getUrn("urn:li:chart:111"), new Status(), testEntityRegistry)
.stream()
.findFirst()
.get();
BatchItem item2 =
TestMCP.ofOneMCP(UrnUtils.getUrn("urn:li:chart:222"), new Status(), testEntityRegistry)
.stream()
.findFirst()
.get();

collection.addException(
new AspectValidationException(item1, ERROR_MESSAGE, ValidationSubType.VALIDATION, null));
collection.addException(
new AspectValidationException(item2, ERROR_MESSAGE, ValidationSubType.VALIDATION, null));

assertEquals(collection.size(), 2);
assertEquals(collection.getSubTypes().size(), 1);
}

@Test
public void testToString() {
BatchItem testItem =
TestMCP.ofOneMCP(TEST_URN, new Status(), testEntityRegistry).stream().findFirst().get();
AspectValidationException exception =
new AspectValidationException(testItem, ERROR_MESSAGE, ValidationSubType.VALIDATION, null);

collection.addException(exception);

String result = collection.toString();
assertTrue(result.contains("ValidationExceptionCollection"));
assertTrue(result.contains("EntityAspect:"));
assertTrue(result.contains("urn:li:chart:123"));
}
}
Loading

0 comments on commit 7f88710

Please sign in to comment.