From dbfed6d3a415414e7a7a10dccb1d8e2895dd245c Mon Sep 17 00:00:00 2001 From: Jonathan Wilkinson Date: Wed, 15 Oct 2025 13:32:33 +0100 Subject: [PATCH 1/2] fix: rename filterOptions to filters and implement filter expression classes --- .../core/datacapture/FormSchemasRouter.kt | 4 +- .../FormSubmissionVersionsRouter.kt | 4 +- .../core/datacapture/FormSubmissionsRouter.kt | 2 +- .../core/router/request/FilterExpression.kt | 35 ++++++++++++ .../core/router/request/RequestParameters.kt | 28 +++++---- .../request/RequestParametersFiltersTest.kt | 57 +++++++++++++++++++ 6 files changed, 113 insertions(+), 17 deletions(-) create mode 100644 src/main/kotlin/com/ctrlhub/core/router/request/FilterExpression.kt create mode 100644 src/test/kotlin/com/ctrlhub/core/router/request/RequestParametersFiltersTest.kt diff --git a/src/main/kotlin/com/ctrlhub/core/datacapture/FormSchemasRouter.kt b/src/main/kotlin/com/ctrlhub/core/datacapture/FormSchemasRouter.kt index 0e23dee..85892ae 100644 --- a/src/main/kotlin/com/ctrlhub/core/datacapture/FormSchemasRouter.kt +++ b/src/main/kotlin/com/ctrlhub/core/datacapture/FormSchemasRouter.kt @@ -27,12 +27,12 @@ enum class FormSchemaIncludes(val value: String) : JsonApiIncludes { class FormSchemaRequestParameters( offset: Int = 0, limit: Int = 100, - filterOptions: List = emptyList(), + filters: List = emptyList(), includes: List = emptyList() ) : RequestParametersWithIncludes( offset = offset, limit = limit, - filterOptions = filterOptions, + filters = filters, includes = includes, ) diff --git a/src/main/kotlin/com/ctrlhub/core/datacapture/FormSubmissionVersionsRouter.kt b/src/main/kotlin/com/ctrlhub/core/datacapture/FormSubmissionVersionsRouter.kt index 37e9797..a882ec2 100644 --- a/src/main/kotlin/com/ctrlhub/core/datacapture/FormSubmissionVersionsRouter.kt +++ b/src/main/kotlin/com/ctrlhub/core/datacapture/FormSubmissionVersionsRouter.kt @@ -34,12 +34,12 @@ enum class FormSubmissionVersionIncludes(val key: String) : JsonApiIncludes { class FormSubmissionVersionRequestParameters( offset: Int = 0, limit: Int = 100, - filterOptions: List = emptyList(), + filters: List = emptyList(), includes: List = emptyList() ) : RequestParametersWithIncludes( offset = offset, limit = limit, - filterOptions = filterOptions, + filters = filters, includes = includes, ) diff --git a/src/main/kotlin/com/ctrlhub/core/datacapture/FormSubmissionsRouter.kt b/src/main/kotlin/com/ctrlhub/core/datacapture/FormSubmissionsRouter.kt index aba79d8..e0d967e 100644 --- a/src/main/kotlin/com/ctrlhub/core/datacapture/FormSubmissionsRouter.kt +++ b/src/main/kotlin/com/ctrlhub/core/datacapture/FormSubmissionsRouter.kt @@ -18,7 +18,7 @@ class FormSubmissionsRouter(httpClient: HttpClient) : Router(httpClient) { suspend fun all(organisationId: String, formId: String): PaginatedList { return fetchPaginatedJsonApiResources( "/v3/orgs/$organisationId/data-capture/forms/$formId/submissions", - queryParameters = emptyMap(), + queryParameters = emptyMap(), FormSubmission::class.java, User::class.java, Form::class.java, diff --git a/src/main/kotlin/com/ctrlhub/core/router/request/FilterExpression.kt b/src/main/kotlin/com/ctrlhub/core/router/request/FilterExpression.kt new file mode 100644 index 0000000..4624354 --- /dev/null +++ b/src/main/kotlin/com/ctrlhub/core/router/request/FilterExpression.kt @@ -0,0 +1,35 @@ +package com.ctrlhub.core.router.request + +sealed interface FilterOption { + fun format(): String +} + +class FieldFilterExpression(val field: String, val values: List) : FilterOption { + override fun format(): String { + val quoted = values.joinToString(",") { "'${it}'" } + return "${field}($quoted)" + } +} + +class ValueFilterExpression(val values: List) : FilterOption { + override fun format(): String = values.joinToString(",") +} + +class FunctionFilterExpression(val name: String, val args: List = emptyList()) : FilterOption { + override fun format(): String { + return if (args.isEmpty()) { + "${name}()" + } else { + val joined = args.joinToString(",") { it } + "${name}(${joined})" + } + } +} + +class AndExpression(val parts: List) : FilterOption { + override fun format(): String = "and(${parts.joinToString(",") { it.format() }})" +} + +class OrExpression(val parts: List) : FilterOption { + override fun format(): String = "or(${parts.joinToString(",") { it.format() }})" +} \ No newline at end of file diff --git a/src/main/kotlin/com/ctrlhub/core/router/request/RequestParameters.kt b/src/main/kotlin/com/ctrlhub/core/router/request/RequestParameters.kt index 6235114..a42f170 100644 --- a/src/main/kotlin/com/ctrlhub/core/router/request/RequestParameters.kt +++ b/src/main/kotlin/com/ctrlhub/core/router/request/RequestParameters.kt @@ -1,21 +1,24 @@ package com.ctrlhub.core.router.request -data class FilterOption( - val field: String, - val value: String -) - abstract class AbstractRequestParameters( val offset: Int? = null, val limit: Int? = null, - val filterOptions: List + val filters: List = emptyList() ) { open fun toMap(): Map { val queryParams = mutableMapOf() offset?.let { queryParams["offset"] = it.toString() } limit?.let { queryParams["limit"] = it.toString() } - filterOptions.forEach { queryParams["filter"] = "${it.field}('${it.value}')" } + val parts = mutableListOf() + + for (expr in filters) { + parts += expr.format() + } + + if (parts.isNotEmpty()) { + queryParams["filter"] = parts.joinToString(",") + } return queryParams } @@ -30,10 +33,11 @@ class RequestParameters( open class RequestParametersWithIncludes( offset: Int? = null, limit: Int? = null, - filterOptions: List = emptyList(), + filters: List = emptyList(), val includes: List = emptyList() -) : AbstractRequestParameters(offset, limit, filterOptions) where TIncludes : JsonApiIncludes { +) : AbstractRequestParameters(offset, limit, filters) where TIncludes : JsonApiIncludes { + @Suppress("unused") fun withIncludes(vararg includes: TIncludes): RequestParametersWithIncludes { return copy(includes = this.includes + includes) } @@ -53,7 +57,7 @@ open class RequestParametersWithIncludes( } private fun copy( - filterOptions: List = this.filterOptions, - includes: List = this.includes - ) = RequestParametersWithIncludes(offset, limit, filterOptions, includes) + includes: List = this.includes, + filters: List = this.filters + ) = RequestParametersWithIncludes(offset, limit, filters, includes) } diff --git a/src/test/kotlin/com/ctrlhub/core/router/request/RequestParametersFiltersTest.kt b/src/test/kotlin/com/ctrlhub/core/router/request/RequestParametersFiltersTest.kt new file mode 100644 index 0000000..1a99295 --- /dev/null +++ b/src/test/kotlin/com/ctrlhub/core/router/request/RequestParametersFiltersTest.kt @@ -0,0 +1,57 @@ +package com.ctrlhub.core.router.request + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class RequestParametersFiltersTest { + + @Test + fun `and of two field expressions formats correctly`() { + val expr = AndExpression( + listOf( + FieldFilterExpression("status", listOf("open")), + FieldFilterExpression("category", listOf("news")) + ) + ) + + val params = RequestParameters(filterOptions = listOf(expr)) + val map = params.toMap() + + assertEquals("and(status('open'),category('news'))", map["filter"]) + } + + @Test + fun `and of functions formats correctly`() { + val expr = AndExpression( + listOf( + ValueFilterExpression( + listOf( + "is_latest()", + "no_start()" + ) + ), + ) + ) + + val params = RequestParameters(filterOptions = listOf(expr)) + val map = params.toMap() + + assertEquals("and(is_latest(),no_start())", map["filter"]) + } + + @Test + fun `mixed and expression with field and function`() { + val expr = AndExpression( + listOf( + FieldFilterExpression("status", listOf("active")), + FunctionFilterExpression("is_latest") + ) + ) + + val params = RequestParameters(filterOptions = listOf(expr)) + val map = params.toMap() + + assertEquals("and(status('active'),is_latest())", map["filter"]) + } +} + From e090f5974a12e379500a7d7bcbef41021388fc90 Mon Sep 17 00:00:00 2001 From: Jonathan Wilkinson Date: Wed, 15 Oct 2025 13:44:47 +0100 Subject: [PATCH 2/2] fix: simplify how filters are built --- .../core/router/request/FilterExpression.kt | 15 ++------------- .../request/RequestParametersFiltersTest.kt | 10 +++------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/src/main/kotlin/com/ctrlhub/core/router/request/FilterExpression.kt b/src/main/kotlin/com/ctrlhub/core/router/request/FilterExpression.kt index 4624354..7269d51 100644 --- a/src/main/kotlin/com/ctrlhub/core/router/request/FilterExpression.kt +++ b/src/main/kotlin/com/ctrlhub/core/router/request/FilterExpression.kt @@ -11,19 +11,8 @@ class FieldFilterExpression(val field: String, val values: List) : Filte } } -class ValueFilterExpression(val values: List) : FilterOption { - override fun format(): String = values.joinToString(",") -} - -class FunctionFilterExpression(val name: String, val args: List = emptyList()) : FilterOption { - override fun format(): String { - return if (args.isEmpty()) { - "${name}()" - } else { - val joined = args.joinToString(",") { it } - "${name}(${joined})" - } - } +class ValueFilterExpression(val value: String) : FilterOption { + override fun format(): String = value } class AndExpression(val parts: List) : FilterOption { diff --git a/src/test/kotlin/com/ctrlhub/core/router/request/RequestParametersFiltersTest.kt b/src/test/kotlin/com/ctrlhub/core/router/request/RequestParametersFiltersTest.kt index 1a99295..c283971 100644 --- a/src/test/kotlin/com/ctrlhub/core/router/request/RequestParametersFiltersTest.kt +++ b/src/test/kotlin/com/ctrlhub/core/router/request/RequestParametersFiltersTest.kt @@ -24,12 +24,8 @@ class RequestParametersFiltersTest { fun `and of functions formats correctly`() { val expr = AndExpression( listOf( - ValueFilterExpression( - listOf( - "is_latest()", - "no_start()" - ) - ), + ValueFilterExpression("is_latest()"), + ValueFilterExpression("no_start()"), ) ) @@ -44,7 +40,7 @@ class RequestParametersFiltersTest { val expr = AndExpression( listOf( FieldFilterExpression("status", listOf("active")), - FunctionFilterExpression("is_latest") + ValueFilterExpression("is_latest()") ) )