From 45e931e5f098cf17d3820274b47457929ab80b0a Mon Sep 17 00:00:00 2001 From: Fabian Engelniederhammer Date: Thu, 14 Dec 2023 14:19:40 +0100 Subject: [PATCH] feat: make fields case-insensitive #502 --- .../lapis/config/SequenceFilterFields.kt | 79 +++++++--- .../lapis/controller/LapisController.kt | 13 +- .../lapis/model/SiloFilterExpressionMapper.kt | 23 +-- .../genspectrum/lapis/model/SiloQueryModel.kt | 4 +- .../genspectrum/lapis/openApi/OpenApiDocs.kt | 4 +- .../request/CaseInsensitiveFieldsCleaner.kt | 13 ++ .../org/genspectrum/lapis/request/Field.kt | 20 +++ .../genspectrum/lapis/request/OrderByField.kt | 16 +- .../SequenceFiltersRequestWithFields.kt | 7 +- .../lapis/DummySequenceFilterFields.kt | 49 ++++++ .../lapis/config/SequenceFilterFieldsTest.kt | 68 ++++++-- .../LapisControllerCommonFieldsTest.kt | 44 ++++-- .../controller/LapisControllerCsvTest.kt | 3 +- .../lapis/controller/LapisControllerTest.kt | 35 +++-- .../model/SiloFilterExpressionMapperTest.kt | 50 +++--- .../SequenceFiltersRequestWithFieldsTest.kt | 148 +++++++++++------- 16 files changed, 408 insertions(+), 168 deletions(-) create mode 100644 lapis2/src/main/kotlin/org/genspectrum/lapis/request/CaseInsensitiveFieldsCleaner.kt create mode 100644 lapis2/src/main/kotlin/org/genspectrum/lapis/request/Field.kt create mode 100644 lapis2/src/test/kotlin/org/genspectrum/lapis/DummySequenceFilterFields.kt diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/config/SequenceFilterFields.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/config/SequenceFilterFields.kt index c0bd4387..cca391c5 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/config/SequenceFilterFields.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/config/SequenceFilterFields.kt @@ -1,6 +1,9 @@ package org.genspectrum.lapis.config +import java.util.Locale + typealias SequenceFilterFieldName = String +typealias LowercaseName = String const val SARS_COV2_VARIANT_QUERY_FEATURE = "sarsCoV2VariantQuery" @@ -8,16 +11,15 @@ val FEATURES_FOR_SEQUENCE_FILTERS = listOf( SARS_COV2_VARIANT_QUERY_FEATURE, ) -data class SequenceFilterFields(val fields: Map) { +data class SequenceFilterFields(val fields: Map) { companion object { fun fromDatabaseConfig(databaseConfig: DatabaseConfig): SequenceFilterFields { val metadataFields = databaseConfig.schema.metadata - .map(::mapToSequenceFilterFields) - .flatten() - .toMap() + .flatMap(::mapToSequenceFilterField) + .associateBy { it.name.lowercase(Locale.US) } val featuresFields = if (databaseConfig.schema.features.isEmpty()) { - emptyMap() + emptyMap() } else { databaseConfig.schema.features .filter { it.name in FEATURES_FOR_SEQUENCE_FILTERS } @@ -29,35 +31,76 @@ data class SequenceFilterFields(val fields: Map listOf(databaseMetadata.name to SequenceFilterFieldType.String) - MetadataType.PANGO_LINEAGE -> listOf(databaseMetadata.name to SequenceFilterFieldType.PangoLineage) + MetadataType.STRING -> listOf( + SequenceFilterField( + name = databaseMetadata.name, + type = SequenceFilterFieldType.String, + ), + ) + + MetadataType.PANGO_LINEAGE -> listOf( + SequenceFilterField( + name = databaseMetadata.name, + type = SequenceFilterFieldType.PangoLineage, + ), + ) + MetadataType.DATE -> listOf( - databaseMetadata.name to SequenceFilterFieldType.Date, - "${databaseMetadata.name}From" to SequenceFilterFieldType.DateFrom(databaseMetadata.name), - "${databaseMetadata.name}To" to SequenceFilterFieldType.DateTo(databaseMetadata.name), + SequenceFilterField(name = databaseMetadata.name, type = SequenceFilterFieldType.Date), + SequenceFilterField( + name = "${databaseMetadata.name}From", + type = SequenceFilterFieldType.DateFrom(databaseMetadata.name), + ), + SequenceFilterField( + name = "${databaseMetadata.name}To", + type = SequenceFilterFieldType.DateTo(databaseMetadata.name), + ), ) MetadataType.INT -> listOf( - databaseMetadata.name to SequenceFilterFieldType.Int, - "${databaseMetadata.name}From" to SequenceFilterFieldType.IntFrom(databaseMetadata.name), - "${databaseMetadata.name}To" to SequenceFilterFieldType.IntTo(databaseMetadata.name), + SequenceFilterField(name = databaseMetadata.name, type = SequenceFilterFieldType.Int), + SequenceFilterField( + name = "${databaseMetadata.name}From", + type = SequenceFilterFieldType.IntFrom(databaseMetadata.name), + ), + SequenceFilterField( + name = "${databaseMetadata.name}To", + type = SequenceFilterFieldType.IntTo(databaseMetadata.name), + ), ) MetadataType.FLOAT -> listOf( - databaseMetadata.name to SequenceFilterFieldType.Float, - "${databaseMetadata.name}From" to SequenceFilterFieldType.FloatFrom(databaseMetadata.name), - "${databaseMetadata.name}To" to SequenceFilterFieldType.FloatTo(databaseMetadata.name), + SequenceFilterField(name = databaseMetadata.name, type = SequenceFilterFieldType.Float), + SequenceFilterField( + name = "${databaseMetadata.name}From", + type = SequenceFilterFieldType.FloatFrom(databaseMetadata.name), + ), + SequenceFilterField( + name = "${databaseMetadata.name}To", + type = SequenceFilterFieldType.FloatTo(databaseMetadata.name), + ), ) MetadataType.NUCLEOTIDE_INSERTION -> emptyList() MetadataType.AMINO_ACID_INSERTION -> emptyList() } +private const val VARIANT_QUERY_FIELD = "variantQuery" + private fun mapToSequenceFilterFieldsFromFeatures(databaseFeature: DatabaseFeature) = when (databaseFeature.name) { - SARS_COV2_VARIANT_QUERY_FEATURE -> "variantQuery" to SequenceFilterFieldType.VariantQuery + SARS_COV2_VARIANT_QUERY_FEATURE -> VARIANT_QUERY_FIELD.lowercase(Locale.US) to SequenceFilterField( + name = VARIANT_QUERY_FIELD, + type = SequenceFilterFieldType.VariantQuery, + ) + else -> throw IllegalArgumentException( "Unknown feature '${databaseFeature.name}'", ) diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/LapisController.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/LapisController.kt index 3c83a9a1..1ed0b94f 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/LapisController.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/LapisController.kt @@ -39,6 +39,7 @@ import org.genspectrum.lapis.openApi.REQUEST_SCHEMA_WITH_MIN_PROPORTION import org.genspectrum.lapis.request.AminoAcidInsertion import org.genspectrum.lapis.request.AminoAcidMutation import org.genspectrum.lapis.request.CommonSequenceFilters +import org.genspectrum.lapis.request.Field import org.genspectrum.lapis.request.MutationProportionsRequest import org.genspectrum.lapis.request.NucleotideInsertion import org.genspectrum.lapis.request.NucleotideMutation @@ -85,7 +86,7 @@ class LapisController( sequenceFilters: Map?, @FieldsToAggregateBy @RequestParam - fields: List?, + fields: List?, @AggregatedOrderByFields @RequestParam orderBy: List?, @@ -140,7 +141,7 @@ class LapisController( sequenceFilters: Map?, @FieldsToAggregateBy @RequestParam - fields: List?, + fields: List?, @AggregatedOrderByFields @RequestParam orderBy: List?, @@ -193,7 +194,7 @@ class LapisController( sequenceFilters: Map?, @FieldsToAggregateBy @RequestParam - fields: List?, + fields: List?, @AggregatedOrderByFields @RequestParam orderBy: List?, @@ -683,7 +684,7 @@ class LapisController( sequenceFilters: Map?, @DetailsFields @RequestParam - fields: List?, + fields: List?, @DetailsOrderByFields @RequestParam orderBy: List?, @@ -736,7 +737,7 @@ class LapisController( sequenceFilters: Map?, @DetailsFields @RequestParam - fields: List?, + fields: List?, @DetailsOrderByFields @RequestParam orderBy: List?, @@ -786,7 +787,7 @@ class LapisController( sequenceFilters: Map?, @DetailsFields @RequestParam - fields: List?, + fields: List?, @DetailsOrderByFields @RequestParam orderBy: List?, diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/model/SiloFilterExpressionMapper.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/model/SiloFilterExpressionMapper.kt index f7a5f217..63b2bf07 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/model/SiloFilterExpressionMapper.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/model/SiloFilterExpressionMapper.kt @@ -1,5 +1,6 @@ package org.genspectrum.lapis.model +import org.genspectrum.lapis.config.SequenceFilterField import org.genspectrum.lapis.config.SequenceFilterFieldType import org.genspectrum.lapis.config.SequenceFilterFields import org.genspectrum.lapis.controller.BadRequestException @@ -27,6 +28,7 @@ import org.genspectrum.lapis.silo.True import org.springframework.stereotype.Component import java.time.LocalDate import java.time.format.DateTimeParseException +import java.util.Locale data class SequenceFilterValue(val type: SequenceFilterFieldType, val value: String, val originalKey: String) @@ -45,8 +47,8 @@ class SiloFilterExpressionMapper( val allowedSequenceFiltersWithType = sequenceFilters .sequenceFilters .map { (key, value) -> - val nullableType = allowedSequenceFilterFields.fields[key] - val (filterExpressionId, type) = mapToFilterExpressionIdentifier(nullableType, key) + val nullableField = allowedSequenceFilterFields.fields[key.lowercase(Locale.US)] + val (filterExpressionId, type) = mapToFilterExpressionIdentifier(nullableField, key) filterExpressionId to SequenceFilterValue(type, value, key) } .groupBy({ it.first }, { it.second }) @@ -87,26 +89,27 @@ class SiloFilterExpressionMapper( } private fun mapToFilterExpressionIdentifier( - type: SequenceFilterFieldType?, + field: SequenceFilterField?, key: SequenceFilterFieldName, ): Pair, SequenceFilterFieldType> { + val type = field?.type val filterExpressionId = when (type) { is SequenceFilterFieldType.DateFrom -> Pair(type.associatedField, Filter.DateBetween) is SequenceFilterFieldType.DateTo -> Pair(type.associatedField, Filter.DateBetween) - SequenceFilterFieldType.Date -> Pair(key, Filter.DateBetween) - SequenceFilterFieldType.PangoLineage -> Pair(key, Filter.PangoLineage) - SequenceFilterFieldType.String -> Pair(key, Filter.StringEquals) - SequenceFilterFieldType.VariantQuery -> Pair(key, Filter.VariantQuery) - SequenceFilterFieldType.Int -> Pair(key, Filter.IntEquals) + SequenceFilterFieldType.Date -> Pair(field.name, Filter.DateBetween) + SequenceFilterFieldType.PangoLineage -> Pair(field.name, Filter.PangoLineage) + SequenceFilterFieldType.String -> Pair(field.name, Filter.StringEquals) + SequenceFilterFieldType.VariantQuery -> Pair(field.name, Filter.VariantQuery) + SequenceFilterFieldType.Int -> Pair(field.name, Filter.IntEquals) is SequenceFilterFieldType.IntFrom -> Pair(type.associatedField, Filter.IntBetween) is SequenceFilterFieldType.IntTo -> Pair(type.associatedField, Filter.IntBetween) - SequenceFilterFieldType.Float -> Pair(key, Filter.FloatEquals) + SequenceFilterFieldType.Float -> Pair(field.name, Filter.FloatEquals) is SequenceFilterFieldType.FloatFrom -> Pair(type.associatedField, Filter.FloatBetween) is SequenceFilterFieldType.FloatTo -> Pair(type.associatedField, Filter.FloatBetween) null -> throw BadRequestException( "'$key' is not a valid sequence filter key. Valid keys are: " + - allowedSequenceFilterFields.fields.keys.joinToString(), + allowedSequenceFilterFields.fields.values.joinToString { it.name }, ) } return Pair(filterExpressionId, type) diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/model/SiloQueryModel.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/model/SiloQueryModel.kt index 531449a2..b3007884 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/model/SiloQueryModel.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/model/SiloQueryModel.kt @@ -26,7 +26,7 @@ class SiloQueryModel( siloClient.sendQuery( SiloQuery( SiloAction.aggregated( - sequenceFilters.fields, + sequenceFilters.fields.map { it.fieldName }, sequenceFilters.orderByFields, sequenceFilters.limit, sequenceFilters.offset, @@ -88,7 +88,7 @@ class SiloQueryModel( siloClient.sendQuery( SiloQuery( SiloAction.details( - sequenceFilters.fields, + sequenceFilters.fields.map { it.fieldName }, sequenceFilters.orderByFields, sequenceFilters.limit, sequenceFilters.offset, diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/openApi/OpenApiDocs.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/openApi/OpenApiDocs.kt index c47eaea0..ba2a9a92 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/openApi/OpenApiDocs.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/openApi/OpenApiDocs.kt @@ -281,8 +281,8 @@ private fun mapToOpenApiType(type: MetadataType): String = private fun primitiveSequenceFilterFieldSchemas(sequenceFilterFields: SequenceFilterFields) = sequenceFilterFields.fields - .map { (fieldName, fieldType) -> fieldName to Schema().type(fieldType.openApiType) } - .toMap() + .values + .associate { (fieldName, field) -> fieldName to Schema().type(field.openApiType) } private fun requestSchemaForCommonSequenceFilters( requestProperties: Map>, diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/request/CaseInsensitiveFieldsCleaner.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/request/CaseInsensitiveFieldsCleaner.kt new file mode 100644 index 00000000..1f1380d0 --- /dev/null +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/request/CaseInsensitiveFieldsCleaner.kt @@ -0,0 +1,13 @@ +package org.genspectrum.lapis.request + +import org.genspectrum.lapis.config.DatabaseConfig +import org.springframework.stereotype.Component + +@Component +class CaseInsensitiveFieldsCleaner(databaseConfig: DatabaseConfig) { + private val fieldsMap = databaseConfig.schema.metadata.map { it.name }.associateBy { it.lowercase() } + + fun clean(fieldName: String) = fieldsMap[fieldName.lowercase()] + + fun getKnownFields() = fieldsMap.values +} diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/request/Field.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/request/Field.kt new file mode 100644 index 00000000..d17819cc --- /dev/null +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/request/Field.kt @@ -0,0 +1,20 @@ +package org.genspectrum.lapis.request + +import org.genspectrum.lapis.controller.BadRequestException +import org.springframework.core.convert.converter.Converter +import org.springframework.stereotype.Component + +data class Field(val fieldName: String) + +@Component +class FieldConverter(private val caseInsensitiveFieldsCleaner: CaseInsensitiveFieldsCleaner) : + Converter { + override fun convert(source: String): Field { + val cleaned = caseInsensitiveFieldsCleaner.clean(source) + ?: throw BadRequestException( + "Unknown field: $source, known values are ${caseInsensitiveFieldsCleaner.getKnownFields()}", + ) + + return Field(cleaned) + } +} diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/request/OrderByField.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/request/OrderByField.kt index 7ada6d76..b6b94d36 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/request/OrderByField.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/request/OrderByField.kt @@ -28,13 +28,14 @@ enum class Order { } @JsonComponent -class OrderByFieldDeserializer : JsonDeserializer() { +class OrderByFieldDeserializer(private val orderByFieldsCleaner: OrderByFieldsCleaner) : + JsonDeserializer() { override fun deserialize( jsonParser: JsonParser, ctxt: DeserializationContext, ): OrderByField { return when (val value = jsonParser.readValueAsTree()) { - is TextNode -> OrderByField(value.asText(), Order.ASCENDING) + is TextNode -> OrderByField(orderByFieldsCleaner.clean(value.asText()), Order.ASCENDING) is ObjectNode -> deserializeOrderByField(value) else -> throw BadRequestException("orderByField must be a string or an object") } @@ -52,11 +53,16 @@ class OrderByFieldDeserializer : JsonDeserializer() { else -> throw BadRequestException("orderByField type must be \"ascending\" or \"descending\"") } - return OrderByField(fieldNode.asText(), ascending) + return OrderByField(orderByFieldsCleaner.clean(fieldNode.asText()), ascending) } } @Component -class OrderByFieldConverter : Converter { - override fun convert(source: String) = OrderByField(source, Order.ASCENDING) +class OrderByFieldConverter(private val orderByFieldsCleaner: OrderByFieldsCleaner) : Converter { + override fun convert(source: String) = OrderByField(orderByFieldsCleaner.clean(source), Order.ASCENDING) +} + +@Component +class OrderByFieldsCleaner(private val caseInsensitiveFieldsCleaner: CaseInsensitiveFieldsCleaner) { + fun clean(fieldName: String): String = caseInsensitiveFieldsCleaner.clean(fieldName) ?: fieldName } diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/request/SequenceFiltersRequestWithFields.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/request/SequenceFiltersRequestWithFields.kt index 530a4a2c..43dbb9f1 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/request/SequenceFiltersRequestWithFields.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/request/SequenceFiltersRequestWithFields.kt @@ -15,14 +15,15 @@ data class SequenceFiltersRequestWithFields( override val aaMutations: List, override val nucleotideInsertions: List, override val aminoAcidInsertions: List, - val fields: List, + val fields: List, override val orderByFields: List = emptyList(), override val limit: Int? = null, override val offset: Int? = null, ) : CommonSequenceFilters @JsonComponent -class SequenceFiltersRequestWithFieldsDeserializer : JsonDeserializer() { +class SequenceFiltersRequestWithFieldsDeserializer(private val fieldConverter: FieldConverter) : + JsonDeserializer() { override fun deserialize( jsonParser: JsonParser, ctxt: DeserializationContext, @@ -32,7 +33,7 @@ class SequenceFiltersRequestWithFieldsDeserializer : JsonDeserializer emptyList() - is ArrayNode -> fields.asSequence().map { it.asText() }.toList() + is ArrayNode -> fields.asSequence().map { fieldConverter.convert(it.asText()) }.toList() else -> throw BadRequestException( "fields must be an array or null", ) diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/DummySequenceFilterFields.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/DummySequenceFilterFields.kt new file mode 100644 index 00000000..41f0ec9b --- /dev/null +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/DummySequenceFilterFields.kt @@ -0,0 +1,49 @@ +package org.genspectrum.lapis + +import org.genspectrum.lapis.config.SequenceFilterField +import org.genspectrum.lapis.config.SequenceFilterFieldType +import org.genspectrum.lapis.config.SequenceFilterFields +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.CoreMatchers.not +import org.hamcrest.MatcherAssert.assertThat +import org.junit.jupiter.api.Test + +const val PANGO_LINEAGE_FIELD = "pangoLineage" +const val DATE_FIELD = "date" + +const val FIELD_WITH_UPPERCASE_LETTER = PANGO_LINEAGE_FIELD +const val FIELD_WITH_ONLY_LOWERCASE_LETTERS = DATE_FIELD + +val dummySequenceFilterFields = SequenceFilterFields( + mapOf( + DATE_FIELD to SequenceFilterFieldType.Date, + "dateTo" to SequenceFilterFieldType.DateTo(DATE_FIELD), + "dateFrom" to SequenceFilterFieldType.DateFrom(DATE_FIELD), + PANGO_LINEAGE_FIELD to SequenceFilterFieldType.PangoLineage, + "some_metadata" to SequenceFilterFieldType.String, + "other_metadata" to SequenceFilterFieldType.String, + "variantQuery" to SequenceFilterFieldType.VariantQuery, + "intField" to SequenceFilterFieldType.Int, + "intFieldTo" to SequenceFilterFieldType.IntTo("intField"), + "intFieldFrom" to SequenceFilterFieldType.IntFrom("intField"), + "floatField" to SequenceFilterFieldType.Float, + "floatFieldTo" to SequenceFilterFieldType.FloatTo("floatField"), + "floatFieldFrom" to SequenceFilterFieldType.FloatFrom("floatField"), + ) + .map { (name, type) -> name.lowercase() to SequenceFilterField(name, type) } + .toMap(), +) + +class ConstantsFulfillAssumptionsThatTheirNameSuggestsTest { + @Test + fun `field with uppercase letter has indeed uppercase and lowercase letters`() { + assertThat(FIELD_WITH_UPPERCASE_LETTER.uppercase(), `is`(not(equalTo(FIELD_WITH_UPPERCASE_LETTER)))) + assertThat(FIELD_WITH_UPPERCASE_LETTER.lowercase(), `is`(not(equalTo(FIELD_WITH_UPPERCASE_LETTER)))) + } + + @Test + fun `field with only lowercase letters has indeed only lowercase letters`() { + assertThat(FIELD_WITH_ONLY_LOWERCASE_LETTERS.lowercase(), `is`(equalTo(FIELD_WITH_ONLY_LOWERCASE_LETTERS))) + } +} diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/config/SequenceFilterFieldsTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/config/SequenceFilterFieldsTest.kt index 9e15f52a..39621d9c 100644 --- a/lapis2/src/test/kotlin/org/genspectrum/lapis/config/SequenceFilterFieldsTest.kt +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/config/SequenceFilterFieldsTest.kt @@ -22,17 +22,23 @@ class SequenceFilterFieldsTest { val underTest = SequenceFilterFields.fromDatabaseConfig(input) assertThat(underTest.fields, aMapWithSize(1)) - assertThat(underTest.fields, hasEntry("fieldName", SequenceFilterFieldType.String)) + assertThat( + underTest.fields, + hasEntry("fieldname", SequenceFilterField("fieldName", SequenceFilterFieldType.String)), + ) } @Test fun `given database config with a pango_lineage field then contains a pango_lineage field`() { - val input = databaseConfigWithFields(listOf(DatabaseMetadata("pango lineage", MetadataType.PANGO_LINEAGE))) + val input = databaseConfigWithFields(listOf(DatabaseMetadata("pangoLineage", MetadataType.PANGO_LINEAGE))) val underTest = SequenceFilterFields.fromDatabaseConfig(input) assertThat(underTest.fields, aMapWithSize(1)) - assertThat(underTest.fields, hasEntry("pango lineage", SequenceFilterFieldType.PangoLineage)) + assertThat( + underTest.fields, + hasEntry("pangolineage", SequenceFilterField("pangoLineage", SequenceFilterFieldType.PangoLineage)), + ) } @Test @@ -42,9 +48,21 @@ class SequenceFilterFieldsTest { val underTest = SequenceFilterFields.fromDatabaseConfig(input) assertThat(underTest.fields, aMapWithSize(3)) - assertThat(underTest.fields, hasEntry("dateField", SequenceFilterFieldType.Date)) - assertThat(underTest.fields, hasEntry("dateFieldFrom", SequenceFilterFieldType.DateFrom("dateField"))) - assertThat(underTest.fields, hasEntry("dateFieldTo", SequenceFilterFieldType.DateTo("dateField"))) + assertThat( + underTest.fields, + hasEntry("datefield", SequenceFilterField("dateField", SequenceFilterFieldType.Date)), + ) + assertThat( + underTest.fields, + hasEntry( + "datefieldfrom", + SequenceFilterField("dateFieldFrom", SequenceFilterFieldType.DateFrom("dateField")), + ), + ) + assertThat( + underTest.fields, + hasEntry("datefieldto", SequenceFilterField("dateFieldTo", SequenceFilterFieldType.DateTo("dateField"))), + ) } @Test @@ -54,9 +72,15 @@ class SequenceFilterFieldsTest { val underTest = SequenceFilterFields.fromDatabaseConfig(input) assertThat(underTest.fields, aMapWithSize(3)) - assertThat(underTest.fields, hasEntry("intField", SequenceFilterFieldType.Int)) - assertThat(underTest.fields, hasEntry("intFieldFrom", SequenceFilterFieldType.IntFrom("intField"))) - assertThat(underTest.fields, hasEntry("intFieldTo", SequenceFilterFieldType.IntTo("intField"))) + assertThat(underTest.fields, hasEntry("intfield", SequenceFilterField("intField", SequenceFilterFieldType.Int))) + assertThat( + underTest.fields, + hasEntry("intfieldfrom", SequenceFilterField("intFieldFrom", SequenceFilterFieldType.IntFrom("intField"))), + ) + assertThat( + underTest.fields, + hasEntry("intfieldto", SequenceFilterField("intFieldTo", SequenceFilterFieldType.IntTo("intField"))), + ) } @Test @@ -66,9 +90,24 @@ class SequenceFilterFieldsTest { val underTest = SequenceFilterFields.fromDatabaseConfig(input) assertThat(underTest.fields, aMapWithSize(3)) - assertThat(underTest.fields, hasEntry("floatField", SequenceFilterFieldType.Float)) - assertThat(underTest.fields, hasEntry("floatFieldFrom", SequenceFilterFieldType.FloatFrom("floatField"))) - assertThat(underTest.fields, hasEntry("floatFieldTo", SequenceFilterFieldType.FloatTo("floatField"))) + assertThat( + underTest.fields, + hasEntry("floatfield", SequenceFilterField("floatField", SequenceFilterFieldType.Float)), + ) + assertThat( + underTest.fields, + hasEntry( + "floatfieldfrom", + SequenceFilterField("floatFieldFrom", SequenceFilterFieldType.FloatFrom("floatField")), + ), + ) + assertThat( + underTest.fields, + hasEntry( + "floatfieldto", + SequenceFilterField("floatFieldTo", SequenceFilterFieldType.FloatTo("floatField")), + ), + ) } @Test @@ -78,7 +117,10 @@ class SequenceFilterFieldsTest { val underTest = SequenceFilterFields.fromDatabaseConfig(input) assertThat(underTest.fields, aMapWithSize(1)) - assertThat(underTest.fields, hasEntry("variantQuery", SequenceFilterFieldType.VariantQuery)) + assertThat( + underTest.fields, + hasEntry("variantquery", SequenceFilterField("variantQuery", SequenceFilterFieldType.VariantQuery)), + ) } private fun databaseConfigWithFields( diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerCommonFieldsTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerCommonFieldsTest.kt index d9945566..4160aa77 100644 --- a/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerCommonFieldsTest.kt +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerCommonFieldsTest.kt @@ -3,6 +3,8 @@ package org.genspectrum.lapis.controller import com.fasterxml.jackson.databind.node.TextNode import com.ninjasquad.springmockk.MockkBean import io.mockk.every +import org.genspectrum.lapis.FIELD_WITH_ONLY_LOWERCASE_LETTERS +import org.genspectrum.lapis.FIELD_WITH_UPPERCASE_LETTER import org.genspectrum.lapis.model.SiloQueryModel import org.genspectrum.lapis.request.AminoAcidInsertion import org.genspectrum.lapis.request.LapisInfo @@ -66,7 +68,7 @@ class LapisControllerCommonFieldsTest( } @Test - fun `GET aggregated with orderBy fields`() { + fun `GET aggregated with orderBy fields is case insensitive for configured fields`() { every { siloQueryModelMock.getAggregated( SequenceFiltersRequestWithFields( @@ -76,19 +78,25 @@ class LapisControllerCommonFieldsTest( emptyList(), emptyList(), emptyList(), - listOf(OrderByField("country", Order.ASCENDING), OrderByField("date", Order.ASCENDING)), + listOf( + OrderByField("country", Order.ASCENDING), + OrderByField(FIELD_WITH_ONLY_LOWERCASE_LETTERS, Order.ASCENDING), + OrderByField(FIELD_WITH_UPPERCASE_LETTER, Order.ASCENDING), + ), ), ) } returns listOf(AggregationData(0, mapOf("country" to TextNode("Switzerland")))) - mockMvc.perform(getSample("$AGGREGATED_ROUTE?orderBy=country,date")) + val uppercaseField = FIELD_WITH_ONLY_LOWERCASE_LETTERS.uppercase() + val lowercaseField = FIELD_WITH_UPPERCASE_LETTER.lowercase() + mockMvc.perform(getSample("$AGGREGATED_ROUTE?orderBy=country,$uppercaseField,$lowercaseField")) .andExpect(status().isOk) .andExpect(jsonPath("\$.data[0].count").value(0)) .andExpect(jsonPath("\$.data[0].country").value("Switzerland")) } @Test - fun `POST aggregated with flat orderBy fields`() { + fun `POST aggregated with flat orderBy fields is case insensitive for configured fields`() { every { siloQueryModelMock.getAggregated( SequenceFiltersRequestWithFields( @@ -98,13 +106,27 @@ class LapisControllerCommonFieldsTest( emptyList(), emptyList(), emptyList(), - listOf(OrderByField("country", Order.ASCENDING), OrderByField("date", Order.ASCENDING)), + listOf( + OrderByField("country", Order.ASCENDING), + OrderByField(FIELD_WITH_ONLY_LOWERCASE_LETTERS, Order.ASCENDING), + OrderByField(FIELD_WITH_UPPERCASE_LETTER, Order.ASCENDING), + ), ), ) } returns listOf(AggregationData(0, mapOf("country" to TextNode("Switzerland")))) val request = postSample(AGGREGATED_ROUTE) - .content("""{"orderBy": ["country", "date"]}""") + .content( + """ + { + "orderBy": [ + "country", + "${FIELD_WITH_ONLY_LOWERCASE_LETTERS.uppercase()}", + "${FIELD_WITH_UPPERCASE_LETTER.lowercase()}" + ] + } + """.trimIndent(), + ) .contentType(MediaType.APPLICATION_JSON) mockMvc.perform(request) @@ -113,7 +135,7 @@ class LapisControllerCommonFieldsTest( } @Test - fun `POST aggregated with ascending and descending orderBy fields`() { + fun `POST aggregated with ascending and descending orderBy fields is case insensitive for configured fields`() { every { siloQueryModelMock.getAggregated( SequenceFiltersRequestWithFields( @@ -124,8 +146,8 @@ class LapisControllerCommonFieldsTest( emptyList(), emptyList(), listOf( - OrderByField("country", Order.DESCENDING), - OrderByField("date", Order.ASCENDING), + OrderByField(FIELD_WITH_ONLY_LOWERCASE_LETTERS, Order.DESCENDING), + OrderByField(FIELD_WITH_UPPERCASE_LETTER, Order.ASCENDING), OrderByField("age", Order.ASCENDING), ), ), @@ -137,8 +159,8 @@ class LapisControllerCommonFieldsTest( """ { "orderBy": [ - { "field": "country", "type": "descending" }, - { "field": "date", "type": "ascending" }, + { "field": "${FIELD_WITH_ONLY_LOWERCASE_LETTERS.uppercase()}", "type": "descending" }, + { "field": "${FIELD_WITH_UPPERCASE_LETTER.lowercase()}", "type": "ascending" }, { "field": "age" } ] } diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerCsvTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerCsvTest.kt index 49185832..e7399695 100644 --- a/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerCsvTest.kt +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerCsvTest.kt @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.node.TextNode import com.ninjasquad.springmockk.MockkBean import io.mockk.every import org.genspectrum.lapis.model.SiloQueryModel +import org.genspectrum.lapis.request.Field import org.genspectrum.lapis.request.LapisInfo import org.genspectrum.lapis.request.SequenceFiltersRequestWithFields import org.genspectrum.lapis.response.AggregationData @@ -435,7 +436,7 @@ class LapisControllerCsvTest( emptyList(), emptyList(), emptyList(), - fields, + fields.map { Field(it) }, emptyList(), ) } diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerTest.kt index 3e80adfa..fef35029 100644 --- a/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerTest.kt +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerTest.kt @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.node.TextNode import com.ninjasquad.springmockk.MockkBean import io.mockk.every import org.genspectrum.lapis.model.SiloQueryModel +import org.genspectrum.lapis.request.Field import org.genspectrum.lapis.request.MutationProportionsRequest import org.genspectrum.lapis.request.NucleotideMutation import org.genspectrum.lapis.request.SequenceFiltersRequest @@ -102,21 +103,21 @@ class LapisControllerTest( siloQueryModelMock.getAggregated( sequenceFiltersRequestWithFields( mapOf("country" to "Switzerland"), - listOf("country", "age"), + listOf("country", "date"), ), ) } returns listOf( AggregationData( 0, - mapOf("country" to TextNode("Switzerland"), "age" to IntNode(42)), + mapOf("country" to TextNode("Switzerland"), "date" to TextNode("a date")), ), ) - mockMvc.perform(getSample("$AGGREGATED_ROUTE?country=Switzerland&fields=country,age")) + mockMvc.perform(getSample("$AGGREGATED_ROUTE?country=Switzerland&fields=country,date")) .andExpect(status().isOk) .andExpect(jsonPath("\$.data[0].count").value(0)) .andExpect(jsonPath("\$.data[0].country").value("Switzerland")) - .andExpect(jsonPath("\$.data[0].age").value(42)) + .andExpect(jsonPath("\$.data[0].date").value("a date")) } @Test @@ -145,25 +146,25 @@ class LapisControllerTest( siloQueryModelMock.getAggregated( sequenceFiltersRequestWithFields( mapOf("country" to "Switzerland"), - listOf("country", "age"), + listOf("country", "date"), ), ) } returns listOf( AggregationData( 0, - mapOf("country" to TextNode("Switzerland"), "age" to IntNode(42)), + mapOf("country" to TextNode("Switzerland"), "date" to TextNode("a date")), ), ) val request = postSample(AGGREGATED_ROUTE) - .content("""{"country": "Switzerland", "fields": ["country","age"]}""") + .content("""{"country": "Switzerland", "fields": ["country","date"]}""") .contentType(MediaType.APPLICATION_JSON) mockMvc.perform(request) .andExpect(status().isOk) .andExpect(jsonPath("\$.data[0].count").value(0)) .andExpect(jsonPath("\$.data[0].country").value("Switzerland")) - .andExpect(jsonPath("\$.data[0].age").value(42)) + .andExpect(jsonPath("\$.data[0].date").value("a date")) } @ParameterizedTest(name = "GET {0} without explicit minProportion") @@ -389,15 +390,15 @@ class LapisControllerTest( siloQueryModelMock.getDetails( sequenceFiltersRequestWithFields( mapOf("country" to "Switzerland"), - listOf("country", "age"), + listOf("country", "date"), ), ) - } returns listOf(DetailsData(mapOf("country" to TextNode("Switzerland"), "age" to IntNode(42)))) + } returns listOf(DetailsData(mapOf("country" to TextNode("Switzerland"), "date" to TextNode("a date")))) - mockMvc.perform(getSample("$DETAILS_ROUTE?country=Switzerland&fields=country&fields=age")) + mockMvc.perform(getSample("$DETAILS_ROUTE?country=Switzerland&fields=country&fields=date")) .andExpect(status().isOk) .andExpect(jsonPath("\$.data[0].country").value("Switzerland")) - .andExpect(jsonPath("\$.data[0].age").value(42)) + .andExpect(jsonPath("\$.data[0].date").value("a date")) } @Test @@ -424,19 +425,19 @@ class LapisControllerTest( siloQueryModelMock.getDetails( sequenceFiltersRequestWithFields( mapOf("country" to "Switzerland"), - listOf("country", "age"), + listOf("country", "date"), ), ) - } returns listOf(DetailsData(mapOf("country" to TextNode("Switzerland"), "age" to IntNode(42)))) + } returns listOf(DetailsData(mapOf("country" to TextNode("Switzerland"), "date" to TextNode("a date")))) val request = postSample(DETAILS_ROUTE) - .content("""{"country": "Switzerland", "fields": ["country", "age"]}""") + .content("""{"country": "Switzerland", "fields": ["country", "date"]}""") .contentType(MediaType.APPLICATION_JSON) mockMvc.perform(request) .andExpect(status().isOk) .andExpect(jsonPath("\$.data[0].country").value("Switzerland")) - .andExpect(jsonPath("\$.data[0].age").value(42)) + .andExpect(jsonPath("\$.data[0].date").value("a date")) } private fun sequenceFiltersRequestWithFields( @@ -448,7 +449,7 @@ class LapisControllerTest( emptyList(), emptyList(), emptyList(), - fields, + fields.map { Field(it) }, emptyList(), ) diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/model/SiloFilterExpressionMapperTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/model/SiloFilterExpressionMapperTest.kt index 683e6f4c..512bca96 100644 --- a/lapis2/src/test/kotlin/org/genspectrum/lapis/model/SiloFilterExpressionMapperTest.kt +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/model/SiloFilterExpressionMapperTest.kt @@ -1,8 +1,8 @@ package org.genspectrum.lapis.model -import org.genspectrum.lapis.config.SequenceFilterFieldType -import org.genspectrum.lapis.config.SequenceFilterFields +import org.genspectrum.lapis.FIELD_WITH_UPPERCASE_LETTER import org.genspectrum.lapis.controller.BadRequestException +import org.genspectrum.lapis.dummySequenceFilterFields import org.genspectrum.lapis.request.AminoAcidInsertion import org.genspectrum.lapis.request.AminoAcidMutation import org.genspectrum.lapis.request.CommonSequenceFilters @@ -36,35 +36,19 @@ import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource import java.time.LocalDate -class SiloFilterExpressionMapperTest { - private val sequenceFilterFields = SequenceFilterFields( - mapOf( - "date" to SequenceFilterFieldType.Date, - "dateTo" to SequenceFilterFieldType.DateTo("date"), - "dateFrom" to SequenceFilterFieldType.DateFrom("date"), - "pangoLineage" to SequenceFilterFieldType.PangoLineage, - "some_metadata" to SequenceFilterFieldType.String, - "other_metadata" to SequenceFilterFieldType.String, - "variantQuery" to SequenceFilterFieldType.VariantQuery, - "intField" to SequenceFilterFieldType.Int, - "intFieldTo" to SequenceFilterFieldType.IntTo("intField"), - "intFieldFrom" to SequenceFilterFieldType.IntFrom("intField"), - "floatField" to SequenceFilterFieldType.Float, - "floatFieldTo" to SequenceFilterFieldType.FloatTo("floatField"), - "floatFieldFrom" to SequenceFilterFieldType.FloatFrom("floatField"), - ), - ) +private const val SOME_VALUE = "some value" +class SiloFilterExpressionMapperTest { private lateinit var underTest: SiloFilterExpressionMapper @BeforeEach fun setup() { - underTest = SiloFilterExpressionMapper(sequenceFilterFields, VariantQueryFacade()) + underTest = SiloFilterExpressionMapper(dummySequenceFilterFields, VariantQueryFacade()) } @Test fun `given invalid filter key then throws exception`() { - val filterParameter = getSequenceFilters(mapOf("invalid query key" to "some value")) + val filterParameter = getSequenceFilters(mapOf("invalid query key" to SOME_VALUE)) val exception = assertThrows { underTest.map(filterParameter) } @@ -74,6 +58,28 @@ class SiloFilterExpressionMapperTest { ) } + @Test + fun `GIVEN all uppercase key THEN is mapped to corresponding field`() { + val filterParameter = getSequenceFilters(mapOf("PANGOLINEAGE" to SOME_VALUE)) + + val result = underTest.map(filterParameter) + + val expected = + And(listOf(PangoLineageEquals(FIELD_WITH_UPPERCASE_LETTER, SOME_VALUE, includeSublineages = false))) + assertThat(result, equalTo(expected)) + } + + @Test + fun `GIVEN all lowercase key THEN is mapped to corresponding field`() { + val filterParameter = getSequenceFilters(mapOf("pangolineage" to SOME_VALUE)) + + val result = underTest.map(filterParameter) + + val expected = + And(listOf(PangoLineageEquals(FIELD_WITH_UPPERCASE_LETTER, SOME_VALUE, includeSublineages = false))) + assertThat(result, equalTo(expected)) + } + @Test fun `given empty filter parameters then returns a match-all filter`() { val filterParameter = getSequenceFilters(emptyMap()) diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/request/SequenceFiltersRequestWithFieldsTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/request/SequenceFiltersRequestWithFieldsTest.kt index 23a0158c..c7faddff 100644 --- a/lapis2/src/test/kotlin/org/genspectrum/lapis/request/SequenceFiltersRequestWithFieldsTest.kt +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/request/SequenceFiltersRequestWithFieldsTest.kt @@ -2,6 +2,8 @@ package org.genspectrum.lapis.request import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue +import org.genspectrum.lapis.FIELD_WITH_ONLY_LOWERCASE_LETTERS +import org.genspectrum.lapis.FIELD_WITH_UPPERCASE_LETTER import org.genspectrum.lapis.controller.BadRequestException import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo @@ -47,59 +49,89 @@ class SequenceFiltersRequestWithFieldsTest { listOf( Arguments.of( """ - { - "country": "Switzerland", - "fields": ["division", "country"] - } - """, + { + "country": "Switzerland", + "fields": ["date", "country"] + } + """, SequenceFiltersRequestWithFields( mapOf("country" to "Switzerland"), emptyList(), emptyList(), emptyList(), emptyList(), - listOf("division", "country"), + listOf(Field("date"), Field("country")), ), ), Arguments.of( """ - { - "nucleotideMutations": ["T1-", "A23062T"], - "fields": ["division", "country"] - } - """, + { + "fields": ["${FIELD_WITH_UPPERCASE_LETTER.lowercase()}"] + } + """, + SequenceFiltersRequestWithFields( + emptyMap(), + emptyList(), + emptyList(), + emptyList(), + emptyList(), + listOf(Field(FIELD_WITH_UPPERCASE_LETTER)), + ), + ), + Arguments.of( + """ + { + "fields": ["${FIELD_WITH_ONLY_LOWERCASE_LETTERS.uppercase()}"] + } + """, + SequenceFiltersRequestWithFields( + emptyMap(), + emptyList(), + emptyList(), + emptyList(), + emptyList(), + listOf(Field(FIELD_WITH_ONLY_LOWERCASE_LETTERS)), + ), + ), + Arguments.of( + """ + { + "nucleotideMutations": ["T1-", "A23062T"], + "fields": ["date", "country"] + } + """, SequenceFiltersRequestWithFields( emptyMap(), listOf(NucleotideMutation(null, 1, "-"), NucleotideMutation(null, 23062, "T")), emptyList(), emptyList(), emptyList(), - listOf("division", "country"), + listOf(Field("date"), Field("country")), ), ), Arguments.of( """ - { - "aminoAcidMutations": ["S:501Y", "ORF1b:12"], - "fields": ["division", "country"] - } - """, + { + "aminoAcidMutations": ["S:501Y", "ORF1b:12"], + "fields": ["date", "country"] + } + """, SequenceFiltersRequestWithFields( emptyMap(), emptyList(), listOf(AminoAcidMutation("S", 501, "Y"), AminoAcidMutation("ORF1b", 12, null)), emptyList(), emptyList(), - listOf("division", "country"), + listOf(Field("date"), Field("country")), ), ), Arguments.of( """ - { - "nucleotideInsertions": ["ins_S:501:Y", "ins_12:ABCD"], - "fields": ["division", "country"] - } - """, + { + "nucleotideInsertions": ["ins_S:501:Y", "ins_12:ABCD"], + "fields": ["date", "country"] + } + """, SequenceFiltersRequestWithFields( emptyMap(), emptyList(), @@ -109,16 +141,16 @@ class SequenceFiltersRequestWithFieldsTest { NucleotideInsertion(12, "ABCD", null), ), emptyList(), - listOf("division", "country"), + listOf(Field("date"), Field("country")), ), ), Arguments.of( """ - { - "aminoAcidInsertions": ["ins_S:501:Y", "ins_ORF1:12:ABCD"], - "fields": ["division", "country"] - } - """, + { + "aminoAcidInsertions": ["ins_S:501:Y", "ins_ORF1:12:ABCD"], + "fields": ["date", "country"] + } + """, SequenceFiltersRequestWithFields( emptyMap(), emptyList(), @@ -128,15 +160,15 @@ class SequenceFiltersRequestWithFieldsTest { AminoAcidInsertion(501, "S", "Y"), AminoAcidInsertion(12, "ORF1", "ABCD"), ), - listOf("division", "country"), + listOf(Field("date"), Field("country")), ), ), Arguments.of( """ - { - "country": "Switzerland" - } - """, + { + "country": "Switzerland" + } + """, SequenceFiltersRequestWithFields( mapOf("country" to "Switzerland"), emptyList(), @@ -148,10 +180,10 @@ class SequenceFiltersRequestWithFieldsTest { ), Arguments.of( """ - { - "accessKey": "some access key" - } - """, + { + "accessKey": "some access key" + } + """, SequenceFiltersRequestWithFields( emptyMap(), emptyList(), @@ -179,42 +211,42 @@ class SequenceFiltersRequestWithFieldsTest { listOf( Arguments.of( """ - { - "fields": "not an array" - } - """, + { + "fields": "not an array" + } + """, "fields must be an array or null", ), Arguments.of( """ - { - "nucleotideMutations": "not an array" - } - """, + { + "nucleotideMutations": "not an array" + } + """, "nucleotideMutations must be an array or null", ), Arguments.of( """ - { - "aminoAcidMutations": "not an array" - } - """, + { + "aminoAcidMutations": "not an array" + } + """, "aminoAcidMutations must be an array or null", ), Arguments.of( """ - { - "nucleotideInsertions": "not an array" - } - """, + { + "nucleotideInsertions": "not an array" + } + """, "nucleotideInsertions must be an array or null", ), Arguments.of( """ - { - "aminoAcidInsertions": "not an array" - } - """, + { + "aminoAcidInsertions": "not an array" + } + """, "aminoAcidInsertions must be an array or null", ), )