Skip to content

Commit

Permalink
adds SchemaViolation construct, validationutils to return set of viol…
Browse files Browse the repository at this point in the history
…ations instead of boolean
  • Loading branch information
Gunda Abhishek committed Dec 10, 2024
1 parent 152cb5f commit 77b18d0
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 132 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@
package com.grookage.leia.common.utils;

import com.google.common.collect.Sets;
import com.grookage.leia.common.exception.SchemaValidationException;
import com.grookage.leia.common.exception.ValidationErrorCode;
import com.grookage.leia.models.attributes.*;
import com.grookage.leia.common.violation.LeiaSchemaViolation;
import com.grookage.leia.common.violation.ViolationContext;
import com.grookage.leia.models.attributes.ArrayAttribute;
import com.grookage.leia.models.attributes.MapAttribute;
import com.grookage.leia.models.attributes.ObjectAttribute;
import com.grookage.leia.models.attributes.SchemaAttribute;
import com.grookage.leia.models.attributes.SchemaAttributeHandler;
import com.grookage.leia.models.schema.SchemaDetails;
import com.grookage.leia.models.schema.SchemaValidationType;
import com.grookage.leia.models.schema.SchemaValidationVisitor;
Expand All @@ -33,7 +37,7 @@
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
Expand All @@ -44,32 +48,28 @@ public class SchemaValidationUtils {
static Function<Class<?>, Function<SchemaAttribute, Boolean>> assignableCheckFunction =
klass -> attribute -> ClassUtils.isAssignable(klass, attribute.getType().getAssignableClass());

static Function<SchemaAttribute, Boolean> throwException = attribute -> {
log.error("Attribute {} of type {} not compatible with the type provided",
attribute.getName(), attribute.getType());
throw SchemaValidationException.error(ValidationErrorCode.INVALID_SCHEMAS);
};
private static final String TYPE_VIOLATION = "Incompatible Type, expected: %s, provided: %s";

public boolean valid(final SchemaDetails schemaDetails,
final Class<?> klass) {
return valid(schemaDetails.getValidationType(), schemaDetails.getAttributes(), klass);
public Set<LeiaSchemaViolation> valid(final SchemaDetails schemaDetails,
final Class<?> klass) {
return valid(schemaDetails.getValidationType(), schemaDetails.getAttributes(), klass, new ViolationContext());
}

public boolean valid(final SchemaValidationType validationType,
final Set<SchemaAttribute> attributes,
final Class<?> klass) {

public Set<LeiaSchemaViolation> valid(final SchemaValidationType validationType,
final Set<SchemaAttribute> attributes,
final Class<?> klass,
final ViolationContext context) {
final var fields = FieldUtils.getAllFields(klass);
if (!validSchema(validationType, attributes, fields)) {
return false;
}
return attributes.stream().allMatch(
each -> validAttribute(each, fields, validationType));
validSchema(validationType, attributes, fields, klass, context);
attributes.forEach(each -> validAttribute(each, fields, validationType, context));
return context.getViolations();
}

private boolean validSchema(final SchemaValidationType validationType,
final Set<SchemaAttribute> attributes,
final List<Field> fields) {
private void validSchema(final SchemaValidationType validationType,
final Set<SchemaAttribute> attributes,
final List<Field> fields,
final Class<?> klass,
final ViolationContext context) {
final var fieldNames = fields.stream()
.map(Field::getName)
.map(String::toUpperCase)
Expand All @@ -79,139 +79,157 @@ private boolean validSchema(final SchemaValidationType validationType,
.map(String::toUpperCase)
.collect(Collectors.toSet());

return validationType.accept(new SchemaValidationVisitor<>() {
validationType.accept(new SchemaValidationVisitor<>() {
@Override
public Boolean strict() {
public Void strict() {
final var mismatchedAttributes = Sets.symmetricDifference(fieldNames, attributesListed);
if (!mismatchedAttributes.isEmpty()) {
log.error(
"There seems to be a mismatch in the attributes present in the class definition and "
+ "schema. [Validation Failed : MODE STRICT]. The attributes are {}",
mismatchedAttributes);
context.addViolation(String.format("%s - [STRICT] Validation: attributes not found or extra attributes :%s",
klass.getSimpleName(), mismatchedAttributes));
}
return mismatchedAttributes.isEmpty();
return null;
}

@Override
public Boolean matching() {
public Void matching() {
final var attributesMissing = Sets.difference(attributesListed, fieldNames);
if (!attributesMissing.isEmpty()) {
log.error("Some attributes are missing in the class definition" +
"[Validation Failed : MODE MATCHING]. The attributes are {}", attributesMissing);
context.addViolation(String.format("%s - [MATCHING] Validation: Missing attributes found :%s",
klass.getSimpleName(), attributesMissing));
}
return attributesMissing.isEmpty();
return null;
}
});
}

private boolean validAttribute(final SchemaAttribute attribute,
final List<Field> fields,
final SchemaValidationType validationType) {
private void validAttribute(final SchemaAttribute attribute,
final List<Field> fields,
final SchemaValidationType validationType,
final ViolationContext context) {
final var field = fields.stream()
.filter(each -> each.getName().equals(attribute.getName()))
.findFirst().orElse(null);
return null != field && valid(validationType, attribute, field.getGenericType());
context.pushPath(attribute.getName());
if (field == null) {
context.addViolation("Missing Field");
return;
}
valid(validationType, attribute, field.getGenericType(), context);
context.popPath();
}

public boolean valid(final SchemaValidationType validationType,
final SchemaAttribute attribute,
final Type type) {
private void valid(final SchemaValidationType validationType,
final SchemaAttribute attribute,
final Type type,
final ViolationContext context) {
if (type instanceof Class<?> klass) {
return valid(validationType, attribute, klass);
valid(validationType, attribute, klass, context);
} else if (type instanceof ParameterizedType parameterizedType) {
return valid(validationType, attribute, parameterizedType);
valid(validationType, attribute, parameterizedType, context);
} else if (type instanceof GenericArrayType arrayType) {
return valid(validationType, attribute, arrayType);
valid(validationType, attribute, arrayType, context);
} else {
throw SchemaValidationException.error(ValidationErrorCode.NOT_SUPPORTED);
context.addViolation("Unsupported class type: " + type);
}
}

private boolean valid(final SchemaValidationType validationType,
final SchemaAttribute attribute,
final Class<?> klass) {
return attribute.accept(new SchemaAttributeHandler<>(
assignableCheckFunction.apply(klass)) {
private void valid(final SchemaValidationType validationType,
final SchemaAttribute attribute,
final Class<?> klass,
final ViolationContext context) {
if (!isMatchingType(klass, attribute)) {
context.addViolation(String.format(TYPE_VIOLATION, attribute.getType(), klass.getSimpleName()));
return;
}

attribute.accept(new SchemaAttributeHandler<Void>(attribute1 -> null) {
@Override
public Boolean accept(ArrayAttribute attribute) {
public Void accept(ArrayAttribute attribute) {
if (klass.isArray()) {
if (attribute.getElementAttribute() == null) {
return true;
}
return valid(validationType, attribute.getElementAttribute(), klass.getComponentType());
valid(validationType, attribute.getElementAttribute(), klass.getComponentType(), context);
return null;
} else if (!Objects.isNull(attribute.getElementAttribute())) {
context.addViolation(String.format("Missing Type arguments, expected ParameterizedType:%s", attribute.getElementAttribute().getType()));
}
return ClassUtils.isAssignable(klass, Collection.class) && attribute.getElementAttribute() == null;
return null;
}

@Override
public Boolean accept(MapAttribute attribute) {
return ClassUtils.isAssignable(klass, Map.class) && attribute.getKeyAttribute() == null;
public Void accept(MapAttribute attribute) {
if (!Objects.isNull(attribute.getKeyAttribute()) || !Objects.isNull(attribute.getValueAttribute())) {
context.addViolation(String.format("Missing Type Arguments, expected parameterized Types key:%s value:%s",
attribute.getKeyAttribute().getType(), attribute.getValueAttribute().getType()));
}
return null;
}

@Override
public Boolean accept(ObjectAttribute attribute) {
// Handling plain Object.class
if (klass.equals(Object.class) && attribute.getNestedAttributes() == null) {
return true;
}
return valid(validationType, attribute.getNestedAttributes(), klass);
public Void accept(ObjectAttribute attribute) {
valid(validationType, attribute.getNestedAttributes(), klass, context);
return null;
}
});
}

private boolean valid(final SchemaValidationType validationType,
final SchemaAttribute attribute,
final ParameterizedType parameterizedType) {
return attribute.accept(new SchemaAttributeHandler<>(throwException) {
private void valid(final SchemaValidationType validationType,
final SchemaAttribute attribute,
final ParameterizedType parameterizedType,
final ViolationContext context) {
if (attribute instanceof ArrayAttribute arrayAttribute) {
if (arrayAttribute.getElementAttribute() == null) {
return;
}
final var typeArguments = getTypeArguments(parameterizedType);
valid(validationType, arrayAttribute.getElementAttribute(), typeArguments[0], context);
} else if (attribute instanceof MapAttribute mapAttribute) {
if (Objects.isNull(mapAttribute.getKeyAttribute()) || Objects.isNull(mapAttribute.getValueAttribute())) {
return;
}
final var typeArguments = getTypeArguments(parameterizedType);
valid(validationType, mapAttribute.getKeyAttribute(), typeArguments[0], context);
valid(validationType, mapAttribute.getValueAttribute(), typeArguments[1], context);
} else {
context.addViolation(String.format(TYPE_VIOLATION, attribute.getType(), parameterizedType));
}
}

private void valid(final SchemaValidationType validationType,
final SchemaAttribute attribute,
final GenericArrayType arrayType,
final ViolationContext context) {
if (attribute instanceof ArrayAttribute arrayAttribute) {
valid(validationType, arrayAttribute.getElementAttribute(), arrayType.getGenericComponentType(), context);
return;
}
context.addViolation(String.format(TYPE_VIOLATION, attribute.getType(), arrayType));
}

private boolean isMatchingType(final Class<?> klass,
final SchemaAttribute attribute) {
return attribute.accept(new SchemaAttributeHandler<>(assignableCheckFunction.apply(klass)) {
@Override
public Boolean accept(ArrayAttribute attribute) {
if (attribute.getElementAttribute() == null) {
return true;
}
final var rawType = (Class<?>) parameterizedType.getRawType();
if (!ClassUtils.isAssignable(rawType, attribute.getType().getAssignableClass())) {
return false;
}
final var typeArguments = getTypeArguments(parameterizedType);
return valid(validationType, attribute.getElementAttribute(), typeArguments[0]);
return klass.isArray() || ClassUtils.isAssignable(klass, Collection.class);
}

@Override
public Boolean accept(MapAttribute attribute) {
if (attribute.getKeyAttribute() == null) {
return true;
}
final var rawType = (Class<?>) parameterizedType.getRawType();
if (!ClassUtils.isAssignable(rawType, attribute.getType().getAssignableClass())) {
public Boolean accept(ObjectAttribute attribute) {
if (klass.equals(Object.class) && !Objects.isNull(attribute.getNestedAttributes())) {
return false;
}
final var typeArguments = getTypeArguments(parameterizedType);
return valid(validationType, attribute.getKeyAttribute(), typeArguments[0]) &&
valid(validationType, attribute.getValueAttribute(), typeArguments[1]);
return true;
}
});
}

private Type[] getTypeArguments(final ParameterizedType parameterizedType) {
final var typeArguments = parameterizedType.getActualTypeArguments();
if (typeArguments.length == 0) {
throw SchemaValidationException.error(ValidationErrorCode.INVALID_SCHEMAS,
String.format("No type arguments found for %s", parameterizedType));
throw new IllegalArgumentException("No type arguments found for " + parameterizedType);
}
return typeArguments;
}

private boolean valid(final SchemaValidationType validationType,
final SchemaAttribute attribute,
final GenericArrayType arrayType) {
return attribute.accept(new SchemaAttributeHandler<>(throwException) {
@Override
public Boolean accept(final ArrayAttribute attribute) {
return valid(validationType, attribute.getElementAttribute(), arrayType.getGenericComponentType());
}
});
}

public boolean valid(final Class<?> klass,
final SchemaAttribute schemaAttribute) {
return schemaAttribute.accept(new SchemaAttributeHandler<>(assignableCheckFunction.apply(klass)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.grookage.leia.common.violation;

public interface LeiaSchemaViolation {
String message();

String fieldPath();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.grookage.leia.common.violation;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LeiaSchemaViolationImpl implements LeiaSchemaViolation {
private String message;
private String fieldPath;

@Override
public String message() {
return message;
}

@Override
public String fieldPath() {
return fieldPath;
}

@Override
public String toString() {
return String.format("[Violation] %s: %s", fieldPath, message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.grookage.leia.common.violation;

import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;

@Data
@Builder
@NoArgsConstructor
public class ViolationContext {
@Getter
private final Set<LeiaSchemaViolation> violations = new HashSet<>();
private final LinkedList<String> path = new LinkedList<>();

public void addViolation(final String message) {
String fullPath = String.join(".", path);
violations.add(new LeiaSchemaViolationImpl(message, fullPath));
}

public void pushPath(final String element) {
path.addLast(element);
}

public void popPath() {
if (!path.isEmpty()) {
path.removeLast();
}
}
}
Loading

0 comments on commit 77b18d0

Please sign in to comment.