Skip to content

Commit

Permalink
Make Commands Sendable (#188)
Browse files Browse the repository at this point in the history
* Make Option Sendable

* Make Argument Sendable

* Remove last Sendable warnings

* Make Command Sendable

* Make more things Sendable
  • Loading branch information
0xTim authored Oct 11, 2023
1 parent d4b580e commit ccd0773
Show file tree
Hide file tree
Showing 12 changed files with 61 additions and 37 deletions.
2 changes: 1 addition & 1 deletion Sources/ConsoleKit/Command/AnyCommand.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// A type-erased `Command`.
public protocol AnyCommand {
public protocol AnyCommand: Sendable {
/// Text that will be displayed when `--help` is passed.
var help: String { get }

Expand Down
16 changes: 9 additions & 7 deletions Sources/ConsoleKit/Command/Argument.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import NIOConcurrencyHelpers

/// An argument for a console command
///
/// exec command <arg>
Expand All @@ -24,8 +26,8 @@
///
/// See `Command` for more information.
@propertyWrapper
public final class Argument<Value>: AnyArgument
where Value: LosslessStringConvertible
public final class Argument<Value>: AnyArgument, Sendable
where Value: LosslessStringConvertible & Sendable
{
/// The argument's identifying name.
public let name: String
Expand All @@ -38,22 +40,22 @@ public final class Argument<Value>: AnyArgument
/// See `CompletionAction` for more information and available actions.
public let completion: CompletionAction

var value: InputValue<Value>
let value: NIOLockedValueBox<InputValue<Value>>

public var projectedValue: Argument<Value> {
return self
}

public var initialized: Bool {
switch self.value {
switch self.value.withLockedValue({ $0 }) {
case .initialized: return true
case .uninitialized: return false
}
}

/// @propertyWrapper value
public var wrappedValue: Value {
switch self.value {
switch self.value.withLockedValue({ $0 }) {
case let .initialized(value): return value
case .uninitialized: fatalError("Argument \(self.name) was not initialized")
}
Expand All @@ -73,7 +75,7 @@ public final class Argument<Value>: AnyArgument
self.name = name
self.help = help
self.completion = completion
self.value = .uninitialized
self.value = .init(.uninitialized)
}

func load(from input: inout CommandInput) throws {
Expand All @@ -83,6 +85,6 @@ public final class Argument<Value>: AnyArgument
guard let value = Value(argument) else {
throw CommandError.invalidArgumentType(self.name, type: Value.self)
}
self.value = .initialized(value)
self.value.withLockedValue { $0 = .initialized(value) }
}
}
6 changes: 3 additions & 3 deletions Sources/ConsoleKit/Command/CommandSignature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
/// var name: String
/// }
///
public protocol CommandSignature {
public protocol CommandSignature: Sendable {
init()
}

Expand Down Expand Up @@ -41,12 +41,12 @@ extension CommandSignature {
}
}

enum InputValue<T> {
enum InputValue<T: Sendable>: Sendable {
case initialized(T)
case uninitialized
}

internal protocol AnySignatureValue: AnyObject {
internal protocol AnySignatureValue: AnyObject, Sendable {
var help: String { get }
var name: String { get }
var initialized: Bool { get }
Expand Down
4 changes: 2 additions & 2 deletions Sources/ConsoleKit/Command/Commands.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// Represents a top-level group of configured commands. This is usually created by calling `resolve(for:)` on `Commands`.
public struct Commands {
public struct Commands: Sendable {
/// Top-level available commands, stored by unique name.
public var commands: [String: any AnyCommand]

Expand Down Expand Up @@ -80,7 +80,7 @@ public struct Commands {
}
}

private struct _Group: CommandGroup {
private struct _Group: CommandGroup, Sendable {
var commands: [String: any AnyCommand]
var defaultCommand: (any AnyCommand)?
let help: String
Expand Down
4 changes: 2 additions & 2 deletions Sources/ConsoleKit/Command/Completion.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// Shell completion implementations.
public enum Shell: String, LosslessStringConvertible, CaseIterable {
public enum Shell: String, LosslessStringConvertible, CaseIterable, Sendable {
case bash
case zsh

Expand Down Expand Up @@ -463,7 +463,7 @@ extension AnyAsyncCommand {
/// An action to be used in the shell completion script(s) to provide
/// special shell completion behaviors for an `Option`'s argument or a
/// positional `Argument`.
public struct CompletionAction {
public struct CompletionAction: Sendable {

/// The shell-specific implementations of the action.
public let expressions: [Shell: String]
Expand Down
12 changes: 7 additions & 5 deletions Sources/ConsoleKit/Command/Flag.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import NIOConcurrencyHelpers

/// A supported option for a command.
///
/// exec command [--opt -o]
Expand All @@ -14,7 +16,7 @@ public final class Flag: AnyFlag {
public let short: Character?

public var initialized: Bool {
switch self.value {
switch self.value.withLockedValue({ $0 }) {
case .initialized: return true
case .uninitialized: return false
}
Expand All @@ -25,13 +27,13 @@ public final class Flag: AnyFlag {
}

public var wrappedValue: Bool {
switch self.value {
switch self.value.withLockedValue({ $0 }) {
case let .initialized(value): return value
case .uninitialized: fatalError("Flag \(self.name) was not initialized")
}
}

var value: InputValue<Bool>
let value: NIOLockedValueBox<InputValue<Bool>>

/// Creates a new `Option` with the `optionType` set to `.value`.
///
Expand All @@ -49,10 +51,10 @@ public final class Flag: AnyFlag {
self.name = name
self.short = short
self.help = help
self.value = .uninitialized
self.value = .init(.uninitialized)
}

func load(from input: inout CommandInput) throws {
self.value = .initialized(input.nextFlag(name: self.name, short: self.short))
self.value.withLockedValue { $0 = .initialized(input.nextFlag(name: self.name, short: self.short)) }
}
}
30 changes: 19 additions & 11 deletions Sources/ConsoleKit/Command/Option.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import NIOConcurrencyHelpers

/// A supported option for a command.
///
/// exec command [--opt -o]
///
@propertyWrapper
public final class Option<Value>: AnyOption
where Value: LosslessStringConvertible
public final class Option<Value>: AnyOption, Sendable
where Value: LosslessStringConvertible & Sendable
{
/// The option's identifying name.
public let name: String
Expand All @@ -27,27 +29,33 @@ public final class Option<Value>: AnyOption
///
/// app command
/// // signature.option.isPresent == false
public private(set) var isPresent: Bool
public var isPresent: Bool {
get {
_isPresent.withLockedValue { $0 }
}
}

private let _isPresent: NIOLockedValueBox<Bool>

public var projectedValue: Option<Value> {
return self
}

public var initialized: Bool {
switch self.value {
switch self.value.withLockedValue({ $0 }) {
case .initialized: return true
case .uninitialized: return false
}
}

public var wrappedValue: Value? {
switch self.value {
switch self.value.withLockedValue({ $0 }) {
case let .initialized(value): return value
case .uninitialized: fatalError("Option \(self.name) was not initialized")
}
}

var value: InputValue<Value?>
let value: NIOLockedValueBox<InputValue<Value?>>

/// Creates a new `Option` with the `optionType` set to `.value`.
///
Expand All @@ -70,21 +78,21 @@ public final class Option<Value>: AnyOption
self.short = short
self.help = help
self.completion = completion
self.isPresent = false
self.value = .uninitialized
self._isPresent = .init(false)
self.value = .init(.uninitialized)
}

func load(from input: inout CommandInput) throws {
let option = input.nextOption(name: self.name, short: self.short)
self.isPresent = option.passedIn
self._isPresent.withLockedValue { $0 = option.passedIn }

if let rawValue = option.value {
guard let value = Value(rawValue) else {
throw CommandError.invalidOptionType(self.name, type: Value.self)
}
self.value = .initialized(value)
self.value.withLockedValue { $0 = .initialized(value) }
} else {
self.value = .initialized(nil)
self.value.withLockedValue { $0 = .initialized(nil) }
}
}
}
2 changes: 1 addition & 1 deletion Sources/ConsoleKitAsyncExample/entry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Foundation
struct AsyncExample {
static func main() async throws {
let console: Console = Terminal()
let input = CommandInput(arguments: CommandLine.arguments)
let input = CommandInput(arguments: ProcessInfo.processInfo.arguments)

var commands = AsyncCommands(enableAutocomplete: true)
commands.use(DemoCommand(), as: "demo", isDefault: false)
Expand Down
2 changes: 1 addition & 1 deletion Sources/ConsoleKitExample/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Foundation
import Logging

let console: Console = Terminal()
var input = CommandInput(arguments: CommandLine.arguments)
var input = CommandInput(arguments: ProcessInfo.processInfo.arguments)
var context = CommandContext(console: console, input: input)

var commands = Commands(enableAutocomplete: true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,20 @@
import ConsoleKit
import Logging
import XCTest
import NIOConcurrencyHelpers

final class TestConsole: Console {
var lastOutput: String? = nil
var userInfo = [AnySendableHashable: any Sendable]()
let lastOutput: NIOLockedValueBox<String?> = .init(nil)
let _userInfo: NIOLockedValueBox<[AnySendableHashable: any Sendable]> = .init([:])

var userInfo: [AnySendableHashable : Sendable] {
get {
_userInfo.withLockedValue { $0 }
}
set {
_userInfo.withLockedValue { $0 = newValue }
}
}

func input(isSecure: Bool) -> String {
""
Expand Down
2 changes: 1 addition & 1 deletion Tests/ConsoleKitTests/CommandTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class CommandTests: XCTestCase {
}

var help: String = ""
var assertion: (Signature) -> ()
var assertion: @Sendable (Signature) -> ()

func run(using context: CommandContext, signature: OptionInitialized.Signature) throws {
assertion(signature)
Expand Down
4 changes: 3 additions & 1 deletion Tests/ConsoleKitTests/Utilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ final class StrictCommand: Command {

init() { }
}
var help: String = "I error if you pass in bad values"
var help: String {
"I error if you pass in bad values"
}

func run(using context: CommandContext, signature: Signature) throws {
print("Done!")
Expand Down

0 comments on commit ccd0773

Please sign in to comment.