diff --git a/Sources/RubyGateway/RbGateway.swift b/Sources/RubyGateway/RbGateway.swift index f1f4175..dd23fcc 100644 --- a/Sources/RubyGateway/RbGateway.swift +++ b/Sources/RubyGateway/RbGateway.swift @@ -59,6 +59,7 @@ import RubyGatewayHelpers /// to define new classes and modules. Then add methods using `RbObject.defineMethod(...)` /// and `RbObject.defineSingletonMethod(...)`. /// +@dynamicMemberLookup public final class RbGateway: RbObjectAccess { /// The VM - not initialized until `setup()` is called. @@ -358,6 +359,13 @@ extension RbGateway { } } +// MARK: - Dynamic member lookup +public extension RbGateway { + subscript(dynamicMember member: String) -> RbObject! { + try? `get`(member) + } +} + // MARK: - Global declaration /// The shared instance of `RbGateway`. :nodoc: diff --git a/Sources/RubyGateway/RbObject.swift b/Sources/RubyGateway/RbObject.swift index 7b3c6a3..d4d83be 100644 --- a/Sources/RubyGateway/RbObject.swift +++ b/Sources/RubyGateway/RbObject.swift @@ -85,6 +85,8 @@ import RubyGatewayHelpers /// Use `RbObject.defineMethod(...)` and `RbObject.defineSingletonMethod(...)` to /// add methods implemented in Swift to an object or class. Use /// `RbGateway.defineClass(_:parent:under:)` to define entirely new classes. +@dynamicMemberLookup +@dynamicCallable public final class RbObject: RbObjectAccess { internal let valueBox: UnsafeMutablePointer @@ -386,6 +388,51 @@ extension RbObject: Hashable, Equatable, Comparable { } } +// MARK: - Dynamic member lookup and callable +public extension RbObject { + + subscript(dynamicMember memberName: String) -> RbObject! { + get { + if let member = try? get(memberName) { + return member + } else { + // Maybe it's a method + if let _ = try? memberName.checkRubyMethodName() { + return try? call("method", args: [memberName]) + } else { + return nil + } + } + } + set { + if let _ = try? setAttribute(memberName, newValue: newValue) { return } + if let _ = try? setInstanceVar(memberName, newValue: newValue) { return } + } + } + + @discardableResult + func dynamicallyCall(withArguments arguments: [RbObjectConvertible]) -> RbObject! { + switch rubyType { + case .T_CLASS: + do { + let newInstance = try call("new", args: arguments) + return newInstance.isNil ? nil : newInstance + } catch { + preconditionFailure("Couldn't create new instance of class") + } + case .T_DATA: + do { + let result = try call("call", args: arguments) + return result.isNil ? nil : result + } catch { + preconditionFailure("Ruby type was T_DATA but not a method reference. Couldn't call method.") + } + default: + return nil + } + } +} + // MARK: - Array helper extension Array where Element == RbObject { diff --git a/Tests/RubyGatewayTests/TestDynamic.swift b/Tests/RubyGatewayTests/TestDynamic.swift index 174eb1e..e398381 100644 --- a/Tests/RubyGatewayTests/TestDynamic.swift +++ b/Tests/RubyGatewayTests/TestDynamic.swift @@ -9,64 +9,64 @@ import XCTest import RubyGateway // Tests for dynamic member function -//class skip_TestDynamic: XCTestCase { -// -// /// Getter -// func testDynamicMemberLookupRead() { -// doErrorFree { -// try Ruby.require(filename: Helpers.fixturePath("methods.rb")) -// -// guard let obj = RbObject(ofClass: "MethodsTest") else { -// XCTFail("Couldn't create object") -// return -// } -// -// guard let strObj = obj.property else { -// XCTFail("Couldn't access member 'property'") -// return -// } -// -// XCTAssertEqual("Default", String(strObj)) -// -// if let mysterious = obj.not_a_member { -// XCTFail("Accessed not_a_member: \(mysterious)") -// return -// } -// } -// } -// -// /// Write -// func testDynamicMemberLookupWrite() { -// doErrorFree { -// try Ruby.require(filename: Helpers.fixturePath("methods.rb")) -// -// guard let obj = RbObject(ofClass: "MethodsTest") else { -// XCTFail("Couldn't create object") -// return -// } -// -// let newValue = "Changed it!" -// obj.property = RbObject(newValue) -// -// guard let strObj = obj.property else { -// XCTFail("Couldn't access member 'property'") -// return -// } -// -// XCTAssertEqual(newValue, String(strObj)) -// -// RbError.history.clear() -// obj.bad_property = RbObject(23) -// XCTAssertNotNil(RbError.history.mostRecent) -// -// RbError.history.clear() -// obj.property = nil -// XCTAssertEqual(.nilObject, obj.property) -// } -// } -// -// static var allTests = [ -// ("testDynamicMemberLookupRead", testDynamicMemberLookupRead), -// ("testDynamicMemberLookupWrite", testDynamicMemberLookupWrite), -// ] -//} +class TestDynamic: XCTestCase { + + /// Getter + func testDynamicMemberLookupRead() { + doErrorFree { + try Ruby.require(filename: Helpers.fixturePath("methods.rb")) + + guard let obj = RbObject(ofClass: "MethodsTest") else { + XCTFail("Couldn't create object") + return + } + + guard let strObj = obj.property else { + XCTFail("Couldn't access member 'property'") + return + } + + XCTAssertEqual("Default", String(strObj)) + + if let mysterious = obj.not_a_member { + XCTFail("Accessed not_a_member: \(mysterious)") + return + } + } + } + + /// Write + func testDynamicMemberLookupWrite() { + doErrorFree { + try Ruby.require(filename: Helpers.fixturePath("methods.rb")) + + guard let obj = RbObject(ofClass: "MethodsTest") else { + XCTFail("Couldn't create object") + return + } + + let newValue = "Changed it!" + obj.property = RbObject(newValue) + + guard let strObj = obj.property else { + XCTFail("Couldn't access member 'property'") + return + } + + XCTAssertEqual(newValue, String(strObj)) + + RbError.history.clear() + obj.bad_property = RbObject(23) + XCTAssertNotNil(RbError.history.mostRecent) + + RbError.history.clear() + obj.property = nil + XCTAssertEqual(.nilObject, obj.property) + } + } + + static var allTests = [ + ("testDynamicMemberLookupRead", testDynamicMemberLookupRead), + ("testDynamicMemberLookupWrite", testDynamicMemberLookupWrite), + ] +}