From de6af00bec0b22e7b470b41407b114875f965b37 Mon Sep 17 00:00:00 2001 From: Dawid Poliszak Date: Thu, 21 Nov 2024 08:04:52 +0100 Subject: [PATCH 1/5] [NU-1891] remove autocompletion from markdown editors (#7178) * NU-1891 remove autosuggestion from markdown field --- .../editors/expression/AceWrapper.tsx | 17 +++++++++++++---- .../expression/CustomCompleterAceEditor.tsx | 4 +++- .../editors/field/MarkdownFormControl.tsx | 1 + docs/Changelog.md | 1 + 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/designer/client/src/components/graph/node-modal/editors/expression/AceWrapper.tsx b/designer/client/src/components/graph/node-modal/editors/expression/AceWrapper.tsx index 965c87c9b86..037134a079a 100644 --- a/designer/client/src/components/graph/node-modal/editors/expression/AceWrapper.tsx +++ b/designer/client/src/components/graph/node-modal/editors/expression/AceWrapper.tsx @@ -25,18 +25,18 @@ export interface AceWrapperProps extends Pick, ): JSX.Element { const { language, readOnly, rows = 1, editorMode } = inputProps; @@ -183,9 +191,10 @@ export default forwardRef(function AceWrapper( className={readOnly ? " read-only" : ""} wrapEnabled={!!wrapEnabled} showGutter={!!showLineNumbers} - editorProps={DEFAULF_EDITOR_PROPS} + editorProps={DEFAULT_EDITOR_PROPS} setOptions={{ ...DEFAULT_OPTIONS, + enableLiveAutocompletion, showLineNumbers, }} enableBasicAutocompletion={customAceEditorCompleter && [customAceEditorCompleter]} diff --git a/designer/client/src/components/graph/node-modal/editors/expression/CustomCompleterAceEditor.tsx b/designer/client/src/components/graph/node-modal/editors/expression/CustomCompleterAceEditor.tsx index 3b37693fa41..d6682b0427f 100644 --- a/designer/client/src/components/graph/node-modal/editors/expression/CustomCompleterAceEditor.tsx +++ b/designer/client/src/components/graph/node-modal/editors/expression/CustomCompleterAceEditor.tsx @@ -28,10 +28,11 @@ export type CustomCompleterAceEditorProps = { showValidation?: boolean; isMarked?: boolean; className?: string; + enableLiveAutocompletion?: boolean; }; export function CustomCompleterAceEditor(props: CustomCompleterAceEditorProps): JSX.Element { - const { className, isMarked, showValidation, fieldErrors, validationLabelInfo, completer, isLoading } = props; + const { className, isMarked, showValidation, fieldErrors, validationLabelInfo, completer, isLoading, enableLiveAutocompletion } = props; const { value, onValueChange, ref, ...inputProps } = props.inputProps; const [editorFocused, setEditorFocused] = useState(false); @@ -65,6 +66,7 @@ export function CustomCompleterAceEditor(props: CustomCompleterAceEditorProps): ...inputProps, }} customAceEditorCompleter={completer} + enableLiveAutocompletion={enableLiveAutocompletion} /> Date: Thu, 21 Nov 2024 09:17:10 +0100 Subject: [PATCH 2/5] Fix scenario tests with fragment input validation (#7159) --- docs/Changelog.md | 1 + .../process/runner/FlinkTestMainSpec.scala | 59 ++++++++++++++++++- .../engine/compile/ProcessCompilerData.scala | 10 ++-- 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/docs/Changelog.md b/docs/Changelog.md index 1d2b8b56062..6b358bb54c6 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -101,6 +101,7 @@ * [#7102](https://github.com/TouK/nussknacker/pull/7102) Introduce a new UI to defining aggregations within nodes * [#7147](https://github.com/TouK/nussknacker/pull/7147) Fix redundant "ParameterName(...)" wrapper string in exported PDFs in nodes details * [#7178](https://github.com/TouK/nussknacker/pull/7178) Remove autocompletion from markdown editors +* [#7159](https://github.com/TouK/nussknacker/pull/7159) Fix running scenario tests with provided fragment input validation ## 1.17 diff --git a/engine/flink/executor/src/test/scala/pl/touk/nussknacker/engine/process/runner/FlinkTestMainSpec.scala b/engine/flink/executor/src/test/scala/pl/touk/nussknacker/engine/process/runner/FlinkTestMainSpec.scala index dea96b429cc..0a1892d3d0f 100644 --- a/engine/flink/executor/src/test/scala/pl/touk/nussknacker/engine/process/runner/FlinkTestMainSpec.scala +++ b/engine/flink/executor/src/test/scala/pl/touk/nussknacker/engine/process/runner/FlinkTestMainSpec.scala @@ -6,7 +6,7 @@ import org.apache.flink.runtime.client.JobExecutionException import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec import org.scalatest.{BeforeAndAfterEach, Inside, OptionValues} -import pl.touk.nussknacker.engine.api.{CirceUtil, DisplayJsonWithEncoder} +import pl.touk.nussknacker.engine.api.{CirceUtil, DisplayJsonWithEncoder, FragmentSpecificData, MetaData} import pl.touk.nussknacker.engine.api.process.ComponentUseCase import pl.touk.nussknacker.engine.api.test.{ScenarioTestData, ScenarioTestJsonRecord} import pl.touk.nussknacker.engine.build.{GraphBuilder, ScenarioBuilder} @@ -16,11 +16,21 @@ import pl.touk.nussknacker.engine.flink.test.{ RecordingExceptionConsumer, RecordingExceptionConsumerProvider } -import pl.touk.nussknacker.engine.graph.node.Case +import pl.touk.nussknacker.engine.graph.node.{Case, FragmentInputDefinition, FragmentOutputDefinition} import pl.touk.nussknacker.engine.process.helpers.SampleNodes._ import pl.touk.nussknacker.engine.testmode.TestProcess._ import pl.touk.nussknacker.engine.util.ThreadUtils import pl.touk.nussknacker.engine.ModelData +import pl.touk.nussknacker.engine.api.parameter.ParameterName +import pl.touk.nussknacker.engine.graph.expression.Expression +import pl.touk.nussknacker.engine.api.parameter.ParameterValueCompileTimeValidation +import pl.touk.nussknacker.engine.canonicalgraph.canonicalnode.FlatNode +import pl.touk.nussknacker.engine.compile.FragmentResolver +import pl.touk.nussknacker.engine.graph.node.FragmentInputDefinition.{FragmentClazzRef, FragmentParameter} +import pl.touk.nussknacker.engine.process.runner.FlinkTestMainSpec.{ + fragmentWithValidationName, + processWithFragmentParameterValidation +} import java.util.{Date, UUID} import scala.concurrent.ExecutionContext.Implicits.global @@ -654,6 +664,23 @@ class FlinkTestMainSpec extends AnyWordSpec with Matchers with Inside with Befor variable(List(ComponentUseCase.TestRuntime, ComponentUseCase.TestRuntime)) ) } + + "should not throw exception when process fragment has parameter validation defined" in { + val scenario = ScenarioBuilder + .streaming("scenario1") + .source(sourceNodeId, "input") + .fragmentOneOut("sub", fragmentWithValidationName, "output", "fragmentResult", "param" -> "'asd'".spel) + .emptySink("out", "valueMonitor", "Value" -> "1".spel) + + val resolved = FragmentResolver(List(processWithFragmentParameterValidation)).resolve(scenario) + + val results = runFlinkTest( + resolved.valueOr { _ => throw new IllegalArgumentException("Won't happen") }, + ScenarioTestData(List(ScenarioTestJsonRecord(sourceNodeId, Json.fromString("0|1|2|3|4|5|6")))), + useIOMonadInInterpreter + ) + results.exceptions.length shouldBe 0 + } } private def createTestRecord( @@ -710,3 +737,31 @@ class FlinkTestMainSpec extends AnyWordSpec with Matchers with Inside with Befor } } + +object FlinkTestMainSpec { + private val fragmentWithValidationName = "fragmentWithValidation" + + private val processWithFragmentParameterValidation: CanonicalProcess = { + val fragmentParamName = ParameterName("param") + val fragmentParam = FragmentParameter(fragmentParamName, FragmentClazzRef[String]).copy( + valueCompileTimeValidation = Some( + ParameterValueCompileTimeValidation( + validationExpression = Expression.spel("true"), + validationFailedMessage = Some("param validation failed") + ) + ) + ) + + CanonicalProcess( + MetaData(fragmentWithValidationName, FragmentSpecificData()), + List( + FlatNode( + FragmentInputDefinition("start", List(fragmentParam)) + ), + FlatNode(FragmentOutputDefinition("out1", "output", List.empty)) + ), + List.empty + ) + } + +} diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/ProcessCompilerData.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/ProcessCompilerData.scala index f80282eba93..00e2c592c04 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/ProcessCompilerData.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/ProcessCompilerData.scala @@ -39,15 +39,15 @@ object ProcessCompilerData { .filter(_.componentType == ComponentType.Service) val globalVariablesPreparer = GlobalVariablesPreparer(definitionWithTypes.modelDefinition.expressionConfig) - val expressionEvaluator = - ExpressionEvaluator.optimizedEvaluator(globalVariablesPreparer, listeners) + // Here we pass unOptimizedEvaluator for ValidationExpressionParameterValidator + // as the optimized once could cause problems with serialization with its listeners during scenario testing val expressionCompiler = ExpressionCompiler.withOptimization( userCodeClassLoader, dictRegistry, definitionWithTypes.modelDefinition.expressionConfig, definitionWithTypes.classDefinitions, - expressionEvaluator + ExpressionEvaluator.unOptimizedEvaluator(globalVariablesPreparer) ) // for testing environment it's important to take classloader from user jar @@ -69,8 +69,8 @@ object ProcessCompilerData { nodeCompiler, customProcessValidator ) - - val interpreter = Interpreter(listeners, expressionEvaluator, componentUseCase) + val expressionEvaluator = ExpressionEvaluator.optimizedEvaluator(globalVariablesPreparer, listeners) + val interpreter = Interpreter(listeners, expressionEvaluator, componentUseCase) new ProcessCompilerData( processCompiler, From 2a4713b6d4583890f35cab5682af0df4fd9fd372 Mon Sep 17 00:00:00 2001 From: Dawid Poliszak Date: Thu, 21 Nov 2024 09:19:04 +0100 Subject: [PATCH 3/5] [Nu-1889] provide an unique validation message to the scenario labels (#7182) * NU-1889 provide an unique validation message to the scenario label --- designer/client/cypress/e2e/labels.cy.ts | 8 +++++++ .../scenarioDetails/ScenarioLabels.tsx | 22 +++++++++++++++---- docs/Changelog.md | 1 + 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/designer/client/cypress/e2e/labels.cy.ts b/designer/client/cypress/e2e/labels.cy.ts index d3e882c720c..a65a7db60cd 100644 --- a/designer/client/cypress/e2e/labels.cy.ts +++ b/designer/client/cypress/e2e/labels.cy.ts @@ -33,6 +33,14 @@ describe("Scenario labels", () => { cy.get("[data-testid=scenario-label-0]").should("be.visible").contains("tagX"); + cy.get("@labelInput").should("be.visible").click().type("tagX"); + + cy.wait("@labelvalidation"); + + cy.get("@labelInput").should("be.visible").contains("This label already exists. Please enter a unique value."); + + cy.get("@labelInput").find("input").clear(); + cy.get("@labelInput").should("be.visible").click().type("tag2"); cy.wait("@labelvalidation"); diff --git a/designer/client/src/components/toolbars/scenarioDetails/ScenarioLabels.tsx b/designer/client/src/components/toolbars/scenarioDetails/ScenarioLabels.tsx index 34235357cc6..06b5c7d5d50 100644 --- a/designer/client/src/components/toolbars/scenarioDetails/ScenarioLabels.tsx +++ b/designer/client/src/components/toolbars/scenarioDetails/ScenarioLabels.tsx @@ -26,6 +26,13 @@ interface AddLabelProps { onClick: () => void; } +const labelUniqueValidation = (label: string) => ({ + label, + messages: [ + i18next.t("panels.scenarioDetails.labels.validation.uniqueValue", "This label already exists. Please enter a unique value."), + ], +}); + const AddLabel = ({ onClick }: AddLabelProps) => { return ( { setIsEdited(true); }; - const isInputInSelectedOptions = (inputValue: string): boolean => { - return scenarioLabelOptions.some((option) => inputValue === toLabelValue(option)); - }; + const isInputInSelectedOptions = useCallback( + (inputValue: string): boolean => { + return scenarioLabelOptions.some((option) => inputValue === toLabelValue(option)); + }, + [scenarioLabelOptions], + ); const inputHelperText = useMemo(() => { if (inputErrors.length !== 0) { @@ -151,9 +161,13 @@ export const ScenarioLabels = ({ readOnly }: Props) => { } } + if (isInputInSelectedOptions(newInput)) { + setInputErrors((prevState) => [...prevState, labelUniqueValidation(newInput)]); + } + setInputTyping(false); }, 500); - }, []); + }, [isInputInSelectedOptions]); const validateSelectedOptions = useMemo(() => { return debounce(async (labels: LabelOption[]) => { diff --git a/docs/Changelog.md b/docs/Changelog.md index 6b358bb54c6..4bebce77ee8 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -100,6 +100,7 @@ * [#7099](https://github.com/TouK/nussknacker/pull/7099) Provide an option to embedded video to the markdown * [#7102](https://github.com/TouK/nussknacker/pull/7102) Introduce a new UI to defining aggregations within nodes * [#7147](https://github.com/TouK/nussknacker/pull/7147) Fix redundant "ParameterName(...)" wrapper string in exported PDFs in nodes details +* [#7182](https://github.com/TouK/nussknacker/pull/7182) Provide an unique validation message to the scenario labels * [#7178](https://github.com/TouK/nussknacker/pull/7178) Remove autocompletion from markdown editors * [#7159](https://github.com/TouK/nussknacker/pull/7159) Fix running scenario tests with provided fragment input validation From 59f3ac4532184775ccfd67d892e0fdf469bac718 Mon Sep 17 00:00:00 2001 From: Arek Burdach Date: Thu, 21 Nov 2024 12:47:36 +0100 Subject: [PATCH 4/5] [NU-1877] Fix "Failed to get node validation" when using literal lists mixing different types of elements (#7187) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [NU-1877] Fix "Failed to get node validation" when using literal lists mixing different types of elements * test fixes + changelog/migration guide changes * Update docs/MigrationGuide.md Co-authored-by: Mateusz Słabek --------- Co-authored-by: Mateusz Słabek --- .../engine/api/json/FromJsonDecoder.scala | 25 +++++++++++++++++ .../engine/api/typed/ValueDecoder.scala | 19 ++++++------- .../engine/api/json/FromJsonDecoderTest.scala | 21 ++++++++++++++ .../api/typed/TypingResultDecoderSpec.scala | 15 +++++++++- .../engine/api/typed/ValueDecoderSpec.scala | 13 ++++----- .../openapi/extractor/HandleResponse.scala | 4 +-- docs/Changelog.md | 9 +++--- docs/MigrationGuide.md | 20 +++++++------ .../JsonSchemaRequestResponseSource.scala | 6 ++-- .../engine/util/functions/conversion.scala | 5 ++-- .../util/functions/ConversionUtilsSpec.scala | 7 +++-- .../json/serde/CirceJsonDeserializer.scala | 4 +-- .../engine/json/swagger/SwaggerTyped.scala | 2 +- .../FromJsonSchemaBasedDecoder.scala} | 22 ++++++++------- ...sonSchemaTypeDefinitionExtractorTest.scala | 4 +-- .../serde/CirceJsonDeserializerSpec.scala | 2 +- .../FromJsonSchemaBasedDecoderTest.scala} | 28 +++++++++---------- .../engine/util/json/JsonUtils.scala | 18 ------------ 18 files changed, 135 insertions(+), 89 deletions(-) create mode 100644 components-api/src/main/scala/pl/touk/nussknacker/engine/api/json/FromJsonDecoder.scala create mode 100644 components-api/src/test/scala/pl/touk/nussknacker/engine/api/json/FromJsonDecoderTest.scala rename utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/swagger/{extractor/FromJsonDecoder.scala => decode/FromJsonSchemaBasedDecoder.scala} (83%) rename utils/json-utils/src/test/scala/pl/touk/nussknacker/engine/json/swagger/{extractor/FromJsonDecoderTest.scala => decode/FromJsonSchemaBasedDecoderTest.scala} (84%) delete mode 100644 utils/utils/src/main/scala/pl/touk/nussknacker/engine/util/json/JsonUtils.scala diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/json/FromJsonDecoder.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/json/FromJsonDecoder.scala new file mode 100644 index 00000000000..587b4db9614 --- /dev/null +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/json/FromJsonDecoder.scala @@ -0,0 +1,25 @@ +package pl.touk.nussknacker.engine.api.json + +import io.circe.Json +import pl.touk.nussknacker.engine.util.Implicits._ + +import scala.jdk.CollectionConverters._ + +object FromJsonDecoder { + + def jsonToAny(json: Json): Any = json.fold( + jsonNull = null, + jsonBoolean = identity[Boolean], + jsonNumber = jsonNumber => + // we pick the narrowest type as possible to reduce the amount of memory and computations overheads + jsonNumber.toInt orElse + jsonNumber.toLong orElse + // We prefer java big decimal over float/double + jsonNumber.toBigDecimal.map(_.bigDecimal) + getOrElse (throw new IllegalArgumentException(s"Not supported json number: $jsonNumber")), + jsonString = identity[String], + jsonArray = _.map(jsonToAny).asJava, + jsonObject = _.toMap.mapValuesNow(jsonToAny).asJava + ) + +} diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ValueDecoder.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ValueDecoder.scala index 54ff028d39d..045039ab206 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ValueDecoder.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ValueDecoder.scala @@ -2,6 +2,7 @@ package pl.touk.nussknacker.engine.api.typed import cats.implicits.toTraverseOps import io.circe.{ACursor, Decoder, DecodingFailure, Json} +import pl.touk.nussknacker.engine.api.json.FromJsonDecoder import pl.touk.nussknacker.engine.api.typed.typing._ import java.math.BigInteger @@ -58,19 +59,15 @@ object ValueDecoder { case record: TypedObjectTypingResult => for { fieldsJson <- obj.as[Map[String, Json]] - decodedFields <- record.fields.toList.traverse { case (fieldName, fieldType) => - fieldsJson.get(fieldName) match { - case Some(fieldJson) => decodeValue(fieldType, fieldJson.hcursor).map(fieldName -> _) - case None => - Left( - DecodingFailure( - s"Record field '$fieldName' isn't present in encoded Record fields: $fieldsJson", - List() - ) - ) + decodedFields <- + fieldsJson.toList.traverse { case (fieldName, fieldJson) => + val fieldType = record.fields.getOrElse(fieldName, Unknown) + decodeValue(fieldType, fieldJson.hcursor).map(fieldName -> _) } - } } yield decodedFields.toMap.asJava + case Unknown => + /// For Unknown we fallback to generic json to any conversion. It won't work for some types such as LocalDate but for others should work correctly + obj.as[Json].map(FromJsonDecoder.jsonToAny) case typ => Left(DecodingFailure(s"Decoding of type [$typ] is not supported.", List())) } diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/json/FromJsonDecoderTest.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/json/FromJsonDecoderTest.scala new file mode 100644 index 00000000000..c27a2baf320 --- /dev/null +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/json/FromJsonDecoderTest.scala @@ -0,0 +1,21 @@ +package pl.touk.nussknacker.engine.api.json + +import io.circe.Json +import org.scalatest.OptionValues +import org.scalatest.funsuite.AnyFunSuiteLike +import org.scalatest.matchers.should.Matchers + +class FromJsonDecoderTest extends AnyFunSuiteLike with Matchers with OptionValues { + + test("json number decoding pick the narrowest type") { + FromJsonDecoder.jsonToAny(Json.fromInt(1)) shouldBe 1 + FromJsonDecoder.jsonToAny(Json.fromInt(Integer.MAX_VALUE)) shouldBe Integer.MAX_VALUE + FromJsonDecoder.jsonToAny(Json.fromLong(Long.MaxValue)) shouldBe Long.MaxValue + FromJsonDecoder.jsonToAny( + Json.fromBigDecimal(java.math.BigDecimal.valueOf(Double.MaxValue)) + ) shouldBe java.math.BigDecimal.valueOf(Double.MaxValue) + val moreThanLongMaxValue = BigDecimal(Long.MaxValue) * 10 + FromJsonDecoder.jsonToAny(Json.fromBigDecimal(moreThanLongMaxValue)) shouldBe moreThanLongMaxValue.bigDecimal + } + +} diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultDecoderSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultDecoderSpec.scala index 2358f5d6df4..b9cd3c45767 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultDecoderSpec.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultDecoderSpec.scala @@ -56,7 +56,20 @@ class TypingResultDecoderSpec Map("a" -> TypedObjectWithValue(Typed.typedClass[Int], 1)) ), List(Map("a" -> 1).asJava).asJava - ) + ), + typedListWithElementValues( + Typed.record( + List( + "a" -> Typed.typedClass[Int], + "b" -> Typed.typedClass[Int] + ) + ), + List(Map("a" -> 1).asJava, Map("b" -> 2).asJava).asJava + ), + typedListWithElementValues( + Unknown, + List(Map("a" -> 1).asJava, 2).asJava + ), ).foreach { typing => val encoded = TypeEncoders.typingResultEncoder(typing) diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/ValueDecoderSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/ValueDecoderSpec.scala index 5c7a4205012..9cbb14ab760 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/ValueDecoderSpec.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/ValueDecoderSpec.scala @@ -33,7 +33,7 @@ class ValueDecoderSpec extends AnyFunSuite with EitherValuesDetailedMessage with ) } - test("decodeValue should fail when a required Record field is missing") { + test("decodeValue should ignore missing Record field") { val typedRecord = Typed.record( Map( "name" -> Typed.fromInstance("Alice"), @@ -45,12 +45,10 @@ class ValueDecoderSpec extends AnyFunSuite with EitherValuesDetailedMessage with "name" -> "Alice".asJson ) - ValueDecoder.decodeValue(typedRecord, json.hcursor).leftValue.message should include( - "Record field 'age' isn't present in encoded Record fields" - ) + ValueDecoder.decodeValue(typedRecord, json.hcursor).rightValue shouldBe Map("name" -> "Alice").asJava } - test("decodeValue should not include extra fields that aren't typed") { + test("decodeValue should decode extra fields using generic json decoding strategy") { val typedRecord = Typed.record( Map( "name" -> Typed.fromInstance("Alice"), @@ -66,8 +64,9 @@ class ValueDecoderSpec extends AnyFunSuite with EitherValuesDetailedMessage with ValueDecoder.decodeValue(typedRecord, json.hcursor) shouldEqual Right( Map( - "name" -> "Alice", - "age" -> 30 + "name" -> "Alice", + "age" -> 30, + "occupation" -> "nurse" ).asJava ) } diff --git a/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/extractor/HandleResponse.scala b/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/extractor/HandleResponse.scala index 0d95933449b..f0f0257c6df 100644 --- a/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/extractor/HandleResponse.scala +++ b/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/extractor/HandleResponse.scala @@ -2,14 +2,14 @@ package pl.touk.nussknacker.openapi.extractor import java.util.Collections import io.circe.Json -import pl.touk.nussknacker.engine.json.swagger.extractor.FromJsonDecoder +import pl.touk.nussknacker.engine.json.swagger.decode.FromJsonSchemaBasedDecoder import pl.touk.nussknacker.engine.json.swagger.{SwaggerArray, SwaggerTyped} object HandleResponse { def apply(res: Option[Json], responseType: SwaggerTyped): AnyRef = res match { case Some(json) => - FromJsonDecoder.decode(json, responseType) + FromJsonSchemaBasedDecoder.decode(json, responseType) case None => responseType match { case _: SwaggerArray => Collections.EMPTY_LIST diff --git a/docs/Changelog.md b/docs/Changelog.md index 4bebce77ee8..325d5e0141c 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -20,7 +20,7 @@ ### 1.18.0 (Not released yet) -* [6944](https://github.com/TouK/nussknacker/pull/6944) [7166](https://github.com/TouK/nussknacker/pull/7166) Changes around adhoc testing feature +* [#6944](https://github.com/TouK/nussknacker/pull/6944) [#7166](https://github.com/TouK/nussknacker/pull/7166) Changes around adhoc testing feature * `test-with-form` button was renamed to `adhoc-testing` * Improved form validators inside adhoc tests (validation was moved to backend) * Moved `testInfo/*` endpoints to `scenarioTesting/` path and rewrite then using Tapir @@ -40,7 +40,7 @@ in table source and sink components in `Table` parameter * [#6950](https://github.com/TouK/nussknacker/pull/6950) Fix for testing mechanism for table sources: using full, model classpath instead of only flinkTable.jar * [#6716](https://github.com/TouK/nussknacker/pull/6716) Fix type hints for #COLLECTION.merge function. -* [#6695](https://github.com/TouK/nussknacker/pull/6695) [7032](https://github.com/TouK/nussknacker/pull/7032) From now on, arrays on UI are visible as lists but on a background they are stored as it is. +* [#6695](https://github.com/TouK/nussknacker/pull/6695) [#7032](https://github.com/TouK/nussknacker/pull/7032) From now on, arrays on UI are visible as lists but on a background they are stored as it is. * [#6750](https://github.com/TouK/nussknacker/pull/6750) Add varargs to `#COLLECTION.concat` and `#COLLECTION.merge`. * [#6778](https://github.com/TouK/nussknacker/pull/6778) SpeL: check for methods if a property for a given name does not exist. * [#6769](https://github.com/TouK/nussknacker/pull/6769) Added possibility to choose presets and define lists for Long typed parameter inputs in fragments. @@ -103,6 +103,7 @@ * [#7182](https://github.com/TouK/nussknacker/pull/7182) Provide an unique validation message to the scenario labels * [#7178](https://github.com/TouK/nussknacker/pull/7178) Remove autocompletion from markdown editors * [#7159](https://github.com/TouK/nussknacker/pull/7159) Fix running scenario tests with provided fragment input validation +* [#7187](https://github.com/TouK/nussknacker/pull/7187) Fix "Failed to get node validation" when using literal lists that mixes different types of elements ## 1.17 @@ -645,7 +646,7 @@ * [#4254](https://github.com/TouK/nussknacker/pull/4254) Add simple spel expression suggestions endpoint to BE * [#4323](https://github.com/TouK/nussknacker/pull/4323) Improved code suggestions with Typer * [#4406](https://github.com/TouK/nussknacker/pull/4406) `backendCodeSuggestions` set to `true`, so by default Nussknacker will use new suggestion mechanism -* [#4299](https://github.com/TouK/nussknacker/pull/4299)[4322](https://github.com/TouK/nussknacker/pull/4322) `StateStatus` is identified by its name. +* [#4299](https://github.com/TouK/nussknacker/pull/4299)[#4322](https://github.com/TouK/nussknacker/pull/4322) `StateStatus` is identified by its name. `ProcessState` serialization uses this name as serialized state value. For compatibility reasons, it is still represented as a nested object with one `name` field. * [#4312](https://github.com/TouK/nussknacker/pull/4312) Fix for losing unsaved changes in designer after cancel/deploy * [#4332](https://github.com/TouK/nussknacker/pull/4332) Improvements: Don't fetch state for fragments at /api/processes/status @@ -901,7 +902,7 @@ * [#3264](https://github.com/TouK/nussknacker/pull/3264) Added support for generic functions * [#3253](https://github.com/TouK/nussknacker/pull/3253) Separate validation step during scenario deployment * [#3328](https://github.com/TouK/nussknacker/pull/3328) Schema type aware serialization of `NkSerializableParsedSchema` -* [#3071](https://github.com/TouK/nussknacker/pull/3071) [3379](https://github.com/TouK/nussknacker/pull/3379) More strict Avro schema validation: include optional fields validation, +* [#3071](https://github.com/TouK/nussknacker/pull/3071) [#3379](https://github.com/TouK/nussknacker/pull/3379) More strict Avro schema validation: include optional fields validation, handling some invalid cases like putting long to int field, strict union types validation, reduced number of validation modes to lax | strict. * [#3289](https://github.com/TouK/nussknacker/pull/3289) Handle asynchronous deployment and status checks better * [#3071](https://github.com/TouK/nussknacker/pull/3334) Improvements: Allow to import file with different id diff --git a/docs/MigrationGuide.md b/docs/MigrationGuide.md index 24ae0dbbc3d..305f7dabe8e 100644 --- a/docs/MigrationGuide.md +++ b/docs/MigrationGuide.md @@ -6,11 +6,11 @@ To see the biggest differences please consult the [changelog](Changelog.md). ### Configuration changes -* [6944](https://github.com/TouK/nussknacker/pull/6944) +* [#6944](https://github.com/TouK/nussknacker/pull/6944) * Button name for 'test adhoc' was renamed from `test-with-form` to `adhoc-testing` If you are using custom button config remember to update button type to `type: "adhoc-testing"` in `processToolbarConfig` -* [7039](https://github.com/TouK/nussknacker/pull/7039) +* [#7039](https://github.com/TouK/nussknacker/pull/7039) * Scenario Activity audit log is available * logger name, `scenario-activity-audit`, it is optional, does not have to be configured * it uses MDC context, example of configuration in `logback.xml`: @@ -26,6 +26,8 @@ To see the biggest differences please consult the [changelog](Changelog.md). ``` +* [#6979](https://github.com/TouK/nussknacker/pull/6979) Add `type: "activities-panel"` to the `processToolbarConfig` which replaces removed `{ type: "versions-panel" }` `{ type: "comments-panel" }` and `{ type: "attachments-panel" }` + ### Code API changes * [#6971](https://github.com/TouK/nussknacker/pull/6971) `DeploymentManagerDependencies` API changes: @@ -47,7 +49,7 @@ To see the biggest differences please consult the [changelog](Changelog.md). ### REST API changes -* [6944](https://github.com/TouK/nussknacker/pull/6944) +* [#6944](https://github.com/TouK/nussknacker/pull/6944) * New endpoint `/api/scenarioTesting/{scenarioName}/adhoc/validate` * [#6766](https://github.com/TouK/nussknacker/pull/6766) * Process API changes: @@ -93,8 +95,10 @@ To see the biggest differences please consult the [changelog](Changelog.md). * [#7113](https://github.com/TouK/nussknacker/pull/7113) Scala 2.13 was updated to 2.13.15, you should update your `flink-scala-2.13` to 1.1.2 -### Configuration changes -* [#6979](https://github.com/TouK/nussknacker/pull/6979) Add `type: "activities-panel"` to the `processToolbarConfig` which replaces removed `{ type: "versions-panel" }` `{ type: "comments-panel" }` and `{ type: "attachments-panel" }` +* [#7187](https://github.com/TouK/nussknacker/pull/7187) JSON decoding in `request` source (request-response processing mode) + and in `kafka` source (streaming processing mode): For small decimal numbers is used either `Integer` or `Long` (depending on number size) + instead of `BigDecimal`. This change should be transparent in most cases as this value was mostly used after `#CONV.toNumber()` invocation + which still will return a `Number`. ## In version 1.17.0 @@ -103,7 +107,7 @@ To see the biggest differences please consult the [changelog](Changelog.md). * [#6248](https://github.com/TouK/nussknacker/pull/6248) Removed implicit conversion from string to SpeL expression (`pl.touk.nussknacker.engine.spel.Implicits`). The conversion should be replaced by `pl.touk.nussknacker.engine.spel.SpelExtension.SpelExpresion.spel`. -* [6282](https://github.com/TouK/nussknacker/pull/6184) If you relied on the default value of the `topicsExistenceValidationConfig.enabled` +* [#6282](https://github.com/TouK/nussknacker/pull/6184) If you relied on the default value of the `topicsExistenceValidationConfig.enabled` setting, you must now be aware that topics will be validated by default (Kafka's `auto.create.topics.enable` setting is only considered in case of Sinks). Create proper topics manually if needed. * Component's API changes @@ -1088,7 +1092,7 @@ To see the biggest differences please consult the [changelog](Changelog.md). * Some methods from API classes (e.g. `Parameter.validate`) and classes (`InterpretationResult`) moved to interpreter * `DeploymentManagerProvider.createDeploymentManager` takes now `BaseModelData` as an argument instead of `ModelData`. If you want to use this data to invoke scenario, you should cast it to invokable representation via: `import ModelData._; modelData.asInvokableModelData` -* [#2878](https://github.com/TouK/nussknacker/pull/2878) [2898](https://github.com/TouK/nussknacker/pull/2898) [#2924](https://github.com/TouK/nussknacker/pull/2924) Cleaning up of `-utils` modules +* [#2878](https://github.com/TouK/nussknacker/pull/2878) [#2898](https://github.com/TouK/nussknacker/pull/2898) [#2924](https://github.com/TouK/nussknacker/pull/2924) Cleaning up of `-utils` modules * Extracted internal classes, not intended to be used in extensions to nussknacker-internal-utils module * Extracted component classes, not used directly by runtime/designer to nussknacker-components-utils module * Extracted kafka component classes, not used directly by lite-kafka-runtime/kafka-test-utils to nussknacker-kafka-components-utils @@ -1307,7 +1311,7 @@ may cause __runtime__ consequences - make sure your custom services/listeners in * [#2501](https://github.com/TouK/nussknacker/pull/2501) `nussknacker-baseengine-components` module renamed to `nussknacker-lite-base-components` * [#2221](https://github.com/TouK/nussknacker/pull/2221) ReflectUtils `fixedClassSimpleNameWithoutParentModule` renamed to `simpleNameWithoutSuffix` * [#2495](https://github.com/TouK/nussknacker/pull/2495) TypeSpecificDataInitializer trait change to TypeSpecificDataInitializ -* [2245](https://github.com/TouK/nussknacker/pull/2245) `FailedEvent` has been specified in `FailedOnDeployEvent` and `FailedOnRunEvent` +* [#2245](https://github.com/TouK/nussknacker/pull/2245) `FailedEvent` has been specified in `FailedOnDeployEvent` and `FailedOnRunEvent` ## In version 1.0.0 * [#1439](https://github.com/TouK/nussknacker/pull/1439) [#2090](https://github.com/TouK/nussknacker/pull/2090) Upgrade do Flink 1.13. diff --git a/engine/lite/components/request-response/src/main/scala/pl/touk/nussknacker/engine/lite/components/requestresponse/jsonschema/sources/JsonSchemaRequestResponseSource.scala b/engine/lite/components/request-response/src/main/scala/pl/touk/nussknacker/engine/lite/components/requestresponse/jsonschema/sources/JsonSchemaRequestResponseSource.scala index c937e3c0a42..c12d347e4f6 100644 --- a/engine/lite/components/request-response/src/main/scala/pl/touk/nussknacker/engine/lite/components/requestresponse/jsonschema/sources/JsonSchemaRequestResponseSource.scala +++ b/engine/lite/components/request-response/src/main/scala/pl/touk/nussknacker/engine/lite/components/requestresponse/jsonschema/sources/JsonSchemaRequestResponseSource.scala @@ -13,7 +13,7 @@ import pl.touk.nussknacker.engine.api.{CirceUtil, MetaData, NodeId} import pl.touk.nussknacker.engine.json.{JsonSchemaBasedParameter, SwaggerBasedJsonSchemaTypeDefinitionExtractor} import pl.touk.nussknacker.engine.json.serde.CirceJsonDeserializer import pl.touk.nussknacker.engine.json.swagger.SwaggerTyped -import pl.touk.nussknacker.engine.json.swagger.extractor.FromJsonDecoder +import pl.touk.nussknacker.engine.json.swagger.decode.FromJsonSchemaBasedDecoder import pl.touk.nussknacker.engine.lite.components.requestresponse.jsonschema.sinks.JsonRequestResponseSink.SinkRawValueParamName import pl.touk.nussknacker.engine.requestresponse.api.openapi.OpenApiSourceDefinition import pl.touk.nussknacker.engine.requestresponse.api.{RequestResponsePostSource, ResponseEncoder} @@ -94,7 +94,7 @@ class JsonSchemaRequestResponseSource( val json = ToJsonEncoder.defaultForTests.encode(paramValue) val schema = getFirstMatchingSchemaForJson(cs, json) val swaggerTyped: SwaggerTyped = SwaggerBasedJsonSchemaTypeDefinitionExtractor.swaggerType(schema) - FromJsonDecoder.decode(json, swaggerTyped) + FromJsonSchemaBasedDecoder.decode(json, swaggerTyped) } .getOrElse { throw new IllegalArgumentException( // Should never happen since CombinedSchema is created using SinkRawValueParamName but still... @@ -105,7 +105,7 @@ class JsonSchemaRequestResponseSource( case _ => val json = ToJsonEncoder.defaultForTests.encode(TestingParametersSupport.unflattenParameters(params)) val swaggerTyped: SwaggerTyped = SwaggerBasedJsonSchemaTypeDefinitionExtractor.swaggerType(inputSchema) - FromJsonDecoder.decode(json, swaggerTyped) + FromJsonSchemaBasedDecoder.decode(json, swaggerTyped) } } diff --git a/utils/default-helpers/src/main/scala/pl/touk/nussknacker/engine/util/functions/conversion.scala b/utils/default-helpers/src/main/scala/pl/touk/nussknacker/engine/util/functions/conversion.scala index ca259e6e15e..de8998f4cc9 100644 --- a/utils/default-helpers/src/main/scala/pl/touk/nussknacker/engine/util/functions/conversion.scala +++ b/utils/default-helpers/src/main/scala/pl/touk/nussknacker/engine/util/functions/conversion.scala @@ -2,9 +2,10 @@ package pl.touk.nussknacker.engine.util.functions import com.github.ghik.silencer.silent import pl.touk.nussknacker.engine.api.generics.GenericType +import pl.touk.nussknacker.engine.api.json.FromJsonDecoder import pl.touk.nussknacker.engine.api.{Documentation, HideToString, ParamName} import pl.touk.nussknacker.engine.util.functions.NumericUtils.ToNumberTypingFunction -import pl.touk.nussknacker.engine.util.json.{JsonUtils, ToJsonEncoder} +import pl.touk.nussknacker.engine.util.json.ToJsonEncoder object conversion extends ConversionUtils @@ -41,7 +42,7 @@ trait ConversionUtils extends HideToString { private def toJsonEither(value: String): Either[Throwable, Any] = { io.circe.parser.parse(value) match { - case Right(json) => Right(JsonUtils.jsonToAny(json)) + case Right(json) => Right(FromJsonDecoder.jsonToAny(json)) case Left(ex) => Left(new IllegalArgumentException(s"Cannot convert [$value] to JSON", ex)) } } diff --git a/utils/default-helpers/src/test/scala/pl/touk/nussknacker/engine/util/functions/ConversionUtilsSpec.scala b/utils/default-helpers/src/test/scala/pl/touk/nussknacker/engine/util/functions/ConversionUtilsSpec.scala index 56dc8e237ce..b4c9f4ce5ff 100644 --- a/utils/default-helpers/src/test/scala/pl/touk/nussknacker/engine/util/functions/ConversionUtilsSpec.scala +++ b/utils/default-helpers/src/test/scala/pl/touk/nussknacker/engine/util/functions/ConversionUtilsSpec.scala @@ -16,17 +16,18 @@ class ConversionUtilsSpec extends AnyFunSuite with BaseSpelSpec with Matchers { ("expression", "expected"), ("#CONV.toJson('null')", null), ("#CONV.toJson('\"str\"')", "str"), - ("#CONV.toJson('1')", JBigDecimal.valueOf(1)), + ("#CONV.toJson('1')", 1), + ("#CONV.toJson('1.234')", JBigDecimal.valueOf(1.234)), ("#CONV.toJson('true')", true), ("#CONV.toJson('[]')", JList.of()), ("#CONV.toJson('[{}]')", JList.of[JMap[Nothing, Nothing]](JMap.of())), - ("#CONV.toJson('[1, \"str\", true]')", JList.of(JBigDecimal.valueOf(1), "str", true)), + ("#CONV.toJson('[1, \"str\", true]')", JList.of(1, "str", true)), ("#CONV.toJson('{}')", JMap.of()), ( "#CONV.toJson('{ \"a\": 1, \"b\": true, \"c\": \"str\", \"d\": [], \"e\": {} }')", JMap.of( "a", - JBigDecimal.valueOf(1), + 1, "b", true, "c", diff --git a/utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/serde/CirceJsonDeserializer.scala b/utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/serde/CirceJsonDeserializer.scala index 182ad9f2ddf..940cca1dcf3 100644 --- a/utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/serde/CirceJsonDeserializer.scala +++ b/utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/serde/CirceJsonDeserializer.scala @@ -5,7 +5,7 @@ import org.json.JSONTokener import pl.touk.nussknacker.engine.api.typed.CustomNodeValidationException import pl.touk.nussknacker.engine.json.SwaggerBasedJsonSchemaTypeDefinitionExtractor import pl.touk.nussknacker.engine.json.swagger.SwaggerTyped -import pl.touk.nussknacker.engine.json.swagger.extractor.FromJsonDecoder +import pl.touk.nussknacker.engine.json.swagger.decode.FromJsonSchemaBasedDecoder import pl.touk.nussknacker.engine.util.json.JsonSchemaUtils import java.nio.charset.StandardCharsets @@ -36,7 +36,7 @@ class CirceJsonDeserializer(jsonSchema: Schema) { .valueOr(errorMsg => throw CustomNodeValidationException(errorMsg, None)) val circeJson = JsonSchemaUtils.jsonToCirce(validatedJson) - val struct = FromJsonDecoder.decode(circeJson, swaggerTyped) + val struct = FromJsonSchemaBasedDecoder.decode(circeJson, swaggerTyped) struct } 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 cc876093962..8c8803d4a46 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 @@ -10,7 +10,7 @@ import io.swagger.v3.oas.models.media.{ArraySchema, MapSchema, ObjectSchema, Sch import pl.touk.nussknacker.engine.api.typed.typing._ import pl.touk.nussknacker.engine.json.swagger.parser.{PropertyName, SwaggerRefSchemas} import pl.touk.nussknacker.engine.util.Implicits.RichScalaMap -import pl.touk.nussknacker.engine.util.json.JsonUtils.jsonToAny +import pl.touk.nussknacker.engine.api.json.FromJsonDecoder.jsonToAny import pl.touk.nussknacker.engine.util.json.ToJsonEncoder import java.time.{LocalDate, LocalTime, ZonedDateTime} diff --git a/utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/swagger/extractor/FromJsonDecoder.scala b/utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/swagger/decode/FromJsonSchemaBasedDecoder.scala similarity index 83% rename from utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/swagger/extractor/FromJsonDecoder.scala rename to utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/swagger/decode/FromJsonSchemaBasedDecoder.scala index 38eff47a380..0b241b40564 100644 --- a/utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/swagger/extractor/FromJsonDecoder.scala +++ b/utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/swagger/decode/FromJsonSchemaBasedDecoder.scala @@ -1,16 +1,16 @@ -package pl.touk.nussknacker.engine.json.swagger.extractor +package pl.touk.nussknacker.engine.json.swagger.decode import io.circe.{Json, JsonNumber, JsonObject} +import pl.touk.nussknacker.engine.api.json.FromJsonDecoder import pl.touk.nussknacker.engine.api.typed.TypedMap import pl.touk.nussknacker.engine.json.swagger._ -import pl.touk.nussknacker.engine.util.json.JsonUtils.jsonToAny import java.time.format.DateTimeFormatter import java.time.{LocalDate, OffsetTime, ZonedDateTime} import scala.util.Try // TODO: Validated -object FromJsonDecoder { +object FromJsonSchemaBasedDecoder { import scala.jdk.CollectionConverters._ @@ -41,16 +41,16 @@ object FromJsonDecoder { TypedMap( jo.toMap.collect { case (key, value) if obj.elementType.contains(key) => - key -> FromJsonDecoder.decode(value, obj.elementType(key), addPath(key)) + key -> FromJsonSchemaBasedDecoder.decode(value, obj.elementType(key), addPath(key)) case keyValue @ KeyMatchingPatternSchema(patternPropertySchema) => val (key, value) = keyValue - key -> FromJsonDecoder.decode(value, patternPropertySchema, addPath(key)) + key -> FromJsonSchemaBasedDecoder.decode(value, patternPropertySchema, addPath(key)) case (key, value) if obj.additionalProperties != AdditionalPropertiesDisabled => obj.additionalProperties match { case add: AdditionalPropertiesEnabled => - key -> FromJsonDecoder.decode(value, add.value, addPath(key)) + key -> FromJsonSchemaBasedDecoder.decode(value, add.value, addPath(key)) case _ => - key -> jsonToAny(value) + key -> FromJsonDecoder.jsonToAny(value) } } ) @@ -65,7 +65,7 @@ object FromJsonDecoder { case SwaggerString => extract(_.asString) case SwaggerEnum(_) => - extract[AnyRef](j => Option(jsonToAny(j).asInstanceOf[AnyRef])) + extract[AnyRef](j => Option(FromJsonDecoder.jsonToAny(j).asInstanceOf[AnyRef])) case SwaggerBool => extract(_.asBoolean, boolean2Boolean) case SwaggerInteger => @@ -88,7 +88,9 @@ object FromJsonDecoder { case SwaggerArray(elementType) => extract[Vector[Json]]( _.asArray, - _.zipWithIndex.map { case (el, idx) => FromJsonDecoder.decode(el, elementType, s"$path[$idx]") }.asJava + _.zipWithIndex + .map { case (el, idx) => FromJsonSchemaBasedDecoder.decode(el, elementType, s"$path[$idx]") } + .asJava ) case obj: SwaggerObject => extractObject(obj) case u @ SwaggerUnion(types) => @@ -96,7 +98,7 @@ object FromJsonDecoder { .flatMap(aType => Try(decode(json, aType)).toOption) .headOption .getOrElse(throw JsonToObjectError(json, u, path)) - case SwaggerAny => extract[AnyRef](j => Option(jsonToAny(j).asInstanceOf[AnyRef])) + case SwaggerAny => extract[AnyRef](j => Option(FromJsonDecoder.jsonToAny(j).asInstanceOf[AnyRef])) // should not happen as we handle null above case SwaggerNull => throw JsonToObjectError(json, definition, path) } diff --git a/utils/json-utils/src/test/scala/pl/touk/nussknacker/engine/json/SwaggerBasedJsonSchemaTypeDefinitionExtractorTest.scala b/utils/json-utils/src/test/scala/pl/touk/nussknacker/engine/json/SwaggerBasedJsonSchemaTypeDefinitionExtractorTest.scala index 09acfb8b62a..a9aaa197edf 100644 --- a/utils/json-utils/src/test/scala/pl/touk/nussknacker/engine/json/SwaggerBasedJsonSchemaTypeDefinitionExtractorTest.scala +++ b/utils/json-utils/src/test/scala/pl/touk/nussknacker/engine/json/SwaggerBasedJsonSchemaTypeDefinitionExtractorTest.scala @@ -7,7 +7,7 @@ import org.scalatest.matchers.should.Matchers.convertToAnyShouldWrapper import org.scalatest.prop.TableDrivenPropertyChecks import pl.touk.nussknacker.engine.api.typed.TypedMap import pl.touk.nussknacker.engine.api.typed.typing.{Typed, TypedObjectTypingResult, TypingResult, Unknown} -import pl.touk.nussknacker.engine.json.swagger.extractor.FromJsonDecoder +import pl.touk.nussknacker.engine.json.swagger.decode.FromJsonSchemaBasedDecoder import pl.touk.nussknacker.engine.json.swagger._ class SwaggerBasedJsonSchemaTypeDefinitionExtractorTest extends AnyFunSuite with TableDrivenPropertyChecks { @@ -304,7 +304,7 @@ class SwaggerBasedJsonSchemaTypeDefinitionExtractorTest extends AnyFunSuite with val jsonObject = Json.obj("time" -> fromString("2022-07-11T18:12:27+02:00")) val swaggerObject = new SwaggerObject(elementType = Map("time" -> SwaggerDateTime), AdditionalPropertiesDisabled) - val jsonToObjectExtracted = FromJsonDecoder.decode(jsonObject, swaggerObject) + val jsonToObjectExtracted = FromJsonSchemaBasedDecoder.decode(jsonObject, swaggerObject) swaggerTypeExtracted.asInstanceOf[TypedObjectTypingResult].fields("time") shouldBe Typed.fromInstance(jsonToObjectExtracted.asInstanceOf[TypedMap].get("time")) diff --git a/utils/json-utils/src/test/scala/pl/touk/nussknacker/engine/json/serde/CirceJsonDeserializerSpec.scala b/utils/json-utils/src/test/scala/pl/touk/nussknacker/engine/json/serde/CirceJsonDeserializerSpec.scala index 6374f1383af..8cf95710325 100644 --- a/utils/json-utils/src/test/scala/pl/touk/nussknacker/engine/json/serde/CirceJsonDeserializerSpec.scala +++ b/utils/json-utils/src/test/scala/pl/touk/nussknacker/engine/json/serde/CirceJsonDeserializerSpec.scala @@ -284,7 +284,7 @@ class CirceJsonDeserializerSpec extends AnyFunSuite with ValidatedValuesDetailed |}""".stripMargin) result shouldEqual Map( - "additionalInt" -> java.math.BigDecimal.valueOf(1234), + "additionalInt" -> 1234, "additionalString" -> "foo", "additionalObject" -> Map("foo" -> "bar").asJava ).asJava diff --git a/utils/json-utils/src/test/scala/pl/touk/nussknacker/engine/json/swagger/extractor/FromJsonDecoderTest.scala b/utils/json-utils/src/test/scala/pl/touk/nussknacker/engine/json/swagger/decode/FromJsonSchemaBasedDecoderTest.scala similarity index 84% rename from utils/json-utils/src/test/scala/pl/touk/nussknacker/engine/json/swagger/extractor/FromJsonDecoderTest.scala rename to utils/json-utils/src/test/scala/pl/touk/nussknacker/engine/json/swagger/decode/FromJsonSchemaBasedDecoderTest.scala index ba8a497bb43..6f7ae4301f0 100644 --- a/utils/json-utils/src/test/scala/pl/touk/nussknacker/engine/json/swagger/extractor/FromJsonDecoderTest.scala +++ b/utils/json-utils/src/test/scala/pl/touk/nussknacker/engine/json/swagger/decode/FromJsonSchemaBasedDecoderTest.scala @@ -1,4 +1,4 @@ -package pl.touk.nussknacker.engine.json.swagger.extractor +package pl.touk.nussknacker.engine.json.swagger.decode import io.circe.Json import io.circe.Json.{fromBoolean, fromInt, fromLong, fromString, fromValues} @@ -6,12 +6,12 @@ import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers import pl.touk.nussknacker.engine.api.typed.TypedMap import pl.touk.nussknacker.engine.json.swagger._ -import pl.touk.nussknacker.engine.json.swagger.extractor.FromJsonDecoder.JsonToObjectError +import pl.touk.nussknacker.engine.json.swagger.decode.FromJsonSchemaBasedDecoder.JsonToObjectError import java.time.format.DateTimeFormatter import java.time.{LocalDate, OffsetTime, ZoneOffset, ZonedDateTime} -class FromJsonDecoderTest extends AnyFunSuite with Matchers { +class FromJsonSchemaBasedDecoderTest extends AnyFunSuite with Matchers { private val json = Json.obj( "field1" -> fromString("value"), @@ -46,7 +46,7 @@ class FromJsonDecoderTest extends AnyFunSuite with Matchers { AdditionalPropertiesDisabled ) - val value = FromJsonDecoder.decode(json, definition) + val value = FromJsonSchemaBasedDecoder.decode(json, definition) value shouldBe a[TypedMap] val fields = value.asInstanceOf[TypedMap] @@ -62,7 +62,7 @@ class FromJsonDecoderTest extends AnyFunSuite with Matchers { fields.get("nullField").asInstanceOf[AnyRef] shouldBe null val mapField = fields.get("mapField").asInstanceOf[TypedMap] mapField.get("a") shouldBe "1" - mapField.get("b") shouldBe java.math.BigDecimal.valueOf(2) + mapField.get("b") shouldBe 2 mapField.get("c") shouldBe a[java.util.List[_]] fields.get("mapOfStringsField") shouldBe a[TypedMap] } @@ -73,7 +73,7 @@ class FromJsonDecoderTest extends AnyFunSuite with Matchers { AdditionalPropertiesDisabled ) - val ex = intercept[JsonToObjectError](FromJsonDecoder.decode(json, definition)) + val ex = intercept[JsonToObjectError](FromJsonSchemaBasedDecoder.decode(json, definition)) ex.getMessage shouldBe "JSON returned by service has invalid type at mapField.b. Expected: SwaggerString. Returned json: 2" ex.path shouldBe "mapField.b" @@ -82,27 +82,27 @@ class FromJsonDecoderTest extends AnyFunSuite with Matchers { test("should skip additionalFields when schema/SwaggerObject does not allow them") { val definitionWithoutFields = SwaggerObject(elementType = Map("field3" -> SwaggerLong), AdditionalPropertiesDisabled) - extractor.FromJsonDecoder.decode(json, definitionWithoutFields) shouldBe TypedMap(Map.empty) + decode.FromJsonSchemaBasedDecoder.decode(json, definitionWithoutFields) shouldBe TypedMap(Map.empty) val definitionWithOneField = SwaggerObject(elementType = Map("field2" -> SwaggerLong), AdditionalPropertiesDisabled) - extractor.FromJsonDecoder.decode(json, definitionWithOneField) shouldBe TypedMap(Map("field2" -> 1L)) + decode.FromJsonSchemaBasedDecoder.decode(json, definitionWithOneField) shouldBe TypedMap(Map("field2" -> 1L)) } test("should not trim additional fields fields when additionalPropertiesOn") { val json = Json.obj("field1" -> fromString("value"), "field2" -> Json.fromInt(1)) val definition = SwaggerObject(elementType = Map("field3" -> SwaggerLong)) - extractor.FromJsonDecoder.decode(json, definition) shouldBe TypedMap( + decode.FromJsonSchemaBasedDecoder.decode(json, definition) shouldBe TypedMap( Map( "field1" -> "value", - "field2" -> java.math.BigDecimal.valueOf(1) + "field2" -> 1 ) ) val jsonIntegers = Json.obj("field1" -> fromInt(2), "field2" -> fromInt(1)) val definition2 = SwaggerObject(elementType = Map("field3" -> SwaggerLong), AdditionalPropertiesEnabled(SwaggerLong)) - extractor.FromJsonDecoder.decode(jsonIntegers, definition2) shouldBe TypedMap( + decode.FromJsonSchemaBasedDecoder.decode(jsonIntegers, definition2) shouldBe TypedMap( Map( "field1" -> 2L, "field2" -> 1L @@ -115,7 +115,7 @@ class FromJsonDecoderTest extends AnyFunSuite with Matchers { val definition = SwaggerObject(elementType = Map("field3" -> SwaggerLong), AdditionalPropertiesEnabled(SwaggerLong)) val ex = intercept[JsonToObjectError] { - extractor.FromJsonDecoder.decode(json, definition) + decode.FromJsonSchemaBasedDecoder.decode(json, definition) } ex.getMessage shouldBe """JSON returned by service has invalid type at field1. Expected: SwaggerLong. Returned json: "value"""" @@ -125,7 +125,7 @@ class FromJsonDecoderTest extends AnyFunSuite with Matchers { val definition = SwaggerObject(elementType = Map("field2" -> SwaggerUnion(List(SwaggerString, SwaggerLong)))) - val value = FromJsonDecoder.decode(json, definition) + val value = FromJsonSchemaBasedDecoder.decode(json, definition) value shouldBe a[TypedMap] val fields = value.asInstanceOf[TypedMap] @@ -143,7 +143,7 @@ class FromJsonDecoderTest extends AnyFunSuite with Matchers { ) def assertPath(json: Json, path: String) = - intercept[JsonToObjectError](FromJsonDecoder.decode(json, definition)).path shouldBe path + intercept[JsonToObjectError](FromJsonSchemaBasedDecoder.decode(json, definition)).path shouldBe path assertPath(Json.obj("string" -> fromLong(1)), "string") assertPath( diff --git a/utils/utils/src/main/scala/pl/touk/nussknacker/engine/util/json/JsonUtils.scala b/utils/utils/src/main/scala/pl/touk/nussknacker/engine/util/json/JsonUtils.scala deleted file mode 100644 index bbf9f401084..00000000000 --- a/utils/utils/src/main/scala/pl/touk/nussknacker/engine/util/json/JsonUtils.scala +++ /dev/null @@ -1,18 +0,0 @@ -package pl.touk.nussknacker.engine.util.json - -import io.circe.Json -import pl.touk.nussknacker.engine.util.Implicits._ -import scala.jdk.CollectionConverters._ - -object JsonUtils { - - def jsonToAny(json: Json): Any = json.fold( - jsonNull = null, - jsonBoolean = identity[Boolean], - jsonNumber = _.toBigDecimal.map(_.bigDecimal).orNull, // we need here java BigDecimal type - jsonString = identity[String], - jsonArray = _.map(jsonToAny).asJava, - jsonObject = _.toMap.mapValuesNow(jsonToAny).asJava - ) - -} From 7aa80a89afa9f88e3dd28fe96e5d5dfc22ac2d8e Mon Sep 17 00:00:00 2001 From: Dawid Poliszak Date: Thu, 21 Nov 2024 14:03:59 +0100 Subject: [PATCH 5/5] [Nu-1892] add missing tooltips (#7193) * NU-1892 add missing tooltips --- .../toolbars/scenarioDetails/CategoryDetails.tsx | 8 +++++++- .../scenarioDetails/ScenarioDetailsComponents.tsx | 5 +++++ .../toolbars/scenarioDetails/ScenarioLabels.tsx | 5 +++++ .../packages/components/src/common/categoryChip.tsx | 3 +++ .../packages/components/src/common/labelChip.tsx | 3 +++ .../components/src/scenarios/list/processingMode.tsx | 3 +++ .../components/src/scenarios/list/scenarioAvatar.tsx | 7 ++++++- .../components/src/scenarios/list/scenarioStatus.tsx | 1 + .../components/src/scenarios/list/tableCellAvatar.tsx | 3 ++- docs/Changelog.md | 1 + 10 files changed, 36 insertions(+), 3 deletions(-) diff --git a/designer/client/src/components/toolbars/scenarioDetails/CategoryDetails.tsx b/designer/client/src/components/toolbars/scenarioDetails/CategoryDetails.tsx index 60b2d1b3b3b..07be4f6473d 100644 --- a/designer/client/src/components/toolbars/scenarioDetails/CategoryDetails.tsx +++ b/designer/client/src/components/toolbars/scenarioDetails/CategoryDetails.tsx @@ -3,8 +3,10 @@ import { useProcessFormDataOptions } from "../../useProcessFormDataOptions"; import HttpService, { ScenarioParametersCombination } from "../../../http/HttpService"; import { Skeleton, Typography } from "@mui/material"; import { Scenario } from "../../Process/types"; +import { useTranslation } from "react-i18next"; export const CategoryDetails = ({ scenario }: { scenario: Scenario }) => { + const { t } = useTranslation(); const [allCombinations, setAllCombinations] = useState([]); const [isAllCombinationsLoading, setIsAllCombinationsLoading] = useState(false); @@ -33,7 +35,11 @@ export const CategoryDetails = ({ scenario }: { scenario: Scenario }) => { {isAllCombinationsLoading ? ( ) : ( - isCategoryFieldVisible && {scenario.processCategory} / + isCategoryFieldVisible && ( + + {scenario.processCategory} / + + ) )} ); diff --git a/designer/client/src/components/toolbars/scenarioDetails/ScenarioDetailsComponents.tsx b/designer/client/src/components/toolbars/scenarioDetails/ScenarioDetailsComponents.tsx index 21810a99560..1693c49d114 100644 --- a/designer/client/src/components/toolbars/scenarioDetails/ScenarioDetailsComponents.tsx +++ b/designer/client/src/components/toolbars/scenarioDetails/ScenarioDetailsComponents.tsx @@ -1,4 +1,5 @@ import { css, styled, Typography } from "@mui/material"; +import i18next from "i18next"; export const PanelScenarioDetails = styled("div")( ({ theme }) => css` @@ -26,6 +27,10 @@ export const ScenarioDetailsItemWrapper = styled("div")( export const ProcessName = styled(Typography)``; +ProcessName.defaultProps = { + title: i18next.t("panels.scenarioDetails.tooltip.name", "Name"), +}; + export const ProcessRename = styled(ProcessName)(({ theme }) => ({ color: theme.palette.warning.main, })); diff --git a/designer/client/src/components/toolbars/scenarioDetails/ScenarioLabels.tsx b/designer/client/src/components/toolbars/scenarioDetails/ScenarioLabels.tsx index 06b5c7d5d50..6126f30b30c 100644 --- a/designer/client/src/components/toolbars/scenarioDetails/ScenarioLabels.tsx +++ b/designer/client/src/components/toolbars/scenarioDetails/ScenarioLabels.tsx @@ -21,6 +21,7 @@ import i18next from "i18next"; import { editScenarioLabels } from "../../../actions/nk"; import { debounce } from "lodash"; import { ScenarioLabelValidationError } from "../../Labels/types"; +import { useTranslation } from "react-i18next"; interface AddLabelProps { onClick: () => void; @@ -107,6 +108,7 @@ interface Props { } export const ScenarioLabels = ({ readOnly }: Props) => { + const { t } = useTranslation(); const scenarioLabels = useSelector(getScenarioLabels); const scenarioLabelOptions: LabelOption[] = useMemo(() => scenarioLabels.map(toLabelOption), [scenarioLabels]); const initialScenarioLabelOptionsErrors = useSelector(getScenarioLabelsErrors).filter((error) => @@ -352,6 +354,9 @@ export const ScenarioLabels = ({ readOnly }: Props) => { const labelError = labelOptionsErrors.find((error) => error.label === toLabelValue(option)); return ( filterValues.includes(category), [filterValues, category]); const onClick = useCallback( @@ -36,6 +38,7 @@ export function CategoryButton({ category, filterValues, setFilter }: Props): JS return ( filterValue.includes(value), [filterValue, value]); const onClick = useCallback( @@ -26,6 +28,7 @@ export function LabelChip({ id, value, filterValue, setFilter }: Props): JSX.Ele return ( ; } export const ProcessingModeItem = ({ processingMode, filtersContext }: Props) => { + const { t } = useTranslation(); const { setFilter, getFilter } = filtersContext; const filterValue = useMemo(() => getFilter("PROCESSING_MODE", true), [getFilter]); @@ -58,6 +60,7 @@ export const ProcessingModeItem = ({ processingMode, filtersContext }: Props) => return (