Add CI/CD configuration and API documentation

This commit is contained in:
2026-07-01 21:40:53 +08:00
commit c590135d68
4168 changed files with 740252 additions and 0 deletions
@@ -0,0 +1,19 @@
/// A case-preserving, case-insensitive identifier
/// that matches the ASCII version of sqlite3_stricmp
struct CaseInsensitiveIdentifier: Hashable {
private let lowercased: String
let rawValue: String
init(rawValue: String) {
self.lowercased = rawValue.lowercased()
self.rawValue = rawValue
}
static func == (lhs: CaseInsensitiveIdentifier, rhs: CaseInsensitiveIdentifier) -> Bool {
lhs.lowercased == rhs.lowercased
}
func hash(into hasher: inout Hasher) {
hasher.combine(lowercased)
}
}
@@ -0,0 +1,138 @@
// Copyright (C) 2015-2023 Gwendal Roué
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// =============================================================================
//
// Copyright (c) 2005-2019 David Heinemeier Hansson
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
extension Inflections {
/// The default inflections
public static var `default`: Inflections = {
// Defines the standard inflection rules. These are the starting point
// for new projects and are not considered complete. The current set of
// inflection rules is frozen. This means, we do not change them to
// become more complete. This is a safety measure to keep existing
// applications from breaking.
//
// https://github.com/rails/rails/blob/b2eb1d1c55a59fee1e6c4cba7030d8ceb524267c/activesupport/lib/active_support/inflections.rb
var inflections = Inflections()
inflections.plural("$", "s")
inflections.plural("s$", "s")
inflections.plural("^(ax|test)is$", "$1es")
inflections.plural("(octop|vir)us$", "$1i")
inflections.plural("(octop|vir)i$", "$1i")
inflections.plural("(alias|status)$", "$1es")
inflections.plural("(bu)s$", "$1ses")
inflections.plural("(buffal|tomat|her)o$", "$1oes")
inflections.plural("([ti])um$", "$1a")
inflections.plural("([ti])a$", "$1a")
inflections.plural("sis$", "ses")
inflections.plural("(?:([^f])fe|([lr])f)$", "$1$2ves")
inflections.plural("(hive)$", "$1s")
inflections.plural("([^aeiouy]|qu)y$", "$1ies")
inflections.plural("(x|ch|ss|sh)$", "$1es")
inflections.plural("(matr|vert|ind)(?:ix|ex)$", "$1ices")
inflections.plural("^(m|l)ouse$", "$1ice")
inflections.plural("^(m|l)ice$", "$1ice")
inflections.plural("^(ox)$", "$1en")
inflections.plural("^(oxen)$", "$1")
inflections.plural("(quiz)$", "$1zes")
inflections.plural("(canva)s$", "$1ses")
inflections.singular("s$", "")
inflections.singular("(ss)$", "$1")
inflections.singular("(n)ews$", "$1ews")
inflections.singular("([ti])a$", "$1um")
inflections.singular("((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$", "$1sis")
inflections.singular("(^analy)(sis|ses)$", "$1sis")
inflections.singular("([^f])ves$", "$1fe")
inflections.singular("(hive)s$", "$1")
inflections.singular("(tive)s$", "$1")
inflections.singular("([lr])ves$", "$1f")
inflections.singular("([^aeiouy]|qu)ies$", "$1y")
inflections.singular("(s)eries$", "$1eries")
inflections.singular("(m)ovies$", "$1ovie")
inflections.singular("(x|ch|ss|sh)es$", "$1")
inflections.singular("^(m|l)ice$", "$1ouse")
inflections.singular("(bus)(es)?$", "$1")
inflections.singular("(o)es$", "$1")
inflections.singular("(shoe)s$", "$1")
inflections.singular("(cris|test)(is|es)$", "$1is")
inflections.singular("^(a)x[ie]s$", "$1xis")
inflections.singular("(octop|vir)(us|i)$", "$1us")
inflections.singular("(alias|status)(es)?$", "$1")
inflections.singular("^(ox)en$", "$1")
inflections.singular("(vert|ind)ices$", "$1ex")
inflections.singular("(matr)ices$", "$1ix")
inflections.singular("(quiz)zes$", "$1")
inflections.singular("(database)s$", "$1")
inflections.singular("(canvas)(es)?$", "$1")
inflections.uncountableWords([
"advice",
"corps",
"dice",
"equipment",
"fish",
"information",
"jeans",
"kudos",
"money",
"offspring",
"police",
"rice",
"sheep",
"species",
])
inflections.irregularSuffix("child", "children")
inflections.irregularSuffix("foot", "feet")
inflections.irregularSuffix("leaf", "leaves")
inflections.irregularSuffix("man", "men")
inflections.irregularSuffix("move", "moves")
inflections.irregularSuffix("person", "people")
inflections.irregularSuffix("sex", "sexes")
inflections.irregularSuffix("specimen", "specimens")
inflections.irregularSuffix("zombie", "zombies")
return inflections
}()
}
@@ -0,0 +1,247 @@
import Foundation
extension String {
/// "player" -> "Player"
var uppercasingFirstCharacter: String {
guard let first else {
return self
}
return String(first).uppercased() + dropFirst()
}
/// "player" -> "players"
/// "players" -> "players"
var pluralized: String {
Inflections.default.pluralize(self)
}
/// "player" -> "player"
/// "players" -> "player"
var singularized: String {
Inflections.default.singularize(self)
}
/// "bar" -> "bar"
/// "foo12" -> "foo"
var digitlessRadical: String {
String(prefix(upTo: Inflections.endIndexOfDigitlessRadical(self)))
}
}
/// A type that controls GRDB string inflections.
///
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
public struct Inflections: Sendable {
private var pluralizeRules: [(NSRegularExpression, String)] = []
private var singularizeRules: [(NSRegularExpression, String)] = []
private var uncountablesRegularExpressions: [String: NSRegularExpression] = [:]
// For testability
var uncountables: Set<String> {
Set(uncountablesRegularExpressions.keys)
}
// MARK: - Initialization
public init() {
}
// MARK: - Configuration
/// Appends a pluralization rule.
///
/// var inflections = Inflections()
/// inflections.plural("$", "s")
/// inflections.pluralize("player") // "players"
///
/// - parameters:
/// - pattern: A regular expression pattern.
/// - options: Regular expression options (defaults to
/// `[.caseInsensitive]`).
/// - template: A replacement template string.
public mutating func plural(
_ pattern: String,
options: NSRegularExpression.Options = [.caseInsensitive],
_ template: String)
{
let reg = try! NSRegularExpression(pattern: pattern, options: options)
pluralizeRules.append((reg, template))
}
/// Appends a singularization rule.
///
/// var inflections = Inflections()
/// inflections.singular("s$", "")
/// inflections.singularize("players") // "player"
///
/// - parameters:
/// - pattern: A regular expression pattern.
/// - options: Regular expression options (defaults to
/// `[.caseInsensitive]`).
/// - template: A replacement template string.
public mutating func singular(
_ pattern: String,
options: NSRegularExpression.Options = [.caseInsensitive],
_ template: String)
{
let reg = try! NSRegularExpression(pattern: pattern, options: options)
singularizeRules.append((reg, template))
}
/// Appends uncountable words.
///
/// var inflections = Inflections()
/// inflections.plural("$", "s")
/// inflections.uncountableWords(["foo"])
/// inflections.pluralize("foo") // "foo"
/// inflections.pluralize("bar") // "bars"
public mutating func uncountableWords(_ words: [String]) {
for word in words {
uncountableWord(word)
}
}
/// Appends an irregular singular/plural pair.
///
/// var inflections = Inflections()
/// inflections.plural("$", "s")
/// inflections.irregularSuffix("man", "men")
/// inflections.pluralize("man") // "men"
/// inflections.singularizes("women") // "woman"
///
/// - parameters:
/// - singular: The singular form.
/// - plural: The plural form.
public mutating func irregularSuffix(_ singular: String, _ plural: String) {
let s0 = singular.first!
let srest = singular.dropFirst()
let p0 = plural.first!
let prest = plural.dropFirst()
if s0.uppercased() == p0.uppercased() {
self.plural("(\(s0))\(srest)$", options: [.caseInsensitive], "$1\(prest)")
self.plural("(\(p0))\(prest)$", options: [.caseInsensitive], "$1\(prest)")
self.singular("(\(s0))\(srest)$", options: [.caseInsensitive], "$1\(srest)")
self.singular("(\(p0))\(prest)$", options: [.caseInsensitive], "$1\(srest)")
} else {
self.plural("\(s0.uppercased())(?i)\(srest)$", options: [], p0.uppercased() + prest)
self.plural("\(s0.lowercased())(?i)\(srest)$", options: [], p0.lowercased() + prest)
self.plural("\(p0.uppercased())(?i)\(prest)$", options: [], p0.uppercased() + prest)
self.plural("\(p0.lowercased())(?i)\(prest)$", options: [], p0.lowercased() + prest)
self.singular("\(s0.uppercased())(?i)\(srest)$", options: [], s0.uppercased() + srest)
self.singular("\(s0.lowercased())(?i)\(srest)$", options: [], s0.lowercased() + srest)
self.singular("\(p0.uppercased())(?i)\(prest)$", options: [], s0.uppercased() + srest)
self.singular("\(p0.lowercased())(?i)\(prest)$", options: [], s0.lowercased() + srest)
}
}
// MARK: - Inflections
/// Returns a pluralized string.
///
/// Inflections.default.pluralize("player") // "players"
public func pluralize(_ string: String) -> String {
inflectString(string, with: pluralizeRules)
}
/// Returns a singularized string.
public func singularize(_ string: String) -> String {
inflectString(string, with: singularizeRules)
}
// MARK: - Utils
/// Appends an uncountable word.
///
/// var inflections = Inflections()
/// inflections.plural("$", "s")
/// inflections.uncountableWord("foo")
/// inflections.pluralize("foo") // "foo"
/// inflections.pluralize("bar") // "bars"
private mutating func uncountableWord(_ word: String) {
let escWord = NSRegularExpression.escapedPattern(for: word)
uncountablesRegularExpressions[word] = try! NSRegularExpression(
pattern: "\\b\(escWord)\\Z",
options: [.caseInsensitive])
}
private func isUncountable(_ string: String) -> Bool {
let range = NSRange(location: 0, length: string.utf16.count)
for (_, reg) in uncountablesRegularExpressions {
if reg.firstMatch(in: string, options: [], range: range) != nil {
return true
}
}
return false
}
private func inflectString(_ string: String, with rules: [(NSRegularExpression, String)]) -> String {
let indexOfLastWord = Inflections.startIndexOfLastWord(string)
let endIndexOfDigitlessRadical = Inflections.endIndexOfDigitlessRadical(string)
let lastWord = String(string[indexOfLastWord..<endIndexOfDigitlessRadical])
if isUncountable(lastWord) {
return string
}
return """
\(string.prefix(upTo: indexOfLastWord))\
\(inflectWord(lastWord, with: rules))\
\(string.suffix(from: endIndexOfDigitlessRadical))
"""
}
private func inflectWord(_ string: String, with rules: [(NSRegularExpression, String)]) -> String {
if string.isEmpty {
return string
}
let range = NSRange(string.startIndex..<string.endIndex, in: string)
for (reg, template) in rules.reversed() {
let result = NSMutableString(string: string)
let matchCount = reg.replaceMatches(in: result, options: [], range: range, withTemplate: template)
if matchCount > 0 {
return String(result)
}
}
return string
}
/// startIndexOfLastWord("foo") -> "foo"
/// startIndexOfLastWord("foo bar") -> "bar"
/// startIndexOfLastWord("foo_bar") -> "bar"
/// startIndexOfLastWord("fooBar") -> "Bar"
static func startIndexOfLastWord(_ string: String) -> String.Index {
let range = NSRange(string.startIndex..<string.endIndex, in: string)
let index1: String.Index? = wordBoundaryReg.firstMatch(in: string, options: [], range: range).flatMap {
if $0.range.location == NSNotFound { return nil }
return Range($0.range, in: string)?.lowerBound
}
let index2: String.Index? = underscoreBoundaryReg.firstMatch(in: string, options: [], range: range).flatMap {
if $0.range.location == NSNotFound { return nil }
return Range($0.range, in: string).map { string.index(after: $0.lowerBound) }
}
let index3: String.Index? = caseBoundaryReg.firstMatch(in: string, options: [], range: range).flatMap {
if $0.range.location == NSNotFound { return nil }
return Range($0.range, in: string).map { string.index(after: $0.lowerBound) }
}
return [index1, index2, index3].compactMap { $0 }.max() ?? string.startIndex
}
/// "bar" -> "bar"
/// "foo12" -> "foo"
static func endIndexOfDigitlessRadical(_ string: String) -> String.Index {
let digits: ClosedRange<Character> = "0"..."9"
return string // "foo12"
.reversed() // "21oof"
.prefix(while: { digits.contains($0) }) // "21"
.endIndex // reversed(foo^12)
.base // foo^12
}
private static let wordBoundaryReg = try! NSRegularExpression(pattern: "\\b\\w+$", options: [])
private static let underscoreBoundaryReg = try! NSRegularExpression(pattern: "_[^_]+$", options: [])
private static let caseBoundaryReg = try! NSRegularExpression(pattern: "[^A-Z][A-Z]+[a-z1-9]+$", options: [])
}
@@ -0,0 +1,72 @@
import Foundation
/// A LockedBox protects a value with an NSLock.
@propertyWrapper
final class LockedBox<T> {
private var _wrappedValue: T
private var lock = NSLock()
var wrappedValue: T {
get { read { $0 } }
set { update { $0 = newValue } }
}
var projectedValue: LockedBox<T> { self }
init(wrappedValue: T) {
_wrappedValue = wrappedValue
}
/// Runs the provided closure while holding a lock on the value.
///
/// For example:
///
/// // Prints "0"
/// @LockedBox var count = 0
/// $count.read { print($0) }
///
/// - parameter block: A closure that accepts the value.
@inline(__always)
@usableFromInline
func read<U>(_ block: (T) throws -> U) rethrows -> U {
lock.lock()
defer { lock.unlock() }
return try block(_wrappedValue)
}
/// Runs the provided closure while holding a lock on the value.
///
/// For example:
///
/// // Prints "1"
/// @LockedBox var count = 0
/// $count.update { $0 += 1 }
/// print(count)
///
/// - parameter block: A closure that can modify the value.
func update<U>(_ block: (inout T) throws -> U) rethrows -> U {
lock.lock()
defer { lock.unlock() }
return try block(&_wrappedValue)
}
}
extension LockedBox where T: Numeric {
@discardableResult
func increment() -> T {
update { n in
n += 1
return n
}
}
@discardableResult
func decrement() -> T {
update { n in
n -= 1
return n
}
}
}
extension LockedBox: @unchecked Sendable where T: Sendable { }
@@ -0,0 +1,116 @@
#if canImport(Combine)
import Combine
import Foundation
/// A publisher that eventually produces one value and then finishes or fails.
///
/// Like a Combine.Future wrapped in Combine.Deferred, OnDemandFuture starts
/// producing its value on demand.
///
/// Unlike Combine.Future wrapped in Combine.Deferred, OnDemandFuture guarantees
/// that it starts producing its value on demand, **synchronously**, and that
/// it produces its value on promise completion, **synchronously**.
///
/// Both two extra scheduling guarantees are used by GRDB in order to be
/// able to spawn concurrent database reads right from the database writer
/// queue, and fulfill GRDB preconditions.
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
struct OnDemandFuture<Output, Failure: Error>: Publisher {
typealias Promise = (Result<Output, Failure>) -> Void
typealias Output = Output
typealias Failure = Failure
fileprivate let attemptToFulfill: (@escaping Promise) -> Void
init(_ attemptToFulfill: @escaping (@escaping Promise) -> Void) {
self.attemptToFulfill = attemptToFulfill
}
func receive<S>(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input {
let subscription = OnDemandFutureSubscription(
attemptToFulfill: attemptToFulfill,
downstream: subscriber)
subscriber.receive(subscription: subscription)
}
}
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
private class OnDemandFutureSubscription<Downstream: Subscriber>: Subscription {
typealias Promise = (Result<Downstream.Input, Downstream.Failure>) -> Void
private enum State {
case waitingForDemand(downstream: Downstream, attemptToFulfill: (@escaping Promise) -> Void)
case waitingForFulfillment(downstream: Downstream)
case finished
}
private var state: State
private let lock = NSRecursiveLock() // Allow re-entrancy
init(
attemptToFulfill: @escaping (@escaping Promise) -> Void,
downstream: Downstream)
{
self.state = .waitingForDemand(downstream: downstream, attemptToFulfill: attemptToFulfill)
}
func request(_ demand: Subscribers.Demand) {
lock.synchronized {
switch state {
case let .waitingForDemand(downstream: downstream, attemptToFulfill: attemptToFulfill):
guard demand > 0 else {
return
}
state = .waitingForFulfillment(downstream: downstream)
attemptToFulfill { result in
switch result {
case let .success(value):
self.receive(value)
case let .failure(error):
self.receive(completion: .failure(error))
}
}
case .waitingForFulfillment, .finished:
break
}
}
}
func cancel() {
lock.synchronized {
state = .finished
}
}
private func receive(_ value: Downstream.Input) {
lock.synchronized { sideEffect in
switch state {
case let .waitingForFulfillment(downstream: downstream):
state = .finished
sideEffect = {
_ = downstream.receive(value)
downstream.receive(completion: .finished)
}
case .waitingForDemand, .finished:
break
}
}
}
private func receive(completion: Subscribers.Completion<Downstream.Failure>) {
lock.synchronized { sideEffect in
switch state {
case let .waitingForFulfillment(downstream: downstream):
state = .finished
sideEffect = {
downstream.receive(completion: completion)
}
case .waitingForDemand, .finished:
break
}
}
}
}
#endif
@@ -0,0 +1,214 @@
/// A dictionary with guaranteed keys ordering.
///
/// var dict = OrderedDictionary<String, Int>()
/// dict.append(1, forKey: "foo")
/// dict.append(2, forKey: "bar")
///
/// dict["foo"] // 1
/// dict["bar"] // 2
/// dict["qux"] // nil
/// dict.map { $0.key } // ["foo", "bar"], in this order.
struct OrderedDictionary<Key: Hashable, Value> {
private(set) var keys: [Key]
private(set) var dictionary: [Key: Value]
var values: [Value] { keys.map { dictionary[$0]! } }
private init(keys: [Key], dictionary: [Key: Value]) {
assert(Set(keys) == Set(dictionary.keys))
self.keys = keys
self.dictionary = dictionary
}
/// Creates an empty ordered dictionary.
init() {
keys = []
dictionary = [:]
}
/// Creates an empty ordered dictionary.
init(minimumCapacity: Int) {
keys = []
keys.reserveCapacity(minimumCapacity)
dictionary = Dictionary(minimumCapacity: minimumCapacity)
}
/// Returns the value associated with key, or nil.
subscript(_ key: Key) -> Value? {
get { dictionary[key] }
set {
if let value = newValue {
updateValue(value, forKey: key)
} else {
removeValue(forKey: key)
}
}
}
/// Returns the value associated with key, or the default value.
subscript(_ key: Key, default defaultValue: Value) -> Value {
get { dictionary[key] ?? defaultValue }
set { self[key] = newValue }
}
/// Appends the given value for the given key.
///
/// - precondition: There is no value associated with key yet.
mutating func appendValue(_ value: Value, forKey key: Key) {
guard updateValue(value, forKey: key) == nil else {
fatalError("key is already defined")
}
}
/// Updates the value stored in the dictionary for the given key, or
/// appnds a new key-value pair if the key does not exist.
///
/// Use this method instead of key-based subscripting when you need to know
/// whether the new value supplants the value of an existing key. If the
/// value of an existing key is updated, updateValue(_:forKey:) returns the
/// original value. If the given key is not present in the dictionary, this
/// method appends the key-value pair and returns nil.
@discardableResult
mutating func updateValue(_ value: Value, forKey key: Key) -> Value? {
if let oldValue = dictionary.updateValue(value, forKey: key) {
return oldValue
}
keys.append(key)
return nil
}
/// Removes the value associated with key.
@discardableResult
mutating func removeValue(forKey key: Key) -> Value? {
guard let value = dictionary.removeValue(forKey: key) else {
return nil
}
let index = keys.firstIndex { $0 == key }!
keys.remove(at: index)
return value
}
/// Returns a new ordered dictionary containing the keys of this dictionary
/// with the values transformed by the given closure.
func mapValues<T>(_ transform: (Value) throws -> T) rethrows -> OrderedDictionary<Key, T> {
try reduce(into: .init()) { dict, pair in
let value = try transform(pair.value)
dict.appendValue(value, forKey: pair.key)
}
}
/// Returns a new ordered dictionary containing only the key-value pairs
/// that have non-nil values as the result of transformation by the
/// given closure.
func compactMapValues<T>(_ transform: (Value) throws -> T?) rethrows -> OrderedDictionary<Key, T> {
try reduce(into: .init()) { dict, pair in
if let value = try transform(pair.value) {
dict.appendValue(value, forKey: pair.key)
}
}
}
func filter(_ isIncluded: ((key: Key, value: Value)) throws -> Bool) rethrows -> OrderedDictionary<Key, Value> {
let dictionary = try self.dictionary.filter(isIncluded)
let keys = self.keys.filter(dictionary.keys.contains)
return OrderedDictionary(keys: keys, dictionary: dictionary)
}
mutating func merge<S>(
_ other: S,
uniquingKeysWith combine: (Value, Value) throws -> Value)
rethrows
where S: Sequence, S.Element == (Key, Value)
{
for (key, value) in other {
if let current = self[key] {
self[key] = try combine(current, value)
} else {
self[key] = value
}
}
}
mutating func merge<S>(
_ other: S,
uniquingKeysWith combine: (Value, Value) throws -> Value)
rethrows
where S: Sequence, S.Element == (key: Key, value: Value)
{
for (key, value) in other {
if let current = self[key] {
self[key] = try combine(current, value)
} else {
self[key] = value
}
}
}
func merging<S>(
_ other: S,
uniquingKeysWith combine: (Value, Value) throws -> Value)
rethrows -> OrderedDictionary<Key, Value>
where S: Sequence, S.Element == (Key, Value)
{
var result = self
try result.merge(other, uniquingKeysWith: combine)
return result
}
func merging<S>(
_ other: S,
uniquingKeysWith combine: (Value, Value) throws -> Value)
rethrows -> OrderedDictionary<Key, Value>
where S: Sequence, S.Element == (key: Key, value: Value)
{
var result = self
try result.merge(other, uniquingKeysWith: combine)
return result
}
}
extension OrderedDictionary: Collection {
typealias Index = Int
var startIndex: Int { 0 }
var endIndex: Int { keys.count }
func index(after i: Int) -> Int { i + 1 }
subscript(position: Int) -> (key: Key, value: Value) {
let key = keys[position]
return (key: key, value: dictionary[key]!)
}
}
extension OrderedDictionary: ExpressibleByDictionaryLiteral {
init(dictionaryLiteral elements: (Key, Value)...) {
self.keys = elements.map { $0.0 }
self.dictionary = Dictionary(uniqueKeysWithValues: elements)
}
}
extension OrderedDictionary: Equatable where Value: Equatable {
static func == (lhs: OrderedDictionary, rhs: OrderedDictionary) -> Bool {
(lhs.keys == rhs.keys) && (lhs.dictionary == rhs.dictionary)
}
}
extension OrderedDictionary: CustomStringConvertible {
var description: String {
let chunks = map { (key, value) in
"\(String(reflecting: key)): \(String(reflecting: value))"
}
if chunks.isEmpty {
return "[:]"
} else {
return "[\(chunks.joined(separator: ", "))]"
}
}
}
extension Dictionary {
init(_ orderedDictionary: OrderedDictionary<Key, Value>) {
self = orderedDictionary.dictionary
}
}
@@ -0,0 +1,188 @@
import Dispatch
/// A Pool maintains a set of elements that are built them on demand. A pool has
/// a maximum number of elements.
///
/// // A pool of 3 integers
/// var number = 0
/// let pool = Pool<Int>(maximumCount: 3, makeElement: {
/// number = number + 1
/// return number
/// })
///
/// The function get() dequeues an available element and gives this element to
/// the block argument. During the block execution, the element is not
/// available. When the block is ended, the element is available again.
///
/// // got 1
/// pool.get { n in
/// print("got \(n)")
/// }
///
/// If there is no available element, the pool builds a new element, unless the
/// maximum number of elements is reached. In this case, the get() method
/// blocks the current thread, until an element eventually turns available again.
///
/// DispatchQueue.concurrentPerform(iterations: 6) { _ in
/// pool.get { n in
/// print("got \(n)")
/// }
/// }
///
/// got 1
/// got 2
/// got 3
/// got 2
/// got 1
/// got 3
final class Pool<T> {
private class Item {
let element: T
var isAvailable: Bool
init(element: T, isAvailable: Bool) {
self.element = element
self.isAvailable = isAvailable
}
}
private let makeElement: () throws -> T
@ReadWriteBox private var items: [Item] = []
private let itemsSemaphore: DispatchSemaphore // limits the number of elements
private let itemsGroup: DispatchGroup // knows when no element is used
private let barrierQueue: DispatchQueue
private let semaphoreWaitingQueue: DispatchQueue // Inspired by https://khanlou.com/2016/04/the-GCD-handbook/
/// Creates a Pool.
///
/// - parameters:
/// - maximumCount: The maximum number of elements.
/// - qos: The quality of service of asynchronous accesses.
/// - makeElement: A function that creates an element. It is called
/// on demand.
init(
maximumCount: Int,
qos: DispatchQoS = .unspecified,
makeElement: @escaping () throws -> T)
{
GRDBPrecondition(maximumCount > 0, "Pool size must be at least 1")
self.makeElement = makeElement
self.itemsSemaphore = DispatchSemaphore(value: maximumCount)
self.itemsGroup = DispatchGroup()
self.barrierQueue = DispatchQueue(label: "GRDB.Pool.barrier", qos: qos, attributes: [.concurrent])
self.semaphoreWaitingQueue = DispatchQueue(label: "GRDB.Pool.wait", qos: qos)
}
/// Returns a tuple (element, release)
/// Client must call release(), only once, after the element has been used.
func get() throws -> (element: T, release: (PoolCompletion) -> Void) {
try barrierQueue.sync {
itemsSemaphore.wait()
itemsGroup.enter()
do {
let item = try $items.update { items -> Item in
if let item = items.first(where: \.isAvailable) {
item.isAvailable = false
return item
} else {
let element = try makeElement()
let item = Item(element: element, isAvailable: false)
items.append(item)
return item
}
}
return (element: item.element, release: { self.release(item, completion: $0) })
} catch {
itemsSemaphore.signal()
itemsGroup.leave()
throw error
}
}
}
/// Eventually produces a tuple (element, release), where element is
/// intended to be used asynchronously.
///
/// Client must call release(), only once, after the element has been used.
///
/// - important: The `execute` argument is executed in a serial dispatch
/// queue, so make sure you use the element asynchronously.
func asyncGet(_ execute: @escaping (Result<(element: T, release: (PoolCompletion) -> Void), Error>) -> Void) {
// Inspired by https://khanlou.com/2016/04/the-GCD-handbook/
// > We wait on the semaphore in the serial queue, which means that
// > well have at most one blocked thread when we reach maximum
// > executing blocks on the concurrent queue. Any other tasks the user
// > enqueues will sit inertly on the serial queue waiting to be
// > executed, and wont cause new threads to be started.
semaphoreWaitingQueue.async {
execute(Result { try self.get() })
}
}
/// Performs a synchronous block with an element. The element turns
/// available after the block has executed.
func get<U>(block: (T) throws -> U) throws -> U {
let (element, completion) = try get()
defer { completion(.reuse) }
return try block(element)
}
private func release(_ item: Item, completion: PoolCompletion) {
$items.update { items in
switch completion {
case .reuse:
// This is why Item is a class, not a struct: so that we can
// release it without having to find in it the items array.
item.isAvailable = true
case .discard:
// Discard should be rare: perform lookup.
if let index = items.firstIndex(where: { $0 === item }) {
items.remove(at: index)
}
}
}
itemsSemaphore.signal()
itemsGroup.leave()
}
/// Performs a block on each pool element, available or not.
/// The block is run is some arbitrary dispatch queue.
func forEach(_ body: (T) throws -> Void) rethrows {
try $items.read { items in
for item in items {
try body(item.element)
}
}
}
/// Removes all elements from the pool.
/// Currently used elements won't be reused.
func removeAll() {
items = []
}
/// Blocks until no element is used, and runs the `barrier` function before
/// any other element is dequeued.
func barrier<R>(execute barrier: () throws -> R) rethrows -> R {
try barrierQueue.sync(flags: [.barrier]) {
itemsGroup.wait()
return try barrier()
}
}
/// Asynchronously runs the `barrier` function when no element is used, and
/// before any other element is dequeued.
func asyncBarrier(execute barrier: @escaping () -> Void) {
barrierQueue.async(flags: [.barrier]) {
self.itemsGroup.wait()
barrier()
}
}
}
enum PoolCompletion {
// Reuse the element
case reuse
// Discard the element
case discard
}
@@ -0,0 +1,51 @@
import Dispatch
/// A ReadWriteBox grants multiple readers and single-writer guarantees on a
/// value. It is backed by a concurrent DispatchQueue.
@propertyWrapper
final class ReadWriteBox<T> {
private var _wrappedValue: T
private var queue: DispatchQueue
var wrappedValue: T {
get { read { $0 } }
set { update { $0 = newValue } }
}
var projectedValue: ReadWriteBox<T> { self }
init(wrappedValue: T) {
_wrappedValue = wrappedValue
queue = DispatchQueue(label: "GRDB.ReadWriteBox", attributes: [.concurrent])
}
func read<U>(_ block: (T) throws -> U) rethrows -> U {
try queue.sync {
try block(_wrappedValue)
}
}
func update<U>(_ block: (inout T) throws -> U) rethrows -> U {
try queue.sync(flags: [.barrier]) {
try block(&_wrappedValue)
}
}
}
extension ReadWriteBox where T: Numeric {
@discardableResult
func increment() -> T {
update { n in
n += 1
return n
}
}
@discardableResult
func decrement() -> T {
update { n in
n -= 1
return n
}
}
}
@@ -0,0 +1,224 @@
#if canImport(Combine)
import Combine
import Foundation
/// A publisher that delivers values to its downstream subscriber on a
/// specific scheduler.
///
/// Unlike Combine's Publishers.ReceiveOn, ReceiveValuesOn only re-schedule
/// values and completion. It does not re-schedule subscription.
///
/// This scheduling guarantee is used by GRDB in order to be able
/// to make promises on the scheduling of database values without surprising
/// the users as in <https://forums.swift.org/t/28631>.
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
struct ReceiveValuesOn<Upstream: Publisher, Context: Scheduler>: Publisher {
typealias Output = Upstream.Output
typealias Failure = Upstream.Failure
fileprivate let upstream: Upstream
fileprivate let context: Context
fileprivate let options: Context.SchedulerOptions?
func receive<S>(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input {
let subscription = ReceiveValuesOnSubscription(
upstream: upstream,
context: context,
options: options,
downstream: subscriber)
subscriber.receive(subscription: subscription)
}
}
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
private class ReceiveValuesOnSubscription<Upstream, Context, Downstream>: Subscription, Subscriber
where
Upstream: Publisher,
Context: Scheduler,
Downstream: Subscriber,
Upstream.Failure == Downstream.Failure,
Upstream.Output == Downstream.Input
{
private struct Target {
let context: Context
let options: Context.SchedulerOptions?
let downstream: Downstream
}
private enum State {
case waitingForRequest(Upstream, Target)
case waitingForSubscription(Target, Subscribers.Demand)
case subscribed(Target, Subscription)
case finished
}
private var state: State
private let lock = NSRecursiveLock()
init(
upstream: Upstream,
context: Context,
options: Context.SchedulerOptions?,
downstream: Downstream)
{
let target = Target(context: context, options: options, downstream: downstream)
self.state = .waitingForRequest(upstream, target)
}
// MARK: Subscription
func request(_ demand: Subscribers.Demand) {
lock.synchronized { sideEffect in
switch state {
case let .waitingForRequest(upstream, target):
state = .waitingForSubscription(target, demand)
sideEffect = {
upstream.receive(subscriber: self)
}
case let .waitingForSubscription(target, currentDemand):
state = .waitingForSubscription(target, demand + currentDemand)
case let .subscribed(_, subcription):
sideEffect = {
subcription.request(demand)
}
case .finished:
break
}
}
}
func cancel() {
lock.synchronized { sideEffect in
switch state {
case .waitingForRequest, .waitingForSubscription:
state = .finished
case let .subscribed(_, subcription):
state = .finished
sideEffect = {
subcription.cancel()
}
case .finished:
break
}
}
}
// MARK: Subscriber
func receive(subscription: Subscription) {
lock.synchronized { sideEffect in
switch state {
case let .waitingForSubscription(target, currentDemand):
state = .subscribed(target, subscription)
sideEffect = {
subscription.request(currentDemand)
}
case .waitingForRequest, .subscribed:
preconditionFailure()
case .finished:
// We receive the upstream subscription requested by
// `upstream.receive(subscriber: self)` above.
//
// But self has been cancelled since, so let's cancel this
// upstream subscription that has turned purposeless.
//
// This cancellation avoids the bug described in
// https://github.com/groue/GRDB.swift/pull/932
// TODO: write a regression test.
sideEffect = {
subscription.cancel()
}
}
}
}
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
lock.synchronized { sideEffect in
switch state {
case let .subscribed(target, _):
sideEffect = {
target.context.schedule(options: target.options) {
self._receive(input)
}
}
case .waitingForRequest, .waitingForSubscription, .finished:
break
}
}
// TODO: what problem are we creating by returning .unlimited and
// ignoring downstream's result?
//
// `Publisher.receive(on:options:)` does not document its behavior
// regarding backpressure.
return .unlimited
}
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
lock.synchronized { sideEffect in
switch state {
case .waitingForRequest, .waitingForSubscription:
break
case let .subscribed(target, _):
sideEffect = {
target.context.schedule(options: target.options) {
self._receive(completion: completion)
}
}
case .finished:
break
}
}
}
private func _receive(_ input: Upstream.Output) {
lock.synchronized { sideEffect in
switch state {
case .waitingForRequest, .waitingForSubscription:
break
case let .subscribed(target, _):
// TODO: don't ignore demand
sideEffect = {
_ = target.downstream.receive(input)
}
case .finished:
break
}
}
}
private func _receive(completion: Subscribers.Completion<Upstream.Failure>) {
lock.synchronized { sideEffect in
switch state {
case .waitingForRequest, .waitingForSubscription:
break
case let .subscribed(target, _):
state = .finished
sideEffect = {
target.downstream.receive(completion: completion)
}
case .finished:
break
}
}
}
}
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
extension Publisher {
/// Specifies the scheduler on which to receive values from the publisher
///
/// The difference with the stock `receive(on:options:)` Combine method is
/// that only values and completion are re-scheduled. Subscriptions are not.
func receiveValues<S: Scheduler>(on scheduler: S, options: S.SchedulerOptions? = nil) -> ReceiveValuesOn<Self, S> {
ReceiveValuesOn(upstream: self, context: scheduler, options: options)
}
}
#endif
@@ -0,0 +1,36 @@
/// A marker protocol for refinable types.
///
/// For example:
///
/// struct Player {
/// var name: String
/// var score: Int
/// }
///
/// extension Player: Refinable { }
///
/// let player = Player(name: "Arthur", score: 1000)
///
/// // Player(name: "Arthur", score: 100)
/// let newPlayer = player.with {
/// $0.score = 100
/// }
protocol Refinable { }
extension Refinable {
/// Returns self modified with the *update* function.
///
/// For example:
///
/// let player = Player(name: "Arthur", score: 1000)
/// let newPlayer = player.with {
/// $0.score = 100
/// }
/// newPlayer.name // "Arthur"
/// newPlayer.score // 100
func with(_ update: (inout Self) throws -> Void) rethrows -> Self {
var result = self
try update(&result)
return result
}
}
@@ -0,0 +1,196 @@
import Foundation
// MARK: - Public
extension String {
/// Returns the receiver, quoted for safe insertion as an identifier in an
/// SQL query.
///
/// db.execute(sql: "SELECT * FROM \(tableName.quotedDatabaseIdentifier)")
public var quotedDatabaseIdentifier: String {
// See <https://www.sqlite.org/lang_keywords.html>
return "\"\(self)\""
}
}
/// Return as many question marks separated with commas as the *count* argument.
///
/// databaseQuestionMarks(count: 3) // "?,?,?"
public func databaseQuestionMarks(count: Int) -> String {
repeatElement("?", count: count).joined(separator: ",")
}
// MARK: - Internal
/// Reserved for GRDB: do not use.
@inline(__always)
@inlinable
func GRDBPrecondition(
_ condition: @autoclosure() -> Bool,
_ message: @autoclosure() -> String = "",
file: StaticString = #file,
line: UInt = #line)
{
// Custom precondition function which aims at solving
// <https://bugs.swift.org/browse/SR-905> and
// <https://github.com/groue/GRDB.swift/issues/37>
if !condition() {
fatalError(message(), file: file, line: line)
}
}
func fatalError<E: Error>(_ error: E) -> Never {
try! { throw error }()
}
// Workaround Swift inconvenience around factory methods of non-final classes
func cast<T, U>(_ value: T) -> U? {
value as? U
}
extension RangeReplaceableCollection {
/// Removes the first object that matches *predicate*.
mutating func removeFirst(where predicate: (Element) throws -> Bool) rethrows {
if let index = try firstIndex(where: predicate) {
remove(at: index)
}
}
}
extension Dictionary {
/// Removes the first object that matches *predicate*.
mutating func removeFirst(where predicate: (Element) throws -> Bool) rethrows {
if let index = try firstIndex(where: predicate) {
remove(at: index)
}
}
}
extension DispatchQueue {
private static var mainKey: DispatchSpecificKey<()> = {
let key = DispatchSpecificKey<()>()
DispatchQueue.main.setSpecific(key: key, value: ())
return key
}()
static var isMain: Bool {
DispatchQueue.getSpecific(key: mainKey) != nil
}
}
extension Sequence {
func countElements(where predicate: (Element) throws -> Bool) rethrows -> Int {
var count = 0
for e in self where try predicate(e) {
count += 1
}
return count
}
}
/// Makes sure the `finally` function is executed even if `execute` throws, and
/// rethrows the eventual first thrown error.
///
/// Usage:
///
/// try setup()
/// try throwingFirstError(
/// execute: work,
/// finally: cleanup)
func throwingFirstError<T>(execute: () throws -> T, finally: () throws -> Void) throws -> T {
var result: T?
var firstError: Error?
do {
result = try execute()
} catch {
firstError = error
}
do {
try finally()
} catch {
if firstError == nil {
firstError = error
}
}
if let firstError {
throw firstError
}
return result!
}
struct PrintOutputStream: TextOutputStream {
func write(_ string: String) {
Swift.print(string)
}
}
/// Concatenates two functions
func concat(_ rhs: (() -> Void)?, _ lhs: (() -> Void)?) -> (() -> Void)? {
switch (rhs, lhs) {
case let (rhs, nil):
return rhs
case let (nil, lhs):
return lhs
case let (rhs?, lhs?):
return {
rhs()
lhs()
}
}
}
/// Concatenates two functions
func concat<T>(_ rhs: ((T) -> Void)?, _ lhs: ((T) -> Void)?) -> ((T) -> Void)? {
switch (rhs, lhs) {
case let (rhs, nil):
return rhs
case let (nil, lhs):
return lhs
case let (rhs?, lhs?):
return {
rhs($0)
lhs($0)
}
}
}
extension NSLocking {
func synchronized<T>(
_ message: @autoclosure () -> String = #function,
_ block: () throws -> T)
rethrows -> T
{
lock()
defer { unlock() }
return try block()
}
// // Verbose version which helps understanding locking bugs
// func synchronized<T>(_ message: @autoclosure () -> String = "", _ block: () throws -> T) rethrows -> T {
// let queueName = String(validatingUTF8: __dispatch_queue_get_label(nil))
// print("\(queueName ?? "n/d"): \(message()) acquiring \(self)")
// lock()
// print("\(queueName ?? "n/d"): \(message()) acquired \(self)")
// defer {
// print("\(queueName ?? "n/d"): \(message()) releasing \(self)")
// unlock()
// print("\(queueName ?? "n/d"): \(message()) released \(self)")
// }
// return try block()
// }
/// Performs the side effect outside of the synchronized block. This allows
/// avoiding deadlocks, when the side effect feedbacks.
func synchronized(
_ message: @autoclosure () -> String = #function,
_ block: (inout (() -> Void)?) -> Void)
{
var sideEffect: (() -> Void)?
synchronized(message()) { block(&sideEffect) }
sideEffect?()
}
}
#if !canImport(ObjectiveC)
@inlinable func autoreleasepool<Result>(invoking body: () throws -> Result) rethrows -> Result { try body() }
#endif