diff --git a/README.md b/README.md
index 5f52111..52d4c8c 100644
--- a/README.md
+++ b/README.md
@@ -107,14 +107,17 @@ public class Profile {
```java
@GetMapping("/users")
+@WebQuery(entityClass = User.class)
public Page
* If a requested sort field is not annotated as {@link Sortable}, a
* {@link QueryValidationException} is thrown.
@@ -71,16 +81,18 @@ public boolean supportsParameter(MethodParameter parameter) {
* The process is as follows:
* Example controller usage:
* The RSQL query is read from the request parameter defined by
- * {@link RsqlSpec#paramName()}. The query is then parsed, validated,
- * and converted into a JPA {@link Specification}.
+ * {@link RsqlSpec#paramName()}. Entity metadata and alias mappings are read
+ * from {@link WebQuery} on the same controller method. The query is then
+ * parsed, validated, and converted into a JPA {@link Specification}.
*
* @param parameter the method parameter to resolve
* @param mavContainer the model and view container
@@ -157,7 +160,14 @@ public Specification> resolveArgument(
)
{
try {
- // Retrieve the @RsqlSpec annotation to access configuration
+ // Retrieve the @WebQuery annotation from the method parameter to access entity and field mapping configuration
+ WebQuery webQueryAnnotation = AnnotationUtil.resolveWebQueryFromParameter(parameter);
+
+ // Extract entity class and field mappings from the @WebQuery annotation
+ Class> entityClass = webQueryAnnotation.entityClass();
+ FieldMapping[] fieldMappings = webQueryAnnotation.fieldMappings();
+
+ // Retrieve the @RsqlSpec annotation from the method parameter to access parameter-specific configuration
RsqlSpec annotation = parameter.getParameterAnnotation(RsqlSpec.class);
// Extract the RSQL query string from the request using the configured parameter name
String filter = webRequest.getParameter(annotation.paramName());
@@ -168,28 +178,28 @@ public Specification> resolveArgument(
Node root = rsqlParser.parse(filter);
// Validate the parsed AST against the target entity and its @RsqlFilterable fields
ValidationRSQLVisitor validationVisitor = new ValidationRSQLVisitor(
- annotation.entityClass(),
- annotation.fieldMappings(),
+ entityClass,
+ fieldMappings,
customOperators
);
root.accept(validationVisitor);
// Convert field mappings to aliases map which rsql jpa support library accepts
- Map Usage example: This visitor is typically used in combination with
@@ -166,7 +166,7 @@ private void validate(ComparisonNode node) {
}
catch (Exception ex) {
throw new QueryValidationException(MessageFormat.format(
- "Unknown field ''{0}''", fieldName
+ "Unknown field ''{0}''", reqFieldName
), ex);
}
diff --git a/spring-web-query-core/src/main/java/in/co/akshitbansal/springwebquery/annotation/FieldMapping.java b/spring-web-query-core/src/main/java/in/co/akshitbansal/springwebquery/annotation/FieldMapping.java
index abd79f9..3192b31 100644
--- a/spring-web-query-core/src/main/java/in/co/akshitbansal/springwebquery/annotation/FieldMapping.java
+++ b/spring-web-query-core/src/main/java/in/co/akshitbansal/springwebquery/annotation/FieldMapping.java
@@ -5,25 +5,27 @@
import java.lang.annotation.Target;
/**
- * Defines a mapping between a query parameter field name and an actual entity field name.
+ * Defines a mapping between an API-facing field name and an actual entity field name.
*
- * This annotation is used within {@link RsqlSpec#fieldMappings()} to create aliases
- * for entity fields in RSQL queries. It allows clients to use simpler or more
- * intuitive names in query strings while mapping them to the actual field names on
- * the entity.
+ * This annotation is used within {@link WebQuery#fieldMappings()} to create aliases
+ * for entity fields in filtering and sorting requests. It allows clients to use
+ * simpler or more intuitive names while mapping them to the actual field names
+ * on the entity.
* Example usage:
* This is the name that clients will use when constructing their queries.
*
* When {@code false} (default), only the alias name defined in {@link #name()}
- * can be used in queries. When {@code true}, both the alias and the original
- * field name are allowed.
+ * can be used in filter and sort expressions. When {@code true}, both the alias
+ * and the original field name are allowed.
* When applied, the pageable argument is validated so that sorting is only
- * allowed on entity fields explicitly annotated with {@link Sortable}.
*
*
* @param methodParameter the method parameter for which the value should be resolved
* @param mavContainer the ModelAndViewContainer (can be {@code null})
* @param webRequest the current request
* @param binderFactory a factory for creating WebDataBinder instances (can be {@code null})
- * @return a {@link Pageable} object containing page, size, and validated sort information
+ * @return a {@link Pageable} object containing page, size, validated sort information,
+ * and mapped sort field paths
* @throws QueryValidationException if any requested sort field is not marked as {@link Sortable}
*/
@Override
@@ -90,16 +102,38 @@ public Pageable resolveArgument(
@NonNull NativeWebRequest webRequest,
WebDataBinderFactory binderFactory
) {
+ // Resolve the @WebQuery annotation to access entity metadata for validation
+ WebQuery webQueryAnnotation = AnnotationUtil.resolveWebQueryFromParameter(methodParameter);
+ // Extract entity class and field mappings from the @WebQuery annotation
+ Class> entityClass = webQueryAnnotation.entityClass();
+ FieldMapping[] fieldMappings = webQueryAnnotation.fieldMappings();
+
+ // Create maps for quick lookup of field mappings by both API name and original field name
+ Map
+ *
{@code
* @GetMapping("/users")
+ * @WebQuery(entityClass = User.class)
* public List{@code
* Node root = new RSQLParser().parse("status==ACTIVE;age>30");
- * new ValidationRSQLVisitor(User.class).visit(root);
+ * new ValidationRSQLVisitor(User.class, new FieldMapping[0], Set.of()).visit(root);
* }
*
* {@code
* @GetMapping("/users")
+ * @WebQuery(
+ * entityClass = User.class,
+ * fieldMappings = {
+ * @FieldMapping(name = "id", field = "userId"),
+ * @FieldMapping(name = "fullName", field = "profile.name")
+ * }
+ * )
* public List
This annotation does not affect pagination parameters * such as page number or page size. It only governs which fields may be used @@ -21,8 +23,9 @@ * *
{@code
* @GetMapping
+ * @WebQuery(entityClass = User.class)
* public Page search(
- * @RestrictedPageable(entityClass = User.class) Pageable pageable
+ * @RestrictedPageable Pageable pageable
* ) {
* ...
* }
@@ -33,14 +36,4 @@
@Documented
public @interface RestrictedPageable {
- /**
- * Entity class whose fields define the set of properties
- * that may be used for sorting.
- *
- * Only fields annotated with {@link Sortable} on this entity
- * are permitted in the {@code sort} request parameter.
- *
- * @return the entity class used for sort validation
- */
- Class> entityClass();
}
diff --git a/spring-web-query-core/src/main/java/in/co/akshitbansal/springwebquery/annotation/RsqlSpec.java b/spring-web-query-core/src/main/java/in/co/akshitbansal/springwebquery/annotation/RsqlSpec.java
index 78f3093..7be07b5 100644
--- a/spring-web-query-core/src/main/java/in/co/akshitbansal/springwebquery/annotation/RsqlSpec.java
+++ b/spring-web-query-core/src/main/java/in/co/akshitbansal/springwebquery/annotation/RsqlSpec.java
@@ -9,14 +9,16 @@
* When applied to a method parameter, the annotated parameter will receive a Specification
* built from the RSQL query provided in the HTTP request. The RSQL query is parsed,
* validated against the target entity's {@link RsqlFilterable} annotations, and converted
- * into a Spring Data JPA {@link org.springframework.data.jpa.domain.Specification}.
+ * into a Spring Data JPA {@link org.springframework.data.jpa.domain.Specification}. Entity
+ * metadata and alias mappings are sourced from {@link WebQuery} on the same controller method.
*
*
* Example usage in a controller:
* {@code
* @GetMapping("/users")
+ * @WebQuery(entityClass = User.class)
* public List search(
- * @RsqlSpec(entityClass = User.class, paramName = "filter") Specification spec
+ * @RsqlSpec(paramName = "filter") Specification spec
* ) {
* return userRepository.findAll(spec);
* }
@@ -33,25 +35,6 @@
@Documented
public @interface RsqlSpec {
- /**
- * The entity class for which the Specification should be built.
- * This class is used to validate the RSQL query fields and operators
- * against the {@link RsqlFilterable} annotations.
- *
- * @return the target entity class
- */
- Class> entityClass();
-
- /**
- * Optional field mappings for aliasing entity fields.
- *
- * Allows mapping query parameter field names (aliases) to actual entity field names.
- *
- *
- * @return an array of {@link FieldMapping} annotations
- */
- FieldMapping[] fieldMappings() default {};
-
/**
* The name of the query parameter that contains the RSQL string.
* Defaults to "filter".
diff --git a/spring-web-query-core/src/main/java/in/co/akshitbansal/springwebquery/annotation/WebQuery.java b/spring-web-query-core/src/main/java/in/co/akshitbansal/springwebquery/annotation/WebQuery.java
new file mode 100644
index 0000000..fd5be7e
--- /dev/null
+++ b/spring-web-query-core/src/main/java/in/co/akshitbansal/springwebquery/annotation/WebQuery.java
@@ -0,0 +1,35 @@
+package in.co.akshitbansal.springwebquery.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Declares shared web-query metadata for a controller method.
+ *
+ * This annotation is intended to be placed on handler methods so query-related
+ * configuration can be defined once and reused by both filtering and sorting
+ * argument resolvers.
+ *
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface WebQuery {
+
+ /**
+ * Entity class against which filter and sort fields are resolved.
+ *
+ * @return target entity class
+ */
+ Class> entityClass();
+
+ /**
+ * Optional field mappings used to expose API-facing aliases for entity fields.
+ *
+ * @return mappings between API names and entity paths
+ */
+ FieldMapping[] fieldMappings() default {};
+}
diff --git a/spring-web-query-core/src/main/java/in/co/akshitbansal/springwebquery/util/AnnotationUtil.java b/spring-web-query-core/src/main/java/in/co/akshitbansal/springwebquery/util/AnnotationUtil.java
new file mode 100644
index 0000000..af20f93
--- /dev/null
+++ b/spring-web-query-core/src/main/java/in/co/akshitbansal/springwebquery/util/AnnotationUtil.java
@@ -0,0 +1,41 @@
+package in.co.akshitbansal.springwebquery.util;
+
+import in.co.akshitbansal.springwebquery.annotation.WebQuery;
+import in.co.akshitbansal.springwebquery.exception.QueryConfigurationException;
+import lombok.NonNull;
+import org.springframework.core.MethodParameter;
+
+import java.lang.reflect.Method;
+import java.text.MessageFormat;
+
+/**
+ * Utility methods for resolving query-related annotations from controller metadata.
+ */
+public class AnnotationUtil {
+
+ /**
+ * Resolves {@link WebQuery} from the controller method that declares the
+ * provided Spring MVC method parameter.
+ *
+ * @param parameter controller method parameter currently being resolved
+ * @return resolved {@link WebQuery} annotation
+ * @throws QueryConfigurationException if the method cannot be resolved or is not annotated with {@link WebQuery}
+ */
+ public static WebQuery resolveWebQueryFromParameter(@NonNull MethodParameter parameter) {
+ // Retrieve the controller method
+ Method controllerMethod = parameter.getMethod();
+ // Ensure that the method is not null (should not happen for valid controller parameters)
+ if(controllerMethod == null) throw new QueryConfigurationException(MessageFormat.format(
+ "Unable to resolve controller method for parameter {0}", parameter
+ ));
+ // Retrieve the @WebQuery annotation from the controller method to access query configuration
+ WebQuery webQueryAnnotation = controllerMethod.getAnnotation(WebQuery.class);
+ // Ensure that the method is annotated with @WebQuery to access query configuration
+ if(webQueryAnnotation == null)
+ throw new QueryConfigurationException(MessageFormat.format(
+ "Controller method {0} must be annotated with @WebQuery to use query argument resolvers",
+ controllerMethod
+ ));
+ return webQueryAnnotation;
+ }
+}
diff --git a/spring-web-query-core/src/test/java/in/co/akshitbansal/springwebquery/RestrictedPageableArgumentResolverTest.java b/spring-web-query-core/src/test/java/in/co/akshitbansal/springwebquery/RestrictedPageableArgumentResolverTest.java
index eee85f0..81f7661 100644
--- a/spring-web-query-core/src/test/java/in/co/akshitbansal/springwebquery/RestrictedPageableArgumentResolverTest.java
+++ b/spring-web-query-core/src/test/java/in/co/akshitbansal/springwebquery/RestrictedPageableArgumentResolverTest.java
@@ -1,7 +1,10 @@
package in.co.akshitbansal.springwebquery;
+import in.co.akshitbansal.springwebquery.annotation.FieldMapping;
import in.co.akshitbansal.springwebquery.annotation.RestrictedPageable;
import in.co.akshitbansal.springwebquery.annotation.Sortable;
+import in.co.akshitbansal.springwebquery.annotation.WebQuery;
+import in.co.akshitbansal.springwebquery.exception.QueryConfigurationException;
import in.co.akshitbansal.springwebquery.exception.QueryValidationException;
import org.junit.jupiter.api.Test;
import org.springframework.core.MethodParameter;
@@ -46,6 +49,16 @@ void resolveArgument_allowsSortableField() throws NoSuchMethodException {
assertEquals("name", pageable.getSort().iterator().next().getProperty());
}
+ @Test
+ void resolveArgument_rewritesMappedAliasField() throws NoSuchMethodException {
+ Method method = TestController.class.getDeclaredMethod("searchWithMapping", Pageable.class);
+ MethodParameter parameter = new MethodParameter(method, 0);
+ NativeWebRequest request = requestWithSort("displayName,asc");
+
+ Pageable pageable = resolver.resolveArgument(parameter, null, request, null);
+ assertEquals("name", pageable.getSort().iterator().next().getProperty());
+ }
+
@Test
void resolveArgument_rejectsNonSortableField() throws NoSuchMethodException {
Method method = TestController.class.getDeclaredMethod("search", Pageable.class);
@@ -55,6 +68,34 @@ void resolveArgument_rejectsNonSortableField() throws NoSuchMethodException {
assertThrows(QueryValidationException.class, () -> resolver.resolveArgument(parameter, null, request, null));
}
+ @Test
+ void resolveArgument_rejectsOriginalMappedFieldWhenNotAllowed() throws NoSuchMethodException {
+ Method method = TestController.class.getDeclaredMethod("searchWithMapping", Pageable.class);
+ MethodParameter parameter = new MethodParameter(method, 0);
+ NativeWebRequest request = requestWithSort("name,asc");
+
+ assertThrows(QueryValidationException.class, () -> resolver.resolveArgument(parameter, null, request, null));
+ }
+
+ @Test
+ void resolveArgument_allowsOriginalMappedFieldWhenAllowed() throws NoSuchMethodException {
+ Method method = TestController.class.getDeclaredMethod("searchWithMappingAllowOriginal", Pageable.class);
+ MethodParameter parameter = new MethodParameter(method, 0);
+ NativeWebRequest request = requestWithSort("name,asc");
+
+ Pageable pageable = resolver.resolveArgument(parameter, null, request, null);
+ assertEquals("name", pageable.getSort().iterator().next().getProperty());
+ }
+
+ @Test
+ void resolveArgument_rejectsWhenWebQueryMissing() throws NoSuchMethodException {
+ Method method = TestController.class.getDeclaredMethod("searchWithoutWebQuery", Pageable.class);
+ MethodParameter parameter = new MethodParameter(method, 0);
+ NativeWebRequest request = requestWithSort("name,asc");
+
+ assertThrows(QueryConfigurationException.class, () -> resolver.resolveArgument(parameter, null, request, null));
+ }
+
private static NativeWebRequest requestWithSort(String sort) {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("sort", sort);
@@ -63,16 +104,37 @@ private static NativeWebRequest requestWithSort(String sort) {
@SuppressWarnings("unused")
private static class TestController {
- void search(@RestrictedPageable(entityClass = SortEntity.class) Pageable pageable) {
+
+ @WebQuery(entityClass = SortEntity.class)
+ void search(@RestrictedPageable Pageable pageable) {
}
void searchWithoutAnnotation(Pageable pageable) {
}
+
+ @WebQuery(
+ entityClass = SortEntity.class,
+ fieldMappings = {@FieldMapping(name = "displayName", field = "name")}
+ )
+ void searchWithMapping(@RestrictedPageable Pageable pageable) {
+ }
+
+ @WebQuery(
+ entityClass = SortEntity.class,
+ fieldMappings = {@FieldMapping(name = "displayName", field = "name", allowOriginalFieldName = true)}
+ )
+ void searchWithMappingAllowOriginal(@RestrictedPageable Pageable pageable) {
+ }
+
+ void searchWithoutWebQuery(@RestrictedPageable Pageable pageable) {
+ }
}
private static class SortEntity {
+
@Sortable
private String name;
+
private String secret;
}
}
diff --git a/spring-web-query-core/src/test/java/in/co/akshitbansal/springwebquery/RsqlSpecificationArgumentResolverTest.java b/spring-web-query-core/src/test/java/in/co/akshitbansal/springwebquery/RsqlSpecificationArgumentResolverTest.java
index 974722d..9d5d710 100644
--- a/spring-web-query-core/src/test/java/in/co/akshitbansal/springwebquery/RsqlSpecificationArgumentResolverTest.java
+++ b/spring-web-query-core/src/test/java/in/co/akshitbansal/springwebquery/RsqlSpecificationArgumentResolverTest.java
@@ -4,6 +4,8 @@
import in.co.akshitbansal.springwebquery.annotation.FieldMapping;
import in.co.akshitbansal.springwebquery.annotation.RsqlFilterable;
import in.co.akshitbansal.springwebquery.annotation.RsqlSpec;
+import in.co.akshitbansal.springwebquery.annotation.WebQuery;
+import in.co.akshitbansal.springwebquery.exception.QueryConfigurationException;
import in.co.akshitbansal.springwebquery.exception.QueryValidationException;
import in.co.akshitbansal.springwebquery.operator.RsqlCustomOperator;
import in.co.akshitbansal.springwebquery.operator.RsqlOperator;
@@ -50,7 +52,8 @@ void resolveArgument_returnsUnrestrictedWhenFilterMissing() throws NoSuchMethodE
Method method = TestController.class.getDeclaredMethod("search", Specification.class);
MethodParameter parameter = new MethodParameter(method, 0);
- resolver.resolveArgument(parameter, null, emptyRequest(), null);
+ Specification> spec = resolver.resolveArgument(parameter, null, emptyRequest(), null);
+ assertNotNull(spec);
}
@Test
@@ -102,6 +105,25 @@ void resolveArgument_allowsCustomOperator() throws NoSuchMethodException {
resolverWithCustom.resolveArgument(parameter, null, webRequest, null);
}
+ @Test
+ void resolveArgument_usesCustomParameterName() throws NoSuchMethodException {
+ Method method = TestController.class.getDeclaredMethod("searchWithCustomParam", Specification.class);
+ MethodParameter parameter = new MethodParameter(method, 0);
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ request.setParameter("q", "name==john");
+
+ resolver.resolveArgument(parameter, null, new ServletWebRequest(request), null);
+ }
+
+ @Test
+ void resolveArgument_rejectsWhenWebQueryMissing() throws NoSuchMethodException {
+ Method method = TestController.class.getDeclaredMethod("searchWithoutWebQuery", Specification.class);
+ MethodParameter parameter = new MethodParameter(method, 0);
+ NativeWebRequest webRequest = requestWithFilter("name==john");
+
+ assertThrows(QueryConfigurationException.class, () -> resolver.resolveArgument(parameter, null, webRequest, null));
+ }
+
private static NativeWebRequest emptyRequest() {
return new ServletWebRequest(new MockHttpServletRequest());
}
@@ -132,38 +154,53 @@ public Predicate toPredicate(RSQLCustomPredicateInput input) {
@SuppressWarnings("unused")
private static class TestController {
- void search(@RsqlSpec(entityClass = TestEntity.class) Specification specification) {
+
+ @WebQuery(entityClass = TestEntity.class)
+ void search(@RsqlSpec Specification specification) {
}
void searchWithoutAnnotation(Specification specification) {
}
- void searchWithMapping(@RsqlSpec(
+ @WebQuery(
entityClass = TestEntity.class,
fieldMappings = {
@FieldMapping(name = "displayName", field = "name")
}
- ) Specification specification) {
+ )
+ void searchWithMapping(@RsqlSpec Specification specification) {
}
- void searchWithMappingAllowOriginal(@RsqlSpec(
+ @WebQuery(
entityClass = TestEntity.class,
fieldMappings = {
@FieldMapping(name = "displayName", field = "name", allowOriginalFieldName = true)
}
- ) Specification specification) {
+ )
+ void searchWithMappingAllowOriginal(@RsqlSpec Specification specification) {
+ }
+
+ @WebQuery(entityClass = TestEntityWithCustom.class)
+ void searchWithCustom(@RsqlSpec Specification specification) {
}
- void searchWithCustom(@RsqlSpec(entityClass = TestEntityWithCustom.class) Specification specification) {
+ @WebQuery(entityClass = TestEntity.class)
+ void searchWithCustomParam(@RsqlSpec(paramName = "q") Specification specification) {
+ }
+
+ void searchWithoutWebQuery(@RsqlSpec Specification specification) {
}
}
private static class TestEntity {
+
@RsqlFilterable(operators = {RsqlOperator.EQUAL})
+
private String name;
}
private static class TestEntityWithCustom {
+
@RsqlFilterable(operators = {RsqlOperator.EQUAL}, customOperators = {MockCustomOperator.class})
private String name;
}
diff --git a/spring-web-query-core/src/test/java/in/co/akshitbansal/springwebquery/ValidationRSQLVisitorTest.java b/spring-web-query-core/src/test/java/in/co/akshitbansal/springwebquery/ValidationRSQLVisitorTest.java
index 7e5c27a..6ba7d41 100644
--- a/spring-web-query-core/src/test/java/in/co/akshitbansal/springwebquery/ValidationRSQLVisitorTest.java
+++ b/spring-web-query-core/src/test/java/in/co/akshitbansal/springwebquery/ValidationRSQLVisitorTest.java
@@ -70,6 +70,40 @@ void visit_rejectsDisallowedOperator() {
assertEquals("Operator '!=' not allowed on field 'name'", ex.getMessage());
}
+ @Test
+ void visit_allowsMappedAliasField() {
+ ValidationRSQLVisitor visitor = new ValidationRSQLVisitor(
+ TestEntity.class,
+ new FieldMapping[]{mapping("displayName", "name", false)},
+ Collections.emptySet()
+ );
+ parser.parse("displayName==john").accept(visitor);
+ }
+
+ @Test
+ void visit_rejectsOriginalMappedFieldWhenNotAllowed() {
+ ValidationRSQLVisitor visitor = new ValidationRSQLVisitor(
+ TestEntity.class,
+ new FieldMapping[]{mapping("displayName", "name", false)},
+ Collections.emptySet()
+ );
+ QueryValidationException ex = assertThrows(
+ QueryValidationException.class,
+ () -> parser.parse("name==john").accept(visitor)
+ );
+ assertEquals("Unknown field 'name'", ex.getMessage());
+ }
+
+ @Test
+ void visit_allowsOriginalMappedFieldWhenAllowed() {
+ ValidationRSQLVisitor visitor = new ValidationRSQLVisitor(
+ TestEntity.class,
+ new FieldMapping[]{mapping("displayName", "name", true)},
+ Collections.emptySet()
+ );
+ parser.parse("name==john").accept(visitor);
+ }
+
@Test
void visit_allowsCustomOperator() {
Set> customOperators = Set.of(new MockCustomOperator());
@@ -108,6 +142,7 @@ public Predicate toPredicate(RSQLCustomPredicateInput input) {
}
private static class TestEntity {
+
@RsqlFilterable(operators = {RsqlOperator.EQUAL})
private String name;
@@ -115,7 +150,33 @@ private static class TestEntity {
}
private static class TestEntityWithCustom {
+
@RsqlFilterable(operators = {RsqlOperator.EQUAL}, customOperators = {MockCustomOperator.class})
private String name;
}
+
+ private static FieldMapping mapping(String name, String field, boolean allowOriginalFieldName) {
+ return new FieldMapping() {
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public String field() {
+ return field;
+ }
+
+ @Override
+ public boolean allowOriginalFieldName() {
+ return allowOriginalFieldName;
+ }
+
+ @Override
+ public Class extends java.lang.annotation.Annotation> annotationType() {
+ return FieldMapping.class;
+ }
+ };
+ }
}
diff --git a/spring-web-query-core/src/test/java/in/co/akshitbansal/springwebquery/util/AnnotationUtilTest.java b/spring-web-query-core/src/test/java/in/co/akshitbansal/springwebquery/util/AnnotationUtilTest.java
new file mode 100644
index 0000000..236dde4
--- /dev/null
+++ b/spring-web-query-core/src/test/java/in/co/akshitbansal/springwebquery/util/AnnotationUtilTest.java
@@ -0,0 +1,48 @@
+package in.co.akshitbansal.springwebquery.util;
+
+import in.co.akshitbansal.springwebquery.annotation.RsqlSpec;
+import in.co.akshitbansal.springwebquery.annotation.WebQuery;
+import in.co.akshitbansal.springwebquery.exception.QueryConfigurationException;
+import org.junit.jupiter.api.Test;
+import org.springframework.core.MethodParameter;
+import org.springframework.data.jpa.domain.Specification;
+
+import java.lang.reflect.Method;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class AnnotationUtilTest {
+
+ @Test
+ void resolveWebQueryFromParameter_returnsAnnotation() throws NoSuchMethodException {
+ Method method = TestController.class.getDeclaredMethod("search", Specification.class);
+ MethodParameter parameter = new MethodParameter(method, 0);
+
+ WebQuery annotation = AnnotationUtil.resolveWebQueryFromParameter(parameter);
+
+ assertEquals(TestEntity.class, annotation.entityClass());
+ }
+
+ @Test
+ void resolveWebQueryFromParameter_throwsWhenMissing() throws NoSuchMethodException {
+ Method method = TestController.class.getDeclaredMethod("searchWithoutWebQuery", Specification.class);
+ MethodParameter parameter = new MethodParameter(method, 0);
+
+ assertThrows(QueryConfigurationException.class, () -> AnnotationUtil.resolveWebQueryFromParameter(parameter));
+ }
+
+ @SuppressWarnings("unused")
+ private static class TestController {
+
+ @WebQuery(entityClass = TestEntity.class)
+ void search(@RsqlSpec Specification specification) {
+ }
+
+ void searchWithoutWebQuery(@RsqlSpec Specification specification) {
+ }
+ }
+
+ private static class TestEntity {
+ }
+}