Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: A storage container used to share data between commands. #8

Merged
merged 5 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Sources/Sake/CLI/RunCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ struct RunCommand: SakeParsableCommand {
environment: ProcessInfo.processInfo.environment,
appDirectory: Bundle.main.bundleURL.findBuildDirectory()?.deletingLastPathComponent()
.path ?? "<Could not find SakeApp directory>",
runDirectory: FileManager.default.currentDirectoryPath
runDirectory: FileManager.default.currentDirectoryPath,
storage: Command.Context.Storage()
)
let runner = CommandRunner(command: command, context: context)
do {
Expand Down
105 changes: 105 additions & 0 deletions Sources/Sake/Command+Context.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import Foundation

public extension Command {
/// Represents the context in which a command is executed.
///
/// The `Context` contains information such as command-line arguments,
/// environment variables, and directories related to the app and its execution.
struct Context: Sendable {
/// The arguments passed to the command.
///
/// This array contains the arguments that were provided when the command was executed.
public let arguments: [String]

/// The environment variables available during command execution.
///
/// A dictionary of environment variables, where the key is the variable name,
/// and the value is the variable's content.
public let environment: [String: String]

/// The directory where the application resides.
///
/// This is typically the directory of the SakeApp where commands are managed.
public let appDirectory: String

/// The directory where the command is executed.
///
/// This is the current working directory when the command runs.
public let runDirectory: String

/// A storage container for the context.
///
/// This storage is used to share data between commands and store information
public let storage: Storage

/// Initializes a new `Context` for command execution.
///
/// - Parameters:
/// - arguments: The arguments passed to the command.
/// - environment: The environment variables available during execution.
/// - appDirectory: The directory where the app is located.
/// - runDirectory: The directory from which the command is run.
/// - storage: A storage container for the context.
public init(
arguments: [String],
environment: [String: String],
appDirectory: String,
runDirectory: String,
storage: Storage
) {
self.arguments = arguments
self.environment = environment
self.appDirectory = appDirectory
self.runDirectory = runDirectory
self.storage = storage
}
}
}

public extension Command.Context {
/// Represents a storage container for the context.
///
/// The `Storage` class provides a thread-safe storage container for the context.
/// It allows for sharing data between commands and storing information during execution.
final class Storage: @unchecked Sendable {
private var dictionary: [String: Any] = [:]
private let lock = NSRecursiveLock()

public init() {}

public subscript(key: String) -> Any? {
get { get(forKey: key) }
set { set(newValue, forKey: key) }
}

public func set(_ value: Any?, forKey key: String) {
lock.lock()
defer { lock.unlock() }
dictionary[key] = value
}

public func get(forKey key: String) -> Any? {
lock.lock()
defer { lock.unlock() }
return dictionary[key]
}

public func remove(forKey key: String) {
lock.lock()
defer { lock.unlock() }
dictionary.removeValue(forKey: key)
}

public func removeAll() {
lock.lock()
defer { lock.unlock() }
dictionary.removeAll()
}

public func contains(key: String) -> Bool {
lock.lock()
defer { lock.unlock() }
return dictionary.keys.contains(key)
}
}
}
6 changes: 4 additions & 2 deletions Sources/Sake/Command+Map.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ public extension Command.Context {
arguments: transform(arguments),
environment: environment,
appDirectory: appDirectory,
runDirectory: runDirectory
runDirectory: runDirectory,
storage: storage
)
}

Expand All @@ -77,7 +78,8 @@ public extension Command.Context {
arguments: arguments,
environment: transform(environment),
appDirectory: appDirectory,
runDirectory: runDirectory
runDirectory: runDirectory,
storage: storage
)
}
}
48 changes: 0 additions & 48 deletions Sources/Sake/Command.swift
Original file line number Diff line number Diff line change
@@ -1,51 +1,3 @@
public extension Command {
/// Represents the context in which a command is executed.
///
/// The `Context` contains information such as command-line arguments,
/// environment variables, and directories related to the app and its execution.
struct Context: Sendable {
/// The arguments passed to the command.
///
/// This array contains the arguments that were provided when the command was executed.
public let arguments: [String]

/// The environment variables available during command execution.
///
/// A dictionary of environment variables, where the key is the variable name,
/// and the value is the variable's content.
public let environment: [String: String]

/// The directory where the application resides.
///
/// This is typically the directory of the SakeApp where commands are managed.
public let appDirectory: String

/// The directory where the command is executed.
///
/// This is the current working directory when the command runs.
public let runDirectory: String

/// Initializes a new `Context` for command execution.
///
/// - Parameters:
/// - arguments: The arguments passed to the command.
/// - environment: The environment variables available during execution.
/// - appDirectory: The directory where the app is located.
/// - runDirectory: The directory from which the command is run.
public init(
arguments: [String],
environment: [String: String],
appDirectory: String,
runDirectory: String
) {
self.arguments = arguments
self.environment = environment
self.appDirectory = appDirectory
self.runDirectory = runDirectory
}
}
}

