浏览代码

First commit.

Mhd Hejazi 5 年之前
当前提交
cd15334a96

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
+.DS_Store
+/.build
+/Packages
+/*.xcodeproj
+xcuserdata/

+ 37 - 0
.swiftlint.yml

@@ -0,0 +1,37 @@
+disabled_rules:
+  - type_body_length
+  - file_length
+  - function_body_length
+  - vertical_parameter_alignment # Many false positives because of tabs
+
+opt_in_rules:
+  - empty_count
+  - overridden_super_call
+  - redundant_nil_coalescing
+  - private_outlet
+  - operator_usage_whitespace
+  - first_where
+  - number_separator
+  - prohibited_super_call
+  - fatal_error_message
+  - closure_spacing
+  - object_literal
+  - attributes
+  - toggle_bool
+  - collection_alignment
+  - closure_end_indentation
+  - no_extension_access_modifier
+  - implicit_return
+
+custom_rules:
+  indent_by_spaces:
+    name: "Indentation Style"
+    regex: "(\t)"
+    message: "Please use spaces for indentation, not tabs."
+    severity: warning
+
+indentation: spaces
+
+identifier_name:
+  excluded:
+    - id

+ 22 - 0
Package.swift

@@ -0,0 +1,22 @@
+// swift-tools-version:5.2
+
+import PackageDescription
+
+let package = Package(
+    name: "Dynamic",
+    platforms: [
+        .macOS(.v10_10),
+        .iOS(.v8),
+        .tvOS(.v9),
+        .watchOS(.v2)
+    ],
+    products: [
+        .library(name: "Dynamic", targets: ["Dynamic"])
+    ],
+    dependencies: [],
+    targets: [
+        .target(name: "Dynamic", dependencies: []),
+        .testTarget(name: "DynamicTests", dependencies: ["Dynamic"])
+    ],
+    swiftLanguageVersions: [.v5]
+)

+ 270 - 0
Sources/Dynamic/Dynamic.swift

@@ -0,0 +1,270 @@
+//
+//  Dynamic
+//  Created by Mhd Hejazi on 4/15/20.
+//  Copyright © 2020 Samabox. All rights reserved.
+//
+
+import Foundation
+
+@dynamicCallable
+@dynamicMemberLookup
+public class Dynamic: CustomDebugStringConvertible, Loggable {
+    public static var loggingEnabled: Bool = false {
+        didSet {
+            Invocation.loggingEnabled = loggingEnabled
+        }
+    }
+    var loggingEnabled: Bool { Self.loggingEnabled }
+
+    private let object: AnyObject?
+    private let memberName: String?
+
+    private var invocation: Invocation?
+    private var error: Error?
+
+    public var debugDescription: String { object?.debugDescription ?? "<nil>" }
+
+    public init(_ object: AnyObject?, memberName: String? = nil) {
+        self.object = object
+        self.memberName = memberName
+
+        log(.end).log(.start)
+        log("# Dynamic")
+        log("Object:", object ?? "<nil>").log("Member:", memberName ?? "<nil>")
+    }
+
+    public init(className: String) {
+        self.object = NSClassFromString(className)
+        self.memberName = nil
+
+        log(.end).log(.start)
+        log("# Dynamic")
+        log("Class:", className)
+    }
+
+    public func `init`() -> Dynamic {
+        log("Init:", "\(object?.debugDescription ?? "").init()")
+        log(.end)
+
+        return Dynamic((object as? NSObject.Type)?.init())
+    }
+
+    public static subscript(dynamicMember className: String) -> Dynamic {
+        Dynamic(className: className)
+    }
+
+    public subscript(dynamicMember member: String) -> Dynamic {
+        get {
+            log("Get:", "\(object?.debugDescription ?? "").\(member)")
+
+            let resolved = resolve()
+            log(.end)
+
+            return Dynamic(resolved, memberName: member)
+        }
+        set {
+            self[dynamicMember: member] = newValue.resolve()
+        }
+    }
+
+    public subscript<T>(dynamicMember member: String) -> T? {
+        get {
+            return self[dynamicMember: member].unwrap()
+        }
+        set {
+            log("Set:", "\(object?.debugDescription ?? "").\(member)")
+
+            let resolved = resolve()
+            log(.end)
+
+            let setter = "set" + (member.first?.uppercased() ?? "") + member.dropFirst()
+            _ = Dynamic(resolved, memberName: setter).dynamicallyCall(withArguments: [newValue])
+        }
+    }
+
+    public func dynamicallyCall(withArguments args: [Any?]) -> Dynamic {
+        if object is AnyClass? && memberName == nil {
+            return `init`()
+        }
+
+        guard let name = memberName else { return self }
+
+        let selector = name + repeatElement(":", count: args.count).joined(separator: "_")
+        call(selector, with: args)
+        return self
+    }
+
+    public func dynamicallyCall<T>(withArguments args: [Any?]) -> T? {
+        let result: Dynamic = dynamicallyCall(withArguments: args)
+        return result.unwrap()
+    }
+
+    public func dynamicallyCall(withKeywordArguments pairs: KeyValuePairs<String, Any?>) -> Dynamic {
+        guard let name = memberName else { return self }
+
+        let selector = name + pairs.reduce("") { result, pair in
+            if result.isEmpty {
+                return (pair.key.first?.uppercased() ?? "") + pair.key.dropFirst() + ":"
+            } else {
+                return result + (pair.key + ":")
+            }
+        }
+        call(selector, with: pairs.map { $0.value })
+        return self
+    }
+
+    public func dynamicallyCall<T>(withKeywordArguments pairs: KeyValuePairs<String, Any?>) -> T? {
+        let result: Dynamic = dynamicallyCall(withKeywordArguments: pairs)
+        return result.unwrap()
+    }
+
+    private func call(_ selector: String, with arguments: [Any?] = []) {
+        guard let target = object as? NSObject, !(object is Error) else { return }
+        log("Call: [\(type(of: target)) \(selector)]")
+
+        var invocation: Invocation
+        do {
+            invocation = try Invocation(target: target, selector: NSSelectorFromString(selector))
+        } catch {
+            self.error = error
+            return
+        }
+
+        self.invocation = invocation
+
+        for index in 0..<invocation.numberOfArguments - 2 {
+            let argument = arguments[index]
+            invocation.setArgument(argument, at: index + 2)
+        }
+
+        invocation.invoke()
+    }
+
+    private func resolve() -> AnyObject? {
+        /// This is a class. Return it.
+        if object is AnyClass? && memberName == nil {
+            return object
+        }
+
+        guard let object = object else {
+            return nil
+        }
+
+        /// This is a method we have called before. Return the result.
+        if let result = invocation?.returnedObject() {
+            return result
+        }
+
+        /// This is an error caused by a previous call. Just pass it.
+        if object is Error {
+            return object
+        }
+        if error != nil {
+            return error as AnyObject?
+        }
+
+        /// This is a wrapped object. Return it.
+        guard let name = memberName else {
+            return object
+        }
+
+        /// This is a wrapped object with a member name. Return the member.
+        if invocation?.isInvoked != true {
+            call(name)
+        }
+
+        return invocation?.returnedObject() ?? error as AnyObject?
+    }
+}
+
+extension Dynamic {
+    public var asObject: AnyObject? {
+        let result = resolve()
+        log(.end)
+        return result
+    }
+
+    public var asValue: NSValue? {
+        if let object = resolve() {
+            log(.end)
+            return NSValue(nonretainedObject: object)
+        }
+
+        log(.end)
+
+        guard let invocation = invocation,
+            let returnType = invocation.returnType,
+            invocation.returnsAny else { return nil }
+
+        let buffer = UnsafeMutablePointer<Int8>.allocate(capacity: invocation.returnLength)
+        defer { buffer.deallocate() }
+        buffer.initialize(repeating: 0, count: invocation.returnLength)
+
+        invocation.getReturnValue(result: &buffer.pointee)
+
+        let value = NSValue(bytes: buffer, objCType: UnsafePointer<Int8>(returnType))
+        return value
+    }
+
+    public var asInt8: Int8? { unwrap() }
+    public var asUInt8: UInt8? { unwrap() }
+    public var asInt16: Int16? { unwrap() }
+    public var asUInt16: UInt16? { unwrap() }
+    public var asInt32: Int32? { unwrap() }
+    public var asUInt32: UInt32? { unwrap() }
+    public var asInt64: Int64? { unwrap() }
+    public var asUInt64: UInt64? { unwrap() }
+    public var asFloat: Float? { unwrap() }
+    public var asDouble: Double? { unwrap() }
+    public var asBool: Bool? { unwrap() }
+    public var asInt: Int? { unwrap() }
+    public var asUInt: UInt? { unwrap() }
+    public var asSelector: Selector? { unwrap() }
+    public var asString: String? { asObject?.description }
+
+    private func unwrap<T>() -> T? {
+        guard let value = asValue else { return nil }
+        guard let invocation = invocation else {
+            if let result = object as? T {
+                return result
+            }
+            return nil
+        }
+
+        if T.self is AnyObject.Type {
+            let encoding = invocation.returnTypeString
+            guard encoding == "^v" || encoding == "@" else {
+                return nil
+            }
+            return value.nonretainedObjectValue as? T
+        }
+
+        var storedSize = 0
+        var storedAlignment = 0
+        NSGetSizeAndAlignment(invocation.returnType!, &storedSize, &storedAlignment)
+        guard MemoryLayout<T>.size == storedSize && MemoryLayout<T>.alignment == storedAlignment else {
+            return nil
+        }
+
+        let buffer = UnsafeMutablePointer<T>.allocate(capacity: 1)
+        defer { buffer.deallocate() }
+        value.getValue(buffer)
+
+        return buffer.pointee
+    }
+}
+
+#if canImport(UIKit)
+import UIKit
+
+extension Dynamic {
+    public var asCGPoint: CGPoint? { unwrap() }
+    public var asCGVector: CGVector? { unwrap() }
+    public var asCGSize: CGSize? { unwrap() }
+    public var asCGRect: CGRect? { unwrap() }
+    public var asCGAffineTransform: CGAffineTransform? { unwrap() }
+    public var asUIEdgeInsets: UIEdgeInsets? { unwrap() }
+    public var asUIOffset: UIOffset? { unwrap() }
+    public var asCATransform3D: CATransform3D? { unwrap() }
+}
+#endif

+ 194 - 0
Sources/Dynamic/Invocation.swift

@@ -0,0 +1,194 @@
+//
+//  Dynamic
+//  Created by Mhd Hejazi on 4/15/20.
+//  Copyright © 2020 Samabox. All rights reserved.
+//
+
+import Foundation
+
+class Invocation: Loggable {
+    public static var loggingEnabled: Bool = false
+    var loggingEnabled: Bool { Self.loggingEnabled }
+
+    private let target: NSObject
+    private let selector: Selector
+
+    var invocation: NSObject?
+
+    var numberOfArguments: Int = 0
+    var returnLength: Int = 0
+    var returnType: UnsafePointer<CChar>?
+    var returnTypeString: String? {
+        guard let returnType = returnType else { return nil }
+        return String(cString: returnType)
+    }
+    var returnsObject: Bool {
+        /// `@` is the type encoding for an object
+        returnTypeString == "@"
+    }
+    var returnsAny: Bool {
+        /// `v` is the type encoding for Void
+        returnTypeString != "v"
+    }
+    private(set) var isInvoked: Bool = false
+
+    init(target: NSObject, selector: Selector) throws {
+        self.target = target
+        self.selector = selector
+
+        log(.start)
+        log("# Invocation")
+        log("[\(type(of: target)) \(selector)]")
+        log("Target:", target)
+        log("Selector:", selector)
+
+        try initialize()
+    }
+
+    private func initialize() throws {
+        /// `NSMethodSignature *methodSignature = [target methodSignatureForSelector: selector]`
+        let methodSignature: NSObject
+        do {
+            let selector = NSSelectorFromString("methodSignatureForSelector:")
+            let signature = (@convention(c)(NSObject, Selector, Selector) -> Any).self
+            let method = unsafeBitCast(target.method(for: selector), to: signature)
+            guard let result = method(target, selector, self.selector) as? NSObject else {
+                let error = InvocationError.doesNotRecognizeSelector(type(of: target), self.selector)
+                log("ERROR:", error)
+                throw error
+            }
+            methodSignature = result
+        }
+
+        /// `numberOfArguments = methodSignature.numberOfArguments`
+        self.numberOfArguments = methodSignature.value(forKeyPath: "numberOfArguments") as? Int ?? 0
+        log("NumberOfArguments:", numberOfArguments)
+
+        /// `methodReturnLength = methodSignature.methodReturnLength`
+        self.returnLength = methodSignature.value(forKeyPath: "methodReturnLength") as? Int ?? 0
+        log("ReturnLength:", returnLength)
+
+        /// `methodReturnType = methodSignature.methodReturnType`
+        let methodReturnType: UnsafePointer<CChar>
+        do {
+            let selector = NSSelectorFromString("methodReturnType")
+            let signature = (@convention(c)(NSObject, Selector) -> UnsafePointer<CChar>).self
+            let method = unsafeBitCast(methodSignature.method(for: selector), to: signature)
+            methodReturnType = method(methodSignature, selector)
+        }
+        self.returnType = methodReturnType
+        log("ReturnType:", self.returnTypeString ?? "?")
+
+        /// `NSInvocation *invocation = [NSInvocation invocationWithMethodSignature: methodSignature]`
+        let invocation: NSObject
+        do {
+            let NSInvocation = NSClassFromString("NSInvocation") as AnyObject
+            let selector = NSSelectorFromString("invocationWithMethodSignature:")
+            let signature = (@convention(c)(AnyObject, Selector, AnyObject) -> AnyObject).self
+            let method = unsafeBitCast(NSInvocation.method(for: selector), to: signature)
+            guard let result = method(NSInvocation, selector, methodSignature) as? NSObject else {
+                let error = InvocationError.doesNotRecognizeSelector(type(of: target), self.selector)
+                log("ERROR:", error)
+                throw error
+            }
+            invocation = result
+        }
+        self.invocation = invocation
+
+        /// `invocation.selector = selector`
+        do {
+            let selector = NSSelectorFromString("setSelector:")
+            let signature = (@convention(c)(NSObject, Selector, Selector) -> Void).self
+            let method = unsafeBitCast(invocation.method(for: selector), to: signature)
+            method(invocation, selector, self.selector)
+        }
+    }
+
+    func setArgument(_ argument: Any?, at index: NSInteger) {
+        guard let invocation = invocation else { return }
+
+        log("Argument #\(index - 1):", argument ?? "<nil>")
+
+        /// `[invocation setArgument:&argument atIndex:i + 2]`
+        let selector = NSSelectorFromString("setArgument:atIndex:")
+        let signature = (@convention(c)(NSObject, Selector, UnsafeRawPointer, Int) -> Void).self
+        let method = unsafeBitCast(invocation.method(for: selector), to: signature)
+        withUnsafePointer(to: argument) { pointer in
+            method(invocation, selector, pointer, index)
+        }
+    }
+
+    func invoke() {
+        guard let invocation = invocation, !isInvoked else { return }
+
+        log("Invoking...")
+
+        isInvoked = true
+
+        /// `[invocation invokeWithTarget: target]`
+        do {
+            let selector = NSSelectorFromString("invokeWithTarget:")
+            let signature = (@convention(c)(NSObject, Selector, AnyObject) -> Void).self
+            let method = unsafeBitCast(invocation.method(for: selector), to: signature)
+            method(invocation, selector, target)
+        }
+
+        log(.end)
+    }
+
+    func getReturnValue<T>(result: inout T) {
+        guard let invocation = invocation else { return }
+
+        /// `[invocation getReturnValue: returnValue]`
+        do {
+            let selector = NSSelectorFromString("getReturnValue:")
+            let signature = (@convention(c)(NSObject, Selector, UnsafeMutableRawPointer) -> Void).self
+            let method = unsafeBitCast(invocation.method(for: selector), to: signature)
+            withUnsafeMutablePointer(to: &result) { pointer in
+                method(invocation, selector, pointer)
+            }
+        }
+
+        log("getReturnValue() ->", result)
+    }
+
+    func returnedObject() -> AnyObject? {
+        guard returnsObject, returnLength > 0 else {
+            return nil
+        }
+
+        var result: AnyObject?
+
+        getReturnValue(result: &result)
+
+        guard let object = result else {
+            return nil
+        }
+
+        /// `NSInvocation.getReturnValue()` doesn't give us the ownership of the returned object, but the compiler
+        /// tries to release this object anyway. So, we are retaining it to balance with the compiler's release.
+        return Unmanaged.passRetained(object).takeUnretainedValue()
+    }
+}
+
+public enum InvocationError: CustomNSError {
+    case doesNotRecognizeSelector(_ classType: AnyClass, _ selector: Selector)
+
+    public static var errorDomain: String { String(describing: Invocation.self) }
+
+    public var errorCode: Int {
+        switch self {
+        case .doesNotRecognizeSelector:
+            return 404
+        }
+    }
+
+    public var errorUserInfo: [String: Any] {
+        var message: String
+        switch self {
+        case .doesNotRecognizeSelector(let classType, let selector):
+            message = "'\(String(describing: classType))' doesn't recognize selector '\(selector)'"
+        }
+        return [NSLocalizedDescriptionKey: message]
+    }
+}

+ 117 - 0
Sources/Dynamic/Logger.swift

@@ -0,0 +1,117 @@
+//
+//  Dynamic
+//  Created by Mhd Hejazi on 4/15/20.
+//  Copyright © 2020 Samabox. All rights reserved.
+//
+
+import Foundation
+
+protocol Loggable: AnyObject {
+    var loggingEnabled: Bool { get }
+    func print(_ items: Any...)
+}
+
+extension Loggable {
+    var loggingEnabled: Bool { false }
+    var logUsingPrint: Bool { true }
+
+    func print(_ items: Any...) {
+        guard logUsingPrint else {
+            switch items.count {
+            case 1: Swift.print(items[0])
+            case 2: Swift.print(items[0], items[1])
+            case 3: Swift.print(items[0], items[1], items[2])
+            case 4: Swift.print(items[0], items[1], items[2], items[3])
+            case 5: Swift.print(items[0], items[1], items[2], items[3], items[4])
+            default: Swift.print(items)
+            }
+            return
+        }
+
+        guard loggingEnabled else { return }
+        Logger.logger(for: self).log(items)
+    }
+
+    @discardableResult
+    func log(_ items: Any...) -> Logger {
+        guard loggingEnabled else { return Logger.dummy }
+        return Logger.logger(for: self).log(items)
+    }
+
+    @discardableResult
+    func log(_ group: Logger.Group) -> Logger {
+        guard loggingEnabled else { return Logger.dummy }
+        return Logger.logger(for: self).log(group)
+    }
+}
+
+class Logger {
+    enum Group {
+        case start, end
+    }
+
+    static let dummy = DummyLogger()
+
+    private static var loggers: [ObjectIdentifier: Logger] = [:]
+    private static var level: Int = 0
+
+    static func logger(for object: AnyObject) -> Logger {
+        let id = ObjectIdentifier(object)
+        if let logger = Self.loggers[id] {
+            return logger
+        }
+
+        let logger = Logger()
+        Self.loggers[id] = logger
+
+        return logger
+    }
+
+    @discardableResult
+    func log(_ items: Any..., withBullet: Bool = true) -> Logger {
+        return log(items, withBullet: withBullet)
+    }
+
+    @discardableResult
+    func log(_ items: [Any], withBullet: Bool = true) -> Logger {
+        let message = items.lazy.map { String(describing: $0) }.joined(separator: " ")
+        var indent = String(repeating: " ╷  ", count: Self.level)
+        if !indent.isEmpty, withBullet {
+            indent = indent.dropLast(2) + "‣ "
+        }
+        print(indent + message)
+        return self
+    }
+
+    @discardableResult
+    func log(_ group: Group) -> Logger {
+        switch group {
+        case .start: logGroupStart()
+        case .end: logGroupEnd()
+        }
+        return self
+    }
+
+    private func logGroupStart() {
+        log([" ╭╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴"], withBullet: false)
+        Self.level += 1
+    }
+
+    private func logGroupEnd() {
+        guard Self.level > 0 else { return }
+        Self.level -= 1
+        log([" ╰╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴"], withBullet: false)
+    }
+}
+
+class DummyLogger: Logger {
+    @discardableResult
+    override func log(_ items: [Any], withBullet: Bool = true) -> Logger {
+        return self
+    }
+
+    @discardableResult
+    override func log(_ group: Group) -> Logger {
+        return self
+    }
+}

+ 15 - 0
Tests/DynamicTests/DynamicTests.swift

@@ -0,0 +1,15 @@
+import XCTest
+@testable import Dynamic
+
+final class DynamicTests: XCTestCase {
+    func testExample() {
+        // This is an example of a functional test case.
+        // Use XCTAssert and related functions to verify your tests produce the correct
+        // results.
+        XCTAssertEqual(Dynamic().text, "Hello, World!")
+    }
+
+    static var allTests = [
+        ("testExample", testExample)
+    ]
+}

+ 9 - 0
Tests/DynamicTests/XCTestManifests.swift

@@ -0,0 +1,9 @@
+import XCTest
+
+#if !canImport(ObjectiveC)
+public func allTests() -> [XCTestCaseEntry] {
+    return [
+        testCase(DynamicTests.allTests)
+    ]
+}
+#endif

+ 7 - 0
Tests/LinuxMain.swift

@@ -0,0 +1,7 @@
+import XCTest
+
+import DynamicTests
+
+var tests = [XCTestCaseEntry]()
+tests += DynamicTests.allTests()
+XCTMain(tests)