Skip to content

Commit

Permalink
Add @EnvironmentVariable property wrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
wlisac committed Aug 3, 2019
1 parent 74427c5 commit b8c64b0
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 1 deletion.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,22 @@ let host: URL? = Environment.HOST
Environment.PORT = 8000
```

### Property Wrappers

The `EnvironmentVariable` [property wrapper](https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md) enables properties to be backed by environment variables in Swift 5.1.

The following example shows how to use the `EnvironmentVariable` property wrapper to expose static properties backed by enviornment variables (`"HOST"` and `"PORT"`).

```swift
enum ServerSettings {
@EnvironmentVariable(name: "HOST")
static var host: URL?

@EnvironmentVariable(name: "PORT", defaultValue: 8000)
static var port: Int
}
```

### Type-Safe Variables

Environment variables can be converted from a `String` representation to any type that conforms to the `EnvironmentStringConvertible` protocol.
Expand Down
2 changes: 1 addition & 1 deletion Sources/Environment/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ extension Environment {
/// for the given `dynamicMember` if `dynamicMember` is in the environment; otherwise, `nil`.
public static subscript<T>(dynamicMember member: String) -> T? where T: EnvironmentStringConvertible {
get {
return self[member]
self[member]
}
set {
self[member] = newValue
Expand Down
65 changes: 65 additions & 0 deletions Sources/Environment/EnvironmentVariable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// EnvironmentVariable.swift
// Environment
//
// Created by Will Lisac on 7/27/19.
//

#if swift(>=5.1)
/// A property wrapper that uses the specified environment variable name for property access and backing storage.
///
/// The following example shows how to use the `EnvironmentVariable` property wrapper to expose
/// static properties backed by enviornment variables (`"HOST"` and `"PORT"`).
///
/// enum ServerSettings {
/// @EnvironmentVariable(name: "HOST")
/// static var host: URL?
///
/// @EnvironmentVariable(name: "PORT", defaultValue: 8000)
/// static var port: Int
/// }
///
@propertyWrapper
public struct EnvironmentVariable<T> {
private let name: String
private let defaultValue: T

private let fromEnvironmentString: (String) -> T?
private let toEnvironmentString: (T) -> String?

/// The environment variable value converted to type `T` using the `EnvironmentStringConvertible` protocol
/// for the specified environment variable `name` if `name` is in the environment; otherwise, the specified `defaultValue`.
public var wrappedValue: T {
get {
Environment[name].flatMap { fromEnvironmentString($0) } ?? defaultValue
}
nonmutating set {
Environment[name] = toEnvironmentString(newValue)
}
}
}

extension EnvironmentVariable where T: EnvironmentStringConvertible {
/// Instantiates an `EnvironmentVariable` property wrapper for the specified environment variable name and default value.
/// - Parameter name: The environment variable name to use for property access and backing storage.
/// - Parameter defaultValue: The default value to use if the name does not exist in the environment or if type conversion fails.
public init(name: String, defaultValue: T) {
self.name = name
self.defaultValue = defaultValue
self.fromEnvironmentString = { T(environmentString: $0) }
self.toEnvironmentString = { $0.environmentString }
}
}

extension EnvironmentVariable {
/// Instantiates an `EnvironmentVariable` property wrapper for the specified environment variable name and default value.
/// - Parameter name: The environment variable name to use for property access and backing storage.
/// - Parameter defaultValue: The default value to use if the name does not exist in the environment or if type conversion fails.
public init<U: EnvironmentStringConvertible>(name: String, defaultValue: T = nil) where T == U? {
self.name = name
self.defaultValue = defaultValue
self.fromEnvironmentString = { U(environmentString: $0) }
self.toEnvironmentString = { $0?.environmentString }
}
}
#endif
157 changes: 157 additions & 0 deletions Tests/EnvironmentTests/EnvironmentVariableTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
//
// EnvironmentVariableTests.swift
// EnvironmentTests
//
// Created by Will Lisac on 7/27/19.
//

import Environment
import XCTest

//swiftlint:disable let_var_whitespace
class EnvironmentVariableTests: XCTestCase {
override func setUp() {
super.setUp()
resetEnvironment()
}

func testString() {
#if swift(>=5.1)
XCTAssertEqual(EnvironmentSettings.string, "hello")

Environment.STRING = "string"
XCTAssertEqual(EnvironmentSettings.string, "string")

Environment.STRING = nil
XCTAssertEqual(EnvironmentSettings.string, "hello")

EnvironmentSettings.string = "custom"
XCTAssertEqual(EnvironmentSettings.string, "custom")
XCTAssertEqual(Environment.STRING, "custom")
#endif
}

func testOptionalString() {
#if swift(>=5.1)
XCTAssertNil(EnvironmentSettings.optionalString)

Environment.OPTIONAL_STRING = "string"
XCTAssertEqual(EnvironmentSettings.optionalString, "string")

Environment.OPTIONAL_STRING = nil
XCTAssertNil(EnvironmentSettings.optionalString)

EnvironmentSettings.optionalString = "custom"
XCTAssertEqual(EnvironmentSettings.optionalString, "custom")
XCTAssertEqual(Environment.OPTIONAL_STRING, "custom")

EnvironmentSettings.optionalString = nil
XCTAssertNil(EnvironmentSettings.optionalString)
XCTAssertNil(Environment.OPTIONAL_STRING)
#endif
}

func testInt() {
#if swift(>=5.1)
XCTAssertEqual(EnvironmentSettings.int, 10)

Environment.INT = 50
XCTAssertEqual(EnvironmentSettings.int, 50)

Environment.INT = nil
XCTAssertEqual(EnvironmentSettings.int, 10)

EnvironmentSettings.int = 30
XCTAssertEqual(EnvironmentSettings.int, 30)
XCTAssertEqual(Environment.INT, 30)
XCTAssertEqual(Environment.INT, "30")
#endif
}

func testOptionalInt() {
#if swift(>=5.1)
XCTAssertNil(EnvironmentSettings.optionalInt)

Environment.OPTIONAL_INT = 10
XCTAssertEqual(EnvironmentSettings.optionalInt, 10)

Environment.OPTIONAL_INT = nil
XCTAssertNil(EnvironmentSettings.optionalInt)

EnvironmentSettings.optionalInt = 30
XCTAssertEqual(EnvironmentSettings.optionalInt, 30)
XCTAssertEqual(Environment.OPTIONAL_INT, 30)
XCTAssertEqual(Environment.OPTIONAL_INT, "30")

EnvironmentSettings.optionalInt = nil
XCTAssertNil(EnvironmentSettings.optionalInt)
XCTAssertNil(Environment.OPTIONAL_INT)
#endif
}

func testIntArray() {
#if swift(>=5.1)
XCTAssertEqual(EnvironmentSettings.intArray, [])

Environment.INT_ARRAY = "1,2,3"
XCTAssertEqual(EnvironmentSettings.intArray, [1, 2, 3])

Environment.INT_ARRAY = nil
XCTAssertEqual(EnvironmentSettings.intArray, [])

EnvironmentSettings.intArray = [3, 2, 1]
XCTAssertEqual(EnvironmentSettings.intArray, [3, 2, 1])
XCTAssertEqual(Environment.INT_ARRAY, "3,2,1")
#endif
}

func testOptionalIntArray() {
#if swift(>=5.1)
XCTAssertNil(EnvironmentSettings.optionalIntArray)

Environment.OPTIONAL_INT_ARRAY = [10, 20]
XCTAssertEqual(EnvironmentSettings.optionalIntArray, [10, 20])

Environment.OPTIONAL_INT_ARRAY = nil
XCTAssertNil(EnvironmentSettings.optionalIntArray)

EnvironmentSettings.optionalIntArray = [30, 40]
XCTAssertEqual(EnvironmentSettings.optionalIntArray, [30, 40])
XCTAssertEqual(Environment.OPTIONAL_INT_ARRAY, [30, 40])
XCTAssertEqual(Environment.OPTIONAL_INT_ARRAY, "30,40")

EnvironmentSettings.optionalIntArray = nil
XCTAssertNil(EnvironmentSettings.optionalIntArray)
XCTAssertNil(Environment.OPTIONAL_INT_ARRAY)
#endif
}

private func resetEnvironment() {
ProcessInfo.processInfo.environment.keys.forEach { unsetenv($0) }

XCTAssert(ProcessInfo.processInfo.environment.isEmpty)
}
}

#if swift(>=5.1)
enum EnvironmentSettings {
@EnvironmentVariable(name: "STRING", defaultValue: "hello")
static var string: String

@EnvironmentVariable(name: "OPTIONAL_STRING")
static var optionalString: String?

@EnvironmentVariable(name: "INT", defaultValue: 10)
static var int: Int

@EnvironmentVariable(name: "OPTIONAL_INT")
static var optionalInt: Int?

@EnvironmentVariable(name: "INT_ARRAY", defaultValue: [])
static var intArray: [Int]

@EnvironmentVariable(name: "OPTIONAL_INT_ARRAY")
// swiftlint:disable:next discouraged_optional_collection
static var optionalIntArray: [Int]?
}
#endif
15 changes: 15 additions & 0 deletions Tests/EnvironmentTests/XCTestManifests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,24 @@ extension EnvironmentTests {
]
}

extension EnvironmentVariableTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__EnvironmentVariableTests = [
("testInt", testInt),
("testIntArray", testIntArray),
("testOptionalInt", testOptionalInt),
("testOptionalIntArray", testOptionalIntArray),
("testOptionalString", testOptionalString),
("testString", testString),
]
}

public func __allTests() -> [XCTestCaseEntry] {
return [
testCase(EnvironmentTests.__allTests__EnvironmentTests),
testCase(EnvironmentVariableTests.__allTests__EnvironmentVariableTests),
]
}
#endif

0 comments on commit b8c64b0

Please sign in to comment.