packages feed

interpolator-0.1: test/Data/InterpolationSpec.hs

module Data.InterpolationSpec where

import Prelude

import Control.Monad.Reader (runReader)
import Data.Aeson (FromJSON, ToJSON, decode, eitherDecodeStrict', encode)
import Data.Containers (mapFromList, singletonMap)
import Data.Either (isLeft)
import Data.Either.Validation (validationToEither)
import Data.Map (Map)
import Data.Profunctor.Product.Default (Default, def)
import Data.Profunctor.Product.TH (makeAdaptorAndInstance)
import qualified Data.Text as T
import GHC.Stack (HasCallStack)
import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy)
import Test.QuickCheck (Gen, arbitrary, forAll, listOf, resize, suchThat, (===))

-- the module being tested
import Data.Interpolation

upToNCharacters :: Int -> Gen T.Text
upToNCharacters n = fmap T.pack . resize n . listOf . suchThat arbitrary $ \ x -> x /= '\0'

codecLaws :: (ToJSON a, FromJSON a, Eq a, Show a) => Gen a -> Spec
codecLaws gen = do
  it "decode . encode == Just" $
    forAll gen $ \x -> (decode . encode) x === Just x
  it "encode . decode == Just (for any JSON resulting from encoding)" $
    forAll gen $ \(y :: b) ->
      let json = encode y
      in (encode <$> (decode json :: Maybe b)) === Just json

newtype Bar = Bar { unBar :: Int }
  deriving (Eq, Ord, Show, FromTemplateValue, ToTemplateValue)

data Foo' a b c d e = Foo
  { fooA :: a
  , fooB :: b
  , fooC :: c
  , fooD :: d
  , fooE :: e
  } deriving (Eq, Ord, Show)
type UninterpolatedFoo = Foo' (Uninterpolated T.Text) (Uninterpolated Int) (Uninterpolated Bool) [Uninterpolated Bar] (Map Char (Uninterpolated T.Text))
type Foo = Foo' T.Text Int Bool [Bar] (Map Char T.Text)

makeAdaptorAndInstance "pFoo" ''Foo'

fooInterpolator :: Interpolator UninterpolatedFoo Foo
fooInterpolator = def

key0, key1, key2, key3, key4, key5 :: TemplateKey
key0 = TemplateKey "key0"
key1 = TemplateKey "key1"
key2 = TemplateKey "key2"
key3 = TemplateKey "key3"
key4 = TemplateKey "key4"
key5 = TemplateKey "key5"

emptyContext, defaultContext :: InterpolationContext
emptyContext = InterpolationContext mempty
defaultContext = InterpolationContext . mapFromList $
  [ (key1, (TemplateValue "asdf"))
  , (key2, (TemplateValue "1"))
  , (key3, (TemplateValue "true"))
  , (key4, (TemplateValue "2"))
  , (key5, (TemplateValue "fdsa"))
  ]

singletonContext :: ToTemplateValue a => TemplateKey -> a -> InterpolationContext
singletonContext k v = InterpolationContext . singletonMap k . toTemplateValue $ v

run :: InterpolationContext -> Interpolator a b -> a -> Either [InterpolationFailure] b
run ctx x = validationToEither . flip runReader ctx . runInterpolator x

identityLaw :: (HasCallStack, Eq a, Show a) => Interpolator (Uninterpolated a) a -> Gen a -> Spec
identityLaw interp gen = describe "Identity Law" $ do
  it "always succeeds on a literal" $
    forAll gen $ \ x ->
      run emptyContext interp (Literal x) `shouldBe` Right x

roundtripLaw :: (HasCallStack, Eq a, Show a, ToTemplateValue a) => Interpolator (Uninterpolated a) a -> Gen a -> Spec
roundtripLaw interp gen = describe "Round Trip Law" $ do
  it "can always interpolate a rendered value" $
    forAll ((,) <$> gen <*> arbitrary) $ \ (x, k) ->
      run (singletonContext k x) interp (Templated (Template k Nothing)) `shouldBe` Right x

defaultLaws :: (HasCallStack, Eq a, Show a) => Interpolator (Uninterpolated a) a -> Gen a -> Spec
defaultLaws interp gen = describe "Default Laws" $ do
  it "defaults if the value is not found" $
    forAll ((,) <$> gen <*> arbitrary) $ \ (x, k) ->
      run emptyContext interp (Templated (Template k (Just x))) `shouldBe` Right x
  it "fails if the value is not found and no default" $
    forAll arbitrary $ \ k ->
      run emptyContext interp (Templated (Template k Nothing)) `shouldBe` Left [InterpolationFailureKeyNotFound k]

