Skip to content

Commit

Permalink
Merge pull request #805 from zalando/714-query-parameter-collection-f…
Browse files Browse the repository at this point in the history
…ormat-rule-migration

714 query parameter collection format rule migration
  • Loading branch information
maxim-tschumak committed Aug 22, 2018
2 parents 21d7e59 + 35ca4da commit 43b1890
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 164 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,34 @@ import de.zalando.zally.rule.api.Context
import de.zalando.zally.rule.api.Rule
import de.zalando.zally.rule.api.Severity
import de.zalando.zally.rule.api.Violation
import de.zalando.zally.util.allSchemas
import de.zalando.zally.util.getAllSchemas
import io.swagger.v3.oas.models.media.Schema
import org.springframework.beans.factory.annotation.Autowired

@Rule(
ruleSet = ZalandoRuleSet::class,
id = "174",
severity = Severity.MUST,
title = "Use common field names"
ruleSet = ZalandoRuleSet::class,
id = "174",
severity = Severity.MUST,
title = "Use common field names"
)
class CommonFieldTypesRule(@Autowired rulesConfig: Config) {

@Suppress("UNCHECKED_CAST")
private val commonFields = rulesConfig.getConfig("${javaClass.simpleName}.common_types").entrySet()
.map { (key, config) -> key to config.unwrapped() as List<String?> }.toMap()
.map { (key, config) -> key to config.unwrapped() as List<String?> }.toMap()

@Check(severity = Severity.MUST)
fun checkTypesOfCommonFields(context: Context): List<Violation> =
allSchemas(context.api).flatMap {
checkAllPropertiesOf(it, check = { name, schema ->
val violationDesc = checkField(name, schema)
if (violationDesc != null) {
context.violations(violationDesc, schema)
} else {
emptyList()
}
})
}
context.api.getAllSchemas().flatMap {
checkAllPropertiesOf(it, check = { name, schema ->
val violationDesc = checkField(name, schema)
if (violationDesc != null) {
context.violations(violationDesc, schema)
} else {
emptyList()
}
})
}

private fun checkAllPropertiesOf(
objectSchema: Schema<Any>,
Expand All @@ -52,11 +52,11 @@ class CommonFieldTypesRule(@Autowired rulesConfig: Config) {
}

internal fun checkField(name: String, property: Schema<Any>): String? =
commonFields[name]?.let { (type, format) ->
if (property.type != type)
"field '$name' has type '${property.type}' (expected type '$type')"
else if (property.format != format && format != null)
"field '$name' has type '${property.type}' with format '${property.format}' (expected format '$format')"
else null
}
commonFields[name]?.let { (type, format) ->
if (property.type != type)
"field '$name' has type '${property.type}' (expected type '$type')"
else if (property.format != format && format != null)
"field '$name' has type '${property.type}' with format '${property.format}' (expected format '$format')"
else null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import de.zalando.zally.rule.api.Context
import de.zalando.zally.rule.api.Rule
import de.zalando.zally.rule.api.Severity
import de.zalando.zally.rule.api.Violation
import de.zalando.zally.util.allSchemas
import de.zalando.zally.util.getAllSchemas
import org.springframework.beans.factory.annotation.Autowired

@Rule(
Expand All @@ -24,7 +24,7 @@ class FormatForNumbersRule(@Autowired rulesConfig: Config) {

@Check(severity = Severity.MUST)
fun checkNumberFormat(context: Context): List<Violation> =
allSchemas(context.api)
context.api.getAllSchemas()
.flatMap { it.properties.orEmpty().values }
.filter { it.type in numberTypes }
.filter { it.format == null || !isValid(it.type, it.format) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import de.zalando.zally.rule.api.Rule
import de.zalando.zally.rule.api.Severity
import de.zalando.zally.rule.api.Violation
import de.zalando.zally.util.WordUtil.isPlural
import de.zalando.zally.util.allSchemas
import de.zalando.zally.util.getAllSchemas

@Rule(
ruleSet = ZalandoRuleSet::class,
Expand All @@ -20,7 +20,7 @@ class PluralizeNamesForArraysRule {

@Check(severity = Severity.SHOULD)
fun checkArrayPropertyNamesArePlural(context: Context): List<Violation> =
allSchemas(context.api)
context.api.getAllSchemas()
.flatMap { it.properties.orEmpty().entries }
.filter { "array" == it.value.type }
.filterNot { isPlural(it.key) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,29 @@
package de.zalando.zally.rule.zalando

import de.zalando.zally.rule.api.Check
import de.zalando.zally.rule.api.Context
import de.zalando.zally.rule.api.Rule
import de.zalando.zally.rule.api.Severity
import de.zalando.zally.rule.api.Violation
import io.swagger.models.Swagger
import io.swagger.models.parameters.Parameter
import io.swagger.models.parameters.QueryParameter
import de.zalando.zally.util.getAllParameters
import io.swagger.v3.oas.models.parameters.Parameter

@Rule(
ruleSet = ZalandoRuleSet::class,
id = "154",
severity = Severity.SHOULD,
title = "Explicitly define the Collection Format of Query Parameters"
ruleSet = ZalandoRuleSet::class,
id = "154",
severity = Severity.SHOULD,
title = "Use and Specify Explicitly the Form-Style Query Format for Collection Parameters"
)
class QueryParameterCollectionFormatRule {
private val formatsAllowed = listOf("csv", "multi")
private val violationDescription = "CollectionFormat should be one of: $formatsAllowed"
private val allowedStyle = Parameter.StyleEnum.FORM
private val description = "Parameter style have to be `form`"

@Check(severity = Severity.SHOULD)
fun validate(swagger: Swagger): Violation? {
fun Collection<Parameter>?.extractInvalidQueryParam(path: String) =
orEmpty().filterIsInstance<QueryParameter>()
.filter { it.`type` == "array" && it.collectionFormat !in formatsAllowed }
.map { path to it.name }

val fromParams = swagger.parameters.orEmpty().values.extractInvalidQueryParam("parameters")
val fromPaths = swagger.paths.orEmpty().entries.flatMap { (name, path) ->
path.parameters.extractInvalidQueryParam(name) + path.operations.flatMap { operation ->
operation.parameters.extractInvalidQueryParam(name)
}
}

val allHeaders = fromParams + fromPaths
val paths = allHeaders
.map { "${it.first} ${it.second}" }
.distinct()

return if (paths.isNotEmpty()) createViolation(paths) else null
}

fun createViolation(paths: List<String>): Violation {
return Violation(violationDescription, paths)
}
fun checkParametersCollectionFormat(context: Context): List<Violation> =
if (context.isOpenAPI3())
context.api.getAllParameters().values
.filter { "query" == it.`in` && "array" == it.schema.type }
.filter { it.style == null || allowedStyle != it.style }
.map { context.violation(description, it) }
else emptyList()
}
25 changes: 0 additions & 25 deletions server/src/main/java/de/zalando/zally/util/OpenAPIObjectUtils.kt

This file was deleted.

31 changes: 31 additions & 0 deletions server/src/main/java/de/zalando/zally/util/OpenApiUtil.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package de.zalando.zally.util

import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.media.Schema
import io.swagger.v3.oas.models.parameters.Parameter
import io.swagger.v3.oas.models.responses.ApiResponse

Expand Down Expand Up @@ -35,3 +36,33 @@ fun OpenAPI.getAllHeaders(): Set<HeaderElement> {

return fromParams + fromPaths
}

/**
* Returns all defined schemas of an API specification
* @return a collection of schemas
*/
fun OpenAPI.getAllSchemas(): Collection<Schema<Any>> = this.components.schemas.orEmpty().values +
this.components.responses.values.flatMap { it.content.values.mapNotNull { it.schema } } +
this.components.requestBodies.values.flatMap { it.content.values.mapNotNull { it.schema } } +
this.paths.orEmpty().flatMap {
it.value.readOperations().flatMap { it.parameters.orEmpty().mapNotNull { it.schema } }
} +
this.paths.orEmpty().flatMap {
it.value.readOperations().flatMap {
it.responses.orEmpty().flatMap { it.value.content.orEmpty().values.mapNotNull { it.schema } }
}
} +
this.paths.orEmpty().flatMap {
it.value.readOperations().flatMap { it.requestBody?.content.orEmpty().values.mapNotNull { it.schema } }
}

/**
* Returns all defined parameters of an API specification
* @return a collection of parameters
*/
fun OpenAPI.getAllParameters(): Map<String, Parameter> = this.components.parameters.orEmpty() +
this.paths.orEmpty().values.flatMap { it.parameters.orEmpty().mapNotNull { it.name to it } } +
this.paths.orEmpty().values.flatMap {
it.readOperations()
.flatMap { it.parameters.orEmpty().mapNotNull { it.name to it } }
}
Original file line number Diff line number Diff line change
@@ -1,97 +1,78 @@
package de.zalando.zally.rule.zalando

import io.swagger.models.Operation
import io.swagger.models.Path
import io.swagger.models.Swagger
import io.swagger.models.parameters.QueryParameter
import de.zalando.zally.getOpenApiContextFromContent
import de.zalando.zally.getSwaggerContextFromContent
import org.assertj.core.api.Assertions.assertThat
import org.intellij.lang.annotations.Language
import org.junit.Test

class QueryParameterCollectionFormatRuleTest {

private val rule = QueryParameterCollectionFormatRule()

@Test
fun negativeCaseCollectionFormatNotSupported() {
val swagger = Swagger().apply {
parameters = mapOf("test" to QueryParameter().apply { name = "test"; type = "array"; collectionFormat = "notSupported" })
}

val result = rule.validate(swagger)!!
assertThat(result.description).isEqualTo("CollectionFormat should be one of: [csv, multi]")
assertThat(result.paths).isEqualTo(listOf("parameters test"))
}

@Test
fun negativeCaseCollectionFormatNotSupportedFromPath() {
val paramList = listOf(QueryParameter().apply { name = "test"; type = "array"; collectionFormat = "notSupported" })
val swagger = Swagger().apply {
paths = mapOf("/apis" to Path().apply { get = Operation().apply { parameters = paramList } })
}

val result = rule.validate(swagger)!!
assertThat(result.description).isEqualTo("CollectionFormat should be one of: [csv, multi]")
assertThat(result.paths).isEqualTo(listOf("/apis test"))
}

@Test
fun negativeCaseCollectionFormatNull() {
val swagger = Swagger().apply {
parameters = mapOf("test" to QueryParameter().apply { name = "test"; type = "array"; collectionFormat = null })
}

val result = rule.validate(swagger)!!
assertThat(result.description).isEqualTo("CollectionFormat should be one of: [csv, multi]")
assertThat(result.paths).isEqualTo(listOf("parameters test"))
fun `checkParametersCollectionFormat should return violation if invalid collection format is set`() {
@Language("YAML")
val content = """
openapi: 3.0.1
components:
parameters:
filters:
in: query
style: spaceDelimited
schema:
type: array
""".trimIndent()
val context = getOpenApiContextFromContent(content)

val violations = rule.checkParametersCollectionFormat(context)

assertThat(violations).isNotEmpty
assertThat(violations.size).isEqualTo(1)
assertThat(violations[0].description).isEqualTo("Parameter style have to be `form`")
assertThat(violations[0].pointer.toString()).isEqualTo("/components/parameters/filters")
}

@Test
fun negativeCaseCollectionFormatNullFromPath() {
val paramList = listOf(QueryParameter().apply { name = "test"; type = "array"; collectionFormat = null })
val swagger = Swagger().apply {
paths = mapOf("/apis" to Path().apply { get = Operation().apply { parameters = paramList } })
}

val result = rule.validate(swagger)!!
assertThat(result.description).isEqualTo("CollectionFormat should be one of: [csv, multi]")
assertThat(result.paths).isEqualTo(listOf("/apis test"))
fun `checkParametersCollectionFormat should return no violation if valid collection format is set`() {
@Language("YAML")
val content = """
openapi: 3.0.1
components:
parameters:
filters:
in: query
style: form
schema:
type: array
""".trimIndent()
val context = getOpenApiContextFromContent(content)

val violations = rule.checkParametersCollectionFormat(context)

assertThat(violations).isEmpty()
}

@Test
fun positiveCaseCsv() {
val swagger = Swagger().apply {
parameters = mapOf("test" to QueryParameter().apply { name = "test"; type = "array"; collectionFormat = "csv" })
}

assertThat(rule.validate(swagger)).isNull()
}

@Test
fun positiveCaseCsvFromPath() {
val paramList = listOf(QueryParameter().apply { name = "test"; type = "array"; collectionFormat = "csv" })
val swagger = Swagger().apply {
paths = mapOf("/apis" to Path().apply { get = Operation().apply { parameters = paramList } })
}

assertThat(rule.validate(swagger)).isNull()
}

@Test
fun positiveCaseMulti() {
val swagger = Swagger().apply {
parameters = mapOf("test" to QueryParameter().apply { name = "test"; type = "array"; collectionFormat = "multi" })
}

assertThat(rule.validate(swagger)).isNull()
}

@Test
fun positiveCaseMultiFromPath() {
val paramList = listOf(QueryParameter().apply { name = "test"; type = "array"; collectionFormat = "multi" })
val swagger = Swagger().apply {
paths = mapOf("/apis" to Path().apply { get = Operation().apply { parameters = paramList } })
}

assertThat(rule.validate(swagger)).isNull()
fun `checkParametersCollectionFormat should ignore OpenAPI 2 (Swagger) specifications`() {
@Language("YAML")
val content = """
swagger: 2.0
info:
title: Old API
version: 1.0.0
parameters:
filters:
name: filters
in: query
type: array
items:
type: string
""".trimIndent()
val context = getSwaggerContextFromContent(content)

val violations = rule.checkParametersCollectionFormat(context)

assertThat(violations).isEmpty()
}
}

0 comments on commit 43b1890

Please sign in to comment.