{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE PackageImports #-} {-# LANGUAGE PatternGuards #-} {-| As a bit of background, this package is built on a few other packages I wrote. yaml is a low-level wrapper around the C libyaml library, with an enumerator interface. data-object is a package defining a data type: @ data Object k v = Scalar v | Sequence [Object k v] | Mapping [(k, Object k v)] @ In other words, it can represent JSON data fully, and YAML data almost fully. In particular, it doesn't handle cyclical aliases, which I hope doesn't really occur too much in real life. Another package to deal with is failure: it basically replaces using an Either for error-handling into a typeclass. It has instances for Maybe, IO and lists by default. The last package is convertible-text, which is a fork of John Goerzen's convertible package. The difference is it supports both conversions that are guaranteed to succeed (Int -> String) and ones which may fail (String -> Int), and also supports various textual datatypes (String, lazy\/strict ByteString, lazy\/string Text). /YamlScalar and YamlObject/ We have a @type YamlObject = Object YamlScalar YamlScalar@, where a YamlScalar is just a ByteString value with a tag and a style. A \"style\" is how the data was represented in the underlying YAML file: single quoted, double quoted, etc. Then there is an IsYamlScalar typeclass, which provides fromYamlScalar and toYamlScalar conversion functions. There are instances for all the \"text-like\" datatypes: String, ByteString and Text. The built-in instances all assume a UTF-8 data encoding. And around this we have toYamlObject and fromYamlObject functions, which do exactly what they sound like. /Encoding and decoding/ There are two encoding files: encode and encodeFile. You can guess the different: the former produces a ByteString (strict) and the latter writes to a file. They both take an Object, whose keys and values must be an instance of IsYamlScalar. So, for example: @ encodeFile "myfile.yaml" $ Mapping [ ("Michael", Mapping [ ("age", Scalar "26") , ("color", Scalar "blue") ]) , ("Eliezer", Mapping [ ("age", Scalar "2") , ("color", Scalar "green") ]) ] @ decoding is only slightly more complicated, since the decoding can fail. In particular, the return type is an IO wrapped around a Failure. For example, you could use: @ maybeObject <- decodeFile "myfile.yaml" case maybeObject of Nothing -> putStrLn "Error parsing YAML file." Just object -> putStrLn "Successfully parsed." @ If you just want to throw any parse errors as IO exception, you can use join: @ import Control.Monad (join) object <- join $ decodeFile "myfile.yaml" @ This takes advantage of the IO instance of Failure. /Parsing an Object/ In order to pull the data out of an Object, you can use the helper functions from Data.Object. For example: @ import Data.Object import Data.Object.Yaml import Control.Monad main = do object <- join $ decodeFile "myfile.yaml" people <- fromMapping object michael <- lookupMapping "Michael" people age <- lookupScalar "age" michael putStrLn $ "Michael is " ++ age ++ " years old." @ lookupScalar and friends implement Maybe, so you can test for optional attributes by switching on Nothing/Just a: @ name <- lookupScalar "middleName" michael :: Maybe String @ /And that's it/ There's really not more to know about this library. Enjoy! -} module Data.Object.Yaml ( -- * Definition of 'YamlObject' YamlScalar (..) , YamlObject -- * Automatic scalar conversions , IsYamlScalar (..) , toYamlObject , fromYamlObject -- * Encoding/decoding , encode , encodeFile , decode , decodeFile -- * Exceptions , ParseException (..) ) where import qualified Text.Libyaml as Y import Text.Libyaml hiding (encode, decode, encodeFile, decodeFile) import Data.Object import Data.ByteString (ByteString) import qualified Data.Map as Map import System.IO.Unsafe (unsafePerformIO) import Control.Failure (Failure (failure)) import qualified Data.Text import qualified Data.Text.Lazy import qualified Data.ByteString import qualified Data.ByteString.Lazy import Data.Convertible.Text (cs) import Data.Data import Control.Monad.Trans.Class import Control.Monad.IO.Class import Control.Monad.Trans.State import Control.Monad import qualified Data.Conduit as C import qualified Data.Conduit.List as CL import Prelude hiding (catch) import Control.Exception (throwIO, Exception, fromException, try) import Data.String (IsString (fromString)) -- | Equality depends on 'value' and 'tag', not 'style'. data YamlScalar = YamlScalar { value :: ByteString , tag :: Tag , style :: Style } deriving (Show, Read, Data, Typeable) instance Eq YamlScalar where (YamlScalar v t _) == (YamlScalar v' t' _) = v == v' && t == t' instance IsString YamlScalar where fromString = toYamlScalar type YamlObject = Object YamlScalar YamlScalar class (Eq a) => IsYamlScalar a where fromYamlScalar :: YamlScalar -> a toYamlScalar :: a -> YamlScalar instance IsYamlScalar YamlScalar where fromYamlScalar = id toYamlScalar = id instance IsYamlScalar Data.Text.Text where fromYamlScalar = cs . value toYamlScalar t = YamlScalar (cs t) NoTag Any instance IsYamlScalar Data.Text.Lazy.Text where fromYamlScalar = cs . value toYamlScalar t = YamlScalar (cs t) NoTag Any instance IsYamlScalar [Char] where fromYamlScalar = cs . value toYamlScalar s = YamlScalar (cs s) NoTag Any instance IsYamlScalar Data.ByteString.ByteString where fromYamlScalar = value toYamlScalar b = YamlScalar b NoTag Any instance IsYamlScalar Data.ByteString.Lazy.ByteString where fromYamlScalar = cs . value toYamlScalar b = YamlScalar (cs b) NoTag Any -- | Merge assoc-lists by keys. -- First list overrides second: -- [(k1, x), (k2, y)] `mergeAssocLists` [(k3, z)] == [(k1, x), (k2, y), (k3, z)] -- [(k1, x), (k2, y)] `mergeAssocLists` [(k2, z)] == [(k1, x), (k2, y)] mergeAssocLists :: (Eq k) => [(k, v)] -> [(k, v)] -> [(k, v)] mergeAssocLists a [] = a mergeAssocLists [] b = b mergeAssocLists a ((bk, bv):bs) = case lookup bk a of Nothing -> (bk, bv) : mergeAssocLists a bs Just av -> (bk, av) : mergeAssocLists (filter (\(x, _) -> x /= bk) a) bs toYamlObject :: IsYamlScalar k => IsYamlScalar v => Object k v -> YamlObject toYamlObject = mapKeysValues toYamlScalar toYamlScalar fromYamlObject :: IsYamlScalar k => IsYamlScalar v => YamlObject -> Object k v fromYamlObject = mapKeysValues fromYamlScalar fromYamlScalar encode :: (IsYamlScalar k, IsYamlScalar v) => Object k v -> ByteString encode obj = unsafePerformIO $ C.runResourceT $ CL.sourceList (objToEvents $ toYamlObject obj) C.$$ Y.encode encodeFile :: (IsYamlScalar k, IsYamlScalar v) => FilePath -> Object k v -> IO () encodeFile fp obj = C.runResourceT $ CL.sourceList (objToEvents $ toYamlObject obj) C.$$ Y.encodeFile fp objToEvents :: YamlObject -> [Y.Event] objToEvents o = (:) EventStreamStart . (:) EventDocumentStart $ objToEvents' o [ EventDocumentEnd , EventStreamEnd ] scalarToEvent :: YamlScalar -> Event scalarToEvent (YamlScalar v t s) = EventScalar v t s Nothing objToEvents' :: YamlObject -> [Y.Event] -> [Y.Event] objToEvents' (Scalar s) rest = scalarToEvent s : rest objToEvents' (Sequence list) rest = EventSequenceStart Nothing : foldr ($) (EventSequenceEnd : rest) (map objToEvents' list) objToEvents' (Mapping pairs) rest = EventMappingStart Nothing : foldr ($) (EventMappingEnd : rest) (map pairToEvents pairs) pairToEvents :: (YamlScalar, YamlObject) -> [Y.Event] -> [Y.Event] pairToEvents (k, v) rest = scalarToEvent k : objToEvents' v rest -- Parsing data ParseException = NonScalarKey | UnknownAlias { _anchorName :: Y.AnchorName } | UnexpectedEvent { _received :: Maybe Event , _expected :: Maybe Event } | InvalidYaml (Maybe String) deriving (Show, Typeable) instance Exception ParseException newtype PErrorT m a = PErrorT { runPErrorT :: m (Either ParseException a) } instance Monad m => Monad (PErrorT m) where return = PErrorT . return . Right (PErrorT m) >>= f = PErrorT $ do e <- m case e of Left e' -> return $ Left e' Right a -> runPErrorT $ f a instance MonadTrans PErrorT where lift = PErrorT . liftM Right instance MonadIO m => MonadIO (PErrorT m) where liftIO = lift . liftIO type Parser = StateT (Map.Map String YamlObject) IO requireEvent :: Event -> C.Sink Event Parser () requireEvent e = do f <- CL.head if f == Just e then return () else liftIO $ throwIO $ UnexpectedEvent f $ Just e parse :: C.Sink Event Parser YamlObject parse = do requireEvent EventStreamStart requireEvent EventDocumentStart res <- parseO requireEvent EventDocumentEnd requireEvent EventStreamEnd return res parseScalar :: ByteString -> Tag -> Style -> Anchor -> C.Sink Event Parser YamlScalar parseScalar v t s a = do let res = YamlScalar v t s case a of Nothing -> return res Just an -> do lift $ modify (Map.insert an $ Scalar res) return res parseO :: C.Sink Event Parser YamlObject parseO = do me <- CL.head case me of Just (EventScalar v t s a) -> Scalar `liftM` parseScalar v t s a Just (EventSequenceStart a) -> parseS a id Just (EventMappingStart a) -> parseM a id Just (EventAlias an) -> do m <- lift get case Map.lookup an m of Nothing -> liftIO $ throwIO $ UnknownAlias an Just v -> return v _ -> liftIO $ throwIO $ UnexpectedEvent me Nothing parseS :: Y.Anchor -> ([YamlObject] -> [YamlObject]) -> C.Sink Event Parser YamlObject parseS a front = do me <- CL.peek case me of Just EventSequenceEnd -> do CL.drop 1 let res = Sequence $ front [] case a of Nothing -> return res Just an -> do lift $ modify $ Map.insert an res return res _ -> do o <- parseO parseS a $ front . (:) o parseM :: Y.Anchor -> ([(YamlScalar, YamlObject)] -> [(YamlScalar, YamlObject)]) -> C.Sink Event Parser YamlObject parseM a front = do me <- CL.peek case me of Just EventMappingEnd -> do CL.drop 1 let res = Mapping $ front [] case a of Nothing -> return res Just an -> do lift $ modify $ Map.insert an res return res _ -> do me' <- CL.head s <- case me' of Just (EventScalar v t s a') -> parseScalar v t s a' _ -> liftIO $ throwIO $ UnexpectedEvent me' Nothing o <- parseO let al = mergeAssocLists [(s, o)] $ front [] al' = if fromYamlScalar s == "<<" then case o of Scalar _ -> al Mapping l -> mergeAssocLists al l Sequence l -> mergeAssocLists al $ foldl merge' [] l else al parseM a (`mergeAssocLists` al') where merge' :: (Eq k) => [(k, Object k v)] -> Object k v -> [(k, Object k v)] merge' al (Mapping om) = mergeAssocLists al om merge' al _ = al decode :: (Failure ParseException m, IsYamlScalar k, IsYamlScalar v) => ByteString -> m (Object k v) decode bs = unsafePerformIO $ decodeHelper (Y.decode bs) decodeFile :: (Failure ParseException m, IsYamlScalar k, IsYamlScalar v) => FilePath -> IO (m (Object k v)) decodeFile fp = decodeHelper (Y.decodeFile fp) decodeHelper :: (Failure ParseException m, IsYamlScalar k, IsYamlScalar v) => C.Source Parser Y.Event -> IO (m (Object k v)) decodeHelper src = do x <- try $ flip evalStateT Map.empty $ C.runResourceT $ src C.$$ parse case x of Left e | Just pe <- fromException e -> return $ failure (pe :: ParseException) | otherwise -> return $ failure $ InvalidYaml $ Just $ show e Right y -> return $ return $ fromYamlObject y