feat: Breaking out more dependencies.

This commit is contained in:
2024-12-10 17:08:17 -05:00
parent 87390c4b63
commit 92cd6afa2b
20 changed files with 1408 additions and 691 deletions

View File

@@ -3,8 +3,6 @@ import DependenciesMacros
import Foundation
import ShellClient
// TODO: Drop support for non-json configuration.
public extension DependencyValues {
var cliClient: CliClient {
get { self[CliClient.self] }
@@ -14,11 +12,7 @@ public extension DependencyValues {
@DependencyClient
public struct CliClient: Sendable {
public var decoder: @Sendable () -> JSONDecoder = { .init() }
public var encoder: @Sendable () -> JSONEncoder = { .init() }
public var loadConfiguration: @Sendable () throws -> Configuration
public var runCommand: @Sendable ([String], Bool, ShellCommand.Shell) async throws -> Void
public var createConfiguration: @Sendable (_ path: String, _ json: Bool) throws -> Void
public var findVaultFileInCurrentDirectory: @Sendable () throws -> String
public func runCommand(
@@ -40,30 +34,12 @@ public struct CliClient: Sendable {
extension CliClient: DependencyKey {
// swiftlint:disable function_body_length
public static func live(
decoder: JSONDecoder = .init(),
encoder: JSONEncoder = .init(),
env: [String: String]
) -> Self {
@Dependency(\.fileClient) var fileClient
@Dependency(\.logger) var logger
return .init {
decoder
} encoder: {
encoder
} loadConfiguration: {
let url = try findConfigurationFiles(env: env)
var env = env
logger.trace("Loading configuration from: \(url)")
guard let config = try fileClient.loadFile(url, &env, decoder) else {
return try .fromEnv(env)
}
return config
} runCommand: { args, quiet, shell in
return .init { args, quiet, shell in
@Dependency(\.asyncShellClient) var shellClient
if !quiet {
try await shellClient.foreground(.init(
@@ -80,37 +56,15 @@ extension CliClient: DependencyKey {
args
))
}
} createConfiguration: { path, json in
// Early out if a file exists at the path already.
guard !fileClient.fileExists(path) else {
throw CliClientError.fileExistsAtPath(path)
}
var path = path
let data: Data
if !json {
// Write the default env template.
data = Data(Configuration.fileTemplate.utf8)
} else {
if !path.contains(".json") {
path += ".json"
}
data = try jsonEncoder.encode(Configuration.mock)
}
try fileClient.write(path, data)
} findVaultFileInCurrentDirectory: {
guard let url = try fileClient.findVaultFileInCurrentDirectory() else {
throw CliClientError.vaultFileNotFound
}
return path(for: url)
fatalError()
// guard let url = try fileClient.findVaultFileInCurrentDirectory() else {
// throw CliClientError.vaultFileNotFound
// }
// return path(for: url)
}
}
// swiftlint:enable function_body_length
public static var liveValue: CliClient {
.live(env: ProcessInfo.processInfo.environment)
}

View File

@@ -1,194 +0,0 @@
import Dependencies
import Foundation
import ShellClient
public struct Configuration2: Codable, Equatable, Sendable {
public let playbook: Playbook
public let template: Template
public let vault: Vault
public init(
playbook: Playbook,
template: Template,
vault: Vault
) {
self.playbook = playbook
self.template = template
self.vault = vault
}
public static var mock: Self {
.init(playbook: .mock, template: .mock, vault: .mock)
}
public struct Playbook: Codable, Equatable, Sendable {
public let directory: String?
public let inventory: String?
public let args: [String]?
public let useVaultArgs: Bool
public init(
directory: String? = nil,
inventory: String? = nil,
args: [String]? = nil,
useVaultArgs: Bool = false
) {
self.directory = directory
self.inventory = inventory
self.args = args
self.useVaultArgs = useVaultArgs
}
public static var mock: Self {
.init(
directory: "/path/to/local/playbook-directory",
inventory: "/path/to/local/inventory.ini",
args: [],
useVaultArgs: true
)
}
}
public struct Template: Codable, Equatable, Sendable {
let url: String?
let version: String?
let directory: String?
public init(
url: String? = nil,
version: String? = nil,
directory: String? = nil
) {
self.url = url
self.version = version
self.directory = directory
}
public static var mock: Self {
.init(
url: "https://git.example.com/consult-template.git",
version: "main",
directory: "/path/to/local/template-directory"
)
}
}
public struct Vault: Codable, Equatable, Sendable {
public let args: [String]?
public let encryptId: String?
public init(
args: [String]? = nil,
encryptId: String? = nil
) {
self.args = args
self.encryptId = encryptId
}
public static var mock: Self {
.init(
args: [
"--vault-id=myId@$SCRIPTS/vault-gopass-client"
],
encryptId: "myId"
)
}
}
}
/// Represents the configurable items for the command line tool.
///
///
public struct Configuration: Codable, Equatable, Sendable {
public let playbookDir: String?
public let inventoryPath: String?
public let templateRepo: String?
public let templateRepoVersion: String?
public let templateDir: String?
public let defaultPlaybookArgs: [String]?
public let defaultVaultArgs: [String]?
public let defaultVaultEncryptId: String?
fileprivate enum CodingKeys: String, CodingKey {
case playbookDir = "HPA_PLAYBOOK_DIR"
case inventoryPath = "HPA_DEFAULT_INVENTORY"
case templateRepo = "HPA_TEMPLATE_REPO"
case templateRepoVersion = "HPA_TEMPLATE_VERSION"
case templateDir = "HPA_TEMPLATE_DIR"
case defaultPlaybookArgs = "HPA_DEFAULT_PLAYBOOK_ARGS"
case defaultVaultArgs = "HPA_DEFAULT_VAULT_ARGS"
case defaultVaultEncryptId = "HPA_DEFAULT_VAULT_ENCRYPT_ID"
}
public static func fromEnv(
_ env: [String: String]
) throws -> Self {
@Dependency(\.logger) var logger
logger.trace("Creating configuration from env...")
let hpaValues: [String: String] = env.filter { $0.key.contains("HPA") }
logger.debug("HPA env vars: \(hpaValues)")
return Configuration(
playbookDir: hpaValues.value(for: .playbookDir),
inventoryPath: hpaValues.value(for: .inventoryPath),
templateRepo: hpaValues.value(for: .templateRepo),
templateRepoVersion: hpaValues.value(for: .templateRepoVersion),
templateDir: hpaValues.value(for: .templateDir),
defaultPlaybookArgs: hpaValues.array(for: .defaultPlaybookArgs),
defaultVaultArgs: hpaValues.array(for: .defaultVaultArgs),
defaultVaultEncryptId: hpaValues.value(for: .defaultVaultEncryptId)
)
}
static var mock: Self {
.init(
playbookDir: "/path/to/playbook",
inventoryPath: "/path/to/inventory.ini",
templateRepo: "https://git.example.com/consult-template.git",
templateRepoVersion: "main",
templateDir: "/path/to/local/template",
defaultPlaybookArgs: ["--tags", "debug"],
defaultVaultArgs: ["--vault-id=myId@$SCRIPTS/vault-gopass-client"],
defaultVaultEncryptId: "myId"
)
}
static var fileTemplate: String {
"""
# vi: ft=sh
# Example configuration, uncomment the lines and set the values appropriate for your
# usage.
# Set this to the location of the ansible-hpa-playbook on your local machine.
#HPA_PLAYBOOK_DIR="/path/to/ansible-hpa-playbook"
# Set this to the location of a template repository, which is used to create new assessment projects.
#HPA_TEMPLATE_REPO="https://git.example.com/your/template.git"
# Specify a branch, version, or sha of the template repository.
#HPA_TEMPLATE_VERSION="main" # branch, version, or sha
# Set this to a location of a template directory to use to create new projects.
#HPA_TEMPLATE_DIR="/path/to/local/template"
# Extra arguments that get passed directly to the ansible-playbook command.
#HPA_DEFAULT_PLAYBOOK_ARGS="--vault-id=consults@$SCRIPTS/vault-gopass-client"
"""
}
}
extension [String: String] {
fileprivate func value(for codingKey: Configuration.CodingKeys) -> String? {
self[codingKey.rawValue]
}
fileprivate func array(for codingKey: Configuration.CodingKeys) -> [String]? {
value(for: codingKey).flatMap { $0.split(separator: ",").map(String.init) }
}
}

View File

@@ -0,0 +1,13 @@
/// Holds keys associated with environment values.
public enum EnvironmentKey {
public static let xdgConfigHomeKey = "XDG_CONFIG_HOME"
public static let hpaConfigDirKey = "HPA_CONFIG_DIR"
public static let hpaConfigFileKey = "HPA_CONFIG_FILE"
}
/// Holds keys associated with hpa configuration files.
public enum HPAKey {
public static let defaultConfigHome = "~/.config/\(Self.hpaConfigDirectoryName)/\(Self.defaultConfigFileName)"
public static let defaultConfigFileName = "config.toml"
public static let hpaConfigDirectoryName = "hpa"
}

View File

@@ -1,159 +1,183 @@
import Dependencies
import DependenciesMacros
import Foundation
@_spi(Internal)
public extension DependencyValues {
var fileClient: FileClient {
get { self[FileClient.self] }
set { self[FileClient.self] = newValue }
}
}
@_spi(Internal)
@DependencyClient
public struct FileClient: Sendable {
/// Loads a file at the given path into the environment unless it can decode it as json,
/// at which point it will return the decoded file contents as a ``Configuration`` item.
public var loadFile: @Sendable (URL, inout [String: String], JSONDecoder) throws -> Configuration?
/// Returns the user's home directory path.
public var homeDir: @Sendable () -> URL = { URL(string: "~/")! }
/// Check if a path is a directory.
public var isDirectory: @Sendable (URL) -> Bool = { _ in false }
/// Check if a path is a readable.
public var isReadable: @Sendable (URL) -> Bool = { _ in false }
/// Check if a file exists at the path.
public var fileExists: @Sendable (String) -> Bool = { _ in false }
public var findVaultFile: @Sendable (String) throws -> URL?
/// Write data to a file.
public var write: @Sendable (String, Data) throws -> Void
public func findVaultFileInCurrentDirectory() throws -> URL? {
try findVaultFile(".")
}
}
@_spi(Internal)
extension FileClient: DependencyKey {
public static let testValue: FileClient = Self()
public static func live(fileManager: FileManager = .default) -> Self {
let client = LiveFileClient(fileManager: fileManager)
return Self(
loadFile: { try client.loadFile(at: $0, into: &$1, decoder: $2) },
homeDir: { client.homeDir },
isDirectory: { client.isDirectory(url: $0) },
isReadable: { client.isReadable(url: $0) },
fileExists: { client.fileExists(at: $0) },
findVaultFile: { try client.findVaultFile(in: $0) },
write: { path, data in
try data.write(to: URL(filePath: path))
}
)
}
public static let liveValue = Self.live()
}
private struct LiveFileClient: @unchecked Sendable {
private let fileManager: FileManager
init(fileManager: FileManager) {
self.fileManager = fileManager
}
var homeDir: URL { fileManager.homeDirectoryForCurrentUser }
func isDirectory(url: URL) -> Bool {
var isDirectory: ObjCBool = false
fileManager.fileExists(atPath: path(for: url), isDirectory: &isDirectory)
return isDirectory.boolValue
}
func isReadable(url: URL) -> Bool {
fileManager.isReadableFile(atPath: path(for: url))
}
func fileExists(at path: String) -> Bool {
fileManager.fileExists(atPath: path)
}
// func findVaultFileInCurrentDirectory() throws -> URL? {
func findVaultFile(in filePath: String) throws -> URL? {
let urls = try fileManager
.contentsOfDirectory(at: URL(filePath: filePath), includingPropertiesForKeys: nil)
if let vault = urls.firstVaultFile {
return vault
}
// check for folders that end with "vars" and search those next.
for folder in urls.filter({ $0.absoluteString.hasSuffix("vars/") }) {
let files = try fileManager.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil)
if let vault = files.firstVaultFile {
return vault
}
}
// Fallback to check all sub-folders
for folder in urls.filter({ self.isDirectory(url: $0) && !$0.absoluteString.hasSuffix("vars/") }) {
let files = try fileManager.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil)
if let vault = files.firstVaultFile {
return vault
}
}
return nil
}
func loadFile(
at url: URL,
into env: inout [String: String],
decoder: JSONDecoder
) throws -> Configuration? {
@Dependency(\.logger) var logger
logger.trace("Begin load file for: \(path(for: url))")
if url.absoluteString.hasSuffix(".json") {
// Handle json file.
let data = try Data(contentsOf: url)
return try decoder.decode(Configuration.self, from: data)
}
let string = try String(contentsOfFile: path(for: url), encoding: .utf8)
logger.trace("Loaded file contents: \(string)")
let lines = string.split(separator: "\n")
for line in lines {
logger.trace("Line: \(line)")
let strippedLine = line.trimmingCharacters(in: .whitespacesAndNewlines)
let splitLine = strippedLine.split(separator: "=").map {
$0.replacingOccurrences(of: "\"", with: "")
}
logger.trace("Split Line: \(splitLine)")
guard splitLine.count >= 2 else { continue }
if splitLine.count > 2 {
let rest = splitLine.dropFirst()
env[String(splitLine[0])] = String(rest.joined(separator: "="))
} else {
env[String(splitLine[0])] = String(splitLine[1])
}
}
return nil
}
}
private extension Array where Element == URL {
var firstVaultFile: URL? {
first { $0.absoluteString.hasSuffix("vault.yml") }
}
}
// import CodersClient
// import Dependencies
// import DependenciesMacros
// import Foundation
// import TOMLKit
//
// @_spi(Internal)
// public extension DependencyValues {
// var fileClient: FileClient {
// get { self[FileClient.self] }
// set { self[FileClient.self] = newValue }
// }
// }
//
// @_spi(Internal)
// @DependencyClient
// public struct FileClient: Sendable {
//
// /// Load a file as `Data`.
// public var loadFile: @Sendable (URL) throws -> Data
//
// /// Returns the user's home directory path.
// public var homeDir: @Sendable () -> URL = { URL(string: "~/")! }
//
// /// Check if a path is a directory.
// public var isDirectory: @Sendable (URL) -> Bool = { _ in false }
//
// /// Check if a path is a readable.
// public var isReadable: @Sendable (URL) -> Bool = { _ in false }
//
// /// Check if a file exists at the path.
// public var fileExists: @Sendable (String) -> Bool = { _ in false }
//
// public var findVaultFile: @Sendable (String) throws -> URL?
//
// /// Write data to a file.
// public var write: @Sendable (File, Data) throws -> Void
//
// public func findVaultFileInCurrentDirectory() throws -> URL? {
// try findVaultFile(".")
// }
//
// public func loadFile(_ file: File) throws -> Data {
// try loadFile(file.url)
// }
//
// public func loadConfiguration(
// _ file: File
// ) throws -> Configuration {
// @Dependency(\.coders) var coders
//
// let data = try loadFile(file)
//
// switch file {
// case .json:
// return try coders.jsonDecoder().decode(Configuration.self, from: data)
// case .toml:
// guard let string = String(data: data, encoding: .utf8) else {
// throw DecodingError()
// }
// return try coders.tomlDecoder().decode(Configuration.self, from: string)
// }
// }
// }
//
// @_spi(Internal)
// extension FileClient: DependencyKey {
// public static let testValue: FileClient = Self()
//
// public static func live(fileManager: FileManager = .default) -> Self {
// let client = LiveFileClient(fileManager: fileManager)
// return Self(
// loadFile: { try Data(contentsOf: $0) },
// homeDir: { client.homeDir },
// isDirectory: { client.isDirectory(url: $0) },
// isReadable: { client.isReadable(url: $0) },
// fileExists: { client.fileExists(at: $0) },
// findVaultFile: { try client.findVaultFile(in: $0) },
// write: { file, data in
// try data.write(to: file.url)
// }
// )
// }
//
// public static let liveValue = Self.live()
// }
//
// private struct LiveFileClient: @unchecked Sendable {
//
// private let fileManager: FileManager
//
// init(fileManager: FileManager) {
// self.fileManager = fileManager
// }
//
// var homeDir: URL { fileManager.homeDirectoryForCurrentUser }
//
// func isDirectory(url: URL) -> Bool {
// var isDirectory: ObjCBool = false
// fileManager.fileExists(atPath: path(for: url), isDirectory: &isDirectory)
// return isDirectory.boolValue
// }
//
// func isReadable(url: URL) -> Bool {
// fileManager.isReadableFile(atPath: path(for: url))
// }
//
// func fileExists(at path: String) -> Bool {
// fileManager.fileExists(atPath: path)
// }
//
// func findVaultFile(in filePath: String) throws -> URL? {
// let urls = try fileManager
// .contentsOfDirectory(at: URL(filePath: filePath), includingPropertiesForKeys: nil)
//
// if let vault = urls.firstVaultFile {
// return vault
// }
//
// // check for folders that end with "vars" and search those next.
// for folder in urls.filter({ $0.absoluteString.hasSuffix("vars/") }) {
// let files = try fileManager.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil)
// if let vault = files.firstVaultFile {
// return vault
// }
// }
//
// // Fallback to check all sub-folders
// for folder in urls.filter({ self.isDirectory(url: $0) && !$0.absoluteString.hasSuffix("vars/") }) {
// let files = try fileManager.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil)
// if let vault = files.firstVaultFile {
// return vault
// }
// }
// return nil
// }
//
// func loadFile(
// at url: URL,
// into env: inout [String: String],
// decoder: JSONDecoder
// ) throws -> Configuration? {
// @Dependency(\.logger) var logger
// logger.trace("Begin load file for: \(path(for: url))")
//
// if url.absoluteString.hasSuffix(".json") {
// // Handle json file.
// let data = try Data(contentsOf: url)
// return try decoder.decode(Configuration.self, from: data)
// }
//
// let string = try String(contentsOfFile: path(for: url), encoding: .utf8)
//
// logger.trace("Loaded file contents: \(string)")
//
// let lines = string.split(separator: "\n")
// for line in lines {
// logger.trace("Line: \(line)")
// let strippedLine = line.trimmingCharacters(in: .whitespacesAndNewlines)
// let splitLine = strippedLine.split(separator: "=").map {
// $0.replacingOccurrences(of: "\"", with: "")
// }
// logger.trace("Split Line: \(splitLine)")
// guard splitLine.count >= 2 else { continue }
//
// if splitLine.count > 2 {
// let rest = splitLine.dropFirst()
// env[String(splitLine[0])] = String(rest.joined(separator: "="))
// } else {
// env[String(splitLine[0])] = String(splitLine[1])
// }
// }
// return nil
// }
// }
//
// struct DecodingError: Error {}
//
// private extension Array where Element == URL {
// var firstVaultFile: URL? {
// first { $0.absoluteString.hasSuffix("vault.yml") }
// }
// }

View File

@@ -1,110 +1,116 @@
import Dependencies
import Foundation
import ShellClient
@_spi(Internal)
public func findConfigurationFiles(
env: [String: String] = ProcessInfo.processInfo.environment
) throws -> URL {
@Dependency(\.logger) var logger
@Dependency(\.fileClient) var fileClient
logger.debug("Begin find configuration files.")
logger.trace("Env: \(env)")
// Check for environment variable pointing to a directory that the
// the configuration lives.
if let pwd = env["PWD"],
let url = fileClient.checkUrl(.file(pwd, ".hparc"))
{
logger.debug("Found configuration in current working directory.")
return url
}
// Check for environment variable pointing to a file that the
// the configuration lives.
if let configFile = env["HPA_CONFIG_FILE"],
let url = fileClient.checkUrl(.file(configFile))
{
logger.debug("Found configuration from hpa config file env var.")
return url
}
// Check for environment variable pointing to a directory that the
// the configuration lives.
if let configHome = env["HPA_CONFIG_HOME"],
let url = fileClient.checkUrl(.directory(configHome))
{
logger.debug("Found configuration from hpa config home env var.")
return url
}
// Check home directory for a `.hparc` file.
if let url = fileClient.checkUrl(.file(fileClient.homeDir().appending(path: ".hparc"))) {
logger.debug("Found configuration in home directory")
return url
}
// Check in xdg config home, under an hpa-playbook directory.
if let xdgConfigHome = env["XDG_CONFIG_HOME"],
let url = fileClient.checkUrl(.directory(xdgConfigHome, "hpa-playbook"))
{
logger.debug("XDG Config url: \(url.absoluteString)")
return url
}
// We could not find configuration in any usual places.
throw ConfigurationError.configurationNotFound
}
@_spi(Internal)
public func path(for url: URL) -> String {
url.absoluteString.replacing("file://", with: "")
}
enum ConfigurationError: Error {
case configurationNotFound
}
private extension FileClient {
enum ConfigurationUrlCheck {
case file(URL)
case directory(URL)
static func file(_ path: String) -> Self { .file(URL(filePath: path)) }
static func file(_ paths: String...) -> Self {
var url = URL(filePath: paths[0])
url = paths.dropFirst().reduce(into: url) { $0.append(path: $1) }
return .file(url)
}
static func directory(_ path: String) -> Self { .directory(URL(filePath: path)) }
static func directory(_ paths: String...) -> Self {
var url = URL(filePath: paths[0])
url = paths.dropFirst().reduce(into: url) { $0.append(path: $1) }
return .directory(url)
}
}
func checkUrl(_ check: ConfigurationUrlCheck) -> URL? {
switch check {
case let .file(url):
if isReadable(url) { return url }
return nil
case let .directory(url):
return findConfigurationInDirectory(url)
}
}
func findConfigurationInDirectory(_ url: URL) -> URL? {
for file in ["config", "config.json"] {
let fileUrl = url.appending(path: file)
if isReadable(fileUrl) {
return fileUrl
}
}
return nil
}
}
// import Dependencies
// import Foundation
// import ShellClient
//
// @_spi(Internal)
// public func findConfigurationFiles(
// env: [String: String] = ProcessInfo.processInfo.environment
// ) throws -> File {
// @Dependency(\.logger) var logger
// @Dependency(\.fileClient) var fileClient
//
// logger.debug("Begin find configuration files.")
// logger.trace("Env: \(env)")
//
// // Check for environment variable pointing to a directory that the
// // the configuration lives.
// if let pwd = env["PWD"],
// let file = fileClient.checkUrl(.directory(pwd))
// {
// logger.debug("Found configuration in current working directory.")
// return file
// }
//
// // Check for environment variable pointing to a file that the
// // the configuration lives.
// if let configFile = env[EnvironmentKey.hpaConfigFileKey],
// let file = fileClient.checkUrl(.file(configFile))
// {
// logger.debug("Found configuration from hpa config file env var.")
// return file
// }
//
// // Check for environment variable pointing to a directory that the
// // the configuration lives.
// if let configHome = env[EnvironmentKey.hpaConfigDirKey],
// let file = fileClient.checkUrl(.directory(configHome))
// {
// logger.debug("Found configuration from hpa config home env var.")
// return file
// }
//
// // Check home directory for a `.hparc` file.
// if let file = fileClient.checkUrl(.directory(fileClient.homeDir())) {
// logger.debug("Found configuration in home directory")
// return file
// }
//
// // Check in xdg config home, under an hpa-playbook directory.
// if let xdgConfigHome = env[EnvironmentKey.xdgConfigHomeKey],
// let file = fileClient.checkUrl(.directory(xdgConfigHome, HPAKey.hpaConfigDirectoryName))
// {
// logger.debug("XDG Config url: \(file.path)")
// return file
// }
//
// // We could not find configuration in any usual places.
// throw ConfigurationError.configurationNotFound
// }
//
// @_spi(Internal)
// public func path(for url: URL) -> String {
// url.absoluteString.replacing("file://", with: "")
// }
//
// enum ConfigurationError: Error {
// case configurationNotFound
// }
//
// private extension FileClient {
//
// static let validFileNames = [
// "config.json", "config.toml",
// ".hparc.json", ".hparc.toml"
// ]
//
// enum ConfigurationUrlCheck {
// case file(URL)
// case directory(URL)
//
// static func file(_ path: String) -> Self { .file(URL(filePath: path)) }
//
// static func file(_ paths: String...) -> Self {
// var url = URL(filePath: paths[0])
// url = paths.dropFirst().reduce(into: url) { $0.append(path: $1) }
// return .file(url)
// }
//
// static func directory(_ path: String) -> Self { .directory(URL(filePath: path)) }
//
// static func directory(_ paths: String...) -> Self {
// var url = URL(filePath: paths[0])
// url = paths.dropFirst().reduce(into: url) { $0.append(path: $1) }
// return .directory(url)
// }
// }
//
// func checkUrl(_ check: ConfigurationUrlCheck) -> File? {
// switch check {
// case let .file(url):
// if isReadable(url) { return .init(url) }
// return nil
// case let .directory(url):
// return findConfigurationInDirectory(url)
// }
// }
//
// func findConfigurationInDirectory(_ url: URL) -> File? {
// for file in Self.validFileNames {
// let fileUrl = url.appending(path: file)
// if isReadable(fileUrl) {
// return .init(fileUrl)
// }
// }
// return nil
// }
// }