├── .gitignore ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── ExtendedKit │ ├── AppSession │ │ └── AppSession.swift │ ├── CLLocation.swift │ ├── Cocoa │ │ ├── NSActionHandler.swift │ │ ├── NSMenu.swift │ │ └── NSTextField.swift │ ├── CoreData │ │ ├── Fetch.swift │ │ ├── FetchFilter.swift │ │ ├── FetchObserver.swift │ │ ├── FetchOne.swift │ │ ├── FetchResults.swift │ │ ├── Fetchable.swift │ │ └── NSFetchRequest.swift │ ├── ExtendedKit.swift │ ├── Logging │ │ ├── CoreDataLogHandler.swift │ │ ├── LogEntry.swift │ │ ├── LogRedactor.swift │ │ ├── Logger.Metadata.swift │ │ ├── Logger.swift │ │ └── OSLogHandler.swift │ ├── Map │ │ ├── MapCluster.swift │ │ ├── MapItem.swift │ │ ├── MapView.swift │ │ └── _MapView.swift │ ├── MapKit │ │ └── MKCoordinateRegion.swift │ ├── Path+NSWorkspace.swift │ ├── Platform.swift │ ├── SwiftUI │ │ ├── Alert.swift │ │ ├── Angle.swift │ │ ├── Binding.swift │ │ ├── EdgeInsets.swift │ │ ├── InterfaceOrientation │ │ │ ├── InterfaceHostingController.swift │ │ │ ├── InterfaceOrientation.swift │ │ │ ├── InterfacePreferenceKeys.swift │ │ │ └── View+InterfaceOrientation.swift │ │ ├── PartialCapsule.swift │ │ ├── Path.swift │ │ ├── PathControl.swift │ │ ├── ProxyTransferable.swift │ │ ├── Section.swift │ │ ├── Staged.swift │ │ ├── Symbol+SwiftUI.swift │ │ ├── TextStyle.swift │ │ ├── UnitPoint.swift │ │ └── View.swift │ ├── Symbol.swift │ └── XML │ │ └── XMLNode.swift ├── ExtendedMacros │ └── Macros.swift ├── ExtendedMacrosImpl │ ├── DiagnosticMacro.swift │ ├── ExtendedMacrosImpl.swift │ └── ObfuscateMacro.swift ├── ExtendedObjC │ ├── include │ │ ├── ExtendedObjC.h │ │ └── Runtime.h │ └── src │ │ └── Runtime.m ├── ExtendedSwift │ ├── CloudKit │ │ ├── CKQueryObserver.swift │ │ ├── CKRecordCodable.swift │ │ └── CKSubscriptionObserver.swift │ ├── Codable │ │ ├── AnyCodingKey.swift │ │ ├── CodingPath.swift │ │ ├── Decoding.swift │ │ ├── DecodingError.swift │ │ ├── Encoding.swift │ │ ├── EncodingError.swift │ │ ├── Plist │ │ │ ├── PlistCodableSupport.swift │ │ │ ├── PlistDecoder.swift │ │ │ └── PlistEncoder.swift │ │ ├── PlistCodable.swift │ │ └── UserDefaultsCodable.swift │ ├── Combine │ │ └── Subscriber.swift │ ├── CoreData │ │ ├── NSAttributeDescription.swift │ │ ├── NSEntityDescription.swift │ │ ├── NSFetchRequestPublisher.swift │ │ ├── NSManagedObjectContext.swift │ │ ├── NSManagedObjectModel.swift │ │ ├── NSPersistedAttributeType.swift │ │ └── NSPersistentContainer.swift │ ├── FSEvents │ │ ├── FSEvent.swift │ │ ├── FSPublisher.swift │ │ └── NSFileObserver.swift │ ├── Foundation │ │ ├── ActionScheduler.swift │ │ ├── Bookmark.swift │ │ ├── Bundle.swift │ │ ├── Calendar.swift │ │ ├── DateFormatter.swift │ │ ├── Entitlements.swift │ │ ├── FileManager.swift │ │ ├── FileWrapper.swift │ │ ├── Geometry.swift │ │ ├── NSLock.swift │ │ ├── NSPredicate.swift │ │ ├── Process+Which.swift │ │ ├── Process.swift │ │ ├── ProcessInfo.swift │ │ ├── RunLoop.swift │ │ ├── Scanner+Characters.swift │ │ ├── Scanner+Data.swift │ │ ├── Scanner+Peek.swift │ │ ├── Scanner+Scan.swift │ │ ├── Scanner.swift │ │ ├── Spotlight.swift │ │ └── URL.swift │ ├── Mach │ │ ├── Dyld.swift │ │ ├── FAT.swift │ │ ├── LoadCommands │ │ │ ├── CodeSignature.swift │ │ │ ├── LoadDylibCommand.swift │ │ │ ├── MachLoadCommand.swift │ │ │ ├── RPathCommand.swift │ │ │ └── UUIDLoadCommand.swift │ │ ├── Mach+Convenience.swift │ │ ├── Mach+Header.swift │ │ ├── Mach+LoadCommand.swift │ │ ├── Mach+Section.swift │ │ └── Mach+Segment.swift │ ├── Path │ │ ├── Path+Bundle.swift │ │ ├── Path+Data+String.swift │ │ ├── Path+FileHandle.swift │ │ ├── Path+FileManager.swift │ │ ├── Path+Helpers.swift │ │ ├── Path+Home.swift │ │ ├── Path+URL.swift │ │ ├── Path.swift │ │ ├── PathProtocol.swift │ │ └── RelativePath.swift │ ├── Resources │ │ └── entities.json │ ├── Result Builders │ │ └── ArrayBuilder.swift │ ├── Standard │ │ ├── AnyAsyncSequence.swift │ │ ├── Atomic.swift │ │ ├── BidirectionalCollection.swift │ │ ├── Bimap.swift │ │ ├── Bool.swift │ │ ├── Buildable.swift │ │ ├── Character.swift │ │ ├── Clocks │ │ │ ├── Clock.swift │ │ │ ├── Date+InstantProtocol.swift │ │ │ ├── ManualClock.swift │ │ │ ├── MutableClock.swift │ │ │ └── UserClock.swift │ │ ├── Collection+Flatten.swift │ │ ├── Collection+Slicing.swift │ │ ├── Collection+Trimming.swift │ │ ├── Collection+Unique.swift │ │ ├── Collection.swift │ │ ├── Comparable.swift │ │ ├── CountedSet.swift │ │ ├── Data.swift │ │ ├── Date.swift │ │ ├── Dictionary.swift │ │ ├── Duration.swift │ │ ├── Error.swift │ │ ├── Fatal.swift │ │ ├── ID.swift │ │ ├── Int.swift │ │ ├── Interpolator.swift │ │ ├── KeyPath.swift │ │ ├── LazyTask.swift │ │ ├── Never.swift │ │ ├── Newtype.swift │ │ ├── Normalized.swift │ │ ├── Once.swift │ │ ├── Optional.swift │ │ ├── Range.swift │ │ ├── RangeReplaceableCollection.swift │ │ ├── Regex+Replacements.swift │ │ ├── Regex.swift │ │ ├── Result+CollectionOperators.swift │ │ ├── Result+Operators.swift │ │ ├── Result.swift │ │ ├── Sandbox.swift │ │ ├── Sendable.swift │ │ ├── Sequence.swift │ │ ├── SetAlgebra.swift │ │ ├── Shim.swift │ │ ├── String+HTML.swift │ │ ├── String+Interpolation.swift │ │ ├── String.swift │ │ ├── TaskQueue.swift │ │ ├── UUID.swift │ │ ├── Unsafe.swift │ │ ├── Updateable.swift │ │ └── Warn.swift │ └── Trees │ │ ├── BreadthFirstTraversal.swift │ │ ├── InOrderTraversal.swift │ │ ├── PostOrderTraversal.swift │ │ ├── PreAndPostOrderTraversal.swift │ │ ├── PreOrderTraversal.swift │ │ ├── Tree.swift │ │ └── TreeTraversal.swift ├── ExtendedTest │ └── XCTestHelpers.swift ├── HTTP │ ├── Bodies │ │ ├── DataBody.swift │ │ ├── FormBody.swift │ │ └── JSONBody.swift │ ├── Loader │ │ ├── DeduplicatingLoader.swift │ │ ├── HTTPEnvironmentLoader.swift │ │ ├── HTTPLoader.swift │ │ ├── ManualLoader.swift │ │ ├── ModifyingLoader.swift │ │ ├── RetryLoader.swift │ │ ├── ThrottledLoader.swift │ │ └── URLSessionLoader.swift │ ├── Message │ │ ├── HTTPBody.swift │ │ ├── HTTPError.swift │ │ ├── HTTPHeader.swift │ │ ├── HTTPHeaders.swift │ │ ├── HTTPMethod.swift │ │ ├── HTTPOption.swift │ │ ├── HTTPQuery.swift │ │ ├── HTTPRequest.swift │ │ ├── HTTPRequestToken.swift │ │ ├── HTTPResponse.swift │ │ ├── HTTPResult+Convenience.swift │ │ ├── HTTPResult.swift │ │ └── HTTPStatus.swift │ ├── Options │ │ ├── AuthenticationChallenges.swift │ │ ├── Deduplication.swift │ │ ├── Redirection.swift │ │ ├── RetryStrategy.swift │ │ └── Throttling.swift │ └── Utilities │ │ ├── AsyncStream.swift │ │ ├── Duration.swift │ │ ├── HTTPError+Construction.swift │ │ ├── HTTPRequest+URLRequest.swift │ │ ├── HTTPResponse+URLResponse.swift │ │ ├── LoaderChain.swift │ │ ├── Pairs.swift │ │ └── URLSession │ │ ├── URLSessionAdapter.swift │ │ ├── URLSessionAdapterDelegate.swift │ │ └── URLSessionTaskState.swift ├── PrivateAPI │ ├── include │ │ ├── AppSession.h │ │ ├── GregorianDate+Format.h │ │ ├── GregorianDate+Formatters.h │ │ ├── GregorianDate.h │ │ ├── JSON+Serialize.h │ │ ├── JSON.h │ │ ├── NSUUID+Time.h │ │ └── SignalSafe.h │ └── src │ │ ├── AppSession.m │ │ ├── GregorianDate+Format.m │ │ ├── GregorianDate+Formatters.m │ │ ├── GregorianDate.m │ │ ├── JSON+Serialize.m │ │ ├── JSON.m │ │ ├── NSUUID+Time.m │ │ └── SignalSafe.m └── debug │ └── debug.swift └── Tests ├── ExtendedObjCTests └── RuntimeTests.m ├── ExtendedSwiftTests ├── BuilderTests.swift ├── CharacterTests.swift ├── ClockTests.swift ├── CollectionTests.swift ├── CountedSetTests.swift ├── PlistCodableTests.swift ├── RegexTests.swift ├── ScannerTests.swift ├── StringTests.swift └── TaskQueueTests.swift ├── HTTPTests ├── DeduplicationTests.swift ├── HTTP+TestHelpers.swift ├── HTTPTests.swift ├── ManualLoaderTests.swift ├── ModifyingLoaderTests.swift └── RetryTests.swift └── PrivateAPITests └── GregorianDateTests.m /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | .swiftpm 11 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-algorithms", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-algorithms.git", 7 | "state" : { 8 | "revision" : "b14b7f4c528c942f121c8b860b9410b2bf57825e", 9 | "version" : "1.0.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-log", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-log.git", 16 | "state" : { 17 | "revision" : "32e8d724467f8fe623624570367e3d50c5638e46", 18 | "version" : "1.5.2" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-numerics", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/swift-numerics", 25 | "state" : { 26 | "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", 27 | "version" : "1.0.2" 28 | } 29 | } 30 | ], 31 | "version" : 2 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExtendedSwift 2 | 3 | Things that should¹ be in Swift, but aren't. 4 | 5 | Because anything worth doing is worth over-doing. 6 | 7 | ## Disclaimer 8 | 9 | This is a repository of things I have found useful while writing apps and packages for myself. 10 | 11 | You should not include this project in your own code. Instead, use it as an example of how I approach problems and a possible way that you might solve the same problem. 12 | 13 | --- 14 | 15 | ¹ - By "should", I mean "At some point in the past, I found this useful enough to pull out into its own thing." 16 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/CLLocation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/14/23. 6 | // 7 | 8 | import Foundation 9 | import CoreLocation 10 | 11 | extension CLLocationCoordinate2D: Equatable { 12 | 13 | public static func ==(lhs: Self, rhs: Self) -> Bool { 14 | guard lhs.isValid == rhs.isValid else { return false } 15 | return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude 16 | } 17 | 18 | public var isValid: Bool { CLLocationCoordinate2DIsValid(self) } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/Cocoa/NSActionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSActionHandler.swift 3 | // ExtendedKit 4 | // 5 | // Created by Dave DeLong on 12/31/23. 6 | // 7 | 8 | import Foundation 9 | 10 | #if os(macOS) 11 | 12 | import AppKit 13 | 14 | extension NSControl { 15 | 16 | public func setActionHandler(_ handler: @escaping (Any) -> Void) { 17 | let target = ActionHandler(handler: handler) 18 | self.target = target 19 | self.action = #selector(ActionHandler.perform(_:)) 20 | objc_setAssociatedObject(self, &actionKey, handler, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) 21 | } 22 | 23 | } 24 | 25 | extension NSMenuItem { 26 | 27 | public func setActionHandler(_ handler: @escaping (Any) -> Void) { 28 | let target = ActionHandler(handler: handler) 29 | self.target = target 30 | self.action = #selector(ActionHandler.perform(_:)) 31 | objc_setAssociatedObject(self, &actionKey, handler, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) 32 | } 33 | } 34 | 35 | #endif 36 | 37 | private var actionKey: UInt8 = 0 38 | 39 | @objc 40 | private class ActionHandler: NSObject { 41 | 42 | let handler: (Any) -> Void 43 | 44 | init(handler: @escaping (Any) -> Void) { 45 | self.handler = handler 46 | } 47 | 48 | @objc 49 | func perform(_ sender: Any) { 50 | handler(sender) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/Cocoa/NSMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSMenu.swift 3 | // ExtendedKit 4 | // 5 | // Created by Dave DeLong on 3/8/23. 6 | // 7 | 8 | import Foundation 9 | 10 | #if os(macOS) 11 | 12 | import Cocoa 13 | 14 | extension NSMenu { 15 | 16 | @discardableResult 17 | func addItem(withTitle title: String, target: AnyObject, action: Selector, representedObject: Any? = nil) -> NSMenuItem { 18 | let i = self.addItem(withTitle: title, action: action, keyEquivalent: "") 19 | i.target = target 20 | i.representedObject = representedObject 21 | return i 22 | } 23 | 24 | @discardableResult 25 | func addItem(withTitle title: String, handler: @escaping (Any) -> Void) -> NSMenuItem { 26 | let i = self.addItem(withTitle: title, action: nil, keyEquivalent: "") 27 | i.setActionHandler(handler) 28 | return i 29 | } 30 | 31 | } 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/CoreData/Fetch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 7/9/23. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | @propertyWrapper 12 | public struct Fetch: DynamicProperty { 13 | @Environment(\.managedObjectContext) var context 14 | 15 | @StateObject private var observer: FetchObserver 16 | 17 | private let transaction: Transaction 18 | public let baseFilter: T.Filter 19 | 20 | public var autoupdates: Bool { 21 | get { observer.autoUpdates } 22 | nonmutating set { observer.autoUpdates = newValue } 23 | } 24 | 25 | public var autoupdateBinding: Binding { 26 | Binding(get: { observer.autoUpdates }, 27 | set: { observer.autoUpdates = $0 }) 28 | } 29 | 30 | public var wrappedValue: FetchResults { 31 | observer.results 32 | } 33 | 34 | public var filter: T.Filter { 35 | get { observer.filter } 36 | nonmutating set { observer.filter = newValue } 37 | } 38 | 39 | public var projectedValue: Binding { 40 | return Binding(get: { self.filter }, 41 | set: { self.filter = $0 }) 42 | } 43 | 44 | public init(_ filter: T.Filter, animation: Animation? = nil) { 45 | _observer = StateObject(wrappedValue: FetchObserver(filter: filter, context: nil)) 46 | 47 | self.baseFilter = filter 48 | self.transaction = Transaction(animation: animation) 49 | } 50 | 51 | public mutating func update() { 52 | observer.withoutPublishingChanges { 53 | $0.managedObjectContext = self.context 54 | } 55 | _ = observer.results // trigger a fetch (if necessary) by calling .results 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/CoreData/FetchFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 7/9/23. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | public protocol FetchFilter: Equatable { 12 | 13 | associatedtype ResultType: NSFetchRequestResult 14 | 15 | func fetchRequest() -> NSFetchRequest 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/CoreData/FetchOne.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 7/9/23. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | @propertyWrapper 12 | public struct FetchOne: DynamicProperty { 13 | @Fetch var inner: FetchResults 14 | 15 | public var wrappedValue: T? { inner.first } 16 | 17 | public var filter: T.Filter { 18 | get { _inner.filter } 19 | nonmutating set { _inner.filter = newValue } 20 | } 21 | 22 | public var projectedValue: Binding { _inner.projectedValue } 23 | public var autoupdateBinding: Binding { _inner.autoupdateBinding } 24 | 25 | public init(_ filter: T.Filter, animation: Animation? = nil) { 26 | _inner = Fetch(filter, animation: animation) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/CoreData/FetchResults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 7/9/23. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | public struct FetchResults: RandomAccessCollection { 12 | 13 | private class Cache { 14 | var storage = Dictionary() 15 | } 16 | 17 | private let results: NSArray 18 | private let context: NSManagedObjectContext? 19 | private var cache = Cache() 20 | 21 | internal init() { 22 | self.context = nil 23 | self.results = NSArray() 24 | } 25 | 26 | internal init(results: NSArray, context: NSManagedObjectContext) { 27 | self.context = context 28 | self.results = results 29 | } 30 | 31 | public var count: Int { results.count } 32 | public var startIndex: Int { 0 } 33 | public var endIndex: Int { count } 34 | 35 | public var allValues: Array { 36 | guard let context else { return [] } 37 | 38 | return context.performAndWait { 39 | (0 ..< count).map { self._onqueue_get(at: $0) } 40 | } 41 | } 42 | 43 | public subscript(position: Int) -> T { 44 | return context!.performAndWait { 45 | self._onqueue_get(at: position) 46 | } 47 | } 48 | 49 | private func _onqueue_get(at position: Int) -> T { 50 | if let e = cache.storage[position] { return e } 51 | let object = results.object(at: position) as! T.Filter.ResultType 52 | let built = T(result: object) 53 | cache.storage[position] = built 54 | return built 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/CoreData/Fetchable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 7/9/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol Fetchable { 11 | associatedtype Filter: FetchFilter 12 | 13 | init(result: Filter.ResultType) 14 | } 15 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/CoreData/NSFetchRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 7/9/23. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | extension NSFetchRequest { 12 | 13 | @objc internal func update(toMatch other: NSFetchRequest) { 14 | 15 | self.update(\.predicate, to: other.predicate) 16 | self.update(\.sortDescriptors, to: other.sortDescriptors) 17 | self.update(\.fetchLimit, to: other.fetchLimit) 18 | self.update(\.affectedStores, to: other.affectedStores) 19 | self.update(\.includesSubentities, to: other.includesSubentities) 20 | 21 | self.update(\.includesPropertyValues, to: other.includesPropertyValues) 22 | self.update(\.returnsObjectsAsFaults, to: other.returnsObjectsAsFaults) 23 | self.update(\.relationshipKeyPathsForPrefetching, to: other.relationshipKeyPathsForPrefetching) 24 | self.update(\.includesPendingChanges, to: other.includesPendingChanges) 25 | self.update(\.returnsDistinctResults, to: other.returnsDistinctResults) 26 | 27 | self.update(\.fetchOffset, to: other.fetchOffset) 28 | self.update(\.fetchBatchSize, to: other.fetchBatchSize) 29 | self.update(\.shouldRefreshRefetchedObjects, to: other.shouldRefreshRefetchedObjects) 30 | 31 | self.update(\.havingPredicate, to: other.havingPredicate) 32 | 33 | // these are Array values and are not equatable 34 | self.propertiesToFetch = other.propertiesToFetch 35 | self.propertiesToGroupBy = other.propertiesToGroupBy 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/ExtendedKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 12/17/23. 6 | // 7 | 8 | import Foundation 9 | 10 | @_exported import ExtendedObjC 11 | @_exported import ExtendedSwift 12 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/Logging/LogEntry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 7/10/23. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | import Logging 11 | 12 | public struct LogEntry: Identifiable { 13 | public var id: Date { timestamp } 14 | public let timestamp: Date 15 | public let category: String 16 | public let level: Logger.Level 17 | public let message: Logger.Message 18 | public let location: String 19 | public let source: String 20 | public let metadata: Logger.Metadata? 21 | } 22 | 23 | extension LogEntry: Fetchable { 24 | 25 | internal static var entity: NSEntityDescription { LogSchema.entities[0] } 26 | 27 | public struct Filter: FetchFilter { 28 | public typealias ResultType = NSManagedObject 29 | public static let all = Filter() 30 | 31 | public func fetchRequest() -> NSFetchRequest { 32 | let r = NSFetchRequest() 33 | r.entity = LogEntry.entity 34 | r.predicate = NSPredicate(value: true) 35 | r.sortDescriptors = [NSSortDescriptor(key: "timestamp", ascending: false)] 36 | 37 | return r 38 | } 39 | 40 | public var description: String { "all" } 41 | } 42 | 43 | public init(result: NSManagedObject) { 44 | self.timestamp = result.value(forKey: "timestamp") as! Date 45 | self.level = Logger.Level(rawValue: result.value(forKey: "level") as! String)! 46 | self.category = result.value(forKey: "category") as! String 47 | self.message = Logger.Message(stringLiteral: result.value(forKey: "message") as! String) 48 | self.location = result.value(forKey: "location") as? String ?? "" 49 | self.source = result.value(forKey: "source") as! String 50 | self.metadata = nil 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/Logging/LogRedactor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 7/10/23. 6 | // 7 | 8 | import Foundation 9 | import Logging 10 | 11 | struct Redactor: LogHandler { 12 | 13 | private(set) var inner: LogHandler 14 | 15 | subscript(metadataKey key: String) -> Logger.Metadata.Value? { 16 | get { inner[metadataKey: key] } 17 | set { inner[metadataKey: key] = newValue } 18 | } 19 | 20 | var metadata: Logger.Metadata { 21 | get { inner.metadata } 22 | set { inner.metadata = newValue } 23 | } 24 | 25 | var logLevel: Logger.Level { 26 | get { inner.logLevel } 27 | set { inner.logLevel = newValue } 28 | } 29 | 30 | func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, source: String, file: String, function: String, line: UInt) { 31 | let redacted = Logger.Message(stringLiteral: message.description.redact()) 32 | inner.log(level: level, message: redacted, metadata: metadata, source: source, file: file, function: function, line: line) 33 | } 34 | 35 | 36 | } 37 | 38 | extension String { 39 | 40 | fileprivate func redact() -> String { 41 | var copy = self 42 | copy.replaceAllMatches(of: redactor, with: #""\#(\.1)": "(redacted)""#) 43 | return copy 44 | } 45 | 46 | } 47 | 48 | private let redactor = /"(token|password|data)"\s*:\s*"(.+?)"/ 49 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/Logging/Logger.Metadata.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 7/10/23. 6 | // 7 | 8 | import Foundation 9 | import Logging 10 | 11 | extension Logger.MetadataValue: Codable { 12 | 13 | public init(from decoder: Decoder) throws { 14 | if var u = try? decoder.unkeyedContainer() { 15 | var a = Array() 16 | while u.isAtEnd == false { 17 | a.append(try u.decode(Self.self)) 18 | } 19 | self = .array(a) 20 | } else if let k = try? decoder.container(keyedBy: AnyCodingKey.self) { 21 | var d = Logger.Metadata() 22 | for key in k.allKeys { 23 | d[key.stringValue] = try k.decode(Self.self, forKey: key) 24 | } 25 | self = .dictionary(d) 26 | } else if let s = try? decoder.singleValueContainer() { 27 | let string = try s.decode(String.self) 28 | self = .string(string) 29 | } else { 30 | throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not decode MetadataValue")) 31 | } 32 | } 33 | 34 | public func encode(to encoder: Encoder) throws { 35 | switch self { 36 | case .array(let values): 37 | var unkeyed = encoder.unkeyedContainer() 38 | for v in values { try unkeyed.encode(v) } 39 | case .dictionary(let d): 40 | var keyed = encoder.container(keyedBy: AnyCodingKey.self) 41 | for (k, v) in d { 42 | try keyed.encode(v, forKey: AnyCodingKey(stringValue: k)) 43 | } 44 | case .string(let s): 45 | var single = encoder.singleValueContainer() 46 | try single.encode(s) 47 | case .stringConvertible(let s): 48 | var single = encoder.singleValueContainer() 49 | try single.encode(s.description) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/Logging/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 7/10/23. 6 | // 7 | 8 | import Foundation 9 | import Logging 10 | 11 | extension Logger { 12 | 13 | public static func named(_ name: String) -> Logger { 14 | return namedLogs.with { existing in 15 | if let e = existing[name] { return e } 16 | let new = Logger(label: name) 17 | existing[name] = new 18 | return new 19 | } 20 | } 21 | 22 | } 23 | 24 | private let namedLogs = Atomic>([:]) 25 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/Logging/OSLogHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 7/22/23. 6 | // 7 | 8 | import Foundation 9 | import Logging 10 | import OSLog 11 | 12 | public struct OSLogHandler: LogHandler { 13 | 14 | private let logger: os.Logger 15 | 16 | public var metadata: Logging.Logger.Metadata 17 | 18 | public var logLevel: Logging.Logger.Level 19 | 20 | public init(subsystem: String, label: String) { 21 | self.logger = os.Logger(subsystem: subsystem, category: label) 22 | self.metadata = [:] 23 | self.logLevel = .trace 24 | } 25 | 26 | public subscript(metadataKey _: String) -> Logging.Logger.Metadata.Value? { 27 | get { return nil } 28 | set(newValue) { } 29 | } 30 | 31 | public func log(level: Logging.Logger.Level, message: Logging.Logger.Message, metadata: Logging.Logger.Metadata?, source: String, file: String, function: String, line: UInt) { 32 | let type: OSLogType 33 | switch level { 34 | case .trace: type = .debug 35 | case .debug: type = .debug 36 | case .info: type = .info 37 | case .notice: type = .default 38 | case .warning: type = .error 39 | case .error: type = .error 40 | case .critical: type = .fault 41 | } 42 | logger.log(level: type, "\(message.description, privacy: .auto)") 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/Map/MapCluster.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/14/23. 6 | // 7 | 8 | import Foundation 9 | import MapKit 10 | 11 | public protocol MapCluster { 12 | associatedtype Marker: MKAnnotationView 13 | associatedtype Item: MapItem 14 | 15 | static func configure(view: Marker, members: Array) 16 | 17 | } 18 | 19 | extension Never: MapCluster { 20 | public typealias Marker = MKAnnotationView 21 | public typealias Item = BasicMapItem 22 | 23 | public static func configure(view: MKAnnotationView, members: Array) { 24 | fatalError("Unreachable") 25 | } 26 | } 27 | 28 | extension MapCluster { 29 | 30 | internal static var isValid: Bool { 31 | return self != Never.self 32 | } 33 | 34 | internal static var markerView: Marker.Type { Marker.self } 35 | 36 | internal static var clusteringIdentifier: String? { 37 | guard isValid else { return nil } 38 | return String(describing: self) 39 | } 40 | 41 | internal static func configure(annotations: Array, for marker: MKAnnotationView) { 42 | guard let typedMarker = marker as? Marker else { return } 43 | 44 | let mapAnnotations = annotations.filter(is: _MapAnnotation.self) 45 | let items = mapAnnotations.map(\.item) 46 | if items.isEmpty { return } 47 | 48 | self.configure(view: typedMarker, members: items) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/Map/MapItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/14/23. 6 | // 7 | 8 | import Foundation 9 | import MapKit 10 | 11 | public protocol MapItem: Identifiable { 12 | var coordinate: CLLocationCoordinate2D { get } 13 | var cluster: (any MapCluster.Type) { get } 14 | var title: String? { get } 15 | var subtitle: String? { get } 16 | } 17 | 18 | extension MapItem { 19 | public var cluster: any MapCluster.Type { Never.self } 20 | public var title: String? { nil } 21 | public var subtitle: String? { nil } 22 | 23 | internal var resolvedCluster: (any MapCluster.Type)? { 24 | if cluster.isValid { return cluster } 25 | return nil 26 | } 27 | } 28 | 29 | public struct BasicMapItem: MapItem { 30 | public let mapItem: MKMapItem 31 | public var id: ObjectIdentifier { ObjectIdentifier(mapItem) } 32 | public var coordinate: CLLocationCoordinate2D { mapItem.placemark.coordinate } 33 | public var title: String? { mapItem.name ?? mapItem.placemark.title } 34 | public var subtitle: String? { mapItem.placemark.subtitle } 35 | 36 | public init(mapItem: MKMapItem) { 37 | self.mapItem = mapItem 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/MapKit/MKCoordinateRegion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/14/23. 6 | // 7 | 8 | import Foundation 9 | import MapKit 10 | 11 | extension MKCoordinateRegion: Equatable { 12 | public static func ==(lhs: Self, rhs: Self) -> Bool { 13 | return lhs.center == rhs.center && lhs.span == rhs.span 14 | } 15 | } 16 | 17 | extension MKCoordinateSpan: Equatable { 18 | public static func ==(lhs: Self, rhs: Self) -> Bool { 19 | return lhs.latitudeDelta == rhs.latitudeDelta && lhs.longitudeDelta == rhs.longitudeDelta 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/Path+NSWorkspace.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 11/9/23. 6 | // 7 | 8 | #if os(macOS) 9 | 10 | import AppKit 11 | 12 | extension NSWorkspace { 13 | 14 | public func icon(for path: Path) -> NSImage { 15 | return self.icon(forFile: path.fileSystemPath) 16 | } 17 | 18 | } 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/Platform.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/4/23. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | #if os(macOS) 12 | import AppKit 13 | 14 | public typealias PlatformImage = NSImage 15 | public typealias PlatformView = NSView 16 | public typealias PlatformViewRepresentable = NSViewRepresentable 17 | 18 | #else 19 | import UIKit 20 | 21 | public typealias PlatformImage = UIImage 22 | public typealias PlatformView = UIView 23 | public typealias PlatformViewRepresentable = UIViewRepresentable 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/SwiftUI/Alert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 12/13/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | 12 | public func alert(_ titleKey: LocalizedStringKey, item: Binding, @ViewBuilder message: (V) -> Message, @ViewBuilder actions: (V) -> Actions) -> some View { 13 | self.alert(titleKey, isPresented: item.isNotNull(), actions: { 14 | if let value = item.wrappedValue { 15 | actions(value) 16 | } 17 | }, message: { 18 | if let value = item.wrappedValue { 19 | message(value) 20 | } 21 | }) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/SwiftUI/Angle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 10/12/24. 6 | // 7 | 8 | import Foundation 9 | import ExtendedSwift 10 | import SwiftUI 11 | 12 | // option-shift-8 on US keyboards 13 | postfix operator ° 14 | 15 | extension Double { 16 | 17 | public static postfix func °(lhs: Double) -> Angle { 18 | return Angle(degrees: lhs) 19 | } 20 | 21 | } 22 | 23 | extension Angle { 24 | 25 | public init(clockHour: Int, cycle: Locale.HourCycle = .oneToTwelve) { 26 | let up = 270.0 27 | let degreesPerHour = 360.0 / Double(cycle.numberOfHours) 28 | self.init(degrees: up + (Double(clockHour) * degreesPerHour)) 29 | } 30 | 31 | public init(clockMinute: Int) { 32 | let up = 270.0 33 | let degreesPerMinute = 360.0 / 60.0 34 | self.init(degrees: up + (Double(clockMinute) * degreesPerMinute)) 35 | } 36 | 37 | } 38 | 39 | extension CGPoint { 40 | 41 | public init(angle: Angle, length: Double) { 42 | self.init(polarAngle: angle.radians, length: length) 43 | } 44 | 45 | } 46 | 47 | extension Locale.HourCycle { 48 | 49 | fileprivate var numberOfHours: Int { 50 | switch self { 51 | case .oneToTwelve: return 12 52 | case .zeroToEleven: return 12 53 | case .oneToTwentyFour: return 24 54 | case .zeroToTwentyThree: return 24 55 | @unknown default: return 12 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/SwiftUI/EdgeInsets.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/7/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension EdgeInsets { 11 | 12 | public static var zero: EdgeInsets { EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0) } 13 | 14 | public init(horizontal: CGFloat) { 15 | self.init(horizontal: horizontal, vertical: 0) 16 | } 17 | 18 | public init(vertical: CGFloat) { 19 | self.init(horizontal: 0, vertical: vertical) 20 | } 21 | 22 | public init(horizontal: CGFloat, vertical: CGFloat) { 23 | self.init(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) 24 | } 25 | 26 | public var inverted: Self { 27 | return .init(top: -top, leading: -leading, bottom: -bottom, trailing: -trailing) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/SwiftUI/InterfaceOrientation/InterfaceOrientation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 11/9/23. 6 | // 7 | 8 | #if !os(macOS) 9 | 10 | import SwiftUI 11 | import UIKit 12 | 13 | public struct InterfaceOrientationContainer: View { 14 | 15 | private let content: Content 16 | 17 | public init(@ViewBuilder view: () -> Content) { 18 | self.content = view() 19 | } 20 | 21 | public var body: some View { 22 | _InterfaceOrientationContainer(content: content) 23 | } 24 | 25 | } 26 | 27 | private struct _InterfaceOrientationContainer: UIViewControllerRepresentable { 28 | 29 | let content: Content 30 | 31 | func makeUIViewController(context: Context) -> UIInterfaceHostingController { 32 | return UIInterfaceHostingController(rootView: content) 33 | } 34 | 35 | func updateUIViewController(_ uiViewController: UIInterfaceHostingController, context: Context) { 36 | 37 | } 38 | } 39 | 40 | 41 | #endif 42 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/SwiftUI/InterfaceOrientation/InterfacePreferenceKeys.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 11/9/23. 6 | // 7 | 8 | #if !os(macOS) 9 | import SwiftUI 10 | import UIKit 11 | 12 | enum Keys { 13 | 14 | struct PreferredInterfaceOrientation: PreferenceKey { 15 | static var defaultValue: UIInterfaceOrientation = .portrait 16 | static func reduce(value: inout UIInterfaceOrientation, nextValue: () -> UIInterfaceOrientation) { 17 | value = nextValue() 18 | } 19 | } 20 | 21 | struct SupportedInterfaceOrientations: PreferenceKey { 22 | static var defaultValue: UIInterfaceOrientationMask = .allButUpsideDown 23 | static func reduce(value: inout UIInterfaceOrientationMask, nextValue: () -> UIInterfaceOrientationMask) { 24 | value = nextValue() 25 | } 26 | } 27 | 28 | struct PrefersHomeIndicatorAutoHidden: PreferenceKey { 29 | static var defaultValue: Bool = false 30 | static func reduce(value: inout Bool, nextValue: () -> Bool) { 31 | value = nextValue() 32 | } 33 | } 34 | 35 | struct PrefersStatusBarHidden: PreferenceKey { 36 | static var defaultValue: Bool = false 37 | static func reduce(value: inout Bool, nextValue: () -> Bool) { 38 | value = nextValue() 39 | } 40 | } 41 | 42 | struct PreferredStatusBarStyle: PreferenceKey { 43 | static var defaultValue: UIStatusBarStyle = .default 44 | static func reduce(value: inout UIStatusBarStyle, nextValue: () -> UIStatusBarStyle) { 45 | value = nextValue() 46 | } 47 | } 48 | } 49 | 50 | #endif 51 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/SwiftUI/PartialCapsule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 12/19/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct PartialCapsule: Shape { 11 | 12 | public var edges: HorizontalEdge.Set 13 | 14 | public init(_ edges: HorizontalEdge.Set = .all) { 15 | self.edges = edges 16 | } 17 | 18 | public func path(in rect: CGRect) -> SwiftUI.Path { 19 | var p = Path() 20 | 21 | let radius = rect.size.height / 2.0 22 | 23 | if edges.contains(.leading) { 24 | p.move(to: CGPoint(x: radius, y: 0)) 25 | } else { 26 | p.move(to: CGPoint(x: 0, y: 0)) 27 | } 28 | 29 | if edges.contains(.trailing) { 30 | p.addLine(to: CGPoint(x: rect.size.width - radius, y: 0)) 31 | p.addArc(center: CGPoint(x: rect.size.width - radius, y: rect.size.height / 2), radius: radius, startAngle: .degrees(270), endAngle: .degrees(90), clockwise: false) 32 | } else { 33 | p.addLine(to: CGPoint(x: rect.size.width, y: 0)) 34 | p.addLine(to: CGPoint(x: rect.size.width, y: rect.size.height)) 35 | } 36 | 37 | if edges.contains(.leading) { 38 | p.addLine(to: CGPoint(x: radius, y: rect.size.height)) 39 | p.addArc(center: CGPoint(x: radius, y: rect.size.height / 2), radius: radius, startAngle: .degrees(270), endAngle: .degrees(90), clockwise: true) 40 | } else { 41 | p.addLine(to: CGPoint(x: 0, y: rect.size.height)) 42 | p.addLine(to: CGPoint(x: 0, y: 0)) 43 | } 44 | 45 | p.closeSubpath() 46 | 47 | return p 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/SwiftUI/Path.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 9/13/23. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | extension SwiftUI.Path { 12 | 13 | // return point at the curve 14 | public func point(at offset: Normalized) -> CGPoint { 15 | return trimmedPath(from: 0, to: offset.rawValue).cgPath.currentPoint 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/SwiftUI/ProxyTransferable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 8/18/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public protocol ProxyTransferable: Transferable where Representation == ProxyRepresentation { 11 | associatedtype Proxy: Transferable 12 | 13 | // Implement one of these: 14 | func transferableProxy() throws -> Proxy 15 | func transferableProxy() async throws -> Proxy 16 | } 17 | 18 | extension ProxyTransferable { 19 | 20 | public func transferableProxy() throws -> Proxy { 21 | throw UnimplementedError() 22 | } 23 | 24 | public func transferableProxy() async throws -> Proxy { 25 | // Needed because `try self.transferableProxy()` thinks its refering to the async version 26 | let method: () throws -> Proxy = self.transferableProxy 27 | return try method() 28 | } 29 | 30 | public static var transferRepresentation: ProxyRepresentation { 31 | ProxyRepresentation(exporting: { simple in 32 | return try await simple.transferableProxy() 33 | }) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/SwiftUI/Section.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/7/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Section where Content: View, Footer: View, Parent == Text { 11 | 12 | public init(_ header: LocalizedStringKey, @ViewBuilder footer: () -> Footer, @ViewBuilder content: () -> Content) { 13 | self.init(content: content, header: { Text(header) }, footer: footer) 14 | } 15 | 16 | } 17 | 18 | extension Section where Content: View, Parent == Text, Footer == Text { 19 | 20 | public init(header: LocalizedStringKey, footer: LocalizedStringKey, @ViewBuilder content: () -> Content) { 21 | self.init(content: content, header: { Text(header) }, footer: { Text(footer) }) 22 | } 23 | 24 | @_disfavoredOverload 25 | public init(header: String, footer: LocalizedStringKey, @ViewBuilder content: () -> Content) { 26 | self.init(content: content, header: { Text(header) }, footer: { Text(footer) }) 27 | } 28 | 29 | @_disfavoredOverload 30 | public init(header: LocalizedStringKey, footer: String, @ViewBuilder content: () -> Content) { 31 | self.init(content: content, header: { Text(header) }, footer: { Text(footer) }) 32 | } 33 | 34 | @_disfavoredOverload 35 | public init(header: String, footer: String, @ViewBuilder content: () -> Content) { 36 | self.init(content: content, header: { Text(header) }, footer: { Text(footer) }) 37 | } 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/SwiftUI/Staged.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 1/2/24. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | // source: https://gist.github.com/IanKeen/a85e4ed74a10a25341c44a98f43cf386 12 | // note: originally called "Transaction", but there's already a SwiftUI type with that name 13 | 14 | @propertyWrapper 15 | @dynamicMemberLookup 16 | public struct Staged: DynamicProperty { 17 | 18 | @State private var derived: Value 19 | @Binding private var source: Value 20 | 21 | fileprivate init(source: Binding) { 22 | self._source = source 23 | self._derived = State(wrappedValue: source.wrappedValue) 24 | } 25 | 26 | public var wrappedValue: Value { 27 | get { derived } 28 | nonmutating set { derived = newValue } 29 | } 30 | 31 | public var projectedValue: Staged { self } 32 | 33 | public subscript(dynamicMember keyPath: WritableKeyPath) -> Binding { 34 | return $derived[dynamicMember: keyPath] 35 | } 36 | 37 | public var binding: Binding { $derived } 38 | 39 | public func commit() { 40 | source = derived 41 | } 42 | 43 | public func revert() { 44 | derived = source 45 | } 46 | } 47 | 48 | extension Staged where Value: Equatable { 49 | public var hasChanges: Bool { return source != derived } 50 | } 51 | 52 | extension Binding { 53 | public func staged() -> Staged { .init(source: self) } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/ExtendedKit/SwiftUI/UnitPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 5/22/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension CGRect { 11 | 12 | public subscript(_ unitPoint: UnitPoint) -> CGRect { 13 | // via https://mas.to/@kayleesdevlog/112475071330602405 14 | return CGRect(x: minX + unitPoint.x * width, 15 | y: minY + unitPoint.y * height, 16 | width: width, 17 | height: height) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /Sources/ExtendedMacros/Macros.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 9/15/23. 6 | // 7 | 8 | import Foundation 9 | 10 | @freestanding(expression) 11 | public macro obfuscate(_ string: String) -> String = #externalMacro(module: "ExtendedMacrosImpl", type: "ObfuscateMacro") 12 | 13 | @freestanding(declaration) 14 | public macro todo(_ string: String) = #externalMacro(module: "ExtendedMacrosImpl", type: "DiagnosticMacro") 15 | 16 | @freestanding(declaration) 17 | public macro info(_ string: String) = #externalMacro(module: "ExtendedMacrosImpl", type: "DiagnosticMacro") 18 | 19 | @freestanding(declaration) 20 | public macro note(_ string: String) = #externalMacro(module: "ExtendedMacrosImpl", type: "DiagnosticMacro") 21 | -------------------------------------------------------------------------------- /Sources/ExtendedMacrosImpl/ExtendedMacrosImpl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 9/15/23. 6 | // 7 | 8 | import Foundation 9 | import SwiftCompilerPlugin 10 | import SwiftSyntaxMacros 11 | 12 | @main 13 | struct ExtendedMacrosImplPlugin: CompilerPlugin { 14 | let providingMacros: Array = [ 15 | ObfuscateMacro.self, 16 | DiagnosticMacro.self, 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /Sources/ExtendedMacrosImpl/ObfuscateMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 9/27/23. 6 | // 7 | 8 | import Foundation 9 | import SwiftSyntax 10 | import SwiftSyntaxBuilder 11 | import SwiftSyntaxMacros 12 | import MacroToolkit 13 | 14 | struct ObfuscateMacro: ExpressionMacro { 15 | 16 | static func expansion(of node: some SwiftSyntax.FreestandingMacroExpansionSyntax, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> SwiftSyntax.ExprSyntax { 17 | 18 | guard node.argumentList.count == 1 else { 19 | throw MacroError("Expecting 1 argument, but got \(node.argumentList.count)") 20 | } 21 | 22 | let stringArg = node.argumentList.first! 23 | 24 | guard let string = Expr(stringArg.expression).asStringLiteral?.value else { 25 | throw MacroError("Argument is not a string literal") 26 | } 27 | 28 | let encodedString = Data(string.utf8).base64EncodedString() 29 | 30 | return "String(base64String: \"\(raw: encodedString)\")!" 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Sources/ExtendedObjC/include/ExtendedObjC.h: -------------------------------------------------------------------------------- 1 | // 2 | // Header.h 3 | // 4 | // 5 | // Created by Dave DeLong on 7/22/23. 6 | // 7 | 8 | #ifndef ExtendedObjC_h 9 | #define ExtendedObjC_h 10 | 11 | #import "Runtime.h" 12 | 13 | #endif /* ExtendedObjC_h */ 14 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/CloudKit/CKSubscriptionObserver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 10/24/23. 6 | // 7 | 8 | #if canImport(CloudKit) 9 | 10 | import Foundation 11 | import CloudKit 12 | import Combine 13 | 14 | public class CKSubscriptionObserver: ObservableObject { 15 | 16 | private let database: CKDatabase 17 | private let timerInterval: TimeInterval 18 | private var sink: AnyCancellable? 19 | 20 | public let objectDidChange = ObservableObjectPublisher() 21 | public private(set) var isSearching = false 22 | public private(set) var results = Array() 23 | public private(set) var mostRecentError: Error? 24 | 25 | public init(database: CKDatabase, refreshInterval: TimeInterval = 60) { 26 | self.database = database 27 | self.timerInterval = refreshInterval 28 | self.triggerRefresh() 29 | } 30 | 31 | private func rescheduleTimer() { 32 | sink = Timer.publish(every: timerInterval, tolerance: timerInterval/10, on: .main, in: .default, options: nil) 33 | .autoconnect() 34 | .sink(receiveValue: { [unowned self] _ in 35 | self.triggerRefresh() 36 | }) 37 | } 38 | 39 | private func performChange(_ action: () -> Void) { 40 | self.objectWillChange.send() 41 | action() 42 | self.objectDidChange.send() 43 | } 44 | 45 | @MainActor 46 | public func refresh() async { 47 | if self.isSearching == true { return } 48 | 49 | self.performChange { 50 | self.isSearching = true 51 | self.sink?.cancel() 52 | self.sink = nil 53 | } 54 | 55 | let result = await Result { try await self.database.allSubscriptions() } 56 | 57 | self.performChange { 58 | switch result { 59 | case .success(let subs): 60 | self.mostRecentError = nil 61 | if subs != self.results { 62 | self.results = subs 63 | } 64 | case .failure(let error): 65 | self.mostRecentError = error 66 | } 67 | self.isSearching = false 68 | } 69 | 70 | self.rescheduleTimer() 71 | } 72 | 73 | public func triggerRefresh() { 74 | Task { await refresh() } 75 | } 76 | 77 | } 78 | 79 | #endif 80 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Codable/AnyCodingKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/1/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct AnyCodingKey: CodingKey { 11 | 12 | public var stringValue: String 13 | 14 | public init(stringValue: String) { 15 | self.stringValue = stringValue 16 | self.intValue = nil 17 | } 18 | 19 | public var intValue: Int? 20 | 21 | public init?(intValue: Int) { 22 | self.intValue = intValue 23 | self.stringValue = "\(intValue)" 24 | } 25 | 26 | public init(_ other: Other) { 27 | self.stringValue = other.stringValue 28 | self.intValue = other.intValue 29 | } 30 | 31 | } 32 | 33 | extension AnyCodingKey: ExpressibleByIntegerLiteral, ExpressibleByStringLiteral { 34 | public init(integerLiteral value: Int) { 35 | self.intValue = value 36 | self.stringValue = "\(value)" 37 | } 38 | 39 | public init(stringLiteral value: String) { 40 | self.intValue = nil 41 | self.stringValue = value 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Codable/Encoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/7/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Encoder { 11 | 12 | public func anyKeyedContainer() -> KeyedEncodingContainer { 13 | self.container(keyedBy: AnyCodingKey.self) 14 | } 15 | 16 | } 17 | 18 | extension KeyedEncodingContainer { 19 | 20 | public mutating func encode(ifNotEmpty value: E?, key: Key) throws { 21 | guard let value else { return } 22 | guard value.isEmpty == false else { return } 23 | try self.encode(value, forKey: key) 24 | } 25 | 26 | } 27 | 28 | extension UnkeyedEncodingContainer { 29 | 30 | public mutating func encode(ifNotEmpty value: E?) throws { 31 | guard let value else { return } 32 | guard value.isEmpty == false else { return } 33 | try self.encode(value) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Codable/EncodingError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 11/26/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Encoder { 11 | 12 | public var encodingError: EncodingErrorBuilder { 13 | .init(provider: self, codingPath: codingPath) 14 | } 15 | 16 | public func errorContext(for key: CodingKey? = nil, debugDescription: String, underlyingError: Error? = nil) -> EncodingError.Context { 17 | var newPath = codingPath 18 | if let key { newPath.append(key) } 19 | return .init(codingPath: newPath, 20 | debugDescription: debugDescription, 21 | underlyingError: underlyingError) 22 | } 23 | 24 | } 25 | 26 | extension KeyedEncodingContainer { 27 | 28 | public var encodingError: EncodingErrorBuilder { 29 | .init(provider: self, codingPath: codingPath) 30 | } 31 | 32 | } 33 | 34 | extension KeyedEncodingContainerProtocol { 35 | 36 | public var encodingError: EncodingErrorBuilder { 37 | .init(provider: self, codingPath: codingPath) 38 | } 39 | 40 | } 41 | 42 | extension UnkeyedEncodingContainer { 43 | 44 | public var encodingError: EncodingErrorBuilder { 45 | .init(provider: self, codingPath: codingPath) 46 | } 47 | 48 | } 49 | 50 | extension SingleValueEncodingContainer { 51 | 52 | public var encodingError: EncodingErrorBuilder { 53 | .init(provider: self, codingPath: codingPath) 54 | } 55 | 56 | } 57 | 58 | public struct EncodingErrorBuilder

{ 59 | let provider: P 60 | let codingPath: Array 61 | 62 | public func context(debugDescription: String = "", underlyingError: Error? = nil) -> EncodingError.Context { 63 | return EncodingError.Context(codingPath: codingPath, 64 | debugDescription: debugDescription, 65 | underlyingError: underlyingError) 66 | } 67 | 68 | public func invalidValue(_ value: Any, _ debugDescription: String = "", underlyingError: Error? = nil) -> EncodingError { 69 | 70 | let ctx = context(debugDescription: debugDescription, underlyingError: underlyingError) 71 | return .invalidValue(value, ctx) 72 | 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Codable/Plist/PlistCodableSupport.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 11/25/23. 6 | // 7 | 8 | import Foundation 9 | 10 | internal let PlistNullValue = "ExtendedSwift.PlistCoding.NULL" 11 | 12 | internal protocol PlistWriter { 13 | func write(value: Any) 14 | } 15 | 16 | internal class PlistRootWriter: PlistWriter { 17 | var encoded: Any? 18 | 19 | init() { } 20 | 21 | func write(value: Any) { 22 | encoded = value 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Codable/PlistCodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 11/26/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public class PlistEncoder { 11 | 12 | public init() { } 13 | 14 | public func encode(_ value: T) throws -> Any { 15 | let wrapper = PlistRootWriter() 16 | let encoder = _PlistEncoder(codingPath: [], parent: wrapper) 17 | try value.encode(to: encoder) 18 | return wrapper.encoded! 19 | } 20 | 21 | } 22 | 23 | public class PlistDecoder { 24 | 25 | public init() { } 26 | 27 | public func decode(_ type: V.Type = V.self, from plist: Any) throws -> V { 28 | let decoder = _PlistDecoder(codingPath: [], contents: plist) 29 | return try V.init(from: decoder) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Codable/UserDefaultsCodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 11/24/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension UserDefaults { 11 | 12 | public func encode(_ value: V, forKey key: String) throws { 13 | let encoded = try PlistEncoder().encode(value) 14 | self.set(encoded, forKey: key) 15 | } 16 | 17 | public func decode(_ type: V.Type = V.self, forKey key: String) throws -> V { 18 | guard let value = self.object(forKey: key) else { 19 | throw DecodingError.keyNotFound(AnyCodingKey(stringValue: key), 20 | DecodingError.Context(codingPath: [], debugDescription: "")) 21 | } 22 | return try PlistDecoder().decode(from: value) 23 | } 24 | 25 | public func decodeIfPresent(_ type: V.Type = V.self, forKey key: String) throws -> V? { 26 | guard let value = self.object(forKey: key) else { 27 | return nil 28 | } 29 | return try PlistDecoder().decode(from: value) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Combine/Subscriber.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 12/19/23. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | extension Subscriber { 12 | 13 | public func receive(_ result: Result) -> Subscribers.Demand { 14 | switch result { 15 | case .success(let input): 16 | return self.receive(input) 17 | case .failure(let failure): 18 | self.receive(completion: .failure(failure)) 19 | return .none 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/CoreData/NSEntityDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 11/16/23. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | extension NSEntityDescription { 12 | 13 | public convenience init(_ name: String, properties: Array) { 14 | self.init() 15 | self.name = name 16 | self.properties = properties 17 | } 18 | 19 | public convenience init(_ name: String, @ArrayBuilder properties: () -> Array) { 20 | self.init() 21 | self.name = name 22 | self.properties = properties() 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/CoreData/NSManagedObjectModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 12/26/23. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | extension NSManagedObjectModel { 12 | 13 | public convenience init(@ArrayBuilder entities: () -> Array) { 14 | self.init() 15 | self.entities = entities() 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/CoreData/NSPersistedAttributeType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 12/26/23. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | public protocol NSPersistedAttributeType { 12 | static var attributeType: NSAttributeType { get } 13 | static var transformer: NSValueTransformerName? { get } 14 | } 15 | 16 | extension NSPersistedAttributeType { 17 | public static var transformer: NSValueTransformerName? { nil } 18 | } 19 | 20 | extension String: NSPersistedAttributeType { 21 | public static var attributeType: NSAttributeType { .stringAttributeType } 22 | } 23 | 24 | extension Int: NSPersistedAttributeType { 25 | public static var attributeType: NSAttributeType { .integer64AttributeType } 26 | } 27 | 28 | extension Int64: NSPersistedAttributeType { 29 | public static var attributeType: NSAttributeType { .integer64AttributeType } 30 | } 31 | 32 | extension Int32: NSPersistedAttributeType { 33 | public static var attributeType: NSAttributeType { .integer32AttributeType } 34 | } 35 | 36 | extension Int16: NSPersistedAttributeType { 37 | public static var attributeType: NSAttributeType { .integer16AttributeType } 38 | } 39 | 40 | extension Decimal: NSPersistedAttributeType { 41 | public static var attributeType: NSAttributeType { .decimalAttributeType } 42 | } 43 | 44 | extension Double: NSPersistedAttributeType { 45 | public static var attributeType: NSAttributeType { .doubleAttributeType } 46 | } 47 | 48 | extension Float: NSPersistedAttributeType { 49 | public static var attributeType: NSAttributeType { .floatAttributeType } 50 | } 51 | 52 | extension Bool: NSPersistedAttributeType { 53 | public static var attributeType: NSAttributeType { .booleanAttributeType } 54 | } 55 | 56 | extension Date: NSPersistedAttributeType { 57 | public static var attributeType: NSAttributeType { .dateAttributeType } 58 | } 59 | 60 | extension Data: NSPersistedAttributeType { 61 | public static var attributeType: NSAttributeType { .binaryDataAttributeType } 62 | } 63 | 64 | extension UUID: NSPersistedAttributeType { 65 | public static var attributeType: NSAttributeType { .UUIDAttributeType } 66 | } 67 | 68 | extension URL: NSPersistedAttributeType { 69 | public static var attributeType: NSAttributeType { .URIAttributeType } 70 | } 71 | 72 | extension NSManagedObjectID: NSPersistedAttributeType { 73 | public static var attributeType: NSAttributeType { .objectIDAttributeType } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/CoreData/NSPersistentContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 10/30/23. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | extension NSPersistentContainer { 12 | 13 | @discardableResult 14 | public func withBackgroundContext(perform work: @escaping (NSManagedObjectContext) -> T) async -> T { 15 | let moc = self.newBackgroundContext() 16 | return await moc.perform(work) 17 | } 18 | 19 | @discardableResult 20 | public func withBackgroundContext(perform work: @escaping (NSManagedObjectContext) throws -> T) async throws -> T { 21 | let moc = self.newBackgroundContext() 22 | return try await moc.perform(work) 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Foundation/Bundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/2/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Bundle { 11 | 12 | public var identifier: ID? { 13 | guard let bundleIdentifier else { return nil } 14 | return ID(rawValue: bundleIdentifier) 15 | } 16 | 17 | public var shortVersionString: String? { 18 | if let s = string(forInfoDictionaryKey: "CFBundleShortVersionString") { return s } 19 | return nil 20 | } 21 | 22 | public var versionString: String? { 23 | if let s = string(forInfoDictionaryKey: "CFBundleVersion") { return s } 24 | if let s = shortVersionString { return s } 25 | return "" 26 | } 27 | 28 | public var name: String { 29 | if let s = string(forInfoDictionaryKey: "CFBundleName") { return s } 30 | if let s = string(forInfoDictionaryKey: "CFBundleDisplayName") { return s } 31 | if let s = bundleIdentifier { return s } 32 | return bundleURL.lastPathComponent 33 | } 34 | 35 | public func string(forInfoDictionaryKey key: String) -> String? { 36 | return object(forInfoDictionaryKey: key) as? String 37 | } 38 | 39 | public var entitlementsDictionary: Dictionary? { 40 | if self == Bundle.main { return ProcessInfo.processInfo.entitlementsDictionary } 41 | 42 | guard let executableURL = self.executableURL else { return nil } 43 | let path = Path(executableURL) 44 | return ProcessInfo.entitlementsDictionary(for: path) 45 | } 46 | 47 | public var entitlements: Entitlements? { 48 | guard let dict = entitlementsDictionary else { return nil } 49 | return Entitlements(source: dict) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Foundation/Entitlements.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 7/9/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Entitlements { 11 | 12 | let source: Dictionary 13 | 14 | public subscript(key: String) -> Any? { 15 | return source[key] 16 | } 17 | 18 | public subscript(string key: String) -> String? { 19 | return source[key] as? String 20 | } 21 | 22 | } 23 | 24 | extension Entitlements { 25 | 26 | public var teamIdentifier: String? { self[string: "com.apple.developer.team-identifier"] } 27 | public var appIdentifier: String? { self[string: "application-identifier"] } 28 | 29 | public var keychainAccessGroups: Array? { 30 | self["keychain-access-groups"] as? Array 31 | } 32 | 33 | public var applicationGroups: Array? { 34 | self["com.apple.security.application-groups"] as? Array 35 | } 36 | 37 | public var isSandboxed: Bool { 38 | #if os(macOS) 39 | (self["com.apple.security.app-sandbox"] as? Bool) == true 40 | #else 41 | return true 42 | #endif 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Foundation/FileManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/4/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension FileManager { 11 | 12 | public func directoryExists(at url: URL) -> Bool { 13 | return self.folderExists(at: url) 14 | } 15 | 16 | public func createDirectory(at url: URL, withIntermediateDirectorys: Bool = true, attributes: [FileAttributeKey: Any]? = nil) throws { 17 | 18 | try self.createDirectory(atPath: url.path(percentEncoded: false), 19 | withIntermediateDirectories: withIntermediateDirectorys, 20 | attributes: attributes) 21 | } 22 | 23 | public func folderExists(at url: URL) -> Bool { 24 | var isDir: ObjCBool = false 25 | let exists = self.fileExists(atPath: url.path(percentEncoded: false), isDirectory: &isDir) 26 | return exists && isDir.boolValue == true 27 | } 28 | 29 | public func fileExists(at url: URL) -> Bool { 30 | var isDir: ObjCBool = false 31 | let exists = self.fileExists(atPath: url.path(percentEncoded: false), isDirectory: &isDir) 32 | return exists && isDir.boolValue == false 33 | } 34 | 35 | public func displayName(at url: URL) -> String { 36 | return self.displayName(atPath: url.path(percentEncoded: false)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Foundation/FileWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 12/7/23. 6 | // 7 | 8 | import Foundation 9 | import UniformTypeIdentifiers 10 | 11 | extension FileWrapper { 12 | 13 | public var contentsURL: URL? { 14 | guard let anyValue = self.value(forKey: "_contentsURL") else { return nil } 15 | return anyValue as? URL 16 | } 17 | 18 | public var fileType: UTType? { 19 | guard let anyValue = self.value(forKey: "_fileType") else { return nil } 20 | guard let idString = anyValue as? String else { return nil } 21 | return UTType(idString) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Foundation/Geometry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/1/23. 6 | // 7 | 8 | import Foundation 9 | import CoreGraphics 10 | 11 | extension CGFloat { 12 | 13 | public static let tau: CGFloat = 2 * CGFloat.pi 14 | 15 | } 16 | 17 | 18 | extension CGPoint { 19 | 20 | public init(polarAngle: CGFloat, length: CGFloat) { 21 | self.init(x: cos(polarAngle) * length, 22 | y: sin(polarAngle) * -length) 23 | } 24 | 25 | } 26 | 27 | extension CGRect { 28 | 29 | public var center: CGPoint { 30 | get { 31 | return CGPoint(x: midX, y: midY) 32 | } 33 | set { 34 | self = .init(center: newValue, size: size) 35 | } 36 | } 37 | 38 | public var area: CGFloat { 39 | if self.isEmpty { return 0 } 40 | if self.isNull { return 0 } 41 | if self.isInfinite { return CGFloat.greatestFiniteMagnitude } 42 | 43 | return abs(self.width * self.height) 44 | } 45 | 46 | public init(center: CGPoint, size: CGSize) { 47 | let origin = CGPoint(x: center.x - size.width / 2, 48 | y: center.y - size.height / 2) 49 | 50 | self.init(origin: origin, size: size) 51 | } 52 | 53 | public init(center: CGPoint, square: CGFloat) { 54 | self.init(center: center, size: CGSize(width: square, height: square)) 55 | } 56 | 57 | public init(origin: CGPoint, square: CGFloat) { 58 | self.init(origin: origin, size: CGSize(width: square, height: square)) 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Foundation/NSPredicate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 10/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension NSPredicate { 11 | 12 | public static var `true`: NSPredicate { NSPredicate(value: true) } 13 | 14 | public static var `false`: NSPredicate { NSPredicate(value: false) } 15 | 16 | public static func and(_ predicates: Array) -> NSPredicate { 17 | switch predicates.count { 18 | case 0: return .true 19 | case 1: return predicates[0] 20 | default: return NSCompoundPredicate(andPredicateWithSubpredicates: predicates) 21 | } 22 | } 23 | 24 | } 25 | 26 | extension NSPredicate: Tree { 27 | 28 | public typealias Value = NSPredicate 29 | 30 | public var treeValue: NSPredicate { self } 31 | 32 | public var children: Array> { 33 | if let compound = self as? NSCompoundPredicate { 34 | return compound.subpredicates as! Array 35 | } 36 | return [] 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Foundation/ProcessInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/3/23. 6 | // 7 | 8 | import Foundation 9 | @_implementationOnly import PrivateAPI 10 | 11 | extension ProcessInfo { 12 | 13 | public var isDebuggerAttached: Bool { 14 | #if DEBUG 15 | var name = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()] 16 | let nameSize = UInt32(name.count) 17 | var info: kinfo_proc = kinfo_proc() 18 | var info_size = MemoryLayout.size 19 | 20 | let success = sysctl(&name, nameSize, &info, &info_size, nil, 0) == 0 21 | 22 | if success && ((info.kp_proc.p_flag & P_TRACED) != 0) { 23 | return true 24 | } 25 | #endif 26 | return false 27 | } 28 | 29 | public var entitlementsDictionary: Dictionary { _entitlementsDict } 30 | 31 | public var entitlements: Entitlements { Entitlements(source: _entitlementsDict) } 32 | 33 | public static func entitlementsDictionary(for path: Path) -> Dictionary? { 34 | autoreleasepool { 35 | let fat = FAT(contentsOf: path) 36 | return fat?.headers.firstMap(\.entitlements) 37 | } 38 | } 39 | 40 | } 41 | 42 | private let _entitlementsDict: Dictionary = { 43 | let exe = Dyld.executable 44 | return exe.header.entitlements ?? [:] 45 | }() 46 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Foundation/RunLoop.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/10/23. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | extension RunLoop { 12 | 13 | public func onEntry(perform work: @escaping () -> Void) -> Cancellable { 14 | return self.on(.entry, perform: work) 15 | } 16 | 17 | public func onExit(perform work: @escaping () -> Void) -> Cancellable { 18 | return self.on(.exit, perform: work) 19 | } 20 | 21 | public func on(_ activity: CFRunLoopActivity, perform work: @escaping () -> Void) -> Cancellable { 22 | let observer = CFRunLoopObserverCreateWithHandler(nil, 23 | activity.rawValue, 24 | true, 25 | 0, 26 | { _, _ in 27 | work() 28 | }) 29 | 30 | let cf = self.getCFRunLoop() 31 | CFRunLoopAddObserver(cf, observer, CFRunLoopMode.defaultMode) 32 | 33 | return AnyCancellable({ 34 | CFRunLoopPerformBlock(cf, CFRunLoopMode.defaultMode as CFTypeRef, { 35 | CFRunLoopRemoveObserver(cf, observer, CFRunLoopMode.defaultMode) 36 | }) 37 | }) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Foundation/Scanner+Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 6/7/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Scanner where Element == UInt8 { 11 | 12 | private mutating func scanBinaryInteger() throws -> BI { 13 | let data = try scan(count: MemoryLayout.size) 14 | return data.reversed().reduce(BI.zero) { $0 << 8 + BI($1) } 15 | } 16 | 17 | @discardableResult 18 | public mutating func scan(_ type: BI.Type = BI.self) throws -> BI { 19 | return try scanBinaryInteger() 20 | } 21 | 22 | @discardableResult 23 | public mutating func scanUInt32() throws -> UInt32 { 24 | return try scanBinaryInteger() 25 | } 26 | 27 | @discardableResult 28 | public mutating func scanStruct(_ type: T.Type = T.self) throws -> T { 29 | let bytes = try scan(count: MemoryLayout.size) 30 | let data = Data(bytes) 31 | return data.withUnsafeBytes { $0.load(as: type) } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Foundation/Scanner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/3/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Scanner { 11 | 12 | public typealias Element = C.Element 13 | 14 | public enum ScannerError: Error { 15 | case isAtEnd 16 | case invalidElement(C.Element) 17 | case invalidSequence(C.SubSequence) 18 | } 19 | 20 | public let data: C 21 | public var location: C.Index { 22 | willSet { 23 | guard newValue >= data.startIndex && newValue <= data.endIndex else { 24 | fatalError("Setting the location to an invalid index is a programmer error") 25 | } 26 | } 27 | } 28 | public var isAtEnd: Bool { location >= data.endIndex } 29 | 30 | public init(data: C) { 31 | self.data = data 32 | self.location = data.startIndex 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Foundation/URL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/1/23. 6 | // 7 | 8 | import Foundation 9 | import UniformTypeIdentifiers 10 | 11 | extension URL { 12 | 13 | public static var devNull: URL { URL(fileURLWithPath: "/dev/null") } 14 | 15 | public init(_ raw: StaticString) { 16 | self.init(string: raw.description)! 17 | } 18 | 19 | public var parent: URL? { return self.deletingLastPathComponent() } 20 | 21 | public var contentType: UTType? { 22 | let values = try? self.resourceValues(forKeys: [.contentTypeKey]) 23 | return values?.contentType 24 | } 25 | 26 | public func relationship(to other: URL) -> FileManager.URLRelationship { 27 | var relationship: FileManager.URLRelationship = .other 28 | _ = try? FileManager.default.getRelationship(&relationship, ofDirectoryAt: self, toItemAt: other) 29 | return relationship 30 | } 31 | 32 | public func contains(_ other: URL) -> Bool { 33 | let r = relationship(to: other) 34 | return (r == .contains || r == .same) 35 | } 36 | 37 | public func value(forQueryItem item: String) -> String? { 38 | guard let c = URLComponents(url: self, resolvingAgainstBaseURL: true) else { 39 | return nil 40 | } 41 | return c.queryItems?.first(where: { $0.name == item })?.value 42 | } 43 | 44 | public func deletingQueryItem(_ name: String) -> URL { 45 | guard var c = URLComponents(url: self, resolvingAgainstBaseURL: true) else { 46 | return self 47 | } 48 | c.queryItems = c.queryItems?.filter { $0.name != name } 49 | return c.url ?? self 50 | } 51 | 52 | } 53 | 54 | extension URL { 55 | 56 | public var isIncludedInBackup: Bool { 57 | get { 58 | let values = try? resourceValues(forKeys: [.isExcludedFromBackupKey]) 59 | return (values?.isExcludedFromBackup == false) 60 | } 61 | nonmutating set { 62 | var copy = self 63 | var newValues = URLResourceValues() 64 | newValues.isExcludedFromBackup = (newValue == false) 65 | try? copy.setResourceValues(newValues) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Mach/LoadCommands/LoadDylibCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 3/15/24. 6 | // 7 | 8 | import Foundation 9 | import MachO 10 | 11 | extension Mach { 12 | 13 | public struct LoadDylib: MachLoadCommand { 14 | 15 | public let header: Mach.Header 16 | public let pointer: UnsafePointer 17 | 18 | public var isWeak: Bool { 19 | self.commandType == .loadWeakDylib 20 | } 21 | 22 | public var name: String { 23 | return readLCString(\dylib_command.dylib.name) 24 | } 25 | 26 | public init?(header: Mach.Header, pointer: UnsafePointer) { 27 | self.header = header 28 | self.pointer = pointer 29 | 30 | guard self.commandType == .loadDylib || self.commandType == .loadWeakDylib else { return nil } 31 | } 32 | 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Mach/LoadCommands/RPathCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 3/15/24. 6 | // 7 | 8 | import Foundation 9 | import MachO 10 | 11 | extension Mach { 12 | 13 | public struct RPath: MachLoadCommand { 14 | 15 | public let header: Mach.Header 16 | public let pointer: UnsafePointer 17 | 18 | public var name: String { 19 | return readLCString(\rpath_command.path) 20 | } 21 | 22 | public init?(header: Mach.Header, pointer: UnsafePointer) { 23 | self.header = header 24 | self.pointer = pointer 25 | 26 | guard self.commandType == .rpath else { return nil } 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Mach/LoadCommands/UUIDLoadCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 10/1/23. 6 | // 7 | 8 | import Foundation 9 | import MachO 10 | 11 | extension Mach { 12 | 13 | public struct UUID: MachLoadCommand { 14 | 15 | public let header: Mach.Header 16 | public let pointer: UnsafePointer 17 | 18 | public var uuid: Foundation.UUID { 19 | let ptr = pointer.rebound(to: uuid_command.self).pointer(to: \.uuid)! 20 | return Foundation.UUID(uuid: ptr.pointee) 21 | } 22 | 23 | public init?(header: Mach.Header, pointer: UnsafePointer) { 24 | self.header = header 25 | self.pointer = pointer 26 | 27 | guard self.commandType == .uuid else { return nil } 28 | } 29 | 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Mach/Mach+LoadCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 9/30/23. 6 | // 7 | 8 | import Foundation 9 | import MachO 10 | 11 | extension Mach { 12 | 13 | public struct LoadCommand: CustomStringConvertible, MachLoadCommand { 14 | 15 | public let header: Mach.Header 16 | public let pointer: UnsafePointer 17 | 18 | public init(header: Mach.Header, pointer: UnsafePointer) { 19 | self.header = header 20 | self.pointer = pointer 21 | } 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Path/Path+Bundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/2/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Bundle { 11 | 12 | public var path: Path { return Path(bundleURL) } 13 | 14 | public convenience init?(path: Path) { 15 | self.init(url: path.fileURL) 16 | } 17 | 18 | public func absolutePath(forResource name: String?, withExtension ext: String?) -> Path? { 19 | guard let url = self.url(forResource: name, withExtension: ext) else { return nil } 20 | return Path(url) 21 | } 22 | 23 | public func absolutePath(forResource name: String?, withExtension ext: String?, subdirectory subpath: String?) -> Path? { 24 | guard let url = self.url(forResource: name, withExtension: ext, subdirectory: subpath) else { return nil } 25 | return Path(url) 26 | } 27 | 28 | public func absolutePath(forResource name: String?, withExtension ext: String?, subdirectory subpath: String?, localization localizationName: String?) -> Path? { 29 | guard let url = self.url(forResource: name, withExtension: ext, subdirectory: subpath, localization: localizationName) else { return nil } 30 | return Path(url) 31 | } 32 | 33 | public func absolutePaths(forResourcesWithExtension ext: String?, subdirectory subpath: String?) -> Array? { 34 | guard let urls = self.urls(forResourcesWithExtension: ext, subdirectory: subpath) else { return nil } 35 | return urls.map { Path($0) } 36 | } 37 | 38 | public func absolutePaths(forResourcesWithExtension ext: String?, subdirectory subpath: String?, localization localizationName: String?) -> Array? { 39 | guard let urls = self.urls(forResourcesWithExtension: ext, subdirectory: subpath, localization: localizationName) else { return nil } 40 | return urls.map { Path($0) } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Path/Path+Data+String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 5/5/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Data { 11 | 12 | public init(contentsOf path: Path, options: Data.ReadingOptions = []) throws { 13 | try self.init(contentsOf: path.fileURL, options: options) 14 | } 15 | 16 | public func write(to path: Path, options: Data.WritingOptions = []) throws { 17 | try self.write(to: path.fileURL, options: options) 18 | } 19 | 20 | } 21 | 22 | extension String { 23 | 24 | public init(contentsOf path: Path) throws { 25 | try self.init(contentsOf: path.fileURL) 26 | } 27 | 28 | public func write(to path: Path, atomically: Bool = true, encoding: String.Encoding = .utf8) throws { 29 | try self.write(to: path.fileURL, atomically: atomically, encoding: encoding) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Path/Path+FileHandle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 2/1/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension FileHandle { 11 | 12 | public convenience init(forReading path: Path) throws { 13 | try self.init(forReadingFrom: path.fileURL) 14 | } 15 | 16 | public convenience init(forWriting path: Path) throws { 17 | try self.init(forWritingTo: path.fileURL) 18 | } 19 | 20 | public convenience init(forUpdating path: Path) throws { 21 | try self.init(forUpdating: path.fileURL) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Path/Path+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/1/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension PathComponent { 11 | 12 | internal static func reduce(_ components: Array, allowRelative: Bool) -> Array { 13 | var newComponents = Array() 14 | for c in components { 15 | switch c { 16 | case .this: 17 | continue 18 | case .up: 19 | if newComponents.last?.itemString != nil { 20 | // remove the last item 21 | newComponents.removeLast() 22 | } else if allowRelative == true { 23 | newComponents.append(c) 24 | } 25 | case .item(let s, let e): 26 | if s == PathSeparator && e == nil { 27 | continue 28 | } else if s.hasPrefix("~") && e == nil { 29 | newComponents.removeAll() 30 | let expanded = Path(fileSystemPath: s) 31 | newComponents.append(contentsOf: expanded.components) 32 | } else { 33 | newComponents.append(c) 34 | } 35 | } 36 | } 37 | return newComponents 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Path/Path+Home.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 12/10/23. 6 | // 7 | 8 | import Foundation 9 | 10 | #if os(macOS) 11 | 12 | /* 13 | import Collaboration 14 | import OpenDirectory 15 | 16 | // Leaving this here for posterity, since this is very interesting code 17 | 18 | public enum User { 19 | 20 | public static let currentUser: Any? = { 21 | let session = ODSession.default() 22 | let root = try! ODNode(session: session, name: "/Local/Default") 23 | let query = try! ODQuery(node: root, forRecordTypes: kODRecordTypeUsers, attribute: nil, matchType: 0, queryValues: nil, returnAttributes: nil, maximumResults: 0) 24 | let results = try! query.resultsAllowingPartial(false) 25 | print(results) 26 | return results 27 | }() 28 | 29 | public static let currentUserIdentity: CBIdentity? = { 30 | let q = CSIdentityQueryCreateForCurrentUser(kCFAllocatorDefault)?.takeRetainedValue() 31 | let flag = CSIdentityQueryFlags(kCSIdentityQueryGenerateUpdateEvents) 32 | guard CSIdentityQueryExecute(q, flag, nil) else { return nil } 33 | 34 | let results = CSIdentityQueryCopyResults(q)?.takeRetainedValue() as NSArray? 35 | guard let rawIdentities = results as? Array else { return nil } 36 | guard let rawIdentity = rawIdentities.first else { return nil } 37 | guard let rawPOSIXName = CSIdentityGetPosixName(rawIdentity)?.takeRetainedValue() else { return nil } 38 | 39 | let name = (rawPOSIXName as NSString) as String 40 | return CBIdentity(name: name, authority: .local()) 41 | }() 42 | 43 | } 44 | */ 45 | 46 | #endif 47 | 48 | extension Path { 49 | 50 | public static let home: Self = { 51 | #if os(macOS) 52 | let passInfo = getpwuid(getuid()) 53 | if let homeDir = passInfo?.pointee.pw_dir { 54 | let homePath = String(cString: homeDir) 55 | return Path(fileSystemPath: homePath) 56 | } 57 | #endif 58 | 59 | var p = Path(fileSystemPath: NSHomeDirectory()) 60 | if ProcessInfo.processInfo.entitlements.isSandboxed == false { 61 | return p 62 | } else { 63 | // ~/Library/Containers/{bundle id}/Data 64 | let homeComponents = p.components.dropLast(4) 65 | return Path(Array(homeComponents)) 66 | } 67 | }() 68 | 69 | } 70 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Path/Path+URL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/2/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URL { 11 | 12 | public var absolutePath: Path { Path(fileSystemPath: self.path(percentEncoded: false)) } 13 | 14 | public init(fileURL path: Path) { 15 | self = path.fileURL 16 | } 17 | 18 | } 19 | 20 | extension URLComponents { 21 | 22 | public var absolutePath: Path { 23 | get { Path(fileSystemPath: self.path) } 24 | set { self.path = newValue.fileSystemPath } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Path/RelativePath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/1/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct RelativePath: PathProtocol { 11 | public let components: Array 12 | 13 | public var fileSystemPath: String { 14 | return components.map(\.pathString).joined(separator: PathSeparator) 15 | } 16 | 17 | public init(path: String) { 18 | let pieces = (path as NSString).pathComponents 19 | self.init(pieces) 20 | } 21 | 22 | public init(_ pieces: String...) { 23 | let components = pieces.filter { $0 != PathSeparator }.map { PathComponent($0) } 24 | self.init(components) 25 | } 26 | 27 | public init(_ pieces: Array) { 28 | let components = pieces.filter { $0 != PathSeparator }.map { PathComponent($0) } 29 | self.init(components) 30 | } 31 | 32 | public init(_ components: Array = []) { 33 | self.init(components, shouldReduce: true) 34 | } 35 | 36 | public init(_ components: Array = [], shouldReduce: Bool) { 37 | if shouldReduce { 38 | self.components = PathComponent.reduce(components, allowRelative: true) 39 | } else { 40 | self.components = components 41 | } 42 | } 43 | 44 | public func resolve(against: Path) -> Path { 45 | return Path(against.components + components) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Result Builders/ArrayBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 12/26/23. 6 | // 7 | 8 | import Foundation 9 | 10 | @resultBuilder 11 | public struct ArrayBuilder { 12 | 13 | // single expressions of one or more value 14 | public static func buildExpression(_ expression: T) -> Array { 15 | return [expression] 16 | } 17 | 18 | public static func buildExpression(_ expression: Array) -> Array { 19 | return expression 20 | } 21 | 22 | // sequence of zero or more values 23 | public static func buildBlock() -> Array { 24 | return [] 25 | } 26 | 27 | public static func buildBlock(_ components: T...) -> Array { 28 | return components 29 | } 30 | 31 | public static func buildBlock(_ components: Array...) -> Array { 32 | return components.flattened() 33 | } 34 | 35 | // for loops of values 36 | public static func buildArray(_ components: [Array]) -> Array { 37 | return components.flattened() 38 | } 39 | 40 | // optionals 41 | 42 | public static func buildExpression(_ expression: T?) -> Array { 43 | return expression.map { [$0] } ?? [] 44 | } 45 | 46 | public static func buildExpression(_ expression: Array?) -> Array { 47 | return expression ?? [] 48 | } 49 | 50 | // conditionals 51 | 52 | public static func buildEither(first component: Array) -> Array { 53 | return component 54 | } 55 | 56 | public static func buildEither(second component: Array) -> Array { 57 | return component 58 | } 59 | 60 | // availability 61 | 62 | public static func buildLimitedAvailability(_ component: Array) -> Array { 63 | return component 64 | } 65 | 66 | // final 67 | 68 | public static func buildFinalResult(_ component: Array) -> Array { 69 | return component 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/AnyAsyncSequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/10/23. 6 | // 7 | 8 | import Foundation 9 | 10 | @discardableResult 11 | public func withExtendedLifetime(_ x: X, _ body: () async throws -> R) async rethrows -> R { 12 | defer { _fixLifetime(x) } 13 | return try await body() 14 | } 15 | 16 | @discardableResult 17 | public func withExtendedLifetime(_ x: X, _ body: (X) async throws -> R) async rethrows -> R { 18 | defer { _fixLifetime(x) } 19 | return try await body(x) 20 | } 21 | 22 | extension AsyncSequence { 23 | 24 | public func eraseToAnySequence() -> AnyAsyncSequence { 25 | return AnyAsyncSequence(self) 26 | } 27 | 28 | } 29 | 30 | public struct AnyAsyncSequence: AsyncSequence { 31 | public typealias AsyncIterator = AnyAsyncIterator 32 | 33 | private let getIterator: () -> AnyAsyncIterator 34 | 35 | public init(_ other: O) where O.Element == Element { 36 | self.getIterator = { 37 | AnyAsyncIterator(other.makeAsyncIterator()) 38 | } 39 | } 40 | 41 | public func makeAsyncIterator() -> AsyncIterator { 42 | return getIterator() 43 | } 44 | 45 | } 46 | 47 | public struct AnyAsyncIterator: AsyncIteratorProtocol { 48 | 49 | private let getNext: () async throws -> Element? 50 | 51 | public init(_ other: O) where O.Element == Element { 52 | var copy = other 53 | self.getNext = { try await copy.next() } 54 | } 55 | 56 | public func next() async throws -> Element? { 57 | return try await getNext() 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Atomic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 7/10/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public final class Atomic { 11 | private let _lock = NSLock() 12 | private var _value: T 13 | 14 | public init(_ value: T) { 15 | _value = value 16 | } 17 | 18 | public func with( _ value: (inout T) -> U) -> U { 19 | _lock.lock() 20 | let returnValue = value(&_value) 21 | _lock.unlock() 22 | return returnValue 23 | } 24 | 25 | public func modify( _ modify: (T) -> T) { 26 | _lock.lock() 27 | _value = modify(_value) 28 | _lock.unlock() 29 | } 30 | 31 | @discardableResult 32 | public func swap(_ value: T) -> T { 33 | _lock.lock() 34 | let current = _value 35 | _value = value 36 | _lock.unlock() 37 | return current 38 | } 39 | 40 | public var value: T { 41 | get { 42 | _lock.lock() 43 | let value = _value 44 | _lock.unlock() 45 | return value 46 | } 47 | set { 48 | _lock.lock() 49 | _value = newValue 50 | _lock.unlock() 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/BidirectionalCollection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/1/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension BidirectionalCollection { 11 | 12 | public var lastIndex: Index? { self.indices.last } 13 | 14 | public func last(_ k: Int) -> SubSequence { 15 | guard k > 0 else { 16 | return self[endIndex ..< endIndex] 17 | } 18 | 19 | if let start = self.index(self.endIndex, offsetBy: -k, limitedBy: self.startIndex) { 20 | return self[start ..< endIndex] 21 | } else { 22 | return self[...] 23 | } 24 | } 25 | 26 | } 27 | 28 | extension BidirectionalCollection where Self: RangeReplaceableCollection { 29 | 30 | public var last: Element? { 31 | get { 32 | guard let lastIndex else { return nil } 33 | return self[lastIndex] 34 | } 35 | set { 36 | if let newValue { 37 | if let lastIndex { 38 | self.replaceSubrange(lastIndex ..< endIndex, with: [newValue]) 39 | } else { 40 | self = .init() 41 | self.append(newValue) 42 | } 43 | } else { 44 | // remove the last value 45 | self.removeLast() 46 | } 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Bimap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 5/14/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A Bimap is like a dictionary, except that it's bidirectional: 11 | /// you can look up keys based on values as well. 12 | /// 13 | /// It uses twice as much storage as a regular dictionary 14 | public struct Bimap { 15 | 16 | private var leftToRight = Dictionary() 17 | private var rightToLeft = Dictionary() 18 | 19 | public init() { } 20 | 21 | public subscript(key: Left) -> Right? { 22 | get { 23 | return leftToRight[key] 24 | } 25 | set { 26 | if let newB = newValue { 27 | let oldB = leftToRight.removeValue(forKey: key) 28 | leftToRight[key] = newB 29 | 30 | if let oldB { rightToLeft.removeValue(forKey: oldB) } 31 | rightToLeft[newB] = key 32 | } else { 33 | if let oldB = leftToRight.removeValue(forKey: key) { 34 | rightToLeft.removeValue(forKey: oldB) 35 | } 36 | } 37 | } 38 | } 39 | 40 | public subscript(key: Right) -> Left? { 41 | get { 42 | return rightToLeft[key] 43 | } 44 | set { 45 | if let newA = newValue { 46 | let oldA = rightToLeft.removeValue(forKey: key) 47 | rightToLeft[key] = newA 48 | 49 | if let oldA { leftToRight.removeValue(forKey: oldA) } 50 | leftToRight[newA] = key 51 | } else { 52 | if let oldA = rightToLeft.removeValue(forKey: key) { 53 | leftToRight.removeValue(forKey: oldA) 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | extension Bimap: ExpressibleByDictionaryLiteral { 61 | 62 | public init(dictionaryLiteral elements: (Left, Right)...) { 63 | self.init() 64 | for (l, r) in elements { 65 | self[l] = r 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Bool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/3/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Bool { 11 | 12 | // useful in filtering, such as: 13 | // string.map(\.isNumber.negated) 14 | public var negated: Bool { !self } 15 | 16 | public func toggled() -> Bool { negated } 17 | 18 | public var isTrue: Bool { self == true } 19 | 20 | public var isFalse: Bool { self == false } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Character.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/1/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Character { 11 | 12 | public static var newline: Self { "\n" } 13 | public static var space: Self { " " } 14 | public static var hyphen: Self { "-" } 15 | public static var comma: Self { "," } 16 | public static var backslash: Self { "\\" } 17 | public static var doubleQuote: Self { "\"" } 18 | 19 | public var isASCIIDigit: Bool { isASCII && isWholeNumber } 20 | 21 | public var isAlphanumeric: Bool { isLetter || isNumber } 22 | 23 | public var isWhitespaceOrNewline: Bool { isWhitespace || isNewline } 24 | 25 | public var isOctalDigit: Bool { octalDigitValue != nil } 26 | 27 | public var octalDigitValue: Int? { 28 | guard let hexDigitValue else { return nil } 29 | guard hexDigitValue >= 0 && hexDigitValue < 8 else { return nil } 30 | return hexDigitValue 31 | } 32 | 33 | public var isSuperscript: Bool { 34 | switch self { 35 | case "\u{00B2}": return true 36 | case "\u{00B3}": return true 37 | case "\u{00B9}": return true 38 | case "\u{0670}": return true 39 | case "\u{0711}": return true 40 | case "\u{2070}"..."\u{207F}": return true 41 | default: return false 42 | } 43 | } 44 | 45 | public var isSubscript: Bool { 46 | switch self { 47 | case "\u{0656}": return true 48 | case "\u{1D62}"..."\u{1D6A}": return true 49 | case "\u{2080}"..."\u{209C}": return true 50 | case "\u{2C7C}": return true 51 | default: return false 52 | } 53 | } 54 | 55 | public init?(ascii: Int) { 56 | guard let scalar = Unicode.Scalar(ascii) else { return nil } 57 | self.init(scalar) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Clocks/Clock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 9/17/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Clock { 11 | 12 | public func mutableClock() -> MutableClock { 13 | return MutableClock(clock: self) 14 | } 15 | 16 | public func manualClock() -> ManualClock { 17 | return ManualClock(base: self) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Clocks/Date+InstantProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 9/17/23. 6 | // 7 | 8 | import Foundation 9 | 10 | private let ASEC_PER_SEC = 1e18 11 | 12 | extension Date: InstantProtocol { 13 | public typealias Duration = Swift.Duration 14 | 15 | public func advanced(by duration: Duration) -> Date { 16 | var distance = TimeInterval(duration.components.seconds) 17 | distance += TimeInterval(duration.components.attoseconds) / ASEC_PER_SEC 18 | return self.addingTimeInterval(distance) 19 | } 20 | 21 | public func duration(to other: Date) -> Duration { 22 | let distance = other.timeIntervalSince1970 - self.timeIntervalSince1970 23 | let wholeSeconds = distance.rounded(.towardZero) 24 | let subSeconds = distance - wholeSeconds 25 | 26 | return Duration(secondsComponent: Int64(wholeSeconds), 27 | attosecondsComponent: Int64(subSeconds * ASEC_PER_SEC)) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Clocks/ManualClock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 9/17/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public final class ManualClock: Clock, @unchecked Sendable { 11 | public typealias Instant = C.Instant 12 | public typealias Duration = C.Duration 13 | 14 | private let inner: C 15 | private var _now: Instant 16 | private var pendingSleeps = Dictionary]>() 17 | 18 | public init(base: C) { 19 | self.inner = base 20 | self._now = base.now 21 | } 22 | 23 | public var now: Instant { 24 | get { 25 | return _now 26 | } 27 | set { 28 | _now = newValue 29 | let pastDeadlines = pendingSleeps.keys.sorted(by: <).filter { $0 < newValue } 30 | let continuations = pastDeadlines.flatMap { pendingSleeps[$0] ?? [] } 31 | for deadline in pastDeadlines { 32 | pendingSleeps.removeValue(forKey: deadline) 33 | } 34 | 35 | for continuation in continuations { 36 | continuation.resume() 37 | } 38 | } 39 | } 40 | 41 | public var minimumResolution: C.Duration { inner.minimumResolution } 42 | 43 | public func sleep(until deadline: C.Instant, tolerance: C.Instant.Duration?) async throws { 44 | await withCheckedContinuation { continuation in 45 | self.pendingSleeps[deadline, default: []].append(continuation) 46 | } 47 | } 48 | 49 | public func advance(by duration: Duration) { 50 | self.now = _now.advanced(by: duration) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Clocks/MutableClock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 9/17/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public final class MutableClock: Clock, @unchecked Sendable { 11 | public typealias Instant = C.Instant 12 | public typealias Duration = C.Duration 13 | 14 | private let inner: C 15 | 16 | // mutating this is technically not safe 17 | // however... changing "now" should be very rare 18 | private var offsetFromInner: Duration 19 | 20 | public init(clock: C) { 21 | self.inner = clock 22 | self.offsetFromInner = .zero 23 | } 24 | 25 | public var now: Instant { 26 | get { 27 | let innerNow = inner.now 28 | return innerNow.advanced(by: offsetFromInner) 29 | } 30 | set { 31 | let innerNow = inner.now 32 | self.offsetFromInner = innerNow.duration(to: newValue) 33 | } 34 | } 35 | 36 | public var minimumResolution: C.Duration { 37 | inner.minimumResolution 38 | } 39 | 40 | public func reset() { 41 | self.offsetFromInner = .zero 42 | } 43 | 44 | public func sleep(until deadline: Instant, tolerance: Duration?) async throws { 45 | let currentNow = self.now 46 | let timeToWait = currentNow.duration(to: deadline) 47 | let innerNow = inner.now 48 | let targetDeadline = innerNow.advanced(by: timeToWait) 49 | try await inner.sleep(until: targetDeadline, tolerance: tolerance) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Clocks/UserClock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 9/17/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Clock where Self == UserClock { 11 | 12 | public static var user: UserClock { UserClock() } 13 | 14 | public static var wall: UserClock { UserClock() } 15 | 16 | } 17 | 18 | public struct UserClock: Clock { 19 | public typealias Instant = Date 20 | public typealias Duration = Instant.Duration 21 | 22 | public init() { 23 | 24 | } 25 | 26 | public var now: Date { 27 | return Date() 28 | } 29 | 30 | public var minimumResolution: Instant.Duration { 31 | return Duration(secondsComponent: 0, attosecondsComponent: Int64(NSEC_PER_SEC)) 32 | } 33 | 34 | public func sleep(until deadline: Date, tolerance: Duration?) async throws { 35 | let time = deadline.timeIntervalSince1970 - now.timeIntervalSince1970 36 | if time <= 0 { return } 37 | let nSec = time * Double(NSEC_PER_SEC) 38 | try await Task.sleep(nanoseconds: UInt64(nSec)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Collection+Flatten.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 5/14/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Collection where Element: Collection { 11 | 12 | public func flattened() -> Array { 13 | return self.flatMap { $0 } 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Collection+Trimming.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/1/23. 6 | // 7 | 8 | import Foundation 9 | import Algorithms 10 | 11 | /* 12 | TODO: removingPrefix, removingSuffix, hasPrefix, hasSuffix 13 | TODO: removePrefix, removeSuffix where Self == SubSequence ? 14 | */ 15 | 16 | extension Collection { 17 | 18 | public func trimmingPrefix(where matches: (Element) -> Bool) -> SubSequence { 19 | return self.trimmingPrefix(while: matches) 20 | } 21 | 22 | } 23 | 24 | extension BidirectionalCollection { 25 | 26 | public func trimmingSuffix(where matches: (Element) -> Bool) -> SubSequence { 27 | return self.trimmingSuffix(while: matches) 28 | } 29 | 30 | public func trimming(where matches: (Element) -> Bool) -> SubSequence { 31 | return self.trimming(while: matches) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Collection+Unique.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/1/23. 6 | // 7 | 8 | import Foundation 9 | import Algorithms 10 | 11 | extension Collection { 12 | 13 | public func uniqued(by value: (Element) -> V) -> Array { 14 | return self.uniqued(on: value) 15 | } 16 | 17 | } 18 | 19 | extension Collection where Element: Hashable { 20 | 21 | public func uniqued() -> Array { 22 | return self.uniqued(by: { $0 }) 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Comparable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/1/23. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | extension Comparable { 12 | 13 | public func compare(_ other: Self) -> ComparisonResult { 14 | if self < other { return .orderedAscending } 15 | if self == other { return .orderedSame } 16 | return .orderedDescending 17 | } 18 | 19 | } 20 | 21 | extension Collection { 22 | 23 | public func sorted(by value: (Element) -> V) -> Array { 24 | return sorted(by: { 25 | value($0) < value($1) 26 | }) 27 | } 28 | 29 | public func max(of property: (Element) -> C) -> C? { 30 | return self.lazy.map(property).max() 31 | } 32 | 33 | public func min(of property: (Element) -> C) -> C? { 34 | return self.lazy.map(property).min() 35 | } 36 | 37 | public func max(by property: (Element) -> C) -> Element? { 38 | return self.max(by: { (l, r) -> Bool in 39 | let lValue = property(l) 40 | let rValue = property(r) 41 | return lValue < rValue 42 | }) 43 | } 44 | 45 | public func min(by property: (Element) -> C) -> Element? { 46 | return self.min(by: { (l, r) -> Bool in 47 | let lValue = property(l) 48 | let rValue = property(r) 49 | return lValue < rValue 50 | }) 51 | } 52 | 53 | public func range(of value: (Element) -> C) -> ClosedRange? { 54 | guard isNotEmpty else { return nil } 55 | 56 | let firstValue = value(self[startIndex]) 57 | var range = firstValue ... firstValue 58 | 59 | for index in self.indices.dropFirst() { 60 | let itemValue = value(self[index]) 61 | if itemValue < range.lowerBound { range = itemValue ... range.upperBound } 62 | if itemValue > range.upperBound { range = range.lowerBound ... itemValue } 63 | } 64 | return range 65 | } 66 | 67 | } 68 | 69 | extension Collection where Element: Comparable { 70 | 71 | public var max: Element? { 72 | return self.max(by: { $0 }) 73 | } 74 | 75 | public var min: Element? { 76 | return self.min(by: { $0 }) 77 | } 78 | 79 | public var range: ClosedRange? { 80 | return self.range(of: { $0 }) 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 11/17/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Data { 11 | 12 | public init?(hexString: String) { 13 | guard hexString.allSatisfy(\.isHexDigit) else { return nil } 14 | guard hexString.count.isMultiple(of: 2) else { return nil } 15 | 16 | let bytes = hexString.chunks(ofCount: 2).compactMap { UInt8($0, radix: 16) } 17 | guard bytes.count == hexString.count / 2 else { return nil } 18 | 19 | self.init(bytes) 20 | } 21 | 22 | public var hexDescription: String { 23 | return self.map { String(format: "%02X", $0) }.joined() 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Date.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 11/27/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Date { 11 | 12 | public static func - (lhs: Self, rhs: Self) -> TimeInterval { 13 | return lhs.timeIntervalSinceReferenceDate - rhs.timeIntervalSinceReferenceDate 14 | } 15 | 16 | public static func - (lhs: Self, rhs: TimeInterval) -> Self { 17 | return lhs.addingTimeInterval(-rhs) 18 | } 19 | 20 | public static func + (lhs: Self, rhs: TimeInterval) -> Self { 21 | return lhs.addingTimeInterval(rhs) 22 | } 23 | 24 | public static func -= (lhs: inout Self, rhs: TimeInterval) { 25 | lhs = lhs.addingTimeInterval(-rhs) 26 | } 27 | 28 | public static func += (lhs: inout Self, rhs: TimeInterval) { 29 | lhs = lhs.addingTimeInterval(rhs) 30 | } 31 | 32 | public static func time(_ work: () throws -> Void) rethrows -> TimeInterval { 33 | let start = Date() 34 | try work() 35 | let end = Date() 36 | return end.timeIntervalSince(start) 37 | } 38 | 39 | public static func time(_ work: () async throws -> Void) async rethrows -> TimeInterval { 40 | let start = Date() 41 | try await work() 42 | let end = Date() 43 | return end.timeIntervalSince(start) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Dictionary.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/1/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Dictionary { 11 | 12 | public func mapKeys(_ mapper: (Key) -> NewKey) -> Dictionary { 13 | var f = Dictionary() 14 | for (key, value) in self { 15 | f[mapper(key)] = value 16 | } 17 | return f 18 | } 19 | 20 | public func compactMapKeys(_ mapper: (Key) -> NewKey?) -> Dictionary { 21 | var f = Dictionary() 22 | for (key, value) in self { 23 | if let newKey = mapper(key) { 24 | f[newKey] = value 25 | } 26 | } 27 | return f 28 | } 29 | 30 | public func flatMapKeys(_ mapper: (Key) -> Keys) -> Dictionary where Keys.Element: Hashable { 31 | var f = Dictionary() 32 | for (key, value) in self { 33 | let newKeys = mapper(key) 34 | for newKey in newKeys { 35 | f[newKey] = value 36 | } 37 | } 38 | return f 39 | } 40 | 41 | @discardableResult 42 | public mutating func removeValues(forKeys keysToRemove: C) -> Dictionary where C.Element == Key { 43 | var final = Dictionary() 44 | for key in keysToRemove { 45 | if let value = removeValue(forKey: key) { 46 | final[key] = value 47 | } 48 | } 49 | return final 50 | } 51 | 52 | public mutating func removeKeys(where predicate: (Key) -> Bool) { 53 | for key in keys { 54 | if predicate(key) == true { 55 | removeValue(forKey: key) 56 | } 57 | } 58 | } 59 | 60 | public mutating func removeValues(where predicate: (Value) -> Bool) { 61 | for (key, value) in self { 62 | if predicate(value) == true { 63 | removeValue(forKey: key) 64 | } 65 | } 66 | } 67 | 68 | public subscript(key: Key, inserting value: @autoclosure () -> Value) -> Value { 69 | mutating get { 70 | if let e = self[key] { return e } 71 | let newValue = value() 72 | self[key] = newValue 73 | return newValue 74 | } 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Duration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 12/2/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Duration { 11 | 12 | public static func measure(_ work: () throws -> Void) rethrows -> Self { 13 | return try ContinuousClock().measure(work) 14 | } 15 | 16 | public static func measure(_ work: () async throws -> Void) async rethrows -> Self { 17 | return try await ContinuousClock().measure(work) 18 | } 19 | 20 | public var formattedDescription: String { 21 | var time = Double(components.seconds) + (Double(components.attoseconds) / 1.0e18) 22 | let unit: String 23 | if time > 1.0 { 24 | unit = "s" 25 | } else if time > 0.001 { 26 | unit = "ms" 27 | time *= 1_000 28 | } else if time > 0.000_001 { 29 | unit = "µs" 30 | time *= 1_000_000 31 | } else { 32 | unit = "ns" 33 | time *= 1_000_000_000 34 | } 35 | return timeFormatter.string(from: NSNumber(value: time))! + unit 36 | } 37 | 38 | } 39 | 40 | private let timeFormatter: NumberFormatter = { 41 | let f = NumberFormatter() 42 | f.maximumFractionDigits = 3 43 | return f 44 | }() 45 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 8/18/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct AnyError: Error, CustomStringConvertible { 11 | 12 | public let function: StaticString 13 | public let fileID: StaticString 14 | public let line: UInt 15 | public let description: String 16 | public let underlyingError: Error? 17 | 18 | public init(_ description: String, function: StaticString = #function, fileID: StaticString = #fileID, line: UInt = #line) { 19 | self.function = function 20 | self.fileID = fileID 21 | self.line = line 22 | self.description = description 23 | self.underlyingError = nil 24 | } 25 | 26 | public init(_ other: Error, function: StaticString = #function, fileID: StaticString = #fileID, line: UInt = #line) { 27 | if let any = other as? AnyError { 28 | self = any 29 | } else { 30 | self.function = function 31 | self.fileID = fileID 32 | self.line = line 33 | self.description = other.localizedDescription 34 | self.underlyingError = other 35 | } 36 | } 37 | 38 | } 39 | 40 | public struct UnimplementedError: Error, CustomStringConvertible { 41 | 42 | public let function: StaticString 43 | public let fileID: StaticString 44 | public let line: UInt 45 | 46 | public init(function: StaticString = #function, file: StaticString = #fileID, line: UInt = #line) { 47 | self.function = function 48 | self.fileID = file 49 | self.line = line 50 | } 51 | 52 | public var description: String { 53 | "The function \(function) in \(fileID):\(line) is unimplemented. This is a developer error." 54 | } 55 | 56 | } 57 | 58 | public struct Unreachable: Error, CustomStringConvertible { 59 | 60 | public let function: StaticString 61 | public let fileID: StaticString 62 | public let line: UInt 63 | 64 | public init(function: StaticString = #function, file: StaticString = #fileID, line: UInt = #line) { 65 | self.function = function 66 | self.fileID = file 67 | self.line = line 68 | } 69 | 70 | public var description: String { 71 | "The function \(function) in \(fileID):\(line) should be unreachable. This is a developer error." 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Fatal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/1/23. 6 | // 7 | 8 | import Foundation 9 | 10 | infix operator ?!: NilCoalescingPrecedence 11 | 12 | public func ?! (lhs: T?, rhs: @autoclosure () -> Error) throws -> T { 13 | 14 | if let value = lhs { 15 | return value 16 | } 17 | 18 | throw rhs() 19 | } 20 | 21 | infix operator !!: NilCoalescingPrecedence 22 | 23 | public func !! (lhs: T?, rhs: @autoclosure () -> String) -> T { 24 | 25 | if let value = lhs { 26 | return value 27 | } 28 | 29 | fatalError("Error unwrapping value of type \(T.self): \(rhs())") 30 | } 31 | 32 | // public typealias None = Never 33 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/ID.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/4/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct ID: Newtype, RawRepresentable { 11 | public let rawValue: RawValue 12 | 13 | public init(rawValue: RawValue) { 14 | self.rawValue = rawValue 15 | } 16 | } 17 | 18 | extension ID: Equatable where RawValue: Equatable { } 19 | extension ID: Hashable where RawValue: Hashable { } 20 | extension ID: Identifiable where RawValue: Identifiable { } 21 | extension ID: Decodable where RawValue: Decodable { } 22 | extension ID: Encodable where RawValue: Encodable { } 23 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Int.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/3/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension FixedWidthInteger { 11 | 12 | public static func leastCommonMultiple(_ values: Self...) -> Self { 13 | return self.leastCommonMultiple(of: values) 14 | } 15 | 16 | public static func leastCommonMultiple(of values: some Collection) -> Self { 17 | if values.isEmpty { return .zero } 18 | let v = values.first! 19 | let r = values.dropFirst() 20 | if r.isEmpty { return v } 21 | 22 | let lcmR = leastCommonMultiple(of: r) 23 | return v / greatestCommonDivisor(of: v, and: lcmR) * lcmR 24 | } 25 | 26 | public static func greatestCommonDivisor(of m: Self, and n: Self) -> Self { 27 | var a = Self.zero 28 | var b = Swift.max(m, n) 29 | var r = Swift.min(m, n) 30 | while r != 0 { 31 | a = b 32 | b = r 33 | r = a % b 34 | } 35 | return b 36 | } 37 | 38 | public func swapping(_ shouldSwap: Bool) -> Self { 39 | guard shouldSwap else { return self } 40 | return self.byteSwapped 41 | } 42 | 43 | @available(*, deprecated, renamed: "leastCommonMultiple") 44 | public static func lcm(_ values: Self...) -> Self { 45 | return self.leastCommonMultiple(of: values) 46 | } 47 | 48 | @available(*, deprecated, renamed: "leastCommonMultiple") 49 | public static func lcm(of values: some Collection) -> Self { 50 | return self.leastCommonMultiple(of: values) 51 | } 52 | 53 | @available(*, deprecated, renamed: "greatestCommonDivisor") 54 | public static func gcd(of m: Self, and n: Self) -> Self { 55 | return self.greatestCommonDivisor(of: m, and: n) 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/KeyPath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/3/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension KeyPath { 11 | 12 | public static prefix func !(rhs: KeyPath) -> KeyPath { 13 | rhs.appending(path: \.negated) 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/LazyTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 9/2/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public actor LazyTask where Success: Sendable, Failure: Error { 11 | 12 | private enum State { 13 | case waiting 14 | case task(Task) 15 | case cancelled 16 | } 17 | 18 | private let generator: () -> Task 19 | private var state: State 20 | 21 | public var value: Success { 22 | get async throws { 23 | if case .waiting = state { 24 | let task = generator() 25 | self.state = .task(task) 26 | return try await task.value 27 | } else if case .task(let task) = state { 28 | return try await task.value 29 | } else { 30 | throw CancellationError() 31 | } 32 | } 33 | } 34 | 35 | public init(priority: TaskPriority? = nil, _ promise: @escaping @Sendable () async -> Success) where Failure == Never { 36 | self.state = .waiting 37 | self.generator = { 38 | return Task(priority: priority, operation: promise) 39 | } 40 | } 41 | 42 | public func cancel() { 43 | if case .task(let t) = state { 44 | self.state = .cancelled 45 | t.cancel() 46 | } else { 47 | self.state = .cancelled 48 | } 49 | } 50 | 51 | public func reset() { 52 | self.state = .waiting 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Never.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 12/21/23. 6 | // 7 | 8 | import Foundation 9 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Normalized.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 9/13/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Normalized { 11 | 12 | public let rawValue: Double 13 | 14 | public init(rawValue: Double) { 15 | self.rawValue = rawValue.clamped(to: 0 ... 1) 16 | } 17 | 18 | public init(_ value: FP, in range: ClosedRange) { 19 | let span = Double(range.upperBound - range.lowerBound) 20 | let val = Double(value - range.lowerBound) 21 | self.rawValue = val / span 22 | } 23 | 24 | public init(_ value: I, in range: ClosedRange) { 25 | let span = Double(range.upperBound - range.lowerBound) 26 | let val = Double(value - range.lowerBound) 27 | self.rawValue = val / span 28 | } 29 | 30 | public init(_ value: I, in range: Range) { 31 | let closedRange = range.lowerBound ... (range.upperBound - 1) 32 | self.init(value, in: closedRange) 33 | } 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Once.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 5/10/23. 6 | // 7 | 8 | import Foundation 9 | 10 | @propertyWrapper 11 | public class ThrowingOnce { 12 | private var hasInvoked: Bool = false 13 | private let file: StaticString 14 | private let line: UInt 15 | private let closure: (In) throws -> Out 16 | 17 | public var wrappedValue: (In) throws -> Out { 18 | return { [self] input in 19 | if self.hasInvoked { 20 | fatalError("Closure (\(In.self)) -> \(Out.self) captured at \(file):\(line) was invoked more than once") 21 | } 22 | self.hasInvoked = true 23 | return try self.closure(input) 24 | } 25 | } 26 | 27 | fileprivate init(closure: @escaping (In) throws -> Out, file: StaticString, line: UInt) { 28 | self.closure = closure 29 | self.file = file 30 | self.line = line 31 | } 32 | 33 | public convenience init(wrappedValue: @escaping (In) throws -> Out, file: StaticString = #fileID, line: UInt = #line) { 34 | self.init(closure: wrappedValue, file: file, line: line) 35 | } 36 | 37 | @available(*, unavailable, message: "Use @Once for non-throwing closures") 38 | public convenience init(wrappedValue: @escaping (In) -> Out, file: StaticString = #fileID, line: UInt = #line) { 39 | self.init(closure: wrappedValue, file: file, line: line) 40 | } 41 | 42 | deinit { 43 | if hasInvoked == false { 44 | fatalError("Closure (\(In.self)) -> \(Out.self) captured at \(file):\(line) was never invoked") 45 | } 46 | } 47 | } 48 | 49 | @propertyWrapper 50 | public class Once: ThrowingOnce { 51 | 52 | public override var wrappedValue: (In) -> Out { 53 | let superClosure = super.wrappedValue 54 | 55 | return { try! superClosure($0) } 56 | } 57 | 58 | public init(wrappedValue: @escaping (In) -> Out, file: StaticString = #fileID, line: UInt = #line) { 59 | super.init(closure: wrappedValue, file: file, line: line) 60 | } 61 | 62 | @available(*, unavailable, message: "Use @ThrowingOnce for throwing closures") 63 | public init(wrappedValue: @escaping (In) throws -> Out, file: StaticString = #fileID, line: UInt = #line) { 64 | super.init(closure: wrappedValue, file: file, line: line) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Optional.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 7/10/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Optional { 11 | 12 | public var unwrapped: Wrapped { 13 | return self !! "Cannot unwrap nil \(Self.self)" 14 | } 15 | 16 | public func apply(_ closure: (Wrapped) throws -> Void) rethrows { 17 | if let value = self { 18 | try closure(value) 19 | } 20 | } 21 | } 22 | 23 | extension Optional: Sequence where Wrapped: Sequence { 24 | public typealias Element = Wrapped.Element 25 | public typealias Iterator = OptionalIterator 26 | 27 | public func makeIterator() -> OptionalIterator { 28 | return OptionalIterator(inner: self?.makeIterator()) 29 | } 30 | 31 | } 32 | 33 | public struct OptionalIterator: IteratorProtocol { 34 | var inner: Wrapped.Iterator? 35 | 36 | public mutating func next() -> Wrapped.Element? { 37 | return inner?.next() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Range.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/3/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Range where Bound: Strideable { 11 | 12 | public init(start: Bound, length: Bound.Stride) { 13 | let end = start.advanced(by: length) 14 | if start <= end { 15 | self = start ..< end 16 | } else { 17 | self = end ..< start 18 | } 19 | } 20 | 21 | } 22 | 23 | extension ClosedRange where Bound: Strideable { 24 | 25 | public init(start: Bound, length: Bound.Stride) { 26 | if length == 0 { fatalError("Cannot create an empty ClosedRange") } 27 | let end = start.advanced(by: length - 1) 28 | if start <= end { 29 | self = start ... end 30 | } else { 31 | self = end ... start 32 | } 33 | } 34 | 35 | } 36 | 37 | extension Range where Bound: Strideable { 38 | 39 | public func clamping(_ value: Bound) -> Bound { 40 | if value < lowerBound { return lowerBound } 41 | if value >= upperBound { return upperBound.advanced(by: -1) } 42 | return value 43 | } 44 | 45 | } 46 | 47 | extension ClosedRange { 48 | 49 | public func clamping(_ value: Bound) -> Bound { 50 | if value < lowerBound { return lowerBound } 51 | if value > upperBound { return upperBound } 52 | return value 53 | } 54 | 55 | } 56 | 57 | extension Comparable { 58 | 59 | public func clamped(to range: ClosedRange) -> Self { 60 | return range.clamping(self) 61 | } 62 | 63 | } 64 | 65 | extension Comparable where Self: Strideable { 66 | 67 | public func clamped(to range: Range) -> Self { 68 | return range.clamping(self) 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Result.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/1/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Result { 11 | 12 | public var success: Success? { 13 | if case .success(let s) = self { return s } 14 | return nil 15 | } 16 | 17 | public var failure: Failure? { 18 | if case .failure(let failure) = self { return failure } 19 | return nil 20 | } 21 | 22 | public var isSuccess: Bool { 23 | if case .success = self { return true } 24 | return false 25 | } 26 | 27 | public var isFailure: Bool { 28 | if case .failure = self { return true } 29 | return false 30 | } 31 | 32 | public func eraseSuccess() -> Result { 33 | switch self { 34 | case .success: return .success(()) 35 | case .failure(let e): return .failure(e) 36 | } 37 | } 38 | 39 | public func eraseToAnyError() -> Result { 40 | switch self { 41 | case .success(let s): return .success(s) 42 | case .failure(let e): return .failure(e) 43 | } 44 | } 45 | 46 | } 47 | 48 | extension Result where Success == Void { 49 | 50 | public static var success: Self { return Result.success(()) } 51 | 52 | } 53 | 54 | extension Result where Failure == Error { 55 | 56 | public init(attempting: () async throws -> Success) async { 57 | do { 58 | let value = try await attempting() 59 | self = .success(value) 60 | } catch { 61 | self = .failure(error) 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Sendable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 10/13/23. 6 | // 7 | 8 | import Foundation 9 | 10 | // source: https://mastodon.world/@bjhomer/111227953983044879 11 | 12 | 13 | /// A `Sendable` wrapper for things that are not sendable, but can be accessed in Sendable ways 14 | /// 15 | /// For example, `Notification` is not sendable because its `userInfo` cannot be Sendable. 16 | /// However, if you only care about the notification *name*, then the `Notification` can be 17 | /// partially Sendable and can be safely wrapped in this struct. 18 | @dynamicMemberLookup 19 | public struct UncheckedSendable: @unchecked Sendable { 20 | 21 | public let value: Wrapped 22 | 23 | public init(_ value: Wrapped) { 24 | self.value = value 25 | } 26 | 27 | public subscript(dynamicMember keyPath: KeyPath) -> V { 28 | return value[keyPath: keyPath] 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Sequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 10/3/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Sequence { 11 | 12 | public func firstMap(_ mapper: (Element) -> Output?) -> Output? { 13 | for item in self { 14 | if let m = mapper(item) { return m } 15 | } 16 | return nil 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Shim.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 5/13/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Shim { 11 | 12 | public let value: Value 13 | 14 | public init(_ value: Value) { 15 | self.value = value 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/String+Interpolation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 9/30/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String.StringInterpolation { 11 | 12 | public mutating func appendInterpolation(describing value: Value?) { 13 | self.appendInterpolation(String(describing: value)) 14 | } 15 | 16 | public mutating func appendInterpolation(hex value: Value) { 17 | self.appendInterpolation("0x" + String(value, radix: 16, uppercase: true)) 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/UUID.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 11/29/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension UUID { 11 | 12 | public static var timestampedUUID: Self { NSUUID.extended_timed() as UUID } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Unsafe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 9/30/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension UnsafePointer { 11 | 12 | public func rebound(to type: T.Type) -> UnsafePointer { 13 | return self.withMemoryRebound(to: type, capacity: 1, { return $0 }) 14 | } 15 | 16 | } 17 | 18 | extension UnsafeBufferPointer { 19 | 20 | public init?(pointer: UnsafeRawPointer, count: Int) where Element == UInt8 { 21 | let ptr = pointer.assumingMemoryBound(to: UInt8.self) 22 | self.init(start: ptr, count: count) 23 | } 24 | 25 | } 26 | 27 | extension Data { 28 | 29 | public init(pointer: UnsafeRawPointer, count: Int) { 30 | let bytes = pointer.assumingMemoryBound(to: UInt8.self) 31 | let buffer = UnsafeBufferPointer(start: bytes, count: count) 32 | self.init(buffer: buffer) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Updateable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 7/9/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol Updateable: AnyObject { } 11 | 12 | extension NSObject: Updateable { } 13 | 14 | extension Updateable /*where Self: NSObject*/ { 15 | public func update(_ keyPath: ReferenceWritableKeyPath, to value: V) { 16 | updateObj(self, keyPath: keyPath, value: value) 17 | } 18 | 19 | public func update(_ keyPath: ReferenceWritableKeyPath, to value: V) { 20 | updateObj(self, keyPath: keyPath, value: value) 21 | } 22 | } 23 | 24 | private func updateObj(_ obj: T, keyPath: ReferenceWritableKeyPath, value: V) { 25 | if let existing = obj[keyPath: keyPath], existing == value { return } 26 | obj[keyPath: keyPath] = value 27 | } 28 | 29 | private func updateObj(_ obj: T, keyPath: ReferenceWritableKeyPath, value: V) { 30 | if obj[keyPath: keyPath] != value { 31 | obj[keyPath: keyPath] = value 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Standard/Warn.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 10/9/23. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | import Logging 11 | 12 | // based on https://www.pointfree.co/blog/posts/70-unobtrusive-runtime-warnings-for-libraries 13 | 14 | extension Logging.Logger { 15 | 16 | @inline(__always) 17 | @_transparent 18 | public static func runtimeWarning(_ message: StaticString, _ category: String = "Warn", handle: UnsafeRawPointer = #dsohandle) { 19 | os_log(.fault, 20 | dso: Dyld.swiftUIPointer ?? handle, 21 | log: OSLog( 22 | subsystem: "com.apple.runtime-issues", 23 | category: category 24 | ), 25 | message 26 | ) 27 | } 28 | 29 | } 30 | 31 | extension Dyld { 32 | 33 | public static var swiftUIPointer: UnsafeRawPointer? { _swiftUIInfo } 34 | 35 | } 36 | 37 | public let _swiftUIInfo: UnsafeRawPointer? = { 38 | // see if we can find the header manually 39 | if let images = Dyld.images.first(where: { $0.name.hasSuffix("SwiftUI") }) { 40 | return images.header.pointer 41 | } 42 | 43 | var info = Dl_info() 44 | let imagePointers = dlopen(nil, RTLD_LAZY) 45 | guard let symbol = dlsym(imagePointers, "$s7SwiftUI4ViewMp") else { 46 | return nil 47 | } 48 | 49 | let status = dladdr(symbol, &info) 50 | if status == 0 { return nil } 51 | guard let base = info.dli_fbase else { return nil } 52 | return UnsafeRawPointer(base) 53 | }() 54 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Trees/BreadthFirstTraversal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/2/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct BreadthFirstTreeTraversal: TreeTraversal { 11 | 12 | public enum Action: TreeTraversalAction { 13 | public static var keepGoing: Action { return .continue } 14 | 15 | case halt 16 | case `continue` 17 | case skipChildren 18 | 19 | public var halts: Bool { return self == .halt } 20 | } 21 | 22 | @discardableResult 23 | public func traverse(tree: any Tree, visitor: (any Tree, Context) throws -> Action) rethrows -> Action { 24 | return try self.traverse(tree: tree, context: .init(), visitor: visitor) 25 | } 26 | 27 | private func traverse(tree: any Tree, context: Context, visitor: (any Tree, Context) throws -> Action) rethrows -> Action { 28 | var nodesToVisit = ArraySlice([(tree, context)]) 29 | 30 | while let (nextNode, nodeContext) = nodesToVisit.popFirst() { 31 | let nodeDisposition = try visitor(nextNode, nodeContext) 32 | switch nodeDisposition { 33 | case .halt: return .halt 34 | case .continue: nodesToVisit.append(contentsOf: nextNode.children.map { ($0, nodeContext.increment()) }) 35 | case .skipChildren: continue 36 | } 37 | } 38 | 39 | return .continue 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Trees/InOrderTraversal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/2/23. 6 | // 7 | 8 | import Foundation 9 | import Logging 10 | 11 | // In-order traversal only works on binary tree nodes, because general tree nodes don't have a notion of "left" or "right" children 12 | public struct InOrderTraversal: TreeTraversal { 13 | 14 | public enum Action: TreeTraversalAction { 15 | public static var keepGoing: Action { return .continue } 16 | 17 | case halt 18 | case `continue` 19 | case skipChildren 20 | 21 | public var halts: Bool { return self == .halt } 22 | } 23 | 24 | @discardableResult 25 | public func traverse(tree: any Tree, visitor: (any Tree, Context) throws -> Action) rethrows -> Action { 26 | return try traverse(tree: tree, context: .init(), visitor: visitor) 27 | } 28 | 29 | private func traverse(tree: any Tree, context: Context, visitor: (any Tree, Context) throws -> Action) rethrows -> Action { 30 | 31 | guard let btree = tree as? any BinaryTree else { 32 | Logger.runtimeWarning("InOrderTraversal can only be used with BinaryTrees") 33 | return .halt 34 | } 35 | 36 | if let l = btree.left { 37 | let d = try traverse(tree: l, context: context.increment(), visitor: visitor) 38 | if d.halts { return d } 39 | } 40 | 41 | let d = try visitor(tree, context) 42 | if d.halts { return d } 43 | 44 | if let r = btree.right, d != .skipChildren { 45 | let d = try traverse(tree: r, context: context.increment(), visitor: visitor) 46 | if d.halts { return d } 47 | } 48 | 49 | return .continue 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Trees/PostOrderTraversal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/2/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct PostOrderTraversal: TreeTraversal { 11 | 12 | public enum Action: TreeTraversalAction { 13 | public static var keepGoing: Action { return .continue } 14 | 15 | case halt 16 | case `continue` 17 | 18 | public var halts: Bool { return self == .halt } 19 | } 20 | 21 | public init() { } 22 | 23 | @discardableResult 24 | public func traverse(tree: any Tree, visitor: (any Tree, Context) throws -> Action) rethrows -> Action { 25 | return try traverse(tree: tree, context: .init(), visitor: visitor) 26 | } 27 | 28 | private func traverse(tree: any Tree, context: Context, visitor: (any Tree, Context) throws -> Action) rethrows -> Action { 29 | for child in tree.children { 30 | let nodeD = try traverse(tree: child, context: context.increment(), visitor: visitor) 31 | if nodeD.halts { return nodeD } 32 | } 33 | 34 | return try visitor(tree, context) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Trees/PreAndPostOrderTraversal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 10/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct PreAndPostOrderTraversal: TreeTraversal { 11 | 12 | public enum Action: TreeTraversalAction { 13 | public static var keepGoing: Action { return .continue } 14 | 15 | case halt 16 | case `continue` 17 | case skipChildren 18 | 19 | public var halts: Bool { return self == .halt } 20 | } 21 | 22 | public enum State { 23 | case leaf 24 | case preOrder 25 | case postOrder 26 | } 27 | 28 | public init() { } 29 | 30 | @discardableResult 31 | public func traverse(tree: any Tree, visitor: (any Tree, Context) throws -> Action) rethrows -> Action { 32 | return try traverse(tree: tree, 33 | context: .init(level: 0, state: .preOrder), 34 | visitor: visitor) 35 | } 36 | 37 | private func traverse(tree: any Tree, context: Context, visitor: (any Tree, Context) throws -> Action) rethrows -> Action { 38 | 39 | var c = context 40 | 41 | let isLeaf = tree.isLeaf 42 | 43 | c.state = isLeaf ? .leaf : .preOrder 44 | 45 | let finalDisposition = try visitor(tree, c) 46 | 47 | if finalDisposition.halts == false && finalDisposition != .skipChildren { 48 | for child in tree.children { 49 | let nodeD = try traverse(tree: child, context: c.increment(), visitor: visitor) 50 | if nodeD.halts { 51 | break 52 | } 53 | } 54 | } 55 | 56 | if isLeaf == false { 57 | c.state = .postOrder 58 | _ = try visitor(tree, c) 59 | } 60 | 61 | return finalDisposition 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Trees/PreOrderTraversal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/2/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct PreOrderTraversal: TreeTraversal { 11 | 12 | public enum Action: TreeTraversalAction { 13 | public static var keepGoing: Action { return .continue } 14 | 15 | case halt 16 | case `continue` 17 | case skipChildren 18 | 19 | public var halts: Bool { return self == .halt } 20 | } 21 | 22 | public init() { } 23 | 24 | @discardableResult 25 | public func traverse(tree: any Tree, visitor: (any Tree, Self.Context) throws -> Action) rethrows -> Action { 26 | return try traverse(tree: tree, context: .init(), visitor: visitor) 27 | } 28 | 29 | private func traverse(tree: any Tree, context: Self.Context, visitor: (any Tree, Self.Context) throws -> Action) rethrows -> Action { 30 | let d = try visitor(tree, context) 31 | if d.halts { return d } 32 | 33 | if d != .skipChildren { 34 | for child in tree.children { 35 | let nodeD = try traverse(tree: child, context: context.increment(), visitor: visitor) 36 | if nodeD.halts { return nodeD } 37 | } 38 | } 39 | return .continue 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Sources/ExtendedSwift/Trees/Tree.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/2/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol Tree { 11 | associatedtype Value = Self 12 | 13 | var treeValue: Value { get } 14 | var children: Array> { get } 15 | var isLeaf: Bool { get } 16 | } 17 | 18 | extension Tree { 19 | 20 | public var isLeaf: Bool { return children.isEmpty } 21 | 22 | } 23 | 24 | extension Tree where Value == Self { 25 | 26 | public var value: Value { self } 27 | 28 | } 29 | 30 | public protocol BinaryTree: Tree { 31 | 32 | var left: (any BinaryTree)? { get } 33 | var right: (any BinaryTree)? { get } 34 | 35 | } 36 | 37 | extension BinaryTree { 38 | 39 | public var isLeaf: Bool { return left == nil && right == nil } 40 | 41 | public var children: Array> { 42 | if let l = left, let r = right { return [l, r] } 43 | if let l = left { return [l] } 44 | if let r = right { return [r] } 45 | return [] 46 | } 47 | 48 | public var inOrderValues: Array { flattenValues(in: InOrderTraversal()) } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Sources/ExtendedTest/XCTestHelpers.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @discardableResult 4 | public func XCTAssertSuccess(_ result: Result, file: StaticString = #file, line: UInt = #line) -> T? { 5 | 6 | switch result { 7 | case .success(let value): 8 | return value 9 | case .failure(let error): 10 | XCTFail("Unexpected failure: \(error)", file: file, line: line) 11 | return nil 12 | } 13 | 14 | } 15 | 16 | @discardableResult 17 | public func XCTAssertFailure(_ result: Result, file: StaticString = #file, line: UInt = #line) -> E? { 18 | 19 | switch result { 20 | case .success(let value): 21 | XCTFail("Unexpected success: \(value)", file: file, line: line) 22 | return nil 23 | case .failure(let error): 24 | return error 25 | } 26 | 27 | } 28 | 29 | extension XCTestCase { 30 | 31 | @MainActor 32 | public func allExpectations(timeout: TimeInterval = 1.0) async throws { 33 | return try await withCheckedThrowingContinuation { continuation in 34 | self.waitForExpectations(timeout: timeout, handler: { error in 35 | if let error { 36 | continuation.resume(throwing: error) 37 | } else { 38 | continuation.resume() 39 | } 40 | }) 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Sources/HTTP/Bodies/DataBody.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | 6 | import Foundation 7 | 8 | public struct DataBody: HTTPSynchronousBody { 9 | 10 | public let bodyData: Data 11 | 12 | public let headers: HTTPHeaders 13 | 14 | public init(_ data: Data, headers: HTTPHeaders? = nil) { 15 | self.bodyData = data 16 | self.headers = headers ?? .init() 17 | } 18 | 19 | public var stream: AsyncStream { 20 | return AsyncStream(sequence: bodyData) 21 | } 22 | 23 | } 24 | 25 | extension Data: HTTPSynchronousBody { 26 | 27 | public var bodyData: Data { self } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Sources/HTTP/Bodies/FormBody.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | 6 | import Foundation 7 | 8 | public struct FormBody: HTTPSynchronousBody { 9 | 10 | public let headers: HTTPHeaders 11 | 12 | public var bodyData: Data { 13 | Data(values.map { (key, value) -> String in 14 | let k = key.addingPercentEncoding(withAllowedCharacters: formBodyAllowed) ?? "" 15 | let v = value.addingPercentEncoding(withAllowedCharacters: formBodyAllowed) ?? "" 16 | return "\(k)=\(v)" 17 | }.joined(separator: "&").utf8) 18 | } 19 | 20 | public let values: Dictionary 21 | 22 | public init(values: Dictionary) { 23 | self.values = values 24 | self.headers = [ 25 | "Content-Type": "application/x-www-form-urlencoded; charset=utf-8" 26 | ] 27 | } 28 | } 29 | 30 | private let formBodyAllowed = CharacterSet(charactersIn: "&=:/, $%+").inverted 31 | -------------------------------------------------------------------------------- /Sources/HTTP/Bodies/JSONBody.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | 6 | import Foundation 7 | 8 | public struct JSONBody: HTTPSynchronousBody { 9 | 10 | public let encoder: JSONEncoder 11 | public let value: E 12 | 13 | public var bodyData: Data { 14 | get throws { 15 | try encoder.encode(value) 16 | } 17 | } 18 | 19 | public let headers: HTTPHeaders 20 | 21 | public init(value: E, encoder: JSONEncoder? = nil) { 22 | self.encoder = encoder ?? JSONEncoder() 23 | self.value = value 24 | self.headers = [ 25 | "Content-Type": "application/json; charset=utf-8" 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/HTTP/Loader/HTTPEnvironmentLoader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct HTTPRequestEnvironment: Sendable, HTTPOption { 4 | 5 | public static let defaultValue: HTTPRequestEnvironment? = nil 6 | 7 | public var method: HTTPMethod? 8 | 9 | public var host: String? 10 | 11 | public var pathPrefix: String 12 | 13 | public var query: HTTPQuery? 14 | 15 | public var headers: HTTPHeaders? 16 | 17 | public init(method: HTTPMethod? = nil, host: String? = nil, pathPrefix: String = "/", query: HTTPQuery? = nil, headers: HTTPHeaders? = nil) { 18 | 19 | let prefix = pathPrefix.hasPrefix("/") ? "" : "/" 20 | 21 | self.method = method 22 | self.host = host 23 | self.pathPrefix = prefix + pathPrefix 24 | self.query = query 25 | self.headers = headers 26 | } 27 | 28 | fileprivate func apply(to request: inout HTTPRequest) { 29 | if let method { 30 | request.method = method 31 | } 32 | 33 | if let host, request.host == nil { 34 | request.host = host 35 | } 36 | 37 | let requestPath = request.path ?? "" 38 | if requestPath.isEmpty { 39 | request.path = pathPrefix 40 | } else if requestPath.hasPrefix("/") == false { 41 | if pathPrefix.hasSuffix("/") == false { 42 | request.path = pathPrefix + "/" + requestPath 43 | } else { 44 | request.path = pathPrefix + requestPath 45 | } 46 | } 47 | 48 | if let query { 49 | for (name, value) in query { 50 | request.query.addValue(value, for: name) 51 | } 52 | } 53 | 54 | if let headers { 55 | for (header, value) in headers { 56 | request.headers.addValue(value, for: header) 57 | } 58 | } 59 | } 60 | 61 | } 62 | 63 | public actor HTTPEnvironmentLoader: HTTPLoader { 64 | 65 | public let environment: HTTPRequestEnvironment 66 | 67 | public init(environment: HTTPRequestEnvironment) { 68 | self.environment = environment 69 | } 70 | 71 | public func load(request: HTTPRequest, token: HTTPRequestToken) async -> HTTPResult { 72 | return await withNextLoader(for: request) { next in 73 | var copy = request 74 | let environment = request[option: HTTPRequestEnvironment.self] ?? self.environment 75 | environment.apply(to: ©) 76 | return await next.load(request: copy, token: token) 77 | } 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /Sources/HTTP/Loader/HTTPLoader.swift: -------------------------------------------------------------------------------- 1 | public protocol HTTPLoader: Actor { 2 | 3 | func load(request: HTTPRequest, token: HTTPRequestToken) async -> HTTPResult 4 | 5 | } 6 | 7 | extension HTTPLoader { 8 | 9 | public nonisolated var nextLoader: HTTPLoader? { 10 | get { LoaderChain.shared.nextLoader(for: self) } 11 | set { LoaderChain.shared.setNextLoader(newValue, for: self) } 12 | } 13 | 14 | public func withNextLoader(for request: HTTPRequest, perform: (HTTPLoader) async -> HTTPResult) async -> HTTPResult { 15 | guard let next = nextLoader else { 16 | let error = HTTPError(code: .cannotConnect, 17 | request: request, 18 | message: "\(type(of: self)) does not have a nextLoader") 19 | return .failure(error) 20 | } 21 | 22 | return await perform(next) 23 | } 24 | 25 | public func load(request: HTTPRequest) async -> HTTPResult { 26 | let token = HTTPRequestToken() 27 | return await load(request: request, token: token) 28 | } 29 | 30 | } 31 | 32 | precedencegroup HTTPLoaderChainingPrecedence { 33 | higherThan: NilCoalescingPrecedence 34 | associativity: right 35 | } 36 | 37 | infix operator --> : HTTPLoaderChainingPrecedence 38 | 39 | @discardableResult 40 | public func --> (lhs: HTTPLoader?, rhs: HTTPLoader) async -> HTTPLoader { 41 | lhs?.nextLoader = rhs 42 | return lhs ?? rhs 43 | } 44 | 45 | @discardableResult 46 | public func --> (lhs: HTTPLoader?, rhs: HTTPLoader?) async -> HTTPLoader? { 47 | lhs?.nextLoader = rhs 48 | return lhs ?? rhs 49 | } 50 | -------------------------------------------------------------------------------- /Sources/HTTP/Loader/ManualLoader.swift: -------------------------------------------------------------------------------- 1 | public actor ManualLoader: HTTPLoader { 2 | 3 | public typealias ManualHandler = (HTTPRequest, HTTPRequestToken) async -> HTTPResult 4 | 5 | private var next = [ManualHandler]() 6 | private var defaultHandler: ManualHandler? 7 | 8 | public init() { } 9 | 10 | public func setDefaultHandler(_ handler: @escaping ManualHandler) { 11 | self.defaultHandler = handler 12 | } 13 | 14 | @discardableResult 15 | public func then(_ perform: @escaping ManualHandler) -> Self { 16 | next.append(perform) 17 | return self 18 | } 19 | 20 | public func load(request: HTTPRequest, token: HTTPRequestToken) async -> HTTPResult { 21 | if next.isEmpty == false { 22 | let handler = next.removeFirst() 23 | return await handler(request, token) 24 | } 25 | 26 | if let defaultHandler { 27 | return await defaultHandler(request, token) 28 | } 29 | 30 | return await withNextLoader(for: request) { next in 31 | return await next.load(request: request, token: token) 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Sources/HTTP/Loader/RetryLoader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public actor RetryLoader: HTTPLoader { 4 | 5 | public init() { } 6 | 7 | public func load(request: HTTPRequest, token: HTTPRequestToken) async -> HTTPResult { 8 | return await withNextLoader(for: request) { next in 9 | 10 | var strategy: any HTTPRetryStrategy = request[option: \.retryStrategy] ?? NoRetry() 11 | 12 | var latestResult: HTTPResult? 13 | var attemptCount = 0 14 | 15 | while true { 16 | if token.isCancelled { 17 | break 18 | } 19 | 20 | let attemptResult = await next.load(request: request, token: token) 21 | latestResult = attemptResult 22 | 23 | if attemptResult.failure?.code == .cancelled { 24 | break 25 | } else if let delay = strategy.nextDelay(after: attemptResult) { 26 | attemptCount += 1 27 | // this will loop around and attempt the request again 28 | // as long as the request hasn't been cancelled 29 | if delay > 0 { 30 | do { 31 | try await Task.sleep(for: Duration(delay)) 32 | } catch { 33 | let error = HTTPError(code: .internal, 34 | request: request, 35 | response: attemptResult.response, 36 | message: "Async task was cancelled", 37 | underlyingError: error) 38 | latestResult = .failure(error) 39 | break 40 | } 41 | } 42 | } else { 43 | // no retry delay; 44 | break 45 | } 46 | } 47 | 48 | var result = latestResult ?? .failure(HTTPError(code: .internal, request: request)) 49 | result = result.modifyResponse { 50 | $0[header: .xRetryCount] = "\(attemptCount)" 51 | } 52 | return result 53 | } 54 | } 55 | 56 | } 57 | 58 | extension HTTPHeader { 59 | 60 | public static let xRetryCount = HTTPHeader(rawValue: "X-HTTP-Retry-Count") 61 | 62 | } 63 | 64 | private struct NoRetry: HTTPRetryStrategy { 65 | mutating func nextDelay(after result: HTTPResult) -> TimeInterval? { return nil } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/HTTP/Loader/ThrottledLoader.swift: -------------------------------------------------------------------------------- 1 | public actor ThrottledLoader: HTTPLoader { 2 | 3 | private var maximumNumberOfTasks: Int 4 | 5 | private var ongoingCount = 0 6 | private var pending = [UnsafeContinuation]() 7 | 8 | public init(maximumNumberOfTasks: Int = Int.max) { 9 | self.maximumNumberOfTasks = max(maximumNumberOfTasks, 0) 10 | } 11 | 12 | public func setMaximumNumberOfTasks(_ count: Int) { 13 | self.maximumNumberOfTasks = max(count, 0) 14 | signalAvailableCapacity() 15 | } 16 | 17 | public func load(request: HTTPRequest, token: HTTPRequestToken) async -> HTTPResult { 18 | if request[option: \.throttleBehavior] == .unthrottled { 19 | return await withNextLoader(for: request) { next in 20 | return await next.load(request: request, token: token) 21 | } 22 | } 23 | 24 | if maximumNumberOfTasks <= 0 { 25 | print("Received request \(request.id) but \(type(of: self)) is paused (maximumNumberOfTasks = 0)") 26 | } 27 | 28 | #warning("TODO: handle cancellation") 29 | await waitForCapacity() 30 | 31 | return await withNextLoader(for: request) { next in 32 | ongoingCount += 1 33 | let result = await next.load(request: request, token: token) 34 | ongoingCount -= 1 35 | signalAvailableCapacity() 36 | return result 37 | } 38 | } 39 | 40 | private func waitForCapacity() async { 41 | if ongoingCount < maximumNumberOfTasks { 42 | return 43 | } 44 | 45 | return await withUnsafeContinuation { continuation in 46 | pending.append(continuation) 47 | } 48 | } 49 | 50 | private func signalAvailableCapacity() { 51 | let availableCapacity = maximumNumberOfTasks - ongoingCount 52 | guard availableCapacity > 0 else { 53 | return 54 | } 55 | 56 | let numberToSignal = min(availableCapacity, pending.count) 57 | let continuations = pending.prefix(numberToSignal) 58 | pending.removeFirst(numberToSignal) 59 | 60 | continuations.forEach { 61 | $0.resume() 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /Sources/HTTP/Loader/URLSessionLoader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public actor URLSessionLoader: HTTPLoader { 4 | 5 | private let adapter: URLSessionAdapter 6 | 7 | public init(configuration: URLSessionConfiguration) { 8 | self.adapter = URLSessionAdapter(configuration: configuration) 9 | } 10 | 11 | public func load(request: HTTPRequest, token: HTTPRequestToken) async -> HTTPResult { 12 | return await adapter.execute(request, token: token) 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Sources/HTTP/Message/HTTPBody.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol HTTPBody: Sendable { 4 | 5 | var headers: HTTPHeaders { get } 6 | var stream: AsyncStream { get throws } 7 | 8 | } 9 | 10 | public protocol HTTPSynchronousBody: HTTPBody { 11 | 12 | var bodyData: Data { get throws } 13 | 14 | } 15 | 16 | extension HTTPBody { 17 | 18 | public var headers: HTTPHeaders { 19 | return .init() 20 | } 21 | 22 | } 23 | 24 | extension HTTPSynchronousBody { 25 | 26 | public var stream: AsyncStream { 27 | get throws { 28 | AsyncStream(sequence: try self.bodyData) 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Sources/HTTP/Message/HTTPError.swift: -------------------------------------------------------------------------------- 1 | public struct HTTPError: Error, CustomStringConvertible { 2 | 3 | public enum Code { 4 | case cancelled 5 | case invalidRequest 6 | case cannotConnect 7 | case insecureConnection 8 | case cannotAuthenticate 9 | case timedOut 10 | case invalidResponse 11 | case cannotDecodeResponse 12 | case unknown 13 | case `internal` 14 | } 15 | 16 | public let code: Code 17 | public let request: HTTPRequest 18 | public let response: HTTPResponse? 19 | public let message: String? 20 | 21 | public let underlyingError: Error? 22 | 23 | public init(code: HTTPError.Code, request: HTTPRequest, response: HTTPResponse? = nil, message: String? = nil, underlyingError: Error? = nil) { 24 | self.code = code 25 | self.request = request 26 | self.response = response 27 | self.message = message 28 | self.underlyingError = underlyingError 29 | } 30 | 31 | public var description: String { 32 | var information = Array() 33 | 34 | let codeInfo: String 35 | switch code { 36 | case .cancelled: codeInfo = "Cancelled" 37 | case .invalidRequest: codeInfo = "Invalid request" 38 | case .cannotConnect: codeInfo = "Cannot connect" 39 | case .insecureConnection: codeInfo = "Insecure connection" 40 | case .cannotAuthenticate: codeInfo = "Cannot authenticate" 41 | case .timedOut: codeInfo = "Timed out" 42 | case .invalidResponse: codeInfo = "Invalid response" 43 | case .cannotDecodeResponse: codeInfo = "Cannot decode response" 44 | case .unknown: codeInfo = "Unknown" 45 | case .internal: codeInfo = "Internal" 46 | } 47 | if let message { 48 | information.append("\(codeInfo): \(message)") 49 | } else { 50 | information.append(codeInfo) 51 | } 52 | 53 | information.append("REQUEST:") 54 | information.append(contentsOf: request.descriptionLines) 55 | 56 | if let response { 57 | information.append("") 58 | information.append("RESPONSE:") 59 | information.append(contentsOf: response.descriptionLines) 60 | } 61 | 62 | if let underlyingError { 63 | information.append("") 64 | information.append("UNDERLYING ERROR:") 65 | information.append("\(underlyingError)") 66 | } 67 | 68 | return information.joined(separator: "\n") 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/HTTP/Message/HTTPHeaders.swift: -------------------------------------------------------------------------------- 1 | public struct HTTPHeaders: Sendable, Collection { 2 | 3 | private var pairs = Pairs() 4 | 5 | public init() { } 6 | 7 | public subscript(name: HTTPHeader) -> [String] { 8 | get { pairs[name] } 9 | set { pairs[name] = newValue } 10 | } 11 | 12 | public func firstValue(for header: HTTPHeader) -> String? { 13 | pairs.firstValue(for: header) 14 | } 15 | 16 | public mutating func setValue(_ value: String?, for header: HTTPHeader) { 17 | pairs.setValue(value, for: header) 18 | } 19 | 20 | public mutating func addValue(_ value: String, for header: HTTPHeader) { 21 | pairs.addValue(value, for: header) 22 | } 23 | 24 | public typealias Element = (HTTPHeader, String) 25 | 26 | public func makeIterator() -> IndexingIterator> { 27 | return pairs.makeIterator() 28 | } 29 | 30 | public var count: Int { pairs.count } 31 | public var startIndex: Int { pairs.startIndex } 32 | public var endIndex: Int { pairs.endIndex } 33 | public subscript(position: Int) -> Element { pairs[position] } 34 | public func index(after i: Int) -> Int { pairs.index(after: i) } 35 | } 36 | 37 | extension HTTPHeaders: ExpressibleByDictionaryLiteral { 38 | 39 | public init(dictionaryLiteral elements: (HTTPHeader, String)...) { 40 | self.init() 41 | 42 | for (header, value) in elements { 43 | self.addValue(value, for: header) 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /Sources/HTTP/Message/HTTPMethod.swift: -------------------------------------------------------------------------------- 1 | public struct HTTPMethod: RawRepresentable, Hashable, Sendable { 2 | 3 | public static let get = HTTPMethod(rawValue: "GET") 4 | public static let post = HTTPMethod(rawValue: "POST") 5 | public static let patch = HTTPMethod(rawValue: "PATCH") 6 | public static let put = HTTPMethod(rawValue: "PUT") 7 | public static let delete = HTTPMethod(rawValue: "DELETE") 8 | public static let head = HTTPMethod(rawValue: "HEAD") 9 | 10 | public let rawValue: String 11 | 12 | public init(rawValue: String) { 13 | self.rawValue = rawValue 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /Sources/HTTP/Message/HTTPOption.swift: -------------------------------------------------------------------------------- 1 | public protocol HTTPOption { 2 | 3 | associatedtype Value: Sendable 4 | 5 | static var defaultValue: Value { get } 6 | 7 | } 8 | 9 | public struct HTTPOptions: Sendable { 10 | 11 | private var storage = [ObjectIdentifier: any Sendable]() 12 | 13 | public subscript(type: O.Type) -> O.Value { 14 | get { 15 | let id = ObjectIdentifier(type) 16 | if let override = storage[id] as? O.Value { 17 | return override 18 | } 19 | return O.defaultValue 20 | } 21 | set { 22 | let id = ObjectIdentifier(type) 23 | storage[id] = newValue 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Sources/HTTP/Message/HTTPQuery.swift: -------------------------------------------------------------------------------- 1 | public struct HTTPQuery: Sendable, Collection { 2 | 3 | private var pairs = Pairs() 4 | 5 | public init() { } 6 | 7 | public subscript(name: String) -> [String] { 8 | get { pairs[name] } 9 | set { pairs[name] = newValue } 10 | } 11 | 12 | public func firstValue(for name: String) -> String? { 13 | pairs.firstValue(for: name) 14 | } 15 | 16 | public mutating func setValue(_ value: String?, for name: String) { 17 | pairs.setValue(value, for: name) 18 | } 19 | 20 | public mutating func addValue(_ value: String, for name: String) { 21 | pairs.addValue(value, for: name) 22 | } 23 | 24 | public typealias Element = (String, String) 25 | 26 | public func makeIterator() -> IndexingIterator> { 27 | return pairs.makeIterator() 28 | } 29 | 30 | public var count: Int { pairs.count } 31 | public var startIndex: Int { pairs.startIndex } 32 | public var endIndex: Int { pairs.endIndex } 33 | public subscript(position: Int) -> Element { pairs[position] } 34 | public func index(after i: Int) -> Int { pairs.index(after: i) } 35 | 36 | } 37 | 38 | extension HTTPQuery: ExpressibleByArrayLiteral, ExpressibleByDictionaryLiteral { 39 | 40 | public init(arrayLiteral elements: Element...) { 41 | self.init() 42 | for (key, value) in elements { 43 | self.addValue(value, for: key) 44 | } 45 | } 46 | 47 | public init(dictionaryLiteral elements: Element...) { 48 | self.init() 49 | for (key, value) in elements { 50 | self.addValue(value, for: key) 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Sources/HTTP/Message/HTTPRequestToken.swift: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | public class HTTPRequestToken: @unchecked Sendable { 4 | 5 | private typealias Handler = () -> Void 6 | 7 | // The state of the token 8 | // - Array with 0-or-more elements = uncancelled 9 | // - nil = cancelled 10 | private typealias State = Array? 11 | 12 | private var lock: OSAllocatedUnfairLock 13 | 14 | public var isCancelled: Bool { 15 | return lock.withLock { $0 == nil } 16 | } 17 | 18 | public init() { 19 | lock = OSAllocatedUnfairLock(initialState: []) 20 | } 21 | 22 | public func cancel() { 23 | let handlersToExecute = lock.withLock { state in 24 | let copy = state 25 | state = nil 26 | return copy ?? [] 27 | } 28 | 29 | for handler in handlersToExecute.reversed() { 30 | handler() 31 | } 32 | } 33 | 34 | public func addCancellationHandler(_ handler: @escaping () -> Void) { 35 | let handlerToExecute = lock.withLock { state -> Handler? in 36 | if state != nil { 37 | state?.append(handler) 38 | return nil 39 | } else { 40 | return handler 41 | } 42 | } 43 | 44 | handlerToExecute?() 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /Sources/HTTP/Message/HTTPResult+Convenience.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension HTTPResult { 4 | 5 | public static func failure(_ code: HTTPError.Code, request: HTTPRequest, response: HTTPResponse? = nil, underlyingError: Error? = nil) -> HTTPResult { 6 | let error = HTTPError(code: code, request: request, response: response, underlyingError: underlyingError) 7 | return .failure(error) 8 | } 9 | 10 | public static func ok(_ request: HTTPRequest) -> HTTPResult { 11 | let response = HTTPResponse(request: request, status: .ok) 12 | return .success(response) 13 | } 14 | 15 | public func ok(_ request: HTTPRequest, json: Body) -> HTTPResult { 16 | return HTTPResult(request: request) { 17 | let body = try JSONEncoder().encode(json) 18 | var headers = HTTPHeaders() 19 | headers[.contentType] = ["application/json; charset=utf-8"] 20 | return HTTPResponse(request: self.request, 21 | status: .ok, 22 | headers: headers, 23 | body: body) 24 | } 25 | } 26 | 27 | public static func internalServerError(_ request: HTTPRequest) -> HTTPResult { 28 | let response = HTTPResponse(request: request, status: .internalServerError) 29 | return .success(response) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Sources/HTTP/Options/AuthenticationChallenges.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct HTTPAuthenticationChallengeResponse { 4 | public static let cancelRequest = HTTPAuthenticationChallengeResponse(disposition: .cancelAuthenticationChallenge, credential: nil) 5 | 6 | public static let performDefaultAction = HTTPAuthenticationChallengeResponse(disposition: .performDefaultHandling, credential: nil) 7 | 8 | public static let rejectProtectionSpace = HTTPAuthenticationChallengeResponse(disposition: .rejectProtectionSpace, credential: nil) 9 | 10 | public static func useCredential(_ credential: URLCredential) -> HTTPAuthenticationChallengeResponse { 11 | return HTTPAuthenticationChallengeResponse(disposition: .useCredential, credential: credential) 12 | } 13 | 14 | internal let disposition: URLSession.AuthChallengeDisposition 15 | internal let credential: URLCredential? 16 | } 17 | 18 | public protocol HTTPAuthenticationChallengeHandler { 19 | func evaluate(_ challenge: URLAuthenticationChallenge, for request: HTTPRequest) async -> HTTPAuthenticationChallengeResponse 20 | } 21 | 22 | extension HTTPOptions { 23 | 24 | public var authenticationChallengeHandler: (any HTTPAuthenticationChallengeHandler)? { 25 | get { self[HTTPAuthenticationChallengeOption.self] } 26 | set { self[HTTPAuthenticationChallengeOption.self] = newValue } 27 | } 28 | 29 | } 30 | 31 | private enum HTTPAuthenticationChallengeOption: HTTPOption { 32 | static let defaultValue: (any HTTPAuthenticationChallengeHandler)? = nil 33 | } 34 | -------------------------------------------------------------------------------- /Sources/HTTP/Options/Deduplication.swift: -------------------------------------------------------------------------------- 1 | 2 | extension HTTPOptions { 3 | 4 | public var deduplicationIdentifier: String? { 5 | get { self[HTTPDeduplicationIdentifier.self] } 6 | set { self[HTTPDeduplicationIdentifier.self] = newValue } 7 | } 8 | 9 | } 10 | 11 | private enum HTTPDeduplicationIdentifier: HTTPOption { 12 | static let defaultValue: String? = nil 13 | } 14 | -------------------------------------------------------------------------------- /Sources/HTTP/Options/Redirection.swift: -------------------------------------------------------------------------------- 1 | public protocol HTTPRedirectionHandler { 2 | 3 | func handleRedirection(for request: HTTPRequest, response: HTTPResponse, proposedRedirection: HTTPRequest) async -> HTTPRequest? 4 | 5 | } 6 | 7 | extension HTTPOptions { 8 | 9 | public var redirectionHandler: (any HTTPRedirectionHandler)? { 10 | get { self[HTTPRedirectonOption.self] } 11 | set { self[HTTPRedirectonOption.self] = newValue } 12 | } 13 | 14 | } 15 | 16 | private enum HTTPRedirectonOption: HTTPOption { 17 | static let defaultValue: (any HTTPRedirectionHandler)? = nil 18 | } 19 | -------------------------------------------------------------------------------- /Sources/HTTP/Options/RetryStrategy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension HTTPOptions { 4 | 5 | /// Indicates how the request should be retried if it is not cancelled. 6 | /// 7 | /// The default value is `nil`, which means the request will not be retried. 8 | public var retryStrategy: (any HTTPRetryStrategy)? { 9 | get { self[HTTPRetryOption.self] } 10 | set { self[HTTPRetryOption.self] = newValue } 11 | } 12 | 13 | } 14 | 15 | public protocol HTTPRetryStrategy { 16 | mutating func nextDelay(after result: HTTPResult) -> TimeInterval? 17 | } 18 | 19 | public struct BackoffRetry: HTTPRetryStrategy { 20 | private var delays: Array 21 | 22 | public static func immediately(maximumNumberOfAttempts: Int) -> HTTPRetryStrategy { 23 | let count = max(maximumNumberOfAttempts - 1, 0) 24 | return explicit(delays: Array(repeating: 0, count: count)) 25 | } 26 | 27 | public static func linear(delay: TimeInterval, maximumNumberOfAttempts: Int) -> HTTPRetryStrategy { 28 | let count = max(maximumNumberOfAttempts - 1, 0) 29 | return explicit(delays: Array(repeating: delay, count: count)) 30 | } 31 | 32 | public static func exponential(maximumNumberOfAttempts: Int) -> HTTPRetryStrategy { 33 | let count = max(maximumNumberOfAttempts, 0) 34 | let delays = (0 ..< count).map { TimeInterval(pow(1.5, Double($0))) } 35 | return explicit(delays: delays) 36 | } 37 | 38 | public static func explicit(delays: Array) -> HTTPRetryStrategy { 39 | return BackoffRetry(delays: delays) 40 | } 41 | 42 | private init(delays: Array) { 43 | self.delays = delays 44 | } 45 | 46 | public mutating func nextDelay(after result: HTTPResult) -> TimeInterval? { 47 | guard result.isFailure else { return nil } 48 | guard delays.isEmpty == false else { return nil } 49 | return delays.removeFirst() 50 | } 51 | } 52 | 53 | public struct CustomRetry: HTTPRetryStrategy { 54 | 55 | private let computeDelay: (HTTPResult) -> TimeInterval? 56 | 57 | public init(_ delay: @escaping (HTTPResult) -> TimeInterval?) { 58 | self.computeDelay = delay 59 | } 60 | 61 | public func nextDelay(after result: HTTPResult) -> TimeInterval? { 62 | return computeDelay(result) 63 | } 64 | 65 | } 66 | 67 | private enum HTTPRetryOption: HTTPOption { 68 | static let defaultValue: HTTPRetryStrategy? = nil 69 | } 70 | -------------------------------------------------------------------------------- /Sources/HTTP/Options/Throttling.swift: -------------------------------------------------------------------------------- 1 | public enum HTTPThrottleBehavior: Sendable, Equatable { 2 | case throttled 3 | case unthrottled 4 | } 5 | 6 | extension HTTPOptions { 7 | 8 | public var throttleBehavior: HTTPThrottleBehavior { 9 | get { self[HTTPThrottleOption.self] } 10 | set { self[HTTPThrottleOption.self] = newValue } 11 | } 12 | 13 | } 14 | 15 | private enum HTTPThrottleOption: HTTPOption { 16 | static let defaultValue: HTTPThrottleBehavior = .throttled 17 | } 18 | -------------------------------------------------------------------------------- /Sources/HTTP/Utilities/AsyncStream.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension AsyncStream { 4 | 5 | internal init(sequence: S) where S.Element == Element { 6 | self.init { continuation in 7 | for element in sequence { 8 | continuation.yield(element) 9 | } 10 | continuation.finish() 11 | } 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Sources/HTTP/Utilities/Duration.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Duration { 4 | 5 | init(_ timeInterval: TimeInterval) { 6 | self = Duration.seconds(timeInterval) 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /Sources/HTTP/Utilities/HTTPRequest+URLRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension HTTPRequest { 4 | 5 | internal init(request: URLRequest) { 6 | self.init() 7 | let components = request.url.flatMap { URLComponents(url: $0, resolvingAgainstBaseURL: true) } 8 | 9 | if let m = request.httpMethod { 10 | self.method = HTTPMethod(rawValue: m) 11 | } 12 | self.host = components?.host 13 | self.path = components?.path 14 | self.fragment = components?.fragment 15 | 16 | for item in components?.queryItems ?? [] { 17 | self.query.addValue(item.value ?? "", for: item.name) 18 | } 19 | 20 | for (header, value) in request.allHTTPHeaderFields ?? [:] { 21 | self.headers.addValue(value, for: HTTPHeader(rawValue: header)) 22 | } 23 | } 24 | 25 | internal func convertToURLRequest() -> URLRequest? { 26 | var components = URLComponents() 27 | components.scheme = "https" 28 | 29 | guard let host = host else { 30 | return nil 31 | } 32 | components.host = host 33 | components.path = path ?? "" 34 | components.fragment = fragment 35 | components.queryItems = query.map { name, value in 36 | return URLQueryItem(name: name, value: value.isEmpty ? nil : value) 37 | } 38 | 39 | guard let url = components.url else { 40 | return nil 41 | } 42 | 43 | var urlRequest = URLRequest(url: url) 44 | urlRequest.httpMethod = method.rawValue 45 | 46 | for (header, value) in headers { 47 | urlRequest.addValue(value, forHTTPHeaderField: header.rawValue) 48 | } 49 | 50 | if let body = body { 51 | for (header, value) in body.headers { 52 | urlRequest.addValue(value, forHTTPHeaderField: header.rawValue) 53 | } 54 | 55 | if let syncBody = body as? HTTPSynchronousBody { 56 | do { 57 | urlRequest.httpBody = try syncBody.bodyData 58 | } catch { 59 | print("Error encoding body data: \(error)") 60 | return nil 61 | } 62 | } else { 63 | fatalError("Async bodies are not supported yet") 64 | } 65 | } 66 | 67 | return urlRequest 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/HTTP/Utilities/HTTPResponse+URLResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension HTTPResponse { 4 | 5 | internal init(request: HTTPRequest, response: HTTPURLResponse) { 6 | self.request = request 7 | self.status = HTTPStatus(rawValue: response.statusCode) 8 | 9 | for (anyHeader, anyValue) in response.allHeaderFields { 10 | let header = HTTPHeader(rawValue: anyHeader.description) 11 | if let str = anyValue as? String { 12 | self.headers.addValue(str, for: header) 13 | } else if let strs = anyValue as? [String] { 14 | for str in strs { 15 | self.headers.addValue(str, for: header) 16 | } 17 | } else { 18 | print("UNKNOWN HEADER VALUE", anyValue) 19 | } 20 | } 21 | 22 | self.body = nil 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Sources/HTTP/Utilities/LoaderChain.swift: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | internal class LoaderChain { 4 | 5 | static let shared = LoaderChain() 6 | 7 | private typealias State = [ObjectIdentifier: HTTPLoader] 8 | 9 | // BUG: this will retain loaders indefinitely 10 | private var lock: OSAllocatedUnfairLock 11 | 12 | private init() { 13 | lock = OSAllocatedUnfairLock(initialState: [:]) 14 | } 15 | 16 | func nextLoader(for loader: HTTPLoader) -> HTTPLoader? { 17 | return lock.withLock { state in 18 | let id = ObjectIdentifier(loader) 19 | return state[id] 20 | } 21 | } 22 | 23 | func setNextLoader(_ next: HTTPLoader?, for loader: HTTPLoader) { 24 | lock.withLock { state in 25 | let id = ObjectIdentifier(loader) 26 | if let n = next { 27 | var seen = Set() 28 | seen.insert(id) 29 | 30 | var current = id 31 | while let nextLoader = state[current] { 32 | let nextID = ObjectIdentifier(nextLoader) 33 | if seen.contains(nextID) { 34 | fatalError("Cycle detected while setting the nextLoader") 35 | } else { 36 | seen.insert(nextID) 37 | current = nextID 38 | } 39 | } 40 | 41 | state[id] = n 42 | } else { 43 | state.removeValue(forKey: id) 44 | } 45 | } 46 | 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Sources/HTTP/Utilities/Pairs.swift: -------------------------------------------------------------------------------- 1 | internal struct Pairs: Sendable, ExpressibleByArrayLiteral { 2 | typealias Element = (Key, Value) 3 | 4 | private var values = [Element]() 5 | 6 | internal init() { } 7 | 8 | init(arrayLiteral elements: Element...) { 9 | self.values = elements 10 | } 11 | 12 | internal subscript(key: Key) -> [Value] { 13 | get { values(for: key) } 14 | set { setValues(newValue, for: key) } 15 | } 16 | 17 | internal func firstValue(for key: Key) -> Value? { 18 | return values.first(where: { $0.0 == key })?.1 19 | } 20 | 21 | internal mutating func setValue(_ value: Value?, for key: Key) { 22 | if let value { 23 | self.setValues([value], for: key) 24 | } else { 25 | self.setValues([], for: key) 26 | } 27 | } 28 | 29 | internal mutating func addValue(_ value: Value, for key: Key) { 30 | values.append((key, value)) 31 | } 32 | 33 | private func values(for key: Key) -> [Value] { 34 | return values.compactMap { $0 == key ? $1 : nil } 35 | } 36 | 37 | private mutating func setValues(_ newValues: [Value], for key: Key) { 38 | var remaining = newValues.makeIterator() 39 | var new = [Element]() 40 | 41 | for (existingKey, value) in values { 42 | if existingKey == key { 43 | if let next = remaining.next() { 44 | // there's a replacement value 45 | new.append((existingKey, next)) 46 | } else { 47 | // there is no replacement value; do not append 48 | } 49 | } else { 50 | new.append((existingKey, value)) 51 | } 52 | } 53 | 54 | while let next = remaining.next() { 55 | new.append((key, next)) 56 | } 57 | 58 | self.values = new 59 | } 60 | } 61 | 62 | extension Pairs: Sequence { 63 | typealias Iterator = Array.Iterator 64 | 65 | func makeIterator() -> IndexingIterator<[Element]> { 66 | return values.makeIterator() 67 | } 68 | 69 | } 70 | 71 | extension Pairs: Collection { 72 | 73 | typealias Index = Array.Index 74 | 75 | var count: Int { values.count } 76 | 77 | var startIndex: Index { values.startIndex } 78 | 79 | var endIndex: Index { values.endIndex } 80 | 81 | subscript(position: Index) -> Element { values[position] } 82 | 83 | func index(after i: Index) -> Index { values.index(after: i) } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /Sources/HTTP/Utilities/URLSession/URLSessionAdapterDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal class URLSessionAdapterDelegate: NSObject, URLSessionTaskDelegate, URLSessionDataDelegate { 4 | 5 | let queue: OperationQueue 6 | weak var adapter: URLSessionAdapter? 7 | 8 | override init() { 9 | self.queue = OperationQueue() 10 | super.init() 11 | 12 | self.queue.name = "\(type(of: self))" 13 | self.queue.maxConcurrentOperationCount = 1 14 | } 15 | 16 | // MARK: - URLSessionTaskDelegate 17 | 18 | func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest) async -> URLRequest? { 19 | guard let adapter else { 20 | return nil 21 | } 22 | 23 | return await adapter.task(task, willPerformHTTPRedirection: response, newRequest: request) 24 | } 25 | 26 | func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { 27 | guard let adapter else { 28 | return (.cancelAuthenticationChallenge, nil) 29 | } 30 | 31 | return await adapter.task(task, didReceive: challenge) 32 | } 33 | 34 | func urlSession(_ session: URLSession, needNewBodyStreamForTask task: URLSessionTask) async -> InputStream? { 35 | guard let adapter else { 36 | return nil 37 | } 38 | 39 | return await adapter.task(needsNewBodyStream: task) 40 | } 41 | 42 | func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { 43 | adapter?.task(task, didSendBodyData: bytesSent, totalBytesSent: totalBytesSent, totalBytesExpectedToSend: totalBytesExpectedToSend) 44 | } 45 | 46 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 47 | adapter?.task(task, didCompleteWithError: error) 48 | } 49 | 50 | // MARK: - URLSessionDataDelegate 51 | 52 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse) async -> URLSession.ResponseDisposition { 53 | guard let adapter else { 54 | return .cancel 55 | } 56 | 57 | return await adapter.task(dataTask, didReceive: response) 58 | } 59 | 60 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { 61 | adapter?.task(dataTask, didReceive: data) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/HTTP/Utilities/URLSession/URLSessionTaskState.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal struct URLSessionTaskState { 4 | let httpRequest: HTTPRequest 5 | let token: HTTPRequestToken 6 | 7 | let dataTask: URLSessionDataTask 8 | 9 | var response: HTTPResponse? 10 | var data: Data? 11 | 12 | var continuation: UnsafeContinuation 13 | } 14 | -------------------------------------------------------------------------------- /Sources/PrivateAPI/include/AppSession.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppSession.h 3 | // 4 | // 5 | // Created by Dave DeLong on 2/24/23. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | NSUUID *_Nonnull app_session_initialize(NSURL * _Nonnull logFolder); 13 | 14 | void app_session_crash_metadata_add_bool(NSString * _Nonnull key, BOOL value); 15 | void app_session_crash_metadata_add_int(NSString * _Nonnull key, NSInteger value); 16 | void app_session_crash_metadata_add_double(NSString * _Nonnull key, double value); 17 | void app_session_crash_metadata_add_string(NSString * _Nonnull key, NSString * _Nonnull value); 18 | 19 | NSArray *_Nonnull app_session_all_crash_files(void); 20 | 21 | 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /Sources/PrivateAPI/include/GregorianDate+Format.h: -------------------------------------------------------------------------------- 1 | // 2 | // GregorianDate+Format.h 3 | // 4 | // 5 | // Created by Dave DeLong on 6/3/23. 6 | // 7 | 8 | #import 9 | #import "GregorianDate.h" 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | // format a date into a string, using TR35 syntax 14 | const char * _Nullable GregorianDateFormat(GregorianDate date, const char *format); 15 | size_t GregorianDateFormatBuffer(GregorianDate date, const char *format, char *_Nullable buffer); 16 | 17 | 18 | NS_ASSUME_NONNULL_END 19 | -------------------------------------------------------------------------------- /Sources/PrivateAPI/include/GregorianDate+Formatters.h: -------------------------------------------------------------------------------- 1 | // 2 | // GregorianDate+Formatters.h 3 | // 4 | // 5 | // Created by Dave DeLong on 6/4/23. 6 | // 7 | 8 | #import 9 | #import "GregorianDate.h" 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | typedef size_t(*_GDFormatFunction)(GregorianDate date, size_t formatCount, char *buffer); 14 | 15 | bool _GDIsFormatCharacter(char unit); 16 | 17 | _Nullable _GDFormatFunction _GDFormatterLookup(char unit, size_t formatCount); 18 | 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /Sources/PrivateAPI/include/GregorianDate.h: -------------------------------------------------------------------------------- 1 | // 2 | // GregorianDate.h 3 | // 4 | // 5 | // Created by Dave DeLong on 5/29/23. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | typedef struct GregorianDate { 13 | int16_t year; 14 | int8_t month; 15 | int8_t day; 16 | 17 | int8_t hour; 18 | int8_t minute; 19 | int8_t second; 20 | 21 | int16_t tzoffset; 22 | } GregorianDate; 23 | 24 | GregorianDate GregorianDateParseTimestamp(time_t timestamp, int16_t tzoffset); 25 | time_t GregorianDateTimestamp(GregorianDate date); 26 | GregorianDate GregorianDateNormalizeToUTC(GregorianDate date); 27 | 28 | int8_t GregorianDateQuarter(GregorianDate date); 29 | int8_t GregorianDateDayOfYear(GregorianDate date); 30 | int64_t GregorianDateJulianDay(GregorianDate date); 31 | 32 | // comparisons 33 | bool GregorianDateIsEqual(GregorianDate left, GregorianDate right); 34 | bool GregorianDateIsBefore(GregorianDate left, GregorianDate right); 35 | bool GregorianDateIsAfter(GregorianDate left, GregorianDate right); 36 | 37 | bool GregorianDateIsValid(GregorianDate date); 38 | bool GregorianDateIsLeapYear(GregorianDate date); 39 | 40 | GregorianDate GregorianDateIncrementDay(GregorianDate date); 41 | GregorianDate GregorianDateDecrementDay(GregorianDate date); 42 | 43 | // Utilities 44 | 45 | int8_t GregorianDaysInYear(int16_t year); 46 | bool GregorianIsLeapYear(int16_t year); 47 | 48 | NS_ASSUME_NONNULL_END 49 | -------------------------------------------------------------------------------- /Sources/PrivateAPI/include/JSON+Serialize.h: -------------------------------------------------------------------------------- 1 | // 2 | // JSON+Serialize.h 3 | // 4 | // 5 | // Created by Dave DeLong on 5/27/23. 6 | // 7 | 8 | #import "JSON.h" 9 | 10 | typedef struct JSONSerializationOptions { 11 | bool prettyPrint; 12 | bool includeNullByte; 13 | bool requireSignalSafety; 14 | } JSONSerializationOptions; 15 | 16 | void *JSONSerialize(JSON *json, JSONSerializationOptions opts); 17 | 18 | void JSONSerializeToFile(JSON *json, int fd, JSONSerializationOptions opts); 19 | NSString *JSONSerializeToString(JSON *json, JSONSerializationOptions opts); 20 | -------------------------------------------------------------------------------- /Sources/PrivateAPI/include/NSUUID+Time.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSUUID+Time.h 3 | // 4 | // 5 | // Created by Dave DeLong on 7/13/23. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface NSUUID (ExtendedObjC) 13 | 14 | + (instancetype)extended_timedUUID; 15 | 16 | @end 17 | 18 | NS_ASSUME_NONNULL_END 19 | -------------------------------------------------------------------------------- /Sources/PrivateAPI/include/SignalSafe.h: -------------------------------------------------------------------------------- 1 | // 2 | // SignalSafe.h 3 | // 4 | // 5 | // Created by Dave DeLong on 5/28/23. 6 | // 7 | 8 | #import 9 | #import "GregorianDate.h" 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | void SafeFormatUInt(char *buffer, int d, size_t length); 14 | void SafeFormatHex(char *buffer, int d, size_t length); 15 | 16 | void SafeFormatUUID(__darwin_uuid_string_t _Nonnull buffer, uuid_t _Nonnull uuid); 17 | 18 | NS_ASSUME_NONNULL_END 19 | -------------------------------------------------------------------------------- /Sources/PrivateAPI/src/NSUUID+Time.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSUUID+Time.m 3 | // 4 | // 5 | // Created by Dave DeLong on 7/13/23. 6 | // 7 | 8 | #import "NSUUID+Time.h" 9 | #import 10 | #import "SignalSafe.h" 11 | 12 | @implementation NSUUID (ExtendedObjC) 13 | 14 | + (instancetype)extended_timedUUID { 15 | uuid_t u = {0}; 16 | uuid_generate(u); 17 | 18 | uuid_string_t s = {0}; 19 | SafeFormatUUID(s, u); 20 | 21 | NSCalendar *cal = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian]; 22 | NSDateComponents *c = [cal componentsInTimeZone:[NSTimeZone defaultTimeZone] fromDate:[NSDate date]]; 23 | 24 | char *yearStart = s + 0; 25 | char *monthStart = s + 4; 26 | char *dayStart = s + 6; 27 | SafeFormatUInt(yearStart, (int)c.year, 4); 28 | SafeFormatUInt(monthStart, (int)c.month, 2); 29 | SafeFormatUInt(dayStart, (int)c.day, 2); 30 | 31 | char *hourStart = s + 9; 32 | char *minStart = s + 11; 33 | SafeFormatUInt(hourStart, (int)c.hour, 2); 34 | SafeFormatUInt(minStart, (int)c.minute, 2); 35 | 36 | char *secStart = s + 14; 37 | char *subStart = s + 16; 38 | SafeFormatUInt(secStart, (int)c.second, 2); 39 | 40 | NSInteger subSecond = c.nanosecond * 100 / NSEC_PER_SEC; 41 | SafeFormatUInt(subStart, (int)subSecond, 2); 42 | 43 | return [[NSUUID alloc] initWithUUIDString:@(s)]; 44 | } 45 | 46 | @end 47 | -------------------------------------------------------------------------------- /Sources/debug/debug.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/8/23. 6 | // 7 | 8 | import Combine 9 | import Compression 10 | import Foundation 11 | import ExtendedSwift 12 | import ExtendedKit 13 | import UniformTypeIdentifiers 14 | import ExtendedObjC 15 | 16 | @main 17 | struct Debug { 18 | static func main() async throws { 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/ExtendedObjCTests/RuntimeTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // RuntimeTests.m 3 | // 4 | // 5 | // Created by Dave DeLong on 7/20/23. 6 | // 7 | 8 | #import 9 | #import "Runtime.h" 10 | 11 | @interface RuntimeTests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation RuntimeTests 16 | 17 | - (void)testTypeEncodingIteration { 18 | 19 | const char *t1 = "v@:"; 20 | __block int count = 0; 21 | typeEncoding_enumerateTypes(t1, ^(const char * _Nonnull type, size_t size, BOOL * _Nonnull keepGoing) { 22 | count += 1; 23 | if (count == 1) { 24 | XCTAssertTrue(strcmp(type, "v") == 0); 25 | } else if (count == 2) { 26 | XCTAssertTrue(strcmp(type, "@") == 0); 27 | } else if (count == 3) { 28 | XCTAssertTrue(strcmp(type, ":") == 0); 29 | } else { 30 | XCTFail(); 31 | } 32 | }); 33 | 34 | XCTAssertEqual(count, 3); 35 | 36 | } 37 | 38 | - (void)testInvalidTypeIteration { 39 | const char *t1 = "hello, world!"; 40 | __block int count = 0; 41 | typeEncoding_enumerateTypes(t1, ^(const char * _Nonnull type, size_t size, BOOL * _Nonnull keepGoing) { 42 | count += 1; 43 | }); 44 | XCTAssertEqual(count, 0); 45 | 46 | count = 0; 47 | typeEncoding_enumerateTypes("void", ^(const char * _Nonnull type, size_t size, BOOL * _Nonnull keepGoing) { 48 | count += 1; 49 | }); 50 | XCTAssertEqual(count, 3); 51 | 52 | Class nsview = NSClassFromString(@"NSView"); 53 | class_enumerateIvars(nsview, ^(Ivar _Nonnull i, BOOL * _Nonnull keepGoing) { 54 | const char *typeEncoding = ivar_getTypeEncoding(i); 55 | const char *name = ivar_getName(i); 56 | 57 | printf("%s - %s\n", name, typeEncoding); 58 | typeEncoding_enumerateTypes(typeEncoding, ^(const char * _Nonnull type, size_t size, BOOL * _Nonnull keepGoing) { 59 | printf(" %s\n", type); 60 | }); 61 | }); 62 | class_enumerateInstanceMethods(nsview, ^(Method _Nonnull m, BOOL * _Nonnull keepGoing) { 63 | const char *typeEncoding = method_getTypeEncoding(m); 64 | const char *name = sel_getName(method_getName(m)); 65 | 66 | printf("%s - %s\n", name, typeEncoding); 67 | typeEncoding_enumerateTypes(typeEncoding, ^(const char * _Nonnull type, size_t size, BOOL * _Nonnull keepGoing) { 68 | printf(" %s\n", type); 69 | }); 70 | }); 71 | } 72 | 73 | @end 74 | -------------------------------------------------------------------------------- /Tests/ExtendedSwiftTests/BuilderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 6/25/23. 6 | // 7 | 8 | import Foundation 9 | import ExtendedSwift 10 | import XCTest 11 | 12 | class BuilderTests: XCTestCase { 13 | 14 | func testDefaultInit() { 15 | let f = TestFoo(build: { _ in }) 16 | XCTAssertEqual(f.string, "string") 17 | XCTAssertNil(f.int) 18 | } 19 | 20 | func testSimpleInit() { 21 | let f = TestFoo { 22 | $0.string = "hello" 23 | $0.int = 42 24 | } 25 | 26 | XCTAssertEqual(f.string, "hello") 27 | XCTAssertEqual(f.int, 42) 28 | } 29 | 30 | func testChainedInit() { 31 | let f = TestFoo.builder() 32 | .string("hello") 33 | .int(42) 34 | .build() 35 | 36 | XCTAssertEqual(f.string, "hello") 37 | XCTAssertEqual(f.int, 42) 38 | } 39 | 40 | func testReusedBuilder() { 41 | var b = TestFoo.builder() 42 | b.string = "hello" 43 | 44 | let f1 = b.build() 45 | XCTAssertEqual(f1.string, "hello") 46 | XCTAssertNil(f1.int) 47 | 48 | b.string = "world" 49 | b.int = 42 50 | let f2 = b.build() 51 | 52 | XCTAssertEqual(f2.string, "world") 53 | XCTAssertEqual(f2.int, 42) 54 | } 55 | 56 | func testInnerBuilder() { 57 | let f = TestFoo.builder() 58 | .string("hello") 59 | .other({ $0.name = "world" }) 60 | .build() 61 | 62 | XCTAssertEqual(f.string, "hello") 63 | XCTAssertNil(f.int) 64 | XCTAssertEqual(f.other.name, "world") 65 | XCTAssertNil(f.maybe) 66 | } 67 | 68 | } 69 | 70 | struct TestFoo: Buildable { 71 | 72 | let string: String 73 | let int: Int? 74 | let other: Other 75 | let maybe: Other? 76 | 77 | init(builder: Builder) { 78 | self.string = builder.string ?? "string" 79 | self.int = builder.int 80 | self.other = builder.other ?? Other.buildDefault() 81 | self.maybe = builder.maybe 82 | } 83 | 84 | } 85 | 86 | struct Other: Buildable { 87 | let name: String 88 | 89 | init(builder: Builder) { 90 | name = builder.name ?? "name" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Tests/ExtendedSwiftTests/CharacterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/1/23. 6 | // 7 | 8 | import Foundation 9 | import ExtendedSwift 10 | import XCTest 11 | 12 | class CharacterTests: XCTestCase { 13 | 14 | func testIsASCIIDigit() { 15 | for character in "0123456789" { 16 | XCTAssertTrue(character.isASCIIDigit) 17 | } 18 | 19 | for character in "abcdefghijklmnopqurstuvwxyz" { 20 | XCTAssertFalse(character.isASCIIDigit) 21 | } 22 | 23 | for character in "٠١٢٣٤٥٦٧٨٩๐๑๒๓๔๕๖๗๘๙" { 24 | XCTAssertTrue(character.isWholeNumber) 25 | XCTAssertFalse(character.isASCIIDigit) 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Tests/ExtendedSwiftTests/ClockTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 9/17/23. 6 | // 7 | 8 | import XCTest 9 | import ExtendedSwift 10 | 11 | class ClockTests: XCTestCase { 12 | 13 | func testMutableClock() async throws { 14 | let user = UserClock() 15 | let mutable = user.mutableClock() 16 | 17 | var userNow = user.now 18 | var mutableNow = mutable.now 19 | XCTAssertEqual(userNow.timeIntervalSinceReferenceDate, mutableNow.timeIntervalSinceReferenceDate, accuracy: 0.1) 20 | 21 | let referenceDate = Date(timeIntervalSinceReferenceDate: 0) 22 | mutable.now = referenceDate 23 | 24 | userNow = user.now 25 | mutableNow = mutable.now 26 | XCTAssertNotEqual(userNow.timeIntervalSinceReferenceDate, mutableNow.timeIntervalSinceReferenceDate, accuracy: 0.1) 27 | XCTAssertEqual(mutableNow.timeIntervalSinceReferenceDate, 0, accuracy: 0.1) 28 | 29 | try await mutable.sleep(for: .seconds(1)) 30 | 31 | mutableNow = mutable.now 32 | XCTAssertEqual(mutableNow.timeIntervalSinceReferenceDate, 1, accuracy: 0.1) 33 | } 34 | 35 | func testManualClock() async throws { 36 | let user = UserClock() 37 | let manual = user.manualClock() 38 | 39 | var userNow = user.now 40 | let manualNow = manual.now 41 | XCTAssertEqual(userNow.timeIntervalSinceReferenceDate, manualNow.timeIntervalSinceReferenceDate, accuracy: 0.1) 42 | 43 | try await user.sleep(for: .seconds(1)) 44 | 45 | userNow = user.now 46 | var newManualNow = manual.now 47 | XCTAssertNotEqual(userNow.timeIntervalSinceReferenceDate, newManualNow.timeIntervalSinceReferenceDate, accuracy: 0.1) 48 | XCTAssertEqual(manualNow, newManualNow) 49 | 50 | manual.advance(by: .seconds(1)) 51 | newManualNow = manual.now 52 | 53 | XCTAssertEqual(userNow.timeIntervalSinceReferenceDate, newManualNow.timeIntervalSinceReferenceDate, accuracy: 0.1) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Tests/ExtendedSwiftTests/CollectionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/1/23. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | 11 | class CollectionTests: XCTestCase { 12 | 13 | func testIsNotEmpty() { 14 | XCTAssertFalse(Array().isNotEmpty) 15 | XCTAssertTrue([1, 2, 3].isNotEmpty) 16 | 17 | XCTAssertTrue([1, 2, 3][0 ..< 0].isEmpty) 18 | XCTAssertFalse([1, 2, 3][0 ..< 0].isNotEmpty) 19 | } 20 | 21 | func testAllPrefixes() { 22 | let p1 = "abcde".allPrefixes() 23 | XCTAssertEqual(p1.count, 5) 24 | XCTAssertEqual(p1[0], "a") 25 | XCTAssertEqual(p1[1], "ab") 26 | XCTAssertEqual(p1[2], "abc") 27 | XCTAssertEqual(p1[3], "abcd") 28 | XCTAssertEqual(p1[4], "abcde") 29 | 30 | let p2 = "a".allPrefixes() 31 | XCTAssertEqual(p2.count, 1) 32 | XCTAssertEqual(p2[0], "a") 33 | 34 | let p3 = Array().allPrefixes() 35 | XCTAssertEqual(p3.count, 0) 36 | } 37 | 38 | func testTrimming() { 39 | XCTAssertEqual(" abc ".trimming(where: \.isWhitespace), "abc") 40 | XCTAssertEqual(" abc ".trimmingPrefix(where: \.isWhitespace), "abc ") 41 | XCTAssertEqual(" abc ".trimmingSuffix(where: \.isWhitespace), " abc") 42 | 43 | XCTAssertEqual(" ".trimming(where: \.isWhitespace), "") 44 | XCTAssertEqual(" ".trimmingPrefix(where: \.isWhitespace), "") 45 | XCTAssertEqual(" ".trimmingSuffix(where: \.isWhitespace), "") 46 | } 47 | 48 | func testLastK() { 49 | let s = "abc" 50 | XCTAssertEqual(s.last(-1), "") 51 | XCTAssertEqual(s.last(0), "") 52 | XCTAssertEqual(s.last(1), "c") 53 | XCTAssertEqual(s.last(2), "bc") 54 | XCTAssertEqual(s.last(3), "abc") 55 | XCTAssertEqual(s.last(4), "abc") 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/ExtendedSwiftTests/PlistCodableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 11/25/23. 6 | // 7 | 8 | import XCTest 9 | @testable import ExtendedSwift 10 | 11 | func XCTAssertEqualPlists(_ plist1: Any, _ plist2: T, file: StaticString = #file, line: UInt = #line) { 12 | let ns1 = plist1 as! NSObject 13 | let ns2 = plist2 as! NSObject 14 | 15 | XCTAssertEqual(ns1, ns2, file: file, line: line) 16 | } 17 | 18 | class PlistCodableTests: XCTestCase { 19 | 20 | func testSingleValue() throws { 21 | let e = try PlistEncoder().encode("Hello") 22 | XCTAssertEqualPlists(e, "Hello") 23 | 24 | let d = try PlistDecoder().decode(String.self, from: e) 25 | XCTAssertEqual(d, "Hello") 26 | } 27 | 28 | func testArray() throws { 29 | let e = try PlistEncoder().encode(["a", "b", "c"]) 30 | XCTAssertEqualPlists(e, ["a", "b", "c"]) 31 | 32 | let d = try PlistDecoder().decode(Array.self, from: e) 33 | XCTAssertEqual(d, ["a", "b", "c"]) 34 | } 35 | 36 | func testObject() throws { 37 | struct Foo: Codable, Equatable { 38 | let a: Int 39 | let b: String 40 | } 41 | 42 | let f = Foo(a: 42, b: "b") 43 | 44 | let e = try PlistEncoder().encode(f) 45 | XCTAssertEqualPlists(e, ["a": 42, "b": "b"]) 46 | 47 | let d = try PlistDecoder().decode(Foo.self, from: e) 48 | XCTAssertEqual(d, f) 49 | } 50 | 51 | func testNestedObject() throws { 52 | struct Foo: Codable, Equatable { 53 | let a: Int 54 | let b: String 55 | let bar: Bar? 56 | } 57 | struct Bar: Codable, Equatable { 58 | let c: Array 59 | let d: String? 60 | } 61 | 62 | 63 | let bar = Bar(c: ["a", "b"], d: nil) 64 | let foo = Foo(a: 42, b: "b", bar: bar) 65 | 66 | let e = try PlistEncoder().encode(foo) 67 | XCTAssertEqualPlists(e, [ 68 | "a": 42, 69 | "b": "b", 70 | "bar": [ 71 | "c": ["a", "b"] 72 | ] 73 | ]) 74 | 75 | let d = try PlistDecoder().decode(Foo.self, from: e) 76 | XCTAssertEqual(d, foo) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Tests/ExtendedSwiftTests/RegexTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 5/14/23. 6 | // 7 | 8 | import XCTest 9 | import ExtendedSwift 10 | 11 | class RegexTests: XCTestCase { 12 | 13 | func testLastMatch() throws { 14 | let r = /(\d+)/ 15 | if let m1 = r.lastMatch(in: "123 456") { 16 | XCTAssertEqual(m1.1, "6") 17 | } else { 18 | XCTFail() 19 | } 20 | } 21 | 22 | func testAllMatches() throws { 23 | let r = /\d+/ 24 | 25 | let m1 = r.allMatches(in: "123 456") 26 | XCTAssertEqual(m1.count, 2) 27 | 28 | let m2 = r.allMatches(in: "hello, world") 29 | XCTAssertEqual(m2.count, 0) 30 | 31 | let r2 = /^ab/ 32 | let m3 = r2.allMatches(in: "abab") 33 | XCTExpectFailure("All matches for anchored regexes do not work properly") 34 | XCTAssertEqual(m3.count, 1) 35 | } 36 | 37 | func testFirstReplacement() throws { 38 | let r = /\d+/ 39 | let s1 = "123 456".replacingFirstMatch(of: r, with: "bob") 40 | XCTAssertEqual(s1, "bob 456") 41 | } 42 | 43 | func testLastReplacement() throws { 44 | let r = /\d+/ 45 | let s1 = "123 456".replacingLastMatch(of: r, with: "bob") 46 | XCTAssertEqual(s1, "123 45bob") 47 | } 48 | 49 | func testReplacement() throws { 50 | let r1 = /(\d+)/ 51 | let s1 = "bob123".replacingAllMatches(of: r1, with: "") 52 | XCTAssertEqual(s1, "bob") 53 | 54 | let s2 = "bob".replacingAllMatches(of: r1, with: "") 55 | XCTAssertEqual(s2, "bob") 56 | } 57 | 58 | func testReplacementInterpolation() throws { 59 | let r1 = /(\d+)/ 60 | let s1 = "bob123".replacingAllMatches(of: r1, with: " \(\.1)") 61 | XCTAssertEqual(s1, "bob 123") 62 | let s2 = "bob123-456".replacingAllMatches(of: r1, with: " \(\.1)") 63 | XCTAssertEqual(s2, "bob 123- 456") 64 | } 65 | 66 | func testReplacementBuilder() throws { 67 | let r1 = /(\d+)/ 68 | let s1 = "bob123".replacingAllMatches(of: r1, with: { 69 | " " 70 | \.1 71 | }) 72 | XCTAssertEqual(s1, "bob 123") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Tests/ExtendedSwiftTests/StringTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 4/1/23. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | 11 | class StringTests: XCTestCase { 12 | 13 | func testHTMLDecoding() { 14 | XCTAssertEqual("ééééé".decodingHTMLEntities(), "ééééé") 15 | } 16 | 17 | func testHTMLEncoding() { 18 | XCTAssertEqual("é".encodingHTMLEntities(options: [.preferNamedEntities]), "é") 19 | XCTAssertEqual("é".encodingHTMLEntities(options: [.useHexEntities]), "é") 20 | XCTAssertEqual("é".encodingHTMLEntities(options: [.useHexEntities, .useUppercaseHex]), "é") 21 | XCTAssertEqual("é".encodingHTMLEntities(options: [.useHexEntities, .padNumericEntitiesToFourDigits]), "é") 22 | XCTAssertEqual("é".encodingHTMLEntities(options: [.useHexEntities, .useUppercaseHex, .padNumericEntitiesToFourDigits]), "é") 23 | XCTAssertEqual("é".encodingHTMLEntities(), "é") 24 | XCTAssertEqual("é".encodingHTMLEntities(options: [.padNumericEntitiesToFourDigits]), "é") 25 | } 26 | 27 | func testCommonPrefix() { 28 | var prefix = String.longestCommonPrefix(of: ["ABC", "ABD"]) 29 | XCTAssertEqual(prefix, "AB") 30 | 31 | prefix = String.longestCommonPrefix(of: ["A", "B"]) 32 | XCTAssertEqual(prefix, "") 33 | 34 | var suffix = String.longestCommonSuffix(of: ["CBA", "DBA"]) 35 | XCTAssertEqual(suffix, "BA") 36 | 37 | suffix = String.longestCommonSuffix(of: ["A", "B"]) 38 | XCTAssertEqual(suffix, "") 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Tests/HTTPTests/HTTP+TestHelpers.swift: -------------------------------------------------------------------------------- 1 | import HTTP 2 | 3 | extension HTTPRequest { 4 | 5 | static func build(using builder: (inout HTTPRequest) -> Void) -> HTTPRequest { 6 | var request = HTTPRequest() 7 | builder(&request) 8 | return request 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /Tests/HTTPTests/HTTPTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | final class HTTPTests: XCTestCase { 5 | func testExample() async throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | let chain = URLSessionLoader(configuration: .ephemeral) 10 | 11 | var r = HTTPRequest() 12 | r.host = "swapi.dev" 13 | r.path = "/api/people/1" 14 | 15 | let result = await chain.load(request: r) 16 | print(result) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/HTTPTests/ManualLoaderTests.swift: -------------------------------------------------------------------------------- 1 | import HTTP 2 | import XCTest 3 | 4 | class ManualLoaderTests: XCTestCase { 5 | 6 | var loader = ManualLoader() 7 | 8 | func testNoHandlers() async { 9 | let result = await loader.load(request: .init()) 10 | XCTAssertFailure(result) 11 | } 12 | 13 | func testDefaultHandler() async throws { 14 | let expectation = self.expectation(description: #function) 15 | 16 | await loader.setDefaultHandler({ req, token in 17 | expectation.fulfill() 18 | return .ok(req) 19 | }) 20 | 21 | let result = await loader.load(request: .init()) 22 | XCTAssertSuccess(result) 23 | try await allExpectations() 24 | } 25 | 26 | func testSingleHandler() async { 27 | await loader.setDefaultHandler({ req, token in 28 | XCTFail() 29 | return .failure(.cannotConnect, request: req) 30 | }) 31 | 32 | await loader.then { req, token in 33 | return .ok(req) 34 | } 35 | 36 | let result = await loader.load(request: .init()) 37 | if let response = XCTAssertSuccess(result) { 38 | XCTAssertEqual(response.status, .ok) 39 | } 40 | } 41 | 42 | func testMultipleHandlers() async { 43 | await loader.setDefaultHandler({ req, token in 44 | XCTFail() 45 | return .failure(.cannotConnect, request: req) 46 | }) 47 | 48 | await loader.then { req, _ in return .ok(req) } 49 | await loader.then { req, _ in return .internalServerError(req) } 50 | 51 | XCTAssertSuccess(await loader.load(request: .init())) 52 | 53 | if let response = XCTAssertSuccess(await loader.load(request: .init())) { 54 | XCTAssertEqual(response.status, .internalServerError) 55 | } 56 | } 57 | 58 | func testFallbackToDefaultHandler() async throws { 59 | let expectation = self.expectation(description: #function) 60 | 61 | await loader.setDefaultHandler({ req, _ in 62 | expectation.fulfill() 63 | return .internalServerError(req) 64 | }) 65 | 66 | await loader.then { req, _ in return .ok(req) } 67 | 68 | XCTAssertSuccess(await loader.load(request: .init())) 69 | 70 | if let response = XCTAssertSuccess(await loader.load(request: .init())) { 71 | XCTAssertEqual(response.status, .internalServerError) 72 | } 73 | 74 | try await allExpectations() 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /Tests/HTTPTests/RetryTests.swift: -------------------------------------------------------------------------------- 1 | import HTTP 2 | import XCTest 3 | 4 | class RetryTests: XCTestCase { 5 | 6 | var manual = ManualLoader() 7 | 8 | func testRequestWithoutStrategyDoesNotRetry() async throws { 9 | let chain = await RetryLoader() --> manual 10 | 11 | let r1 = HTTPRequest() 12 | let e1 = expectation(description: "r1") 13 | 14 | await manual.then { req, _ in e1.fulfill(); return .ok(req) } 15 | await manual.then { req, _ in 16 | XCTFail() 17 | return .failure(.cannotConnect, request: req) 18 | } 19 | 20 | let result = await chain.load(request: r1) 21 | 22 | XCTAssertSuccess(result) 23 | XCTAssertEqual(result.response?[header: .xRetryCount], "0") 24 | try await allExpectations() 25 | } 26 | 27 | func testCancelledRequestDoesNotRetry() async throws { 28 | let chain = await RetryLoader() --> manual 29 | 30 | let r1 = HTTPRequest.build { 31 | $0[option: \.retryStrategy] = BackoffRetry.immediately(maximumNumberOfAttempts: 3) 32 | } 33 | let e1 = expectation(description: "r1") 34 | 35 | await manual.then { req, _ in e1.fulfill(); return .failure(.cancelled, request: req) } 36 | await manual.then { req, _ in 37 | XCTFail() 38 | return .failure(.cannotConnect, request: req) 39 | } 40 | 41 | let result = await chain.load(request: r1) 42 | 43 | XCTAssertFailure(result) 44 | try await allExpectations() 45 | } 46 | 47 | func testBasicRetry() async throws { 48 | let chain = await RetryLoader() --> manual 49 | 50 | let r1 = HTTPRequest.build { 51 | $0[option: \.retryStrategy] = BackoffRetry.immediately(maximumNumberOfAttempts: 3) 52 | } 53 | let e1 = expectation(description: "r1") 54 | e1.expectedFulfillmentCount = 3 55 | 56 | await manual.then { req, _ in e1.fulfill(); return .failure(.cannotConnect, request: req) } 57 | await manual.then { req, _ in e1.fulfill(); return .failure(.cannotConnect, request: req) } 58 | await manual.then { req, _ in e1.fulfill(); return .ok(req) } 59 | await manual.then { req, _ in 60 | XCTFail() 61 | return .failure(.cannotConnect, request: req) 62 | } 63 | 64 | let result = await chain.load(request: r1) 65 | 66 | XCTAssertSuccess(result) 67 | XCTAssertEqual(result.response?[header: .xRetryCount], "2") 68 | try await allExpectations() 69 | } 70 | 71 | } 72 | --------------------------------------------------------------------------------