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

Weaker typing for input parameters in open api enrichers #6850

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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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""""))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,63 +173,70 @@ 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) = {
Typed.genericTypeClass(classOf[util.Map[_, _]], List(Typed[PropertyName], valueType))
}
if (elementType.isEmpty) {
val patternPropertiesTypesSet = patternProperties.map { case PatternWithSwaggerTyped(_, propertySwaggerTyped) =>
typingResult(propertySwaggerTyped)
typingResult(propertySwaggerTyped, resolveListOfObject)
}
additionalProperties match {
case AdditionalPropertiesDisabled if patternPropertiesTypesSet.isEmpty =>
Typed.record(Map.empty[String, TypingResult])
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)))
}
}

Expand Down
Loading