diff --git a/README.md b/README.md index a43909b..9392aad 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,7 @@ annotations. ```java interface JsonAddress extends JsonObject { @Required + @Validation( maxLength = 50 ) default String street() { return getString( "street" ).string(); } @@ -174,6 +175,7 @@ interface JsonAddress extends JsonObject { default Integer no() { return getNumber( "no" ).integer(); } + @Validation( maxLength = 50 ) default JsonString city() { return getString( "city" ); } diff --git a/src/main/java/org/hisp/dhis/jsontree/Validation.java b/src/main/java/org/hisp/dhis/jsontree/Validation.java index 7d1335f..5c58fec 100644 --- a/src/main/java/org/hisp/dhis/jsontree/Validation.java +++ b/src/main/java/org/hisp/dhis/jsontree/Validation.java @@ -38,6 +38,8 @@ * * 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 diff --git a/src/main/java/org/hisp/dhis/jsontree/validation/ObjectValidation.java b/src/main/java/org/hisp/dhis/jsontree/validation/ObjectValidation.java index ecbec1d..1a7f110 100644 --- a/src/main/java/org/hisp/dhis/jsontree/validation/ObjectValidation.java +++ b/src/main/java/org/hisp/dhis/jsontree/validation/ObjectValidation.java @@ -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 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 diff --git a/src/test/java/org/hisp/dhis/jsontree/validation/Assertions.java b/src/test/java/org/hisp/dhis/jsontree/validation/Assertions.java index ccc106c..80770a2 100644 --- a/src/test/java/org/hisp/dhis/jsontree/validation/Assertions.java +++ b/src/test/java/org/hisp/dhis/jsontree/validation/Assertions.java @@ -12,7 +12,8 @@ class Assertions { - public static Validation.Error assertValidationError( String actualJson, Class> schema, + public static Validation.Error assertValidationError( String actualJson, + Class> schema, Validation.Rule expected, Object... args ) { return assertValidationError( JsonMixed.of( actualJson ), schema, expected, args ); } diff --git a/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMaxItemsTest.java b/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMaxItemsTest.java index 9ec3481..78d6917 100644 --- a/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMaxItemsTest.java +++ b/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMaxItemsTest.java @@ -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; @@ -26,7 +25,7 @@ class JsonValidationMaxItemsTest { public interface JsonMaxItemsExampleA extends JsonObject { - @Validation( maxItems = 2, required = YES) + @Validation( maxItems = 2, required = YES ) default List names() { return getArray( "names" ).stringValues(); } @@ -34,7 +33,7 @@ default List names() { public interface JsonMaxItemsExampleB extends JsonObject { - @Validation( maxItems = 3) + @Validation( maxItems = 3 ) default JsonList points() { return getList( "points", JsonInteger.class ); } diff --git a/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMaxLengthTest.java b/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMaxLengthTest.java index 2205685..b00a600 100644 --- a/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMaxLengthTest.java +++ b/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMaxLengthTest.java @@ -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; @@ -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(); } diff --git a/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMaxPropertiesTest.java b/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMaxPropertiesTest.java index d3a65ff..3423b4c 100644 --- a/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMaxPropertiesTest.java +++ b/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMaxPropertiesTest.java @@ -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; @@ -34,7 +33,7 @@ default Map config() { public interface JsonMaxPropertiesExampleB extends JsonObject { - @Validation( maxProperties = 3) + @Validation( maxProperties = 3 ) default JsonMap points() { return getMap( "points", JsonInteger.class ); } @@ -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 diff --git a/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMiscCollectionTest.java b/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMiscCollectionTest.java new file mode 100644 index 0000000..4ad29a4 --- /dev/null +++ b/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMiscCollectionTest.java @@ -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 { + + } + + public interface JsonPage extends JsonObject { + + default JsonIntList getEntries() { + return get( "entries", JsonIntList.class ); + } + } + + public interface JsonBook extends JsonObject { + + default JsonMap 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() ); + } +} diff --git a/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMiscDeepGraphTest.java b/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMiscDeepGraphTest.java index c6d0f1b..2f185ef 100644 --- a/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMiscDeepGraphTest.java +++ b/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMiscDeepGraphTest.java @@ -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 getEntries() { return getList( "entries", JsonEntry.class ); } + default JsonList 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 attributes() { return getList( "attributes", JsonAttribute.class ); } - default JsonMap getValues() { return getMap( "values", JsonString.class ); } + @Validation( minLength = 11, maxLength = 11 ) + default String id() { + return getString( "id" ).string(); + } + + @Validation( uniqueItems = YES ) + default JsonList attributes() { + return getList( "attributes", JsonAttribute.class ); + } + + default JsonMap 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 @@ -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() ); } @@ -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() ); } @@ -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() ); } @@ -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() ); } @@ -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() ); } @@ -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() ); } @@ -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() ); } @@ -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() ); } diff --git a/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationPatternTest.java b/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationPatternTest.java index d46a201..a446086 100644 --- a/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationPatternTest.java +++ b/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationPatternTest.java @@ -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; @@ -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(); } diff --git a/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationValidatorTest.java b/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationValidatorTest.java new file mode 100644 index 0000000..ac0892e --- /dev/null +++ b/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationValidatorTest.java @@ -0,0 +1,54 @@ +package org.hisp.dhis.jsontree.validation; + +import org.hisp.dhis.jsontree.JsonMixed; +import org.hisp.dhis.jsontree.JsonObject; +import org.hisp.dhis.jsontree.Validation; +import org.hisp.dhis.jsontree.Validation.Error; +import org.hisp.dhis.jsontree.Validation.Rule; +import org.hisp.dhis.jsontree.Validator; +import org.junit.jupiter.api.Test; + +import java.util.Set; +import java.util.function.Consumer; + +import static org.hisp.dhis.jsontree.validation.Assertions.assertValidationError; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +/** + * Basic test for a custom {@link org.hisp.dhis.jsontree.Validation.Validator} class. + * + * @author Jan Bernitt + */ +class JsonValidationValidatorTest { + + public record UserValidator() implements Validation.Validator { + + @Override + public void validate( JsonMixed value, Consumer addError ) { + if ( !value.isString() ) return; + if ( !Set.of( "user1", "user2" ).contains( value.string() ) ) + addError.accept( + Error.of( Rule.CUSTOM, value, "Not a valid user %s", value.string() ) ); + } + } + + public interface JsonUserUpdate extends JsonObject { + + @Validator( UserValidator.class ) + default String user() { + return getString( "user" ).string(); + } + } + + @Test + void testValidator_OK() { + assertDoesNotThrow( () -> JsonMixed.of( """ + {"user": "user1"}""" ).validate( JsonUserUpdate.class ) ); + } + + @Test + void testValidator_UnknownUser() { + assertValidationError( """ + {"user": "user47"}""", JsonUserUpdate.class, Rule.CUSTOM, "user47" ); + } +}