diff --git a/README.md b/README.md index 5a53d0b..73191bc 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ Add `.package(url: "https://github.com/SwiftScream/swift-environment-decoder.git ## Usage +[Detailed documentation](https://swiftpackageindex.com/SwiftScream/swift-environment-decoder/master/documentation/environmentdecoder) is available on the swift package index. + +This should be enough to get you started... + ### Decoding the Environment ```swift diff --git a/Sources/EnvironmentDecoder/EnvironmentDecoder.docc/EnvironmentDecoder.md b/Sources/EnvironmentDecoder/EnvironmentDecoder.docc/EnvironmentDecoder.md new file mode 100644 index 0000000..f8f5010 --- /dev/null +++ b/Sources/EnvironmentDecoder/EnvironmentDecoder.docc/EnvironmentDecoder.md @@ -0,0 +1,154 @@ +# ``EnvironmentDecoder`` + + +## Overview + +Suppose you're building a server app and want to enable specifying the port to bind to in the environment. You might write something like this: + +``` +let port = Int(ProcessInfo.processInfo.environment["PORT"]) ?? 3000 +``` + +As your use of the environment grows, this can become cumbersome and error prone. + +``EnvironmentDecoder`` enables type-safe access to the environment leveraging Swift's `Decodable`. + +To use it you specify the shape of the environment that your application expects as a `Decodable` struct. ``EnvironmentDecoder`` can instantiate an instance of this struct from the current environment, throwing an Error if the environment is not valid. + +## Example + +Let's say you have a server app and want the environment to specify the port to bind to, a list of allowed regions, and a couple of boolean feature flags. + +Your environment might look like: + +``` +PORT=1234 +ALLOWED_REGIONS=regionA,regionB,regionC +FEATURE_FLAGS_ENABLE_A=true +FEATURE_FLAGS_ENABLE_B=false +``` + +``EnvironmentDecoder`` enables access to this like: + +```swift +import EnvironmentDecoder + +struct MyEnvironment: Decodable { + let port: UInt16 + let allowedRegions: [String] + let featureFlags: MyEnvironmentFeatureFlags +} + +struct MyEnvironmentFeatureFlags: Decodable { + let enableA: Bool + let enableB: Bool +} + +let environment = try EnvironmentDecoder().decode(MyEnvironment.self) +``` + +## Mapping Properties to Environment Variables + +Environment variables are a flat namespace, but Decodable types can be nested to create meaningful hierarchy. + +``EnvironmentDecoder`` will, by default, leverage the type hierarcy to create meaningful environment variable names. This can be disabled in the initializer by specifying `prefixKeysWithCodingPath: false`. + +#### An Example: + +```swift +struct DatabaseConnection: Decodable { + let connectionString: String +} + +struct WebServiceConnection: Decodable { + let host: String + let port: Int16 +} + +struct Environment: Decodable { + let region: String + let database: DatabaseConnection + let webService: WebServiceConnection +} +``` + +When `prefixKeysWithCodingPath` is enabled the environment is expected to be: +``` +REGION +DATABASE_CONNECTION_STRING +WEB_SERVICE_HOST +WEB_SERVICE_PORT +``` + +When `prefixKeysWithCodingPath` is disabled the environment is expected to be: +``` +REGION +CONNECTION_STRING +HOST +PORT +``` + +The usual methods that the Codable framework provides can be used to customise coding keys. + +## Supported Data Types + +Almost all `Decodable` data structures are supported. + +The one limitation is that elements of an Unkeyed Container must not be represented by an Unkeyed Container or Keyed Container. +For example, elements of an array cannot themselves be an array, or an object represented by key/value pairs. + +It is possible to use a custom Decodable type in an array, so long as that type decodes from a single value. + +Outside of this limitation, all Decodable types are supported. Some notes on specific data types follows. + +#### Data + +Several strategies are supported for decoding `Data`. + +Refer to ``EnvironmentDecoder/DataDecodingStrategy`` + +#### Date + +Several strategies are supported for decoding `Date`. + +Refer to ``EnvironmentDecoder/DateDecodingStrategy`` + +#### Unkeyed Containers + +UnkeyedContainers, for example Arrays, are supported so long as the element value is represented by a single value. + +UnkeyedContainer values are decoded from a character-separated list of values. +By default the separator character is a comma. This can be customised by specifying `unkeyedContainerSeparator` in the initializer. + +By default whitespace is trimmed from unkeyed container values. This can be disabled by specifying `trimWhitespaceFromUnkeyedContainerValues` in the initializer. + + +## Error Handling + +If the environment cannot be decoded into the specified type, a `Swift.DecodingError` is thrown. + +This is a great way to catch issues with the environment early. In some cases it may be appropriate to output a message, and terminate the process. + +Some examples of why this may occur: + + - a required environment variable is not specified + - the value of an environment variable cannot be decoded as the specified type + +```swift +let environment: MyEnvironment +do { + environment = try EnvironmentDecoder().decode(MyEnvironment.self) +} catch { + fatalError(error.localizedDescription) +} +``` + +## Alternative Environment + +By default EnvironmentDecoder will decode `ProcessInfo.processInfo.environment`. +If that's not what you want, you can pass in a `[String: String]` instead. + +```swift +let someEnvDictionary: [String: String] = //... +let environment = try EnvironmentDecoder().decode(MyEnvironment.self, from: someEnvDictionary) +```