Files
divkit/client/ios/DivKit/Expressions/ExpressionResolver.swift
T
denlvovich ecb30a7aea Fixed a bug with resolving custom functions
commit_hash:f8b56a2141dd492d8db216704023bc9e56d6e626
2025-10-08 11:55:50 +03:00

330 lines
9.9 KiB
Swift

import Foundation
import LayoutKit
import VGSL
public typealias ExpressionErrorTracker = (ExpressionError) -> Void
public final class ExpressionResolver {
private let customFunctionsStorageProvider: (String) -> DivFunctionsStorage?
private let functionsProvider: FunctionsProvider
private let variableValueProvider: (String) -> Any?
private let errorTracker: ExpressionErrorTracker
private var resolutionStack: Set<String> = []
private lazy var context = ExpressionContext(
evaluators: functionsProvider.evaluators,
variableValueProvider: { [weak self] variableName in
guard let self else { return nil }
if self.resolutionStack.contains(variableName) {
return nil
}
self.resolutionStack.insert(variableName)
defer { self.resolutionStack.remove(variableName) }
return self.variableValueProvider(variableName)
},
customFunctionsStorageProvider: customFunctionsStorageProvider,
dynamicVariablesEvaluator: functionsProvider.dynamicVariablesEvaluator,
errorTracker: errorTracker
)
@_spi(Legacy)
public convenience init(
variableValueProvider: @escaping (String) -> Any?,
persistentValuesStorage: DivPersistentValuesStorage = DivPersistentValuesStorage(),
errorTracker: ExpressionErrorTracker? = nil
) {
self.init(
functionsProvider: FunctionsProvider(
persistentValuesStorage: persistentValuesStorage
),
customFunctionsStorageProvider: { _ in nil },
variableValueProvider: variableValueProvider,
errorTracker: { errorTracker?($0) }
)
}
init(
functionsProvider: FunctionsProvider,
customFunctionsStorageProvider: @escaping (String) -> DivFunctionsStorage?,
variableValueProvider: @escaping (String) -> Any?,
errorTracker: @escaping ExpressionErrorTracker
) {
self.functionsProvider = functionsProvider
self.customFunctionsStorageProvider = customFunctionsStorageProvider
self.variableValueProvider = variableValueProvider
self.errorTracker = errorTracker
}
init(
path: UIElementPath,
variablesStorage: DivVariablesStorage,
functionsStorage: DivFunctionsStorage?,
persistentValuesStorage: DivPersistentValuesStorage,
reporter: DivReporter
) {
self.functionsProvider = FunctionsProvider(
persistentValuesStorage: persistentValuesStorage
)
self.customFunctionsStorageProvider = {
functionsStorage?.getStorage(path: path, contains: $0)
}
self.variableValueProvider = {
let variableName = DivVariableName(rawValue: $0)
return variablesStorage.getVariableValue(path: path, name: variableName)
}
self.errorTracker = reporter.asExpressionErrorTracker(cardId: path.cardId)
}
public func resolve(_ expression: String) -> Any? {
if let link: ExpressionLink<Any> = makeLink(expression) {
return resolveAnyLink(link)
}
return nil
}
public func resolveString(_ expression: String) -> String? {
resolveString(expression, initializer: { $0 })
}
public func resolveColor(_ expression: String) -> Color? {
resolveString(expression, initializer: Color.color(withHexString:))
}
public func resolveUrl(_ expression: String) -> URL? {
resolveString(expression, initializer: URL.init(string:))
}
public func resolveEnum<T: RawRepresentable>(
_ expression: String
) -> T? where T.RawValue == String {
resolveString(expression, initializer: T.init(rawValue:))
}
public func resolveNumeric<T>(_ expression: String) -> T? {
if let link: ExpressionLink<T> = makeLink(expression) {
return resolveNumeric(.link(link))
}
return nil
}
func resolveString(_ expression: Expression<String>?) -> String? {
resolveString(expression, initializer: { $0 })
}
func resolveColor(_ expression: Expression<Color>?) -> Color? {
resolveString(expression, initializer: Color.color(withHexString:))
}
func resolveUrl(_ expression: Expression<URL>?) -> URL? {
resolveString(expression, initializer: URL.init(string:))
}
func resolveEnum<T: RawRepresentable>(
_ expression: Expression<T>?
) -> T? where T.RawValue == String {
resolveString(expression, initializer: T.init(rawValue:))
}
func resolveNumeric<T>(_ expression: Expression<T>?) -> T? {
switch expression {
case let .value(value):
value
case let .link(link):
resolveNotStringBasedLink(link)
case .none:
nil
}
}
func resolveArray(_ expression: Expression<[Any]>?) -> [Any]? {
resolveNumeric(expression)
}
func resolveDict(_ expression: Expression<[String: Any]>?) -> [String: Any]? {
resolveNumeric(expression)
}
func extractDynamicVariables(_ link: ExpressionLink<some Any>) -> [String] {
link.extractDynamicVariableNames(context)
}
private func resolveAnyLink(_ link: ExpressionLink<Any>) -> Any? {
var result: Any?
func appendResult(_ value: Any?) {
guard let value else {
return
}
if let prevResult = result {
result = ExpressionValueConverter.stringify(prevResult)
+ ExpressionValueConverter.stringify(value)
} else {
result = value
}
}
for item in link.items {
switch item {
case let .calcExpression(expression):
do {
try appendResult(evaluate(expression))
} catch {
errorTracker(ExpressionError(error.localizedDescription, expression: link.rawValue))
return nil
}
case let .string(value):
appendResult(value)
case let .nestedExpression(expression):
if let nestedLink: ExpressionLink<Any> = resolveNestedLink(expression) {
appendResult(resolveAnyLink(nestedLink))
}
}
}
return result
}
private func resolveNotStringBasedLink<T>(_ link: ExpressionLink<T>) -> T? {
guard link.items.count == 1, let item = link.items.first else {
errorTracker(ExpressionError(
"Not string based expression expected",
expression: link.rawValue
))
return nil
}
switch item {
case let .calcExpression(expression):
do {
return try validate(evaluate(expression), link: link)
} catch {
errorTracker(ExpressionError(error.localizedDescription, expression: link.rawValue))
}
case let .nestedExpression(expression):
if let nestedLink: ExpressionLink<T> = resolveNestedLink(expression) {
return validate(resolveNotStringBasedLink(nestedLink), link: link)
}
case .string:
errorTracker(ExpressionError("Expression expected", expression: link.rawValue))
}
return nil
}
private func resolveStringBasedLink<T>(
_ link: ExpressionLink<T>,
initializer: (String) -> T? = { $0 }
) -> T? {
var stringValue = ""
for item in link.items {
switch item {
case let .calcExpression(expression):
do {
if let value: String = try evaluate(expression) {
stringValue += value
}
} catch {
errorTracker(ExpressionError(error.localizedDescription, expression: link.rawValue))
return nil
}
case let .string(value):
stringValue += value
case let .nestedExpression(expression):
if let nestedLink: ExpressionLink<String> = resolveNestedLink(expression),
let value: String = resolveStringBasedLink(nestedLink) {
stringValue += value
}
}
}
if let value = initializer(stringValue) {
return validate(value, link: link)
}
errorTracker(ExpressionError(
"Failed to initialize \(formatTypeForError(T.self)) from string: \(stringValue).",
expression: link.rawValue
))
return nil
}
private func resolveNestedLink<T>(_ nestedLink: ExpressionLink<String>) -> ExpressionLink<T>? {
guard let expressionString: String = resolveStringBasedLink(nestedLink) else {
return nil
}
return ExpressionLink<T>(
rawValue: "@{\(expressionString)}",
errorTracker: errorTracker,
resolveNested: false
)
}
private func evaluate<T>(_ expression: CalcExpression) throws -> T? {
let value = try expression.evaluate(context)
if let castedValue: T = ExpressionValueConverter.cast(value) {
return castedValue
}
throw ExpressionError(
"Invalid result type: expected \(formatTypeForError(T.self)), got \(formatTypeForError(value))."
)
}
private func validate<T>(_ value: T?, link: ExpressionLink<T>) -> T? {
guard let validator = link.validator, let value else {
return value
}
if validator.isValid(value) {
return value
}
errorTracker(ExpressionError("Failed to validate value: \(value).", expression: link.rawValue))
return nil
}
private func makeLink<T>(_ expression: String) -> ExpressionLink<T>? {
ExpressionLink<T>(rawValue: expression, errorTracker: errorTracker)
}
private func resolveString<T>(
_ expression: String,
initializer: (String) -> T?
) -> T? {
if let link: ExpressionLink<T> = makeLink(expression) {
return resolveStringBasedLink(link, initializer: initializer)
}
return initializer(expression)
}
private func resolveString<T>(
_ expression: Expression<T>?,
initializer: (String) -> T?
) -> T? {
switch expression {
case let .value(value):
if let stringValue = value as? String {
return ExpressionValueConverter.unescape(stringValue, errorTracker: errorTracker) as? T
}
return value
case let .link(link):
return resolveStringBasedLink(link, initializer: initializer)
case .none:
return nil
}
}
}
extension ExpressionResolver {
func modifying(variableValueProvider: @escaping (String) -> Any?) -> ExpressionResolver {
ExpressionResolver(
functionsProvider: functionsProvider,
customFunctionsStorageProvider: customFunctionsStorageProvider,
variableValueProvider: {
variableValueProvider($0) ?? self.variableValueProvider($0)
},
errorTracker: errorTracker
)
}
}