Skip to content

SwiftFormat integration #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 28 additions & 14 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,22 @@ name: Test

on:
push:
branches: [ master ]
branches: [ main ]
pull_request:
branches: [ master ]
branches: [ main ]
jobs:
linux-build:
name: Build and test on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
formatlint:
name: Format linting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: swift-actions/setup-swift@v1
- name: Build
run: swift build
- name: Run tests
run: swift test
- uses: actions/checkout@v3
- uses: sinoru/actions-setup-swift@v2
with:
swift-version: '5.6.1'
- name: GitHub Action for SwiftFormat
uses: CassiusPacheco/[email protected]
with:
swiftformat-version: '0.49.17'
macos-build:
name: Build and test on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
Expand All @@ -34,3 +33,18 @@ jobs:
run: swift build
- name: Run tests
run: swift test
linux-build:
name: Build and test ${{ matrix.swift }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
swift: ["5.4", "5.5", "5.6"]
steps:
- uses: swift-actions/setup-swift@v1
with:
swift-version: ${{ matrix.swift }}
- uses: actions/checkout@v2
- name: Test
run: swift test

7 changes: 7 additions & 0 deletions .swiftformat
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
--maxwidth 100
--semicolons never
--xcodeindentation enabled
--wraparguments before-first
--wrapcollections before-first
--wrapconditions before-first
--wrapparameters before-first
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,13 @@ to reproduce your problem. 💥
When creating a pull request, please adhere to the current coding style where possible, and create tests with your
code so it keeps providing an awesome test coverage level 💪

This repo uses [SwiftFormat](https://github.com/nicklockwood/SwiftFormat), and includes lint checks to enforce these formatting standards.
To format your code, install `swiftformat` and run:

```bash
swiftformat .
```

## Acknowledgements 👏

This library is entirely a Swift version of Facebooks [DataLoader](https://github.com/facebook/dataloader).
Expand Down
149 changes: 82 additions & 67 deletions Sources/DataLoader/DataLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ public enum DataLoaderFutureValue<T> {
case failure(Error)
}

public typealias BatchLoadFunction<Key, Value> = (_ keys: [Key]) throws -> EventLoopFuture<[DataLoaderFutureValue<Value>]>
private typealias LoaderQueue<Key, Value> = Array<(key: Key, promise: EventLoopPromise<Value>)>
public typealias BatchLoadFunction<Key, Value> = (_ keys: [Key]) throws
-> EventLoopFuture<[DataLoaderFutureValue<Value>]>
private typealias LoaderQueue<Key, Value> = [(key: Key, promise: EventLoopPromise<Value>)]

/// DataLoader creates a public API for loading data from a particular
/// data back-end with unique keys such as the id column of a SQL table
Expand All @@ -17,14 +18,13 @@ private typealias LoaderQueue<Key, Value> = Array<(key: Key, promise: EventLoopP
/// when used in long-lived applications or those which serve many users
/// with different access permissions and consider creating a new instance
/// per data request.
final public class DataLoader<Key: Hashable, Value> {

public final class DataLoader<Key: Hashable, Value> {
private let batchLoadFunction: BatchLoadFunction<Key, Value>
private let options: DataLoaderOptions<Key, Value>

private var cache = [Key: EventLoopFuture<Value>]()
private var queue = LoaderQueue<Key, Value>()

private var dispatchScheduled = false
private let lock = Lock()

Expand All @@ -39,7 +39,7 @@ final public class DataLoader<Key: Hashable, Value> {
/// Loads a key, returning an `EventLoopFuture` for the value represented by that key.
public func load(key: Key, on eventLoopGroup: EventLoopGroup) throws -> EventLoopFuture<Value> {
let cacheKey = options.cacheKeyFunction?(key) ?? key

return lock.withLock {
if options.cachingEnabled, let cachedFuture = cache[cacheKey] {
return cachedFuture
Expand All @@ -57,14 +57,18 @@ final public class DataLoader<Key: Hashable, Value> {
}
} else {
do {
_ = try batchLoadFunction([key]).map { results in
_ = try batchLoadFunction([key]).map { results in
if results.isEmpty {
promise.fail(DataLoaderError.noValueForKey("Did not return value for key: \(key)"))
promise
.fail(
DataLoaderError
.noValueForKey("Did not return value for key: \(key)")
)
} else {
let result = results[0]
switch result {
case .success(let value): promise.succeed(value)
case .failure(let error): promise.fail(error)
case let .success(value): promise.succeed(value)
case let .failure(error): promise.fail(error)
}
}
}
Expand All @@ -82,7 +86,7 @@ final public class DataLoader<Key: Hashable, Value> {
return future
}
}

/// Loads multiple keys, promising an array of values:
///
/// ```
Expand All @@ -97,14 +101,17 @@ final public class DataLoader<Key: Hashable, Value> {
/// myLoader.load(key: "b", on: eventLoopGroup)
/// ].flatten(on: eventLoopGroup).wait()
/// ```
public func loadMany(keys: [Key], on eventLoopGroup: EventLoopGroup) throws -> EventLoopFuture<[Value]> {
public func loadMany(
keys: [Key],
on eventLoopGroup: EventLoopGroup
) throws -> EventLoopFuture<[Value]> {
guard !keys.isEmpty else {
return eventLoopGroup.next().makeSucceededFuture([])
}
let futures = try keys.map { try load(key: $0, on: eventLoopGroup) }
return EventLoopFuture.whenAllSucceed(futures, on: eventLoopGroup.next())
}

/// Clears the value at `key` from the cache, if it exists. Returns itself for
/// method chaining.
@discardableResult
Expand All @@ -115,7 +122,7 @@ final public class DataLoader<Key: Hashable, Value> {
}
return self
}

/// Clears the entire cache. To be used when some event results in unknown
/// invalidations across this particular `DataLoader`. Returns itself for
/// method chaining.
Expand All @@ -130,9 +137,13 @@ final public class DataLoader<Key: Hashable, Value> {
/// Adds the provied key and value to the cache. If the key already exists, no
/// change is made. Returns itself for method chaining.
@discardableResult
public func prime(key: Key, value: Value, on eventLoop: EventLoopGroup) -> DataLoader<Key, Value> {
public func prime(
key: Key,
value: Value,
on eventLoop: EventLoopGroup
) -> DataLoader<Key, Value> {
let cacheKey = options.cacheKeyFunction?(key) ?? key

lock.withLockVoid {
if cache[cacheKey] == nil {
let promise: EventLoopPromise<Value> = eventLoop.next().makePromise()
Expand Down Expand Up @@ -160,27 +171,27 @@ final public class DataLoader<Key: Hashable, Value> {
dispatchScheduled = false
}
}

guard batch.count > 0 else {
return ()
}

// If a maxBatchSize was provided and the queue is longer, then segment the
// queue into multiple batches, otherwise treat the queue as a single batch.
if let maxBatchSize = options.maxBatchSize, maxBatchSize > 0 && maxBatchSize < batch.count {
for i in 0...(batch.count / maxBatchSize) {
if let maxBatchSize = options.maxBatchSize, maxBatchSize > 0, maxBatchSize < batch.count {
for i in 0 ... (batch.count / maxBatchSize) {
let startIndex = i * maxBatchSize
let endIndex = (i + 1) * maxBatchSize
let slicedBatch = batch[startIndex..<min(endIndex, batch.count)]
let slicedBatch = batch[startIndex ..< min(endIndex, batch.count)]
try executeBatch(batch: Array(slicedBatch))
}
} else {
try executeBatch(batch: batch)
try executeBatch(batch: batch)
}
}

private func executeBatch(batch: LoaderQueue<Key, Value>) throws {
let keys = batch.map { $0.key }
let keys = batch.map(\.key)

if keys.isEmpty {
return
Expand All @@ -191,22 +202,25 @@ final public class DataLoader<Key: Hashable, Value> {
do {
_ = try batchLoadFunction(keys).flatMapThrowing { values in
if values.count != keys.count {
throw DataLoaderError.typeError("The function did not return an array of the same length as the array of keys. \nKeys count: \(keys.count)\nValues count: \(values.count)")
throw DataLoaderError
.typeError(
"The function did not return an array of the same length as the array of keys. \nKeys count: \(keys.count)\nValues count: \(values.count)"
)
}

for entry in batch.enumerated() {
let result = values[entry.offset]

switch result {
case .failure(let error): entry.element.promise.fail(error)
case .success(let value): entry.element.promise.succeed(value)
case let .failure(error): entry.element.promise.fail(error)
case let .success(value): entry.element.promise.succeed(value)
}
}
}.recover { error in
self.failedExecution(batch: batch, error: error)
}
} catch {
self.failedExecution(batch: batch, error: error)
failedExecution(batch: batch, error: error)
}
}

Expand All @@ -220,48 +234,49 @@ final public class DataLoader<Key: Hashable, Value> {

#if compiler(>=5.5) && canImport(_Concurrency)

/// Batch load function using async await
public typealias ConcurrentBatchLoadFunction<Key, Value> = @Sendable (_ keys: [Key]) async throws -> [DataLoaderFutureValue<Value>]
/// Batch load function using async await
public typealias ConcurrentBatchLoadFunction<Key, Value> =
@Sendable (_ keys: [Key]) async throws -> [DataLoaderFutureValue<Value>]

public extension DataLoader {
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
convenience init(
on eventLoop: EventLoop,
options: DataLoaderOptions<Key, Value> = DataLoaderOptions(),
throwing asyncThrowingLoadFunction: @escaping ConcurrentBatchLoadFunction<Key, Value>
) {
self.init(options: options, batchLoadFunction: { keys in
let promise = eventLoop.next().makePromise(of: [DataLoaderFutureValue<Value>].self)
promise.completeWithTask {
try await asyncThrowingLoadFunction(keys)
}
return promise.futureResult
})
}
public extension DataLoader {
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
convenience init(
on eventLoop: EventLoop,
options: DataLoaderOptions<Key, Value> = DataLoaderOptions(),
throwing asyncThrowingLoadFunction: @escaping ConcurrentBatchLoadFunction<Key, Value>
) {
self.init(options: options, batchLoadFunction: { keys in
let promise = eventLoop.next().makePromise(of: [DataLoaderFutureValue<Value>].self)
promise.completeWithTask {
try await asyncThrowingLoadFunction(keys)
}
return promise.futureResult
})
}

/// Asynchronously loads a key, returning the value represented by that key.
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
func load(key: Key, on eventLoopGroup: EventLoopGroup) async throws -> Value {
try await load(key: key, on: eventLoopGroup).get()
}
/// Asynchronously loads a key, returning the value represented by that key.
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
func load(key: Key, on eventLoopGroup: EventLoopGroup) async throws -> Value {
try await load(key: key, on: eventLoopGroup).get()
}

/// Asynchronously loads multiple keys, promising an array of values:
///
/// ```
/// let aAndB = try await myLoader.loadMany(keys: [ "a", "b" ], on: eventLoopGroup)
/// ```
///
/// This is equivalent to the more verbose:
///
/// ```
/// async let a = myLoader.load(key: "a", on: eventLoopGroup)
/// async let b = myLoader.load(key: "b", on: eventLoopGroup)
/// let aAndB = try await a + b
/// ```
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
func loadMany(keys: [Key], on eventLoopGroup: EventLoopGroup) async throws -> [Value] {
try await loadMany(keys: keys, on: eventLoopGroup).get()
/// Asynchronously loads multiple keys, promising an array of values:
///
/// ```
/// let aAndB = try await myLoader.loadMany(keys: [ "a", "b" ], on: eventLoopGroup)
/// ```
///
/// This is equivalent to the more verbose:
///
/// ```
/// async let a = myLoader.load(key: "a", on: eventLoopGroup)
/// async let b = myLoader.load(key: "b", on: eventLoopGroup)
/// let aAndB = try await a + b
/// ```
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
func loadMany(keys: [Key], on eventLoopGroup: EventLoopGroup) async throws -> [Value] {
try await loadMany(keys: keys, on: eventLoopGroup).get()
}
}
}

#endif
#endif
19 changes: 10 additions & 9 deletions Sources/DataLoader/DataLoaderOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,33 @@ public struct DataLoaderOptions<Key: Hashable, Value> {
/// `batchLoadFunction` with a single load key. This is
/// equivalent to setting `maxBatchSize` to `1`.
public let batchingEnabled: Bool

/// Default `nil`. Limits the number of items that get passed in to the
/// `batchLoadFn`. May be set to `1` to disable batching.
public let maxBatchSize: Int?

/// Default `true`. Set to `false` to disable memoization caching, creating a
/// new `EventLoopFuture` and new key in the `batchLoadFunction`
/// for every load of the same key.
public let cachingEnabled: Bool

/// Default `2ms`. Defines the period of time that the DataLoader should
/// wait and collect its queue before executing. Faster times result
/// in smaller batches quicker resolution, slower times result in larger
/// batches but slower resolution.
/// This is irrelevant if batching is disabled.
public let executionPeriod: TimeAmount?

/// Default `nil`. Produces cache key for a given load key. Useful
/// when objects are keys and two objects should be considered equivalent.
public let cacheKeyFunction: ((Key) -> Key)?

public init(batchingEnabled: Bool = true,
cachingEnabled: Bool = true,
maxBatchSize: Int? = nil,
executionPeriod: TimeAmount? = .milliseconds(2),
cacheKeyFunction: ((Key) -> Key)? = nil
public init(
batchingEnabled: Bool = true,
cachingEnabled: Bool = true,
maxBatchSize: Int? = nil,
executionPeriod: TimeAmount? = .milliseconds(2),
cacheKeyFunction: ((Key) -> Key)? = nil
) {
self.batchingEnabled = batchingEnabled
self.cachingEnabled = cachingEnabled
Expand Down
Loading