https://github.com/sajjon/cachingservice
https://github.com/sajjon/cachingservice
api-wrapper caching networking services swift
Last synced: 9 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/sajjon/cachingservice
- Owner: Sajjon
- License: mit
- Created: 2017-11-09T14:23:46.000Z (over 8 years ago)
- Default Branch: master
- Last Pushed: 2018-10-24T11:52:30.000Z (over 7 years ago)
- Last Synced: 2025-02-22T01:36:18.918Z (over 1 year ago)
- Topics: api-wrapper, caching, networking, services, swift
- Language: Swift
- Size: 39.1 MB
- Stars: 2
- Watchers: 2
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: MIT-LICENSE.md
Awesome Lists containing this project
README
## Easily write API services, cache models, and per request specify options
# What is this?
This is a protocol-driven RxSwift based convenience project for quick creation of services. You get everything for free conforming to this simple protocol:
```swift
protocol Service {
var httpClient: HTTPClientProtocol { get }
}
```
# Usage
```swift
protocol CoinService: Service, Persisting {
func getCoins(fromSource source: ServiceSource) -> Observable<[Coin]>
}
final class DefaultCoinService: CoinService {
let httpClient: HTTPClientProtocol
let cache: AsyncCache
}
extension DefaultCoinService {
func getCoins() -> Observable<[Coin]> {
return get(request: CoinRouter.all, from: .cacheAndBackend)
}
}
```
The [CoinService.swift](Example/Source/Services/CoinService.swift) above conforms to `Service` which is all you need to get access to the methods `get`, `post` etc of a service. Have a look at the [`Example`](Example) app, fetching a list of cryptocurrencies which you can filter.
# Implementation
## Service
Actually, the protocol `Service` looks like this, but since all functions have default implementations, so the only thing you need to in order to conform to `Service` is to have `httpClient` property.
```swift
protocol Service {
var httpClient: HTTPClientProtocol { get }
// Methods that have default implementations, which you should not implement your self.
func get(request: Router, from source: ServiceSource, jsonDecoder: JSONDecoder, key: Key?) -> Observable where Model: Codable
func post(request: Router, jsonDecoder: JSONDecoder) -> Observable where Model: Codable
func postFireForget(request: Router) -> Observable
// These should preferrably be `private`, however "overridden" by ImageService
func getFromBackend(request: Router, from source: ServiceSource, jsonDecoder: JSONDecoder) -> Observable where Model: Codable
func getFromCacheIfAbleTo(from source: ServiceSource, key: Key?) -> Observable where Model: Codable
}
```
`CachingService` comes with a default HTTPClient, which you can use in your types, so you don't have to do anything really.
### ServiceSource - Options
Where you can specify `ServiceSource`, which is an `enum` with three cases:
```swift
public enum ServiceSource {
case cacheAndBackendOptions(ServiceOptionsInfo)
case cache
case backendOptions(ServiceOptionsInfo)
}
```
where `ServiceOptionsInfo` being a typealias for `[ServiceOptionsInfoItem]`:
```swift
public enum ServiceOptionsInfoItem {
case emitValue
case emitError
case shouldCache
case ifCachedPreventDownload
case retryWhenReachable(ServiceRetry)
}
```
Using this you can easily specify if you request should fetch data from the:
1. Cache only
2. Backend only
3. Cache and backend
- Only fetch from backend if the cache was empty
- Skip updating cache if value existed and got new from backend
For all requests towards your backend you can specify the retry policy for failing request:
```swift
public enum ServiceRetry {
case count(Int) // TODO: Should be able to specify paus period between retries
case forever // TODO: Should be using exponential backoff
case timeout(TimeInterval)
}
```
## RxSwift - Nothing but Observables
`CachingService` relies heavily on RxSwift.
The heart of the project lies in the method `getFromBackendAndCacheIfAbleTo` below:
```swift
func getFromBackendAndCacheIfAbleTo(request: Router, from source: ServiceSource, jsonDecoder: JSONDecoder, key: Key?) -> Observable where Model: Codable {
return getFromBackend(request: request, from: source, jsonDecoder: jsonDecoder)
.retryOnConnect(options: source.retryWhenReachable, reachability: reachability)
.catchError { self.handleErrorIfNeeded($0, from: source) }
.flatMap { model in self.updateCacheIfAbleTo(with: model, from: source, key: key) }
.filterNil()
.filter(source.emitEventForValueFromBackend)
}
```
## Persisting
Optionally you can mark your service with the conformance to the `Persisting` protocol:
```swift
protocol Persisting {
var cache: AsyncCache { get }
// Has default implementation
func getModels(using filter: FilterConvertible) -> Observable<[Model]> where Model: Codable & Filterable
}
```
Once again, the `getModels` already has a default implementation, so really you just need the `cache: AsyncCache` property.
### AsyncCache
The `AsyncCache` protocol looks like this:
```swift
protocol AsyncCache: Cache {
func asyncSave(value: Value, for key: Key, done: Done?) where Value: Codable
func asyncDelete(for key: Key, done: Done?)
func asyncDeleteAll(done: Done?)
func asyncLoadValue(for key: Key, done: Done?) where Value: Codable
func asyncHasValue(ofType type: Value.Type, for key: Key, done: Done?) where Value: Codable
}
```
`CachingService` comes with a default implementation of said protocol, conformed to by the type `Storage` of the project [`Cache`](https://github.com/hyperoslo/Cache)(Pod/Carthage support exists).