Skip to content

Commit

Permalink
docs: fine tune OpenAPI spec
Browse files Browse the repository at this point in the history
issue: #283
  • Loading branch information
fengelniederhammer committed Jul 6, 2023
1 parent ee40bae commit 9884558
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 67 deletions.
97 changes: 64 additions & 33 deletions lapis2/src/main/kotlin/org/genspectrum/lapis/OpenApiDocs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,32 @@ import io.swagger.v3.oas.models.Components
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.media.Schema
import org.genspectrum.lapis.config.DatabaseConfig
import org.genspectrum.lapis.config.MetadataType
import org.genspectrum.lapis.config.OpennessLevel
import org.genspectrum.lapis.config.SequenceFilterFieldName
import org.genspectrum.lapis.config.SequenceFilterFields
import org.genspectrum.lapis.controller.AGGREGATED_GROUP_BY_FIELDS_DESCRIPTION
import org.genspectrum.lapis.controller.AGGREGATED_REQUEST_SCHEMA
import org.genspectrum.lapis.controller.AGGREGATED_RESPONSE_SCHEMA
import org.genspectrum.lapis.controller.DETAILS_FIELDS_DESCRIPTION
import org.genspectrum.lapis.controller.DETAILS_REQUEST_SCHEMA
import org.genspectrum.lapis.controller.DETAILS_RESPONSE_SCHEMA
import org.genspectrum.lapis.controller.FIELDS_PROPERTY
import org.genspectrum.lapis.controller.MIN_PROPORTION_PROPERTY
import org.genspectrum.lapis.controller.REQUEST_SCHEMA
import org.genspectrum.lapis.controller.REQUEST_SCHEMA_WITH_FIELDS
import org.genspectrum.lapis.controller.REQUEST_SCHEMA_WITH_MIN_PROPORTION
import org.genspectrum.lapis.controller.RESPONSE_SCHEMA_AGGREGATED
import org.genspectrum.lapis.controller.SEQUENCE_FILTERS_SCHEMA
import org.genspectrum.lapis.response.COUNT_PROPERTY

