module Text.XML.TyDom.Conduit
(
ToElem (toElem)
, FromElem (fromElem)
, ToXText (toXText)
, FromXText (fromXText)
, Conv (conv)
, Attr (Attr)
, Child (Child)
, Content (Content)
, AttrName (AttrName)
, ElemName (ElemName)
, XTextError (XTextError)
, Result (Success, Failure)
, Path (PathItem, PathRoot)
, Cause (Cause, Leftover, WrongElementName, MissingAttribute,
FailParseAttribute, FailParseContent, FailParseCData,
MissingElement, NoMoreElements, MissingContent,
MissingCData, NoSuccessfulChildren)
, OptionsElement(OptionsElement)
, ReadNodeOrdering(All, Sequence)
, ReadLeftovers(LeftoversError, LeftoversOK)
, genericToElem
, genericFromElem
, genericConv
, unAttr
, unChild
, unContent
, unAttrName
, unElemName
, expectedElementName
, actualElementName
, optConstructorElemName
, optSelectorElemName
, optAttrName
, optReadNodeOrdering
, optReadLeftovers
, defaultOptionsElement
, render
, renderFailure
, parse
, ignoreWSContent
, getAttrValue
, xTextErrType
) where
import GHC.Generics (Generic, Rep)
import qualified Data.Char as C (isSpace)
import Data.List (break, partition)
import qualified Data.Map as Map (delete, empty, insert, lookup, null)
import Data.Maybe (catMaybes, fromMaybe, mapMaybe)
import Data.Monoid ((<>))
import Data.Text (Text)
import qualified Data.Text as Text (all, concat, empty, null, pack,
unpack, unlines, lines, intercalate)
import qualified Data.Text.Lazy as Text (fromStrict, toStrict)
import qualified Data.Time.Calendar as Time (Day, showGregorian)
import Text.Read (readMaybe)
import qualified Text.XML as XML (Document (..), Element (..), Name (..),
Node (..), Prologue (..), RenderSettings, def,
documentRoot, elementAttributes, elementName,
elementNodes, parseText, renderText)
import Text.XML.TyDom.Core.Generics (ReadNodeOrdering (..),
ReadLeftovers (..),
genericConv)
import qualified Text.XML.TyDom.Core.Generics as G (GFromElem, GToElem,
OptionsElement (..),
genericFromElem,
genericToElem,
optAttrName,
optConstructorElemName,
optReadNodeOrdering,
optReadLeftovers,
optSelectorElemName)
import Text.XML.TyDom.Core.Types (Attr (..), Child (..),
Content (..), Conv, conv,
unAttr, unChild, unContent,
XTextError (..),
xTextErrType)
import qualified Text.XML.TyDom.Core.Types as T (Cause (..), FromElem,
FromXText, Path (..),
Result (..), ToElem,
ToXText, fromElem,
fromXText, toElem,
toXText)
import Text.XML.TyDom.Core.XMLInterface (Compose (..), Decompose (..),
cAttr, cCData, cChild,
cContent, cEmpty, cFreeze,
cName, cNull, cThaw,
dAllCData, dAllContent,
dAllNextCData, dAttr, dEmpty,
dEmptyTxt, dFreeze, dName,
dNextCData, dNextChildNamed,
dNextContent, dNextSeqCData,
dNextSeqChild,
dNextSeqContent, dNull,
dRename, dSuccessChild,
dSuccessChildren,
dSuccessNextChildren, dThaw)
import qualified Text.XML.TyDom.Core.XMLInterface as XMLi (Result (..))
newtype AttrName = AttrName { unAttrName :: XML.Name } deriving (Eq, Show)
newtype ElemName = ElemName { unElemName :: XML.Name } deriving (Eq, Show)
class ToElem a where
toElem :: a -> XML.Element
class FromElem a where
fromElem :: XML.Element -> Result a
class ToXText a where
toXText :: a -> Text
class FromXText a where
fromXText :: Text -> Either XTextError a
instance ToXText a => T.ToXText Text a where
toXText = toXText
instance FromXText a => T.FromXText Text a where
fromXText = fromXText
instance ToElem a => T.ToElem XML.Element a where
toElem = toElem
instance FromElem a => T.FromElem XML.Element ElemName AttrName Text a where
fromElem = toTyResult . fromElem
data Result a = Success a
| Failure Path Cause
deriving (Eq, Show, Functor)
fromTyResult :: T.Result XML.Element ElemName AttrName Text a -> Result a
fromTyResult (T.Success x) = Success x
fromTyResult (T.Failure p c) = Failure (fromTyPath p) (fromTyCause c)
toTyResult :: Result a -> T.Result XML.Element ElemName AttrName Text a
toTyResult (Success x) = T.Success x
toTyResult (Failure p c) = T.Failure (toTyPath p) (toTyCause c)
instance Applicative Result where
pure = Success
Success f <*> Success a = Success (f a)
_ <*> Failure p c = Failure p c
Failure p c <*> _ = Failure p c
instance Monad Result where
Success x >>= f = f x
Failure p c >>= _ = Failure p c
data Path = PathItem ElemName Path
| PathRoot
deriving (Eq, Show)
fromTyPath :: T.Path ElemName -> Path
fromTyPath (T.PathItem n p) = PathItem n (fromTyPath p)
fromTyPath T.PathRoot = PathRoot
toTyPath :: Path -> T.Path ElemName
toTyPath (PathItem n p) = T.PathItem n (toTyPath p)
toTyPath PathRoot = T.PathRoot
data Cause
= Cause Text
| Leftover XML.Element
| WrongElementName
{ expectedElementName :: ElemName
, actualElementName :: ElemName }
| MissingAttribute AttrName
| FailParseAttribute AttrName XTextError
| FailParseContent XTextError
| FailParseCData XTextError
| MissingElement ElemName
| NoMoreElements
| MissingContent
| MissingCData
| NoSuccessfulChildren
deriving (Eq, Show)
fromTyCause :: T.Cause XML.Element ElemName AttrName Text -> Cause
fromTyCause (T.Cause t) = Cause t
fromTyCause (T.Leftover e) = Leftover e
fromTyCause (T.WrongElementName e a) = WrongElementName e a
fromTyCause (T.MissingAttribute n) = MissingAttribute n
fromTyCause (T.FailParseAttribute n e) = FailParseAttribute n e
fromTyCause (T.FailParseContent e) = FailParseContent e
fromTyCause (T.FailParseCData e) = FailParseCData e
fromTyCause (T.MissingElement n) = MissingElement n
fromTyCause T.NoMoreElements = NoMoreElements
fromTyCause T.MissingContent = MissingContent
fromTyCause T.MissingCData = MissingCData
fromTyCause T.NoSuccessfulChildren = NoSuccessfulChildren
toTyCause :: Cause -> T.Cause XML.Element ElemName AttrName Text
toTyCause (Cause t) = T.Cause t
toTyCause (Leftover e) = T.Leftover e
toTyCause (WrongElementName e a) = T.WrongElementName e a
toTyCause (MissingAttribute n) = T.MissingAttribute n
toTyCause (FailParseAttribute n e) = T.FailParseAttribute n e
toTyCause (FailParseContent e) = T.FailParseContent e
toTyCause (FailParseCData e) = T.FailParseCData e
toTyCause (MissingElement n) = T.MissingElement n
toTyCause NoMoreElements = T.NoMoreElements
toTyCause MissingContent = T.MissingContent
toTyCause MissingCData = T.MissingCData
toTyCause NoSuccessfulChildren = T.NoSuccessfulChildren
instance ToXText Text where toXText = id
instance FromXText Text where fromXText = Right
instance ToXText Int where toXText = Text.pack . show
instance FromXText Int where
fromXText t = case (readMaybe . Text.unpack) t of
Just i -> Right i
Nothing -> xTextErrType "Int" t
instance ToXText Time.Day where toXText = Text.pack . Time.showGregorian
instance FromXText Time.Day where
fromXText t = case (readMaybe . Text.unpack) t of
Just i -> Right i
Nothing -> xTextErrType "Day" t
render :: XML.Element -> Text
render e =
let
rs = XML.def :: XML.RenderSettings
prologue = XML.Prologue [] Nothing []
document = XML.Document prologue e []
in
Text.toStrict $ XML.renderText rs document
parse :: Text -> Result XML.Element
parse t = case XML.parseText XML.def (Text.fromStrict t) of
Left ex -> Failure PathRoot (Cause ((Text.pack . show) ex))
Right doc -> Success (XML.documentRoot doc)
ignoreWSContent :: XML.Element -> XML.Element
ignoreWSContent e =
let
f (XML.NodeElement c) = Just $ XML.NodeElement (ignoreWSContent c)
f c@(XML.NodeContent txt) = if Text.all C.isSpace txt
then Nothing
else Just c
f n = Just n
nodes' = mapMaybe f (XML.elementNodes e)
in
e { XML.elementNodes = nodes' }
getAttrValue :: (FromXText a) => AttrName -> XML.Element -> Result a
getAttrValue an@(AttrName n) e = case Map.lookup n (XML.elementAttributes e) of
Nothing -> Failure (PathItem (ElemName (XML.elementName e)) PathRoot)
(MissingAttribute an)
Just text -> case fromXText text of
Left err -> Failure (PathItem (ElemName (XML.elementName e)) PathRoot)
(FailParseAttribute an err)
Right a -> Success a
data OptionsElement = OptionsElement
{ optConstructorElemName :: Text -> ElemName
, optSelectorElemName :: Text -> ElemName
, optAttrName :: Text -> AttrName
, optReadNodeOrdering :: ReadNodeOrdering
, optReadLeftovers :: ReadLeftovers }
defaultOptionsElement :: OptionsElement
defaultOptionsElement = OptionsElement
{ optConstructorElemName = \t -> ElemName (XML.Name t Nothing Nothing)
, optSelectorElemName = \t -> ElemName (XML.Name t Nothing Nothing)
, optAttrName = \t -> AttrName (XML.Name t Nothing Nothing)
, optReadNodeOrdering = Sequence
, optReadLeftovers = LeftoversError }
optionsElementTy :: OptionsElement -> G.OptionsElement ElemName AttrName
optionsElementTy o = G.OptionsElement
{ G.optConstructorElemName = optConstructorElemName o
, G.optSelectorElemName = optSelectorElemName o
, G.optAttrName = optAttrName o
, G.optReadNodeOrdering = optReadNodeOrdering o
, G.optReadLeftovers = optReadLeftovers o }
genericFromElem :: (Generic z,
G.GFromElem XML.Element ElemName AttrName Text (Rep z))
=> OptionsElement
-> XML.Element
-> Result z
genericFromElem o e = fromTyResult
$ G.genericFromElem
(optionsElementTy o) decomposeConduit e
genericToElem :: (Generic z,
G.GToElem XML.Element ElemName AttrName Text (Rep z))
=> OptionsElement
-> z
-> XML.Element
genericToElem o z = G.genericToElem (optionsElementTy o) composeConduit z
composeConduit :: Compose XML.Element ElemName AttrName Text XML.Element
composeConduit = Compose
{ cEmpty = ccEmpty
, cName = ccName
, cAttr = ccAttr
, cChild = ccChild
, cContent = ccContent
, cCData = ccCData
, cFreeze = ccFreeze
, cThaw = ccThaw
, cNull = ccNull
}
decomposeConduit :: Decompose XML.Element ElemName AttrName Text XML.Element
decomposeConduit = Decompose
{ dThaw = cdThaw
, dFreeze = cdFreeze
, dEmpty = cdEmpty
, dNull = cdNull
, dName = cdName
, dRename = cdRename
, dAttr = cdAttr
, dNextSeqChild = cdNextSeqChild
, dNextSeqContent = cdNextSeqContent
, dNextSeqCData = cdNextSeqCData
, dNextChildNamed = cdNextChildNamed
, dNextContent = cdNextContent
, dNextCData = cdNextCData
, dSuccessChild = cdSuccessChild
, dSuccessNextChildren = cdSuccessNextChildren
, dSuccessChildren = cdSuccessChildren
, dAllContent = cdAllContent
, dAllNextCData = cdAllNextCData
, dAllCData = cdAllCData
, dEmptyTxt = cdEmptyTxt
}
ccEmpty :: XML.Element
ccEmpty = XML.Element (XML.Name "" Nothing Nothing) Map.empty []
ccName :: ElemName -> XML.Element -> XML.Element
ccName name e = e { XML.elementName = unElemName name }
ccAttr :: AttrName -> Text -> XML.Element -> XML.Element
ccAttr name value e =
let
attr = XML.elementAttributes e
attr' = Map.insert (unAttrName name) value attr
in
e { XML.elementAttributes = attr' }
ccChild :: XML.Element -> XML.Element -> XML.Element
ccChild child parent =
let
nodes' = XML.NodeElement child : XML.elementNodes parent
in
parent { XML.elementNodes = nodes' }
ccContent :: Text -> XML.Element -> XML.Element
ccContent text e =
let
nodes' = XML.NodeContent text : XML.elementNodes e
in
e { XML.elementNodes = nodes' }
ccCData :: Text -> XML.Element -> XML.Element
ccCData = error "CDATA not yet implemented for xml-conduit"
ccFreeze :: XML.Element -> XML.Element
ccFreeze e =
let
nodes' = reverse (XML.elementNodes e)
in
e { XML.elementNodes = nodes' }
ccThaw :: XML.Element -> XML.Element
ccThaw e =
let
nodes' = reverse (XML.elementNodes e)
in
e { XML.elementNodes = nodes' }
ccNull :: Text -> Bool
ccNull = Text.null
cdThaw :: XML.Element -> XML.Element
cdThaw = id
cdFreeze :: XML.Element -> XML.Element
cdFreeze = id
cdEmpty :: XML.Element
cdEmpty = XML.Element (XML.Name "" Nothing Nothing) Map.empty []
cdNull :: XML.Element -> Bool
cdNull e = null (XML.elementNodes e) && Map.null (XML.elementAttributes e)
cdName :: XML.Element -> ElemName
cdName e = ElemName (XML.elementName e)
cdRename :: ElemName -> XML.Element -> XML.Element
cdRename (ElemName n) e = e { XML.elementName = n }
cdAttr :: AttrName -> XML.Element -> XMLi.Result XML.Element Text
cdAttr name e =
let
txtName = unAttrName name
attrs = XML.elementAttributes e
attr = Map.lookup txtName attrs
attrs' = Map.delete txtName attrs
e' = e { XML.elementAttributes = attrs' }
in
fromMaybe XMLi.Failure (XMLi.Success e' <$> attr)
cdNextSeqChild :: XML.Element -> XMLi.Result XML.Element XML.Element
cdNextSeqChild e = case XML.elementNodes e of
XML.NodeElement child : ns' ->
let
e' = e { XML.elementNodes = ns' }
in
XMLi.Success e' child
_ -> XMLi.Failure
cdNextSeqContent :: XML.Element -> XMLi.Result XML.Element Text
cdNextSeqContent e = case XML.elementNodes e of
XML.NodeContent child : ns' ->
let
e' = e { XML.elementNodes = ns' }
in
XMLi.Success e' child
_ -> XMLi.Failure
cdNextSeqCData :: XML.Element -> XMLi.Result XML.Element Text
cdNextSeqCData = error "dNextSeqCData not supported for xml-conduit"
cdNextChildNamed :: ElemName
-> XML.Element
-> XMLi.Result XML.Element XML.Element
cdNextChildNamed (ElemName name) e =
let
f (XML.NodeElement child) | XML.elementName child == name = True
f _ = False
(before, at) = break f (XML.elementNodes e)
in case at of
XML.NodeElement child : ns' ->
let
e' = e { XML.elementNodes = before <> ns' }
in
XMLi.Success e' child
_ -> XMLi.Failure
cdNextContent :: XML.Element -> XMLi.Result XML.Element Text
cdNextContent e =
let
f (XML.NodeContent _) = True
f _ = False
(before, at) = break f (XML.elementNodes e)
in case at of
XML.NodeContent text : ns' ->
let
e' = e { XML.elementNodes = before <> ns' }
in
XMLi.Success e' text
_ -> XMLi.Failure
cdNextCData :: XML.Element -> XMLi.Result XML.Element Text
cdNextCData = error "dNextCData not yet implemented for xml-conduit"
cdSuccessChild :: XML.Element
-> (XML.Element -> Maybe a)
-> XMLi.Result XML.Element a
cdSuccessChild e f =
let
g node@(XML.NodeElement child) = (node, f child)
g node = (node, Nothing)
h (_, Just _) = True
h _ = False
(before, at) = break h $ g <$> XML.elementNodes e
in case at of
(_, Just r) : ns' ->
let
e' = e { XML.elementNodes = fst <$> (before <> ns') }
in
XMLi.Success e' r
_ -> XMLi.Failure
cdSuccessNextChildren :: XML.Element
-> (XML.Element -> Maybe a)
-> (XML.Element, [a])
cdSuccessNextChildren e f =
let
g node@(XML.NodeElement child) = (node, f child)
g node = (node, Nothing)
h (_, Just _) = True
h _ = False
(at, after) = span h $ g <$> XML.elementNodes e
as = catMaybes $ snd <$> at
e' = e { XML.elementNodes = fst <$> after }
in
(e', as)
cdSuccessChildren :: XML.Element
-> (XML.Element -> Maybe a)
-> (XML.Element, [a])
cdSuccessChildren e f =
let
g node@(XML.NodeElement child) = (node, f child)
g node = (node, Nothing)
h (_, Just _) = True
h _ = False
(succeeded, others) = partition h $ g <$> XML.elementNodes e
as = catMaybes $ snd <$> succeeded
e' = e { XML.elementNodes = fst <$> others }
in
(e', as)
cdAllContent :: XML.Element -> XMLi.Result XML.Element Text
cdAllContent e =
let
g (XML.NodeContent _) = True
g _ = False
(contents, others) = partition g $ XML.elementNodes e
textFn (XML.NodeContent t) = Just t
textFn _ = Nothing
text = Text.concat $ mapMaybe textFn contents
e' = e { XML.elementNodes = others }
in
XMLi.Success e' text
cdAllNextCData :: XML.Element -> (XML.Element, [Text])
cdAllNextCData = error "dAllNextCData not yet implemented for xml-conduit."
cdAllCData :: XML.Element -> (XML.Element, [Text])
cdAllCData = error "dAllCData not yet implemented for xml-conduit."
cdEmptyTxt :: Text
cdEmptyTxt = Text.empty
pathToList :: Path -> [ElemName]
pathToList = reverse . pathToList'
where
pathToList' PathRoot = []
pathToList' (PathItem name path') = name : pathToList' path'
renderPath :: Path -> Text
renderPath path =
let
names = (XML.nameLocalName . unElemName) <$> (pathToList path)
in
if null names
then "(No Path)"
else Text.concat [ "Path: ", Text.intercalate " -> " names ]
causeToText :: Cause -> Text
causeToText (Cause t) = t
causeToText (Leftover el) = Text.unlines
[ "The following element components were left-over after reading:"
, Text.unlines $ drop 1 $ Text.lines (render el) ]
causeToText (WrongElementName expected actual) = Text.concat
[ "Expected element ["
, XML.nameLocalName $ unElemName expected
, "] but encountered ["
, XML.nameLocalName $ unElemName actual
, "]"]
causeToText (MissingAttribute attrName) = Text.concat
[ "Missing attribute [", XML.nameLocalName $ unAttrName attrName, "]" ]
causeToText (FailParseAttribute attrName xTextError) = Text.concat
[ "Bad attribute ["
, XML.nameLocalName $ unAttrName attrName
, "]: "
, unXTextError xTextError ]
causeToText (FailParseContent xTextError) = Text.concat
[ "Bad content node: ", unXTextError xTextError ]
causeToText (FailParseCData xTextError) = Text.concat
[ "Bad CDATA node: ", unXTextError xTextError ]
causeToText (MissingElement elemName) = Text.concat
[ "Could not locate element ["
, XML.nameLocalName $ unElemName elemName
, "]" ]
causeToText NoMoreElements = "Expected more elements"
causeToText MissingContent = "Expected a content node"
causeToText MissingCData = "Expected a CDATA node"
causeToText NoSuccessfulChildren =
"Did not read any child elements successfully"
renderFailure :: Result a -> Text
renderFailure (Success _) = "Well, this is embarrassing - not a failure!"
renderFailure (Failure path cause) =
Text.unlines [ renderPath path, causeToText cause ]