commonMain.com.caesarealabs.searchit.impl.QueryValidator.kt Maven / Gradle / Ivy
The newest version!
package com.caesarealabs.searchit.impl
import com.caesarealabs.searchit.DataKeys
import com.caesarealabs.searchit.DataLens
import com.caesarealabs.searchit.SpecialFilter
import com.caesarealabs.searchit.impl.platform.fuzzySearch
internal typealias SpecialFilters = List>
internal object QueryValidator {
// Returns null if it is valid
internal fun validateQuery(query: List, lens: DataLens<*>): String? {
validateParentheses(query)?.let { return it }
validateOperators(query)?.let { return it }
validateTime(query, QueryParser.StartDateToken)?.let { return it }
validateTime(query, QueryParser.EndDateToken)?.let { return it }
validateKeys(query, lens)?.let { return it }
return null
}
// Check that the specified keys are in the "keys" set
private fun validateKeys(query: List, lens: DataLens<*>): String? {
// Not constant keys - everything is fine
val keysValue = (lens.indirectKeys as? DataKeys.Constant)?.options ?: return null
val allowedKeys = (keysValue + QueryParser.allDateTokens).map { it.lowercase() }.toHashSet()
val violator = query.find { it is QueryToken.KeyValue && it.key.lowercase() !in allowedKeys } as QueryToken.KeyValue? ?: return null
val extraction = fuzzySearch(violator.key, keysValue, 75)
return "No such key '${violator.key}'.".appendIf(extraction.isNotEmpty()) { " Did you mean: ${extraction.joinToString { "'${it}'" }}?" }
}
private fun validateOperators(query: List): String? {
for ((i, token) in query.withIndex()) {
if (token is QueryToken.Operator) {
if (token == QueryToken.Operator.Not) {
// The not operator doesn't need an operand to the left, and having 'and not' and 'or not' is valid.
if (i == query.size - 1) return "Logical operator '${token} doesn't have an operand to the right"
else if (i > 0 && query[i - 1] == QueryToken.Operator.Not) return "'not not' is not valid. "
} else {
when {
i == 0 -> return "Logical operator '${token}' doesn't have an operand to the left"
i == query.size - 1 -> return "Logical operator '${token} doesn't have an operand to the right"
query[i - 1] is QueryToken.Operator -> return "Logical operators '${query[i - 1]}' and '${token}' can't be placed next to each other (in that order)"
}
}
}
}
return null
}
private fun validateParentheses(query: List): String? {
var openParenthesesCount = 0
for (token in query) {
if (token is QueryToken.Parentheses) {
when (token) {
QueryToken.Parentheses.Opening -> {
openParenthesesCount++
}
QueryToken.Parentheses.Closing -> {
if (openParenthesesCount == 0) return "Too many closing parentheses"
openParenthesesCount--
}
}
}
}
if (openParenthesesCount != 0) {
return "A parentheses was not closed"
}
return null
}
private fun validateTime(query: List, timeToken: String): String? {
fun isTimeToken(token: QueryToken) = token is QueryToken.KeyValue && token.key == timeToken
val index = query.indexOfFirst { isTimeToken(it) }
// If nothing was specified we have nothing to worry about
if (index == -1) return null
val lastIndex = query.indexOfLast { isTimeToken(it) }
if (index != lastIndex) return "'${timeToken}' was specified twice. It may only be specified once."
// Check logical operator before
if (index > 0 && query[index - 1] is QueryToken.Operator) {
return "'${timeToken}' cannot be used with the logical operator '${query[index - 1]}'."
}
// Check logical operator after
if (index < query.size - 1 && query[index + 1] is QueryToken.Operator) {
return "'${timeToken}' cannot be used with the logical operator '${query[index + 1]}'."
}
// Check in parentheses
var openParenthesesCount = 0
for (token in query) {
if (token is QueryToken.Parentheses) {
when (token) {
QueryToken.Parentheses.Opening -> openParenthesesCount++
QueryToken.Parentheses.Closing -> openParenthesesCount--
}
} else if (isTimeToken(token)) {
if (openParenthesesCount > 0) {
return "'${timeToken}' cannot be used inside of parentheses."
}
}
}
return null
}
}