Skip to content

🍰 λ‹¬λ‹¬ν•œ Rest λ„€νŠΈμ›Œν‚Ή λͺ¨λ“ˆ 인데 이제 E-Tagλ₯Ό 곁듀인

License

Notifications You must be signed in to change notification settings

Monsteel/Dessert

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

12 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🍰 Dessert

🍰 λ‹¬λ‹¬ν•œ Rest λ„€νŠΈμ›Œν‚Ή λͺ¨λ“ˆ 인데, 이제 E-Tagλ₯Ό 곁듀인.

πŸ’πŸ»β€β™‚οΈ GET μš”μ²­ μ‹œ, E-Tagλ₯Ό μ‚¬μš©ν•˜μ—¬ 응닡을 캐싱할 수 μžˆμŠ΅λ‹ˆλ‹€.
πŸ’πŸ»β€β™‚οΈ EventMonitor, Interceptor, RetirerκΈ°λŠ₯을 μ œκ³΅ν•©λ‹ˆλ‹€.
πŸ’πŸ»β€β™‚οΈ Router κ΅¬ν˜„μ„ 톡해, 가독성 λ†’κ²Œ APIλ₯Ό μ—΄κ±°ν•˜μ—¬ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

μž₯점

βœ… E-Tagλ₯Ό 기반으둜 μΊμ‹±λ˜μ–΄, 데이터가 λ³€κ²½λœ κ²½μš°μ—λ§Œ λ„€νŠΈμ›Œν¬ μš”μ²­μ„ λ°›μŠ΅λ‹ˆλ‹€.
βœ… E-Tag 캐싱 μ‚¬μš©μ„ ν™œμ„±ν™”ν•˜λ©΄ λ©”λͺ¨λ¦¬ 캐싱은 기본적으둜 제곡되며, 섀정을 톡해 λ””μŠ€ν¬ 캐싱도 μ‚¬μš© κ°€λŠ₯ν•©λ‹ˆλ‹€.

μ‚¬μš©λ°©λ²•

Routerκ΅¬ν˜„ν•˜κΈ°

μ•„λž˜μ²˜λŸΌ λΌμš°ν„°λ₯Ό κ΅¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

import Dessert
import Foundation
import Model

public enum ExampleAPI {
  case fetchData
  case createPost(title: String, content: String)
  case updatePost(id: Int, title: String, content: String)
  case deletePost(id: Int)
}

extension ExampleAPI: Router {
  public var baseURL: URL {
    return URL(string: "https://example.com/api/v1/")!
  }

  public var path: String {
    switch self {
    case .fetchData:
      return "/posts"
    case .createPost:
      return "/posts"
    case let .updatePost(id, _, _):
      return "/posts/\(id)"
    case let .deletePost(id):
      return "/posts/\(id)"
    }
  }

  public var method: HttpMethod {
    switch self {
    case .fetchData:
      return .get(enableEtag: true, enableDiskCache: true)
    case .createPost:
      return .post
    case .updatePost:
      return .put
    case .deletePost:
      return .delete
    }
  }

  public var task: RouterTask {
    switch self {
    case .fetchData:
      return .requestPlain
    case let .createPost(title, content):
      let parameters = ["title": title, "content": content]
      return .requestParameters(parameters: parameters, type: .body)
    case let .updatePost(_, title, content):
      let parameters = ["title": title, "content": content]
      return .requestParameters(parameters: parameters, type: .body)
    case .deletePost:
      return .requestPlain
    }
  }

  public var headers: [String: String]? {
    return nil
  }
}

Interceptor κ΅¬ν˜„ν•˜κΈ°

μ•„λž˜μ²˜λŸΌ Interceptorλ₯Ό κ΅¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
async throws νƒ€μž…μœΌλ‘œ κ΅¬ν˜„λ˜μ–΄, Token μž¬λ°œκΈ‰ λ“±μ˜ 처리λ₯Ό ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

import FirebaseAuth
import Dessert
import Foundation
import Then

public final class SampleDessertInterceptor: Interceptor {
  private let firebaseAuth: Auth

  init(firebaseAuth: Auth) {
    self.firebaseAuth = firebaseAuth
  }

  public func intercept(_ request: URLRequest) async throws -> URLRequest {
    guard let user = firebaseAuth.currentUser else {
      return request
    }

    let idToken = try await user.getIDToken()

    if request.url?.host?.contains("example.com") == false { return request }

    let newRequest = request.with({
      $0.setValue(idToken, forHTTPHeaderField: "X-USER-ID-TOKEN")
    })

    return newRequest
  }
}

Retrier κ΅¬ν˜„ν•˜κΈ°

μ•„λž˜μ²˜λŸΌ Retrier κ΅¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
asyncνƒ€μž…μœΌλ‘œ κ΅¬ν˜„λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.

import Dessert
import Foundation

public final class SampleDessertRetrier: Retrier {
  public init() {}

  public func retry(router: Router, dueTo error: Error, retryCount: Int) async -> Bool {
    let error = error as NSError

    guard let underlying = error.userInfo[NSUnderlyingErrorKey] as? NSError else {
      return false
    }

    guard
      underlying.domain == NSURLErrorDomain && underlying.code == NSURLErrorNetworkConnectionLost
    else {
      return false
    }

    guard let underlying2 = underlying.userInfo[NSUnderlyingErrorKey] as? NSError else {
      return false
    }

    guard
      let domainKey = underlying2.userInfo["_kCFStreamErrorDomainKey"] as? Int,
      let codeKey = underlying2.userInfo["_kCFStreamErrorCodeKey"] as? Int,
      domainKey == CFStreamErrorDomain.POSIX.rawValue,
      codeKey == ECONNABORTED
    else {
      return false
    }

    return true
  }

}

