11import Foundation
2-
3- #if canImport(TOML)
4- import TOML
5- #endif
2+ import TOML
63
74// MARK: - Config Loader Errors
85
@@ -13,8 +10,6 @@ enum ConfigError: Error, CustomStringConvertible {
1310 /// Reserved for custom validation. Currently unused — TOML library validates enums via Codable.
1411 case invalidValue( key: String , value: String , validOptions: [ String ] )
1512 case readError( path: String , underlying: Error )
16- /// TOML support not available (Linux Homebrew builds)
17- case tomlNotAvailable
1813
1914 var description : String {
2015 switch self {
@@ -31,9 +26,6 @@ enum ConfigError: Error, CustomStringConvertible {
3126 " Invalid value ' \( value) ' for ' \( key) '. Valid options: \( validOptions. joined ( separator: " , " ) ) "
3227 case . readError( let path, let underlying) :
3328 return " Failed to read ' \( path) ': \( underlying. localizedDescription) "
34- case . tomlNotAvailable:
35- return
36- " Configuration files are not supported in this build. Use CLI flags instead. "
3729 }
3830 }
3931}
@@ -61,31 +53,23 @@ struct ConfigLoader {
6153 /// - Returns: Configuration if found, nil if no config file exists (and explicitPath is nil)
6254 /// - Throws: ConfigError for parsing/validation errors
6355 func loadConfig( explicitPath: String ? ) throws -> Configuration ? {
64- #if canImport(TOML)
65- let path : String
66-
67- if let explicit = explicitPath {
68- // Explicit path must exist
69- guard fileSystem. fileExists ( atPath: explicit) else {
70- throw ConfigError . fileNotFound ( path: explicit)
71- }
72- path = explicit
73- } else {
74- // Search order: CWD, then user config
75- guard let foundPath = findConfigFile ( ) else {
76- return nil // No config file, use defaults
77- }
78- path = foundPath
79- }
56+ let path : String
8057
81- return try parseConfigFile ( at: path)
82- #else
83- // TOML support not available (Homebrew Linux builds)
84- if explicitPath != nil {
85- throw ConfigError . tomlNotAvailable
58+ if let explicit = explicitPath {
59+ // Explicit path must exist
60+ guard fileSystem. fileExists ( atPath: explicit) else {
61+ throw ConfigError . fileNotFound ( path: explicit)
62+ }
63+ path = explicit
64+ } else {
65+ // Search order: CWD, then user config
66+ guard let foundPath = findConfigFile ( ) else {
67+ return nil // No config file, use defaults
8668 }
87- return nil // Silently use CLI defaults when no explicit path requested
88- #endif
69+ path = foundPath
70+ }
71+
72+ return try parseConfigFile ( at: path)
8973 }
9074
9175 /// Generates a template configuration file content with all options commented out
@@ -132,81 +116,87 @@ struct ConfigLoader {
132116
133117 // MARK: - Private Methods
134118
135- #if canImport(TOML)
136- private func findConfigFile( ) -> String ? {
137- // 1. Check current working directory
138- let cwdPath = URL ( fileURLWithPath: fileSystem. currentDirectoryPath)
139- . appendingPathComponent ( Self . configFileName) . path
140- if fileSystem. fileExists ( atPath: cwdPath) {
141- return cwdPath
142- }
143-
144- // 2. Check user config directory
145- let userPath = fileSystem. homeDirectoryForCurrentUser
146- . appendingPathComponent ( Self . userConfigPath) . path
147- if fileSystem. fileExists ( atPath: userPath) {
148- return userPath
149- }
119+ private func findConfigFile( ) -> String ? {
120+ // 1. Check current working directory
121+ let cwdPath = URL ( fileURLWithPath: fileSystem. currentDirectoryPath)
122+ . appendingPathComponent ( Self . configFileName) . path
123+ if fileSystem. fileExists ( atPath: cwdPath) {
124+ return cwdPath
125+ }
150126
151- return nil
127+ // 2. Check user config directory
128+ let userPath = fileSystem. homeDirectoryForCurrentUser
129+ . appendingPathComponent ( Self . userConfigPath) . path
130+ if fileSystem. fileExists ( atPath: userPath) {
131+ return userPath
152132 }
153133
154- private func parseConfigFile( at path: String ) throws -> Configuration {
155- let contents : String
156- do {
157- contents = try fileSystem. contentsOfFile ( atPath: path)
158- } catch {
159- throw ConfigError . readError ( path: path, underlying: error)
160- }
134+ return nil
135+ }
136+
137+ private func parseConfigFile( at path: String ) throws -> Configuration {
138+ let contents : String
139+ do {
140+ contents = try fileSystem. contentsOfFile ( atPath: path)
141+ } catch {
142+ throw ConfigError . readError ( path: path, underlying: error)
143+ }
161144
162- let decoder = TOMLDecoder ( )
145+ let decoder = TOMLDecoder ( )
163146
164- do {
165- return try decoder. decode ( Configuration . self, from: contents)
166- } catch let error as DecodingError {
167- throw mapDecodingError ( error)
168- } catch {
169- // Generic TOML parsing error
170- throw ConfigError . syntaxError ( line: 0 , column: 0 , message: error. localizedDescription)
147+ do {
148+ return try decoder. decode ( Configuration . self, from: contents)
149+ } catch let error as TOMLDecodingError {
150+ // Handle swift-toml 2.0 specific errors with line/column info
151+ switch error {
152+ case . invalidSyntax( let line, let column, let message) :
153+ throw ConfigError . syntaxError ( line: line, column: column, message: message)
154+ default :
155+ throw ConfigError . syntaxError ( line: 0 , column: 0 , message: error. description)
171156 }
157+ } catch let error as DecodingError {
158+ throw mapDecodingError ( error)
159+ } catch {
160+ // Generic parsing error
161+ throw ConfigError . syntaxError ( line: 0 , column: 0 , message: error. localizedDescription)
172162 }
163+ }
173164
174- private func mapDecodingError( _ error: DecodingError ) -> ConfigError {
175- switch error {
176- case . typeMismatch( let type, let context) :
177- let path = context. codingPath. map { $0. stringValue } . joined ( separator: " . " )
178- return . syntaxError(
179- line: 0 ,
180- column: 0 ,
181- message: " Type mismatch at ' \( path) ': expected \( type) "
182- )
183- case . valueNotFound( let type, let context) :
184- let path = context. codingPath. map { $0. stringValue } . joined ( separator: " . " )
185- return . syntaxError(
186- line: 0 ,
187- column: 0 ,
188- message: " Missing value at ' \( path) ': expected \( type) "
189- )
190- case . keyNotFound( let key, let context) :
191- let path =
192- ( context. codingPath. map { $0. stringValue } + [ key. stringValue] ) . joined (
193- separator: " . "
194- )
195- return . syntaxError(
196- line: 0 ,
197- column: 0 ,
198- message: " Missing key: ' \( path) ' "
165+ private func mapDecodingError( _ error: DecodingError ) -> ConfigError {
166+ switch error {
167+ case . typeMismatch( let type, let context) :
168+ let path = context. codingPath. map { $0. stringValue } . joined ( separator: " . " )
169+ return . syntaxError(
170+ line: 0 ,
171+ column: 0 ,
172+ message: " Type mismatch at ' \( path) ': expected \( type) "
173+ )
174+ case . valueNotFound( let type, let context) :
175+ let path = context. codingPath. map { $0. stringValue } . joined ( separator: " . " )
176+ return . syntaxError(
177+ line: 0 ,
178+ column: 0 ,
179+ message: " Missing value at ' \( path) ': expected \( type) "
180+ )
181+ case . keyNotFound( let key, let context) :
182+ let path =
183+ ( context. codingPath. map { $0. stringValue } + [ key. stringValue] ) . joined (
184+ separator: " . "
199185 )
200- case . dataCorrupted( let context) :
201- let path = context. codingPath. map { $0. stringValue } . joined ( separator: " . " )
202- let message =
203- path. isEmpty
204- ? context. debugDescription
205- : " Invalid data at ' \( path) ': \( context. debugDescription) "
206- return . syntaxError( line: 0 , column: 0 , message: message)
207- @unknown default :
208- return . syntaxError( line: 0 , column: 0 , message: error. localizedDescription)
209- }
186+ return . syntaxError(
187+ line: 0 ,
188+ column: 0 ,
189+ message: " Missing key: ' \( path) ' "
190+ )
191+ case . dataCorrupted( let context) :
192+ let path = context. codingPath. map { $0. stringValue } . joined ( separator: " . " )
193+ let message =
194+ path. isEmpty
195+ ? context. debugDescription
196+ : " Invalid data at ' \( path) ': \( context. debugDescription) "
197+ return . syntaxError( line: 0 , column: 0 , message: message)
198+ @unknown default :
199+ return . syntaxError( line: 0 , column: 0 , message: error. localizedDescription)
210200 }
211- #endif
201+ }
212202}
0 commit comments