From 227caa2029ca73312dcb880000fe56bbfc8779dd Mon Sep 17 00:00:00 2001 From: Filip Michalski Date: Tue, 24 Sep 2024 14:50:55 +0200 Subject: [PATCH 1/2] * Create backend validation for Adhoc testing * Extend ParametersValidator to handle validators as well * Change string formatter to remove quotes when changed value is empty. --- .../client/src/actions/nk/genericAction.ts | 5 +- designer/client/src/actions/nk/testAdhoc.ts | 8 ++ .../editors/expression/Formatter.ts | 5 +- .../GenericAction/GenericActionDialog.tsx | 5 +- .../useGenericActionValidation.tsx | 25 +++-- .../GenericAction/useTestWithFormAction.tsx | 5 +- designer/client/src/http/HttpService.ts | 17 +++- .../ui/api/ManagementResources.scala | 10 +- .../ui/api/NodesApiHttpService.scala | 27 ++++++ .../ui/api/TestInfoResources.scala | 2 +- .../api/description/NodesApiEndpoints.scala | 93 ++++++++++++++++--- .../ui/process/test/ScenarioTestService.scala | 13 ++- .../server/AkkaHttpBasedRouteProvider.scala | 3 +- .../ui/validation/ParametersValidator.scala | 47 +++++++++- 14 files changed, 222 insertions(+), 43 deletions(-) create mode 100644 designer/client/src/actions/nk/testAdhoc.ts diff --git a/designer/client/src/actions/nk/genericAction.ts b/designer/client/src/actions/nk/genericAction.ts index 19a64c854de..4deb7d7bfe7 100644 --- a/designer/client/src/actions/nk/genericAction.ts +++ b/designer/client/src/actions/nk/genericAction.ts @@ -2,6 +2,7 @@ import HttpService from "../../http/HttpService"; import { Expression, NodeValidationError, TypingResult, VariableTypes } from "../../types"; import { debounce } from "lodash"; +import { TestAdhocValidationRequest } from "./testAdhoc"; export interface GenericValidationData { validationErrors: NodeValidationError[]; @@ -20,8 +21,8 @@ export interface GenericValidationRequest { } export const validateGenericActionParameters = debounce( - async (processingType: string, validationRequestData: GenericValidationRequest, callback: (data: GenericValidationData) => void) => { - const { data } = await HttpService.validateGenericActionParameters(processingType, validationRequestData); + async (scenarioName: string, validationRequestData: TestAdhocValidationRequest, callback: (data: GenericValidationData) => void) => { + const { data } = await HttpService.validateAdhocTestParameters(scenarioName, validationRequestData); callback(data); }, 500, diff --git a/designer/client/src/actions/nk/testAdhoc.ts b/designer/client/src/actions/nk/testAdhoc.ts new file mode 100644 index 00000000000..bc3d1f22895 --- /dev/null +++ b/designer/client/src/actions/nk/testAdhoc.ts @@ -0,0 +1,8 @@ +import { ScenarioGraph } from "../../types"; +import { UIValueParameter } from "./genericAction"; +import { SourceWithParametersTest } from "../../http/HttpService"; + +export interface TestAdhocValidationRequest { + sourceParameters: SourceWithParametersTest; + scenarioGraph: ScenarioGraph; +} diff --git a/designer/client/src/components/graph/node-modal/editors/expression/Formatter.ts b/designer/client/src/components/graph/node-modal/editors/expression/Formatter.ts index 49a0f484e6a..82eee8bfee6 100644 --- a/designer/client/src/components/graph/node-modal/editors/expression/Formatter.ts +++ b/designer/client/src/components/graph/node-modal/editors/expression/Formatter.ts @@ -29,7 +29,10 @@ const valueStartsWithQuotationMark = (value) => startsWith(value, '"') || starts const quotationMark = (value) => (valueStartsWithQuotationMark(value) ? valueQuotationMark(value) : defaultQuotationMark); export const stringSpelFormatter: Formatter = { - encode: (value) => quotationMark(value) + value + quotationMark(value), + encode: (value) => { + if (value === "") return value; + else return quotationMark(value) + value + quotationMark(value); + }, decode: (value) => value.substring(1, value.length - 1), }; diff --git a/designer/client/src/components/modals/GenericAction/GenericActionDialog.tsx b/designer/client/src/components/modals/GenericAction/GenericActionDialog.tsx index 328fcb3ed91..9dd18173119 100644 --- a/designer/client/src/components/modals/GenericAction/GenericActionDialog.tsx +++ b/designer/client/src/components/modals/GenericAction/GenericActionDialog.tsx @@ -1,7 +1,7 @@ import { WindowButtonProps, WindowContentProps } from "@touk/window-manager"; import React, { ElementType, ReactElement, useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { UIParameter, VariableTypes } from "../../../types"; +import { ScenarioGraph, UIParameter, VariableTypes } from "../../../types"; import { WindowContent, WindowKind } from "../../../windowManager"; import { ContentSize } from "../../graph/node-modal/node/ContentSize"; import { LoadingButtonTypes } from "../../../windowManager/LoadingButton"; @@ -33,8 +33,11 @@ export interface GenericActionParameters { export interface GenericAction extends GenericActionParameters { variableTypes: VariableTypes; processingType: string; + scenarioName: string; initialValues: ActionValues; onConfirmAction: (values: ActionValues) => void; + sourceId: string; + scenarioGraph: ScenarioGraph; } export interface GenericActionData { diff --git a/designer/client/src/components/modals/GenericAction/useGenericActionValidation.tsx b/designer/client/src/components/modals/GenericAction/useGenericActionValidation.tsx index 0b1f1ddf6f6..e052710c53b 100644 --- a/designer/client/src/components/modals/GenericAction/useGenericActionValidation.tsx +++ b/designer/client/src/components/modals/GenericAction/useGenericActionValidation.tsx @@ -3,32 +3,39 @@ import { useCallback, useEffect, useState } from "react"; import { ActionValues } from "./GenericActionFormContext"; import { validateGenericActionParameters } from "../../../actions/nk/genericAction"; import { GenericAction } from "./GenericActionDialog"; +import { SourceWithParametersTest } from "../../../http/HttpService"; export function useGenericActionValidation( - action: Pick, + action: Pick, value: ActionValues, ): { isValid: boolean; errors: NodeValidationError[]; } { - const { processingType, parameters, variableTypes } = action; + const { scenarioName, parameters, sourceId, scenarioGraph } = action; const [errors, setErrors] = useState([]); const validate = useCallback( (value: ActionValues) => { validateGenericActionParameters( - processingType, + scenarioName, { - parameters: parameters.map((param) => ({ - ...param, - expression: value[param.name], - })), - variableTypes, + sourceParameters: { + sourceId, + parameterExpressions: parameters.reduce( + (obj, param) => ({ + ...obj, + [param.name]: value[param.name], + }), + {}, + ), + }, + scenarioGraph: scenarioGraph, }, ({ validationErrors }) => setErrors(validationErrors), ); }, - [parameters, processingType, variableTypes], + [parameters, scenarioName, scenarioGraph, sourceId], ); useEffect(() => { diff --git a/designer/client/src/components/modals/GenericAction/useTestWithFormAction.tsx b/designer/client/src/components/modals/GenericAction/useTestWithFormAction.tsx index bbbf2bafabf..c5278fd6a78 100644 --- a/designer/client/src/components/modals/GenericAction/useTestWithFormAction.tsx +++ b/designer/client/src/components/modals/GenericAction/useTestWithFormAction.tsx @@ -88,9 +88,12 @@ export function useTestWithFormAction(): GenericAction { parameters, variableTypes, processingType, + scenarioName, initialValues, onConfirmAction, + sourceId, + scenarioGraph, }), - [initialValues, onConfirmAction, parameters, processingType, variableTypes], + [initialValues, onConfirmAction, parameters, sourceId, scenarioGraph, processingType, scenarioName, variableTypes], ); } diff --git a/designer/client/src/http/HttpService.ts b/designer/client/src/http/HttpService.ts index 0776de1c467..c31141e0f7a 100644 --- a/designer/client/src/http/HttpService.ts +++ b/designer/client/src/http/HttpService.ts @@ -27,9 +27,9 @@ import { AdditionalInfo } from "../components/graph/node-modal/NodeAdditionalInf import { withoutHackOfEmptyEdges } from "../components/graph/GraphPartialsInTS/EdgeUtils"; import { CaretPosition2d, ExpressionSuggestion } from "../components/graph/node-modal/editors/expression/ExpressionSuggester"; import { GenericValidationRequest } from "../actions/nk/genericAction"; -import { EventTrackingSelector } from "../containers/event-tracking"; import { EventTrackingSelectorType, EventTrackingType } from "../containers/event-tracking/use-register-tracking-events"; import { AvailableScenarioLabels, ScenarioLabelsValidationResponse } from "../components/Labels/types"; +import { TestAdhocValidationRequest } from "../actions/nk/testAdhoc"; type HealthCheckProcessDeploymentType = { status: string; @@ -476,6 +476,21 @@ class HttpService { return promise; } + validateAdhocTestParameters( + scenarioName: string, + validationRequest: TestAdhocValidationRequest, + ): Promise> { + const promise = api.post(`/parameters/${encodeURIComponent(scenarioName)}/validateAdhoc`, validationRequest); + promise.catch((error) => + this.#addError( + i18next.t("notification.error.failedToValidateAdhocTestParameters", "Failed to validate parameters"), + error, + true, + ), + ); + return promise; + } + validateScenarioLabels(labels: string[]): Promise> { const data = { labels: labels }; return api diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ManagementResources.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ManagementResources.scala index 91afc5da7e7..f4f4d6e7b54 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ManagementResources.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ManagementResources.scala @@ -16,10 +16,7 @@ import pl.touk.nussknacker.engine.api.deployment._ import pl.touk.nussknacker.engine.api.graph.ScenarioGraph import pl.touk.nussknacker.engine.testmode.TestProcess._ import pl.touk.nussknacker.restmodel.{CustomActionRequest, CustomActionResponse} -import pl.touk.nussknacker.ui.api.description.NodesApiEndpoints.Dtos.{ - TestFromParametersRequest, - prepareTestFromParametersDecoder -} +import pl.touk.nussknacker.ui.api.description.NodesApiEndpoints.Dtos.AdhocTestParametersRequest import pl.touk.nussknacker.ui.api.utils.ScenarioDetailsOps._ import pl.touk.nussknacker.ui.api.ProcessesResources.ProcessUnmarshallingError import pl.touk.nussknacker.ui.metrics.TimeMeasuring.measureTime @@ -248,10 +245,7 @@ class ManagementResources( path("testWithParameters" / ProcessNameSegment) { processName => { (post & processDetailsForName(processName)) { process => - val modelData = typeToConfig.forProcessingTypeUnsafe(process.processingType) - implicit val requestDecoder: Decoder[TestFromParametersRequest] = - prepareTestFromParametersDecoder(modelData) - (post & entity(as[TestFromParametersRequest])) { testParametersRequest => + (post & entity(as[AdhocTestParametersRequest])) { testParametersRequest => { canDeploy(process.idWithNameUnsafe) { complete { diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/NodesApiHttpService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/NodesApiHttpService.scala index 6ce2f1dfdc8..0ce638ee3da 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/NodesApiHttpService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/NodesApiHttpService.scala @@ -37,8 +37,10 @@ import pl.touk.nussknacker.ui.api.description.NodesApiEndpoints.Dtos.{ import pl.touk.nussknacker.ui.api.utils.ScenarioHttpServiceExtensions import pl.touk.nussknacker.ui.api.utils.ScenarioDetailsOps._ import pl.touk.nussknacker.ui.process.ProcessService +import pl.touk.nussknacker.ui.process.label.ScenarioLabel import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataProvider import pl.touk.nussknacker.ui.process.repository.ProcessDBQueryRepository.ProcessNotFoundError +import pl.touk.nussknacker.ui.process.test.ScenarioTestService import pl.touk.nussknacker.ui.security.api.{AuthManager, LoggedUser} import pl.touk.nussknacker.ui.suggester.ExpressionSuggester import pl.touk.nussknacker.ui.validation.{NodeValidator, ParametersValidator, UIProcessValidator} @@ -52,6 +54,7 @@ class NodesApiHttpService( processingTypeToNodeValidator: ProcessingTypeDataProvider[NodeValidator, _], processingTypeToExpressionSuggester: ProcessingTypeDataProvider[ExpressionSuggester, _], processingTypeToParametersValidator: ProcessingTypeDataProvider[ParametersValidator, _], + processingTypeToScenarioTestServices: ProcessingTypeDataProvider[ScenarioTestService, _], protected override val scenarioService: ProcessService )(override protected implicit val executionContext: ExecutionContext) extends BaseHttpService(authManager) @@ -159,6 +162,30 @@ class NodesApiHttpService( } } + expose { + nodesApiEndpoints.adhocTestParametersValidationEndpoint + .serverSecurityLogic(authorizeKnownUser[NodesError]) + .serverLogicEitherT { implicit loggedUser => + { case (scenarioName, request) => + for { + scenarioWithDetails <- getScenarioWithDetailsByName(scenarioName) + validator = processingTypeToParametersValidator.forProcessingTypeUnsafe(scenarioWithDetails.processingType) + scenarioTestService = processingTypeToScenarioTestServices.forProcessingTypeUnsafe( + scenarioWithDetails.processingType + ) + inputParameters = scenarioTestService.testParametersDefinition( + request.scenarioGraph, + scenarioName, + scenarioWithDetails.isFragment, + scenarioWithDetails.labels.map(ScenarioLabel) + ) + metaData = request.scenarioGraph.properties.toMetaData(scenarioName) + validationResults = validator.validate(request, inputParameters)(metaData) + } yield ParametersValidationResultDto(validationResults, validationPerformed = true) + } + } + } + expose { nodesApiEndpoints.parametersSuggestionsEndpoint .serverSecurityLogic(authorizeKnownUser[NodesError]) diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/TestInfoResources.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/TestInfoResources.scala index c6271ddef0e..c9ce7966b9a 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/TestInfoResources.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/TestInfoResources.scala @@ -48,7 +48,7 @@ class TestInfoResources( } } ~ path("testParameters") { complete { - scenarioTestService.testParametersDefinition( + scenarioTestService.testUISourceParametersDefinition( scenarioGraph, processName, processDetails.isFragment, diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/NodesApiEndpoints.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/NodesApiEndpoints.scala index c00e18d7ed9..0aa8d641976 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/NodesApiEndpoints.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/NodesApiEndpoints.scala @@ -11,7 +11,7 @@ import org.springframework.util.ClassUtils import pl.touk.nussknacker.engine.ModelData import pl.touk.nussknacker.engine.additionalInfo.{AdditionalInfo, MarkdownAdditionalInfo} import pl.touk.nussknacker.engine.api.CirceUtil._ -import pl.touk.nussknacker.engine.api.{LayoutData, ProcessAdditionalFields} +import pl.touk.nussknacker.engine.api.{LayoutData, ProcessAdditionalFields, StreamMetaData} import pl.touk.nussknacker.engine.api.definition.{ FixedExpressionValue, FixedExpressionValueWithIcon, @@ -52,6 +52,7 @@ import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions.SecuredEndpoint import pl.touk.nussknacker.restmodel.definition.{UIParameter, UIValueParameter} import pl.touk.nussknacker.restmodel.validation.ValidationResults.{NodeValidationError, NodeValidationErrorType} import pl.touk.nussknacker.security.AuthCredentials +import pl.touk.nussknacker.ui.api.TapirCodecs.ScenarioGraphCodec._ import pl.touk.nussknacker.ui.api.TapirCodecs.ScenarioNameCodec._ import pl.touk.nussknacker.ui.api.description.NodesApiEndpoints.Dtos.NodesError.{ MalformedTypingResult, @@ -348,6 +349,74 @@ class NodesApiEndpoints(auth: EndpointInput[AuthCredentials]) extends BaseEndpoi .withSecurity(auth) } + lazy val adhocTestParametersValidationEndpoint: SecuredEndpoint[ + (ProcessName, AdhocTestParametersRequest), + NodesError, + ParametersValidationResultDto, + Any + ] = { + baseNuApiEndpoint + .summary("Validate given parameters") + .tag("Nodes") + .post + .in("parameters" / path[ProcessName]("scenarioName") / "validateAdhoc") + .in( + jsonBody[AdhocTestParametersRequest] + .example( + Example.of( + summary = Some("TODO"), + value = AdhocTestParametersRequest( + TestSourceParameters("source", Map(ParameterName("name") -> Expression.spel("'Amadeus'"))), + ScenarioGraph( + ProcessProperties(StreamMetaData()), + List(), + List(), + ) + ) + ) + ) + ) + .out( + statusCode(Ok).and( + jsonBody[ParametersValidationResultDto] + .examples( + List( + Example.of( + summary = Some("Validate correct parameters"), + value = ParametersValidationResultDto( + validationErrors = List.empty, + validationPerformed = true + ) + ), + Example.of( + summary = Some("Validate incorrect parameters"), + value = ParametersValidationResultDto( + List( + NodeValidationError( + "ExpressionParserCompilationError", + "Failed to parse expression: Bad expression type, expected: Boolean, found: Long(5)", + "There is problem with expression in field Some(condition) - it could not be parsed.", + Some("condition"), + NodeValidationErrorType.SaveAllowed, + details = None + ) + ), + validationPerformed = true + ), + ) + ) + ) + ) + ) + .errorOut( + oneOf[NodesError]( + noScenarioExample, + malformedTypingResultExample + ) + ) + .withSecurity(auth) + } + lazy val parametersValidationEndpoint: SecuredEndpoint[ (ProcessingType, ParametersValidationRequestDto), NodesError, @@ -1400,24 +1469,22 @@ object NodesApiEndpoints { new TypingResultDecoder(name => ClassUtils.forName(name, classLoader)).decodeTypingResults } - def prepareTestFromParametersDecoder(modelData: ModelData): Decoder[TestFromParametersRequest] = { - implicit val parameterNameDecoder: KeyDecoder[ParameterName] = KeyDecoder.decodeKeyString.map(ParameterName.apply) - implicit val typeDecoder: Decoder[TypingResult] = prepareTypingResultDecoder( - modelData.modelClassLoader.classLoader - ) - implicit val testSourceParametersDecoder: Decoder[TestSourceParameters] = - deriveConfiguredDecoder[TestSourceParameters] - deriveConfiguredDecoder[TestFromParametersRequest] - } + implicit val parameterNameCodec: KeyEncoder[ParameterName] = KeyEncoder.encodeKeyString.contramap(_.value) + implicit val parameterNameDecoder: KeyDecoder[ParameterName] = KeyDecoder.decodeKeyString.map(ParameterName.apply) - implicit val parameterNameCodec: KeyEncoder[ParameterName] = KeyEncoder.encodeKeyString.contramap(_.value) + implicit val mapParameterNameExpressionSchema: Typeclass[Map[ParameterName, Expression]] = + Schema.schemaForMap[ParameterName, Expression](_.value) + implicit val testSourceParametersDecoder: Decoder[TestSourceParameters] = + deriveConfiguredDecoder[TestSourceParameters] - @JsonCodec(encodeOnly = true) final case class TestSourceParameters( + @derive(schema, encoder, decoder) + final case class TestSourceParameters( sourceId: String, parameterExpressions: Map[ParameterName, Expression] ) - @JsonCodec(encodeOnly = true) final case class TestFromParametersRequest( + @derive(schema, encoder, decoder) + final case class AdhocTestParametersRequest( sourceParameters: TestSourceParameters, scenarioGraph: ScenarioGraph ) diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/test/ScenarioTestService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/test/ScenarioTestService.scala index 5625def93d3..47bf3da3a7e 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/test/ScenarioTestService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/test/ScenarioTestService.scala @@ -3,7 +3,7 @@ package pl.touk.nussknacker.ui.process.test import com.carrotsearch.sizeof.RamUsageEstimator import com.typesafe.scalalogging.LazyLogging import io.circe.Json -import pl.touk.nussknacker.engine.api.definition.StringParameterEditor +import pl.touk.nussknacker.engine.api.definition.{Parameter, StringParameterEditor} import pl.touk.nussknacker.engine.api.graph.ScenarioGraph import pl.touk.nussknacker.engine.api.process.{ProcessIdWithName, ProcessName} import pl.touk.nussknacker.engine.api.test.ScenarioTestData @@ -50,10 +50,19 @@ class ScenarioTestService( processName: ProcessName, isFragment: Boolean, labels: List[ScenarioLabel], - )(implicit user: LoggedUser): List[UISourceParameters] = { + )(implicit user: LoggedUser): Map[String, List[Parameter]] = { val canonical = toCanonicalProcess(scenarioGraph, processName, isFragment, labels) testInfoProvider .getTestParameters(canonical) + } + + def testUISourceParametersDefinition( + scenarioGraph: ScenarioGraph, + processName: ProcessName, + isFragment: Boolean, + labels: List[ScenarioLabel], + )(implicit user: LoggedUser): List[UISourceParameters] = { + testParametersDefinition(scenarioGraph, processName, isFragment, labels) .map { case (id, params) => UISourceParameters(id, params.map(DefinitionsService.createUIParameter)) } .map { assignUserFriendlyEditor } .toList diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/server/AkkaHttpBasedRouteProvider.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/server/AkkaHttpBasedRouteProvider.scala index 94087cbd276..444a91c6449 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/server/AkkaHttpBasedRouteProvider.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/server/AkkaHttpBasedRouteProvider.scala @@ -377,7 +377,8 @@ class AkkaHttpBasedRouteProvider( processingTypeToParametersValidator = processingTypeDataProvider.mapValues(v => new ParametersValidator(v.designerModelData.modelData, v.deploymentData.scenarioPropertiesConfig.keys) ), - scenarioService = processService + processingTypeToScenarioTestServices = scenarioTestService, + scenarioService = processService, ) val scenarioActivityApiHttpService = new ScenarioActivityApiHttpService( diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/validation/ParametersValidator.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/validation/ParametersValidator.scala index 2fb76897eda..6b6ec6d8333 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/validation/ParametersValidator.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/validation/ParametersValidator.scala @@ -1,14 +1,21 @@ package pl.touk.nussknacker.ui.validation import cats.data.Validated.Invalid +import cats.instances.list._ +import pl.touk.nussknacker.engine.util.validated.ValidatedSyntax._ import pl.touk.nussknacker.engine.ModelData -import pl.touk.nussknacker.engine.api.NodeId +import pl.touk.nussknacker.engine.api.{MetaData, NodeId} +import pl.touk.nussknacker.engine.api.definition.Parameter import pl.touk.nussknacker.engine.api.parameter.ParameterName -import pl.touk.nussknacker.engine.compile.ExpressionCompiler +import pl.touk.nussknacker.engine.compile.{ExpressionCompiler, Validations} +import pl.touk.nussknacker.engine.graph.evaluatedparam import pl.touk.nussknacker.engine.variables.GlobalVariablesPreparer import pl.touk.nussknacker.restmodel.validation.PrettyValidationErrors import pl.touk.nussknacker.restmodel.validation.ValidationResults.NodeValidationError -import pl.touk.nussknacker.ui.api.description.NodesApiEndpoints.Dtos.ParametersValidationRequest +import pl.touk.nussknacker.ui.api.description.NodesApiEndpoints.Dtos.{ + AdhocTestParametersRequest, + ParametersValidationRequest +} class ParametersValidator(modelData: ModelData, scenarioPropertiesNames: Iterable[String]) { @@ -30,4 +37,38 @@ class ParametersValidator(modelData: ModelData, scenarioPropertiesNames: Iterabl .flatten } + def validate(request: AdhocTestParametersRequest, parameters: Map[String, List[Parameter]])( + implicit metaData: MetaData + ): List[NodeValidationError] = { + implicit val nodeId: NodeId = NodeId("") + val context = validationContextGlobalVariablesOnly + + val parameterList: List[Parameter] = + parameters.getOrElse( + request.sourceParameters.sourceId, + throw new IllegalStateException( // This should never happen, it would mean that API is not consistent + s"There is no source ${request.sourceParameters.sourceId} associated with generated test parameters" + ) + ) + + val parameterWithExpression = parameterList.map { parameter => + parameter -> request.sourceParameters.parameterExpressions.getOrElse(parameter.name, parameter.finalDefaultValue) + } + + parameterWithExpression + .map { case (parameter, expression) => + val validatorsCompilationResult = parameter.validators.map { validator => + expressionCompiler.compileValidator(validator, parameter.name, parameter.typ, context.globalVariables) + }.sequence + + validatorsCompilationResult.andThen { validators => + expressionCompiler + .compileParam(evaluatedparam.Parameter(parameter.name, expression), context, parameter) + .andThen(Validations.validate(validators, _)) + } + } + .collect { case Invalid(a) => a.map(PrettyValidationErrors.formatErrorMessage).toList } + .flatten + } + } From c2d0dbc7b8bb4b17cf83fddb2706b93d1dda7489 Mon Sep 17 00:00:00 2001 From: Filip Michalski Date: Wed, 25 Sep 2024 12:10:41 +0200 Subject: [PATCH 2/2] Update openApi designer definitions --- docs-internal/api/nu-designer-openapi.yaml | 157 +++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/docs-internal/api/nu-designer-openapi.yaml b/docs-internal/api/nu-designer-openapi.yaml index 0f262c85d45..9592aec91f9 100644 --- a/docs-internal/api/nu-designer-openapi.yaml +++ b/docs-internal/api/nu-designer-openapi.yaml @@ -1303,6 +1303,136 @@ paths: security: - {} - httpAuth: [] + /api/parameters/{scenarioName}/validateAdhoc: + post: + tags: + - Nodes + summary: Validate given parameters + operationId: postApiParametersScenarionameValidateadhoc + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AdhocTestParametersRequest' + examples: + Example: + summary: TODO + value: + sourceParameters: + sourceId: source + parameterExpressions: + name: + language: spel + expression: '''Amadeus''' + scenarioGraph: + properties: + additionalFields: + properties: + parallelism: '' + spillStateToDisk: 'true' + useAsyncInterpretation: '' + checkpointIntervalInSeconds: '' + metaDataType: StreamMetaData + showDescription: false + nodes: [] + edges: [] + required: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ParametersValidationResultDto' + examples: + Example0: + summary: Validate correct parameters + value: + validationErrors: [] + validationPerformed: true + Example1: + summary: Validate incorrect parameters + value: + validationErrors: + - typ: ExpressionParserCompilationError + message: 'Failed to parse expression: Bad expression type, expected: + Boolean, found: Long(5)' + description: There is problem with expression in field Some(condition) + - it could not be parsed. + fieldName: condition + errorType: SaveAllowed + validationPerformed: true + '400': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Malformed TypingResult sent in request + value: |- + The request content was malformed: + Couldn't decode value 'WrongType'. Allowed values: 'TypedUnion,TypedDict,TypedObjectTypingResult,TypedTaggedValue,TypedClass,TypedObjectWithValue,TypedNull,Unknown + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] /api/nodes/{scenarioName}/additionalInfo: post: tags: @@ -4075,6 +4205,17 @@ components: title: AdditionalInfo oneOf: - $ref: '#/components/schemas/MarkdownAdditionalInfo' + AdhocTestParametersRequest: + title: AdhocTestParametersRequest + type: object + required: + - sourceParameters + - scenarioGraph + properties: + sourceParameters: + $ref: '#/components/schemas/TestSourceParameters' + scenarioGraph: + type: object ApiVersion: title: ApiVersion type: object @@ -4862,6 +5003,11 @@ components: type: object additionalProperties: type: string + Map_ParameterName_Expression: + title: Map_ParameterName_Expression + type: object + additionalProperties: + $ref: '#/components/schemas/Expression' Map_String: title: Map_String type: object @@ -6155,6 +6301,17 @@ components: TabularTypedDataEditor: title: TabularTypedDataEditor type: object + TestSourceParameters: + title: TestSourceParameters + type: object + required: + - sourceId + - parameterExpressions + properties: + sourceId: + type: string + parameterExpressions: + $ref: '#/components/schemas/Map_ParameterName_Expression' TextareaParameterEditor: title: TextareaParameterEditor type: object