[XRAY-2] Part 2a - Move foundation extensions package to its own repository (#1)
Works on xiiagency/Xray#2
This commit is contained in:
parent
4f5866bbe3
commit
3560bdd3bb
|
@ -0,0 +1,2 @@
|
|||
# Swift Package Manager
|
||||
.swiftpm
|
|
@ -0,0 +1,29 @@
|
|||
// swift-tools-version:5.5
|
||||
import PackageDescription
|
||||
|
||||
let package =
|
||||
Package(
|
||||
name: "SwiftFoundationExtensions",
|
||||
platforms: [
|
||||
.iOS(.v15),
|
||||
.watchOS(.v8),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "SwiftFoundationExtensions",
|
||||
targets: ["SwiftFoundationExtensions"]
|
||||
),
|
||||
],
|
||||
dependencies: [],
|
||||
targets: [
|
||||
.target(
|
||||
name: "SwiftFoundationExtensions",
|
||||
dependencies: []
|
||||
),
|
||||
// NOTE: Re-enable when tests are added.
|
||||
// .testTarget(
|
||||
// name: "SwiftFoundationExtensionsTests",
|
||||
// dependencies: ["SwiftFoundationExtensions"]
|
||||
// ),
|
||||
]
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
# SwiftFoundationExtensions
|
||||
|
||||
Provides extensions and utilities for the core `Foundation` Swift libraries.
|
|
@ -0,0 +1,79 @@
|
|||
import Foundation
|
||||
|
||||
extension Array {
|
||||
/**
|
||||
Returns a `Dictionary` containing unique key/value pairs, extracted by the provided closure for each element in the `Array`.
|
||||
*/
|
||||
public func mapped<Key : Hashable, MappedValue>(
|
||||
by pairExtractor: @escaping (Element) -> (Key, MappedValue)
|
||||
) -> [Key: MappedValue] {
|
||||
Dictionary(uniqueKeysWithValues: map(pairExtractor))
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a `Dictionary` containing unique key/value pairs, extracted by the provided closure for each element in the `Array`.
|
||||
*/
|
||||
public func mapped<Key : Hashable, MappedValue>(
|
||||
by pairExtractor: @escaping (Element) throws -> (Key, MappedValue)
|
||||
) throws -> [Key: MappedValue] {
|
||||
Dictionary(uniqueKeysWithValues: try map(pairExtractor))
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a `Dictionary` containing the elements of this array, uniquely keyed by the results of the provided key extractor.
|
||||
*/
|
||||
public func mapped<Key : Hashable>(
|
||||
by keyExtractor: @escaping (Element) -> Key
|
||||
) -> [Key: Element] {
|
||||
mapped(by: { element in (keyExtractor(element), element) })
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a `Dictionary` containing the elements of this array, uniquely keyed by the results of the provided key extractor.
|
||||
*/
|
||||
public func mapped<Key : Hashable>(
|
||||
by keyExtractor: @escaping (Element) throws -> Key
|
||||
) throws -> [Key: Element] {
|
||||
try mapped(by: { element in (try keyExtractor(element), element) })
|
||||
}
|
||||
|
||||
/**
|
||||
Returns `Dictionary` containing the elements of this array, grouped by a key returned from the provided closure.
|
||||
*/
|
||||
public func grouped<Key : Hashable>(
|
||||
by keyExtractor: @escaping (Element) -> Key
|
||||
) -> [Key: [Element]] {
|
||||
Dictionary(
|
||||
grouping: self,
|
||||
by: { element in keyExtractor(element) }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns `Dictionary` containing the elements of this array, grouped by a key returned from the provided closure.
|
||||
*/
|
||||
public func grouped<Key : Hashable>(
|
||||
by keyExtractor: @escaping (Element) throws -> Key
|
||||
) throws -> [Key: [Element]] {
|
||||
try Dictionary(
|
||||
grouping: self,
|
||||
by: { element in try keyExtractor(element) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element: Identifiable {
|
||||
/**
|
||||
Returns a `Dictionary` containing the elements of this array, uniquely keyed by their IDs.
|
||||
*/
|
||||
public func mappedById() -> [Element.ID: Element] {
|
||||
mapped(by: { element in element.id })
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a `Dictionary` containing the elements of this array, grouped by their IDs.
|
||||
*/
|
||||
public func groupedById() -> [Element.ID: [Element]] {
|
||||
grouped(by: { element in element.id })
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import Foundation
|
||||
|
||||
extension Dictionary {
|
||||
/**
|
||||
Allows for incrementing the value stored under the requested `Key`.
|
||||
If there is no such value it is assumed to be `0`.
|
||||
*/
|
||||
public mutating func increment(key: Key, by value: Value) where Value == Int {
|
||||
self[key] = (self[key] ?? 0) + value
|
||||
}
|
||||
|
||||
/**
|
||||
Allows for incrementing by one the value stored under the requested `Key`.
|
||||
If there is no such value it is assumed to be `0`.
|
||||
*/
|
||||
public mutating func increment(key: Key) where Value == Int {
|
||||
increment(key: key, by: 1)
|
||||
}
|
||||
|
||||
/**
|
||||
Allows for decrementing by one the value stored under the requested `Key`.
|
||||
If there is no such value it is assumed to be `0`.
|
||||
*/
|
||||
public mutating func decrement(key: Key) where Value == Int {
|
||||
increment(key: key, by: -1)
|
||||
}
|
||||
|
||||
/**
|
||||
Allows for incrementing the value stored under the requested `Key`.
|
||||
If there is no such value it is assumed to be `0.0`.
|
||||
*/
|
||||
public mutating func increment(key: Key, by value: Value) where Value == Double {
|
||||
self[key] = (self[key] ?? 0.0) + value
|
||||
}
|
||||
|
||||
/**
|
||||
Allows for incrementing by one the value stored under the requested `Key`.
|
||||
If there is no such value it is assumed to be `0.0`.
|
||||
*/
|
||||
public mutating func increment(key: Key) where Value == Double {
|
||||
increment(key: key, by: 1.0)
|
||||
}
|
||||
|
||||
/**
|
||||
Allows for decrementing by one the value stored under the requested `Key`.
|
||||
If there is no such value it is assumed to be `0.0`.
|
||||
*/
|
||||
public mutating func decrement(key: Key) where Value == Double {
|
||||
increment(key: key, by: -1)
|
||||
}
|
||||
|
||||
/**
|
||||
Inverts the key->value mapping to one of value->key. Assumes that all values are unique and will raise a fatal error otherwise.
|
||||
*/
|
||||
public func inverted() -> [Value: Key] where Value : Hashable {
|
||||
let invertedPairs: [(Value, Key)] = map { (key, value) in (value, key) }
|
||||
return Dictionary<Value, Key>(uniqueKeysWithValues: invertedPairs)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import Foundation
|
||||
|
||||
extension Date {
|
||||
/**
|
||||
Returns the start of the week (dependent on device locale) for the given date.
|
||||
*/
|
||||
public var startOfWeek: Date {
|
||||
let calendar = Calendar.current
|
||||
let components = calendar.dateComponents([.yearForWeekOfYear, .weekOfYear], from: self)
|
||||
return calendar.date(from: components)!
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the number of days remaining this week as an `Int`.
|
||||
*/
|
||||
public var daysRemainingThisWeek: Int {
|
||||
let calendar = Calendar.current
|
||||
let numberOfDays = calendar.dateComponents([.day], from: self.startOfWeek, to: self)
|
||||
return numberOfDays.day!
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the start of the month for the given date.
|
||||
*/
|
||||
public var startOfMonth: Date {
|
||||
let calendar = Calendar.current
|
||||
let components = calendar.dateComponents([.year, .month], from: self)
|
||||
return calendar.date(from: components)!
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the start of the year for the given date.
|
||||
*/
|
||||
public var startOfYear: Date {
|
||||
let calendar = Calendar.current
|
||||
let components = calendar.dateComponents([.year], from: self)
|
||||
return calendar.date(from: components)!
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a new date from year/month/day values using the current `Calendar`.
|
||||
*/
|
||||
public init(year: Int, month: Int, day: Int) {
|
||||
let components = DateComponents(year: year, month: month, day: day)
|
||||
self = Calendar.current.date(from: components)!
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import Foundation
|
||||
|
||||
/**
|
||||
Full version and build number of the main bundle in the format;
|
||||
`v[MAJOR].[MINOR].[PATCH]-[BUILD]`
|
||||
|
||||
Example:
|
||||
```
|
||||
v1.2.3-456
|
||||
```
|
||||
*/
|
||||
public let mainBundleVersion: String = {
|
||||
// Version number
|
||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?"
|
||||
|
||||
// Build number
|
||||
let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "?"
|
||||
|
||||
return "v\(appVersion)-\(appBuild)"
|
||||
}()
|
||||
|
||||
/**
|
||||
Returns true if the main bundle was installed from the app store (not in a sandbox environment), false otherwise.
|
||||
|
||||
NOTE: An app is considered installed from the App Store if-and-only-if:
|
||||
- the install was not in the sandbox environment
|
||||
- the app has mobile provisioning
|
||||
*/
|
||||
public let isInstalledFromAppstore: Bool = {
|
||||
let isSandbox =
|
||||
Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"
|
||||
|
||||
let hasMobileProvisioning =
|
||||
Bundle.main.path(forResource: "embedded", ofType: "mobileprovision") != nil
|
||||
|
||||
return !isSandbox && hasMobileProvisioning
|
||||
}()
|
|
@ -0,0 +1,49 @@
|
|||
import Foundation
|
||||
|
||||
/**
|
||||
Extend Error to be able to send a user readable message to `OSLog` (via `Logger`).
|
||||
*/
|
||||
extension Error {
|
||||
/**
|
||||
Returns the description of this error.
|
||||
Useful for printing error names via `OSLog` (via `Logger`).
|
||||
*/
|
||||
public var description: String {
|
||||
String(describing: self)
|
||||
}
|
||||
|
||||
/**
|
||||
Wraps an `Error` to a `ErrorWithTrace`, capturing the call site's file/line.
|
||||
|
||||
`withTrace` should only be called at the call site where the error is thrown (or sent through the continuation) like this:
|
||||
|
||||
```
|
||||
throw MyErrorEnum.MyErrorValue(someArg).withTrace()
|
||||
```
|
||||
*/
|
||||
public func withTrace(filePath: String = #file, line: Int = #line) -> ErrorWithTrace {
|
||||
ErrorWithTrace(
|
||||
file: (filePath as NSString).lastPathComponent,
|
||||
line: line,
|
||||
error: self
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
Provides the ability to convert an `ErrorWithTrace` to it's underlying error or cast a general `Error` to a specific type.
|
||||
Attempts to cast the current error as the requested target, returns the value if the cast is successful.
|
||||
Then checks if the error is a `ErrorWithTrace`, attempting to cast its underlying `error` as the target, returning the value.
|
||||
If either conversion fails, nil is returned.
|
||||
*/
|
||||
public func castError<Target : Error>(to type: Target.Type) -> Target? {
|
||||
if let directCast = self as? Target {
|
||||
return directCast
|
||||
}
|
||||
|
||||
if let indirectCast = (self as? ErrorWithTrace)?.error as? Target {
|
||||
return indirectCast
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import Foundation
|
||||
|
||||
/**
|
||||
A Specialized error that captures the file/line number of a thrown error to aid in debugging.
|
||||
|
||||
NOTE: This struct is not initialized directly but rather the `Error.withTrace()` extension should be used instead.
|
||||
*/
|
||||
public struct ErrorWithTrace : Error, CustomStringConvertible {
|
||||
/**
|
||||
The name of the file where `.withTrace()` conversion was called.
|
||||
*/
|
||||
let file: String
|
||||
|
||||
/**
|
||||
The line within the file where `.withTrace()` conversion was called.
|
||||
*/
|
||||
let line: Int
|
||||
|
||||
/**
|
||||
The underlying error.
|
||||
*/
|
||||
let error: Error
|
||||
|
||||
/**
|
||||
Returns the decorated description of this error.
|
||||
*/
|
||||
public var description: String {
|
||||
"⚠️ \(error.description) @ \(file):\(line)"
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the decorated description, same as `description`.
|
||||
*/
|
||||
public var localizedDescription: String {
|
||||
description
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import Foundation
|
||||
import os
|
||||
|
||||
extension Logger {
|
||||
// Retrieve the bundle id so that it doesn't have be retrieved every time we build a Logger.
|
||||
private static var subsystem = Bundle.main.bundleIdentifier!
|
||||
|
||||
/**
|
||||
Returns a Logger instance for the given category and the current application bundle id.
|
||||
|
||||
Usage:
|
||||
```
|
||||
private struct FooView : View {
|
||||
private static let logger: Logger = .loggerFor("FooView")
|
||||
...
|
||||
}
|
||||
```
|
||||
*/
|
||||
public static func loggerFor(_ category: String) -> Logger {
|
||||
Logger(subsystem: subsystem, category: category)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a Logger instance for the given type and the current application bundle id.
|
||||
|
||||
Usage:
|
||||
```
|
||||
private struct FooView : View {
|
||||
private static let logger: Logger = .loggerFor(FooView.self)
|
||||
...
|
||||
}
|
||||
```
|
||||
*/
|
||||
public static func loggerFor<Type>(_ type: Type.Type) -> Logger {
|
||||
loggerFor(String(reflecting: type))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import Foundation
|
||||
|
||||
extension Comparable {
|
||||
/**
|
||||
Returns the value clamped to the provided closed range.
|
||||
This is equivalent to: `min(max([VALUE], [LOWER_BOUND]), [UPPER_BOUND])`.
|
||||
*/
|
||||
public func clamped(to limits: ClosedRange<Self>) -> Self {
|
||||
min(max(self, limits.lowerBound), limits.upperBound)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the value clamped to the provided half-open range.
|
||||
This is equivalent to: `min(max([VALUE], [LOWER_BOUND]), [UPPER_BOUND] - 1)`.
|
||||
*/
|
||||
public func clamped(to limits: Range<Self>) -> Self where Self : Strideable {
|
||||
min(max(self, limits.lowerBound), limits.upperBound.advanced(by: -1))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import Foundation
|
||||
|
||||
|
||||
extension Double {
|
||||
/**
|
||||
The level of tolerance for Double "closeness" comparisons = 4 decimal places.
|
||||
*/
|
||||
public static let CLOSENESS_TOLERANCE = 0.0001
|
||||
|
||||
/**
|
||||
Returns true if the current value and the provided one are closer than the "closeness" tolerance level, false otherwise.
|
||||
|
||||
NOTE: This is needed due to the way Double/Float instances are stored in memory on devices.
|
||||
The number 0.5 may be stored as 0.49999999997 and if you attempt to compare these it will result in false.
|
||||
*/
|
||||
public func isCloseTo(_ other: Double) -> Bool {
|
||||
abs(self - other) < Self.CLOSENESS_TOLERANCE
|
||||
}
|
||||
|
||||
/**
|
||||
Returns true if the current value is greater than the provided one, or if they are closer than the "closeness" tolerance level,
|
||||
false otherwise.
|
||||
|
||||
NOTE: This is needed due to the way Double/Float instances are stored in memory on devices.
|
||||
The number 0.5 may be stored as 0.49999999997 and if you attempt to compare these it will result in false.
|
||||
*/
|
||||
public func isCloseToOrGreaterThan(_ other: Double) -> Bool {
|
||||
(self - other) > -Self.CLOSENESS_TOLERANCE
|
||||
}
|
||||
|
||||
/**
|
||||
Returns true if the current value is less than the provided one, or if they are closer than the "closeness" tolerance level, false otherwise.
|
||||
|
||||
NOTE: This is needed due to the way Double/Float instances are stored in memory on devices.
|
||||
The number 0.5 may be stored as 0.49999999997 and if you attempt to compare these it will result in false.
|
||||
*/
|
||||
public func isCloseToOrLessThan(_ other: Double) -> Bool {
|
||||
(other - self) > -Self.CLOSENESS_TOLERANCE
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the value rounded upwards and converted to an `Int`.
|
||||
*/
|
||||
public var roundedUp: Int {
|
||||
Int(rounded(.up))
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the value rounded downwards and converted to an `Int`.
|
||||
*/
|
||||
public var roundedDown: Int {
|
||||
Int(rounded(.down))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue