-
Notifications
You must be signed in to change notification settings - Fork 0
chore: add configuartion reader #10
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
128 changes: 128 additions & 0 deletions
128
Sources/BinaryDependencyManager/Utils/BinaryDependenciesConfigurationReader.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| import Foundation | ||
| import Yams | ||
| import ArgumentParser | ||
|
|
||
|
|
||
| /// A utility for resolving and loading binary dependencies configuration files. | ||
| struct BinaryDependenciesConfigurationReader { | ||
| private static let defaultConfigurationFilenames: [String] = [ | ||
| ".binary-dependencies.yaml", | ||
| ".binary-dependencies.yml", | ||
| "Dependencies.json" // CMM, Setapp | ||
| ] | ||
|
|
||
| private static let defaultOutputDirectoryPaths: [String] = [ | ||
| "Dependencies/Binary", | ||
| "Dependencies", // CMM, Setapp | ||
| ] | ||
|
|
||
| private static let defaultCacheDirectoryPaths: [String] = [ | ||
| ".cache/binary-dependencies", | ||
| ".cache/dependencies", // Setapp | ||
| ".dependencies-cache", // CMM | ||
| ] | ||
|
|
||
| private let fileManager: FileManagerProtocol | ||
|
|
||
| init(fileManager: FileManagerProtocol = FileManager.default) { | ||
| self.fileManager = fileManager | ||
| } | ||
|
|
||
| /// Resolves the file path based on the provided path, or searches through the specified variations. | ||
| /// - Parameters: | ||
| /// - filePath: The path to resolve, or nil to search through variations. | ||
| /// - variations: A list of possible filenames or directory names to check if filePath is nil. | ||
| /// - Returns: The resolved file URL. Crashes if no path is found. | ||
| private func resolveFilePath(_ filePath: String?, variations: [String]) -> URL { | ||
| let existingFileURL: URL? = filePath.map(\.asFileURL) | ||
| guard existingFileURL == .none else { return existingFileURL! } | ||
|
|
||
| let fileURL = (variations.first(where: fileManager.fileExists(atPath:)) ?? variations.first).map(\.asFileURL) | ||
|
|
||
| guard let fileURL else { | ||
| preconditionFailure("Path must be always resolved") | ||
| } | ||
|
|
||
| return fileURL | ||
| } | ||
|
|
||
| /// Resolves the configuration file URL, ensuring the file exists on disk. | ||
| /// - Parameter configurationFilePath: Optional configuration file path to use, or nil to search defaults. | ||
| /// - Throws: ValidationError if the file does not exist. | ||
| /// - Returns: The resolved configuration file URL. | ||
| func resolveConfigurationFileURL(_ configurationFilePath: String?) throws -> URL { | ||
| let configurationFileURL = resolveFilePath(configurationFilePath, variations: Self.defaultConfigurationFilenames) | ||
| guard fileManager.fileExists(atPath: configurationFileURL.path) else { | ||
| throw ValidationError("No configuration file found") | ||
| } | ||
| return configurationFileURL | ||
| } | ||
|
|
||
| /// Resolves the output directory URL using the provided path, or falls back to the default output directories. | ||
| /// - Parameter outputDirectory: Optional output directory path. | ||
| /// - Returns: The resolved output directory URL. | ||
| func resolveOutputDirectoryURL(_ outputDirectory: String?) -> URL { | ||
| resolveFilePath(outputDirectory, variations: Self.defaultOutputDirectoryPaths) | ||
| } | ||
|
|
||
| /// Resolves the cache directory URL using the provided path, or falls back to the default cache directories. | ||
| /// - Parameter cacheDirectory: Optional cache directory path. | ||
| /// - Returns: The resolved cache directory URL. | ||
| func resolveCacheDirectoryURL(_ cacheDirectory: String?) -> URL { | ||
| resolveFilePath(cacheDirectory, variations: Self.defaultCacheDirectoryPaths) | ||
| } | ||
|
|
||
| /// Parses and returns a `BinaryDependenciesConfiguration` from the specified configuration file path. | ||
| /// - Parameter configurationPath: Optional path to the configuration file. | ||
| /// - Throws: An error if the file cannot be found, decoded, or the version check fails. | ||
| /// - Returns: The parsed `BinaryDependenciesConfiguration` object. | ||
| func readConfiguration(at configurationPath: String?) throws -> BinaryDependenciesConfiguration { | ||
|
|
||
| let configurationURL: URL = try resolveConfigurationFileURL(configurationPath) | ||
|
|
||
| // Get the contents of the file | ||
| guard let dependenciesData: Data = fileManager.contents(atPath: configurationURL.path) else { | ||
| throw ValidationError("Can't get contents of configuration file at \(configurationURL.path)") | ||
| } | ||
|
|
||
| // Decoder selection: Check if this is yaml, and fallback to JSONDecoder. | ||
| let decoder: TopLevelDataDecoder | ||
| if ["yaml", "yml"].contains(configurationURL.pathExtension) { | ||
| decoder = YAMLDecoder() | ||
| } else { | ||
| decoder = JSONDecoder() | ||
| } | ||
|
|
||
| // Parse configuration | ||
| let configuration = try decoder.decode(BinaryDependenciesConfiguration.self, from: dependenciesData) | ||
|
|
||
| // Check minimum required version | ||
| let minimumRequiredVersion = configuration.minimumVersion ?? BinaryDependenciesManager.version | ||
| guard BinaryDependenciesManager.version >= minimumRequiredVersion else { | ||
| throw ValidationError( | ||
| "\(configurationPath ?? configurationURL.lastPathComponent) requires version '\(minimumRequiredVersion)', but current version '\(BinaryDependenciesManager.version)' is lower." | ||
| ) | ||
| } | ||
|
|
||
| let dependencies = configuration.dependencies | ||
| let dependenciesInfo = dependencies | ||
| .map { " \($0.repo)(\($0.tag))" } | ||
| .joined(separator: "\n") | ||
| Logger.log( | ||
| "[Read] Found \(dependencies.count) dependencies:\n\(dependenciesInfo)" | ||
| ) | ||
|
|
||
| return configuration | ||
| } | ||
| } | ||
|
|
||
|
|
||
| /// A type that defines methods for decoding Data. | ||
| /// Similar to the TopLevelDecoder with a restricted input to the Data type. | ||
| protocol TopLevelDataDecoder { | ||
| func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable | ||
| } | ||
|
|
||
| extension JSONDecoder: TopLevelDataDecoder {} | ||
| extension YAMLDecoder: TopLevelDataDecoder {} | ||
|
|
18 changes: 18 additions & 0 deletions
18
Sources/BinaryDependencyManager/Utils/FileManagerProtocol.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import Foundation | ||
|
|
||
| protocol FileManagerProtocol { | ||
| /// Returns a Boolean value that indicates whether a file or directory exists at a specified path. | ||
| /// | ||
| /// - Parameter path: The path of the file or directory. If path begins with a tilde (~), it must first be expanded with expandingTildeInPath; otherwise, this method returns false. | ||
| /// | ||
| /// - Returns: true if a file at the specified path exists, or false if the file does not exist or its existence could not be determined. | ||
| func fileExists(atPath path: String) -> Bool | ||
|
|
||
| /// Returns the contents of the file at the specified path. | ||
| /// - Parameters: | ||
| /// - path: The path of the file whose contents you want. | ||
| /// - Returns: An `Data` object with the contents of the file. If path specifies a directory, or if some other error occurs, this method returns nil. | ||
| func contents(atPath path: String) -> Data? | ||
| } | ||
|
|
||
| extension FileManager: FileManagerProtocol {} |
13 changes: 13 additions & 0 deletions
13
Sources/BinaryDependencyManager/Utils/String+fileURL.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import Foundation | ||
|
|
||
| extension String { | ||
| /// Absolute file URL with standardized path. | ||
| var asFileURL: URL { | ||
| let url = if #available(macOS 13.0, *) { | ||
| URL(filePath: self) | ||
| } else { | ||
| URL(fileURLWithPath: self) | ||
| } | ||
| return url.standardizedFileURL.absoluteURL | ||
| } | ||
| } | ||
82 changes: 82 additions & 0 deletions
82
Tests/BinaryDependencyManagerTests/BinaryDependenciesConfigurationReaderTests.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| @testable import binary_dependencies_manager | ||
| import XCTest | ||
| import Testing | ||
| import Yams | ||
| import Foundation | ||
|
|
||
| final class BinaryDependenciesConfigurationReaderTests: XCTestCase { | ||
| private func makeReader(withFiles files: [String]) -> BinaryDependenciesConfigurationReader { | ||
| let mockFileManager = FileManagerProtocolMock() | ||
| mockFileManager.files = Set(files) | ||
| return BinaryDependenciesConfigurationReader(fileManager: mockFileManager) | ||
| } | ||
|
|
||
| func test_resolveConfigurationFileURL_withExplicitPath() throws { | ||
| let sut = makeReader(withFiles: ["/the/path/.binary-dependencies.yaml"]) | ||
| let url = try sut.resolveConfigurationFileURL("/the/path/.binary-dependencies.yaml") | ||
| XCTAssertEqual(url.path, "/the/path/.binary-dependencies.yaml") | ||
| } | ||
|
|
||
| func test_resolveConfigurationFileURL_fallbackToDefault() throws { | ||
| let sut = makeReader(withFiles: [".binary-dependencies.yaml".asFileURL.path]) | ||
| let url = try sut.resolveConfigurationFileURL(nil) | ||
| XCTAssertEqual(url.lastPathComponent, ".binary-dependencies.yaml") | ||
| } | ||
|
|
||
| func test_resolveConfigurationFileURL_fileDoesNotExist_throws() { | ||
| let sut = makeReader(withFiles: []) | ||
| XCTAssertThrowsError(try sut.resolveConfigurationFileURL(nil)) | ||
| } | ||
|
|
||
| func test_resolveOutputDirectoryURL_explicit() { | ||
| let sut = makeReader(withFiles: []) | ||
| let url = sut.resolveOutputDirectoryURL("Explicit/Output") | ||
| XCTAssertEqual(url, "Explicit/Output".asFileURL) | ||
| } | ||
|
|
||
| func test_resolveCacheDirectoryURL_explicit() { | ||
| let sut = makeReader(withFiles: []) | ||
| let url = sut.resolveCacheDirectoryURL("Explicit/Cache") | ||
| XCTAssertEqual(url, "Explicit/Cache".asFileURL) | ||
| } | ||
|
|
||
| func test_readConfiguration_parsesYAML() throws { | ||
| // GIVEN | ||
| let yamlString = """ | ||
| minimumVersion: 0.0.1 | ||
| outputDirectory: output/directory | ||
| cacheDirectory: cache/directory | ||
| dependencies: | ||
| - repo: test/repo | ||
| tag: "0.0.1" | ||
| pattern: pattern1 | ||
| checksum: "check1" | ||
| """ | ||
| let filePath = ".binary-dependencies.yaml".asFileURL.path | ||
| let data = Data(yamlString.utf8) | ||
| let mockFileManager = FileManagerProtocolMock() | ||
| mockFileManager.files = [filePath] | ||
| mockFileManager.contents = [filePath: data] | ||
|
|
||
| let sut = BinaryDependenciesConfigurationReader(fileManager: mockFileManager) | ||
|
|
||
| // WHEN | ||
| let config = try sut.readConfiguration(at: .none) | ||
|
|
||
| // THEN | ||
| let expected = BinaryDependenciesConfiguration( | ||
| minimumVersion: Version(string: "0.0.1"), | ||
| outputDirectory: "output/directory", | ||
| cacheDirectory: "cache/directory", | ||
| dependencies: [ | ||
| Dependency( | ||
| repo: "test/repo", | ||
| tag: "0.0.1", | ||
| assets: [Dependency.Asset(checksum: "check1", pattern: "pattern1")] | ||
| ) | ||
| ] | ||
| ) | ||
| XCTAssertEqual(config, expected) | ||
| } | ||
| } | ||
|
|
14 changes: 14 additions & 0 deletions
14
Tests/BinaryDependencyManagerTests/Mocks/FileManagerProtocolMock.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| @testable import binary_dependencies_manager | ||
| import Foundation | ||
|
|
||
| class FileManagerProtocolMock: FileManagerProtocol { | ||
| var files: Set<String> = [] | ||
| func fileExists(atPath path: String) -> Bool { | ||
| files.contains(path) | ||
| } | ||
|
|
||
| var contents: [String: Data] = [:] | ||
| func contents(atPath path: String) -> Data? { | ||
| contents[path] | ||
| } | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fileURL with starts from about 10.9
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's kinda deprecated, or soon to be deprecated. Along with the
path.