我已经为我的firebase API客户端提供了代码。
使用泛型是否明智,以这种方式直接从json初始化任何实体?
要下载列表,我需要一个指示器让我知道我正在请求实体列表 - 因为有不同的实现,所以我在我的HTTPMethod枚举中添加了一个GETALL案例,这是不是很糟糕什么东西会让别人感到困惑?
我也觉得这不灵活,因为如果嵌套在不同的级别,我就无法在节点上获得所需的实体。希望这是有道理的。因此,这可能不遵循开放/封闭原则,因为如果我必须将我的实体嵌套在firebase中,则不得不再次更改FirebaseAPI中的实现。
从我看过的开源代码中,我还没有看到像这样设计的休息客户端,并且不确定我是否使用反模式或其他东西。任何帮助或指导,使这可维护。
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()
}
}
}
以下枚举包含可以对firebase进行的所有请求。 符合端点协议,因此其中一个枚举成员将成为FirebaseAPI请求方法的输入。 问题:当唯一发生变化的事情是请求中涉及的实体时,似乎有多余的CRUD操作案例。
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
}
}
端点协议
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方法
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()
}
}
}