NetworkMonitor κ΅¬ν˜„ν•˜κΈ°

import Dessert
import Foundation

public final class SampleDessertNetworkEventMonitor: NetworkEventMonitor {
  public init() {}

  public func requestDidStart(_ request: URLRequest) {
    print("""

    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ 🍰 DESSERT Request LOG ───────────────┐
    β”‚ Description: \(request.description)
    β”‚ URL: \(request.url?.absoluteString ?? "")
    β”‚ Method: \(request.httpMethod ?? "")
    β”‚ Headers: \(request.allHTTPHeaderFields ?? [:])
    β”‚ Body: \(request.httpBody?.toPrettyPrintedString ?? "")
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

    """)
  }

  public func requestDidFinish(_ request: URLRequest, _ response: URLResponse?, _ data: Data?) {
    guard let response = response as? HTTPURLResponse else { return }

    print("""

    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ 🍰 DESSERT Response LOG ──────────────┐
    β”‚ URL: \(request.url?.absoluteString ?? "")
    β”‚ StatusCode: \(response.statusCode)
    β”‚ Data: \(data?.toPrettyPrintedString ?? "")
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

    """)
  }
}

extension Data {
  fileprivate var toPrettyPrintedString: String? {
    guard let object = try? JSONSerialization.jsonObject(with: self, options: []),
          let data = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted]),
          let prettyPrintedString = NSString(data: data, encoding: String.Encoding.utf8.rawValue) else { return nil }
    return prettyPrintedString as String
  }
}

μš”μ²­ 보내기

Common

import Dessert


public func fetchData() async throws-> ResponseDTO {
  let interceptor = SampleDessertInterceptor(firebaseAuth: .auth())
  let retrier = SampleDessertRetrier()
  let networkEventMonitor = SampleDessertNetworkEventMonitor()

  let routerManager = RouterManager<ExampleAPI>.init(
    interceptor: interceptor,
    retrier: retrier,
    networkEventMonitor: networkEventMonitor,
    diskCacheLoader: .default, // or init
    memoryCacheLoader: .default // or init
  )

  let data = try await routerManager.request(.fetchData, requestType: .remote)

  return try JSONDecoder().decode(ResponseDTO.self, from: data)
}

Combine

import CombineDessert

public func fetchData() -> AnyPublisher<ResponseDTO, Error> {
  let interceptor = SampleDessertInterceptor(firebaseAuth: .auth())
  let retrier = SampleDessertRetrier()
  let networkEventMonitor = SampleDessertNetworkEventMonitor()

  let routerManager = RouterManager<ExampleAPI>.init(
    interceptor: interceptor,
    retrier: retrier,
    networkEventMonitor: networkEventMonitor
  )

  return routerManager.request(.fetchData, requestType: .remote)
    .map { try JSONDecoder().decode(ResponseDTO.self, from: $0) }
    .eraseToAnyPublisher()
}

RxSwift

import RxDessert

public func fetchData() -> Observable<ResponseDTO> {
  let interceptor = SampleDessertInterceptor(firebaseAuth: .auth())
  let retrier = SampleDessertRetrier()
  let networkEventMonitor = SampleDessertNetworkEventMonitor()

  let routerManager = RouterManager<ExampleAPI>.init(
    interceptor: interceptor,
    retrier: retrier,
    networkEventMonitor: networkEventMonitor
  )

  return routerManager.request(.fetchData, requestType: .remote)
    .map { try JSONDecoder().decode(ResponseDTO.self, from: $0) }
    .asObservable()
}

Swift Package Manager(SPM) 을 톡해 μ‚¬μš©ν•  수 μžˆμ–΄μš”

dependencies: [
  .package(url: "https://github.com/Monsteel/Dessert.git", .upToNextMajor(from: "0.0.1"))
]

μ‚¬μš©ν•˜κ³  μžˆλŠ” κ³³.

νšŒμ‚¬ μ„€λͺ…
SwiftUI와 UIKit을 μ‚¬μš©ν•˜μ—¬ 개발된 μ •μœ‘κ° 컀머슀 μ•±μ—μ„œ λ„€νŠΈμ›Œν¬ 톡신에 μ‚¬μš©ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

ν•¨κ»˜ λ§Œλ“€μ–΄ λ‚˜κ°€μš”

κ°œμ„ μ˜ 여지가 μžˆλŠ” λͺ¨λ“  것듀에 λŒ€ν•΄ μ—΄λ €μžˆμŠ΅λ‹ˆλ‹€.
PullRequestλ₯Ό 톡해 κΈ°μ—¬ν•΄μ£Όμ„Έμš”. πŸ™

License

Dessert λŠ” MIT λΌμ΄μ„ μŠ€λ‘œ μ΄μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μžμ„Έν•œ λ‚΄μš©μ€ λΌμ΄μ„ μŠ€ νŒŒμΌμ„ μ°Έμ‘°ν•΄ μ£Όμ„Έμš”.

Auther

μ΄μ˜μ€(Tony) | [email protected]

Hits

About

🍰 λ‹¬λ‹¬ν•œ Rest λ„€νŠΈμ›Œν‚Ή λͺ¨λ“ˆ 인데 이제 E-Tagλ₯Ό 곁듀인

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages