From 338ab8cfc215b337b6f9f47daab00a9770bfb875 Mon Sep 17 00:00:00 2001 From: Kattouf Date: Fri, 25 Oct 2024 10:35:17 +0700 Subject: [PATCH 1/5] add storage --- Sources/Sake/CLI/RunCommand.swift | 3 +- Sources/Sake/Command+Map.swift | 6 ++-- Sources/Sake/Command.swift | 60 ++++++++++++++++++++++++++++++- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/Sources/Sake/CLI/RunCommand.swift b/Sources/Sake/CLI/RunCommand.swift index ccf5b52..1665748 100644 --- a/Sources/Sake/CLI/RunCommand.swift +++ b/Sources/Sake/CLI/RunCommand.swift @@ -30,7 +30,8 @@ struct RunCommand: SakeParsableCommand { environment: ProcessInfo.processInfo.environment, appDirectory: Bundle.main.bundleURL.findBuildDirectory()?.deletingLastPathComponent() .path ?? "", - runDirectory: FileManager.default.currentDirectoryPath + runDirectory: FileManager.default.currentDirectoryPath, + storage: Command.Context.Storage() ) let runner = CommandRunner(command: command, context: context) do { diff --git a/Sources/Sake/Command+Map.swift b/Sources/Sake/Command+Map.swift index 5671b8b..580212b 100644 --- a/Sources/Sake/Command+Map.swift +++ b/Sources/Sake/Command+Map.swift @@ -60,7 +60,8 @@ public extension Command.Context { arguments: transform(arguments), environment: environment, appDirectory: appDirectory, - runDirectory: runDirectory + runDirectory: runDirectory, + storage: storage ) } @@ -77,7 +78,8 @@ public extension Command.Context { arguments: arguments, environment: transform(environment), appDirectory: appDirectory, - runDirectory: runDirectory + runDirectory: runDirectory, + storage: storage ) } } diff --git a/Sources/Sake/Command.swift b/Sources/Sake/Command.swift index 2252e5b..e221c27 100644 --- a/Sources/Sake/Command.swift +++ b/Sources/Sake/Command.swift @@ -1,3 +1,53 @@ +import Foundation + +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() {} + + 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) + } + } +} + public extension Command { /// Represents the context in which a command is executed. /// @@ -25,6 +75,11 @@ public extension Command { /// 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: @@ -32,16 +87,19 @@ public extension 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 + runDirectory: String, + storage: Storage ) { self.arguments = arguments self.environment = environment self.appDirectory = appDirectory self.runDirectory = runDirectory + self.storage = storage } } } From 4d87253fa45b971b796b6d68b7081a96cb3f7b96 Mon Sep 17 00:00:00 2001 From: Kattouf Date: Fri, 25 Oct 2024 10:36:48 +0700 Subject: [PATCH 2/5] move command context to specific file --- Sources/Sake/Command+Context.swift | 105 ++++++++++++++++++++++++++++ Sources/Sake/Command.swift | 106 ----------------------------- 2 files changed, 105 insertions(+), 106 deletions(-) create mode 100644 Sources/Sake/Command+Context.swift diff --git a/Sources/Sake/Command+Context.swift b/Sources/Sake/Command+Context.swift new file mode 100644 index 0000000..8d8ade1 --- /dev/null +++ b/Sources/Sake/Command+Context.swift @@ -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() {} + + 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) + } + } +} diff --git a/Sources/Sake/Command.swift b/Sources/Sake/Command.swift index e221c27..74cd167 100644 --- a/Sources/Sake/Command.swift +++ b/Sources/Sake/Command.swift @@ -1,109 +1,3 @@ -import Foundation - -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() {} - - 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) - } - } -} - -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 - } - } -} - /// Represents a command that can be executed in the Sake project. /// /// Defines a command with dependencies, execution logic, and optional skip conditions. From ac14b7a42e570497a687996b1e74a01bdfa742c1 Mon Sep 17 00:00:00 2001 From: Kattouf Date: Fri, 25 Oct 2024 11:40:44 +0700 Subject: [PATCH 3/5] context storage tests --- Tests/SakeTests/CommandMapTests.swift | 12 ++++--- Tests/SakeTests/CommandRunnerTests.swift | 44 +++++++++++++++++++++++- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/Tests/SakeTests/CommandMapTests.swift b/Tests/SakeTests/CommandMapTests.swift index 8d13640..3fca6a4 100644 --- a/Tests/SakeTests/CommandMapTests.swift +++ b/Tests/SakeTests/CommandMapTests.swift @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/Tests/SakeTests/CommandRunnerTests.swift b/Tests/SakeTests/CommandRunnerTests.swift index d8e962b..e25186d 100644 --- a/Tests/SakeTests/CommandRunnerTests.swift +++ b/Tests/SakeTests/CommandRunnerTests.swift @@ -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 { @@ -123,7 +164,8 @@ private extension Command.Context { arguments: [], environment: [:], appDirectory: "", - runDirectory: "" + runDirectory: "", + storage: .init() ) } } From cbce817bdb9290dcaf3e298b22ab131093fd545c Mon Sep 17 00:00:00 2001 From: Kattouf Date: Fri, 25 Oct 2024 11:48:42 +0700 Subject: [PATCH 4/5] make storage subscript public --- Sources/Sake/Command+Context.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Sake/Command+Context.swift b/Sources/Sake/Command+Context.swift index 8d8ade1..52a7902 100644 --- a/Sources/Sake/Command+Context.swift +++ b/Sources/Sake/Command+Context.swift @@ -67,7 +67,7 @@ public extension Command.Context { public init() {} - subscript(key: String) -> Any? { + public subscript(key: String) -> Any? { get { get(forKey: key) } set { set(newValue, forKey: key) } } From 826481c6935919b15ccc4176098f5225353eb754 Mon Sep 17 00:00:00 2001 From: Kattouf Date: Fri, 25 Oct 2024 12:09:40 +0700 Subject: [PATCH 5/5] add info about context storage to doc --- docs/commands-run-context.md | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/commands-run-context.md b/docs/commands-run-context.md index 1edbf45..47c8c2d 100644 --- a/docs/commands-run-context.md +++ b/docs/commands-run-context.md @@ -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 @@ -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.