git.iko.soy/typeable/dom-parser
on master help sign in
17 files changed+879−278
since splitsnapshots
unifiedsplit
.gitignore+2 .travis.yml+32 CHANGELOG.md+50 README.md+5 dom-parser.cabal+23−9 ghc-963.yaml+7 src/Text/XML/DOM/Parser.hs+10−2 src/Text/XML/DOM/Parser/Attributes.hs+64 src/Text/XML/DOM/Parser/Buildable.hs+26 src/Text/XML/DOM/Parser/Combinators.hs+61−98 src/Text/XML/DOM/Parser/Content.hs+129 src/Text/XML/DOM/Parser/FromAttribute.hs+112 src/Text/XML/DOM/Parser/Class.hs → src/Text/XML/DOM/Parser/FromDom.hs+56−80 src/Text/XML/DOM/Parser/Types.hs+159−60 src/Text/XML/Soap.hs+46 stack.yaml+1 test/Main.hs+96−29
.gitignorenew file+2
@@ -0,0 +1,2 @@
1+TAGS
2+.stack-work
.travis.ymlnew file+32
@@ -0,0 +1,32 @@
1+# Use new container infrastructure to enable caching
2+sudo: false
3+
4+# Do not choose a language; we provide our own build tools.
5+language: generic
6+
7+env:
8+ - STACK_YAML=stack.yaml
9+ - STACK_YAML=ghc-802.yaml
10+ - STACK_YAML=ghc-822.yaml
11+ - STACK_YAML=ghc-841.yaml
12+
13+# Caching so the next build will be fast too.
14+cache:
15+ directories:
16+ - $HOME/.stack
17+
18+# Ensure necessary system libraries are present
19+addons:
20+ apt:
21+ packages:
22+ - libgmp-dev
23+
24+before_install:
25+# Download and unpack the stack executable
26+- mkdir -p ~/.local/bin
27+- export PATH=$HOME/.local/bin:$PATH
28+- travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack'
29+
30+script:
31+# Build the package, its tests, and its docs and run the tests
32+- stack --no-terminal --install-ghc test --haddock --no-haddock-deps
CHANGELOG.md+50
@@ -1,4 +1,54 @@
11# CHANGELOG
22
3+## HEAD
4+
5+## 3.2.0
6+
7+### Added
8+
9+* Text.XML.Soap module with XmlEnvelope stuff in it.
10+* GHC 9 compatibility.
11+
12+### Removed
13+
14+* Old stackage files.
15+
16+## 3.1.0
17+
18+### Added
19+
20+* Additional builds with GHC 8.0.2 and 8.2.1
21+* Compatibility with GHC 8.4.1
22+
23+## 3.0.0
24+
25+* GHC 8 compatible release
26+
27+### Removed
28+
29+* `open-union` dependency
30+
31+## 2.0.0
32+
33+### Added
34+
35+* `NameMatcher` and `ElemMatcher` as more flexible mechanisms for
36+ matching names and elements instead of just matching by name.
37+* Class `FromAttribute` and some default instances
38+
39+### Changed
40+
41+* `DomTraversable` renamed to `Buildable`
42+* Combinators use `NameMatcher` and `ElemMatcher`
43+* Documentation improved
44+
45+## 1.0.0
46+
47+* Added support for parsing attributes
48+* Changed public interface for 'parseContent'
49+* Added useful functions
50+* Dropped unnecessary dependency from shakespeare
51+
352## 0.1.0
53+
454* Public release
README.mdnew file+5
@@ -0,0 +1,5 @@
1+# dom-parser
2+
3+[![Build Status](https://travis-ci.org/typeable/dom-parser.svg?branch=master)](https://travis-ci.org/typeable/dom-parser)
4+
5+Simple monadic DOM parser
dom-parser.cabal+23−9
@@ -1,5 +1,5 @@
11name: dom-parser
2−version: 0.1.0
2+version: 3.2.0
33synopsis: Simple monadic DOM parser
44license: MIT
55license-file: LICENSE
@@ -7,8 +7,17 @@ author: Typeable.io contributors
77maintainer: makeit@typeable.io
88category: XML
99build-type: Simple
10−extra-source-files: CHANGELOG.md
1110cabal-version: >=1.10
11+homepage: https://github.com/typeable/dom-parser
12+tested-with: GHC == 9.2.7
13+ , GHC == 9.4.8
14+ , GHC == 9.6.3
15+extra-source-files: CHANGELOG.md
16+ , README.md
17+
18+source-repository head
19+ type: git
20+ location: git@github.com:typeable/dom-parser.git
1221
1322library
1423 hs-source-dirs: src
@@ -35,21 +44,26 @@ library
3544 , TypeFamilies
3645 , TypeOperators
3746 , UndecidableInstances
38− build-depends: base >= 4.7 && < 5
47+ build-depends: base >= 4.11 && < 5
48+ , case-insensitive
49+ , containers
3950 , lens
40− , mtl
41− , open-union
42− , semigroups
43− , shakespeare
51+ , mtl < 2.3 || > 2.3
52+ , scientific
4453 , text
4554 , transformers
46− , type-fun
4755 , xml-conduit
56+ , xml-conduit-writer
4857 , xml-lens
4958 exposed-modules: Text.XML.DOM.Parser
50− , Text.XML.DOM.Parser.Class
59+ , Text.XML.DOM.Parser.Attributes
60+ , Text.XML.DOM.Parser.Buildable
5161 , Text.XML.DOM.Parser.Combinators
62+ , Text.XML.DOM.Parser.Content
63+ , Text.XML.DOM.Parser.FromAttribute
64+ , Text.XML.DOM.Parser.FromDom
5265 , Text.XML.DOM.Parser.Types
66+ , Text.XML.Soap
5367
5468test-suite test
5569 type: exitcode-stdio-1.0
ghc-963.yamlnew file+7
@@ -0,0 +1,7 @@
1+resolver: lts-22.6
2+
3+extra-deps:
4+ - xml-conduit-writer-0.1.1.5
5+
6+nix:
7+ packages: [zlib]
src/Text/XML/DOM/Parser.hs+10−2
@@ -1,9 +1,17 @@
11module Text.XML.DOM.Parser
2− ( module Text.XML.DOM.Parser.Class
2+ ( module Text.XML.DOM.Parser.Attributes
3+ , module Text.XML.DOM.Parser.Buildable
34 , module Text.XML.DOM.Parser.Combinators
5+ , module Text.XML.DOM.Parser.Content
6+ , module Text.XML.DOM.Parser.FromAttribute
7+ , module Text.XML.DOM.Parser.FromDom
48 , module Text.XML.DOM.Parser.Types
59 ) where
610
7−import Text.XML.DOM.Parser.Class
11+import Text.XML.DOM.Parser.Attributes
12+import Text.XML.DOM.Parser.Buildable
813import Text.XML.DOM.Parser.Combinators
14+import Text.XML.DOM.Parser.Content
15+import Text.XML.DOM.Parser.FromAttribute
16+import Text.XML.DOM.Parser.FromDom
917import Text.XML.DOM.Parser.Types
src/Text/XML/DOM/Parser/Attributes.hsnew file+64
@@ -0,0 +1,64 @@
1+module Text.XML.DOM.Parser.Attributes
2+ ( parseAttribute
3+ , parseAttributeMaybe
4+ , getCurrentAttributes
5+ , getCurrentAttribute
6+ ) where
7+
8+import Control.Lens
9+import Data.Map.Strict as M
10+import Data.Text as T
11+import Text.XML.DOM.Parser.Types
12+import Text.XML.Lens
13+
14+-- | Parses attribute with given name, throws error if attribute is not found.
15+--
16+-- @since 1.0.0
17+parseAttribute
18+ :: (Monad m)
19+ => NameMatcher
20+ -- ^ Attribute name
21+ -> (Text -> Either Text a)
22+ -- ^ Attribute content parser
23+ -> DomParserT Identity m a
24+parseAttribute attrName parser =
25+ parseAttributeMaybe attrName parser >>= \case
26+ Nothing -> throwParserError $ PEAttributeNotFound attrName
27+ Just a -> return a
28+
29+-- | Parses attribute with given name. Returns Nothing if attribute is
30+-- not found.
31+--
32+-- @since 1.0.0
33+parseAttributeMaybe
34+ :: (Monad m)
35+ => NameMatcher
36+ -- ^ Attribute name
37+ -> (Text -> Either Text a)
38+ -- ^ Attribute content parser
39+ -> DomParserT Identity m (Maybe a)
40+parseAttributeMaybe attrName parser =
41+ getCurrentAttribute attrName >>= \case
42+ Nothing -> return Nothing
43+ Just aval -> case parser aval of
44+ Left err -> throwParserError $ PEAttributeWrongFormat attrName err
45+ Right a -> return $ Just a
46+
47+-- | Retuns map of attributes of current element
48+--
49+-- @since 1.0.0
50+getCurrentAttributes
51+ :: (Monad m)
52+ => DomParserT Identity m (M.Map Name Text)
53+getCurrentAttributes = view $ pdElements . to runIdentity . attrs
54+
55+-- | Returns element with given name or 'Nothing'
56+--
57+-- @since 1.0.0
58+getCurrentAttribute
59+ :: (Monad m)
60+ => NameMatcher
61+ -> DomParserT Identity m (Maybe Text)
62+getCurrentAttribute attrName
63+ = preview $ pdElements . to runIdentity . attrs . to M.toList
64+ . traversed . filtered (views _1 (attrName ^. nmMatch)) . _2
src/Text/XML/DOM/Parser/Buildable.hsnew file+26
@@ -0,0 +1,26 @@
1+module Text.XML.DOM.Parser.Buildable
2+ ( Buildable(..)
3+ ) where
4+
5+import Data.Functor.Identity
6+import Data.List.NonEmpty as NE
7+import Data.Maybe
8+
9+-- | Class of traversable functors which may be constructed from list
10+class Traversable f => Buildable f where
11+ -- | If method return Nothing this means we can not build
12+ -- traversable from given list. In this case 'inFilteredTrav' should
13+ -- fail traversing.
14+ build :: [a] -> Maybe (f a)
15+
16+instance Buildable Identity where
17+ build = fmap Identity . listToMaybe
18+
19+instance Buildable [] where
20+ build = Just
21+
22+instance Buildable Maybe where
23+ build = Just . listToMaybe
24+
25+instance Buildable NonEmpty where
26+ build = NE.nonEmpty
src/Text/XML/DOM/Parser/Combinators.hs+61−98
@@ -2,7 +2,7 @@ module Text.XML.DOM.Parser.Combinators
22 ( -- * Generic combinators to traverse descendants
33 traverseElems
44 , inFilteredTrav
5− -- * Using 'DomTraversable'
5+ -- * Using 'Buildable'
66 , inElemTrav
77 , inElem
88 , inElemAll
@@ -15,105 +15,106 @@ module Text.XML.DOM.Parser.Combinators
1515 , ignoreElem
1616 , ignoreEmpty
1717 , ignoreBlank
18− -- * Checking current element properties
19− , checkCurrentName
20− -- * Parsing arbitrary content
21− , parseContent
22− , readContent
2318 ) where
2419
25−import Control.Lens
26−import Control.Monad.Except
27−import Control.Monad.Reader
28−import Data.List.NonEmpty (NonEmpty(..))
29−import Data.Monoid
30−import Data.Text (Text)
31−import qualified Data.Text as T
32−import Data.Traversable
33−import Data.Typeable
34−import Text.Read
35−import Text.Shakespeare.Text (st)
36−import Text.XML
37−import Text.XML.DOM.Parser.Types
38−import Text.XML.Lens
39−
40−
41−-- | Generic function to traverse arbitrary inner cursors.
20+import Control.Lens
21+import Control.Monad.Reader
22+import Data.Foldable as F
23+import Data.List as L
24+import Data.List.NonEmpty (NonEmpty(..))
25+import Data.Text as T
26+import Data.Traversable
27+import Text.XML
28+import Text.XML.DOM.Parser.Buildable
29+import Text.XML.DOM.Parser.Types
30+import Text.XML.Lens
31+
32+
33+-- | Generic function to traverse arbitrary inner elements.
4234traverseElems
4335 :: (Monad m, Foldable g, Traversable f)
44− => ([Element] -> DomParserT g m (f ([Text], Element)))
45− -- ^ Takes set of current elements and
36+ => ([Element] -> DomParserT g m (f (DomPath, Element)))
37+ -- ^ Takes list of current elements and returns container with
38+ -- pairs of subpath (relatively to current elements) and element
39+ -- to run parser in
4640 -> DomParserT Identity m a
47− -- ^ Parser will be runned for each element found in traversable
41+ -- ^ Parser to run for each element found in traversable 'f'
4842 -> DomParserT g m (f a)
4943traverseElems trav parser = do
5044 pd <- ask
5145 inner <- trav $ pd ^.. pdElements . folded
5246 for inner $ \(subpath, e) -> do
53− let newpd = ParserData
54− { _pdElements = Identity e
55− , _pdPath = pd ^. pdPath <> subpath }
56− magnify (to $ const newpd) parser
47+ let
48+ newpd = ParserData
49+ { _pdElements = Identity e
50+ , _pdPath = pd ^. pdPath <> subpath }
51+ lift $ runReaderT parser newpd
5752
58−-- | Takes function filtering
53+-- | Traverses elements located in same path using filtering function
5954inFilteredTrav
60− :: (Monad m, Foldable g, DomTraversable f)
61− => ([Element] -> ([Text], [Element]))
62− -- ^ Function returning some filtered elements with path suffixes which will
63− -- be appended to parser's state
55+ :: (Monad m, Foldable g, Buildable f)
56+ => ([Element] -> (DomPath, [Element]))
57+ -- ^ Takes list of current elements and returns some descendants
58+ -- subset and path this descendants located at. Path is should be
59+ -- same for all descendants and required for error message
6460 -> DomParserT Identity m a
6561 -> DomParserT g m (f a)
6662inFilteredTrav deeper = traverseElems trav
6763 where
6864 trav e = do
6965 let (path, elems) = deeper e
70− case buildDomTraversable elems of
66+ case build elems of
7167 Nothing -> throwParserError $ PENotFound . (<> path)
7268 Just tr -> return $ fmap (path,) tr
7369
70+-- | Runs parser arbitrary times, depending on 'Buildable' instance of
71+-- 'f'. For example if 'f' becomes 'NonEmpty' then 'inElemTrav' finds
72+-- @one or more@ elements matched by given 'ElemMatcher' and run
73+-- parser in each found element, then returns @NonEmpty a@ of results.
7474inElemTrav
75− :: (Monad m, Foldable g, DomTraversable f)
76− => Text
75+ :: (Monad m, Foldable g, Buildable f)
76+ => ElemMatcher -- ^ Tag(s) matcher to traverse in
7777 -> DomParserT Identity m a
7878 -> DomParserT g m (f a)
7979inElemTrav n = inFilteredTrav deeper
8080 where
81− deeper = ([n],) . toListOf (folded . nodes . folded . _Element . ell n)
81+ elemsFold = folded . nodes . folded . _Element . elMatch n
82+ deeper = (DomPath [_emShow n],) . toListOf elemsFold
8283
83−-- | Runs parser inside first children element with given name
84+-- | Runs parser inside first children element matched by macher
8485inElem
8586 :: (Monad m, Foldable g)
86− => Text
87+ => ElemMatcher
8788 -> DomParserT Identity m a
8889 -> DomParserT g m a
8990inElem n = fmap runIdentity . inElemTrav n
9091
9192inElemAll
9293 :: (Monad m, Foldable g)
93− => Text
94+ => ElemMatcher
9495 -> DomParserT Identity m a
9596 -> DomParserT g m [a]
9697inElemAll = inElemTrav
9798
9899inElemMay
99100 :: (Monad m, Foldable g)
100− => Text
101+ => ElemMatcher
101102 -> DomParserT Identity m a
102103 -> DomParserT g m (Maybe a)
103104inElemMay = inElemTrav
104105
105106inElemNe
106107 :: (Monad m, Foldable g)
107− => Text
108+ => ElemMatcher
108109 -> DomParserT Identity m a
109110 -> DomParserT g m (NonEmpty a)
110111inElemNe = inElemTrav
111112
112113{- | Dive given parser's current tags set into the given path. The @divePath
113−["a", "b"]@ differs from @inElem "a" . inElem "b"@. Namely the first variant
114+["a", "b"]@ differs from @inElem "a" $ inElem "b"@. Namely the first variant
114115will not fail if occured tag "a" which does not contains tag "b". This
115116behaviour is desireable when you dont want to parse whole XML and just want
116−to pull tags in some path. The other difference is in traversing inner
117+to pull tags located in some path. The other difference is in traversing inner
117118elements. Consider this code
118119
119120@
@@ -131,26 +132,31 @@ which translates like: @a > b > c > fromDom@
131132As you can see, inElem always takes first element and runs inner parser in this
132133single element, unlike 'divePath' which runs inner parser @in all@ descendants
133134in given path.
135+
136+Note also that 'divePath' takes parser parameterized by @[]@ not by
137+'Identity'. This because when you dive using some path you will get a
138+list of found elements and all these elements will be @current@ for
139+parser.
134140-}
135141
136142divePath
137143 :: forall m g a
138144 . (Monad m, Foldable g)
139− => [Text]
145+ => [ElemMatcher]
140146 -> DomParserT [] m a
141147 -> DomParserT g m a
142148divePath path = magnify $ to modElems
143149 where
144150 modElems
145151 = over pdElements (toListOf $ folded . diver)
146− . over pdPath (<> path)
152+ . over pdPath (<> DomPath (L.map _emShow path))
147153 diver :: Fold Element Element
148− diver = foldr (.) id $ map toDive path
149− toDive n = nodes . folded . _Element . ell n
154+ diver = F.foldr (.) id $ L.map toDive path
155+ toDive n = nodes . folded . _Element . elMatch n
150156
151157diveElem
152158 :: (Monad m, Foldable g)
153− => Text
159+ => ElemMatcher
154160 -> DomParserT [] m a
155161 -> DomParserT g m a
156162diveElem p = divePath [p]
@@ -160,7 +166,7 @@ ignoreElem
160166 :: (Monad m)
161167 => (Element -> Bool)
162168 -- ^ Predicate checking that we must ignore some current tag. If returns
163− -- true then parser will not be runned and combinator just returns Nothing.
169+ -- 'True' then parser will not be runned and combinator just returns Nothing.
164170 -> DomParserT Identity m a
165171 -> DomParserT Identity m (Maybe a)
166172ignoreElem test parser = do
@@ -168,7 +174,7 @@ ignoreElem test parser = do
168174 if ign then pure Nothing else Just <$> parser
169175
170176-- | If current element has no children nodes does not run parser and returns
171−-- Nothing. Otherwise runs parser inside current element. Usefull when you got
177+-- Nothing. Otherwise runs parser inside current element. Useful when you got
172178-- XML with strange empty elements which must be just ignored, but `inElem` runs
173179-- parser inside of this elements which causes to parser error.
174180ignoreEmpty
@@ -177,7 +183,7 @@ ignoreEmpty
177183 -> DomParserT Identity m (Maybe a)
178184ignoreEmpty = ignoreElem test
179185 where
180− test e = null $ e ^. nodes
186+ test e = L.null $ e ^. nodes
181187
182188-- | If all current elements contains blank content, or contains nothing at all
183189-- , then returns Nothing, else runs parser.
@@ -191,49 +197,6 @@ ignoreBlank = ignoreElem test
191197 let
192198 elems = e ^.. nodes . folded . _Element
193199 cont = mconcat $ e ^.. nodes . folded . _Content
194− in if | not $ null elems -> False
200+ in if | not $ L.null elems -> False
195201 | T.null $ T.strip cont -> True
196202 | otherwise -> False
197−
198−-- | If name of current tag differs from first argument throws 'PENotFound' with
199−-- tag name replaced in last path's segment. Usefull for checking root
200−-- document's element name.
201−checkCurrentName
202− :: (Monad m)
203− => Text
204− -> DomParserT Identity m ()
205−checkCurrentName n = do
206− cn <- view $ pdElements . to runIdentity . localName
207− unless (cn == n) $ do
208− p <- view pdPath
209− let pinit = if null p then [] else init p
210− throwError $ ParserErrors [PENotFound $ pinit ++ [n]]
211− return ()
212−
213−-- | Parses content inside current tag. It expects current element set consists
214−-- of exactly ONE element. Throws error if current elements set contains
215−-- multiple of them.
216−parseContent
217− :: (Monad m)
218− => (Text -> DomParserT Identity m a)
219− -> DomParserT Identity m a
220−parseContent parse = do
221− e <- view $ pdElements . to runIdentity
222− let
223− nds = e ^. nodes
224− els = nds ^.. folded . _Element
225− conts = nds ^.. folded . _Content
226− when (not $ null els) $ throwParserError PEContentNotFound
227− when (null conts) $ throwParserError PEContentNotFound
228− parse $ mconcat conts
229−
230−readContent
231− :: forall m g a
232− . (Read a, Typeable a, Monad m)
233− => Text
234− -> DomParserT g m a
235−readContent t = case readMaybe $ T.unpack t of
236− Nothing -> throwParserError $ PEWrongFormat [st|Not readable #{n}: #{t}|]
237− Just a -> pure a
238− where
239− n = show $ typeRep (Proxy :: Proxy a)
src/Text/XML/DOM/Parser/Content.hsnew file+129
@@ -0,0 +1,129 @@
1+module Text.XML.DOM.Parser.Content
2+ (
3+ -- * Parsing element's content
4+ parseContent
5+ -- * Getting current element's properties
6+ , getCurrentName
7+ , getCurrentContent
8+ , checkCurrentName
9+ -- * Internal
10+ , maybeReadContent
11+ , readContent
12+ , readBool
13+ , readChar
14+ ) where
15+
16+import Control.Lens
17+import Control.Monad
18+import Control.Monad.Except
19+import Data.List as L
20+import Data.Monoid
21+import Data.Text as T
22+import Data.Typeable
23+import Prelude as P
24+import Text.Read
25+import Text.XML.DOM.Parser.Types
26+import Text.XML.Lens
27+
28+
29+-- | Parses content inside current tag. It expects current element set
30+-- consists of exactly ONE element.
31+parseContent
32+ :: (Monad m)
33+ => (Text -> Either Text a)
34+ -- ^ Content parser, return error msg if value is not parsed
35+ -> DomParserT Identity m a
36+parseContent parse = getCurrentContent >>= \case
37+ Nothing -> throwParserError PEContentNotFound
38+ Just c -> case parse c of
39+ Left e -> throwParserError $ PEContentWrongFormat e
40+ Right a -> return a
41+
42+
43+-- | Returns name of current element.
44+--
45+-- @since 1.0.0
46+getCurrentName :: (Monad m) => DomParserT Identity m Name
47+getCurrentName = view $ pdElements . to runIdentity . name
48+
49+-- | If name of current tag differs from first argument throws 'PENotFound' with
50+-- tag name replaced in last path's segment. Useful for checking root
51+-- document's element name.
52+checkCurrentName
53+ :: (Monad m)
54+ => NameMatcher
55+ -> DomParserT Identity m ()
56+checkCurrentName n = do
57+ cn <- getCurrentName
58+ unless ((n ^. nmMatch) cn) $ do
59+ p <- view pdPath
60+ let pinit = if L.null (unDomPath p) then [] else L.init $ unDomPath p
61+ throwError $ ParserErrors [PENotFound $ DomPath $ pinit ++ [_nmShow n]]
62+ return ()
63+
64+-- | Get current content. If current element contains no content or
65+-- have inner elements then Nothing returned
66+--
67+-- @since 1.0.0
68+getCurrentContent :: (Monad m) => DomParserT Identity m (Maybe Text)
69+getCurrentContent = do
70+ nds <- view $ pdElements . to runIdentity . nodes
71+ let
72+ els :: [Element]
73+ els = nds ^.. folded . _Element
74+ conts :: [Text]
75+ conts = nds ^.. folded . _Content
76+ return $ if
77+ | not $ L.null els -> Nothing
78+ | L.null conts -> Nothing
79+ | otherwise -> Just $ mconcat conts
80+
81+-- | If reader returns 'Nothing' then resulting function returns 'Left
82+-- "error message"'. 'Typeable' is used for generating usefull error
83+-- message.
84+--
85+-- @since 1.0.0
86+maybeReadContent
87+ :: forall a
88+ . (Typeable a)
89+ => (Text -> Maybe a)
90+ -- ^ Content or attribute reader
91+ -> Text
92+ -- ^ Content or attribute value
93+ -> Either Text a
94+maybeReadContent f t = maybe (Left msg) Right $ f t
95+ where
96+ msg = "Not readable " <> n <> ": " <> t
97+ n = T.pack $ P.show $ typeRep (Proxy :: Proxy a)
98+
99+
100+-- | Tries to read given text to value using 'Read'. Useful to use
101+-- with 'parseContent' and 'parseAttribute'. Content is stripped
102+-- before reading.
103+readContent
104+ :: (Read a, Typeable a)
105+ => Text
106+ -> Either Text a
107+readContent = maybeReadContent $ readMaybe . T.unpack . T.strip
108+
109+
110+-- | @since 1.0.0
111+readBool :: Text -> Either Text Bool
112+readBool t =
113+ let
114+ lowt = T.toLower $ T.strip t
115+ tvals = ["y", "yes", "t", "true", "1"]
116+ fvals = ["n", "no", "f", "false", "0"]
117+ in if
118+ | lowt `L.elem` tvals -> Right True
119+ | lowt `L.elem` fvals -> Right False
120+ | otherwise ->
121+ Left $ "Could not read " <> t <> " as Bool"
122+
123+-- | Expects text to be single character
124+--
125+-- @since 1.0.0
126+readChar :: Text -> Either Text Char
127+readChar t = case T.unpack $ T.strip t of
128+ [c] -> Right c
129+ _ -> Left "Should have exactly one non-blank character"
src/Text/XML/DOM/Parser/FromAttribute.hsnew file+112
@@ -0,0 +1,112 @@
1+module Text.XML.DOM.Parser.FromAttribute
2+ ( -- * FromAttribute
3+ FromAttribute(..)
4+ , proxyFromAttribute
5+ -- * Explicit methods
6+ , textFromAttribute
7+ , stringFromAttribute
8+ , charFromAttribute
9+ , intFromAttribute
10+ , integerFromAttribute
11+ , doubleFromAttribute
12+ , fixedFromAttribute
13+ , boolFromAttribute
14+ , unitFromAttribute
15+ , voidFromAttribute
16+ , scientificFromAttribute
17+ ) where
18+
19+import Data.Fixed
20+import Data.Scientific
21+import Data.Text as T
22+import Data.Typeable
23+import Data.Void
24+import Text.XML.DOM.Parser.Content
25+
26+-- | Class of types which can be get from attribute value. Method
27+-- 'fromAttribute' is a convenient default parameter for
28+-- 'parseAttribute'
29+--
30+-- @since 2.0.0
31+class FromAttribute a where
32+ fromAttribute
33+ :: Text
34+ -- ^ Attribute contents to parse
35+ -> Either Text a
36+ -- ^ Either error message or result
37+
38+proxyFromAttribute
39+ :: FromAttribute a
40+ => Proxy a
41+ -> Text
42+ -> Either Text a
43+proxyFromAttribute _ = fromAttribute
44+
45+instance FromAttribute () where
46+ fromAttribute = unitFromAttribute
47+
48+unitFromAttribute :: Text -> Either Text ()
49+unitFromAttribute _ = Right ()
50+
51+instance FromAttribute Void where
52+ fromAttribute = voidFromAttribute
53+
54+voidFromAttribute :: Text -> Either Text Void
55+voidFromAttribute _ = Left "Void has been parsed"
56+
57+instance FromAttribute Text where
58+ fromAttribute = textFromAttribute
59+
60+textFromAttribute :: Text -> Either Text Text
61+textFromAttribute = Right
62+
63+instance FromAttribute String where
64+ fromAttribute = stringFromAttribute
65+
66+stringFromAttribute :: Text -> Either Text String
67+stringFromAttribute = Right . T.unpack
68+
69+instance FromAttribute Char where
70+ fromAttribute = charFromAttribute
71+
72+charFromAttribute :: Text -> Either Text Char
73+charFromAttribute = readChar
74+
75+instance FromAttribute Int where
76+ fromAttribute = intFromAttribute
77+
78+intFromAttribute :: Text -> Either Text Int
79+intFromAttribute = readContent
80+
81+instance FromAttribute Integer where
82+ fromAttribute = integerFromAttribute
83+
84+integerFromAttribute :: Text -> Either Text Integer
85+integerFromAttribute = readContent
86+
87+instance FromAttribute Double where
88+ fromAttribute = doubleFromAttribute
89+
90+doubleFromAttribute :: Text -> Either Text Double
91+doubleFromAttribute = readContent
92+
93+instance (Typeable a, HasResolution a) => FromAttribute (Fixed a) where
94+ fromAttribute = fixedFromAttribute
95+
96+fixedFromAttribute
97+ :: (HasResolution a, Typeable a)
98+ => Text
99+ -> Either Text (Fixed a)
100+fixedFromAttribute = readContent
101+
102+instance FromAttribute Bool where
103+ fromAttribute = boolFromAttribute
104+
105+boolFromAttribute :: Text -> Either Text Bool
106+boolFromAttribute = readBool
107+
108+instance FromAttribute Scientific where
109+ fromAttribute = scientificFromAttribute
110+
111+scientificFromAttribute :: Text -> Either Text Scientific
112+scientificFromAttribute = readContent
src/Text/XML/DOM/Parser/Class.hs → src/Text/XML/DOM/Parser/FromDom.hsrenamed+56−80
@@ -1,10 +1,9 @@
1−module Text.XML.DOM.Parser.Class
1+module Text.XML.DOM.Parser.FromDom
22 ( -- * FromDom
33 FromDom(..)
44 , proxyFromDom
55 -- * Explicit methods for convenience
66 , elementFromDom
7− , unionFromDom
87 , textFromDom
98 , stringFromDom
109 , charFromDom
@@ -15,21 +14,19 @@ module Text.XML.DOM.Parser.Class
1514 , boolFromDom
1615 , unitFromDom
1716 , voidFromDom
17+ , scientificFromDom
1818 ) where
1919
20−import Control.Applicative
21−import Control.Lens
22−import Data.Fixed
23−import Data.OpenUnion
24−import Data.Text (Text)
25−import qualified Data.Text as T
26−import Data.Typeable
27−import Data.Void
28−import Text.Shakespeare.Text (st)
29−import Text.XML
30−import Text.XML.DOM.Parser.Combinators
31−import Text.XML.DOM.Parser.Types
32−import TypeFun.Data.List hiding (Union)
20+import Control.Applicative
21+import Control.Lens
22+import Data.Fixed
23+import Data.Scientific
24+import Data.Text as T hiding (empty)
25+import Data.Typeable
26+import Data.Void
27+import Text.XML
28+import Text.XML.DOM.Parser.Content
29+import Text.XML.DOM.Parser.Types
3330
3431proxyFromDom
3532 :: forall proxy m a
@@ -38,107 +35,86 @@ proxyFromDom
3835 -> DomParserT Identity m a
3936proxyFromDom _ = fromDom
4037
41−-- | Class of types which can be parsed from single XML element.
38+-- | Class of types which can be parsed from single XML element. The
39+-- method 'fromDom' is convenient default to use with 'inElem'
4240class FromDom a where
4341 fromDom :: (Monad m) => DomParserT Identity m a
4442
4543instance FromDom () where
4644 fromDom = unitFromDom
4745
46+-- | Always successfully parses any DOM to @()@
47+unitFromDom :: (Monad m) => DomParserT Identity m ()
48+unitFromDom = pure ()
49+
4850instance FromDom Void where
4951 fromDom = voidFromDom
5052
53+-- | Never parses successfully. It is just 'empty'
54+voidFromDom :: (Monad m) => DomParserT Identity m Void
55+voidFromDom = empty
56+
5157instance FromDom Text where
5258 fromDom = textFromDom
5359
60+textFromDom :: (Monad m) => DomParserT Identity m Text
61+textFromDom = parseContent Right
62+
5463instance FromDom String where
5564 fromDom = stringFromDom
5665
66+stringFromDom :: (Monad m) => DomParserT Identity m String
67+stringFromDom = parseContent $ Right . T.unpack
68+
5769instance FromDom Char where
5870 fromDom = charFromDom
5971
72+charFromDom :: (Monad m) => DomParserT Identity m Char
73+charFromDom = parseContent readChar
74+
6075instance FromDom Int where
6176 fromDom = intFromDom
6277
78+intFromDom :: (Monad m) => DomParserT Identity m Int
79+intFromDom = parseContent readContent
80+
6381instance FromDom Integer where
6482 fromDom = integerFromDom
6583
84+integerFromDom :: (Monad m) => DomParserT Identity m Integer
85+integerFromDom = parseContent readContent
86+
6687instance FromDom Double where
6788 fromDom = doubleFromDom
6889
90+doubleFromDom :: (Monad m) => DomParserT Identity m Double
91+doubleFromDom = parseContent readContent
92+
6993instance (HasResolution a, Typeable a) => FromDom (Fixed a) where
7094 fromDom = fixedFromDom
7195
96+fixedFromDom
97+ :: (Monad m, Typeable a, HasResolution a)
98+ => DomParserT Identity m (Fixed a)
99+fixedFromDom = parseContent readContent
100+
101+instance FromDom Scientific where
102+ fromDom = scientificFromDom
103+
104+scientificFromDom :: Monad m => DomParserT Identity m Scientific
105+scientificFromDom = parseContent readContent
106+
72107instance FromDom Bool where
73108 fromDom = boolFromDom
74109
75−instance FromDom (Union '[]) where
76− fromDom = empty
77−
78−instance ( Typeable a, FromDom a, FromDom (Union as)
79− , SubList as (a ': as) )
80− => FromDom (Union (a ': as)) where
81− -- fromDom :: forall m. (DomParserMonad m) => m a
82− fromDom = (liftUnion <$> (proxyFromDom (Proxy :: Proxy a)))
83− <|> (reUnion <$> (proxyFromDom (Proxy :: Proxy (Union as))))
110+-- | Expects content to be y, yes, t, true or 1 for True Values n, no,
111+-- f, false or 0 for False. Case is not significant, blank characters
112+-- are striped.
113+boolFromDom :: (Monad m) => DomParserT Identity m Bool
114+boolFromDom = parseContent readBool
84115
85116instance FromDom Element where
86117 fromDom = elementFromDom
87118
88119elementFromDom :: (Monad m) => DomParserT Identity m Element
89120elementFromDom = view $ pdElements . to runIdentity
90−
91−unionFromDom
92− :: (Monad m, FromDom (Union as))
93− => proxy as
94− -> DomParserT Identity m (Union as)
95−unionFromDom _ = fromDom
96−
97−textFromDom :: (Monad m) => DomParserT Identity m Text
98−textFromDom = parseContent pure
99−
100−stringFromDom :: (Monad m) => DomParserT Identity m String
101−stringFromDom = parseContent $ pure . T.unpack
102−
103−charFromDom :: (Monad m) => DomParserT Identity m Char
104−charFromDom = parseContent $ \t -> case T.unpack $ T.strip t of
105− [c] -> pure c
106− _ -> throwParserError $ PEWrongFormat
107− "Should have exactly one non-blank character"
108−
109−intFromDom :: (Monad m) => DomParserT Identity m Int
110−intFromDom = parseContent readContent
111−
112−integerFromDom :: (Monad m) => DomParserT Identity m Integer
113−integerFromDom = parseContent readContent
114−
115−doubleFromDom :: (Monad m) => DomParserT Identity m Double
116−doubleFromDom = parseContent readContent
117−
118−fixedFromDom
119− :: (Monad m, Typeable a, HasResolution a)
120− => DomParserT Identity m (Fixed a)
121−fixedFromDom = parseContent readContent
122−
123−-- | Expects content to be y, yes, t, true or 1 for True value. n, no,
124−-- f, false or 0 for False value. Case is not significant, blank
125−-- characters are striped.
126−boolFromDom :: (Monad m) => DomParserT Identity m Bool
127−boolFromDom = parseContent $ \t ->
128− let
129− lowt = T.toLower $ T.strip t
130− tvals = ["y", "yes", "t", "true", "1"]
131− fvals = ["n", "no", "f", "false", "0"]
132− in if | lowt `elem` tvals -> return True
133− | lowt `elem` fvals -> return False
134− | otherwise ->
135− let msg = [st|Could not read "#{t}" as Bool|]
136− in throwParserError $ PEWrongFormat msg
137−
138−-- | Always successfully parses any DOM to @()@
139−unitFromDom :: (Monad m) => DomParserT Identity m ()
140−unitFromDom = pure ()
141−
142−-- | Never parses successfully. It is just 'mzero'
143−voidFromDom :: (Monad m) => DomParserT Identity m Void
144−voidFromDom = empty
src/Text/XML/DOM/Parser/Types.hs+159−60
@@ -1,59 +1,182 @@
11module Text.XML.DOM.Parser.Types
2− ( -- * Parser internals
3− ParserError(..)
2+ (-- * Element matching
3+ ElemMatcher(..)
4+ , emMatch
5+ , emShow
6+ , matchElemName
7+ , elMatch
8+ -- * Name matching
9+ , NameMatcher(..)
10+ , nmMatch
11+ , nmShow
12+ , matchName
13+ , matchLocalName
14+ , matchCILocalName
15+ -- * Parser internals
16+ , DomPath(..)
17+ , ParserError(..)
418 , pePath
519 , peDetails
20+ , peAttributeName
621 , ParserErrors(..)
722 , _ParserErrors
823 , ParserData(..)
924 , pdElements
1025 , pdPath
11− -- * Parser type
1226 , DomParserT
1327 , DomParser
1428 , runDomParserT
1529 , runDomParser
1630 -- * Auxiliary
17− , DomTraversable(..)
1831 , throwParserError
19− , throwWrongFormat
2032 ) where
2133
22−import Control.Exception
23−import Control.Lens
24−import Control.Monad.Except
25−import Control.Monad.Reader
26−import Data.List.NonEmpty (NonEmpty(..))
27−import qualified Data.List.NonEmpty as NE
28−import Data.Maybe
29−import Data.Text (Text)
30−import GHC.Generics (Generic)
31−import Text.XML
32−import Text.XML.Lens
34+import Control.Exception
35+import Control.Lens
36+import Control.Monad.Except
37+import Control.Monad.Reader
38+import Data.CaseInsensitive as CI
39+import Data.String
40+import Data.Text as T
41+import GHC.Generics (Generic)
42+import Text.XML
43+import Text.XML.Lens
44+
45+-- | Arbitrary element matcher
46+--
47+-- @since 2.0.0
48+data ElemMatcher = ElemMatcher
49+ { _emMatch :: Element -> Bool
50+ , _emShow :: Text
51+ -- ^ Field for 'Show' instance and bulding usefull errors
52+ }
53+
54+makeLenses ''ElemMatcher
55+
56+-- | Instance using instance of 'NameMatcher'
57+instance IsString ElemMatcher where
58+ fromString = matchElemName . fromString
59+
60+instance Show ElemMatcher where
61+ show = T.unpack . _emShow
62+
63+-- | Match element by name
64+--
65+-- @since 2.0.0
66+matchElemName :: NameMatcher -> ElemMatcher
67+matchElemName (NameMatcher matchName showName) = ElemMatcher
68+ { _emMatch = views name matchName
69+ , _emShow = showName
70+ }
71+
72+-- | Match over elements
73+--
74+-- @since 2.0.0
75+elMatch :: ElemMatcher -> Traversal' Element Element
76+elMatch (ElemMatcher match _) = filtered match
77+
78+-- | Arbitrary name matcher. Match name any way you want, but
79+-- considered to be used as comparator with some name with some rules
80+--
81+-- @since 2.0.0
82+data NameMatcher = NameMatcher
83+ { _nmMatch :: Name -> Bool
84+ -- ^ Name matching function, usually should be simple comparsion
85+ -- function takin in account only local name or other components
86+ -- of 'Name'
87+ , _nmShow :: Text
88+ -- ^ Field for 'Show' instance and bulding usefull errors
89+ }
90+
91+-- | Instance use 'matchCILocalName' as most general and liberal
92+-- matching strategy (while XML is often malformed).
93+--
94+-- @since 2.0.0
95+instance IsString NameMatcher where
96+ fromString = matchCILocalName . T.pack
97+
98+instance Show NameMatcher where
99+ show = T.unpack . _nmShow
100+
101+-- | Makes matcher which matches only local part of name igoring
102+-- namespace and prefix. Local name matching is case sensitive.
103+--
104+-- @since 2.0.0
105+matchLocalName :: Text -> NameMatcher
106+matchLocalName tname = NameMatcher
107+ { _nmMatch = \n -> nameLocalName n == tname
108+ , _nmShow = tname
109+ }
110+
111+-- | Makes matcher which matches only local part of name igoring
112+-- namespace and prefix. Local name matching is case insensitive. This
113+-- is the most common case.
114+--
115+-- @since 2.0.0
116+matchCILocalName :: Text -> NameMatcher
117+matchCILocalName tname = NameMatcher
118+ { _nmMatch = \n -> CI.mk (nameLocalName n) == CI.mk tname
119+ , _nmShow = tname
120+ }
121+
122+makeLenses ''NameMatcher
123+
124+-- | Makes matcher which match name by 'Eq' with given
125+--
126+-- @since 2.0.0
127+matchName :: Name -> NameMatcher
128+matchName n = NameMatcher
129+ { _nmMatch = (== n)
130+ , _nmShow = nameLocalName n
131+ }
132+
133+-- | Path some element should be found at. Path starts from the root
134+-- element of the document. Errors are much more usefull with path.
135+newtype DomPath = DomPath
136+ { unDomPath :: [Text]
137+ } deriving (Eq, Ord, Show, Semigroup, Monoid)
33138
34139-- | DOM parser error description.
35140data ParserError
36141 -- | Tag not found which should be.
37142 = PENotFound
38− { _pePath :: [Text]
143+ { _pePath :: DomPath
144+ -- ^ Path of element error occured in
39145 }
40146
41− -- | Tag contents has wrong format, (could not read text to value)
42− | PEWrongFormat
43− { _peDetails :: Text
44− , _pePath :: [Text] -- ^ path of element
147+ -- | Expected attribute but not found
148+ --
149+ -- @since 1.0.0
150+ | PEAttributeNotFound
151+ { _peAttributeName :: NameMatcher
152+ , _pePath :: DomPath
153+ }
154+
155+ -- | Could not parse attribute
156+ --
157+ -- @since 1.0.0
158+ | PEAttributeWrongFormat
159+ { _peAttributeName :: NameMatcher
160+ , _peDetails :: Text
161+ , _pePath :: DomPath
45162 }
46163
47164 -- | Node should have text content, but it does not.
48165 | PEContentNotFound
49− { _pePath :: [Text]
166+ { _pePath :: DomPath
167+ }
168+
169+ -- | Tag contents has wrong format, (could not read text to value)
170+ | PEContentWrongFormat
171+ { _peDetails :: Text
172+ , _pePath :: DomPath
50173 }
51174
52175 -- | Some other error
53176 | PEOther
54177 { _peDetails :: Text
55− , _pePath :: [Text]
56− } deriving (Eq, Ord, Show, Generic)
178+ , _pePath :: DomPath
179+ } deriving (Show, Generic)
57180
58181makeLenses ''ParserError
59182
@@ -61,15 +184,16 @@ instance Exception ParserError
61184
62185newtype ParserErrors = ParserErrors
63186 { unParserErrors :: [ParserError]
64− } deriving (Ord, Eq, Show, Monoid, Generic)
187+ } deriving (Show, Semigroup, Monoid, Generic)
65188
66189makePrisms ''ParserErrors
67190
68191instance Exception ParserErrors
69192
70193
71−{- | Parser scope parser runs in. Functor argument is usually @Identity@ or
72−@[]@.
194+{- | Parser scope.
195+
196+Functor argument is usually @Identity@ or @[]@.
73197
74198If functor is @Identity@ then parser expects exactly ONE current element. This
75199is common behavior for content parsers, or parsers expecting strict XML
@@ -86,7 +210,7 @@ data ParserData f = ParserData
86210 { _pdElements :: f Element
87211 -- ^ Current element(s). Functor is intended to be either @Identity@ or
88212 -- @[]@
89− , _pdPath :: [Text]
213+ , _pdPath :: DomPath
90214 -- ^ Path for error reporting
91215 }
92216
@@ -102,10 +226,11 @@ runDomParserT
102226 -> DomParserT Identity m a
103227 -> m (Either ParserErrors a)
104228runDomParserT doc par =
105− let pd = ParserData
106− { _pdElements = doc ^. root . to pure
107− , _pdPath = [doc ^. root . localName]
108− }
229+ let
230+ pd = ParserData
231+ { _pdElements = doc ^. root . to pure
232+ , _pdPath = DomPath [doc ^. root . name . to nameLocalName]
233+ }
109234 in runExceptT $ runReaderT par pd
110235
111236runDomParser
@@ -114,36 +239,10 @@ runDomParser
114239 -> Either ParserErrors a
115240runDomParser doc par = runIdentity $ runDomParserT doc par
116241
117−-- | Class of traversable functors which may be constructed from list. Or may
118−-- not.
119−class Traversable f => DomTraversable f where
120− -- | If method return Nothing this means we can not build traversable from
121− -- given list. In this case combinator should fail traversing.
122− buildDomTraversable :: [a] -> Maybe (f a)
123−
124−instance DomTraversable Identity where
125− buildDomTraversable = fmap Identity . listToMaybe
126−
127−instance DomTraversable [] where
128− buildDomTraversable = Just
129−
130−instance DomTraversable Maybe where
131− buildDomTraversable = Just . listToMaybe
132−
133−instance DomTraversable NonEmpty where
134− buildDomTraversable = NE.nonEmpty
135−
136242throwParserError
137243 :: (MonadError ParserErrors m, MonadReader (ParserData f) m)
138− => ([Text] -> ParserError)
244+ => (DomPath -> ParserError)
139245 -> m a
140246throwParserError mkerr = do
141247 path <- view pdPath
142− throwError $ ParserErrors [mkerr path]
143−
144−-- | Throw 'PEWrongFormat' as very common case
145−throwWrongFormat
146− :: (MonadError ParserErrors m, MonadReader (ParserData f) m)
147− => Text
148− -> m a
149−throwWrongFormat err = throwParserError $ PEWrongFormat err
248+ throwError $ ParserErrors $ [mkerr path]
src/Text/XML/Soap.hsnew file+46
@@ -0,0 +1,46 @@
1+module Text.XML.Soap where
2+
3+import Control.Lens
4+import Data.String
5+import Text.XML
6+import Text.XML.DOM.Parser
7+import Text.XML.Writer
8+
9+data XmlEnvelope h b = XmlEnvelope
10+ { _xeHeader :: !(Maybe h)
11+ , _xeBody :: !(Maybe b)
12+ } deriving (Eq, Show)
13+
14+makeLenses ''XmlEnvelope
15+
16+xmlEnvelopeToDocument :: (ToXML h, ToXML b) => XmlEnvelope h b -> Document
17+xmlEnvelopeToDocument xe = doc
18+ where
19+ sname n = fromString $ "{http://schemas.xmlsoap.org/soap/envelope/}" <> n
20+ doc =
21+ Document
22+ { documentPrologue = Prologue [] Nothing []
23+ , documentRoot =
24+ Element
25+ { elementName = sname "Envelope"
26+ , elementAttributes = mempty
27+ , elementNodes = [NodeElement header, NodeElement body]
28+ }
29+ , documentEpilogue = []
30+ }
31+ header =
32+ Element
33+ { elementName = sname "Header"
34+ , elementAttributes = mempty
35+ , elementNodes = render . toXML $ xe ^. xeHeader
36+ }
37+ body =
38+ Element
39+ { elementName = sname "Body"
40+ , elementAttributes = mempty
41+ , elementNodes = render . toXML $ xe ^. xeBody
42+ }
43+
44+instance (FromDom h, FromDom b) => FromDom (XmlEnvelope h b) where
45+ fromDom = XmlEnvelope <$> inElemMay "Header" fromDom
46+ <*> inElemMay "Body" fromDom
stack.yamlnew file+1
@@ -0,0 +1 @@
1+ghc-963.yaml
test/Main.hs+96−29
@@ -27,8 +27,15 @@ testStructureFromDom = do
2727 tsBools <- inElemNe "bool" boolFromDom
2828 return TestStructure{..}
2929
30−instance FromDom TestStructure where
31− fromDom = testStructureFromDom
30+testStructureAttributes :: (Monad m) => DomParserT Identity m TestStructure
31+testStructureAttributes = do
32+ name <- getCurrentName
33+ int <- parseAttribute "int" readContent
34+ bool <- parseAttribute "bool" readBool
35+ return $ TestStructure
36+ { tsName = nameLocalName name
37+ , tsInts = [int]
38+ , tsBools = pure bool }
3239
3340docStruct :: Document
3441docStruct = parseText_ def [lt|
@@ -41,6 +48,13 @@ docStruct = parseText_ def [lt|
4148</root>
4249|]
4350
51+docAttrStruct :: Document
52+docAttrStruct = parseText_ def [lt|
53+<?xml version="1.0" encoding="utf-8"?>
54+<structure int="10" bool="t">
55+</structure>
56+|]
57+
4458docNone :: Document
4559docNone = parseText_ def [lt|
4660<?xml version="1.0" encoding="utf-8"?>
@@ -48,8 +62,26 @@ docNone = parseText_ def [lt|
4862</root>
4963|]
5064
51−docEmpty :: Document
52−docEmpty = parseText_ def [lt|
65+docSimpleAttr :: Document
66+docSimpleAttr = parseText_ def [lt|
67+<?xml version="1.0" encoding="utf-8"?>
68+<root>
69+<a attr="content"/>
70+</root>
71+|]
72+
73+docMultipleAttr :: Document
74+docMultipleAttr = parseText_ def [lt|
75+<?xml version="1.0" encoding="utf-8"?>
76+<root>
77+<a attr="content1"/>
78+<a attr="content2"/>
79+<a attr="content3"/>
80+</root>
81+|]
82+
83+docSingleEmpty :: Document
84+docSingleEmpty = parseText_ def [lt|
5385<?xml version="1.0" encoding="utf-8"?>
5486<root>
5587 <a/>
@@ -124,7 +156,7 @@ specParser
124156 :: String -- ^ Name of spec
125157 -> Document -- ^ Document to parse
126158 -> (a -> Maybe String) -- ^ Value checker, Nothing if all ok
127− -> DomParser Identity a -- ^ Parser itself
159+ -> DomParser Identity a -- ^ Parser itself
128160 -> Spec
129161specParser name doc check parser = it name $ do
130162 result <- either throwIO return $ runDomParser doc parser
@@ -134,9 +166,9 @@ specParser name doc check parser = it name $ do
134166
135167specParserEq
136168 :: (Eq a, Show a)
137− => String -- ^ Name of spec
138− -> Document -- ^ Document to parse
139− -> a -- ^ Value parser should return
169+ => String -- ^ Name of spec
170+ -> Document -- ^ Document to parse
171+ -> a -- ^ Value parser should return
140172 -> DomParser Identity a -- ^ Parser itself
141173 -> Spec
142174specParserEq name doc a parser = specParser name doc check parser
@@ -161,7 +193,7 @@ specParserFailedPath
161193 :: (Show a)
162194 => String -- ^ Name of spec
163195 -> Document
164− -> [Text] -- ^ Expteced path in error
196+ -> DomPath -- ^ Expteced path in error
165197 -> DomParser Identity a
166198 -> Spec
167199specParserFailedPath name doc path parser = it name $ example $ do
@@ -176,13 +208,37 @@ specParserFailedPath name doc path parser = it name $ example $ do
176208combinationsSpec :: Spec
177209combinationsSpec = do
178210 describe "inElem" $ do
179− let parser = inElem "a" textFromDom
180211 describe "succeeds" $ do
181− specParserEq "docSimple" docSimple "content" parser
182− specParserEq "docMultiple" docMultiple "content1" parser
212+ describe "simple" $ do
213+ let parser = inElem "a" textFromDom
214+ specParserEq "docSimple" docSimple "content" parser
215+ specParserEq "docMultiple" docMultiple "content1" parser
216+ describe "deep" $ do
217+ let parser = inElem "a" $ inElem "b" textFromDom
218+ specParserEq "docDeep" docDeep "content" parser
219+ specParserEq "docDeepMultiple1" docDeepMultiple1 "content1" parser
220+ specParserEq "docDeepWithContent" docDeepWithContent "content" parser
183221 describe "fails" $ do
184− specParserFailed "docNone" docNone parser
185− specParserFailed "docEmpty" docEmpty parser
222+ describe "simple" $ do
223+ let parser = inElem "a" textFromDom
224+ specParserFailed "docNone" docNone parser
225+ specParserFailed "docSingleEmpty" docSingleEmpty parser
226+ describe "deep" $ do
227+ let parser = inElem "a" $ inElem "b" textFromDom
228+ specParserFailedPath "docDeepMultiple2" docDeepMultiple2
229+ (DomPath ["root", "a", "b"]) parser
230+ -- should fail here because first tag "a" does not contains
231+ -- tag "b" which is expected by parser @inElem "b"@
232+
233+ describe "parseAttribute" $ do
234+ describe "succeeds" $ do
235+ specParserEq "docSimpleAttr" docSimpleAttr "content"
236+ $ inElem "a" $ parseAttribute "attr" Right
237+ let res = ["content1", "content2", "content3"]
238+ specParserEq "docMultipleAttr" docMultipleAttr res
239+ $ inElemAll "a" $ parseAttribute "attr" Right
240+ describe "fails" $ do
241+ specParserFailed "docNone" docNone $ parseAttribute "attr" Right
186242
187243 describe "inElemAll" $ do
188244 let parser = inElemAll "a" textFromDom
@@ -192,7 +248,7 @@ combinationsSpec = do
192248 specParserEq "docMultiple" docMultiple
193249 ["content1", "content2", "content3"] parser
194250 describe "fails" $ do
195− specParserFailed "docEmpty" docEmpty parser
251+ specParserFailed "docSingleEmpty" docSingleEmpty parser
196252
197253 describe "inElemMay" $ do
198254 let parser = inElemMay "a" textFromDom
@@ -201,7 +257,7 @@ combinationsSpec = do
201257 specParserEq "docSimple" docSimple (Just "content") parser
202258 specParserEq "docMultiple" docMultiple (Just "content1") parser
203259 describe "fails" $ do
204− specParserFailed "docEmpty" docEmpty parser
260+ specParserFailed "docSingleEmpty" docSingleEmpty parser
205261
206262 describe "inElemNe" $ do
207263 let parser = inElemNe "a" textFromDom
@@ -211,7 +267,7 @@ combinationsSpec = do
211267 (NE.fromList ["content1", "content2", "content3"]) parser
212268 describe "fails" $ do
213269 specParserFailed "docNone" docNone parser
214− specParserFailed "docEmpty" docEmpty parser
270+ specParserFailed "docSingleEmpty" docSingleEmpty parser
215271
216272 describe "diveElem" $ do
217273 describe "single element" $ do
@@ -220,7 +276,7 @@ combinationsSpec = do
220276 specParserEq "docDeepMultiple1" docDeepMultiple1 "content1" parser
221277 specParserEq "docDeepMultiple2" docDeepMultiple2 "content1" parser
222278 describe "fails" $ do
223− specParserFailedPath "docSimple" docSimple ["root", "a", "b"] parser
279+ specParserFailedPath "docSimple" docSimple (DomPath ["root", "a", "b"]) parser
224280 describe "multiple elements" $ do
225281 let
226282 parser = diveElem "a" $ inElemAll "b" textFromDom
@@ -234,7 +290,7 @@ combinationsSpec = do
234290 describe "mandatory element" $ do
235291 let parser = inElem "a" $ ignoreEmpty textFromDom
236292 describe "succeeds" $ do
237− specParserEq "docEmpty" docEmpty Nothing parser
293+ specParserEq "docSingleEmpty" docSingleEmpty Nothing parser
238294 specParserEq "docSimple" docSimple (Just "content") parser
239295 specParserEq "docMultiple" docMultiple (Just "content1") parser
240296 describe "fails" $ do
@@ -243,7 +299,7 @@ combinationsSpec = do
243299 let parser = fmap join $ inElemMay "a" $ ignoreEmpty textFromDom
244300 describe "succeeds" $ do
245301 specParserEq "docNone" docNone Nothing parser
246− specParserEq "docEmpty" docEmpty Nothing parser
302+ specParserEq "docSingleEmpty" docSingleEmpty Nothing parser
247303 specParserEq "docSimple" docSimple (Just "content") parser
248304 specParserEq "docMultiple" docMultiple (Just "content1") parser
249305
@@ -255,9 +311,9 @@ combinationsSpec = do
255311 inElem "a" $ do
256312 checkCurrentName "a"
257313 describe "fails" $ do
258− specParserFailedPath "root element" docSimple ["toor"] $ do
314+ specParserFailedPath "root element" docSimple (DomPath ["toor"]) $ do
259315 checkCurrentName "toor"
260− specParserFailedPath "inner element" docSimple ["root", "b"] $ do
316+ specParserFailedPath "inner element" docSimple (DomPath ["root", "b"]) $ do
261317 inElem "a" $ checkCurrentName "b"
262318
263319contentSpec :: Spec
@@ -271,14 +327,25 @@ contentSpec = do
271327structSpec :: Spec
272328structSpec = do
273329 describe "succeeds" $ do
274− let
275− result = TestStructure
276− { tsName = "name"
277− , tsInts = [1,2]
278− , tsBools = pure True }
279− specParserEq "docStruct" docStruct result fromDom
330+ describe "dom structure" $ do
331+ let
332+ result = TestStructure
333+ { tsName = "name"
334+ , tsInts = [1,2]
335+ , tsBools = pure True }
336+ specParserEq "docStruct" docStruct result testStructureFromDom
337+ describe "attribute structure" $ do
338+ let
339+ result = TestStructure
340+ { tsName = "structure"
341+ , tsInts = [10]
342+ , tsBools = pure True }
343+ specParserEq "docAttrStruct" docAttrStruct result testStructureAttributes
280344 describe "fails" $ do
281− specParserFailed "docSimple" docSimple testStructureFromDom
345+ describe "dom structure" $ do
346+ specParserFailed "docSimple" docSimple testStructureFromDom
347+ describe "attr structure" $ do
348+ specParserFailed "docNone" docNone testStructureAttributes
282349
283350main :: IO ()
284351main = hspec $ do