270 lines
7.7 KiB
Swift
270 lines
7.7 KiB
Swift
//
|
|
// Object.swift
|
|
// ObjectUI
|
|
//
|
|
// Created by Leif on 5/24/21.
|
|
//
|
|
|
|
import SwiftFu
|
|
import SwiftUI
|
|
|
|
@dynamicMemberLookup
|
|
public class Object: FuableClass, ObservableObject {
|
|
public enum ObjectVariable: String, Hashable {
|
|
case value
|
|
case child
|
|
case array
|
|
case json
|
|
}
|
|
|
|
/// Variables of the object
|
|
@Published public var variables: [AnyHashable: Any] = [:]
|
|
|
|
/// @dynamicMemberLookup
|
|
public subscript(dynamicMember member: String) -> Object {
|
|
variable(named: member)
|
|
}
|
|
|
|
// MARK: public init
|
|
|
|
public init() { }
|
|
public convenience init(_ closure: (Object) -> Void) {
|
|
self.init()
|
|
|
|
closure(self)
|
|
}
|
|
public init(_ value: Any? = nil, _ closure: ((Object) -> Void)? = nil) {
|
|
defer {
|
|
if let closure = closure {
|
|
configure(closure)
|
|
}
|
|
}
|
|
|
|
guard let value = value else {
|
|
return
|
|
}
|
|
let unwrappedValue = unwrap(value)
|
|
if let _ = unwrappedValue as? NSNull {
|
|
return
|
|
}
|
|
if let object = unwrappedValue as? Object {
|
|
consume(object)
|
|
} else if let array = unwrappedValue as? [Any] {
|
|
consume(Object(array: array))
|
|
} else if let dictionary = unwrappedValue as? [AnyHashable: Any] {
|
|
consume(Object(dictionary: dictionary))
|
|
} else if let data = unwrappedValue as? Data {
|
|
consume(Object(data: data))
|
|
} else {
|
|
consume(Object().set(value: unwrappedValue))
|
|
}
|
|
}
|
|
|
|
// MARK: private init
|
|
|
|
private init(array: [Any]) {
|
|
set(
|
|
variable: ObjectVariable.array.rawValue,
|
|
value: array.map { Object($0) }
|
|
)
|
|
}
|
|
private init(dictionary: [AnyHashable: Any]) {
|
|
variables = dictionary
|
|
}
|
|
private init(data: Data) {
|
|
defer {
|
|
set(variable: ObjectVariable.json.rawValue, value: String(data: data, encoding: .utf8))
|
|
set(value: data)
|
|
}
|
|
if let json = try? JSONSerialization.jsonObject(with: data,
|
|
options: .allowFragments) as? [Any] {
|
|
set(variable: ObjectVariable.array.rawValue, value: json)
|
|
return
|
|
}
|
|
guard let json = try? JSONSerialization.jsonObject(with: data,
|
|
options: .allowFragments) as? [AnyHashable: Any] else {
|
|
return
|
|
}
|
|
consume(Object(json))
|
|
}
|
|
}
|
|
|
|
// MARK: public variables
|
|
|
|
|
|
public extension Object {
|
|
var array: [Object] {
|
|
if let array = variables[ObjectVariable.array.rawValue] as? [Data] {
|
|
return array.map { Object(data: $0) }
|
|
} else if let array = variables[ObjectVariable.array.rawValue] as? [Any] {
|
|
return array.map { value in
|
|
guard let json = value as? [AnyHashable: Any] else {
|
|
return Object(value)
|
|
}
|
|
return Object(dictionary: json)
|
|
}
|
|
}
|
|
return []
|
|
}
|
|
|
|
var child: Object {
|
|
(variables[ObjectVariable.child.rawValue] as? Object) ?? Object()
|
|
}
|
|
|
|
var json: Object {
|
|
(variables[ObjectVariable.json.rawValue] as? Object) ?? Object()
|
|
}
|
|
|
|
var value: Any {
|
|
variables[ObjectVariable.value.rawValue] ?? Object()
|
|
}
|
|
}
|
|
|
|
// MARK: public functions
|
|
|
|
|
|
public extension Object {
|
|
/// Retrieve a Value from the current object
|
|
@discardableResult
|
|
func variable(named: AnyHashable) -> Object {
|
|
guard let value = variables[named] else {
|
|
return Object()
|
|
}
|
|
if let array = value as? [Any] {
|
|
return Object(array: array)
|
|
}
|
|
guard let object = value as? Object else {
|
|
return Object(unwrap(value))
|
|
}
|
|
return object
|
|
}
|
|
/// Set a named Value to the current object
|
|
@discardableResult
|
|
func set(variable named: AnyHashable = ObjectVariable.value.rawValue, value: Any?) -> Self {
|
|
guard let value = value,
|
|
(unwrap(value) as? NSNull) == nil else {
|
|
return self
|
|
}
|
|
|
|
variables[named] = value
|
|
|
|
return self
|
|
}
|
|
/// Modify a Value with a name to the current object
|
|
@discardableResult
|
|
func modify<T>(variable named: AnyHashable = ObjectVariable.value.rawValue, modifier: (T?) -> T?) -> Self {
|
|
guard let variable = variables[named],
|
|
let value = variable as? T else {
|
|
variables[named] = modifier(nil)
|
|
|
|
return self
|
|
}
|
|
variables[named] = modifier(value)
|
|
|
|
return self
|
|
}
|
|
/// Update a Value with a name to the current object
|
|
@discardableResult
|
|
func update<T>(variable named: AnyHashable = ObjectVariable.value.rawValue, modifier: (T) -> T) -> Self {
|
|
guard let variable = variables[named],
|
|
let value = variable as? T else {
|
|
return self
|
|
}
|
|
variables[named] = modifier(value)
|
|
|
|
return self
|
|
}
|
|
/// Set the ChildObject with a name of `_object` to the current object
|
|
@discardableResult
|
|
func set(childObject object: Object) -> Self {
|
|
variables[ObjectVariable.child.rawValue] = object
|
|
|
|
return self
|
|
}
|
|
/// Set the Array with a name of `_array` to the current object
|
|
@discardableResult
|
|
func set(array: [Any]) -> Self {
|
|
variables[ObjectVariable.array.rawValue] = array
|
|
|
|
return self
|
|
}
|
|
|
|
@discardableResult
|
|
func configure(_ closure: (Object) -> Void) -> Object {
|
|
closure(self)
|
|
|
|
return self
|
|
}
|
|
|
|
@discardableResult
|
|
func consume(_ object: Object) -> Object {
|
|
object.variables.forEach { (key, value) in
|
|
self.set(variable: key, value: value)
|
|
}
|
|
|
|
return self
|
|
}
|
|
|
|
func value<T>(as type: T.Type? = nil) -> T? {
|
|
value as? T
|
|
}
|
|
|
|
func value<T>(decodedAs type: T.Type) -> T? where T: Decodable {
|
|
guard let data = value(as: Data.self) else {
|
|
return nil
|
|
}
|
|
|
|
return try? JSONDecoder().decode(T.self, from: data)
|
|
}
|
|
}
|
|
|
|
private extension Object {
|
|
/// Unwraps the <Optional> Any type
|
|
func unwrap(_ value: Any) -> Any {
|
|
let mValue = Mirror(reflecting: value)
|
|
let isValueOptional = mValue.displayStyle != .optional
|
|
let isValueEmpty = mValue.children.isEmpty
|
|
if isValueOptional { return value }
|
|
if isValueEmpty { return NSNull() }
|
|
guard let (_, unwrappedValue) = mValue.children.first else { return NSNull() }
|
|
return unwrappedValue
|
|
}
|
|
}
|
|
|
|
extension Object: CustomStringConvertible {
|
|
public var description: String {
|
|
"""
|
|
Object {
|
|
\(
|
|
variables
|
|
.map { (key, value) in
|
|
guard let object = value as? Object else {
|
|
return "|\t* \(key): \(value) (\(type(of: value)))"
|
|
}
|
|
|
|
let values = object.description.split(separator: "\n")
|
|
.dropFirst()
|
|
|
|
if values.dropLast().isEmpty {
|
|
return "|\t* \(key): Object { }"
|
|
}
|
|
|
|
return "|\t* \(key): Object {\n\(values.map { "|\t \($0)" }.joined(separator: "\n"))"
|
|
}
|
|
.joined(separator: "\n")
|
|
)
|
|
}
|
|
"""
|
|
}
|
|
}
|
|
|
|
extension Object: Hashable {
|
|
public static func == (lhs: Object, rhs: Object) -> Bool {
|
|
lhs.description == rhs.description
|
|
}
|
|
|
|
public func hash(into hasher: inout Hasher) {
|
|
hasher.combine(description)
|
|
}
|
|
}
|