问题
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