packages feed

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 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]