Skip to content

Commit

Permalink
Big refinement for 1.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Alkenso committed May 6, 2024
1 parent 5f058eb commit ceabfd2
Show file tree
Hide file tree
Showing 68 changed files with 1,165 additions and 1,621 deletions.
3 changes: 0 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ jobs:
strategy:
matrix:
include:
- xcode: "14.2" # Swift 5.7
macOS: "13"
iOS: "16.0"
- xcode: "15.0" # Swift 5.9
macOS: "13"
iOS: "17.0"
Expand Down
12 changes: 10 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.7
// swift-tools-version:5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
Expand All @@ -15,6 +15,10 @@ let package = Package(
name: "SpellbookHTTP",
targets: ["SpellbookHTTP"]
),
.library(
name: "SpellbookBinaryParsing",
targets: ["SpellbookBinaryParsing"]
),
.library(
name: "SpellbookTestUtils",
targets: ["SpellbookTestUtils"]
Expand All @@ -38,13 +42,17 @@ let package = Package(
name: "SpellbookHTTP",
dependencies: ["SpellbookFoundation"]
),
.target(
name: "SpellbookBinaryParsing",
dependencies: ["SpellbookFoundation"]
),
.target(
name: "SpellbookTestUtils",
dependencies: ["SpellbookFoundation"]
),
.testTarget(
name: "SpellbookTests",
dependencies: ["SpellbookFoundation", "SpellbookTestUtils"]
dependencies: ["SpellbookFoundation", "SpellbookBinaryParsing", "SpellbookTestUtils"]
),
.testTarget(
name: "SpellbookTestUtilsTests",
Expand Down
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
SwiftSpellbook is collection of additions to Swift standard library that makes development easier.

<p>
<img src="https://img.shields.io/badge/swift-5.7 | 5.8 | 5.9-orange" />
<img src="https://img.shields.io/badge/swift-5.9-orange" />
<img src="https://img.shields.io/badge/platforms-macOS 10.15 | iOS 13 | watchOS 6 | tvOS 13-freshgreen" />
<img src="https://img.shields.io/badge/Xcode-14 | 15-blue" />
<img src="https://img.shields.io/badge/Xcode-15-blue" />
<img src="https://github.com/Alkenso/SwiftSpellbook/actions/workflows/main.yml/badge.svg" />
</p>

Expand All @@ -16,16 +16,14 @@ Once I've decided stop to copy-paste code from project to project and make singl
At top level, the code is organized into libraries that cover big areas.
Now there are only two:
- SpellbookFoundation: utility code
- SpellbookBinaryParsing: convenient way to read and write binary data byte-by-byte
- SpellbookHTTP: HTTP client
- SpellbookTestUtils: utility code frequently used for Unit-Tests

The libraries/targets are organized as one level nested folders to distinguish between areas they are related to.

## SpellbookFoundation
The most of utility code lives here.
- BinaryParsing: read and write data buffers or files in raw binary format
- Combine: Combine.framework extensions
- Common:
- Common: Mix of commonly used entities
- DictionaryParsing: deal with data nested deeply in dictionaries
- Filesystem & Bundle: FileManager, Bundle and same utilities
- GUI: CoreGraphics utilities. This is NOT an AppKit/UIKit/SwiftUI
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
// SOFTWARE.

import Foundation
import SpellbookFoundation

public struct BinaryWriter {
private let _output: BinaryWriterOutput
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,9 @@ extension Publisher where Output: Equatable, Failure == Never {
return proxy.eraseToAnyPublisher()
}
}

extension Cancellable {
public func capturing(_ object: Any) -> AnyCancellable {
.init { withExtendedLifetime(object, self.cancel) }
}
}
6 changes: 5 additions & 1 deletion Sources/SpellbookFoundation/Common/Benchmark.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ public enum Benchmark {
/// Executes the given block and prints the `name` and the number of seconds
/// with nanosecond precision it takes to execute the block.
/// This function is for debugging and performance analysis work.
public static func measure<R>(_ name: String, execute: () throws -> R) rethrows -> R {
public static func measure<R>(
_ name: String,
print: (String) -> Void = { print($0) },
execute: () throws -> R
) rethrows -> R {
let (result, durationSec) = try measure(execute: execute)
print("\(name) takes \(durationSec) sec")

Expand Down
30 changes: 15 additions & 15 deletions Sources/SpellbookFoundation/Common/CancellationToken.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,39 +24,39 @@ import Combine
import Foundation

public class CancellationToken {
private let _queue: DispatchQueue
private let _onDeinit: Bool
private let _onCancel: () -> Void
private let queue: DispatchQueue
private let onDeinit: Bool
private let onCancel: () -> Void

@Atomic private var _cancelled = false
private var _children = Synchronized<[CancellationToken]>(.serial)
@Atomic private var cancelled = false
private var children = Synchronized<[CancellationToken]>(.serial)

public var isCancelled: Bool { _cancelled }
public var isCancelled: Bool { cancelled }

public init(on queue: DispatchQueue = .global(), onDeinit: Bool = false, cancel: @escaping () -> Void) {
_queue = queue
_onDeinit = onDeinit
_onCancel = cancel
self.queue = queue
self.onDeinit = onDeinit
self.onCancel = cancel
}

deinit {
if _onDeinit {
if onDeinit {
cancel()
}
}

public func cancel() {
guard !__cancelled.exchange(true) else { return }
_queue.async(execute: _onCancel)
_children.read().forEach { $0._queue.async(execute: $0.cancel) }
guard !$cancelled.exchange(true) else { return }
queue.async(execute: onCancel)
children.exchange([]).forEach { $0.queue.async(execute: $0.cancel) }
}

public func addChild(_ token: CancellationToken) {
_children.writeAsync {
children.write {
if !self.isCancelled {
$0.append(token)
} else {
token._queue.async(execute: token.cancel)
token.queue.async(execute: token.cancel)
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/SpellbookFoundation/Common/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

import Foundation

/// Different indicators related to Application environment.
/// Different indicators related to Application build environment.
public enum BuildEnvironment {
/// Runtime check if run in debug mode.
public static let isDebug: Bool = {
Expand All @@ -34,7 +34,7 @@ public enum BuildEnvironment {
}()
}

/// Different indicators related to Application environment.
/// Different indicators related to Application run environment.
public enum RunEnvironment {
/// Runtime check if run inside XCTest bundle.
public static let isXCTesting: Bool = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ extension NSException: NonSwiftException {
}

/// Error-like wrapper around C++ `std::exception` to make it Swift.Error compatible.
public struct StdException: Error {
public struct CppException: Error {
public var what: String

public init(what: String) {
Expand All @@ -60,13 +60,13 @@ public struct StdException: Error {
}
}

extension StdException: NonSwiftException {
extension CppException: NonSwiftException {
public static func evaluate(_ body: () -> Void) -> String? {
SpellbookObjC.cppException_catching(body)
}

public static func create(with nonSwiftError: String) -> Self {
StdException(what: nonSwiftError)
CppException(what: nonSwiftError)
}
}

Expand Down Expand Up @@ -105,7 +105,7 @@ extension NonSwiftException {
/// - Warning: Use it reasonably, catching code related to
/// Objective-C and C++ exceptions may affect the performance.
public func catchingAny<R>(_ body: () throws -> R) throws -> R {
try StdException.catchingAll {
try CppException.catchingAll {
try NSException.catchingAll(body)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,15 @@ extension Collection {
return mutated
}
}

public func firstMapped<T>(where transform: (Element) throws -> T?) rethrows -> T? {
for element in self {
if let mapped = try transform(element) {
return mapped
}
}
return nil
}
}

extension Collection {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ extension DateFormatter {
public convenience init(
_ dateFormat: String,
dateStyle: DateFormatter.Style? = nil,
timeStyle: DateFormatter.Style? = nil
timeStyle: DateFormatter.Style? = nil,
timeZone: TimeZone? = nil
) {
self.init()

self.dateFormat = dateFormat
dateStyle.flatMap { self.dateStyle = $0 }
timeStyle.flatMap { self.timeStyle = $0 }
timeZone.flatMap { self.timeZone = $0 }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,17 @@ extension Optional where Wrapped: Error {
}

extension Optional {
/// Sets the value to `defaultValue` only if current value is `nil`.
public mutating func coalesce(_ defaultValue: @autoclosure () throws -> Wrapped) rethrows -> Wrapped {
if let self {
return self
} else {
let value = try defaultValue()
self = value
return value
}
}

/// Provides convenient way of mutating optional values.
///
/// ```
Expand Down Expand Up @@ -442,20 +453,20 @@ extension Optional {
}
}

// MARK: - TimeInterval & Date
// MARK: - Date & Time

extension TimeInterval {
extension timespec {
/// Creates `TimeInverval` from `timespec` structure.
public init(ts: timespec) {
self = TimeInterval(ts.tv_sec) + (TimeInterval(ts.tv_nsec) / TimeInterval(NSEC_PER_SEC))
public var timeInterval: TimeInterval {
TimeInterval(tv_sec) + (TimeInterval(tv_nsec) / TimeInterval(NSEC_PER_SEC))
}
}

extension Date {
/// Creates `TimeInverval` from `timespec` structure.
/// - Note: Expected accuracy of resulting `Date` is ~100 nanoseconds.
public init(ts: timespec) {
self.init(timeIntervalSince1970: TimeInterval(ts: ts))
self.init(timeIntervalSince1970: ts.timeInterval)
}
}

Expand Down
2 changes: 0 additions & 2 deletions Sources/SpellbookFoundation/Common/SBUnit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ import Foundation
public protocol SBUnit: RawRepresentable where RawValue == Double {}

extension SBUnit {
// public static func convert(_ value: Double, to: Self) -> Double { value / to.rawValue }
// public static func restore(_ magnitude: Double, of: Self) -> Double { magnitude * of.rawValue }
/// Perform conversion between measurement units.
/// - Parameters:
/// - value: Unit magnitude.
Expand Down
27 changes: 16 additions & 11 deletions Sources/SpellbookFoundation/Common/SpellbookLog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ extension SpellbookLogSource: CustomStringConvertible {
public var description: String { "\(subsystem)/\(category)" }
}

public struct SpellbookLogDestination {
public var minLevel: SpellbookLogLevel
public var log: (SpellbookLogRecord) -> Void

public init(minLevel: SpellbookLogLevel = .info, log: @escaping (SpellbookLogRecord) -> Void) {
self.log = log
self.minLevel = minLevel
}
}

public final class SpellbookLogger {
private let queue: DispatchQueue?

Expand All @@ -128,9 +138,7 @@ public final class SpellbookLogger {

public var source = SpellbookLogSource.default()

public var destinations: [(SpellbookLogRecord) -> Void] = []

public var minLevel: SpellbookLogLevel = .info
public var destinations: [SpellbookLogDestination] = []

/// If log messages with `assert = true` should really produce asserts.
public var isAssertsEnabled = true
Expand Down Expand Up @@ -172,22 +180,19 @@ extension SpellbookLogger: SpellbookLog {
source: SpellbookLogSource, level: SpellbookLogLevel, message: @autoclosure () -> Any, assert: Bool,
file: StaticString, function: StaticString, line: Int
) {
guard level >= minLevel else { return }

if assert && isAssertsEnabled {
assertionFailure("\(message())", file: file, line: UInt(line))
}

let destinations = destinations.filter { level >= $0.minLevel }
guard !destinations.isEmpty else { return }

let record = SpellbookLogRecord(
source: source, level: level, message: message(),
file: file, function: function, line: line
)
if let queue {
queue.async {
self.destinations.forEach { $0(record) }
}
} else {
self.destinations.forEach { $0(record) }
queue.async {
destinations.forEach { $0.log(record) }
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions Sources/SpellbookFoundation/Common/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ public func updateSwap<T>(_ a: inout T, _ b: T) -> T {
a = b
return copy
}

public func throwingCast<T>(name: String? = nil, _ object: Any, to: T.Type) throws -> T {
try (object as? T).get(CommonError.cast(name: name, object, to: to))
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@

import Foundation

public protocol ObjectBuilder {}
public protocol ValueBuilder {}

extension ObjectBuilder {
extension ValueBuilder {
public func set<T>(_ keyPath: WritableKeyPath<Self, T>, _ value: T?) -> Self {
guard let value = value else { return self }
var copy = self
Expand Down
Loading

0 comments on commit ceabfd2

Please sign in to comment.