/// Represents a command that can be executed in the Sake project.
///
/// Defines a command with dependencies, execution logic, and optional skip conditions.
Expand Down
12 changes: 8 additions & 4 deletions Tests/SakeTests/CommandMapTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ final class CommandMapTests: XCTestCase {
arguments: ["--option1", "value1", "--option2", "value2", "argument1", "argument2"],
environment: ["foo": "bar"],
appDirectory: "/path/to/app",
runDirectory: "/path/to/run"
runDirectory: "/path/to/run",
storage: .init()
)

let mappedCommandContext = try commandContext.mapArguments { arguments in
Expand All @@ -25,7 +26,8 @@ final class CommandMapTests: XCTestCase {
arguments: ["--option1", "value1", "--option2", "value2", "argument1", "argument2"],
environment: ["foo": "bar"],
appDirectory: "/path/to/app",
runDirectory: "/path/to/run"
runDirectory: "/path/to/run",
storage: .init()
)

let mappedCommandContext = try commandContext.mapEnvironment { environment in
Expand Down Expand Up @@ -58,7 +60,8 @@ final class CommandMapTests: XCTestCase {
arguments: ["--option1", "value1", "--option2", "value2", "argument1", "argument2"],
environment: ["foo": "bar"],
appDirectory: "/path/to/app",
runDirectory: "/path/to/run"
runDirectory: "/path/to/run",
storage: .init()
)

let mappedCommand = command.mapArguments { arguments in
Expand Down Expand Up @@ -98,7 +101,8 @@ final class CommandMapTests: XCTestCase {
arguments: ["--option1", "value1", "--option2", "value2", "argument1", "argument2"],
environment: ["foo": "bar"],
appDirectory: "/path/to/app",
runDirectory: "/path/to/run"
runDirectory: "/path/to/run",
storage: .init()
)

let mappedCommand = command.mapEnvironment { environment in
Expand Down
44 changes: 43 additions & 1 deletion Tests/SakeTests/CommandRunnerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,47 @@ final class CommandRunnerTests: XCTestCase {

XCTAssertEqual(runnedCommands, ["skipIf2", "dependency3", "command"])
}

func testSharingDataBetweenCommandsViaContextStorage() async throws {
nonisolated(unsafe) var runnedCommands: [String] = []

let dependency1 = Command(
run: { context in
context.storage["someKey"] = "someData"
runnedCommands.append("dependency1")
}
)

let dependency2 = Command(
dependencies: [dependency1],
run: { context in
XCTAssertEqual(context.storage["someKey"] as? String, "someData")
runnedCommands.append("dependency2")
}
)

let dependency3 = Command(
dependencies: [dependency2],
run: { context in
XCTAssertEqual(context.storage["someKey"] as? String, "someData")
context.storage["someKey"] = "otherData"
runnedCommands.append("dependency3")
}
)

let command = Command(
dependencies: [dependency3],
run: { context in
XCTAssertEqual(context.storage["someKey"] as? String, "otherData")
runnedCommands.append("command")
}
)

let runner = CommandRunner(command: command, context: .empty)
try await runner.run()

XCTAssertEqual(runnedCommands, ["dependency1", "dependency2", "dependency3", "command"])
}
}

private extension Command.Context {
Expand All @@ -123,7 +164,8 @@ private extension Command.Context {
arguments: [],
environment: [:],
appDirectory: "",
runDirectory: ""
runDirectory: "",
storage: .init()
)
}
}
33 changes: 32 additions & 1 deletion docs/commands-run-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Sake provides a `Command.Context` structure that is used to provide runtime info
- **`environment`**: A dictionary representing the environment variables available during the command's execution.
- **`appDirectory`**: The path to the SakeApp directory.
- **`runDirectory`**: The directory from which the command was run.
- **`storage`**: A storage container used to share data between commands.

### Using in Commands

Expand All @@ -36,7 +37,37 @@ In this example:
- The `skipIf` block checks if there are no arguments. If no arguments are provided, the command is skipped.
- The `run` block executes the main logic, which in this case is printing the arguments provided by the user.

### Mapping
### Sharing Data Between Commands

The `storage` property enables sharing data between different command blocks like `run` and `skipIf`, as well as across commands that have dependencies.

Here's an example of using `storage` to share data between a command and its dependency:

```swift
public static var commandA: Command {
Command(
run: { context in
context.storage["command-a-data"] = "jepa"
print("Command A running")
}
)
}

public static var commandB: Command {
Command(
dependencies: [commandA], // commandA runs before commandB
run: { context in
let commandAData = context.storage["command-a-data"] as? String
// commandAData == "jepa"
print("Command B running")
}
)
}
```

In this example, `commandA` stores a value in the `context.storage`, which `commandB` then retrieves when it runs. This allows for flexible data sharing and coordination between dependent commands.

### Modifying

Sake provides a way to modify (`map`) the `arguments` and `environment` properties of the `Context` before executing a command. This feature can be particularly useful when working with command dependencies, allowing for customization of their behavior.

Expand Down
Loading