diff --git a/.github/badges/branches.svg b/.github/badges/branches.svg
index e3ca5be..cc80306 100644
--- a/.github/badges/branches.svg
+++ b/.github/badges/branches.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg
index a4283c9..4f41679 100644
--- a/.github/badges/jacoco.svg
+++ b/.github/badges/jacoco.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 7ddf481..86d6628 100644
--- a/README.md
+++ b/README.md
@@ -96,6 +96,37 @@ Receive a `MagicFilter` as parameter of a `@GetMapping` and use `filter.getSpec(
//}
```
+## Enable support for Spring WebFlux and R2DBC
+
+This lib is compatible with R2DBC. Check out an example in the `examples\java-gradle-reactive`.
+
+```java
+@EnableR2dbcMagicFilter
+//@SpringBootApplication
+//public class DemoApplication
+```
+
+You can either use the fluent api:
+```java
+// https://docs.spring.io/spring-data/r2dbc/docs/current/reference/html/#r2dbc.entityoperations.fluent-api
+@GetMapping
+Flux getUsers(R2dbcMagicFilter filter, Pageable pageable) {
+ Criteria criteria = filter.toCriteria(User.class, DbFeatures.NONE);
+ return r2dbcTemplate.select(User.class).matching(query(criteria)).all();
+}
+```
+
+Or, extend ReactiveCrudRepository and implement ReactiveSearchRepository
+
+```java
+// see how on my discussion on stack overflow (https://stackoverflow.com/questions/73424096/reactivecrudrepository-criteria-query)
+@GetMapping("/paged")
+Mono> getUsersPaged(R2dbcMagicFilter filter, Pageable pageable) {
+ Criteria criteria = filter.toCriteria(User.class, DbFeatures.NONE);
+ return userRepository.findAll(criteria, pageable, User.class);
+}
+```
+
## How to use
A `filter predicate` is composed of a `field`, an `operator`, and a `value`.
@@ -119,7 +150,7 @@ where u.name like '%Matthew%' and u.age > 23 and c.name = 'London'
## Supported Operators
| Operator | Suffix | Example |
-| ------------------ | ------------- | --------------------------- |
+|--------------------|---------------|-----------------------------|
| EQUAL | | `?name=Matthew` |
| GREATER_THAN | _gt | `?age_gt=32` |
| GREATER_THAN_EQUAL | _ge | `?age_ge=32` |
@@ -248,9 +279,15 @@ filter.toSpecification(User::class.java, DbFeatures.POSTGRES)
I'd like to suggest you have a look at the tests. You might have some fun exploring it.
-Also, you will find an java+maven project example in the folder: `examples/java-maven-h2`.
+Also, you will find a java+maven project example in the folder: `examples/java-maven-h2`.
## Contributing to the Project
If you'd like to contribute code to this project you can do so through GitHub by forking the repository and generating a pull request.
By contributing your code, you agree to license your contribution under the terms of the Apache Licence.
+
+## TODO
+1. add reactive example
+2. rename the examples
+3. remove the SEARCH_IN_SEPARATOR_PRM & SEARCH_IN_SEPARATOR_DEF
+4. remove POSTGRES unaccent for reactive
diff --git a/build.gradle.kts b/build.gradle.kts
index 521cf59..1dc2724 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -2,15 +2,15 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.util.Base64
plugins {
- id("org.springframework.boot") version "2.6.1"
- id("io.spring.dependency-management") version "1.0.11.RELEASE"
- kotlin("jvm") version "1.6.0"
- kotlin("plugin.spring") version "1.6.0"
- kotlin("plugin.jpa") version "1.6.10"
+ id("org.springframework.boot") version "2.7.3"
+ id("io.spring.dependency-management") version "1.0.13.RELEASE"
+ kotlin("jvm") version "1.6.21"
+ kotlin("plugin.spring") version "1.6.21"
+ kotlin("plugin.jpa") version "1.6.21"
- kotlin("kapt") version "1.6.0"
+ kotlin("kapt") version "1.6.21"
- id("org.jlleitschuh.gradle.ktlint") version "10.2.0"
+ id("org.jlleitschuh.gradle.ktlint") version "10.3.0"
`java-library`
`maven-publish`
id("io.github.gradle-nexus.publish-plugin") version "1.1.0"
@@ -19,7 +19,7 @@ plugins {
}
group = "io.github.verissimor.lib"
-version = System.getenv("RELEASE_VERSION") ?: "0.0.11-SNAPSHOT"
+version = System.getenv("RELEASE_VERSION") ?: "1.0.0-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_1_8
@@ -41,10 +41,12 @@ repositories {
dependencies {
// spring
- implementation("org.springframework.boot:spring-boot-starter-data-jpa")
- implementation("org.springframework.boot:spring-boot-starter-web")
- implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
+ compileOnly("org.springframework.boot:spring-boot-starter-data-jpa")
+ compileOnly("org.springframework.boot:spring-boot-starter-web")
+ compileOnly("org.springframework.boot:spring-boot-starter-data-r2dbc")
+ compileOnly("org.springframework.boot:spring-boot-starter-webflux")
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
@@ -55,6 +57,9 @@ dependencies {
// test service
testImplementation("com.h2database:h2")
+ testImplementation("org.springframework.boot:spring-boot-starter-data-jpa")
+ testImplementation("org.springframework.boot:spring-boot-starter-web")
+ testImplementation("org.springframework.data:spring-data-relational:2.4.2")
}
tasks.withType {
diff --git a/examples/java-gradle-reactive/README.md b/examples/java-gradle-reactive/README.md
new file mode 100644
index 0000000..ca20add
--- /dev/null
+++ b/examples/java-gradle-reactive/README.md
@@ -0,0 +1,25 @@
+# R2dbc Magic Filter Example
+
+This is an example of usage of the library `R2dbc Magic Filter`
+
+## Running the project
+
+To run this project, use:
+
+```shell
+./gradlew bootRun
+```
+
+## get users
+
+Use the paraters as described in the readme.md of the main lib. Eg.:
+
+```
+http://localhost:8080/api/users?name_like=erick&gender_in=MALE,FEMALE&cityId_gt=1
+
+http://localhost:8080/api/users/paged?size=1&gender=FEMALE
+
+http://localhost:8080/api/users/fluent?size=1&gender=FEMALE
+```
+
+Have fun ;)
\ No newline at end of file
diff --git a/examples/java-gradle-reactive/src/main/java/io/github/verissimor/examples/reactive/javagradlereactive/configuration/PageableWebFluxConfiguration.java b/examples/java-gradle-reactive/src/main/java/io/github/verissimor/examples/reactive/javagradlereactive/configuration/PageableWebFluxConfiguration.java
new file mode 100644
index 0000000..87c98b4
--- /dev/null
+++ b/examples/java-gradle-reactive/src/main/java/io/github/verissimor/examples/reactive/javagradlereactive/configuration/PageableWebFluxConfiguration.java
@@ -0,0 +1,15 @@
+package io.github.verissimor.examples.reactive.javagradlereactive.configuration;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.web.ReactivePageableHandlerMethodArgumentResolver;
+import org.springframework.web.reactive.config.WebFluxConfigurer;
+import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
+
+@Configuration
+public class PageableWebFluxConfiguration implements WebFluxConfigurer {
+
+ @Override
+ public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
+ configurer.addCustomResolver(new ReactivePageableHandlerMethodArgumentResolver());
+ }
+}
diff --git a/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/domain/ParsedField.kt b/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/domain/ParsedField.kt
index 38c77aa..8c85ebd 100644
--- a/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/domain/ParsedField.kt
+++ b/src/main/kotlin/io/github/verissimor/lib/jpamagicfilter/domain/ParsedField.kt
@@ -9,7 +9,6 @@ import java.lang.reflect.Field
import java.math.BigDecimal
import java.time.Instant
import java.time.LocalDate
-import javax.persistence.Enumerated
import javax.persistence.criteria.Path
import javax.persistence.criteria.Root
@@ -30,18 +29,29 @@ data class ParsedField(
return fullPath ?: error("unexpected condition to parse path of $resolvedFieldName")
}
- fun getFieldType(): FieldType? = when {
- fieldClass == null -> null
- fieldClass.getDeclaredAnnotationsByType(Enumerated::class.java).isNotEmpty() -> ENUMERATED
- fieldClass.type == LocalDate::class.java -> LOCAL_DATE
- fieldClass.type == Instant::class.java -> INSTANT
- fieldClass.type == Boolean::class.java -> BOOLEAN
+ fun getFieldType(): FieldType? = getFieldType(fieldClass)
- fieldClass.type == Int::class.java ||
- fieldClass.type == Long::class.java ||
- fieldClass.type == BigDecimal::class.java ||
- fieldClass.type.isAssignableFrom(Number::class.java) -> NUMBER
+ companion object {
+ fun getFieldType(fieldClass: Field?): FieldType? = when {
+ fieldClass == null -> null
+ fieldClass.type?.superclass?.name == "java.lang.Enum" -> ENUMERATED
+ fieldClass.type == LocalDate::class.java -> LOCAL_DATE
+ fieldClass.type == Instant::class.java -> INSTANT
+ fieldClass.type == Boolean::class.java ||
+ // this solves conflicts between kotlin/java
+ fieldClass.type.name == "java.lang.Boolean" -> BOOLEAN
- else -> FieldType.GENERIC
+ fieldClass.type == Int::class.java ||
+ fieldClass.type == Long::class.java ||
+ fieldClass.type == BigDecimal::class.java ||
+ fieldClass.type.isAssignableFrom(Number::class.java) ||
+ // this solves conflicts between kotlin/java
+ fieldClass.type.name == "java.lang.Integer" ||
+ fieldClass.type.name == "java.math.BigDecimal" ||
+ fieldClass.type.name == "java.lang.Long" ||
+ fieldClass.type.isAssignableFrom(java.lang.Number::class.java) -> NUMBER
+
+ else -> FieldType.GENERIC
+ }
}
}
diff --git a/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/EnableR2dbcMagicFilter.kt b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/EnableR2dbcMagicFilter.kt
new file mode 100644
index 0000000..2494ccb
--- /dev/null
+++ b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/EnableR2dbcMagicFilter.kt
@@ -0,0 +1,6 @@
+package io.github.verissimor.lib.r2dbcmagicfilter
+
+import org.springframework.context.annotation.Import
+
+@Import(R2dbcMagicFilterConfigurer::class)
+annotation class EnableR2dbcMagicFilter
diff --git a/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcFieldParser.kt b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcFieldParser.kt
new file mode 100644
index 0000000..1655428
--- /dev/null
+++ b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcFieldParser.kt
@@ -0,0 +1,48 @@
+package io.github.verissimor.lib.r2dbcmagicfilter
+
+import io.github.verissimor.lib.jpamagicfilter.domain.FilterOperator
+import io.github.verissimor.lib.r2dbcmagicfilter.domain.R2dbcParsedField
+import java.lang.reflect.Field
+
+object R2dbcFieldParser {
+
+ fun parseField(field: String, value: Array?, clazz: Class<*>): R2dbcParsedField {
+ val normalized = normalize(field)
+ val filterType = fieldToType(normalized, value)
+ val resolvedFieldName = resolveFieldName(normalized, filterType)
+ val fieldClass: Field? = fieldToClass(resolvedFieldName, clazz)
+
+ return R2dbcParsedField(filterType, resolvedFieldName, fieldClass)
+ }
+
+ private fun normalize(field: String) = field.trim().replace("[]", "")
+
+ private fun fieldToType(field: String, value: Array?): FilterOperator {
+ val type = FilterOperator.values()
+ .sortedByDescending { it.suffix.length }
+ .firstOrNull { field.contains(it.suffix) } ?: FilterOperator.EQUAL
+
+ if (value != null && type == FilterOperator.EQUAL && value.size > 1) {
+ return FilterOperator.IN
+ }
+
+ return type
+ }
+
+ private fun resolveFieldName(field: String, type: FilterOperator?) =
+ type?.let { field.replace(it.suffix, "") } ?: field
+
+ private fun fieldToClass(field: String, root: Class<*>): Field? {
+ var resultField: Field? = null
+ field.split(".")
+ .forEach { fieldP ->
+ resultField = if (resultField == null) {
+ root.declaredFields.firstOrNull { it.name == fieldP }
+ } else {
+ resultField?.type?.declaredFields?.firstOrNull { it.name == fieldP }
+ }
+ }
+
+ return resultField
+ }
+}
diff --git a/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilter.kt b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilter.kt
new file mode 100644
index 0000000..210b250
--- /dev/null
+++ b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilter.kt
@@ -0,0 +1,39 @@
+package io.github.verissimor.lib.r2dbcmagicfilter
+
+import io.github.verissimor.lib.jpamagicfilter.MagicFilter.Companion.SEARCH_TYPE_AND
+import io.github.verissimor.lib.jpamagicfilter.MagicFilter.Companion.SEARCH_TYPE_OR
+import io.github.verissimor.lib.jpamagicfilter.MagicFilter.Companion.SEARCH_TYPE_PRM
+import io.github.verissimor.lib.jpamagicfilter.domain.DbFeatures
+import io.github.verissimor.lib.jpamagicfilter.domain.DbFeatures.NONE
+import io.github.verissimor.lib.jpamagicfilter.toSingleParameter
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import org.springframework.data.relational.core.query.Criteria
+import org.springframework.util.MultiValueMap
+
+class R2dbcMagicFilter(
+ private val parameterMap: MultiValueMap
+) {
+
+ val log: Logger = LoggerFactory.getLogger(R2dbcMagicFilter::class.java)
+
+ fun toCriteria(clazz: Class<*>, dbFeatures: DbFeatures = NONE): Criteria {
+
+ val map: Map?> = parameterMap.keys.associateWith { parameterMap[it]?.toTypedArray() }
+ val parsed = R2dbcPredicateParser.parsePredicates(map, clazz, dbFeatures)
+
+ if (parsed.isEmpty()) {
+ return Criteria.empty()
+ }
+
+ val criteria = when (map.toSingleParameter(SEARCH_TYPE_PRM)) {
+ SEARCH_TYPE_AND, null -> parsed.reduce { acc, criteria -> acc.and(criteria) }
+ SEARCH_TYPE_OR -> parsed.reduce { acc, criteria -> acc.or(criteria) }
+ else -> error("Invalid searchType. Only allowed: and, or")
+ }
+
+ log.info(criteria.toString())
+
+ return criteria
+ }
+}
diff --git a/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterConfigurer.kt b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterConfigurer.kt
new file mode 100644
index 0000000..9917286
--- /dev/null
+++ b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterConfigurer.kt
@@ -0,0 +1,30 @@
+package io.github.verissimor.lib.r2dbcmagicfilter
+
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.MethodParameter
+import org.springframework.web.reactive.BindingContext
+import org.springframework.web.reactive.config.WebFluxConfigurer
+import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver
+import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer
+import org.springframework.web.server.ServerWebExchange
+import org.springframework.web.servlet.config.annotation.EnableWebMvc
+import reactor.core.publisher.Mono
+
+@Configuration
+@EnableWebMvc
+class R2dbcMagicFilterConfigurer : WebFluxConfigurer {
+ override fun configureArgumentResolvers(configurer: ArgumentResolverConfigurer) {
+ super.configureArgumentResolvers(configurer)
+ configurer.addCustomResolver(R2dbcMagicFilterAttributeResolver())
+ }
+}
+
+class R2dbcMagicFilterAttributeResolver : HandlerMethodArgumentResolver {
+ override fun supportsParameter(parameter: MethodParameter): Boolean {
+ return parameter.parameterType == R2dbcMagicFilter::class.java
+ }
+
+ override fun resolveArgument(parameter: MethodParameter, bindingContext: BindingContext, exchange: ServerWebExchange): Mono {
+ return Mono.just(R2dbcMagicFilter(exchange.request.queryParams))
+ }
+}
diff --git a/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcPredicateParser.kt b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcPredicateParser.kt
new file mode 100644
index 0000000..721ec27
--- /dev/null
+++ b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcPredicateParser.kt
@@ -0,0 +1,178 @@
+package io.github.verissimor.lib.r2dbcmagicfilter
+
+import io.github.verissimor.lib.jpamagicfilter.MagicFilter.Companion.SEARCH_IN_SEPARATOR_DEF
+import io.github.verissimor.lib.jpamagicfilter.MagicFilter.Companion.SEARCH_IN_SEPARATOR_PRM
+import io.github.verissimor.lib.jpamagicfilter.domain.DbFeatures
+import io.github.verissimor.lib.jpamagicfilter.domain.DbFeatures.NONE
+import io.github.verissimor.lib.jpamagicfilter.domain.DbFeatures.POSTGRES
+import io.github.verissimor.lib.jpamagicfilter.domain.FieldType.BOOLEAN
+import io.github.verissimor.lib.jpamagicfilter.domain.FieldType.ENUMERATED
+import io.github.verissimor.lib.jpamagicfilter.domain.FieldType.GENERIC
+import io.github.verissimor.lib.jpamagicfilter.domain.FieldType.INSTANT
+import io.github.verissimor.lib.jpamagicfilter.domain.FieldType.LOCAL_DATE
+import io.github.verissimor.lib.jpamagicfilter.domain.FieldType.NUMBER
+import io.github.verissimor.lib.jpamagicfilter.domain.FilterOperator.EQUAL
+import io.github.verissimor.lib.jpamagicfilter.domain.FilterOperator.GREATER_THAN
+import io.github.verissimor.lib.jpamagicfilter.domain.FilterOperator.GREATER_THAN_EQUAL
+import io.github.verissimor.lib.jpamagicfilter.domain.FilterOperator.IN
+import io.github.verissimor.lib.jpamagicfilter.domain.FilterOperator.IS_NOT_NULL
+import io.github.verissimor.lib.jpamagicfilter.domain.FilterOperator.IS_NULL
+import io.github.verissimor.lib.jpamagicfilter.domain.FilterOperator.LESS_THAN
+import io.github.verissimor.lib.jpamagicfilter.domain.FilterOperator.LESS_THAN_EQUAL
+import io.github.verissimor.lib.jpamagicfilter.domain.FilterOperator.LIKE
+import io.github.verissimor.lib.jpamagicfilter.domain.FilterOperator.LIKE_EXP
+import io.github.verissimor.lib.jpamagicfilter.domain.FilterOperator.NOT_IN
+import io.github.verissimor.lib.jpamagicfilter.domain.FilterOperator.NOT_LIKE
+import io.github.verissimor.lib.jpamagicfilter.domain.FilterOperator.NOT_LIKE_EXP
+import io.github.verissimor.lib.jpamagicfilter.toSingleBigDecimal
+import io.github.verissimor.lib.jpamagicfilter.toSingleBoolean
+import io.github.verissimor.lib.jpamagicfilter.toSingleDate
+import io.github.verissimor.lib.jpamagicfilter.toSingleInstant
+import io.github.verissimor.lib.jpamagicfilter.toSingleParameter
+import io.github.verissimor.lib.jpamagicfilter.toSingleString
+import io.github.verissimor.lib.jpamagicfilter.unaccent
+import io.github.verissimor.lib.r2dbcmagicfilter.domain.R2dbcParsedField
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import org.springframework.data.relational.core.query.Criteria
+import org.springframework.data.relational.core.query.Criteria.where
+import java.time.Instant
+import java.time.LocalDate
+
+object R2dbcPredicateParser {
+
+ private val log: Logger = LoggerFactory.getLogger(this::class.java)
+
+ fun parsePredicates(
+ params: Map?>,
+ clazz: Class<*>,
+ dbFeatures: DbFeatures,
+ ): List = params.mapNotNull { (field, value) ->
+ val parsedField: R2dbcParsedField = R2dbcFieldParser.parseField(field, value, clazz)
+
+ if (parsedField.fieldClass == null) {
+ log.debug("Ignoring parameter $field - field not found")
+ return@mapNotNull null
+ }
+
+ if (value == null) {
+ log.debug("Ignoring parameter $field - value is null (you can use _is_null)")
+ return@mapNotNull null
+ }
+
+ when (parsedField.filterOperator) {
+ EQUAL -> parseEqual(parsedField, value).ignoreCase(true)
+
+ GREATER_THAN -> parseGreaterThan(parsedField, value)
+ GREATER_THAN_EQUAL -> parseGreaterThanEqual(parsedField, value)
+ LESS_THAN -> parseLessThan(parsedField, value)
+ LESS_THAN_EQUAL -> parseLessThanEqual(parsedField, value)
+
+ LIKE -> parseLike(parsedField, value, dbFeatures).ignoreCase(true)
+ LIKE_EXP -> parseLikeExp(parsedField, value, dbFeatures).ignoreCase(true)
+ NOT_LIKE -> parseNotLike(parsedField, value, dbFeatures).ignoreCase(true)
+ NOT_LIKE_EXP -> parseNotLikeExp(parsedField, value, dbFeatures).ignoreCase(true)
+
+ IN -> parseIn(parsedField, value, params)
+ NOT_IN -> parseNotIn(parsedField, value, params)
+
+ IS_NULL -> where(parsedField.resolvedFieldName).isNull
+ IS_NOT_NULL -> where(parsedField.resolvedFieldName).isNotNull
+ }
+ }
+
+ private fun parseLike(parsedField: R2dbcParsedField, value: Array, dbFeatures: DbFeatures) = when (dbFeatures) {
+ POSTGRES -> where("unaccent(lower(${parsedField.resolvedFieldName}))").like("%${value.toSingleString()?.lowercase()?.unaccent()}%")
+ NONE -> where(parsedField.resolvedFieldName).like("%${value.toSingleString()?.lowercase()}%")
+ }
+
+ private fun parseLikeExp(parsedField: R2dbcParsedField, value: Array, dbFeatures: DbFeatures) = when (dbFeatures) {
+ POSTGRES -> where("unaccent(lower(${parsedField.resolvedFieldName}))").like(value.toSingleString()!!.lowercase().unaccent())
+ NONE -> where(parsedField.resolvedFieldName).like(value.toSingleString()!!.lowercase())
+ }
+
+ private fun parseNotLike(parsedField: R2dbcParsedField, value: Array, dbFeatures: DbFeatures) = when (dbFeatures) {
+ POSTGRES -> where("unaccent(lower(${parsedField.resolvedFieldName}))").notLike("%${value.toSingleString()?.lowercase()?.unaccent()}%")
+ NONE -> where(parsedField.resolvedFieldName).notLike("%${value.toSingleString()?.lowercase()}%")
+ }
+
+ private fun parseNotLikeExp(parsedField: R2dbcParsedField, value: Array, dbFeatures: DbFeatures) = when (dbFeatures) {
+ POSTGRES -> where("unaccent(lower(${parsedField.resolvedFieldName}))").notLike(value.toSingleString()!!.lowercase().unaccent())
+ NONE -> where(parsedField.resolvedFieldName).notLike(value.toSingleString()!!.lowercase())
+ }
+
+ private fun parseEqual(parsedField: R2dbcParsedField, value: Array) = when (parsedField.getFieldType()) {
+ ENUMERATED -> where(parsedField.resolvedFieldName).`is`(value.toSingleString()!!)
+ NUMBER -> where(parsedField.resolvedFieldName).`is`(value.toSingleBigDecimal()!!)
+ LOCAL_DATE -> where(parsedField.resolvedFieldName).`is`(value.toSingleDate()!!)
+ INSTANT -> where(parsedField.resolvedFieldName).`is`(value.toSingleInstant()!!)
+ BOOLEAN -> where(parsedField.resolvedFieldName).`is`(value.toSingleBoolean()!!)
+ GENERIC -> where(parsedField.resolvedFieldName).`is`(value.toSingleString()!!)
+ null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseEqual")
+ }
+
+ private fun parseGreaterThan(parsedField: R2dbcParsedField, value: Array?) = when (parsedField.getFieldType()) {
+ NUMBER -> where(parsedField.resolvedFieldName).greaterThan(value.toSingleBigDecimal()!!)
+ LOCAL_DATE -> where(parsedField.resolvedFieldName).greaterThan(value.toSingleDate()!!)
+ INSTANT -> where(parsedField.resolvedFieldName).greaterThan(value.toSingleInstant()!!)
+ ENUMERATED, GENERIC, BOOLEAN, null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseGreaterThan")
+ }
+
+ private fun parseGreaterThanEqual(parsedField: R2dbcParsedField, value: Array?) = when (parsedField.getFieldType()) {
+ NUMBER -> where(parsedField.resolvedFieldName).greaterThanOrEquals(value.toSingleBigDecimal()!!)
+ LOCAL_DATE -> where(parsedField.resolvedFieldName).greaterThanOrEquals(value.toSingleDate()!!)
+ INSTANT -> where(parsedField.resolvedFieldName).greaterThanOrEquals(value.toSingleInstant()!!)
+ ENUMERATED, GENERIC, BOOLEAN, null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseGreaterThanEqual")
+ }
+
+ private fun parseLessThan(parsedField: R2dbcParsedField, value: Array?) = when (parsedField.getFieldType()) {
+ NUMBER -> where(parsedField.resolvedFieldName).lessThan(value.toSingleBigDecimal()!!)
+ LOCAL_DATE -> where(parsedField.resolvedFieldName).lessThan(value.toSingleDate()!!)
+ INSTANT -> where(parsedField.resolvedFieldName).lessThan(value.toSingleInstant()!!)
+ ENUMERATED, GENERIC, BOOLEAN, null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseLessThan")
+ }
+
+ private fun parseLessThanEqual(parsedField: R2dbcParsedField, value: Array?) = when (parsedField.getFieldType()) {
+ NUMBER -> where(parsedField.resolvedFieldName).lessThanOrEquals(value.toSingleBigDecimal()!!)
+ LOCAL_DATE -> where(parsedField.resolvedFieldName).lessThanOrEquals(value.toSingleDate()!!)
+ INSTANT -> where(parsedField.resolvedFieldName).lessThanOrEquals(value.toSingleInstant()!!)
+ ENUMERATED, GENERIC, BOOLEAN, null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseLessThanEqual")
+ }
+
+ private fun parseInValues(parsedField: R2dbcParsedField, value: Array?, params: Map?>): List {
+ val separator: String = params.toSingleParameter(SEARCH_IN_SEPARATOR_PRM)?.toString() ?: SEARCH_IN_SEPARATOR_DEF
+
+ return when {
+ value == null -> emptyList()
+ value.size == 1 -> value[0].split(separator).toList()
+ value.size > 1 -> value.toList()
+ else -> error("field `${parsedField.resolvedFieldName}` has no value to filter by parseIn")
+ }
+ }
+
+ private fun parseIn(parsedField: R2dbcParsedField, value: Array?, params: Map?>): Criteria? {
+ val values = parseInValues(parsedField, value, params)
+
+ return when (parsedField.getFieldType()) {
+ ENUMERATED -> where(parsedField.resolvedFieldName).`in`(values)
+ NUMBER -> where(parsedField.resolvedFieldName).`in`(values.map { it.toBigDecimal() })
+ LOCAL_DATE -> where(parsedField.resolvedFieldName).`in`(values.map { LocalDate.parse(it) })
+ INSTANT -> where(parsedField.resolvedFieldName).`in`(values.map { Instant.parse(it) })
+ GENERIC -> where(parsedField.resolvedFieldName).`in`(values)
+ BOOLEAN, null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseIn")
+ }
+ }
+
+ private fun parseNotIn(parsedField: R2dbcParsedField, value: Array?, params: Map?>): Criteria? {
+ val values = parseInValues(parsedField, value, params)
+
+ return when (parsedField.getFieldType()) {
+ ENUMERATED -> where(parsedField.resolvedFieldName).notIn(values)
+ NUMBER -> where(parsedField.resolvedFieldName).notIn(values.map { it.toBigDecimal() })
+ LOCAL_DATE -> where(parsedField.resolvedFieldName).notIn(values.map { LocalDate.parse(it) })
+ INSTANT -> where(parsedField.resolvedFieldName).notIn(values.map { Instant.parse(it) })
+ GENERIC -> where(parsedField.resolvedFieldName).notIn(values)
+ BOOLEAN, null -> error("field `${parsedField.resolvedFieldName}` is `${parsedField.fieldClass}` and doesn't support parseNotIn")
+ }
+ }
+}
diff --git a/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/domain/R2dbcParsedField.kt b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/domain/R2dbcParsedField.kt
new file mode 100644
index 0000000..d8c4520
--- /dev/null
+++ b/src/main/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/domain/R2dbcParsedField.kt
@@ -0,0 +1,15 @@
+package io.github.verissimor.lib.r2dbcmagicfilter.domain
+
+import io.github.verissimor.lib.jpamagicfilter.domain.FieldType
+import io.github.verissimor.lib.jpamagicfilter.domain.FilterOperator
+import io.github.verissimor.lib.jpamagicfilter.domain.ParsedField
+import java.lang.reflect.Field
+
+data class R2dbcParsedField(
+ val filterOperator: FilterOperator,
+ val resolvedFieldName: String,
+ val fieldClass: Field?,
+) {
+
+ fun getFieldType(): FieldType? = ParsedField.getFieldType(fieldClass)
+}
diff --git a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/DemoApplication.kt b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/DemoApplication.kt
index 4769982..73fa5cc 100644
--- a/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/DemoApplication.kt
+++ b/src/test/kotlin/io/github/verissimor/lib/jpamagicfilter/DemoApplication.kt
@@ -29,6 +29,7 @@ import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id
import javax.persistence.ManyToOne
+import javax.persistence.Table
import java.time.Instant.parse as instant
import java.time.LocalDate.parse as date
@@ -52,14 +53,15 @@ class UserController(
return userRepository.findAll(specification)
}
- @GetMapping("postgre-like-dbs")
- fun getCurrentUserPostgre(filter: MagicFilter): List {
+ @GetMapping("postgres-like-dbs")
+ fun getCurrentUserPostgres(filter: MagicFilter): List {
val specification: Specification = filter.toSpecification(User::class.java, POSTGRES)
return userRepository.findAll(specification)
}
}
@Entity
+@Table(name = "app_user")
data class User(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
diff --git a/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterEqualTest.kt b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterEqualTest.kt
new file mode 100644
index 0000000..5e44bb8
--- /dev/null
+++ b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterEqualTest.kt
@@ -0,0 +1,80 @@
+package io.github.verissimor.lib.r2dbcmagicfilter
+
+import io.github.verissimor.lib.r2dbcmagicfilter.domain.ReactiveUser
+import io.github.verissimor.lib.r2dbcmagicfilter.domain.toMultiMap
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import java.time.Instant
+
+class R2dbcMagicFilterEqualTest {
+ @Test
+ fun `test equals string`() {
+ val params = listOf("name" to "Joe").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("name = 'Joe'")
+ }
+
+ @Test
+ fun `test equals enum`() {
+ val params = listOf("gender" to "MALE").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("gender = 'MALE'")
+ }
+
+ @Test
+ fun `test equals integer`() {
+ val params = listOf("age" to "19").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("age = 19")
+ }
+
+ @Test
+ fun `test equals long`() {
+ val params = listOf("id" to "1").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("id = 1")
+ }
+
+ @Test
+ fun `test equals date`() {
+ val params = listOf("createdDate" to "2022-12-31").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("createdDate = '2022-12-31'")
+ }
+
+ @Test
+ fun `test equals instant`() {
+ val now = Instant.now().toString()
+ val params = listOf("createdAt" to now).toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("createdAt = '$now'")
+ }
+
+ @Test
+ fun `test equals boolean`() {
+ val params = listOf("enabled" to "0").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("enabled = 'false'")
+ }
+}
diff --git a/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterGreaterTest.kt b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterGreaterTest.kt
new file mode 100644
index 0000000..9371824
--- /dev/null
+++ b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterGreaterTest.kt
@@ -0,0 +1,108 @@
+package io.github.verissimor.lib.r2dbcmagicfilter
+
+import io.github.verissimor.lib.r2dbcmagicfilter.domain.ReactiveUser
+import io.github.verissimor.lib.r2dbcmagicfilter.domain.toMultiMap
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import java.time.Instant
+
+class R2dbcMagicFilterGreaterTest {
+ @Test
+ fun `test greater string`() {
+ val params = listOf("name_gt" to "Joe").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ assertThrows { filter.toCriteria(ReactiveUser::class.java) }
+ }
+
+ @Test
+ fun `test greater integer`() {
+ val params = listOf("age_gt" to "19").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("age > 19")
+ }
+
+ @Test
+ fun `test greater long`() {
+ val params = listOf("id_gt" to "1").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("id > 1")
+ }
+
+ @Test
+ fun `test greater date`() {
+ val params = listOf("createdDate_gt" to "2022-12-31").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("createdDate > '2022-12-31'")
+ }
+
+ @Test
+ fun `test greater instant`() {
+ val now = Instant.now().toString()
+ val params = listOf("createdAt_gt" to now).toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("createdAt > '$now'")
+ }
+
+ @Test
+ fun `test greater equals string`() {
+ val params = listOf("name_ge" to "Joe").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ assertThrows { filter.toCriteria(ReactiveUser::class.java) }
+ }
+
+ @Test
+ fun `test greater equals integer`() {
+ val params = listOf("age_ge" to "19").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("age >= 19")
+ }
+
+ @Test
+ fun `test greater equals long`() {
+ val params = listOf("id_ge" to "1").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("id >= 1")
+ }
+
+ @Test
+ fun `test greater equals date`() {
+ val params = listOf("createdDate_ge" to "2022-12-31").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("createdDate >= '2022-12-31'")
+ }
+
+ @Test
+ fun `test greater equals instant`() {
+ val now = Instant.now().toString()
+ val params = listOf("createdAt_ge" to now).toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("createdAt >= '$now'")
+ }
+}
diff --git a/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterInTest.kt b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterInTest.kt
new file mode 100644
index 0000000..2d12fa1
--- /dev/null
+++ b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterInTest.kt
@@ -0,0 +1,92 @@
+package io.github.verissimor.lib.r2dbcmagicfilter
+
+import io.github.verissimor.lib.r2dbcmagicfilter.domain.ReactiveUser
+import io.github.verissimor.lib.r2dbcmagicfilter.domain.toMultiMap
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import java.time.Instant
+
+class R2dbcMagicFilterInTest {
+ @Test
+ fun `test in string comma separated`() {
+ val params = listOf("name_in" to "Joe,Jane").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("name IN ('Joe', 'Jane')")
+ }
+
+ @Test
+ fun `test in string repeated param`() {
+ val params = listOf("name" to "Joe", "name" to "Jane").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("name IN ('Joe', 'Jane')")
+ }
+
+ @Test
+ fun `test in enum`() {
+ val params = listOf("gender_in" to "MALE,FEMALE").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("gender IN ('MALE', 'FEMALE')")
+ }
+
+ @Test
+ fun `test in integer`() {
+ val params = listOf("age_in" to "19,20,21").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("age IN (19, 20, 21)")
+ }
+
+ @Test
+ fun `test in long`() {
+ val params = listOf("id_in" to "1,2").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("id IN (1, 2)")
+ }
+
+ @Test
+ fun `test in date`() {
+ val params = listOf("createdDate_in" to "2022-12-30,2022-12-31").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("createdDate IN ('2022-12-30', '2022-12-31')")
+ }
+
+ @Test
+ fun `test in instant`() {
+ val now = Instant.now().toString()
+ val later = Instant.now().plusMillis(1000).toString()
+ val params = listOf("createdAt_in" to "$now,$later").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("createdAt IN ('$now', '$later')")
+ }
+
+ @Test
+ fun `test in boolean`() {
+ val params = listOf("enabled_in" to "0,1").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ assertThrows {
+ filter.toCriteria(ReactiveUser::class.java)
+ }
+ }
+}
diff --git a/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterIsNullTest.kt b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterIsNullTest.kt
new file mode 100644
index 0000000..86598e2
--- /dev/null
+++ b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterIsNullTest.kt
@@ -0,0 +1,28 @@
+package io.github.verissimor.lib.r2dbcmagicfilter
+
+import io.github.verissimor.lib.r2dbcmagicfilter.domain.ReactiveUser
+import io.github.verissimor.lib.r2dbcmagicfilter.domain.toMultiMap
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+
+class R2dbcMagicFilterIsNullTest {
+ @Test
+ fun `test is null string`() {
+ val params = listOf("name_is_null" to null).toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("name IS NULL")
+ }
+
+ @Test
+ fun `test is not null string`() {
+ val params = listOf("name_is_not_null" to null).toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("name IS NOT NULL")
+ }
+}
diff --git a/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterLikeTest.kt b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterLikeTest.kt
new file mode 100644
index 0000000..01511a5
--- /dev/null
+++ b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterLikeTest.kt
@@ -0,0 +1,46 @@
+package io.github.verissimor.lib.r2dbcmagicfilter
+
+import io.github.verissimor.lib.r2dbcmagicfilter.domain.ReactiveUser
+import io.github.verissimor.lib.r2dbcmagicfilter.domain.toMultiMap
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+
+class R2dbcMagicFilterLikeTest {
+ @Test
+ fun `test like string`() {
+ val params = listOf("name_like" to "joe").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+ // when applied to sql this will become: upper(name) like upper('%joe%')
+ // see PredicateParser.kt: EQUAL -> parseEqual(parsedField, value).ignoreCase(true)
+ assertThat(criteria.toString()).isEqualTo("name LIKE '%joe%'")
+ }
+
+ @Test
+ fun `test like exp string`() {
+ val params = listOf("name_like_exp" to "j%e").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+ assertThat(criteria.toString()).isEqualTo("name LIKE 'j%e'")
+ }
+
+ @Test
+ fun `test not like string`() {
+ val params = listOf("name_not_like" to "joe").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+ assertThat(criteria.toString()).isEqualTo("name NOT LIKE '%joe%'")
+ }
+
+ @Test
+ fun `test not like exp string`() {
+ val params = listOf("name_not_like_exp" to "j%e").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+ assertThat(criteria.toString()).isEqualTo("name NOT LIKE 'j%e'")
+ }
+}
diff --git a/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterLowerTest.kt b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterLowerTest.kt
new file mode 100644
index 0000000..8b0ac56
--- /dev/null
+++ b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterLowerTest.kt
@@ -0,0 +1,108 @@
+package io.github.verissimor.lib.r2dbcmagicfilter
+
+import io.github.verissimor.lib.r2dbcmagicfilter.domain.ReactiveUser
+import io.github.verissimor.lib.r2dbcmagicfilter.domain.toMultiMap
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import java.time.Instant
+
+class R2dbcMagicFilterLowerTest {
+ @Test
+ fun `test lower string`() {
+ val params = listOf("name_lt" to "Joe").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ assertThrows { filter.toCriteria(ReactiveUser::class.java) }
+ }
+
+ @Test
+ fun `test lower integer`() {
+ val params = listOf("age_lt" to "19").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("age < 19")
+ }
+
+ @Test
+ fun `test lower long`() {
+ val params = listOf("id_lt" to "1").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("id < 1")
+ }
+
+ @Test
+ fun `test lower date`() {
+ val params = listOf("createdDate_lt" to "2022-12-31").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("createdDate < '2022-12-31'")
+ }
+
+ @Test
+ fun `test lower instant`() {
+ val now = Instant.now().toString()
+ val params = listOf("createdAt_lt" to now).toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("createdAt < '$now'")
+ }
+
+ @Test
+ fun `test lower equals string`() {
+ val params = listOf("name_le" to "Joe").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ assertThrows { filter.toCriteria(ReactiveUser::class.java) }
+ }
+
+ @Test
+ fun `test lower equals integer`() {
+ val params = listOf("age_le" to "19").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("age <= 19")
+ }
+
+ @Test
+ fun `test lower equals long`() {
+ val params = listOf("id_le" to "1").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("id <= 1")
+ }
+
+ @Test
+ fun `test lower equals date`() {
+ val params = listOf("createdDate_le" to "2022-12-31").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("createdDate <= '2022-12-31'")
+ }
+
+ @Test
+ fun `test lower equals instant`() {
+ val now = Instant.now().toString()
+ val params = listOf("createdAt_le" to now).toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("createdAt <= '$now'")
+ }
+}
diff --git a/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterNotInTest.kt b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterNotInTest.kt
new file mode 100644
index 0000000..eeefb9f
--- /dev/null
+++ b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/R2dbcMagicFilterNotInTest.kt
@@ -0,0 +1,82 @@
+package io.github.verissimor.lib.r2dbcmagicfilter
+
+import io.github.verissimor.lib.r2dbcmagicfilter.domain.ReactiveUser
+import io.github.verissimor.lib.r2dbcmagicfilter.domain.toMultiMap
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import java.time.Instant
+
+class R2dbcMagicFilterNotInTest {
+ @Test
+ fun `test not in string comma separated`() {
+ val params = listOf("name_not_in" to "Joe,Jane").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("name NOT IN ('Joe', 'Jane')")
+ }
+
+ @Test
+ fun `test not in enum`() {
+ val params = listOf("gender_not_in" to "MALE,FEMALE").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("gender NOT IN ('MALE', 'FEMALE')")
+ }
+
+ @Test
+ fun `test not in integer`() {
+ val params = listOf("age_not_in" to "19,20,21").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("age NOT IN (19, 20, 21)")
+ }
+
+ @Test
+ fun `test not in long`() {
+ val params = listOf("id_not_in" to "1,2").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("id NOT IN (1, 2)")
+ }
+
+ @Test
+ fun `test not in date`() {
+ val params = listOf("createdDate_not_in" to "2022-12-30,2022-12-31").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("createdDate NOT IN ('2022-12-30', '2022-12-31')")
+ }
+
+ @Test
+ fun `test not in instant`() {
+ val now = Instant.now().toString()
+ val later = Instant.now().plusMillis(1000).toString()
+ val params = listOf("createdAt_not_in" to "$now,$later").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ val criteria = filter.toCriteria(ReactiveUser::class.java)
+
+ assertThat(criteria.toString()).isEqualTo("createdAt NOT IN ('$now', '$later')")
+ }
+
+ @Test
+ fun `test not in boolean`() {
+ val params = listOf("enabled_not_in" to "0,1").toMultiMap()
+ val filter = R2dbcMagicFilter(params)
+
+ assertThrows {
+ filter.toCriteria(ReactiveUser::class.java)
+ }
+ }
+}
diff --git a/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/domain/MultiMap.kt b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/domain/MultiMap.kt
new file mode 100644
index 0000000..229483c
--- /dev/null
+++ b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/domain/MultiMap.kt
@@ -0,0 +1,11 @@
+package io.github.verissimor.lib.r2dbcmagicfilter.domain
+
+import org.springframework.http.HttpHeaders
+
+fun List>.toMultiMap(): HttpHeaders {
+ val result = HttpHeaders()
+ this.map { it.first }.distinct().forEach { key ->
+ result[key] = this.filter { it.first == key }.map { it.second }
+ }
+ return result
+}
diff --git a/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/domain/ReactiveUser.kt b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/domain/ReactiveUser.kt
new file mode 100644
index 0000000..3f20b34
--- /dev/null
+++ b/src/test/kotlin/io/github/verissimor/lib/r2dbcmagicfilter/domain/ReactiveUser.kt
@@ -0,0 +1,20 @@
+package io.github.verissimor.lib.r2dbcmagicfilter.domain
+
+import io.github.verissimor.lib.jpamagicfilter.Gender
+import java.time.Instant
+import java.time.LocalDate
+import javax.persistence.Id
+import javax.persistence.Table
+
+@Table(name = "app_user")
+data class ReactiveUser(
+ @Id
+ val id: Long?,
+ val name: String,
+ val age: Int,
+ val gender: Gender,
+ val cityId: Long?,
+ val createdDate: LocalDate?,
+ val createdAt: Instant?,
+ val enabled: Boolean
+)