mirror of
https://github.com/divkit/divkit.git
synced 2026-06-06 20:07:59 +00:00
supported get/set stored values by card_id
commit_hash:d7fcc6410470b5d9b6830747c6e106da972bf713
This commit is contained in:
@@ -177,7 +177,8 @@ public final class DivActionHandler {
|
||||
let cardId = path.cardId
|
||||
let expressionResolver = ExpressionResolver(
|
||||
functionsProvider: FunctionsProvider(
|
||||
persistentValuesStorage: persistentValuesStorage
|
||||
persistentValuesStorage: persistentValuesStorage,
|
||||
cardId: cardId
|
||||
),
|
||||
customFunctionsStorageProvider: { [weak functionsStorage] in
|
||||
functionsStorage?.getStorage(path: path, contains: $0)
|
||||
@@ -364,8 +365,9 @@ public final class DivActionHandler {
|
||||
context.updateCard(.state(cardId))
|
||||
case let .timer(timerId, action):
|
||||
timerActionHandler.handle(cardId: cardId, timerId: timerId, action: action)
|
||||
case let .setStoredValue(storedValue):
|
||||
persistentValuesStorage.set(value: storedValue)
|
||||
case let .setStoredValue(storedValue, scope):
|
||||
let scopeCardId: DivCardID? = scope == .card ? cardId : nil
|
||||
persistentValuesStorage.set(value: storedValue, cardId: scopeCardId)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ enum DivActionIntent: Hashable {
|
||||
case scroll(id: String, mode: ScrollMode)
|
||||
case timer(id: String, action: DivTimerAction)
|
||||
case video(id: String, action: DivVideoAction)
|
||||
case setStoredValue(DivStoredValue)
|
||||
case setStoredValue(DivStoredValue, DivStoredValueScope)
|
||||
|
||||
static let scheme = "div-action"
|
||||
|
||||
@@ -99,10 +99,10 @@ enum DivActionIntent: Hashable {
|
||||
}
|
||||
self = .video(id: id, action: action)
|
||||
case "set_stored_value":
|
||||
guard let storedValue = url.storedValue else {
|
||||
guard let (storedValue, scope) = url.storedValue else {
|
||||
return nil
|
||||
}
|
||||
self = .setStoredValue(storedValue)
|
||||
self = .setStoredValue(storedValue, scope)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
@@ -200,7 +200,7 @@ extension URL {
|
||||
queryParamValue(forName: "step").flatMap(Int.init)
|
||||
}
|
||||
|
||||
fileprivate var storedValue: DivStoredValue? {
|
||||
fileprivate var storedValue: (DivStoredValue, DivStoredValueScope)? {
|
||||
guard let name = getParam(forName: "name"),
|
||||
let value = getParam(forName: "value"),
|
||||
let lifetime = getParam(forName: "lifetime").flatMap(Int.init),
|
||||
@@ -211,6 +211,7 @@ extension URL {
|
||||
DivKitLogger.error("Unsupported stored value type: \(typeStr)")
|
||||
return nil
|
||||
}
|
||||
let scope = getParam(forName: "scope").flatMap(DivStoredValueScope.init) ?? .global
|
||||
let storedValue = DivStoredValue(
|
||||
name: name,
|
||||
value: value,
|
||||
@@ -221,7 +222,7 @@ extension URL {
|
||||
DivKitLogger.error("Incorrect value: \(value) for type: \(type)")
|
||||
return nil
|
||||
}
|
||||
return storedValue
|
||||
return (storedValue, scope)
|
||||
}
|
||||
|
||||
fileprivate func getParam(forName name: String) -> String? {
|
||||
|
||||
@@ -14,13 +14,16 @@ final class SetStoredValueActionHandler {
|
||||
return
|
||||
}
|
||||
|
||||
let scope = action.resolveScope(expressionResolver)
|
||||
let cardId: DivCardID? = scope == .card ? context.cardId : nil
|
||||
persistentValuesStorage.set(
|
||||
value: DivStoredValue(
|
||||
name: name,
|
||||
value: ExpressionValueConverter.stringify(value),
|
||||
type: type,
|
||||
lifetimeInSec: lifetime
|
||||
)
|
||||
),
|
||||
cardId: cardId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +162,8 @@ public struct DivBlockModelingContext {
|
||||
self.extensionHandlers = extensionHandlers
|
||||
self.layoutProviderHandler = layoutProviderHandler
|
||||
self.functionsProvider = FunctionsProvider(
|
||||
persistentValuesStorage: persistentValuesStorage
|
||||
persistentValuesStorage: persistentValuesStorage,
|
||||
cardId: path.cardId
|
||||
)
|
||||
self.idToPath = idToPath ?? IdToPath()
|
||||
self.animatorController = animatorController
|
||||
|
||||
@@ -40,7 +40,8 @@ final class CustomFunction: SimpleFunction {
|
||||
)
|
||||
let resolver = ExpressionResolver(
|
||||
functionsProvider: FunctionsProvider(
|
||||
persistentValuesStorage: DivPersistentValuesStorage()
|
||||
persistentValuesStorage: DivPersistentValuesStorage(),
|
||||
cardId: nil
|
||||
),
|
||||
customFunctionsStorageProvider: context.customFunctionsStorageProvider,
|
||||
variableValueProvider: { [weak self] name in
|
||||
|
||||
@@ -38,7 +38,8 @@ public final class ExpressionResolver {
|
||||
) {
|
||||
self.init(
|
||||
functionsProvider: FunctionsProvider(
|
||||
persistentValuesStorage: persistentValuesStorage
|
||||
persistentValuesStorage: persistentValuesStorage,
|
||||
cardId: nil
|
||||
),
|
||||
customFunctionsStorageProvider: { _ in nil },
|
||||
variableValueProvider: variableValueProvider,
|
||||
@@ -66,7 +67,8 @@ public final class ExpressionResolver {
|
||||
reporter: DivReporter
|
||||
) {
|
||||
self.functionsProvider = FunctionsProvider(
|
||||
persistentValuesStorage: persistentValuesStorage
|
||||
persistentValuesStorage: persistentValuesStorage,
|
||||
cardId: path.cardId
|
||||
)
|
||||
self.customFunctionsStorageProvider = {
|
||||
functionsStorage?.getStorage(path: path, contains: $0)
|
||||
|
||||
@@ -3,15 +3,22 @@ import VGSL
|
||||
|
||||
extension [String: Function] {
|
||||
mutating func addGetStoredValueFunctions(
|
||||
_ valueProvider: @escaping (String) -> Any?
|
||||
_ valueProvider: @escaping (String, DivStoredValueScope) -> Any?
|
||||
) {
|
||||
addFunction("getStoredBooleanValue", makeFunction(Bool.self, valueProvider))
|
||||
addFunction("getStoredBooleanValue", makeTernaryWithScope(Bool.self, valueProvider))
|
||||
addFunction("getStoredIntegerValue", makeFunction(Int.self, valueProvider))
|
||||
addFunction("getStoredIntegerValue", makeTernaryWithScope(Int.self, valueProvider))
|
||||
addFunction("getStoredNumberValue", makeFunction(Double.self, valueProvider))
|
||||
addFunction("getStoredNumberValue", makeTernaryWithScope(Double.self, valueProvider))
|
||||
addFunction("getStoredStringValue", makeFunction(String.self, valueProvider))
|
||||
addFunction("getStoredStringValue", makeTernaryWithScope(String.self, valueProvider))
|
||||
addFunction("getStoredColorValue", makeColorFunction(valueProvider))
|
||||
addFunction("getStoredUrlValue", makeUrlFunction(valueProvider))
|
||||
addFunction("getStoredArrayValue", makeFunctionWithoutFallback(DivArray.self, valueProvider))
|
||||
addFunction(
|
||||
"getStoredArrayValue",
|
||||
makeFunctionWithoutFallback(DivArray.self, valueProvider)
|
||||
)
|
||||
addFunction(
|
||||
"getStoredDictValue",
|
||||
makeFunctionWithoutFallback(DivDictionary.self, valueProvider)
|
||||
@@ -19,19 +26,50 @@ extension [String: Function] {
|
||||
}
|
||||
}
|
||||
|
||||
private func parseStoredValueScope(_ raw: String) throws -> DivStoredValueScope {
|
||||
switch raw.lowercased() {
|
||||
case "global":
|
||||
return .global
|
||||
case "card":
|
||||
return .card
|
||||
default:
|
||||
throw ExpressionError("Unknown stored value scope '\(raw)'. Expected 'global' or 'card'.")
|
||||
}
|
||||
}
|
||||
|
||||
private func makeFunction<T>(
|
||||
_: T.Type,
|
||||
_ valueProvider: @escaping (String) -> Any?
|
||||
_ valueProvider: @escaping (String, DivStoredValueScope) -> Any?
|
||||
) -> FunctionBinary<String, T, T> {
|
||||
makeFunction(valueProvider) { $0 }
|
||||
}
|
||||
|
||||
private func makeFunction<T, U>(
|
||||
_ valueProvider: @escaping (String) -> Any?,
|
||||
_ valueProvider: @escaping (String, DivStoredValueScope) -> Any?,
|
||||
transform: @escaping (U) throws -> T
|
||||
) -> FunctionBinary<String, U, T> {
|
||||
FunctionBinary { name, fallbackValue in
|
||||
guard let value = valueProvider(name) as? T else {
|
||||
guard let value = valueProvider(name, .global) as? T else {
|
||||
return try transform(fallbackValue)
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
private func makeTernaryWithScope<T>(
|
||||
_: T.Type,
|
||||
_ valueProvider: @escaping (String, DivStoredValueScope) -> Any?
|
||||
) -> FunctionTernary<String, String, T, T> {
|
||||
makeTernaryWithScope(valueProvider) { $0 }
|
||||
}
|
||||
|
||||
private func makeTernaryWithScope<T, U>(
|
||||
_ valueProvider: @escaping (String, DivStoredValueScope) -> Any?,
|
||||
transform: @escaping (U) throws -> T
|
||||
) -> FunctionTernary<String, String, U, T> {
|
||||
FunctionTernary { name, scopeString, fallbackValue in
|
||||
let scope = try parseStoredValueScope(scopeString)
|
||||
guard let value = valueProvider(name, scope) as? T else {
|
||||
return try transform(fallbackValue)
|
||||
}
|
||||
return value
|
||||
@@ -40,19 +78,27 @@ private func makeFunction<T, U>(
|
||||
|
||||
private func makeFunctionWithoutFallback<T>(
|
||||
_: T.Type,
|
||||
_ valueProvider: @escaping (String) -> Any?
|
||||
) -> FunctionUnary<String, T> {
|
||||
FunctionUnary {
|
||||
if let value = valueProvider($0) as? T {
|
||||
_ valueProvider: @escaping (String, DivStoredValueScope) -> Any?
|
||||
) -> Function {
|
||||
let unary: FunctionUnary<String, T> = FunctionUnary {
|
||||
if let value = valueProvider($0, .global) as? T {
|
||||
return value
|
||||
}
|
||||
throw ExpressionError("Missing value.")
|
||||
}
|
||||
let binary: FunctionBinary<String, String, T> = FunctionBinary { name, scopeString in
|
||||
let scope = try parseStoredValueScope(scopeString)
|
||||
if let value = valueProvider(name, scope) as? T {
|
||||
return value
|
||||
}
|
||||
throw ExpressionError("Missing value.")
|
||||
}
|
||||
return OverloadedFunction(functions: [unary, binary])
|
||||
}
|
||||
|
||||
private func makeColorFunction(_ valueProvider: @escaping (String) -> Any?) -> Function {
|
||||
let fromColorFunction = makeFunction(Color.self, valueProvider)
|
||||
let fromStringFunction: FunctionBinary<String, String, Color> =
|
||||
private func makeColorFunction(_ valueProvider: @escaping (String, DivStoredValueScope) -> Any?) -> Function {
|
||||
let fromColorBinary: FunctionBinary<String, Color, Color> = makeFunction(Color.self, valueProvider)
|
||||
let fromStringBinary: FunctionBinary<String, String, Color> =
|
||||
makeFunction(valueProvider) {
|
||||
guard let color = Color.color(withHexString: $0) else {
|
||||
throw ExpressionError(
|
||||
@@ -61,12 +107,28 @@ private func makeColorFunction(_ valueProvider: @escaping (String) -> Any?) -> F
|
||||
}
|
||||
return color
|
||||
}
|
||||
return OverloadedFunction(functions: [fromColorFunction, fromStringFunction])
|
||||
let fromColorTernary: FunctionTernary<String, String, Color, Color> =
|
||||
makeTernaryWithScope(Color.self, valueProvider)
|
||||
let fromStringTernary: FunctionTernary<String, String, String, Color> =
|
||||
makeTernaryWithScope(valueProvider) {
|
||||
guard let color = Color.color(withHexString: $0) else {
|
||||
throw ExpressionError(
|
||||
"Failed to get Color from (\($0)). Unable to convert value to Color."
|
||||
)
|
||||
}
|
||||
return color
|
||||
}
|
||||
return OverloadedFunction(functions: [
|
||||
fromColorBinary,
|
||||
fromStringBinary,
|
||||
fromColorTernary,
|
||||
fromStringTernary,
|
||||
])
|
||||
}
|
||||
|
||||
private func makeUrlFunction(_ valueProvider: @escaping (String) -> Any?) -> Function {
|
||||
let fromUrlFunction = makeFunction(URL.self, valueProvider)
|
||||
let fromStringFunction: FunctionBinary<String, String, URL> =
|
||||
private func makeUrlFunction(_ valueProvider: @escaping (String, DivStoredValueScope) -> Any?) -> Function {
|
||||
let fromUrlBinary: FunctionBinary<String, URL, URL> = makeFunction(URL.self, valueProvider)
|
||||
let fromStringBinary: FunctionBinary<String, String, URL> =
|
||||
makeFunction(valueProvider) {
|
||||
guard let url = URL(string: $0) else {
|
||||
throw ExpressionError(
|
||||
@@ -75,5 +137,21 @@ private func makeUrlFunction(_ valueProvider: @escaping (String) -> Any?) -> Fun
|
||||
}
|
||||
return url
|
||||
}
|
||||
return OverloadedFunction(functions: [fromUrlFunction, fromStringFunction])
|
||||
let fromUrlTernary: FunctionTernary<String, String, URL, URL> =
|
||||
makeTernaryWithScope(URL.self, valueProvider)
|
||||
let fromStringTernary: FunctionTernary<String, String, String, URL> =
|
||||
makeTernaryWithScope(valueProvider) {
|
||||
guard let url = URL(string: $0) else {
|
||||
throw ExpressionError(
|
||||
"Failed to get URL from (\($0)). Unable to convert value to URL."
|
||||
)
|
||||
}
|
||||
return url
|
||||
}
|
||||
return OverloadedFunction(functions: [
|
||||
fromUrlBinary,
|
||||
fromStringBinary,
|
||||
fromUrlTernary,
|
||||
fromStringTernary,
|
||||
])
|
||||
}
|
||||
|
||||
@@ -12,9 +12,13 @@ final class FunctionsProvider {
|
||||
}()
|
||||
|
||||
lazy var functions: [String: Function] =
|
||||
lock.withLock {
|
||||
lock.withLock { [weak self] in
|
||||
guard let self else { return [:] }
|
||||
var functions = staticFunctions
|
||||
functions.addGetStoredValueFunctions(persistentValuesStorage.get)
|
||||
functions.addGetStoredValueFunctions({ name, scope in
|
||||
let storageCardId: DivCardID? = (scope == .global) ? nil : self.cardId
|
||||
return self.persistentValuesStorage.get(name: name, cardId: storageCardId)
|
||||
})
|
||||
return functions
|
||||
}
|
||||
|
||||
@@ -58,12 +62,15 @@ final class FunctionsProvider {
|
||||
}
|
||||
|
||||
private let persistentValuesStorage: DivPersistentValuesStorage
|
||||
private let cardId: DivCardID?
|
||||
private let lock = AllocatedUnfairLock()
|
||||
|
||||
init(
|
||||
persistentValuesStorage: DivPersistentValuesStorage
|
||||
persistentValuesStorage: DivPersistentValuesStorage,
|
||||
cardId: DivCardID?
|
||||
) {
|
||||
self.persistentValuesStorage = persistentValuesStorage
|
||||
self.cardId = cardId
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import Foundation
|
||||
import LayoutKit
|
||||
|
||||
public protocol Clearable {
|
||||
func clear()
|
||||
func clear(cardId: DivCardID)
|
||||
}
|
||||
|
||||
@@ -23,9 +23,10 @@ public final class DivPersistentValuesStorage {
|
||||
removeOutdatedStoredValues()
|
||||
}
|
||||
|
||||
func set(value: DivStoredValue) {
|
||||
func set(value: DivStoredValue, cardId: DivCardID? = nil) {
|
||||
let key = storageKey(name: value.name, cardId: cardId)
|
||||
var items = storage.value.items
|
||||
items[value.name] = StoredValue(
|
||||
items[key] = StoredValue(
|
||||
timestamp: timestampProvider.value,
|
||||
value: value.value,
|
||||
type: value.type,
|
||||
@@ -34,10 +35,11 @@ public final class DivPersistentValuesStorage {
|
||||
storage.value = StoredValues(items: items)
|
||||
}
|
||||
|
||||
func get<T>(name: String) -> T? {
|
||||
func get<T>(name: String, cardId: DivCardID? = nil) -> T? {
|
||||
let key = storageKey(name: name, cardId: cardId)
|
||||
let items = storage.value.items
|
||||
let currentTimestamp = timestampProvider.value
|
||||
guard let storedValue = items[name] else {
|
||||
guard let storedValue = items[key] else {
|
||||
return nil
|
||||
}
|
||||
let elapsedTimeInSec = (currentTimestamp - storedValue.timestamp) / 1000
|
||||
@@ -57,6 +59,13 @@ public final class DivPersistentValuesStorage {
|
||||
return value
|
||||
}
|
||||
|
||||
func clearValues(forCardId cardId: DivCardID) {
|
||||
let prefix = cardScopedKeyPrefix(cardId: cardId)
|
||||
var items = storage.value.items
|
||||
items = items.filter { !$0.key.hasPrefix(prefix) }
|
||||
storage.value = StoredValues(items: items)
|
||||
}
|
||||
|
||||
func reset() {
|
||||
storage.value = StoredValues(items: [:])
|
||||
}
|
||||
@@ -74,10 +83,14 @@ public final class DivPersistentValuesStorage {
|
||||
}
|
||||
}
|
||||
|
||||
extension DivPersistentValuesStorage: Clearable {
|
||||
extension DivPersistentValuesStorage: Clearable {
|
||||
public func clear() {
|
||||
reset()
|
||||
}
|
||||
|
||||
public func clear(cardId: DivCardID) {
|
||||
clearValues(forCardId: cardId)
|
||||
}
|
||||
}
|
||||
|
||||
private struct StoredValues: Equatable, Codable {
|
||||
@@ -150,3 +163,14 @@ extension DivStoredValue {
|
||||
variable != nil
|
||||
}
|
||||
}
|
||||
|
||||
private func cardScopedKeyPrefix(cardId: DivCardID) -> String {
|
||||
"\(cardKeyPrefix)\(cardId.rawValue)"
|
||||
}
|
||||
private func storageKey(name: String, cardId: DivCardID?) -> String {
|
||||
guard let cardId else { // global scope
|
||||
return name
|
||||
}
|
||||
return "\(cardScopedKeyPrefix(cardId: cardId))\(name)"
|
||||
}
|
||||
private let cardKeyPrefix = "card_"
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import Foundation
|
||||
|
||||
public enum DivStoredValueScope: String, Hashable, Sendable, Decodable {
|
||||
case global
|
||||
case card
|
||||
}
|
||||
|
||||
struct DivStoredValue: Hashable {
|
||||
enum ValueType: String, Codable {
|
||||
case string
|
||||
|
||||
@@ -9,6 +9,7 @@ public final class DivActionSetStoredValue: Sendable {
|
||||
public let lifetime: Expression<Int>
|
||||
public let name: Expression<String>
|
||||
public let value: DivTypedValue
|
||||
public let scope: Expression<String>?
|
||||
|
||||
public func resolveLifetime(_ resolver: ExpressionResolver) -> Int? {
|
||||
resolver.resolveNumeric(lifetime)
|
||||
@@ -18,22 +19,35 @@ public final class DivActionSetStoredValue: Sendable {
|
||||
resolver.resolveString(name)
|
||||
}
|
||||
|
||||
public func resolveScope(_ resolver: ExpressionResolver) -> DivStoredValueScope {
|
||||
guard let scope else {
|
||||
return .global
|
||||
}
|
||||
guard let raw = resolver.resolveString(scope) else {
|
||||
return .global
|
||||
}
|
||||
return DivStoredValueScope(rawValue: raw.lowercased()) ?? .global
|
||||
}
|
||||
|
||||
public convenience init(dictionary: [String: Any], context: ParsingContext) throws {
|
||||
self.init(
|
||||
lifetime: try dictionary.getExpressionField("lifetime", context: context),
|
||||
name: try dictionary.getExpressionField("name", context: context),
|
||||
value: try dictionary.getField("value", transform: { (dict: [String: Any]) in try DivTypedValue(dictionary: dict, context: context) }, context: context)
|
||||
value: try dictionary.getField("value", transform: { (dict: [String: Any]) in try DivTypedValue(dictionary: dict, context: context) }, context: context),
|
||||
scope: try dictionary.getOptionalExpressionField("scope", context: context)
|
||||
)
|
||||
}
|
||||
|
||||
init(
|
||||
lifetime: Expression<Int>,
|
||||
name: Expression<String>,
|
||||
value: DivTypedValue
|
||||
value: DivTypedValue,
|
||||
scope: Expression<String>? = nil
|
||||
) {
|
||||
self.lifetime = lifetime
|
||||
self.name = name
|
||||
self.value = value
|
||||
self.scope = scope
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +57,8 @@ extension DivActionSetStoredValue: Equatable {
|
||||
guard
|
||||
lhs.lifetime == rhs.lifetime,
|
||||
lhs.name == rhs.name,
|
||||
lhs.value == rhs.value
|
||||
lhs.value == rhs.value,
|
||||
lhs.scope == rhs.scope
|
||||
else {
|
||||
return false
|
||||
}
|
||||
@@ -60,6 +75,9 @@ extension DivActionSetStoredValue: Serializable {
|
||||
result["lifetime"] = lifetime.toValidSerializationValue()
|
||||
result["name"] = name.toValidSerializationValue()
|
||||
result["value"] = value.toDictionary()
|
||||
if let scope {
|
||||
result["scope"] = scope.toValidSerializationValue()
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,15 @@ public final class DivActionSetStoredValueTemplate: TemplateValue, Sendable {
|
||||
public let lifetime: Field<Expression<Int>>?
|
||||
public let name: Field<Expression<String>>?
|
||||
public let value: Field<DivTypedValueTemplate>?
|
||||
public let scope: Field<Expression<String>>?
|
||||
|
||||
public convenience init(dictionary: [String: Any], templateToType: [TemplateName: String]) throws {
|
||||
self.init(
|
||||
parent: dictionary["type"] as? String,
|
||||
lifetime: dictionary.getOptionalExpressionField("lifetime"),
|
||||
name: dictionary.getOptionalExpressionField("name"),
|
||||
value: dictionary.getOptionalField("value", templateToType: templateToType)
|
||||
value: dictionary.getOptionalField("value", templateToType: templateToType),
|
||||
scope: dictionary.getOptionalExpressionField("scope")
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,22 +26,26 @@ public final class DivActionSetStoredValueTemplate: TemplateValue, Sendable {
|
||||
parent: String?,
|
||||
lifetime: Field<Expression<Int>>? = nil,
|
||||
name: Field<Expression<String>>? = nil,
|
||||
value: Field<DivTypedValueTemplate>? = nil
|
||||
value: Field<DivTypedValueTemplate>? = nil,
|
||||
scope: Field<Expression<String>>? = nil
|
||||
) {
|
||||
self.parent = parent
|
||||
self.lifetime = lifetime
|
||||
self.name = name
|
||||
self.value = value
|
||||
self.scope = scope
|
||||
}
|
||||
|
||||
private static func resolveOnlyLinks(context: TemplatesContext, parent: DivActionSetStoredValueTemplate?) -> DeserializationResult<DivActionSetStoredValue> {
|
||||
let lifetimeValue = { parent?.lifetime?.resolveValue(context: context) ?? .noValue }()
|
||||
let nameValue = { parent?.name?.resolveValue(context: context) ?? .noValue }()
|
||||
let valueValue = { parent?.value?.resolveValue(context: context, useOnlyLinks: true) ?? .noValue }()
|
||||
let scopeValue = { parent?.scope?.resolveOptionalValue(context: context) ?? .noValue }()
|
||||
var errors = mergeErrors(
|
||||
lifetimeValue.errorsOrWarnings?.map { .nestedObjectError(field: "lifetime", error: $0) },
|
||||
nameValue.errorsOrWarnings?.map { .nestedObjectError(field: "name", error: $0) },
|
||||
valueValue.errorsOrWarnings?.map { .nestedObjectError(field: "value", error: $0) }
|
||||
valueValue.errorsOrWarnings?.map { .nestedObjectError(field: "value", error: $0) },
|
||||
scopeValue.errorsOrWarnings?.map { .nestedObjectError(field: "scope", error: $0) }
|
||||
)
|
||||
if case .noValue = lifetimeValue {
|
||||
errors.append(.requiredFieldIsMissing(field: "lifetime"))
|
||||
@@ -60,7 +66,8 @@ public final class DivActionSetStoredValueTemplate: TemplateValue, Sendable {
|
||||
let result = DivActionSetStoredValue(
|
||||
lifetime: { lifetimeNonNil }(),
|
||||
name: { nameNonNil }(),
|
||||
value: { valueNonNil }()
|
||||
value: { valueNonNil }(),
|
||||
scope: { scopeValue.value }()
|
||||
)
|
||||
return errors.isEmpty ? .success(result) : .partialSuccess(result, warnings: NonEmptyArray(errors)!)
|
||||
}
|
||||
@@ -72,6 +79,7 @@ public final class DivActionSetStoredValueTemplate: TemplateValue, Sendable {
|
||||
var lifetimeValue: DeserializationResult<Expression<Int>> = { parent?.lifetime?.value() ?? .noValue }()
|
||||
var nameValue: DeserializationResult<Expression<String>> = { parent?.name?.value() ?? .noValue }()
|
||||
var valueValue: DeserializationResult<DivTypedValue> = .noValue
|
||||
var scopeValue: DeserializationResult<Expression<String>> = { parent?.scope?.value() ?? .noValue }()
|
||||
_ = {
|
||||
// Each field is parsed in its own lambda to keep the stack size managable
|
||||
// Otherwise the compiler will allocate stack for each intermediate variable
|
||||
@@ -92,6 +100,11 @@ public final class DivActionSetStoredValueTemplate: TemplateValue, Sendable {
|
||||
valueValue = deserialize(__dictValue, templates: context.templates, templateToType: context.templateToType, type: DivTypedValueTemplate.self).merged(with: valueValue)
|
||||
}
|
||||
}()
|
||||
_ = {
|
||||
if key == "scope" {
|
||||
scopeValue = deserialize(__dictValue).merged(with: scopeValue)
|
||||
}
|
||||
}()
|
||||
_ = {
|
||||
if key == parent?.lifetime?.link {
|
||||
lifetimeValue = lifetimeValue.merged(with: { deserialize(__dictValue) })
|
||||
@@ -107,15 +120,22 @@ public final class DivActionSetStoredValueTemplate: TemplateValue, Sendable {
|
||||
valueValue = valueValue.merged(with: { deserialize(__dictValue, templates: context.templates, templateToType: context.templateToType, type: DivTypedValueTemplate.self) })
|
||||
}
|
||||
}()
|
||||
_ = {
|
||||
if key == parent?.scope?.link {
|
||||
scopeValue = scopeValue.merged(with: { deserialize(__dictValue) })
|
||||
}
|
||||
}()
|
||||
}
|
||||
}()
|
||||
if let parent = parent {
|
||||
_ = { valueValue = valueValue.merged(with: { parent.value?.resolveValue(context: context, useOnlyLinks: true) }) }()
|
||||
_ = { scopeValue = scopeValue.merged(with: { parent.scope?.resolveOptionalValue(context: context) }) }()
|
||||
}
|
||||
var errors = mergeErrors(
|
||||
lifetimeValue.errorsOrWarnings?.map { .nestedObjectError(field: "lifetime", error: $0) },
|
||||
nameValue.errorsOrWarnings?.map { .nestedObjectError(field: "name", error: $0) },
|
||||
valueValue.errorsOrWarnings?.map { .nestedObjectError(field: "value", error: $0) }
|
||||
valueValue.errorsOrWarnings?.map { .nestedObjectError(field: "value", error: $0) },
|
||||
scopeValue.errorsOrWarnings?.map { .nestedObjectError(field: "scope", error: $0) }
|
||||
)
|
||||
if case .noValue = lifetimeValue {
|
||||
errors.append(.requiredFieldIsMissing(field: "lifetime"))
|
||||
@@ -136,7 +156,8 @@ public final class DivActionSetStoredValueTemplate: TemplateValue, Sendable {
|
||||
let result = DivActionSetStoredValue(
|
||||
lifetime: { lifetimeNonNil }(),
|
||||
name: { nameNonNil }(),
|
||||
value: { valueNonNil }()
|
||||
value: { valueNonNil }(),
|
||||
scope: { scopeValue.value }()
|
||||
)
|
||||
return errors.isEmpty ? .success(result) : .partialSuccess(result, warnings: NonEmptyArray(errors)!)
|
||||
}
|
||||
@@ -152,7 +173,8 @@ public final class DivActionSetStoredValueTemplate: TemplateValue, Sendable {
|
||||
parent: nil,
|
||||
lifetime: lifetime ?? mergedParent.lifetime,
|
||||
name: name ?? mergedParent.name,
|
||||
value: value ?? mergedParent.value
|
||||
value: value ?? mergedParent.value,
|
||||
scope: scope ?? mergedParent.scope
|
||||
)
|
||||
}
|
||||
|
||||
@@ -163,7 +185,8 @@ public final class DivActionSetStoredValueTemplate: TemplateValue, Sendable {
|
||||
parent: nil,
|
||||
lifetime: merged.lifetime,
|
||||
name: merged.name,
|
||||
value: try merged.value?.resolveParent(templates: templates)
|
||||
value: try merged.value?.resolveParent(templates: templates),
|
||||
scope: merged.scope
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,12 +118,10 @@ struct DivActionIntentTests {
|
||||
func setStoredValue_String() {
|
||||
#expect(
|
||||
intent("div-action://set_stored_value?name=var&value=value&type=string&lifetime=100") ==
|
||||
.setStoredValue(DivStoredValue(
|
||||
name: "var",
|
||||
value: "value",
|
||||
type: .string,
|
||||
lifetimeInSec: 100
|
||||
))
|
||||
.setStoredValue(
|
||||
DivStoredValue(name: "var", value: "value", type: .string, lifetimeInSec: 100),
|
||||
.global
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -131,12 +129,10 @@ struct DivActionIntentTests {
|
||||
func setStoredValue_Boolean() {
|
||||
#expect(
|
||||
intent("div-action://set_stored_value?name=var&value=true&type=boolean&lifetime=100") ==
|
||||
.setStoredValue(DivStoredValue(
|
||||
name: "var",
|
||||
value: "true",
|
||||
type: .boolean,
|
||||
lifetimeInSec: 100
|
||||
))
|
||||
.setStoredValue(
|
||||
DivStoredValue(name: "var", value: "true", type: .boolean, lifetimeInSec: 100),
|
||||
.global
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -144,12 +140,10 @@ struct DivActionIntentTests {
|
||||
func setStoredValue_Bool() {
|
||||
#expect(
|
||||
intent("div-action://set_stored_value?name=var&value=true&type=bool&lifetime=100") ==
|
||||
.setStoredValue(DivStoredValue(
|
||||
name: "var",
|
||||
value: "true",
|
||||
type: .bool,
|
||||
lifetimeInSec: 100
|
||||
))
|
||||
.setStoredValue(
|
||||
DivStoredValue(name: "var", value: "true", type: .bool, lifetimeInSec: 100),
|
||||
.global
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -173,6 +167,28 @@ struct DivActionIntentTests {
|
||||
intent("div-action://set_stored_value?name=var&value=value&type=string") == nil
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func setStoredValue_ScopeGlobal() {
|
||||
#expect(
|
||||
intent("div-action://set_stored_value?name=var&value=v&type=string&lifetime=100&scope=global") ==
|
||||
.setStoredValue(
|
||||
DivStoredValue(name: "var", value: "v", type: .string, lifetimeInSec: 100),
|
||||
.global
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func setStoredValue_ScopeCard() {
|
||||
#expect(
|
||||
intent("div-action://set_stored_value?name=var&value=v&type=string&lifetime=100&scope=card") ==
|
||||
.setStoredValue(
|
||||
DivStoredValue(name: "var", value: "v", type: .string, lifetimeInSec: 100),
|
||||
.card
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func intent(_ url: String) -> DivActionIntent? {
|
||||
|
||||
@@ -14,7 +14,7 @@ final class SetStoredValueActionHandlerTests: XCTestCase {
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
persistentValuesStorage.reset()
|
||||
persistentValuesStorage.clear()
|
||||
}
|
||||
|
||||
func test_SetStringValue() {
|
||||
@@ -134,13 +134,60 @@ final class SetStoredValueActionHandlerTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
private func handle(_ action: DivActionSetStoredValue) {
|
||||
func test_SetCardScopedValue() {
|
||||
handle(
|
||||
action(
|
||||
name: "name",
|
||||
value: .stringValue(StringValue(value: .value("card value"))),
|
||||
scope: .card
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
persistentValuesStorage.get(name: "name", cardId: cardId),
|
||||
"card value"
|
||||
)
|
||||
XCTAssertNil(persistentValuesStorage.get(name: "name", cardId: nil))
|
||||
}
|
||||
|
||||
func test_SetCardScopedValue_IsolatedPerPathCardId() {
|
||||
let otherCard = DivCardID(rawValue: "other_stored_card")
|
||||
let setName = "per_path_key"
|
||||
handle(
|
||||
action(
|
||||
name: setName,
|
||||
value: .stringValue(StringValue(value: .value("from_default"))),
|
||||
scope: .card
|
||||
),
|
||||
path: cardId.path
|
||||
)
|
||||
handle(
|
||||
action(
|
||||
name: setName,
|
||||
value: .stringValue(StringValue(value: .value("from_other"))),
|
||||
scope: .card
|
||||
),
|
||||
path: otherCard.path
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
persistentValuesStorage.get(name: setName, cardId: cardId) as String?,
|
||||
"from_default"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
persistentValuesStorage.get(name: setName, cardId: otherCard) as String?,
|
||||
"from_other"
|
||||
)
|
||||
XCTAssertNil(persistentValuesStorage.get(name: setName, cardId: nil) as String?)
|
||||
}
|
||||
|
||||
private func handle(_ action: DivActionSetStoredValue, path: UIElementPath = cardId.path) {
|
||||
handler.handle(
|
||||
divAction(
|
||||
logId: "action_id",
|
||||
typed: .divActionSetStoredValue(action)
|
||||
),
|
||||
path: cardId.path,
|
||||
path: path,
|
||||
source: .callback,
|
||||
sender: nil
|
||||
)
|
||||
@@ -152,12 +199,21 @@ private let cardId = DivBlockModelingContext.testCardId
|
||||
private func action(
|
||||
name: String,
|
||||
value: DivTypedValue,
|
||||
lifetime: Int = 1000
|
||||
lifetime: Int = 1000,
|
||||
scope: DivStoredValueScope? = nil
|
||||
) -> DivActionSetStoredValue {
|
||||
DivActionSetStoredValue(
|
||||
lifetime: .value(lifetime),
|
||||
name: .value(name),
|
||||
value: value
|
||||
value: value,
|
||||
scope: scope.map {
|
||||
switch $0 {
|
||||
case .global:
|
||||
return .value("global")
|
||||
case .card:
|
||||
return .value("card")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,44 @@ final class DivPersistentValuesStorageTests: XCTestCase {
|
||||
XCTAssertNil(stored)
|
||||
}
|
||||
|
||||
func test_cardScope_IsolatedByCardId() {
|
||||
let cardA = DivCardID(rawValue: "card-a")
|
||||
let cardB = DivCardID(rawValue: "card-b")
|
||||
storage.set(
|
||||
value: DivStoredValue(name: "k", value: "a", type: .string, lifetimeInSec: 86400),
|
||||
cardId: cardA
|
||||
)
|
||||
storage.set(
|
||||
value: DivStoredValue(name: "k", value: "b", type: .string, lifetimeInSec: 86400),
|
||||
cardId: cardB
|
||||
)
|
||||
|
||||
XCTAssertEqual(storage.get(name: "k", cardId: cardA), "a")
|
||||
XCTAssertEqual(storage.get(name: "k", cardId: cardB), "b")
|
||||
XCTAssertNil(storage.get(name: "k", cardId: nil))
|
||||
}
|
||||
|
||||
func test_globalScope_UsesPlainKey_BackwardsCompatible() {
|
||||
storage.set(
|
||||
value: DivStoredValue(name: "k", value: "g", type: .string, lifetimeInSec: 86400)
|
||||
)
|
||||
XCTAssertEqual(storage.get(name: "k"), "g")
|
||||
}
|
||||
|
||||
func test_clearForCardId_RemovesOnlyCardScopedKeys() {
|
||||
let cardId = DivCardID(rawValue: "c1")
|
||||
storage.set(value: DivStoredValue(name: "g", value: "global", type: .string, lifetimeInSec: 86400))
|
||||
storage.set(
|
||||
value: DivStoredValue(name: "x", value: "card", type: .string, lifetimeInSec: 86400),
|
||||
cardId: cardId
|
||||
)
|
||||
|
||||
storage.clear(cardId: cardId)
|
||||
|
||||
XCTAssertEqual(storage.get(name: "g"), "global")
|
||||
XCTAssertNil(storage.get(name: "x", cardId: cardId))
|
||||
}
|
||||
|
||||
private func makeStorage() -> DivPersistentValuesStorage {
|
||||
DivPersistentValuesStorage(
|
||||
timestampProvider: Variable { self.currentTimestamp }
|
||||
|
||||
@@ -13,7 +13,8 @@ struct ExpressionResolverAnyTests {
|
||||
|
||||
lazy var expressionResolver = ExpressionResolver(
|
||||
functionsProvider: FunctionsProvider(
|
||||
persistentValuesStorage: DivPersistentValuesStorage()
|
||||
persistentValuesStorage: DivPersistentValuesStorage(),
|
||||
cardId: nil
|
||||
),
|
||||
customFunctionsStorageProvider: { _ in nil },
|
||||
variableValueProvider: { [unowned self] in
|
||||
|
||||
@@ -26,7 +26,8 @@ struct ExpressionResolverTests {
|
||||
|
||||
lazy var expressionResolver = ExpressionResolver(
|
||||
functionsProvider: FunctionsProvider(
|
||||
persistentValuesStorage: DivPersistentValuesStorage()
|
||||
persistentValuesStorage: DivPersistentValuesStorage(),
|
||||
cardId: nil
|
||||
),
|
||||
customFunctionsStorageProvider: { _ in nil },
|
||||
variableValueProvider: { [unowned self] in
|
||||
@@ -575,6 +576,58 @@ struct ExpressionResolverTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("GetStoredCardScope")
|
||||
struct GetStoredCardScopeExpressionTests {
|
||||
private let cardA = DivCardID(rawValue: "stored_scope_card_a")
|
||||
private let cardB = DivCardID(rawValue: "stored_scope_card_b")
|
||||
|
||||
@Test
|
||||
func getStoredStringValue_cardScope_usesFunctionsProviderCardId() {
|
||||
let storage = DivPersistentValuesStorage()
|
||||
defer { storage.clear() }
|
||||
storage.set(
|
||||
value: DivStoredValue(name: "k", value: "alpha", type: .string, lifetimeInSec: 1000),
|
||||
cardId: cardA
|
||||
)
|
||||
storage.set(
|
||||
value: DivStoredValue(name: "k", value: "beta", type: .string, lifetimeInSec: 1000),
|
||||
cardId: cardB
|
||||
)
|
||||
|
||||
let expr = "@{getStoredStringValue('k', 'card', 'fb')}"
|
||||
#expect(resolver(storage: storage, cardId: cardA).resolveString(expression(expr)) == "alpha")
|
||||
#expect(resolver(storage: storage, cardId: cardB).resolveString(expression(expr)) == "beta")
|
||||
}
|
||||
|
||||
@Test
|
||||
func getStoredStringValue_globalScope_readsSameKeyForBothCardProviders() {
|
||||
let storage = DivPersistentValuesStorage()
|
||||
defer { storage.clear() }
|
||||
storage.set(
|
||||
value: DivStoredValue(name: "g", value: "global_only", type: .string, lifetimeInSec: 1000)
|
||||
)
|
||||
|
||||
let ternaryGlobal = "@{getStoredStringValue('g', 'global', 'fb')}"
|
||||
#expect(resolver(storage: storage, cardId: cardA).resolveString(expression(ternaryGlobal)) == "global_only")
|
||||
#expect(resolver(storage: storage, cardId: cardB).resolveString(expression(ternaryGlobal)) == "global_only")
|
||||
|
||||
let binaryImplicitGlobal = "@{getStoredStringValue('g', 'fb')}"
|
||||
#expect(resolver(storage: storage, cardId: cardA).resolveString(expression(binaryImplicitGlobal)) == "global_only")
|
||||
}
|
||||
|
||||
private func resolver(storage: DivPersistentValuesStorage, cardId: DivCardID) -> ExpressionResolver {
|
||||
ExpressionResolver(
|
||||
functionsProvider: FunctionsProvider(
|
||||
persistentValuesStorage: storage,
|
||||
cardId: cardId
|
||||
),
|
||||
customFunctionsStorageProvider: { _ in nil },
|
||||
variableValueProvider: { _ in nil },
|
||||
errorTracker: { _ in }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func expression(_ expression: String) -> ExpressionLink<String> {
|
||||
ExpressionLink<String>(rawValue: expression)!
|
||||
}
|
||||
|
||||
@@ -119,7 +119,10 @@ private struct ExpressionTestCase: Decodable {
|
||||
}
|
||||
self.variables = variables.extractDivVariableValues(
|
||||
ExpressionResolver(
|
||||
functionsProvider: FunctionsProvider(persistentValuesStorage: DivPersistentValuesStorage()),
|
||||
functionsProvider: FunctionsProvider(
|
||||
persistentValuesStorage: DivPersistentValuesStorage(),
|
||||
cardId: nil
|
||||
),
|
||||
customFunctionsStorageProvider: { _ in nil },
|
||||
variableValueProvider: { _ in nil },
|
||||
errorTracker: { XCTFail($0.description) }
|
||||
@@ -161,7 +164,8 @@ private struct ExpressionTestCase: Decodable {
|
||||
errorTracker: ExpressionErrorTracker? = nil
|
||||
) -> ExpressionResolver {
|
||||
let functionsProvider = FunctionsProvider(
|
||||
persistentValuesStorage: DivPersistentValuesStorage()
|
||||
persistentValuesStorage: DivPersistentValuesStorage(),
|
||||
cardId: nil
|
||||
)
|
||||
for function in functions {
|
||||
functionsProvider.functions.addFunction(function.name, function)
|
||||
|
||||
@@ -24,7 +24,8 @@ private func makeTestCases() -> [(String, SignatureTestCase)] {
|
||||
|
||||
private func runTest(_ testCase: SignatureTestCase) {
|
||||
let functionsProvider = FunctionsProvider(
|
||||
persistentValuesStorage: DivPersistentValuesStorage()
|
||||
persistentValuesStorage: DivPersistentValuesStorage(),
|
||||
cardId: nil
|
||||
)
|
||||
let functionName = testCase.functionName
|
||||
let functions = testCase.isMethod ? FunctionsProvider.methods : functionsProvider.functions
|
||||
|
||||
@@ -460,7 +460,8 @@
|
||||
],
|
||||
"return_type": "integer",
|
||||
"platforms": [
|
||||
"android"
|
||||
"android",
|
||||
"ios"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -517,7 +518,8 @@
|
||||
],
|
||||
"return_type": "number",
|
||||
"platforms": [
|
||||
"android"
|
||||
"android",
|
||||
"ios"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -574,7 +576,8 @@
|
||||
],
|
||||
"return_type": "string",
|
||||
"platforms": [
|
||||
"android"
|
||||
"android",
|
||||
"ios"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -631,7 +634,8 @@
|
||||
],
|
||||
"return_type": "url",
|
||||
"platforms": [
|
||||
"android"
|
||||
"android",
|
||||
"ios"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -688,7 +692,8 @@
|
||||
],
|
||||
"return_type": "url",
|
||||
"platforms": [
|
||||
"android"
|
||||
"android",
|
||||
"ios"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -745,7 +750,8 @@
|
||||
],
|
||||
"return_type": "color",
|
||||
"platforms": [
|
||||
"android"
|
||||
"android",
|
||||
"ios"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -802,7 +808,8 @@
|
||||
],
|
||||
"return_type": "color",
|
||||
"platforms": [
|
||||
"android"
|
||||
"android",
|
||||
"ios"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -859,7 +866,8 @@
|
||||
],
|
||||
"return_type": "boolean",
|
||||
"platforms": [
|
||||
"android"
|
||||
"android",
|
||||
"ios"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -903,7 +911,8 @@
|
||||
],
|
||||
"return_type": "array",
|
||||
"platforms": [
|
||||
"android"
|
||||
"android",
|
||||
"ios"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -947,7 +956,8 @@
|
||||
],
|
||||
"return_type": "dict",
|
||||
"platforms": [
|
||||
"android"
|
||||
"android",
|
||||
"ios"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user