Basic Path type with parsers and test suite
This commit is contained in:
parent
accc5c3794
commit
1e5b6675c2
2
.ghci
Executable file
2
.ghci
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
:set -package HUnit -package hspec
|
||||||
|
:set -package template-haskell
|
19
paths.cabal
19
paths.cabal
@ -14,11 +14,18 @@ cabal-version: >=1.8
|
|||||||
library
|
library
|
||||||
hs-source-dirs: src/
|
hs-source-dirs: src/
|
||||||
ghc-options: -Wall -O2
|
ghc-options: -Wall -O2
|
||||||
exposed-modules:
|
exposed-modules: Path, Path.Internal
|
||||||
build-depends: base >= 4 && <5
|
build-depends: base >= 4 && <5
|
||||||
|
, exceptions
|
||||||
|
, filepath
|
||||||
|
, template-haskell
|
||||||
|
|
||||||
executable paths
|
test-suite test
|
||||||
hs-source-dirs: src/
|
type: exitcode-stdio-1.0
|
||||||
ghc-options: -Wall -O2
|
main-is: Main.hs
|
||||||
main-is: Main.hs
|
hs-source-dirs: test
|
||||||
build-depends: base >= 4 && < 5
|
build-depends: HUnit
|
||||||
|
, base
|
||||||
|
, hspec
|
||||||
|
, mtl
|
||||||
|
, paths
|
||||||
|
193
src/Path.hs
193
src/Path.hs
@ -1,3 +1,192 @@
|
|||||||
-- |
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
{-# LANGUAGE DeriveDataTypeable #-}
|
||||||
|
{-# LANGUAGE EmptyDataDecls #-}
|
||||||
|
|
||||||
module Path where
|
-- | A normalizing well-typed path type.
|
||||||
|
|
||||||
|
module Path
|
||||||
|
(-- * Types
|
||||||
|
Path
|
||||||
|
,Abs
|
||||||
|
,Rel
|
||||||
|
,File
|
||||||
|
,Dir
|
||||||
|
-- * Parsing
|
||||||
|
,parseAbsDir
|
||||||
|
,parseRelDir
|
||||||
|
,parseAbsFile
|
||||||
|
,parseRelFile
|
||||||
|
,PathParseException
|
||||||
|
-- * Constructors
|
||||||
|
,mkAbsDir
|
||||||
|
,mkRelDir
|
||||||
|
,mkAbsFile
|
||||||
|
,mkRelFile
|
||||||
|
-- * Conversion
|
||||||
|
,toFilePath
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Control.Exception (Exception)
|
||||||
|
import Control.Monad.Catch (MonadThrow(..))
|
||||||
|
import Data.Data
|
||||||
|
import Data.List
|
||||||
|
import Language.Haskell.TH
|
||||||
|
import Path.Internal
|
||||||
|
import qualified System.FilePath as FilePath
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Types
|
||||||
|
|
||||||
|
-- | An absolute path.
|
||||||
|
data Abs
|
||||||
|
|
||||||
|
-- | A relative path; one without a root.
|
||||||
|
data Rel
|
||||||
|
|
||||||
|
-- | A file path.
|
||||||
|
data File
|
||||||
|
|
||||||
|
-- | A directory path.
|
||||||
|
data Dir
|
||||||
|
|
||||||
|
-- | Exception when parsing a location.
|
||||||
|
data PathParseException
|
||||||
|
= InvalidAbsDir FilePath
|
||||||
|
| InvalidRelDir FilePath
|
||||||
|
| InvalidAbsFile FilePath
|
||||||
|
| InvalidRelFile FilePath
|
||||||
|
deriving (Show,Typeable)
|
||||||
|
instance Exception PathParseException
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Parsers
|
||||||
|
|
||||||
|
-- | Get a location for an absolute directory. Produces a normalized
|
||||||
|
-- path which always ends in a path separator.
|
||||||
|
--
|
||||||
|
-- Throws: 'PathParseException'
|
||||||
|
--
|
||||||
|
parseAbsDir :: MonadThrow m
|
||||||
|
=> FilePath -> m (Path Abs Dir)
|
||||||
|
parseAbsDir filepath =
|
||||||
|
if FilePath.isAbsolute filepath &&
|
||||||
|
not (null (normalizeDir filepath)) &&
|
||||||
|
not (isPrefixOf "~/" filepath)
|
||||||
|
then return (Path (normalizeDir filepath))
|
||||||
|
else throwM (InvalidAbsDir filepath)
|
||||||
|
|
||||||
|
-- | Get a location for a relative directory. Produces a normalized
|
||||||
|
-- path which always ends in a path separator.
|
||||||
|
--
|
||||||
|
-- Throws: 'PathParseException'
|
||||||
|
--
|
||||||
|
parseRelDir :: MonadThrow m
|
||||||
|
=> FilePath -> m (Path Rel Dir)
|
||||||
|
parseRelDir filepath =
|
||||||
|
if not (FilePath.isAbsolute filepath) &&
|
||||||
|
not (null filepath) &&
|
||||||
|
not (isPrefixOf "~/" filepath) &&
|
||||||
|
not (null (normalizeDir filepath))
|
||||||
|
then return (Path (normalizeDir filepath))
|
||||||
|
else throwM (InvalidRelDir filepath)
|
||||||
|
|
||||||
|
-- | Get a location for an absolute file. Produces a normalized
|
||||||
|
-- path which always ends in a path separator.
|
||||||
|
--
|
||||||
|
-- Throws: 'PathParseException'
|
||||||
|
--
|
||||||
|
parseAbsFile :: MonadThrow m
|
||||||
|
=> FilePath -> m (Path Abs File)
|
||||||
|
parseAbsFile filepath =
|
||||||
|
if FilePath.isAbsolute filepath &&
|
||||||
|
not (FilePath.hasTrailingPathSeparator filepath) &&
|
||||||
|
not (isPrefixOf "~/" filepath) &&
|
||||||
|
not (null (normalizeFile filepath))
|
||||||
|
then return (Path (normalizeFile filepath))
|
||||||
|
else throwM (InvalidAbsFile filepath)
|
||||||
|
|
||||||
|
-- | Get a location for a relative file. Produces a normalized
|
||||||
|
-- path which always ends in a path separator.
|
||||||
|
--
|
||||||
|
-- Throws: 'PathParseException'
|
||||||
|
--
|
||||||
|
parseRelFile :: MonadThrow m
|
||||||
|
=> FilePath -> m (Path Rel File)
|
||||||
|
parseRelFile filepath =
|
||||||
|
if not (FilePath.isAbsolute filepath || FilePath.hasTrailingPathSeparator filepath) &&
|
||||||
|
not (null filepath) &&
|
||||||
|
not (isPrefixOf "~/" filepath) &&
|
||||||
|
not (null (normalizeFile filepath))
|
||||||
|
then return (Path (normalizeFile filepath))
|
||||||
|
else throwM (InvalidRelFile filepath)
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Constructors
|
||||||
|
|
||||||
|
-- | Make a 'Path Abs Dir'.
|
||||||
|
--
|
||||||
|
-- Remember: due to the nature of absolute paths this (e.g. @\/home\/foo@)
|
||||||
|
-- may compile on your platform, but it may not compile on another
|
||||||
|
-- platform (Windows).
|
||||||
|
mkAbsDir :: FilePath -> Q Exp
|
||||||
|
mkAbsDir s =
|
||||||
|
case parseAbsDir s of
|
||||||
|
Left err -> error (show err)
|
||||||
|
Right (Path str) ->
|
||||||
|
[|Path $(return (LitE (StringL str))) :: Path Abs Dir|]
|
||||||
|
|
||||||
|
-- | Make a 'Path Rel Dir'.
|
||||||
|
mkRelDir :: FilePath -> Q Exp
|
||||||
|
mkRelDir s =
|
||||||
|
case parseRelDir s of
|
||||||
|
Left err -> error (show err)
|
||||||
|
Right (Path str) ->
|
||||||
|
[|Path $(return (LitE (StringL str))) :: Path Rel Dir|]
|
||||||
|
|
||||||
|
-- | Make a 'Path Abs File'.
|
||||||
|
--
|
||||||
|
-- Remember: due to the nature of absolute paths this (e.g. @\/home\/foo@)
|
||||||
|
-- may compile on your platform, but it may not compile on another
|
||||||
|
-- platform (Windows).
|
||||||
|
mkAbsFile :: FilePath -> Q Exp
|
||||||
|
mkAbsFile s =
|
||||||
|
case parseAbsFile s of
|
||||||
|
Left err -> error (show err)
|
||||||
|
Right (Path str) ->
|
||||||
|
[|Path $(return (LitE (StringL str))) :: Path Abs File|]
|
||||||
|
|
||||||
|
-- | Make a 'Path Rel File'.
|
||||||
|
mkRelFile :: FilePath -> Q Exp
|
||||||
|
mkRelFile s =
|
||||||
|
case parseRelFile s of
|
||||||
|
Left err -> error (show err)
|
||||||
|
Right (Path str) ->
|
||||||
|
[|Path $(return (LitE (StringL str))) :: Path Rel File|]
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Conversion
|
||||||
|
|
||||||
|
-- | Convert to a 'FilePath' type.
|
||||||
|
toFilePath :: Path b t -> FilePath
|
||||||
|
toFilePath (Path l) = l
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Internal functions
|
||||||
|
|
||||||
|
-- | Internal use for normalizing a directory.
|
||||||
|
normalizeDir :: FilePath -> FilePath
|
||||||
|
normalizeDir =
|
||||||
|
clean . FilePath.addTrailingPathSeparator . FilePath.normalise
|
||||||
|
where clean "./" = ""
|
||||||
|
clean ('/':'/':xs) = clean ('/':xs)
|
||||||
|
clean x = x
|
||||||
|
|
||||||
|
-- | Internal use for normalizing a fileectory.
|
||||||
|
normalizeFile :: FilePath -> FilePath
|
||||||
|
normalizeFile =
|
||||||
|
clean . FilePath.normalise
|
||||||
|
where clean "./" = ""
|
||||||
|
clean ('/':'/':xs) = clean ('/':xs)
|
||||||
|
clean x = x
|
||||||
|
19
src/Path/Internal.hs
Normal file
19
src/Path/Internal.hs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
{-# LANGUAGE DeriveDataTypeable #-}
|
||||||
|
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||||
|
|
||||||
|
-- | Internal types and functions.
|
||||||
|
|
||||||
|
module Path.Internal
|
||||||
|
(Path(..))
|
||||||
|
where
|
||||||
|
|
||||||
|
import Data.Data
|
||||||
|
import GHC.Generics
|
||||||
|
|
||||||
|
-- | Path of some base and type.
|
||||||
|
newtype Path b t = Path FilePath
|
||||||
|
deriving (Eq,Ord,Typeable,Data,Generic)
|
||||||
|
|
||||||
|
instance Show (Path b t) where
|
||||||
|
show (Path x) = show x
|
110
test/Main.hs
Normal file
110
test/Main.hs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
-- | Test suite.
|
||||||
|
|
||||||
|
module Main where
|
||||||
|
|
||||||
|
import Path
|
||||||
|
import Path.Internal
|
||||||
|
|
||||||
|
import Data.Monoid
|
||||||
|
import Test.Hspec
|
||||||
|
|
||||||
|
-- | Test suite entry point, returns exit failure if any test fails.
|
||||||
|
main :: IO ()
|
||||||
|
main = hspec spec
|
||||||
|
|
||||||
|
-- | Test suite.
|
||||||
|
spec :: Spec
|
||||||
|
spec = do
|
||||||
|
describe "Parsing: Path Abs Dir" parseAbsDirSpec
|
||||||
|
describe "Parsing: Path Rel Dir" parseRelDirSpec
|
||||||
|
describe "Parsing: Path Abs File" parseAbsFileSpec
|
||||||
|
describe "Parsing: Path Rel File" parseRelFileSpec
|
||||||
|
|
||||||
|
-- | Tests for the tokenizer.
|
||||||
|
parseAbsDirSpec :: Spec
|
||||||
|
parseAbsDirSpec =
|
||||||
|
do failing ""
|
||||||
|
failing "./"
|
||||||
|
failing "~/"
|
||||||
|
failing "foo.txt"
|
||||||
|
succeeding "/" (Path "/")
|
||||||
|
succeeding "//" (Path "/")
|
||||||
|
succeeding "///foo//bar//mu/" (Path "/foo/bar/mu/")
|
||||||
|
succeeding "///foo//bar////mu" (Path "/foo/bar/mu/")
|
||||||
|
succeeding "///foo//bar/.//mu" (Path "/foo/bar/mu/")
|
||||||
|
where failing x = parserTest parseAbsDir x Nothing
|
||||||
|
succeeding x with = parserTest parseAbsDir x (Just with)
|
||||||
|
|
||||||
|
-- | Tests for the tokenizer.
|
||||||
|
parseRelDirSpec :: Spec
|
||||||
|
parseRelDirSpec =
|
||||||
|
do failing ""
|
||||||
|
failing "/"
|
||||||
|
failing "//"
|
||||||
|
failing "~/"
|
||||||
|
failing "/"
|
||||||
|
failing "./"
|
||||||
|
failing "//"
|
||||||
|
failing "///foo//bar//mu/"
|
||||||
|
failing "///foo//bar////mu"
|
||||||
|
failing "///foo//bar/.//mu"
|
||||||
|
succeeding "foo.bak" (Path "foo.bak/")
|
||||||
|
succeeding "./foo" (Path "foo/")
|
||||||
|
succeeding "foo//bar//mu//" (Path "foo/bar/mu/")
|
||||||
|
succeeding "foo//bar////mu" (Path "foo/bar/mu/")
|
||||||
|
succeeding "foo//bar/.//mu" (Path "foo/bar/mu/")
|
||||||
|
where failing x = parserTest parseRelDir x Nothing
|
||||||
|
succeeding x with = parserTest parseRelDir x (Just with)
|
||||||
|
|
||||||
|
-- | Tests for the tokenizer.
|
||||||
|
parseAbsFileSpec :: Spec
|
||||||
|
parseAbsFileSpec =
|
||||||
|
do failing ""
|
||||||
|
failing "./"
|
||||||
|
failing "~/"
|
||||||
|
failing "./foo.txt"
|
||||||
|
failing "/"
|
||||||
|
failing "//"
|
||||||
|
failing "///foo//bar//mu/"
|
||||||
|
succeeding "/foo.txt" (Path "/foo.txt")
|
||||||
|
succeeding "///foo//bar////mu.txt" (Path "/foo/bar/mu.txt")
|
||||||
|
succeeding "///foo//bar/.//mu.txt" (Path "/foo/bar/mu.txt")
|
||||||
|
where failing x = parserTest parseAbsFile x Nothing
|
||||||
|
succeeding x with = parserTest parseAbsFile x (Just with)
|
||||||
|
|
||||||
|
-- | Tests for the tokenizer.
|
||||||
|
parseRelFileSpec :: Spec
|
||||||
|
parseRelFileSpec =
|
||||||
|
do failing ""
|
||||||
|
failing "/"
|
||||||
|
failing "//"
|
||||||
|
failing "~/"
|
||||||
|
failing "/"
|
||||||
|
failing "./"
|
||||||
|
failing "//"
|
||||||
|
failing "///foo//bar//mu/"
|
||||||
|
failing "///foo//bar////mu"
|
||||||
|
failing "///foo//bar/.//mu"
|
||||||
|
succeeding "foo.txt" (Path "foo.txt")
|
||||||
|
succeeding "./foo.txt" (Path "foo.txt")
|
||||||
|
succeeding "foo//bar//mu.txt" (Path "foo/bar/mu.txt")
|
||||||
|
succeeding "foo//bar////mu.txt" (Path "foo/bar/mu.txt")
|
||||||
|
succeeding "foo//bar/.//mu.txt" (Path "foo/bar/mu.txt")
|
||||||
|
where failing x = parserTest parseRelFile x Nothing
|
||||||
|
succeeding x with = parserTest parseRelFile x (Just with)
|
||||||
|
|
||||||
|
-- | Parser test.
|
||||||
|
parserTest :: (Show a1,Show a,Eq a1)
|
||||||
|
=> (a -> Maybe a1) -> a -> Maybe a1 -> SpecWith ()
|
||||||
|
parserTest parser input expected =
|
||||||
|
it ((case expected of
|
||||||
|
Nothing -> "Failing: "
|
||||||
|
Just{} -> "Succeeding: ") <>
|
||||||
|
"Parsing " <>
|
||||||
|
show input <>
|
||||||
|
" " <>
|
||||||
|
case expected of
|
||||||
|
Nothing -> "should fail."
|
||||||
|
Just x -> "should succeed with: " <> show x)
|
||||||
|
(actual == expected)
|
||||||
|
where actual = parser input
|
Loading…
Reference in New Issue
Block a user