diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index e56059c..0000000 --- a/Package.resolved +++ /dev/null @@ -1,16 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "swift-collections", - "repositoryURL": "https://github.com/apple/swift-collections.git", - "state": { - "branch": null, - "revision": "48254824bb4248676bf7ce56014ff57b142b77eb", - "version": "1.0.2" - } - } - ] - }, - "version": 1 -} diff --git a/Package.swift b/Package.swift index ca4a614..12525cc 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.6 +// swift-tools-version:5.7 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -11,15 +11,11 @@ let package = Package( targets: ["SwiftDotenv"] ) ], - dependencies: [ - .package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.0")) - ], + dependencies: [], targets: [ .target( name: "SwiftDotenv", - dependencies: [ - .product(name: "Collections", package: "swift-collections") - ], + dependencies: [], path: "Sources" ), .testTarget( diff --git a/README.md b/README.md index e484d4d..561cc44 100644 --- a/README.md +++ b/README.md @@ -15,76 +15,65 @@ A one-stop shop for working with environment values in a Swift program. `SwiftDotenv` supports Swift Package Manager and can be added by adding this entry to your `Package.swift` manifest file: ```swift -.package(url: "https://github.com/thebarndog/swift-dotenv.git", .upToNextMajor("1.0.0")) +.package(url: "https://github.com/thebarndog/swift-dotenv.git", .upToNextMajor("2.0.0")) ``` ## Usage ```swift import SwiftDotenv -``` - -### `Environment` - -`Environment` is an immutable model representing an `.env` file. It can be created by loading it via the `Dotenv` structure or created in code via dictionary literals. `Environment` uses type-safe representations of environment values such as `.boolean(Bool)` or `.integer(Int)`. - -To create an environment from scratch: -```swift -let environment = try Environment(values: ["API_KEY": "some-key"]) -``` +// load in environment variables +try Dotenv.configure() -or with the type-safe api: - -```swift -let environment = try Environment(values: ["FEATURE_ON": .boolean(true)]) +// access values +print(Dotenv.apiSecret) ``` -`Environment` also supports `@dynamicMemberLookup`: +### `Dotenv` -``` -# .env file -API_KEY=some-key -ONBOARDING_ENABLED=false -``` +To configure the environment with values from your environment file, call `Dotenv.configure(atPath:overwrite:)`: ```swift -let key = environment.apiKey // "some-key" -let enabled = environment.onboardingEnabled // false +try Dotenv.configure() ``` -### `Dotenv` - -To load an environment from a `.env` file, use `Dotenv.load(path:)`: +It can optionally be provided with a path: ```swift -let environment = try Dotenv.load(path: ".env") +try Dotenv.configure(atPath: ".custom-env") ``` -Enviroments that were created programmatically can also be saved to disk via `Dotenv.save(environment:toPath:force:)`: +and the ability to not overwrite currently existing environment variables: ```swift -let environment = try Environment(values: ["API_KEY": "some-key"]) -try Dotenv.save(environment, atPath: ".env", force: false) // wont overwrite an existing file when force == false +try Dotenv.configure(overwrite: false) ``` -By default, `Dotenv` uses `FileManger.default` to load and save files but even that can be swapped out: +To read values: ```swift -Dotenv.fileManger = someCustomInstance +let key = Dotenv.apiKey // using dynamic member lookup +let key = Dotenv["API_KEY"] // using regular subscripting ``` -### `ProcessInfo` & `FallbackStrategy` +To set new values: -If a value doesn't exist in the set of values fetched from your `.env` file, `Environment` will then fallback and look in `ProcessInfo` for the desired value. Custom fallback strategies can be also be set so that `Environment` will query first from `ProcessInfo` and then to the environment values pulled from the configuration file. +```swift +Dotenv.apiKey = .string("some-secret") +Dotenv["API_KEY"] = .string("some-secret") -The default fallback strategy is `.init(query: .configuration, fallback: .process)` meaning `Environment` will first look for the value in the configuration file and then fallback to `ProcessInfo` if it can't find it. The `fallback` parameter can also be `nil`, removing fallback functionality. +// set a value and turn off overwriting +Dotenv.set(value: "false", forKey: "DEBUG_MODE", overwrite: false) +``` -To modify the fallback strategy: +The `Dotenv` structure can also be given a custom delimeter, file manager, and process info: ```swift -Environment.fallbackStrategy = .init(query: .process) -``` +Dotenv.delimeter = "-" // default is "=" +Dotenv.processInfo = ProcessInfo() // default is `ProcessInfo.processInfo` +Dotenv.fileManager = FileManager() // default is `FileManager.default` +``` ### Contributing diff --git a/Sources/Dotenv.swift b/Sources/Dotenv.swift index dc10056..60b22ba 100644 --- a/Sources/Dotenv.swift +++ b/Sources/Dotenv.swift @@ -1,16 +1,77 @@ -// -// Dotenv.swift -// SwiftDotenv -// -// Created by Brendan Conron on 10/17/21. -// - +import Darwin import Foundation /// Structure used to load and save environment files. -public struct Dotenv { +@dynamicMemberLookup +public enum Dotenv { // MARK: - Types + + /// Type-safe representation of the value types in a `.env` file. + public enum Value: Equatable { + /// Reprensents a boolean value, `true`, or `false`. + case boolean(Bool) + /// Represents any double literal. + case double(Double) + /// Represents any integer literal. + case integer(Int) + /// Represents any string literal. + case string(String) + + /// Convert a value to its string representation. + public var stringValue: String { + switch self { + case let .boolean(value): + return String(describing: value) + case let .double(value): + return String(describing: value) + case let .integer(value): + return String(describing: value) + case let .string(value): + return value + } + } + + /// Create a value from a string value. + /// - Parameter stringValue: String value. + init?(_ stringValue: String?) { + guard let stringValue else { + return nil + } + // order of operations is important, double should get checked before integer + // because integer's downcasting is more permissive + if let boolValue = Bool(stringValue) { + self = .boolean(boolValue) + // enforcing exclusion on the double conversion + } else if let doubleValue = Double(stringValue), Int(stringValue) == nil { + self = .double(doubleValue) + } else if let integerValue = Int(stringValue) { + self = .integer(integerValue) + } else { + // replace escape double quotes + self = .string(stringValue.trimmingCharacters(in: .init(charactersIn: "\"")).replacingOccurrences(of: "\\\"", with: "\"")) + } + } + + // MARK: - Equatable + + public static func == (lhs: Value, rhs: Value) -> Bool { + switch (lhs, rhs) { + case let (.boolean(a), .boolean(b)): + return a == b + case let (.double(a), .double(b)): + return a == b + case let (.integer(a), .integer(b)): + return a == b + case let (.string(a), .string(b)): + return a == b + default: + return false + } + } + } + + // MARK: Errors /// Failures that can occur during loading an environment. public enum LoadingFailure: Error { @@ -20,10 +81,12 @@ public struct Dotenv { case unableToReadEnvironmentFile } - /// Failures that can occur when saving an environment to disk. - public enum SavingFailure: Error { - /// A configuration file already exists at that path. - case fileAlreadyExists(path: String) + /// Represents errors that can occur during encoding. + public enum DecodingFailure: Error { + // the key value pair is in some way malformed + case malformedKeyValuePair + /// Either the key or value is empty. + case emptyKeyValuePair(pair: (String, String)) } // MARK: - Configuration @@ -31,36 +94,106 @@ public struct Dotenv { /// `FileManager` instance used to load and save configuration files. Can be replaced with a custom instance. public static var fileManager = FileManager.default - // MARK: - Loading & Saving - - /// Load an environment file. - /// - Parameter path: Path to load from, defaults to `.env`. - /// - Returns: Environment object. - /// - Throws: Any error that occurs during loading. - public static func load(path: String = ".env") throws -> Environment { + /// Delimeter for key value pairs, defaults to `=`. + public static var delimeter: Character = "=" + + /// Process info instance. + public static var processInfo: ProcessInfo = ProcessInfo.processInfo + + /// Configure the environment with environment values loaded from the environment file. + /// - Parameters: + /// - path: Path for the environment file, defaults to `.env`. + /// - overwrite: Flag that indicates if pre-existing values in the environment should be overwritten with values from the environment file, defaults to `true`. + public static func configure(atPath path: String = ".env", overwrite: Bool = true) throws { + let contents = try readFileContents(atPath: path) + let lines = contents.split(separator: "\n") + // we loop over all the entries in the file which are already separated by a newline + for line in lines { + // ignore comments + if line.starts(with: "#") { + continue + } + // split by the delimeter + let substrings = line.split(separator: Self.delimeter) + + // make sure we can grab two and only two string values + guard + let key = substrings.first?.trimmingCharacters(in: .whitespacesAndNewlines), + let value = substrings.last?.trimmingCharacters(in: .whitespacesAndNewlines), + substrings.count == 2, + !key.isEmpty, + !value.isEmpty else { + throw DecodingFailure.malformedKeyValuePair + } + setenv(key, value, overwrite ? 1 : 0) + } + } + + private static func readFileContents(atPath path: String) throws -> String { let fileManager = Self.fileManager guard fileManager.fileExists(atPath: path) else { throw LoadingFailure.environmentFileIsMissing } - guard let stringData = try? String(contentsOf: URL(fileURLWithPath: path)) else { + guard let contents = try? String(contentsOf: URL(fileURLWithPath: path)) else { throw LoadingFailure.unableToReadEnvironmentFile } - return try Environment(contents: stringData) + return contents } - - /// Save an environment object to a file. - /// - Important: This method does not serialize process values to disk. + + // MARK: - Values + + /// All environment values. + public static var values: [String: String] { + processInfo.environment + } + + // MARK: - Modification + + /// Set a value in the environment. /// - Parameters: - /// - environment: Environment to serialize. - /// - path: Path to save the environment to. - /// - force: Flag that indicates if the environment should overwrite an existing file. - /// - Throws: Any error that occurs during saving such as a file already existing in the specified path. - public static func save(environment: Environment, toPath path: String, force: Bool = false) throws { - let contents = try environment.serialize() - let fileManager = Self.fileManager - guard !fileManager.fileExists(atPath: path) || force else { - throw SavingFailure.fileAlreadyExists(path: path) + /// - value: Value to set. + /// - key: Key to set the value with. + /// - overwrite: Flag that indicates if any existing value should be overwritten, defaults to `true`. + public static func set(value: Value, forKey key: String, overwrite: Bool = true) { + set(value: value.stringValue, forKey: key, overwrite: overwrite) + } + + /// Set a value in the environment. + /// - Parameters: + /// - value: Value to set. + /// - key: Key to set the value with. + /// - overwrite: Flag that indicates if any existing value should be overwritten, defaults to `true`. + public static func set(value: String, forKey key: String, overwrite: Bool = true) { + setenv(key, value, overwrite ? 1 : 0) + } + + // MARK: - Subscripting + + public static subscript(key: String) -> Value? { + get { + Value(values[key]) + } set { + guard let newValue else { return } + set(value: newValue, forKey: key) + } + } + + public static subscript(key: String, default defaultValue: @autoclosure () -> Value) -> Value { + get { + Value(values[key]) ?? defaultValue() + } set { + set(value: newValue, forKey: key) + } + } + + // MARK: Dynamic Member Lookup + + public static subscript(dynamicMember member: String) -> Value? { + get { + Value(values[member.camelCaseToSnakeCase().uppercased()]) + } set { + guard let newValue else { return } + set(value: newValue, forKey: member.camelCaseToSnakeCase().uppercased()) } - fileManager.createFile(atPath: path, contents: contents.data(using: .utf8)) } } diff --git a/Sources/Environment.swift b/Sources/Environment.swift deleted file mode 100644 index 6cf8aee..0000000 --- a/Sources/Environment.swift +++ /dev/null @@ -1,316 +0,0 @@ -// -// Environment.swift -// SwiftDotenv -// -// Created by Brendan Conron on 10/17/21. -// - -import Collections -import Foundation - -/// Environment object that represents a `.env` configuration file. -@dynamicMemberLookup -public struct Environment { - - // MARK: - Types - - /// Type-safe representation of the value types in a `.env` file. - public enum Value: Equatable { - /// Reprensents a boolean value, `true`, or `false`. - case boolean(Bool) - /// Represents any double literal. - case double(Double) - /// Represents any integer literal. - case integer(Int) - /// Represents any string literal. - case string(String) - - /// Convert a value to its string representation. - public var stringValue: String { - switch self { - case let .boolean(value): - return String(describing: value) - case let .double(value): - return String(describing: value) - case let .integer(value): - return String(describing: value) - case let .string(value): - return value - } - } - - /// Create a value from a string value. - /// - Parameter stringValue: String value. - init(_ stringValue: String) { - // order of operations is important, double should get checked before integer - // because integer's downcasting is more permissive - if let boolValue = Bool(stringValue) { - self = .boolean(boolValue) - // enforcing exclusion on the double conversion - } else if let doubleValue = Double(stringValue), Int(stringValue) == nil { - self = .double(doubleValue) - } else if let integerValue = Int(stringValue) { - self = .integer(integerValue) - } else { - // replace escape double quotes - self = .string(stringValue.trimmingCharacters(in: .init(charactersIn: "\"")).replacingOccurrences(of: "\\\"", with: "\"")) - } - } - - // MARK: - Equatable - - public static func == (lhs: Value, rhs: Value) -> Bool { - switch (lhs, rhs) { - case let (.boolean(a), .boolean(b)): - return a == b - case let (.double(a), .double(b)): - return a == b - case let (.integer(a), .integer(b)): - return a == b - case let (.string(a), .string(b)): - return a == b - default: - return false - } - } - } - - /// Describes a data source that provides environment values. - public enum DataSource { - /// `.env` configuration file. - case configuration - /// `ProcessInfo` instance. - case process - } - - /// Describes how the various datasources of environmental values are queried as well as fallback strategy if values don't exist. - public struct FallbackStrategy { - - /// The data source to be queried. - let query: DataSource - - /// The data source to fallback and query if the `query` data source produces a `nil` value. Can be `nil` which disables fallback. - let fallback: DataSource? - - // MARK: - Initialization - - /// Create a fallback strategy. - /// - Parameters: - /// - query: The query data source. - /// - fallback: The fallback data source, defaults to `.process`. - public init(query: DataSource, fallback: DataSource? = .process) { - self.query = query - self.fallback = fallback == query ? nil : fallback - } - } - - // MARK: - Errors - - /// Represents errors that can occur during encoding. - public enum DecodingFailure: Error { - // the key value pair is in some way malformed - case malformedKeyValuePair - /// Either the key or value is empty. - case emptyKeyValuePair(pair: (String, String)) - } - - // MARK: - Configuration - - /// Delimeter for key value pairs, defaults to `=`. - public static var delimeter: Character = "=" - - /// Environment fallback strategy. - public static var fallbackStrategy: FallbackStrategy = .init(query: .configuration, fallback: .process) - - // MARK: - Storage - - /// Backing environment values. - public private(set) var values: OrderedDictionary - - /// Process info instance. - private let processInfo: ProcessInfo - - // MARK: - Initialization - - /// Create an environment from a dictionary of keys and values. - /// - Parameters: - /// - values: Dictionaries of keys and values to seed the environment will. - /// - processInfo: The process info instance to read system environment values from, defaults to `ProcessInfo.processInfo`. - public init(values: OrderedDictionary, processInfo: ProcessInfo = ProcessInfo.processInfo) throws { - let transformedValues: OrderedDictionary = try values - .reduce(into: OrderedDictionary.init()) { accumulated, current in - // remove invalid values from - guard !current.key.isEmpty && !current.value.isEmpty else { - throw DecodingFailure.emptyKeyValuePair(pair: current) - } - accumulated[current.key] = Value(current.value) - } - try self.init(values: transformedValues, processInfo: processInfo) - } - - /// Create an environment from a dictionary of keys and values. - /// - Parameter values: Dictionary of keys and values to seed the environment with. - /// - Throws: If any key value pair is malformed or empty. - public init(values: OrderedDictionary, processInfo: ProcessInfo = ProcessInfo.processInfo) throws { - self.values = try values - .filter { - // have to check the string case and make sure that the key and value pair are not empty - // only needs to be checked for strings which unlike booleans, doubles, and integers, can be empty - guard case let .string(value) = $0.value else { - return true - } - guard !value.isEmpty && !$0.key.isEmpty else { - throw DecodingFailure.emptyKeyValuePair(pair: ($0.key, value)) - } - return true - } - - self.processInfo = processInfo - } - - /// Create an empty environment. - /// - Parameter processInfo: Process info to seed the environment with. - public init(processInfo: ProcessInfo = ProcessInfo.processInfo) throws { - try self.init(values: [:] as OrderedDictionary, processInfo: processInfo) - } - - /// Create an environment from the contents of a file. - /// - Parameter contents: File contents. - init(contents: String, processInfo: ProcessInfo = ProcessInfo.processInfo) throws { - let lines = contents.split(separator: "\n") - // we loop over all the entries in the file which are already separated by a newline - var values: OrderedDictionary = .init() - for line in lines { - // ignore comments - if line.starts(with: "#") { - continue - } - // split by the delimeter - let substrings = line.split(separator: Self.delimeter) - // make sure we can grab two and only two string values - guard - let key = substrings.first?.trimmingCharacters(in: .whitespacesAndNewlines), - let value = substrings.last?.trimmingCharacters(in: .whitespacesAndNewlines), - substrings.count == 2, - !key.isEmpty, - !value.isEmpty else { - throw DecodingFailure.malformedKeyValuePair - } - // add the results to our values - values[String(key)] = Value(String(value)) - } - self.values = values - self.processInfo = processInfo - } - - // MARK: - Modifying the Environment - - /// Set a new value in the environment for the given key, optionally specifiying if the value should overwrite an existing value, if any exists. - /// - Parameters: - /// - value: Value to set in the environment. - /// - key: Key to set value for. - /// - force: Flag that indicates if the value should be forced if a value with the same key exists, defaults to `false`. - /// - Important: If the environment is set to read from the process, this method is a no-op as the process' environment is read-only. - public mutating func setValue(_ value: Value?, forKey key: String, force: Bool = false) { - guard values[key] == nil || force else { - return - } - values[key] = value - } - - /// Set a new value in the environment for the given key, optionally specifiying if the value should overwrite an existing value, if any exists. - /// - Parameters: - /// - value: Value to set in the environment. - /// - key: Key to set value for. - /// - force: Flag that indicates if the value should be forced if a value with the same key exists, defaults to `false`. - /// - Important: If the environment is set to read from the process, this method is a no-op as the process' environment is read-only. - public mutating func setValue(_ value: String, forKey key: String, force: Bool = false) { - setValue(Value(value), forKey: key, force: force) - } - - /// Remove a value for the given key, returning the old value, if any exsts. - /// - Parameter key: Key to remove. - /// - Returns: Old value that was removed, if any. - @discardableResult - public mutating func removeValue(forKey key: String) -> Value? { - let oldValue = queryValue(forKey: key) - setValue(nil, forKey: key, force: true) - return oldValue - } - - // MARK: - Serialization - - /// Transform the environment into a string representation that can be written to disk. - /// - Important: If the environment is backed by the current process, this method will **not** serialize those values to disk, only those manually set via `setValue(_:forKey:force:)`. - /// - Returns: File contents. - func serialize() throws -> String { - values.enumerated().reduce(into: "") { accumulated, current in - accumulated += "\(current.element.key)\(Self.delimeter)\(current.element.value.stringValue)\n" - } - } - - // MARK: - Subscript - - public subscript(key: String) -> Value? { - get { - queryValue(forKey: key) - } set { - setValue(newValue, forKey: key) - } - } - - public subscript(key: String, default defaultValue: @autoclosure () -> Value) -> Value { - get { - queryValue(forKey: key) ?? defaultValue() - } set { - setValue(newValue, forKey: key) - } - } - - // MARK: Dynamic Member Lookup - - public subscript(dynamicMember member: String) -> Value? { - get { - queryValue(forKey: member.camelCaseToSnakeCase().uppercased()) - } set { - setValue(newValue, forKey: member.camelCaseToSnakeCase().uppercased()) - } - } - - // MARK: - Helpers - - /// Fetch the value for the given key. - /// - Parameter key: Key to query for. - /// - Returns: Value if any exists. - private func queryValue(forKey key: String) -> Value? { - // check which should be queried first - let value: Value? = query(datasource: Self.fallbackStrategy.query, forKey: key) - // if we pulled a non-nil value out, then no need to fallback - guard value != nil else { - return query(datasource: Self.fallbackStrategy.fallback, forKey: key) - } - return value - } - - /// Helper function to query the environment based on the given datasource. - /// - Parameters: - /// - datasource: Environment datasource to query. The parameter is optional so that optionality checks are performed internal - /// to this method, rather then at every callsite, which matters for the fallback strategy where `fallbackStrategy.fallback` is optional. - /// - key: Key to query for. - /// - Returns: Value if any exists. - private func query(datasource: DataSource?, forKey key: String) -> Value? { - // early exit if the datasource is nil - guard let datasource = datasource else { - return nil - } - switch datasource { - case .configuration: - return values[key] - case .process: - guard let environmentValue = processInfo.environment[key] else { - return nil - } - return Value(environmentValue) - } - } -} diff --git a/Tests/DotenvTests.swift b/Tests/DotenvTests.swift index 7b4f001..0f700cd 100644 --- a/Tests/DotenvTests.swift +++ b/Tests/DotenvTests.swift @@ -9,52 +9,70 @@ import SwiftDotenv import XCTest final class DotenvTests: XCTestCase { - + private static var temporarySaveLocation: String { "\(NSTemporaryDirectory())swift-dotenv/" } - + override func setUpWithError() throws { try FileManager.default.createDirectory( at: URL(fileURLWithPath: Self.temporarySaveLocation), withIntermediateDirectories: true, attributes: nil ) + guard let path = Bundle.module.path(forResource: "fixture", ofType: "env") else { + XCTFail("unable to find env file") + return + } + + try Dotenv.configure(atPath: path) + } - + override func tearDownWithError() throws { if FileManager.default.fileExists(atPath: Self.temporarySaveLocation) { try FileManager.default.removeItem(at: URL(fileURLWithPath: Self.temporarySaveLocation)) } } - - func testLoadingEnvironment() throws { - guard let path = Bundle.module.path(forResource: "fixture", ofType: "env") else { - XCTFail("unable to find env file") - return - } - let env = try Dotenv.load(path: path) - print(env.values) - XCTAssertEqual(env.apiKey, .string("some-value")) - XCTAssertEqual(env.buildNumber, .integer(5)) - XCTAssertEqual(env.identifier, .string("com.app.example")) - XCTAssertEqual(env.mailTemplate, .string("The \"Quoted\" Title")) - XCTAssertEqual(env.dbPassphrase, .string("1qaz?#@\"' wsx$")) - XCTAssertNil(env.nonExistentValue) - } - - func testSavingEnvironment() throws { - let env = try Environment(values: [ - "apiKey": .string("some-secret"), - "onboardingEnabled": .boolean(true), - "networkRetries": .integer(3), - "networkTimeout": .double(10.5) - ]) - - let filePath = Self.temporarySaveLocation + "/test.env" - try Dotenv.save(environment: env, toPath: filePath) - - let stringValue = try String(contentsOf: URL(fileURLWithPath: filePath)) - - XCTAssertEqual(stringValue, "apiKey=some-secret\nonboardingEnabled=true\nnetworkRetries=3\nnetworkTimeout=10.5\n") + + func testConfiguringEnvironment() throws { + XCTAssertEqual(Dotenv.apiKey, .string("some-value")) + XCTAssertEqual(Dotenv.buildNumber, .integer(5)) + XCTAssertEqual(Dotenv.identifier, .string("com.app.example")) + XCTAssertEqual(Dotenv.mailTemplate, .string("The \"Quoted\" Title")) + XCTAssertEqual(Dotenv.dbPassphrase, .string("1qaz?#@\"' wsx$")) + XCTAssertNil(Dotenv.nonExistentValue) + } + + func testSubscriptingByStrings() throws { + // implicitly testing string subscripting + XCTAssertEqual(Dotenv["API_KEY"], .string("some-value")) + XCTAssertEqual(Dotenv["BUILD_NUMBER"], .integer(5)) + XCTAssertEqual(Dotenv["IDENTIFIER"], .string("com.app.example")) + } + + func testSubscriptingNonexistantValue() { + XCTAssertNil(Dotenv.randomVariable) + } + + func testSettingValues() { + Dotenv.set(value: "1234", forKey: "API_KEY") + + XCTAssertEqual(Dotenv.apiKey, .integer(1234)) + XCTAssertEqual(Dotenv.processInfo.environment["API_KEY"], "1234") + } + + func testOverridingValues() { + setenv("API_KEY", "1234", 1) + + XCTAssertEqual(Dotenv.processInfo.environment["API_KEY"], "1234") + + Dotenv.set(value: "secret-key", forKey: "API_KEY", overwrite: true) + + XCTAssertEqual(Dotenv.processInfo.environment["API_KEY"], "secret-key") + + Dotenv.set(value: "super-secret-key", forKey: "API_KEY", overwrite: false) + + XCTAssertEqual(Dotenv.processInfo.environment["API_KEY"], "secret-key") } } + diff --git a/Tests/EnvironmentTests.swift b/Tests/EnvironmentTests.swift deleted file mode 100644 index d573bca..0000000 --- a/Tests/EnvironmentTests.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// EnvironmentTests.swift -// SwiftDotenv -// -// Created by Brendan Conron on 10/17/21. -// - -import SwiftDotenv -import XCTest - -final class EnvironmentTests: XCTestCase { - - func testCreatingEnvironmentFromStringDictionary() throws { - let env = try Environment(values: [ - "API_KEY": "some-secret", - "ONBOARDING_ENABLED": "true", - "NETWORK_RETRIES": "3", - "NETWORK_TIMEOUT": "10.5" - ]) - - // implicitly testing string subscripting - XCTAssertEqual(env["API_KEY"], .string("some-secret")) - XCTAssertEqual(env["ONBOARDING_ENABLED"], .boolean(true)) - XCTAssertEqual(env["NETWORK_RETRIES"], .integer(3)) - XCTAssertEqual(env["NETWORK_TIMEOUT"], .double(10.5)) - } - - func testCreatingEnvironmentFromTypeSafeConstruct() throws { - let env = try Environment(values: [ - "API_KEY": .string("some-secret"), - "ONBOARDING_ENABLED": .boolean(true), - "NETWORK_RETRIES": .integer(3), - "NETWORK_TIMEOUT": .double(10.5) - ]) - - // implicitly testing subscripting - XCTAssertEqual(env.apiKey, .string("some-secret")) - XCTAssertEqual(env.onboardingEnabled, .boolean(true)) - XCTAssertEqual(env.networkRetries, .integer(3)) - XCTAssertEqual(env.networkTimeout, .double(10.5)) - } - - // MARK: - Adding and Removing Values - - func testAddingValueForNonexistantKey() throws { - var environment = try Environment() - - XCTAssertNil(environment.key) - - environment.setValue(.integer(1), forKey: "KEY") - - XCTAssertEqual(environment.key, .integer(1)) - print(environment.values) - } - - func testAddingValueForExistingKeyWithoutForcing() throws { - var environment = try Environment(values: [ - "KEY": .integer(1) - ]) - - XCTAssertEqual(environment.key, .integer(1)) - - environment.setValue(.integer(2), forKey: "KEY") - - XCTAssertEqual(environment.key, .integer(1)) - } - - func testAddingValueForExistingValueWithForcing() throws { - var environment = try Environment(values: [ - "KEY": .integer(1) - ]) - - XCTAssertEqual(environment.key, .integer(1)) - - environment.setValue(.integer(2), forKey: "KEY", force: true) - - XCTAssertEqual(environment.key, .integer(2)) - } - - func testRemovingNonexistantValue() throws { - var environment = try Environment() - - XCTAssertNil(environment.key) - - let oldValue = environment.removeValue(forKey: "KEY") - - XCTAssertNil(oldValue) - } - - - func testRemovingValue() throws { - var environment = try Environment(values: [ - "KEY": .integer(1) - ]) - - XCTAssertNotNil(environment.key) - - let oldValue = environment.removeValue(forKey: "KEY") - - XCTAssertEqual(oldValue, .integer(1)) - XCTAssertNil(environment.key) - } -}