diff --git a/CHANGELOG.md b/CHANGELOG.md index d75bc38..26d29ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # Changelog All notable changes to this project will be documented in this file. +## [0.0.1-RC7] + +- Introduced a `leia-common` module to host all the common utils classes +- Added annotation classes for the Qualifiers( PII, Encrypted, ShortLived) that can be added on the members of + the Schema class +- Replaced the `SchemaValidatable` annotation and moved it to a generic `SchemaDefinition` +- Introduced `SchemaBuilder`: For building the schema request against a class annotated with SchemaDefinition +- Introduced `SchemaPayloadValidator`: For validating a schema json payload against a specified SchemaKey +- Addressed issues in handling plain `Object` and Primitive Class types in `SchemaValidationUtils` + ## [0.0.1-RC6] - LeiaClient - Introduced AuthHeaderSupplier in LeiaClientSupplier to support authentication on leia-server diff --git a/README.md b/README.md index 578c76c..661a4d8 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Leia is a governance and metadata framework aimed at meeting compliance requirem com.grookage.leia leia-bom - 0.0.1-RC6 + 0.0.1-RC7 ``` @@ -92,9 +92,11 @@ A sample schema looks like the following "type": "ARRAY", "name": "testAttribute", "optional": true, - "qualifierInfo": { - "type": "PII" - } + "qualifiers": [ + { + "type": "PII" + } + ] }, { "type": "ENUM", @@ -103,9 +105,11 @@ A sample schema looks like the following "values": [ "TEST_ENUM" ], - "qualifierInfo": { - "type": "PII" - } + "qualifiers": [ + { + "type": "PII" + } + ] } ], "transformationTargets": [ @@ -132,6 +136,8 @@ A sample schema looks like the following the `LeiaMessageProduceClient`, will multiplex the testSchema to both the versions, the transformationTargets ought to be jsonPathRules. +Please refer to the `SchemaBuilder` class which can be used to generate schema against a class. + #### Using the LeiaClientBundle On the client side, please use the `LeiaClientBundle` diff --git a/leia-bom/pom.xml b/leia-bom/pom.xml index a8f5330..5897097 100644 --- a/leia-bom/pom.xml +++ b/leia-bom/pom.xml @@ -21,7 +21,7 @@ com.grookage.leia leia - 0.0.1-RC6 + 0.0.1-RC7 leia-bom @@ -39,6 +39,11 @@ leia-models ${project.version} + + com.grookage.leia + leia-common + ${project.version} + com.grookage.leia leia-core diff --git a/leia-client-dropwizard/pom.xml b/leia-client-dropwizard/pom.xml index b0e5970..c722706 100644 --- a/leia-client-dropwizard/pom.xml +++ b/leia-client-dropwizard/pom.xml @@ -22,7 +22,7 @@ com.grookage.leia leia-parent - 0.0.1-RC6 + 0.0.1-RC7 ../leia-parent diff --git a/leia-client/pom.xml b/leia-client/pom.xml index 8217c29..d1f72eb 100644 --- a/leia-client/pom.xml +++ b/leia-client/pom.xml @@ -22,13 +22,14 @@ com.grookage.leia leia-parent - 0.0.1-RC6 + 0.0.1-RC7 ../leia-parent leia-client + 3.11 false @@ -73,6 +74,11 @@ leia-models + + com.grookage.leia + leia-common + + com.grookage.leia leia-schema-validator diff --git a/leia-common/pom.xml b/leia-common/pom.xml new file mode 100644 index 0000000..e88605f --- /dev/null +++ b/leia-common/pom.xml @@ -0,0 +1,92 @@ + + + + + + 4.0.0 + + com.grookage.leia + leia-parent + 0.0.1-RC7 + ../leia-parent + + + leia-common + + + 3.11 + false + + + + + lombok + org.projectlombok + + + + org.apache.commons + commons-lang3 + ${lang3.version} + + + + junit-jupiter + org.junit.jupiter + + + + com.grookage.leia + leia-models + + + + mockito-core + org.mockito + test + + + + mockito-junit-jupiter + org.mockito + test + + + + mockito-inline + org.mockito + test + + + + com.grookage.leia + leia-models + test-jar + + + * + * + + + + + + + + \ No newline at end of file diff --git a/leia-common/src/main/java/com/grookage/leia/common/builder/SchemaBuilder.java b/leia-common/src/main/java/com/grookage/leia/common/builder/SchemaBuilder.java new file mode 100644 index 0000000..5a87de2 --- /dev/null +++ b/leia-common/src/main/java/com/grookage/leia/common/builder/SchemaBuilder.java @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2024. Koushik R . + * + * 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 com.grookage.leia.common.builder; + +import com.grookage.leia.common.utils.FieldUtils; +import com.grookage.leia.common.utils.QualifierUtils; +import com.grookage.leia.models.annotations.SchemaDefinition; +import com.grookage.leia.models.attributes.ArrayAttribute; +import com.grookage.leia.models.attributes.BooleanAttribute; +import com.grookage.leia.models.attributes.DoubleAttribute; +import com.grookage.leia.models.attributes.EnumAttribute; +import com.grookage.leia.models.attributes.FloatAttribute; +import com.grookage.leia.models.attributes.IntegerAttribute; +import com.grookage.leia.models.attributes.LongAttribute; +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.StringAttribute; +import com.grookage.leia.models.qualifiers.QualifierInfo; +import com.grookage.leia.models.schema.ingestion.CreateSchemaRequest; +import lombok.experimental.UtilityClass; +import org.apache.commons.lang3.ClassUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +@UtilityClass +public class SchemaBuilder { + public Optional buildSchemaRequest(final Class klass) { + if (Objects.isNull(klass) || !klass.isAnnotationPresent(SchemaDefinition.class)) { + return Optional.empty(); + } + final var schemaDefinition = klass.getAnnotation(SchemaDefinition.class); + return Optional.of(CreateSchemaRequest.builder() + .schemaName(schemaDefinition.name()) + .namespace(schemaDefinition.namespace()) + .description(schemaDefinition.description()) + .schemaType(schemaDefinition.type()) + .validationType(schemaDefinition.validation()) + .attributes(getSchemaAttributes(klass)) + .build() + ); + } + + public Set getSchemaAttributes(final Class klass) { + return FieldUtils.getAllFields(klass) + .stream() + .map(SchemaBuilder::schemaAttribute) + .collect(Collectors.toSet()); + } + + private SchemaAttribute schemaAttribute(final Field field) { + return schemaAttribute( + field.getGenericType(), + field.getName(), + QualifierUtils.getQualifiers(field), + isOptional(field) + ); + } + + private SchemaAttribute schemaAttribute(final Type type, + final String name, + final Set qualifiers, + final boolean optional) { + // Handle Class instances (eg. String, Enum classes, Complex POJO Objects etc.) + if (type instanceof Class klass) { + return schemaAttribute(klass, name, qualifiers, optional); + } + + // Handle ParameterizedType (e.g., List, Map) + if (type instanceof ParameterizedType parameterizedType) { + return schemaAttribute(parameterizedType, name, qualifiers, optional); + } + + // Handle GenericArrayType (e.g., T[], List) + if (type instanceof GenericArrayType genericArrayType) { + return schemaAttribute(genericArrayType, name, qualifiers, optional); + } + + throw new UnsupportedOperationException("Unsupported field type: " + type.getTypeName()); + } + + private SchemaAttribute schemaAttribute(final ParameterizedType parameterizedType, + final String name, + final Set qualifiers, + final boolean optional) { + final var rawType = (Class) parameterizedType.getRawType(); + // Handle List or Set + if (ClassUtils.isAssignable(rawType, Collection.class)) { + return handleCollection(parameterizedType, name, qualifiers, optional); + } + + // Handle Map + if (ClassUtils.isAssignable(rawType, Map.class)) { + return handleMap(parameterizedType, name, qualifiers, optional); + } + throw new UnsupportedOperationException("Unsupported field type: " + parameterizedType.getTypeName()); + } + + private SchemaAttribute handleMap(final ParameterizedType parameterizedType, + final String name, + final Set qualifiers, + final boolean optional) { + final var keyType = parameterizedType.getActualTypeArguments()[0]; + final var valueType = parameterizedType.getActualTypeArguments()[1]; + return new MapAttribute( + name, + optional, + qualifiers, + schemaAttribute(keyType, "key", QualifierUtils.getQualifiers(keyType), isOptional(keyType)), + schemaAttribute(valueType, "value", QualifierUtils.getQualifiers(valueType), isOptional(valueType)) + ); + } + + private SchemaAttribute handleCollection(final ParameterizedType parameterizedType, + final String name, + final Set qualifiers, boolean optional) { + final var elementType = parameterizedType.getActualTypeArguments()[0]; + return new ArrayAttribute( + name, + optional, + qualifiers, + schemaAttribute(elementType, "element", QualifierUtils.getQualifiers(elementType), + isOptional(elementType)) + ); + } + + private SchemaAttribute schemaAttribute(final GenericArrayType genericArrayType, + final String name, + final Set qualifiers, + final boolean optional) { + final var componentType = genericArrayType.getGenericComponentType(); + return new ArrayAttribute( + name, + optional, + qualifiers, + schemaAttribute(componentType, "element", QualifierUtils.getQualifiers(componentType), + isOptional(componentType)) + ); + } + + + private SchemaAttribute schemaAttribute(final Class klass, + final String name, + final Set qualifiers, + final boolean optional) { + if (klass == String.class) { + return new StringAttribute(name, optional, qualifiers); + } + + if (klass.isEnum()) { + return new EnumAttribute(name, optional, qualifiers, getEnumValues(klass)); + } + + // Handle int, long, boolean etc. + if (klass.isPrimitive()) { + return handlePrimitive(klass, name, qualifiers, optional); + } + + // Handle String[], Object[] etc. + if (klass.isArray()) { + final var componentType = klass.getComponentType(); + return new ArrayAttribute( + name, + optional, + qualifiers, + schemaAttribute(componentType, "element", QualifierUtils.getQualifiers(componentType), + isOptional(componentType)) + ); + } + + // Handle Raw List, Set + if (ClassUtils.isAssignable(klass, Collection.class)) { + return new ArrayAttribute(name, optional, qualifiers, null); + } + + // Handle Raw Map + if (ClassUtils.isAssignable(klass, Map.class)) { + return new MapAttribute(name, optional, qualifiers, null, null); + } + + if (klass.equals(Object.class)) { + return new ObjectAttribute(name, optional, qualifiers, null); + } + + // Handling custom defined POJO's + final var schemaAttributes = getSchemaAttributes(klass); + return new ObjectAttribute(name, optional, qualifiers, schemaAttributes); + } + + private SchemaAttribute handlePrimitive(final Class klass, + final String name, + final Set qualifiers, + final boolean optional) { + if (klass == Integer.class || klass == int.class) { + return new IntegerAttribute(name, optional, qualifiers); + } + if (klass == Boolean.class || klass == boolean.class) { + return new BooleanAttribute(name, optional, qualifiers); + } + if (klass == Double.class || klass == double.class) { + return new DoubleAttribute(name, optional, qualifiers); + } + if (klass == Long.class || klass == long.class) { + return new LongAttribute(name, optional, qualifiers); + } + if (klass == Float.class || klass == float.class) { + return new FloatAttribute(name, optional, qualifiers); + } + + throw new UnsupportedOperationException("Unsupported primitive class type: " + klass.getName()); + + } + + private Set getEnumValues(final Class klass) { + return Arrays.stream(klass.getEnumConstants()) + .map(enumConstant -> ((Enum) enumConstant).name()) + .collect(Collectors.toSet()); + } + + private boolean isOptional(final Type type) { + if (type instanceof Class klass) { + return isOptional(klass); + } + return false; + } + + private boolean isOptional(final Class klass) { + return klass.isAnnotationPresent(com.grookage.leia.models.annotations.attribute.Optional.class); + } + + private boolean isOptional(final Field field) { + return field.isAnnotationPresent(com.grookage.leia.models.annotations.attribute.Optional.class); + } +} diff --git a/leia-schema-validator/src/main/java/com/grookage/leia/validator/exception/SchemaValidationException.java b/leia-common/src/main/java/com/grookage/leia/common/exception/SchemaValidationException.java similarity index 98% rename from leia-schema-validator/src/main/java/com/grookage/leia/validator/exception/SchemaValidationException.java rename to leia-common/src/main/java/com/grookage/leia/common/exception/SchemaValidationException.java index 99667f3..20b8aca 100644 --- a/leia-schema-validator/src/main/java/com/grookage/leia/validator/exception/SchemaValidationException.java +++ b/leia-common/src/main/java/com/grookage/leia/common/exception/SchemaValidationException.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.grookage.leia.validator.exception; +package com.grookage.leia.common.exception; import lombok.Builder; import lombok.Getter; diff --git a/leia-schema-validator/src/main/java/com/grookage/leia/validator/exception/ValidationErrorCode.java b/leia-common/src/main/java/com/grookage/leia/common/exception/ValidationErrorCode.java similarity index 94% rename from leia-schema-validator/src/main/java/com/grookage/leia/validator/exception/ValidationErrorCode.java rename to leia-common/src/main/java/com/grookage/leia/common/exception/ValidationErrorCode.java index 03284aa..fff4e56 100644 --- a/leia-schema-validator/src/main/java/com/grookage/leia/validator/exception/ValidationErrorCode.java +++ b/leia-common/src/main/java/com/grookage/leia/common/exception/ValidationErrorCode.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.grookage.leia.validator.exception; +package com.grookage.leia.common.exception; import lombok.Getter; diff --git a/leia-schema-validator/src/main/java/com/grookage/leia/validator/annotations/SchemaValidatable.java b/leia-common/src/main/java/com/grookage/leia/common/utils/FieldUtils.java similarity index 55% rename from leia-schema-validator/src/main/java/com/grookage/leia/validator/annotations/SchemaValidatable.java rename to leia-common/src/main/java/com/grookage/leia/common/utils/FieldUtils.java index fd19e09..c60c815 100644 --- a/leia-schema-validator/src/main/java/com/grookage/leia/validator/annotations/SchemaValidatable.java +++ b/leia-common/src/main/java/com/grookage/leia/common/utils/FieldUtils.java @@ -14,21 +14,22 @@ * limitations under the License. */ -package com.grookage.leia.validator.annotations; +package com.grookage.leia.common.utils; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; +import lombok.experimental.UtilityClass; -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -public @interface SchemaValidatable { - - String schemaName(); - - String versionId(); - - String namespace(); +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +@UtilityClass +public class FieldUtils { + public List getAllFields(final Class type) { + List fields = new ArrayList<>(); + for (Class c = type; c != null; c = c.getSuperclass()) { + fields.addAll(Arrays.asList(c.getDeclaredFields())); + } + return fields; + } } diff --git a/leia-common/src/main/java/com/grookage/leia/common/utils/QualifierUtils.java b/leia-common/src/main/java/com/grookage/leia/common/utils/QualifierUtils.java new file mode 100644 index 0000000..6e8e3ee --- /dev/null +++ b/leia-common/src/main/java/com/grookage/leia/common/utils/QualifierUtils.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024. Koushik R . + * + * 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 com.grookage.leia.common.utils; + +import com.grookage.leia.models.annotations.attribute.qualifiers.Encrypted; +import com.grookage.leia.models.annotations.attribute.qualifiers.PII; +import com.grookage.leia.models.annotations.attribute.qualifiers.ShortLived; +import com.grookage.leia.models.qualifiers.EncryptedQualifier; +import com.grookage.leia.models.qualifiers.PIIQualifier; +import com.grookage.leia.models.qualifiers.QualifierInfo; +import com.grookage.leia.models.qualifiers.QualifierType; +import com.grookage.leia.models.qualifiers.ShortLivedQualifier; +import lombok.experimental.UtilityClass; + +import java.lang.reflect.Field; +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +@UtilityClass +public class QualifierUtils { + public Optional filter(final Set qualifiers, + final QualifierType type) { + if (Objects.isNull(qualifiers)) { + return Optional.empty(); + } + return qualifiers.stream() + .filter(qualifierInfo -> qualifierInfo.getType().equals(type)) + .findFirst(); + } + + public Set getQualifiers(final Type type) { + if (type instanceof Class klass) { + return getQualifiers(klass); + } + return new HashSet<>(); + } + + public Set getQualifiers(final Field field) { + Set qualifiers = new HashSet<>(); + if (field.isAnnotationPresent(Encrypted.class)) { + qualifiers.add(new EncryptedQualifier()); + } + if (field.isAnnotationPresent(PII.class)) { + qualifiers.add(new PIIQualifier()); + } + if (field.isAnnotationPresent(ShortLived.class)) { + final var shortLived = field.getAnnotation(ShortLived.class); + qualifiers.add(new ShortLivedQualifier(shortLived.ttlSeconds())); + } + return qualifiers; + } + + public Set getQualifiers(final Class klass) { + Set qualifiers = new HashSet<>(); + if (klass.isAnnotationPresent(Encrypted.class)) { + qualifiers.add(new EncryptedQualifier()); + } + if (klass.isAnnotationPresent(PII.class)) { + qualifiers.add(new PIIQualifier()); + } + if (klass.isAnnotationPresent(ShortLived.class)) { + final var shortLived = klass.getAnnotation(ShortLived.class); + qualifiers.add(new ShortLivedQualifier(shortLived.ttlSeconds())); + } + return qualifiers; + } +} diff --git a/leia-schema-validator/src/main/java/com/grookage/leia/validator/utils/SchemaValidationUtils.java b/leia-common/src/main/java/com/grookage/leia/common/utils/SchemaValidationUtils.java similarity index 69% rename from leia-schema-validator/src/main/java/com/grookage/leia/validator/utils/SchemaValidationUtils.java rename to leia-common/src/main/java/com/grookage/leia/common/utils/SchemaValidationUtils.java index 076b170..0afba08 100644 --- a/leia-schema-validator/src/main/java/com/grookage/leia/validator/utils/SchemaValidationUtils.java +++ b/leia-common/src/main/java/com/grookage/leia/common/utils/SchemaValidationUtils.java @@ -14,23 +14,31 @@ * limitations under the License. */ -package com.grookage.leia.validator.utils; +package com.grookage.leia.common.utils; import com.google.common.collect.Sets; -import com.grookage.leia.models.attributes.*; +import com.grookage.leia.common.exception.SchemaValidationException; +import com.grookage.leia.common.exception.ValidationErrorCode; +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; -import com.grookage.leia.validator.exception.SchemaValidationException; -import com.grookage.leia.validator.exception.ValidationErrorCode; import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ClassUtils; import java.lang.reflect.Field; import java.lang.reflect.GenericArrayType; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import java.util.*; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -38,7 +46,7 @@ @Slf4j public class SchemaValidationUtils { static Function, Function> assignableCheckFunction = - klass -> attribute -> attribute.getType().getAssignableClass().isAssignableFrom(klass); + klass -> attribute -> ClassUtils.isAssignable(klass, attribute.getType().getAssignableClass()); static Function throwException = attribute -> { log.error("Attribute {} of type {} not compatible with the type provided", @@ -46,15 +54,16 @@ public class SchemaValidationUtils { throw SchemaValidationException.error(ValidationErrorCode.INVALID_SCHEMAS); }; - public static boolean valid(final SchemaDetails schemaDetails, - final Class klass) { + public boolean valid(final SchemaDetails schemaDetails, + final Class klass) { return valid(schemaDetails.getValidationType(), schemaDetails.getAttributes(), klass); } - public static boolean valid(final SchemaValidationType validationType, - Set attributes, final Class klass) { + public boolean valid(final SchemaValidationType validationType, + final Set attributes, + final Class klass) { - final var fields = getAllFields(klass); + final var fields = FieldUtils.getAllFields(klass); if (!validSchema(validationType, attributes, fields)) { return false; } @@ -62,8 +71,9 @@ public static boolean valid(final SchemaValidationType validationType, each -> validAttribute(each, fields, validationType)); } - private static boolean validSchema(SchemaValidationType validationType, Set attributes, - List fields) { + private boolean validSchema(final SchemaValidationType validationType, + final Set attributes, + final List fields) { final var fieldNames = fields.stream() .map(Field::getName) .map(String::toUpperCase) @@ -98,24 +108,18 @@ public Boolean matching() { }); } - private static List getAllFields(Class type) { - List fields = new ArrayList<>(); - for (Class c = type; c != null; c = c.getSuperclass()) { - fields.addAll(Arrays.asList(c.getDeclaredFields())); - } - return fields; - } - - private static boolean validAttribute(final SchemaAttribute attribute, - List fields, SchemaValidationType validationType) { + private boolean validAttribute(final SchemaAttribute attribute, + final List fields, + final SchemaValidationType validationType) { final var field = fields.stream() .filter(each -> each.getName().equals(attribute.getName())) .findFirst().orElse(null); return null != field && valid(validationType, attribute, field.getGenericType()); } - public static boolean valid(final SchemaValidationType validationType, - SchemaAttribute attribute, final Type type) { + public boolean valid(final SchemaValidationType validationType, + final SchemaAttribute attribute, + final Type type) { if (type instanceof Class klass) { return valid(validationType, attribute, klass); } else if (type instanceof ParameterizedType parameterizedType) { @@ -127,8 +131,9 @@ public static boolean valid(final SchemaValidationType validationType, } } - private static boolean valid(final SchemaValidationType validationType, - SchemaAttribute attribute, final Class klass) { + private boolean valid(final SchemaValidationType validationType, + final SchemaAttribute attribute, + final Class klass) { return attribute.accept(new SchemaAttributeHandler<>( assignableCheckFunction.apply(klass)) { @Override @@ -139,23 +144,28 @@ public Boolean accept(ArrayAttribute attribute) { } return valid(validationType, attribute.getElementAttribute(), klass.getComponentType()); } - return Collection.class.isAssignableFrom(klass) && attribute.getElementAttribute() == null; + return ClassUtils.isAssignable(klass, Collection.class) && attribute.getElementAttribute() == null; } @Override public Boolean accept(MapAttribute attribute) { - return Map.class.isAssignableFrom(klass) && attribute.getKeyAttribute() == null; + return ClassUtils.isAssignable(klass, Map.class) && attribute.getKeyAttribute() == 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); } }); } - private static boolean valid(final SchemaValidationType validationType, - SchemaAttribute attribute, final ParameterizedType parameterizedType) { + private boolean valid(final SchemaValidationType validationType, + final SchemaAttribute attribute, + final ParameterizedType parameterizedType) { return attribute.accept(new SchemaAttributeHandler<>(throwException) { @Override public Boolean accept(ArrayAttribute attribute) { @@ -163,7 +173,7 @@ public Boolean accept(ArrayAttribute attribute) { return true; } final var rawType = (Class) parameterizedType.getRawType(); - if (!attribute.getType().getAssignableClass().isAssignableFrom(rawType)) { + if (!ClassUtils.isAssignable(rawType, attribute.getType().getAssignableClass())) { return false; } final var typeArguments = getTypeArguments(parameterizedType); @@ -176,7 +186,7 @@ public Boolean accept(MapAttribute attribute) { return true; } final var rawType = (Class) parameterizedType.getRawType(); - if (!attribute.getType().getAssignableClass().isAssignableFrom(rawType)) { + if (!ClassUtils.isAssignable(rawType, attribute.getType().getAssignableClass())) { return false; } final var typeArguments = getTypeArguments(parameterizedType); @@ -186,8 +196,18 @@ public Boolean accept(MapAttribute attribute) { }); } - private static boolean valid(final SchemaValidationType validationType, - SchemaAttribute attribute, final GenericArrayType arrayType) { + 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)); + } + 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) { @@ -196,16 +216,8 @@ public Boolean accept(final ArrayAttribute attribute) { }); } - private static Type[] getTypeArguments(ParameterizedType parameterizedType) { - Type[] typeArguments = parameterizedType.getActualTypeArguments(); - if (typeArguments.length == 0) { - throw SchemaValidationException.error(ValidationErrorCode.INVALID_SCHEMAS, - String.format("No type arguments found for %s", parameterizedType)); - } - return typeArguments; - } - - public static boolean valid(Class klass, SchemaAttribute schemaAttribute) { + public boolean valid(final Class klass, + final SchemaAttribute schemaAttribute) { return schemaAttribute.accept(new SchemaAttributeHandler<>(assignableCheckFunction.apply(klass)) { }); } diff --git a/leia-common/src/main/java/com/grookage/leia/common/validation/SchemaPayloadValidator.java b/leia-common/src/main/java/com/grookage/leia/common/validation/SchemaPayloadValidator.java new file mode 100644 index 0000000..f12cf98 --- /dev/null +++ b/leia-common/src/main/java/com/grookage/leia/common/validation/SchemaPayloadValidator.java @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2024. Koushik R . + * + * 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 com.grookage.leia.common.validation; + +import com.fasterxml.jackson.databind.JsonNode; +import com.grookage.leia.models.attributes.ArrayAttribute; +import com.grookage.leia.models.attributes.BooleanAttribute; +import com.grookage.leia.models.attributes.ByteAttribute; +import com.grookage.leia.models.attributes.DoubleAttribute; +import com.grookage.leia.models.attributes.EnumAttribute; +import com.grookage.leia.models.attributes.FloatAttribute; +import com.grookage.leia.models.attributes.IntegerAttribute; +import com.grookage.leia.models.attributes.LongAttribute; +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.SchemaAttributeAcceptor; +import com.grookage.leia.models.attributes.StringAttribute; +import com.grookage.leia.models.schema.SchemaValidationType; +import com.grookage.leia.models.utils.MapperUtils; +import lombok.experimental.UtilityClass; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +@UtilityClass +public class SchemaPayloadValidator { + public static List validate(final JsonNode jsonNode, + final SchemaValidationType validationType, + final Set schemaAttributes) { + List validationErrors = new ArrayList<>(); + + Map schemaMap = new HashMap<>(); + for (SchemaAttribute attribute : schemaAttributes) { + schemaMap.put(attribute.getName(), attribute); + } + + // Validate extra fields in case of Strict Validation + if (validationType == SchemaValidationType.STRICT) { + jsonNode.fieldNames().forEachRemaining(fieldName -> { + if (!schemaMap.containsKey(fieldName)) { + validationErrors.add("Unexpected field: " + fieldName); + } + }); + } + + // Validate missing and type mismatched fields + for (SchemaAttribute attribute : schemaAttributes) { + final var fieldName = attribute.getName(); + // Check the attribute only if the jsonNode is an object + if (jsonNode.isObject() && !jsonNode.has(fieldName)) { + if (!attribute.isOptional()) { + validationErrors.add("Missing required field: " + fieldName); + } + continue; + } + + if (jsonNode.isValueNode()) { + validateField(jsonNode, attribute, validationType, validationErrors); + continue; + } + final var fieldNode = jsonNode.get(fieldName); + validateField(fieldNode, attribute, validationType, validationErrors); + } + + return validationErrors; + } + + private void validateField(final JsonNode fieldNode, + final SchemaAttribute attribute, + final SchemaValidationType validationType, + final List validationErrors) { + final var fieldName = attribute.getName(); + if (!isMatchingType(fieldNode, attribute)) { + validationErrors.add("Type mismatch for field: " + fieldName + + ". Expected: " + attribute.getType() + + ", Found: " + fieldNode.getNodeType()); + return; + } + + // Recursively validate nested objects + if (attribute instanceof ObjectAttribute objectAttribute) { + if (objectAttribute.getNestedAttributes() != null) { + validationErrors.addAll(validate(fieldNode, validationType, objectAttribute.getNestedAttributes())); + } + return; + } + if (attribute instanceof ArrayAttribute arrayAttribute) { + validateCollectionAttribute(fieldNode, arrayAttribute, validationType, validationErrors); + } + if (attribute instanceof MapAttribute mapAttribute) { + validateMapAttribute(fieldNode, mapAttribute, validationType, validationErrors); + } + } + + private void validateCollectionAttribute(final JsonNode fieldNode, + final ArrayAttribute arrayAttribute, + final SchemaValidationType schemaValidationType, + final List validationErrors) { + // Handling Non-Parameterized Collections (eg: List.class, Set.class, Map.class etc.) + if (arrayAttribute.getElementAttribute() == null) { + return; + } + + for (JsonNode arrayElement : fieldNode) { + validateField(arrayElement, arrayAttribute.getElementAttribute(), schemaValidationType, validationErrors); + } + } + + private void validateMapAttribute(final JsonNode fieldNode, + final MapAttribute mapAttribute, + final SchemaValidationType schemaValidationType, + final List validationErrors) { + // Handling Raw Map.class + if (Objects.isNull(mapAttribute.getKeyAttribute()) && Objects.isNull(mapAttribute.getValueAttribute())) { + return; + } + fieldNode.fields().forEachRemaining(entry -> { + final var keyNode = entry.getKey() != null + ? MapperUtils.mapper().convertValue(entry.getKey(), JsonNode.class) + : null; + if (!Objects.isNull(keyNode)) { + // validate Key + validateField(keyNode, mapAttribute.getKeyAttribute(), schemaValidationType, validationErrors); + // Validate value + validateField(entry.getValue(), mapAttribute.getValueAttribute(), schemaValidationType, validationErrors); + } else { + validationErrors.add("Key Not present for map attribute field:" + + mapAttribute.getName()); + } + }); + + } + + private boolean isMatchingType(final JsonNode fieldNode, + final SchemaAttribute attribute) { + return attribute.accept(new SchemaAttributeAcceptor<>() { + @Override + public Boolean accept(BooleanAttribute attribute) { + return fieldNode.isBoolean(); + } + + @Override + public Boolean accept(ByteAttribute attribute) { + return fieldNode.isArray(); + } + + @Override + public Boolean accept(DoubleAttribute attribute) { + return fieldNode.isDouble() || fieldNode.isFloat() || fieldNode.isInt(); + } + + @Override + public Boolean accept(EnumAttribute attribute) { + return fieldNode.isTextual() && attribute.getValues().contains(fieldNode.asText()); + } + + @Override + public Boolean accept(FloatAttribute attribute) { + return fieldNode.isFloat(); + } + + @Override + public Boolean accept(IntegerAttribute attribute) { + return fieldNode.isInt(); + } + + @Override + public Boolean accept(LongAttribute attribute) { + return fieldNode.isLong() || fieldNode.isInt(); + } + + @Override + public Boolean accept(StringAttribute attribute) { + return fieldNode.isTextual(); + } + + @Override + public Boolean accept(ArrayAttribute attribute) { + return fieldNode.isArray(); + } + + @Override + public Boolean accept(MapAttribute attribute) { + return fieldNode.isObject(); + } + + @Override + public Boolean accept(ObjectAttribute attribute) { + // Handling Object.class + if (attribute.getNestedAttributes() == null) { + return true; + } + return fieldNode.isObject(); + } + }); + } +} diff --git a/leia-common/src/test/java/com/grookage/leia/common/LeiaTestUtils.java b/leia-common/src/test/java/com/grookage/leia/common/LeiaTestUtils.java new file mode 100644 index 0000000..331e35b --- /dev/null +++ b/leia-common/src/test/java/com/grookage/leia/common/LeiaTestUtils.java @@ -0,0 +1,164 @@ +package com.grookage.leia.common; + +import com.grookage.leia.common.utils.QualifierUtils; +import com.grookage.leia.models.attributes.ArrayAttribute; +import com.grookage.leia.models.attributes.BooleanAttribute; +import com.grookage.leia.models.attributes.ByteAttribute; +import com.grookage.leia.models.attributes.DoubleAttribute; +import com.grookage.leia.models.attributes.EnumAttribute; +import com.grookage.leia.models.attributes.FloatAttribute; +import com.grookage.leia.models.attributes.IntegerAttribute; +import com.grookage.leia.models.attributes.LongAttribute; +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.SchemaAttributeAcceptor; +import com.grookage.leia.models.attributes.StringAttribute; +import com.grookage.leia.models.qualifiers.QualifierInfo; +import com.grookage.leia.models.qualifiers.QualifierType; +import com.grookage.leia.models.qualifiers.ShortLivedQualifier; +import lombok.experimental.UtilityClass; +import org.junit.jupiter.api.Assertions; + +import java.util.Iterator; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +@UtilityClass +public class LeiaTestUtils { + public Optional filter(Set schemaAttributes, + String name) { + return schemaAttributes.stream() + .filter(schemaAttribute -> schemaAttribute.getName().equals(name)) + .findFirst(); + } + + public void assertEquals(SchemaAttribute expected, + SchemaAttribute original) { + if (Objects.isNull(expected) && Objects.isNull(original)) { + return; + } + Assertions.assertEquals(expected.getType(), original.getType(), "Type mismatch"); + Assertions.assertEquals(expected.getName(), original.getName(), "Name mismatch"); + Assertions.assertEquals(expected.isOptional(), original.isOptional(), "Optionality mismatch"); + + // Compare QualifierInfo + assertEquals(expected.getQualifiers(), original.getQualifiers()); + + // Accept the expected attribute type and perform specific validations + expected.accept(new SchemaAttributeAcceptor() { + @Override + public Void accept(BooleanAttribute attribute) { + Assertions.assertInstanceOf(BooleanAttribute.class, original, "Original is not BooleanAttribute"); + return null; + } + + @Override + public Void accept(ByteAttribute attribute) { + Assertions.assertInstanceOf(ByteAttribute.class, original, "Original is not ByteAttribute"); + return null; + } + + @Override + public Void accept(DoubleAttribute attribute) { + Assertions.assertInstanceOf(DoubleAttribute.class, original, "Original is not DoubleAttribute"); + return null; + } + + @Override + public Void accept(EnumAttribute attribute) { + Assertions.assertInstanceOf(EnumAttribute.class, original, "Original is not EnumAttribute"); + EnumAttribute originalEnum = (EnumAttribute) original; + Assertions.assertEquals(attribute.getValues(), originalEnum.getValues(), "Enum values mismatch"); + return null; + } + + @Override + public Void accept(FloatAttribute attribute) { + Assertions.assertInstanceOf(FloatAttribute.class, original, "Original is not FloatAttribute"); + return null; + } + + @Override + public Void accept(IntegerAttribute attribute) { + Assertions.assertInstanceOf(IntegerAttribute.class, original, "Original is not IntegerAttribute"); + return null; + } + + @Override + public Void accept(LongAttribute attribute) { + Assertions.assertInstanceOf(LongAttribute.class, original, "Original is not LongAttribute"); + return null; + } + + @Override + public Void accept(StringAttribute attribute) { + Assertions.assertInstanceOf(StringAttribute.class, original, "Original is not StringAttribute"); + return null; + } + + @Override + public Void accept(ArrayAttribute attribute) { + Assertions.assertInstanceOf(ArrayAttribute.class, original, "Original is not ArrayAttribute"); + ArrayAttribute originalArray = (ArrayAttribute) original; + assertEquals(attribute.getElementAttribute(), originalArray.getElementAttribute()); // Recursive comparison for elementAttribute + return null; + } + + @Override + public Void accept(MapAttribute attribute) { + Assertions.assertInstanceOf(MapAttribute.class, original, "Original is not MapAttribute"); + MapAttribute originalMap = (MapAttribute) original; + assertEquals(attribute.getKeyAttribute(), originalMap.getKeyAttribute()); // Recursive comparison for key + assertEquals(attribute.getValueAttribute(), originalMap.getValueAttribute()); // Recursive comparison for value + return null; + } + + @Override + public Void accept(ObjectAttribute attribute) { + Assertions.assertInstanceOf(ObjectAttribute.class, original, "Original is not ObjectAttribute"); + ObjectAttribute originalObject = (ObjectAttribute) original; + + if (Objects.isNull(attribute.getNestedAttributes()) && Objects.isNull(originalObject.getNestedAttributes())) { + return null; + } + + // Recursive comparison of nested attributes + Assertions.assertEquals(attribute.getNestedAttributes().size(), originalObject.getNestedAttributes().size(), + "Nested attributes size mismatch"); + Iterator expectedIterator = attribute.getNestedAttributes().iterator(); + Iterator originalIterator = originalObject.getNestedAttributes().iterator(); + + while (expectedIterator.hasNext() && originalIterator.hasNext()) { + assertEquals(expectedIterator.next(), originalIterator.next()); + } + return null; + } + }); + } + + public void assertEquals(Set expected, + Set original) { + Assertions.assertNotNull(expected, "Expected qualifiers should not be null"); + Assertions.assertNotNull(original, "Actual qualifiers should not be null"); + Assertions.assertEquals(expected.size(), original.size(), "Qualifier sets size mismatch"); + + expected.forEach(expectedQualifier -> { + Optional matchingQualifier = QualifierUtils.filter(original, expectedQualifier.getType()); + + Assertions.assertTrue(matchingQualifier.isPresent(), + "Missing qualifier of type: " + expectedQualifier.getType()); + + if (expectedQualifier.getType() == QualifierType.SHORT_LIVED) { + Assertions.assertInstanceOf(ShortLivedQualifier.class, matchingQualifier.get(), "Actual SHORT_LIVED qualifier must be of type ShortLivedQualifier"); + + ShortLivedQualifier expectedShortLived = (ShortLivedQualifier) expectedQualifier; + ShortLivedQualifier actualShortLived = (ShortLivedQualifier) matchingQualifier.get(); + + Assertions.assertEquals(expectedShortLived.getTtlSeconds(), actualShortLived.getTtlSeconds(), + "Mismatch in TTL seconds for SHORT_LIVED qualifier"); + } + }); + } +} diff --git a/leia-common/src/test/java/com/grookage/leia/common/builder/SchemaBuilderTest.java b/leia-common/src/test/java/com/grookage/leia/common/builder/SchemaBuilderTest.java new file mode 100644 index 0000000..3fb5d8f --- /dev/null +++ b/leia-common/src/test/java/com/grookage/leia/common/builder/SchemaBuilderTest.java @@ -0,0 +1,221 @@ +package com.grookage.leia.common.builder; + +import com.grookage.leia.common.LeiaTestUtils; +import com.grookage.leia.common.stubs.NestedStub; +import com.grookage.leia.common.stubs.RecordStub; +import com.grookage.leia.common.stubs.TestEnum; +import com.grookage.leia.common.stubs.TestObjectStub; +import com.grookage.leia.common.stubs.TestParameterizedStub; +import com.grookage.leia.common.stubs.TestRawCollectionStub; +import com.grookage.leia.models.annotations.SchemaDefinition; +import com.grookage.leia.models.annotations.attribute.Optional; +import com.grookage.leia.models.annotations.attribute.qualifiers.Encrypted; +import com.grookage.leia.models.annotations.attribute.qualifiers.PII; +import com.grookage.leia.models.attributes.ArrayAttribute; +import com.grookage.leia.models.attributes.EnumAttribute; +import com.grookage.leia.models.attributes.IntegerAttribute; +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.StringAttribute; +import com.grookage.leia.models.qualifiers.EncryptedQualifier; +import com.grookage.leia.models.qualifiers.PIIQualifier; +import com.grookage.leia.models.schema.SchemaType; +import com.grookage.leia.models.schema.SchemaValidationType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.Set; + +class SchemaBuilderTest { + @Test + void testSchemaRequest() { + final var schemaCreateRequest = SchemaBuilder.buildSchemaRequest(TestRecord.class) + .orElse(null); + Assertions.assertNotNull(schemaCreateRequest); + final var schemaAttributes = SchemaBuilder.getSchemaAttributes(TestRecord.class); + Assertions.assertEquals(TestRecord.NAME, schemaCreateRequest.getSchemaName()); + Assertions.assertEquals(TestRecord.NAMESPACE, schemaCreateRequest.getNamespace()); + Assertions.assertEquals(TestRecord.DESCRIPTION, schemaCreateRequest.getDescription()); + Assertions.assertEquals(SchemaType.JSON, schemaCreateRequest.getSchemaType()); + Assertions.assertEquals(SchemaValidationType.MATCHING, schemaCreateRequest.getValidationType()); + Assertions.assertEquals(schemaAttributes.size(), schemaCreateRequest.getAttributes().size()); + } + + @Test + void testSchemaRequest_WithInvalidClass() { + Assertions.assertTrue(SchemaBuilder.buildSchemaRequest(null).isEmpty()); + Assertions.assertTrue(SchemaBuilder.buildSchemaRequest(TestObject.class).isEmpty()); + } + + @Test + void testSchemaAttributes_WithPrimitiveClass() { + final var schemaAttributeSet = SchemaBuilder.getSchemaAttributes(PrimitiveTestClass.class); + Assertions.assertNotNull(schemaAttributeSet); + Assertions.assertEquals(2, schemaAttributeSet.size()); + final var nameAttribute = new StringAttribute("name", true, new HashSet<>()); + LeiaTestUtils.assertEquals(nameAttribute, LeiaTestUtils.filter(schemaAttributeSet, "name").orElse(null)); + final var idAttribute = new IntegerAttribute("id", false, new HashSet<>()); + LeiaTestUtils.assertEquals(idAttribute, LeiaTestUtils.filter(schemaAttributeSet, "id").orElse(null)); + } + + @Test + void testSchemaAttributes_WithRecordClass() { + final var schemaAttributeSet = SchemaBuilder.getSchemaAttributes(RecordStub.class); + Assertions.assertNotNull(schemaAttributeSet); + Assertions.assertEquals(2, schemaAttributeSet.size()); + final var nameAttribute = new StringAttribute("name", false, Set.of(new PIIQualifier())); + LeiaTestUtils.assertEquals(nameAttribute, LeiaTestUtils.filter(schemaAttributeSet, "name").orElse(null)); + final var idAttribute = new IntegerAttribute("id", true, new HashSet<>()); + LeiaTestUtils.assertEquals(idAttribute, LeiaTestUtils.filter(schemaAttributeSet, "id").orElse(null)); + } + + @Test + void testSchemaAttributes_WithNestedObject() { + final var schemaAttributes = SchemaBuilder.getSchemaAttributes(NestedStub.class); + Assertions.assertFalse(schemaAttributes.isEmpty()); + Assertions.assertEquals(6, schemaAttributes.size()); + final var nameAttribute = new StringAttribute("name", false, new HashSet<>()); + LeiaTestUtils.assertEquals(nameAttribute, LeiaTestUtils.filter(schemaAttributes, "name").orElse(null)); + + final var idAttribute = new IntegerAttribute("id", false, new HashSet<>()); + LeiaTestUtils.assertEquals(idAttribute, LeiaTestUtils.filter(schemaAttributes, "id").orElse(null)); + + final var testPIIDataAttributes = new HashSet(); + final var piiNameAttribute = new StringAttribute("name", false, new HashSet<>()); + final var accountNumberAttribute = new StringAttribute("accountNumber", false, Set.of(new EncryptedQualifier())); + testPIIDataAttributes.add(piiNameAttribute); + testPIIDataAttributes.add(accountNumberAttribute); + final var piiDataAttribute = new ObjectAttribute("piiData", false, Set.of(new PIIQualifier(), new EncryptedQualifier()), testPIIDataAttributes); + LeiaTestUtils.assertEquals(piiDataAttribute, LeiaTestUtils.filter(schemaAttributes, "piiData").orElse(null)); + + final var testRecordAttributes = new HashSet(); + final var recordNameAttribute = new StringAttribute("name", false, Set.of(new PIIQualifier())); + final var recordIdAttribute = new IntegerAttribute("id", true, new HashSet<>()); + testRecordAttributes.add(recordNameAttribute); + testRecordAttributes.add(recordIdAttribute); + final var testRecordAttribute = new ObjectAttribute("recordStub", false, Set.of(new EncryptedQualifier()), + testRecordAttributes); + LeiaTestUtils.assertEquals(testRecordAttribute, LeiaTestUtils.filter(schemaAttributes, "recordStub").orElse(null)); + + final var enumClassAttribute = new EnumAttribute("enumClass", false, new HashSet<>(), Set.of(TestEnum.ONE.name(), + TestEnum.TWO.name())); + LeiaTestUtils.assertEquals(enumClassAttribute, LeiaTestUtils.filter(schemaAttributes, "enumClass").orElse(null)); + + final var phoneNoAttribute = new StringAttribute("phoneNumber", false, Set.of(new PIIQualifier())); + LeiaTestUtils.assertEquals(phoneNoAttribute, LeiaTestUtils.filter(schemaAttributes, "phoneNumber").orElse(null)); + } + + @Test + void testSchemaAttributes_WithParameterizedType() { + final var schemaAttributes = SchemaBuilder.getSchemaAttributes(TestParameterizedStub.class); + Assertions.assertNotNull(schemaAttributes); + Assertions.assertEquals(3, schemaAttributes.size()); + + final var valuesAttributes = new ArrayAttribute("values", false, new HashSet<>(), + new StringAttribute("element", false, new HashSet<>())); + LeiaTestUtils.assertEquals(valuesAttributes, LeiaTestUtils.filter(schemaAttributes, "values").orElse(null)); + + final var testPIIDataAttributes = new HashSet(); + final var piiNameAttribute = new StringAttribute("name", false, new HashSet<>()); + final var accountNumberAttribute = new StringAttribute("accountNumber", false, Set.of(new EncryptedQualifier())); + testPIIDataAttributes.add(piiNameAttribute); + testPIIDataAttributes.add(accountNumberAttribute); + final var piiDataListAttribute = new ArrayAttribute("piiDataList", false, Set.of(new PIIQualifier()), + new ObjectAttribute("element", false, Set.of(new PIIQualifier()), testPIIDataAttributes)); + LeiaTestUtils.assertEquals(piiDataListAttribute, LeiaTestUtils.filter(schemaAttributes, "piiDataList").orElse(null)); + + final var mapAttribute = new MapAttribute("map", false, Set.of(new EncryptedQualifier()), + new EnumAttribute("key", false, new HashSet<>(), Set.of(TestEnum.ONE.name(), TestEnum.TWO.name())), + new StringAttribute("value", false, new HashSet<>())); + LeiaTestUtils.assertEquals(mapAttribute, LeiaTestUtils.filter(schemaAttributes, "map").orElse(null)); + } + + @Test + void testSchemaAttributes_WithRawCollections() { + final var schemaAttributes = SchemaBuilder.getSchemaAttributes(TestRawCollectionStub.class); + Assertions.assertNotNull(schemaAttributes); + Assertions.assertEquals(6, schemaAttributes.size()); + + final var rawListAttribute = new ArrayAttribute("rawList", false, Set.of(), null); + LeiaTestUtils.assertEquals(rawListAttribute, LeiaTestUtils.filter(schemaAttributes, "rawList").orElse(null)); + + final var rawLinkedListAttribute = new ArrayAttribute("rawLinkedList", false, Set.of(), null); + LeiaTestUtils.assertEquals(rawLinkedListAttribute, LeiaTestUtils.filter(schemaAttributes, "rawLinkedList").orElse(null)); + + final var rawSetAttribute = new ArrayAttribute("rawSet", false, Set.of(), null); + LeiaTestUtils.assertEquals(rawSetAttribute, LeiaTestUtils.filter(schemaAttributes, "rawSet").orElse(null)); + + final var rawHashSetAttribute = new ArrayAttribute("rawHashSet", false, Set.of(), null); + LeiaTestUtils.assertEquals(rawHashSetAttribute, LeiaTestUtils.filter(schemaAttributes, "rawHashSet").orElse(null)); + + final var rawMapAttribute = new MapAttribute("rawMap", false, Set.of(), null, null); + LeiaTestUtils.assertEquals(rawMapAttribute, LeiaTestUtils.filter(schemaAttributes, "rawMap").orElse(null)); + + final var rawSortedMapAttribute = new MapAttribute("rawSortedMap", false, Set.of(), null, null); + LeiaTestUtils.assertEquals(rawSortedMapAttribute, LeiaTestUtils.filter(schemaAttributes, "rawSortedMap").orElse(null)); + } + + @Test + void testSchemaAttributes_WithObjects() { + final var schemaAttributes = SchemaBuilder.getSchemaAttributes(TestObjectStub.class); + Assertions.assertNotNull(schemaAttributes); + Assertions.assertEquals(5, schemaAttributes.size()); + + final var objectAttribute = new ObjectAttribute("object", false, Set.of(), null); + LeiaTestUtils.assertEquals(objectAttribute, LeiaTestUtils.filter(schemaAttributes, "object").orElse(null)); + + final var objectsAttribute = new ArrayAttribute("objects", false, Set.of(), + new ObjectAttribute("element", false, Set.of(), null)); + LeiaTestUtils.assertEquals(objectsAttribute, LeiaTestUtils.filter(schemaAttributes, "objects").orElse(null)); + + final var objectListAttribute = new ArrayAttribute("objectList", false, Set.of(), + new ObjectAttribute("element", false, Set.of(), null)); + LeiaTestUtils.assertEquals(objectListAttribute, LeiaTestUtils.filter(schemaAttributes, "objectList").orElse(null)); + + final var objectSetAttribute = new ArrayAttribute("objectSet", false, Set.of(), + new ObjectAttribute("element", false, Set.of(), null)); + LeiaTestUtils.assertEquals(objectSetAttribute, LeiaTestUtils.filter(schemaAttributes, "objectSet").orElse(null)); + + final var objectMapAttribute = new MapAttribute("objectMap", false, Set.of(), + new StringAttribute("key", false, Set.of()), + new ObjectAttribute("value", false, Set.of(), null)); + LeiaTestUtils.assertEquals(objectMapAttribute, LeiaTestUtils.filter(schemaAttributes, "objectMap").orElse(null)); + + } + + static class PrimitiveTestClass { + @Optional + String name; + int id; + } + + @SchemaDefinition( + name = TestRecord.NAME, + namespace = TestRecord.NAMESPACE, + version = TestRecord.VERSION, + description = TestRecord.DESCRIPTION, + type = SchemaType.JSON, + validation = SchemaValidationType.MATCHING + ) + static class TestRecord { + static final String NAME = "TEST_RECORD"; + static final String NAMESPACE = "test"; + static final String VERSION = "v1"; + static final String DESCRIPTION = "Test Record"; + + int id; + String name; + @PII + @Encrypted + String accountNumber; + long ttl; + @Optional + String accountId; + } + + static class TestObject { + String name; + } +} \ No newline at end of file diff --git a/leia-common/src/test/java/com/grookage/leia/common/stubs/NestedStub.java b/leia-common/src/test/java/com/grookage/leia/common/stubs/NestedStub.java new file mode 100644 index 0000000..d30b784 --- /dev/null +++ b/leia-common/src/test/java/com/grookage/leia/common/stubs/NestedStub.java @@ -0,0 +1,23 @@ +package com.grookage.leia.common.stubs; + +import com.grookage.leia.models.annotations.attribute.qualifiers.Encrypted; +import com.grookage.leia.models.annotations.attribute.qualifiers.PII; +import lombok.Builder; +import lombok.Data; +import lombok.extern.jackson.Jacksonized; + +@Data +@Builder +@Jacksonized +public class NestedStub { + String name; + int id; + @PII + @Encrypted + PIIData piiData; + @Encrypted + RecordStub recordStub; + TestEnum enumClass; + @PII + String phoneNumber; +} diff --git a/leia-common/src/test/java/com/grookage/leia/common/stubs/PIIData.java b/leia-common/src/test/java/com/grookage/leia/common/stubs/PIIData.java new file mode 100644 index 0000000..351551d --- /dev/null +++ b/leia-common/src/test/java/com/grookage/leia/common/stubs/PIIData.java @@ -0,0 +1,17 @@ +package com.grookage.leia.common.stubs; + +import com.grookage.leia.models.annotations.attribute.qualifiers.Encrypted; +import com.grookage.leia.models.annotations.attribute.qualifiers.PII; +import lombok.Builder; +import lombok.Data; +import lombok.extern.jackson.Jacksonized; + +@PII +@Data +@Builder +@Jacksonized +public class PIIData { + String name; + @Encrypted + String accountNumber; +} diff --git a/leia-common/src/test/java/com/grookage/leia/common/stubs/RecordStub.java b/leia-common/src/test/java/com/grookage/leia/common/stubs/RecordStub.java new file mode 100644 index 0000000..cdd8316 --- /dev/null +++ b/leia-common/src/test/java/com/grookage/leia/common/stubs/RecordStub.java @@ -0,0 +1,12 @@ +package com.grookage.leia.common.stubs; + +import com.grookage.leia.models.annotations.attribute.Optional; +import com.grookage.leia.models.annotations.attribute.qualifiers.PII; +import lombok.Builder; +import lombok.extern.jackson.Jacksonized; + +@Builder +@Jacksonized +public record RecordStub(@PII String name, + @Optional int id) { +} diff --git a/leia-common/src/test/java/com/grookage/leia/common/stubs/TestEnum.java b/leia-common/src/test/java/com/grookage/leia/common/stubs/TestEnum.java new file mode 100644 index 0000000..c655da6 --- /dev/null +++ b/leia-common/src/test/java/com/grookage/leia/common/stubs/TestEnum.java @@ -0,0 +1,6 @@ +package com.grookage.leia.common.stubs; + +public enum TestEnum { + ONE, + TWO +} diff --git a/leia-common/src/test/java/com/grookage/leia/common/stubs/TestObjectStub.java b/leia-common/src/test/java/com/grookage/leia/common/stubs/TestObjectStub.java new file mode 100644 index 0000000..d1f6c19 --- /dev/null +++ b/leia-common/src/test/java/com/grookage/leia/common/stubs/TestObjectStub.java @@ -0,0 +1,20 @@ +package com.grookage.leia.common.stubs; + +import lombok.Builder; +import lombok.Data; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Data +@Builder +@Jacksonized +public class TestObjectStub { + Object object; + Object[] objects; + List objectList; + Set objectSet; + Map objectMap; +} diff --git a/leia-common/src/test/java/com/grookage/leia/common/stubs/TestParameterizedStub.java b/leia-common/src/test/java/com/grookage/leia/common/stubs/TestParameterizedStub.java new file mode 100644 index 0000000..8093af3 --- /dev/null +++ b/leia-common/src/test/java/com/grookage/leia/common/stubs/TestParameterizedStub.java @@ -0,0 +1,21 @@ +package com.grookage.leia.common.stubs; + +import com.grookage.leia.models.annotations.attribute.qualifiers.Encrypted; +import com.grookage.leia.models.annotations.attribute.qualifiers.PII; +import lombok.Builder; +import lombok.Data; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; +import java.util.Map; + +@Data +@Builder +@Jacksonized +public class TestParameterizedStub { + String[] values; + @PII + List piiDataList; + @Encrypted + Map map; +} diff --git a/leia-common/src/test/java/com/grookage/leia/common/stubs/TestRawCollectionStub.java b/leia-common/src/test/java/com/grookage/leia/common/stubs/TestRawCollectionStub.java new file mode 100644 index 0000000..b5c5123 --- /dev/null +++ b/leia-common/src/test/java/com/grookage/leia/common/stubs/TestRawCollectionStub.java @@ -0,0 +1,24 @@ +package com.grookage.leia.common.stubs; + +import lombok.Builder; +import lombok.Data; +import lombok.extern.jackson.Jacksonized; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; + +@Data +@Builder +@Jacksonized +public class TestRawCollectionStub { + List rawList; + LinkedList rawLinkedList; + Set rawSet; + HashSet rawHashSet; + Map rawMap; + SortedMap rawSortedMap; +} diff --git a/leia-schema-validator/src/test/java/com/grookage/leia/validator/SchemaValidationUtilsTest.java b/leia-common/src/test/java/com/grookage/leia/common/utils/SchemaValidationUtilsTest.java similarity index 90% rename from leia-schema-validator/src/test/java/com/grookage/leia/validator/SchemaValidationUtilsTest.java rename to leia-common/src/test/java/com/grookage/leia/common/utils/SchemaValidationUtilsTest.java index a55c54f..1944db9 100644 --- a/leia-schema-validator/src/test/java/com/grookage/leia/validator/SchemaValidationUtilsTest.java +++ b/leia-common/src/test/java/com/grookage/leia/common/utils/SchemaValidationUtilsTest.java @@ -1,27 +1,20 @@ -/* - * Copyright (c) 2024. Koushik R . - * - * 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 com.grookage.leia.validator; +package com.grookage.leia.common.utils; +import com.grookage.leia.common.exception.ValidationErrorCode; import com.grookage.leia.models.ResourceHelper; -import com.grookage.leia.models.attributes.*; +import com.grookage.leia.models.attributes.ArrayAttribute; +import com.grookage.leia.models.attributes.BooleanAttribute; +import com.grookage.leia.models.attributes.ByteAttribute; +import com.grookage.leia.models.attributes.DoubleAttribute; +import com.grookage.leia.models.attributes.EnumAttribute; +import com.grookage.leia.models.attributes.FloatAttribute; +import com.grookage.leia.models.attributes.IntegerAttribute; +import com.grookage.leia.models.attributes.LongAttribute; +import com.grookage.leia.models.attributes.MapAttribute; +import com.grookage.leia.models.attributes.ObjectAttribute; +import com.grookage.leia.models.attributes.StringAttribute; import com.grookage.leia.models.schema.SchemaDetails; import com.grookage.leia.models.schema.SchemaValidationType; -import com.grookage.leia.validator.exception.ValidationErrorCode; -import com.grookage.leia.validator.utils.SchemaValidationUtils; import lombok.SneakyThrows; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -32,7 +25,6 @@ import java.util.concurrent.ConcurrentHashMap; class SchemaValidationUtilsTest { - @Test @SneakyThrows void testSchemaValidator() { @@ -86,6 +78,9 @@ void testAllFields() { final var mapAttribute = new MapAttribute("testAttribute", true, null, stringAttribute, stringAttribute); Assertions.assertTrue(SchemaValidationUtils.valid(Map.class, mapAttribute)); + final var plainObjectAttribute = new ObjectAttribute("testAttribute", true, null, null); + Assertions.assertTrue(SchemaValidationUtils.valid(Object.class, plainObjectAttribute)); + final var objectAttribute = new ObjectAttribute("testAttribute", true, null, Set.of(stringAttribute)); Assertions.assertTrue(SchemaValidationUtils.valid(SchemaDetails.class, objectAttribute)); @@ -228,4 +223,5 @@ static class InvalidNestedObjectTestClass { static class GenericArrayTestClass { List[] arrayAttribute; } -} + +} \ No newline at end of file diff --git a/leia-common/src/test/java/com/grookage/leia/common/validation/SchemaPayloadValidatorTest.java b/leia-common/src/test/java/com/grookage/leia/common/validation/SchemaPayloadValidatorTest.java new file mode 100644 index 0000000..67cfd92 --- /dev/null +++ b/leia-common/src/test/java/com/grookage/leia/common/validation/SchemaPayloadValidatorTest.java @@ -0,0 +1,247 @@ +package com.grookage.leia.common.validation; + +import com.grookage.leia.common.builder.SchemaBuilder; +import com.grookage.leia.common.stubs.NestedStub; +import com.grookage.leia.common.stubs.TestObjectStub; +import com.grookage.leia.common.stubs.TestParameterizedStub; +import com.grookage.leia.common.stubs.TestRawCollectionStub; +import com.grookage.leia.models.ResourceHelper; +import com.grookage.leia.models.attributes.ArrayAttribute; +import com.grookage.leia.models.attributes.BooleanAttribute; +import com.grookage.leia.models.attributes.IntegerAttribute; +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.StringAttribute; +import com.grookage.leia.models.schema.SchemaValidationType; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SchemaPayloadValidatorTest { + @Test + void testValidJsonAgainstSchema() throws Exception { + final var jsonNode = ResourceHelper.getObjectMapper().readTree(""" + { + "name": "John Doe", + "age": 30, + "isActive": true + } + """); + + final var schemaAttributes = Set.of( + new StringAttribute("name", false, null), + new IntegerAttribute("age", false, null), + new BooleanAttribute("isActive", false, null) + ); + + final var errors = SchemaPayloadValidator.validate(jsonNode, SchemaValidationType.STRICT, schemaAttributes); + + assertTrue(errors.isEmpty()); + } + + @Test + void testUnexpectedFieldInJson() throws Exception { + final var jsonNode = ResourceHelper.getObjectMapper().readTree(""" + { + "name": "John Doe", + "age": 30, + "isActive": true, + "unexpectedField": "extra" + } + """); + + final var schemaAttributes = Set.of( + new StringAttribute("name", false, null), + new IntegerAttribute("age", false, null), + new BooleanAttribute("isActive", false, null) + ); + + final var errors = SchemaPayloadValidator.validate(jsonNode, SchemaValidationType.STRICT, schemaAttributes); + + assertFalse(errors.isEmpty()); + assertEquals(1, errors.size()); + assertEquals("Unexpected field: unexpectedField", errors.get(0)); + } + + @Test + void testMissingRequiredField() throws Exception { + final var jsonNode = ResourceHelper.getObjectMapper().readTree(""" + { + "name": "John Doe" + } + """); + + final var schemaAttributes = Set.of( + new StringAttribute("name", false, null), + new IntegerAttribute("age", false, null) + ); + + final var errors = SchemaPayloadValidator.validate(jsonNode, SchemaValidationType.STRICT, schemaAttributes); + + assertFalse(errors.isEmpty()); + assertEquals(1, errors.size()); + assertEquals("Missing required field: age", errors.get(0)); + } + + @Test + void testTypeMismatch() throws Exception { + final var jsonNode = ResourceHelper.getObjectMapper().readTree(""" + { + "name": "John Doe", + "age": "thirty" + } + """); + + final var schemaAttributes = Set.of( + new StringAttribute("name", false, null), + new IntegerAttribute("age", false, null) + ); + + final var errors = SchemaPayloadValidator.validate(jsonNode, SchemaValidationType.STRICT, schemaAttributes); + + assertFalse(errors.isEmpty()); + assertEquals(1, errors.size()); + assertEquals("Type mismatch for field: age. Expected: INTEGER, Found: STRING", errors.get(0)); + } + + @Test + void testNestedObjectValidation() throws Exception { + final var jsonNode = ResourceHelper.getObjectMapper().readTree(""" + { + "user": { + "id": 1, + "username": "johndoe" + } + } + """); + + final Set nestedAttributes = Set.of( + new IntegerAttribute("id", false, null), + new StringAttribute("username", false, null) + ); + + final Set schemaAttributes = Set.of( + new ObjectAttribute("user", false, null, nestedAttributes) + ); + + final var errors = SchemaPayloadValidator.validate(jsonNode, SchemaValidationType.STRICT, schemaAttributes); + + assertTrue(errors.isEmpty()); + } + + @Test + void testArrayValidation() throws Exception { + final var jsonNode = ResourceHelper.getObjectMapper().readTree(""" + { + "numbers": [1, 2, 3, 4] + } + """); + + final Set schemaAttributes = Set.of( + new ArrayAttribute("numbers", false, null, new IntegerAttribute("element", false, null)) + ); + + final var errors = SchemaPayloadValidator.validate(jsonNode, SchemaValidationType.STRICT, schemaAttributes); + + assertTrue(errors.isEmpty()); + } + + @Test + void testMapValidation() throws Exception { + final var jsonNode = ResourceHelper.getObjectMapper().readTree(""" + { + "attributes": { + "key1": "value1", + "key2": "value2" + } + } + """); + + final Set schemaAttributes = Set.of( + new MapAttribute( + "attributes", + false, + null, + new StringAttribute("key", false, null), + new StringAttribute("value", false, null) + ) + ); + + final var errors = SchemaPayloadValidator.validate(jsonNode, SchemaValidationType.STRICT, schemaAttributes); + + assertTrue(errors.isEmpty()); + } + + @Test + void testInvalidMapValueType() throws Exception { + final var jsonNode = ResourceHelper.getObjectMapper().readTree(""" + { + "attributes": { + "key1": 100 + } + } + """); + + final Set schemaAttributes = Set.of( + new MapAttribute( + "attributes", + false, + null, + new StringAttribute("key", false, null), + new StringAttribute("value", false, null) + ) + ); + + final var errors = SchemaPayloadValidator.validate(jsonNode, SchemaValidationType.STRICT, schemaAttributes); + + assertFalse(errors.isEmpty()); + assertEquals(1, errors.size()); + } + + @SneakyThrows + @Test + void testValidateNested() { + final var jsonNode = ResourceHelper.getObjectMapper().valueToTree(ResourceHelper.getResource("stubs/validNestedStub.json", + NestedStub.class)); + final var schemaAttributes = SchemaBuilder.getSchemaAttributes(NestedStub.class); + final var errors = SchemaPayloadValidator.validate(jsonNode, SchemaValidationType.STRICT, schemaAttributes); + assertTrue(errors.isEmpty()); + } + + @SneakyThrows + @Test + void testValidateParameterizedStub() { + final var jsonNode = ResourceHelper.getObjectMapper().valueToTree(ResourceHelper.getResource("stubs/validParameterizedStub.json", + TestParameterizedStub.class)); + final var schemaAttributes = SchemaBuilder.getSchemaAttributes(TestParameterizedStub.class); + final var errors = SchemaPayloadValidator.validate(jsonNode, SchemaValidationType.STRICT, schemaAttributes); + assertTrue(errors.isEmpty()); + } + + @SneakyThrows + @Test + void testObjectValidation() { + final var jsonNode = ResourceHelper.getObjectMapper().valueToTree(ResourceHelper.getResource("stubs/validObjectStub.json", + TestObjectStub.class)); + final var schemaAttributes = SchemaBuilder.getSchemaAttributes(TestObjectStub.class); + final var errors = SchemaPayloadValidator.validate(jsonNode, SchemaValidationType.STRICT, schemaAttributes); + Assertions.assertTrue(errors.isEmpty()); + } + + @SneakyThrows + @Test + void testRawCollectionSchemaValidation() { + final var jsonNode = ResourceHelper.getObjectMapper().valueToTree(ResourceHelper.getResource("stubs/validRawCollectionStub.json", + TestRawCollectionStub.class)); + final var schemaAttributes = SchemaBuilder.getSchemaAttributes(TestRawCollectionStub.class); + final var errors = SchemaPayloadValidator.validate(jsonNode, SchemaValidationType.STRICT, schemaAttributes); + Assertions.assertTrue(errors.isEmpty()); + } +} \ No newline at end of file diff --git a/leia-common/src/test/resources/stubs/validNestedStub.json b/leia-common/src/test/resources/stubs/validNestedStub.json new file mode 100644 index 0000000..12e67ed --- /dev/null +++ b/leia-common/src/test/resources/stubs/validNestedStub.json @@ -0,0 +1,14 @@ +{ + "name": "John Doe", + "id": 123, + "piiData": { + "name": "John Doe", + "accountNumber": "1234-5678-9012" + }, + "recordStub": { + "name": "Record Name", + "id": 456 + }, + "enumClass": "ONE", + "phoneNumber": "555-1234" +} \ No newline at end of file diff --git a/leia-common/src/test/resources/stubs/validObjectStub.json b/leia-common/src/test/resources/stubs/validObjectStub.json new file mode 100644 index 0000000..627cf8e --- /dev/null +++ b/leia-common/src/test/resources/stubs/validObjectStub.json @@ -0,0 +1,26 @@ +{ + "object": "Sample String", + "objects": [ + true, + false, + true + ], + "objectList": [ + "String 1", + "String 2", + "String 3" + ], + "objectSet": [ + 1, + 2, + 3 + ], + "objectMap": { + "ONE": { + "key": "value 1" + }, + "TWO": { + "key": "value 2" + } + } +} \ No newline at end of file diff --git a/leia-common/src/test/resources/stubs/validParameterizedStub.json b/leia-common/src/test/resources/stubs/validParameterizedStub.json new file mode 100644 index 0000000..02343f7 --- /dev/null +++ b/leia-common/src/test/resources/stubs/validParameterizedStub.json @@ -0,0 +1,21 @@ +{ + "values": [ + "value1", + "value2", + "value3" + ], + "piiDataList": [ + { + "name": "John Doe", + "accountNumber": "1234-5678-9012" + }, + { + "name": "Jane Smith", + "accountNumber": "9876-5432-1098" + } + ], + "map": { + "ONE": "Value 1", + "TWO": "Value 2" + } +} \ No newline at end of file diff --git a/leia-common/src/test/resources/stubs/validRawCollectionStub.json b/leia-common/src/test/resources/stubs/validRawCollectionStub.json new file mode 100644 index 0000000..afe354b --- /dev/null +++ b/leia-common/src/test/resources/stubs/validRawCollectionStub.json @@ -0,0 +1,38 @@ +{ + "rawList": [ + 42, + 87, + 19 + ], + "rawLinkedList": [ + 10000000000, + 20000000000, + 30000000000 + ], + "rawSet": [ + true, + false, + true + ], + "rawHashSet": [ + 3.14, + 2.71, + 1.41 + ], + "rawMap": { + "1": 123, + "2": 456, + "3": 789 + }, + "rawSortedMap": { + "1": { + "key": "Nested Value 1" + }, + "2": { + "key": "Nested Value 2" + }, + "3": { + "key": "Nested Value 3" + } + } +} \ No newline at end of file diff --git a/leia-schema-validator/src/test/resources/validNestedSchema.json b/leia-common/src/test/resources/validNestedSchema.json similarity index 70% rename from leia-schema-validator/src/test/resources/validNestedSchema.json rename to leia-common/src/test/resources/validNestedSchema.json index a424eeb..db00ff5 100644 --- a/leia-schema-validator/src/test/resources/validNestedSchema.json +++ b/leia-common/src/test/resources/validNestedSchema.json @@ -14,25 +14,31 @@ "type": "STRING", "name": "stringAttribute", "optional": true, - "qualifierInfo": { - "type": "PII" - } + "qualifiers": [ + { + "type": "PII" + } + ] }, { "type": "OBJECT", "name": "nestedObjectAttribute", "optional": true, - "qualifierInfo": { - "type": "PII" - }, + "qualifiers": [ + { + "type": "PII" + } + ], "nestedAttributes": [ { "type": "INTEGER", "name": "integerAttribute", "optional": true, - "qualifierInfo": { - "type": "PII" - } + "qualifiers": [ + { + "type": "PII" + } + ] } ] } diff --git a/leia-schema-validator/src/test/resources/validSchema.json b/leia-common/src/test/resources/validSchema.json similarity index 78% rename from leia-schema-validator/src/test/resources/validSchema.json rename to leia-common/src/test/resources/validSchema.json index 9c06511..a86fbf1 100644 --- a/leia-schema-validator/src/test/resources/validSchema.json +++ b/leia-common/src/test/resources/validSchema.json @@ -17,9 +17,11 @@ "elementAttribute": { "type": "STRING" }, - "qualifierInfo": { - "type": "PII" - } + "qualifiers": [ + { + "type": "PII" + } + ] }, { "type": "ENUM", @@ -28,9 +30,11 @@ "values": [ "TEST_ENUM" ], - "qualifierInfo": { - "type": "PII" - } + "qualifiers": [ + { + "type": "PII" + } + ] } ] } \ No newline at end of file diff --git a/leia-core/pom.xml b/leia-core/pom.xml index 205f66b..72b0d8f 100644 --- a/leia-core/pom.xml +++ b/leia-core/pom.xml @@ -21,7 +21,7 @@ com.grookage.leia leia-parent - 0.0.1-RC6 + 0.0.1-RC7 ../leia-parent @@ -33,6 +33,11 @@ leia-models + + com.grookage.leia + leia-common + + com.grookage.leia leia-repository diff --git a/leia-dropwizard-es/pom.xml b/leia-dropwizard-es/pom.xml index 1f0d83f..9652139 100644 --- a/leia-dropwizard-es/pom.xml +++ b/leia-dropwizard-es/pom.xml @@ -22,7 +22,7 @@ com.grookage.leia leia-parent - 0.0.1-RC6 + 0.0.1-RC7 ../leia-parent diff --git a/leia-dropwizard/pom.xml b/leia-dropwizard/pom.xml index 5d9fcd8..0bc25d3 100644 --- a/leia-dropwizard/pom.xml +++ b/leia-dropwizard/pom.xml @@ -21,7 +21,7 @@ com.grookage.leia leia-parent - 0.0.1-RC6 + 0.0.1-RC7 ../leia-parent diff --git a/leia-dropwizard/src/main/java/com/grookage/leia/dropwizard/bundle/resources/SchemaResource.java b/leia-dropwizard/src/main/java/com/grookage/leia/dropwizard/bundle/resources/SchemaResource.java index 19ef220..2d1f56e 100644 --- a/leia-dropwizard/src/main/java/com/grookage/leia/dropwizard/bundle/resources/SchemaResource.java +++ b/leia-dropwizard/src/main/java/com/grookage/leia/dropwizard/bundle/resources/SchemaResource.java @@ -18,6 +18,10 @@ import com.codahale.metrics.annotation.ExceptionMetered; import com.codahale.metrics.annotation.Timed; +import com.fasterxml.jackson.databind.JsonNode; +import com.grookage.leia.common.validation.SchemaPayloadValidator; +import com.grookage.leia.core.exception.LeiaErrorCode; +import com.grookage.leia.core.exception.LeiaException; import com.grookage.leia.core.retrieval.SchemaRetriever; import com.grookage.leia.models.GenericResponse; import com.grookage.leia.models.request.NamespaceRequest; @@ -78,4 +82,25 @@ public List getAllSchemaDetails(@Valid final NamespaceRequest nam return schemaRetriever.getAllSchemaDetails(namespaceRequest.getNamespaces()); } + @POST + @Timed + @ExceptionMetered + @Path("/details/validate") + public GenericResponse> validateSchema(@Valid SchemaKey schemaKey, + @Valid JsonNode jsonNode) { + final var schemaDetails = schemaRetriever.getSchemaDetails(schemaKey) + .orElseThrow(() -> LeiaException.error(LeiaErrorCode.NO_SCHEMA_FOUND)); + final var validationErrors = SchemaPayloadValidator.validate(jsonNode, schemaDetails.getValidationType(), + schemaDetails.getAttributes()); + if (validationErrors.isEmpty()) { + return GenericResponse.>builder() + .success(true) + .build(); + } + return GenericResponse.>builder() + .data(validationErrors) + .build(); + + } + } diff --git a/leia-elastic/pom.xml b/leia-elastic/pom.xml index 9cb71e2..4f2152b 100644 --- a/leia-elastic/pom.xml +++ b/leia-elastic/pom.xml @@ -22,7 +22,7 @@ com.grookage.leia leia-parent - 0.0.1-RC6 + 0.0.1-RC7 ../leia-parent diff --git a/leia-models/pom.xml b/leia-models/pom.xml index 6f26400..dea8fa5 100644 --- a/leia-models/pom.xml +++ b/leia-models/pom.xml @@ -21,7 +21,7 @@ com.grookage.leia leia-parent - 0.0.1-RC6 + 0.0.1-RC7 ../leia-parent diff --git a/leia-models/src/main/java/com/grookage/leia/models/annotations/SchemaDefinition.java b/leia-models/src/main/java/com/grookage/leia/models/annotations/SchemaDefinition.java new file mode 100644 index 0000000..beb9b47 --- /dev/null +++ b/leia-models/src/main/java/com/grookage/leia/models/annotations/SchemaDefinition.java @@ -0,0 +1,27 @@ +package com.grookage.leia.models.annotations; + +import com.grookage.leia.models.schema.SchemaType; +import com.grookage.leia.models.schema.SchemaValidationType; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface SchemaDefinition { + String name(); + + String namespace(); + + String version(); + + String description() default ""; + + SchemaType type() default SchemaType.JSON; + + SchemaValidationType validation() default SchemaValidationType.MATCHING; + + Class[] transformationTargets() default {}; +} diff --git a/leia-models/src/main/java/com/grookage/leia/models/annotations/attribute/Optional.java b/leia-models/src/main/java/com/grookage/leia/models/annotations/attribute/Optional.java new file mode 100644 index 0000000..c1c92a8 --- /dev/null +++ b/leia-models/src/main/java/com/grookage/leia/models/annotations/attribute/Optional.java @@ -0,0 +1,11 @@ +package com.grookage.leia.models.annotations.attribute; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.FIELD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Optional { +} diff --git a/leia-models/src/main/java/com/grookage/leia/models/annotations/attribute/qualifiers/Encrypted.java b/leia-models/src/main/java/com/grookage/leia/models/annotations/attribute/qualifiers/Encrypted.java new file mode 100644 index 0000000..68ef32b --- /dev/null +++ b/leia-models/src/main/java/com/grookage/leia/models/annotations/attribute/qualifiers/Encrypted.java @@ -0,0 +1,11 @@ +package com.grookage.leia.models.annotations.attribute.qualifiers; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Encrypted { +} diff --git a/leia-models/src/main/java/com/grookage/leia/models/annotations/attribute/qualifiers/PII.java b/leia-models/src/main/java/com/grookage/leia/models/annotations/attribute/qualifiers/PII.java new file mode 100644 index 0000000..5336aa5 --- /dev/null +++ b/leia-models/src/main/java/com/grookage/leia/models/annotations/attribute/qualifiers/PII.java @@ -0,0 +1,11 @@ +package com.grookage.leia.models.annotations.attribute.qualifiers; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface PII { +} diff --git a/leia-models/src/main/java/com/grookage/leia/models/annotations/attribute/qualifiers/ShortLived.java b/leia-models/src/main/java/com/grookage/leia/models/annotations/attribute/qualifiers/ShortLived.java new file mode 100644 index 0000000..cc5e00d --- /dev/null +++ b/leia-models/src/main/java/com/grookage/leia/models/annotations/attribute/qualifiers/ShortLived.java @@ -0,0 +1,12 @@ +package com.grookage.leia.models.annotations.attribute.qualifiers; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ShortLived { + long ttlSeconds(); +} diff --git a/leia-models/src/main/java/com/grookage/leia/models/attributes/ArrayAttribute.java b/leia-models/src/main/java/com/grookage/leia/models/attributes/ArrayAttribute.java index 3168147..38d22f1 100644 --- a/leia-models/src/main/java/com/grookage/leia/models/attributes/ArrayAttribute.java +++ b/leia-models/src/main/java/com/grookage/leia/models/attributes/ArrayAttribute.java @@ -22,6 +22,8 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import java.util.Set; + @EqualsAndHashCode(callSuper = true) @Data @JsonIgnoreProperties(ignoreUnknown = true) @@ -31,9 +33,9 @@ public class ArrayAttribute extends SchemaAttribute { public ArrayAttribute(final String name, final boolean optional, - final QualifierInfo qualifierInfo, + final Set qualifiers, SchemaAttribute elementAttribute) { - super(DataType.ARRAY, name, optional, qualifierInfo); + super(DataType.ARRAY, name, optional, qualifiers); this.elementAttribute = elementAttribute; } diff --git a/leia-models/src/main/java/com/grookage/leia/models/attributes/BooleanAttribute.java b/leia-models/src/main/java/com/grookage/leia/models/attributes/BooleanAttribute.java index 89aa779..5c255df 100644 --- a/leia-models/src/main/java/com/grookage/leia/models/attributes/BooleanAttribute.java +++ b/leia-models/src/main/java/com/grookage/leia/models/attributes/BooleanAttribute.java @@ -22,6 +22,8 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import java.util.Set; + @EqualsAndHashCode(callSuper = true) @Data @JsonIgnoreProperties(ignoreUnknown = true) @@ -30,8 +32,8 @@ public class BooleanAttribute extends SchemaAttribute { public BooleanAttribute(final String name, final boolean optional, - final QualifierInfo qualifierInfo) { - super(DataType.BOOLEAN, name, optional, qualifierInfo); + final Set qualifiers) { + super(DataType.BOOLEAN, name, optional, qualifiers); } @Override diff --git a/leia-models/src/main/java/com/grookage/leia/models/attributes/ByteAttribute.java b/leia-models/src/main/java/com/grookage/leia/models/attributes/ByteAttribute.java index 5ecc453..7dba446 100644 --- a/leia-models/src/main/java/com/grookage/leia/models/attributes/ByteAttribute.java +++ b/leia-models/src/main/java/com/grookage/leia/models/attributes/ByteAttribute.java @@ -23,6 +23,8 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import java.util.Set; + @EqualsAndHashCode(callSuper = true) @Data @JsonIgnoreProperties(ignoreUnknown = true) @@ -31,8 +33,8 @@ public class ByteAttribute extends SchemaAttribute { public ByteAttribute(final String name, final boolean optional, - final QualifierInfo qualifierInfo) { - super(DataType.BYTES, name, optional, qualifierInfo); + final Set qualifiers) { + super(DataType.BYTES, name, optional, qualifiers); } @Override diff --git a/leia-models/src/main/java/com/grookage/leia/models/attributes/DoubleAttribute.java b/leia-models/src/main/java/com/grookage/leia/models/attributes/DoubleAttribute.java index a454771..55a0ee8 100644 --- a/leia-models/src/main/java/com/grookage/leia/models/attributes/DoubleAttribute.java +++ b/leia-models/src/main/java/com/grookage/leia/models/attributes/DoubleAttribute.java @@ -22,6 +22,8 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import java.util.Set; + @EqualsAndHashCode(callSuper = true) @Data @JsonIgnoreProperties(ignoreUnknown = true) @@ -30,8 +32,8 @@ public class DoubleAttribute extends SchemaAttribute { public DoubleAttribute(final String name, final boolean optional, - final QualifierInfo qualifierInfo) { - super(DataType.DOUBLE, name, optional, qualifierInfo); + final Set qualifiers) { + super(DataType.DOUBLE, name, optional, qualifiers); } @Override diff --git a/leia-models/src/main/java/com/grookage/leia/models/attributes/EnumAttribute.java b/leia-models/src/main/java/com/grookage/leia/models/attributes/EnumAttribute.java index 591d22a..10df1ef 100644 --- a/leia-models/src/main/java/com/grookage/leia/models/attributes/EnumAttribute.java +++ b/leia-models/src/main/java/com/grookage/leia/models/attributes/EnumAttribute.java @@ -34,9 +34,9 @@ public class EnumAttribute extends SchemaAttribute { public EnumAttribute(final String name, final boolean optional, - final QualifierInfo qualifierInfo, + final Set qualifiers, final Set values) { - super(DataType.ENUM, name, optional, qualifierInfo); + super(DataType.ENUM, name, optional, qualifiers); this.values = values; } diff --git a/leia-models/src/main/java/com/grookage/leia/models/attributes/FloatAttribute.java b/leia-models/src/main/java/com/grookage/leia/models/attributes/FloatAttribute.java index 85ed563..9c4c7e3 100644 --- a/leia-models/src/main/java/com/grookage/leia/models/attributes/FloatAttribute.java +++ b/leia-models/src/main/java/com/grookage/leia/models/attributes/FloatAttribute.java @@ -22,6 +22,8 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import java.util.Set; + @EqualsAndHashCode(callSuper = true) @Data @JsonIgnoreProperties(ignoreUnknown = true) @@ -30,8 +32,8 @@ public class FloatAttribute extends SchemaAttribute { public FloatAttribute(final String name, final boolean optional, - final QualifierInfo qualifierInfo) { - super(DataType.FLOAT, name, optional, qualifierInfo); + final Set qualifiers) { + super(DataType.FLOAT, name, optional, qualifiers); } @Override diff --git a/leia-models/src/main/java/com/grookage/leia/models/attributes/IntegerAttribute.java b/leia-models/src/main/java/com/grookage/leia/models/attributes/IntegerAttribute.java index a3be7d8..7549474 100644 --- a/leia-models/src/main/java/com/grookage/leia/models/attributes/IntegerAttribute.java +++ b/leia-models/src/main/java/com/grookage/leia/models/attributes/IntegerAttribute.java @@ -22,6 +22,8 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import java.util.Set; + @EqualsAndHashCode(callSuper = true) @Data @JsonIgnoreProperties(ignoreUnknown = true) @@ -30,8 +32,8 @@ public class IntegerAttribute extends SchemaAttribute { public IntegerAttribute(final String name, final boolean optional, - final QualifierInfo qualifierInfo) { - super(DataType.INTEGER, name, optional, qualifierInfo); + final Set qualifiers) { + super(DataType.INTEGER, name, optional, qualifiers); } @Override diff --git a/leia-models/src/main/java/com/grookage/leia/models/attributes/LongAttribute.java b/leia-models/src/main/java/com/grookage/leia/models/attributes/LongAttribute.java index b1efa0c..fa6d2ef 100644 --- a/leia-models/src/main/java/com/grookage/leia/models/attributes/LongAttribute.java +++ b/leia-models/src/main/java/com/grookage/leia/models/attributes/LongAttribute.java @@ -22,6 +22,8 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import java.util.Set; + @EqualsAndHashCode(callSuper = true) @Data @JsonIgnoreProperties(ignoreUnknown = true) @@ -30,8 +32,8 @@ public class LongAttribute extends SchemaAttribute { public LongAttribute(final String name, final boolean optional, - final QualifierInfo qualifierInfo) { - super(DataType.LONG, name, optional, qualifierInfo); + final Set qualifiers) { + super(DataType.LONG, name, optional, qualifiers); } @Override diff --git a/leia-models/src/main/java/com/grookage/leia/models/attributes/MapAttribute.java b/leia-models/src/main/java/com/grookage/leia/models/attributes/MapAttribute.java index 31b771f..6f4e8b0 100644 --- a/leia-models/src/main/java/com/grookage/leia/models/attributes/MapAttribute.java +++ b/leia-models/src/main/java/com/grookage/leia/models/attributes/MapAttribute.java @@ -23,6 +23,8 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import java.util.Set; + @EqualsAndHashCode(callSuper = true) @Data @JsonIgnoreProperties(ignoreUnknown = true) @@ -34,10 +36,10 @@ public class MapAttribute extends SchemaAttribute { public MapAttribute(final String name, final boolean optional, - final QualifierInfo qualifierInfo, + final Set qualifiers, SchemaAttribute keyAttribute, SchemaAttribute valueAttribute) { - super(DataType.MAP, name, optional, qualifierInfo); + super(DataType.MAP, name, optional, qualifiers); Preconditions.checkArgument(keyAttribute == null || valueAttribute != null); this.keyAttribute = keyAttribute; this.valueAttribute = valueAttribute; diff --git a/leia-models/src/main/java/com/grookage/leia/models/attributes/ObjectAttribute.java b/leia-models/src/main/java/com/grookage/leia/models/attributes/ObjectAttribute.java index 49892bd..5362346 100644 --- a/leia-models/src/main/java/com/grookage/leia/models/attributes/ObjectAttribute.java +++ b/leia-models/src/main/java/com/grookage/leia/models/attributes/ObjectAttribute.java @@ -33,9 +33,9 @@ public class ObjectAttribute extends SchemaAttribute { public ObjectAttribute(final String name, final boolean optional, - final QualifierInfo qualifierInfo, + final Set qualifiers, Set nestedAttributes) { - super(DataType.OBJECT, name, optional, qualifierInfo); + super(DataType.OBJECT, name, optional, qualifiers); this.nestedAttributes = nestedAttributes; } diff --git a/leia-models/src/main/java/com/grookage/leia/models/attributes/SchemaAttribute.java b/leia-models/src/main/java/com/grookage/leia/models/attributes/SchemaAttribute.java index 5121651..e210865 100644 --- a/leia-models/src/main/java/com/grookage/leia/models/attributes/SchemaAttribute.java +++ b/leia-models/src/main/java/com/grookage/leia/models/attributes/SchemaAttribute.java @@ -25,6 +25,7 @@ import lombok.NoArgsConstructor; import javax.validation.constraints.Pattern; +import java.util.Set; @Data @NoArgsConstructor @@ -53,7 +54,7 @@ public abstract class SchemaAttribute { private boolean optional; - private QualifierInfo qualifierInfo; + private Set qualifiers; public abstract T accept(SchemaAttributeAcceptor attributeAcceptor); } diff --git a/leia-models/src/main/java/com/grookage/leia/models/attributes/StringAttribute.java b/leia-models/src/main/java/com/grookage/leia/models/attributes/StringAttribute.java index ee768ac..567b7d5 100644 --- a/leia-models/src/main/java/com/grookage/leia/models/attributes/StringAttribute.java +++ b/leia-models/src/main/java/com/grookage/leia/models/attributes/StringAttribute.java @@ -22,6 +22,8 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import java.util.Set; + @EqualsAndHashCode(callSuper = true) @Data @JsonIgnoreProperties(ignoreUnknown = true) @@ -30,8 +32,8 @@ public class StringAttribute extends SchemaAttribute { public StringAttribute(final String name, final boolean optional, - final QualifierInfo qualifierInfo) { - super(DataType.STRING, name, optional, qualifierInfo); + final Set qualifiers) { + super(DataType.STRING, name, optional, qualifiers); } @Override diff --git a/leia-models/src/test/java/com/grookage/leia/models/attributes/AttributeTest.java b/leia-models/src/test/java/com/grookage/leia/models/attributes/AttributeTest.java index 7d12b02..b85d714 100644 --- a/leia-models/src/test/java/com/grookage/leia/models/attributes/AttributeTest.java +++ b/leia-models/src/test/java/com/grookage/leia/models/attributes/AttributeTest.java @@ -17,7 +17,9 @@ package com.grookage.leia.models.attributes; import com.grookage.leia.models.ResourceHelper; +import com.grookage.leia.models.qualifiers.EncryptedQualifier; import com.grookage.leia.models.qualifiers.QualifierType; +import com.grookage.leia.models.qualifiers.ShortLivedQualifier; import lombok.SneakyThrows; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -40,6 +42,23 @@ void testAttributeStructures() { Assertions.assertNotNull(attribute); Assertions.assertEquals("testAttribute", attribute.getName()); Assertions.assertSame(DataType.ARRAY, attribute.getType()); - Assertions.assertTrue(attribute.getQualifierInfo() != null && attribute.getQualifierInfo().getType() == QualifierType.PII); + Assertions.assertEquals(1, attribute.getQualifiers().size()); + Assertions.assertEquals(QualifierType.PII, attribute.getQualifiers().stream().findFirst().get().getType()); + attribute = ResourceHelper.getResource("attributes/attributeWithMultipleQualifiers.json", SchemaAttribute.class); + Assertions.assertNotNull(attribute); + Assertions.assertEquals("testAttribute", attribute.getName()); + Assertions.assertSame(DataType.ARRAY, attribute.getType()); + Assertions.assertEquals(2, attribute.getQualifiers().size()); + final var shortLivedQualifier = (ShortLivedQualifier) attribute.getQualifiers().stream() + .filter(qualifierInfo -> qualifierInfo.getType().equals(QualifierType.SHORT_LIVED)) + .findFirst().orElse(null); + Assertions.assertNotNull(shortLivedQualifier); + Assertions.assertEquals(100, shortLivedQualifier.getTtlSeconds()); + final var encryptedQualifier = (EncryptedQualifier) attribute.getQualifiers().stream() + .filter(qualifierInfo -> qualifierInfo.getType().equals(QualifierType.ENCRYPTED)) + .findFirst() + .orElse(null); + Assertions.assertNotNull(encryptedQualifier); + } } diff --git a/leia-models/src/test/resources/attributes/attributeWithMultipleQualifiers.json b/leia-models/src/test/resources/attributes/attributeWithMultipleQualifiers.json new file mode 100644 index 0000000..4bb9cb7 --- /dev/null +++ b/leia-models/src/test/resources/attributes/attributeWithMultipleQualifiers.json @@ -0,0 +1,14 @@ +{ + "type": "ARRAY", + "name": "testAttribute", + "optional": true, + "qualifiers": [ + { + "type": "ENCRYPTED" + }, + { + "type": "SHORT_LIVED", + "ttlSeconds": 100 + } + ] +} \ No newline at end of file diff --git a/leia-models/src/test/resources/attributes/attributeWithQualifier.json b/leia-models/src/test/resources/attributes/attributeWithQualifier.json index 02f792c..e6bbaba 100644 --- a/leia-models/src/test/resources/attributes/attributeWithQualifier.json +++ b/leia-models/src/test/resources/attributes/attributeWithQualifier.json @@ -2,7 +2,9 @@ "type": "ARRAY", "name": "testAttribute", "optional": true, - "qualifierInfo": { - "type": "PII" - } + "qualifiers": [ + { + "type": "PII" + } + ] } \ No newline at end of file diff --git a/leia-models/src/test/resources/attributes/enumAttribute.json b/leia-models/src/test/resources/attributes/enumAttribute.json index 2b9e6ce..14cac5b 100644 --- a/leia-models/src/test/resources/attributes/enumAttribute.json +++ b/leia-models/src/test/resources/attributes/enumAttribute.json @@ -5,7 +5,9 @@ "values": [ "TEST_ENUM" ], - "qualifierInfo": { - "type": "PII" - } + "qualifiers": [ + { + "type": "PII" + } + ] } \ No newline at end of file diff --git a/leia-models/src/test/resources/schema/createSchemaRequest.json b/leia-models/src/test/resources/schema/createSchemaRequest.json index 9b473a1..64bfa43 100644 --- a/leia-models/src/test/resources/schema/createSchemaRequest.json +++ b/leia-models/src/test/resources/schema/createSchemaRequest.json @@ -7,9 +7,11 @@ "type": "ARRAY", "name": "testAttribute", "optional": true, - "qualifierInfo": { - "type": "PII" - } + "qualifiers": [ + { + "type": "PII" + } + ] } ] } \ No newline at end of file diff --git a/leia-models/src/test/resources/schema/schemaDetails.json b/leia-models/src/test/resources/schema/schemaDetails.json index 096fb47..af05efd 100644 --- a/leia-models/src/test/resources/schema/schemaDetails.json +++ b/leia-models/src/test/resources/schema/schemaDetails.json @@ -14,9 +14,11 @@ "type": "ARRAY", "name": "testAttribute", "optional": true, - "qualifierInfo": { - "type": "PII" - } + "qualifiers": [ + { + "type": "PII" + } + ] }, { "type": "ENUM", @@ -25,9 +27,14 @@ "values": [ "TEST_ENUM" ], - "qualifierInfo": { - "type": "PII" - } + "qualifiers": [ + { + "type": "PII" + }, + { + "type": "ENCRYPTED" + } + ] } ], "transformationTargets": [ diff --git a/leia-models/src/test/resources/schema/updateSchemaRequest.json b/leia-models/src/test/resources/schema/updateSchemaRequest.json index 570bdd4..d0fd2cf 100644 --- a/leia-models/src/test/resources/schema/updateSchemaRequest.json +++ b/leia-models/src/test/resources/schema/updateSchemaRequest.json @@ -9,9 +9,11 @@ "type": "ARRAY", "name": "testAttribute", "optional": true, - "qualifierInfo": { - "type": "PII" - } + "qualifiers": [ + { + "type": "PII" + } + ] }, { "type": "ENUM", @@ -20,9 +22,11 @@ "values": [ "TEST_ENUM" ], - "qualifierInfo": { - "type": "PII" - } + "qualifiers": [ + { + "type": "PII" + } + ] } ] } \ No newline at end of file diff --git a/leia-parent/pom.xml b/leia-parent/pom.xml index d9ea9f5..39fb8f7 100644 --- a/leia-parent/pom.xml +++ b/leia-parent/pom.xml @@ -21,7 +21,7 @@ com.grookage.leia leia-bom - 0.0.1-RC6 + 0.0.1-RC7 ../leia-bom diff --git a/leia-refresher/pom.xml b/leia-refresher/pom.xml index 9d95f40..30cb48d 100644 --- a/leia-refresher/pom.xml +++ b/leia-refresher/pom.xml @@ -21,7 +21,7 @@ com.grookage.leia leia-parent - 0.0.1-RC6 + 0.0.1-RC7 ../leia-parent diff --git a/leia-repository/pom.xml b/leia-repository/pom.xml index 50fc7e6..cc330ef 100644 --- a/leia-repository/pom.xml +++ b/leia-repository/pom.xml @@ -22,7 +22,7 @@ com.grookage.leia leia-parent - 0.0.1-RC6 + 0.0.1-RC7 ../leia-parent diff --git a/leia-schema-validator/pom.xml b/leia-schema-validator/pom.xml index 2951e32..c6e3ed2 100644 --- a/leia-schema-validator/pom.xml +++ b/leia-schema-validator/pom.xml @@ -22,7 +22,7 @@ com.grookage.leia leia-parent - 0.0.1-RC6 + 0.0.1-RC7 ../leia-parent @@ -33,6 +33,7 @@ false 0.9.9 7.0.0 + 3.11 @@ -40,24 +41,22 @@ com.grookage.leia leia-models + + com.grookage.leia + leia-common + reflections org.reflections ${reflections.version} - - leia-models - test-jar - - - * - * - - - com.grookage.leia + org.apache.commons + commons-lang3 + ${lang3.version} + \ No newline at end of file diff --git a/leia-schema-validator/src/main/java/com/grookage/leia/validator/StaticSchemaValidator.java b/leia-schema-validator/src/main/java/com/grookage/leia/validator/StaticSchemaValidator.java index fe2dce8..5d599b8 100644 --- a/leia-schema-validator/src/main/java/com/grookage/leia/validator/StaticSchemaValidator.java +++ b/leia-schema-validator/src/main/java/com/grookage/leia/validator/StaticSchemaValidator.java @@ -16,12 +16,12 @@ package com.grookage.leia.validator; +import com.grookage.leia.common.exception.SchemaValidationException; +import com.grookage.leia.common.exception.ValidationErrorCode; +import com.grookage.leia.common.utils.SchemaValidationUtils; +import com.grookage.leia.models.annotations.SchemaDefinition; import com.grookage.leia.models.schema.SchemaDetails; import com.grookage.leia.models.schema.SchemaKey; -import com.grookage.leia.validator.annotations.SchemaValidatable; -import com.grookage.leia.validator.exception.SchemaValidationException; -import com.grookage.leia.validator.exception.ValidationErrorCode; -import com.grookage.leia.validator.utils.SchemaValidationUtils; import lombok.Builder; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -64,12 +64,12 @@ public void start() { log.info("Starting the schema validator"); packageRoots.forEach(handlerPackage -> { final var reflections = new Reflections(handlerPackage); - final var annotatedClasses = reflections.getTypesAnnotatedWith(SchemaValidatable.class); + final var annotatedClasses = reflections.getTypesAnnotatedWith(SchemaDefinition.class); annotatedClasses.forEach(annotatedClass -> { - final var annotation = annotatedClass.getAnnotation(SchemaValidatable.class); + final var annotation = annotatedClass.getAnnotation(SchemaDefinition.class); final var schemaKey = SchemaKey.builder() - .schemaName(annotation.schemaName()) - .version(annotation.versionId()) + .schemaName(annotation.name()) + .version(annotation.version()) .namespace(annotation.namespace()) .build(); klassRegistry.putIfAbsent(schemaKey, annotatedClass); diff --git a/pom.xml b/pom.xml index dc78d7f..52fc0b0 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ 4.0.0 com.grookage.leia leia - 0.0.1-RC6 + 0.0.1-RC7 pom Leia @@ -31,6 +31,7 @@ leia-parent leia-core leia-models + leia-common leia-dropwizard leia-repository leia-elastic