Skip to content

Commit

Permalink
Merge pull request #45 from Infomaniak/ikMailShareExtension
Browse files Browse the repository at this point in the history
Moved core code form ikMail share extension.
  • Loading branch information
PhilippeWeidmann committed Jul 11, 2023
2 parents 1a473e0 + 0482d59 commit 5a36a9a
Show file tree
Hide file tree
Showing 7 changed files with 301 additions and 5 deletions.
14 changes: 10 additions & 4 deletions Sources/InfomaniakCore/Asynchronous/ParallelTaskMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,20 @@

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
///
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public struct ParallelTaskMapper {
/// internal processing TaskQueue
let taskQueue: TaskQueue
/// private processing TaskQueue
private let taskQueue: TaskQueue

/// Init function
/// - Parameter concurrency: execution depth, keep default for optimized threading.
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)
}
}
}
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.
public typealias CurriedClosure<Input, Output> = (Input) -> Output

/// A closure that take no argument and return nothing, but technically curried.
///
/// Calling this closure type requires the use of `Void` which is, in swift, an empty tuple `()`.
public typealias SimpleClosure = CurriedClosure<Void, Void>

/// Append a SimpleClosure to another one
///
/// This allows you to append / prepend a closure to another one with the + operator.
/// This is not function composition.
public func + (_ lhs: @escaping SimpleClosure, _ rhs: @escaping SimpleClosure) -> SimpleClosure {
let closure: SimpleClosure = { _ in
lhs(())
rhs(())
}
return closure
}
1 change: 1 addition & 0 deletions Sources/InfomaniakCore/Extensions/Bundle+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import Foundation

public extension Bundle {
/// Returns `true` if executing in app extension context
var isExtension: Bool {
return bundleURL.pathExtension == "appex"
}
Expand Down
8 changes: 8 additions & 0 deletions Sources/InfomaniakCore/Extensions/Collection+Safe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,11 @@ public extension Collection {
return indices.contains(index) ? self[index] : nil
}
}

public extension Array {
/// Returns an `ArraySlice` for the specified indexes if possible.
/// The Slice is bounded to the size of the current collection.
subscript(safe range: Range<Index>) -> ArraySlice<Element> {
return self[Swift.min(range.startIndex, endIndex) ..< Swift.min(range.endIndex, endIndex)]
}
}
93 changes: 93 additions & 0 deletions Tests/InfomaniakCoreTests/UTCurriedClosure.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
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

public final class CountedFulfillmentTestExpectation: XCTestExpectation {
private(set) var currentFulfillmentCount = 0

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

final class UTCurriedClosure: XCTestCase {
func testAppendToClosure() {
// GIVEN
let expectationA = CountedFulfillmentTestExpectation(description: "Closure is called")
let expectationB = CountedFulfillmentTestExpectation(description: "Closure is called")
let expectations = [expectationA, expectationB]

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

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

// WHEN
let computation = a + b
computation(())

// THEN
wait(for: expectations, timeout: 10.0)
}

func testAppendToClosure_3chain() {
// 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: SimpleClosure = { _ 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 b: SimpleClosure = { _ 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 c: SimpleClosure = { _ 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()
}

// WHEN
let computation = a + b + c
computation(())

// THEN
wait(for: expectations, timeout: 10.0)
}
}
78 changes: 77 additions & 1 deletion Tests/InfomaniakCoreTests/UTParallelTaskMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import XCTest

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
final class UTParallelTaskMapper: XCTestCase {
func testAsyncMap() async {
func testAsyncMapToArray() async {
// GIVEN
let taskMapper = ParallelTaskMapper()
let collectionToProcess = Array(0 ... 50)
Expand Down Expand Up @@ -53,4 +53,80 @@ final class UTParallelTaskMapper: XCTestCase {
return
}
}

func testAsyncMapToArraySlice() async {
// GIVEN
let taskMapper = ParallelTaskMapper()
let collectionToProcess = Array(0 ... 50)
let collectionSlice: ArraySlice<Int> = collectionToProcess[0 ... 10]

// WHEN
do {
let result = try await taskMapper.map(collection: collectionSlice) { item in
// Make the process take some short arbitrary time to complete
let randomShortTime = UInt64.random(in: 1 ... 100)
try await Task.sleep(nanoseconds: randomShortTime)

return item * 10
}

// THEN
// We check order is preserved
_ = result.reduce(-1) { partialResult, item in
guard let item = item else {
fatalError("Unexpected")
}
XCTAssertGreaterThan(item, partialResult)
return item
}

XCTAssertEqual(result.count, collectionSlice.count)

} catch {
XCTFail("Unexpected")
return
}
}

func testAsyncMapToDictionary() async {
// GIVEN
let taskMapper = ParallelTaskMapper()
let key = Array(0 ... 50)

var dictionaryToProcess = [String: Int]()
for (key, value) in key.enumerated() {
dictionaryToProcess["\(key)"] = value
}

XCTAssertEqual(dictionaryToProcess.count, 51, "sanity check precond")

// WHEN
do {
let result = try await taskMapper.map(collection: dictionaryToProcess) { item in
let newItem = (item.key, item.value * 10)
return newItem
}

// THEN

// NOTE: Not checking for order, since this is a Dictionary

XCTAssertEqual(result.count, dictionaryToProcess.count)

for (_, tuple) in result.enumerated() {
guard let key = tuple?.0,
let intKey = Int(key),
let value = tuple?.1 else {
XCTFail("Unexpected")
return
}

XCTAssertEqual(intKey * 10, value, "expecting the computation to have happened")
}

} catch {
XCTFail("Unexpected")
return
}
}
}

0 comments on commit 5a36a9a

Please sign in to comment.