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 608446b
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 52 deletions.
85 changes: 52 additions & 33 deletions lapis2/src/main/kotlin/org/genspectrum/lapis/OpenApiDocs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,30 @@ 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.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 +40,63 @@ 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(sequenceFilterFieldSchemas(sequenceFilterFields))),
).addSchemas(
DETAILS_RESPONSE_SCHEMA,
Schema<String>()
.type("object")
.description("The response contains the metadata of every sequence matching the sequence filters.")
.properties(sequenceFilterFieldSchemas(sequenceFilterFields)),
),
)
}

private val accessKeySchema = Schema<String>()
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
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 = "AggregatedRequest"
const val DETAILS_REQUEST_SCHEMA = "DetailsRequest"
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

0 comments on commit 608446b

Please sign in to comment.