{-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE OverloadedStrings #-} -- | This module provides XML picklers that plug into the XML tree of the -- /xml-types/ package. This module was \"inspired\" by /hexpat-pickle/. -- -- The API differences between /hexpat-pickle/ and this module include: -- -- * When unpickling, picklers will /consume/ matching elmements so that they -- will be ignored by sucessive picklers. To circumvent this behaviour, use -- @'xpPeek'@ -- -- * Wrappers like 'xpWrap' are uncurried -- -- * There are no lazy unpicklers -- -- * Most unpicklers will produce an error when their child unpicklers fail to -- consume all elements. Use 'xpClean' to discard those elements -- -- The data type @'PU' t a@ represents both a pickler (converting Haskell data -- to XML) and an unpickler (XML to Haskell data), so your code only needs to -- be written once for both serialization and deserialization. The 'PU' -- primitives, such as 'xpElem' for XML elements, may be composed into complex -- arrangements using 'xpPair' and other combinators. -- -- Most picklers will try to find the /first match/ rather than failing when -- the first element doesn't match. This is why the target type often is a -- list. To prevent this behaviour and commit the pickler to the first element -- available, use 'xpIsolate'. -- -- The top level of the document does not follow this rule, because it is a -- single node type. 'xpRoot' is needed to adapt this to type ['Node'] for your -- pickler to use. You would typically define a pickler for a whole document -- with 'xpElem', then pickle it to a single 'Node' with @'pickleTree' (xpRoot -- myDocPickler) value@. -- -- /NB/: Unresolved entities are considered an error and will trigger an -- exception -- -- When unpickling, the following invariant regarding the list of remaining -- elements should be observed: -- -- * The returned list should be a subset of or the initial list itself, that -- is, no elements should be added or changed -- -- * The relative order of elements should be preserved -- -- * Elements may, however, be removed from anywhere in the list -- -- Here is a simple example to get you started: -- -- > {-# LANGUAGE OverloadedStrings #-} -- > import Data.Text -- > import Data.XML.Types -- > import Data.XML.Pickle -- > -- > -- Person name, age and description -- > data Person = Person Text Int Text -- > -- > xpPerson :: PU [Node] Person -- > xpPerson = -- > -- How to wrap and unwrap a Person -- > xpWrap (\((name, age), descr) -> Person name age descr) -- > (\(Person name age descr) -> ((name, age), descr)) $ -- > xpElem "person" -- > (xpPair -- > (xpAttr "name" xpId) -- > (xpAttr "age" xpPrim)) -- > (xpContent xpId) -- > -- > people = [ -- > Person "Dave" 27 "A fat thin man with long short hair", -- > Person "Jane" 21 "Lives in a white house with green windows"] -- > -- > main = do -- > print $ pickle (xpRoot $ xpElemNodes "people" $ xpAll xpPerson) people -- -- Program output would be an xml-value equivalent to: -- -- > -- > -- > A fat thin man with long short hair -- > -- > -- > Lives in a white house with green windows -- > -- > -- -- Functions marked with /compat/ are included for compatibility with -- /hexpat-pickle/. module Data.XML.Pickle ( -- * Types PU(..) , Attribute , UnpickleResult(..) -- * Pickler invocation , pickle , unpickle -- * Primitive picklers , xpUnit , xpZero , xpThrow , xpIso , xpPartial -- * Value-preserving picklers , xpId , xpFst , xpSnd , xpTrees , xpHead , xpTree , xpText0 , xpText , xpString , xpRoot , xpPrim -- * XML specific picklers -- ** Attributes , xpAttribute , xpAttribute' , xpAttribute_ , xpAttr , xpAttrImplied , xpAttrFixed , xpAddFixedAttr -- ** Elements , xpElem , xpElemWithName , xpElemByNamespace , xpElemVerbatim , xpElemAttrs , xpElemNodes , xpElemText , xpElemBlank , xpElemExists , xpElems -- ** Character Content , xpContent , xpBool -- * Pickler combinators -- ** Choice , xpOption , xpDefault , xpWithDefault , xpMap , xpAlt , xpChoice , xpEither , xpTryCatch -- ** Sequencing -- | /NB/: The sequencing operations /do not/ enforce any order on the -- matched elements unless stated otherwise, but you can commit individial -- picklers to the next available element with 'xpIsolate'. Applying -- @xpIsolate@ on all nested Picklers will in effect enforce order. -- -- Howver, once a pickler consumes an element it will not be available to -- following picklers. You can circumvent this behaviour with 'xpPeek'. -- -- If you want ensure that all elements are consumed after the last pickler -- is run you may want to use 'xpClean'. -- -- *** Lists -- | The List pickler combinators will pickle lists in the given order -- without any special treatment and unpickle as stated. , xpFindMatches , xpFindFirst , xpAll , xpSubsetAll , xpAllByNamespace , xpList0 , xpSeqWhile , xpList , xpListMinLen -- *** Tuples -- | Tuple combinators apply their picklers from left to right. They will -- succeed when all their constituents produce a value. , xp2Tuple , xpPair , (<#>) , xp3Tuple , xpTriple , xp4Tuple , xp5Tuple , xp6Tuple -- ** Wrappers -- *** Value wrappers , xpWrap , xpConst , xpWrapEither , xpWrapMaybe , xpWrapMaybe_ , xpAssert , xpMayFail , xpUnliftElems -- *** Bookkeeping -- | Change the semantics of picklers , xpIsolate , xpPeek -- *** Cleannes -- | Picklers keep track of elements left over after unpickling, so the may -- be [@clean@] an unpickling is considered @clean@ when it doesn't leave any -- remainng elements. , xpClean -- * Error handling , UnpickleError(..) , ppUnpickleError , (<++>) , () , () , () , UnresolvedEntityException(..) -- * Helper functions , flattenContent , tErr , getRest ) where import Control.Applicative ((<$>)) import Control.Arrow import Control.Exception import Control.Monad import Data.Char (isSpace) import Data.Either import Data.List (partition) import qualified Data.Map as M import Data.Maybe import Data.Monoid (Monoid, mempty) import Data.Text (Text) import qualified Data.Text as Text import Data.XML.Types import Data.XML.Pickle.Tuples import Data.XML.Pickle.Basic -- | Pickle a tree. pickle :: PU t a -> a -> t pickle = pickleTree -- | Unpickle a tree. unpickle :: PU t a -> t -> Either UnpickleError a unpickle xp x = case unpickleTree xp x of UnpickleError e -> Left e NoResult e -> Left . ErrorMessage $ "Entity not found " `Text.append` e Result r _ -> Right r for :: [a] -> (a -> b) -> [b] for = flip map type Attribute = (Name,[Content]) -- | Isomorphic pickler. xpIso :: (a -> b) -> (b -> a) -> PU a b xpIso f g = PU (\t -> Result (f t) Nothing) g xpPartial :: (a -> Either Text b) -> (b -> a) -> PU a b xpPartial f g = ("xpEither", "") PU { pickleTree = g , unpickleTree = \v -> case f v of Left e -> UnpickleError $ ErrorMessage e Right r -> Result r Nothing } -- | Doesn't create or consume anything, always succeeds. xpUnit :: PU [a] () xpUnit = PU (Result () . remList) (const []) -- | Returns everything (remaining), untouched. xpId :: PU a a xpId = xpIso id id -- | 'xpId'. (/compat/) xpTrees :: PU a a xpTrees = xpId -- | Converts Booleans to XML boolean values -- -- * \"true\" and \"1\" are read as @True@ -- -- * \"false\" and \"0\" are read as @False@ -- -- * All other values generate an unpickle error -- -- Will always generate \"true\" or \"false\" (not \"0\" or \"1\") when -- pickling. xpBool :: PU Text Bool xpBool = ("xpBool" ,"") PU { unpickleTree = \v -> case () of () | v `elem` ["true", "1"] -> Result True Nothing | v `elem` ["false", "0"] -> Result False Nothing | otherwise -> UnpickleError (ErrorMessage $ "Not a boolean value: " `Text.append` v) , pickleTree = \v -> case v of True -> "true" False -> "false" } -- | Apply a bijection before pickling / after unpickling. xpWrap :: (a -> b) -> (b -> a) -> PU t a -> PU t b xpWrap to from xp = ("xpWrap","") PU { unpickleTree = \x -> to <$> unpickleTree xp x , pickleTree = pickleTree xp . from } -- | Like xpWrap, but strips @Just@ (and treats @Nothing@ as a failure) during -- unpickling. xpWrapMaybe :: (a -> Maybe b) -> (b -> a) -> PU t a -> PU t b xpWrapMaybe a2b b2a pua = ("xpWrapMaybe","") xpWrapMaybe_ "xpWrapMaybe can't encode Nothing" a2b b2a pua -- | Like xpWrap, but strips @Just@ (and treats @Nothing@ as a failure) during -- unpickling, with specified error message for @Nothing@ value. xpWrapMaybe_ :: String -> (a -> Maybe b) -> ( b -> a) -> PU t a -> PU t b xpWrapMaybe_ errorMsg a2b b2a pua = ("xpWrapMaybe_","") PU { unpickleTree = \t -> case unpickleTree pua t of Result val rest -> case a2b val of Just val' -> Result val' rest Nothing -> UnpickleError $ upe errorMsg NoResult e -> NoResult e UnpickleError e -> UnpickleError e , pickleTree = pickleTree pua . b2a } -- | Lift a pickler. Nothing is returned when the given pickler doesn't return -- a value (e.g. the element isn't found). Does not affect unpickling errors. -- @Nothing@ is pickled to @mempty@. -- -- A typical example is: -- -- > xpElemAttributes "score" $ xpOption $ xpAttribute "value" xpPrim -- -- in which @Just 5@ is encoded as @\@ and @Nothing@ -- as @\@. xpOption :: PU [t] a -> PU [t] (Maybe a) xpOption pu = PU { unpickleTree = doUnpickle , pickleTree = \mValue -> case mValue of Just value -> pickleTree pu value Nothing -> mempty } where doUnpickle t = case unpickleTree pu t of Result r t' -> Result (Just r) t' NoResult _e -> Result Nothing (remList t) UnpickleError e -> UnpickleError e -- | Return one element, untouched. xpHead :: PU [a] a xpHead = PU {unpickleTree = \t' -> case t' of [] -> UnpickleError $ ("xpHead","") <++> upe "No element remaining" t:ts -> Result t (if null ts then Nothing else Just ts) , pickleTree = return } -- | 'xpHead'. (/compat/) xpTree :: PU [a] a xpTree = xpHead -- | Specialised version of 'xpId'. (/compat/) xpText0 :: PU Text Text xpText0 = xpId -- | Convert @Text@ to/from @String@. xpString :: PU Text String xpString = ("xpString", "") xpIso Text.unpack Text.pack -- | Test predicate when unpickling. Fails with given error message when the -- predicate return false. -- -- /NB/: The predicate will only be tested while /unpickling/. When pickling, -- this is a noop. xpAssert :: Text -> (a -> Bool) -> PU t a -> PU t a xpAssert err p xp = ("xpAssert",err) PU { unpickleTree = \t -> do r <- unpickleTree xp t unless (p r) $ UnpickleError assertErr return r , pickleTree = pickleTree xp } where assertErr = upe ("Assertion failed; " ++ Text.unpack err) -- | Like 'xpText0', but fails on non-empty input. xpText :: PU Text Text xpText = ("xpText","") xpAssert "Input is empty" (not . Text.null) xpText0 -- | Transforms a pickler on lists to a pickler on single elements. -- -- /NB/: Will error when the given pickler doesn't produce exactly one -- element. xpRoot ::PU [a] b -> PU a b xpRoot pa = ("xpRoot","") PU { unpickleTree = \t -> case unpickleTree pa [t] of Result x Nothing -> Result x Nothing Result _x (Just _) -> UnpickleError $ upe "Leftover entities" UnpickleError e -> UnpickleError e NoResult e -> NoResult e , pickleTree = \t -> case pickleTree pa t of [t1] -> t1 _ -> error "pickler called by xpRoot must output exactly one element" } getFirst :: (t -> Bool) -> [t] -> Maybe (t, [t]) getFirst _ [] = Nothing getFirst p (x:xs) = case p x of True -> Just (x,xs) False -> second (x:) <$> getFirst p xs -- | Pickle to/from attribute. xpAttribute :: Name -> PU Text a -> PU [Attribute] a xpAttribute name pu = ("xpAttr" , Text.pack $ ppName name) PU { unpickleTree = doUnpickle , pickleTree = \value -> [(name, [ContentText $ pickleTree pu value])] } where doUnpickle attrs = case getFirst ((== name) . fst) attrs of Nothing -> NoResult $ Text.pack $ ppName name Just ((_, c), rem') | all isContentText c -> case unpickleTree pu (contentToText c) of NoResult e -> missingE $ Text.unpack e UnpickleError e -> UnpickleError e Result _ (Just e) -> leftoverE $ show e Result r Nothing -> Result r (remList rem') -- Content entities are not supported. | otherwise -> UnpickleError $ upe ("Unresolved entities in " ++ ppName name ++ ".") contentToText = Text.concat . map contentToText_ contentToText_ (ContentText t) = t contentToText_ (ContentEntity t) = t isContentText (ContentText _) = True isContentText (ContentEntity _) = False -- | 'xpAttribute' (/compat/) xpAttr :: Name -> PU Text a -> PU [Attribute] a xpAttr = xpAttribute -- | Pickle attribute if @Just@ is given, on unpickling return @Just @ -- when the attribute is found, @Nothing@ otherwise. xpAttribute' :: Name -> PU Text a -> PU [Attribute] (Maybe a) xpAttribute' name pu = xpOption $ xpAttr name pu xpAttrImplied :: Name -> PU Text a -> PU [Attribute] (Maybe a) xpAttrImplied = xpAttribute' -- | Pickle an attribute with the specified name and value, fail if the same -- attribute is not present on unpickle. xpAttribute_ :: Name -> Text -> PU [Attribute] () xpAttribute_ name val = xpWrapMaybe_ ("expected fixed attribute "++ ppName name++"="++show val) (\v -> if v == val then Just () else Nothing) (const val) $ xpAttr name xpId -- | 'xpAttribute_' (/compat/) xpAttrFixed :: Name -> Text -> PU [Attribute] () xpAttrFixed = xpAttribute_ -- | Merge successive @NodeContent@ values. flattenContent :: [Node] -> [Node] flattenContent xs = case foldr (\x (buf, res) -> case x of NodeContent (ContentText txt) -> (txt : buf, res) NodeComment _ -> (buf, res) e@(NodeElement _) -> ([] , e : addConcatText buf res) _ -> throw UnresolvedEntityException ) ([], []) xs of (buf, res) -> addConcatText buf res where nc = NodeContent . ContentText addConcatText [] = id addConcatText xs' = let txt = Text.concat xs' in if Text.all isSpace txt then id else (nc txt :) -- | When unpickling, tries to find the first element with the supplied name. -- Once such an element is found, it will commit to it and /fail/ if any of the -- picklers don't match. xpElem :: Name -- ^ Name of the 'Element' -> PU [Attribute] a -- ^ Pickler for attributes -> PU [Node] n -- ^ Pickler for child nodes -> PU [Node] (a,n) xpElem name attrP nodeP = tr PU { unpickleTree = doUnpickleTree , pickleTree = \(a,n) -> [NodeElement $ Element name (pickleTree attrP a) (pickleTree nodeP n) ] } where doUnpickleTree nodes = case getFirst (nodeElementNameHelper name) nodes of Just (NodeElement (Element _ attrs children), rem') -> do as <- ("attrs","") <++.> child attrP attrs cs <- ("children","") <++.> child nodeP (flattenContent children) leftover $ remList rem' return (as, cs) _ -> NoResult $ Text.pack $ ppName name tr = ("xpElem", Text.pack $ ppName name) nodeElementNameHelper name' (NodeElement (Element n _ _)) = n == name' nodeElementNameHelper _ _ = False -- | Handle all elements with a given name. The unpickler will fail when any of -- the elements fails to unpickle. xpElems :: Name -- ^ Name of the 'Element' values -> PU [Attribute] a -- ^ Pickler for attributes -> PU [Node] n -- ^ Pickler for child nodes -> PU [Node] [(a, n)] xpElems name attrs children = tr xpSubsetAll isThisElem (xpElem name attrs children) where isThisElem (NodeElement (Element name' _ _)) = name' == name isThisElem _ = False tr = ("xpElems", Text.pack $ ppName name) -- | Tries to apply the pickler to all the remaining elements; fails if any of -- them don't match. xpAll :: PU [a] b -> PU [a] [b] xpAll xp = ("xpAll", "") PU { unpickleTree = doUnpickleTree , pickleTree = concatMap (pickleTree xp) } where doUnpickleTree = mapM (child' xp . return) -- | For unpickling, apply the given pickler to a subset of the elements -- determined by a given predicate. -- -- Pickles like 'xpAll'. xpSubsetAll :: (a -> Bool) -- ^ Predicate to select the subset -> PU [a] b -- ^ Pickler to apply on the subset -> PU [a] [b] xpSubsetAll p xp = ("xpSubsetAll","") PU { unpickleTree = \t -> let (targets, rest) = partition p t in do leftover $ remList rest child' (xpAll xp) targets , pickleTree = pickleTree $ xpAll xp } -- | Apply unpickler to all elements with the given namespace. -- -- Pickles like 'xpAll'. xpAllByNamespace :: Text -> PU [Node] b -> PU [Node] [b] xpAllByNamespace namespace xp = ("xpAllByNamespace",namespace) xpSubsetAll helper xp where helper (NodeElement (Element (Name _local (Just ns) _pre) _ _ )) = ns == namespace helper _ = False -- | Pickle 'Element' without a restriction on the name. The name as taken / -- returned as the first element of the triple. xpElemWithName :: PU [Attribute] a -- ^ Pickler for attributes -> PU [Node] n -- ^ Pickler for child nodes -> PU [Node] (Name,a,n) xpElemWithName attrP nodeP = ("xpElemWithName", "") PU { unpickleTree = doUnpickleTree , pickleTree = \(name, a,n) -> [NodeElement $ Element name (pickleTree attrP a) (pickleTree nodeP n) ] } where doUnpickleTree nodes = case getFirst nodeElementHelper nodes of Just (NodeElement (Element name attrs children), rem') -> do x <- child attrP attrs y <- child nodeP $ flattenContent children leftover $ remList rem' return (name, x, y) _ -> NoResult "element" nodeElementHelper (NodeElement Element{}) = True nodeElementHelper _ = False -- | Find element by name space, prefixes are ignored. xpElemByNamespace :: Text -- ^ Namespace -> PU Text name -- ^ Pickler for the local name -> PU [Attribute] a -- ^ Pickler for attributes -> PU [Node] n -- ^ Pickler for child nodes -> PU [Node] (name,a,n) xpElemByNamespace ns nameP attrP nodeP = PU { unpickleTree = doUnpickleTree , pickleTree = \(name, a,n) -> [NodeElement $ Element (Name (pickleTree nameP name) (Just ns) Nothing) (pickleTree attrP a) (pickleTree nodeP n) ] } where doUnpickleTree nodes = case getFirst (nodeElementNSHelper ns) nodes of Just (NodeElement (Element name attrs children), rem') -> tr name $ do name' <- child nameP (nameLocalName name) attrs' <- child attrP attrs nodes' <- child nodeP children leftover $ remList rem' return (name', attrs', nodes') _ -> NoResult $ "Element with namepspace " `Text.append` ns tr a res = case res of UnpickleError e -> UnpickleError (TraceStep ( "xpElemByNamespace" , Text.concat [ ns , " ; " , nameLocalName a]) e) x -> x nodeElementNSHelper ns' (NodeElement (Element n _ _)) = nameNamespace n == Just ns' nodeElementNSHelper _ns _ = False -- | Pickler returns the first found 'Element' untouched. -- -- Unpickler wraps element in 'NodeElement'. xpElemVerbatim :: PU [Node] Element xpElemVerbatim = PU { unpickleTree = doUnpickleTree , pickleTree = \e -> [NodeElement e] } where doUnpickleTree nodes = case getFirst nodeElementHelper nodes of Just (NodeElement e@Element{}, re) -> Result e (remList re) _ -> NoResult "element" nodeElementHelper (NodeElement Element{}) = True nodeElementHelper _ = False -- | A helper variant of 'xpElem' for elements that contain attributes but no -- child tags. xpElemAttrs :: Name -> PU [Attribute] b -> PU [Node] b xpElemAttrs name puAttrs = xpWrap fst (\a -> (a,())) $ xpElem name puAttrs xpUnit -- | A helper variant of 'xpElem' for elements that contain child nodes but no -- attributes. xpElemNodes :: Name -> PU [Node] b -> PU [Node] b xpElemNodes name puChildren = xpWrap snd (\a -> ((),a)) $ xpElem name xpUnit puChildren -- | A helper variant of 'xpElem' for elements that contain only character -- data. xpElemText :: Name -> PU [Node] Text xpElemText name = xpElemNodes name $ xpContent xpId -- | Helper for 'Element' values that don't contain anything. xpElemBlank :: Name -> PU [Node] () xpElemBlank name = ("xpElemBlank", "") xpWrap (const () ) (const ((),())) $ xpElem name xpUnit xpUnit -- | When pickling, creates an empty element iff parameter is @True@. -- -- When unpickling, checks whether element exists. Generates an error when the -- element is not empty. xpElemExists :: Name -> PU [Node] Bool xpElemExists name = ("xpElemBlank", "") xpWrap (\x -> case x of Nothing -> False; Just _ -> True) (\x -> if x then Just () else Nothing) $ xpOption (xpElemBlank name) -- | Get the first non-element 'NodeContent' from a node. xpContent :: PU Text a -> PU [Node] a xpContent xp = ("xpContent","") PU { unpickleTree = doUnpickle , pickleTree = return . NodeContent . ContentText . pickleTree xp } where doUnpickle nodes = case getFirst nodeContentHelper (filter (\node -> case node of NodeComment _ -> False _ -> True) nodes) of -- flatten Just (NodeContent (ContentText t), _re) -> child xp t Just (NodeContent (ContentEntity t), _) -> UnpickleError . upe $ "Unresolved entity" ++ show t ++ "." _ -> NoResult "node content" nodeContentHelper (NodeContent _) = True nodeContentHelper _ = False -- | Unlift a pickler on 'Node' values to a pickler on 'Element' values. Nodes -- generated during pickling that are not 'Element' values will be silently -- discarded. xpUnliftElems :: PU [Node] a -> PU [Element] a xpUnliftElems xp = ("xpUnliftElems","") PU { unpickleTree = doUnpickle , pickleTree = nodesToElems . pickleTree xp } where doUnpickle nodes = case unpickleTree xp (map NodeElement nodes) of UnpickleError e -> UnpickleError e NoResult e -> NoResult e Result a r -> let r' = case r of Nothing -> Nothing Just rs' -> case nodesToElems rs' of [] -> Nothing rs -> Just rs in Result a r' nodesToElems = foldr (\n es -> case n of NodeElement e -> e : es _ -> es) [] -- | Optional conversion with default value. -- -- Unlike 'xpWithDefault' the default value is not encoded in the XML document, -- during unpickling the default value is inserted if the pickler doesn't -- return a value. xpDefault :: (Eq a) => a -> PU [t] a -> PU [t] a xpDefault df = xpWrap (fromMaybe df) (\ x -> if x == df then Nothing else Just x) . xpOption -- | Attempt to use a pickler. Return a default value when the pickler doesn't -- return anything (doesn't touch on 'UnpickleError'). -- -- Unlike 'xpDefault', the default value /is/ encoded in the XML document. xpWithDefault :: a -> PU t a -> PU t a xpWithDefault a pa = PU { pickleTree = pickleTree pa , unpickleTree = \v -> case unpickleTree pa v of Result r t -> Result r t NoResult _ -> Result a (Just v) UnpickleError e -> UnpickleError e } -- | 'xp2Tuple'. (/compat/) xpPair :: PU [a] b1 -> PU [a] b2 -> PU [a] (b1, b2) xpPair l r = "xpPair" xp2Tuple l r -- | 'xp2Tuple'. (<#>) :: PU [a] b1 -> PU [a] b2 -> PU [a] (b1, b2) (<#>) l r = "(<#>)" xp2Tuple l r -- | 'xp3Tuple'. (/compat/) xpTriple :: PU [a] a1 -> PU [a] a2 -> PU [a] a3 -> PU [a] (a1, a2, a3) xpTriple l m r = "xpTriple" xp3Tuple l m r -- | When unpickling, don't consume the matched element(s), noop when pickling. xpPeek :: PU t a -> PU t a xpPeek xp = PU { pickleTree = pickleTree xp , unpickleTree = \xs -> case unpickleTree xp xs of Result r _ -> Result r (Just xs) x -> x } -- | Noop when pickling. -- -- When unpickling, only give access to the first element. xpIsolate :: PU [t] a -> PU [t] a xpIsolate xp = ("xpIsolate","") PU { pickleTree = pickleTree xp , unpickleTree = \xs' -> case xs' of [] -> NoResult "entity" (x:xs) -> case unpickleTree xp [x] of Result r t -> Result r (remList $ mbToList t ++ xs) NoResult e -> missingE $ Text.unpack e y -> y } where mbToList Nothing = [] mbToList (Just r) = r -- | Select a single element from the list and apply unpickler to it. -- -- Returns no value when no element matches the predicate. -- -- Fails when the unpickler doesn't return a value. -- -- When pickling, this is a noop. xpFindFirst :: (t -> Bool) -> PU [t] a -> PU [t] a xpFindFirst p xp = ("xpFindFirst","") PU { pickleTree = pickleTree xp , unpickleTree = \xs -> case break p xs of (_, []) -> NoResult "entity" (ys,z:zs) -> do leftover . remList $ ys ++ zs child' xp [z] } -- | Ignore input/output and replace with constant values. xpConst :: a -> PU t () -> PU t a xpConst c xp = ("xpConst" ,"") xpWrap (const c) (const ()) xp -- | Convert text to/from any type that implements 'Read' and 'Show'. Fails on -- unpickle if 'read' fails. xpPrim :: (Show a, Read a) => PU Text a xpPrim = PU { unpickleTree = \x -> case reads $ Text.unpack x of [] -> UnpickleError $ ("xpPrim","") <++> upe ("Couldn't read " ++ show x ++ ".") (r,rest):_ -> Result r (Text.pack <$> remList rest) , pickleTree = Text.pack . show } -- | When unpickling, tries to apply the pickler to all elements returning and -- consuming only matched elements. xpFindMatches :: PU [b] a -> PU [b] [a] xpFindMatches xp = PU { unpickleTree = doUnpickleTree , pickleTree = \xs -> pickleTree xp =<< xs } where doUnpickleTree xs = let (ls, rs) = partitionEithers . for xs $ \x -> case unpickleTree xp [x] of NoResult _ -> Left x Result r Nothing -> Right $ Result r Nothing Result _r (Just _) -> Right $ leftoverE "" UnpickleError e -> Right $ UnpickleError e in leftover (remList ls) >> sequence rs -- | 'xpAll'. (/compat/) xpList0 :: PU [a] b -> PU [a] [b] xpList0 = xpAll -- | Like 'xpList', but only succeed during unpickling if at least a minimum -- number of elements are unpickled. xpListMinLen :: Int -> PU [a] b -> PU [a] [b] xpListMinLen ml = xpWrapEither testLength id . xpList where testLength as | length as < ml = Left $ "Expecting at least " ++ show ml ++ " elements" testLength as = Right as -- | When unpickling, successively applies pickler to single elements until it -- doesn't return anything; returns all matched elements. xpSeqWhile :: PU [a] b -> PU [a] [b] xpSeqWhile pu = ("xpSeqWhile", "") PU { unpickleTree = doUnpickle , pickleTree = concatMap $ pickleTree pu } where doUnpickle [] = Result [] Nothing doUnpickle es@(elt:re) = case unpickleTree pu [elt] of Result val _ -> case doUnpickle re of Result xs r -> Result (val:xs) r e -> e NoResult _ -> Result [] (Just es) UnpickleError e -> UnpickleError e -- | 'xpSeqWhile'. (/compat/) xpList :: PU [a] b -> PU [a] [b] xpList = xpSeqWhile -- | Standard pickler for maps -- -- This pickler converts a map into a list of pairs of the form -- -- > value xpMap :: Ord k => Name -- ^ Element name (elt) -> Name -- ^ Attribute name (attr) -> PU Text k -- ^ Pickler for keys (key) -> PU [Node] a -- ^ Pickler for values (value) -> PU [Node] (M.Map k a) xpMap en an xpk xpv = xpWrap M.fromList M.toList $ xpAll $ xpElem en (xpAttr an xpk) xpv -- | Like xpWrap, except it strips @Right@ (and treats @Left@ as a failure) -- during unpickling. -- -- Not to be confused with 'xpEither'. xpWrapEither :: Show e => (a -> Either e b) -> (b -> a) -> PU t a -> PU t b xpWrapEither a2b b2a pua = ("xpWrapEither","") PU { unpickleTree = \t -> case unpickleTree pua t of Result val rest -> case a2b val of Left e -> UnpickleError . upe $ "Function returned Left " ++ show e Right r -> Result r rest NoResult e -> NoResult e UnpickleError e -> UnpickleError e , pickleTree = pickleTree pua . b2a } -- | Execute one of a list of picklers. The /selector function/ is used during -- pickling, and the integer returned is taken as a 0-based index to select a -- pickler from /pickler options/. Unpickling is done by trying each list -- element in order until one returns a @Result@ (the /selector/ is not used). -- -- This is typically used to handle each constructor of a data type. However, -- it can be used wherever multiple serialization strategies apply to a single -- type. -- -- /NB/: This function will ignore all errors as long as one of the branches -- returns a result. Also, it will produce an error when all branches return -- @NoResult@. Use 'xpChoice' for a saner version of this function. xpAlt :: (a -> Int) -- ^ Selector function -> [PU t a] -- ^ List of picklers -> PU t a xpAlt selector picklers = PU { unpickleTree = doUnpickle, pickleTree = \value -> pickleTree (picklers !! selector value) value } where eitherResult (Result r t) = Right (Result r t) eitherResult (UnpickleError e) = Left e eitherResult (NoResult e) = Left . missing $ Text.unpack e splitResults v = partitionEithers $ map (eitherResult . flip unpickleTree v) picklers doUnpickle v = case splitResults v of (_, Result r t:_) -> Result r t (es, []) -> ("xpAlt", "") <++.> UnpickleError (Variants es) _ -> error "xpAlt: splitResults returned impossible result" -- | Execute one of a list of picklers. The /selector function/ is used during -- pickling, and the integer returned is taken as a 0-based index to select a -- pickler from /pickler options/. Unpickling is done by trying each list -- element in order until one returns a @Result@ or an @Error@. -- -- This is typically used to handle each constructor of a data type. However, -- it can be used wherever multiple serialization strategies apply to a single -- type. -- -- This function is similar to 'xpAlt' but it will stop unpickling on the first -- error. It will return @NoResult@ iff all of the picklers return @NoResult@ -- (or the list of picklers is empty). xpChoice :: (a -> Int) -- ^ Selector function -> [PU t a] -- ^ List of picklers -> PU t a xpChoice selector picklers = PU { unpickleTree = go picklers (1 :: Integer) , pickleTree = \value -> pickleTree (picklers !! selector value) value } where go [] _ _ = NoResult "entity" go (p:ps) i v = case unpickleTree p v of r@Result{} -> r UnpickleError e -> UnpickleError $ ("xpChoice", Text.pack $ show i) <++> e NoResult _ -> go ps (i+1) v -- | Try the left pickler first and if that doesn't produce anything the right -- one. Wrapping the result in @Left@ or @Right@, respectively. -- -- Not to be confused with 'xpWrapEither'. xpEither :: PU n t1 -> PU n t2 -> PU n (Either t1 t2) xpEither xpl xpr = PU { unpickleTree = doUnpickle, pickleTree = \v -> case v of Left l -> pickleTree xpl l Right r -> pickleTree xpr r } where doUnpickle t = case unpickleTree xpl t of Result r s -> Result (Left r) s NoResult e1 -> case unpickleTree xpr t of Result r s -> Result (Right r) s NoResult e2 -> UnpickleError $ ("xpEither","") <++> Variants [ missing $ Text.unpack e1 , missing $ Text.unpack e2 ] UnpickleError e -> UnpickleError $ ("xpEither","Right") <++> e UnpickleError e -> UnpickleError $ ("xpEither","Left") <++> e -- | Pickler that during pickling always uses the first pickler, and during -- unpickling tries the first, and on failure then tries the second. xpTryCatch :: PU t a -> PU t a -> PU t a xpTryCatch pu1 pu2 = PU { unpickleTree = \t -> case unpickleTree pu1 t of Result val1 rest -> Result val1 rest NoResult e1 -> case unpickleTree pu2 t of Result val2 rest -> Result val2 rest NoResult e2 -> NoResult $ Text.concat [e1, " / ", e2] UnpickleError e2 -> UnpickleError $ ("xpTryCatch","Right") <++> e2 UnpickleError e1 -> case unpickleTree pu2 t of Result val2 rest -> Result val2 rest NoResult e2 -> UnpickleError $ Variants [ e1 , upe $ " / not found:" ++ Text.unpack e2 ] UnpickleError e2 -> UnpickleError $ ("xpTryCatch","") <++> Variants [e1, e2] , pickleTree = pickleTree pu1 } -- | The zero pickler. -- -- Encodes nothing, always fails during unpickling. (Same as @'xpThrow' -- \"got xpZero\"@). xpZero :: PU [t] a xpZero = ("xpZero","") xpThrow "got xpZero" -- | No output when pickling, always generates an error with the specified -- message on unpickling. xpThrow :: Monoid m => String -- ^ Error message -> PU m a xpThrow msg = PU { unpickleTree = \_ -> UnpickleError $ ("xpThrow",Text.pack msg) <++> upe msg , pickleTree = const mempty } -- | Add an attribute with a fixed value. xpAddFixedAttr :: Name -> Text -> PU [Attribute] b -> PU [Attribute] b xpAddFixedAttr name val pa = xpWrap snd ((,) ()) $ xp2Tuple (xpAttrFixed name val) pa xpFst :: Monoid b => PU t (a, b) -> PU t a xpFst = xpWrap fst (\x -> (x, mempty)) xpSnd :: Monoid a => PU t (a, b) -> PU t b xpSnd = xpWrap snd (\y -> (mempty, y)) -- | Instead of failing the pickler will return no result. xpMayFail :: PU t a -> PU t a xpMayFail xp = PU { pickleTree = pickleTree xp , unpickleTree = \v -> case unpickleTree xp v of UnpickleError _ -> NoResult "failed with xpMayFail" x -> x } -- | Run unpickler and consume and discard remaining elements. -- -- When pickling, this is a noop. xpClean :: PU t a -> PU t a xpClean xp = PU { unpickleTree = \x -> case unpickleTree xp x of Result r _ -> Result r Nothing e -> e , pickleTree = pickleTree xp }