-- | -- Module : Amazonka.Data.XML -- Copyright : (c) 2013-2023 Brendan Hay -- License : Mozilla Public License, v. 2.0. -- Maintainer : Brendan Hay -- Stability : provisional -- Portability : non-portable (GHC extensions) module Amazonka.Data.XML where import Amazonka.Data.ByteString import Amazonka.Data.Text import Amazonka.Prelude import qualified Data.ByteString.Lazy as LBS import qualified Data.Conduit as Conduit import qualified Data.Conduit.Lazy as Conduit.Lazy import qualified Data.Conduit.List as Conduit.List import qualified Data.HashMap.Strict as HashMap import qualified Data.List.NonEmpty as NonEmpty import Data.XML.Types (Event (..)) import System.IO.Unsafe (unsafePerformIO) import Text.XML import qualified Text.XML.Stream.Render as XML.Stream import qualified Text.XML.Unresolved as XML.Unresolved 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 => ByteStringLazy -> Either String a decodeXML lbs = bimap show documentRoot (parseLBS def lbs) >>= parseXML . childrenOf -- The following is taken from xml-conduit.Text.XML which uses -- unsafePerformIO anyway, with the following caveat: -- 'not generally safe, but we know that runResourceT -- will not deallocate any of the resources being used -- by the process.' encodeXML :: ToElement a => a -> ByteStringLazy encodeXML x = LBS.fromChunks . unsafePerformIO . Conduit.Lazy.lazyConsume $ Conduit.List.sourceList (XML.Unresolved.toEvents doc) Conduit..| Conduit.List.map rename Conduit..| XML.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 -- | Convert to an 'Element', only if the resulting element contains @> 0@ nodes. maybeElement :: ToElement a => a -> Maybe Element maybeElement x = case toElement x of e@(Element _ _ ns) | null ns -> Nothing | otherwise -> Just e -- | Provides a way to make the operators for ToXML instance -- declaration be consistent WRT to single nodes or lists of nodes. data XML = XNull | XAttr Name Text | XOne Node | XMany [(Name, Text)] [Node] deriving stock (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 parseXMLMap :: (Eq k, Hashable k, FromText k, FromXML v) => Text -> Text -> Text -> [Node] -> Either String (HashMap k v) parseXMLMap e k v = fmap HashMap.fromList . traverse f . mapMaybe (childNodesOf e) where f ns = (,) <$> (ns .@ k >>= fromText) <*> ns .@ v parseXMLList1 :: FromXML a => Text -> [Node] -> Either String (NonEmpty a) parseXMLList1 n = parseXMLList n >=> parse where parse xs = maybe (Left $ "Error parsing empty List1 when expecting at least one element: " ++ show n) Right (NonEmpty.nonEmpty xs) 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 -- | Find a specific named NodeElement, at the current depth in the node tree. -- -- Fails if absent. findElement :: Text -> [Node] -> Either String [Node] findElement n ns = missingElement n ns . listToMaybe $ mapMaybe (childNodesOf n) ns -- | Find the first specific named NodeElement, at any depth in the node tree. -- -- Fails if absent. 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 -- | An inefficient mechanism for retreiving the root -- element name of an XML document. rootElementName :: ByteStringLazy -> 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)