nri-env-parser (empty) → 0.1.0.0
raw patch · 5 files changed
+444/−0 lines, 5 filesdep +basedep +modern-uridep +network-uri
Dependencies added: base, modern-uri, network-uri, nri-prelude, text
Files
- CHANGELOG.md +3/−0
- LICENSE +29/−0
- README.md +9/−0
- nri-env-parser.cabal +46/−0
- src/Environment.hs +357/−0
+ CHANGELOG.md view
@@ -0,0 +1,3 @@+# 0.1.0.0++- Initial release.
+ LICENSE view
@@ -0,0 +1,29 @@+BSD 3-Clause License++Copyright (c) 2017, NoRedInk+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++* Redistributions of source code must retain the above copyright notice, this+ list of conditions and the following disclaimer.++* Redistributions in binary form must reproduce the above copyright notice,+ this list of conditions and the following disclaimer in the documentation+ and/or other materials provided with the distribution.++* Neither the name of the copyright holder nor the names of its+ contributors may be used to endorse or promote products derived from+ this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,9 @@+# nri-env-parser++_Reviewed last on 2019-07-20_++The 12-factor app uses environment variables for configuration.+A Haskell app will need to parse environmental values into types.+This library provides functionality to do so for commonly used typed.++It also provides a way to generate an overview of all environment variables an app consumes.
+ nri-env-parser.cabal view
@@ -0,0 +1,46 @@+cabal-version: 1.18++-- This file has been generated from package.yaml by hpack version 0.33.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: d22fe61021208189a56a80ca09f2b903d8654a0bb3e96494641826d0dae29ca2++name: nri-env-parser+version: 0.1.0.0+synopsis: Read environment variables as settings to build 12-factor apps.+description: Please see the README at <https://github.com/NoRedInk/haskell-libraries/tree/trunk/env-parser>.+homepage: https://github.com/NoRedInk/haskell-libraries#readme+bug-reports: https://github.com/NoRedInk/haskell-libraries/issues+author: NoRedInk+maintainer: haskell-open-source@noredink.com+copyright: 2020 NoRedInk Corp.+license: BSD3+license-file: LICENSE+build-type: Simple+extra-doc-files:+ README.md+ LICENSE+ CHANGELOG.md++source-repository head+ type: git+ location: https://github.com/NoRedInk/haskell-libraries+ subdir: env-parser++library+ exposed-modules:+ Environment+ other-modules:+ Paths_nri_env_parser+ hs-source-dirs:+ src+ default-extensions: DataKinds DeriveGeneric FlexibleContexts FlexibleInstances GeneralizedNewtypeDeriving MultiParamTypeClasses NamedFieldPuns NoImplicitPrelude OverloadedStrings PartialTypeSignatures ScopedTypeVariables Strict TypeOperators+ ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wpartial-fields -Wredundant-constraints -Wincomplete-uni-patterns+ build-depends:+ base >=4.12.0.0 && <4.13+ , modern-uri >=0.3.1.0 && <0.4+ , network-uri >=2.6.2.0 && <2.8+ , nri-prelude >=0.1.0.0 && <0.2+ , text >=1.2.3.1 && <1.3+ default-language: Haskell2010
+ src/Environment.hs view
@@ -0,0 +1,357 @@+{-# LANGUAGE DeriveFunctor #-}++-- | A module for reading configuration options from environment variables.+--+-- Applications have configuration options. [The Twelve-Factor App] recommends+-- applications read these from environment variables. This requires us to+-- decode environment variables, which are strings, into the different types the+-- app's configuration options might have. This module helps with that.+--+-- There's a couple of similar modules out there (like @envparse@ and @envy@),+-- but we find them overly complicated in some ways and lacking in others.+-- Here's what sets this package apart from these other approaches:+--+-- - Very small API, supporting just one way to do environment parsing.+-- - Comes with parsers for common configuration option types, such as uris.+-- Not using type classes for these parsers means we don't have to write a+-- bunch of orphan instances.+-- - Mandatory documentation of each environment variable we want to decode.+-- - The decoders keep track of all the environment variables they depend on.+-- That way the decoder for an application can tell us all the environment+-- variables an application depends on and (because of the mandatory+-- documentation) what those environment variables are used for.+--+-- [The Twelve-Factor App]: https://12factor.net/import NriPrelude+module Environment+ ( -- * Parsers+ Parser,+ text,+ int,+ float,+ boolean,+ uri,+ filePath,+ networkURI,+ secret,+ custom,++ -- * Decoders+ Decoder,+ consumes,+ Variable (Variable, name, description, defaultValue),+ variable,+ either,+ decode,+ decodeDefaults,+ decodePairs,++ -- * Decoders for just the variables+ decodeVariables,+ decodeVariablePairs,+ DecodedVariable+ ( DecodedVariable,+ decodedVariable,+ decodedCurrent,+ decodedErrors+ ),+ )+where++import qualified Data.Text+import qualified Debug+import qualified Dict+import qualified List+import qualified Log+import qualified Maybe+import qualified Network.URI+import NriPrelude+import qualified Result+import qualified System.Environment+import Text.Read (readMaybe)+import qualified Text.URI+import qualified Tuple+import Prelude+ ( Applicative,+ Either (Left, Right),+ FilePath,+ Functor,+ IO,+ Integer,+ Integral,+ Semigroup,+ fail,+ fromIntegral,+ mempty,+ pure,+ )++-- |+-- A function that can read values of a type from text. For example, a+-- @Parser Int@ knows how to read a string like "412" and extract from that the+-- number @412@.+--+-- Parsing functions can fail when they read a text that they do not understand.+-- For example, the @Parser Int@ parser will fail if ran against the string+-- "Not a number in the slightest!".+newtype Parser a+ = Parser (Text -> Result Text a)+ deriving (Functor)++-- | Parse a text from an environment variable.+text :: Parser Text+text = Parser Ok++-- | Parse an integer from an environment variable.+-- Works for any integer type (@Integer@, @Int@, @Int@, ...).+int :: (Integral a) => Parser a+int =+ Parser <| \str ->+ case readMaybe (Data.Text.unpack str) of+ Nothing -> Err ("Could not parse as integer: " ++ str)+ Just (n :: Integer) -> Ok (fromIntegral n)++-- | Parse a floating point number from an environment variable.+float :: Parser Float+float =+ Parser <| \str ->+ case readMaybe (Data.Text.unpack str) of+ Nothing -> Err ("Could not parse as float: " ++ str)+ Just n -> Ok n++-- | Parse a boolean from an environment variable.+boolean :: Parser Bool+boolean =+ Parser <| \str ->+ case readMaybe (Data.Text.unpack str) of+ Nothing -> Err ("Could not parse as boolean: " ++ str)+ Just x -> Ok x++-- | Parse a URI from an environment variable.+uri :: Parser Text.URI.URI+uri =+ Parser <| \str ->+ case Text.URI.mkURI str of+ Left err ->+ ["Unexpected exception parsing uri:", str, ". Error reads:", Debug.toString err]+ |> Data.Text.unwords+ |> Err+ Right x -> Ok x++-- | Parse a file path from an environment variable.+filePath :: Parser FilePath+filePath = Parser (Ok << Data.Text.unpack)++-- | Parse a secret value from an environment variable.+--+-- Check the documentation for the @Log@ module of @nri-prelude@ to learn more+-- about secrets.+secret :: Parser a -> Parser (Log.Secret a)+secret = map Log.mkSecret++-- | There's two @URI@ types that are in vogue in the Haskell ecosystem. We would+-- like to standardized on the @Text.URI@ package, since it's the more modern+-- @Text@ based version (no @Strings@ for us!), but most libraries require the+-- other type. This function helps convert.+networkURI :: Parser Network.URI.URI+networkURI =+ Parser <| \str ->+ case Network.URI.parseURI (Data.Text.unpack str) of+ Nothing -> Err "Oh no! We have a valid Network.URI.URI but can't seem to parse it as a Network.URI.URI."+ Just uri' -> Ok uri' {Network.URI.uriPath = ""}++-- | Create a parser for custom types. Build on the back of one of the primitve+-- parsers from this module.+--+-- data Environment = Development | Production+--+-- environment :: Parser Environment+-- environment =+-- custom text <| \str ->+-- case str of+-- "development" -> Ok Development+-- "production" -> Ok Production+-- _ -> Err ("Unknown environment: " ++ str)+custom :: Parser a -> (a -> Result Text b) -> Parser b+custom (Parser base) fn = Parser (\val -> base val |> andThen fn)++-- | An environment decoder knows how to read an app's configuration from+-- environment variables. Check out the @variable@ function to see how you can+-- begin building decoders.+data Decoder config+ = Decoder+ { -- | The list of @Variable@s that this decoder will read when ran.+ consumes :: [Variable],+ readFromEnvironment :: Dict.Dict Text Text -> Result [ParseError] config+ }+ deriving (Functor)++instance Applicative Decoder where+ pure x = Decoder [] (\_ -> Ok x)++ (Decoder consumes1 f) <*> (Decoder consumes2 x) =+ Decoder+ { consumes = consumes1 ++ consumes2,+ readFromEnvironment = readFromEnvironment' f x+ }+ where+ readFromEnvironment' ::+ Semigroup err =>+ (env -> Result err (a -> config)) ->+ (env -> Result err a) ->+ env ->+ Result err config+ readFromEnvironment' f' x' env =+ -- This is the same as <*> except that when both sides are errors we+ -- want to capture both, not just the first.+ case (f' env, x' env) of+ (Err fe, Err xe) ->+ Err (fe ++ xe)+ (fr, xr) ->+ fr <*> xr++-- | An environment variable with a description of what it is used for.+data Variable+ = Variable+ { name :: Text,+ description :: Text,+ defaultValue :: Text+ }+ deriving (Show)++data ParseError+ = ParseError+ { failingVariable :: Variable,+ failingReason :: Text+ }+ deriving (Show)++-- | Describe a decoded variable for informational purposes.+data DecodedVariable+ = DecodedVariable+ { decodedVariable :: Variable,+ decodedCurrent :: Maybe Text,+ -- A single environment variable can be decoded by multiple decoders,+ -- each with their own constraints.+ decodedErrors :: List Text+ }+ deriving (Show)++-- | Produce a configuration from a single environment veriable. Usually you+-- will combine these with @mapN@ functions to build larger configurations.+--+-- Data Settings = Settings+-- { amountOfHats :: Int+-- , furLined :: Bool+-- }+--+-- map2+-- Settings+-- (variable (Variable "HATS" "Amount of hats" "2") int)+-- (variable (Variable "FUR_LINED" "Do hats have fur lining?" "False") boolean)+variable :: Variable -> Parser a -> Decoder a+variable var (Parser parse) =+ Decoder+ { consumes = [var],+ readFromEnvironment = \env ->+ let value =+ Dict.get (name var) env+ |> Maybe.withDefault (defaultValue var)+ in parse value |> Result.mapError (pure << ParseError var)+ }++-- | If the first decoder fails, try the second.+either :: Decoder a -> Decoder a -> Decoder a+either (Decoder consumes1 fa) (Decoder consumes2 fb) =+ Decoder+ { consumes = consumes1 ++ consumes2,+ readFromEnvironment = \env ->+ case fa env of+ Err ea -> case fb env of+ Err eb -> Err (ea ++ eb)+ Ok r -> Ok r+ Ok r -> Ok r+ }++-- | Attempt to decode a configuration by reading environment variables.+-- This will fail if one or more environment variables fail to parse.+--+-- It will not fail if certain environment variables are absent. Defaults will+-- be used for those missing values.+decode :: Decoder a -> IO a+decode configuration = do+ env <- getEnv+ case decodePairs configuration env of+ Err err -> fail (Data.Text.unpack err)+ Ok x -> pure x++-- | Same as 'decode', but takes the environment to decode as a dictionary.+decodePairs :: Decoder a -> Dict.Dict Text Text -> Result Text a+decodePairs configuration env =+ case readFromEnvironment configuration env of+ Err err -> Err (errorsToText err)+ Ok x -> Ok x++-- | Run a decoder. Instead of returnin the decoded value return metadata about+-- each variable that was decoded.+--+-- This can be helpful when generating a @--help@ command, for listing all the+-- variables that the application supports and what they are currently set to.+decodeVariables :: Decoder a -> IO [DecodedVariable]+decodeVariables configuration =+ fmap (decodeVariablePairs configuration) getEnv++-- | Same as 'decodeVariables', but takes the environment to decode as a+-- dictionary.+decodeVariablePairs :: Decoder a -> Dict.Dict Text Text -> [DecodedVariable]+decodeVariablePairs configuration env = do+ var <- consumes configuration+ pure+ ( DecodedVariable+ { decodedVariable = var,+ decodedCurrent = Dict.get (name var) env,+ decodedErrors =+ Dict.get (name var) errors+ |> Maybe.withDefault []+ }+ )+ where+ errors =+ map errorPair parseErrors+ |> List.foldl insert Dict.empty+ insert (k, v) dict =+ Dict.update+ k+ ( \prev ->+ case prev of+ Nothing -> Just v+ Just v' -> Just (v' ++ v)+ )+ dict+ errorPair err =+ ((failingVariable >> name) err, [failingReason err])+ parseErrors =+ case readFromEnvironment configuration env of+ Err errs -> errs+ Ok _ -> []++getEnv :: IO (Dict.Dict Text Text)+getEnv = do+ pairs <- System.Environment.getEnvironment+ pure <| Dict.fromList <| map (Tuple.mapBoth Data.Text.pack Data.Text.pack) pairs++-- | Build a configuration using only default values of environment variables.+-- Similar to @decode@, except this version doesn't read any environment+-- variables.+--+-- This is sometimes useful for tests, where you might not care about the exact+-- values of settings.+decodeDefaults :: Decoder a -> Result Text a+decodeDefaults configuration =+ readFromEnvironment configuration mempty |> Result.mapError errorsToText++errorsToText :: [ParseError] -> Text+errorsToText errors = map errorToText errors |> Data.Text.intercalate "\n\n"++errorToText :: ParseError -> Text+errorToText ParseError {failingVariable, failingReason} =+ Data.Text.unwords ["Parsing", name failingVariable, "failed:", failingReason]