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