ReactiveSwift+Alamofire サンプル

# はじめに
ReactiveSwiftでAlamofireを使う場合のAPIクライアントのサンプル実装
JSONのdecodeやエラーの構造体などについては記事には書かない

# 実装

```swift:Service.swift
import Foundation
import Alamofire
import Result
import ReactiveSwift
import Argo

internal enum Router: URLRequestConvertible {
    static let baseURL = "https://example.com"
    
    case sample(sampleId: Int)
    
    var requestProperties: (method: Alamofire.HTTPMethod, path: String, query: [String: Any]) {
        switch self {
        case let .sample(sampleId):
            let params: [String: Any] = ["sampleId": sampleId]
            return (.get, "/v1", params)
        }
    }
    
    func asURLRequest() throws -> URLRequest {
        let url = URL(string: Router.baseURL)!
        var urlRequest = URLRequest(url: url.appendingPathComponent(requestProperties.path))
        urlRequest.httpMethod = requestProperties.method.rawValue
        
        return try Alamofire.URLEncoding.default.encode(urlRequest, with: requestProperties.query)
    }
}

public struct Service {

    // Service

    internal func fetchSample(sampleId: Int)
        -> SignalProducer<Sample, ErrorEnvelope> {
            return request(.sample(sampleId: sampleId))
    }

    // Request

    private func request<M: Argo.Decodable>(_ router: Router)
        -> SignalProducer<M, ErrorEnvelope> where M == M.DecodedType {
            return self.rac_JSONResponse(router)
                .flatMap(.concat, decodeModel) // json decode
    }
    
    private func request<M: Argo.Decodable>(_ router: Router)
        -> SignalProducer<[M], ErrorEnvelope> where M == M.DecodedType {
            return self.rac_JSONResponse(router)
                .flatMap(.concat, decodeModels) // json decode
    }

    // Session

    private func rac_JSONResponse(_ router: Router) -> SignalProducer<Any, ErrorEnvelope> {
        return self.rac_dataResponse(router)
            .map(parseJSONData)
            .flatMap(.concat) { json -> SignalProducer<Any, ErrorEnvelope> in
                guard let json = json else {
                    return .init(error: .couldNotParseJSON)
                }
                return .init(value: json)
        }
    }

    private func rac_dataResponse(_ router: Router) -> SignalProducer<Data, ErrorEnvelope> {
        let dataRequest = Alamofire.request(router)
            .validate(statusCode: 200..<300)
            .validate(contentType: ["application/json"])
        
        let producer = self.dataResponse(with: dataRequest)
        
        print("API Starting request \(dataRequest.request?.url?.absoluteString ?? "nil")")
        
        return producer
            .flatMap(.concat) { response -> SignalProducer<Data, ErrorEnvelope> in
                guard let data = response.data else {
                    return SignalProducer(error: .couldNotParseErrorEnvelopeJSON)
                }
                
                if response.error != nil {
                    if let json = parseJSONData(data) {
                        switch decode(json) as Decoded<ErrorEnvelope> {
                        case let .success(envelope):
                            print("API Failure \(dataRequest.request?.url?.absoluteString ?? "nil") \n Error - \(envelope)")
                            return SignalProducer(error: envelope)
                        case let .failure(error):
                            print("API Failure  \(error) \n Argo decoding error - \(error)")
                            return SignalProducer(error: .couldNotDecodeJSON(error))
                        }
                    } else {
                        print("API Failure \(dataRequest.request?.url?.absoluteString ?? "nil")")
                        return SignalProducer(error: .couldNotParseErrorEnvelopeJSON)
                    }
                }
                print("API Success \(dataRequest.request?.url?.absoluteString ?? "nil")")
                return SignalProducer(value: data)
        }
    }

    private func dataResponse(with request: DataRequest, queue: DispatchQueue? = nil) ->
        SignalProducer<DefaultDataResponse, NoError> {
            
            return SignalProducer { observer, disposable in
                let response = request.response(queue: queue) { defaultDataResponse in
                    observer.send(value: defaultDataResponse)
                    observer.sendCompleted()
                }
                
                disposable.observeEnded {
                    response.cancel()
                }
                
                if (Alamofire.SessionManager.default.startRequestsImmediately == false) {
                    request.resume()
                }
            }
    }

    private func parseJSONData(_ data: Data) -> Any? {
        return (try? JSONSerialization.jsonObject(with: data, options: []))
    }
}
```