Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 43 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,33 +244,40 @@ NOTE: Requires Java 11 or greater.
```

### @ObjectType
- **Description**: Checks that the type of an object is one of a list of candidate types.
- **Applies to**: Fields of the Object type, including Collection of objects or Collection of Collection of objects. Collection can be any subinterface such as: List, Set, etc.
- **Description**: Checks that the type of an object matches a specific schema pattern. Supports direct objects, collections, nested collections, maps, and maps with collection values.
- **Applies to**: Fields of the Object type, including various collection and map structures.
- **Parameters**:
- _baseClass_: A candidate base class for the field. Mandatory.
- _firstGroup_: A boolean to indicate if the type is a Collection of the base class. By default is false.
- _secondGroup_: A boolean to indicate if the type is a Collection of Collection of the base class. By default is false.
- _maxSize_: The greatest size of the first Collection if set. By default is 0.
- **Error messages for each @ObjectType**:
- If only _baseClass_ was set and the field type does not match:
- _{baseClass}_
- If _firstGroup_ was set and the field is not Collection of the baseClass:
- _Collection<{baseClass}>_
- If _firstGroup_ and _maxSize_ were set and the field is not a Collection of the baseClass or collection size is greater than maxSize:
- _Collection<{baseClass}> (max {maxSize} items)_
- If _firstGroup_ and _secondGroup_ were set and the field is not a Collection of Collection of the baseClass:
- _Collection<Collection<{baseClass}>>_
- If _firstGroup_, _secondGroup_ and _maxSize_ were set aand the field is not a Collection of Collection of the baseClass or first collection size is greater than maxSize:
- _Collection<Collection<{baseClass}>> (max {maxSize} items)_
- **Error message for all @ObjectType**:
- If the field type does not match any ObjectType:
- _type must be or {msg for ObjectType 1} or {msg for ObjectType 2} ... or {msg for ObjectType N}._
- _schema_: The schema pattern to validate against. Options: DIRECT, COLL, COLL_COLL, MAP, MAP_COLL. By default is DIRECT.
- _baseClass_: Array of candidate base classes for the field. Mandatory.
- _keyClass_: The class type for map keys when using MAP or MAP_COLL schema. By default is void.class.
- _maxSize_: The maximum size of the outer collection or map. By default is Integer.MAX_VALUE.
- _maxInnerSize_: The maximum size of inner collections. By default is Integer.MAX_VALUE.
- _maxChecks_: The maximum number of items to check for performance optimization. By default is 20.
- _allowNull_: Whether to allow null values in collections or maps. By default is true.
- _allowInnerNull_: Whether to allow null values in inner collections. By default is true.
- **Schema Types**:
- _DIRECT_: Validates direct object types (baseClass)
- _COLL_: Validates Collection\<baseClass>
- _COLL_COLL_: Validates Collection<Collection\<baseClass>>
- _MAP_: Validates Map<keyClass, baseClass>
- _MAP_COLL_: Validates Map<keyClass, Collection\<baseClass>>
- **Error messages**:
- Dynamically generated based on schema type and configuration parameters
- **Example**:
```java
@ObjectType(baseClass = String.class)
@ObjectType(baseClass = String.class, firstGroup = true, maxSize = 2)
@ObjectType(baseClass = String.class, firstGroup = true, secondGroup = true, maxSize = 2)
Object reference;
// directReference could be: String or Integer
@ObjectType(baseClass = {String.class, Integer.class})
private Object directReference;

// mapReference could be: Map<String, Double> or Map<String, Integer>
@ObjectType(schema = Schema.MAP, keyClass = String.class, baseClass = {Double.class, Integer.class})
private Object mapReference;

// multiSchemaReference could be: String or List<String> or List<List<String>>
@ObjectType(baseClass = {String.class})
@ObjectType(schema = Schema.COLL, baseClass = {String.class}, maxSize = 2)
@ObjectType(schema = Schema.COLL_COLL, baseClass = {String.class}, maxSize = 2)
private Object multiSchemaReference;
```

### @Valid
Expand Down Expand Up @@ -373,6 +380,15 @@ public class YourNewValidator implements ConstraintValidator<YourNewConstraint,
...
}

@Override
public String getMessage() {
// Implement this method when complex logic is needed to build the message
// This is optional - if not implemented, the message from the annotation will be used
// Example:
// return "Custom message with " + annotMethod1 + " and " + annotMethod2;
return null; // Return null to use the annotation's message
}

@Override
public boolean isValid(Object value) {
// This condition applies for field-level constraints only
Expand All @@ -389,8 +405,9 @@ public class YourNewValidator implements ConstraintValidator<YourNewConstraint,
```
- Implement the `ConstraintValidator<A, T>` interface, where A represents YourNewConstraint and T represents the class of the objects to validate, in this case, you can use `Object` if your validations applies to more than one class.
- Create as field members as annotation methods you have in YourNewConstraint, excluding message().
- Overrides the `initialize()` method to capture the annotation method values in your field members.
- Overrides the `isValid()` method to do the validation logic. For field-level constraints only: your first validation step must return true if the object to validate is null, because we have the annotation `@Required` to validate that condition, we don't want to evaluate that nullity here.
- Override the `initialize()` method to capture the annotation method values in your field members.
- Override the `getMessage()` method (optional) when you need complex logic to build the error message. If not implemented or returns null, the framework will use the message from the annotation with template processing.
- Override the `isValid()` method to do the validation logic. For field-level constraints only: your first validation step must return true if the object to validate is null, because we have the annotation `@Required` to validate that condition, we don't want to evaluate that nullity here.

