supported get/set stored values by card_id

commit_hash:d7fcc6410470b5d9b6830747c6e106da972bf713
This commit is contained in:
tabaina
2026-04-30 18:20:45 +03:00
parent fdcf66d842
commit 8e868feec9
21 changed files with 433 additions and 87 deletions
@@ -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
+20 -10
View File
@@ -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"
]
}
]