Files
divkit/client/ios/Core/BaseTiny/JSONObject.swift
T
morevsavva 62ead6a8ee Add blur and tint mode implementation.
Add blur and tint mode implementation
2022-11-14 22:16:48 +03:00

560 lines
15 KiB
Swift

// Copyright 2021 Yandex LLC. All rights reserved.
import Foundation
public typealias JSONDictionary = JSONObject.Object
public enum JSONObject: Codable, Equatable {
public enum Error: Swift.Error {
case invalidPathComponent(String)
case noValueFound(subpath: String)
case typeMismatch(subpath: String, expected: String, received: String)
}
public typealias Array = [JSONObject]
public typealias Object = [String: JSONObject]
case bool(Bool)
case number(Double)
case string(String)
case null
indirect case array(Array)
indirect case object(JSONDictionary)
public static let `true` = JSONObject.bool(true)
public static let `false` = JSONObject.bool(false)
@inlinable
public static func number<IntType: BinaryInteger>(_ integer: IntType) -> Self {
.number(Double(integer))
}
private init?(untypedScalar: Any?) {
if let number = untypedScalar as? NSNumber {
if number === kCFBooleanTrue {
self = .bool(true)
} else if number === kCFBooleanFalse {
self = .bool(false)
} else {
self = .number(number.doubleValue)
}
} else if untypedScalar is NSNull {
self = .null
} else if let string = untypedScalar as? String {
self = .string(string)
} else if let json = untypedScalar as? JSONObject {
self = json
} else {
return nil
}
}
private init?(singleValueContainer: SingleValueDecodingContainer) {
if singleValueContainer.decodeNil() {
self = .null
return
}
let value: Any? = (try? singleValueContainer.decode(Double.self))
?? (try? singleValueContainer.decode(Bool.self))
?? (try? singleValueContainer.decode(String.self))
if let obj = JSONObject(untypedScalar: value) {
self = obj
} else {
return nil
}
}
public init(from decoder: Decoder) throws {
if let container = try? decoder.singleValueContainer(),
let result = JSONObject(singleValueContainer: container) {
self = result
} else if var array = try? decoder.unkeyedContainer() {
var result: [JSONObject] = []
while !array.isAtEnd {
result.append(try array.decode(JSONObject.self))
}
self = .array(result)
} else if let obj = try? decoder.container(keyedBy: StringCodingKey.self) {
var result: [String: JSONObject] = [:]
try obj.allKeys.forEach {
result[$0.stringValue] = try obj.decode(JSONObject.self, forKey: $0)
}
self = .object(result)
} else {
throw DecodingError
.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: ""))
}
}
public func encode(to encoder: Encoder) throws {
switch self {
case let .bool(bool):
var container = encoder.singleValueContainer()
try container.encode(bool)
case let .number(number):
var container = encoder.singleValueContainer()
try container.encode(number)
case let .string(string):
var container = encoder.singleValueContainer()
try container.encode(string)
case let .array(array):
var container = encoder.unkeyedContainer()
try array.forEach { try container.encode($0) }
case let .object(object):
var container = encoder.container(keyedBy: StringCodingKey.self)
try object.forEach {
try container.encode(
$0.value,
forKey: StringCodingKey(stringValue: $0.key)
)
}
case .null:
var container = encoder.singleValueContainer()
try container.encodeNil()
}
}
}
extension JSONObject {
private var shortDescription: String {
switch self {
case let .bool(bool):
return "bool: \(bool)"
case let .number(number):
return "number: \(number)"
case let .string(string):
return "string: \(string)"
case .array:
return "array"
case .object:
return "object"
case .null:
return "null"
}
}
private func value<C>(
atPath path: C,
subpath: String = ""
) throws -> JSONObject
where C: Collection, C.Element == Path.Component {
guard !path.isEmpty else {
return self
}
let currentComponent = path.first!
let nextSubpath = subpath + currentComponent.stringValue
switch (self, currentComponent) {
case let (.object(dict), .key(key)):
guard let enclosedValue = dict[key] else {
throw Error.noValueFound(subpath: nextSubpath)
}
return try enclosedValue.value(
atPath: Swift.Array(path.dropFirst()),
subpath: nextSubpath
)
case let (.array(array), .index(index)):
guard index < array.count else {
throw Error.noValueFound(subpath: nextSubpath)
}
return try array[index].value(
atPath: Swift.Array(path.dropFirst()),
subpath: nextSubpath
)
case (.object, _),
(.array, _),
(.bool, _),
(.number, _),
(.string, _),
(.null, _):
throw Error.typeMismatch(
subpath: nextSubpath,
expected: currentComponent.expectedType,
received: shortDescription
)
}
}
public func value(atPath path: Path) throws -> JSONObject {
try value(atPath: path.components)
}
public func value(atPath path: String) throws -> JSONObject {
try value(atPath: Path(string: path))
}
public func getString(atPath path: String) throws -> String {
let val = try value(atPath: Path.Component.components(from: path))
guard case let .string(string) = val else {
throw Error.typeMismatch(
subpath: path,
expected: "String",
received: val.shortDescription
)
}
return string
}
}
extension JSONObject {
public var untypedValue: Any {
switch self {
case let .bool(value):
return NSNumber(value: value)
case let .number(value):
return value as NSNumber
case let .string(value):
return value
case let .array(array):
return array.map { $0.untypedValue }
case let .object(dict):
return dict.mapValues {
$0.untypedValue
}
case .null:
return NSNull()
}
}
public func makeDictionary() -> [String: Any]? {
untypedValue as? [String: Any]
}
fileprivate init?(
value: Any,
fallback: (Any) -> JSONObject?,
errorHandler: (Any) throws -> Void
) rethrows {
if let result = JSONObject(untypedScalar: value) {
self = result
} else if let array = value as? [Any] {
self = try JSONObject(array: array, fallback: fallback, errorHandler: errorHandler)
} else if let dict = value as? [String: Any] {
self = try JSONObject(
dictionary: dict,
fallback: fallback,
errorHandler: errorHandler
)
} else if let fallbackResult = fallback(value) {
self = fallbackResult
} else {
try errorHandler(value)
return nil
}
}
private init(
array: [Any],
fallback: (Any) -> JSONObject?,
errorHandler: (Any) throws -> Void
) rethrows {
self = .array(try array.compactMap {
try JSONObject(value: $0, fallback: fallback, errorHandler: errorHandler)
})
}
public static func from(untypedValue: Any) -> JSONObject? {
from(untypedValue: untypedValue, fallback: { _ in nil })
}
public static func from(
untypedValue: Any,
fallback: (Any) -> JSONObject?
) -> JSONObject? {
(try? JSONObject(
value: untypedValue,
fallback: fallback,
errorHandler: {
throw Error.typeMismatch(
subpath: "N/A",
expected: "Valid JSON value",
received: "\($0)"
)
}
)).flatMap { $0 }
}
public init(
dictionary: [String: Any],
fallback: (Any) -> JSONObject? = { _ in nil },
errorHandler: (Any) throws -> Void = { _ in }
) rethrows {
self = .object(try dictionary.typedJSON(fallback: fallback, errorHandler: errorHandler))
}
}
extension JSONObject: CustomStringConvertible {
private func indentedDescription(indent: String) -> String {
switch self {
case let .bool(value):
return "\(value)"
case let .number(num):
return "\(num)"
case let .string(string):
return string
case let .array(array):
let contents = array
.map { $0.indentedDescription(indent: indent + " ") }
.joined(separator: ",\n")
return "\n\(indent)[\n" + contents + "\n\(indent)]"
case let .object(dict):
let contents = dict
.map { "\(indent + " ")\($0.key): \($0.value.indentedDescription(indent: indent + " "))" }
.joined(separator: ",\n")
return "\(indent){\n" + contents + "\n\(indent)}"
case .null:
return indent + "null"
}
}
public var description: String {
indentedDescription(indent: "")
}
}
extension Dictionary where Key == String {
public func typedJSON(fallback: (Any) -> JSONObject? = { _ in nil }) -> [String: JSONObject] {
typedJSON(fallback: fallback, errorHandler: { _ in })
}
public func typedJSON(
fallback: (Any) -> JSONObject?,
errorHandler: (Any) throws -> Void
) rethrows
-> [String: JSONObject] {
let keyValueArray: [(String, JSONObject)] = try compactMap {
let key = $0.key
return try JSONObject(
value: $0.value,
fallback: fallback,
errorHandler: errorHandler
).map { (key, $0) }
}
return [String: JSONObject](keyValueArray, uniquingKeysWith: { $1 })
}
}
extension Dictionary where Key == String, Value == JSONObject {
public func untypedJSON() -> [String: Any] {
mapValues {
$0.untypedValue
}
}
}
extension Dictionary where Key == String, Value == JSONObject {
public func value(atPath path: JSONObject.Path) throws -> JSONObject {
try JSONObject.object(self).value(atPath: path)
}
}
extension Dictionary where Key == String, Value == JSONObject {
public func flatten() -> JSONDictionary {
reduce(into: [:]) { result, entry in
let (key, value) = entry
if case let .object(dict) = value {
dict.flatten().forEach { result[key + "." + $0] = $1 }
} else {
result[key] = value
}
}
}
}
extension JSONObject {
public struct Path: Codable, CustomDebugStringConvertible, ExpressibleByStringLiteral, Hashable {
public typealias Component = JSONPathComponent
public private(set) var components: [Component]
public private(set) var asString: String
private var asConcatinationSuffix: String {
switch components.first {
case .none:
return ""
case .key:
return "." + asString
case .index:
return asString
}
}
public var isEmpty: Bool {
components.isEmpty
}
private init(
components: [Component],
string: String
) {
self.components = components
self.asString = string
}
public init(component: Component) {
self.components = [component]
asString = component.stringValue
}
public init(components: [Component]) {
self.components = components
asString = Self.stringify(components: components)
}
public init(string: String) throws {
components = try Component.components(from: string)
asString = string
}
public init(stringLiteral value: String) {
try! self.init(string: value)
}
public init(from decoder: Decoder) throws {
try self.init(string: try String(from: decoder))
}
public static let empty = Self(components: [], string: "")
public func encode(to encoder: Encoder) throws {
try asString.encode(to: encoder)
}
public func appending(path: Path) -> Path {
if components.isEmpty {
return path
}
return Path(
components: components + path.components,
string: asString + path.asConcatinationSuffix
)
}
public mutating func append(path: Path) {
if components.isEmpty {
components = path.components
asString = path.asString
} else {
asString.append(path.asConcatinationSuffix)
components.append(contentsOf: path.components)
}
}
public func appending(component: Component) -> Path {
var p = self
p.append(component: component)
return p
}
public mutating func append(component: Component) {
if components.isEmpty {
components.append(component)
asString = component.stringValue
} else {
switch component {
case let .key(key):
asString.append(".")
asString.append(key)
case let .index(index):
asString.append("[")
asString.append(String(index))
asString.append("]")
}
components.append(component)
}
}
private static func stringify(components: [Component]) -> String {
components
.reduce(into: [String]()) { partialResult, component in
switch component {
case let .index(value):
partialResult.append("[\(value)]")
case let .key(value):
let prefix = partialResult.isEmpty ? "" : "."
partialResult.append("\(prefix)\(value)")
}
}
.joined()
}
public var debugDescription: String { asString }
}
}
extension Data {
public func decodeJSON() throws -> JSONObject {
try JSONDecoder().decode(JSONObject.self, from: self)
}
}
private struct StringCodingKey: CodingKey {
let stringValue: String
init(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { nil }
public init?(intValue _: Int) { nil }
}
public enum JSONPathComponent: Hashable {
case key(String)
case index(Int)
var stringValue: String {
switch self {
case let .index(index):
return "[\(index)]"
case let .key(key):
return key
}
}
var expectedType: String {
switch self {
case .key:
return "Object"
case .index:
return "Array"
}
}
static func components(from string: String) throws -> [Self] {
if string.isEmpty {
return []
}
var result: [Self] = []
try string.components(separatedBy: ".").forEach { component in
let error = JSONObject.Error.invalidPathComponent(component)
let subscriptComponents = component.components(separatedBy: "]")
if subscriptComponents.count > 1 {
for `subscript` in subscriptComponents.filter({ !$0.isEmpty }) {
let components = `subscript`.components(separatedBy: "[")
guard components.count == 2,
let int = Int(components[1]) else {
throw error
}
if !components[0].isEmpty {
result.append(.key(components[0]))
}
result.append(.index(int))
}
} else {
guard !component.isEmpty else {
throw error
}
result.append(.key(component))
}
}
return result
}
}