Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: custom validator discovery #42

Merged
merged 1 commit into from
Jan 22, 2024
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,15 @@ annotations.
```java
interface JsonAddress extends JsonObject {
@Required
@Validation( maxLength = 50 )
default String street() {
return getString( "street" ).string();
}
@Validation( required = YesNo.YES, minimum = 1)
default Integer no() {
return getNumber( "no" ).integer();
}
@Validation( maxLength = 50 )
default JsonString city() {
return getString( "city" );
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/hisp/dhis/jsontree/Validation.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
* </ol>
* Sources with higher priority override values of sources with lower priority unless the higher priority value is "undefined".
*
* @see Required
*
* @author Jan Bernitt
* @see org.hisp.dhis.jsontree.Validator
* @since 0.11
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,16 @@ private static PropertyValidation fromValueTypeDeclaration( @Surly Class<?> type
private static PropertyValidation fromAnnotations( AnnotatedElement src ) {
PropertyValidation meta = fromMetaAnnotations( src );
Validation validation = getValidationAnnotation( src );
if ( validation == null ) return meta;
PropertyValidation main = toPropertyValidation( validation );
return (meta == null ? main : meta.overlay( main ))
.withCustoms( toValidators( src ) )
.withItems( fromItems( src ) );
PropertyValidation main = validation == null ? null : toPropertyValidation( validation );
List<Validation.Validator> validators = toValidators( src );
PropertyValidation items = fromItems( src );
PropertyValidation base = meta == null ? main : meta.overlay( main );
if (base == null && items == null && validators.isEmpty()) return null;
if (base == null)
base = new PropertyValidation( Set.of(), null, null, null, null, null, null );
return base
.withCustoms( validators )
.withItems( items );
}

@Maybe
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@

class Assertions {

public static Validation.Error assertValidationError( String actualJson, Class<? extends JsonAbstractObject<?>> schema,
public static Validation.Error assertValidationError( String actualJson,
Class<? extends JsonAbstractObject<?>> schema,
Validation.Rule expected, Object... args ) {
return assertValidationError( JsonMixed.of( actualJson ), schema, expected, args );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import java.util.List;
import java.util.Set;

import static org.hisp.dhis.jsontree.Validation.YesNo.NO;
import static org.hisp.dhis.jsontree.Validation.YesNo.YES;
import static org.hisp.dhis.jsontree.validation.Assertions.assertValidationError;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
Expand All @@ -26,15 +25,15 @@ class JsonValidationMaxItemsTest {

public interface JsonMaxItemsExampleA extends JsonObject {

@Validation( maxItems = 2, required = YES)
@Validation( maxItems = 2, required = YES )
default List<String> names() {
return getArray( "names" ).stringValues();
}
}

public interface JsonMaxItemsExampleB extends JsonObject {

@Validation( maxItems = 3)
@Validation( maxItems = 3 )
default JsonList<JsonInteger> points() {
return getList( "points", JsonInteger.class );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

import java.util.Set;

import static org.hisp.dhis.jsontree.Validation.YesNo.NO;
import static org.hisp.dhis.jsontree.Validation.YesNo.YES;
import static org.hisp.dhis.jsontree.validation.Assertions.assertValidationError;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
Expand All @@ -23,7 +22,7 @@ class JsonValidationMaxLengthTest {

public interface JsonMaxLengthExampleA extends JsonObject {

@Validation( maxLength = 2, required = YES)
@Validation( maxLength = 2, required = YES )
default String name() {
return getString( "name" ).string();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import java.util.Map;
import java.util.Set;

import static org.hisp.dhis.jsontree.Validation.YesNo.NO;
import static org.hisp.dhis.jsontree.Validation.YesNo.YES;
import static org.hisp.dhis.jsontree.validation.Assertions.assertValidationError;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
Expand All @@ -34,7 +33,7 @@ default Map<String, Integer> config() {

public interface JsonMaxPropertiesExampleB extends JsonObject {

@Validation( maxProperties = 3)
@Validation( maxProperties = 3 )
default JsonMap<JsonInteger> points() {
return getMap( "points", JsonInteger.class );
}
Expand Down Expand Up @@ -64,9 +63,11 @@ void testMaxProperties_Undefined() {
@Test
void testMaxProperties_TooMany() {
assertValidationError( """
{"config":{"hey": 1, "ho": 2, "silver": 3}}""", JsonMaxPropertiesExampleA.class, Rule.MAX_PROPERTIES, 2, 3 );
{"config":{"hey": 1, "ho": 2, "silver": 3}}""", JsonMaxPropertiesExampleA.class, Rule.MAX_PROPERTIES, 2,
3 );
assertValidationError( """
{"points":{"x": 1, "y": 2, "z": 3, "w": 5}}""", JsonMaxPropertiesExampleB.class, Rule.MAX_PROPERTIES, 3, 4 );
{"points":{"x": 1, "y": 2, "z": 3, "w": 5}}""", JsonMaxPropertiesExampleB.class, Rule.MAX_PROPERTIES, 3,
4 );
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package org.hisp.dhis.jsontree.validation;

import org.hisp.dhis.jsontree.JsonInteger;
import org.hisp.dhis.jsontree.JsonList;
import org.hisp.dhis.jsontree.JsonMap;
import org.hisp.dhis.jsontree.JsonMixed;
import org.hisp.dhis.jsontree.JsonObject;
import org.hisp.dhis.jsontree.Validation;
import org.hisp.dhis.jsontree.Validation.NodeType;
import org.hisp.dhis.jsontree.Validation.Rule;
import org.junit.jupiter.api.Test;

import java.util.Set;

import static org.hisp.dhis.jsontree.validation.Assertions.assertValidationError;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;

/**
* Test to demonstrate how annotations can be used to validate types like @{@link JsonIntList} as the automatic Java
* type analysis will not provide full recursive validation for the items.
*
* @author Jan Bernitt
*/
class JsonValidationMiscCollectionTest {

@Validation( type = NodeType.ARRAY )
@Validation.Items( @Validation( type = NodeType.INTEGER ) )
public interface JsonIntList extends JsonList<JsonInteger> {

}

public interface JsonPage extends JsonObject {

default JsonIntList getEntries() {
return get( "entries", JsonIntList.class );
}
}

public interface JsonBook extends JsonObject {

default JsonMap<JsonPage> pages() {
return getMap( "pages", JsonPage.class );
}
}

@Test
void testCollection_OK() {
assertDoesNotThrow( () -> JsonMixed.of( """
{"entries": []}""" ).validate( JsonPage.class ) );
assertDoesNotThrow( () -> JsonMixed.of( """
{"entries": [1,2,3]}""" ).validate( JsonPage.class ) );
}

@Test
void testCollection_NotAnArray() {
assertValidationError( """
{"entries": {}}""", JsonPage.class, Rule.TYPE, Set.of( NodeType.ARRAY ), NodeType.OBJECT );
}

@Test
void testCollection_NotAnIntElement() {
assertValidationError( """
{"entries": [true]}""", JsonPage.class, Rule.TYPE, Set.of( NodeType.INTEGER ), NodeType.BOOLEAN );
}

@Test
void testCollection_NotAnIntElementDeep() {
Validation.Error error = assertValidationError( """
{"pages": { "title": {"entries": [13, 42.5]}}}""", JsonBook.class, Rule.TYPE, Set.of( NodeType.INTEGER ),
NodeType.NUMBER );
assertEquals( "$.pages.title.entries[1]", error.path() );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,39 +26,69 @@
class JsonValidationMiscDeepGraphTest {

public interface JsonPage extends JsonObject {

@Required
default JsonPager pager() { return get( "pager", JsonPager.class ); }
default JsonPager pager() {
return get( "pager", JsonPager.class );
}

@Required
default JsonList<JsonEntry> getEntries() { return getList( "entries", JsonEntry.class ); }
default JsonList<JsonEntry> getEntries() {
return getList( "entries", JsonEntry.class );
}
}

public interface JsonPager extends JsonObject {
@Validation(minimum = 1)
default int size() { return getNumber( "size" ).intValue(); }
@Validation(minimum = 0)
default int page() { return getNumber( "page" ).intValue(); }
@Validation(minimum = 0)
default Integer total() { return getNumber( "total" ).integer(); }

@Validation( minimum = 1 )
default int size() {
return getNumber( "size" ).intValue();
}

@Validation( minimum = 0 )
default int page() {
return getNumber( "page" ).intValue();
}

@Validation( minimum = 0 )
default Integer total() {
return getNumber( "total" ).integer();
}
}

public interface JsonEntry extends JsonObject {
@Validation(minLength = 11, maxLength = 11)
default String id() { return getString( "id" ).string(); }
@Validation(uniqueItems = YES)
default JsonList<JsonAttribute> attributes() { return getList( "attributes", JsonAttribute.class ); }

default JsonMap<JsonString> getValues() { return getMap( "values", JsonString.class ); }
@Validation( minLength = 11, maxLength = 11 )
default String id() {
return getString( "id" ).string();
}

@Validation( uniqueItems = YES )
default JsonList<JsonAttribute> attributes() {
return getList( "attributes", JsonAttribute.class );
}

default JsonMap<JsonString> getValues() {
return getMap( "values", JsonString.class );
}
}

public interface JsonAttribute extends JsonObject {

@Required
default String name() { return getString( "name" ).string(); }
default String name() {
return getString( "name" ).string();
}

@Validation(dependentRequired = "val^")
default String text() { return getString( "text" ).string(); }
@Validation( dependentRequired = "val^" )
default String text() {
return getString( "text" ).string();
}

@Validation(dependentRequired = "val^")
default Number value() { return getNumber( "value" ).number(); }
@Validation( dependentRequired = "val^" )
default Number value() {
return getNumber( "value" ).number();
}
}

@Test
Expand Down Expand Up @@ -109,16 +139,16 @@ void testDeep_Error_PagerPageNegative() {
@Test
void testDeep_Error_PagerPageNoInteger() {
Validation.Error error = assertValidationError( """
{"pager": {"size": 20, "page": true}, "entries":[]}""", JsonPage.class, Rule.TYPE,
Set.of( NodeType.INTEGER), NodeType.BOOLEAN );
{"pager": {"size": 20, "page": true}, "entries":[]}""", JsonPage.class, Rule.TYPE,
Set.of( NodeType.INTEGER ), NodeType.BOOLEAN );
assertEquals( "$.pager.page", error.path() );
}

@Test
void testDeep_Error_PagerTotalWrongType() {
Validation.Error error = assertValidationError( """
{"pager": {"size": 20, "page": 0, "total": "yes"}, "entries":[]}""", JsonPage.class, Rule.TYPE,
Set.of( NodeType.INTEGER), NodeType.STRING );
{"pager": {"size": 20, "page": 0, "total": "yes"}, "entries":[]}""", JsonPage.class, Rule.TYPE,
Set.of( NodeType.INTEGER ), NodeType.STRING );
assertEquals( "$.pager.total", error.path() );
}

Expand All @@ -141,7 +171,7 @@ void testDeep_Error_IdWrongFormat() {
String json = """
{"pager": {"size": 20, "page": 0}, "entries":[
{"id": "ABC"}]}""";
Validation.Error error = assertValidationError(json, JsonPage.class, Rule.MIN_LENGTH, 11, 3);
Validation.Error error = assertValidationError( json, JsonPage.class, Rule.MIN_LENGTH, 11, 3 );
assertEquals( "$.entries[0].id", error.path() );
}

Expand All @@ -150,7 +180,8 @@ void testDeep_Error_IdWrongType() {
String json = """
{"pager": {"size": 20, "page": 0}, "entries":[
{"id": 42}]}""";
Validation.Error error = assertValidationError(json, JsonPage.class, Rule.TYPE, Set.of( NodeType.STRING ), NodeType.NUMBER);
Validation.Error error = assertValidationError( json, JsonPage.class, Rule.TYPE, Set.of( NodeType.STRING ),
NodeType.NUMBER );
assertEquals( "$.entries[0].id", error.path() );
}

Expand All @@ -159,7 +190,8 @@ void testDeep_Error_ValuesWrongType() {
String json = """
{"pager": {"size": 20, "page": 0}, "entries":[
{"id": "a0123456789", "values": false}]}""";
Validation.Error error = assertValidationError(json, JsonPage.class, Rule.TYPE, Set.of( NodeType.OBJECT ), NodeType.BOOLEAN);
Validation.Error error = assertValidationError( json, JsonPage.class, Rule.TYPE, Set.of( NodeType.OBJECT ),
NodeType.BOOLEAN );
assertEquals( "$.entries[0].values", error.path() );
}

Expand All @@ -168,7 +200,8 @@ void testDeep_Error_AttributesAsMap() {
String json = """
{"pager": {"size": 20, "page": 0}, "entries":[
{"id": "a0123456789", "attributes": {"name": "foo", "value": 1}}]}""";
Validation.Error error = assertValidationError(json, JsonPage.class, Rule.TYPE, Set.of( NodeType.ARRAY ), NodeType.OBJECT);
Validation.Error error = assertValidationError( json, JsonPage.class, Rule.TYPE, Set.of( NodeType.ARRAY ),
NodeType.OBJECT );
assertEquals( "$.entries[0].attributes", error.path() );
}

Expand All @@ -177,7 +210,7 @@ void testDeep_Error_AttributeNoName() {
String json = """
{"pager": {"size": 20, "page": 0}, "entries":[
{"id": "a0123456789", "attributes": [{"value": 1}]}]}""";
Validation.Error error = assertValidationError(json, JsonPage.class, Rule.REQUIRED, "name");
Validation.Error error = assertValidationError( json, JsonPage.class, Rule.REQUIRED, "name" );
assertEquals( "$.entries[0].attributes[0].name", error.path() );
}

Expand All @@ -187,7 +220,8 @@ void testDeep_Error_AttributeNoTextOrValue() {
{"pager": {"size": 20, "page": 0}, "entries":[
{"id": "a0123456789", "attributes": [{"name": "foo"}]}]}""";

Validation.Error error = assertValidationError(json, JsonPage.class, Rule.DEPENDENT_REQUIRED, Set.of("text", "value"), Set.of() );
Validation.Error error = assertValidationError( json, JsonPage.class, Rule.DEPENDENT_REQUIRED,
Set.of( "text", "value" ), Set.of() );
assertEquals( "$.entries[0].attributes[0]", error.path() );
}

Expand All @@ -201,7 +235,7 @@ void testDeep_Error_AttributeNotUnique() {
{"name":"foo","value":1}
]}]}""";

Validation.Error error = assertValidationError(json, JsonPage.class, Rule.UNIQUE_ITEMS,
Validation.Error error = assertValidationError( json, JsonPage.class, Rule.UNIQUE_ITEMS,
"{\"name\":\"foo\",\"value\":1}", 0, 2 );
assertEquals( "$.entries[0].attributes", error.path() );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

import java.util.Set;

import static org.hisp.dhis.jsontree.Validation.YesNo.YES;
import static org.hisp.dhis.jsontree.validation.Assertions.assertValidationError;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;

Expand All @@ -22,7 +21,7 @@ class JsonValidationPatternTest {

public interface JsonPatternExampleA extends JsonObject {

@Validation( pattern = "[0-9]{1,4}[A-Z]?")
@Validation( pattern = "[0-9]{1,4}[A-Z]?" )
default String no() {
return getString( "no" ).string();
}
Expand Down
Loading