packages feed

dojang-0.1.0: test/Dojang/Syntax/FilePathExpression/ParserSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module Dojang.Syntax.FilePathExpression.ParserSpec (spec) where

import Control.Monad (forM_)
import Prelude hiding (unlines)

import Data.Text (unlines)
import Test.Hspec (Spec, specify)
import Test.Hspec.Expectations.Pretty (shouldBe)
import Test.Hspec.Megaparsec
  ( eeof
  , elabel
  , err
  , etok
  , etoks
  , shouldFailWith
  , shouldParse
  , ueof
  , utok
  , utoks
  )
import Text.Megaparsec (eof, parse)

import Dojang.Syntax.FilePathExpression.Parser
  ( bareComponent
  , environmentVariable
  , errorBundlePretty
  , filePathExpression
  , filePathExpression'
  , parseFilePathExpression
  , root
  , substitution
  , substitutionConditional
  , substitutionDefault
  , substitutionInBraces
  , substitutionModifierAfterColon
  )
import Dojang.Types.FilePathExpression (FilePathExpression (..), (+/+))


spec :: Spec
spec = do
  specify "environmentVariable" $ do
    let environmentVariable' = environmentVariable <* eof
    parse environmentVariable' "" "FOO_BAR" `shouldParse` "FOO_BAR"
    parse environmentVariable' "" "foo_bar" `shouldParse` "foo_bar"
    parse environmentVariable "" "_BAR_123" `shouldParse` "_BAR_123"
    parse environmentVariable "" "123_FOO"
      `shouldFailWith` err 0 (utok '1' <> elabel "environment variable")
    parse environmentVariable "" "-FOO"
      `shouldFailWith` err 0 (utok '-' <> elabel "environment variable")
    parse environmentVariable' "" "FOO-BAR"
      `shouldFailWith` err 3 (utok '-' <> eeof)
    parse environmentVariable "" "甲_乙"
      `shouldFailWith` err 0 (utok '甲' <> elabel "environment variable")
    parse environmentVariable' "" "FOO_乙"
      `shouldFailWith` err 4 (utok '乙' <> eeof)
    parse environmentVariable "" ""
      `shouldFailWith` err 0 (ueof <> elabel "environment variable")

  specify "bareComponent" $ do
    let bareComponent' = bareComponent "" <* eof
    parse bareComponent' "" "foo" `shouldParse` BareComponent "foo"
    parse bareComponent' "" "123" `shouldParse` BareComponent "123"
    parse bareComponent' "" "甲" `shouldParse` BareComponent "甲"
    parse bareComponent' "" "123-foo-乙"
      `shouldParse` BareComponent "123-foo-乙"
    parse (bareComponent "") "" "/foo"
      `shouldFailWith` err
        0
        (utok '/' <> elabel "component")
    parse bareComponent' "" "foo/"
      `shouldFailWith` err
        3
        (utok '/' <> elabel "component" <> eeof)
    parse bareComponent' "" ""
      `shouldFailWith` err
        0
        (ueof <> elabel "component")

  specify "root" $ do
    let root' = root <* eof
    parse root' "" "/" `shouldParse` Root Nothing
    parse root' "" "C:\\" `shouldParse` Root (Just 'C')
    parse root' "" "C:/" `shouldParse` Root (Just 'C')
    parse root "" ""
      `shouldFailWith` err 0 (ueof <> etok '/' <> elabel "drive letter")
    parse root "" "C" `shouldFailWith` err 1 (ueof <> etok ':')

  specify "substitution" $ do
    let substitution' = substitution <* eof
    parse substitution' "" "$FOO" `shouldParse` Substitution "FOO"
    parse substitution' "" "$FOO_BAR" `shouldParse` Substitution "FOO_BAR"
    parse substitution "" ""
      `shouldFailWith` err 0 (ueof <> etok '$')
    parse substitution "" "FOO"
      `shouldFailWith` err 0 (utok 'F' <> etok '$')
    parse substitution' "" "$FOO-"
      `shouldFailWith` err 4 (utok '-' <> eeof)

  trueFalse $ \begin -> specify ("substitutionDefault " ++ show begin) $ do
    let substitutionDefault' env = substitutionDefault begin env <* eof
    parse (substitutionDefault' "FOO") "" "-bar"
      `shouldParse` SubstitutionWithDefault "FOO" (BareComponent "bar")
    parse (substitutionDefault' "FOO") "" "-bar${BAZ}qux"
      `shouldParse` SubstitutionWithDefault
        "FOO"
        ( Concatenation
            (Concatenation (BareComponent "bar") (Substitution "BAZ"))
            $ BareComponent "qux"
        )
    parse (substitutionDefault begin "FOO") "" ""
      `shouldFailWith` err 0 (ueof <> etok '-')
    parse (substitutionDefault begin "FOO") "" "+bar"
      `shouldFailWith` err 0 (utok '+' <> etok '-')
    parse (substitutionDefault' "FOO") "" "-$BAR}"
      `shouldFailWith` err
        5
        ( utok '}'
            <> etoks "${"
            <> etok '$'
            <> elabel "component"
            <> eeof
        )
    parse (substitutionDefault' "FOO") "" "-${BAR}}"
      `shouldFailWith` err
        7
        ( utok '}'
            <> etoks "${"
            <> etok '$'
            <> elabel "component"
            <> eeof
        )

  trueFalse $ \begin -> specify ("substitutionConditional " ++ show begin) $ do
    let substitutionConditional' env = substitutionConditional begin env <* eof
    parse (substitutionConditional' "FOO") "" "+bar"
      `shouldParse` ConditionalSubstitution "FOO" (BareComponent "bar")
    parse (substitutionConditional' "FOO") "" "+bar${BAZ}qux"
      `shouldParse` ConditionalSubstitution
        "FOO"
        ( Concatenation
            (Concatenation (BareComponent "bar") (Substitution "BAZ"))
            $ BareComponent "qux"
        )
    parse (substitutionConditional begin "FOO") "" ""
      `shouldFailWith` err 0 (ueof <> etok '+')
    parse (substitutionConditional begin "FOO") "" "-bar"
      `shouldFailWith` err 0 (utok '-' <> etok '+')
    parse (substitutionConditional' "FOO") "" "+$BAR}"
      `shouldFailWith` err
        5
        ( utok '}'
            <> etoks "${"
            <> etok '$'
            <> elabel "component"
            <> eeof
        )
    parse (substitutionConditional' "FOO") "" "+${BAR}}"
      `shouldFailWith` err
        7
        ( utok '}'
            <> etoks "${"
            <> etok '$'
            <> elabel "component"
            <> eeof
        )

  trueFalse $ \begin -> specify
    ("substitutionModifierAfterColon" ++ show begin)
    $ do
      let substitutionModifierAfterColon' env =
            substitutionModifierAfterColon begin env <* eof
      parse (substitutionModifierAfterColon' "FOO") "" "-bar"
        `shouldParse` SubstitutionWithDefault "FOO" (BareComponent "bar")
      parse (substitutionModifierAfterColon' "FOO") "" "+bar"
        `shouldParse` ConditionalSubstitution "FOO" (BareComponent "bar")
      parse (substitutionModifierAfterColon begin "FOO") "" "!bar"
        `shouldFailWith` err 0 (utok '!' <> etok '+' <> etok '-')

  trueFalse $ \begin -> specify ("substitutionInBraces" ++ show begin) $ do
    let substitutionInBraces' = substitutionInBraces begin <* eof
    parse substitutionInBraces' "" "${FOO}"
      `shouldParse` Substitution "FOO"
    parse substitutionInBraces' "" "${FOO:-bar}"
      `shouldParse` SubstitutionWithDefault "FOO" (BareComponent "bar")
    parse substitutionInBraces' "" "${FOO:+bar}"
      `shouldParse` ConditionalSubstitution "FOO" (BareComponent "bar")
    parse (substitutionInBraces begin) "" ""
      `shouldFailWith` err 0 (ueof <> etoks "${")
    parse (substitutionInBraces begin) "" "${FOO-bar}"
      `shouldFailWith` err 5 (utok '-' <> etok ':' <> etok '}')

  specify "filePathExpression' False" $ do
    let filePathExpr' disallowed = filePathExpression' False disallowed <* eof
    parse (filePathExpr' []) "" "foo}"
      `shouldParse` BareComponent "foo}"
    parse (filePathExpr' "}") "" "foo}"
      `shouldFailWith` err
        3
        ( utok '}'
            <> etoks "${"
            <> etok '$'
            <> elabel "component"
            <> eeof
        )
    parse (filePathExpr' []) "" "/foo"
      `shouldFailWith` err
        0
        (utoks "/f" <> etoks "${" <> etok '$' <> elabel "component")
    parse (filePathExpr' []) "" "C:\\foo"
      `shouldParse` PathSeparator
        (BareComponent "C:")
        (BareComponent "foo")
    parse (filePathExpr' []) "" "D:/bar"
      `shouldParse` PathSeparator
        (BareComponent "D:")
        (BareComponent "bar")

  specify "filePathExpression' True" $ do
    let filePathExpr' = filePathExpression' True [] <* eof
    parse filePathExpr' "" "/foo"
      `shouldParse` PathSeparator (Root Nothing) (BareComponent "foo")
    parse filePathExpr' "" "C:\\foo"
      `shouldParse` PathSeparator (Root $ Just 'C') (BareComponent "foo")
    parse filePathExpr' "" "D:/bar"
      `shouldParse` PathSeparator (Root $ Just 'D') (BareComponent "bar")

  specify "filePathExpression" $ do
    let filePathExpr = filePathExpression <* eof
    parse filePathExpr "" "/" `shouldParse` Root (Nothing)
    parse filePathExpr "" "A" `shouldParse` BareComponent "A"
    parse filePathExpr "" "A/" `shouldParse` BareComponent "A"
    parse filePathExpr "" "A:\\" `shouldParse` Root (Just 'A')
    parse filePathExpr "" "A:/" `shouldParse` Root (Just 'A')
    parse filePathExpr "" "foo" `shouldParse` BareComponent "foo"
    parse filePathExpr "" "foo/" `shouldParse` BareComponent "foo"
    parse filePathExpr "" "foo/bar"
      `shouldParse` PathSeparator (BareComponent "foo") (BareComponent "bar")
    parse filePathExpr "" "foo/bar/"
      `shouldParse` PathSeparator (BareComponent "foo") (BareComponent "bar")
    parse filePathExpr "" "foo/bar/baz"
      `shouldParse` PathSeparator
        (PathSeparator (BareComponent "foo") (BareComponent "bar"))
        (BareComponent "baz")
    parse filePathExpr "" "foo${BAR}baz/qux"
      `shouldParse` PathSeparator
        ( Concatenation
            (Concatenation (BareComponent "foo") (Substitution "BAR"))
            $ BareComponent "baz"
        )
        (BareComponent "qux")
    parse filePathExpr "" "foo/bar${BAZ}qux"
      `shouldParse` PathSeparator
        (BareComponent "foo")
        ( Concatenation
            (Concatenation (BareComponent "bar") (Substitution "BAZ"))
            $ BareComponent "qux"
        )
    parse filePathExpr "" "foo/${BAR:-bar}/${BAZ:+baz}"
      `shouldParse` PathSeparator
        ( PathSeparator
            (BareComponent "foo")
            (SubstitutionWithDefault "BAR" (BareComponent "bar"))
        )
        (ConditionalSubstitution "BAZ" (BareComponent "baz"))
    parse filePathExpr "" "/$FOO/bar"
      `shouldParse` PathSeparator
        (PathSeparator (Root Nothing) (Substitution "FOO"))
        (BareComponent "bar")
    parse filePathExpr "" "C:\\D:\\bar"
      `shouldParse` PathSeparator
        (PathSeparator (Root $ Just 'C') (BareComponent "D:"))
        (BareComponent "bar")
    parse filePathExpr "" "${FOO:-C:\\bar}"
      `shouldParse` SubstitutionWithDefault
        "FOO"
        (PathSeparator (Root $ Just 'C') (BareComponent "bar"))

  specify "parseFilePathExpression" $ do
    parseFilePathExpression "" "foo"
      `shouldBe` Right (BareComponent "foo")
    parseFilePathExpression "" "/foo/$BAR"
      `shouldBe` Right (Root Nothing +/+ "foo" +/+ Substitution "BAR")
    parseFilePathExpression "" "C:\\foo\\$BAR"
      `shouldBe` Right (Root (Just 'C') +/+ "foo" +/+ Substitution "BAR")

  specify "errorBundlePretty" $ do
    let Left errorBundle = parseFilePathExpression "" "/foo/${BAR/baz"
    -- FIXME: The error message is not very helpful in this case.
    errorBundlePretty errorBundle
      `shouldBe` unlines
        [ "1:6:"
        , "  |"
        , "1 | /foo/${BAR/baz"
        , "  |      ^"
        , "unexpected '$'"
        , "expecting end of input"
        ]


trueFalse :: (Monad m) => (Bool -> m a) -> m ()
trueFalse = forM_ [False, True]