Generic way to call Network Requests for API Client

时间秒杀一切 提交于 2019-12-11 17:51:45

问题


I have provided the code for my firebase API client.

Is it smart to use generics, to initialize any entity straight from json this way?

To download lists, I needed an indicator to let me know that I'm requesting a list of entities- since there's different implementation, so I added a GETALL case to my HTTPMethod enum, is this bad, and something that would be confusing to others?

I also feel like this isn't flexible because I can't get desired entities on a node if there are nested at different levels. Hopefully that makes sense. So this probably doesn't follow open/closed principle, because If i have to nest my entities in firebase differently ill have to change implementation inside FirebaseAPI again.

From open source code i've seen, I haven't quite seen an rest client designed like this and not sure if I'm using an anti pattern or something. Any help or guidance to make this maintainable.

class FirebaseAPI {

    private let session: URLSession

    init() {

        self.session = URLSession.shared
    }

    /// Responsible for Making actual API requests & Handling response
    /// Returns an observable object that conforms to JSONable protocol.
    /// Entities that confrom to JSONable just means they can be initialized with json.
    func rx_fireRequest<Entity: JSONable>(_ endpoint: Endpoint) -> Observable<[Entity]> {

        return Observable.create { [weak self] observer in

            self?.session.dataTask(with: endpoint.request, completionHandler: { (data, response, error) in

                /// Parse response from request.
                let parsedResponse = Parser.init(data: data, response: response, error: error)

                    .parse()


                switch parsedResponse {


                case .error(let error):

                    observer.onError(error)

                    return




                case .success(let data):

                    var entities = [Entity]()

                    /// Consider edge case where a list of entities are retrieved, rather than a single entity.
                    /// Iterate through each entity and initialize.
                    /// Denoted by 'GETALL' method.
                    switch endpoint.method {

                    case .GETALL:


                        /// Key (underscored) is unique identifier for each entity, which is not needed here.
                        /// value is k/v pairs of entity attributes.
                        for (_, value) in data {

                            if let value = value as? [String: AnyObject], let entity = Entity(json: value) {

                                entities.append(entity)

                            } else {

                                observer.onError(NetworkError.initializationFailure)

                                return
                            }

                            observer.onNext(entities)

                            observer.onCompleted()

                            return
                        }



                    default:

                        if let entity = Entity(json: data) {

                        observer.onNext([entity])

                        observer.onCompleted()

                    } else {

                        observer.onError(NetworkError.initializationFailure)

                        }

                    }
                }
            })

            return Disposables.create()
        }


    }
}

Enum below Contains all request that can be made to firebase. Conforms to Endpoint protocol, so one of these enum members will be the input for FirebaseAPI request method. PROBLEM: Seems redundant to have multiple cases for CRUD operations when the only thing that changes is entity involved in the request.

enum FirebaseRequest {

    case saveUser(data: [String: AnyObject])
    case findUser(id: String)
    case removeUser(id: String)

    case saveItem(data: [String: AnyObject])
    case findItem(id: String)
    case findItems
    case removeItem(id: String)

    case saveMessage(data: [String: AnyObject])
    case findMessages(chatroomId: String)
    case removeMessage(id: String)

}


extension FirebaseRequest: Endpoint {



    var base: String {

        return ""https://<APPNAME>.firebaseio.com/""
    }

    var path: String {

        switch self {

        case .saveUser(let data): return "\(Constant.users)/\(data[Constant.id])"
        case .findUser(let id): return "\(Constant.users)/\(id)"
        case .removeUser(let id): return "\(Constant.users)/\(id)"

        case .saveItem(let data): return "\(Constant.items)/\(data[Constant.id])"
        case .findItem(let id): return "\(Constant.items)/\(id)"
        case .findItems: return "\(Constant.items)"
        case .removeItem(let id): return "\(Constant.items)/\(id)"

        case .saveMessage(let data): return "\(Constant.messages)/\(data[Constant.id])"
        case .findMessages(let chatroomId): return "\(Constant.messages)/\(chatroomId)"
        case .removeMessage(let id): return "\(Constant.messages)/\(id)"

        /// This is still incomplete... Will have more request.

        }
    }


    var method: Method {

        /// URLRequest method is GET by default, so just consider PUT & DELETE methods.
        switch self {

            /// If saving, return PUT
            /// If removing, return DELETE

        default: return .GET

        }
    }


    var body: [String : AnyObject]? {

        /// If saving, get associated value from enum case, and return that.

        return nil
    }

}

Endpoint Protocol

protocol Endpoint {

    var base: String { get }
    var path: String { get }
    var method: Method { get }
    var body: [String: AnyObject]? { get }
    // no params needed for firebase. auth token just goes in url. 
}


