From fc56f4bef4234b03eb411a6dbc2acb9c546f0815 Mon Sep 17 00:00:00 2001 From: Blazej SLEBODA <5544365+Adobels@users.noreply.github.com> Date: Tue, 16 Dec 2025 15:31:29 +0100 Subject: [PATCH 01/12] Add support for 1, 0 and YES, NO boolean strings in EnvironementVariablesProvider --- .../EnvironmentVariablesProvider.swift | 21 +++++- .../ConfigBoolsFromStringDecoder.swift | 64 +++++++++++++++++++ .../ConfigBoolsFromStringDecoderTests.swift | 45 +++++++++++++ .../EnvironmentVariablesProviderTests.swift | 39 ++++++++++- 4 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 Sources/Configuration/ValueCoders/ConfigBoolsFromStringDecoder.swift create mode 100644 Tests/ConfigurationTests/ConfigBoolsFromStringDecoderTests.swift diff --git a/Sources/Configuration/Providers/EnvironmentVariables/EnvironmentVariablesProvider.swift b/Sources/Configuration/Providers/EnvironmentVariables/EnvironmentVariablesProvider.swift index 1608823..013629c 100644 --- a/Sources/Configuration/Providers/EnvironmentVariables/EnvironmentVariablesProvider.swift +++ b/Sources/Configuration/Providers/EnvironmentVariables/EnvironmentVariablesProvider.swift @@ -142,6 +142,9 @@ public struct EnvironmentVariablesProvider: Sendable { /// A decoder of arrays from a string. var arrayDecoder: EnvironmentValueArrayDecoder + + /// A decoder of bool values from a string + let boolDecoder: BoolDecoder } /// The underlying snapshot of the provider. @@ -165,16 +168,19 @@ public struct EnvironmentVariablesProvider: Sendable { /// - Parameters: /// - secretsSpecifier: Specifies which environment variables should be treated as secrets. /// - bytesDecoder: The decoder used for converting string values to byte arrays. + /// - boolDecoder: The decoder used for converting string values to bool values. /// - arraySeparator: The character used to separate elements in array values. public init( secretsSpecifier: SecretsSpecifier = .none, bytesDecoder: some ConfigBytesFromStringDecoder = .base64, + boolDecoder: BoolDecoder = .allBooleanStrings, arraySeparator: Character = "," ) { self.init( environmentVariables: ProcessInfo.processInfo.environment, secretsSpecifier: secretsSpecifier, bytesDecoder: bytesDecoder, + boolDecoder: boolDecoder, arraySeparator: arraySeparator ) } @@ -200,11 +206,13 @@ public struct EnvironmentVariablesProvider: Sendable { /// - environmentVariables: A dictionary of environment variable names and values. /// - secretsSpecifier: Specifies which environment variables should be treated as secrets. /// - bytesDecoder: The decoder used for converting string values to byte arrays. + /// - boolDecoder: The decoder used for converting string to bool values. /// - arraySeparator: The character used to separate elements in array values. public init( environmentVariables: [String: String], secretsSpecifier: SecretsSpecifier = .none, bytesDecoder: some ConfigBytesFromStringDecoder = .base64, + boolDecoder: BoolDecoder = .allBooleanStrings, arraySeparator: Character = "," ) { let tuples: [(String, EnvironmentValue)] = environmentVariables.map { key, value in @@ -219,7 +227,8 @@ public struct EnvironmentVariablesProvider: Sendable { self._snapshot = .init( environmentVariables: Dictionary(uniqueKeysWithValues: tuples), bytesDecoder: bytesDecoder, - arrayDecoder: EnvironmentValueArrayDecoder(separator: arraySeparator) + arrayDecoder: EnvironmentValueArrayDecoder(separator: arraySeparator), + boolDecoder: boolDecoder ) } @@ -245,6 +254,7 @@ public struct EnvironmentVariablesProvider: Sendable { /// - When `true`, if the file is missing, treats it as empty. Malformed files still throw an error. /// - secretsSpecifier: Specifies which environment variables should be treated as secrets. /// - bytesDecoder: The decoder used for converting string values to byte arrays. + /// - boolDecoder: The decoder used for converting string values to bool values. /// - arraySeparator: The character used to separate elements in array values. /// - Throws: If the file is malformed, or if missing when allowMissing is `false`. public init( @@ -252,6 +262,7 @@ public struct EnvironmentVariablesProvider: Sendable { allowMissing: Bool = false, secretsSpecifier: SecretsSpecifier = .none, bytesDecoder: some ConfigBytesFromStringDecoder = .base64, + boolDecoder: BoolDecoder = .allBooleanStrings, arraySeparator: Character = "," ) async throws { try await self.init( @@ -260,6 +271,7 @@ public struct EnvironmentVariablesProvider: Sendable { fileSystem: LocalCommonProviderFileSystem(), secretsSpecifier: secretsSpecifier, bytesDecoder: bytesDecoder, + boolDecoder: boolDecoder, arraySeparator: arraySeparator ) } @@ -273,6 +285,7 @@ public struct EnvironmentVariablesProvider: Sendable { /// - fileSystem: The file system implementation to use. /// - secretsSpecifier: Specifies which environment variables should be treated as secrets. /// - bytesDecoder: The decoder used for converting string values to byte arrays. + /// - boolDecoder: The decoder used for converting string values to bool values. /// - arraySeparator: The character used to separate elements in array values. /// - Throws: If the file is malformed, or if missing when allowMissing is `false`. internal init( @@ -281,6 +294,7 @@ public struct EnvironmentVariablesProvider: Sendable { fileSystem: some CommonProviderFileSystem, secretsSpecifier: SecretsSpecifier = .none, bytesDecoder: some ConfigBytesFromStringDecoder = .base64, + boolDecoder: BoolDecoder = .allBooleanStrings, arraySeparator: Character = "," ) async throws { let loadedData = try await fileSystem.fileContents(atPath: environmentFilePath) @@ -297,6 +311,7 @@ public struct EnvironmentVariablesProvider: Sendable { environmentVariables: EnvironmentFileParser.parsed(contents), secretsSpecifier: secretsSpecifier, bytesDecoder: bytesDecoder, + boolDecoder: boolDecoder, arraySeparator: arraySeparator ) } @@ -393,7 +408,7 @@ extension EnvironmentVariablesProvider.Snapshot { } content = .double(doubleValue) case .bool: - guard let boolValue = Bool(stringValue) else { + guard let boolValue = boolDecoder.decodeBool(from: stringValue) else { try throwMismatch() } content = .bool(boolValue) @@ -426,7 +441,7 @@ extension EnvironmentVariablesProvider.Snapshot { case .boolArray: let arrayValue = arrayDecoder.decode(stringValue) let boolArray = try arrayValue.map { stringValue in - guard let boolValue = Bool(stringValue) else { + guard let boolValue = boolDecoder.decodeBool(from: stringValue) else { try throwMismatch() } return boolValue diff --git a/Sources/Configuration/ValueCoders/ConfigBoolsFromStringDecoder.swift b/Sources/Configuration/ValueCoders/ConfigBoolsFromStringDecoder.swift new file mode 100644 index 0000000..6d92e6f --- /dev/null +++ b/Sources/Configuration/ValueCoders/ConfigBoolsFromStringDecoder.swift @@ -0,0 +1,64 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftConfiguration open source project +// +// Copyright (c) 2025 Apple Inc. and the SwiftConfiguration project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftConfiguration project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +/// A decoder that converts a boolean string into a Bool, taking into account different boolean string pairs. +/// +/// This decoder is able to convert a string to Bool values when the string’s Boolean value format is 0 or 1, true or false, or yes or no. +/// Boolean strings taken into account when decoding are configurable at init time only. +/// +/// ## Boolean values +/// +/// By default, following boolean string pairs are decoded to a Bool value: trueFalse (true, false), oneZero (1, 0), yesNo (yes, no). +/// Decoding is case-insensitive. +@available(Configuration 1.0, *) +public struct BoolDecoder: Sendable { + + public enum BooleanString: Sendable { case trueFalse, oneZero, yesNo } + + public static var allBooleanStrings: Self { .init(booleanStrings: [.trueFalse, .oneZero, .yesNo]) } + + private let booleanStrings: [BooleanString] + + public init(booleanStrings: [BooleanString]) { + self.booleanStrings = booleanStrings + } + + func decodeBool(from string: String) -> Bool? { + for semantic in self.booleanStrings { + switch semantic { + case .trueFalse: + let stringLowercased = string.lowercased() + if ["true", "false"].contains(stringLowercased) { + return stringLowercased == "true" + } + case .oneZero: + if ["1", "0"].contains(string) { + return string == "1" + } + case .yesNo: + let stringLowercased = string.lowercased() + if ["yes", "no"].contains(stringLowercased) { + return stringLowercased == "yes" + } + } + } + return nil + } +} diff --git a/Tests/ConfigurationTests/ConfigBoolsFromStringDecoderTests.swift b/Tests/ConfigurationTests/ConfigBoolsFromStringDecoderTests.swift new file mode 100644 index 0000000..70bcb92 --- /dev/null +++ b/Tests/ConfigurationTests/ConfigBoolsFromStringDecoderTests.swift @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftConfiguration open source project +// +// Copyright (c) 2025 Apple Inc. and the SwiftConfiguration project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftConfiguration project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing +import Foundation +@testable import Configuration + +struct ConfigBoolsFromStringDecoderTests { + + @available(Configuration 1.0, *) + @Test("boolDecoder, all boolean strings enabled") + func boolDecoderAllBooleanStringsEnabled() throws { + let bd = BoolDecoder(booleanStrings: [.oneZero, .trueFalse, .yesNo]) + #expect(bd.decodeBool(from: "1") == true) + #expect(bd.decodeBool(from: "0") == false) + #expect(["Yes", "yes", "YES", "yES"].allSatisfy { bd.decodeBool(from: $0) == true }) + #expect(["No", "no", "NO", "nO"].allSatisfy { bd.decodeBool(from: $0) == false }) + #expect(["true", "TRUE", "trUe"].allSatisfy { bd.decodeBool(from: $0) == true }) + #expect(["false", "FALSE", "faLse"].allSatisfy { bd.decodeBool(from: $0) == false }) + #expect(["_true_", "_false_", "11", "00"].allSatisfy { bd.decodeBool(from: $0) == nil }) + } + + @available(Configuration 1.0, *) + @Test("boolDecoder, only .oneZero boolean strings enabled") + func boolDecoderOnlyOneZeroBooleanStringsEnabled() throws { + let bd = BoolDecoder(booleanStrings: [.oneZero]) + #expect(bd.decodeBool(from: "1") == true) + #expect(bd.decodeBool(from: "0") == false) + #expect(bd.decodeBool(from: "true") == nil) + #expect(bd.decodeBool(from: "false") == nil) + #expect(bd.decodeBool(from: "yes") == nil) + #expect(bd.decodeBool(from: "no") == nil) + } +} diff --git a/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift b/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift index ab72485..89da9fb 100644 --- a/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift +++ b/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift @@ -33,6 +33,10 @@ struct EnvironmentVariablesProviderTests { "OTHER_DOUBLE": "2.72", "BOOL": "true", "OTHER_BOOL": "false", + "OTHER_BOOL_1": "1", + "OTHER_BOOL_0": "0", + "OTHER_BOOL_YES": "YES", + "OTHER_BOOL_NO": "NO", "BYTES": "bWFnaWM=", "OTHER_BYTES": "bWFnaWMy", "STRINGY_ARRAY": "Hello,World", @@ -43,6 +47,8 @@ struct EnvironmentVariablesProviderTests { "OTHER_DOUBLY_ARRAY": "0.9,1.8", "BOOLY_ARRAY": "true,false", "OTHER_BOOLY_ARRAY": "false,true,true", + "OTHER_BOOLY_ARRAY_1_0": "0, 1, 1", + "OTHER_BOOLY_ARRAY_YES_NO": "NO, YES, YES", "BYTE_CHUNKY_ARRAY": "bWFnaWM=,bWFnaWMy", "OTHER_BYTE_CHUNKY_ARRAY": "bWFnaWM=,bWFnaWMy,bWFnaWM=", ], @@ -55,7 +61,7 @@ struct EnvironmentVariablesProviderTests { @available(Configuration 1.0, *) @Test func printingDescription() throws { let expectedDescription = #""" - EnvironmentVariablesProvider[20 values] + EnvironmentVariablesProvider[26 values] """# #expect(provider.description == expectedDescription) } @@ -63,11 +69,40 @@ struct EnvironmentVariablesProviderTests { @available(Configuration 1.0, *) @Test func printingDebugDescription() throws { let expectedDebugDescription = #""" - EnvironmentVariablesProvider[20 values: BOOL=true, BOOLY_ARRAY=true,false, BYTES=bWFnaWM=, BYTE_CHUNKY_ARRAY=bWFnaWM=,bWFnaWMy, DOUBLE=3.14, DOUBLY_ARRAY=3.14,2.72, INT=42, INTY_ARRAY=42,24, OTHER_BOOL=false, OTHER_BOOLY_ARRAY=false,true,true, OTHER_BYTES=bWFnaWMy, OTHER_BYTE_CHUNKY_ARRAY=bWFnaWM=,bWFnaWMy,bWFnaWM=, OTHER_DOUBLE=2.72, OTHER_DOUBLY_ARRAY=0.9,1.8, OTHER_INT=24, OTHER_INTY_ARRAY=16,32, OTHER_STRING=Other Hello, OTHER_STRINGY_ARRAY=Hello,Swift, STRING=, STRINGY_ARRAY=Hello,World] + EnvironmentVariablesProvider[26 values: BOOL=true, BOOLY_ARRAY=true,false, BYTES=bWFnaWM=, BYTE_CHUNKY_ARRAY=bWFnaWM=,bWFnaWMy, DOUBLE=3.14, DOUBLY_ARRAY=3.14,2.72, INT=42, INTY_ARRAY=42,24, OTHER_BOOL=false, OTHER_BOOLY_ARRAY=false,true,true, OTHER_BOOLY_ARRAY_1_0=0, 1, 1, OTHER_BOOLY_ARRAY_YES_NO=NO, YES, YES, OTHER_BOOL_0=0, OTHER_BOOL_1=1, OTHER_BOOL_NO=NO, OTHER_BOOL_YES=YES, OTHER_BYTES=bWFnaWMy, OTHER_BYTE_CHUNKY_ARRAY=bWFnaWM=,bWFnaWMy,bWFnaWM=, OTHER_DOUBLE=2.72, OTHER_DOUBLY_ARRAY=0.9,1.8, OTHER_INT=24, OTHER_INTY_ARRAY=16,32, OTHER_STRING=Other Hello, OTHER_STRINGY_ARRAY=Hello,Swift, STRING=, STRINGY_ARRAY=Hello,World] """# #expect(provider.debugDescription == expectedDebugDescription) } + @available(Configuration 1.0, *) + @Test func valuesForKeys() throws { + #expect(try provider.value(forKey: "OTHER_BOOL_1", type: .bool).value == true) + #expect(try provider.value(forKey: "OTHER_BOOL_0", type: .bool).value == false) + #expect(try provider.value(forKey: "OTHER_BOOL_YES", type: .bool).value == true) + #expect(try provider.value(forKey: "OTHER_BOOL_NO", type: .bool).value == false) + #expect( + try provider + .value(forKey: "OTHER_BOOLY_ARRAY", type: .boolArray).value == .init( + [false, true, true], + isSecret: false + ) + ) + #expect( + try provider + .value(forKey: "OTHER_BOOLY_ARRAY_1_0", type: .boolArray).value == .init( + [false, true, true], + isSecret: false + ) + ) + #expect( + try provider + .value(forKey: "OTHER_BOOLY_ARRAY_YES_NO", type: .boolArray).value == .init( + [false, true, true], + isSecret: false + ) + ) + } + @available(Configuration 1.0, *) @Test func compat() async throws { try await ProviderCompatTest(provider: provider).runTest() From 9b6bc7b9f253186d63afa456921852a5892c5735 Mon Sep 17 00:00:00 2001 From: Blazej SLEBODA <5544365+Adobels@users.noreply.github.com> Date: Tue, 16 Dec 2025 16:49:30 +0100 Subject: [PATCH 02/12] Update docc documentation --- .../Reference/EnvironmentVariablesProvider.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Configuration/Documentation.docc/Reference/EnvironmentVariablesProvider.md b/Sources/Configuration/Documentation.docc/Reference/EnvironmentVariablesProvider.md index 8b20dbf..b0eead7 100644 --- a/Sources/Configuration/Documentation.docc/Reference/EnvironmentVariablesProvider.md +++ b/Sources/Configuration/Documentation.docc/Reference/EnvironmentVariablesProvider.md @@ -4,9 +4,9 @@ ### Creating an environment variable provider -- ``init(secretsSpecifier:bytesDecoder:arraySeparator:)`` -- ``init(environmentVariables:secretsSpecifier:bytesDecoder:arraySeparator:)`` -- ``init(environmentFilePath:allowMissing:secretsSpecifier:bytesDecoder:arraySeparator:)`` +- ``init(secretsSpecifier:bytesDecoder:boolDecoder:arraySeparator:)`` +- ``init(environmentVariables:secretsSpecifier:bytesDecoder:boolDecoder:arraySeparator:)`` +- ``init(environmentFilePath:allowMissing:secretsSpecifier:bytesDecoder:boolDecoder:arraySeparator:)`` ### Inspecting an environment variable provider From 85667b46e7789afc85fb83d0f7412c1c84846e97 Mon Sep 17 00:00:00 2001 From: Blazej SLEBODA <5544365+Adobels@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:53:18 +0100 Subject: [PATCH 03/12] Address review comments - Remove boolDecoder init customization - Revert changes to the default set of values in tests - Create a dedicated test to cover various string boolean variants --- .../EnvironmentVariablesProvider.swift | 8 +- .../ConfigBoolsFromStringDecoder.swift | 43 +++------- .../ConfigBoolsFromStringDecoderTests.swift | 38 ++++----- .../ConfigSnapshotReaderMethodTestsGet2.swift | 83 +++++-------------- .../EnvironmentVariablesProviderTests.swift | 64 +++++++------- 5 files changed, 86 insertions(+), 150 deletions(-) diff --git a/Sources/Configuration/Providers/EnvironmentVariables/EnvironmentVariablesProvider.swift b/Sources/Configuration/Providers/EnvironmentVariables/EnvironmentVariablesProvider.swift index 013629c..c793270 100644 --- a/Sources/Configuration/Providers/EnvironmentVariables/EnvironmentVariablesProvider.swift +++ b/Sources/Configuration/Providers/EnvironmentVariables/EnvironmentVariablesProvider.swift @@ -173,7 +173,7 @@ public struct EnvironmentVariablesProvider: Sendable { public init( secretsSpecifier: SecretsSpecifier = .none, bytesDecoder: some ConfigBytesFromStringDecoder = .base64, - boolDecoder: BoolDecoder = .allBooleanStrings, + boolDecoder: BoolDecoder = .init(), arraySeparator: Character = "," ) { self.init( @@ -212,7 +212,7 @@ public struct EnvironmentVariablesProvider: Sendable { environmentVariables: [String: String], secretsSpecifier: SecretsSpecifier = .none, bytesDecoder: some ConfigBytesFromStringDecoder = .base64, - boolDecoder: BoolDecoder = .allBooleanStrings, + boolDecoder: BoolDecoder = .init(), arraySeparator: Character = "," ) { let tuples: [(String, EnvironmentValue)] = environmentVariables.map { key, value in @@ -262,7 +262,7 @@ public struct EnvironmentVariablesProvider: Sendable { allowMissing: Bool = false, secretsSpecifier: SecretsSpecifier = .none, bytesDecoder: some ConfigBytesFromStringDecoder = .base64, - boolDecoder: BoolDecoder = .allBooleanStrings, + boolDecoder: BoolDecoder = .init(), arraySeparator: Character = "," ) async throws { try await self.init( @@ -294,7 +294,7 @@ public struct EnvironmentVariablesProvider: Sendable { fileSystem: some CommonProviderFileSystem, secretsSpecifier: SecretsSpecifier = .none, bytesDecoder: some ConfigBytesFromStringDecoder = .base64, - boolDecoder: BoolDecoder = .allBooleanStrings, + boolDecoder: BoolDecoder = .init(), arraySeparator: Character = "," ) async throws { let loadedData = try await fileSystem.fileContents(atPath: environmentFilePath) diff --git a/Sources/Configuration/ValueCoders/ConfigBoolsFromStringDecoder.swift b/Sources/Configuration/ValueCoders/ConfigBoolsFromStringDecoder.swift index 6d92e6f..79fa852 100644 --- a/Sources/Configuration/ValueCoders/ConfigBoolsFromStringDecoder.swift +++ b/Sources/Configuration/ValueCoders/ConfigBoolsFromStringDecoder.swift @@ -21,44 +21,27 @@ import Foundation /// A decoder that converts a boolean string into a Bool, taking into account different boolean string pairs. /// /// This decoder is able to convert a string to Bool values when the string’s Boolean value format is 0 or 1, true or false, or yes or no. -/// Boolean strings taken into account when decoding are configurable at init time only. /// /// ## Boolean values /// -/// By default, following boolean string pairs are decoded to a Bool value: trueFalse (true, false), oneZero (1, 0), yesNo (yes, no). +/// Following boolean string pairs are decoded to a Bool value: trueFalse (true, false), oneZero (1, 0), yesNo (yes, no). /// Decoding is case-insensitive. @available(Configuration 1.0, *) public struct BoolDecoder: Sendable { - public enum BooleanString: Sendable { case trueFalse, oneZero, yesNo } + /// Creates a new bool decoder. + public init() {} - public static var allBooleanStrings: Self { .init(booleanStrings: [.trueFalse, .oneZero, .yesNo]) } - - private let booleanStrings: [BooleanString] - - public init(booleanStrings: [BooleanString]) { - self.booleanStrings = booleanStrings - } - - func decodeBool(from string: String) -> Bool? { - for semantic in self.booleanStrings { - switch semantic { - case .trueFalse: - let stringLowercased = string.lowercased() - if ["true", "false"].contains(stringLowercased) { - return stringLowercased == "true" - } - case .oneZero: - if ["1", "0"].contains(string) { - return string == "1" - } - case .yesNo: - let stringLowercased = string.lowercased() - if ["yes", "no"].contains(stringLowercased) { - return stringLowercased == "yes" - } - } + public func decodeBool(from string: String) -> Bool? { + let stringLowercased = string.lowercased() + return if ["true", "false"].contains(stringLowercased) { + stringLowercased == "true" + } else if ["yes", "no"].contains(stringLowercased) { + stringLowercased == "yes" + } else if ["1", "0"].contains(stringLowercased) { + stringLowercased == "1" + } else { + nil } - return nil } } diff --git a/Tests/ConfigurationTests/ConfigBoolsFromStringDecoderTests.swift b/Tests/ConfigurationTests/ConfigBoolsFromStringDecoderTests.swift index 70bcb92..a3445fa 100644 --- a/Tests/ConfigurationTests/ConfigBoolsFromStringDecoderTests.swift +++ b/Tests/ConfigurationTests/ConfigBoolsFromStringDecoderTests.swift @@ -18,28 +18,24 @@ import Foundation struct ConfigBoolsFromStringDecoderTests { + @Test() @available(Configuration 1.0, *) - @Test("boolDecoder, all boolean strings enabled") - func boolDecoderAllBooleanStringsEnabled() throws { - let bd = BoolDecoder(booleanStrings: [.oneZero, .trueFalse, .yesNo]) - #expect(bd.decodeBool(from: "1") == true) - #expect(bd.decodeBool(from: "0") == false) - #expect(["Yes", "yes", "YES", "yES"].allSatisfy { bd.decodeBool(from: $0) == true }) - #expect(["No", "no", "NO", "nO"].allSatisfy { bd.decodeBool(from: $0) == false }) - #expect(["true", "TRUE", "trUe"].allSatisfy { bd.decodeBool(from: $0) == true }) - #expect(["false", "FALSE", "faLse"].allSatisfy { bd.decodeBool(from: $0) == false }) - #expect(["_true_", "_false_", "11", "00"].allSatisfy { bd.decodeBool(from: $0) == nil }) - } + func stringToBool() throws { + let bd = BoolDecoder() + let cases: [(expected: Bool?, input: [String])] = [ + (true, ["1"]), + (false, ["0"]), + (true, ["Yes", "yes", "YES", "yES"]), + (false, ["No", "no", "NO", "nO"]), + (true, ["true", "TRUE", "trUe"]), + (false, ["false", "FALSE", "faLse"]), + (nil, ["", "_true_", "_false_", "_yes_", "_no_", "_1_", "_0_", "11", "00"]) + ] - @available(Configuration 1.0, *) - @Test("boolDecoder, only .oneZero boolean strings enabled") - func boolDecoderOnlyOneZeroBooleanStringsEnabled() throws { - let bd = BoolDecoder(booleanStrings: [.oneZero]) - #expect(bd.decodeBool(from: "1") == true) - #expect(bd.decodeBool(from: "0") == false) - #expect(bd.decodeBool(from: "true") == nil) - #expect(bd.decodeBool(from: "false") == nil) - #expect(bd.decodeBool(from: "yes") == nil) - #expect(bd.decodeBool(from: "no") == nil) + for (expected, inputs) in cases { + for input in inputs { + #expect(bd.decodeBool(from: input) == expected, "input: \(input)") + } + } } } diff --git a/Tests/ConfigurationTests/ConfigReaderTests/ConfigSnapshotReaderMethodTestsGet2.swift b/Tests/ConfigurationTests/ConfigReaderTests/ConfigSnapshotReaderMethodTestsGet2.swift index 1ae2735..991475b 100644 --- a/Tests/ConfigurationTests/ConfigReaderTests/ConfigSnapshotReaderMethodTestsGet2.swift +++ b/Tests/ConfigurationTests/ConfigReaderTests/ConfigSnapshotReaderMethodTestsGet2.swift @@ -12,18 +12,20 @@ // //===----------------------------------------------------------------------===// -// ############################################################################# -// # # -// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # -// # # -// ############################################################################# + + // ############################################################################# + // # # + // # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # + // # # + // ############################################################################# + import Testing @testable import Configuration import ConfigurationTestingInternal struct ConfigSnapshotReaderMethodTestsGet2 { - + typealias Defaults = ConfigReaderTests.Defaults typealias TestEnum = ConfigReaderTests.TestEnum typealias TestStringConvertible = ConfigReaderTests.TestStringConvertible @@ -32,14 +34,11 @@ struct ConfigSnapshotReaderMethodTestsGet2 { @Test func get() async throws { let config = ConfigReaderTests.config - do { + do { let snapshot = config.snapshot() // Optional - success - #expect( - snapshot.string(forKey: "stringConvertible", as: TestStringConvertible.self) - == Defaults.stringConvertible - ) + #expect(snapshot.string(forKey: "stringConvertible", as: TestStringConvertible.self) == Defaults.stringConvertible) // Optional - missing #expect(snapshot.string(forKey: "absentStringConvertible", as: TestStringConvertible.self) == nil) @@ -48,50 +47,25 @@ struct ConfigSnapshotReaderMethodTestsGet2 { #expect(snapshot.string(forKey: "failure", as: TestStringConvertible.self) == nil) // Defaulted - success - #expect( - snapshot.string( - forKey: "stringConvertible", - as: TestStringConvertible.self, - default: Defaults.otherStringConvertible - ) == Defaults.stringConvertible - ) + #expect(snapshot.string(forKey: "stringConvertible", as: TestStringConvertible.self, default: Defaults.otherStringConvertible) == Defaults.stringConvertible) // Defaulted - missing - #expect( - snapshot.string( - forKey: "absentStringConvertible", - as: TestStringConvertible.self, - default: Defaults.otherStringConvertible - ) == Defaults.otherStringConvertible - ) + #expect(snapshot.string(forKey: "absentStringConvertible", as: TestStringConvertible.self, default: Defaults.otherStringConvertible) == Defaults.otherStringConvertible) // Defaulted - failing - #expect( - snapshot.string( - forKey: "failure", - as: TestStringConvertible.self, - default: Defaults.otherStringConvertible - ) == Defaults.otherStringConvertible - ) + #expect(snapshot.string(forKey: "failure", as: TestStringConvertible.self, default: Defaults.otherStringConvertible) == Defaults.otherStringConvertible) // Required - success - try #expect( - snapshot.requiredString(forKey: "stringConvertible", as: TestStringConvertible.self) - == Defaults.stringConvertible - ) + try #expect(snapshot.requiredString(forKey: "stringConvertible", as: TestStringConvertible.self) == Defaults.stringConvertible) // Required - missing - let error1 = #expect(throws: ConfigError.self) { - try snapshot.requiredString(forKey: "absentStringConvertible", as: TestStringConvertible.self) - } + let error1 = #expect(throws: ConfigError.self) { try snapshot.requiredString(forKey: "absentStringConvertible", as: TestStringConvertible.self) } #expect(error1 == .missingRequiredConfigValue(AbsoluteConfigKey(["absentStringConvertible"]))) // Required - failing - #expect(throws: TestProvider.TestError.self) { - try snapshot.requiredString(forKey: "failure", as: TestStringConvertible.self) - } + #expect(throws: TestProvider.TestError.self) { try snapshot.requiredString(forKey: "failure", as: TestStringConvertible.self) } } - do { + do { let snapshot = config.snapshot() // Optional - success @@ -104,36 +78,23 @@ struct ConfigSnapshotReaderMethodTestsGet2 { #expect(snapshot.string(forKey: "failure", as: TestEnum.self) == nil) // Defaulted - success - #expect( - snapshot.string(forKey: "stringEnum", as: TestEnum.self, default: Defaults.otherStringEnum) - == Defaults.stringEnum - ) + #expect(snapshot.string(forKey: "stringEnum", as: TestEnum.self, default: Defaults.otherStringEnum) == Defaults.stringEnum) // Defaulted - missing - #expect( - snapshot.string(forKey: "absentStringEnum", as: TestEnum.self, default: Defaults.otherStringEnum) - == Defaults.otherStringEnum - ) + #expect(snapshot.string(forKey: "absentStringEnum", as: TestEnum.self, default: Defaults.otherStringEnum) == Defaults.otherStringEnum) // Defaulted - failing - #expect( - snapshot.string(forKey: "failure", as: TestEnum.self, default: Defaults.otherStringEnum) - == Defaults.otherStringEnum - ) + #expect(snapshot.string(forKey: "failure", as: TestEnum.self, default: Defaults.otherStringEnum) == Defaults.otherStringEnum) // Required - success try #expect(snapshot.requiredString(forKey: "stringEnum", as: TestEnum.self) == Defaults.stringEnum) // Required - missing - let error1 = #expect(throws: ConfigError.self) { - try snapshot.requiredString(forKey: "absentStringEnum", as: TestEnum.self) - } + let error1 = #expect(throws: ConfigError.self) { try snapshot.requiredString(forKey: "absentStringEnum", as: TestEnum.self) } #expect(error1 == .missingRequiredConfigValue(AbsoluteConfigKey(["absentStringEnum"]))) // Required - failing - #expect(throws: TestProvider.TestError.self) { - try snapshot.requiredString(forKey: "failure", as: TestEnum.self) - } + #expect(throws: TestProvider.TestError.self) { try snapshot.requiredString(forKey: "failure", as: TestEnum.self) } } } } diff --git a/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift b/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift index 89da9fb..ab06b22 100644 --- a/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift +++ b/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift @@ -33,10 +33,6 @@ struct EnvironmentVariablesProviderTests { "OTHER_DOUBLE": "2.72", "BOOL": "true", "OTHER_BOOL": "false", - "OTHER_BOOL_1": "1", - "OTHER_BOOL_0": "0", - "OTHER_BOOL_YES": "YES", - "OTHER_BOOL_NO": "NO", "BYTES": "bWFnaWM=", "OTHER_BYTES": "bWFnaWMy", "STRINGY_ARRAY": "Hello,World", @@ -47,8 +43,6 @@ struct EnvironmentVariablesProviderTests { "OTHER_DOUBLY_ARRAY": "0.9,1.8", "BOOLY_ARRAY": "true,false", "OTHER_BOOLY_ARRAY": "false,true,true", - "OTHER_BOOLY_ARRAY_1_0": "0, 1, 1", - "OTHER_BOOLY_ARRAY_YES_NO": "NO, YES, YES", "BYTE_CHUNKY_ARRAY": "bWFnaWM=,bWFnaWMy", "OTHER_BYTE_CHUNKY_ARRAY": "bWFnaWM=,bWFnaWMy,bWFnaWM=", ], @@ -61,7 +55,7 @@ struct EnvironmentVariablesProviderTests { @available(Configuration 1.0, *) @Test func printingDescription() throws { let expectedDescription = #""" - EnvironmentVariablesProvider[26 values] + EnvironmentVariablesProvider[20 values] """# #expect(provider.description == expectedDescription) } @@ -69,38 +63,40 @@ struct EnvironmentVariablesProviderTests { @available(Configuration 1.0, *) @Test func printingDebugDescription() throws { let expectedDebugDescription = #""" - EnvironmentVariablesProvider[26 values: BOOL=true, BOOLY_ARRAY=true,false, BYTES=bWFnaWM=, BYTE_CHUNKY_ARRAY=bWFnaWM=,bWFnaWMy, DOUBLE=3.14, DOUBLY_ARRAY=3.14,2.72, INT=42, INTY_ARRAY=42,24, OTHER_BOOL=false, OTHER_BOOLY_ARRAY=false,true,true, OTHER_BOOLY_ARRAY_1_0=0, 1, 1, OTHER_BOOLY_ARRAY_YES_NO=NO, YES, YES, OTHER_BOOL_0=0, OTHER_BOOL_1=1, OTHER_BOOL_NO=NO, OTHER_BOOL_YES=YES, OTHER_BYTES=bWFnaWMy, OTHER_BYTE_CHUNKY_ARRAY=bWFnaWM=,bWFnaWMy,bWFnaWM=, OTHER_DOUBLE=2.72, OTHER_DOUBLY_ARRAY=0.9,1.8, OTHER_INT=24, OTHER_INTY_ARRAY=16,32, OTHER_STRING=Other Hello, OTHER_STRINGY_ARRAY=Hello,Swift, STRING=, STRINGY_ARRAY=Hello,World] + EnvironmentVariablesProvider[20 values: BOOL=true, BOOLY_ARRAY=true,false, BYTES=bWFnaWM=, BYTE_CHUNKY_ARRAY=bWFnaWM=,bWFnaWMy, DOUBLE=3.14, DOUBLY_ARRAY=3.14,2.72, INT=42, INTY_ARRAY=42,24, OTHER_BOOL=false, OTHER_BOOLY_ARRAY=false,true,true, OTHER_BYTES=bWFnaWMy, OTHER_BYTE_CHUNKY_ARRAY=bWFnaWM=,bWFnaWMy,bWFnaWM=, OTHER_DOUBLE=2.72, OTHER_DOUBLY_ARRAY=0.9,1.8, OTHER_INT=24, OTHER_INTY_ARRAY=16,32, OTHER_STRING=Other Hello, OTHER_STRINGY_ARRAY=Hello,Swift, STRING=, STRINGY_ARRAY=Hello,World] """# #expect(provider.debugDescription == expectedDebugDescription) } @available(Configuration 1.0, *) - @Test func valuesForKeys() throws { - #expect(try provider.value(forKey: "OTHER_BOOL_1", type: .bool).value == true) - #expect(try provider.value(forKey: "OTHER_BOOL_0", type: .bool).value == false) - #expect(try provider.value(forKey: "OTHER_BOOL_YES", type: .bool).value == true) - #expect(try provider.value(forKey: "OTHER_BOOL_NO", type: .bool).value == false) - #expect( - try provider - .value(forKey: "OTHER_BOOLY_ARRAY", type: .boolArray).value == .init( - [false, true, true], - isSecret: false - ) - ) - #expect( - try provider - .value(forKey: "OTHER_BOOLY_ARRAY_1_0", type: .boolArray).value == .init( - [false, true, true], - isSecret: false - ) - ) - #expect( - try provider - .value(forKey: "OTHER_BOOLY_ARRAY_YES_NO", type: .boolArray).value == .init( - [false, true, true], - isSecret: false - ) - ) + @Test func boolDecoderValuesForKeys() throws { + let ep = EnvironmentVariablesProvider( + environmentVariables: [ + "BOOL_TRUE": "true", + "BOOL_FALSE": "false", + "BOOL_1": "1", + "BOOL_0": "0", + "BOOL_YES": "YES", + "BOOL_NO": "NO", + "BOOL_THROWS_ERROR_EMPTY": "", + "BOOL_THROWS_ERROR_NOT_BOOL_STRING": "2", + "BOOLY_ARRAY_TRUE": "true,1,,YES", + "BOOLY_ARRAY_FALSE": "false,0,NO", + "BOOLY_ARRAY_THROWS_1": "true,1,YESS", + "BOOLY_ARRAY_THROWS_2": "false,00,no", + ]) + #expect(try ep.value(forKey: "BOOL_TRUE", type: .bool).value == true) + #expect(try ep.value(forKey: "BOOL_FALSE", type: .bool).value == false) + #expect(try ep.value(forKey: "BOOL_1", type: .bool).value == true) + #expect(try ep.value(forKey: "BOOL_0", type: .bool).value == false) + #expect(try ep.value(forKey: "BOOL_YES", type: .bool).value == true) + #expect(try ep.value(forKey: "BOOL_NO", type: .bool).value == false) + #expect(throws: ConfigError.self) { try ep.value(forKey: "BOOL_THROWS_ERROR_EMPTY", type: .bool) } + #expect(throws: ConfigError.self) { try ep.value(forKey: "BOOL_THROWS_ERROR_NOT_BOOL_STRING", type: .bool) } + #expect(try ep.value(forKey: "BOOLY_ARRAY_TRUE", type: .boolArray).value == .init([true, true, true], isSecret: false)) + #expect(try ep.value(forKey: "BOOLY_ARRAY_FALSE", type: .boolArray).value == .init([false, false, false], isSecret: false)) + #expect(throws: ConfigError.self) { try ep.value(forKey: "BOOLY_ARRAY_THROWS_1", type: .boolArray) } + #expect(throws: ConfigError.self) { try ep.value(forKey: "BOOLY_ARRAY_THROWS_2", type: .boolArray) } } @available(Configuration 1.0, *) From 5ba67cb414d9f00461b5ff17a291d45e16cf95d2 Mon Sep 17 00:00:00 2001 From: Blazej SLEBODA <5544365+Adobels@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:03:56 +0100 Subject: [PATCH 04/12] Delete a generated file --- .../ConfigSnapshotReaderMethodTestsGet2.swift | 100 ------------------ 1 file changed, 100 deletions(-) delete mode 100644 Tests/ConfigurationTests/ConfigReaderTests/ConfigSnapshotReaderMethodTestsGet2.swift diff --git a/Tests/ConfigurationTests/ConfigReaderTests/ConfigSnapshotReaderMethodTestsGet2.swift b/Tests/ConfigurationTests/ConfigReaderTests/ConfigSnapshotReaderMethodTestsGet2.swift deleted file mode 100644 index 991475b..0000000 --- a/Tests/ConfigurationTests/ConfigReaderTests/ConfigSnapshotReaderMethodTestsGet2.swift +++ /dev/null @@ -1,100 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftConfiguration open source project -// -// Copyright (c) 2025 Apple Inc. and the SwiftConfiguration project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftConfiguration project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - - - // ############################################################################# - // # # - // # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # - // # # - // ############################################################################# - - -import Testing -@testable import Configuration -import ConfigurationTestingInternal - -struct ConfigSnapshotReaderMethodTestsGet2 { - - typealias Defaults = ConfigReaderTests.Defaults - typealias TestEnum = ConfigReaderTests.TestEnum - typealias TestStringConvertible = ConfigReaderTests.TestStringConvertible - - @available(Configuration 1.0, *) - @Test func get() async throws { - let config = ConfigReaderTests.config - - do { - let snapshot = config.snapshot() - - // Optional - success - #expect(snapshot.string(forKey: "stringConvertible", as: TestStringConvertible.self) == Defaults.stringConvertible) - - // Optional - missing - #expect(snapshot.string(forKey: "absentStringConvertible", as: TestStringConvertible.self) == nil) - - // Optional - failing - #expect(snapshot.string(forKey: "failure", as: TestStringConvertible.self) == nil) - - // Defaulted - success - #expect(snapshot.string(forKey: "stringConvertible", as: TestStringConvertible.self, default: Defaults.otherStringConvertible) == Defaults.stringConvertible) - - // Defaulted - missing - #expect(snapshot.string(forKey: "absentStringConvertible", as: TestStringConvertible.self, default: Defaults.otherStringConvertible) == Defaults.otherStringConvertible) - - // Defaulted - failing - #expect(snapshot.string(forKey: "failure", as: TestStringConvertible.self, default: Defaults.otherStringConvertible) == Defaults.otherStringConvertible) - - // Required - success - try #expect(snapshot.requiredString(forKey: "stringConvertible", as: TestStringConvertible.self) == Defaults.stringConvertible) - - // Required - missing - let error1 = #expect(throws: ConfigError.self) { try snapshot.requiredString(forKey: "absentStringConvertible", as: TestStringConvertible.self) } - #expect(error1 == .missingRequiredConfigValue(AbsoluteConfigKey(["absentStringConvertible"]))) - - // Required - failing - #expect(throws: TestProvider.TestError.self) { try snapshot.requiredString(forKey: "failure", as: TestStringConvertible.self) } - } - do { - let snapshot = config.snapshot() - - // Optional - success - #expect(snapshot.string(forKey: "stringEnum", as: TestEnum.self) == Defaults.stringEnum) - - // Optional - missing - #expect(snapshot.string(forKey: "absentStringEnum", as: TestEnum.self) == nil) - - // Optional - failing - #expect(snapshot.string(forKey: "failure", as: TestEnum.self) == nil) - - // Defaulted - success - #expect(snapshot.string(forKey: "stringEnum", as: TestEnum.self, default: Defaults.otherStringEnum) == Defaults.stringEnum) - - // Defaulted - missing - #expect(snapshot.string(forKey: "absentStringEnum", as: TestEnum.self, default: Defaults.otherStringEnum) == Defaults.otherStringEnum) - - // Defaulted - failing - #expect(snapshot.string(forKey: "failure", as: TestEnum.self, default: Defaults.otherStringEnum) == Defaults.otherStringEnum) - - // Required - success - try #expect(snapshot.requiredString(forKey: "stringEnum", as: TestEnum.self) == Defaults.stringEnum) - - // Required - missing - let error1 = #expect(throws: ConfigError.self) { try snapshot.requiredString(forKey: "absentStringEnum", as: TestEnum.self) } - #expect(error1 == .missingRequiredConfigValue(AbsoluteConfigKey(["absentStringEnum"]))) - - // Required - failing - #expect(throws: TestProvider.TestError.self) { try snapshot.requiredString(forKey: "failure", as: TestEnum.self) } - } - } -} From e235d9bef01baab6ee286fdcf3e5f18d01ca3e0d Mon Sep 17 00:00:00 2001 From: Blazej SLEBODA <5544365+Adobels@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:05:56 +0100 Subject: [PATCH 05/12] Rename test name boolDecoderValuesForKeys -> valueForKeyOfBoolAndBoolArrayTypes --- .../ConfigurationTests/EnvironmentVariablesProviderTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift b/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift index ab06b22..2131cc8 100644 --- a/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift +++ b/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift @@ -69,7 +69,7 @@ struct EnvironmentVariablesProviderTests { } @available(Configuration 1.0, *) - @Test func boolDecoderValuesForKeys() throws { + @Test func valueForKeyOfBoolAndBoolArrayTypes() throws { let ep = EnvironmentVariablesProvider( environmentVariables: [ "BOOL_TRUE": "true", From eaf878d2a04e9c8e24ce8bba09e909bf09411483 Mon Sep 17 00:00:00 2001 From: Blazej SLEBODA <5544365+Adobels@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:09:54 +0100 Subject: [PATCH 06/12] Revert "Delete a generated file" This reverts commit 5ba67cb414d9f00461b5ff17a291d45e16cf95d2. --- .../ConfigSnapshotReaderMethodTestsGet2.swift | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 Tests/ConfigurationTests/ConfigReaderTests/ConfigSnapshotReaderMethodTestsGet2.swift diff --git a/Tests/ConfigurationTests/ConfigReaderTests/ConfigSnapshotReaderMethodTestsGet2.swift b/Tests/ConfigurationTests/ConfigReaderTests/ConfigSnapshotReaderMethodTestsGet2.swift new file mode 100644 index 0000000..991475b --- /dev/null +++ b/Tests/ConfigurationTests/ConfigReaderTests/ConfigSnapshotReaderMethodTestsGet2.swift @@ -0,0 +1,100 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftConfiguration open source project +// +// Copyright (c) 2025 Apple Inc. and the SwiftConfiguration project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftConfiguration project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + + + // ############################################################################# + // # # + // # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # + // # # + // ############################################################################# + + +import Testing +@testable import Configuration +import ConfigurationTestingInternal + +struct ConfigSnapshotReaderMethodTestsGet2 { + + typealias Defaults = ConfigReaderTests.Defaults + typealias TestEnum = ConfigReaderTests.TestEnum + typealias TestStringConvertible = ConfigReaderTests.TestStringConvertible + + @available(Configuration 1.0, *) + @Test func get() async throws { + let config = ConfigReaderTests.config + + do { + let snapshot = config.snapshot() + + // Optional - success + #expect(snapshot.string(forKey: "stringConvertible", as: TestStringConvertible.self) == Defaults.stringConvertible) + + // Optional - missing + #expect(snapshot.string(forKey: "absentStringConvertible", as: TestStringConvertible.self) == nil) + + // Optional - failing + #expect(snapshot.string(forKey: "failure", as: TestStringConvertible.self) == nil) + + // Defaulted - success + #expect(snapshot.string(forKey: "stringConvertible", as: TestStringConvertible.self, default: Defaults.otherStringConvertible) == Defaults.stringConvertible) + + // Defaulted - missing + #expect(snapshot.string(forKey: "absentStringConvertible", as: TestStringConvertible.self, default: Defaults.otherStringConvertible) == Defaults.otherStringConvertible) + + // Defaulted - failing + #expect(snapshot.string(forKey: "failure", as: TestStringConvertible.self, default: Defaults.otherStringConvertible) == Defaults.otherStringConvertible) + + // Required - success + try #expect(snapshot.requiredString(forKey: "stringConvertible", as: TestStringConvertible.self) == Defaults.stringConvertible) + + // Required - missing + let error1 = #expect(throws: ConfigError.self) { try snapshot.requiredString(forKey: "absentStringConvertible", as: TestStringConvertible.self) } + #expect(error1 == .missingRequiredConfigValue(AbsoluteConfigKey(["absentStringConvertible"]))) + + // Required - failing + #expect(throws: TestProvider.TestError.self) { try snapshot.requiredString(forKey: "failure", as: TestStringConvertible.self) } + } + do { + let snapshot = config.snapshot() + + // Optional - success + #expect(snapshot.string(forKey: "stringEnum", as: TestEnum.self) == Defaults.stringEnum) + + // Optional - missing + #expect(snapshot.string(forKey: "absentStringEnum", as: TestEnum.self) == nil) + + // Optional - failing + #expect(snapshot.string(forKey: "failure", as: TestEnum.self) == nil) + + // Defaulted - success + #expect(snapshot.string(forKey: "stringEnum", as: TestEnum.self, default: Defaults.otherStringEnum) == Defaults.stringEnum) + + // Defaulted - missing + #expect(snapshot.string(forKey: "absentStringEnum", as: TestEnum.self, default: Defaults.otherStringEnum) == Defaults.otherStringEnum) + + // Defaulted - failing + #expect(snapshot.string(forKey: "failure", as: TestEnum.self, default: Defaults.otherStringEnum) == Defaults.otherStringEnum) + + // Required - success + try #expect(snapshot.requiredString(forKey: "stringEnum", as: TestEnum.self) == Defaults.stringEnum) + + // Required - missing + let error1 = #expect(throws: ConfigError.self) { try snapshot.requiredString(forKey: "absentStringEnum", as: TestEnum.self) } + #expect(error1 == .missingRequiredConfigValue(AbsoluteConfigKey(["absentStringEnum"]))) + + // Required - failing + #expect(throws: TestProvider.TestError.self) { try snapshot.requiredString(forKey: "failure", as: TestEnum.self) } + } + } +} From 0f3bde29537c8c0376c636745d72c514b6a4f5cb Mon Sep 17 00:00:00 2001 From: Blazej SLEBODA <5544365+Adobels@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:13:22 +0100 Subject: [PATCH 07/12] Revert accidental file changes. --- .../ConfigSnapshotReaderMethodTestsGet2.swift | 83 ++++++++++++++----- 1 file changed, 61 insertions(+), 22 deletions(-) diff --git a/Tests/ConfigurationTests/ConfigReaderTests/ConfigSnapshotReaderMethodTestsGet2.swift b/Tests/ConfigurationTests/ConfigReaderTests/ConfigSnapshotReaderMethodTestsGet2.swift index 991475b..1ae2735 100644 --- a/Tests/ConfigurationTests/ConfigReaderTests/ConfigSnapshotReaderMethodTestsGet2.swift +++ b/Tests/ConfigurationTests/ConfigReaderTests/ConfigSnapshotReaderMethodTestsGet2.swift @@ -12,20 +12,18 @@ // //===----------------------------------------------------------------------===// - - // ############################################################################# - // # # - // # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # - // # # - // ############################################################################# - +// ############################################################################# +// # # +// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # +// # # +// ############################################################################# import Testing @testable import Configuration import ConfigurationTestingInternal struct ConfigSnapshotReaderMethodTestsGet2 { - + typealias Defaults = ConfigReaderTests.Defaults typealias TestEnum = ConfigReaderTests.TestEnum typealias TestStringConvertible = ConfigReaderTests.TestStringConvertible @@ -34,11 +32,14 @@ struct ConfigSnapshotReaderMethodTestsGet2 { @Test func get() async throws { let config = ConfigReaderTests.config - do { + do { let snapshot = config.snapshot() // Optional - success - #expect(snapshot.string(forKey: "stringConvertible", as: TestStringConvertible.self) == Defaults.stringConvertible) + #expect( + snapshot.string(forKey: "stringConvertible", as: TestStringConvertible.self) + == Defaults.stringConvertible + ) // Optional - missing #expect(snapshot.string(forKey: "absentStringConvertible", as: TestStringConvertible.self) == nil) @@ -47,25 +48,50 @@ struct ConfigSnapshotReaderMethodTestsGet2 { #expect(snapshot.string(forKey: "failure", as: TestStringConvertible.self) == nil) // Defaulted - success - #expect(snapshot.string(forKey: "stringConvertible", as: TestStringConvertible.self, default: Defaults.otherStringConvertible) == Defaults.stringConvertible) + #expect( + snapshot.string( + forKey: "stringConvertible", + as: TestStringConvertible.self, + default: Defaults.otherStringConvertible + ) == Defaults.stringConvertible + ) // Defaulted - missing - #expect(snapshot.string(forKey: "absentStringConvertible", as: TestStringConvertible.self, default: Defaults.otherStringConvertible) == Defaults.otherStringConvertible) + #expect( + snapshot.string( + forKey: "absentStringConvertible", + as: TestStringConvertible.self, + default: Defaults.otherStringConvertible + ) == Defaults.otherStringConvertible + ) // Defaulted - failing - #expect(snapshot.string(forKey: "failure", as: TestStringConvertible.self, default: Defaults.otherStringConvertible) == Defaults.otherStringConvertible) + #expect( + snapshot.string( + forKey: "failure", + as: TestStringConvertible.self, + default: Defaults.otherStringConvertible + ) == Defaults.otherStringConvertible + ) // Required - success - try #expect(snapshot.requiredString(forKey: "stringConvertible", as: TestStringConvertible.self) == Defaults.stringConvertible) + try #expect( + snapshot.requiredString(forKey: "stringConvertible", as: TestStringConvertible.self) + == Defaults.stringConvertible + ) // Required - missing - let error1 = #expect(throws: ConfigError.self) { try snapshot.requiredString(forKey: "absentStringConvertible", as: TestStringConvertible.self) } + let error1 = #expect(throws: ConfigError.self) { + try snapshot.requiredString(forKey: "absentStringConvertible", as: TestStringConvertible.self) + } #expect(error1 == .missingRequiredConfigValue(AbsoluteConfigKey(["absentStringConvertible"]))) // Required - failing - #expect(throws: TestProvider.TestError.self) { try snapshot.requiredString(forKey: "failure", as: TestStringConvertible.self) } + #expect(throws: TestProvider.TestError.self) { + try snapshot.requiredString(forKey: "failure", as: TestStringConvertible.self) + } } - do { + do { let snapshot = config.snapshot() // Optional - success @@ -78,23 +104,36 @@ struct ConfigSnapshotReaderMethodTestsGet2 { #expect(snapshot.string(forKey: "failure", as: TestEnum.self) == nil) // Defaulted - success - #expect(snapshot.string(forKey: "stringEnum", as: TestEnum.self, default: Defaults.otherStringEnum) == Defaults.stringEnum) + #expect( + snapshot.string(forKey: "stringEnum", as: TestEnum.self, default: Defaults.otherStringEnum) + == Defaults.stringEnum + ) // Defaulted - missing - #expect(snapshot.string(forKey: "absentStringEnum", as: TestEnum.self, default: Defaults.otherStringEnum) == Defaults.otherStringEnum) + #expect( + snapshot.string(forKey: "absentStringEnum", as: TestEnum.self, default: Defaults.otherStringEnum) + == Defaults.otherStringEnum + ) // Defaulted - failing - #expect(snapshot.string(forKey: "failure", as: TestEnum.self, default: Defaults.otherStringEnum) == Defaults.otherStringEnum) + #expect( + snapshot.string(forKey: "failure", as: TestEnum.self, default: Defaults.otherStringEnum) + == Defaults.otherStringEnum + ) // Required - success try #expect(snapshot.requiredString(forKey: "stringEnum", as: TestEnum.self) == Defaults.stringEnum) // Required - missing - let error1 = #expect(throws: ConfigError.self) { try snapshot.requiredString(forKey: "absentStringEnum", as: TestEnum.self) } + let error1 = #expect(throws: ConfigError.self) { + try snapshot.requiredString(forKey: "absentStringEnum", as: TestEnum.self) + } #expect(error1 == .missingRequiredConfigValue(AbsoluteConfigKey(["absentStringEnum"]))) // Required - failing - #expect(throws: TestProvider.TestError.self) { try snapshot.requiredString(forKey: "failure", as: TestEnum.self) } + #expect(throws: TestProvider.TestError.self) { + try snapshot.requiredString(forKey: "failure", as: TestEnum.self) + } } } } From d45e629784a4e163782303a32fdd0e4f1242b1fe Mon Sep 17 00:00:00 2001 From: Blazej SLEBODA <5544365+Adobels@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:02:32 +0100 Subject: [PATCH 08/12] Move decodeBool to Snapshot of EnvironmentVariablesProvider Apply review comments --- .../Reference/EnvironmentVariablesProvider.md | 6 +-- .../EnvironmentVariablesProvider.swift | 27 ++++++----- .../ConfigBoolsFromStringDecoder.swift | 47 ------------------- .../ConfigBoolsFromStringDecoderTests.swift | 41 ---------------- .../EnvironmentVariablesProviderTests.swift | 20 ++++++++ 5 files changed, 38 insertions(+), 103 deletions(-) delete mode 100644 Sources/Configuration/ValueCoders/ConfigBoolsFromStringDecoder.swift delete mode 100644 Tests/ConfigurationTests/ConfigBoolsFromStringDecoderTests.swift diff --git a/Sources/Configuration/Documentation.docc/Reference/EnvironmentVariablesProvider.md b/Sources/Configuration/Documentation.docc/Reference/EnvironmentVariablesProvider.md index b0eead7..8b20dbf 100644 --- a/Sources/Configuration/Documentation.docc/Reference/EnvironmentVariablesProvider.md +++ b/Sources/Configuration/Documentation.docc/Reference/EnvironmentVariablesProvider.md @@ -4,9 +4,9 @@ ### Creating an environment variable provider -- ``init(secretsSpecifier:bytesDecoder:boolDecoder:arraySeparator:)`` -- ``init(environmentVariables:secretsSpecifier:bytesDecoder:boolDecoder:arraySeparator:)`` -- ``init(environmentFilePath:allowMissing:secretsSpecifier:bytesDecoder:boolDecoder:arraySeparator:)`` +- ``init(secretsSpecifier:bytesDecoder:arraySeparator:)`` +- ``init(environmentVariables:secretsSpecifier:bytesDecoder:arraySeparator:)`` +- ``init(environmentFilePath:allowMissing:secretsSpecifier:bytesDecoder:arraySeparator:)`` ### Inspecting an environment variable provider diff --git a/Sources/Configuration/Providers/EnvironmentVariables/EnvironmentVariablesProvider.swift b/Sources/Configuration/Providers/EnvironmentVariables/EnvironmentVariablesProvider.swift index c793270..08e2a72 100644 --- a/Sources/Configuration/Providers/EnvironmentVariables/EnvironmentVariablesProvider.swift +++ b/Sources/Configuration/Providers/EnvironmentVariables/EnvironmentVariablesProvider.swift @@ -144,7 +144,18 @@ public struct EnvironmentVariablesProvider: Sendable { var arrayDecoder: EnvironmentValueArrayDecoder /// A decoder of bool values from a string - let boolDecoder: BoolDecoder + static func decodeBool(from string: String) -> Bool? { + let stringLowercased = string.lowercased() + return if ["true", "false"].contains(stringLowercased) { + stringLowercased == "true" + } else if ["yes", "no"].contains(stringLowercased) { + stringLowercased == "yes" + } else if ["1", "0"].contains(stringLowercased) { + stringLowercased == "1" + } else { + nil + } + } } /// The underlying snapshot of the provider. @@ -173,14 +184,12 @@ public struct EnvironmentVariablesProvider: Sendable { public init( secretsSpecifier: SecretsSpecifier = .none, bytesDecoder: some ConfigBytesFromStringDecoder = .base64, - boolDecoder: BoolDecoder = .init(), arraySeparator: Character = "," ) { self.init( environmentVariables: ProcessInfo.processInfo.environment, secretsSpecifier: secretsSpecifier, bytesDecoder: bytesDecoder, - boolDecoder: boolDecoder, arraySeparator: arraySeparator ) } @@ -212,7 +221,6 @@ public struct EnvironmentVariablesProvider: Sendable { environmentVariables: [String: String], secretsSpecifier: SecretsSpecifier = .none, bytesDecoder: some ConfigBytesFromStringDecoder = .base64, - boolDecoder: BoolDecoder = .init(), arraySeparator: Character = "," ) { let tuples: [(String, EnvironmentValue)] = environmentVariables.map { key, value in @@ -227,8 +235,7 @@ public struct EnvironmentVariablesProvider: Sendable { self._snapshot = .init( environmentVariables: Dictionary(uniqueKeysWithValues: tuples), bytesDecoder: bytesDecoder, - arrayDecoder: EnvironmentValueArrayDecoder(separator: arraySeparator), - boolDecoder: boolDecoder + arrayDecoder: EnvironmentValueArrayDecoder(separator: arraySeparator) ) } @@ -262,7 +269,6 @@ public struct EnvironmentVariablesProvider: Sendable { allowMissing: Bool = false, secretsSpecifier: SecretsSpecifier = .none, bytesDecoder: some ConfigBytesFromStringDecoder = .base64, - boolDecoder: BoolDecoder = .init(), arraySeparator: Character = "," ) async throws { try await self.init( @@ -271,7 +277,6 @@ public struct EnvironmentVariablesProvider: Sendable { fileSystem: LocalCommonProviderFileSystem(), secretsSpecifier: secretsSpecifier, bytesDecoder: bytesDecoder, - boolDecoder: boolDecoder, arraySeparator: arraySeparator ) } @@ -294,7 +299,6 @@ public struct EnvironmentVariablesProvider: Sendable { fileSystem: some CommonProviderFileSystem, secretsSpecifier: SecretsSpecifier = .none, bytesDecoder: some ConfigBytesFromStringDecoder = .base64, - boolDecoder: BoolDecoder = .init(), arraySeparator: Character = "," ) async throws { let loadedData = try await fileSystem.fileContents(atPath: environmentFilePath) @@ -311,7 +315,6 @@ public struct EnvironmentVariablesProvider: Sendable { environmentVariables: EnvironmentFileParser.parsed(contents), secretsSpecifier: secretsSpecifier, bytesDecoder: bytesDecoder, - boolDecoder: boolDecoder, arraySeparator: arraySeparator ) } @@ -408,7 +411,7 @@ extension EnvironmentVariablesProvider.Snapshot { } content = .double(doubleValue) case .bool: - guard let boolValue = boolDecoder.decodeBool(from: stringValue) else { + guard let boolValue = Self.decodeBool(from: stringValue) else { try throwMismatch() } content = .bool(boolValue) @@ -441,7 +444,7 @@ extension EnvironmentVariablesProvider.Snapshot { case .boolArray: let arrayValue = arrayDecoder.decode(stringValue) let boolArray = try arrayValue.map { stringValue in - guard let boolValue = boolDecoder.decodeBool(from: stringValue) else { + guard let boolValue = Self.decodeBool(from: stringValue) else { try throwMismatch() } return boolValue diff --git a/Sources/Configuration/ValueCoders/ConfigBoolsFromStringDecoder.swift b/Sources/Configuration/ValueCoders/ConfigBoolsFromStringDecoder.swift deleted file mode 100644 index 79fa852..0000000 --- a/Sources/Configuration/ValueCoders/ConfigBoolsFromStringDecoder.swift +++ /dev/null @@ -1,47 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftConfiguration open source project -// -// Copyright (c) 2025 Apple Inc. and the SwiftConfiguration project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftConfiguration project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -#if canImport(FoundationEssentials) -import FoundationEssentials -#else -import Foundation -#endif - -/// A decoder that converts a boolean string into a Bool, taking into account different boolean string pairs. -/// -/// This decoder is able to convert a string to Bool values when the string’s Boolean value format is 0 or 1, true or false, or yes or no. -/// -/// ## Boolean values -/// -/// Following boolean string pairs are decoded to a Bool value: trueFalse (true, false), oneZero (1, 0), yesNo (yes, no). -/// Decoding is case-insensitive. -@available(Configuration 1.0, *) -public struct BoolDecoder: Sendable { - - /// Creates a new bool decoder. - public init() {} - - public func decodeBool(from string: String) -> Bool? { - let stringLowercased = string.lowercased() - return if ["true", "false"].contains(stringLowercased) { - stringLowercased == "true" - } else if ["yes", "no"].contains(stringLowercased) { - stringLowercased == "yes" - } else if ["1", "0"].contains(stringLowercased) { - stringLowercased == "1" - } else { - nil - } - } -} diff --git a/Tests/ConfigurationTests/ConfigBoolsFromStringDecoderTests.swift b/Tests/ConfigurationTests/ConfigBoolsFromStringDecoderTests.swift deleted file mode 100644 index a3445fa..0000000 --- a/Tests/ConfigurationTests/ConfigBoolsFromStringDecoderTests.swift +++ /dev/null @@ -1,41 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftConfiguration open source project -// -// Copyright (c) 2025 Apple Inc. and the SwiftConfiguration project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftConfiguration project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Testing -import Foundation -@testable import Configuration - -struct ConfigBoolsFromStringDecoderTests { - - @Test() - @available(Configuration 1.0, *) - func stringToBool() throws { - let bd = BoolDecoder() - let cases: [(expected: Bool?, input: [String])] = [ - (true, ["1"]), - (false, ["0"]), - (true, ["Yes", "yes", "YES", "yES"]), - (false, ["No", "no", "NO", "nO"]), - (true, ["true", "TRUE", "trUe"]), - (false, ["false", "FALSE", "faLse"]), - (nil, ["", "_true_", "_false_", "_yes_", "_no_", "_1_", "_0_", "11", "00"]) - ] - - for (expected, inputs) in cases { - for input in inputs { - #expect(bd.decodeBool(from: input) == expected, "input: \(input)") - } - } - } -} diff --git a/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift b/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift index 2131cc8..1fa2f7f 100644 --- a/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift +++ b/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift @@ -68,6 +68,26 @@ struct EnvironmentVariablesProviderTests { #expect(provider.debugDescription == expectedDebugDescription) } + @Test() + @available(Configuration 1.0, *) + func decodeBoolFromString() throws { + let cases: [(expected: Bool?, input: [String])] = [ + (true, ["1"]), + (false, ["0"]), + (true, ["Yes", "yes", "YES", "yES"]), + (false, ["No", "no", "NO", "nO"]), + (true, ["true", "TRUE", "trUe"]), + (false, ["false", "FALSE", "faLse"]), + (nil, ["", "_true_", "_false_", "_yes_", "_no_", "_1_", "_0_", "11", "00"]) + ] + + for (expected, inputs) in cases { + for input in inputs { + #expect(EnvironmentVariablesProvider.Snapshot.decodeBool(from: input) == expected, "input: \(input)") + } + } + } + @available(Configuration 1.0, *) @Test func valueForKeyOfBoolAndBoolArrayTypes() throws { let ep = EnvironmentVariablesProvider( From 8ad1813ed76d5e16bd9fafe81a7fc431d15b183c Mon Sep 17 00:00:00 2001 From: Blazej SLEBODA <5544365+Adobels@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:04:32 +0100 Subject: [PATCH 09/12] Delete outdated comments --- .../EnvironmentVariables/EnvironmentVariablesProvider.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/Configuration/Providers/EnvironmentVariables/EnvironmentVariablesProvider.swift b/Sources/Configuration/Providers/EnvironmentVariables/EnvironmentVariablesProvider.swift index 08e2a72..5f5f327 100644 --- a/Sources/Configuration/Providers/EnvironmentVariables/EnvironmentVariablesProvider.swift +++ b/Sources/Configuration/Providers/EnvironmentVariables/EnvironmentVariablesProvider.swift @@ -179,7 +179,6 @@ public struct EnvironmentVariablesProvider: Sendable { /// - Parameters: /// - secretsSpecifier: Specifies which environment variables should be treated as secrets. /// - bytesDecoder: The decoder used for converting string values to byte arrays. - /// - boolDecoder: The decoder used for converting string values to bool values. /// - arraySeparator: The character used to separate elements in array values. public init( secretsSpecifier: SecretsSpecifier = .none, @@ -215,7 +214,6 @@ public struct EnvironmentVariablesProvider: Sendable { /// - environmentVariables: A dictionary of environment variable names and values. /// - secretsSpecifier: Specifies which environment variables should be treated as secrets. /// - bytesDecoder: The decoder used for converting string values to byte arrays. - /// - boolDecoder: The decoder used for converting string to bool values. /// - arraySeparator: The character used to separate elements in array values. public init( environmentVariables: [String: String], @@ -261,7 +259,6 @@ public struct EnvironmentVariablesProvider: Sendable { /// - When `true`, if the file is missing, treats it as empty. Malformed files still throw an error. /// - secretsSpecifier: Specifies which environment variables should be treated as secrets. /// - bytesDecoder: The decoder used for converting string values to byte arrays. - /// - boolDecoder: The decoder used for converting string values to bool values. /// - arraySeparator: The character used to separate elements in array values. /// - Throws: If the file is malformed, or if missing when allowMissing is `false`. public init( @@ -290,7 +287,6 @@ public struct EnvironmentVariablesProvider: Sendable { /// - fileSystem: The file system implementation to use. /// - secretsSpecifier: Specifies which environment variables should be treated as secrets. /// - bytesDecoder: The decoder used for converting string values to byte arrays. - /// - boolDecoder: The decoder used for converting string values to bool values. /// - arraySeparator: The character used to separate elements in array values. /// - Throws: If the file is malformed, or if missing when allowMissing is `false`. internal init( From b4cda9ee1b8099e63ce042f9f349fa9935656234 Mon Sep 17 00:00:00 2001 From: Blazej SLEBODA <5544365+Adobels@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:13:37 +0100 Subject: [PATCH 10/12] Use sut for instances under test in tests --- .../EnvironmentVariablesProviderTests.swift | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift b/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift index 1fa2f7f..a716423 100644 --- a/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift +++ b/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift @@ -71,6 +71,7 @@ struct EnvironmentVariablesProviderTests { @Test() @available(Configuration 1.0, *) func decodeBoolFromString() throws { + let sut = EnvironmentVariablesProvider.Snapshot.decodeBool let cases: [(expected: Bool?, input: [String])] = [ (true, ["1"]), (false, ["0"]), @@ -80,17 +81,16 @@ struct EnvironmentVariablesProviderTests { (false, ["false", "FALSE", "faLse"]), (nil, ["", "_true_", "_false_", "_yes_", "_no_", "_1_", "_0_", "11", "00"]) ] - for (expected, inputs) in cases { for input in inputs { - #expect(EnvironmentVariablesProvider.Snapshot.decodeBool(from: input) == expected, "input: \(input)") + #expect(sut(input) == expected, "input: \(input)") } } } @available(Configuration 1.0, *) @Test func valueForKeyOfBoolAndBoolArrayTypes() throws { - let ep = EnvironmentVariablesProvider( + let sut = EnvironmentVariablesProvider( environmentVariables: [ "BOOL_TRUE": "true", "BOOL_FALSE": "false", @@ -105,18 +105,18 @@ struct EnvironmentVariablesProviderTests { "BOOLY_ARRAY_THROWS_1": "true,1,YESS", "BOOLY_ARRAY_THROWS_2": "false,00,no", ]) - #expect(try ep.value(forKey: "BOOL_TRUE", type: .bool).value == true) - #expect(try ep.value(forKey: "BOOL_FALSE", type: .bool).value == false) - #expect(try ep.value(forKey: "BOOL_1", type: .bool).value == true) - #expect(try ep.value(forKey: "BOOL_0", type: .bool).value == false) - #expect(try ep.value(forKey: "BOOL_YES", type: .bool).value == true) - #expect(try ep.value(forKey: "BOOL_NO", type: .bool).value == false) - #expect(throws: ConfigError.self) { try ep.value(forKey: "BOOL_THROWS_ERROR_EMPTY", type: .bool) } - #expect(throws: ConfigError.self) { try ep.value(forKey: "BOOL_THROWS_ERROR_NOT_BOOL_STRING", type: .bool) } - #expect(try ep.value(forKey: "BOOLY_ARRAY_TRUE", type: .boolArray).value == .init([true, true, true], isSecret: false)) - #expect(try ep.value(forKey: "BOOLY_ARRAY_FALSE", type: .boolArray).value == .init([false, false, false], isSecret: false)) - #expect(throws: ConfigError.self) { try ep.value(forKey: "BOOLY_ARRAY_THROWS_1", type: .boolArray) } - #expect(throws: ConfigError.self) { try ep.value(forKey: "BOOLY_ARRAY_THROWS_2", type: .boolArray) } + #expect(try sut.value(forKey: "BOOL_TRUE", type: .bool).value == true) + #expect(try sut.value(forKey: "BOOL_FALSE", type: .bool).value == false) + #expect(try sut.value(forKey: "BOOL_1", type: .bool).value == true) + #expect(try sut.value(forKey: "BOOL_0", type: .bool).value == false) + #expect(try sut.value(forKey: "BOOL_YES", type: .bool).value == true) + #expect(try sut.value(forKey: "BOOL_NO", type: .bool).value == false) + #expect(throws: ConfigError.self) { try sut.value(forKey: "BOOL_THROWS_ERROR_EMPTY", type: .bool) } + #expect(throws: ConfigError.self) { try sut.value(forKey: "BOOL_THROWS_ERROR_NOT_BOOL_STRING", type: .bool) } + #expect(try sut.value(forKey: "BOOLY_ARRAY_TRUE", type: .boolArray).value == .init([true, true, true], isSecret: false)) + #expect(try sut.value(forKey: "BOOLY_ARRAY_FALSE", type: .boolArray).value == .init([false, false, false], isSecret: false)) + #expect(throws: ConfigError.self) { try sut.value(forKey: "BOOLY_ARRAY_THROWS_1", type: .boolArray) } + #expect(throws: ConfigError.self) { try sut.value(forKey: "BOOLY_ARRAY_THROWS_2", type: .boolArray) } } @available(Configuration 1.0, *) From e851e094e23889cee7349f9d33e5f351858afc62 Mon Sep 17 00:00:00 2001 From: Blazej SLEBODA <5544365+Adobels@users.noreply.github.com> Date: Wed, 17 Dec 2025 19:09:19 +0100 Subject: [PATCH 11/12] Update Sources/Configuration/Providers/EnvironmentVariables/EnvironmentVariablesProvider.swift Use a compact switch-based implementation for decodeBool. Co-authored-by: Honza Dvorsky --- .../EnvironmentVariablesProvider.swift | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Sources/Configuration/Providers/EnvironmentVariables/EnvironmentVariablesProvider.swift b/Sources/Configuration/Providers/EnvironmentVariables/EnvironmentVariablesProvider.swift index 5f5f327..829b98e 100644 --- a/Sources/Configuration/Providers/EnvironmentVariables/EnvironmentVariablesProvider.swift +++ b/Sources/Configuration/Providers/EnvironmentVariables/EnvironmentVariablesProvider.swift @@ -145,15 +145,10 @@ public struct EnvironmentVariablesProvider: Sendable { /// A decoder of bool values from a string static func decodeBool(from string: String) -> Bool? { - let stringLowercased = string.lowercased() - return if ["true", "false"].contains(stringLowercased) { - stringLowercased == "true" - } else if ["yes", "no"].contains(stringLowercased) { - stringLowercased == "yes" - } else if ["1", "0"].contains(stringLowercased) { - stringLowercased == "1" - } else { - nil + switch string.lowercased() { + case "true", "yes", "1": true + case "false", "no", "0": false + default: nil } } } From a99f74538e5ba0e453c141bce458494890e68e26 Mon Sep 17 00:00:00 2001 From: Blazej SLEBODA <5544365+Adobels@users.noreply.github.com> Date: Wed, 17 Dec 2025 19:11:29 +0100 Subject: [PATCH 12/12] Update Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift Fix the order of macros used in a test case to align with the test target order. Co-authored-by: Honza Dvorsky --- .../ConfigurationTests/EnvironmentVariablesProviderTests.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift b/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift index a716423..fb15ff8 100644 --- a/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift +++ b/Tests/ConfigurationTests/EnvironmentVariablesProviderTests.swift @@ -68,9 +68,8 @@ struct EnvironmentVariablesProviderTests { #expect(provider.debugDescription == expectedDebugDescription) } - @Test() @available(Configuration 1.0, *) - func decodeBoolFromString() throws { + @Test func decodeBoolFromString() throws { let sut = EnvironmentVariablesProvider.Snapshot.decodeBool let cases: [(expected: Bool?, input: [String])] = [ (true, ["1"]),