Hi...
I wanna build some kind of Rest SDK for a iOS app which need to communicate with various servers (all the servers expose the same endpoints)... In the login form, you need to type the homeserver, so the baseURL needs that parameter, how can i do that?
@gregpardo you can treat the baseUrl like the other parameters and use a switch statement to load it. It would look something like this:
enum MyApi: TargetType {
case login(baseUrl: URL)
var baseUrl: URL {
switch self {
case let .login(baseUrl):
return baseUrl
}
}
/* TargetType implementation */
}
But if you know the other base URL's at compile time, I would recommend using a separate enum to drive those:
enum Server {
case northAmerica
case europe
var baseUrl: URL {
switch self {
case .northAmerica:
return <north america URL>
case .europe:
return <europe URL>
}
}
}
enum MyApi: TargetType {
case login(server: Server)
var baseUrl: URL {
switch self {
case let .login(server):
return server.baseUrl
}
}
/* TargetType implementation */
}
Does that answer your question?
@scottrhoyt mmmm, i will try the first approach (because i don't know the servers urls), but i don't like the idea of passing the parameter over more than 50 endpoints xD. I will try also with a singleton class to store the url
@gperdomor maybe I can help you further if I understand a bit more about your use case. If you don't know the URLs at compile time, how do you retrieve them?
I wanna build a Rest Client (matrix.org) for matrix protocol (matrix.org), so i can have my own matrix server and you have another matrix server, so we can install my app but i wanna stablish connection with my server and you with yours... So, in the login page, the users needs typing their url servers...
Other use case can be a GitlabCE client for example, one app, which connect to multiple gitlab servers, but the app don't know that servers
Ah, I see, so then I think the singleton idea make a lot of sense.
Another idea that would be slightly more complicated but eliminate the need for a singleton:
struct DynamicTarget: TargetType {
let baseURL: URL
let target: TargetType
var path: String { return target.path }
var method: Moya.Method { return target.method }
/* ... */
}
class DynamicProvider: MoyaProvider<DynamicTarget> {
let baseURL: URL
// add initializer to take `baseUrl` and call super with the rest of the arguments
func request(_ subTarget: TargetType, completion: ((Result<Response, MoyaError>) -> Void)? = nil) {
let dynamicTarget = DynamicTarget(baseUrl: baseUrl, target: subTarget)
super.request(dynamicTarget, completion: completion)
}
}
Does that make sense? That way you initialize the provider once with the URL and from there you can just pass regular TargetTypes to it. Another option is to create a protocol that has all the properties of TargetType except a baseUrl, you could then use that in this structure and avoid needing to put garbage URLs in for all the regular TargetTypes.
protocol SubTarget {
var path: String { get }
var method: Moya.Method { get }
/* ... */
}
struct DynamicTarget: TargetType {
let baseURL: URL
let subTarget: SubTarget
var path: String { return subTarget.path }
var method: Moya.Method { return subTarget.method }
/* ... */
}
class DynamicProvider<Target: SubTarget>: MoyaProvider<DynamicTarget> {
let baseURL: URL
// add initializer to take `baseUrl` and call super with the rest of the arguments
func request(_ subTarget: Target, completion: ((Result<Response, MoyaError>) -> Void)? = nil) {
let dynamicTarget = DynamicTarget(baseURL: baseURL, subTarget: subTarget)
super.request(dynamicTarget, completion: completion)
}
}
There are a number of variations around this idea, but struct TargetTypes would be the key.
DynamicTarget not conform TargetType protocol because of let baseUrl: URL
let baseURL: URL should satisfy a protocol requirement of var baseURL: { get }. I used it in such a way here.
(there are some small mistakes in my example like baseUrl instead of baseURL that you will need to fix, but the concept works)
@scottrhoyt My bad, init was missing xD... Apparently it works, would you consider adding this functionality to the framework natively? Singleton works too, but protocol approach seems to be better, but having a SubTarget Protocol implies that we need to update that protocols if Moya changes the TargetTypeprotocol
There is some similar functionality in MultiTarget and I proposed another idea that has some overlap, but that wasn't accepted. In neither case would it have solved this issue. In general I would say most users of Moya are interacting with a single baseURL per target, so I don't think there is a ton of need for this to be native. But if we continue to get requests like this, we can consider adding it.
@gperdomor yes, I have used something similar to the singleton method in some of my projects. It has worked quite well.
The SubTarget protocol definitely adds some maintenance overhead, but I believe it to be pretty small. If those maintenance concerns are bigger to you than having a garbage URL in the code, then you could easily do something like this:
protocol SubTarget: TargetType { }
extension SubtTarget {
var baseURL: URL { return URL(string: "http://YouShouldNeverUseThisURL.com/")! }
}
@scottrhoyt thanks... I will keep the issue open for a few days to see if anyone come up with other solution :D
@scottrhoyt it's possible override the sampleData of a Target to implement this only in a test target?
No, not directly I believe, but maybe you can provide a fileprivate protocol extension in the test file that overrides all TargetType sampleData (haven't tried that). What I do is provide an override for the endpoint closure that provides data only in testing. The advantage to this approach to is that you can simulate different responses per test and even simulate things like different HTTP status codes or network errors.
I generally simplify this by creating a testing subclass of MoyaProvider that takes a responseClosure which maps the Target to a EndpointSampleResponse, then I construct the EndpointClosure with this. It looks something like this:
class TestProvider<Target: TargetType>: MoyaProvider<Target> {
init(responseClosure: ((Target) -> EndpointSampleResponse)? = nil) {
var endpointClosure: EndpointClosure
if let responseClosure = responseClosure {
endpointClosure = {
target in
let sampleResponseClosure: Endpoint<Target>.SampleResponseClosure = {
return responseClosure(target)
}
return Endpoint(
url: target.baseURL.absoluteString,
sampleResponseClosure: sampleResponseClosure,
method: target.method,
parameters: target.parameters,
parameterEncoding: target.parameterEncoding,
httpHeaderFields: nil
)
}
} else {
endpointClosure = MoyaProvider<Target>.defaultEndpointMapping
}
super.init(endpointClosure: endpointClosure, stubClosure: MoyaProvider.immediatelyStub, plugins: [])
}
}
Note this also stubs immediately and makes no attempts to put anything into the HTTP header fields, so change that behavior as necessary.
The usage then looks like this:
enum TestTarget: TargetType {
case success, failure
// Implement TargetType
}
let responseClosure: (TestTarget) -> EndpointSampleResponse = {
target in
switch target {
case .success:
return .networkResponse(200, fixture("ApiSuccess"))
case .failure:
return .networkResponse(400, fixture("ApiFailure"))
}
}
let provider = TestProvider<TestTarget>(responseClosure: responseClosure)
Note I am using a helper function here fixture(_:) that loads sample data from fixture files.
Let me know if that helps.
Mmmm... this seems overkilling xD... I need implement a TargetType for each endpoint?... Can you provide a link to one of your tests?.
BTW, i tried the protocol extension before asking but no works
I think using OHHTTPStubsis a better way, the only thing i need to do is a stub call, before each request:
stub(condition: isHost("domain.com")) { _ in
let obj = fixtureJSON("FIXTURE KEY")
return OHHTTPStubsResponse(jsonObject: obj, statusCode: 200, headers: nil)
}
Unfortunately, I have only used this method in closed source projects, so I am unable to provide a link.
For the approach I outlined above, you wouldn't need a new TargetType for each endpoint, you would just need a new responseClosure anytime you want to change the behavior of sample data. In essence you achieve very similar results to OHHTTPStubs but you can utilize the enum semantics of the TargetType itself to write more declaratively. That being said, I know many people that use OHHTTPStubs and have good success with that route.
I have a Session target with three endpoints, login, tokenRefresh and logout, how can i mix this session target with the test target to test a success and error response for each endpoint, how they work together? i don't see that
There are a number of ways to do it. You could create a responseClosure for successes and one for failures. Or you could create a function that returns a responseClosure based on a successful flag if you need more control over the failure case. Here is what 2 separate closures looks like:
let successfulResponseClosure: (Session) -> EndpointSampleResponse = { target in
switch target {
case .login:
return .networkResponse(200, fixture("LoginSuccess"))
case .tokenRefresh:
return .networkResponse(200, fixture("TokenRefresh"))
case .logout:
return .networkResponse(200, fixture("Logout"))
}
}
let successfulProvider = TestProvider<Session>(requestClosure: successfulResponseClosure)
/* ... Run Your Success Tests ... */
let failedResponseClosure: (Session) -> EndpointSampleResponse = { _ in
return .networkResponse(404, fixture("BadRequest"))
}
let failedProvider = TestProvider<Session>(requestClosure: failedResponseClosure)
/* ... Run Your Failing Tests ... */
It looks like that solved your problem @gperdomor, let us know if you still need any help with this! Closing for now 馃槃
@scottrhoyt I adopted your DynamicTarget solution, but I encountered a compiler errors.
Below is my code:
public struct MoyaDynamicTarget: TargetType {
public let baseURL: URL
public let target: TargetType
public var path: String { return target.path }
public var method: Moya.Method { return target.method }
public var sampleData: Data { return target.sampleData }
public var task: Task { return target.task }
public var headers: [String : String]? { return target.headers }
}
public class MoyaDynamicProvider: MoyaProvider<MoyaDynamicTarget> {
fileprivate let baseURL: URL
public init(baseURL: URL,
endpointClosure: @escaping MoyaProvider<Target>.EndpointClosure = MoyaProvider.defaultEndpointMapping,
requestClosure: @escaping MoyaProvider<Target>.RequestClosure = MoyaProvider.defaultRequestMapping,
stubClosure: @escaping MoyaProvider<Target>.StubClosure = MoyaProvider.neverStub,
callbackQueue: DispatchQueue? = nil,
manager: Manager = MoyaProvider<Target>.defaultAlamofireManager(),
plugins: [PluginType] = [],
trackInflights: Bool = false) {
self.baseURL = baseURL
super.init(endpointClosure: endpointClosure, requestClosure: requestClosure, stubClosure: stubClosure, callbackQueue: callbackQueue, manager: manager, plugins: plugins, trackInflights: trackInflights)
}
override public func request(_ target: Target, callbackQueue: DispatchQueue? = .none, progress: ProgressBlock? = .none, completion: @escaping Completion) -> Cancellable {
let dynamicTarget = MoyaDynamicTarget(baseURL: baseURL, target: target)
return super.request(dynamicTarget, callbackQueue: callbackQueue, progress: progress, completion: completion)
}
}
enum WiFiCameraAPIs_8Lens {
case getDeviceInfo
case getCameraStatus
}
extension WiFiCameraAPIs_8Lens: TargetType {
var baseURL: URL { return URL(string: "http://127.0.0.1")! }
var path: String {
switch self {
case .getDeviceInfo:
return "/deviceinfo"
case .getCameraStatus:
return "/camera/state"
}
}
var method: Moya.Method {
return .get
}
var sampleData: Data {
switch self {
case .getDeviceInfo:
return "{\"sw_version\":\"3574277109\",\"device_type\":\"1\",\"manufacturer\":\"Sony\",\"device_family\":\"1\",\"hardware\":\"qcom\",\"bluetooth_mac\":\"40:40:A7:C6:8F:9A\",\"model\":\"E6883\"}".data(using: .utf8)!
case .getCameraStatus:
return "{\"streamingPort\":\"5555\",\"streamingIp\":\"0.0.0.0\",\"state\":\"1\",\"batteryLevel\":\"100\"}".data(using: .utf8)!
}
}
var task: Task {
return .requestPlain
}
var headers: [String : String]? {
return ["Content-Type": "application/json"]
}
}
Then, I create my own provider:
let apis = MoyaDynamicProvider(baseURL: URL(string: "http://www.baidu.com")!)
apis.request(target: ##MoyaDynamicTarget##>) { (result) in
}
then, I send a request, but request wants an type of ##MoyaDynamicTarget##, so I can not use the target I want. I wonder that how you implemented the dynamic base url provider? Hopefully looking forward your help, many thanks!
Personally, I think that base url should not be a part of the target, it prevents configuration injection. That should be the task of some network service, that builds a provider instance.
We probably should make an alternative DynamicTarget type.
let baseURL: URLshould satisfy a protocol requirement ofvar baseURL: { get }. I used it in such a way here.(there are some small mistakes in my example like
baseUrlinstead ofbaseURLthat you will need to fix, but the concept works)
@scottrhoyt The link is broken :(
I am also facing this issue :sparkles:
Most helpful comment
Personally, I think that base url should not be a part of the target, it prevents configuration injection. That should be the task of some network service, that builds a provider instance.
We probably should make an alternative DynamicTarget type.