Skip to content

Commit

Permalink
Do not use Validation for user-facing API
Browse files Browse the repository at this point in the history
  • Loading branch information
supki committed May 27, 2014
1 parent a46d756 commit daa0042
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 26 deletions.
2 changes: 1 addition & 1 deletion Guardfile
@@ -1,7 +1,7 @@
# A sample Guardfile
# More info at https://github.com/guard/guard#readme

guard :haskell, ghci_options: ["-ignore-dot-ghci", "-Wall"], all_on_start: true do
guard :haskell, ghci_options: ["-ignore-dot-ghci", "-DTEST"], all_on_start: true do
watch(%r{test/.+Spec\.l?hs$})
watch(%r{src/.+\.l?hs$})
end
2 changes: 2 additions & 0 deletions directory-layout.cabal
Expand Up @@ -97,3 +97,5 @@ test-suite spec
System.Directory.LayoutSpec
System.Directory.Layout.InternalSpec
System.Directory.Layout.InterpreterSpec
cpp-options:
-DTEST
58 changes: 35 additions & 23 deletions src/System/Directory/Layout/Interpreter.hs
@@ -1,23 +1,27 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE ViewPatterns #-}
-- | A bunch of 'Layout' description interpreters
module System.Directory.Layout.Interpreter
( pretty
, examples
, Validation(..)
, fromErrors
, fit
, FitError(..)
, FitContentsError(..)
, make
, remake
, MakeError(..)
#ifdef TEST
, (\/)(..)
#endif
) where

import Control.Applicative
Expand Down Expand Up @@ -105,7 +109,7 @@ examplesC Nothing = "regular"

validate
:: Exception e
=> (forall a. FilePath -> F a -> IO ()) -> FilePath -> Layout b -> IO (Validation (NonEmpty e) ())
=> (forall a. FilePath -> F a -> IO ()) -> FilePath -> Layout b -> IO (NonEmpty e \/ ())
validate g p = getCompose . go p . unL where
go root (Free f@(F _ _ _ m)) =
sequenceA_ [Compose (validateF root f), go root m]
Expand All @@ -118,12 +122,12 @@ validate g p = getCompose . go p . unL where

validateF root = validateIO . g root

validateIO :: Exception e => IO a -> IO (Validation (NonEmpty e) a)
validateIO :: Exception e => IO a -> IO (NonEmpty e \/ a)
validateIO io = first pure . fromEither <$> try io

-- | Check the real directory layout fits the description
fit :: FilePath -> Layout a -> IO (Validation (NonEmpty FitError) ())
fit = validate fitIO
fit :: FilePath -> Layout a -> IO (Either (NonEmpty FitError) ())
fit p = fmap toEither . validate fitIO p

fitIO :: FilePath -> F a -> IO ()
fitIO root = go where
Expand Down Expand Up @@ -258,13 +262,14 @@ instance Exception FitError where
| otherwise = cast e

-- | Make the real directory layout from the description
make :: FilePath -> Layout a -> IO (Validation (NonEmpty MakeError) ())
make = validate makeIO
make :: FilePath -> Layout a -> IO (Either (NonEmpty MakeError) ())
make p = fmap toEither . validate makeIO p

-- | Make the real directory layout from the description removing any previous contents
remake :: FilePath -> Layout a -> IO (Validation (NonEmpty MakeError) ())
remake p l = getCompose $
Compose (validateIO (removeDirectoryRecursive p *> createDirectory p)) *> Compose (make p l)
remake :: FilePath -> Layout a -> IO (Either (NonEmpty MakeError) ())
remake p l = fmap toEither . getCompose $
Compose (validateIO (removeDirectoryRecursive p *> createDirectory p)) *>
Compose (fmap fromEither (make p l))

makeIO :: FilePath -> F a -> IO ()
makeIO root = go where
Expand Down Expand Up @@ -325,46 +330,53 @@ getGroupname :: Posix.GroupID -> IO String
getGroupname = fmap Posix.groupName . Posix.getGroupEntryForID

-- | This type is isomorphic to 'Either' but its 'Applicative' instance accumulates errors
data Validation e a = Error e | Result a
data e \/ a = Error e | Result a
deriving (Show, Eq, Ord, Functor, Foldable, Traversable, Typeable, Data, Generic)

instance Bifunctor Validation where
instance Bifunctor (\/) where
bimap f _ (Error a) = Error (f a)
bimap _ g (Result a) = Result (g a)

instance Bifoldable Validation where
instance Bifoldable (\/) where
bifoldMap f _ (Error a) = f a
bifoldMap _ g (Result a) = g a

instance Bitraversable Validation where
instance Bitraversable (\/) where
bitraverse f _ (Error a) = Error <$> f a
bitraverse _ g (Result a) = Result <$> g a

instance Semigroup e => Applicative (Validation e) where
instance Semigroup e => Applicative ((\/) e) where
pure = Result
Error f <*> Error x = Error (f <> x)
Error f <*> _ = Error f
_ <*> Error x = Error x
Result f <*> Result x = Result (f x)

fromEither :: Either e a -> Validation e a
fromEither :: Either e a -> e \/ a
fromEither = either Error Result

toEither :: e \/ a -> Either e a
toEither = validation Left Right

validation :: (e -> t) -> (a -> t) -> e \/ a -> t
validation f _ (Error e) = f e
validation _ g (Result a) = g a

-- | Construct 'Validation' value from the list of errors
--
-- >>> fromErrors []
-- Result ()
-- Right ()
--
-- >>> fromErrors Nothing
-- Result ()
-- Right ()
--
-- >>> fromErrors "hello"
-- Error ('h' :| "ello")
-- Left ('h' :| "ello")
--
-- >>> fromErrors (Just "hello")
-- Error ("hello" :| [])
fromErrors :: Foldable t => t e -> Validation (NonEmpty e) ()
-- Left ("hello" :| [])
fromErrors :: Foldable t => t e -> Either (NonEmpty e) ()
fromErrors = go . toList
where
go [] = Result ()
go (x : xs) = Error (x :| xs)
go [] = Right ()
go (x : xs) = Left (x :| xs)
6 changes: 4 additions & 2 deletions test/System/Directory/Layout/InterpreterSpec.hs
@@ -1,5 +1,6 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeOperators #-}
module System.Directory.Layout.InterpreterSpec
( spec
) where
Expand All @@ -9,6 +10,7 @@ import Control.Lens
import qualified Data.ByteString as ByteString
import Data.Foldable (traverse_)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NonEmpty
import System.Directory (createDirectoryIfMissing, removeDirectoryRecursive)
import System.FilePath ((</>))
import System.IO.Error (doesNotExistErrorType, permissionErrorType)
Expand All @@ -23,7 +25,7 @@ spec :: Spec
spec = do
describe "Validation" $
it "combines failures with the Semigroup instance's (<>)" $
traverse_ tonel ([1, 2, 3, 4] :: [Int]) `shouldBe` fromErrors [1,2,3,4]
traverse_ tonel ([1, 2, 3, 4] :: [Int]) `shouldBe` Error (NonEmpty.fromList [1,2,3,4])

describe "fit" $ do
it "tests regular file existence" $ do
Expand Down Expand Up @@ -448,7 +450,7 @@ spec = do
remake p (file "foo" & contents ?~ text "bar") `shouldReturn` fromErrors []
fit p' (file "quux" & contents ?~ "symlink source") `shouldReturn` fromErrors []

tonel :: a -> Validation (NonEmpty a) b
tonel :: a -> NonEmpty a \/ b
tonel = Error . pure

makefit :: Layout a -> IO ()
Expand Down

0 comments on commit daa0042

Please sign in to comment.