From 71dd75a346618bc23eb0222baed3db49e42e509b Mon Sep 17 00:00:00 2001 From: Sarah Kumwimba Date: Wed, 17 Jun 2026 11:21:33 +0200 Subject: [PATCH 1/9] Use JsonPointer syntax --- .../ee/core/jaxrs/ProblemConfigurator.java | 1 + .../core/jaxrs/ProblemConfiguratorTest.java | 18 +++++++ ...JacksonJsonMappingExceptionMapperTest.java | 4 +- ...sonMismatchedInputExceptionMapperTest.java | 2 +- .../core/QuarkusProblemConfigurator.java | 5 +- .../core/QuarkusProblemConfiguratorTest.java | 14 +++-- .../ProblemConfigurationProperties.java | 11 ++++ .../ProblemConfigurationPropertiesTest.java | 15 ++++++ .../AbstractRoutingExceptionsHandlerTest.java | 6 +-- .../rest/problem/config/ProblemConfig.java | 15 ++++++ .../rest/problem/internal/Jackson2Util.java | 12 +++-- .../rest/problem/internal/Jackson3Util.java | 12 +++-- .../problem/internal/Jackson2UtilTest.java | 51 ++++++++++++++++--- .../problem/internal/Jackson3UtilTest.java | 51 ++++++++++++++++--- src/main/asciidoc/index.adoc | 44 ++++++++++++++++ src/main/asciidoc/release-notes.adoc | 6 +++ 16 files changed, 236 insertions(+), 31 deletions(-) diff --git a/belgif-rest-problem-java-ee-core/src/main/java/io/github/belgif/rest/problem/ee/core/jaxrs/ProblemConfigurator.java b/belgif-rest-problem-java-ee-core/src/main/java/io/github/belgif/rest/problem/ee/core/jaxrs/ProblemConfigurator.java index 9f87b6cf..4a484488 100644 --- a/belgif-rest-problem-java-ee-core/src/main/java/io/github/belgif/rest/problem/ee/core/jaxrs/ProblemConfigurator.java +++ b/belgif-rest-problem-java-ee-core/src/main/java/io/github/belgif/rest/problem/ee/core/jaxrs/ProblemConfigurator.java @@ -25,6 +25,7 @@ public void contextInitialized(ServletContextEvent sce) { setBooleanConfig(sce, ProblemConfig.PROPERTY_STACK_TRACE_ENABLED, ProblemConfig::setStackTraceEnabled); setBooleanConfig(sce, ProblemConfig.PROPERTY_EXT_ISSUE_TYPES_ENABLED, ProblemConfig::setExtIssueTypesEnabled); setBooleanConfig(sce, ProblemConfig.PROPERTY_EXT_INPUTS_ARRAY_ENABLED, ProblemConfig::setExtInputsArrayEnabled); + setBooleanConfig(sce, ProblemConfig.PROPERTY_JSON_POINTER_ENABLED, ProblemConfig::setJsonPointerEnabled); } private void setBooleanConfig(ServletContextEvent sce, String key, Consumer configSetter) { diff --git a/belgif-rest-problem-java-ee-core/src/test/java/io/github/belgif/rest/problem/ee/core/jaxrs/ProblemConfiguratorTest.java b/belgif-rest-problem-java-ee-core/src/test/java/io/github/belgif/rest/problem/ee/core/jaxrs/ProblemConfiguratorTest.java index 808830b5..a0dcbef3 100644 --- a/belgif-rest-problem-java-ee-core/src/test/java/io/github/belgif/rest/problem/ee/core/jaxrs/ProblemConfiguratorTest.java +++ b/belgif-rest-problem-java-ee-core/src/test/java/io/github/belgif/rest/problem/ee/core/jaxrs/ProblemConfiguratorTest.java @@ -37,15 +37,18 @@ void notConfigured() { boolean stackTraceEnabledBefore = ProblemConfig.isStackTraceEnabled(); boolean extIssueTypesEnabledBefore = ProblemConfig.isExtIssueTypesEnabled(); boolean extInputsArrayEnabledBefore = ProblemConfig.isExtInputsArrayEnabled(); + boolean jsonPointerEnabledBefore = ProblemConfig.isJsonPointerEnabled(); when(servletContext.getInitParameter(ProblemConfig.PROPERTY_I18N_ENABLED)).thenReturn(null); when(servletContext.getInitParameter(ProblemConfig.PROPERTY_STACK_TRACE_ENABLED)).thenReturn(null); when(servletContext.getInitParameter(ProblemConfig.PROPERTY_EXT_ISSUE_TYPES_ENABLED)).thenReturn(null); when(servletContext.getInitParameter(ProblemConfig.PROPERTY_EXT_INPUTS_ARRAY_ENABLED)).thenReturn(null); + when(servletContext.getInitParameter(ProblemConfig.PROPERTY_JSON_POINTER_ENABLED)).thenReturn(null); configurator.contextInitialized(new ServletContextEvent(servletContext)); assertThat(ProblemConfig.isI18nEnabled()).isEqualTo(i18nEnabledBefore); assertThat(ProblemConfig.isStackTraceEnabled()).isEqualTo(stackTraceEnabledBefore); assertThat(ProblemConfig.isExtIssueTypesEnabled()).isEqualTo(extIssueTypesEnabledBefore); assertThat(ProblemConfig.isExtInputsArrayEnabled()).isEqualTo(extInputsArrayEnabledBefore); + assertThat(ProblemConfig.isJsonPointerEnabled()).isEqualTo(jsonPointerEnabledBefore); } @Test @@ -54,11 +57,13 @@ void enabledViaInitParam() { when(servletContext.getInitParameter(ProblemConfig.PROPERTY_STACK_TRACE_ENABLED)).thenReturn("true"); when(servletContext.getInitParameter(ProblemConfig.PROPERTY_EXT_ISSUE_TYPES_ENABLED)).thenReturn("true"); when(servletContext.getInitParameter(ProblemConfig.PROPERTY_EXT_INPUTS_ARRAY_ENABLED)).thenReturn("true"); + when(servletContext.getInitParameter(ProblemConfig.PROPERTY_JSON_POINTER_ENABLED)).thenReturn("true"); configurator.contextInitialized(new ServletContextEvent(servletContext)); assertThat(ProblemConfig.isI18nEnabled()).isTrue(); assertThat(ProblemConfig.isStackTraceEnabled()).isTrue(); assertThat(ProblemConfig.isExtIssueTypesEnabled()).isTrue(); assertThat(ProblemConfig.isExtInputsArrayEnabled()).isTrue(); + assertThat(ProblemConfig.isJsonPointerEnabled()).isTrue(); } @Test @@ -67,11 +72,13 @@ void disabledViaInitParam() { when(servletContext.getInitParameter(ProblemConfig.PROPERTY_STACK_TRACE_ENABLED)).thenReturn("false"); when(servletContext.getInitParameter(ProblemConfig.PROPERTY_EXT_ISSUE_TYPES_ENABLED)).thenReturn("false"); when(servletContext.getInitParameter(ProblemConfig.PROPERTY_EXT_INPUTS_ARRAY_ENABLED)).thenReturn("false"); + when(servletContext.getInitParameter(ProblemConfig.PROPERTY_JSON_POINTER_ENABLED)).thenReturn("false"); configurator.contextInitialized(new ServletContextEvent(servletContext)); assertThat(ProblemConfig.isI18nEnabled()).isFalse(); assertThat(ProblemConfig.isStackTraceEnabled()).isFalse(); assertThat(ProblemConfig.isExtIssueTypesEnabled()).isFalse(); assertThat(ProblemConfig.isExtInputsArrayEnabled()).isFalse(); + assertThat(ProblemConfig.isJsonPointerEnabled()).isFalse(); } @Test @@ -79,12 +86,14 @@ void disabledViaInitParam() { @SetSystemProperty(key = ProblemConfig.PROPERTY_STACK_TRACE_ENABLED, value = "true") @SetSystemProperty(key = ProblemConfig.PROPERTY_EXT_ISSUE_TYPES_ENABLED, value = "true") @SetSystemProperty(key = ProblemConfig.PROPERTY_EXT_INPUTS_ARRAY_ENABLED, value = "true") + @SetSystemProperty(key = ProblemConfig.PROPERTY_JSON_POINTER_ENABLED, value = "true") void enabledViaSystemProperties() { configurator.contextInitialized(new ServletContextEvent(servletContext)); assertThat(ProblemConfig.isI18nEnabled()).isTrue(); assertThat(ProblemConfig.isStackTraceEnabled()).isTrue(); assertThat(ProblemConfig.isExtIssueTypesEnabled()).isTrue(); assertThat(ProblemConfig.isExtInputsArrayEnabled()).isTrue(); + assertThat(ProblemConfig.isJsonPointerEnabled()).isTrue(); verifyNoInteractions(servletContext); } @@ -93,12 +102,14 @@ void enabledViaSystemProperties() { @SetSystemProperty(key = ProblemConfig.PROPERTY_STACK_TRACE_ENABLED, value = "false") @SetSystemProperty(key = ProblemConfig.PROPERTY_EXT_ISSUE_TYPES_ENABLED, value = "false") @SetSystemProperty(key = ProblemConfig.PROPERTY_EXT_INPUTS_ARRAY_ENABLED, value = "false") + @SetSystemProperty(key = ProblemConfig.PROPERTY_JSON_POINTER_ENABLED, value = "false") void disabledViaSystemProperty() { configurator.contextInitialized(new ServletContextEvent(servletContext)); assertThat(ProblemConfig.isI18nEnabled()).isFalse(); assertThat(ProblemConfig.isStackTraceEnabled()).isFalse(); assertThat(ProblemConfig.isExtIssueTypesEnabled()).isFalse(); assertThat(ProblemConfig.isExtInputsArrayEnabled()).isFalse(); + assertThat(ProblemConfig.isJsonPointerEnabled()).isFalse(); verifyNoInteractions(servletContext); } @@ -107,12 +118,14 @@ void disabledViaSystemProperty() { @SetEnvironmentVariable(key = ProblemConfig.PROPERTY_STACK_TRACE_ENABLED, value = "true") @SetEnvironmentVariable(key = ProblemConfig.PROPERTY_EXT_ISSUE_TYPES_ENABLED, value = "true") @SetEnvironmentVariable(key = ProblemConfig.PROPERTY_EXT_INPUTS_ARRAY_ENABLED, value = "true") + @SetEnvironmentVariable(key = ProblemConfig.PROPERTY_JSON_POINTER_ENABLED, value = "true") void enabledViaEnvironmentVariable() { configurator.contextInitialized(new ServletContextEvent(servletContext)); assertThat(ProblemConfig.isI18nEnabled()).isTrue(); assertThat(ProblemConfig.isStackTraceEnabled()).isTrue(); assertThat(ProblemConfig.isExtIssueTypesEnabled()).isTrue(); assertThat(ProblemConfig.isExtInputsArrayEnabled()).isTrue(); + assertThat(ProblemConfig.isJsonPointerEnabled()).isTrue(); verifyNoInteractions(servletContext); } @@ -121,12 +134,14 @@ void enabledViaEnvironmentVariable() { @SetEnvironmentVariable(key = ProblemConfig.PROPERTY_STACK_TRACE_ENABLED, value = "false") @SetEnvironmentVariable(key = ProblemConfig.PROPERTY_EXT_ISSUE_TYPES_ENABLED, value = "false") @SetEnvironmentVariable(key = ProblemConfig.PROPERTY_EXT_INPUTS_ARRAY_ENABLED, value = "false") + @SetEnvironmentVariable(key = ProblemConfig.PROPERTY_JSON_POINTER_ENABLED, value = "false") void disabledViaEnvironmentVariable() { configurator.contextInitialized(new ServletContextEvent(servletContext)); assertThat(ProblemConfig.isI18nEnabled()).isFalse(); assertThat(ProblemConfig.isStackTraceEnabled()).isFalse(); assertThat(ProblemConfig.isExtIssueTypesEnabled()).isFalse(); assertThat(ProblemConfig.isExtInputsArrayEnabled()).isFalse(); + assertThat(ProblemConfig.isJsonPointerEnabled()).isFalse(); verifyNoInteractions(servletContext); } @@ -135,16 +150,19 @@ void disabledViaEnvironmentVariable() { @SetEnvironmentVariable(key = ProblemConfig.PROPERTY_STACK_TRACE_ENABLED, value = "false") @SetEnvironmentVariable(key = ProblemConfig.PROPERTY_EXT_ISSUE_TYPES_ENABLED, value = "false") @SetEnvironmentVariable(key = ProblemConfig.PROPERTY_EXT_INPUTS_ARRAY_ENABLED, value = "false") + @SetEnvironmentVariable(key = ProblemConfig.PROPERTY_JSON_POINTER_ENABLED, value = "false") @SetSystemProperty(key = ProblemConfig.PROPERTY_I18N_ENABLED, value = "true") @SetSystemProperty(key = ProblemConfig.PROPERTY_STACK_TRACE_ENABLED, value = "true") @SetSystemProperty(key = ProblemConfig.PROPERTY_EXT_ISSUE_TYPES_ENABLED, value = "true") @SetSystemProperty(key = ProblemConfig.PROPERTY_EXT_INPUTS_ARRAY_ENABLED, value = "true") + @SetSystemProperty(key = ProblemConfig.PROPERTY_JSON_POINTER_ENABLED, value = "true") void systemPropertyHasPrecedenceOverEnvironmentVariable() { configurator.contextInitialized(new ServletContextEvent(servletContext)); assertThat(ProblemConfig.isI18nEnabled()).isTrue(); assertThat(ProblemConfig.isStackTraceEnabled()).isTrue(); assertThat(ProblemConfig.isExtIssueTypesEnabled()).isTrue(); assertThat(ProblemConfig.isExtInputsArrayEnabled()).isTrue(); + assertThat(ProblemConfig.isJsonPointerEnabled()).isTrue(); verifyNoInteractions(servletContext); } diff --git a/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/jaxrs/JacksonJsonMappingExceptionMapperTest.java b/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/jaxrs/JacksonJsonMappingExceptionMapperTest.java index e19dd1a1..5efff176 100644 --- a/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/jaxrs/JacksonJsonMappingExceptionMapperTest.java +++ b/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/jaxrs/JacksonJsonMappingExceptionMapperTest.java @@ -30,7 +30,7 @@ void toResponse() { InputValidationIssue issue = problem.getIssues().get(0); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("id"); + assertThat(issue.getName()).isEqualTo("/id"); assertThat(issue.getValue()).isNull(); assertThat(issue.getDetail()).isEqualTo("detail"); } @@ -52,7 +52,7 @@ void valueInstantiationExceptionToResponse() { InputValidationIssue issue = problem.getIssues().get(0); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("id"); + assertThat(issue.getName()).isEqualTo("/id"); assertThat(issue.getValue()).isNull(); assertThat(issue.getDetail()).isEqualTo("Unexpected value 'XXL'"); } diff --git a/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/jaxrs/JacksonMismatchedInputExceptionMapperTest.java b/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/jaxrs/JacksonMismatchedInputExceptionMapperTest.java index 337a820b..08950232 100644 --- a/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/jaxrs/JacksonMismatchedInputExceptionMapperTest.java +++ b/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/jaxrs/JacksonMismatchedInputExceptionMapperTest.java @@ -28,7 +28,7 @@ void toResponse() { InputValidationIssue issue = problem.getIssues().get(0); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("id"); + assertThat(issue.getName()).isEqualTo("/id"); assertThat(issue.getValue()).isNull(); assertThat(issue.getDetail()).isEqualTo("detail"); } diff --git a/belgif-rest-problem-quarkus-core/src/main/java/io/github/belgif/rest/problem/quarkus/core/QuarkusProblemConfigurator.java b/belgif-rest-problem-quarkus-core/src/main/java/io/github/belgif/rest/problem/quarkus/core/QuarkusProblemConfigurator.java index 27b4e4eb..dc01e2f3 100644 --- a/belgif-rest-problem-quarkus-core/src/main/java/io/github/belgif/rest/problem/quarkus/core/QuarkusProblemConfigurator.java +++ b/belgif-rest-problem-quarkus-core/src/main/java/io/github/belgif/rest/problem/quarkus/core/QuarkusProblemConfigurator.java @@ -26,11 +26,14 @@ public QuarkusProblemConfigurator( @ConfigProperty( name = ProblemConfig.PROPERTY_EXT_ISSUE_TYPES_ENABLED) Optional extIssueTypesEnabled, @ConfigProperty( - name = ProblemConfig.PROPERTY_EXT_INPUTS_ARRAY_ENABLED) Optional extInputsArrayEnabled) { + name = ProblemConfig.PROPERTY_EXT_INPUTS_ARRAY_ENABLED) Optional extInputsArrayEnabled, + @ConfigProperty( + name = ProblemConfig.PROPERTY_JSON_POINTER_ENABLED) Optional jsonPointerEnabled) { i18nEnabled.ifPresent(ProblemConfig::setI18nEnabled); stackTraceEnabled.ifPresent(ProblemConfig::setStackTraceEnabled); extIssueTypesEnabled.ifPresent(ProblemConfig::setExtIssueTypesEnabled); extInputsArrayEnabled.ifPresent(ProblemConfig::setExtInputsArrayEnabled); + jsonPointerEnabled.ifPresent(ProblemConfig::setJsonPointerEnabled); } } diff --git a/belgif-rest-problem-quarkus-core/src/test/java/io/github/belgif/rest/problem/quarkus/core/QuarkusProblemConfiguratorTest.java b/belgif-rest-problem-quarkus-core/src/test/java/io/github/belgif/rest/problem/quarkus/core/QuarkusProblemConfiguratorTest.java index e2c0a187..c0eceb04 100644 --- a/belgif-rest-problem-quarkus-core/src/test/java/io/github/belgif/rest/problem/quarkus/core/QuarkusProblemConfiguratorTest.java +++ b/belgif-rest-problem-quarkus-core/src/test/java/io/github/belgif/rest/problem/quarkus/core/QuarkusProblemConfiguratorTest.java @@ -20,20 +20,23 @@ void cleanup() { @Test void allEnabled() { - new QuarkusProblemConfigurator(Optional.of(true), Optional.of(true), Optional.of(true), Optional.of(true)); + new QuarkusProblemConfigurator(Optional.of(true), Optional.of(true), Optional.of(true), Optional.of(true), + Optional.of(true)); assertThat(ProblemConfig.isI18nEnabled()).isTrue(); assertThat(ProblemConfig.isStackTraceEnabled()).isTrue(); assertThat(ProblemConfig.isExtIssueTypesEnabled()).isTrue(); assertThat(ProblemConfig.isExtInputsArrayEnabled()).isTrue(); + assertThat(ProblemConfig.isJsonPointerEnabled()).isTrue(); } @Test void allDisabled() { - new QuarkusProblemConfigurator(Optional.of(false), Optional.of(false), Optional.of(false), Optional.of(false)); + new QuarkusProblemConfigurator(Optional.of(false), Optional.of(false), Optional.of(false), Optional.of(false), + Optional.of(false)); assertThat(ProblemConfig.isI18nEnabled()).isFalse(); assertThat(ProblemConfig.isStackTraceEnabled()).isFalse(); assertThat(ProblemConfig.isExtIssueTypesEnabled()).isFalse(); - assertThat(ProblemConfig.isExtInputsArrayEnabled()).isFalse(); + assertThat(ProblemConfig.isJsonPointerEnabled()).isFalse(); } @Test @@ -42,11 +45,14 @@ void notConfigured() { boolean stackTraceEnabledBefore = ProblemConfig.isStackTraceEnabled(); boolean extIssueTypesEnabledBefore = ProblemConfig.isExtIssueTypesEnabled(); boolean extInputsArrayEnabledBefore = ProblemConfig.isExtInputsArrayEnabled(); - new QuarkusProblemConfigurator(Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()); + boolean jsonPointerEnabledBefore = ProblemConfig.isJsonPointerEnabled(); + new QuarkusProblemConfigurator(Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty()); assertThat(ProblemConfig.isI18nEnabled()).isEqualTo(i18nEnabledBefore); assertThat(ProblemConfig.isStackTraceEnabled()).isEqualTo(stackTraceEnabledBefore); assertThat(ProblemConfig.isExtIssueTypesEnabled()).isEqualTo(extIssueTypesEnabledBefore); assertThat(ProblemConfig.isExtInputsArrayEnabled()).isEqualTo(extInputsArrayEnabledBefore); + assertThat(ProblemConfig.isJsonPointerEnabled()).isEqualTo(jsonPointerEnabledBefore); } } diff --git a/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/ProblemConfigurationProperties.java b/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/ProblemConfigurationProperties.java index c104f59f..5a40592e 100644 --- a/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/ProblemConfigurationProperties.java +++ b/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/ProblemConfigurationProperties.java @@ -21,6 +21,8 @@ public class ProblemConfigurationProperties implements InitializingBean { private Boolean stackTraceEnabled = null; + private Boolean jsonPointerEnabled = null; + @Value("${io.github.belgif.rest.problem.scan-additional-problem-packages:#{{}}}") public void setScanAdditionalProblemPackages(List scanAdditionalProblemPackages) { this.scanAdditionalProblemPackages = scanAdditionalProblemPackages; @@ -40,6 +42,11 @@ public void setStackTraceEnabled(Boolean stackTraceEnabled) { this.stackTraceEnabled = stackTraceEnabled; } + @Value("${io.github.belgif.rest.problem.json-pointer-enabled:#{null}}") + public void setJsonPointerEnabled(Boolean jsonPointerEnabled) { + this.jsonPointerEnabled = jsonPointerEnabled; + } + @Override public void afterPropertiesSet() { if (i18nEnabled != null) { @@ -48,6 +55,10 @@ public void afterPropertiesSet() { if (stackTraceEnabled != null) { ProblemConfig.setStackTraceEnabled(stackTraceEnabled); } + + if (jsonPointerEnabled != null) { + ProblemConfig.setJsonPointerEnabled(jsonPointerEnabled); + } } } diff --git a/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/ProblemConfigurationPropertiesTest.java b/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/ProblemConfigurationPropertiesTest.java index a8125c15..3f7e8a15 100644 --- a/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/ProblemConfigurationPropertiesTest.java +++ b/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/ProblemConfigurationPropertiesTest.java @@ -25,10 +25,12 @@ void cleanup() { void empty() { boolean i18nEnabledBefore = ProblemConfig.isI18nEnabled(); boolean stackTraceEnabledBefore = ProblemConfig.isStackTraceEnabled(); + boolean jsonPointerEnabledBefore = ProblemConfig.isJsonPointerEnabled(); properties.afterPropertiesSet(); assertThat(properties.getScanAdditionalProblemPackages()).isEmpty(); assertThat(ProblemConfig.isI18nEnabled()).isEqualTo(i18nEnabledBefore); assertThat(ProblemConfig.isStackTraceEnabled()).isEqualTo(stackTraceEnabledBefore); + assertThat(ProblemConfig.isJsonPointerEnabled()).isEqualTo(jsonPointerEnabledBefore); } @Test @@ -66,4 +68,17 @@ void stackTraceDisabled() { assertThat(ProblemConfig.isStackTraceEnabled()).isFalse(); } + @Test + void jsonPointerEnabled() { + properties.setJsonPointerEnabled(true); + properties.afterPropertiesSet(); + assertThat(ProblemConfig.isJsonPointerEnabled()).isTrue(); + } + + @Test + void jsonPointerDisabled() { + properties.setJsonPointerEnabled(false); + properties.afterPropertiesSet(); + assertThat(ProblemConfig.isJsonPointerEnabled()).isFalse(); + } } diff --git a/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/server/AbstractRoutingExceptionsHandlerTest.java b/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/server/AbstractRoutingExceptionsHandlerTest.java index e3f39da2..62b14df6 100644 --- a/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/server/AbstractRoutingExceptionsHandlerTest.java +++ b/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/server/AbstractRoutingExceptionsHandlerTest.java @@ -62,7 +62,7 @@ void handleMissingServletRequestParameterException() { assertThat(problem.getIssues()).hasSize(1); assertThat(problem.getIssues().get(0).getType()).isEqualTo(ISSUE_TYPE_SCHEMA_VIOLATION); assertThat(problem.getIssues().get(0).getIn()).isEqualTo(InEnum.QUERY); - assertThat(problem.getIssues().get(0).getName()).isEqualTo("name"); + assertThat(problem.getIssues().get(0).getName()).isEqualTo("/name"); assertThat(problem.getIssues().get(0).getDetail()).isEqualTo( "Required request parameter 'name' for method parameter type String is not present"); }); @@ -79,7 +79,7 @@ void handleMissingRequestHeaderException() throws Exception { assertThat(problem.getIssues()).hasSize(1); assertThat(problem.getIssues().get(0).getType()).isEqualTo(ISSUE_TYPE_SCHEMA_VIOLATION); assertThat(problem.getIssues().get(0).getIn()).isEqualTo(InEnum.HEADER); - assertThat(problem.getIssues().get(0).getName()).isEqualTo("name"); + assertThat(problem.getIssues().get(0).getName()).isEqualTo("/name"); assertThat(problem.getIssues().get(0).getDetail()).isEqualTo( "Required request header 'name' for method parameter type Object is not present"); }); @@ -98,7 +98,7 @@ void handleHttpMessageNotReadableJacksonMismatchedInputException() { assertThat(problem.getIssues().get(0).getType()) .hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); assertThat(problem.getIssues().get(0).getIn()).isEqualTo(InEnum.BODY); - assertThat(problem.getIssues().get(0).getName()).isEqualTo("id"); + assertThat(problem.getIssues().get(0).getName()).isEqualTo("/id"); assertThat(problem.getIssues().get(0).getValue()).isNull(); assertThat(problem.getIssues().get(0).getDetail()).isEqualTo("detail"); }); diff --git a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/config/ProblemConfig.java b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/config/ProblemConfig.java index 8ca23093..1e7b5ebd 100644 --- a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/config/ProblemConfig.java +++ b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/config/ProblemConfig.java @@ -17,6 +17,9 @@ public class ProblemConfig { public static final String PROPERTY_EXT_INPUTS_ARRAY_ENABLED = "io.github.belgif.rest.problem.ext.inputs-array-enabled"; + public static final String PROPERTY_JSON_POINTER_ENABLED = + "io.github.belgif.rest.problem.json-pointer-enabled"; + private static final boolean DEFAULT_I18N_ENABLED = true; private static final boolean DEFAULT_STACK_TRACE_ENABLED = false; @@ -24,6 +27,7 @@ public class ProblemConfig { private static final boolean DEFAULT_EXT_ISSUE_TYPES_ENABLED = false; private static final boolean DEFAULT_EXT_INPUTS_ARRAY_ENABLED = false; + private static final boolean DEFAULT_JSON_POINTER_ENABLED = true; private static boolean i18nEnabled = DEFAULT_I18N_ENABLED; @@ -33,6 +37,8 @@ public class ProblemConfig { private static boolean extInputsArrayEnabled = DEFAULT_EXT_INPUTS_ARRAY_ENABLED; + private static boolean jsonPointerEnabled = DEFAULT_JSON_POINTER_ENABLED; + private static final ThreadLocal LOCAL_EXT_ISSUE_TYPES_ENABLED = new InheritableThreadLocal() { @Override protected Boolean initialValue() { @@ -96,6 +102,14 @@ public static void setLocalExtInputsArrayEnabled(boolean extInputsArrayEnabled) LOCAL_EXT_INPUTS_ARRAY_ENABLED.set(extInputsArrayEnabled); } + public static boolean isJsonPointerEnabled() { + return jsonPointerEnabled; + } + + public static void setJsonPointerEnabled(boolean jsonPointerEnabled) { + ProblemConfig.jsonPointerEnabled = jsonPointerEnabled; + } + public static void clearLocal() { LOCAL_EXT_ISSUE_TYPES_ENABLED.remove(); LOCAL_EXT_INPUTS_ARRAY_ENABLED.remove(); @@ -106,6 +120,7 @@ public static void reset() { stackTraceEnabled = DEFAULT_STACK_TRACE_ENABLED; extIssueTypesEnabled = DEFAULT_EXT_ISSUE_TYPES_ENABLED; extInputsArrayEnabled = DEFAULT_EXT_INPUTS_ARRAY_ENABLED; + jsonPointerEnabled = DEFAULT_JSON_POINTER_ENABLED; } } diff --git a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson2Util.java b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson2Util.java index 4ee30822..903e5635 100644 --- a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson2Util.java +++ b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson2Util.java @@ -13,6 +13,7 @@ import io.github.belgif.rest.problem.BadRequestProblem; import io.github.belgif.rest.problem.api.InEnum; import io.github.belgif.rest.problem.api.InputValidationIssues; +import io.github.belgif.rest.problem.config.ProblemConfig; /** * Internal jackson 2 utility class. @@ -59,17 +60,20 @@ public static BadRequestProblem toBadRequestProblem(JsonMappingException e) { } private static String getName(List path) { + String rootPrefix = ProblemConfig.isJsonPointerEnabled() ? "/" : ""; + String indexPrefix = ProblemConfig.isJsonPointerEnabled() ? "/" : "["; + String indexSuffix = ProblemConfig.isJsonPointerEnabled() ? "" : "]"; + String fieldNamePrefix = ProblemConfig.isJsonPointerEnabled() ? "/" : "."; + if (path.isEmpty()) { return null; } StringBuilder builder = new StringBuilder(); for (Reference reference : path) { if (reference.getFrom() instanceof List) { - builder.append("[").append(reference.getIndex()).append("]"); + builder.append(indexPrefix).append(reference.getIndex()).append(indexSuffix); } else { - if (builder.length() > 0) { - builder.append("."); - } + builder.append(builder.length() > 0 ? fieldNamePrefix : rootPrefix); builder.append(reference.getFieldName()); } } diff --git a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson3Util.java b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson3Util.java index b7942490..91796d00 100644 --- a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson3Util.java +++ b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson3Util.java @@ -7,6 +7,7 @@ import io.github.belgif.rest.problem.BadRequestProblem; import io.github.belgif.rest.problem.api.InEnum; import io.github.belgif.rest.problem.api.InputValidationIssues; +import io.github.belgif.rest.problem.config.ProblemConfig; import tools.jackson.core.JacksonException.Reference; import tools.jackson.core.exc.StreamReadException; import tools.jackson.databind.DatabindException; @@ -43,17 +44,20 @@ public static BadRequestProblem toBadRequestProblem(DatabindException e) { } private static String getName(List path) { + String rootPrefix = ProblemConfig.isJsonPointerEnabled() ? "/" : ""; + String indexPrefix = ProblemConfig.isJsonPointerEnabled() ? "/" : "["; + String indexSuffix = ProblemConfig.isJsonPointerEnabled() ? "" : "]"; + String fieldNamePrefix = ProblemConfig.isJsonPointerEnabled() ? "/" : "."; + if (path.isEmpty()) { return null; } StringBuilder name = new StringBuilder(); for (Reference reference : path) { if (reference.from() instanceof List) { - name.append("[").append(reference.getIndex()).append("]"); + name.append(indexPrefix).append(reference.getIndex()).append(indexSuffix); } else { - if (name.length() > 0) { - name.append("."); - } + name.append(name.length() > 0 ? fieldNamePrefix : rootPrefix); name.append(reference.getPropertyName()); } } diff --git a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/internal/Jackson2UtilTest.java b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/internal/Jackson2UtilTest.java index 5bd62a86..d41cd950 100644 --- a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/internal/Jackson2UtilTest.java +++ b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/internal/Jackson2UtilTest.java @@ -4,6 +4,7 @@ import java.util.List; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import com.fasterxml.jackson.annotation.JsonCreator; @@ -18,11 +19,33 @@ import io.github.belgif.rest.problem.BadRequestProblem; import io.github.belgif.rest.problem.api.InEnum; import io.github.belgif.rest.problem.api.InputValidationIssue; +import io.github.belgif.rest.problem.config.ProblemConfig; class Jackson2UtilTest { + @BeforeEach + void resetProblemConfif() { + ProblemConfig.reset(); + } + @Test void mismatchedInput() { + assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(() -> { + new ObjectMapper().readValue("{}", Model.class); + }).satisfies(e -> { + BadRequestProblem problem = Jackson2Util.toBadRequestProblem(e); + InputValidationIssue issue = problem.getIssues().get(0); + assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); + assertThat(issue.getIn()).isEqualTo(InEnum.BODY); + assertThat(issue.getName()).isEqualTo("/id"); + assertThat(issue.getValue()).isNull(); + assertThat(issue.getDetail()).isEqualTo("must not be null"); + }); + } + + @Test + void mismatchedInputWithJsonPointerDisabled() { + ProblemConfig.setJsonPointerEnabled(false); assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(() -> { new ObjectMapper().readValue("{}", Model.class); }).satisfies(e -> { @@ -45,7 +68,7 @@ void mismatchedInputType() { InputValidationIssue issue = problem.getIssues().get(0); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("nbr"); + assertThat(issue.getName()).isEqualTo("/nbr"); assertThat(issue.getValue()).isEqualTo("twenty-two"); assertThat(issue.getDetail()).isEqualTo("not a valid `int` value"); }); @@ -60,7 +83,7 @@ void valueInstantiationException() { InputValidationIssue issue = problem.getIssues().get(0); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("size"); + assertThat(issue.getName()).isEqualTo("/size"); assertThat(issue.getValue()).isNull(); assertThat(issue.getDetail()).isEqualTo("Unexpected value 'XXL'"); }); @@ -75,7 +98,7 @@ void invalidFormatException() { InputValidationIssue issue = problem.getIssues().get(0); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("size2"); + assertThat(issue.getName()).isEqualTo("/size2"); assertThat(issue.getValue()).isEqualTo("XXL"); assertThat(issue.getDetail()).isEqualTo("not one of the values accepted for enumeration: [S, L, M]"); }); @@ -90,7 +113,7 @@ void jsonParseExceptionWrappedJsonMappingException() { InputValidationIssue issue = problem.getIssues().get(0); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("model"); + assertThat(issue.getName()).isEqualTo("/model"); assertThat(issue.getValue()).isNull(); assertThat(issue.getDetail()).isEqualTo("JSON syntax error"); }); @@ -135,7 +158,7 @@ void mismatchedInputNested() { InputValidationIssue issue = problem.getIssues().get(0); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("model.id"); + assertThat(issue.getName()).isEqualTo("/model/id"); assertThat(issue.getValue()).isNull(); assertThat(issue.getDetail()).isEqualTo("must not be null"); }); @@ -143,6 +166,22 @@ void mismatchedInputNested() { @Test void mismatchedInputNestedWithArray() { + assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(() -> { + new ObjectMapper().readValue("{\"models\": [{}]}", NestedWithArray.class); + }).satisfies(e -> { + BadRequestProblem problem = Jackson2Util.toBadRequestProblem(e); + InputValidationIssue issue = problem.getIssues().get(0); + assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); + assertThat(issue.getIn()).isEqualTo(InEnum.BODY); + assertThat(issue.getName()).isEqualTo("/models/0/id"); + assertThat(issue.getValue()).isNull(); + assertThat(issue.getDetail()).isEqualTo("must not be null"); + }); + } + + @Test + void mismatchedInputNestedWithArrayWithJsonPointerDisabled() { + ProblemConfig.setJsonPointerEnabled(false); assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(() -> { new ObjectMapper().readValue("{\"models\": [{}]}", NestedWithArray.class); }).satisfies(e -> { @@ -165,7 +204,7 @@ void mismatchedInputFormatError() { InputValidationIssue issue = problem.getIssues().get(0); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("id"); + assertThat(issue.getName()).isEqualTo("/id"); assertThat(issue.getValue()).isEqualTo("one two three"); assertThat(issue.getDetail()).isEqualTo( "not a valid `int` value"); diff --git a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/internal/Jackson3UtilTest.java b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/internal/Jackson3UtilTest.java index 09cbf729..f93f8c9d 100644 --- a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/internal/Jackson3UtilTest.java +++ b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/internal/Jackson3UtilTest.java @@ -4,6 +4,7 @@ import java.util.List; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import com.fasterxml.jackson.annotation.JsonCreator; @@ -12,6 +13,7 @@ import io.github.belgif.rest.problem.BadRequestProblem; import io.github.belgif.rest.problem.api.InEnum; import io.github.belgif.rest.problem.api.InputValidationIssue; +import io.github.belgif.rest.problem.config.ProblemConfig; import tools.jackson.core.exc.StreamReadException; import tools.jackson.databind.DatabindException; import tools.jackson.databind.ObjectMapper; @@ -22,8 +24,29 @@ class Jackson3UtilTest { + @BeforeEach + void resetProblemConfif() { + ProblemConfig.reset(); + } + @Test void mismatchedInput() { + assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(() -> { + new JsonMapper().readValue("{}", Model.class); + }).satisfies(e -> { + BadRequestProblem problem = Jackson3Util.toBadRequestProblem(e); + InputValidationIssue issue = problem.getIssues().get(0); + assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); + assertThat(issue.getIn()).isEqualTo(InEnum.BODY); + assertThat(issue.getName()).isEqualTo("/id"); + assertThat(issue.getValue()).isNull(); + assertThat(issue.getDetail()).isEqualTo("must not be null"); + }); + } + + @Test + void mismatchedInputWithJsonPointerDisabled() { + ProblemConfig.setJsonPointerEnabled(false); assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(() -> { new JsonMapper().readValue("{}", Model.class); }).satisfies(e -> { @@ -46,7 +69,7 @@ void mismatchedInputType() { InputValidationIssue issue = problem.getIssues().get(0); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("nbr"); + assertThat(issue.getName()).isEqualTo("/nbr"); assertThat(issue.getValue()).isEqualTo("twenty-two"); assertThat(issue.getDetail()).isEqualTo("not a valid `int` value"); }); @@ -61,7 +84,7 @@ void valueInstantiationException() { InputValidationIssue issue = problem.getIssues().get(0); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("size"); + assertThat(issue.getName()).isEqualTo("/size"); assertThat(issue.getValue()).isNull(); assertThat(issue.getDetail()).isEqualTo("Unexpected value 'XXL'"); }); @@ -76,7 +99,7 @@ void invalidFormatException() { InputValidationIssue issue = problem.getIssues().get(0); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("size2"); + assertThat(issue.getName()).isEqualTo("/size2"); assertThat(issue.getValue()).isEqualTo("XXL"); assertThat(issue.getDetail()).isEqualTo("not one of the values accepted for enumeration: [S, L, M]"); }); @@ -91,7 +114,7 @@ void streamReadException() { InputValidationIssue issue = problem.getIssues().get(0); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("model"); + assertThat(issue.getName()).isEqualTo("/model"); assertThat(issue.getValue()).isNull(); assertThat(issue.getDetail()).isEqualTo("JSON syntax error"); }); @@ -136,7 +159,7 @@ void mismatchedInputNested() { InputValidationIssue issue = problem.getIssues().get(0); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("model.id"); + assertThat(issue.getName()).isEqualTo("/model/id"); assertThat(issue.getValue()).isNull(); assertThat(issue.getDetail()).isEqualTo("must not be null"); }); @@ -144,6 +167,22 @@ void mismatchedInputNested() { @Test void DatabindExceptionWithArray() { + assertThatExceptionOfType(DatabindException.class).isThrownBy(() -> { + new ObjectMapper().readValue("{\"models\": [{}]}", NestedWithArray.class); + }).satisfies(e -> { + BadRequestProblem problem = Jackson3Util.toBadRequestProblem(e); + InputValidationIssue issue = problem.getIssues().get(0); + assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); + assertThat(issue.getIn()).isEqualTo(InEnum.BODY); + assertThat(issue.getName()).isEqualTo("/models/0/id"); + assertThat(issue.getValue()).isNull(); + assertThat(issue.getDetail()).isEqualTo("must not be null"); + }); + } + + @Test + void DatabindExceptionWithArrayJsonPointerDisabled() { + ProblemConfig.setJsonPointerEnabled(false); assertThatExceptionOfType(DatabindException.class).isThrownBy(() -> { new ObjectMapper().readValue("{\"models\": [{}]}", NestedWithArray.class); }).satisfies(e -> { @@ -166,7 +205,7 @@ void mismatchedInputFormatError() { InputValidationIssue issue = problem.getIssues().get(0); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("id"); + assertThat(issue.getName()).isEqualTo("/id"); assertThat(issue.getValue()).isEqualTo("one two three"); assertThat(issue.getDetail()).isEqualTo("not a valid `int` value"); }); diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index baa784d8..0c174c05 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -822,6 +822,50 @@ Scan additional packages for @ProblemType annotations. This configuration property only applies to *Spring Boot*. On Jakarta EE and Quarkus, all problem types are automatically picked up by CDI. +[[io.github.belgif.rest.problem.json-pointer-enabled]] +==== io.github.belgif.rest.problem.json-pointer-enabled + +Enable using JsonPointer syntax for the 'name' property for a schema violation issue in the request body. +Default `true`. + +[source,json] +---- +{ + "title": "Bad Request", + "status": 400,
type: + "urn:problem-type:belgif:badRequest",href:"https://www.belgif.be/specification/rest/api-guide/problems/badRequest.html", + "issues": [ + { + "type": "urn:problem-type:belgif:input-validation:schemaViolation", + "in": "body", + "name": "person/0/ssin", + "detail": "An SSIN should be 11 digits long", + "value": "1234" + } + ] +} +---- + +If set to false, the JsonPath syntax is used. + +[source,json] +---- +{ + "title": "Bad Request", + "status": 400,
type: + "urn:problem-type:belgif:badRequest",href:"https://www.belgif.be/specification/rest/api-guide/problems/badRequest.html", + "issues": [ + { + "type": "urn:problem-type:belgif:input-validation:schemaViolation", + "in": "body", + "name": "person[0].ssin", + "detail": "An SSIN should be 11 digits long", + "value": "1234" + } + ] +} +---- + [[implementation-details]] == Implementation details diff --git a/src/main/asciidoc/release-notes.adoc b/src/main/asciidoc/release-notes.adoc index 48eedcfc..54427474 100644 --- a/src/main/asciidoc/release-notes.adoc +++ b/src/main/asciidoc/release-notes.adoc @@ -12,6 +12,12 @@ // tag::recent-versions[] +== Version 0.23 + +*belgif-rest-problem*: + +* Use <> by default for schema violation issues in request body. + == Version 0.22 *belgif-rest-problem*: From 279287b63b96119ca773f1e48f15451e37da4052 Mon Sep 17 00:00:00 2001 From: Sarah Kumwimba Date: Fri, 19 Jun 2026 10:37:47 +0200 Subject: [PATCH 2/9] Use JsonPointer syntax part 2 --- .../it/AbstractJacksonSerializationTest.java | 8 +- .../problem/it/AbstractRestProblemIT.java | 18 +- .../rest/problem/LocalDateConverter.java | 4 +- .../rest/problem/LocalDateConverter.java | 4 +- .../quarkus/it/LocalDateConverter.java | 4 +- .../internal/ConstraintViolationUtil.java | 19 +- .../internal/ConstraintViolationUtilTest.java | 60 ++++++- .../jaxrs/BadRequestExceptionMapperTest.java | 8 +- ...onstraintViolationExceptionMapperTest.java | 4 +- .../jaxrs/NotFoundExceptionMapperTest.java | 8 +- .../BeanValidationExceptionsHandler.java | 6 +- .../internal/BeanValidationExceptionUtil.java | 10 +- .../AbstractRoutingExceptionsHandlerTest.java | 4 +- .../BeanValidationExceptionsHandlerTest.java | 12 +- .../BeanValidationExceptionUtilTest.java | 44 +++++ .../validation/AbstractRequestValidator.java | 13 +- .../validation/EmployerIdValidatorTest.java | 10 +- .../EnterpriseNumberValidatorTest.java | 6 +- .../validation/EqualValidatorTest.java | 6 +- .../EstablishmentUnitNumberValidatorTest.java | 6 +- .../validation/ExactlyOneOfValidatorTest.java | 10 +- .../IncompleteDateValidatorTest.java | 24 +-- .../validation/PeriodValidatorTest.java | 16 +- .../RejectedInputValidatorTest.java | 6 +- .../validation/RequestValidatorTest.java | 165 ++++++++++-------- .../RequiredInputValidatorTest.java | 6 +- .../problem/validation/SsinValidatorTest.java | 6 +- .../validation/YearMonthValidatorTest.java | 6 +- .../validation/ZeroOrAllOfValidatorTest.java | 8 +- .../ZeroOrExactlyOneOfValidatorTest.java | 8 +- .../github/belgif/rest/problem/api/Input.java | 3 + .../problem/api/InputValidationIssue.java | 68 ++++++++ .../problem/api/InputValidationIssues.java | 4 +- .../rest/problem/internal/Jackson2Util.java | 19 +- .../rest/problem/internal/Jackson3Util.java | 21 ++- .../belgif/rest/problem/api/InputTest.java | 18 +- .../problem/api/InputValidationIssueTest.java | 131 ++++++++++++++ .../api/InputValidationIssuesTest.java | 131 +++++++------- src/main/asciidoc/index.adoc | 2 +- 39 files changed, 640 insertions(+), 266 deletions(-) diff --git a/belgif-rest-problem-it/belgif-rest-problem-it-common/src/main/java/io/github/belgif/rest/problem/it/AbstractJacksonSerializationTest.java b/belgif-rest-problem-it/belgif-rest-problem-it-common/src/main/java/io/github/belgif/rest/problem/it/AbstractJacksonSerializationTest.java index f6cb5c51..f2dc1015 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-it-common/src/main/java/io/github/belgif/rest/problem/it/AbstractJacksonSerializationTest.java +++ b/belgif-rest-problem-it/belgif-rest-problem-it-common/src/main/java/io/github/belgif/rest/problem/it/AbstractJacksonSerializationTest.java @@ -80,7 +80,7 @@ void retryAfterProblem() throws IOException { @Test void badRequestProblemReplacedSsin() throws IOException { BadRequestProblem problem = new BadRequestProblem( - InputValidationIssues.replacedSsin(InEnum.BODY, "parent[1].ssin", "12345678901", "23456789012")); + InputValidationIssues.replacedSsin(InEnum.BODY, "/parent/1/ssin", "12345678901", "23456789012")); assertSerializationRoundtrip(problem); } @@ -182,7 +182,7 @@ void legacyInvalidParamProblem() throws IOException { + " \"detail\": \"The input message is incorrect\",\n" + " \"invalidParams\": [ {\n" + " \"in\": \"body\",\n" - + " \"name\": \"sector\",\n" + + " \"name\": \"/sector\",\n" + " \"reason\": \"must be less than or equal to 999\",\n" + " \"value\": 9999,\n" + " \"issueType\": \"schemaViolation\"\n" @@ -239,7 +239,7 @@ void issueWithStatusAndInstance() throws IOException { @Test void issueWithNullValue() throws IOException { BadRequestProblem problem = new BadRequestProblem( - new InputValidationIssue(InEnum.BODY, "id", null)); + new InputValidationIssue(InEnum.BODY, "/id", null)); String json = writeProblem(problem); assertThat(json).doesNotContain("null"); assertSerializationRoundtrip(problem); @@ -249,7 +249,7 @@ void issueWithNullValue() throws IOException { void issueWithNullInputValue() throws IOException { ProblemConfig.setExtInputsArrayEnabled(true); BadRequestProblem problem = new BadRequestProblem(new InputValidationIssue() - .inputs(Input.body("a", null), Input.body("b", null))); + .inputs(Input.body("/a", null), Input.body("/b", null))); String json = writeProblem(problem); assertThat(json).doesNotContain("null"); assertSerializationRoundtrip(problem); diff --git a/belgif-rest-problem-it/belgif-rest-problem-it-common/src/main/java/io/github/belgif/rest/problem/it/AbstractRestProblemIT.java b/belgif-rest-problem-it/belgif-rest-problem-it-common/src/main/java/io/github/belgif/rest/problem/it/AbstractRestProblemIT.java index f30aae1f..ff2d22a3 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-it-common/src/main/java/io/github/belgif/rest/problem/it/AbstractRestProblemIT.java +++ b/belgif-rest-problem-it/belgif-rest-problem-it-common/src/main/java/io/github/belgif/rest/problem/it/AbstractRestProblemIT.java @@ -332,11 +332,11 @@ public void constraintViolationBody() { .statusCode(400) .body("type", equalTo("urn:problem-type:belgif:badRequest")) .body("issues[0].in", equalTo("body")) - .body("issues[0].name", equalTo("email")) + .body("issues[0].name", equalTo("/email")) .body("issues[0].value", equalTo("mymail.com")) .body("issues[0].detail", equalTo("must be a well-formed email address")) .body("issues[1].in", equalTo("body")) - .body("issues[1].name", equalTo("name")) + .body("issues[1].name", equalTo("/name")) .body("issues[1].value", nullValue()) .body("issues[1].detail", equalTo("must not be blank")); } @@ -352,11 +352,11 @@ public void constraintViolationBodyNested() { .statusCode(400) .body("type", equalTo("urn:problem-type:belgif:badRequest")) .body("issues[0].in", equalTo("body")) - .body("issues[0].name", equalTo("nested.email")) + .body("issues[0].name", equalTo("/nested/email")) .body("issues[0].value", equalTo("mymail.com")) .body("issues[0].detail", equalTo("must be a well-formed email address")) .body("issues[1].in", equalTo("body")) - .body("issues[1].name", equalTo("nested.name")) + .body("issues[1].name", equalTo("/nested/name")) .body("issues[1].value", nullValue()) .body("issues[1].detail", equalTo("must not be blank")); } @@ -372,11 +372,11 @@ public void constraintViolationBodyInheritance() { .statusCode(400) .body("type", equalTo("urn:problem-type:belgif:badRequest")) .body("issues[0].in", equalTo("body")) - .body("issues[0].name", equalTo("email")) + .body("issues[0].name", equalTo("/email")) .body("issues[0].value", equalTo("mymail.com")) .body("issues[0].detail", equalTo("must be a well-formed email address")) .body("issues[1].in", equalTo("body")) - .body("issues[1].name", equalTo("name")) + .body("issues[1].name", equalTo("/name")) .body("issues[1].value", nullValue()) .body("issues[1].detail", equalTo("must not be blank")); } @@ -389,7 +389,7 @@ public void jacksonMismatchedInputException() { .statusCode(400) .body("type", equalTo("urn:problem-type:belgif:badRequest")) .body("issues[0].in", equalTo("body")) - .body("issues[0].name", equalTo("id")) + .body("issues[0].name", equalTo("/id")) .body("issues[0].detail", equalTo("must not be null")); } @@ -542,7 +542,7 @@ public void invalidJsonNested() { .body("issues[0].title", equalTo("Input value is invalid with respect to the schema")) .body("issues[0].detail", equalTo("JSON syntax error")) .body("issues[0].in", equalTo("body")) - .body("issues[0].name", equalTo("nested")); + .body("issues[0].name", equalTo("/nested")); } @Test @@ -558,7 +558,7 @@ public void invalidJsonType() { .body("issues[0].title", equalTo("Input value is invalid with respect to the schema")) .body("issues[0].detail", equalTo("not a valid `int` value")) .body("issues[0].in", equalTo("body")) - .body("issues[0].name", equalTo("age")) + .body("issues[0].name", equalTo("/age")) .body("issues[0].value", equalTo("twenty-two")); } diff --git a/belgif-rest-problem-it/belgif-rest-problem-jakarta-ee-it/src/main/java/io/github/belgif/rest/problem/LocalDateConverter.java b/belgif-rest-problem-it/belgif-rest-problem-jakarta-ee-it/src/main/java/io/github/belgif/rest/problem/LocalDateConverter.java index ca9a72da..be62376c 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-jakarta-ee-it/src/main/java/io/github/belgif/rest/problem/LocalDateConverter.java +++ b/belgif-rest-problem-it/belgif-rest-problem-jakarta-ee-it/src/main/java/io/github/belgif/rest/problem/LocalDateConverter.java @@ -10,6 +10,7 @@ import jakarta.ws.rs.ext.Provider; import io.github.belgif.rest.problem.api.InEnum; +import io.github.belgif.rest.problem.api.InputValidationIssue; import io.github.belgif.rest.problem.ee.server.jaxrs.AbstractInputParamConverterProvider; @Provider @@ -31,7 +32,8 @@ protected LocalDate fromString(InEnum in, String name, String value) { try { return LocalDate.parse(value, LOCAL_DATE_FORMATTER); } catch (DateTimeParseException e) { - throw new BadRequestProblem(schemaViolation(in, name, value, "date has invalid format")); + throw new BadRequestProblem( + schemaViolation(in, InputValidationIssue.convertName(in, name), value, "date has invalid format")); } } diff --git a/belgif-rest-problem-it/belgif-rest-problem-java-ee-it/src/main/java/io/github/belgif/rest/problem/LocalDateConverter.java b/belgif-rest-problem-it/belgif-rest-problem-java-ee-it/src/main/java/io/github/belgif/rest/problem/LocalDateConverter.java index dafb618e..bc903393 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-java-ee-it/src/main/java/io/github/belgif/rest/problem/LocalDateConverter.java +++ b/belgif-rest-problem-it/belgif-rest-problem-java-ee-it/src/main/java/io/github/belgif/rest/problem/LocalDateConverter.java @@ -10,6 +10,7 @@ import javax.ws.rs.ext.Provider; import io.github.belgif.rest.problem.api.InEnum; +import io.github.belgif.rest.problem.api.InputValidationIssue; import io.github.belgif.rest.problem.ee.server.jaxrs.AbstractInputParamConverterProvider; @Provider @@ -31,7 +32,8 @@ protected LocalDate fromString(InEnum in, String name, String value) { try { return LocalDate.parse(value, LOCAL_DATE_FORMATTER); } catch (DateTimeParseException e) { - throw new BadRequestProblem(schemaViolation(in, name, value, "date has invalid format")); + throw new BadRequestProblem( + schemaViolation(in, InputValidationIssue.convertName(in, name), value, "date has invalid format")); } } diff --git a/belgif-rest-problem-it/belgif-rest-problem-quarkus-it/src/main/java/io/github/belgif/rest/problem/quarkus/it/LocalDateConverter.java b/belgif-rest-problem-it/belgif-rest-problem-quarkus-it/src/main/java/io/github/belgif/rest/problem/quarkus/it/LocalDateConverter.java index 3b56b2fe..3e8ecacd 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-quarkus-it/src/main/java/io/github/belgif/rest/problem/quarkus/it/LocalDateConverter.java +++ b/belgif-rest-problem-it/belgif-rest-problem-quarkus-it/src/main/java/io/github/belgif/rest/problem/quarkus/it/LocalDateConverter.java @@ -11,6 +11,7 @@ import io.github.belgif.rest.problem.BadRequestProblem; import io.github.belgif.rest.problem.api.InEnum; +import io.github.belgif.rest.problem.api.InputValidationIssue; import io.github.belgif.rest.problem.ee.server.jaxrs.AbstractInputParamConverterProvider; @Provider @@ -32,7 +33,8 @@ protected LocalDate fromString(InEnum in, String name, String value) { try { return LocalDate.parse(value, LOCAL_DATE_FORMATTER); } catch (DateTimeParseException e) { - throw new BadRequestProblem(schemaViolation(in, name, value, "date has invalid format")); + throw new BadRequestProblem( + schemaViolation(in, InputValidationIssue.convertName(in, name), value, "date has invalid format")); } } diff --git a/belgif-rest-problem-java-ee-server/src/main/java/io/github/belgif/rest/problem/ee/server/internal/ConstraintViolationUtil.java b/belgif-rest-problem-java-ee-server/src/main/java/io/github/belgif/rest/problem/ee/server/internal/ConstraintViolationUtil.java index 362eec01..e8436a02 100644 --- a/belgif-rest-problem-java-ee-server/src/main/java/io/github/belgif/rest/problem/ee/server/internal/ConstraintViolationUtil.java +++ b/belgif-rest-problem-java-ee-server/src/main/java/io/github/belgif/rest/problem/ee/server/internal/ConstraintViolationUtil.java @@ -15,10 +15,13 @@ import javax.validation.Path.ParameterNode; import javax.ws.rs.BeanParam; +import com.fasterxml.jackson.core.JsonPointer; + import io.github.belgif.rest.problem.api.InEnum; import io.github.belgif.rest.problem.api.Input; import io.github.belgif.rest.problem.api.InputValidationIssue; import io.github.belgif.rest.problem.api.InputValidationIssues; +import io.github.belgif.rest.problem.config.ProblemConfig; import io.github.belgif.rest.problem.internal.AnnotationUtil; /** @@ -59,7 +62,10 @@ public static InputValidationIssue convertToInputValidationIssue(ConstraintViola private static Input determineInput(ConstraintViolation violation, MethodNode methodNode, List propertyPath, List propertyName) { - Input input = Input.body(String.join(".", propertyName), violation.getInvalidValue()); + + Input input = + Input.body(InputValidationIssue.getNameFromProperties(InEnum.BODY, propertyName), + violation.getInvalidValue()); Node last = propertyPath.get(propertyPath.size() - 1); Node parent = propertyPath.size() > 1 ? propertyPath.get(propertyPath.size() - 2) : null; if (last.getKind() == ElementKind.PARAMETER) { @@ -91,7 +97,8 @@ private static Input determineInput(ConstraintViolation violation, InEnum in = ParameterSourceMapper.map(annotation.annotationType()); if (in != null) { input.setIn(in); - input.setName((String) annotation.annotationType().getMethod("value").invoke(annotation)); + input.setName(InputValidationIssue.convertName(in, + (String) annotation.annotationType().getMethod("value").invoke(annotation))); } } } catch (NoSuchFieldException e) { @@ -101,6 +108,14 @@ private static Input determineInput(ConstraintViolation violation, } } } + + if (ProblemConfig.isJsonPointerEnabled() && input.getName() != null && input.getIn() != InEnum.BODY + && input.getName().charAt(0) == JsonPointer.SEPARATOR) { + // remove the '/' created with InputValidationIssue.getNameFromProperties used at the beginning of this + // method + input.setName(input.getName().substring(1)); + } + return input; } diff --git a/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/internal/ConstraintViolationUtilTest.java b/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/internal/ConstraintViolationUtilTest.java index 406a5368..29343b52 100644 --- a/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/internal/ConstraintViolationUtilTest.java +++ b/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/internal/ConstraintViolationUtilTest.java @@ -24,15 +24,22 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import io.github.belgif.rest.problem.api.InEnum; import io.github.belgif.rest.problem.api.InputValidationIssue; +import io.github.belgif.rest.problem.config.ProblemConfig; class ConstraintViolationUtilTest { private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + @BeforeEach + void resetProblemConfig() { + ProblemConfig.reset(); + } + @Test void missingRequiredBody() throws Exception { Set> violations = @@ -60,6 +67,27 @@ void bodyProperty() throws Exception { assertThat(violations).hasSize(1); + InputValidationIssue issue = + ConstraintViolationUtil.convertToInputValidationIssue(violations.iterator().next()); + assertThat(issue.getIn()).isEqualTo(InEnum.BODY); + assertThat(issue.getName()).isEqualTo("/value"); + assertThat(issue.getValue()).isEqualTo(10); + assertThat(issue.getDetail()).isEqualTo("must be less than or equal to 5"); + } + + @Test + void bodyPropertyWithJsonPointerDisabled() throws Exception { + ProblemConfig.setJsonPointerEnabled(false); + + Body target = new Body(); + target.value = 10; + + Set> violations = + validator.forExecutables().validateParameters(new Resource(), + Resource.class.getMethod("bodyParam", Body.class), new Object[] { target }); + + assertThat(violations).hasSize(1); + InputValidationIssue issue = ConstraintViolationUtil.convertToInputValidationIssue(violations.iterator().next()); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); @@ -80,6 +108,28 @@ void nestedBodyProperty() throws Exception { assertThat(violations).hasSize(1); + InputValidationIssue issue = + ConstraintViolationUtil.convertToInputValidationIssue(violations.iterator().next()); + assertThat(issue.getIn()).isEqualTo(InEnum.BODY); + assertThat(issue.getName()).isEqualTo("/nested/1/prop"); + assertThat(issue.getValue()).isNull(); + assertThat(issue.getDetail()).isEqualTo("must not be null"); + } + + @Test + void nestedBodyPropertyWithJsonPointerDisabled() throws Exception { + ProblemConfig.setJsonPointerEnabled(false); + + Body target = new Body(); + target.nested.add(new Nested("OK")); + target.nested.add(new Nested(null)); + + Set> violations = + validator.forExecutables().validateParameters(new Resource(), + Resource.class.getMethod("bodyParam", Body.class), new Object[] { target }); + + assertThat(violations).hasSize(1); + InputValidationIssue issue = ConstraintViolationUtil.convertToInputValidationIssue(violations.iterator().next()); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); @@ -163,7 +213,7 @@ void formParam() throws Exception { InputValidationIssue issue = ConstraintViolationUtil.convertToInputValidationIssue(violations.iterator().next()); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("form"); + assertThat(issue.getName()).isEqualTo("/form"); assertThat(issue.getValue()).isEqualTo(10); assertThat(issue.getDetail()).isEqualTo("must be less than or equal to 5"); } @@ -228,10 +278,10 @@ void beanParam() throws Exception { violations.stream().map(ConstraintViolationUtil::convertToInputValidationIssue) .sorted(Comparator.comparing(InputValidationIssue::getName)).collect(Collectors.toList()); - assertThat(issues.get(0).getIn()).isEqualTo(InEnum.HEADER); - assertThat(issues.get(0).getName()).isEqualTo("cookie"); - assertThat(issues.get(1).getIn()).isEqualTo(InEnum.BODY); - assertThat(issues.get(1).getName()).isEqualTo("form"); + assertThat(issues.get(0).getIn()).isEqualTo(InEnum.BODY); + assertThat(issues.get(0).getName()).isEqualTo("/form"); + assertThat(issues.get(1).getIn()).isEqualTo(InEnum.HEADER); + assertThat(issues.get(1).getName()).isEqualTo("cookie"); assertThat(issues.get(2).getIn()).isEqualTo(InEnum.HEADER); assertThat(issues.get(2).getName()).isEqualTo("header"); assertThat(issues.get(3).getIn()).isEqualTo(InEnum.PATH); diff --git a/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/jaxrs/BadRequestExceptionMapperTest.java b/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/jaxrs/BadRequestExceptionMapperTest.java index edb8d41d..1cf626b8 100644 --- a/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/jaxrs/BadRequestExceptionMapperTest.java +++ b/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/jaxrs/BadRequestExceptionMapperTest.java @@ -25,14 +25,14 @@ void badRequestException() { @Test void badRequestProblem() { BadRequestProblem cause = new BadRequestProblem( - InputValidationIssues.schemaViolation(InEnum.HEADER, "startDate_gt", "2006-087-01", + InputValidationIssues.schemaViolation(InEnum.HEADER, "startDateGt", "2006-087-01", "date has invalid format")); Response response = mapper.toResponse(new BadRequestException("HTTP 400 Bad Request", cause)); assertThat(response.getEntity()).isInstanceOf(BadRequestProblem.class); BadRequestProblem problem = (BadRequestProblem) response.getEntity(); InputValidationIssue issue = problem.getIssues().get(0); assertThat(issue.getIn()).isEqualTo(InEnum.HEADER); - assertThat(issue.getName()).isEqualTo("startDate_gt"); + assertThat(issue.getName()).isEqualTo("startDateGt"); } @Test @@ -41,13 +41,13 @@ void badRequestProblemEnrichFromMessage() { InputValidationIssues.schemaViolation(null, null, "2006-087-01", "date has invalid format")); Response response = mapper.toResponse(new BadRequestException( "RESTEASY003870: Unable to extract parameter from http request: " - + "jakarta.ws.rs.HeaderParam(\"startDate_gt\") value is '2006-087-01'", + + "jakarta.ws.rs.HeaderParam(\"startDateGt\") value is '2006-087-01'", cause)); assertThat(response.getEntity()).isInstanceOf(BadRequestProblem.class); BadRequestProblem problem = (BadRequestProblem) response.getEntity(); InputValidationIssue issue = problem.getIssues().get(0); assertThat(issue.getIn()).isEqualTo(InEnum.HEADER); - assertThat(issue.getName()).isEqualTo("startDate_gt"); + assertThat(issue.getName()).isEqualTo("startDateGt"); } @Test diff --git a/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/jaxrs/ConstraintViolationExceptionMapperTest.java b/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/jaxrs/ConstraintViolationExceptionMapperTest.java index 54cd236f..f5da36db 100644 --- a/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/jaxrs/ConstraintViolationExceptionMapperTest.java +++ b/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/jaxrs/ConstraintViolationExceptionMapperTest.java @@ -52,8 +52,8 @@ void sortsIssuesByName() { assertThat(response.getStatus()).isEqualTo(400); assertThat(response.getEntity()).isInstanceOf(BadRequestProblem.class); BadRequestProblem problem = (BadRequestProblem) response.getEntity(); - assertThat(problem.getIssues().get(0).getName()).isEqualTo("first"); - assertThat(problem.getIssues().get(1).getName()).isEqualTo("second"); + assertThat(problem.getIssues().get(0).getName()).isEqualTo("/first"); + assertThat(problem.getIssues().get(1).getName()).isEqualTo("/second"); } } diff --git a/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/jaxrs/NotFoundExceptionMapperTest.java b/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/jaxrs/NotFoundExceptionMapperTest.java index 88084694..80b1cc37 100644 --- a/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/jaxrs/NotFoundExceptionMapperTest.java +++ b/belgif-rest-problem-java-ee-server/src/test/java/io/github/belgif/rest/problem/ee/server/jaxrs/NotFoundExceptionMapperTest.java @@ -26,14 +26,14 @@ void notFoundException() { @Test void badRequestProblem() { BadRequestProblem cause = new BadRequestProblem( - InputValidationIssues.schemaViolation(InEnum.QUERY, "startDate_gt", "2006-087-01", + InputValidationIssues.schemaViolation(InEnum.QUERY, "startDateGt", "2006-087-01", "date has invalid format")); Response response = mapper.toResponse(new NotFoundException("HTTP 404 Not Found", cause)); assertThat(response.getEntity()).isInstanceOf(BadRequestProblem.class); BadRequestProblem problem = (BadRequestProblem) response.getEntity(); InputValidationIssue issue = problem.getIssues().get(0); assertThat(issue.getIn()).isEqualTo(InEnum.QUERY); - assertThat(issue.getName()).isEqualTo("startDate_gt"); + assertThat(issue.getName()).isEqualTo("startDateGt"); } @Test @@ -42,13 +42,13 @@ void badRequestProblemEnrichFromMessage() { InputValidationIssues.schemaViolation(null, null, "2006-087-01", "date has invalid format")); Response response = mapper.toResponse(new NotFoundException( "RESTEASY003870: Unable to extract parameter from http request: " - + "jakarta.ws.rs.QueryParam(\"startDate_gt\") value is '2006-087-01'", + + "jakarta.ws.rs.QueryParam(\"startDateGt\") value is '2006-087-01'", cause)); assertThat(response.getEntity()).isInstanceOf(BadRequestProblem.class); BadRequestProblem problem = (BadRequestProblem) response.getEntity(); InputValidationIssue issue = problem.getIssues().get(0); assertThat(issue.getIn()).isEqualTo(InEnum.QUERY); - assertThat(issue.getName()).isEqualTo("startDate_gt"); + assertThat(issue.getName()).isEqualTo("startDateGt"); } @Test diff --git a/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/server/BeanValidationExceptionsHandler.java b/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/server/BeanValidationExceptionsHandler.java index 9dd5ab7e..deda8280 100644 --- a/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/server/BeanValidationExceptionsHandler.java +++ b/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/server/BeanValidationExceptionsHandler.java @@ -120,7 +120,8 @@ public void modelAttribute(@Nullable ModelAttribute modelAttribute, ParameterErr if (modelAttribute != null) { errors.getResolvableErrors().forEach(error -> issues.add( InputValidationIssues.schemaViolation( - InEnum.BODY, modelAttribute.value(), errors.getArgument(), + InEnum.BODY, InputValidationIssue.convertName(InEnum.BODY, modelAttribute.value()), + errors.getArgument(), error.getDefaultMessage()))); } } @@ -169,7 +170,8 @@ public void requestParam(@Nullable RequestParam requestParam, ParameterValidatio public void requestPart(RequestPart requestPart, ParameterErrors errors) { errors.getResolvableErrors().forEach(error -> issues.add( InputValidationIssues.schemaViolation( - InEnum.BODY, requestPart.value(), errors.getArgument(), + InEnum.BODY, InputValidationIssue.convertName(InEnum.BODY, requestPart.value()), + errors.getArgument(), error.getDefaultMessage()))); } diff --git a/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/server/internal/BeanValidationExceptionUtil.java b/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/server/internal/BeanValidationExceptionUtil.java index a76fee7c..f16c50ad 100644 --- a/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/server/internal/BeanValidationExceptionUtil.java +++ b/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/server/internal/BeanValidationExceptionUtil.java @@ -3,7 +3,6 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.Objects; -import java.util.stream.Collectors; import jakarta.validation.ConstraintViolation; import jakarta.validation.ElementKind; @@ -45,13 +44,18 @@ public static InputValidationIssue convertToInputValidationIssue(ConstraintViola } } InEnum in = DetermineSourceUtil.determineSource(violation, propertyPath, methodNode); - String name = propertyPath.stream().map(Node::toString).collect(Collectors.joining(".")); + + String name = + InputValidationIssue.getNameFromProperties(in, propertyPath.stream().map(Node::toString).toList()); + return InputValidationIssues.schemaViolation(in, name, violation.getInvalidValue(), violation.getMessage()); } public static InputValidationIssue convertToInputValidationIssue(@NotNull FieldError fieldError, InEnum in) { String invalidValue = Objects.toString(fieldError.getRejectedValue(), null); - return InputValidationIssues.schemaViolation(in, fieldError.getField(), invalidValue, + String name = fieldError.getField(); + + return InputValidationIssues.schemaViolation(in, InputValidationIssue.convertName(in, name), invalidValue, fieldError.getDefaultMessage()); } diff --git a/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/server/AbstractRoutingExceptionsHandlerTest.java b/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/server/AbstractRoutingExceptionsHandlerTest.java index 62b14df6..4963b8f0 100644 --- a/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/server/AbstractRoutingExceptionsHandlerTest.java +++ b/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/server/AbstractRoutingExceptionsHandlerTest.java @@ -62,7 +62,7 @@ void handleMissingServletRequestParameterException() { assertThat(problem.getIssues()).hasSize(1); assertThat(problem.getIssues().get(0).getType()).isEqualTo(ISSUE_TYPE_SCHEMA_VIOLATION); assertThat(problem.getIssues().get(0).getIn()).isEqualTo(InEnum.QUERY); - assertThat(problem.getIssues().get(0).getName()).isEqualTo("/name"); + assertThat(problem.getIssues().get(0).getName()).isEqualTo("name"); assertThat(problem.getIssues().get(0).getDetail()).isEqualTo( "Required request parameter 'name' for method parameter type String is not present"); }); @@ -79,7 +79,7 @@ void handleMissingRequestHeaderException() throws Exception { assertThat(problem.getIssues()).hasSize(1); assertThat(problem.getIssues().get(0).getType()).isEqualTo(ISSUE_TYPE_SCHEMA_VIOLATION); assertThat(problem.getIssues().get(0).getIn()).isEqualTo(InEnum.HEADER); - assertThat(problem.getIssues().get(0).getName()).isEqualTo("/name"); + assertThat(problem.getIssues().get(0).getName()).isEqualTo("name"); assertThat(problem.getIssues().get(0).getDetail()).isEqualTo( "Required request header 'name' for method parameter type Object is not present"); }); diff --git a/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/server/BeanValidationExceptionsHandlerTest.java b/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/server/BeanValidationExceptionsHandlerTest.java index 96ec2b49..8b86e232 100644 --- a/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/server/BeanValidationExceptionsHandlerTest.java +++ b/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/server/BeanValidationExceptionsHandlerTest.java @@ -65,8 +65,8 @@ void handleConstraintViolationException() { assertThat(entity.getHeaders().getContentType()).isEqualTo(ProblemMediaType.INSTANCE); assertThat(entity.getBody()).isInstanceOfSatisfying(BadRequestProblem.class, problem -> { assertThat(problem.getIssues()).hasSize(2); - assertThat(problem.getIssues().get(0).getName()).isEqualTo("first"); - assertThat(problem.getIssues().get(1).getName()).isEqualTo("second"); + assertThat(problem.getIssues().get(0).getName()).isEqualTo("/first"); + assertThat(problem.getIssues().get(1).getName()).isEqualTo("/second"); }); } @@ -87,11 +87,11 @@ void handleMethodArgumentNotValidException() throws Exception { assertThat(entity.getBody()).isInstanceOfSatisfying(BadRequestProblem.class, problem -> { assertThat(problem.getIssues()).hasSize(2); assertThat(problem.getIssues().get(0).getIn()).isEqualTo(InEnum.BODY); - assertThat(problem.getIssues().get(0).getName()).isEqualTo("first"); + assertThat(problem.getIssues().get(0).getName()).isEqualTo("/first"); assertThat(problem.getIssues().get(0).getValue()).isEqualTo("firstValue"); assertThat(problem.getIssues().get(0).getDetail()).isEqualTo("firstDetail"); assertThat(problem.getIssues().get(1).getIn()).isEqualTo(InEnum.BODY); - assertThat(problem.getIssues().get(1).getName()).isEqualTo("second"); + assertThat(problem.getIssues().get(1).getName()).isEqualTo("/second"); assertThat(problem.getIssues().get(1).getValue()).isEqualTo("secondValue"); assertThat(problem.getIssues().get(1).getDetail()).isEqualTo("secondDetail"); }); @@ -244,7 +244,7 @@ void handlerMethodValidationExceptionVisitorModelAttribute() { assertThat(issues).hasSize(1); assertThat(issues.get(0).getType()).isEqualTo(InputValidationIssues.ISSUE_TYPE_SCHEMA_VIOLATION); assertThat(issues.get(0).getIn()).isEqualTo(InEnum.BODY); - assertThat(issues.get(0).getName()).isEqualTo("name"); + assertThat(issues.get(0).getName()).isEqualTo("/name"); assertThat(issues.get(0).getValue()).isEqualTo("value"); assertThat(issues.get(0).getDetail()).isEqualTo("message"); } @@ -374,7 +374,7 @@ void handlerMethodValidationExceptionVisitorRequestPart() { assertThat(issues).hasSize(1); assertThat(issues.get(0).getType()).isEqualTo(InputValidationIssues.ISSUE_TYPE_SCHEMA_VIOLATION); assertThat(issues.get(0).getIn()).isEqualTo(InEnum.BODY); - assertThat(issues.get(0).getName()).isEqualTo("name"); + assertThat(issues.get(0).getName()).isEqualTo("/name"); assertThat(issues.get(0).getValue()).isEqualTo("value"); assertThat(issues.get(0).getDetail()).isEqualTo("message"); } diff --git a/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/server/internal/BeanValidationExceptionUtilTest.java b/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/server/internal/BeanValidationExceptionUtilTest.java index 7ec42ec8..c6f51567 100644 --- a/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/server/internal/BeanValidationExceptionUtilTest.java +++ b/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/server/internal/BeanValidationExceptionUtilTest.java @@ -14,6 +14,7 @@ import jakarta.validation.constraints.NotNull; import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.validation.FieldError; import org.springframework.web.bind.annotation.PathVariable; @@ -23,6 +24,7 @@ import io.github.belgif.rest.problem.api.InEnum; import io.github.belgif.rest.problem.api.InputValidationIssue; import io.github.belgif.rest.problem.api.InputValidationIssues; +import io.github.belgif.rest.problem.config.ProblemConfig; class BeanValidationExceptionUtilTest { @@ -33,6 +35,11 @@ class BeanValidationExceptionUtilTest { .buildValidatorFactory() .getValidator(); + @BeforeEach + void resetProblemConfig() { + ProblemConfig.reset(); + } + @Test void constraintViolationBodyProperty() { Body target = new Body(); @@ -41,6 +48,24 @@ void constraintViolationBodyProperty() { Set> violations = validator.validate(target); assertThat(violations).hasSize(1); + InputValidationIssue issue = + BeanValidationExceptionUtil.convertToInputValidationIssue(violations.iterator().next()); + assertThat(issue.getIn()).isEqualTo(InEnum.BODY); + assertThat(issue.getName()).isEqualTo("/value"); + assertThat(issue.getValue()).isEqualTo(10); + assertThat(issue.getDetail()).isEqualTo("must be less than or equal to 5"); + } + + @Test + void constraintViolationBodyPropertyWithJsonPointerDisabled() { + ProblemConfig.setJsonPointerEnabled(false); + + Body target = new Body(); + target.value = 10; + + Set> violations = validator.validate(target); + assertThat(violations).hasSize(1); + InputValidationIssue issue = BeanValidationExceptionUtil.convertToInputValidationIssue(violations.iterator().next()); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); @@ -58,6 +83,25 @@ void constraintViolationNestedBodyProperty() { Set> violations = validator.validate(target); assertThat(violations).hasSize(1); + InputValidationIssue issue = + BeanValidationExceptionUtil.convertToInputValidationIssue(violations.iterator().next()); + assertThat(issue.getIn()).isEqualTo(InEnum.BODY); + assertThat(issue.getName()).isEqualTo("/nested/1/prop"); + assertThat(issue.getValue()).isNull(); + assertThat(issue.getDetail()).isEqualTo("must not be null"); + } + + @Test + void constraintViolationNestedBodyPropertyWithJsonPointerDisabled() { + ProblemConfig.setJsonPointerEnabled(false); + + Body target = new Body(); + target.nested.add(new Nested("OK")); + target.nested.add(new Nested(null)); + + Set> violations = validator.validate(target); + assertThat(violations).hasSize(1); + InputValidationIssue issue = BeanValidationExceptionUtil.convertToInputValidationIssue(violations.iterator().next()); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); diff --git a/belgif-rest-problem-validator/src/main/java/io/github/belgif/rest/problem/validation/AbstractRequestValidator.java b/belgif-rest-problem-validator/src/main/java/io/github/belgif/rest/problem/validation/AbstractRequestValidator.java index 4db9fb0b..7806ef9c 100644 --- a/belgif-rest-problem-validator/src/main/java/io/github/belgif/rest/problem/validation/AbstractRequestValidator.java +++ b/belgif-rest-problem-validator/src/main/java/io/github/belgif/rest/problem/validation/AbstractRequestValidator.java @@ -116,7 +116,8 @@ public SELF ssins(Input> ssins) { if (ssins != null && ssins.getValue() != null) { int index = 0; for (String ssin : ssins.getValue()) { - ssin(new Input<>(ssins.getIn(), ssins.getName() + "[" + index + "]", ssin)); + String indexFormat = getIndexFormat(index); + ssin(new Input<>(ssins.getIn(), ssins.getName() + indexFormat, ssin)); index++; } } @@ -359,7 +360,8 @@ public SELF refDatas(Input> input, Supplier> allowedRe Collection allowedRefData = allowedRefDataSupplier.get(); int index = 0; for (T value : input.getValue()) { - refData(new Input(input.getIn(), input.getName() + "[" + index + "]", value), allowedRefData); + String indexFormat = getIndexFormat(index); + refData(new Input(input.getIn(), input.getName() + indexFormat, value), allowedRefData); index++; } } @@ -378,7 +380,8 @@ public SELF refDatas(Input> input, Predicate allowedRefDataPredic if (input != null && input.getValue() != null && !input.getValue().isEmpty()) { int index = 0; for (T value : input.getValue()) { - refData(new Input(input.getIn(), input.getName() + "[" + index + "]", value), + String indexFormat = getIndexFormat(index); + refData(new Input(input.getIn(), input.getName() + indexFormat, value), allowedRefDataPredicate); index++; } @@ -514,4 +517,8 @@ protected SELF getThis() { return (SELF) this; } + private String getIndexFormat(int index) { + return ProblemConfig.isJsonPointerEnabled() ? ("/" + index) : ("[" + index + "]"); + } + } diff --git a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/EmployerIdValidatorTest.java b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/EmployerIdValidatorTest.java index 01dcbfcb..d5c493fe 100644 --- a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/EmployerIdValidatorTest.java +++ b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/EmployerIdValidatorTest.java @@ -14,26 +14,26 @@ class EmployerIdValidatorTest { @ParameterizedTest @ValueSource(longs = { 100006L, 212345609L, 312345625L, 200000031L, 499999982L, 187995796L, 168676597L }) void okNssoNumber(Long employerId) { - assertThat(new EmployerIdValidator(Input.body("test", employerId)).validate()).isEmpty(); + assertThat(new EmployerIdValidator(Input.body("/test", employerId)).validate()).isEmpty(); } @ParameterizedTest @ValueSource(longs = { 5134794036L, 5000000120L, 5999999989L, 5678901277L }) void okProvisionalNssoNumber(Long employerId) { - assertThat(new EmployerIdValidator(Input.body("test", employerId)).validate()).isEmpty(); + assertThat(new EmployerIdValidator(Input.body("/test", employerId)).validate()).isEmpty(); } @ParameterizedTest @ValueSource(longs = { 43220065L, 2130057L, 22300094L, 5170096L, 55290097L }) void okPplNumber(Long employerId) { - assertThat(new EmployerIdValidator(Input.body("test", employerId)).validate()).isEmpty(); + assertThat(new EmployerIdValidator(Input.body("/test", employerId)).validate()).isEmpty(); } @ParameterizedTest @ValueSource(longs = { 193L, 196L, 4000000100L, 6999999999L, 5678901279L, 1000000047L, 5000000121L, 6000000086L }) void nok(Long employerId) { - assertThat(new EmployerIdValidator(Input.body("test", employerId)).validate()) - .contains(InputValidationIssues.invalidEmployerId(BODY, "test", employerId)); + assertThat(new EmployerIdValidator(Input.body("/test", employerId)).validate()) + .contains(InputValidationIssues.invalidEmployerId(BODY, "/test", employerId)); } } diff --git a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/EnterpriseNumberValidatorTest.java b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/EnterpriseNumberValidatorTest.java index 8e09d64e..76abc33e 100644 --- a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/EnterpriseNumberValidatorTest.java +++ b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/EnterpriseNumberValidatorTest.java @@ -14,14 +14,14 @@ class EnterpriseNumberValidatorTest { @Test void ok() { - assertThat(new EnterpriseNumberValidator(Input.body("test", "0884303369")).validate()).isEmpty(); + assertThat(new EnterpriseNumberValidator(Input.body("/test", "0884303369")).validate()).isEmpty(); } @ParameterizedTest @ValueSource(strings = { "test", "54321", "20000000032", "2111111111", "0884303370" }) void nok(String value) { - assertThat(new EnterpriseNumberValidator(Input.body("test", value)).validate()) - .contains(InputValidationIssues.invalidEnterpriseNumber(BODY, "test", value)); + assertThat(new EnterpriseNumberValidator(Input.body("/test", value)).validate()) + .contains(InputValidationIssues.invalidEnterpriseNumber(BODY, "/test", value)); } } diff --git a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/EqualValidatorTest.java b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/EqualValidatorTest.java index 3b51fa03..a381a53c 100644 --- a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/EqualValidatorTest.java +++ b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/EqualValidatorTest.java @@ -14,19 +14,19 @@ class EqualValidatorTest { @Test void ok() { - assertThat(new EqualValidator(Arrays.asList(Input.header("id", "25"), Input.body("id", "25"))) + assertThat(new EqualValidator(Arrays.asList(Input.header("id", "25"), Input.body("/id", "25"))) .validate()).isEmpty(); } @Test void okNull() { - assertThat(new EqualValidator(Arrays.asList(Input.header("id", null), Input.body("id", null))) + assertThat(new EqualValidator(Arrays.asList(Input.header("id", null), Input.body("/id", null))) .validate()).isEmpty(); } @Test void nok() { - List> items = Arrays.asList(Input.header("id", "25"), Input.body("id", "26")); + List> items = Arrays.asList(Input.header("id", "25"), Input.body("/id", "26")); assertThat(new EqualValidator(items).validate()).contains(InputValidationIssues.equalExpected(items)); } diff --git a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/EstablishmentUnitNumberValidatorTest.java b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/EstablishmentUnitNumberValidatorTest.java index ac073738..dda0e198 100644 --- a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/EstablishmentUnitNumberValidatorTest.java +++ b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/EstablishmentUnitNumberValidatorTest.java @@ -14,14 +14,14 @@ class EstablishmentUnitNumberValidatorTest { @Test void ok() { - assertThat(new EstablishmentUnitNumberValidator(Input.body("test", "2297964444")).validate()).isEmpty(); + assertThat(new EstablishmentUnitNumberValidator(Input.body("/test", "2297964444")).validate()).isEmpty(); } @ParameterizedTest @ValueSource(strings = { "test", "54321", "0884303369", "2111111111", "2297964445" }) void nok(String value) { - assertThat(new EstablishmentUnitNumberValidator(Input.body("test", value)).validate()) - .contains(InputValidationIssues.invalidEstablishmentUnitNumber(BODY, "test", value)); + assertThat(new EstablishmentUnitNumberValidator(Input.body("/test", value)).validate()) + .contains(InputValidationIssues.invalidEstablishmentUnitNumber(BODY, "/test", value)); } } diff --git a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/ExactlyOneOfValidatorTest.java b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/ExactlyOneOfValidatorTest.java index b456b93b..c6a85ef2 100644 --- a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/ExactlyOneOfValidatorTest.java +++ b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/ExactlyOneOfValidatorTest.java @@ -14,22 +14,22 @@ class ExactlyOneOfValidatorTest { @Test void ok() { - assertThat(new ExactlyOneOfValidator(Arrays.asList(Input.body("cbeNumber", null), Input.body("sector", "25"))) + assertThat(new ExactlyOneOfValidator(Arrays.asList(Input.body("/cbeNumber", null), Input.body("/sector", "25"))) .validate()).isEmpty(); } @Test void nokMoreThanOne() { - List> items = Arrays.asList(Input.body("cbeNumber", "0694965804"), - Input.body("sector", "25")); + List> items = Arrays.asList(Input.body("/cbeNumber", "0694965804"), + Input.body("/sector", "25")); assertThat(new ExactlyOneOfValidator(items).validate()) .contains(InputValidationIssues.exactlyOneOfExpected(items)); } @Test void nokNone() { - List> items = Arrays.asList(Input.body("cbeNumber", null), - Input.body("sector", null)); + List> items = Arrays.asList(Input.body("/cbeNumber", null), + Input.body("/sector", null)); assertThat(new ExactlyOneOfValidator(items).validate()) .contains(InputValidationIssues.exactlyOneOfExpected(items)); } diff --git a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/IncompleteDateValidatorTest.java b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/IncompleteDateValidatorTest.java index 7f89f6c2..c0cc13b8 100644 --- a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/IncompleteDateValidatorTest.java +++ b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/IncompleteDateValidatorTest.java @@ -14,43 +14,43 @@ class IncompleteDateValidatorTest { @Test void okIncomplete() { - assertThat(new IncompleteDateValidator(Input.body("test", "2024-01-00")).validate()).isEmpty(); + assertThat(new IncompleteDateValidator(Input.body("/test", "2024-01-00")).validate()).isEmpty(); } @Test void okComplete() { - assertThat(new IncompleteDateValidator(Input.body("test", "2024-01-01")).validate()).isEmpty(); + assertThat(new IncompleteDateValidator(Input.body("/test", "2024-01-01")).validate()).isEmpty(); } @Test void nokMonthOutOfRange() { - assertThat(new IncompleteDateValidator(Input.body("test", "2024-13-01")).validate()) - .contains(InputValidationIssues.invalidIncompleteDate(BODY, "test", "2024-13-01")); + assertThat(new IncompleteDateValidator(Input.body("/test", "2024-13-01")).validate()) + .contains(InputValidationIssues.invalidIncompleteDate(BODY, "/test", "2024-13-01")); } @Test void nokDateWithoutMonth() { - assertThat(new IncompleteDateValidator(Input.body("test", "2024-00-04")).validate()) - .contains(InputValidationIssues.invalidIncompleteDate(BODY, "test", "2024-00-04")); + assertThat(new IncompleteDateValidator(Input.body("/test", "2024-00-04")).validate()) + .contains(InputValidationIssues.invalidIncompleteDate(BODY, "/test", "2024-00-04")); } @Test void nokDayOutOfRange() { - assertThat(new IncompleteDateValidator(Input.body("test", "2024-02-31")).validate()) - .contains(InputValidationIssues.invalidIncompleteDate(BODY, "test", "2024-02-31")); + assertThat(new IncompleteDateValidator(Input.body("/test", "2024-02-31")).validate()) + .contains(InputValidationIssues.invalidIncompleteDate(BODY, "/test", "2024-02-31")); } @Test void nokInvalidLocalDate() { - assertThat(new IncompleteDateValidator(Input.body("test", "2023-02-29")).validate()) - .contains(InputValidationIssues.invalidIncompleteDate(BODY, "test", "2023-02-29")); + assertThat(new IncompleteDateValidator(Input.body("/test", "2023-02-29")).validate()) + .contains(InputValidationIssues.invalidIncompleteDate(BODY, "/test", "2023-02-29")); } @ParameterizedTest @ValueSource(strings = { "test", "9999-99-99", "2024/01/01" }) void nokPattern(String value) { - assertThat(new IncompleteDateValidator(Input.body("test", value)).validate()) - .contains(InputValidationIssues.invalidIncompleteDate(BODY, "test", value)); + assertThat(new IncompleteDateValidator(Input.body("/test", value)).validate()) + .contains(InputValidationIssues.invalidIncompleteDate(BODY, "/test", value)); } } diff --git a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/PeriodValidatorTest.java b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/PeriodValidatorTest.java index 9c94b7bc..2cf6be0d 100644 --- a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/PeriodValidatorTest.java +++ b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/PeriodValidatorTest.java @@ -15,35 +15,35 @@ class PeriodValidatorTest { @Test void ok() { // only endDate - assertThat(new PeriodValidator(Input.body("period", new InputPeriod(null, LocalDate.of(2023, 10, 12)))) + assertThat(new PeriodValidator(Input.body("/period", new InputPeriod(null, LocalDate.of(2023, 10, 12)))) .validate()).isEmpty(); // only startDate - assertThat(new PeriodValidator(Input.body("period", new InputPeriod(LocalDate.of(2023, 10, 12), null))) + assertThat(new PeriodValidator(Input.body("/period", new InputPeriod(LocalDate.of(2023, 10, 12), null))) .validate()).isEmpty(); // startDate == endDate assertThat(new PeriodValidator( - Input.body("period", new InputPeriod(LocalDate.of(2023, 10, 12), LocalDate.of(2023, 10, 12)))) + Input.body("/period", new InputPeriod(LocalDate.of(2023, 10, 12), LocalDate.of(2023, 10, 12)))) .validate()).isEmpty(); // startDate < endDate assertThat(new PeriodValidator( - Input.body("period", new InputPeriod(LocalDate.of(2023, 10, 11), LocalDate.of(2023, 10, 12)))) + Input.body("/period", new InputPeriod(LocalDate.of(2023, 10, 11), LocalDate.of(2023, 10, 12)))) .validate()).isEmpty(); // no startDate and endDate - assertThat(new PeriodValidator(Input.body("criteria.periods.period", new InputPeriod(null, null))) + assertThat(new PeriodValidator(Input.body("/criteria/periods/period", new InputPeriod(null, null))) .validate()).isEmpty(); } @Test void nokStartDateAfterEndDate() { InputPeriod badPeriod = new InputPeriod(LocalDate.of(2023, 10, 14), LocalDate.of(2023, 10, 12)); - assertThat(new PeriodValidator(Input.body("period", badPeriod)).validate()).contains( - InputValidationIssues.invalidPeriod(BODY, "period", badPeriod)); + assertThat(new PeriodValidator(Input.body("/period", badPeriod)).validate()).contains( + InputValidationIssues.invalidPeriod(BODY, "/period", badPeriod)); } @Test void nokInvalidObject() { assertThatIllegalArgumentException().isThrownBy(() -> new PeriodValidator( - Input.body("period", "oops")).validate()) + Input.body("/period", "oops")).validate()) .withMessage("No startDate field with type class java.time.LocalDate was found" + " on class java.lang.String"); } diff --git a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/RejectedInputValidatorTest.java b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/RejectedInputValidatorTest.java index f6ae8be0..1db876fc 100644 --- a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/RejectedInputValidatorTest.java +++ b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/RejectedInputValidatorTest.java @@ -12,13 +12,13 @@ class RejectedInputValidatorTest { @Test void ok() { - assertThat(new RejectedInputValidator(Input.body("reject", null)).validate()).isEmpty(); + assertThat(new RejectedInputValidator(Input.body("/reject", null)).validate()).isEmpty(); } @Test void nok() { - assertThat(new RejectedInputValidator<>(Input.body("reject", "bad")).validate()) - .contains(InputValidationIssues.rejectedInput(BODY, "reject", "bad")); + assertThat(new RejectedInputValidator<>(Input.body("/reject", "bad")).validate()) + .contains(InputValidationIssues.rejectedInput(BODY, "/reject", "bad")); } } diff --git a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/RequestValidatorTest.java b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/RequestValidatorTest.java index 2a153cb6..03139f9a 100644 --- a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/RequestValidatorTest.java +++ b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/RequestValidatorTest.java @@ -9,183 +9,194 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import io.github.belgif.rest.problem.BadRequestProblem; import io.github.belgif.rest.problem.api.Input; import io.github.belgif.rest.problem.api.InputValidationIssue; import io.github.belgif.rest.problem.api.InputValidationIssues; +import io.github.belgif.rest.problem.config.ProblemConfig; class RequestValidatorTest { + @BeforeEach + void resetProblemConfig() { + ProblemConfig.reset(); + } + @Test void ssinValid() { - assertValid(new RequestValidator().ssin(Input.body("ssin", "00000000196"))); + assertValid(new RequestValidator().ssin(Input.body("/ssin", "00000000196"))); } @Test void ssinNull() { assertValid(new RequestValidator().ssin(null)); - assertValid(new RequestValidator().ssin(Input.body("ssin", null))); + assertValid(new RequestValidator().ssin(Input.body("/ssin", null))); } @Test void ssinInvalid() { assertInvalid( - new RequestValidator().ssin(Input.body("ssin", "22222222222")), - InputValidationIssues.invalidSsin(BODY, "ssin", "22222222222")); + new RequestValidator().ssin(Input.body("/ssin", "22222222222")), + InputValidationIssues.invalidSsin(BODY, "/ssin", "22222222222")); } - @Test - void ssinsInvalid() { + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void ssinsInvalid(boolean jsonPointerEnabled) { + ProblemConfig.setJsonPointerEnabled(jsonPointerEnabled); assertInvalid( - new RequestValidator().ssins(Input.body("ssins", + new RequestValidator().ssins(Input.body(jsonPointerEnabled ? "/ssins" : "ssins", Arrays.asList("00000000196", "11111111111", "00000000295", "22222222222"))), - InputValidationIssues.invalidSsin(BODY, "ssins[1]", "11111111111"), - InputValidationIssues.invalidSsin(BODY, "ssins[3]", "22222222222")); + InputValidationIssues.invalidSsin(BODY, jsonPointerEnabled ? "/ssins/1" : "ssins[1]", "11111111111"), + InputValidationIssues.invalidSsin(BODY, jsonPointerEnabled ? "/ssins/3" : "ssins[3]", "22222222222")); } @Test void enterpriseNumberValid() { - assertValid(new RequestValidator().enterpriseNumber(Input.body("enterpriseNumber", "0884303369"))); + assertValid(new RequestValidator().enterpriseNumber(Input.body("/enterpriseNumber", "0884303369"))); } @Test void enterpriseNumberNull() { assertValid(new RequestValidator().enterpriseNumber(null)); - assertValid(new RequestValidator().enterpriseNumber(Input.body("enterpriseNumber", null))); + assertValid(new RequestValidator().enterpriseNumber(Input.body("/enterpriseNumber", null))); } @Test void enterpriseNumberInvalid() { assertInvalid( - new RequestValidator().enterpriseNumber(Input.body("enterpriseNumber", "2111111112")), - InputValidationIssues.invalidEnterpriseNumber(BODY, "enterpriseNumber", "2111111112")); + new RequestValidator().enterpriseNumber(Input.body("/enterpriseNumber", "2111111112")), + InputValidationIssues.invalidEnterpriseNumber(BODY, "/enterpriseNumber", "2111111112")); } @Test void establishmentUnitNumberValid() { assertValid( - new RequestValidator().establishmentUnitNumber(Input.body("establishmentUnitNumber", "2297964444"))); + new RequestValidator().establishmentUnitNumber(Input.body("/establishmentUnitNumber", "2297964444"))); } @Test void establishmentUnitNumberNull() { assertValid(new RequestValidator().establishmentUnitNumber(null)); - assertValid(new RequestValidator().establishmentUnitNumber(Input.body("establishmentUnitNumber", null))); + assertValid(new RequestValidator().establishmentUnitNumber(Input.body("/establishmentUnitNumber", null))); } @Test void establishmentUnitNumberInvalid() { assertInvalid( - new RequestValidator().establishmentUnitNumber(Input.body("establishmentUnitNumber", "2111111111")), - InputValidationIssues.invalidEstablishmentUnitNumber(BODY, "establishmentUnitNumber", "2111111111")); + new RequestValidator().establishmentUnitNumber(Input.body("/establishmentUnitNumber", "2111111111")), + InputValidationIssues.invalidEstablishmentUnitNumber(BODY, "/establishmentUnitNumber", "2111111111")); } @Test void employerIdValid() { - assertValid(new RequestValidator().employerId(Input.body("employerId", 312345625L))); + assertValid(new RequestValidator().employerId(Input.body("/employerId", 312345625L))); } @Test void employerIdNull() { assertValid(new RequestValidator().employerId(null)); - assertValid(new RequestValidator().employerId(Input.body("employerId", null))); + assertValid(new RequestValidator().employerId(Input.body("/employerId", null))); } @Test void employerIdInvalid() { assertInvalid( - new RequestValidator().employerId(Input.body("employerId", 5678901279L)), - InputValidationIssues.invalidEmployerId(BODY, "employerId", 5678901279L)); + new RequestValidator().employerId(Input.body("/employerId", 5678901279L)), + InputValidationIssues.invalidEmployerId(BODY, "/employerId", 5678901279L)); } @Test void periodValid() { InputPeriod period = new InputPeriod(LocalDate.of(2023, 1, 1), LocalDate.of(2023, 12, 31)); - assertValid(new RequestValidator().period(Input.body("period", period))); + assertValid(new RequestValidator().period(Input.body("/period", period))); } @Test void periodNull() { assertValid(new RequestValidator().period(null)); - assertValid(new RequestValidator().period(Input.body("period", null))); + assertValid(new RequestValidator().period(Input.body("/period", null))); } @Test void periodInvalid() { InputPeriod badPeriod = new InputPeriod(LocalDate.of(2023, 10, 14), LocalDate.of(2023, 10, 12)); - assertInvalid(new RequestValidator().period(Input.body("period", badPeriod)), - InputValidationIssues.invalidPeriod(BODY, "period", badPeriod)); + assertInvalid(new RequestValidator().period(Input.body("/period", badPeriod)), + InputValidationIssues.invalidPeriod(BODY, "/period", badPeriod)); } @Test void temporalPeriodValid() { - assertValid(new RequestValidator().period(Input.body("startDate", LocalDate.of(2023, 1, 1)), - Input.body("endDate", LocalDate.of(2023, 12, 31)))); + assertValid(new RequestValidator().period(Input.body("/startDate", LocalDate.of(2023, 1, 1)), + Input.body("/endDate", LocalDate.of(2023, 12, 31)))); } @Test void temporalPeriodInvalid() { assertInvalid( - new RequestValidator().period(Input.body("startDate", LocalDate.of(2023, 10, 14)), - Input.body("endDate", LocalDate.of(2023, 10, 12))), - InputValidationIssues.invalidPeriod(Input.body("startDate", LocalDate.of(2023, 10, 14)), - Input.body("endDate", LocalDate.of(2023, 10, 12)))); + new RequestValidator().period(Input.body("/startDate", LocalDate.of(2023, 10, 14)), + Input.body("/endDate", LocalDate.of(2023, 10, 12))), + InputValidationIssues.invalidPeriod(Input.body("/startDate", LocalDate.of(2023, 10, 14)), + Input.body("/endDate", LocalDate.of(2023, 10, 12)))); } @Test void incompleteDateValid() { - assertValid(new RequestValidator().incompleteDate(Input.body("date", "2024-00-00"))); + assertValid(new RequestValidator().incompleteDate(Input.body("/date", "2024-00-00"))); } @Test void incompleteDateNull() { assertValid(new RequestValidator().incompleteDate(null)); - assertValid(new RequestValidator().incompleteDate(Input.body("date", null))); + assertValid(new RequestValidator().incompleteDate(Input.body("/date", null))); } @Test void incompleteDateInvalid() { assertInvalid( - new RequestValidator().incompleteDate(Input.body("date", "2024-00-01")), - InputValidationIssues.invalidIncompleteDate(BODY, "date", "2024-00-01")); + new RequestValidator().incompleteDate(Input.body("/date", "2024-00-01")), + InputValidationIssues.invalidIncompleteDate(BODY, "/date", "2024-00-01")); } @Test void yearMonthValid() { - assertValid(new RequestValidator().yearMonth(Input.body("yearMonth", "2024-01"))); + assertValid(new RequestValidator().yearMonth(Input.body("/yearMonth", "2024-01"))); } @Test void yearMonthNull() { assertValid(new RequestValidator().yearMonth(null)); - assertValid(new RequestValidator().yearMonth(Input.body("yearMonth", null))); + assertValid(new RequestValidator().yearMonth(Input.body("/yearMonth", null))); } @Test void yearMonthInvalid() { - assertInvalid(new RequestValidator().yearMonth(Input.body("yearMonth", "2024-99")), - InputValidationIssues.invalidYearMonth(BODY, "yearMonth", "2024-99")); + assertInvalid(new RequestValidator().yearMonth(Input.body("/yearMonth", "2024-99")), + InputValidationIssues.invalidYearMonth(BODY, "/yearMonth", "2024-99")); } @Test void exactlyOneOfValid() { - Input[] inputs = { Input.body("cbeNumber", null), Input.body("sector", "25") }; + Input[] inputs = { Input.body("/cbeNumber", null), Input.body("/sector", "25") }; assertValid(new RequestValidator().exactlyOneOf(inputs)); } @Test void exactlyOneOfInvalidMoreThatOne() { - Input[] inputs = { Input.body("cbeNumber", "0694965804"), Input.body("sector", "25") }; + Input[] inputs = { Input.body("/cbeNumber", "0694965804"), Input.body("/sector", "25") }; assertInvalid(new RequestValidator().exactlyOneOf(inputs), InputValidationIssues.exactlyOneOfExpected(Arrays.asList(inputs))); } @Test void exactlyOneOfInvalidNone() { - Input[] inputs = { Input.body("cbeNumber", null), Input.body("sector", null) }; + Input[] inputs = { Input.body("/cbeNumber", null), Input.body("/sector", null) }; assertInvalid(new RequestValidator().exactlyOneOf(inputs), InputValidationIssues.exactlyOneOfExpected(Arrays.asList(inputs))); } @@ -314,22 +325,30 @@ void refDataPredicateInvalid() { InputValidationIssues.referencedResourceNotFound(QUERY, "refData", "x")); } - @Test - void refDatasCollectionValid() { + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void refDatasCollectionValid(boolean jsonPointerEnabled) { + ProblemConfig.setJsonPointerEnabled(jsonPointerEnabled); assertValid(new RequestValidator().refDatas(Input.query("refDatas", Arrays.asList("a", "b")), Arrays.asList("a", "b", "c"))); } - @Test - void refDatasCollectionInvalid() { + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void refDatasCollectionInvalid(boolean isJsonPointerEnabled) { + ProblemConfig.setJsonPointerEnabled(isJsonPointerEnabled); assertInvalid(new RequestValidator().refDatas(Input.query("refDatas", Arrays.asList("a", "x", "b", "y")), Arrays.asList("a", "b", "c")), - InputValidationIssues.referencedResourceNotFound(QUERY, "refDatas[1]", "x"), - InputValidationIssues.referencedResourceNotFound(QUERY, "refDatas[3]", "y")); + InputValidationIssues.referencedResourceNotFound(QUERY, + isJsonPointerEnabled ? "refDatas/1" : "refDatas[1]", "x"), + InputValidationIssues.referencedResourceNotFound(QUERY, + isJsonPointerEnabled ? "refDatas/3" : "refDatas[3]", "y")); } - @Test - void refDatasSupplierValid() { + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void refDatasSupplierValid(boolean isJsonPointerEnabled) { + ProblemConfig.setJsonPointerEnabled(isJsonPointerEnabled); AtomicInteger calls = new AtomicInteger(0); assertValid(new RequestValidator().refDatas(Input.query("refDatas", Arrays.asList("a", "b")), () -> { @@ -339,16 +358,20 @@ void refDatasSupplierValid() { assertThat(calls).hasValue(1); } - @Test - void refDatasSupplierInvalid() { + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void refDatasSupplierInvalid(boolean isJsonPointerEnabled) { + ProblemConfig.setJsonPointerEnabled(isJsonPointerEnabled); AtomicInteger calls = new AtomicInteger(0); assertInvalid(new RequestValidator().refDatas(Input.query("refDatas", Arrays.asList("a", "x", "b", "y")), () -> { calls.incrementAndGet(); return Arrays.asList("a", "b", "c"); }), - InputValidationIssues.referencedResourceNotFound(QUERY, "refDatas[1]", "x"), - InputValidationIssues.referencedResourceNotFound(QUERY, "refDatas[3]", "y")); + InputValidationIssues.referencedResourceNotFound(QUERY, + isJsonPointerEnabled ? "refDatas/1" : "refDatas[1]", "x"), + InputValidationIssues.referencedResourceNotFound(QUERY, + isJsonPointerEnabled ? "refDatas/3" : "refDatas[3]", "y")); assertThat(calls).hasValue(1); } @@ -362,18 +385,24 @@ void refDatasSupplierValidEmpty() { assertThat(calls).hasValue(0); } - @Test - void refDatasPredicateValid() { + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void refDatasPredicateValid(boolean isJsonPointerEnabled) { + ProblemConfig.setJsonPointerEnabled(isJsonPointerEnabled); assertValid(new RequestValidator().refDatas(Input.query("refDatas", Arrays.asList("a", "b")), Arrays.asList("a", "b", "c")::contains)); } - @Test - void refDatasPredicateInvalid() { + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void refDatasPredicateInvalid(boolean isJsonPointerEnabled) { + ProblemConfig.setJsonPointerEnabled(isJsonPointerEnabled); assertInvalid(new RequestValidator().refDatas(Input.query("refDatas", Arrays.asList("a", "x", "b", "y")), Arrays.asList("a", "b", "c")::contains), - InputValidationIssues.referencedResourceNotFound(QUERY, "refDatas[1]", "x"), - InputValidationIssues.referencedResourceNotFound(QUERY, "refDatas[3]", "y")); + InputValidationIssues.referencedResourceNotFound(QUERY, + isJsonPointerEnabled ? "refDatas/1" : "refDatas[1]", "x"), + InputValidationIssues.referencedResourceNotFound(QUERY, + isJsonPointerEnabled ? "refDatas/3" : "refDatas[3]", "y")); } @Test @@ -474,29 +503,29 @@ void customInvalid() { @Test void chainingValid() { - assertValid(new RequestValidator().ssin(Input.body("ssin", "00000000196")) - .enterpriseNumber(Input.body("enterpriseNumber", "0884303369"))); + assertValid(new RequestValidator().ssin(Input.body("/ssin", "00000000196")) + .enterpriseNumber(Input.body("/enterpriseNumber", "0884303369"))); } @Test void chainingInvalid() { - assertInvalid(new RequestValidator().ssin(Input.body("ssin", "22222222222")) - .enterpriseNumber(Input.body("enterpriseNumber", "2111111112")), - InputValidationIssues.invalidSsin(BODY, "ssin", "22222222222"), - InputValidationIssues.invalidEnterpriseNumber(BODY, "enterpriseNumber", "2111111112")); + assertInvalid(new RequestValidator().ssin(Input.body("/ssin", "22222222222")) + .enterpriseNumber(Input.body("/enterpriseNumber", "2111111112")), + InputValidationIssues.invalidSsin(BODY, "/ssin", "22222222222"), + InputValidationIssues.invalidEnterpriseNumber(BODY, "/enterpriseNumber", "2111111112")); } @Test void extension() { new RequestValidatorExtensionB() - .require(Input.body("test", "value")) + .require(Input.body("/test", "value")) .b() .a() .validate(); new RequestValidatorExtensionB() .a() .b() - .require(Input.body("test", "value")) + .require(Input.body("/test", "value")) .validate(); } diff --git a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/RequiredInputValidatorTest.java b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/RequiredInputValidatorTest.java index 90a54e8a..530bc21b 100644 --- a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/RequiredInputValidatorTest.java +++ b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/RequiredInputValidatorTest.java @@ -12,13 +12,13 @@ class RequiredInputValidatorTest { @Test void ok() { - assertThat(new RequiredInputValidator<>(Input.body("required", "ok")).validate()).isEmpty(); + assertThat(new RequiredInputValidator<>(Input.body("/required", "ok")).validate()).isEmpty(); } @Test void nok() { - assertThat(new RequiredInputValidator(Input.body("required", null)).validate()) - .contains(InputValidationIssues.requiredInput(BODY, "required")); + assertThat(new RequiredInputValidator(Input.body("/required", null)).validate()) + .contains(InputValidationIssues.requiredInput(BODY, "/required")); } } diff --git a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/SsinValidatorTest.java b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/SsinValidatorTest.java index deb1c12b..6fad610d 100644 --- a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/SsinValidatorTest.java +++ b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/SsinValidatorTest.java @@ -122,12 +122,12 @@ void nokTooShort() { } private void assertValidSsin(String ssin) { - assertThat(new SsinValidator(Input.body("test", ssin)).validate()).isEmpty(); + assertThat(new SsinValidator(Input.body("/test", ssin)).validate()).isEmpty(); } private void assertInvalidSsin(String ssin) { - assertThat(new SsinValidator(Input.body("test", ssin)).validate()) - .contains(InputValidationIssues.invalidSsin(BODY, "test", ssin)); + assertThat(new SsinValidator(Input.body("/test", ssin)).validate()) + .contains(InputValidationIssues.invalidSsin(BODY, "/test", ssin)); } private static String createSsinFromDate(LocalDate date) { diff --git a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/YearMonthValidatorTest.java b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/YearMonthValidatorTest.java index 32003189..cb253a61 100644 --- a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/YearMonthValidatorTest.java +++ b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/YearMonthValidatorTest.java @@ -12,13 +12,13 @@ class YearMonthValidatorTest { @Test void ok() { - assertThat(new YearMonthValidator(Input.body("test", "2024-01")).validate()).isEmpty(); + assertThat(new YearMonthValidator(Input.body("/test", "2024-01")).validate()).isEmpty(); } @Test void nok() { - assertThat(new YearMonthValidator(Input.body("criteria.month", "2024-99")).validate()) - .contains(InputValidationIssues.invalidYearMonth(BODY, "criteria.month", "2024-99")); + assertThat(new YearMonthValidator(Input.body("/criteria/month", "2024-99")).validate()) + .contains(InputValidationIssues.invalidYearMonth(BODY, "/criteria/month", "2024-99")); } } diff --git a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/ZeroOrAllOfValidatorTest.java b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/ZeroOrAllOfValidatorTest.java index ea8b535f..5ecebd06 100644 --- a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/ZeroOrAllOfValidatorTest.java +++ b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/ZeroOrAllOfValidatorTest.java @@ -14,20 +14,20 @@ class ZeroOrAllOfValidatorTest { @Test void okZero() { - assertThat(new ZeroOrAllOfValidator(Arrays.asList(Input.body("cbeNumber", null), Input.body("sector", null))) + assertThat(new ZeroOrAllOfValidator(Arrays.asList(Input.body("/cbeNumber", null), Input.body("/sector", null))) .validate()).isEmpty(); } @Test void okAll() { - assertThat(new ZeroOrAllOfValidator(Arrays.asList(Input.body("cbeNumber", "25"), Input.body("sector", "25"))) + assertThat(new ZeroOrAllOfValidator(Arrays.asList(Input.body("/cbeNumber", "25"), Input.body("/sector", "25"))) .validate()).isEmpty(); } @Test void nok() { - List> items = Arrays.asList(Input.body("cbeNumber", "0694965804"), - Input.body("sector", null)); + List> items = Arrays.asList(Input.body("/cbeNumber", "0694965804"), + Input.body("/sector", null)); assertThat(new ZeroOrAllOfValidator(items).validate()).contains( InputValidationIssues.zeroOrAllOfExpected(items)); } diff --git a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/ZeroOrExactlyOneOfValidatorTest.java b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/ZeroOrExactlyOneOfValidatorTest.java index 7e432959..f987e986 100644 --- a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/ZeroOrExactlyOneOfValidatorTest.java +++ b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/ZeroOrExactlyOneOfValidatorTest.java @@ -15,21 +15,21 @@ class ZeroOrExactlyOneOfValidatorTest { @Test void okZero() { assertThat(new ZeroOrExactlyOneOfValidator( - Arrays.asList(Input.body("cbeNumber", null), Input.body("sector", null))) + Arrays.asList(Input.body("/cbeNumber", null), Input.body("/sector", null))) .validate()).isEmpty(); } @Test void okOne() { assertThat(new ZeroOrExactlyOneOfValidator( - Arrays.asList(Input.body("cbeNumber", null), Input.body("sector", "25"))) + Arrays.asList(Input.body("/cbeNumber", null), Input.body("/sector", "25"))) .validate()).isEmpty(); } @Test void validateNOk() { - List> items = Arrays.asList(Input.body("cbeNumber", "0694965804"), - Input.body("sector", "25")); + List> items = Arrays.asList(Input.body("/cbeNumber", "0694965804"), + Input.body("/sector", "25")); assertThat(new ZeroOrExactlyOneOfValidator(items).validate()).contains( InputValidationIssues.zeroOrExactlyOneOfExpected(items)); } diff --git a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/Input.java b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/Input.java index cf60474c..72da3eaa 100644 --- a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/Input.java +++ b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/Input.java @@ -32,6 +32,7 @@ public Input() { } public Input(InEnum in, String name, V value) { + InputValidationIssue.verifyNameFormat(in, name); this.in = in; this.name = name; this.value = value; @@ -42,6 +43,7 @@ public InEnum getIn() { } public void setIn(InEnum in) { + InputValidationIssue.verifyNameFormat(in, name); this.in = in; } @@ -50,6 +52,7 @@ public String getName() { } public void setName(String name) { + InputValidationIssue.verifyNameFormat(in, name); this.name = name; } diff --git a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java index 3ca02753..7d73e380 100644 --- a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java +++ b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java @@ -47,6 +47,14 @@ public class InputValidationIssue { private static final String INPUTS_SETTER_ONE_ITEM = "inputs[] can not be set with a single item, use in(in, name, value) instead"; + private static final String IN_NAME_VALUE_INVALID_FORMAT = "input name has an invalid format"; + + // e.g: /, field, /field, /field/0, /field/0/nested + private static final String JSON_POINTER_BASIC_REGEX = "\\/*[a-zA-Z0-9]*(\\/[a-zA-Z0-9]+)*"; + + // e.g: field, field[0], field[0].nested + private static final String JSON_PATH_REGEX = "[a-zA-Z0-9]+(\\[\\d+\\])*(\\.[a-zA-Z0-9]+(\\[\\d+\\])*)*"; + private URI type; private URI href; private String title; @@ -72,12 +80,14 @@ public InputValidationIssue(URI type, URI href, String title) { } public InputValidationIssue(InEnum in, String name, Object value) { + verifyNameFormat(in, name); this.in = in; this.name = name; this.value = value; } public InputValidationIssue(InEnum in, String name) { + verifyNameFormat(in, name); this.in = in; this.name = name; } @@ -120,6 +130,7 @@ public InEnum getIn() { public void setIn(InEnum in) { verifyNoInputs(in); + verifyNameFormat(in, name); this.in = in; } @@ -129,6 +140,7 @@ public String getName() { public void setName(String name) { verifyNoInputs(name); + verifyNameFormat(in, name); this.name = name; } @@ -182,6 +194,21 @@ private void verifyNoInputs(Object valueToUpdate) { } } + public static void verifyNameFormat(InEnum in, String name) { + + if (name == null) { + return; + } + + if ((!ProblemConfig.isJsonPointerEnabled() && !nameMatchesJsonPathFormat(name)) + || (ProblemConfig.isJsonPointerEnabled() && !nameMatchesJsonPointerFormat(in, name))) { + + throw new IllegalArgumentException( + IN_NAME_VALUE_INVALID_FORMAT + "(In: " + in + ", Name: " + name + ") It should follow " + + (ProblemConfig.isJsonPointerEnabled() ? "JsonPointer" : "JsonPath") + " syntax"); + } + } + private boolean hasInNameValue() { return in != null || name != null || value != null; } @@ -450,4 +477,45 @@ public String toString() { '}'; } + private static boolean nameMatchesJsonPointerFormat(InEnum in, String name) { + return name.matches(JSON_POINTER_BASIC_REGEX) + && (in != InEnum.BODY || name.startsWith("/")) // if for body, the name must start with "/" + && !name.matches(".*\\/\\d+\\/\\d+\\/*") // not two indexes following each other (e.g: person/1/2) + && !name.matches("\\/*\\d+(\\/[a-zA-Z0-9]+)*"); // not starting with an index (e.g: /1/person, 1/person) + } + + private static boolean nameMatchesJsonPathFormat(String name) { + return name.matches(JSON_PATH_REGEX); + } + + public static String convertName(InEnum in, String nameJsonPath) { + + if (nameJsonPath == null || nameJsonPath.trim().isEmpty()) { + return null; + } else if (!ProblemConfig.isJsonPointerEnabled()) { + return nameJsonPath; + } else { + // replace all indexes "[X]" by "/X" and replace all "." by "/" + String convertedName = replaceSquareBrackets(nameJsonPath).replace(".", "/"); + return in == InEnum.BODY ? "/" + convertedName : convertedName; + } + } + + public static String getNameFromProperties(InEnum in, List propertiesName) { + + if (propertiesName == null || propertiesName.isEmpty()) { + return null; + } + + String name = ProblemConfig.isJsonPointerEnabled() ? propertiesName.stream() + .map(InputValidationIssue::replaceSquareBrackets).collect(Collectors.joining("/")) + : String.join(".", propertiesName); + return ProblemConfig.isJsonPointerEnabled() && in == InEnum.BODY ? "/" + name : name; + } + + private static String replaceSquareBrackets(String propertyName) { + // replace all indexes "[X]" by "/X" + return propertyName.replaceAll("\\[(\\d+)\\]", "/$1"); + } + } diff --git a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssues.java b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssues.java index b3d6a3c5..ae8fe088 100644 --- a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssues.java +++ b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssues.java @@ -139,7 +139,9 @@ public static InputValidationIssue referencedResourceNotFound(InEnum in, String * @param The type of the reference */ public static InputValidationIssue referencedResourceNotFound(InEnum in, String name, T value, List source) { - String nameWithIndex = name + "[" + source.indexOf(value) + "]"; + String indexFormat = ProblemConfig.isJsonPointerEnabled() ? ("/" + source.indexOf(value)) + : ("[" + source.indexOf(value) + "]"); + String nameWithIndex = name + indexFormat; return referencedResourceNotFound(in, nameWithIndex, value); } diff --git a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson2Util.java b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson2Util.java index 903e5635..02e97557 100644 --- a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson2Util.java +++ b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson2Util.java @@ -2,6 +2,7 @@ import static io.github.belgif.rest.problem.api.InputValidationIssues.*; +import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -12,8 +13,8 @@ import io.github.belgif.rest.problem.BadRequestProblem; import io.github.belgif.rest.problem.api.InEnum; +import io.github.belgif.rest.problem.api.InputValidationIssue; import io.github.belgif.rest.problem.api.InputValidationIssues; -import io.github.belgif.rest.problem.config.ProblemConfig; /** * Internal jackson 2 utility class. @@ -60,24 +61,22 @@ public static BadRequestProblem toBadRequestProblem(JsonMappingException e) { } private static String getName(List path) { - String rootPrefix = ProblemConfig.isJsonPointerEnabled() ? "/" : ""; - String indexPrefix = ProblemConfig.isJsonPointerEnabled() ? "/" : "["; - String indexSuffix = ProblemConfig.isJsonPointerEnabled() ? "" : "]"; - String fieldNamePrefix = ProblemConfig.isJsonPointerEnabled() ? "/" : "."; if (path.isEmpty()) { return null; } - StringBuilder builder = new StringBuilder(); + List properties = new ArrayList<>(); + for (Reference reference : path) { if (reference.getFrom() instanceof List) { - builder.append(indexPrefix).append(reference.getIndex()).append(indexSuffix); + // append the index to the property name + properties.set(properties.size() - 1, + properties.get(properties.size() - 1) + "[" + reference.getIndex() + "]"); } else { - builder.append(builder.length() > 0 ? fieldNamePrefix : rootPrefix); - builder.append(reference.getFieldName()); + properties.add(reference.getFieldName()); } } - return builder.toString(); + return InputValidationIssue.getNameFromProperties(InEnum.BODY, properties); } @SuppressWarnings("java:S1872") diff --git a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson3Util.java b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson3Util.java index 91796d00..09f7655a 100644 --- a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson3Util.java +++ b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson3Util.java @@ -2,12 +2,13 @@ import static io.github.belgif.rest.problem.api.InputValidationIssues.*; +import java.util.ArrayList; import java.util.List; import io.github.belgif.rest.problem.BadRequestProblem; import io.github.belgif.rest.problem.api.InEnum; +import io.github.belgif.rest.problem.api.InputValidationIssue; import io.github.belgif.rest.problem.api.InputValidationIssues; -import io.github.belgif.rest.problem.config.ProblemConfig; import tools.jackson.core.JacksonException.Reference; import tools.jackson.core.exc.StreamReadException; import tools.jackson.databind.DatabindException; @@ -44,24 +45,22 @@ public static BadRequestProblem toBadRequestProblem(DatabindException e) { } private static String getName(List path) { - String rootPrefix = ProblemConfig.isJsonPointerEnabled() ? "/" : ""; - String indexPrefix = ProblemConfig.isJsonPointerEnabled() ? "/" : "["; - String indexSuffix = ProblemConfig.isJsonPointerEnabled() ? "" : "]"; - String fieldNamePrefix = ProblemConfig.isJsonPointerEnabled() ? "/" : "."; if (path.isEmpty()) { return null; } - StringBuilder name = new StringBuilder(); + + List properties = new ArrayList<>(); + for (Reference reference : path) { if (reference.from() instanceof List) { - name.append(indexPrefix).append(reference.getIndex()).append(indexSuffix); + // append the index to the property name + properties.set(properties.size() - 1, + properties.get(properties.size() - 1) + "[" + reference.getIndex() + "]"); } else { - name.append(name.length() > 0 ? fieldNamePrefix : rootPrefix); - name.append(reference.getPropertyName()); + properties.add(reference.getPropertyName()); } } - return name.toString(); + return InputValidationIssue.getNameFromProperties(InEnum.BODY, properties); } - } diff --git a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputTest.java b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputTest.java index da9dd644..d58dd986 100644 --- a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputTest.java +++ b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputTest.java @@ -2,10 +2,18 @@ import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import io.github.belgif.rest.problem.config.ProblemConfig; + class InputTest { + @BeforeEach + void resetProblemConfig() { + ProblemConfig.reset(); + } + @Test void construct() { Input input = new Input<>(); @@ -21,10 +29,10 @@ void construct() { @Test void body() { - Input bodyInput = Input.body("bodyName", "bodyValue"); + Input bodyInput = Input.body("/bodyName", "bodyValue"); assertThat(bodyInput.getIn()).isEqualTo(InEnum.BODY); - assertThat(bodyInput.getName()).isEqualTo("bodyName"); + assertThat(bodyInput.getName()).isEqualTo("/bodyName"); assertThat(bodyInput.getValue()).isEqualTo("bodyValue"); } @@ -57,9 +65,9 @@ void header() { @Test void equalsHashCodeToString() { - Input input = new Input<>(InEnum.BODY, "name", "value"); - Input equal = new Input<>(InEnum.BODY, "name", "value"); - Input other = new Input<>(InEnum.BODY, "anotherName", "anotherValue"); + Input input = new Input<>(InEnum.BODY, "/name", "value"); + Input equal = new Input<>(InEnum.BODY, "/name", "value"); + Input other = new Input<>(InEnum.BODY, "/anotherName", "anotherValue"); assertThat(input).isEqualTo(input); assertThat(input).hasSameHashCodeAs(input); diff --git a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssueTest.java b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssueTest.java index 96d1e10c..fb9ec540 100644 --- a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssueTest.java +++ b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssueTest.java @@ -1,6 +1,7 @@ package io.github.belgif.rest.problem.api; import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; import java.net.URI; import java.util.ArrayList; @@ -12,6 +13,10 @@ import org.assertj.core.api.ThrowableAssert; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import com.fasterxml.jackson.core.JsonPointer; import io.github.belgif.rest.problem.config.ProblemConfig; import io.github.belgif.rest.problem.i18n.Context; @@ -317,6 +322,132 @@ void equalsHashCodeToString() { assertThat(issue).isNotEqualTo("other type"); } + @ParameterizedTest + @EnumSource(InEnum.class) + void convertName(InEnum in) { + assertThat(InputValidationIssue.convertName(in, null)).isNull(); + assertThat(InputValidationIssue.convertName(in, "")).isNull(); + assertThat(InputValidationIssue.convertName(in, " ")).isNull(); + assertThat(InputValidationIssue.convertName(in, "field")).isEqualTo(in == InEnum.BODY ? "/field" : "field"); + assertThat(InputValidationIssue.convertName(in, "field[0]")) + .isEqualTo(in == InEnum.BODY ? "/field/0" : "field/0"); + assertThat(InputValidationIssue.convertName(in, "field[0].nested")) + .isEqualTo(in == InEnum.BODY ? "/field/0/nested" : "field/0/nested"); + } + + @ParameterizedTest + @EnumSource(InEnum.class) + void convertNameWithJsonPointerDisabled(InEnum in) { + + ProblemConfig.setJsonPointerEnabled(false); + + assertThat(InputValidationIssue.convertName(in, null)).isNull(); + assertThat(InputValidationIssue.convertName(in, "")).isNull(); + assertThat(InputValidationIssue.convertName(in, " ")).isNull(); + assertThat(InputValidationIssue.convertName(in, "field")).isEqualTo("field"); + assertThat(InputValidationIssue.convertName(in, "field[0]")).isEqualTo("field[0]"); + assertThat(InputValidationIssue.convertName(in, "field[0].nested")).isEqualTo("field[0].nested"); + } + + @ParameterizedTest + @EnumSource(InEnum.class) + void getNameFromProperties(InEnum in) { + List properties = null; + assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isNull(); + + properties = new ArrayList<>(); + assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isNull(); + + properties.add("field"); + assertThat(InputValidationIssue.getNameFromProperties(in, properties)) + .isEqualTo(in == InEnum.BODY ? "/field" : "field"); + + properties.set(0, "field[0]"); + assertThat(InputValidationIssue.getNameFromProperties(in, properties)) + .isEqualTo(in == InEnum.BODY ? "/field/0" : "field/0"); + + properties.add("nested"); + assertThat(InputValidationIssue.getNameFromProperties(in, properties)) + .isEqualTo(in == InEnum.BODY ? "/field/0/nested" : "field/0/nested"); + } + + @ParameterizedTest + @EnumSource(InEnum.class) + void getNameFromPropertiesJsonPointerDisabled(InEnum in) { + + ProblemConfig.setJsonPointerEnabled(false); + + List properties = null; + assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isNull(); + + properties = new ArrayList<>(); + assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isNull(); + + properties.add("field"); + assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isEqualTo("field"); + + properties.set(0, "field[0]"); + assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isEqualTo("field[0]"); + + properties.add("nested"); + assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isEqualTo("field[0].nested"); + } + + @ParameterizedTest + @EnumSource(InEnum.class) + void nameMatchingJsonPointerFormatInConstructor(InEnum in) { + String prefix = in == InEnum.BODY ? "/" : ""; + assertDoesNotThrow(() -> new InputValidationIssue(in, prefix + "field")); + assertDoesNotThrow(() -> new InputValidationIssue(in, prefix + "")); + assertDoesNotThrow(() -> new InputValidationIssue(in, prefix + "field", "value")); + assertDoesNotThrow(() -> new InputValidationIssue(in, prefix + "field/0")); + assertDoesNotThrow(() -> new InputValidationIssue(in, prefix + "field/0", "value")); + assertDoesNotThrow(() -> new InputValidationIssue(in, prefix + "field/0/nested/2/nestedAgain")); + assertDoesNotThrow(() -> new InputValidationIssue(in, prefix + "field/0/nested/2/nestedAgain", "value")); + + assertThatIllegalArgumentException().isThrownBy(() -> new InputValidationIssue(in, prefix + "field/0/1")) + .withMessageContaining("format").withMessageContaining("JsonPointer"); + + assertThatIllegalArgumentException().isThrownBy(() -> new InputValidationIssue(in, prefix + "field[0]")) + .withMessageContaining("format").withMessageContaining("JsonPointer"); + assertThatIllegalArgumentException() + .isThrownBy(() -> new InputValidationIssue(in, prefix + "field[0]", "value")) + .withMessageContaining("format").withMessageContaining("JsonPointer"); + assertThatIllegalArgumentException() + .isThrownBy(() -> new InputValidationIssue(in, prefix + "field[0].nested[2]/nestedAgain")) + .withMessageContaining("format").withMessageContaining("JsonPointer"); + assertThatIllegalArgumentException() + .isThrownBy(() -> new InputValidationIssue(in, prefix + "field[0].nested[2]/nestedAgain", "value")) + .withMessageContaining("format").withMessageContaining("JsonPointer"); + } + + @ParameterizedTest + @EnumSource(InEnum.class) + void nameMatchingJsonPathFormatInConstructor(InEnum in) { + ProblemConfig.setJsonPointerEnabled(false); + + JsonPointer.compile("/field[0]"); + + assertDoesNotThrow(() -> new InputValidationIssue(in, "field")); + assertDoesNotThrow(() -> new InputValidationIssue(in, "field", "value")); + assertDoesNotThrow(() -> new InputValidationIssue(in, "field[0]")); + assertDoesNotThrow(() -> new InputValidationIssue(in, "field[0]", "value")); + assertDoesNotThrow(() -> new InputValidationIssue(in, "field[0].nested[2].nestedAgain")); + assertDoesNotThrow(() -> new InputValidationIssue(in, "field[0].nested[2].nestedAgain", "value")); + + assertThatIllegalArgumentException().isThrownBy(() -> new InputValidationIssue(in, "field/0")) + .withMessageContaining("format").withMessageContaining("JsonPath"); + assertThatIllegalArgumentException() + .isThrownBy(() -> new InputValidationIssue(in, "field/0", "value")) + .withMessageContaining("format").withMessageContaining("JsonPath"); + assertThatIllegalArgumentException() + .isThrownBy(() -> new InputValidationIssue(in, "field/0/nested/2/nestedAgain")) + .withMessageContaining("format").withMessageContaining("JsonPath"); + assertThatIllegalArgumentException() + .isThrownBy(() -> new InputValidationIssue(in, "field/0/nested/2/nestedAgain", "value")) + .withMessageContaining("format").withMessageContaining("JsonPath"); + } + private void assertMutuallyExclusiveException(ThrowableAssert.ThrowingCallable throwingCallable) { assertThatIllegalArgumentException() .isThrownBy(throwingCallable) diff --git a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssuesTest.java b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssuesTest.java index bb42165a..23348661 100644 --- a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssuesTest.java +++ b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssuesTest.java @@ -58,13 +58,13 @@ void resetProblemConfig() { @Test void schemaViolation() { InputValidationIssue issue = - InputValidationIssues.schemaViolation(InEnum.BODY, "test", "value", "detail"); + InputValidationIssues.schemaViolation(InEnum.BODY, "/test", "value", "detail"); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); assertThat(issue.getHref()) .hasToString("https://www.belgif.be/specification/rest/api-guide/issues/schemaViolation.html"); assertThat(issue.getTitle()).isEqualTo("Input value is invalid with respect to the schema"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("test"); + assertThat(issue.getName()).isEqualTo("/test"); assertThat(issue.getValue()).isEqualTo("value"); assertThat(issue.getDetail()).isEqualTo("detail"); assertThat(issue).extracting("inputs", "additionalProperties").allMatch(this::isEmpty); @@ -73,28 +73,28 @@ void schemaViolation() { @Test void unknownInput() { InputValidationIssue issue = - InputValidationIssues.unknownInput(InEnum.BODY, "oops", "value"); + InputValidationIssues.unknownInput(InEnum.BODY, "/oops", "value"); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:unknownInput"); assertThat(issue.getHref()) .hasToString("https://www.belgif.be/specification/rest/api-guide/issues/unknownInput.html"); assertThat(issue.getTitle()).isEqualTo("Unknown input"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("oops"); + assertThat(issue.getName()).isEqualTo("/oops"); assertThat(issue.getValue()).isEqualTo("value"); - assertThat(issue.getDetail()).isEqualTo("Input oops is unknown"); + assertThat(issue.getDetail()).isEqualTo("Input /oops is unknown"); assertThat(issue).extracting("inputs", "additionalProperties").allMatch(this::isEmpty); } @Test void invalidInput() { InputValidationIssue issue = - InputValidationIssues.invalidInput(InEnum.BODY, "oops", "value", "detail"); + InputValidationIssues.invalidInput(InEnum.BODY, "/oops", "value", "detail"); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:invalidInput"); assertThat(issue.getHref()) .hasToString("https://www.belgif.be/specification/rest/api-guide/issues/invalidInput.html"); assertThat(issue.getTitle()).isEqualTo("Invalid input"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("oops"); + assertThat(issue.getName()).isEqualTo("/oops"); assertThat(issue.getValue()).isEqualTo("value"); assertThat(issue.getDetail()).isEqualTo("detail"); assertThat(issue).extracting("inputs", "additionalProperties").allMatch(this::isEmpty); @@ -104,7 +104,7 @@ void invalidInput() { @MethodSource("toggleExtIssueTypes") void invalidStructure(boolean extIssueTypes) { InputValidationIssue issue = - InputValidationIssues.invalidStructure(InEnum.BODY, "test", "value", "detail"); + InputValidationIssues.invalidStructure(InEnum.BODY, "/test", "value", "detail"); if (extIssueTypes) { assertThat(issue.getType()).hasToString("urn:problem-type:belgif-ext:input-validation:invalidStructure"); assertThat(issue.getHref()) @@ -117,7 +117,7 @@ void invalidStructure(boolean extIssueTypes) { assertThat(issue.getTitle()).isEqualTo("Invalid input"); } assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("test"); + assertThat(issue.getName()).isEqualTo("/test"); assertThat(issue.getValue()).isEqualTo("value"); assertThat(issue.getDetail()).isEqualTo("detail"); assertThat(issue).extracting("inputs", "additionalProperties").allMatch(this::isEmpty); @@ -127,7 +127,7 @@ void invalidStructure(boolean extIssueTypes) { @MethodSource("toggleExtIssueTypes") void outOfRangeMinMax(boolean extIssueTypes) { InputValidationIssue issue = - InputValidationIssues.outOfRange(InEnum.BODY, "test", 6, 1, 5); + InputValidationIssues.outOfRange(InEnum.BODY, "/test", 6, 1, 5); if (extIssueTypes) { assertThat(issue.getType()).hasToString("urn:problem-type:belgif-ext:input-validation:outOfRange"); assertThat(issue.getHref()) @@ -140,9 +140,9 @@ void outOfRangeMinMax(boolean extIssueTypes) { assertThat(issue.getTitle()).isEqualTo("Invalid input"); } assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("test"); + assertThat(issue.getName()).isEqualTo("/test"); assertThat(issue.getValue()).isEqualTo(6); - assertThat(issue.getDetail()).isEqualTo("Input value test = 6 is out of range [1, 5]"); + assertThat(issue.getDetail()).isEqualTo("Input value /test = 6 is out of range [1, 5]"); assertThat(issue.getAdditionalProperties()).containsOnly(entry("minimum", "1"), entry("maximum", "5")); assertThat(issue.getInputs()).isEmpty(); } @@ -151,7 +151,7 @@ void outOfRangeMinMax(boolean extIssueTypes) { @MethodSource("toggleExtIssueTypes") void outOfRangeMin(boolean extIssueTypes) { InputValidationIssue issue = - InputValidationIssues.outOfRange(InEnum.BODY, "test", 0, 1, null); + InputValidationIssues.outOfRange(InEnum.BODY, "/test", 0, 1, null); if (extIssueTypes) { assertThat(issue.getType()).hasToString("urn:problem-type:belgif-ext:input-validation:outOfRange"); assertThat(issue.getHref()) @@ -164,9 +164,9 @@ void outOfRangeMin(boolean extIssueTypes) { assertThat(issue.getTitle()).isEqualTo("Invalid input"); } assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("test"); + assertThat(issue.getName()).isEqualTo("/test"); assertThat(issue.getValue()).isEqualTo(0); - assertThat(issue.getDetail()).isEqualTo("Input value test = 0 should be at least 1"); + assertThat(issue.getDetail()).isEqualTo("Input value /test = 0 should be at least 1"); assertThat(issue.getAdditionalProperties()).containsExactly(entry("minimum", "1")); assertThat(issue.getInputs()).isEmpty(); } @@ -175,7 +175,7 @@ void outOfRangeMin(boolean extIssueTypes) { @MethodSource("toggleExtIssueTypes") void outOfRangeMax(boolean extIssueTypes) { InputValidationIssue issue = - InputValidationIssues.outOfRange(InEnum.BODY, "test", 6, null, 5); + InputValidationIssues.outOfRange(InEnum.BODY, "/test", 6, null, 5); if (extIssueTypes) { assertThat(issue.getType()).hasToString("urn:problem-type:belgif-ext:input-validation:outOfRange"); assertThat(issue.getHref()) @@ -188,9 +188,9 @@ void outOfRangeMax(boolean extIssueTypes) { assertThat(issue.getTitle()).isEqualTo("Invalid input"); } assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("test"); + assertThat(issue.getName()).isEqualTo("/test"); assertThat(issue.getValue()).isEqualTo(6); - assertThat(issue.getDetail()).isEqualTo("Input value test = 6 should not exceed 5"); + assertThat(issue.getDetail()).isEqualTo("Input value /test = 6 should not exceed 5"); assertThat(issue.getAdditionalProperties()).containsExactly(entry("maximum", "5")); assertThat(issue.getInputs()).isEmpty(); } @@ -205,30 +205,31 @@ void outOfRangeMinAndMaxNull() { @Test void referencedResourceNotFound() { InputValidationIssue issue = - InputValidationIssues.referencedResourceNotFound(InEnum.BODY, "test", "value"); + InputValidationIssues.referencedResourceNotFound(InEnum.BODY, "/test", "value"); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:referencedResourceNotFound"); assertThat(issue.getHref()) .hasToString( "https://www.belgif.be/specification/rest/api-guide/issues/referencedResourceNotFound.html"); assertThat(issue.getTitle()).isEqualTo("Referenced resource not found"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("test"); + assertThat(issue.getName()).isEqualTo("/test"); assertThat(issue.getValue()).isEqualTo("value"); - assertThat(issue.getDetail()).isEqualTo("Referenced resource test = 'value' does not exist"); + assertThat(issue.getDetail()).isEqualTo("Referenced resource /test = 'value' does not exist"); assertThat(issue).extracting("inputs", "additionalProperties").allMatch(this::isEmpty); } @Test void referencedResourceNotFoundDifferentResourceAndParameterName() { InputValidationIssue issue = - InputValidationIssues.referencedResourceNotFound(InEnum.BODY, "partners", "organization", "0123456789"); + InputValidationIssues.referencedResourceNotFound(InEnum.BODY, "/partners", "organization", + "0123456789"); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:referencedResourceNotFound"); assertThat(issue.getHref()) .hasToString( "https://www.belgif.be/specification/rest/api-guide/issues/referencedResourceNotFound.html"); assertThat(issue.getTitle()).isEqualTo("Referenced resource not found"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("partners"); + assertThat(issue.getName()).isEqualTo("/partners"); assertThat(issue.getValue()).isEqualTo("0123456789"); assertThat(issue.getDetail()).isEqualTo("Referenced resource organization = '0123456789' does not exist"); assertThat(issue).extracting("inputs", "additionalProperties").allMatch(this::isEmpty); @@ -237,7 +238,7 @@ void referencedResourceNotFoundDifferentResourceAndParameterName() { @Test void referencedResourceFromCollectionParameterNotFound() { InputValidationIssue issue = - InputValidationIssues.referencedResourceNotFound(InEnum.BODY, "partners", 123, + InputValidationIssues.referencedResourceNotFound(InEnum.BODY, "/partners", 123, Arrays.asList(1, 123, 3)); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:referencedResourceNotFound"); assertThat(issue.getHref()) @@ -245,17 +246,23 @@ void referencedResourceFromCollectionParameterNotFound() { "https://www.belgif.be/specification/rest/api-guide/issues/referencedResourceNotFound.html"); assertThat(issue.getTitle()).isEqualTo("Referenced resource not found"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("partners[1]"); + assertThat(issue.getName()).isEqualTo("/partners/1"); assertThat(issue.getValue()).isEqualTo(123); - assertThat(issue.getDetail()).isEqualTo("Referenced resource partners[1] = '123' does not exist"); + assertThat(issue.getDetail()).isEqualTo("Referenced resource /partners/1 = '123' does not exist"); assertThat(issue).extracting("inputs", "additionalProperties").allMatch(this::isEmpty); + + ProblemConfig.setJsonPointerEnabled(false); + issue = InputValidationIssues.referencedResourceNotFound(InEnum.BODY, "partners", 123, + Arrays.asList(1, 123, 3)); + assertThat(issue.getName()).isEqualTo("partners[1]"); + assertThat(issue.getDetail()).isEqualTo("Referenced resource partners[1] = '123' does not exist"); } @ParameterizedTest @MethodSource("toggleExtIssueTypes") void rejectedInput(boolean extIssueTypes) { InputValidationIssue issue = - InputValidationIssues.rejectedInput(InEnum.BODY, "test", "value"); + InputValidationIssues.rejectedInput(InEnum.BODY, "/test", "value"); if (extIssueTypes) { assertThat(issue.getType()).hasToString("urn:problem-type:belgif-ext:input-validation:rejectedInput"); assertThat(issue.getHref()) @@ -268,9 +275,9 @@ void rejectedInput(boolean extIssueTypes) { assertThat(issue.getTitle()).isEqualTo("Invalid input"); } assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("test"); + assertThat(issue.getName()).isEqualTo("/test"); assertThat(issue.getValue()).isEqualTo("value"); - assertThat(issue.getDetail()).isEqualTo("Input test is not allowed in this context"); + assertThat(issue.getDetail()).isEqualTo("Input /test is not allowed in this context"); assertThat(issue).extracting("inputs", "additionalProperties").allMatch(this::isEmpty); } @@ -278,7 +285,7 @@ void rejectedInput(boolean extIssueTypes) { @MethodSource("toggleExtIssueTypes") void requiredInput(boolean extIssueTypes) { InputValidationIssue issue = - InputValidationIssues.requiredInput(InEnum.BODY, "test"); + InputValidationIssues.requiredInput(InEnum.BODY, "/test"); if (extIssueTypes) { assertThat(issue.getType()).hasToString("urn:problem-type:belgif-ext:input-validation:requiredInput"); assertThat(issue.getHref()) @@ -291,9 +298,9 @@ void requiredInput(boolean extIssueTypes) { assertThat(issue.getTitle()).isEqualTo("Invalid input"); } assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("test"); + assertThat(issue.getName()).isEqualTo("/test"); assertThat(issue.getValue()).isNull(); - assertThat(issue.getDetail()).isEqualTo("Input test is required in this context"); + assertThat(issue.getDetail()).isEqualTo("Input /test is required in this context"); assertThat(issue).extracting("inputs", "additionalProperties").allMatch(this::isEmpty); } @@ -330,11 +337,11 @@ void requiredInputsIfPresent(boolean extIssueTypes, boolean extInputsArray) { @Test void replacedSsin() { InputValidationIssue issue = - InputValidationIssues.replacedSsin(InEnum.BODY, "ssin", "00000000196", "00000000295"); + InputValidationIssues.replacedSsin(InEnum.BODY, "/ssin", "00000000196", "00000000295"); assertThat(issue.getType()).hasToString("urn:problem-type:cbss:input-validation:replacedSsin"); assertThat(issue.getTitle()).isEqualTo("SSIN has been replaced, use new SSIN"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("ssin"); + assertThat(issue.getName()).isEqualTo("/ssin"); assertThat(issue.getValue()).isEqualTo("00000000196"); assertThat(issue.getDetail()).isEqualTo("SSIN 00000000196 has been replaced by 00000000295"); assertThat(issue.getAdditionalProperties()).containsExactly(entry("replacedBy", "00000000295")); @@ -344,11 +351,11 @@ void replacedSsin() { @Test void replacedSsinInput() { InputValidationIssue issue = - InputValidationIssues.replacedSsin(Input.body("ssin", "00000000196"), "00000000295"); + InputValidationIssues.replacedSsin(Input.body("/ssin", "00000000196"), "00000000295"); assertThat(issue.getType()).hasToString("urn:problem-type:cbss:input-validation:replacedSsin"); assertThat(issue.getTitle()).isEqualTo("SSIN has been replaced, use new SSIN"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("ssin"); + assertThat(issue.getName()).isEqualTo("/ssin"); assertThat(issue.getValue()).isEqualTo("00000000196"); assertThat(issue.getDetail()).isEqualTo("SSIN 00000000196 has been replaced by 00000000295"); assertThat(issue.getAdditionalProperties()).containsExactly(entry("replacedBy", "00000000295")); @@ -358,12 +365,12 @@ void replacedSsinInput() { @Test void replacedSsinWithReplacedByHref() { InputValidationIssue issue = - InputValidationIssues.replacedSsin(InEnum.BODY, "ssin", "00000000196", "00000000295", + InputValidationIssues.replacedSsin(InEnum.BODY, "/ssin", "00000000196", "00000000295", URI.create("https://api.company.com/v1/employees?ssin=00000000295")); assertThat(issue.getType()).hasToString("urn:problem-type:cbss:input-validation:replacedSsin"); assertThat(issue.getTitle()).isEqualTo("SSIN has been replaced, use new SSIN"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("ssin"); + assertThat(issue.getName()).isEqualTo("/ssin"); assertThat(issue.getValue()).isEqualTo("00000000196"); assertThat(issue.getDetail()).isEqualTo("SSIN 00000000196 has been replaced by 00000000295"); assertThat(issue.getAdditionalProperties()).containsExactly( @@ -374,11 +381,11 @@ void replacedSsinWithReplacedByHref() { @Test void canceledSsin() { - InputValidationIssue issue = InputValidationIssues.canceledSsin(InEnum.BODY, "ssin", "00000000196"); + InputValidationIssue issue = InputValidationIssues.canceledSsin(InEnum.BODY, "/ssin", "00000000196"); assertThat(issue.getType()).hasToString("urn:problem-type:cbss:input-validation:canceledSsin"); assertThat(issue.getTitle()).isEqualTo("SSIN has been canceled"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("ssin"); + assertThat(issue.getName()).isEqualTo("/ssin"); assertThat(issue.getValue()).isEqualTo("00000000196"); assertThat(issue.getDetail()).isEqualTo("SSIN 00000000196 has been canceled"); assertThat(issue).extracting("href", "inputs", "additionalProperties").allMatch(this::isEmpty); @@ -386,11 +393,11 @@ void canceledSsin() { @Test void canceledSsinInput() { - InputValidationIssue issue = InputValidationIssues.canceledSsin(Input.body("ssin", "00000000196")); + InputValidationIssue issue = InputValidationIssues.canceledSsin(Input.body("/ssin", "00000000196")); assertThat(issue.getType()).hasToString("urn:problem-type:cbss:input-validation:canceledSsin"); assertThat(issue.getTitle()).isEqualTo("SSIN has been canceled"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("ssin"); + assertThat(issue.getName()).isEqualTo("/ssin"); assertThat(issue.getValue()).isEqualTo("00000000196"); assertThat(issue.getDetail()).isEqualTo("SSIN 00000000196 has been canceled"); assertThat(issue).extracting("href", "inputs", "additionalProperties").allMatch(this::isEmpty); @@ -399,7 +406,7 @@ void canceledSsinInput() { @ParameterizedTest @MethodSource("toggleExtIssueTypes") void invalidSsin(boolean extIssueTypes) { - InputValidationIssue issue = InputValidationIssues.invalidSsin(InEnum.BODY, "ssin", "00000000195"); + InputValidationIssue issue = InputValidationIssues.invalidSsin(InEnum.BODY, "/ssin", "00000000195"); if (extIssueTypes) { assertThat(issue.getType()).hasToString("urn:problem-type:belgif-ext:input-validation:invalidStructure"); assertThat(issue.getHref()) @@ -412,7 +419,7 @@ void invalidSsin(boolean extIssueTypes) { assertThat(issue.getTitle()).isEqualTo("Invalid input"); } assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("ssin"); + assertThat(issue.getName()).isEqualTo("/ssin"); assertThat(issue.getValue()).isEqualTo("00000000195"); assertThat(issue.getDetail()).isEqualTo("SSIN 00000000195 is invalid"); assertThat(issue).extracting("inputs", "additionalProperties").allMatch(this::isEmpty); @@ -421,7 +428,7 @@ void invalidSsin(boolean extIssueTypes) { @ParameterizedTest @MethodSource("toggleExtIssueTypes") void invalidSsinInput(boolean extIssueTypes) { - InputValidationIssue issue = InputValidationIssues.invalidSsin(Input.body("ssin", "00000000195")); + InputValidationIssue issue = InputValidationIssues.invalidSsin(Input.body("/ssin", "00000000195")); if (extIssueTypes) { assertThat(issue.getType()).hasToString("urn:problem-type:belgif-ext:input-validation:invalidStructure"); assertThat(issue.getHref()) @@ -434,7 +441,7 @@ void invalidSsinInput(boolean extIssueTypes) { assertThat(issue.getTitle()).isEqualTo("Invalid input"); } assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("ssin"); + assertThat(issue.getName()).isEqualTo("/ssin"); assertThat(issue.getValue()).isEqualTo("00000000195"); assertThat(issue.getDetail()).isEqualTo("SSIN 00000000195 is invalid"); assertThat(issue).extracting("inputs", "additionalProperties").allMatch(this::isEmpty); @@ -442,13 +449,13 @@ void invalidSsinInput(boolean extIssueTypes) { @Test void unknownSsin() { - InputValidationIssue issue = InputValidationIssues.unknownSsin(InEnum.BODY, "ssin", "00000000196"); + InputValidationIssue issue = InputValidationIssues.unknownSsin(InEnum.BODY, "/ssin", "00000000196"); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:referencedResourceNotFound"); assertThat(issue.getHref()).hasToString( "https://www.belgif.be/specification/rest/api-guide/issues/referencedResourceNotFound.html"); assertThat(issue.getTitle()).isEqualTo("Referenced resource not found"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("ssin"); + assertThat(issue.getName()).isEqualTo("/ssin"); assertThat(issue.getValue()).isEqualTo("00000000196"); assertThat(issue.getDetail()).isEqualTo("SSIN 00000000196 does not exist"); assertThat(issue).extracting("inputs", "additionalProperties").allMatch(this::isEmpty); @@ -456,13 +463,13 @@ void unknownSsin() { @Test void unknownSsinInput() { - InputValidationIssue issue = InputValidationIssues.unknownSsin(Input.body("ssin", "00000000196")); + InputValidationIssue issue = InputValidationIssues.unknownSsin(Input.body("/ssin", "00000000196")); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:referencedResourceNotFound"); assertThat(issue.getHref()).hasToString( "https://www.belgif.be/specification/rest/api-guide/issues/referencedResourceNotFound.html"); assertThat(issue.getTitle()).isEqualTo("Referenced resource not found"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("ssin"); + assertThat(issue.getName()).isEqualTo("/ssin"); assertThat(issue.getValue()).isEqualTo("00000000196"); assertThat(issue.getDetail()).isEqualTo("SSIN 00000000196 does not exist"); assertThat(issue).extracting("inputs", "additionalProperties").allMatch(this::isEmpty); @@ -472,7 +479,7 @@ void unknownSsinInput() { @MethodSource("toggleExtIssueTypes") void invalidPeriod(boolean extIssueTypes) { InputValidationIssue issue = - InputValidationIssues.invalidPeriod(InEnum.BODY, "period", "value"); + InputValidationIssues.invalidPeriod(InEnum.BODY, "/period", "value"); if (extIssueTypes) { assertThat(issue.getType()).hasToString("urn:problem-type:belgif-ext:input-validation:invalidPeriod"); assertThat(issue.getHref()) @@ -485,7 +492,7 @@ void invalidPeriod(boolean extIssueTypes) { assertThat(issue.getTitle()).isEqualTo("Invalid input"); } assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("period"); + assertThat(issue.getName()).isEqualTo("/period"); assertThat(issue.getValue()).isEqualTo("value"); assertThat(issue.getDetail()).isEqualTo("endDate should not precede startDate"); assertThat(issue).extracting("inputs", "additionalProperties").allMatch(this::isEmpty); @@ -551,7 +558,7 @@ void invalidPeriodOffsetDateTime(boolean extIssueTypes, boolean extInputsArray) @MethodSource("toggleExtIssueTypes") void invalidIncompleteDate(boolean extIssueTypes) { InputValidationIssue issue = - InputValidationIssues.invalidIncompleteDate(InEnum.BODY, "test", "2024-00-01"); + InputValidationIssues.invalidIncompleteDate(InEnum.BODY, "/test", "2024-00-01"); if (extIssueTypes) { assertThat(issue.getType()).hasToString("urn:problem-type:belgif-ext:input-validation:invalidStructure"); assertThat(issue.getHref()) @@ -564,7 +571,7 @@ void invalidIncompleteDate(boolean extIssueTypes) { assertThat(issue.getTitle()).isEqualTo("Invalid input"); } assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("test"); + assertThat(issue.getName()).isEqualTo("/test"); assertThat(issue.getValue()).isEqualTo("2024-00-01"); assertThat(issue.getDetail()).isEqualTo("Incomplete date 2024-00-01 is invalid"); assertThat(issue).extracting("inputs", "additionalProperties").allMatch(this::isEmpty); @@ -574,7 +581,7 @@ void invalidIncompleteDate(boolean extIssueTypes) { @MethodSource("toggleExtIssueTypes") void invalidYearMonth(boolean extIssueTypes) { InputValidationIssue issue = - InputValidationIssues.invalidYearMonth(InEnum.BODY, "test", "2024-13"); + InputValidationIssues.invalidYearMonth(InEnum.BODY, "/test", "2024-13"); if (extIssueTypes) { assertThat(issue.getType()).hasToString("urn:problem-type:belgif-ext:input-validation:invalidStructure"); assertThat(issue.getHref()) @@ -587,7 +594,7 @@ void invalidYearMonth(boolean extIssueTypes) { assertThat(issue.getTitle()).isEqualTo("Invalid input"); } assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("test"); + assertThat(issue.getName()).isEqualTo("/test"); assertThat(issue.getValue()).isEqualTo("2024-13"); assertThat(issue.getDetail()).isEqualTo("Year month 2024-13 is invalid"); assertThat(issue).extracting("inputs", "additionalProperties").allMatch(this::isEmpty); @@ -597,7 +604,7 @@ void invalidYearMonth(boolean extIssueTypes) { @MethodSource("toggleExtIssueTypes") void invalidEnterpriseNumber(boolean extIssueTypes) { InputValidationIssue issue = - InputValidationIssues.invalidEnterpriseNumber(InEnum.BODY, "test", "0000000001"); + InputValidationIssues.invalidEnterpriseNumber(InEnum.BODY, "/test", "0000000001"); if (extIssueTypes) { assertThat(issue.getType()).hasToString("urn:problem-type:belgif-ext:input-validation:invalidStructure"); assertThat(issue.getHref()) @@ -610,7 +617,7 @@ void invalidEnterpriseNumber(boolean extIssueTypes) { assertThat(issue.getTitle()).isEqualTo("Invalid input"); } assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("test"); + assertThat(issue.getName()).isEqualTo("/test"); assertThat(issue.getValue()).isEqualTo("0000000001"); assertThat(issue.getDetail()).isEqualTo("Enterprise number 0000000001 is invalid"); assertThat(issue).extracting("inputs", "additionalProperties").allMatch(this::isEmpty); @@ -620,7 +627,7 @@ void invalidEnterpriseNumber(boolean extIssueTypes) { @MethodSource("toggleExtIssueTypes") void invalidEstablishmentUnitNumber(boolean extIssueTypes) { InputValidationIssue issue = - InputValidationIssues.invalidEstablishmentUnitNumber(InEnum.BODY, "test", "0000000001"); + InputValidationIssues.invalidEstablishmentUnitNumber(InEnum.BODY, "/test", "0000000001"); if (extIssueTypes) { assertThat(issue.getType()).hasToString("urn:problem-type:belgif-ext:input-validation:invalidStructure"); assertThat(issue.getHref()) @@ -633,7 +640,7 @@ void invalidEstablishmentUnitNumber(boolean extIssueTypes) { assertThat(issue.getTitle()).isEqualTo("Invalid input"); } assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isEqualTo("test"); + assertThat(issue.getName()).isEqualTo("/test"); assertThat(issue.getValue()).isEqualTo("0000000001"); assertThat(issue.getDetail()).isEqualTo("Establishment unit number 0000000001 is invalid"); assertThat(issue).extracting("inputs", "additionalProperties").allMatch(this::isEmpty); @@ -782,9 +789,9 @@ void getHrefConcurrently() throws Exception { List> results = new ArrayList<>(threads); for (int i = 0; i < threads; i++) { results.add(executorService.submit(() -> { - assertThat(InputValidationIssues.schemaViolation(InEnum.BODY, "name", "value", "detail").getHref()) + assertThat(InputValidationIssues.schemaViolation(InEnum.BODY, "/name", "value", "detail").getHref()) .hasToString("https://www.belgif.be/specification/rest/api-guide/issues/schemaViolation.html"); - assertThat(InputValidationIssues.invalidInput(InEnum.BODY, "name", "value", "detail").getHref()) + assertThat(InputValidationIssues.invalidInput(InEnum.BODY, "/name", "value", "detail").getHref()) .hasToString("https://www.belgif.be/specification/rest/api-guide/issues/invalidInput.html"); return null; })); diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 0c174c05..7e389608 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -838,7 +838,7 @@ Default `true`. { "type": "urn:problem-type:belgif:input-validation:schemaViolation", "in": "body", - "name": "person/0/ssin", + "name": "/person/0/ssin", "detail": "An SSIN should be 11 digits long", "value": "1234" } From 1ac1c9cba1c97f8bb12f240c348fb270faaa2048 Mon Sep 17 00:00:00 2001 From: Sarah Kumwimba Date: Fri, 19 Jun 2026 11:34:46 +0200 Subject: [PATCH 3/9] fix pattern for backtracking issue --- .../belgif/rest/problem/api/InputValidationIssue.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java index 7d73e380..9477fdc0 100644 --- a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java +++ b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java @@ -50,10 +50,10 @@ public class InputValidationIssue { private static final String IN_NAME_VALUE_INVALID_FORMAT = "input name has an invalid format"; // e.g: /, field, /field, /field/0, /field/0/nested - private static final String JSON_POINTER_BASIC_REGEX = "\\/*[a-zA-Z0-9]*(\\/[a-zA-Z0-9]+)*"; + private static final String JSON_POINTER_BASIC_REGEX = "\\/*+[a-zA-Z0-9]*+(\\/[a-zA-Z0-9]+)*+"; // e.g: field, field[0], field[0].nested - private static final String JSON_PATH_REGEX = "[a-zA-Z0-9]+(\\[\\d+\\])*(\\.[a-zA-Z0-9]+(\\[\\d+\\])*)*"; + private static final String JSON_PATH_REGEX = "[a-zA-Z0-9]++(\\[\\d++\\])*+(\\.[a-zA-Z0-9]++(\\[\\d++\\])*+)*+"; private URI type; private URI href; @@ -481,7 +481,8 @@ private static boolean nameMatchesJsonPointerFormat(InEnum in, String name) { return name.matches(JSON_POINTER_BASIC_REGEX) && (in != InEnum.BODY || name.startsWith("/")) // if for body, the name must start with "/" && !name.matches(".*\\/\\d+\\/\\d+\\/*") // not two indexes following each other (e.g: person/1/2) - && !name.matches("\\/*\\d+(\\/[a-zA-Z0-9]+)*"); // not starting with an index (e.g: /1/person, 1/person) + && !name.matches("\\/*+\\d++(\\/[a-zA-Z0-9]++)*+"); // not starting with an index (e.g: /1/person, + // 1/person) } private static boolean nameMatchesJsonPathFormat(String name) { From 27952fea0e293620a393f3c5cc90034eee1329a7 Mon Sep 17 00:00:00 2001 From: Sarah Kumwimba Date: Fri, 19 Jun 2026 12:40:22 +0200 Subject: [PATCH 4/9] also allow hyphens (for headers) --- .../belgif/rest/problem/api/InputValidationIssue.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java index 9477fdc0..ca38a728 100644 --- a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java +++ b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java @@ -50,10 +50,10 @@ public class InputValidationIssue { private static final String IN_NAME_VALUE_INVALID_FORMAT = "input name has an invalid format"; // e.g: /, field, /field, /field/0, /field/0/nested - private static final String JSON_POINTER_BASIC_REGEX = "\\/*+[a-zA-Z0-9]*+(\\/[a-zA-Z0-9]+)*+"; + private static final String JSON_POINTER_BASIC_REGEX = "\\/*+[a-zA-Z0-9-]*+(\\/[a-zA-Z0-9-]+)*+"; // e.g: field, field[0], field[0].nested - private static final String JSON_PATH_REGEX = "[a-zA-Z0-9]++(\\[\\d++\\])*+(\\.[a-zA-Z0-9]++(\\[\\d++\\])*+)*+"; + private static final String JSON_PATH_REGEX = "[a-zA-Z0-9-]++(\\[\\d++\\])*+(\\.[a-zA-Z0-9-]++(\\[\\d++\\])*+)*+"; private URI type; private URI href; @@ -200,7 +200,7 @@ public static void verifyNameFormat(InEnum in, String name) { return; } - if ((!ProblemConfig.isJsonPointerEnabled() && !nameMatchesJsonPathFormat(name)) + if ((!ProblemConfig.isJsonPointerEnabled() && !nameMatchesJsonPathFormat(in, name)) || (ProblemConfig.isJsonPointerEnabled() && !nameMatchesJsonPointerFormat(in, name))) { throw new IllegalArgumentException( From 091aac20a85d86da42db447b27dae16f96b300c9 Mon Sep 17 00:00:00 2001 From: Sarah Kumwimba Date: Fri, 19 Jun 2026 13:58:20 +0200 Subject: [PATCH 5/9] fix method call --- .../io/github/belgif/rest/problem/api/InputValidationIssue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java index ca38a728..271eb4dd 100644 --- a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java +++ b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java @@ -200,7 +200,7 @@ public static void verifyNameFormat(InEnum in, String name) { return; } - if ((!ProblemConfig.isJsonPointerEnabled() && !nameMatchesJsonPathFormat(in, name)) + if ((!ProblemConfig.isJsonPointerEnabled() && !nameMatchesJsonPathFormat(name)) || (ProblemConfig.isJsonPointerEnabled() && !nameMatchesJsonPointerFormat(in, name))) { throw new IllegalArgumentException( From be6d5ba998a0eaddd1a364968c341054223c2c48 Mon Sep 17 00:00:00 2001 From: Sarah Kumwimba Date: Mon, 22 Jun 2026 11:41:09 +0200 Subject: [PATCH 6/9] some refactoring --- .../BeanValidationExceptionsHandler.java | 4 ++-- .../validation/AbstractRequestValidator.java | 15 ++++++++------- .../problem/api/InputValidationIssue.java | 14 ++++++++++---- .../problem/api/InputValidationIssues.java | 2 +- .../problem/api/InputValidationIssueTest.java | 19 +++++++++++++++++++ 5 files changed, 40 insertions(+), 14 deletions(-) diff --git a/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/server/BeanValidationExceptionsHandler.java b/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/server/BeanValidationExceptionsHandler.java index deda8280..8d5b2346 100644 --- a/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/server/BeanValidationExceptionsHandler.java +++ b/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/server/BeanValidationExceptionsHandler.java @@ -84,8 +84,8 @@ public ResponseEntity handleBindException(BindException exception, Serv : name + " of incorrect type"; String invalidValue = (String) exception.getValue(); return ProblemMediaType.INSTANCE - .toResponse(new BadRequestProblem(InputValidationIssues.schemaViolation(in, name, invalidValue, - detail))); + .toResponse(new BadRequestProblem(InputValidationIssues.schemaViolation(in, + InputValidationIssue.convertName(in, name), invalidValue, detail))); } @ExceptionHandler(HandlerMethodValidationException.class) diff --git a/belgif-rest-problem-validator/src/main/java/io/github/belgif/rest/problem/validation/AbstractRequestValidator.java b/belgif-rest-problem-validator/src/main/java/io/github/belgif/rest/problem/validation/AbstractRequestValidator.java index 7806ef9c..176fc13b 100644 --- a/belgif-rest-problem-validator/src/main/java/io/github/belgif/rest/problem/validation/AbstractRequestValidator.java +++ b/belgif-rest-problem-validator/src/main/java/io/github/belgif/rest/problem/validation/AbstractRequestValidator.java @@ -1,5 +1,7 @@ package io.github.belgif.rest.problem.validation; +import static io.github.belgif.rest.problem.api.InputValidationIssue.*; + import java.time.LocalDate; import java.time.temporal.Temporal; import java.util.ArrayList; @@ -116,8 +118,8 @@ public SELF ssins(Input> ssins) { if (ssins != null && ssins.getValue() != null) { int index = 0; for (String ssin : ssins.getValue()) { - String indexFormat = getIndexFormat(index); - ssin(new Input<>(ssins.getIn(), ssins.getName() + indexFormat, ssin)); + String name = convertName(ssins.getIn(), ssins.getName() + getIndexFormat(index)); + ssin(new Input<>(ssins.getIn(), name, ssin)); index++; } } @@ -360,8 +362,8 @@ public SELF refDatas(Input> input, Supplier> allowedRe Collection allowedRefData = allowedRefDataSupplier.get(); int index = 0; for (T value : input.getValue()) { - String indexFormat = getIndexFormat(index); - refData(new Input(input.getIn(), input.getName() + indexFormat, value), allowedRefData); + String name = convertName(input.getIn(), input.getName() + getIndexFormat(index)); + refData(new Input(input.getIn(), name, value), allowedRefData); index++; } } @@ -380,9 +382,8 @@ public SELF refDatas(Input> input, Predicate allowedRefDataPredic if (input != null && input.getValue() != null && !input.getValue().isEmpty()) { int index = 0; for (T value : input.getValue()) { - String indexFormat = getIndexFormat(index); - refData(new Input(input.getIn(), input.getName() + indexFormat, value), - allowedRefDataPredicate); + String name = convertName(input.getIn(), input.getName() + getIndexFormat(index)); + refData(new Input(input.getIn(), name, value), allowedRefDataPredicate); index++; } } diff --git a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java index 271eb4dd..ab62629a 100644 --- a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java +++ b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java @@ -50,7 +50,7 @@ public class InputValidationIssue { private static final String IN_NAME_VALUE_INVALID_FORMAT = "input name has an invalid format"; // e.g: /, field, /field, /field/0, /field/0/nested - private static final String JSON_POINTER_BASIC_REGEX = "\\/*+[a-zA-Z0-9-]*+(\\/[a-zA-Z0-9-]+)*+"; + private static final String JSON_POINTER_BASIC_REGEX = "\\/*+[a-zA-Z0-9-]*+(\\/[a-zA-Z0-9-]++)*+"; // e.g: field, field[0], field[0].nested private static final String JSON_PATH_REGEX = "[a-zA-Z0-9-]++(\\[\\d++\\])*+(\\.[a-zA-Z0-9-]++(\\[\\d++\\])*+)*+"; @@ -480,7 +480,7 @@ public String toString() { private static boolean nameMatchesJsonPointerFormat(InEnum in, String name) { return name.matches(JSON_POINTER_BASIC_REGEX) && (in != InEnum.BODY || name.startsWith("/")) // if for body, the name must start with "/" - && !name.matches(".*\\/\\d+\\/\\d+\\/*") // not two indexes following each other (e.g: person/1/2) + && !name.matches(".*\\/\\d+\\/\\d+\\/*+") // not two indexes following each other (e.g: person/1/2) && !name.matches("\\/*+\\d++(\\/[a-zA-Z0-9]++)*+"); // not starting with an index (e.g: /1/person, // 1/person) } @@ -489,6 +489,12 @@ private static boolean nameMatchesJsonPathFormat(String name) { return name.matches(JSON_PATH_REGEX); } + /** + * + * @param in the issue place in the query + * @param nameJsonPath the name in JsonPath syntax + * @return the name converted to JsonPointer syntax + */ public static String convertName(InEnum in, String nameJsonPath) { if (nameJsonPath == null || nameJsonPath.trim().isEmpty()) { @@ -498,7 +504,7 @@ public static String convertName(InEnum in, String nameJsonPath) { } else { // replace all indexes "[X]" by "/X" and replace all "." by "/" String convertedName = replaceSquareBrackets(nameJsonPath).replace(".", "/"); - return in == InEnum.BODY ? "/" + convertedName : convertedName; + return in == InEnum.BODY && convertedName.charAt(0) != '/' ? "/" + convertedName : convertedName; } } @@ -516,7 +522,7 @@ public static String getNameFromProperties(InEnum in, List propertiesNam private static String replaceSquareBrackets(String propertyName) { // replace all indexes "[X]" by "/X" - return propertyName.replaceAll("\\[(\\d+)\\]", "/$1"); + return propertyName.replaceAll("\\[(\\d++)\\]", "/$1"); } } diff --git a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssues.java b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssues.java index ae8fe088..d72df974 100644 --- a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssues.java +++ b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssues.java @@ -142,7 +142,7 @@ public static InputValidationIssue referencedResourceNotFound(InEnum in, Str String indexFormat = ProblemConfig.isJsonPointerEnabled() ? ("/" + source.indexOf(value)) : ("[" + source.indexOf(value) + "]"); String nameWithIndex = name + indexFormat; - return referencedResourceNotFound(in, nameWithIndex, value); + return referencedResourceNotFound(in, InputValidationIssue.convertName(in, nameWithIndex), value); } public static InputValidationIssue rejectedInput(InEnum in, String name, Object value) { diff --git a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssueTest.java b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssueTest.java index fb9ec540..3a0c3ebe 100644 --- a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssueTest.java +++ b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssueTest.java @@ -333,6 +333,15 @@ void convertName(InEnum in) { .isEqualTo(in == InEnum.BODY ? "/field/0" : "field/0"); assertThat(InputValidationIssue.convertName(in, "field[0].nested")) .isEqualTo(in == InEnum.BODY ? "/field/0/nested" : "field/0/nested"); + assertThat(InputValidationIssue.convertName(in, "field/0")) + .isEqualTo(in == InEnum.BODY ? "/field/0" : "field/0"); + assertThat(InputValidationIssue.convertName(in, "field/0/nested")) + .isEqualTo(in == InEnum.BODY ? "/field/0/nested" : "field/0/nested"); + } + + @Test + void convertNameSlashAlreadyForBodyParam() { + assertThat(InputValidationIssue.convertName(InEnum.BODY, "/field")).isEqualTo("/field"); } @ParameterizedTest @@ -369,6 +378,10 @@ void getNameFromProperties(InEnum in) { properties.add("nested"); assertThat(InputValidationIssue.getNameFromProperties(in, properties)) .isEqualTo(in == InEnum.BODY ? "/field/0/nested" : "field/0/nested"); + + properties.add("nestedAgain[1]"); + assertThat(InputValidationIssue.getNameFromProperties(in, properties)) + .isEqualTo(in == InEnum.BODY ? "/field/0/nested/nestedAgain/1" : "field/0/nested/nestedAgain/1"); } @ParameterizedTest @@ -408,6 +421,12 @@ void nameMatchingJsonPointerFormatInConstructor(InEnum in) { assertThatIllegalArgumentException().isThrownBy(() -> new InputValidationIssue(in, prefix + "field/0/1")) .withMessageContaining("format").withMessageContaining("JsonPointer"); + assertThatIllegalArgumentException().isThrownBy(() -> new InputValidationIssue(in, prefix + "/0/nested")) + .withMessageContaining("format").withMessageContaining("JsonPointer"); + + assertThatIllegalArgumentException().isThrownBy(() -> new InputValidationIssue(in, prefix + "0/nested")) + .withMessageContaining("format").withMessageContaining("JsonPointer"); + assertThatIllegalArgumentException().isThrownBy(() -> new InputValidationIssue(in, prefix + "field[0]")) .withMessageContaining("format").withMessageContaining("JsonPointer"); assertThatIllegalArgumentException() From 18d37f0a4fd0cd955bef57816a7c80382e8b0808 Mon Sep 17 00:00:00 2001 From: Sarah Kumwimba Date: Tue, 23 Jun 2026 15:43:43 +0200 Subject: [PATCH 7/9] allow dots in name --- .../problem/api/InputValidationIssue.java | 28 +++-- .../problem/api/InputValidationIssueTest.java | 114 +++++++++++++----- 2 files changed, 99 insertions(+), 43 deletions(-) diff --git a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java index ab62629a..b9705c2f 100644 --- a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java +++ b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java @@ -50,10 +50,11 @@ public class InputValidationIssue { private static final String IN_NAME_VALUE_INVALID_FORMAT = "input name has an invalid format"; // e.g: /, field, /field, /field/0, /field/0/nested - private static final String JSON_POINTER_BASIC_REGEX = "\\/*+[a-zA-Z0-9-]*+(\\/[a-zA-Z0-9-]++)*+"; + private static final String JSON_POINTER_BASIC_REGEX = "\\/*+[a-zA-Z0-9-.]*+(\\/[a-zA-Z0-9-.]++)*+"; // e.g: field, field[0], field[0].nested - private static final String JSON_PATH_REGEX = "[a-zA-Z0-9-]++(\\[\\d++\\])*+(\\.[a-zA-Z0-9-]++(\\[\\d++\\])*+)*+"; + private static final String JSON_PATH_BASIC_REGEX = + "[a-zA-Z0-9-]++(\\[\\d++\\])*+(\\.[a-zA-Z0-9-]++(\\[\\d++\\])*+)*+"; private URI type; private URI href; @@ -200,7 +201,7 @@ public static void verifyNameFormat(InEnum in, String name) { return; } - if ((!ProblemConfig.isJsonPointerEnabled() && !nameMatchesJsonPathFormat(name)) + if ((!ProblemConfig.isJsonPointerEnabled() && !nameMatchesJsonPathFormat(in, name)) || (ProblemConfig.isJsonPointerEnabled() && !nameMatchesJsonPointerFormat(in, name))) { throw new IllegalArgumentException( @@ -481,12 +482,12 @@ private static boolean nameMatchesJsonPointerFormat(InEnum in, String name) { return name.matches(JSON_POINTER_BASIC_REGEX) && (in != InEnum.BODY || name.startsWith("/")) // if for body, the name must start with "/" && !name.matches(".*\\/\\d+\\/\\d+\\/*+") // not two indexes following each other (e.g: person/1/2) - && !name.matches("\\/*+\\d++(\\/[a-zA-Z0-9]++)*+"); // not starting with an index (e.g: /1/person, - // 1/person) + && !name.matches("\\/*+\\d++(\\/[a-zA-Z0-9-.]++)*+"); // not starting with an index (e.g: /1/person, + // 1/person) } - private static boolean nameMatchesJsonPathFormat(String name) { - return name.matches(JSON_PATH_REGEX); + private static boolean nameMatchesJsonPathFormat(InEnum in, String name) { + return in == InEnum.BODY ? name.matches(JSON_PATH_BASIC_REGEX) : name.matches("[a-zA-Z0-9-.]++(\\[\\d++\\])*+"); } /** @@ -501,15 +502,23 @@ public static String convertName(InEnum in, String nameJsonPath) { return null; } else if (!ProblemConfig.isJsonPointerEnabled()) { return nameJsonPath; + } else if (in != InEnum.BODY) { + // replace all indexes "[X]" by "/X" + return replaceSquareBrackets(nameJsonPath); } else { // replace all indexes "[X]" by "/X" and replace all "." by "/" String convertedName = replaceSquareBrackets(nameJsonPath).replace(".", "/"); - return in == InEnum.BODY && convertedName.charAt(0) != '/' ? "/" + convertedName : convertedName; + return convertedName.charAt(0) != '/' ? "/" + convertedName : convertedName; } } public static String getNameFromProperties(InEnum in, List propertiesName) { + if (in != InEnum.BODY && propertiesName != null && propertiesName.size() > 1) { + throw new IllegalArgumentException( + "This method should only be used with several properties for issues located in the body"); + } + if (propertiesName == null || propertiesName.isEmpty()) { return null; } @@ -517,12 +526,11 @@ public static String getNameFromProperties(InEnum in, List propertiesNam String name = ProblemConfig.isJsonPointerEnabled() ? propertiesName.stream() .map(InputValidationIssue::replaceSquareBrackets).collect(Collectors.joining("/")) : String.join(".", propertiesName); - return ProblemConfig.isJsonPointerEnabled() && in == InEnum.BODY ? "/" + name : name; + return in == InEnum.BODY && ProblemConfig.isJsonPointerEnabled() ? "/" + name : name; } private static String replaceSquareBrackets(String propertyName) { // replace all indexes "[X]" by "/X" return propertyName.replaceAll("\\[(\\d++)\\]", "/$1"); } - } diff --git a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssueTest.java b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssueTest.java index 3a0c3ebe..42e11f94 100644 --- a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssueTest.java +++ b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssueTest.java @@ -332,7 +332,7 @@ void convertName(InEnum in) { assertThat(InputValidationIssue.convertName(in, "field[0]")) .isEqualTo(in == InEnum.BODY ? "/field/0" : "field/0"); assertThat(InputValidationIssue.convertName(in, "field[0].nested")) - .isEqualTo(in == InEnum.BODY ? "/field/0/nested" : "field/0/nested"); + .isEqualTo(in == InEnum.BODY ? "/field/0/nested" : "field/0.nested"); assertThat(InputValidationIssue.convertName(in, "field/0")) .isEqualTo(in == InEnum.BODY ? "/field/0" : "field/0"); assertThat(InputValidationIssue.convertName(in, "field/0/nested")) @@ -358,38 +358,31 @@ void convertNameWithJsonPointerDisabled(InEnum in) { assertThat(InputValidationIssue.convertName(in, "field[0].nested")).isEqualTo("field[0].nested"); } - @ParameterizedTest - @EnumSource(InEnum.class) - void getNameFromProperties(InEnum in) { + @Test + void getNameFromProperties() { List properties = null; - assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isNull(); + assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)).isNull(); properties = new ArrayList<>(); - assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isNull(); + assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)).isNull(); properties.add("field"); - assertThat(InputValidationIssue.getNameFromProperties(in, properties)) - .isEqualTo(in == InEnum.BODY ? "/field" : "field"); + assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)).isEqualTo("/field"); properties.set(0, "field[0]"); - assertThat(InputValidationIssue.getNameFromProperties(in, properties)) - .isEqualTo(in == InEnum.BODY ? "/field/0" : "field/0"); + assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)).isEqualTo("/field/0"); properties.add("nested"); - assertThat(InputValidationIssue.getNameFromProperties(in, properties)) - .isEqualTo(in == InEnum.BODY ? "/field/0/nested" : "field/0/nested"); + assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)).isEqualTo("/field/0/nested"); properties.add("nestedAgain[1]"); - assertThat(InputValidationIssue.getNameFromProperties(in, properties)) - .isEqualTo(in == InEnum.BODY ? "/field/0/nested/nestedAgain/1" : "field/0/nested/nestedAgain/1"); + assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)) + .isEqualTo("/field/0/nested/nestedAgain/1"); } @ParameterizedTest - @EnumSource(InEnum.class) - void getNameFromPropertiesJsonPointerDisabled(InEnum in) { - - ProblemConfig.setJsonPointerEnabled(false); - + @EnumSource(value = InEnum.class, names = { "BODY" }, mode = EnumSource.Mode.EXCLUDE) + void getNameFromPropertiesNotBody(InEnum in) { List properties = null; assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isNull(); @@ -400,10 +393,45 @@ void getNameFromPropertiesJsonPointerDisabled(InEnum in) { assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isEqualTo("field"); properties.set(0, "field[0]"); - assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isEqualTo("field[0]"); + assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isEqualTo("field/0"); + } + + @Test + void getNameFromPropertiesJsonPointerDisabled() { + + ProblemConfig.setJsonPointerEnabled(false); + + List properties = null; + assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)).isNull(); + + properties = new ArrayList<>(); + assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)).isNull(); + + properties.add("field"); + assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)).isEqualTo("field"); + + properties.set(0, "field[0]"); + assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)).isEqualTo("field[0]"); properties.add("nested"); - assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isEqualTo("field[0].nested"); + assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)).isEqualTo("field[0].nested"); + } + + @ParameterizedTest + @EnumSource(value = InEnum.class, names = { "BODY" }, mode = EnumSource.Mode.EXCLUDE) + void getNameFromPropertiesIllegalArgument() { + assertThatIllegalArgumentException() + .isThrownBy(() -> InputValidationIssue.getNameFromProperties(InEnum.QUERY, + Arrays.asList("field", "nested"))) + .withMessageContaining("located in the body"); + assertThatIllegalArgumentException() + .isThrownBy( + () -> InputValidationIssue.getNameFromProperties(InEnum.PATH, Arrays.asList("field", "nested"))) + .withMessageContaining("located in the body"); + assertThatIllegalArgumentException() + .isThrownBy(() -> InputValidationIssue.getNameFromProperties(InEnum.HEADER, + Arrays.asList("field", "nested"))) + .withMessageContaining("located in the body"); } @ParameterizedTest @@ -417,42 +445,62 @@ void nameMatchingJsonPointerFormatInConstructor(InEnum in) { assertDoesNotThrow(() -> new InputValidationIssue(in, prefix + "field/0", "value")); assertDoesNotThrow(() -> new InputValidationIssue(in, prefix + "field/0/nested/2/nestedAgain")); assertDoesNotThrow(() -> new InputValidationIssue(in, prefix + "field/0/nested/2/nestedAgain", "value")); + } - assertThatIllegalArgumentException().isThrownBy(() -> new InputValidationIssue(in, prefix + "field/0/1")) + @Test + void nameInBodyMatchingJsonPointerFormatInConstructorSyntaxFault() { + assertThatIllegalArgumentException().isThrownBy(() -> new InputValidationIssue(InEnum.BODY, "field/0/1")) .withMessageContaining("format").withMessageContaining("JsonPointer"); - assertThatIllegalArgumentException().isThrownBy(() -> new InputValidationIssue(in, prefix + "/0/nested")) + assertThatIllegalArgumentException().isThrownBy(() -> new InputValidationIssue(InEnum.BODY, "/0/nested")) .withMessageContaining("format").withMessageContaining("JsonPointer"); - assertThatIllegalArgumentException().isThrownBy(() -> new InputValidationIssue(in, prefix + "0/nested")) + assertThatIllegalArgumentException().isThrownBy(() -> new InputValidationIssue(InEnum.BODY, "0/nested")) .withMessageContaining("format").withMessageContaining("JsonPointer"); - assertThatIllegalArgumentException().isThrownBy(() -> new InputValidationIssue(in, prefix + "field[0]")) + assertThatIllegalArgumentException().isThrownBy(() -> new InputValidationIssue(InEnum.BODY, "field[0]")) .withMessageContaining("format").withMessageContaining("JsonPointer"); assertThatIllegalArgumentException() - .isThrownBy(() -> new InputValidationIssue(in, prefix + "field[0]", "value")) + .isThrownBy(() -> new InputValidationIssue(InEnum.BODY, "field[0]", "value")) .withMessageContaining("format").withMessageContaining("JsonPointer"); assertThatIllegalArgumentException() - .isThrownBy(() -> new InputValidationIssue(in, prefix + "field[0].nested[2]/nestedAgain")) + .isThrownBy(() -> new InputValidationIssue(InEnum.BODY, "field[0].nested[2]/nestedAgain")) .withMessageContaining("format").withMessageContaining("JsonPointer"); assertThatIllegalArgumentException() - .isThrownBy(() -> new InputValidationIssue(in, prefix + "field[0].nested[2]/nestedAgain", "value")) + .isThrownBy(() -> new InputValidationIssue(InEnum.BODY, "field[0].nested[2]/nestedAgain", "value")) .withMessageContaining("format").withMessageContaining("JsonPointer"); } - @ParameterizedTest - @EnumSource(InEnum.class) - void nameMatchingJsonPathFormatInConstructor(InEnum in) { + @Test + void nameInBodyMatchingJsonPathFormatInConstructor() { ProblemConfig.setJsonPointerEnabled(false); JsonPointer.compile("/field[0]"); + assertDoesNotThrow(() -> new InputValidationIssue(InEnum.BODY, "field")); + assertDoesNotThrow(() -> new InputValidationIssue(InEnum.BODY, "field", "value")); + assertDoesNotThrow(() -> new InputValidationIssue(InEnum.BODY, "field[0]")); + assertDoesNotThrow(() -> new InputValidationIssue(InEnum.BODY, "field[0]", "value")); + assertDoesNotThrow(() -> new InputValidationIssue(InEnum.BODY, "field[0].nested[2].nestedAgain")); + assertDoesNotThrow(() -> new InputValidationIssue(InEnum.BODY, "field[0].nested[2].nestedAgain", "value")); + } + + @ParameterizedTest + @EnumSource(value = InEnum.class, names = { "BODY" }, mode = EnumSource.Mode.EXCLUDE) + void nameNotInBodyMatchingJsonPathFormatInConstructor(InEnum in) { + ProblemConfig.setJsonPointerEnabled(false); + assertDoesNotThrow(() -> new InputValidationIssue(in, "field")); assertDoesNotThrow(() -> new InputValidationIssue(in, "field", "value")); assertDoesNotThrow(() -> new InputValidationIssue(in, "field[0]")); assertDoesNotThrow(() -> new InputValidationIssue(in, "field[0]", "value")); - assertDoesNotThrow(() -> new InputValidationIssue(in, "field[0].nested[2].nestedAgain")); - assertDoesNotThrow(() -> new InputValidationIssue(in, "field[0].nested[2].nestedAgain", "value")); + assertDoesNotThrow(() -> new InputValidationIssue(in, "field.id[0]", "value")); + } + + @ParameterizedTest + @EnumSource(InEnum.class) + void nameMatchingJsonPathFormatInConstructorSyntaxFault(InEnum in) { + ProblemConfig.setJsonPointerEnabled(false); assertThatIllegalArgumentException().isThrownBy(() -> new InputValidationIssue(in, "field/0")) .withMessageContaining("format").withMessageContaining("JsonPath"); From 939815770bcb95fd1c4d7ba3c036737cdc24c528 Mon Sep 17 00:00:00 2001 From: Sarah Kumwimba Date: Tue, 23 Jun 2026 21:06:22 +0200 Subject: [PATCH 8/9] Use jsonPointer only for body --- .../validation/AbstractRequestValidator.java | 11 ++++++----- .../validation/RequestValidatorTest.java | 18 ++++++------------ .../problem/api/InputValidationIssue.java | 19 +++++++------------ .../problem/api/InputValidationIssues.java | 2 +- .../problem/api/InputValidationIssueTest.java | 9 +++++---- 5 files changed, 25 insertions(+), 34 deletions(-) diff --git a/belgif-rest-problem-validator/src/main/java/io/github/belgif/rest/problem/validation/AbstractRequestValidator.java b/belgif-rest-problem-validator/src/main/java/io/github/belgif/rest/problem/validation/AbstractRequestValidator.java index 176fc13b..1a3fd874 100644 --- a/belgif-rest-problem-validator/src/main/java/io/github/belgif/rest/problem/validation/AbstractRequestValidator.java +++ b/belgif-rest-problem-validator/src/main/java/io/github/belgif/rest/problem/validation/AbstractRequestValidator.java @@ -15,6 +15,7 @@ import java.util.stream.Collectors; import io.github.belgif.rest.problem.BadRequestProblem; +import io.github.belgif.rest.problem.api.InEnum; import io.github.belgif.rest.problem.api.Input; import io.github.belgif.rest.problem.api.InputValidationIssue; import io.github.belgif.rest.problem.config.ProblemConfig; @@ -118,7 +119,7 @@ public SELF ssins(Input> ssins) { if (ssins != null && ssins.getValue() != null) { int index = 0; for (String ssin : ssins.getValue()) { - String name = convertName(ssins.getIn(), ssins.getName() + getIndexFormat(index)); + String name = convertName(ssins.getIn(), ssins.getName() + getIndexFormat(ssins.getIn(), index)); ssin(new Input<>(ssins.getIn(), name, ssin)); index++; } @@ -362,7 +363,7 @@ public SELF refDatas(Input> input, Supplier> allowedRe Collection allowedRefData = allowedRefDataSupplier.get(); int index = 0; for (T value : input.getValue()) { - String name = convertName(input.getIn(), input.getName() + getIndexFormat(index)); + String name = convertName(input.getIn(), input.getName() + getIndexFormat(input.getIn(), index)); refData(new Input(input.getIn(), name, value), allowedRefData); index++; } @@ -382,7 +383,7 @@ public SELF refDatas(Input> input, Predicate allowedRefDataPredic if (input != null && input.getValue() != null && !input.getValue().isEmpty()) { int index = 0; for (T value : input.getValue()) { - String name = convertName(input.getIn(), input.getName() + getIndexFormat(index)); + String name = convertName(input.getIn(), input.getName() + getIndexFormat(input.getIn(), index)); refData(new Input(input.getIn(), name, value), allowedRefDataPredicate); index++; } @@ -518,8 +519,8 @@ protected SELF getThis() { return (SELF) this; } - private String getIndexFormat(int index) { - return ProblemConfig.isJsonPointerEnabled() ? ("/" + index) : ("[" + index + "]"); + private String getIndexFormat(InEnum in, int index) { + return ProblemConfig.isJsonPointerEnabled() && in == InEnum.BODY ? ("/" + index) : ("[" + index + "]"); } } diff --git a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/RequestValidatorTest.java b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/RequestValidatorTest.java index 03139f9a..e867d1ed 100644 --- a/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/RequestValidatorTest.java +++ b/belgif-rest-problem-validator/src/test/java/io/github/belgif/rest/problem/validation/RequestValidatorTest.java @@ -339,10 +339,8 @@ void refDatasCollectionInvalid(boolean isJsonPointerEnabled) { ProblemConfig.setJsonPointerEnabled(isJsonPointerEnabled); assertInvalid(new RequestValidator().refDatas(Input.query("refDatas", Arrays.asList("a", "x", "b", "y")), Arrays.asList("a", "b", "c")), - InputValidationIssues.referencedResourceNotFound(QUERY, - isJsonPointerEnabled ? "refDatas/1" : "refDatas[1]", "x"), - InputValidationIssues.referencedResourceNotFound(QUERY, - isJsonPointerEnabled ? "refDatas/3" : "refDatas[3]", "y")); + InputValidationIssues.referencedResourceNotFound(QUERY, "refDatas[1]", "x"), + InputValidationIssues.referencedResourceNotFound(QUERY, "refDatas[3]", "y")); } @ParameterizedTest @@ -368,10 +366,8 @@ void refDatasSupplierInvalid(boolean isJsonPointerEnabled) { calls.incrementAndGet(); return Arrays.asList("a", "b", "c"); }), - InputValidationIssues.referencedResourceNotFound(QUERY, - isJsonPointerEnabled ? "refDatas/1" : "refDatas[1]", "x"), - InputValidationIssues.referencedResourceNotFound(QUERY, - isJsonPointerEnabled ? "refDatas/3" : "refDatas[3]", "y")); + InputValidationIssues.referencedResourceNotFound(QUERY, "refDatas[1]", "x"), + InputValidationIssues.referencedResourceNotFound(QUERY, "refDatas[3]", "y")); assertThat(calls).hasValue(1); } @@ -399,10 +395,8 @@ void refDatasPredicateInvalid(boolean isJsonPointerEnabled) { ProblemConfig.setJsonPointerEnabled(isJsonPointerEnabled); assertInvalid(new RequestValidator().refDatas(Input.query("refDatas", Arrays.asList("a", "x", "b", "y")), Arrays.asList("a", "b", "c")::contains), - InputValidationIssues.referencedResourceNotFound(QUERY, - isJsonPointerEnabled ? "refDatas/1" : "refDatas[1]", "x"), - InputValidationIssues.referencedResourceNotFound(QUERY, - isJsonPointerEnabled ? "refDatas/3" : "refDatas[3]", "y")); + InputValidationIssues.referencedResourceNotFound(QUERY, "refDatas[1]", "x"), + InputValidationIssues.referencedResourceNotFound(QUERY, "refDatas[3]", "y")); } @Test diff --git a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java index b9705c2f..92cf1bcf 100644 --- a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java +++ b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java @@ -50,7 +50,7 @@ public class InputValidationIssue { private static final String IN_NAME_VALUE_INVALID_FORMAT = "input name has an invalid format"; // e.g: /, field, /field, /field/0, /field/0/nested - private static final String JSON_POINTER_BASIC_REGEX = "\\/*+[a-zA-Z0-9-.]*+(\\/[a-zA-Z0-9-.]++)*+"; + private static final String JSON_POINTER_BASIC_REGEX = "\\/+[a-zA-Z0-9-]*+(\\/[a-zA-Z0-9-]++)*+"; // e.g: field, field[0], field[0].nested private static final String JSON_PATH_BASIC_REGEX = @@ -201,8 +201,8 @@ public static void verifyNameFormat(InEnum in, String name) { return; } - if ((!ProblemConfig.isJsonPointerEnabled() && !nameMatchesJsonPathFormat(in, name)) - || (ProblemConfig.isJsonPointerEnabled() && !nameMatchesJsonPointerFormat(in, name))) { + if ((in == InEnum.BODY && ProblemConfig.isJsonPointerEnabled() && !nameMatchesJsonPointerFormat(name)) + || (!ProblemConfig.isJsonPointerEnabled() && !nameMatchesJsonPathFormat(in, name))) { throw new IllegalArgumentException( IN_NAME_VALUE_INVALID_FORMAT + "(In: " + in + ", Name: " + name + ") It should follow " @@ -478,12 +478,10 @@ public String toString() { '}'; } - private static boolean nameMatchesJsonPointerFormat(InEnum in, String name) { + private static boolean nameMatchesJsonPointerFormat(String name) { return name.matches(JSON_POINTER_BASIC_REGEX) - && (in != InEnum.BODY || name.startsWith("/")) // if for body, the name must start with "/" && !name.matches(".*\\/\\d+\\/\\d+\\/*+") // not two indexes following each other (e.g: person/1/2) - && !name.matches("\\/*+\\d++(\\/[a-zA-Z0-9-.]++)*+"); // not starting with an index (e.g: /1/person, - // 1/person) + && !name.matches("\\/+\\d++(\\/[a-zA-Z0-9-.]++)*+"); // not starting with an index (e.g: /1/person) } private static boolean nameMatchesJsonPathFormat(InEnum in, String name) { @@ -500,11 +498,8 @@ public static String convertName(InEnum in, String nameJsonPath) { if (nameJsonPath == null || nameJsonPath.trim().isEmpty()) { return null; - } else if (!ProblemConfig.isJsonPointerEnabled()) { + } else if (!ProblemConfig.isJsonPointerEnabled() || in != InEnum.BODY) { return nameJsonPath; - } else if (in != InEnum.BODY) { - // replace all indexes "[X]" by "/X" - return replaceSquareBrackets(nameJsonPath); } else { // replace all indexes "[X]" by "/X" and replace all "." by "/" String convertedName = replaceSquareBrackets(nameJsonPath).replace(".", "/"); @@ -523,7 +518,7 @@ public static String getNameFromProperties(InEnum in, List propertiesNam return null; } - String name = ProblemConfig.isJsonPointerEnabled() ? propertiesName.stream() + String name = ProblemConfig.isJsonPointerEnabled() && in == InEnum.BODY ? propertiesName.stream() .map(InputValidationIssue::replaceSquareBrackets).collect(Collectors.joining("/")) : String.join(".", propertiesName); return in == InEnum.BODY && ProblemConfig.isJsonPointerEnabled() ? "/" + name : name; diff --git a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssues.java b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssues.java index d72df974..eab388ed 100644 --- a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssues.java +++ b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssues.java @@ -139,7 +139,7 @@ public static InputValidationIssue referencedResourceNotFound(InEnum in, String * @param The type of the reference */ public static InputValidationIssue referencedResourceNotFound(InEnum in, String name, T value, List source) { - String indexFormat = ProblemConfig.isJsonPointerEnabled() ? ("/" + source.indexOf(value)) + String indexFormat = ProblemConfig.isJsonPointerEnabled() && in == InEnum.BODY ? ("/" + source.indexOf(value)) : ("[" + source.indexOf(value) + "]"); String nameWithIndex = name + indexFormat; return referencedResourceNotFound(in, InputValidationIssue.convertName(in, nameWithIndex), value); diff --git a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssueTest.java b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssueTest.java index 42e11f94..51be281f 100644 --- a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssueTest.java +++ b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssueTest.java @@ -330,9 +330,9 @@ void convertName(InEnum in) { assertThat(InputValidationIssue.convertName(in, " ")).isNull(); assertThat(InputValidationIssue.convertName(in, "field")).isEqualTo(in == InEnum.BODY ? "/field" : "field"); assertThat(InputValidationIssue.convertName(in, "field[0]")) - .isEqualTo(in == InEnum.BODY ? "/field/0" : "field/0"); + .isEqualTo(in == InEnum.BODY ? "/field/0" : "field[0]"); assertThat(InputValidationIssue.convertName(in, "field[0].nested")) - .isEqualTo(in == InEnum.BODY ? "/field/0/nested" : "field/0.nested"); + .isEqualTo(in == InEnum.BODY ? "/field/0/nested" : "field[0].nested"); assertThat(InputValidationIssue.convertName(in, "field/0")) .isEqualTo(in == InEnum.BODY ? "/field/0" : "field/0"); assertThat(InputValidationIssue.convertName(in, "field/0/nested")) @@ -393,7 +393,7 @@ void getNameFromPropertiesNotBody(InEnum in) { assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isEqualTo("field"); properties.set(0, "field[0]"); - assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isEqualTo("field/0"); + assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isEqualTo("field[0]"); } @Test @@ -414,7 +414,8 @@ void getNameFromPropertiesJsonPointerDisabled() { assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)).isEqualTo("field[0]"); properties.add("nested"); - assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)).isEqualTo("field[0].nested"); + assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)) + .isEqualTo("field[0].nested"); } @ParameterizedTest From ca900f448a521712b2b754af9fdb28d9761ef51e Mon Sep 17 00:00:00 2001 From: Sarah Kumwimba Date: Fri, 26 Jun 2026 11:13:18 +0200 Subject: [PATCH 9/9] Add auto-conversion for name of issue + add warning when applied --- .../pom.xml | 6 + .../pom.xml | 6 + .../belgif-rest-problem-jackson3-it/pom.xml | 6 + .../rest/problem/LocalDateConverter.java | 3 +- .../rest/problem/LocalDateConverter.java | 3 +- .../quarkus/it/LocalDateConverter.java | 3 +- .../internal/ConstraintViolationUtil.java | 2 +- .../BeanValidationExceptionsHandler.java | 6 +- .../internal/BeanValidationExceptionUtil.java | 2 +- .../validation/AbstractRequestValidator.java | 6 +- belgif-rest-problem/pom.xml | 6 + .../github/belgif/rest/problem/api/Input.java | 8 +- .../problem/api/InputValidationIssue.java | 65 ++-- .../problem/api/InputValidationIssues.java | 2 +- .../problem/api/InputValidationIssueTest.java | 279 ++++++++++-------- 15 files changed, 235 insertions(+), 168 deletions(-) diff --git a/belgif-rest-problem-it/belgif-rest-problem-jackson2-latest-it/pom.xml b/belgif-rest-problem-it/belgif-rest-problem-jackson2-latest-it/pom.xml index edafaffe..978265c4 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-jackson2-latest-it/pom.xml +++ b/belgif-rest-problem-it/belgif-rest-problem-jackson2-latest-it/pom.xml @@ -25,6 +25,12 @@ 2.17.0 provided + + org.slf4j + slf4j-api + 2.0.18 + provided + org.junit.jupiter junit-jupiter diff --git a/belgif-rest-problem-it/belgif-rest-problem-jackson2-minimal-it/pom.xml b/belgif-rest-problem-it/belgif-rest-problem-jackson2-minimal-it/pom.xml index dd9b195d..d7c564fd 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-jackson2-minimal-it/pom.xml +++ b/belgif-rest-problem-it/belgif-rest-problem-jackson2-minimal-it/pom.xml @@ -25,6 +25,12 @@ ${version.jackson.minimal} provided + + org.slf4j + slf4j-api + 2.0.18 + provided + org.junit.jupiter junit-jupiter diff --git a/belgif-rest-problem-it/belgif-rest-problem-jackson3-it/pom.xml b/belgif-rest-problem-it/belgif-rest-problem-jackson3-it/pom.xml index 63f4e637..6b657c2f 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-jackson3-it/pom.xml +++ b/belgif-rest-problem-it/belgif-rest-problem-jackson3-it/pom.xml @@ -25,6 +25,12 @@ ${version.jackson3.minimal} provided + + org.slf4j + slf4j-api + 2.0.18 + provided + org.junit.jupiter junit-jupiter diff --git a/belgif-rest-problem-it/belgif-rest-problem-jakarta-ee-it/src/main/java/io/github/belgif/rest/problem/LocalDateConverter.java b/belgif-rest-problem-it/belgif-rest-problem-jakarta-ee-it/src/main/java/io/github/belgif/rest/problem/LocalDateConverter.java index be62376c..d89f0dc4 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-jakarta-ee-it/src/main/java/io/github/belgif/rest/problem/LocalDateConverter.java +++ b/belgif-rest-problem-it/belgif-rest-problem-jakarta-ee-it/src/main/java/io/github/belgif/rest/problem/LocalDateConverter.java @@ -33,7 +33,8 @@ protected LocalDate fromString(InEnum in, String name, String value) { return LocalDate.parse(value, LOCAL_DATE_FORMATTER); } catch (DateTimeParseException e) { throw new BadRequestProblem( - schemaViolation(in, InputValidationIssue.convertName(in, name), value, "date has invalid format")); + schemaViolation(in, InputValidationIssue.transformName(in, name), value, + "date has invalid format")); } } diff --git a/belgif-rest-problem-it/belgif-rest-problem-java-ee-it/src/main/java/io/github/belgif/rest/problem/LocalDateConverter.java b/belgif-rest-problem-it/belgif-rest-problem-java-ee-it/src/main/java/io/github/belgif/rest/problem/LocalDateConverter.java index bc903393..b0bb9ef7 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-java-ee-it/src/main/java/io/github/belgif/rest/problem/LocalDateConverter.java +++ b/belgif-rest-problem-it/belgif-rest-problem-java-ee-it/src/main/java/io/github/belgif/rest/problem/LocalDateConverter.java @@ -33,7 +33,8 @@ protected LocalDate fromString(InEnum in, String name, String value) { return LocalDate.parse(value, LOCAL_DATE_FORMATTER); } catch (DateTimeParseException e) { throw new BadRequestProblem( - schemaViolation(in, InputValidationIssue.convertName(in, name), value, "date has invalid format")); + schemaViolation(in, InputValidationIssue.transformName(in, name), value, + "date has invalid format")); } } diff --git a/belgif-rest-problem-it/belgif-rest-problem-quarkus-it/src/main/java/io/github/belgif/rest/problem/quarkus/it/LocalDateConverter.java b/belgif-rest-problem-it/belgif-rest-problem-quarkus-it/src/main/java/io/github/belgif/rest/problem/quarkus/it/LocalDateConverter.java index 3e8ecacd..1db1d9ba 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-quarkus-it/src/main/java/io/github/belgif/rest/problem/quarkus/it/LocalDateConverter.java +++ b/belgif-rest-problem-it/belgif-rest-problem-quarkus-it/src/main/java/io/github/belgif/rest/problem/quarkus/it/LocalDateConverter.java @@ -34,7 +34,8 @@ protected LocalDate fromString(InEnum in, String name, String value) { return LocalDate.parse(value, LOCAL_DATE_FORMATTER); } catch (DateTimeParseException e) { throw new BadRequestProblem( - schemaViolation(in, InputValidationIssue.convertName(in, name), value, "date has invalid format")); + schemaViolation(in, InputValidationIssue.transformName(in, name), value, + "date has invalid format")); } } diff --git a/belgif-rest-problem-java-ee-server/src/main/java/io/github/belgif/rest/problem/ee/server/internal/ConstraintViolationUtil.java b/belgif-rest-problem-java-ee-server/src/main/java/io/github/belgif/rest/problem/ee/server/internal/ConstraintViolationUtil.java index e8436a02..9092bd2d 100644 --- a/belgif-rest-problem-java-ee-server/src/main/java/io/github/belgif/rest/problem/ee/server/internal/ConstraintViolationUtil.java +++ b/belgif-rest-problem-java-ee-server/src/main/java/io/github/belgif/rest/problem/ee/server/internal/ConstraintViolationUtil.java @@ -97,7 +97,7 @@ private static Input determineInput(ConstraintViolation violation, InEnum in = ParameterSourceMapper.map(annotation.annotationType()); if (in != null) { input.setIn(in); - input.setName(InputValidationIssue.convertName(in, + input.setName(InputValidationIssue.transformName(in, (String) annotation.annotationType().getMethod("value").invoke(annotation))); } } diff --git a/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/server/BeanValidationExceptionsHandler.java b/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/server/BeanValidationExceptionsHandler.java index 8d5b2346..c4098cf3 100644 --- a/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/server/BeanValidationExceptionsHandler.java +++ b/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/server/BeanValidationExceptionsHandler.java @@ -85,7 +85,7 @@ public ResponseEntity handleBindException(BindException exception, Serv String invalidValue = (String) exception.getValue(); return ProblemMediaType.INSTANCE .toResponse(new BadRequestProblem(InputValidationIssues.schemaViolation(in, - InputValidationIssue.convertName(in, name), invalidValue, detail))); + InputValidationIssue.transformName(in, name), invalidValue, detail))); } @ExceptionHandler(HandlerMethodValidationException.class) @@ -120,7 +120,7 @@ public void modelAttribute(@Nullable ModelAttribute modelAttribute, ParameterErr if (modelAttribute != null) { errors.getResolvableErrors().forEach(error -> issues.add( InputValidationIssues.schemaViolation( - InEnum.BODY, InputValidationIssue.convertName(InEnum.BODY, modelAttribute.value()), + InEnum.BODY, InputValidationIssue.transformName(InEnum.BODY, modelAttribute.value()), errors.getArgument(), error.getDefaultMessage()))); } @@ -170,7 +170,7 @@ public void requestParam(@Nullable RequestParam requestParam, ParameterValidatio public void requestPart(RequestPart requestPart, ParameterErrors errors) { errors.getResolvableErrors().forEach(error -> issues.add( InputValidationIssues.schemaViolation( - InEnum.BODY, InputValidationIssue.convertName(InEnum.BODY, requestPart.value()), + InEnum.BODY, InputValidationIssue.transformName(InEnum.BODY, requestPart.value()), errors.getArgument(), error.getDefaultMessage()))); } diff --git a/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/server/internal/BeanValidationExceptionUtil.java b/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/server/internal/BeanValidationExceptionUtil.java index f16c50ad..5cf769ee 100644 --- a/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/server/internal/BeanValidationExceptionUtil.java +++ b/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/server/internal/BeanValidationExceptionUtil.java @@ -55,7 +55,7 @@ public static InputValidationIssue convertToInputValidationIssue(@NotNull FieldE String invalidValue = Objects.toString(fieldError.getRejectedValue(), null); String name = fieldError.getField(); - return InputValidationIssues.schemaViolation(in, InputValidationIssue.convertName(in, name), invalidValue, + return InputValidationIssues.schemaViolation(in, InputValidationIssue.transformName(in, name), invalidValue, fieldError.getDefaultMessage()); } diff --git a/belgif-rest-problem-validator/src/main/java/io/github/belgif/rest/problem/validation/AbstractRequestValidator.java b/belgif-rest-problem-validator/src/main/java/io/github/belgif/rest/problem/validation/AbstractRequestValidator.java index 1a3fd874..e78fd879 100644 --- a/belgif-rest-problem-validator/src/main/java/io/github/belgif/rest/problem/validation/AbstractRequestValidator.java +++ b/belgif-rest-problem-validator/src/main/java/io/github/belgif/rest/problem/validation/AbstractRequestValidator.java @@ -119,7 +119,7 @@ public SELF ssins(Input> ssins) { if (ssins != null && ssins.getValue() != null) { int index = 0; for (String ssin : ssins.getValue()) { - String name = convertName(ssins.getIn(), ssins.getName() + getIndexFormat(ssins.getIn(), index)); + String name = transformName(ssins.getIn(), ssins.getName() + getIndexFormat(ssins.getIn(), index)); ssin(new Input<>(ssins.getIn(), name, ssin)); index++; } @@ -363,7 +363,7 @@ public SELF refDatas(Input> input, Supplier> allowedRe Collection allowedRefData = allowedRefDataSupplier.get(); int index = 0; for (T value : input.getValue()) { - String name = convertName(input.getIn(), input.getName() + getIndexFormat(input.getIn(), index)); + String name = transformName(input.getIn(), input.getName() + getIndexFormat(input.getIn(), index)); refData(new Input(input.getIn(), name, value), allowedRefData); index++; } @@ -383,7 +383,7 @@ public SELF refDatas(Input> input, Predicate allowedRefDataPredic if (input != null && input.getValue() != null && !input.getValue().isEmpty()) { int index = 0; for (T value : input.getValue()) { - String name = convertName(input.getIn(), input.getName() + getIndexFormat(input.getIn(), index)); + String name = transformName(input.getIn(), input.getName() + getIndexFormat(input.getIn(), index)); refData(new Input(input.getIn(), name, value), allowedRefDataPredicate); index++; } diff --git a/belgif-rest-problem/pom.xml b/belgif-rest-problem/pom.xml index 6ee94493..6fa34c3b 100644 --- a/belgif-rest-problem/pom.xml +++ b/belgif-rest-problem/pom.xml @@ -59,6 +59,12 @@ --> true + + org.slf4j + slf4j-api + 2.0.18 + provided + org.junit.jupiter junit-jupiter diff --git a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/Input.java b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/Input.java index 72da3eaa..119e5fcb 100644 --- a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/Input.java +++ b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/Input.java @@ -32,9 +32,8 @@ public Input() { } public Input(InEnum in, String name, V value) { - InputValidationIssue.verifyNameFormat(in, name); this.in = in; - this.name = name; + this.name = InputValidationIssue.convertName(in, name); this.value = value; } @@ -43,8 +42,8 @@ public InEnum getIn() { } public void setIn(InEnum in) { - InputValidationIssue.verifyNameFormat(in, name); this.in = in; + setName(name); } public String getName() { @@ -52,8 +51,7 @@ public String getName() { } public void setName(String name) { - InputValidationIssue.verifyNameFormat(in, name); - this.name = name; + this.name = InputValidationIssue.convertName(in, name); } public V getValue() { diff --git a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java index 92cf1bcf..7e6b8be9 100644 --- a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java +++ b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssue.java @@ -13,6 +13,9 @@ import java.util.Objects; import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonInclude; @@ -41,21 +44,17 @@ public class InputValidationIssue { public static final Comparator BY_NAME = Comparator.comparing(InputValidationIssue::getName); + private static final Logger LOGGER = LoggerFactory.getLogger(InputValidationIssue.class); + private static final String INPUTS_AND_IN_NAME_VALUE_ARE_MUTUALLY_EXCLUSIVE = "inputs[] and in/name/value are mutually exclusive"; private static final String INPUTS_SETTER_ONE_ITEM = "inputs[] can not be set with a single item, use in(in, name, value) instead"; - private static final String IN_NAME_VALUE_INVALID_FORMAT = "input name has an invalid format"; - // e.g: /, field, /field, /field/0, /field/0/nested private static final String JSON_POINTER_BASIC_REGEX = "\\/+[a-zA-Z0-9-]*+(\\/[a-zA-Z0-9-]++)*+"; - // e.g: field, field[0], field[0].nested - private static final String JSON_PATH_BASIC_REGEX = - "[a-zA-Z0-9-]++(\\[\\d++\\])*+(\\.[a-zA-Z0-9-]++(\\[\\d++\\])*+)*+"; - private URI type; private URI href; private String title; @@ -81,16 +80,14 @@ public InputValidationIssue(URI type, URI href, String title) { } public InputValidationIssue(InEnum in, String name, Object value) { - verifyNameFormat(in, name); this.in = in; - this.name = name; + this.name = convertName(in, name); this.value = value; } public InputValidationIssue(InEnum in, String name) { - verifyNameFormat(in, name); this.in = in; - this.name = name; + this.name = convertName(in, name); } public URI getType() { @@ -131,8 +128,8 @@ public InEnum getIn() { public void setIn(InEnum in) { verifyNoInputs(in); - verifyNameFormat(in, name); this.in = in; + this.name = convertName(in, name); } public String getName() { @@ -141,8 +138,7 @@ public String getName() { public void setName(String name) { verifyNoInputs(name); - verifyNameFormat(in, name); - this.name = name; + this.name = convertName(in, name); } public Object getValue() { @@ -195,21 +191,6 @@ private void verifyNoInputs(Object valueToUpdate) { } } - public static void verifyNameFormat(InEnum in, String name) { - - if (name == null) { - return; - } - - if ((in == InEnum.BODY && ProblemConfig.isJsonPointerEnabled() && !nameMatchesJsonPointerFormat(name)) - || (!ProblemConfig.isJsonPointerEnabled() && !nameMatchesJsonPathFormat(in, name))) { - - throw new IllegalArgumentException( - IN_NAME_VALUE_INVALID_FORMAT + "(In: " + in + ", Name: " + name + ") It should follow " - + (ProblemConfig.isJsonPointerEnabled() ? "JsonPointer" : "JsonPath") + " syntax"); - } - } - private boolean hasInNameValue() { return in != null || name != null || value != null; } @@ -479,13 +460,9 @@ public String toString() { } private static boolean nameMatchesJsonPointerFormat(String name) { - return name.matches(JSON_POINTER_BASIC_REGEX) + return name == null || (name.matches(JSON_POINTER_BASIC_REGEX) && !name.matches(".*\\/\\d+\\/\\d+\\/*+") // not two indexes following each other (e.g: person/1/2) - && !name.matches("\\/+\\d++(\\/[a-zA-Z0-9-.]++)*+"); // not starting with an index (e.g: /1/person) - } - - private static boolean nameMatchesJsonPathFormat(InEnum in, String name) { - return in == InEnum.BODY ? name.matches(JSON_PATH_BASIC_REGEX) : name.matches("[a-zA-Z0-9-.]++(\\[\\d++\\])*+"); + && !name.matches("\\/+\\d++(\\/[a-zA-Z0-9-.]++)*+")); // not starting with an index (e.g: /1/person) } /** @@ -494,7 +471,7 @@ private static boolean nameMatchesJsonPathFormat(InEnum in, String name) { * @param nameJsonPath the name in JsonPath syntax * @return the name converted to JsonPointer syntax */ - public static String convertName(InEnum in, String nameJsonPath) { + public static String transformName(InEnum in, String nameJsonPath) { if (nameJsonPath == null || nameJsonPath.trim().isEmpty()) { return null; @@ -507,6 +484,24 @@ public static String convertName(InEnum in, String nameJsonPath) { } } + /** + * + * @param in the issue place in the query + * @param name the name in JsonPath syntax + * @return the name converted (if necessary) to JsonPointer syntax + */ + protected static String convertName(InEnum in, String name) { + if (in == InEnum.BODY && ProblemConfig.isJsonPointerEnabled() && !nameMatchesJsonPointerFormat(name)) { + LOGGER.warn( + "Your application does not use the JsonPointer syntax for issue in the body although it is " + + "enabled [in: %s, name: %s]. Auto-conversion will be applied. ", + in, name); + return transformName(in, name); + } + + return name; + } + public static String getNameFromProperties(InEnum in, List propertiesName) { if (in != InEnum.BODY && propertiesName != null && propertiesName.size() > 1) { diff --git a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssues.java b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssues.java index eab388ed..9ae7c8a6 100644 --- a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssues.java +++ b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/api/InputValidationIssues.java @@ -142,7 +142,7 @@ public static InputValidationIssue referencedResourceNotFound(InEnum in, Str String indexFormat = ProblemConfig.isJsonPointerEnabled() && in == InEnum.BODY ? ("/" + source.indexOf(value)) : ("[" + source.indexOf(value) + "]"); String nameWithIndex = name + indexFormat; - return referencedResourceNotFound(in, InputValidationIssue.convertName(in, nameWithIndex), value); + return referencedResourceNotFound(in, InputValidationIssue.transformName(in, nameWithIndex), value); } public static InputValidationIssue rejectedInput(InEnum in, String name, Object value) { diff --git a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssueTest.java b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssueTest.java index 51be281f..d54e8d4e 100644 --- a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssueTest.java +++ b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/api/InputValidationIssueTest.java @@ -1,13 +1,14 @@ package io.github.belgif.rest.problem.api; import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.*; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.assertj.core.api.InstanceOfAssertFactories; import org.assertj.core.api.ThrowableAssert; @@ -16,8 +17,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; -import com.fasterxml.jackson.core.JsonPointer; - import io.github.belgif.rest.problem.config.ProblemConfig; import io.github.belgif.rest.problem.i18n.Context; @@ -324,65 +323,75 @@ void equalsHashCodeToString() { @ParameterizedTest @EnumSource(InEnum.class) - void convertName(InEnum in) { - assertThat(InputValidationIssue.convertName(in, null)).isNull(); - assertThat(InputValidationIssue.convertName(in, "")).isNull(); - assertThat(InputValidationIssue.convertName(in, " ")).isNull(); - assertThat(InputValidationIssue.convertName(in, "field")).isEqualTo(in == InEnum.BODY ? "/field" : "field"); - assertThat(InputValidationIssue.convertName(in, "field[0]")) + void transformNameInBody(InEnum in) { + assertThat(InputValidationIssue.transformName(in, null)).isNull(); + assertThat(InputValidationIssue.transformName(in, "")).isNull(); + assertThat(InputValidationIssue.transformName(in, " ")).isNull(); + assertThat(InputValidationIssue.transformName(in, "field")).isEqualTo(in == InEnum.BODY ? "/field" : "field"); + assertThat(InputValidationIssue.transformName(in, "field[0]")) .isEqualTo(in == InEnum.BODY ? "/field/0" : "field[0]"); - assertThat(InputValidationIssue.convertName(in, "field[0].nested")) + assertThat(InputValidationIssue.transformName(in, "field[0].nested")) .isEqualTo(in == InEnum.BODY ? "/field/0/nested" : "field[0].nested"); - assertThat(InputValidationIssue.convertName(in, "field/0")) + assertThat(InputValidationIssue.transformName(in, "field/0")) .isEqualTo(in == InEnum.BODY ? "/field/0" : "field/0"); - assertThat(InputValidationIssue.convertName(in, "field/0/nested")) + assertThat(InputValidationIssue.transformName(in, "field/0/nested")) .isEqualTo(in == InEnum.BODY ? "/field/0/nested" : "field/0/nested"); - } - - @Test - void convertNameSlashAlreadyForBodyParam() { - assertThat(InputValidationIssue.convertName(InEnum.BODY, "/field")).isEqualTo("/field"); + assertThat(InputValidationIssue.transformName(in, "/field/0")).isEqualTo("/field/0"); + assertThat(InputValidationIssue.transformName(in, "/field/0/nested")).isEqualTo("/field/0/nested"); + assertThat(InputValidationIssue.transformName(in, "/field")).isEqualTo("/field"); } @ParameterizedTest @EnumSource(InEnum.class) - void convertNameWithJsonPointerDisabled(InEnum in) { - + void transformNameInBodyJsonPointerDisabled(InEnum in) { ProblemConfig.setJsonPointerEnabled(false); - assertThat(InputValidationIssue.convertName(in, null)).isNull(); - assertThat(InputValidationIssue.convertName(in, "")).isNull(); - assertThat(InputValidationIssue.convertName(in, " ")).isNull(); - assertThat(InputValidationIssue.convertName(in, "field")).isEqualTo("field"); - assertThat(InputValidationIssue.convertName(in, "field[0]")).isEqualTo("field[0]"); - assertThat(InputValidationIssue.convertName(in, "field[0].nested")).isEqualTo("field[0].nested"); + assertThat(InputValidationIssue.transformName(in, null)).isNull(); + assertThat(InputValidationIssue.transformName(in, "")).isNull(); + assertThat(InputValidationIssue.transformName(in, " ")).isNull(); + assertThat(InputValidationIssue.transformName(in, "field")).isEqualTo("field"); + assertThat(InputValidationIssue.transformName(in, "field[0]")).isEqualTo("field[0]"); + assertThat(InputValidationIssue.transformName(in, "field[0].nested")).isEqualTo("field[0].nested"); + assertThat(InputValidationIssue.transformName(in, "field/0")).isEqualTo("field/0"); + assertThat(InputValidationIssue.transformName(in, "field/0/nested")).isEqualTo("field/0/nested"); + assertThat(InputValidationIssue.transformName(in, "/field/0")).isEqualTo("/field/0"); + assertThat(InputValidationIssue.transformName(in, "/field/0/nested")).isEqualTo("/field/0/nested"); + assertThat(InputValidationIssue.transformName(in, "/field")).isEqualTo("/field"); } - @Test - void getNameFromProperties() { + @ParameterizedTest + @EnumSource(InEnum.class) + void getNameFromProperties(InEnum in) { List properties = null; - assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)).isNull(); + assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isNull(); properties = new ArrayList<>(); - assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)).isNull(); + assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isNull(); properties.add("field"); - assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)).isEqualTo("/field"); + assertThat(InputValidationIssue.getNameFromProperties(in, properties)) + .isEqualTo(in == InEnum.BODY ? "/field" : "field"); properties.set(0, "field[0]"); - assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)).isEqualTo("/field/0"); + assertThat(InputValidationIssue.getNameFromProperties(in, properties)) + .isEqualTo(in == InEnum.BODY ? "/field/0" : "field[0]"); - properties.add("nested"); - assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)).isEqualTo("/field/0/nested"); + if (in == InEnum.BODY) { + properties.add("nested"); + assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isEqualTo("/field/0/nested"); - properties.add("nestedAgain[1]"); - assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)) - .isEqualTo("/field/0/nested/nestedAgain/1"); + properties.add("nestedAgain[1]"); + assertThat(InputValidationIssue.getNameFromProperties(in, properties)) + .isEqualTo("/field/0/nested/nestedAgain/1"); + } } @ParameterizedTest - @EnumSource(value = InEnum.class, names = { "BODY" }, mode = EnumSource.Mode.EXCLUDE) - void getNameFromPropertiesNotBody(InEnum in) { + @EnumSource(InEnum.class) + void getNameFromPropertiesWithJsonPointerDisabled(InEnum in) { + + ProblemConfig.setJsonPointerEnabled(false); + List properties = null; assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isNull(); @@ -394,28 +403,16 @@ void getNameFromPropertiesNotBody(InEnum in) { properties.set(0, "field[0]"); assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isEqualTo("field[0]"); - } - - @Test - void getNameFromPropertiesJsonPointerDisabled() { - - ProblemConfig.setJsonPointerEnabled(false); - List properties = null; - assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)).isNull(); - - properties = new ArrayList<>(); - assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)).isNull(); - - properties.add("field"); - assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)).isEqualTo("field"); - - properties.set(0, "field[0]"); - assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)).isEqualTo("field[0]"); + if (in == InEnum.BODY) { + properties.add("nested"); + assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isEqualTo("field[0].nested"); + assertThat(InputValidationIssue.getNameFromProperties(in, properties)).isEqualTo("field[0].nested"); - properties.add("nested"); - assertThat(InputValidationIssue.getNameFromProperties(InEnum.BODY, properties)) - .isEqualTo("field[0].nested"); + properties.add("nestedAgain[1]"); + assertThat(InputValidationIssue.getNameFromProperties(in, properties)) + .isEqualTo("field[0].nested.nestedAgain[1]"); + } } @ParameterizedTest @@ -438,82 +435,133 @@ void getNameFromPropertiesIllegalArgument() { @ParameterizedTest @EnumSource(InEnum.class) void nameMatchingJsonPointerFormatInConstructor(InEnum in) { - String prefix = in == InEnum.BODY ? "/" : ""; - assertDoesNotThrow(() -> new InputValidationIssue(in, prefix + "field")); - assertDoesNotThrow(() -> new InputValidationIssue(in, prefix + "")); - assertDoesNotThrow(() -> new InputValidationIssue(in, prefix + "field", "value")); - assertDoesNotThrow(() -> new InputValidationIssue(in, prefix + "field/0")); - assertDoesNotThrow(() -> new InputValidationIssue(in, prefix + "field/0", "value")); - assertDoesNotThrow(() -> new InputValidationIssue(in, prefix + "field/0/nested/2/nestedAgain")); - assertDoesNotThrow(() -> new InputValidationIssue(in, prefix + "field/0/nested/2/nestedAgain", "value")); + List names = Arrays.asList("/field", "/field/0", "/field/0/nested/2/nestedAgain"); + + for (String name : names) { + InputValidationIssue input = new InputValidationIssue(in, name); + assertThat(input.getName()).isEqualTo(name); + input = new InputValidationIssue(in, name, "value"); + assertThat(input.getName()).isEqualTo(name); + } } - @Test - void nameInBodyMatchingJsonPointerFormatInConstructorSyntaxFault() { - assertThatIllegalArgumentException().isThrownBy(() -> new InputValidationIssue(InEnum.BODY, "field/0/1")) - .withMessageContaining("format").withMessageContaining("JsonPointer"); + @ParameterizedTest + @EnumSource(InEnum.class) + void nameNotMatchingJsonPointerFormatInConstructorAutoConversion(InEnum in) { + Map names = new HashMap<>(); + names.put("field", "/field"); + names.put("", null); + names.put("field[0]", "/field/0"); + names.put("field[0].nested[2].nestedAgain", "/field/0/nested/2/nestedAgain"); + names.put("field/0", "/field/0"); + names.put("field/0/nested/2/nestedAgain", "/field/0/nested/2/nestedAgain"); + + for (Map.Entry name : names.entrySet()) { + InputValidationIssue input = new InputValidationIssue(in, name.getKey()); + assertThat(input.getName()).isEqualTo(in == InEnum.BODY ? name.getValue() : name.getKey()); + input = new InputValidationIssue(in, name.getKey(), "value"); + assertThat(input.getName()).isEqualTo(in == InEnum.BODY ? name.getValue() : name.getKey()); + } + } - assertThatIllegalArgumentException().isThrownBy(() -> new InputValidationIssue(InEnum.BODY, "/0/nested")) - .withMessageContaining("format").withMessageContaining("JsonPointer"); + @ParameterizedTest + @EnumSource(InEnum.class) + void nameNotConvertedInConstructorWithJsonPointerDisabled(InEnum in) { + ProblemConfig.setJsonPointerEnabled(false); - assertThatIllegalArgumentException().isThrownBy(() -> new InputValidationIssue(InEnum.BODY, "0/nested")) - .withMessageContaining("format").withMessageContaining("JsonPointer"); + List names = Arrays.asList("field", "/field", "", null, "field[0]", "/field/0", "field/0", + "field[0].nested[2].nestedAgain", "field/0/nested/2/nestedAgain", "/field/0/nested/2/nestedAgain"); - assertThatIllegalArgumentException().isThrownBy(() -> new InputValidationIssue(InEnum.BODY, "field[0]")) - .withMessageContaining("format").withMessageContaining("JsonPointer"); - assertThatIllegalArgumentException() - .isThrownBy(() -> new InputValidationIssue(InEnum.BODY, "field[0]", "value")) - .withMessageContaining("format").withMessageContaining("JsonPointer"); - assertThatIllegalArgumentException() - .isThrownBy(() -> new InputValidationIssue(InEnum.BODY, "field[0].nested[2]/nestedAgain")) - .withMessageContaining("format").withMessageContaining("JsonPointer"); - assertThatIllegalArgumentException() - .isThrownBy(() -> new InputValidationIssue(InEnum.BODY, "field[0].nested[2]/nestedAgain", "value")) - .withMessageContaining("format").withMessageContaining("JsonPointer"); + for (String name : names) { + InputValidationIssue input = new InputValidationIssue(in, name); + assertThat(input.getName()).isEqualTo(name); + input = new InputValidationIssue(in, name, "value"); + assertThat(input.getName()).isEqualTo(name); + } } - @Test - void nameInBodyMatchingJsonPathFormatInConstructor() { + @ParameterizedTest + @EnumSource(InEnum.class) + void setName(InEnum in) { + Map names = new HashMap<>(); + names.put("field", "/field"); + names.put("/field", "/field"); + names.put("", null); + names.put("field[0]", "/field/0"); + names.put("field[0].nested[2].nestedAgain", "/field/0/nested/2/nestedAgain"); + names.put("field/0", "/field/0"); + names.put("/field/0", "/field/0"); + names.put("field/0/nested/2/nestedAgain", "/field/0/nested/2/nestedAgain"); + names.put("/field/0/nested/2/nestedAgain", "/field/0/nested/2/nestedAgain"); + + for (Map.Entry name : names.entrySet()) { + InputValidationIssue input = new InputValidationIssue(null, name.getKey(), null); + input.setName(name.getKey()); + assertThat(input.getName()).isEqualTo(name.getKey()); + input = new InputValidationIssue(in, null, null); + input.setName(name.getKey()); + assertThat(input.getName()).isEqualTo(in == InEnum.BODY ? name.getValue() : name.getKey()); + } + } + + @ParameterizedTest + @EnumSource(InEnum.class) + void setNameJsonPointerDisabled(InEnum in) { ProblemConfig.setJsonPointerEnabled(false); - JsonPointer.compile("/field[0]"); + List names = Arrays.asList("field", "/field", "", "field[0]", "/field/0", + "field[0].nested[2].nestedAgain", "/field/0/nested/2/nestedAgain", "field/0", + "field/0/nested/2/nestedAgain", "/field/0/nested/2/nestedAgain", "/field/0/nested/2/nestedAgain", + "/field/0/nested/2/nestedAgain"); - assertDoesNotThrow(() -> new InputValidationIssue(InEnum.BODY, "field")); - assertDoesNotThrow(() -> new InputValidationIssue(InEnum.BODY, "field", "value")); - assertDoesNotThrow(() -> new InputValidationIssue(InEnum.BODY, "field[0]")); - assertDoesNotThrow(() -> new InputValidationIssue(InEnum.BODY, "field[0]", "value")); - assertDoesNotThrow(() -> new InputValidationIssue(InEnum.BODY, "field[0].nested[2].nestedAgain")); - assertDoesNotThrow(() -> new InputValidationIssue(InEnum.BODY, "field[0].nested[2].nestedAgain", "value")); + for (String name : names) { + InputValidationIssue input = new InputValidationIssue(null, name, null); + input.setName(name); + assertThat(input.getName()).isEqualTo(name); + input = new InputValidationIssue(in, null, null); + input.setName(name); + assertThat(input.getName()).isEqualTo(name); + } } @ParameterizedTest - @EnumSource(value = InEnum.class, names = { "BODY" }, mode = EnumSource.Mode.EXCLUDE) - void nameNotInBodyMatchingJsonPathFormatInConstructor(InEnum in) { - ProblemConfig.setJsonPointerEnabled(false); - - assertDoesNotThrow(() -> new InputValidationIssue(in, "field")); - assertDoesNotThrow(() -> new InputValidationIssue(in, "field", "value")); - assertDoesNotThrow(() -> new InputValidationIssue(in, "field[0]")); - assertDoesNotThrow(() -> new InputValidationIssue(in, "field[0]", "value")); - assertDoesNotThrow(() -> new InputValidationIssue(in, "field.id[0]", "value")); + @EnumSource(InEnum.class) + void setInput(InEnum in) { + Map names = new HashMap<>(); + names.put("field", "/field"); + names.put("/field", "/field"); + names.put("", null); + names.put("field[0]", "/field/0"); + names.put("field[0].nested[2].nestedAgain", "/field/0/nested/2/nestedAgain"); + names.put("field/0", "/field/0"); + names.put("/field/0", "/field/0"); + names.put("field/0/nested/2/nestedAgain", "/field/0/nested/2/nestedAgain"); + names.put("/field/0/nested/2/nestedAgain", "/field/0/nested/2/nestedAgain"); + + for (Map.Entry name : names.entrySet()) { + InputValidationIssue input = new InputValidationIssue(null, name.getKey(), null); + assertThat(input.getName()).isEqualTo(name.getKey()); + input.setIn(in); + assertThat(input.getName()).isEqualTo(in == InEnum.BODY ? name.getValue() : name.getKey()); + } } @ParameterizedTest @EnumSource(InEnum.class) - void nameMatchingJsonPathFormatInConstructorSyntaxFault(InEnum in) { + void setInputJsonPointerDisabled(InEnum in) { ProblemConfig.setJsonPointerEnabled(false); - assertThatIllegalArgumentException().isThrownBy(() -> new InputValidationIssue(in, "field/0")) - .withMessageContaining("format").withMessageContaining("JsonPath"); - assertThatIllegalArgumentException() - .isThrownBy(() -> new InputValidationIssue(in, "field/0", "value")) - .withMessageContaining("format").withMessageContaining("JsonPath"); - assertThatIllegalArgumentException() - .isThrownBy(() -> new InputValidationIssue(in, "field/0/nested/2/nestedAgain")) - .withMessageContaining("format").withMessageContaining("JsonPath"); - assertThatIllegalArgumentException() - .isThrownBy(() -> new InputValidationIssue(in, "field/0/nested/2/nestedAgain", "value")) - .withMessageContaining("format").withMessageContaining("JsonPath"); + List names = Arrays.asList("field", "/field", "", "field[0]", "/field/0", + "field[0].nested[2].nestedAgain", "/field/0/nested/2/nestedAgain", "field/0", + "field/0/nested/2/nestedAgain", "/field/0/nested/2/nestedAgain", "/field/0/nested/2/nestedAgain", + "/field/0/nested/2/nestedAgain"); + + for (String name : names) { + InputValidationIssue input = new InputValidationIssue(null, name, null); + assertThat(input.getName()).isEqualTo(name); + input.setIn(in); + assertThat(input.getName()).isEqualTo(name); + } } private void assertMutuallyExclusiveException(ThrowableAssert.ThrowingCallable throwingCallable) { @@ -521,5 +569,4 @@ private void assertMutuallyExclusiveException(ThrowableAssert.ThrowingCallable t .isThrownBy(throwingCallable) .withMessageContaining(MUTUALLY_EXCLUSIVE_EXC); } - }