appsquickly / typhoon

Powerful dependency injection for Objective-C ✨✨ (https://PILGRIM.PH is the pure Swift successor to Typhoon!!)✨✨
https://pilgrim.ph
Apache License 2.0
2.7k stars 269 forks source link

Run-time Arguments in Swift #294

Closed ghost closed 9 years ago

ghost commented 9 years ago

Hello,

I'm trying to implement assembly method that passes run-time argument into object constructor, but I'm stuck with several issues.

  1. When testing initialization of object, I don't know how to create assembly in Swift to pass run-time arguments. Assembly can't be casted from factory in Swift like in Objective-C as described here: https://github.com/typhoon-framework/Typhoon/issues/253
  2. If I create assembly as it is, then the next issue I get is that I can't extract target object with correct type:
let assembly = ApplicationAssembly()
let sut = assembly.ObjWithRunTimeArg("Hello") as ObjProtocol

Here I'm getting Thread 1: signal SIGABRT when trying to cast with as.

Real example:

RequestCodeApiGateway.swift

@objc public protocol RequestCodeApiGateway: ApiGateway {
    var phone: String! { get }
}

RequestCodeApiGatewayImpl.swift

public class RequestCodeApiGatewayImpl: ApiGatewayImpl, RequestCodeApiGateway {
    public dynamic let phone: String!

    public dynamic init(apiService: ApiService, apiRouter: ApiRouter, phone: String) {
        super.init(apiService: apiService, apiRouter: apiRouter)
        self.phone = phone
    }
}

ApplicationAssembly.swift

@objc public class ApplicationAssembly: TyphoonAssembly {
    // ...
    public dynamic func requestCodeApiGateway(phone: NSString) -> AnyObject {
        return TyphoonDefinition.withClass(RequestCodeApiGatewayImpl.self) { (definition) in
            definition.useInitializer("initWithApiService:apiRouter:phone:") { (initializer) in
                initializer.injectParameterWith(self.apiService())
                initializer.injectParameterWith(self.apiRouter())
                initializer.injectParameterWith(phone)
            }
        }
    }
    // ...
}

Somewhere in tests

let assembly = ApplicationAssembly()
let requestCode = assembly.requestCodeApiGateway("1234567") as RequestCodeApiGateway // Here SIGABRT
expect(requestCode).notTo(beNil())

P.S. I've also tried to implement assembly in Objective-C and to leave the rest in Swift, but I hadn't succeeded because of other 'Stopper' issues:

  1. When It comes to testing, then all the things above are fine, but when in tests I try to get requestCode.phone property's value (which is defined in RequestCodeApiGateway), it says that phone is unrecognized selector.

println(requestCode) results with next log:

TyphoonDefinition: class='MobilePassenger.RequestCodeApiGatewayImpl', key='requestCodeApiGateway:', scope='ObjectGraph'

po requestCode in debugger shows:

 (instance_type = Builtin.RawPointer = 0x00007f836041e450 -> 0x000000010177ead8 (void *)0x000000010177eb00: TyphoonDefinition)

And exception log is here:

-[TyphoonDefinition phone]: unrecognized selector sent to instance 0x7f836041e450
<unknown>:0: error: -[MobilePassenger.ApplicationAssemblySpec Application_Assembly__when_creating_factory_component__with_run_time_arguments__API_gateway__request_code__should_not_be_nil] : failed: caught "NSInvalidArgumentException", "-[TyphoonDefinition phone]: unrecognized selector sent to instance 0x7f836041e450"
(
    0   CoreFoundation                      0x0000000102358495 __exceptionPreprocess + 165
    1   libobjc.A.dylib                     0x00000001020b299e objc_exception_throw + 43
    2   CoreFoundation                      0x00000001023e965d -[NSObject(NSObject) doesNotRecognizeSelector:] + 205
    3   CoreFoundation                      0x0000000102349d8d ___forwarding___ + 973
    4   CoreFoundation                      0x0000000102349938 _CF_forwarding_prep_0 + 120
    5   MobilePassengerTests                0x000000010b429cd4 _TFFFFFFFFC15MobilePassenger23ApplicationAssemblySpec4specFS0_FT_T_U_FT_T_U1_FT_T_U0_FT_T_U_FT_T_U_FT_T_U_FT_T_u0_KT_GSqSS_ + 52
    6   MobilePassengerTests                0x000000010b425296 _TPA__TFFFFFFFFC15MobilePassenger23ApplicationAssemblySpec4specFS0_FT_T_U_FT_T_U1_FT_T_U0_FT_T_U_FT_T_U_FT_T_U_FT_T_u0_KT_GSqSS_ + 54
    7   MobilePassengerTests                0x000000010b3b1567 _TTRXFo__oGSqSS__XFo__iGSqSS__ + 39
    8   MobilePassengerTests                0x000000010b425349 _TPA__TTRXFo__oGSqSS__XFo__iGSqSS__ + 73
    9   Nimble                              0x000000010b592793 _TFF6Nimble16_memoizedClosureU__FFT_Q_FSbQ_U_FSbQ_ + 771
    10  Nimble                              0x000000010b59185f _TPA__TFF6Nimble16_memoizedClosureU__FFT_Q_FSbQ_U_FSbQ_ + 143
    11  Nimble                              0x000000010b592144 _TFV6Nimble10Expression8evaluateU__fGS0_Q__FT_Q_ + 84
    12  Nimble                              0x000000010b55e7c4 _TFF6Nimble5equalUSs9Equatable__FGSqQ__GVS_11MatcherFuncGSqQ___U_FTGVS_10ExpressionGSqQ___CS_14FailureMessage_Sb + 1844
    13  Nimble                              0x000000010b546290 _TFV6Nimble11MatcherFunc7matchesU__fGS0_Q__FTGVS_10ExpressionQ__14failureMessageCS_14FailureMessage_Sb + 560
    14  Nimble                              0x000000010b5463ac _TTWV6Nimble11MatcherFuncS_12BasicMatcherFS1_7matchesUS1__U__fRQPS1_FTGVS_10ExpressionQS2_9ValueType_14failureMessageCS_14FailureMessage_Sb + 76
    15  Nimble                              0x000000010b57f6c6 _TFV6Nimble18FullMatcherWrapper7matchesUS_12BasicMatcher___fGS0_Q_Q0__FTGVS_10ExpressionQ0__14failureMessageCS_14FailureMessage_Sb + 790
    16  Nimble                              0x000000010b58006a _TTWV6Nimble18FullMatcherWrapperS_12BasicMatcherFS1_7matchesUS1__U__fRQPS1_FTGVS_10ExpressionQS2_9ValueType_14failureMessageCS_14FailureMessage_Sb + 442
    17  Nimble                              0x000000010b54720b _TFV6Nimble11Expectation6toImplU__fGS0_Q__US_12BasicMatcher__FQ_T_ + 699
    18  Nimble                              0x000000010b58090c _TFV6Nimble11Expectation2toU__fGS0_Q__US_12BasicMatcher__FQ_T_ + 1116
    19  MobilePassengerTests                0x000000010b429be9 _TFFFFFFFC15MobilePassenger23ApplicationAssemblySpec4specFS0_FT_T_U_FT_T_U1_FT_T_U0_FT_T_U_FT_T_U_FT_T_U_FT_T_ + 2185
    20  Quick                               0x000000010b613bc0 _TFC5Quick7Example3runfS0_FT_T_ + 1296
    21  Quick                               0x000000010b614284 _TToFC5Quick7Example3runfS0_FT_T_ + 36
    22  Quick                               0x000000010b61535c __41+[QuickSpec addInstanceMethodForExample:]_block_invoke + 92
    23  CoreFoundation                      0x000000010234df1c __invoking___ + 140
    24  CoreFoundation                      0x000000010234ddc4 -[NSInvocation invoke] + 308
    25  XCTest                              0x000000010b6a70f7 -[XCTestCase invokeTest] + 253
    26  XCTest                              0x000000010b6a72f8 -[XCTestCase performTest:] + 150
    27  XCTest                              0x000000010b6b0bf5 -[XCTest run] + 260
    28  XCTest                              0x000000010b6a5ffb -[XCTestSuite performTest:] + 379
    29  XCTest                              0x000000010b6b0bf5 -[XCTest run] + 260
    30  XCTest                              0x000000010b6a5ffb -[XCTestSuite performTest:] + 379
    31  XCTest                              0x000000010b6b0bf5 -[XCTest run] + 260
    32  XCTest                              0x000000010b6a5ffb -[XCTestSuite performTest:] + 379
    33  XCTest                              0x000000010b6b0bf5 -[XCTest run] + 260
    34  XCTest                              0x000000010b6b3c6f __24+[XCTestProbe runTests:]_block_invoke189 + 44
    35  XCTest                              0x000000010b6adead -[XCTestObservationCenter _observeTestExecutionForBlock:] + 162
    36  XCTest                              0x000000010b6b3b95 +[XCTestProbe runTests:] + 347
    37  Foundation                          0x0000000101c9c6dc __NSFireDelayedPerform + 354
    38  CoreFoundation                      0x000000010231ac34 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 20
    39  CoreFoundation                      0x000000010231a7b2 __CFRunLoopDoTimer + 962
    40  CoreFoundation                      0x00000001023037be __CFRunLoopRun + 1614
    41  CoreFoundation                      0x0000000102302d83 CFRunLoopRunSpecific + 467
    42  GraphicsServices                    0x0000000104eaff04 GSEventRunModal + 161
    43  UIKit                               0x00000001025dde33 UIApplicationMain + 1010
    44  MobilePassenger                     0x000000010163521e top_level_code + 78
    45  MobilePassenger                     0x000000010163525a main + 42
    46  libdyld.dylib                       0x0000000104a325c9 start + 1
)

Could you please take a look and maybe there is something wrong in my implementation, or there is an issue within Typhoon's framework so it could be unveiled and fixed?

P.P.S. issue is reproduced on 2.3.2 version

ghost commented 9 years ago

After doing some extra research it turned out, that assemblies with run-time arguments written in Swift are working well within the code in Run environment. When we inject assembly itself into some object like in Typhoon Swift Example, then we can call method with run-time arguments without any problem.

So the reason why it all happened is incorrect creation of Assembly in Test environment. If I create assembly with Objective-C as it's shown in documentation, then test passes and everything works fine.

(ApplicationAssembly *)[TyphoonBlockComponentFactory factoryWithAssembly:[ApplicationAssembly assembly]];

So the only workaround I see for now, is to create some Objective-C helpers in Tests target that would only create Assemblies and then to use them in Swift tests.

TestAssembliesHelper.h

@interface TestAssembliesHelper : NSObject

+ (ApplicationAssembly *)applicationAssembly;

@end

TestAssembliesHelper.m

@implementation TestAssembliesHelper

+ (ApplicationAssembly *)applicationAssembly
{
    return (ApplicationAssembly *)[TyphoonBlockComponentFactory factoryWithAssembly:[ApplicationAssembly assembly]];
}

@end

Somewhere in tests

let assembly = TestAssembliesHelper.applicationAssembly()
let requestCode = assembly.requestCodeApiGateway("1234567") as RequestCodeApiGateway
expect(requestCode).notTo(beNil())
expect(requestCode.phone).to(equal("1234567"))

Actually it's fine for me as it still works out-of-box in Run environment, but it would be interesting to know if there are any other solutions to this problem.

alexgarbarev commented 9 years ago

Hello Alexander,

Yes, it's problem in Swift to cast TyphoonBlockComponentFactory into TyphoonAssembly (because Swift hasn't ducky typing support), but we did our main API using duck typing, so it's impossible to create factory using swift. The only way to have factory in swift - create it in objc. In your project target you can inject factory using plist bootstraping, in test target - using objective-c workaround (similar to your).

We will fix that in future versions of typhoon (in typhoon 3.0) - requires rework main API..

jasperblues commented 9 years ago

Linked Issue: #253

ghost commented 9 years ago

Thank's a lot for your answer, and factory through plist - indeed! These solutions work for me especially when Typhoon 3.0 is coming :)