From 168148ccb57038094e37600311a4e298d0455f0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fa=C5=82drowicz?= Date: Fri, 6 Sep 2024 13:05:18 +0200 Subject: [PATCH 1/2] Additional not working test --- .../enrichers-with-optional-fields.yaml | 54 +++++++++++++++++++ .../OpenApiScenarioIntegrationTest.scala | 21 ++++++++ 2 files changed, 75 insertions(+) create mode 100644 components/openapi/src/it/resources/enrichers-with-optional-fields.yaml diff --git a/components/openapi/src/it/resources/enrichers-with-optional-fields.yaml b/components/openapi/src/it/resources/enrichers-with-optional-fields.yaml new file mode 100644 index 00000000000..50bb27252db --- /dev/null +++ b/components/openapi/src/it/resources/enrichers-with-optional-fields.yaml @@ -0,0 +1,54 @@ +openapi: "3.0.0" +info: + title: Simple API overview + version: 2.0.0 +paths: + /customer: + post: + summary: Returns ComponentsBykeys. + requestBody: + description: + Keys of the Customers. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/MultiKeys' + responses: + '201': + description: + OK + content: + application/json: + schema: + $ref: '#/components/schemas/Customer' + +components: + schemas: + MultiKeys: + type: array + minItems: 1 + items: + type: object + properties: + primaryKey: + type: string + additionalKey: + type: string + validFor: + type: integer + format: int64 + minimum: 1 + maximum: 2592000 + required: + - primaryKey + - additionalKey + Customer: + type: object + properties: + name: + type: string + category: + type: string + id: + type: integer diff --git a/components/openapi/src/it/scala/pl/touk/nussknacker/openapi/functional/OpenApiScenarioIntegrationTest.scala b/components/openapi/src/it/scala/pl/touk/nussknacker/openapi/functional/OpenApiScenarioIntegrationTest.scala index fd2671fdab0..0e8290c1789 100644 --- a/components/openapi/src/it/scala/pl/touk/nussknacker/openapi/functional/OpenApiScenarioIntegrationTest.scala +++ b/components/openapi/src/it/scala/pl/touk/nussknacker/openapi/functional/OpenApiScenarioIntegrationTest.scala @@ -12,6 +12,7 @@ import pl.touk.nussknacker.engine.api.typed.TypedMap import pl.touk.nussknacker.engine.build.ScenarioBuilder import pl.touk.nussknacker.engine.flink.test.FlinkSpec import pl.touk.nussknacker.engine.graph.expression.Expression +import pl.touk.nussknacker.engine.spel import pl.touk.nussknacker.engine.util.test.{ClassBasedTestScenarioRunner, RunResult, TestScenarioRunner} import pl.touk.nussknacker.openapi.enrichers.SwaggerEnricher import pl.touk.nussknacker.openapi.parser.SwaggerParser @@ -53,6 +54,11 @@ class OpenApiScenarioIntegrationTest test(prepareScenarioRunner(port, sttpBackend, _.copy(allowedMethods = List("POST")))) } + def withRequestBody(sttpBackend: SttpBackend[Future, Any])(test: ClassBasedTestScenarioRunner => Any) = + new StubService("/enrichers-with-optional-fields.yaml").withCustomerService { port => + test(prepareScenarioRunner(port, sttpBackend, _.copy(allowedMethods = List("POST")))) + } + val stubbedBackend: SttpBackendStub[Future, Any] = SttpBackendStub.asynchronousFuture.whenRequestMatchesPartial { case request => request.headers match { @@ -93,6 +99,21 @@ class OpenApiScenarioIntegrationTest ) } + it should "call enricher with request body" in withRequestBody(stubbedBackend) { testScenarioRunner => + // given + val data = List("10") + val scenario = + scenarioWithEnricher((SingleBodyParameter.name, """{{additionalKey:"sss", primaryKey:"dfgdf"}}""".spel)) + + // when + val result = testScenarioRunner.runWithData(scenario, data) + + // then + result.validValue shouldBe RunResult.success( + TypedMap(Map("name" -> "Robert Wright", "id" -> 10L, "category" -> "GOLD")) + ) + } + it should "call enricher returning string" in withPrimitiveReturnType( SttpBackendStub.asynchronousFuture.whenRequestMatchesPartial { case _ => Response.ok((s""""justAString"""")) From f1469bd991115516bb39d01d0298917a559ced8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fa=C5=82drowicz?= Date: Mon, 9 Sep 2024 13:55:37 +0200 Subject: [PATCH 2/2] workaround for problem with list of objects with optional fields in enricher's parameters --- .../extractor/ParametersExtractor.scala | 2 +- .../engine/json/swagger/SwaggerTyped.scala | 81 ++++++++++--------- 2 files changed, 45 insertions(+), 38 deletions(-) diff --git a/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/extractor/ParametersExtractor.scala b/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/extractor/ParametersExtractor.scala index 4d1a7121ea8..38f2253685d 100644 --- a/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/extractor/ParametersExtractor.scala +++ b/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/extractor/ParametersExtractor.scala @@ -43,7 +43,7 @@ object ParametersExtractor { ParameterWithBodyFlag( Parameter( ParameterName(propertyName), - swaggerType.typingResult, + SwaggerTyped.typingResult(swaggerType, resolveListOfObjects = false), editor = swaggerType.editorOpt, validators = List.empty, defaultValue = None, diff --git a/utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/swagger/SwaggerTyped.scala b/utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/swagger/SwaggerTyped.scala index b405b9d1f19..cc876093962 100644 --- a/utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/swagger/SwaggerTyped.scala +++ b/utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/swagger/SwaggerTyped.scala @@ -173,44 +173,51 @@ object SwaggerTyped { Option(schema.getType) .orElse(Option(schema.getTypes).map(_.asScala.head)) - def typingResult(swaggerTyped: SwaggerTyped): TypingResult = swaggerTyped match { - case SwaggerObject(elementType, additionalProperties, patternProperties) => - handleSwaggerObject(elementType, additionalProperties, patternProperties) - case SwaggerArray(ofType) => - Typed.genericTypeClass(classOf[java.util.List[_]], List(typingResult(ofType))) - case SwaggerEnum(values) => - Typed.fromIterableOrUnknownIfEmpty(values.map(Typed.fromInstance)) - case SwaggerBool => - Typed.typedClass[java.lang.Boolean] - case SwaggerString => - Typed.typedClass[String] - case SwaggerInteger => - Typed.typedClass[java.lang.Integer] - case SwaggerLong => - Typed.typedClass[java.lang.Long] - case SwaggerBigInteger => - Typed.typedClass[java.math.BigInteger] - case SwaggerDouble => - Typed.typedClass[java.lang.Double] - case SwaggerBigDecimal => - Typed.typedClass[java.math.BigDecimal] - case SwaggerDateTime => - Typed.typedClass[ZonedDateTime] - case SwaggerDate => - Typed.typedClass[LocalDate] - case SwaggerTime => - Typed.typedClass[LocalTime] - case SwaggerUnion(types) => Typed.fromIterableOrUnknownIfEmpty(types.map(typingResult)) - case SwaggerAny => - Unknown - case SwaggerNull => - TypedNull - } + // `resolveListOfObjects` flag allows one to stop resolving Type recursion for SwaggerArray[SwaggerObject] + // this is needed for correct validations in openApi enrichers with input parameters that contains list of objects with optional fields + // TODO: validations in openApi enrichers should be based on actual schema instead of `TypingResult` instance + def typingResult(swaggerTyped: SwaggerTyped, resolveListOfObjects: Boolean = true): TypingResult = + swaggerTyped match { + case SwaggerObject(elementType, additionalProperties, patternProperties) => + handleSwaggerObject(elementType, additionalProperties, patternProperties, resolveListOfObjects) + case SwaggerArray(SwaggerObject(_, _, _)) if !resolveListOfObjects => + Typed.genericTypeClass(classOf[java.util.List[_]], List(Unknown)) + case SwaggerArray(ofType) => + Typed.genericTypeClass(classOf[java.util.List[_]], List(typingResult(ofType, resolveListOfObjects))) + case SwaggerEnum(values) => + Typed.fromIterableOrUnknownIfEmpty(values.map(Typed.fromInstance)) + case SwaggerBool => + Typed.typedClass[java.lang.Boolean] + case SwaggerString => + Typed.typedClass[String] + case SwaggerInteger => + Typed.typedClass[java.lang.Integer] + case SwaggerLong => + Typed.typedClass[java.lang.Long] + case SwaggerBigInteger => + Typed.typedClass[java.math.BigInteger] + case SwaggerDouble => + Typed.typedClass[java.lang.Double] + case SwaggerBigDecimal => + Typed.typedClass[java.math.BigDecimal] + case SwaggerDateTime => + Typed.typedClass[ZonedDateTime] + case SwaggerDate => + Typed.typedClass[LocalDate] + case SwaggerTime => + Typed.typedClass[LocalTime] + case SwaggerUnion(types) => Typed.fromIterableOrUnknownIfEmpty(types.map(typingResult(_, resolveListOfObjects))) + case SwaggerAny => + Unknown + case SwaggerNull => + TypedNull + } private def handleSwaggerObject( elementType: Map[PropertyName, SwaggerTyped], additionalProperties: AdditionalProperties, - patternProperties: List[PatternWithSwaggerTyped] + patternProperties: List[PatternWithSwaggerTyped], + resolveListOfObject: Boolean = true ): TypingResult = { import pl.touk.nussknacker.engine.util.Implicits.RichScalaMap def typedStringKeyMap(valueType: TypingResult) = { @@ -218,7 +225,7 @@ object SwaggerTyped { } if (elementType.isEmpty) { val patternPropertiesTypesSet = patternProperties.map { case PatternWithSwaggerTyped(_, propertySwaggerTyped) => - typingResult(propertySwaggerTyped) + typingResult(propertySwaggerTyped, resolveListOfObject) } additionalProperties match { case AdditionalPropertiesDisabled if patternPropertiesTypesSet.isEmpty => @@ -226,10 +233,10 @@ object SwaggerTyped { case AdditionalPropertiesDisabled => typedStringKeyMap(Typed.fromIterableOrUnknownIfEmpty(patternPropertiesTypesSet)) case AdditionalPropertiesEnabled(value) => - typedStringKeyMap(Typed(NonEmptyList(typingResult(value), patternPropertiesTypesSet))) + typedStringKeyMap(Typed(NonEmptyList(typingResult(value, resolveListOfObject), patternPropertiesTypesSet))) } } else { - Typed.record(elementType.mapValuesNow(typingResult)) + Typed.record(elementType.mapValuesNow(typingResult(_, resolveListOfObject))) } }