Skip to content

Commit

Permalink
feat: also enable returning TSV #284
Browse files Browse the repository at this point in the history
  • Loading branch information
fengelniederhammer committed Aug 3, 2023
1 parent d3107d6 commit f2ef102
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ interface CsvRecord {

@Component
class CsvWriter {
fun write(headers: Array<String>, data: List<CsvRecord>): String {
fun write(headers: Array<String>, data: List<CsvRecord>, delimiter: Delimiter): String {
val stringWriter = StringWriter()
CSVPrinter(
stringWriter,
CSVFormat.DEFAULT.builder().setRecordSeparator("\n").setHeader(*headers).build(),
CSVFormat.DEFAULT.builder()
.setRecordSeparator("\n")
.setDelimiter(delimiter.value)
.setHeader(*headers)
.build(),
).use {
for (datum in data) {
it.printRecord(*datum.asArray())
Expand All @@ -32,3 +36,8 @@ fun DetailsData.asCsvRecord() = JsonValuesCsvRecord(this.values)
data class JsonValuesCsvRecord(val values: Collection<JsonNode>) : CsvRecord {
override fun asArray() = values.map { it.asText() }.toTypedArray()
}

enum class Delimiter(val value: Char) {
COMMA(','),
TAB('\t'),
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import java.util.Enumeration

private val log = KotlinLogging.logger {}

const val TEXT_CSV_HEADER = "text/csv"
const val TEXT_TSV_HEADER = "text/tab-separated-values"

@Component
class DataFormatParameterFilter(val objectMapper: ObjectMapper) : OncePerRequestFilter() {

Expand All @@ -37,7 +40,12 @@ class AcceptHeaderModifyingRequestWrapper(
when (reReadableRequest.getRequestFields()[FORMAT_PROPERTY]?.textValue()?.uppercase()) {
"CSV" -> {
log.debug { "Overwriting Accept header to text/csv due to format property" }
return "text/csv"
return TEXT_CSV_HEADER
}

"TSV" -> {
log.debug { "Overwriting Accept header to text/csv due to format property" }
return TEXT_TSV_HEADER
}

else -> {}
Expand All @@ -52,7 +60,12 @@ class AcceptHeaderModifyingRequestWrapper(
when (reReadableRequest.getRequestFields()[FORMAT_PROPERTY]?.textValue()?.uppercase()) {
"CSV" -> {
log.debug { "Overwriting Accept header to text/csv due to format property" }
return Collections.enumeration(listOf("text/csv"))
return Collections.enumeration(listOf(TEXT_CSV_HEADER))
}

"TSV" -> {
log.debug { "Overwriting Accept header to text/csv due to format property" }
return Collections.enumeration(listOf(TEXT_TSV_HEADER))
}

else -> {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ class LapisController(
return siloQueryModel.getDetails(request)
}

@GetMapping("/details", produces = ["text/csv"])
@GetMapping("/details", produces = [TEXT_CSV_HEADER])
@Operation(
description = DETAILS_ENDPOINT_DESCRIPTION,
operationId = "getDetailsAsCsv",
Expand Down Expand Up @@ -285,7 +285,58 @@ class LapisController(
offset,
)

return getDetailsAsCsv(request)
return getDetailsAsCsv(request, Delimiter.COMMA)
}

@GetMapping("/details", produces = [TEXT_TSV_HEADER])
@Operation(
description = DETAILS_ENDPOINT_DESCRIPTION,
operationId = "getDetailsAsCsv",
)
fun getDetailsAsTsv(
@SequenceFilters
@RequestParam
sequenceFilters: Map<String, String>?,
@Parameter(description = DETAILS_FIELDS_DESCRIPTION)
@RequestParam
fields: List<String>?,
@Parameter(
schema = Schema(ref = "#/components/schemas/$ORDER_BY_FIELDS_SCHEMA"),
description = "The fields of the response to order by." +
" Fields specified here must also be present in \"fields\".",
)
@RequestParam
orderBy: List<OrderByField>?,
@Parameter(schema = Schema(ref = "#/components/schemas/$NUCLEOTIDE_MUTATIONS_SCHEMA"))
@RequestParam
nucleotideMutations: List<NucleotideMutation>?,
@Parameter(schema = Schema(ref = "#/components/schemas/$AMINO_ACID_MUTATIONS_SCHEMA"))
@RequestParam
aminoAcidMutations: List<AminoAcidMutation>?,
@Parameter(
schema = Schema(ref = "#/components/schemas/$LIMIT_SCHEMA"),
description = LIMIT_DESCRIPTION,
)
@RequestParam
limit: Int? = null,
@Parameter(
schema = Schema(ref = "#/components/schemas/$OFFSET_SCHEMA"),
description = OFFSET_DESCRIPTION,
)
@RequestParam
offset: Int? = null,
): String {
val request = SequenceFiltersRequestWithFields(
sequenceFilters?.filter { !SPECIAL_REQUEST_PROPERTIES.contains(it.key) } ?: emptyMap(),
nucleotideMutations ?: emptyList(),
aminoAcidMutations ?: emptyList(),
fields ?: emptyList(),
orderBy ?: emptyList(),
limit,
offset,
)

return getDetailsAsCsv(request, Delimiter.TAB)
}

@PostMapping("/details", produces = [MediaType.APPLICATION_JSON_VALUE])
Expand All @@ -304,7 +355,7 @@ class LapisController(
return siloQueryModel.getDetails(request)
}

@PostMapping("/details", produces = ["text/csv"])
@PostMapping("/details", produces = [TEXT_CSV_HEADER])
@Operation(
description = DETAILS_ENDPOINT_DESCRIPTION,
operationId = "postDetailsAsCsv",
Expand All @@ -314,10 +365,23 @@ class LapisController(
@RequestBody
request: SequenceFiltersRequestWithFields,
): String {
return getDetailsAsCsv(request)
return getDetailsAsCsv(request, Delimiter.COMMA)
}

@PostMapping("/details", produces = [TEXT_TSV_HEADER])
@Operation(
description = DETAILS_ENDPOINT_DESCRIPTION,
operationId = "postDetailsAsCsv",
)
fun postDetailsAsTsv(
@Parameter(schema = Schema(ref = "#/components/schemas/$DETAILS_REQUEST_SCHEMA"))
@RequestBody
request: SequenceFiltersRequestWithFields,
): String {
return getDetailsAsCsv(request, Delimiter.TAB)
}

private fun getDetailsAsCsv(request: SequenceFiltersRequestWithFields): String {
private fun getDetailsAsCsv(request: SequenceFiltersRequestWithFields, delimiter: Delimiter): String {
requestContext.filter = request

val data = siloQueryModel.getDetails(request)
Expand All @@ -327,7 +391,7 @@ class LapisController(
}

val headers = data[0].keys.toTypedArray<String>()
return csvWriter.write(headers, data.map { it.asCsvRecord() })
return csvWriter.write(headers, data.map { it.asCsvRecord() }, delimiter)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ class LapisControllerCsvTest(@Autowired val mockMvc: MockMvc) {
@MockkBean
lateinit var siloQueryModelMock: SiloQueryModel

val listOfMetadata = listOf(
mapOf("country" to TextNode("Switzerland"), "age" to IntNode(42)),
mapOf("country" to TextNode("Switzerland"), "age" to IntNode(43)),
)

val metadataCsv = """
country,age
Switzerland,42
Switzerland,43
""".trimIndent()

val metadataTsv = """
country age
Switzerland 42
Switzerland 43
""".trimIndent()

@Test
fun `GET empty details return empty CSV`() {
every {
Expand Down Expand Up @@ -61,27 +78,40 @@ class LapisControllerCsvTest(@Autowired val mockMvc: MockMvc) {
)
}

@Test
fun `GET details as TSV with accept header`() {
every {
siloQueryModelMock.getDetails(sequenceFiltersRequestWithFields(mapOf("country" to "Switzerland")))
} returns listOfMetadata

mockMvc.perform(get("/details?country=Switzerland").header("Accept", "text/tab-separated-values"))
.andExpect(status().isOk)
.andExpect(header().string("Content-Type", "text/tab-separated-values;charset=UTF-8"))
.andExpect(content().string(metadataTsv))
}

@Test
fun `GET details as CSV with request parameter`() {
every {
siloQueryModelMock.getDetails(sequenceFiltersRequestWithFields(mapOf("country" to "Switzerland")))
} returns listOf(
mapOf("country" to TextNode("Switzerland"), "age" to IntNode(42)),
mapOf("country" to TextNode("Switzerland"), "age" to IntNode(43)),
)
} returns listOfMetadata

mockMvc.perform(get("/details?country=Switzerland&dataFormat=csv"))
.andExpect(status().isOk)
.andExpect(header().string("Content-Type", "text/csv;charset=UTF-8"))
.andExpect(
content().string(
"""
country,age
Switzerland,42
Switzerland,43
""".trimIndent(),
),
)
.andExpect(content().string(metadataCsv))
}

@Test
fun `GET details as TSV with request parameter`() {
every {
siloQueryModelMock.getDetails(sequenceFiltersRequestWithFields(mapOf("country" to "Switzerland")))
} returns listOfMetadata

mockMvc.perform(get("/details?country=Switzerland&dataFormat=tsv"))
.andExpect(status().isOk)
.andExpect(header().string("Content-Type", "text/tab-separated-values;charset=UTF-8"))
.andExpect(content().string(metadataTsv))
}

@Test
Expand All @@ -105,7 +135,7 @@ class LapisControllerCsvTest(@Autowired val mockMvc: MockMvc) {
fun `POST details as CSV with accept header`() {
every {
siloQueryModelMock.getDetails(sequenceFiltersRequestWithFields(mapOf("country" to "Switzerland")))
} returns emptyList()
} returns listOfMetadata

val request = post("/details")
.content("""{"country": "Switzerland"}""")
Expand All @@ -115,14 +145,31 @@ class LapisControllerCsvTest(@Autowired val mockMvc: MockMvc) {
mockMvc.perform(request)
.andExpect(status().isOk)
.andExpect(header().string("Content-Type", "text/csv;charset=UTF-8"))
.andExpect(content().string(""))
.andExpect(content().string(metadataCsv))
}

@Test
fun `POST details as TSV with accept header`() {
every {
siloQueryModelMock.getDetails(sequenceFiltersRequestWithFields(mapOf("country" to "Switzerland")))
} returns listOfMetadata

val request = post("/details")
.content("""{"country": "Switzerland"}""")
.contentType(MediaType.APPLICATION_JSON)
.accept("text/tab-separated-values")

mockMvc.perform(request)
.andExpect(status().isOk)
.andExpect(header().string("Content-Type", "text/tab-separated-values;charset=UTF-8"))
.andExpect(content().string(metadataTsv))
}

@Test
fun `POST details as CSV with request parameter`() {
every {
siloQueryModelMock.getDetails(sequenceFiltersRequestWithFields(mapOf("country" to "Switzerland")))
} returns emptyList()
} returns listOfMetadata

val request = post("/details")
.content("""{"country": "Switzerland", "dataFormat": "csv"}""")
Expand All @@ -131,7 +178,23 @@ class LapisControllerCsvTest(@Autowired val mockMvc: MockMvc) {
mockMvc.perform(request)
.andExpect(status().isOk)
.andExpect(header().string("Content-Type", "text/csv;charset=UTF-8"))
.andExpect(content().string(""))
.andExpect(content().string(metadataCsv))
}

@Test
fun `POST details as TSV with request parameter`() {
every {
siloQueryModelMock.getDetails(sequenceFiltersRequestWithFields(mapOf("country" to "Switzerland")))
} returns listOfMetadata

val request = post("/details")
.content("""{"country": "Switzerland", "dataFormat": "tsv"}""")
.contentType(MediaType.APPLICATION_JSON)

mockMvc.perform(request)
.andExpect(status().isOk)
.andExpect(header().string("Content-Type", "text/tab-separated-values;charset=UTF-8"))
.andExpect(content().string(metadataTsv))
}

private fun sequenceFiltersRequestWithFields(
Expand Down

0 comments on commit f2ef102

Please sign in to comment.