interpolationLaws ::
  ( HasCallStack, Eq a, Show a
  , Default Interpolator (Uninterpolated a) a, ToTemplateValue a )
  => Gen a -> Spec
interpolationLaws gen = do
  identityLaw def gen
  roundtripLaw def gen
  defaultLaws def gen

spec :: Spec
spec = describe "InterpolationSpec" $ do
  let isLeftInt :: Either String (Uninterpolated Int) -> Bool
      isLeftInt = isLeft

      noDefaultText :: TemplateKey -> Uninterpolated T.Text
      noDefaultText k = Templated (Template k Nothing)

  describe "Foo Unit Tests" $ do
    it "interpolates variables from context" $ do
      let template = Foo
            { fooA = Templated $ Template key1 Nothing
            , fooB = Templated $ Template key2 Nothing
            , fooC = Templated $ Template key3 Nothing
            , fooD = [Templated $ Template key4 Nothing]
            , fooE = singletonMap 'a' (Templated $ Template key5 Nothing)
            }
          expected = Foo "asdf" 1 True [Bar 2] (singletonMap 'a' "fdsa")
      run defaultContext fooInterpolator template `shouldBe` Right expected
    it "fails with multiple errors" $ do
      let template = Foo
            { fooA = Templated $ Template key0 Nothing
            , fooB = Templated $ Template key1 Nothing
            , fooC = Templated $ Template key3 Nothing
            , fooD = [Templated $ Template key4 Nothing]
            , fooE = singletonMap 'a' (Templated $ Template key5 Nothing)
            }
          expected =
            [ InterpolationFailureKeyNotFound key0
            , InterpolationFailureValueNotReadable key1 (TemplateValue "asdf")
            ]
      run defaultContext fooInterpolator template `shouldBe` Left expected

  describe "Uninterpolated JSON Unit Tests" $ do
    it "parses base case" $ do
      eitherDecodeStrict' "\"_env:key1:foo\"" `shouldBe` Right (Templated (Template key1 (Just ("foo" :: T.Text))))
      eitherDecodeStrict' "\"foo\"" `shouldBe` Right (Literal ("foo" :: T.Text))
      eitherDecodeStrict' "\"_env:key1:1\"" `shouldBe` Right (Templated (Template key1 (Just (1 :: Int))))
      eitherDecodeStrict' "1" `shouldBe` Right (Literal (1 :: Int))
      eitherDecodeStrict' "\"_env:key1:true\"" `shouldBe` Right (Templated (Template key1 (Just True)))
      eitherDecodeStrict' "true" `shouldBe` Right (Literal True)

    it "parses no default" $ do
      eitherDecodeStrict' "\"_env:key1\"" `shouldBe` Right (noDefaultText key1)
      eitherDecodeStrict' "\"_env:key1:\"" `shouldBe` Right (noDefaultText key1)

    it "fails with no key" $ do
      eitherDecodeStrict' "\"_env:\"" `shouldSatisfy` isLeftInt
      eitherDecodeStrict' "\"_env::\"" `shouldSatisfy` isLeftInt
      eitherDecodeStrict' "\"_env::1\"" `shouldSatisfy` isLeftInt

    it "succeeds with colons in default value" $ do
      eitherDecodeStrict' "\"_env:key1:foo:bar\"" `shouldBe` Right (Templated (Template key1 (Just ("foo:bar" :: T.Text))))

    it "succeeds even if you are a bad person and do something wrong sometimes" $ do
      eitherDecodeStrict' "\"_env:a space\"" `shouldBe` Right (noDefaultText $ TemplateKey "a space")

  describe "Text Primitive" $ do
    describe "JSON Laws" $ do
      codecLaws (arbitrary :: Gen (Uninterpolated T.Text))
    describe "Interpolation Laws" $ do
      interpolationLaws $ upToNCharacters 50

  describe "Int Primitive" $ do
    describe "JSON Laws" $ do
      codecLaws (arbitrary :: Gen (Uninterpolated Int))
    describe "Interpolation Laws" $ do
      interpolationLaws (arbitrary :: Gen Int)

  describe "Bool Primitive" $ do
    describe "JSON Laws" $ do
      codecLaws (arbitrary :: Gen (Uninterpolated Bool))
    describe "Interpolation Laws" $ do
      interpolationLaws (arbitrary :: Gen Bool)