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 @@ -branches55.8% \ No newline at end of file +branches56.1% \ 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 @@ -coverage64.6% \ No newline at end of file +coverage70.7% \ 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 +)