{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}
module Network.AWS.Data.XML where
import Control.Monad
import Data.Bifunctor
import Data.Conduit
import Data.Conduit.Lazy (lazyConsume)
import Data.Maybe
import Data.Monoid (Monoid)
import Data.Semigroup (Semigroup, (<>))
import Data.Traversable (traverse)
import Data.XML.Types (Event (..))
import GHC.Exts
import Network.AWS.Data.ByteString
import Network.AWS.Data.Text
import Numeric.Natural
import System.IO.Unsafe (unsafePerformIO)
import Text.XML
import Text.XML.Unresolved (toEvents)
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Conduit.List as Conduit
import qualified Text.XML.Stream.Render as Stream
infixl 7 .@, .@?
(.@) :: FromXML a => [Node] -> Text -> Either String a
ns .@ n = findElement n ns >>= parseXML
(.@?) :: FromXML a => [Node] -> Text -> Either String (Maybe a)
ns .@? n =
case findElement n ns of
Left _ -> Right Nothing
Right xs -> parseXML xs
infixr 7 @=, @@=
(@=) :: ToXML a => Name -> a -> XML
n @= x =
case toXML x of
XNull -> XNull
xs -> XOne . NodeElement $ mkElement n xs
(@@=) :: ToText a => Name -> a -> XML
n @@= x = XAttr n (toText x)
decodeXML :: FromXML a => LazyByteString -> Either String a
decodeXML lbs =
bimap show documentRoot (parseLBS def lbs)
>>= parseXML . childrenOf
encodeXML :: ToElement a => a -> LazyByteString
encodeXML x = LBS.fromChunks . unsafePerformIO . lazyConsume
$ Conduit.sourceList (toEvents doc)
.| Conduit.map rename
.| Stream.renderBytes def
where
doc = toXMLDocument $ Document
{ documentRoot = root
, documentEpilogue = []
, documentPrologue =
Prologue
{ prologueBefore = []
, prologueDoctype = Nothing
, prologueAfter = []
}
}
rename = \case
EventBeginElement n xs -> EventBeginElement (f n) (map (first f) xs)
EventEndElement n -> EventEndElement (f n)
evt -> evt
where
f n | isNothing (nameNamespace n) = n { nameNamespace = ns }
| otherwise = n
ns = nameNamespace (elementName root)
root = toElement x
class FromXML a where
parseXML :: [Node] -> Either String a
instance FromXML [Node] where
parseXML = pure
instance FromXML a => FromXML (Maybe a) where
parseXML [] = pure Nothing
parseXML ns = Just <$> parseXML ns
instance FromXML Text where
parseXML = fmap (fromMaybe mempty) . withContent "Text"
instance FromXML Char where parseXML = parseXMLText "Char"
instance FromXML ByteString where parseXML = parseXMLText "ByteString"
instance FromXML Int where parseXML = parseXMLText "Int"
instance FromXML Integer where parseXML = parseXMLText "Integer"
instance FromXML Natural where parseXML = parseXMLText "Natural"
instance FromXML Double where parseXML = parseXMLText "Double"
instance FromXML Bool where parseXML = parseXMLText "Bool"
class ToElement a where
toElement :: a -> Element
instance ToElement Element where
toElement = id
maybeElement :: ToElement a => a -> Maybe Element
maybeElement x =
case toElement x of
e@(Element _ _ ns)
| null ns -> Nothing
| otherwise -> Just e
data XML
= XNull
| XAttr Name Text
| XOne Node
| XMany [(Name, Text)] [Node]
deriving (Show)
instance Semigroup XML where
XNull <> XNull = XNull
a <> XNull = a
XNull <> b = b
a <> b =
XMany (listXMLAttributes a <> listXMLAttributes b)
(listXMLNodes a <> listXMLNodes b)
instance Monoid XML where
mempty = XNull
mappend = (<>)
listXMLNodes :: XML -> [Node]
listXMLNodes = \case
XNull -> []
XAttr {} -> []
XOne n -> [n]
XMany _ ns -> ns
listXMLAttributes :: XML -> [(Name, Text)]
listXMLAttributes = \case
XNull -> []
XAttr n t -> [(n, t)]
XOne {} -> []
XMany as _ -> as
class ToXML a where
toXML :: a -> XML
instance ToXML XML where
toXML = id
instance ToXML a => ToXML (Maybe a) where
toXML (Just x) = toXML x
toXML Nothing = XNull
instance ToXML Text where toXML = toXMLText
instance ToXML ByteString where toXML = toXMLText
instance ToXML Int where toXML = toXMLText
instance ToXML Integer where toXML = toXMLText
instance ToXML Natural where toXML = toXMLText
instance ToXML Double where toXML = toXMLText
instance ToXML Bool where toXML = toXMLText
parseXMLList :: FromXML a
=> Text
-> [Node]
-> Either String [a]
parseXMLList n = traverse parseXML . mapMaybe (childNodesOf n)
parseXMLText :: FromText a => String -> [Node] -> Either String a
parseXMLText n = withContent n >=>
maybe (Left $ "empty node list, when expecting single node " ++ n)
fromText
toXMLList :: (IsList a, ToXML (Item a)) => Name -> a -> XML
toXMLList n = XMany [] . map (NodeElement . mkElement n) . toList
toXMLText :: ToText a => a -> XML
toXMLText = XOne . NodeContent . toText
mkElement :: ToXML a => Name -> a -> Element
mkElement n (toXML -> x) =
Element n (fromList (listXMLAttributes x)) (listXMLNodes x)
withContent :: String -> [Node] -> Either String (Maybe Text)
withContent k = \case
[] -> Right Nothing
[NodeContent x] -> Right (Just x)
_ -> Left $ "encountered many nodes, when expecting text: " ++ k
findElement :: Text -> [Node] -> Either String [Node]
findElement n ns =
missingElement n ns
. listToMaybe
$ mapMaybe (childNodesOf n) ns
firstElement :: Text -> [Node] -> Either String [Node]
firstElement n ns =
missingElement n ns
. listToMaybe
$ mapMaybe go ns
where
go x = case x of
NodeElement e
| Just n == localName x -> Just (childrenOf e)
| otherwise -> listToMaybe (mapMaybe go (elementNodes e))
_ -> Nothing
childNodesOf :: Text -> Node -> Maybe [Node]
childNodesOf n x = case x of
NodeElement e
| Just n == localName x
-> Just (childrenOf e)
_ -> Nothing
childrenOf :: Element -> [Node]
childrenOf e = elementNodes e <> map node (toList (elementAttributes e))
where
node (k, v) = NodeElement (Element (name k) mempty [NodeContent v])
name k = Name
{ nameLocalName = fromMaybe "" (namePrefix k) <> ":" <> nameLocalName k
, nameNamespace = mempty
, namePrefix = mempty
}
localName :: Node -> Maybe Text
localName = \case
NodeElement e -> Just (nameLocalName (elementName e))
_ -> Nothing
rootElementName :: LazyByteString -> Maybe Text
rootElementName bs =
either (const Nothing)
(Just . nameLocalName . elementName . documentRoot)
(parseLBS def bs)
missingElement :: Text -> [Node] -> Maybe a -> Either String a
missingElement n ns = maybe (Left err) Right
where
err = "unable to find element "
++ show n
++ " in nodes "
++ show (mapMaybe localName ns)