Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

714 query parameter collection format rule migration #805

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 +
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maxim-tschumak :
What happened to the long exhaustive version of that that I did some weeks ago...? Did we discarded it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm... I don't know. This is the function I've implemented last week and now I've just moved it to the right class (as you proposed).

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()
}
}