extension Endpoint {

    private var urlComponents: URLComponents? {

        var components = URLComponents(string: base)

        components?.path = path + "auth=\(AuthService.shared.authToken)" + ".json"

        return components
    }


    var request: URLRequest {

        var request = URLRequest(url: urlComponents!.url!)

        request.httpMethod = self.method.description

        if let body = body {

            do {

                let json = try JSONSerialization.data(withJSONObject: body, options: [])

                request.httpBody = json

            } catch let error {
                // error!
                print(error.localizedDescription)
            }

        }

        return request
    }

}

HTTP Methods

enum Method {

    case GET

    /// Indicates how JSON response should be parsed differently to abastract a list of entities
    case GETALL

    case PUT

    case DELETE

}

extension Method: CustomStringConvertible {

    var description: String {

        switch self {

        case .GET: return "GET"
        case .GETALL: return "GET"
        case .PUT: return "PUT"
        case .DELETE: return "DELETE"

        }
    }
}

AuthService

class AuthService {

    private static let _shared = AuthService()

    static var shared: AuthService {
        return _shared
    }

    private let disposeBag = DisposeBag()

    var currentUserId: String {
        return Auth.auth().currentUser!.uid
    }

    var authToken: AuthCredential {
        return FacebookAuthProvider.credential(withAccessToken: FBSDKAccessToken.current().tokenString)
    }


    func rx_login(viewController: UIViewController) {

        /// Facebook login
        rx_facebookLogin(viewController: viewController)

            .asObservable()
            .subscribe(onNext: { [weak self] (credentials: AuthCredential, userInfo: [String: Any]) in


                /// Firebase Login
                self?.rx_firebaseLogin(with: credentials)

                    .asObservable()
                    .subscribe(onNext: { [weak self] (uid) in

                        /// TODO:  Save in firebase db..





                    }).addDisposableTo((self?.disposeBag)!)

            }).addDisposableTo(disposeBag)

    }




    // - MARK: facebook login
    private func rx_facebookLogin(viewController: UIViewController) -> Observable<(AuthCredential, [String: Any])> {


        return Observable<(AuthCredential, [String: Any])>.create { observable in

            let loginManager = FBSDKLoginManager()

            loginManager.logIn(withReadPermissions: ["public_profile", "email"], from: viewController) { (result, error) in


                guard error == nil else {
                    observable.onError(AuthError.custom(message: error!.localizedDescription))
                    print("debugger: error: \(error!.localizedDescription)")
                    return
                }

                guard let accessToken = FBSDKAccessToken.current() else {
                    observable.onError(AuthError.invalidAccesToken)
                    print("debugger: invalid access token")
                    return
                }


                /// Facebook credentials to login with firebase.
                let credential = FacebookAuthProvider.credential(withAccessToken: accessToken.tokenString)


                /// Build request to get user facebook info.
                guard let request = FBSDKGraphRequest(graphPath: "me", parameters: ["fields":"name"], tokenString: accessToken.tokenString, version: nil, httpMethod: "GET") else {
                    observable.onError(AuthError.facebookGraphRequestFailed)
                    print("debugger: could not create request.")
                    return
                }


                /// - Perform Request
                request.start { (connection, result, error) in

                    guard error == nil else {
                        observable.onError(AuthError.custom(message: error!.localizedDescription))
                        print("debugger: error: \(error!.localizedDescription)")
                        return
                    }


                    print("Debugger: profile results: \(result)")

                    /// TODO: GET CITY FOR LOCALITY
                    guard let result = result as? [String: AnyObject], let name = result[Constant.name] as? String else {
                        observable.onError(AuthError.invalidProfileData)
                        print("debugger: error converting profile results")
                        return
                    }


                    /// Includes data needed to proceed with firebase login process.
                    observable.onNext((credential, ["name": name]))
                    observable.onCompleted()

                    print("Debugger: Successfull login")
                }

            }

            return Disposables.create()
        }
    }




    private func rx_firebaseLogin(with credential: AuthCredential) -> Observable<String> {

        return Observable<String>.create { observable in

            Auth.auth().signIn(with: credential) { (user, error) in


                guard error == nil else {
                    observable.onError(AuthError.custom(message: error!.localizedDescription))
                    print("error firelogin \(error!.localizedDescription)")
                    return
                }

                guard user != nil else {
                    observable.onError(AuthError.invalidFirebaseUser)
                    print("debugger: error with user..")
                    return
                }

                observable.onNext(user!.uid)
                observable.onCompleted()

            }

            return Disposables.create()

        }

    }

}

来源:https://stackoverflow.com/questions/45420616/generic-way-to-call-network-requests-for-api-client

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!