From da09c0cc1abb15e5d16fda828c82798780beada4 Mon Sep 17 00:00:00 2001 From: Roman Kniazevych Date: Wed, 20 Nov 2024 14:12:32 +0200 Subject: [PATCH 1/3] Fix reference link (`$ref:`) of defining examples for issue for `B014` --- README.md | 2 + .../ruleset/RequestResponseExampleRule.kt | 71 +++-- .../ruleset/RequestResponseExampleRuleTest.kt | 255 +----------------- .../request-response-example-success-1.yaml | 254 +++++++++++++++++ 4 files changed, 310 insertions(+), 272 deletions(-) create mode 100644 boat-quay/boat-quay-rules/src/test/resources/request-response-example-success-1.yaml diff --git a/README.md b/README.md index 238c02fa0..8fe118d88 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ It currently consists of # Release Notes BOAT is still under development and subject to change. +## 0.17.52 +* Lint rule `B014` fix reference link to component examples ## 0.17.46 * boat-scaffold * Enhanced ISO8601 Date Formatting with Fractional Seconds Support for Swift template. diff --git a/boat-quay/boat-quay-rules/src/main/kotlin/com/backbase/oss/boat/quay/ruleset/RequestResponseExampleRule.kt b/boat-quay/boat-quay-rules/src/main/kotlin/com/backbase/oss/boat/quay/ruleset/RequestResponseExampleRule.kt index 797bf86ef..8196ed19c 100644 --- a/boat-quay/boat-quay-rules/src/main/kotlin/com/backbase/oss/boat/quay/ruleset/RequestResponseExampleRule.kt +++ b/boat-quay/boat-quay-rules/src/main/kotlin/com/backbase/oss/boat/quay/ruleset/RequestResponseExampleRule.kt @@ -3,11 +3,13 @@ package com.backbase.oss.boat.quay.ruleset import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.JsonNodeFactory +import io.swagger.v3.oas.models.Components import io.swagger.v3.oas.models.Operation import io.swagger.v3.oas.models.PathItem import io.swagger.v3.oas.models.examples.Example import io.swagger.v3.oas.models.media.MediaType import io.swagger.v3.oas.models.media.Schema +import org.apache.commons.lang3.StringUtils import org.zalando.zally.rule.api.* @Rule( @@ -42,7 +44,7 @@ class RequestResponseExampleRule { (method == PathItem.HttpMethod.POST || method == PathItem.HttpMethod.PUT) -> { operation?.requestBody?.content.orEmpty() .map { (type, content) -> - findMissPropsInExample(content).map { missProps -> + findMissPropsInExample(content, context.api.components).map { missProps -> context.violation( "Not defined value(s) (`${missProps.second}`) of example(`${missProps.first}`) for request body of ${operation?.operationId}:${method.name} of $type", content @@ -67,7 +69,7 @@ class RequestResponseExampleRule { .map { (status, response) -> response.content.orEmpty() .map { (type, content) -> - findMissPropsInExample(content).map { missProps -> + findMissPropsInExample(content, context.api.components).map { missProps -> context.violation( "Not defined value(s) (`${missProps.second}`) of example(`${missProps.first}`) for ${operation?.operationId}:${method.name}:$status of $type", content @@ -80,28 +82,15 @@ class RequestResponseExampleRule { private val objectMapper = ObjectMapper() - private fun findMissPropsInExample(content: MediaType): List>> { + private fun findMissPropsInExample(content: MediaType, components: Components?): List>> { val properties = when { content.schema?.properties != null -> content.schema?.properties content.schema?.items?.properties != null -> content.schema?.items?.properties else -> return emptyList() } - var examples = content.examples.orEmpty() - if (examples.isEmpty()) { - examples = mapOf("example" to Example().value(content.example)) - } - val missedExampleProps = examples.map { (name, exampleObject) -> - val jsonObject = - when (val value = exampleObject?.value) { - null -> JsonNodeFactory.instance.objectNode() - is String -> objectMapper.valueToTree( - value - ) - - else -> value as JsonNode - } + val missedExampleProps = prepareExamples(content, components).map { (name, exampleObject) -> val noExampleProps = properties!!.map { (propName, _) -> - hasExample(propName, properties[propName], jsonObject) + hasExample(propName, properties[propName], jsonObject(exampleObject)) }.flatten() Pair(name, noExampleProps) } @@ -111,6 +100,48 @@ class RequestResponseExampleRule { } } + private fun prepareExamples( + content: MediaType, + components: Components? + ): Map { + var examples = content.examples.orEmpty() + if (examples.isEmpty()) { + val example = Example() + if (content.example != null) { + val exampleJsonNode = content.example as JsonNode + if (exampleJsonNode.has("value") && !exampleJsonNode.get("value").isNull) { + example.value(exampleJsonNode.get("value")) + } + if (exampleJsonNode.has("\$ref") && !exampleJsonNode.get("\$ref").isNull) { + example.`$ref`(exampleJsonNode.get("\$ref").toString()) + } + } + examples = mapOf("example" to example) + + } + val examples2 = examples.mapValues { example -> + if (StringUtils.isNotBlank(example.value.`$ref`)) { + components?.examples?.get( + StringUtils.substringAfterLast(example.value.`$ref`, "/").removeSuffix("\"") + ) + } else { + example.value + } + } + return examples2 + } + + private fun jsonObject(exampleObject: Example?): JsonNode { + return when (val value = exampleObject?.value) { + null -> JsonNodeFactory.instance.objectNode() + is String -> objectMapper.valueToTree( + value + ) + + else -> value as JsonNode + } + } + private fun hasExample(propertyName: String, property: Schema?, jsonObject: JsonNode): List { return hasExample(null, propertyName, property, jsonObject) } @@ -126,8 +157,10 @@ class RequestResponseExampleRule { "array" == property?.type && fieldValue.isArray -> { when { property.items.type == "object" -> { - property.items?.properties?.map { prop -> arrayTypeCheck(propertyName, prop, fieldValue) }!!.flatten() + property.items?.properties?.map { prop -> arrayTypeCheck(propertyName, prop, fieldValue) }!! + .flatten() } + else -> { emptyList() } diff --git a/boat-quay/boat-quay-rules/src/test/kotlin/com/backbase/oss/boat/quay/ruleset/RequestResponseExampleRuleTest.kt b/boat-quay/boat-quay-rules/src/test/kotlin/com/backbase/oss/boat/quay/ruleset/RequestResponseExampleRuleTest.kt index e40dc25f9..b5984fa0c 100644 --- a/boat-quay/boat-quay-rules/src/test/kotlin/com/backbase/oss/boat/quay/ruleset/RequestResponseExampleRuleTest.kt +++ b/boat-quay/boat-quay-rules/src/test/kotlin/com/backbase/oss/boat/quay/ruleset/RequestResponseExampleRuleTest.kt @@ -13,260 +13,9 @@ class RequestResponseExampleRuleTest { @Test fun `correct check if example fields are present`() { - @Language("YAML") + @Language("yml") val context = DefaultContextFactory().getOpenApiContext( - """ - openapi: 3.0.3 - info: - title: Thing API - version: 1.0.0 - paths: - /payments: - post: - tags: - - payments - summary: post - description: Create payments - operationId: createPayments - requestBody: - description: Create payments - content: - application/json: - schema: - type: array - items: - required: - - name - type: object - properties: - name: - type: string - description: Request name - minLength: 3 - maxLength: 251 - amountNumberAsString: - description: The amount string in number format - type: string - format: number - minimum: -999.99999 - maximum: 999.99999 - amountNumberAsStringOptional: - description: The amount string in number format - type: string - format: number - minimum: -999.99999 - maximum: 999.99999 - additions: - type: object - additionalProperties: - type: string - examples: - example1: - value: - name: Trading payment - amountNumberAsString: 101.01 - amountNumberAsStringOptional: 100.00 - additions: - test1: value1 - test2: value2 - responses: - "204": - description: OK - /client-api/v1/trading/instruments/{id}/portfolios: - summary: Trading Portfolios By Instrument - description: Trading details information for portfolios selected by instrument - get: - tags: - - portfolio-trading-accounts - summary: Discretionary/non-discretionary portfolios detail information - description: | - Trading portfolios detail information - operationId: getPortfoliosByInstrument - parameters: - - name: id - in: path - description: Instrument internal id - required: true - style: simple - explode: false - schema: - type: string - responses: - "200": - description: Portfolios by instrument information - content: - application/json: - schema: - required: - - portfolios - type: object - properties: - portfolios: - type: array - description: Details of a portfolio. - items: - required: - - name - - portfolioId - type: object - properties: - name: - type: string - description: Portfolio's name. - alias: - type: string - description: Portfolio's alias. - portfolioId: - type: string - description: Portfolio's internal id. - availableBalance: - required: - - amount - - currency - type: object - properties: - amount: - type: number - description: The amount in the specified currency - currency: - pattern: "^[A-Z]{3}${'$'}" - type: string - description: The alpha-3 code (complying with ISO 4217) of the currency - that qualifies the amount - availableForTradingQty: - type: number - description: The number of shares available to sell (holdings - standing - orders). - iban: - type: string - description: "Account ID or an IBAN associated with a portfolio, where applicable." - deprecated: true - arrangementDisplay: - type: object - properties: - name: - type: string - description: The name that can be assigned by the bank to label an arrangement - displayName: - type: string - description: "Represents an arrangement by it's correct naming identifier.\ - \ It could be account alias or user alias depending on the journey selected\ - \ by the financial institution. If none of those is set, the arrangement\ - \ name will be used." - bankAlias: - type: string - description: The name that can be assigned by the customer to label the - arrangement - iban: - type: string - description: "The International Bank Account Number. If specified, it must\ - \ be a valid IBAN, otherwise an invalid value error could be raised." - bban: - type: string - description: BBAN is the country-specific bank account number. It is short - for Basic Bank Account Number. Account numbers usually match the BBAN. - number: - type: string - description: First 6 and/or last 4 digits of a Payment card. All other digits - will/to be masked. Be aware that using card number differently is potential - PCI risk. - bic: - type: string - description: Bank Identifier Code - international bank code that identifies - particular banks worldwide - description: Arrangement information from Arrangement Manger - canSell: - type: boolean - description: Showing that the instrument can be sold from the selected portfolio. - examples: - example1: - value: - portfolios: - - name: Trading portfolio - alias: My portfolio to trade - portfolioId: 68bbeace-274e-11ec-9621-0242ac130002 - availableBalance: - amount: 5068.3 - currency: USD - availableForTradingQty: null - iban: NL79RABO5373380466 - arrangementDisplay: - name: Trading portfolio one - displayName: First portfolio account - bankAlias: Portfolio account - iban: ••••••••••••••••••0466 - bban: ••••••••••0466 - number: •••••ffix - bic: BICExample1 - canSell: true - accounts: - - id: 55bbeace-274e-22ec-5487-0242ac130004 - - id: 44bbeace-274e-22ec-5487-0242ac130005 - example2: - value: - portfolios: - - name: Trading portfolio - alias: My portfolio to trade - portfolioId: 68bbeace-274e-11ec-9621-0242ac130002 - - "400": - description: BadRequest - content: - application/json: - schema: - title: BadRequestError - required: - - key - - message - type: object - properties: - message: - minLength: 1 - type: string - description: Any further information - key: - minLength: 1 - type: string - description: Error summary - errors: - type: array - description: Detailed error information - items: - required: - - key - - message - type: object - title: ErrorItem - properties: - message: - minLength: 1 - type: string - description: Any further information. - key: - minLength: 1 - type: string - description: "{capability-name}.api.{api-key-name}. For generated validation\ - \ errors this is the path in the document the error resolves to. e.g.\ - \ object name + '.' + field" - context: - title: Context - type: object - additionalProperties: - type: string - description: Context can be anything used to construct localised messages. - examples: - example: - summary: example-1 - value: - message: Bad Request - key: GENERAL_ERROR - errors: - - message: "Value Exceeded. Must be between {min} and {max}." - key: common.api.shoesize - context: - max: "50" - min: "1" - """.trimIndent() + {}.javaClass.getResource("/request-response-example-success-1.yaml").readText().trim() ) val violations = cut.checkResponseExampleFulfill(context) diff --git a/boat-quay/boat-quay-rules/src/test/resources/request-response-example-success-1.yaml b/boat-quay/boat-quay-rules/src/test/resources/request-response-example-success-1.yaml new file mode 100644 index 000000000..40995d989 --- /dev/null +++ b/boat-quay/boat-quay-rules/src/test/resources/request-response-example-success-1.yaml @@ -0,0 +1,254 @@ +openapi: 3.0.3 +info: + title: Thing API + version: 1.0.0 +paths: + /payments: + post: + tags: + - payments + summary: post + description: Create payments + operationId: createPayments + requestBody: + description: Create payments + content: + application/json: + schema: + type: array + items: + required: + - name + type: object + properties: + name: + type: string + description: Request name + minLength: 3 + maxLength: 251 + amountNumberAsString: + description: The amount string in number format + type: string + format: number + minimum: -999.99999 + maximum: 999.99999 + amountNumberAsStringOptional: + description: The amount string in number format + type: string + format: number + minimum: -999.99999 + maximum: 999.99999 + additions: + type: object + additionalProperties: + type: string + example: + $ref: "#/components/examples/trading-payment" + responses: + "204": + description: OK + /client-api/v1/trading/instruments/{id}/portfolios: + summary: Trading Portfolios By Instrument + description: Trading details information for portfolios selected by instrument + get: + tags: + - portfolio-trading-accounts + summary: Discretionary/non-discretionary portfolios detail information + description: | + Trading portfolios detail information + operationId: getPortfoliosByInstrument + parameters: + - name: id + in: path + description: Instrument internal id + required: true + style: simple + explode: false + schema: + type: string + responses: + "200": + description: Portfolios by instrument information + content: + application/json: + schema: + required: + - portfolios + type: object + properties: + portfolios: + type: array + description: Details of a portfolio. + items: + required: + - name + - portfolioId + type: object + properties: + name: + type: string + description: Portfolio's name. + alias: + type: string + description: Portfolio's alias. + portfolioId: + type: string + description: Portfolio's internal id. + availableBalance: + required: + - amount + - currency + type: object + properties: + amount: + type: number + description: The amount in the specified currency + currency: + pattern: "^[A-Z]{3}${'$'}" + type: string + description: The alpha-3 code (complying with ISO 4217) of the currency + that qualifies the amount + availableForTradingQty: + type: number + description: The number of shares available to sell (holdings - standing + orders). + iban: + type: string + description: "Account ID or an IBAN associated with a portfolio, where applicable." + deprecated: true + arrangementDisplay: + type: object + properties: + name: + type: string + description: The name that can be assigned by the bank to label an arrangement + displayName: + type: string + description: "Represents an arrangement by it's correct naming identifier.\ + \ It could be account alias or user alias depending on the journey selected\ + \ by the financial institution. If none of those is set, the arrangement\ + \ name will be used." + bankAlias: + type: string + description: The name that can be assigned by the customer to label the + arrangement + iban: + type: string + description: "The International Bank Account Number. If specified, it must\ + \ be a valid IBAN, otherwise an invalid value error could be raised." + bban: + type: string + description: BBAN is the country-specific bank account number. It is short + for Basic Bank Account Number. Account numbers usually match the BBAN. + number: + type: string + description: First 6 and/or last 4 digits of a Payment card. All other digits + will/to be masked. Be aware that using card number differently is potential + PCI risk. + bic: + type: string + description: Bank Identifier Code - international bank code that identifies + particular banks worldwide + description: Arrangement information from Arrangement Manger + canSell: + type: boolean + description: Showing that the instrument can be sold from the selected portfolio. + examples: + example1: + value: + portfolios: + - name: Trading portfolio + alias: My portfolio to trade + portfolioId: 68bbeace-274e-11ec-9621-0242ac130002 + availableBalance: + amount: 5068.3 + currency: USD + availableForTradingQty: null + iban: NL79RABO5373380466 + arrangementDisplay: + name: Trading portfolio one + displayName: First portfolio account + bankAlias: Portfolio account + iban: ••••••••••••••••••0466 + bban: ••••••••••0466 + number: •••••ffix + bic: BICExample1 + canSell: true + accounts: + - id: 55bbeace-274e-22ec-5487-0242ac130004 + - id: 44bbeace-274e-22ec-5487-0242ac130005 + example2: + value: + portfolios: + - name: Trading portfolio + alias: My portfolio to trade + portfolioId: 68bbeace-274e-11ec-9621-0242ac130002 + + "400": + description: BadRequest + content: + application/json: + schema: + title: BadRequestError + required: + - key + - message + type: object + properties: + message: + minLength: 1 + type: string + description: Any further information + key: + minLength: 1 + type: string + description: Error summary + errors: + type: array + description: Detailed error information + items: + required: + - key + - message + type: object + title: ErrorItem + properties: + message: + minLength: 1 + type: string + description: Any further information. + key: + minLength: 1 + type: string + description: "{capability-name}.api.{api-key-name}. For generated validation\ + \ errors this is the path in the document the error resolves to. e.g.\ + \ object name + '.' + field" + context: + title: Context + type: object + additionalProperties: + type: string + description: Context can be anything used to construct localised messages. + examples: + example: + summary: example-1 + value: + message: Bad Request + key: GENERAL_ERROR + errors: + - message: "Value Exceeded. Must be between {min} and {max}." + key: common.api.shoesize + context: + max: "50" + min: "1" +components: + examples: + trading-payment: + summary: trading-payment + value: + name: Trading payment + amountNumberAsString: 101.01 + amountNumberAsStringOptional: 100.00 + additions: + test1: value1 + test2: value2 \ No newline at end of file From 3dc7e6408a06212fba03ee8cbf3c45d4a2d4e46a Mon Sep 17 00:00:00 2001 From: Roman Kniazevych Date: Thu, 21 Nov 2024 11:52:00 +0200 Subject: [PATCH 2/3] code PR improvements --- .../ruleset/RequestResponseExampleRule.kt | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/boat-quay/boat-quay-rules/src/main/kotlin/com/backbase/oss/boat/quay/ruleset/RequestResponseExampleRule.kt b/boat-quay/boat-quay-rules/src/main/kotlin/com/backbase/oss/boat/quay/ruleset/RequestResponseExampleRule.kt index 8196ed19c..e8001e2ff 100644 --- a/boat-quay/boat-quay-rules/src/main/kotlin/com/backbase/oss/boat/quay/ruleset/RequestResponseExampleRule.kt +++ b/boat-quay/boat-quay-rules/src/main/kotlin/com/backbase/oss/boat/quay/ruleset/RequestResponseExampleRule.kt @@ -104,31 +104,30 @@ class RequestResponseExampleRule { content: MediaType, components: Components? ): Map { - var examples = content.examples.orEmpty() - if (examples.isEmpty()) { - val example = Example() - if (content.example != null) { - val exampleJsonNode = content.example as JsonNode - if (exampleJsonNode.has("value") && !exampleJsonNode.get("value").isNull) { - example.value(exampleJsonNode.get("value")) + return content.examples.orEmpty() + .ifEmpty { + val example = Example() + if (content.example != null) { + val jsonNode = content.example as JsonNode + jsonNode.update("value") { value -> example.value(value) } + jsonNode.update("\$ref") { ref -> example.`$ref`(ref.toString()) } } - if (exampleJsonNode.has("\$ref") && !exampleJsonNode.get("\$ref").isNull) { - example.`$ref`(exampleJsonNode.get("\$ref").toString()) + mapOf("example" to example) + }.mapValues { example -> + if (StringUtils.isNotBlank(example.value.`$ref`)) { + components?.examples?.get( + StringUtils.substringAfterLast(example.value.`$ref`, "/").removeSuffix("\"") + ) + } else { + example.value } } - examples = mapOf("example" to example) + } + private fun JsonNode.update(key: String, onFound: (JsonNode) -> Unit) { + if (has(key) && !get(key).isNull && get(key) is JsonNode) { + onFound(get(key)) } - val examples2 = examples.mapValues { example -> - if (StringUtils.isNotBlank(example.value.`$ref`)) { - components?.examples?.get( - StringUtils.substringAfterLast(example.value.`$ref`, "/").removeSuffix("\"") - ) - } else { - example.value - } - } - return examples2 } private fun jsonObject(exampleObject: Example?): JsonNode { From 6a4d772e95cc2037b7ffec181f1abcd4de62431b Mon Sep 17 00:00:00 2001 From: Roman Kniazevych Date: Thu, 21 Nov 2024 11:53:21 +0200 Subject: [PATCH 3/3] code PR improvements --- .../oss/boat/quay/ruleset/RequestResponseExampleRule.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/boat-quay/boat-quay-rules/src/main/kotlin/com/backbase/oss/boat/quay/ruleset/RequestResponseExampleRule.kt b/boat-quay/boat-quay-rules/src/main/kotlin/com/backbase/oss/boat/quay/ruleset/RequestResponseExampleRule.kt index e8001e2ff..2d264c1d1 100644 --- a/boat-quay/boat-quay-rules/src/main/kotlin/com/backbase/oss/boat/quay/ruleset/RequestResponseExampleRule.kt +++ b/boat-quay/boat-quay-rules/src/main/kotlin/com/backbase/oss/boat/quay/ruleset/RequestResponseExampleRule.kt @@ -21,8 +21,8 @@ import org.zalando.zally.rule.api.* class RequestResponseExampleRule { /** - * Validate if the example contain at least one response example with all defined properties in a schema - * It will help to validate full response. Not just the required fields. + * Validate if the response/request contains at least one example with all defined properties in a schema + * It will help to validate full response/request. Not just the required fields. * Check only 2xx responses. * @param context the context to validate * @return list of identified violations