fun buildOpenApiSchema(sequenceFilterFields: SequenceFilterFields, databaseConfig: DatabaseConfig): OpenAPI {
val filterProperties = sequenceFilterFields.fields
.map { (fieldName, fieldType) -> fieldName to Schema<String>().type(fieldType.openApiType) }
.toMap()

val requestProperties = when (databaseConfig.schema.opennessLevel) {
OpennessLevel.PROTECTED -> filterProperties + ("accessKey" to accessKeySchema)
else -> filterProperties
OpennessLevel.PROTECTED -> sequenceFilterFieldSchemas(sequenceFilterFields) + ("accessKey" to accessKeySchema())
else -> sequenceFilterFieldSchemas(sequenceFilterFields)
}

val responseProperties = filterProperties + mapOf(
"count" to Schema<String>().type("number"),
)

return OpenAPI()
.components(
Components().addSchemas(
REQUEST_SCHEMA,
SEQUENCE_FILTERS_SCHEMA,
Schema<String>()
.type("object")
.description("valid filters for sequence data")
Expand All @@ -41,43 +41,74 @@ fun buildOpenApiSchema(sequenceFilterFields: SequenceFilterFields, databaseConfi
.description("valid filters for sequence data")
.properties(requestProperties + Pair(MIN_PROPORTION_PROPERTY, Schema<String>().type("number"))),
).addSchemas(
REQUEST_SCHEMA_WITH_FIELDS,
Schema<String>()
.type("object")
.description("valid filters for sequence data")
.properties(
requestProperties + Pair(
"fields",
fieldsSchema,
),
),
AGGREGATED_REQUEST_SCHEMA,
requestSchemaWithFields(requestProperties, AGGREGATED_GROUP_BY_FIELDS_DESCRIPTION),
).addSchemas(
DETAILS_REQUEST_SCHEMA,
requestSchemaWithFields(requestProperties, DETAILS_FIELDS_DESCRIPTION),
).addSchemas(
RESPONSE_SCHEMA_AGGREGATED,
AGGREGATED_RESPONSE_SCHEMA,
Schema<String>()
.type("object")
.description(
"Aggregated sequence data. " +
"If fields are specified, then these fields are also keys in the result. " +
"The key 'count' is always present.",
)
.required(listOf("count"))
.properties(responseProperties),
.required(listOf(COUNT_PROPERTY))
.properties(getAggregatedResponseProperties(metadataFieldSchemas(databaseConfig))),
).addSchemas(
DETAILS_RESPONSE_SCHEMA,
Schema<String>()
.type("object")
.description("The response contains the metadata of every sequence matching the sequence filters.")
.properties(metadataFieldSchemas(databaseConfig)),
),
)
}

private val accessKeySchema = Schema<String>()
private fun metadataFieldSchemas(databaseConfig: DatabaseConfig) =
databaseConfig.schema.metadata.associate { it.name to Schema<String>().type(mapToOpenApiType(it.type)) }

private fun mapToOpenApiType(type: MetadataType): String = when (type) {
MetadataType.STRING -> "string"
MetadataType.PANGO_LINEAGE -> "string"
MetadataType.DATE -> "string"
MetadataType.INT -> "integer"
MetadataType.FLOAT -> "number"
}

private fun sequenceFilterFieldSchemas(sequenceFilterFields: SequenceFilterFields) = sequenceFilterFields.fields
.map { (fieldName, fieldType) -> fieldName to Schema<String>().type(fieldType.openApiType) }
.toMap()

private fun requestSchemaWithFields(
requestProperties: Map<SequenceFilterFieldName, Schema<Any>>,
fieldsDescription: String,
): Schema<*> =
Schema<String>()
.type("object")
.description("valid filters for sequence data")
.properties(requestProperties + Pair(FIELDS_PROPERTY, fieldsSchema().description(fieldsDescription)))

private fun getAggregatedResponseProperties(filterProperties: Map<SequenceFilterFieldName, Schema<Any>>) =
filterProperties.mapValues { (_, schema) ->
schema.description(
"This field is present if and only if it was specified in \"fields\" in the request. " +
"The response is stratified by this field.",
)
} + mapOf(
COUNT_PROPERTY to Schema<String>().type("number").description("The number of sequences matching the filters."),
)

private fun accessKeySchema() = Schema<String>()
.type("string")
.description(
"An access key that grants access to the protected data that this instance serves. " +
"There are two types or access keys: One only grants access to aggregated data, " +
"the other also grants access to detailed data.",
)

private val fieldsSchema = Schema<String>()
private fun fieldsSchema() = Schema<String>()
.type("array")
.items(Schema<String>().type("string"))
.description(
"The fields to stratify the result by. " +
"The response will contain the fields specified here with their respective values.",
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.genspectrum.lapis.config

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty

data class DatabaseConfig(val schema: DatabaseSchema)

Expand All @@ -14,7 +15,24 @@ data class DatabaseSchema(
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class DatabaseMetadata(val name: String, val type: String, val valuesAreUnique: Boolean = false)
data class DatabaseMetadata(val name: String, val type: MetadataType, val valuesAreUnique: Boolean = false)

enum class MetadataType {
@JsonProperty("string")
STRING,

@JsonProperty("pango_lineage")
PANGO_LINEAGE,

@JsonProperty("date")
DATE,

@JsonProperty("int")
INT,

@JsonProperty("float")
FLOAT,
}

data class DatabaseFeature(val name: String)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,23 @@ data class SequenceFilterFields(val fields: Map<SequenceFilterFieldName, Sequenc
}

private fun mapToSequenceFilterFields(databaseMetadata: DatabaseMetadata) = when (databaseMetadata.type) {
"string" -> listOf(databaseMetadata.name to SequenceFilterFieldType.String)
"pango_lineage" -> listOf(databaseMetadata.name to SequenceFilterFieldType.PangoLineage)
"date" -> listOf(
MetadataType.STRING -> listOf(databaseMetadata.name to SequenceFilterFieldType.String)
MetadataType.PANGO_LINEAGE -> listOf(databaseMetadata.name to 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),
)
"int" -> listOf(
MetadataType.INT -> listOf(
databaseMetadata.name to SequenceFilterFieldType.Int,
"${databaseMetadata.name}From" to SequenceFilterFieldType.IntFrom(databaseMetadata.name),
"${databaseMetadata.name}To" to SequenceFilterFieldType.IntTo(databaseMetadata.name),
)
"float" -> listOf(
MetadataType.FLOAT -> listOf(
databaseMetadata.name to SequenceFilterFieldType.Float,
"${databaseMetadata.name}From" to SequenceFilterFieldType.FloatFrom(databaseMetadata.name),
"${databaseMetadata.name}To" to SequenceFilterFieldType.FloatTo(databaseMetadata.name),
)

else -> throw IllegalArgumentException(
"Unknown field type '${databaseMetadata.type}' for field '${databaseMetadata.name}'",
)
}

private fun mapToSequenceFilterFieldsFromFeatures(databaseFeature: DatabaseFeature) = when (databaseFeature.name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,19 @@ import org.springframework.web.bind.annotation.RestController

const val MIN_PROPORTION_PROPERTY = "minProportion"
const val FIELDS_PROPERTY = "fields"
const val REQUEST_SCHEMA = "SequenceFilters"

const val SEQUENCE_FILTERS_SCHEMA = "SequenceFilters"
const val REQUEST_SCHEMA_WITH_MIN_PROPORTION = "SequenceFiltersWithMinProportion"
const val REQUEST_SCHEMA_WITH_FIELDS = "SequenceFiltersWithFields"
const val RESPONSE_SCHEMA_AGGREGATED = "AggregatedResponse"
const val AGGREGATED_REQUEST_SCHEMA = "AggregatedPostRequest"
const val DETAILS_REQUEST_SCHEMA = "DetailsPostRequest"
const val AGGREGATED_RESPONSE_SCHEMA = "AggregatedResponse"
const val DETAILS_RESPONSE_SCHEMA = "DetailsResponse"

private const val DEFAULT_MIN_PROPORTION = 0.05
const val AGGREGATED_GROUP_BY_FIELDS_DESCRIPTION =
"The fields to stratify by. If empty, only the overall count is returned"
const val DETAILS_FIELDS_DESCRIPTION =
"The fields that the response items should contain. If empty, all fields are returned"

@RestController
class LapisController(private val siloQueryModel: SiloQueryModel, private val requestContext: RequestContext) {
Expand All @@ -41,13 +48,15 @@ class LapisController(private val siloQueryModel: SiloQueryModel, private val re
@LapisAggregatedResponse
fun aggregated(
@Parameter(
schema = Schema(ref = "#/components/schemas/$REQUEST_SCHEMA_WITH_FIELDS"),
schema = Schema(ref = "#/components/schemas/$SEQUENCE_FILTERS_SCHEMA"),
explode = Explode.TRUE,
style = ParameterStyle.FORM,
)
@RequestParam
sequenceFilters: Map<String, String>,
@RequestParam(defaultValue = "") fields: List<String>,
@Schema(description = AGGREGATED_GROUP_BY_FIELDS_DESCRIPTION)
@RequestParam(defaultValue = "")
fields: List<String>,
): List<AggregationData> {
requestContext.filter = sequenceFilters

Expand All @@ -60,7 +69,7 @@ class LapisController(private val siloQueryModel: SiloQueryModel, private val re
@PostMapping("/aggregated")
@LapisAggregatedResponse
fun postAggregated(
@Parameter(schema = Schema(ref = "#/components/schemas/$REQUEST_SCHEMA_WITH_FIELDS"))
@Parameter(schema = Schema(ref = "#/components/schemas/$AGGREGATED_REQUEST_SCHEMA"))
@RequestBody()
request: SequenceFiltersRequestWithFields,
): List<AggregationData> {
Expand All @@ -76,7 +85,7 @@ class LapisController(private val siloQueryModel: SiloQueryModel, private val re
@LapisNucleotideMutationsResponse
fun getNucleotideMutations(
@Parameter(
schema = Schema(ref = "#/components/schemas/$REQUEST_SCHEMA"),
schema = Schema(ref = "#/components/schemas/$SEQUENCE_FILTERS_SCHEMA"),
explode = Explode.TRUE,
style = ParameterStyle.FORM,
)
Expand Down Expand Up @@ -124,13 +133,15 @@ class LapisController(private val siloQueryModel: SiloQueryModel, private val re
@LapisDetailsResponse
fun details(
@Parameter(
schema = Schema(ref = "#/components/schemas/$REQUEST_SCHEMA_WITH_FIELDS"),
schema = Schema(ref = "#/components/schemas/$SEQUENCE_FILTERS_SCHEMA"),
explode = Explode.TRUE,
style = ParameterStyle.FORM,
)
@RequestParam
sequenceFilters: Map<String, String>,
@RequestParam(defaultValue = "") fields: List<String>,
@Schema(description = DETAILS_FIELDS_DESCRIPTION)
@RequestParam(defaultValue = "")
fields: List<String>,
): List<DetailsData> {
requestContext.filter = sequenceFilters

Expand All @@ -143,7 +154,7 @@ class LapisController(private val siloQueryModel: SiloQueryModel, private val re
@PostMapping("/details")
@LapisDetailsResponse
fun postDetails(
@Parameter(schema = Schema(ref = "#/components/schemas/$REQUEST_SCHEMA_WITH_FIELDS"))
@Parameter(schema = Schema(ref = "#/components/schemas/$DETAILS_REQUEST_SCHEMA"))
@RequestBody()
request: SequenceFiltersRequestWithFields,
): List<DetailsData> {
Expand All @@ -167,9 +178,7 @@ class LapisController(private val siloQueryModel: SiloQueryModel, private val re
content = [
Content(
array = ArraySchema(
schema = Schema(
ref = "#/components/schemas/$RESPONSE_SCHEMA_AGGREGATED",
),
schema = Schema(ref = "#/components/schemas/$AGGREGATED_RESPONSE_SCHEMA"),
),
),
],
Expand Down Expand Up @@ -234,9 +243,7 @@ private annotation class LapisNucleotideMutationsResponse
content = [
Content(
array = ArraySchema(
schema = Schema(
ref = "#/components/schemas/$RESPONSE_SCHEMA_AGGREGATED",
),
schema = Schema(ref = "#/components/schemas/$DETAILS_RESPONSE_SCHEMA"),
),
),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import com.fasterxml.jackson.databind.SerializerProvider
import io.swagger.v3.oas.annotations.media.Schema
import org.springframework.boot.jackson.JsonComponent

const val COUNT_PROPERTY = "count"

data class AggregationData(val count: Int, @Schema(hidden = true) val fields: Map<String, JsonNode>)

@JsonComponent
class AggregationDataSerializer : JsonSerializer<AggregationData>() {
override fun serialize(value: AggregationData, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeStartObject()
gen.writeNumberField("count", value.count)
gen.writeNumberField(COUNT_PROPERTY, value.count)
value.fields.forEach { (key, value) -> gen.writeObjectField(key, value) }
gen.writeEndObject()
}
Expand All @@ -26,8 +28,8 @@ class AggregationDataSerializer : JsonSerializer<AggregationData>() {
class AggregationDataDeserializer : JsonDeserializer<AggregationData>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): AggregationData {
val node = p.readValueAsTree<JsonNode>()
val count = node.get("count").asInt()
val fields = node.fields().asSequence().filter { it.key != "count" }.associate { it.key to it.value }
val count = node.get(COUNT_PROPERTY).asInt()
val fields = node.fields().asSequence().filter { it.key != COUNT_PROPERTY }.associate { it.key to it.value }
return AggregationData(count, fields)
}
}
Expand Down
11 changes: 6 additions & 5 deletions siloLapisTests/test/aggregated.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { expect } from 'chai';
import { lapisClient } from './common';
import fs from 'fs';
import {AggregatedResponse, SequenceFiltersWithFields} from './lapisClient';
import { AggregatedPostRequest } from './lapisClient/models/AggregatedPostRequest';
import { AggregatedResponse } from './lapisClient/models/AggregatedResponse';

const queriesPath = __dirname + '/aggregatedQueries';
const aggregatedQueryFiles = fs.readdirSync(queriesPath);

type TestCase = {
testCaseName: string;
lapisRequest: SequenceFiltersWithFields;
expected: AggregatedResponse[];
testCaseName: string;
lapisRequest: AggregatedPostRequest;
expected: AggregatedResponse[];
};

describe('The /aggregated endpoint', () => {
Expand All @@ -18,7 +19,7 @@ describe('The /aggregated endpoint', () => {
.forEach((testCase: TestCase) =>
it('should return data for the test case ' + testCase.testCaseName, async () => {
const result = await lapisClient.postAggregated({
sequenceFiltersWithFields: testCase.lapisRequest,
aggregatedPostRequest: testCase.lapisRequest,
});

const resultWithoutUndefined = result.map((aggregatedResponse: AggregatedResponse) => {
Expand Down

0 comments on commit 9884558

Please sign in to comment.