diff --git a/.mapping.json b/.mapping.json index fe00d6576..cbeca6c57 100644 --- a/.mapping.json +++ b/.mapping.json @@ -10315,8 +10315,7 @@ "client/ios/DivKit.xcodeproj/xcshareddata/xcschemes/SnapshotTests.xcscheme":"divkit/public/client/ios/DivKit.xcodeproj/xcshareddata/xcschemes/SnapshotTests.xcscheme", "client/ios/DivKit.xcodeproj/xcshareddata/xcschemes/SnapshotTestsHostApp.xcscheme":"divkit/public/client/ios/DivKit.xcodeproj/xcshareddata/xcschemes/SnapshotTestsHostApp.xcscheme", "client/ios/DivKit.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme":"divkit/public/client/ios/DivKit.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme", - "client/ios/DivKit/Actions/ArrayInsertValueActionHandler.swift":"divkit/public/client/ios/DivKit/Actions/ArrayInsertValueActionHandler.swift", - "client/ios/DivKit/Actions/ArrayRemoveValueActionHandler.swift":"divkit/public/client/ios/DivKit/Actions/ArrayRemoveValueActionHandler.swift", + "client/ios/DivKit/Actions/ArrayActionsHandler.swift":"divkit/public/client/ios/DivKit/Actions/ArrayActionsHandler.swift", "client/ios/DivKit/Actions/ClearFocusActionHandler.swift":"divkit/public/client/ios/DivKit/Actions/ClearFocusActionHandler.swift", "client/ios/DivKit/Actions/CopyToClipboardActionHandler.swift":"divkit/public/client/ios/DivKit/Actions/CopyToClipboardActionHandler.swift", "client/ios/DivKit/Actions/DictSetValueActionHandler.swift":"divkit/public/client/ios/DivKit/Actions/DictSetValueActionHandler.swift", @@ -10511,6 +10510,8 @@ "client/ios/DivKit/generated_sources/DivActionArrayInsertValueTemplate.swift":"divkit/public/client/ios/DivKit/generated_sources/DivActionArrayInsertValueTemplate.swift", "client/ios/DivKit/generated_sources/DivActionArrayRemoveValue.swift":"divkit/public/client/ios/DivKit/generated_sources/DivActionArrayRemoveValue.swift", "client/ios/DivKit/generated_sources/DivActionArrayRemoveValueTemplate.swift":"divkit/public/client/ios/DivKit/generated_sources/DivActionArrayRemoveValueTemplate.swift", + "client/ios/DivKit/generated_sources/DivActionArraySetValue.swift":"divkit/public/client/ios/DivKit/generated_sources/DivActionArraySetValue.swift", + "client/ios/DivKit/generated_sources/DivActionArraySetValueTemplate.swift":"divkit/public/client/ios/DivKit/generated_sources/DivActionArraySetValueTemplate.swift", "client/ios/DivKit/generated_sources/DivActionClearFocus.swift":"divkit/public/client/ios/DivKit/generated_sources/DivActionClearFocus.swift", "client/ios/DivKit/generated_sources/DivActionClearFocusTemplate.swift":"divkit/public/client/ios/DivKit/generated_sources/DivActionClearFocusTemplate.swift", "client/ios/DivKit/generated_sources/DivActionCopyToClipboard.swift":"divkit/public/client/ios/DivKit/generated_sources/DivActionCopyToClipboard.swift", @@ -15978,6 +15979,7 @@ "schema/div-accessibility.json":"divkit/public/schema/div-accessibility.json", "schema/div-action-array-insert-value.json":"divkit/public/schema/div-action-array-insert-value.json", "schema/div-action-array-remove-value.json":"divkit/public/schema/div-action-array-remove-value.json", + "schema/div-action-array-set-value.json":"divkit/public/schema/div-action-array-set-value.json", "schema/div-action-base.json":"divkit/public/schema/div-action-base.json", "schema/div-action-clear-focus.json":"divkit/public/schema/div-action-clear-focus.json", "schema/div-action-copy-to-clipboard-content.json":"divkit/public/schema/div-action-copy-to-clipboard-content.json", diff --git a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedArrayMutationHandler.kt b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedArrayMutationHandler.kt index a2123268c..b500df27f 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedArrayMutationHandler.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedArrayMutationHandler.kt @@ -6,6 +6,7 @@ import com.yandex.div.internal.util.asList import com.yandex.div.json.expressions.ExpressionResolver import com.yandex.div2.DivActionArrayInsertValue import com.yandex.div2.DivActionArrayRemoveValue +import com.yandex.div2.DivActionArraySetValue import com.yandex.div2.DivActionTyped import org.json.JSONArray import javax.inject.Inject @@ -20,21 +21,22 @@ internal class DivActionTypedArrayMutationHandler @Inject constructor() view: Div2View, resolver: ExpressionResolver, ): Boolean = when (action) { - is DivActionTyped.ArrayInsertValue -> { - handleInsert(action.value, view, resolver) + handle(action.value, view, resolver) true } - is DivActionTyped.ArrayRemoveValue -> { - handleRemove(action.value, view, resolver) + handle(action.value, view, resolver) + true + } + is DivActionTyped.ArraySetValue -> { + handle(action.value, view, resolver) true } - else -> false } - private fun handleInsert( + private fun handle( action: DivActionArrayInsertValue, view: Div2View, resolver: ExpressionResolver @@ -42,67 +44,91 @@ internal class DivActionTypedArrayMutationHandler @Inject constructor() val variableName = action.variableName.evaluate(resolver) val index = action.index?.evaluate(resolver)?.toInt() val newValue = action.value.evaluate(resolver) - view.setVariable(variableName) { variable: Variable -> - if (variable !is Variable.ArrayVariable) { - view.logError( - IllegalArgumentException("array_insert_value action requires array variable") - ) - return@setVariable variable + view.updateVariable(variableName) { array -> + val length = array.length() + when (index) { + null, length -> array.mutate { add(newValue) } + in 0 until length -> array.mutate { add(index, newValue) } + else -> { + view.logError(IndexOutOfBoundsException( + "Index out of bound ($index) for mutation $variableName ($length)" + )) + array + } } - - val value = variable.getValue() as? JSONArray - if (value == null) { - view.logError(IllegalArgumentException("Invalid variable value")) - return@setVariable variable - } - - val length = value.length() - when (val indexToInsert = index ?: length) { - in 0..length -> variable.set(value.mutate { add(indexToInsert, newValue) }) - else -> view.logError(IndexOutOfBoundsException( - "Index out of bound ($indexToInsert) for mutation ${variable.name} (${length})" - )) - } - return@setVariable variable } } - private fun handleRemove( + private fun handle( action: DivActionArrayRemoveValue, view: Div2View, resolver: ExpressionResolver ) { val variableName = action.variableName.evaluate(resolver) val index = action.index.evaluate(resolver).toInt() - view.setVariable(variableName) { variable: Variable -> - if (variable !is Variable.ArrayVariable) { - view.logError( - IllegalArgumentException("array_remove_value action requires array variable") - ) - return@setVariable variable - } - - val value = variable.getValue() as? JSONArray - if (value == null) { - view.logError(IllegalArgumentException("Invalid variable value")) - return@setVariable variable - } - - val length = value.length() + view.updateVariable(variableName) { array -> + val length = array.length() when (index) { - in 0 until length -> variable.set(value.mutate { removeAt(index) }) - else -> view.logError(IndexOutOfBoundsException( - "Index out of bound ($index) for mutation ${variable.name} (${length})" - )) + in 0 until length -> array.mutate { removeAt(index) } + else -> { + view.logError(IndexOutOfBoundsException( + "Index out of bound ($index) for mutation $variableName ($length)" + )) + array + } } - return@setVariable variable } } - private fun JSONArray.mutate(action: MutableList.() -> Unit): JSONArray { - return asList() - .toMutableList() - .apply(action::invoke) - .let(::JSONArray) + private fun handle( + action: DivActionArraySetValue, + view: Div2View, + resolver: ExpressionResolver + ) { + val variableName = action.variableName.evaluate(resolver) + val index = action.index.evaluate(resolver).toInt() + val newValue = action.value.evaluate(resolver) + view.updateVariable(variableName) { array -> + val length = array.length() + when (index) { + in 0 until length -> array.mutate { this[index] = newValue } + else -> { + view.logError(IndexOutOfBoundsException( + "Index out of bound ($index) for mutation $variableName ($length)" + )) + array + } + } + } + } +} + +private fun JSONArray.mutate(action: MutableList.() -> Unit): JSONArray { + return asList() + .toMutableList() + .apply(action::invoke) + .let(::JSONArray) +} + +private fun Div2View.updateVariable( + name: String, + valueMutation: (JSONArray) -> JSONArray +) { + view.setVariable(name) { variable: Variable -> + if (variable !is Variable.ArrayVariable) { + view.logError( + IllegalArgumentException("Action requires array variable") + ) + return@setVariable variable + } + + val value = variable.getValue() as? JSONArray + if (value == null) { + view.logError(IllegalArgumentException("Invalid variable value")) + return@setVariable variable + } + + variable.set(valueMutation(value)) + return@setVariable variable } } diff --git a/client/android/div/src/test/java/com/yandex/div/core/DivActionHandlerTest.kt b/client/android/div/src/test/java/com/yandex/div/core/DivActionHandlerTest.kt index b140d3b5a..4c74d42cf 100644 --- a/client/android/div/src/test/java/com/yandex/div/core/DivActionHandlerTest.kt +++ b/client/android/div/src/test/java/com/yandex/div/core/DivActionHandlerTest.kt @@ -12,6 +12,7 @@ import com.yandex.div2.Div import com.yandex.div2.DivAction import com.yandex.div2.DivActionArrayInsertValue import com.yandex.div2.DivActionArrayRemoveValue +import com.yandex.div2.DivActionArraySetValue import com.yandex.div2.DivActionDictSetValue import com.yandex.div2.DivActionSetVariable import com.yandex.div2.DivActionTyped @@ -184,6 +185,45 @@ class DivActionHandlerTest { ) } + @Test + fun `ArrayInsertValue action with index equal to length inserts value`() { + setVariable("array_var", JSONArray().put("value 1")) + + val isHandled = handleTypedAction( + DivActionTyped.ArrayInsertValue( + DivActionArrayInsertValue( + index = Expression.constant(1), + value = typedValue("new value"), + variableName = Expression.constant("array_var") + ) + ) + ) + + Assert.assertTrue(isHandled) + + Assert.assertEquals( + JSONArray().put("value 1").put("new value"), + getVariableValue("array_var") + ) + } + + @Test + fun `ArrayInsertValue action does not change original value`() { + val array = JSONArray().put("value 1").put("value 2") + setVariable("array_var", array) + + handleTypedAction( + DivActionTyped.ArrayInsertValue( + DivActionArrayInsertValue( + value = typedValue("new value"), + variableName = Expression.constant("array_var") + ) + ) + ) + + Assert.assertEquals(JSONArray().put("value 1").put("value 2"), array) + } + @Test fun `ArrayInsertValue action does nothing for invalid index`() { setVariable("array_var", JSONArray().put("value 1")) @@ -236,6 +276,23 @@ class DivActionHandlerTest { Assert.assertEquals(JSONArray().put("value 2"), getVariableValue("array_var")) } + @Test + fun `ArrayRemoveValue action does not change original array`() { + val array = JSONArray().put("value 1").put("value 2") + setVariable("array_var", array) + + handleTypedAction( + DivActionTyped.ArrayRemoveValue( + DivActionArrayRemoveValue( + index = Expression.constant(0), + variableName = Expression.constant("array_var") + ) + ) + ) + + Assert.assertEquals(JSONArray().put("value 1").put("value 2"), array) + } + @Test fun `ArrayRemoveValue action does nothing for invalid index`() { setVariable("array_var", JSONArray().put("value 1")) @@ -270,6 +327,86 @@ class DivActionHandlerTest { Assert.assertEquals("value", getVariableValue("string_var")) } + @Test + fun `ArraySetValue action sets value`() { + setVariable("array_var", JSONArray().put("value 1").put("value 2")) + + val isHandled = handleTypedAction( + DivActionTyped.ArraySetValue( + DivActionArraySetValue( + index = Expression.constant(1), + value = typedValue("new value"), + variableName = Expression.constant("array_var") + ) + ) + ) + + Assert.assertTrue(isHandled) + + Assert.assertEquals( + JSONArray().put("value 1").put("new value"), + getVariableValue("array_var") + ) + } + + @Test + fun `ArraySetValue action does not change original array`() { + val array = JSONArray().put("value 1").put("value 2") + setVariable("array_var", array) + + handleTypedAction( + DivActionTyped.ArraySetValue( + DivActionArraySetValue( + index = Expression.constant(1), + value = typedValue("new value"), + variableName = Expression.constant("array_var") + ) + ) + ) + + Assert.assertEquals(JSONArray().put("value 1").put("value 2"), array) + } + + @Test + fun `ArraySetValue action does nothing for invalid index`() { + setVariable("array_var", JSONArray().put("value 1").put("value 2")) + + val isHandled = handleTypedAction( + DivActionTyped.ArraySetValue( + DivActionArraySetValue( + index = Expression.constant(2), + value = typedValue("new value"), + variableName = Expression.constant("array_var") + ) + ) + ) + + Assert.assertTrue(isHandled) + + Assert.assertEquals( + JSONArray().put("value 1").put("value 2"), + getVariableValue("array_var") + ) + } + + @Test + fun `ArraySetValue action does nothing for not array variable`() { + setVariable("string_var", "value") + + val isHandled = handleTypedAction( + DivActionTyped.ArraySetValue( + DivActionArraySetValue( + index = Expression.constant(0), + value = typedValue("new value"), + variableName = Expression.constant("string_var") + ) + ) + ) + + Assert.assertTrue(isHandled) + Assert.assertEquals("value", getVariableValue("string_var")) + } + @Test fun `DictSetValue action removes value`() { setVariable("dict_var", JSONObject().put("key", "value")) diff --git a/client/ios/DivKit/Actions/ArrayActionsHandler.swift b/client/ios/DivKit/Actions/ArrayActionsHandler.swift new file mode 100644 index 000000000..5d4d7da2a --- /dev/null +++ b/client/ios/DivKit/Actions/ArrayActionsHandler.swift @@ -0,0 +1,94 @@ +import Foundation + +final class ArrayActionsHandler { + func handle(_ action: DivActionArrayInsertValue, context: DivActionHandlingContext) { + let expressionResolver = context.expressionResolver + guard let variableName = action.resolveName(expressionResolver), + let value = action.value.resolve(expressionResolver), + var array = context.getVariableValue(variableName) else { + return + } + + if let index = action.resolveIndex(expressionResolver) { + guard index >= 0, index <= array.count else { + DivKitLogger.error( + "Failed to insert value in array \(variableName). Index out of bounds: \(index)." + ) + return + } + array.insert(value, at: index) + } else { + array.append(value) + } + + context.setVariableValue(variableName, array) + } + + func handle(_ action: DivActionArrayRemoveValue, context: DivActionHandlingContext) { + let expressionResolver = context.expressionResolver + guard let variableName = action.resolveName(expressionResolver), + let index = action.resolveIndex(expressionResolver), + var array = context.getVariableValue(variableName) else { + return + } + + guard index >= 0, index < array.count else { + DivKitLogger.error( + "Failed to remove value from array \(variableName). Index out of bounds: \(index)." + ) + return + } + + array.remove(at: index) + context.setVariableValue(variableName, array) + } + + func handle(_ action: DivActionArraySetValue, context: DivActionHandlingContext) { + let expressionResolver = context.expressionResolver + guard let variableName = action.resolveName(expressionResolver), + let index = action.resolveIndex(expressionResolver), + let value = action.value.resolve(expressionResolver), + var array = context.getVariableValue(variableName) else { + return + } + + guard index >= 0, index < array.count else { + DivKitLogger.error( + "Failed to set value in array \(variableName). Index out of bounds: \(index)." + ) + return + } + + array[index] = value + context.setVariableValue(variableName, array) + } +} + +extension DivActionHandlingContext { + fileprivate func getVariableValue(_ name: DivVariableName) -> [AnyHashable]? { + variablesStorage.getVariableValue(cardId: cardId, name: name) + } + + fileprivate func setVariableValue(_ name: DivVariableName, _ value: [AnyHashable]) { + variablesStorage.update(cardId: cardId, name: name, value: .array(value)) + } +} + +private protocol ArrayAction { + func resolveVariableName(_ resolver: ExpressionResolver) -> String? +} + +extension ArrayAction { + func resolveName(_ resolver: ExpressionResolver) -> DivVariableName? { + if let name = resolveVariableName(resolver) { + return DivVariableName(rawValue: name) + } + return nil + } +} + +extension DivActionArrayInsertValue: ArrayAction {} + +extension DivActionArrayRemoveValue: ArrayAction {} + +extension DivActionArraySetValue: ArrayAction {} diff --git a/client/ios/DivKit/Actions/ArrayInsertValueActionHandler.swift b/client/ios/DivKit/Actions/ArrayInsertValueActionHandler.swift deleted file mode 100644 index 530815928..000000000 --- a/client/ios/DivKit/Actions/ArrayInsertValueActionHandler.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation - -final class ArrayInsertValueActionHandler { - func handle(_ action: DivActionArrayInsertValue, context: DivActionHandlingContext) { - let expressionResolver = context.expressionResolver - guard let variableName = action.resolveVariableName(expressionResolver), - let value = action.value.resolve(expressionResolver), - var array: [AnyHashable] = context.variablesStorage.getVariableValue( - cardId: context.cardId, - name: DivVariableName(rawValue: variableName) - ) else { - return - } - - if let index = action.resolveIndex(expressionResolver) { - guard index >= 0, index <= array.count else { - DivKitLogger.error( - "Index out of bound \(index) for inserting value in \(variableName) with length \(array.count)" - ) - return - } - array.insert(value, at: index) - } else { - array.append(value) - } - - context.variablesStorage.update( - cardId: context.cardId, - name: DivVariableName(rawValue: variableName), - value: .array(array) - ) - } -} diff --git a/client/ios/DivKit/Actions/ArrayRemoveValueActionHandler.swift b/client/ios/DivKit/Actions/ArrayRemoveValueActionHandler.swift deleted file mode 100644 index 00e1f2e36..000000000 --- a/client/ios/DivKit/Actions/ArrayRemoveValueActionHandler.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Foundation - -final class ArrayRemoveValueActionHandler { - func handle(_ action: DivActionArrayRemoveValue, context: DivActionHandlingContext) { - let expressionResolver = context.expressionResolver - guard let variableName = action.resolveVariableName(expressionResolver), - let index = action.resolveIndex(expressionResolver), - var array: [AnyHashable] = context.variablesStorage.getVariableValue( - cardId: context.cardId, - name: DivVariableName(rawValue: variableName) - ) else { - return - } - - guard index >= 0, index < array.count else { - DivKitLogger.error( - "Index out of bound \(index) for removing value of \(variableName) with length \(array.count)" - ) - return - } - - array.remove(at: index) - - context.variablesStorage.update( - cardId: context.cardId, - name: DivVariableName(rawValue: variableName), - value: .array(array) - ) - } -} diff --git a/client/ios/DivKit/Actions/DivActionHandler.swift b/client/ios/DivKit/Actions/DivActionHandler.swift index 05fbeeeea..902b56fb9 100644 --- a/client/ios/DivKit/Actions/DivActionHandler.swift +++ b/client/ios/DivKit/Actions/DivActionHandler.swift @@ -16,8 +16,7 @@ public final class DivActionHandler { private let updateCard: DivActionURLHandler.UpdateCardAction private let reporter: DivReporter - private let arrayInsertValueActionHandler = ArrayInsertValueActionHandler() - private let arrayRemoveValueActionHandler = ArrayRemoveValueActionHandler() + private let arrayActionsHandler = ArrayActionsHandler() private let dictSetValueActionHandler = DictSetValueActionHandler() private let clearFocusActionHandler = ClearFocusActionHandler() private let copyToClipboardActionHandler = CopyToClipboardActionHandler() @@ -135,9 +134,11 @@ public final class DivActionHandler { var isHandled = true switch action.typed { case let .divActionArrayInsertValue(action): - arrayInsertValueActionHandler.handle(action, context: context) + arrayActionsHandler.handle(action, context: context) case let .divActionArrayRemoveValue(action): - arrayRemoveValueActionHandler.handle(action, context: context) + arrayActionsHandler.handle(action, context: context) + case let .divActionArraySetValue(action): + arrayActionsHandler.handle(action, context: context) case let .divActionDictSetValue(action): dictSetValueActionHandler.handle(action, context: context) case .divActionClearFocus: diff --git a/client/ios/DivKit/generated_sources/DivActionArraySetValue.swift b/client/ios/DivKit/generated_sources/DivActionArraySetValue.swift new file mode 100644 index 000000000..afe5c36ed --- /dev/null +++ b/client/ios/DivKit/generated_sources/DivActionArraySetValue.swift @@ -0,0 +1,56 @@ +// Generated code. Do not modify. + +import CommonCorePublic +import Foundation +import Serialization + +public final class DivActionArraySetValue { + public static let type: String = "array_set_value" + public let index: Expression + public let value: DivTypedValue + public let variableName: Expression + + public func resolveIndex(_ resolver: ExpressionResolver) -> Int? { + resolver.resolveNumeric(index) + } + + public func resolveVariableName(_ resolver: ExpressionResolver) -> String? { + resolver.resolveString(variableName) + } + + init( + index: Expression, + value: DivTypedValue, + variableName: Expression + ) { + self.index = index + self.value = value + self.variableName = variableName + } +} + +#if DEBUG +extension DivActionArraySetValue: Equatable { + public static func ==(lhs: DivActionArraySetValue, rhs: DivActionArraySetValue) -> Bool { + guard + lhs.index == rhs.index, + lhs.value == rhs.value, + lhs.variableName == rhs.variableName + else { + return false + } + return true + } +} +#endif + +extension DivActionArraySetValue: Serializable { + public func toDictionary() -> [String: ValidSerializationValue] { + var result: [String: ValidSerializationValue] = [:] + result["type"] = Self.type + result["index"] = index.toValidSerializationValue() + result["value"] = value.toDictionary() + result["variable_name"] = variableName.toValidSerializationValue() + return result + } +} diff --git a/client/ios/DivKit/generated_sources/DivActionArraySetValueTemplate.swift b/client/ios/DivKit/generated_sources/DivActionArraySetValueTemplate.swift new file mode 100644 index 000000000..6e21e498d --- /dev/null +++ b/client/ios/DivKit/generated_sources/DivActionArraySetValueTemplate.swift @@ -0,0 +1,149 @@ +// Generated code. Do not modify. + +import CommonCorePublic +import Foundation +import Serialization + +public final class DivActionArraySetValueTemplate: TemplateValue { + public static let type: String = "array_set_value" + public let parent: String? + public let index: Field>? + public let value: Field? + public let variableName: Field>? + + public convenience init(dictionary: [String: Any], templateToType: [TemplateName: String]) throws { + self.init( + parent: dictionary["type"] as? String, + index: dictionary.getOptionalExpressionField("index"), + value: dictionary.getOptionalField("value", templateToType: templateToType), + variableName: dictionary.getOptionalExpressionField("variable_name") + ) + } + + init( + parent: String?, + index: Field>? = nil, + value: Field? = nil, + variableName: Field>? = nil + ) { + self.parent = parent + self.index = index + self.value = value + self.variableName = variableName + } + + private static func resolveOnlyLinks(context: TemplatesContext, parent: DivActionArraySetValueTemplate?) -> DeserializationResult { + let indexValue = parent?.index?.resolveValue(context: context) ?? .noValue + let valueValue = parent?.value?.resolveValue(context: context, useOnlyLinks: true) ?? .noValue + let variableNameValue = parent?.variableName?.resolveValue(context: context) ?? .noValue + var errors = mergeErrors( + indexValue.errorsOrWarnings?.map { .nestedObjectError(field: "index", error: $0) }, + valueValue.errorsOrWarnings?.map { .nestedObjectError(field: "value", error: $0) }, + variableNameValue.errorsOrWarnings?.map { .nestedObjectError(field: "variable_name", error: $0) } + ) + if case .noValue = indexValue { + errors.append(.requiredFieldIsMissing(field: "index")) + } + if case .noValue = valueValue { + errors.append(.requiredFieldIsMissing(field: "value")) + } + if case .noValue = variableNameValue { + errors.append(.requiredFieldIsMissing(field: "variable_name")) + } + guard + let indexNonNil = indexValue.value, + let valueNonNil = valueValue.value, + let variableNameNonNil = variableNameValue.value + else { + return .failure(NonEmptyArray(errors)!) + } + let result = DivActionArraySetValue( + index: indexNonNil, + value: valueNonNil, + variableName: variableNameNonNil + ) + return errors.isEmpty ? .success(result) : .partialSuccess(result, warnings: NonEmptyArray(errors)!) + } + + public static func resolveValue(context: TemplatesContext, parent: DivActionArraySetValueTemplate?, useOnlyLinks: Bool) -> DeserializationResult { + if useOnlyLinks { + return resolveOnlyLinks(context: context, parent: parent) + } + var indexValue: DeserializationResult> = parent?.index?.value() ?? .noValue + var valueValue: DeserializationResult = .noValue + var variableNameValue: DeserializationResult> = parent?.variableName?.value() ?? .noValue + context.templateData.forEach { key, __dictValue in + switch key { + case "index": + indexValue = deserialize(__dictValue).merged(with: indexValue) + case "value": + valueValue = deserialize(__dictValue, templates: context.templates, templateToType: context.templateToType, type: DivTypedValueTemplate.self).merged(with: valueValue) + case "variable_name": + variableNameValue = deserialize(__dictValue).merged(with: variableNameValue) + case parent?.index?.link: + indexValue = indexValue.merged(with: { deserialize(__dictValue) }) + case parent?.value?.link: + valueValue = valueValue.merged(with: { deserialize(__dictValue, templates: context.templates, templateToType: context.templateToType, type: DivTypedValueTemplate.self) }) + case parent?.variableName?.link: + variableNameValue = variableNameValue.merged(with: { deserialize(__dictValue) }) + default: break + } + } + if let parent = parent { + valueValue = valueValue.merged(with: { parent.value?.resolveValue(context: context, useOnlyLinks: true) }) + } + var errors = mergeErrors( + indexValue.errorsOrWarnings?.map { .nestedObjectError(field: "index", error: $0) }, + valueValue.errorsOrWarnings?.map { .nestedObjectError(field: "value", error: $0) }, + variableNameValue.errorsOrWarnings?.map { .nestedObjectError(field: "variable_name", error: $0) } + ) + if case .noValue = indexValue { + errors.append(.requiredFieldIsMissing(field: "index")) + } + if case .noValue = valueValue { + errors.append(.requiredFieldIsMissing(field: "value")) + } + if case .noValue = variableNameValue { + errors.append(.requiredFieldIsMissing(field: "variable_name")) + } + guard + let indexNonNil = indexValue.value, + let valueNonNil = valueValue.value, + let variableNameNonNil = variableNameValue.value + else { + return .failure(NonEmptyArray(errors)!) + } + let result = DivActionArraySetValue( + index: indexNonNil, + value: valueNonNil, + variableName: variableNameNonNil + ) + return errors.isEmpty ? .success(result) : .partialSuccess(result, warnings: NonEmptyArray(errors)!) + } + + private func mergedWithParent(templates: [TemplateName: Any]) throws -> DivActionArraySetValueTemplate { + guard let parent = parent, parent != Self.type else { return self } + guard let parentTemplate = templates[parent] as? DivActionArraySetValueTemplate else { + throw DeserializationError.unknownType(type: parent) + } + let mergedParent = try parentTemplate.mergedWithParent(templates: templates) + + return DivActionArraySetValueTemplate( + parent: nil, + index: index ?? mergedParent.index, + value: value ?? mergedParent.value, + variableName: variableName ?? mergedParent.variableName + ) + } + + public func resolveParent(templates: [TemplateName: Any]) throws -> DivActionArraySetValueTemplate { + let merged = try mergedWithParent(templates: templates) + + return DivActionArraySetValueTemplate( + parent: nil, + index: merged.index, + value: try merged.value?.resolveParent(templates: templates), + variableName: merged.variableName + ) + } +} diff --git a/client/ios/DivKit/generated_sources/DivActionTyped.swift b/client/ios/DivKit/generated_sources/DivActionTyped.swift index 7cd9ee98a..966d3ffa8 100644 --- a/client/ios/DivKit/generated_sources/DivActionTyped.swift +++ b/client/ios/DivKit/generated_sources/DivActionTyped.swift @@ -8,6 +8,7 @@ import Serialization public enum DivActionTyped { case divActionArrayInsertValue(DivActionArrayInsertValue) case divActionArrayRemoveValue(DivActionArrayRemoveValue) + case divActionArraySetValue(DivActionArraySetValue) case divActionClearFocus(DivActionClearFocus) case divActionCopyToClipboard(DivActionCopyToClipboard) case divActionDictSetValue(DivActionDictSetValue) @@ -20,6 +21,8 @@ public enum DivActionTyped { return value case let .divActionArrayRemoveValue(value): return value + case let .divActionArraySetValue(value): + return value case let .divActionClearFocus(value): return value case let .divActionCopyToClipboard(value): @@ -42,6 +45,8 @@ extension DivActionTyped: Equatable { return l == r case let (.divActionArrayRemoveValue(l), .divActionArrayRemoveValue(r)): return l == r + case let (.divActionArraySetValue(l), .divActionArraySetValue(r)): + return l == r case let (.divActionClearFocus(l), .divActionClearFocus(r)): return l == r case let (.divActionCopyToClipboard(l), .divActionCopyToClipboard(r)): diff --git a/client/ios/DivKit/generated_sources/DivActionTypedTemplate.swift b/client/ios/DivKit/generated_sources/DivActionTypedTemplate.swift index 12a3ca1a2..273525671 100644 --- a/client/ios/DivKit/generated_sources/DivActionTypedTemplate.swift +++ b/client/ios/DivKit/generated_sources/DivActionTypedTemplate.swift @@ -8,6 +8,7 @@ import Serialization public enum DivActionTypedTemplate: TemplateValue { case divActionArrayInsertValueTemplate(DivActionArrayInsertValueTemplate) case divActionArrayRemoveValueTemplate(DivActionArrayRemoveValueTemplate) + case divActionArraySetValueTemplate(DivActionArraySetValueTemplate) case divActionClearFocusTemplate(DivActionClearFocusTemplate) case divActionCopyToClipboardTemplate(DivActionCopyToClipboardTemplate) case divActionDictSetValueTemplate(DivActionDictSetValueTemplate) @@ -20,6 +21,8 @@ public enum DivActionTypedTemplate: TemplateValue { return value case let .divActionArrayRemoveValueTemplate(value): return value + case let .divActionArraySetValueTemplate(value): + return value case let .divActionClearFocusTemplate(value): return value case let .divActionCopyToClipboardTemplate(value): @@ -39,6 +42,8 @@ public enum DivActionTypedTemplate: TemplateValue { return .divActionArrayInsertValueTemplate(try value.resolveParent(templates: templates)) case let .divActionArrayRemoveValueTemplate(value): return .divActionArrayRemoveValueTemplate(try value.resolveParent(templates: templates)) + case let .divActionArraySetValueTemplate(value): + return .divActionArraySetValueTemplate(try value.resolveParent(templates: templates)) case let .divActionClearFocusTemplate(value): return .divActionClearFocusTemplate(try value.resolveParent(templates: templates)) case let .divActionCopyToClipboardTemplate(value): @@ -78,6 +83,14 @@ public enum DivActionTypedTemplate: TemplateValue { case let .failure(errors): return .failure(errors) case .noValue: return .noValue } + case let .divActionArraySetValueTemplate(value): + let result = value.resolveValue(context: context, useOnlyLinks: useOnlyLinks) + switch result { + case let .success(value): return .success(.divActionArraySetValue(value)) + case let .partialSuccess(value, warnings): return .partialSuccess(.divActionArraySetValue(value), warnings: warnings) + case let .failure(errors): return .failure(errors) + case .noValue: return .noValue + } case let .divActionClearFocusTemplate(value): let result = value.resolveValue(context: context, useOnlyLinks: useOnlyLinks) switch result { @@ -143,6 +156,14 @@ public enum DivActionTypedTemplate: TemplateValue { case let .failure(errors): return .failure(errors) case .noValue: return .noValue } + case DivActionArraySetValue.type: + let result = DivActionArraySetValueTemplate.resolveValue(context: context, useOnlyLinks: useOnlyLinks) + switch result { + case let .success(value): return .success(.divActionArraySetValue(value)) + case let .partialSuccess(value, warnings): return .partialSuccess(.divActionArraySetValue(value), warnings: warnings) + case let .failure(errors): return .failure(errors) + case .noValue: return .noValue + } case DivActionClearFocus.type: let result = DivActionClearFocusTemplate.resolveValue(context: context, useOnlyLinks: useOnlyLinks) switch result { @@ -198,6 +219,8 @@ extension DivActionTypedTemplate { self = .divActionArrayInsertValueTemplate(try DivActionArrayInsertValueTemplate(dictionary: dictionary, templateToType: templateToType)) case DivActionArrayRemoveValueTemplate.type: self = .divActionArrayRemoveValueTemplate(try DivActionArrayRemoveValueTemplate(dictionary: dictionary, templateToType: templateToType)) + case DivActionArraySetValueTemplate.type: + self = .divActionArraySetValueTemplate(try DivActionArraySetValueTemplate(dictionary: dictionary, templateToType: templateToType)) case DivActionClearFocusTemplate.type: self = .divActionClearFocusTemplate(try DivActionClearFocusTemplate(dictionary: dictionary, templateToType: templateToType)) case DivActionCopyToClipboardTemplate.type: diff --git a/client/ios/DivKitTests/Actions/DivActionHandlerTests.swift b/client/ios/DivKitTests/Actions/DivActionHandlerTests.swift index 087527b8a..ea32d0aa8 100644 --- a/client/ios/DivKitTests/Actions/DivActionHandlerTests.swift +++ b/client/ios/DivKitTests/Actions/DivActionHandlerTests.swift @@ -53,17 +53,12 @@ final class DivActionHandlerTests: XCTestCase { func test_ArrayInsertValueAction_AppendsValue() { setVariableValue("array_var", .array([1, "two"])) - handle( - divAction( - logId: "test_log_id", - typed: .divActionArrayInsertValue( - DivActionArrayInsertValue( - value: stringValue("new value"), - variableName: .value("array_var") - ) - ) + handle(.divActionArrayInsertValue( + DivActionArrayInsertValue( + value: stringValue("new value"), + variableName: .value("array_var") ) - ) + )) XCTAssertEqual([1, "two", "new value"] as [AnyHashable], getVariableValue("array_var")) } @@ -71,55 +66,54 @@ final class DivActionHandlerTests: XCTestCase { func test_ArrayInsertValueAction_InsertsValue() { setVariableValue("array_var", .array([1, "two"])) - handle( - divAction( - logId: "test_log_id", - typed: .divActionArrayInsertValue( - DivActionArrayInsertValue( - index: .value(1), - value: stringValue("new value"), - variableName: .value("array_var") - ) - ) + handle(.divActionArrayInsertValue( + DivActionArrayInsertValue( + index: .value(1), + value: stringValue("new value"), + variableName: .value("array_var") ) - ) + )) XCTAssertEqual([1, "new value", "two"] as [AnyHashable], getVariableValue("array_var")) } + func test_ArrayInsertValueAction_WithIndexEqualLength_InsertsValue() { + setVariableValue("array_var", .array(["one", "two"])) + + handle(.divActionArrayInsertValue( + DivActionArrayInsertValue( + index: .value(2), + value: stringValue("new value"), + variableName: .value("array_var") + ) + )) + + XCTAssertEqual(["one", "two", "new value"], getVariableValue("array_var")) + } + func test_ArrayInsertValueAction_DoesNothingForInvalidIndex() { setVariableValue("array_var", .array([1, "two"])) - handle( - divAction( - logId: "test_log_id", - typed: .divActionArrayInsertValue( - DivActionArrayInsertValue( - index: .value(10), - value: stringValue("new value"), - variableName: .value("array_var") - ) - ) + handle(.divActionArrayInsertValue( + DivActionArrayInsertValue( + index: .value(10), + value: stringValue("new value"), + variableName: .value("array_var") ) - ) + )) XCTAssertEqual([1, "two"] as [AnyHashable], getVariableValue("array_var")) } - func test_ArrayInsertValueAction__DoesNothingForDifferentTypeVar() { + func test_ArrayInsertValueAction_DoesNothingForNotArrayVar() { setVariableValue("string_var", .string("value")) - handle( - divAction( - logId: "test_log_id", - typed: .divActionArrayInsertValue( - DivActionArrayInsertValue( - value: stringValue("new value"), - variableName: .value("string_var") - ) - ) + handle(.divActionArrayInsertValue( + DivActionArrayInsertValue( + value: stringValue("new value"), + variableName: .value("string_var") ) - ) + )) XCTAssertEqual("value", getVariableValue("string_var")) } @@ -127,17 +121,12 @@ final class DivActionHandlerTests: XCTestCase { func test_ArrayRemoveValueAction_RemovesValue() { setVariableValue("array_var", .array([1, "two"])) - handle( - divAction( - logId: "test_log_id", - typed: .divActionArrayRemoveValue( - DivActionArrayRemoveValue( - index: .value(1), - variableName: .value("array_var") - ) - ) + handle(.divActionArrayRemoveValue( + DivActionArrayRemoveValue( + index: .value(1), + variableName: .value("array_var") ) - ) + )) XCTAssertEqual([1] as [AnyHashable], getVariableValue("array_var")) } @@ -145,36 +134,81 @@ final class DivActionHandlerTests: XCTestCase { func test_ArrayRemoveValueAction_DoesNothingForInvalidIndex() { setVariableValue("array_var", .array([1, "two"])) - handle( - divAction( - logId: "test_log_id", - typed: .divActionArrayRemoveValue( - DivActionArrayRemoveValue( - index: .value(10), - variableName: .value("array_var") - ) - ) + handle(.divActionArrayRemoveValue( + DivActionArrayRemoveValue( + index: .value(10), + variableName: .value("array_var") ) - ) + )) XCTAssertEqual([1, "two"] as [AnyHashable], getVariableValue("array_var")) } + func test_ArrayRemoveValueAction_DoesNothingForNotArrayVar() { + setVariableValue("string_var", .string("value")) + + handle(.divActionArrayRemoveValue( + DivActionArrayRemoveValue( + index: .value(0), + variableName: .value("string_var") + ) + )) + + XCTAssertEqual("value", getVariableValue("string_var")) + } + + func test_ArraySetValueAction_SetsValue() { + setVariableValue("array_var", .array(["one", "two"])) + + handle(.divActionArraySetValue( + DivActionArraySetValue( + index: .value(1), + value: stringValue("new value"), + variableName: .value("array_var") + ) + )) + + XCTAssertEqual(["one", "new value"], getVariableValue("array_var")) + } + + func test_ArraySetValueAction_DoesNothingForInvalidIndex() { + setVariableValue("array_var", .array(["one", "two"])) + + handle(.divActionArraySetValue( + DivActionArraySetValue( + index: .value(2), + value: stringValue("new value"), + variableName: .value("array_var") + ) + )) + + XCTAssertEqual(["one", "two"], getVariableValue("array_var")) + } + + func test_ArraySetValueAction_DoesNothingForNotArrayVar() { + setVariableValue("string_var", .string("value")) + + handle(.divActionArraySetValue( + DivActionArraySetValue( + index: .value(0), + value: stringValue("new value"), + variableName: .value("string_var") + ) + )) + + XCTAssertEqual("value", getVariableValue("string_var")) + } + func test_DictSetValueAction_AddsValue() { setVariableValue("dict_var", .dict([:])) - handle( - divAction( - logId: "test_log_id", - typed: .divActionDictSetValue( - DivActionDictSetValue( - key: .value("key"), - value: stringValue("new value"), - variableName: .value("dict_var") - ) - ) + handle(.divActionDictSetValue( + DivActionDictSetValue( + key: .value("key"), + value: stringValue("new value"), + variableName: .value("dict_var") ) - ) + )) XCTAssertEqual(["key": "new value"], getVariableValue("dict_var")) } @@ -182,18 +216,13 @@ final class DivActionHandlerTests: XCTestCase { func test_DictSetValueAction_UpdatesValue() { setVariableValue("dict_var", .dict(["key": "value"])) - handle( - divAction( - logId: "test_log_id", - typed: .divActionDictSetValue( - DivActionDictSetValue( - key: .value("key"), - value: .dictValue(DictValue(value: ["new_key": "new value"])), - variableName: .value("dict_var") - ) - ) + handle(.divActionDictSetValue( + DivActionDictSetValue( + key: .value("key"), + value: .dictValue(DictValue(value: ["new_key": "new value"])), + variableName: .value("dict_var") ) - ) + )) XCTAssertEqual(["key": ["new_key": "new value"]], getVariableValue("dict_var")) } @@ -201,36 +230,26 @@ final class DivActionHandlerTests: XCTestCase { func test_DictSetValueAction_RemovesValue() { setVariableValue("dict_var", .dict(["key": "value"])) - handle( - divAction( - logId: "test_log_id", - typed: .divActionDictSetValue( - DivActionDictSetValue( - key: .value("key"), - variableName: .value("dict_var") - ) - ) + handle(.divActionDictSetValue( + DivActionDictSetValue( + key: .value("key"), + variableName: .value("dict_var") ) - ) + )) XCTAssertEqual([String: AnyHashable](), getVariableValue("dict_var")) } - func test_DictSetValueAction_DoesNothingForDifferentTypeVar() { + func test_DictSetValueAction_DoesNothingForNotDictVar() { setVariableValue("array_var", .array(["one", "two"])) - handle( - divAction( - logId: "test_log_id", - typed: .divActionDictSetValue( - DivActionDictSetValue( - key: .value("key"), - value: stringValue("new value"), - variableName: .value("array_var") - ) - ) + handle(.divActionDictSetValue( + DivActionDictSetValue( + key: .value("key"), + value: stringValue("new value"), + variableName: .value("array_var") ) - ) + )) XCTAssertEqual(["one", "two"], getVariableValue("array_var")) } @@ -238,17 +257,12 @@ final class DivActionHandlerTests: XCTestCase { func test_SetVariableAction_SetsStringVariable() { setVariableValue("string_var", .string("default")) - handle( - divAction( - logId: "test_log_id", - typed: .divActionSetVariable( - DivActionSetVariable( - value: stringValue("new value"), - variableName: .value("string_var") - ) - ) + handle(.divActionSetVariable( + DivActionSetVariable( + value: stringValue("new value"), + variableName: .value("string_var") ) - ) + )) XCTAssertEqual("new value", getVariableValue("string_var")) } @@ -256,17 +270,12 @@ final class DivActionHandlerTests: XCTestCase { func test_SetVariableAction_SetsArrayVariable() { setVariableValue("array_var", .array([])) - handle( - divAction( - logId: "test_log_id", - typed: .divActionSetVariable( - DivActionSetVariable( - value: .arrayValue(ArrayValue(value: .value(["value 1", "value 2"]))), - variableName: .value("array_var") - ) - ) + handle(.divActionSetVariable( + DivActionSetVariable( + value: .arrayValue(ArrayValue(value: .value(["value 1", "value 2"]))), + variableName: .value("array_var") ) - ) + )) XCTAssertEqual(["value 1", "value 2"], getVariableValue("array_var")) } @@ -314,10 +323,19 @@ final class DivActionHandlerTests: XCTestCase { ) } + private func handle(_ action: DivActionTyped) { + actionHandler.handle( + divAction(logId: "log_id", typed: action), + cardId: cardId, + source: .tap, + sender: nil + ) + } + private func getVariableValue(_ name: DivVariableName) -> T? { variablesStorage.makeVariables(for: cardId)[name]?.typedValue() } - + private func setVariableValue(_ name: DivVariableName, _ value: DivVariableValue) { variablesStorage.set(cardId: cardId, variables: [name: value]) } diff --git a/schema/div-action-array-set-value.json b/schema/div-action-array-set-value.json new file mode 100644 index 000000000..9431c470a --- /dev/null +++ b/schema/div-action-array-set-value.json @@ -0,0 +1,31 @@ +{ + "type": "object", + "$description": "translations.json#/div_action_array_set_value", + "properties": { + "type": { + "type": "string", + "enum": [ + "array_set_value" + ] + }, + "variable_name": { + "type": "string" + }, + "index": { + "type": "integer" + }, + "value": { + "$ref": "div-typed-value.json" + } + }, + "platforms": [ + "android", + "ios" + ], + "required": [ + "type", + "variable_name", + "index", + "value" + ] +} diff --git a/schema/div-action-typed.json b/schema/div-action-typed.json index 0b5933dad..ef4254de0 100644 --- a/schema/div-action-typed.json +++ b/schema/div-action-typed.json @@ -6,6 +6,9 @@ { "$ref": "div-action-array-remove-value.json" }, + { + "$ref": "div-action-array-set-value.json" + }, { "$ref": "div-action-clear-focus.json" }, diff --git a/schema/translations.json b/schema/translations.json index 05aa3eeec..2772ae385 100644 --- a/schema/translations.json +++ b/schema/translations.json @@ -43,6 +43,10 @@ "en": "Deletes a value from the array", "ru": "Удаляет значение из массива" }, + "div_action_array_set_value": { + "en": "Sets value in the array", + "ru": "Устанавливает значение в массиве по индексу" + }, "div_action_base_download_callbacks": { "en": "Callbacks that are called after [data loading](../../interaction#loading-data).", "ru": "Колбэки, которые вызываются после [дозагрузки данных](../../interaction#loading-data)."