Skip to content

Commit

Permalink
feat: Moved core code form ikMail share extension, fine tuned and uni…
Browse files Browse the repository at this point in the history
…t tested
  • Loading branch information
adrien-coye committed Jul 7, 2023
1 parent 1a473e0 commit bda03ca
Show file tree
Hide file tree
Showing 8 changed files with 459 additions and 3 deletions.
10 changes: 8 additions & 2 deletions Sources/InfomaniakCore/Asynchronous/ParallelTaskMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@

import Foundation

/// Something that behaves like a collection and can also be sequenced
///
/// Some of the conforming types are Array, ArraySlice, Dictionary …
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public typealias SequenceableCollection = Sequence & Collection

/// A concurrent way to map some computation with a closure to a collection of generic items.
///
/// Use default settings for optimised queue depth
Expand All @@ -41,11 +47,11 @@ public struct ParallelTaskMapper {
/// This is using an underlying `TaskQueue` (with an optimized queue depth)
/// Using it to apply work to each item of a given collection.
/// - Parameters:
/// - collection: The input collection of items to be processed
/// - collection: The input collection of items to be processed. Supports Array / ArraySlice / Dictionary …
/// - toOperation: The operation to be applied to the `collection` of items
/// - Returns: An ordered processed collection of the desired type
public func map<Input, Output>(
collection: [Input],
collection: some SequenceableCollection<Input>,
toOperation operation: @escaping @Sendable (_ item: Input) async throws -> Output?
) async throws -> [Output?] {
// Using an ArrayAccumulator to preserve the order of results
Expand Down
73 changes: 73 additions & 0 deletions Sources/InfomaniakCore/Asynchronous/SendableDictionary.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
Infomaniak Core - iOS
Copyright (C) 2023 Infomaniak Network SA
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Foundation

/// A thread safe Dictionary wrapper that does not require `await`. Conforms to Sendable.
///
/// Useful when dealing with UI.
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public final class SendableDictionary<T: Hashable, U>: @unchecked Sendable {
let lock = DispatchQueue(label: "com.infomaniak.core.SendableDictionary.lock")
private(set) var content = [T: U]()

public init() {
// META: keep SonarCloud happy
}

public var values: Dictionary<T, U>.Values {
var buffer: Dictionary<T, U>.Values!
lock.sync {
buffer = content.values
}
return buffer
}

public func value(for key: T) -> U? {
var buffer: U?
lock.sync {
buffer = content[key]
}
return buffer
}

public func setValue(_ value: U?, for key: T) {
lock.sync {
content[key] = value
}
}

@discardableResult
public func removeValue(forKey key: T) -> U? {
var buffer: U?
lock.sync {
buffer = content.removeValue(forKey: key)
}
return buffer
}

/// Bracket get / set pattern
public subscript(_ key: T) -> U? {
get {
value(for: key)
}
set {
setValue(newValue, for: key)
}
}
}
49 changes: 49 additions & 0 deletions Sources/InfomaniakCore/ClosureKit/AsyncCurriedClosure.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
Infomaniak kDrive - iOS App
Copyright (C) 2023 Infomaniak Network SA
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Foundation

/// Represents `any` (ie. all of them not the type) curried closure, of arbitrary type.
///
/// Supports concurrency and error
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public typealias AsyncCurriedClosure<Input, Output> = (Input) async throws -> Output

/// Execute the closure without waiting, discarding result
postfix operator ~

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public postfix func ~ (x: @escaping AsyncCurriedClosure<Void, Any>) {
Task {
try? await x(())
}
}

/// A closure that take no argument and return nothing, but technically curried.
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public typealias AsyncClosure = AsyncCurriedClosure<Void, Void>

/// Append an AsyncClosure to another one
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public func + (_ lhs: @escaping AsyncClosure, _ rhs: @escaping AsyncClosure) -> AsyncClosure {
let closure: AsyncClosure = { _ in
try await lhs(())
try await rhs(())
}
return closure
}
39 changes: 39 additions & 0 deletions Sources/InfomaniakCore/ClosureKit/CurriedClosure.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
Infomaniak kDrive - iOS App
Copyright (C) 2023 Infomaniak Network SA
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Foundation

/// Represents `any` (ie. all of them not the type) curried closure, of arbitrary type.
///
/// see `AsyncCurriedClosure` if you need support for structured concurrency or error feedback
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public typealias CurriedClosure<Input, Output> = (Input) -> Output

/// A closure that take no argument and return nothing, but technically curried.
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public typealias SimpleClosure = CurriedClosure<Void, Void>

/// Append a SimpleClosure to another one
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public func + (_ lhs: @escaping SimpleClosure, _ rhs: @escaping SimpleClosure) -> SimpleClosure {
let closure: SimpleClosure = { _ in
lhs(())
rhs(())
}
return closure
}
6 changes: 6 additions & 0 deletions Sources/InfomaniakCore/Extensions/Collection+Safe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,9 @@ public extension Collection {
return indices.contains(index) ? self[index] : nil
}
}

public extension Array {
subscript(safe range: Range<Index>) -> ArraySlice<Element> {
return self[Swift.min(range.startIndex, endIndex) ..< Swift.min(range.endIndex, endIndex)]
}
}
122 changes: 122 additions & 0 deletions Tests/InfomaniakCoreTests/UTAsyncCurriedClosure.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
Infomaniak Core - iOS
Copyright (C) 2023 Infomaniak Network SA
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import InfomaniakCore
import XCTest

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public final class CountedFulfillmentTestExpectation: XCTestExpectation {
private(set) var currentFulfillmentCount = 0

override public func fulfill() {
currentFulfillmentCount += 1
super.fulfill()
}
}

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
final class UTAsyncCurriedClosure: XCTestCase {
func testAppendToClosure() async {
// GIVEN
let expectationA = CountedFulfillmentTestExpectation(description: "Closure is called")
let expectationB = CountedFulfillmentTestExpectation(description: "Closure is called")
let expectations = [expectationA, expectationB]

let a: AsyncClosure = { _ in
XCTAssertEqual(expectationA.currentFulfillmentCount, 0, "expectationA should not be fulfilled")
XCTAssertEqual(expectationB.currentFulfillmentCount, 0, "expectationB should not be fulfilled")
expectationA.fulfill()

let randomShortTime = UInt64.random(in: 1 ... 100)
try await Task.sleep(nanoseconds: randomShortTime)
}

let b: AsyncClosure = { _ in
XCTAssertEqual(expectationA.currentFulfillmentCount, 1, "expectationA should be fulfilled")
XCTAssertEqual(expectationB.currentFulfillmentCount, 0, "expectationB should not be fulfilled")
expectationB.fulfill()

let randomShortTime = UInt64.random(in: 1 ... 100)
try await Task.sleep(nanoseconds: randomShortTime)
}

// WHEN
let computation = a + b
do {
let _ = try await computation~
}

// THEN
catch {
XCTFail("error: \(error)")
}

await fulfillment(of: expectations)
}

func testAppendToClosure_3chain() async {
// GIVEN
let expectationA = CountedFulfillmentTestExpectation(description: "Closure is called")
let expectationB = CountedFulfillmentTestExpectation(description: "Closure is called")
let expectationC = CountedFulfillmentTestExpectation(description: "Closure is called")
let expectations = [expectationA, expectationB, expectationC]

let a: AsyncClosure = { _ in
XCTAssertEqual(expectationA.currentFulfillmentCount, 0, "expectationA should not be fulfilled")
XCTAssertEqual(expectationB.currentFulfillmentCount, 0, "expectationB should not be fulfilled")
XCTAssertEqual(expectationC.currentFulfillmentCount, 0, "expectationC should not be fulfilled")
expectationA.fulfill()

let randomShortTime = UInt64.random(in: 1 ... 100)
try await Task.sleep(nanoseconds: randomShortTime)
}

let b: AsyncClosure = { _ in
XCTAssertEqual(expectationA.currentFulfillmentCount, 1, "expectationA should be fulfilled")
XCTAssertEqual(expectationB.currentFulfillmentCount, 0, "expectationB should not be fulfilled")
XCTAssertEqual(expectationC.currentFulfillmentCount, 0, "expectationC should not be fulfilled")
expectationB.fulfill()

let randomShortTime = UInt64.random(in: 1 ... 100)
try await Task.sleep(nanoseconds: randomShortTime)
}

let c: AsyncClosure = { _ in
XCTAssertEqual(expectationA.currentFulfillmentCount, 1, "expectation should be fulfilled")
XCTAssertEqual(expectationB.currentFulfillmentCount, 1, "expectation should be fulfilled")
XCTAssertEqual(expectationC.currentFulfillmentCount, 0, "expectationC should not be fulfilled")
expectationC.fulfill()

let randomShortTime = UInt64.random(in: 1 ... 100)
try await Task.sleep(nanoseconds: randomShortTime)
}

// WHEN
let computation = a + b + c
do {
let _ = try await computation~
}

// THEN
catch {
XCTFail("error: \(error)")
}

await fulfillment(of: expectations)
}
}
Loading

0 comments on commit bda03ca

Please sign in to comment.