## 💼 Contributing
Please read our [Contributing](CONTRIBUTING.md) guide to learn and understand how to contribute to this project.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ public interface ConstraintValidator<A extends Annotation, T> {
default void initialize(A annotation) {
}

/**
* Prepare and get the message from this validator. Implement it when major logic is needed.
*
* @return The prepared message.
*/
default String getMessage() {
return null;
}

/**
* Execute the object validation against the constraint.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ public class ConstraintViolation {
private final Object value;
private final String name;
private final AnnotationMetadata annotationMetadata;
private final String message;

public ConstraintViolation(Object value, String name, AnnotationMetadata annotationMetadata) {
public ConstraintViolation(Object value, String name, AnnotationMetadata annotationMetadata, String message) {
this.value = value;
this.name = name;
this.annotationMetadata = annotationMetadata;
this.message = message;
}

public Object getValue() {
Expand All @@ -38,24 +40,27 @@ public String getName() {
* @return The concrete violation message.
*/
public String getMessage() {
if (this.message != null && !message.isBlank()) {
return this.message;
}
final String TMPL_LOOP = "#for(";
final String TMPL_CONDITION = "#if(";
var values = annotationMetadata.getValuesByAnnotMethod();
var message = values.get("message").toString();
if (message.contains(TMPL_LOOP)) {
return replaceLoop(message, getSubMessages(annotationMetadata));
} else if (message.contains(TMPL_CONDITION)) {
return replaceCondition(message, values);
var statedMessage = values.get("message").toString();
if (statedMessage.contains(TMPL_LOOP)) {
return replaceLoop(statedMessage, getSubMessages(annotationMetadata));
} else if (statedMessage.contains(TMPL_CONDITION)) {
return replaceCondition(statedMessage, values);
} else {
return replaceValues(message, values);
return replaceValues(statedMessage, values);
}
}

private List<String> getSubMessages(AnnotationMetadata annotationMetadata) {
List<String> subMessages = new ArrayList<>();
var subAnnotations = annotationMetadata.getSubAnnotations();
for (var subAnnotation : subAnnotations) {
var subViolation = new ConstraintViolation(null, null, subAnnotation);
var subViolation = new ConstraintViolation(null, null, subAnnotation, null);
var subMessage = subViolation.getMessage();
subMessages.add(subMessage);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ private <A extends Annotation, T> Optional<ConstraintViolation> validateAnnotati
if (constraintValidator.isValid(value)) {
return Optional.empty();
} else {
return Optional.of(new ConstraintViolation(value, pathName, annotationMetadata));
var message = constraintValidator.getMessage();
return Optional.of(new ConstraintViolation(value, pathName, annotationMetadata, message));
}
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package io.github.sashirestela.slimvalidator.constraints;

import io.github.sashirestela.slimvalidator.Constraint;
import io.github.sashirestela.slimvalidator.constraints.ObjectType.List;
import io.github.sashirestela.slimvalidator.validators.ObjectTypeListValidator;
import io.github.sashirestela.slimvalidator.constraints.ObjectType.ObjectTypes;
import io.github.sashirestela.slimvalidator.validators.ObjectTypeValidator;
import io.github.sashirestela.slimvalidator.validators.ObjectTypesValidator;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
Expand All @@ -16,32 +16,42 @@
@Constraint(validatedBy = ObjectTypeValidator.class)
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(List.class)
@Repeatable(ObjectTypes.class)
public @interface ObjectType {

String message() default ""
+ "#if(firstGroup)Collection\\<#endif"
+ "#if(secondGroup)Collection\\<#endif"
+ "{baseClass}"
+ "#if(secondGroup)>#endif"
+ "#if(firstGroup)>#endif"
+ "#if(maxSize) (max {maxSize} items)#endif";
enum Schema {
DIRECT, // baseClass
COLL, // Collection<baseClass>
COLL_COLL, // Collection<Collection<baseClass>>
MAP, // Map<keyClass, baseClass>
MAP_COLL // Map<keyClass, Collection<baseClass>>
}

String message() default "";

Schema schema() default Schema.DIRECT;

Class<?>[] baseClass();

Class<?> keyClass() default void.class;

int maxSize() default Integer.MAX_VALUE;

Class<?> baseClass();
int maxInnerSize() default Integer.MAX_VALUE;

boolean firstGroup() default false;
int maxChecks() default 20;

boolean secondGroup() default false;
boolean allowNull() default true;

int maxSize() default 0;
boolean allowInnerNull() default true;

@Documented
@Constraint(validatedBy = ObjectTypeListValidator.class)
@Constraint(validatedBy = ObjectTypesValidator.class)
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@interface List {
@interface ObjectTypes {

String message() default "type must be#for(value) or {message}#endfor.";
String message() default "";

ObjectType[] value();

Expand Down

This file was deleted.

Loading