interpolator-1.0.0: src/Data/Interpolation.hs
module Data.Interpolation where
import Prelude hiding (lookup)
import Control.Applicative ((<|>))
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Reader (Reader, asks, runReader)
import Data.Aeson (FromJSON, ToJSON, Value (String), parseJSON, toJSON, withText)
import Data.Char (isAlphaNum)
import Data.Containers (mapFromList, setFromList, setToList)
import Data.Either.Validation (Validation (Failure, Success), validationToEither)
import Data.Map (Map, lookup)
import Data.Profunctor (Profunctor, dimap)
import Data.Profunctor.Product (ProductProfunctor, SumProfunctor, purePP, (****), (+++!))
import Data.Profunctor.Product.Default (Default, def)
import Data.Semigroup ((<>))
import Data.Sequences (isPrefixOf)
import Data.Set (Set)
import qualified Data.Text as T
import System.Environment (getEnvironment)
import Test.QuickCheck (Arbitrary, Arbitrary1, arbitrary, arbitrary1, liftArbitrary, listOf1, oneof, suchThat)
import Text.Read (readMaybe)
-- |Newtype wrapper for an environment variable key.
newtype TemplateKey = TemplateKey { unTemplateKey :: T.Text }
deriving (Eq, Ord, Show, ToJSON, FromJSON)
-- |Newtype wrapper for an environment variable value.
newtype TemplateValue = TemplateValue { unTemplateValue :: T.Text }
deriving (Eq, Ord, Show, ToJSON, FromJSON)
-- |Type for a value that is described by '_env:ENVIRONMENT_VARIABLE:default' in JSON.
data Template a = Template
{ _templateKey :: TemplateKey
, _templateDefault :: Maybe a
}
deriving (Eq, Ord, Show)
-- |Type for a value that can be described either with '_env...' or as just a literal value in JSON.
data Uninterpolated a
= Templated (Template a)
| Literal a
deriving (Eq, Ord, Show)
data InterpolationFailure
= InterpolationFailureKeyNotFound TemplateKey
| InterpolationFailureValueNotReadable TemplateKey TemplateValue
deriving (Eq, Ord)
instance Show InterpolationFailure where
show = \ case
InterpolationFailureKeyNotFound (TemplateKey k) -> "Interpolation key " <> show k <> " not found"
InterpolationFailureValueNotReadable (TemplateKey k) (TemplateValue v) -> "Value " <> show v <> " for interpolation key " <> show k <> " not readable"
newtype InterpolationContext = InterpolationContext { unInterpolationContext :: Map TemplateKey TemplateValue }
-- |A class for parsing environment variable values, which should only be defined on primitives.
-- Similar to 'Read' except that for text-type values it should parse using identity.
class FromTemplateValue a where
parseTemplateValue :: TemplateValue -> Maybe a
instance FromTemplateValue T.Text where
parseTemplateValue = Just . unTemplateValue
instance FromTemplateValue String where
parseTemplateValue = Just . T.unpack . unTemplateValue
instance FromTemplateValue Int where
parseTemplateValue = readMaybe . T.unpack . unTemplateValue
instance FromTemplateValue Bool where
parseTemplateValue x = case T.toLower (unTemplateValue x) of
"true" -> Just True
"false" -> Just False
_ -> Nothing
-- |A class for showing environment variable values, which should only be defined on primitives.
-- Similar to 'Show' except that for text-type values it should use identity.
class ToTemplateValue a where
toTemplateValue :: a -> TemplateValue
instance ToTemplateValue T.Text where
toTemplateValue = TemplateValue
instance ToTemplateValue String where
toTemplateValue = TemplateValue . T.pack
instance ToTemplateValue Int where
toTemplateValue = TemplateValue . T.pack . show
instance ToTemplateValue Bool where
toTemplateValue = TemplateValue . T.toLower . T.pack . show
newtype Interpolator templates identities = Interpolator
{ runInterpolator :: templates -> Reader InterpolationContext (Validation [InterpolationFailure] identities)
}
instance Functor (Interpolator templates) where
fmap f (Interpolator g) = Interpolator $ fmap (fmap (fmap f)) g
instance Applicative (Interpolator templates) where
pure x = Interpolator $ \ _ -> pure $ Success x
Interpolator f <*> Interpolator g = Interpolator $ \ x -> do
f' <- f x
y <- g x
pure $ f' <*> y
instance Profunctor Interpolator where
dimap f g (Interpolator h) = Interpolator (dimap f (fmap (fmap g)) h)
instance ProductProfunctor Interpolator where
purePP = pure
(****) = (<*>)
instance SumProfunctor Interpolator where
Interpolator f +++! Interpolator g = Interpolator $ \ case
Left x -> fmap Left <$> f x
Right y -> fmap Right <$> g y
-- |Run a template using the interpolation context and failing if the value is not found or not readable.
runTemplate :: FromTemplateValue a => Interpolator (Uninterpolated a) a
runTemplate = Interpolator $ \ case
Literal d -> pure $ Success d
Templated (Template k dMay) -> asks (lookup k . unInterpolationContext) >>= pure . \ case
Just v -> maybe (Failure [InterpolationFailureValueNotReadable k v]) Success $ parseTemplateValue v
Nothing -> maybe (Failure [InterpolationFailureKeyNotFound k]) Success dMay
mkInterpolationContext :: MonadIO m => m InterpolationContext
mkInterpolationContext = InterpolationContext . mapFromList . map toTuple <$> liftIO getEnvironment
where
toTuple (x, y) = (TemplateKey $ T.pack x, TemplateValue $ T.pack y)
interpolateWithContext :: (Default Interpolator templates identities, MonadIO m)
=> templates -> m (Either [InterpolationFailure] identities)
interpolateWithContext = interpolateWithContextExplicit def
interpolateWithContextExplicit :: MonadIO m
=> Interpolator templates identities -> templates -> m (Either [InterpolationFailure] identities)
interpolateWithContextExplicit interpolator x = do
ctx <- mkInterpolationContext
pure . validationToEither . flip runReader ctx . runInterpolator interpolator $ x
-- |Pure transformation for the identity interpolation. FIXME this is too clunky for overlapping
-- instances, define an auxiliary class (or type) for IdentityInterpolation.
instance {-# OVERLAPPABLE #-} Default Interpolator a a where
def = Interpolator $ pure . Success
-- |When we can parse template values, we can interpolate from the template.
instance FromTemplateValue a => Default Interpolator (Uninterpolated a) a where
def = runTemplate
instance Default Interpolator a b => Default Interpolator (Map k a) (Map k b) where
def = Interpolator $ fmap sequenceA . traverse (runInterpolator def)
instance (Default Interpolator a b, Ord a, Ord b) => Default Interpolator (Set a) (Set b) where
def = Interpolator $ fmap (fmap setFromList . sequenceA) . traverse (runInterpolator def) . setToList
instance Default Interpolator a b => Default Interpolator [a] [b] where
def = Interpolator $ fmap sequenceA . traverse (runInterpolator def)
instance Default Interpolator a b => Default Interpolator (Maybe a) (Maybe b) where
def = Interpolator $ fmap sequenceA . traverse (runInterpolator def)
instance FromTemplateValue a => FromJSON (Template a) where
parseJSON jv = flip (withText "Template") jv $ \ t ->
case T.splitOn ":" t of
"_env":k:vs -> do
let v = T.intercalate ":" vs
defaultV = parseTemplateValue =<< if T.null v then Nothing else Just (TemplateValue v)
case T.null k of
False -> pure $ Template (TemplateKey k) defaultV
True -> fail $ "Not a template: " <> T.unpack t
_ -> fail $ "Not a template: " <> T.unpack t
instance ToTemplateValue a => ToJSON (Template a) where
toJSON (Template (TemplateKey k) (Just x)) = String $ "_env:" <> k <> ":" <> unTemplateValue (toTemplateValue x)
toJSON (Template (TemplateKey k) Nothing) = String $ "_env:" <> k
instance (FromTemplateValue a, FromJSON a) => FromJSON (Uninterpolated a) where
parseJSON jv = (Templated <$> parseJSON jv) <|> (Literal <$> parseJSON jv)
instance (ToTemplateValue a, ToJSON a) => ToJSON (Uninterpolated a) where
toJSON = \ case
Templated x -> toJSON x
Literal x -> toJSON x
instance Arbitrary TemplateKey where
arbitrary = TemplateKey <$> varNameAllowed
where varNameAllowed = fmap T.pack . listOf1 $ arbitrary `suchThat` (\c -> isAlphaNum c || c == '_')
instance Arbitrary1 Uninterpolated where
liftArbitrary g = oneof
[ Literal <$> g
, Templated <$> (Template <$> arbitrary <*> liftArbitrary g)
]
instance {-# OVERLAPPABLE #-} Arbitrary a => Arbitrary (Uninterpolated a) where
arbitrary = arbitrary1
instance {-# OVERLAPPING #-} Arbitrary (Uninterpolated T.Text) where
arbitrary = liftArbitrary noEnv
where noEnv = fmap T.pack $ arbitrary `suchThat` (\ s -> not ("_env:" `isPrefixOf` s) && not (null s))