git.iko.soy/typeable/dom-parser
on master help sign in
Merge branch 'feature/fixAndRefactor'
commit33f618ee87d3ba7b59f2ce2b000b9257db85ae03 authorAleksey Uimanov <s9gf4ult@gmail.com> dateSun 8 Jan 2017, 15:39 (10 years ago) parent65a7be497c7e03
11 files changed+614−352
CHANGELOG.md+16 dom-parser.cabal+7−2 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+52−182 src/Text/XML/DOM/Parser/Content.hs+128 src/Text/XML/DOM/Parser/FromAttribute.hs+112 src/Text/XML/DOM/Parser/Class.hs → src/Text/XML/DOM/Parser/FromDom.hs+48−97 src/Text/XML/DOM/Parser/Types.hs+145−63 test/Main.hs+6−6
CHANGELOG.md+16
@@ -1,10 +1,26 @@
11# CHANGELOG
22
3+## 2.0.0
4+
5+### Added
6+
7+* `NameMatcher` and `ElemMatcher` as more flexible mechanisms for
8+ matching names and elements instead of just matching by name.
9+* Class `FromAttribute` and some default instances
10+
11+### Changed
12+
13+* `DomTraversable` renamed to `Buildable`
14+* Combinators use `NameMatcher` and `ElemMatcher`
15+* Documentation improved
16+
317## 1.0.0
18+
419* Added support for parsing attributes
520* Changed public interface for 'parseContent'
621* Added useful functions
722* Dropped unnecessary dependency from shakespeare
823
924## 0.1.0
25+
1026* Public release
dom-parser.cabal+7−2
@@ -1,5 +1,5 @@
11name: dom-parser
2−version: 1.0.0
2+version: 2.0.0
33synopsis: Simple monadic DOM parser
44license: MIT
55license-file: LICENSE
@@ -44,6 +44,7 @@ library
4444 , TypeOperators
4545 , UndecidableInstances
4646 build-depends: base >= 4.7 && < 5
47+ , case-insensitive
4748 , containers
4849 , lens
4950 , mtl
@@ -56,8 +57,12 @@ library
5657 , xml-conduit
5758 , xml-lens
5859 exposed-modules: Text.XML.DOM.Parser
59− , Text.XML.DOM.Parser.Class
60+ , Text.XML.DOM.Parser.Attributes
61+ , Text.XML.DOM.Parser.Buildable
6062 , Text.XML.DOM.Parser.Combinators
63+ , Text.XML.DOM.Parser.Content
64+ , Text.XML.DOM.Parser.FromAttribute
65+ , Text.XML.DOM.Parser.FromDom
6166 , Text.XML.DOM.Parser.Types
6267
6368test-suite test
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+52−182
@@ -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,47 +15,31 @@ module Text.XML.DOM.Parser.Combinators
1515 , ignoreElem
1616 , ignoreEmpty
1717 , ignoreBlank
18− -- * Getting current element's properties
19− , getCurrentName
20− , getCurrentContent
21− , getCurrentAttributes
22− , getCurrentAttribute
23− -- * Current element's checks
24− , checkCurrentName
25− -- * Parsing element's content
26− , parseContent
27− , readContent
28− , maybeReadContent
29− -- * Parsing attributes
30− , parseAttribute
31− , parseAttributeMaybe
3218 ) where
3319
34−import Control.Lens
35−import Control.Monad.Except
36−import Control.Monad.Reader
37−import Data.List.NonEmpty (NonEmpty(..))
38−import qualified Data.Map.Strict as M
39−import Data.Monoid
40−import Data.Text (Text)
41−import qualified Data.Text as T
42−import Data.Traversable
43−import Data.Typeable
44−import Text.Read
45−import Text.XML
46−import Text.XML.DOM.Parser.Types
47−import Text.XML.Lens
48−
49−
50−-- | 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.Monoid
26+import Data.Text as T
27+import Data.Traversable
28+import Text.XML
29+import Text.XML.DOM.Parser.Buildable
30+import Text.XML.DOM.Parser.Types
31+import Text.XML.Lens
32+
33+
34+-- | Generic function to traverse arbitrary inner elements.
5135traverseElems
5236 :: (Monad m, Foldable g, Traversable f)
5337 => ([Element] -> DomParserT g m (f (DomPath, Element)))
5438 -- ^ Takes list of current elements and returns container with
55− -- pairs of subpath (relatively to current element) and element
39+ -- pairs of subpath (relatively to current elements) and element
5640 -- to run parser in
5741 -> DomParserT Identity m a
58− -- ^ Parser will be runned for each element found in traversable
42+ -- ^ Parser to run for each element found in traversable 'f'
5943 -> DomParserT g m (f a)
6044traverseElems trav parser = do
6145 pd <- ask
@@ -65,68 +49,73 @@ traverseElems trav parser = do
6549 newpd = ParserData
6650 { _pdElements = Identity e
6751 , _pdPath = pd ^. pdPath <> subpath }
68− -- type of reader has been changed, local won't work here
69− magnify (to $ const newpd) parser
52+ lift $ runReaderT parser newpd
7053
71−-- | Takes function filtering
54+-- | Traverses elements located in same path using filtering function
7255inFilteredTrav
73− :: (Monad m, Foldable g, DomTraversable f)
56+ :: (Monad m, Foldable g, Buildable f)
7457 => ([Element] -> (DomPath, [Element]))
75− -- ^ Function returning some filtered elements with path suffixes which will
76− -- be appended to parser's state
58+ -- ^ Takes list of current elements and returns some descendants
59+ -- subset and path this descendants located at. Path is should be
60+ -- same for all descendants and required for error message
7761 -> DomParserT Identity m a
7862 -> DomParserT g m (f a)
7963inFilteredTrav deeper = traverseElems trav
8064 where
8165 trav e = do
8266 let (path, elems) = deeper e
83− case buildDomTraversable elems of
67+ case build elems of
8468 Nothing -> throwParserError $ PENotFound . (<> path)
8569 Just tr -> return $ fmap (path,) tr
8670
71+-- | Runs parser arbitrary times, depending on 'Buildable' instance of
72+-- 'f'. For example if 'f' becomes 'NonEmpty' then 'inElemTrav' finds
73+-- @one or more@ elements matched by given 'ElemMatcher' and run
74+-- parser in each found element, then returns @NonEmpty a@ of results.
8775inElemTrav
88− :: (Monad m, Foldable g, DomTraversable f)
89− => Text
76+ :: (Monad m, Foldable g, Buildable f)
77+ => ElemMatcher -- ^ Tag(s) matcher to traverse in
9078 -> DomParserT Identity m a
9179 -> DomParserT g m (f a)
9280inElemTrav n = inFilteredTrav deeper
9381 where
94− deeper = ([n],) . toListOf (folded . nodes . folded . _Element . ell n)
82+ elemsFold = folded . nodes . folded . _Element . elMatch n
83+ deeper = (DomPath [_emShow n],) . toListOf elemsFold
9584
96−-- | Runs parser inside first children element with given name
85+-- | Runs parser inside first children element matched by macher
9786inElem
9887 :: (Monad m, Foldable g)
99− => Text
88+ => ElemMatcher
10089 -> DomParserT Identity m a
10190 -> DomParserT g m a
10291inElem n = fmap runIdentity . inElemTrav n
10392
10493inElemAll
10594 :: (Monad m, Foldable g)
106− => Text
95+ => ElemMatcher
10796 -> DomParserT Identity m a
10897 -> DomParserT g m [a]
10998inElemAll = inElemTrav
11099
111100inElemMay
112101 :: (Monad m, Foldable g)
113− => Text
102+ => ElemMatcher
114103 -> DomParserT Identity m a
115104 -> DomParserT g m (Maybe a)
116105inElemMay = inElemTrav
117106
118107inElemNe
119108 :: (Monad m, Foldable g)
120− => Text
109+ => ElemMatcher
121110 -> DomParserT Identity m a
122111 -> DomParserT g m (NonEmpty a)
123112inElemNe = inElemTrav
124113
125114{- | Dive given parser's current tags set into the given path. The @divePath
126−["a", "b"]@ differs from @inElem "a" . inElem "b"@. Namely the first variant
115+["a", "b"]@ differs from @inElem "a" $ inElem "b"@. Namely the first variant
127116will not fail if occured tag "a" which does not contains tag "b". This
128117behaviour is desireable when you dont want to parse whole XML and just want
129−to pull tags in some path. The other difference is in traversing inner
118+to pull tags located in some path. The other difference is in traversing inner
130119elements. Consider this code
131120
132121@
@@ -144,26 +133,31 @@ which translates like: @a > b > c > fromDom@
144133As you can see, inElem always takes first element and runs inner parser in this
145134single element, unlike 'divePath' which runs inner parser @in all@ descendants
146135in given path.
136+
137+Note also that 'divePath' takes parser parameterized by @[]@ not by
138+'Identity'. This because when you dive using some path you will get a
139+list of found elements and all these elements will be @current@ for
140+parser.
147141-}
148142
149143divePath
150144 :: forall m g a
151145 . (Monad m, Foldable g)
152− => [Text]
146+ => [ElemMatcher]
153147 -> DomParserT [] m a
154148 -> DomParserT g m a
155149divePath path = magnify $ to modElems
156150 where
157151 modElems
158152 = over pdElements (toListOf $ folded . diver)
159− . over pdPath (<> path)
153+ . over pdPath (<> DomPath (L.map _emShow path))
160154 diver :: Fold Element Element
161− diver = foldr (.) id $ map toDive path
162− toDive n = nodes . folded . _Element . ell n
155+ diver = F.foldr (.) id $ L.map toDive path
156+ toDive n = nodes . folded . _Element . elMatch n
163157
164158diveElem
165159 :: (Monad m, Foldable g)
166− => Text
160+ => ElemMatcher
167161 -> DomParserT [] m a
168162 -> DomParserT g m a
169163diveElem p = divePath [p]
@@ -190,7 +184,7 @@ ignoreEmpty
190184 -> DomParserT Identity m (Maybe a)
191185ignoreEmpty = ignoreElem test
192186 where
193− test e = null $ e ^. nodes
187+ test e = L.null $ e ^. nodes
194188
195189-- | If all current elements contains blank content, or contains nothing at all
196190-- , then returns Nothing, else runs parser.
@@ -204,130 +198,6 @@ ignoreBlank = ignoreElem test
204198 let
205199 elems = e ^.. nodes . folded . _Element
206200 cont = mconcat $ e ^.. nodes . folded . _Content
207− in if | not $ null elems -> False
201+ in if | not $ L.null elems -> False
208202 | T.null $ T.strip cont -> True
209203 | otherwise -> False
210−
211−-- | Returns name of current element.
212−--
213−-- @since 1.0.0
214−getCurrentName :: (Monad m) => DomParserT Identity m Text
215−getCurrentName = view $ pdElements . to runIdentity . localName
216−
217−-- | If name of current tag differs from first argument throws 'PENotFound' with
218−-- tag name replaced in last path's segment. Useful for checking root
219−-- document's element name.
220−checkCurrentName
221− :: (Monad m)
222− => Text
223− -> DomParserT Identity m ()
224−checkCurrentName n = do
225− cn <- getCurrentName
226− unless (cn == n) $ do
227− p <- view pdPath
228− let pinit = if null p then [] else init p
229− throwError $ ParserErrors [PENotFound $ pinit ++ [n]]
230− return ()
231−
232−-- | Get current content. If current element contains no content or
233−-- have inner elements then Nothing returned
234−--
235−-- @since 1.0.0
236−getCurrentContent :: (Monad m) => DomParserT Identity m (Maybe Text)
237−getCurrentContent = do
238− nds <- view $ pdElements . to runIdentity . nodes
239− let
240− els :: [Element]
241− els = nds ^.. folded . _Element
242− conts :: [Text]
243− conts = nds ^.. folded . _Content
244− return $ if
245− | not $ null els -> Nothing
246− | null conts -> Nothing
247− | otherwise -> Just $ mconcat conts
248−
249−-- | Parses content inside current tag. It expects current element set
250−-- consists of exactly ONE element. If current element does not
251−-- contains content or have other elements as childs then throws error
252−parseContent
253− :: (Monad m)
254− => (Text -> Either Text a)
255− -- ^ Content parser, return error msg if value is not parsed
256− -> DomParserT Identity m a
257−parseContent parse = getCurrentContent >>= \case
258− Nothing -> throwParserError PEContentNotFound
259− Just c -> case parse c of
260− Left e -> throwParserError $ PEWrongFormat e
261− Right a -> return a
262−
263−-- | If reader returns 'Nothing' then resulting function returns 'Left
264−-- "error message"'
265−--
266−-- @since 1.0.0
267−maybeReadContent
268− :: forall a
269− . (Typeable a)
270− => (Text -> Maybe a)
271− -- ^ Content or attribute reader
272− -> Text
273− -- ^ Content or attribute value
274− -> Either Text a
275−maybeReadContent f t = maybe (Left msg) Right $ f t
276− where
277− msg = "Not readable " <> n <> ": " <> t
278− n = T.pack $ show $ typeRep (Proxy :: Proxy a)
279−
280−-- | Tries to read given text to value using 'Read'. Useful to use
281−-- with 'parseContent' and 'parseAttribute'
282−readContent
283− :: (Read a, Typeable a)
284− => Text
285− -> Either Text a
286−readContent = maybeReadContent $ readMaybe . T.unpack . T.strip
287−
288−-- | Retuns map of attributes of current element
289−--
290−-- @since 1.0.0
291−getCurrentAttributes :: (Monad m) => DomParserT Identity m (M.Map Name Text)
292−getCurrentAttributes = view $ pdElements . to runIdentity . attrs
293−
294−-- | Returns element with given name or 'Nothing'
295−--
296−-- @since 1.0.0
297−getCurrentAttribute :: (Monad m) => Text -> DomParserT Identity m (Maybe Text)
298−getCurrentAttribute attrName'
299− = preview $ pdElements . to runIdentity . attr attrName
300− where
301− attrName = Name attrName' Nothing Nothing
302−
303−-- | Parses attribute with given name, throws error if attribute is not found.
304−--
305−-- @since 1.0.0
306−parseAttribute
307− :: (Monad m)
308− => Text
309− -- ^ Attribute name
310− -> (Text -> Either Text a)
311− -- ^ Attribute content parser
312− -> DomParserT Identity m a
313−parseAttribute attrName parser =
314− parseAttributeMaybe attrName parser >>= \case
315− Nothing -> throwParserError $ PEAttributeNotFound attrName
316− Just a -> return a
317−
318−-- | Parses attribute with given name.
319−--
320−-- @since 1.0.0
321−parseAttributeMaybe
322− :: (Monad m)
323− => Text
324− -- ^ Attribute name
325− -> (Text -> Either Text a)
326− -- ^ Attribute content parser
327− -> DomParserT Identity m (Maybe a)
328−parseAttributeMaybe attrName parser =
329− getCurrentAttribute attrName >>= \case
330− Nothing -> return Nothing
331− Just aval -> case parser aval of
332− Left err -> throwParserError $ PEAttributeWrongFormat attrName err
333− Right a -> return $ Just a
src/Text/XML/DOM/Parser/Content.hsnew file+128
@@ -0,0 +1,128 @@
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 Text.Read
24+import Text.XML.DOM.Parser.Types
25+import Text.XML.Lens
26+
27+
28+-- | Parses content inside current tag. It expects current element set
29+-- consists of exactly ONE element.
30+parseContent
31+ :: (Monad m)
32+ => (Text -> Either Text a)
33+ -- ^ Content parser, return error msg if value is not parsed
34+ -> DomParserT Identity m a
35+parseContent parse = getCurrentContent >>= \case
36+ Nothing -> throwParserError PEContentNotFound
37+ Just c -> case parse c of
38+ Left e -> throwParserError $ PEContentWrongFormat e
39+ Right a -> return a
40+
41+
42+-- | Returns name of current element.
43+--
44+-- @since 1.0.0
45+getCurrentName :: (Monad m) => DomParserT Identity m Name
46+getCurrentName = view $ pdElements . to runIdentity . name
47+
48+-- | If name of current tag differs from first argument throws 'PENotFound' with
49+-- tag name replaced in last path's segment. Useful for checking root
50+-- document's element name.
51+checkCurrentName
52+ :: (Monad m)
53+ => NameMatcher
54+ -> DomParserT Identity m ()
55+checkCurrentName n = do
56+ cn <- getCurrentName
57+ unless ((n ^. nmMatch) cn) $ do
58+ p <- view pdPath
59+ let pinit = if L.null (unDomPath p) then [] else L.init $ unDomPath p
60+ throwError $ ParserErrors [PENotFound $ DomPath $ pinit ++ [_nmShow n]]
61+ return ()
62+
63+-- | Get current content. If current element contains no content or
64+-- have inner elements then Nothing returned
65+--
66+-- @since 1.0.0
67+getCurrentContent :: (Monad m) => DomParserT Identity m (Maybe Text)
68+getCurrentContent = do
69+ nds <- view $ pdElements . to runIdentity . nodes
70+ let
71+ els :: [Element]
72+ els = nds ^.. folded . _Element
73+ conts :: [Text]
74+ conts = nds ^.. folded . _Content
75+ return $ if
76+ | not $ L.null els -> Nothing
77+ | L.null conts -> Nothing
78+ | otherwise -> Just $ mconcat conts
79+
80+-- | If reader returns 'Nothing' then resulting function returns 'Left
81+-- "error message"'. 'Typeable' is used for generating usefull error
82+-- message.
83+--
84+-- @since 1.0.0
85+maybeReadContent
86+ :: forall a
87+ . (Typeable a)
88+ => (Text -> Maybe a)
89+ -- ^ Content or attribute reader
90+ -> Text
91+ -- ^ Content or attribute value
92+ -> Either Text a
93+maybeReadContent f t = maybe (Left msg) Right $ f t
94+ where
95+ msg = "Not readable " <> n <> ": " <> t
96+ n = T.pack $ show $ typeRep (Proxy :: Proxy a)
97+
98+
99+-- | Tries to read given text to value using 'Read'. Useful to use
100+-- with 'parseContent' and 'parseAttribute'. Content is stripped
101+-- before reading.
102+readContent
103+ :: (Read a, Typeable a)
104+ => Text
105+ -> Either Text a
106+readContent = maybeReadContent $ readMaybe . T.unpack . T.strip
107+
108+
109+-- | @since 1.0.0
110+readBool :: Text -> Either Text Bool
111+readBool t =
112+ let
113+ lowt = T.toLower $ T.strip t
114+ tvals = ["y", "yes", "t", "true", "1"]
115+ fvals = ["n", "no", "f", "false", "0"]
116+ in if
117+ | lowt `elem` tvals -> Right True
118+ | lowt `elem` fvals -> Right False
119+ | otherwise ->
120+ Left $ "Could not read " <> t <> " as Bool"
121+
122+-- | Expects text to be single character
123+--
124+-- @since 1.0.0
125+readChar :: Text -> Either Text Char
126+readChar t = case T.unpack $ T.strip t of
127+ [c] -> Right c
128+ _ -> 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+48−97
@@ -1,4 +1,4 @@
1−module Text.XML.DOM.Parser.Class
1+module Text.XML.DOM.Parser.FromDom
22 ( -- * FromDom
33 FromDom(..)
44 , proxyFromDom
@@ -8,31 +8,26 @@ module Text.XML.DOM.Parser.Class
88 , textFromDom
99 , stringFromDom
1010 , charFromDom
11− , readChar
1211 , intFromDom
1312 , integerFromDom
1413 , doubleFromDom
1514 , fixedFromDom
1615 , boolFromDom
17− , readBool
1816 , unitFromDom
1917 , voidFromDom
2018 , scientificFromDom
21− -- * FromAttribute
22− , FromAttribute(..)
2319 ) where
2420
2521import Control.Applicative
2622import Control.Lens
2723import Data.Fixed
28−import Data.Monoid
2924import Data.OpenUnion
3025import Data.Scientific
3126import Data.Text as T hiding (empty)
3227import Data.Typeable
3328import Data.Void
3429import Text.XML
35−import Text.XML.DOM.Parser.Combinators
30+import Text.XML.DOM.Parser.Content
3631import Text.XML.DOM.Parser.Types
3732import TypeFun.Data.List hiding (Union)
3833
@@ -43,43 +38,84 @@ proxyFromDom
4338 -> DomParserT Identity m a
4439proxyFromDom _ = fromDom
4540
46−-- | Class of types which can be parsed from single XML element.
41+-- | Class of types which can be parsed from single XML element. The
42+-- method 'fromDom' is convenient default to use with 'inElem'
4743class FromDom a where
4844 fromDom :: (Monad m) => DomParserT Identity m a
4945
5046instance FromDom () where
5147 fromDom = unitFromDom
5248
49+-- | Always successfully parses any DOM to @()@
50+unitFromDom :: (Monad m) => DomParserT Identity m ()
51+unitFromDom = pure ()
52+
5353instance FromDom Void where
5454 fromDom = voidFromDom
5555
56+-- | Never parses successfully. It is just 'empty'
57+voidFromDom :: (Monad m) => DomParserT Identity m Void
58+voidFromDom = empty
59+
5660instance FromDom Text where
5761 fromDom = textFromDom
5862
63+textFromDom :: (Monad m) => DomParserT Identity m Text
64+textFromDom = parseContent Right
65+
5966instance FromDom String where
6067 fromDom = stringFromDom
6168
69+stringFromDom :: (Monad m) => DomParserT Identity m String
70+stringFromDom = parseContent $ Right . T.unpack
71+
6272instance FromDom Char where
6373 fromDom = charFromDom
6474
75+charFromDom :: (Monad m) => DomParserT Identity m Char
76+charFromDom = parseContent readChar
77+
6578instance FromDom Int where
6679 fromDom = intFromDom
6780
81+intFromDom :: (Monad m) => DomParserT Identity m Int
82+intFromDom = parseContent readContent
83+
6884instance FromDom Integer where
6985 fromDom = integerFromDom
7086
87+integerFromDom :: (Monad m) => DomParserT Identity m Integer
88+integerFromDom = parseContent readContent
89+
7190instance FromDom Double where
7291 fromDom = doubleFromDom
7392
93+doubleFromDom :: (Monad m) => DomParserT Identity m Double
94+doubleFromDom = parseContent readContent
95+
7496instance (HasResolution a, Typeable a) => FromDom (Fixed a) where
7597 fromDom = fixedFromDom
7698
99+fixedFromDom
100+ :: (Monad m, Typeable a, HasResolution a)
101+ => DomParserT Identity m (Fixed a)
102+fixedFromDom = parseContent readContent
103+
77104instance FromDom Scientific where
78105 fromDom = scientificFromDom
79106
107+scientificFromDom :: Monad m => DomParserT Identity m Scientific
108+scientificFromDom = parseContent readContent
109+
80110instance FromDom Bool where
81111 fromDom = boolFromDom
82112
113+-- | Expects content to be y, yes, t, true or 1 for True Values n, no,
114+-- f, false or 0 for False. Case is not significant, blank characters
115+-- are striped.
116+boolFromDom :: (Monad m) => DomParserT Identity m Bool
117+boolFromDom = parseContent readBool
118+
83119instance FromDom (Union '[]) where
84120 fromDom = empty
85121
@@ -91,99 +127,14 @@ instance
91127 fromDom = (liftUnion <$> (proxyFromDom (Proxy :: Proxy a)))
92128 <|> (reUnion <$> (proxyFromDom (Proxy :: Proxy (Union as))))
93129
94−instance FromDom Element where
95− fromDom = elementFromDom
96−
97−elementFromDom :: (Monad m) => DomParserT Identity m Element
98−elementFromDom = view $ pdElements . to runIdentity
99−
100130unionFromDom
101131 :: (Monad m, FromDom (Union as))
102132 => proxy as
103133 -> DomParserT Identity m (Union as)
104134unionFromDom _ = fromDom
105135
106−textFromDom :: (Monad m) => DomParserT Identity m Text
107−textFromDom = parseContent Right
108−
109−stringFromDom :: (Monad m) => DomParserT Identity m String
110−stringFromDom = parseContent $ Right . T.unpack
111−
112−charFromDom :: (Monad m) => DomParserT Identity m Char
113−charFromDom = parseContent readChar
114−
115−-- | Expects text to be single character
116−--
117−-- @since 1.0.0
118−readChar :: Text -> Either Text Char
119−readChar t = case T.unpack $ T.strip t of
120− [c] -> Right c
121− _ -> Left "Should have exactly one non-blank character"
122−
123−intFromDom :: (Monad m) => DomParserT Identity m Int
124−intFromDom = parseContent readContent
125−
126−integerFromDom :: (Monad m) => DomParserT Identity m Integer
127−integerFromDom = parseContent readContent
128−
129−doubleFromDom :: (Monad m) => DomParserT Identity m Double
130−doubleFromDom = parseContent readContent
131−
132−fixedFromDom
133− :: (Monad m, Typeable a, HasResolution a)
134− => DomParserT Identity m (Fixed a)
135−fixedFromDom = parseContent readContent
136−
137−scientificFromDom :: Monad m => DomParserT Identity m Scientific
138−scientificFromDom = parseContent readContent
139−
140−-- | Expects content to be y, yes, t, true or 1 for True value. n, no,
141−-- f, false or 0 for False value. Case is not significant, blank
142−-- characters are striped.
143−boolFromDom :: (Monad m) => DomParserT Identity m Bool
144−boolFromDom = parseContent readBool
145−
146−-- | @since 1.0.0
147−readBool :: Text -> Either Text Bool
148−readBool t =
149− let
150− lowt = T.toLower $ T.strip t
151− tvals = ["y", "yes", "t", "true", "1"]
152− fvals = ["n", "no", "f", "false", "0"]
153− in if
154− | lowt `elem` tvals -> Right True
155− | lowt `elem` fvals -> Right False
156− | otherwise ->
157− Left $ "Could not read " <> t <> " as Bool"
158−
159−-- | Always successfully parses any DOM to @()@
160−unitFromDom :: (Monad m) => DomParserT Identity m ()
161−unitFromDom = pure ()
162−
163−-- | Never parses successfully. It is just 'mzero'
164−voidFromDom :: (Monad m) => DomParserT Identity m Void
165−voidFromDom = empty
166−
167−class FromAttribute a where
168− fromAttribute :: Text -> Either Text a
169−
170−instance FromAttribute () where
171− fromAttribute _ = Right ()
172−
173−instance FromAttribute Text where
174− fromAttribute = Right
175−
176−instance FromAttribute Char where
177− fromAttribute = readChar
178−
179−instance FromAttribute Int where
180− fromAttribute = readContent
181−
182−instance FromAttribute Integer where
183− fromAttribute = readContent
184−
185−instance (Typeable a, HasResolution a) => FromAttribute (Fixed a) where
186− fromAttribute = readContent
136+instance FromDom Element where
137+ fromDom = elementFromDom
187138
188−instance FromAttribute Scientific where
189− fromAttribute = readContent
139+elementFromDom :: (Monad m) => DomParserT Identity m Element
140+elementFromDom = view $ pdElements . to runIdentity
src/Text/XML/DOM/Parser/Types.hs+145−63
@@ -1,6 +1,19 @@
11module Text.XML.DOM.Parser.Types
2− ( -- * Parser internals
3− DomPath
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(..)
417 , ParserError(..)
518 , pePath
619 , peDetails
@@ -10,47 +23,140 @@ module Text.XML.DOM.Parser.Types
1023 , ParserData(..)
1124 , pdElements
1225 , pdPath
13− -- * Parser type
1426 , DomParserT
1527 , DomParser
1628 , runDomParserT
1729 , runDomParser
1830 -- * Auxiliary
19− , DomTraversable(..)
2031 , throwParserError
21− , throwWrongFormat
2232 ) where
2333
24−import Control.Exception
25−import Control.Lens
26−import Control.Monad.Except
27−import Control.Monad.Reader
28−import Data.List.NonEmpty (NonEmpty(..))
29−import qualified Data.List.NonEmpty as NE
30−import Data.Maybe
31−import Data.Text (Text)
32−import GHC.Generics (Generic)
33−import Text.XML
34−import Text.XML.Lens
35−
36−type DomPath = [Text]
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+makeLenses ''NameMatcher
92+
93+-- | Instance use 'matchCILocalName' as most general and liberal
94+-- matching strategy (while XML is often malformed).
95+--
96+-- @since 2.0.0
97+instance IsString NameMatcher where
98+ fromString = matchCILocalName . T.pack
99+
100+instance Show NameMatcher where
101+ show = T.unpack . _nmShow
102+
103+-- | Makes matcher which matches only local part of name igoring
104+-- namespace and prefix. Local name matching is case sensitive.
105+--
106+-- @since 2.0.0
107+matchLocalName :: Text -> NameMatcher
108+matchLocalName tname = NameMatcher
109+ { _nmMatch = \n -> nameLocalName n == tname
110+ , _nmShow = tname
111+ }
112+
113+-- | Makes matcher which matches only local part of name igoring
114+-- namespace and prefix. Local name matching is case insensitive. This
115+-- is the most common case.
116+--
117+-- @since 2.0.0
118+matchCILocalName :: Text -> NameMatcher
119+matchCILocalName tname = NameMatcher
120+ { _nmMatch = \n -> CI.mk (nameLocalName n) == CI.mk tname
121+ , _nmShow = tname
122+ }
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, Monoid)
37138
38139-- | DOM parser error description.
39140data ParserError
40141 -- | Tag not found which should be.
41142 = PENotFound
42143 { _pePath :: DomPath
144+ -- ^ Path of element error occured in
43145 }
44146
45− -- | Tag contents has wrong format, (could not read text to value)
46− | PEWrongFormat
47− { _peDetails :: Text
48− , _pePath :: DomPath -- ^ path of element
147+ -- | Expected attribute but not found
148+ --
149+ -- @since 1.0.0
150+ | PEAttributeNotFound
151+ { _peAttributeName :: NameMatcher
152+ , _pePath :: DomPath
49153 }
50154
51155 -- | Could not parse attribute
156+ --
157+ -- @since 1.0.0
52158 | PEAttributeWrongFormat
53− { _peAttributeName :: Text
159+ { _peAttributeName :: NameMatcher
54160 , _peDetails :: Text
55161 , _pePath :: DomPath
56162 }
@@ -60,17 +166,17 @@ data ParserError
60166 { _pePath :: DomPath
61167 }
62168
63− -- | Expected attribute but not found
64− | PEAttributeNotFound
65− { _peAttributeName :: Text
66− , _pePath :: DomPath
169+ -- | Tag contents has wrong format, (could not read text to value)
170+ | PEContentWrongFormat
171+ { _peDetails :: Text
172+ , _pePath :: DomPath
67173 }
68174
69175 -- | Some other error
70176 | PEOther
71177 { _peDetails :: Text
72178 , _pePath :: DomPath
73− } deriving (Eq, Ord, Show, Generic)
179+ } deriving (Show, Generic)
74180
75181makeLenses ''ParserError
76182
@@ -78,15 +184,16 @@ instance Exception ParserError
78184
79185newtype ParserErrors = ParserErrors
80186 { unParserErrors :: [ParserError]
81− } deriving (Ord, Eq, Show, Monoid, Generic)
187+ } deriving (Show, Monoid, Generic)
82188
83189makePrisms ''ParserErrors
84190
85191instance Exception ParserErrors
86192
87193
88−{- | Parser scope parser runs in. Functor argument is usually @Identity@ or
89−@[]@.
194+{- | Parser scope.
195+
196+Functor argument is usually @Identity@ or @[]@.
90197
91198If functor is @Identity@ then parser expects exactly ONE current element. This
92199is common behavior for content parsers, or parsers expecting strict XML
@@ -119,10 +226,11 @@ runDomParserT
119226 -> DomParserT Identity m a
120227 -> m (Either ParserErrors a)
121228runDomParserT doc par =
122− let pd = ParserData
123− { _pdElements = doc ^. root . to pure
124− , _pdPath = [doc ^. root . localName]
125− }
229+ let
230+ pd = ParserData
231+ { _pdElements = doc ^. root . to pure
232+ , _pdPath = DomPath [doc ^. root . name . to nameLocalName]
233+ }
126234 in runExceptT $ runReaderT par pd
127235
128236runDomParser
@@ -131,36 +239,10 @@ runDomParser
131239 -> Either ParserErrors a
132240runDomParser doc par = runIdentity $ runDomParserT doc par
133241
134−-- | Class of traversable functors which may be constructed from list. Or may
135−-- not.
136−class Traversable f => DomTraversable f where
137− -- | If method return Nothing this means we can not build traversable from
138− -- given list. In this case combinator should fail traversing.
139− buildDomTraversable :: [a] -> Maybe (f a)
140−
141−instance DomTraversable Identity where
142− buildDomTraversable = fmap Identity . listToMaybe
143−
144−instance DomTraversable [] where
145− buildDomTraversable = Just
146−
147−instance DomTraversable Maybe where
148− buildDomTraversable = Just . listToMaybe
149−
150−instance DomTraversable NonEmpty where
151− buildDomTraversable = NE.nonEmpty
152−
153242throwParserError
154243 :: (MonadError ParserErrors m, MonadReader (ParserData f) m)
155− => ([Text] -> ParserError)
244+ => (DomPath -> ParserError)
156245 -> m a
157246throwParserError mkerr = do
158247 path <- view pdPath
159− throwError $ ParserErrors [mkerr path]
160−
161−-- | Throw 'PEWrongFormat' as very common case
162−throwWrongFormat
163− :: (MonadError ParserErrors m, MonadReader (ParserData f) m)
164− => Text
165− -> m a
166−throwWrongFormat err = throwParserError $ PEWrongFormat err
248+ throwError $ ParserErrors $ [mkerr path]
test/Main.hs+6−6
@@ -33,7 +33,7 @@ testStructureAttributes = do
3333 int <- parseAttribute "int" readContent
3434 bool <- parseAttribute "bool" readBool
3535 return $ TestStructure
36− { tsName = name
36+ { tsName = nameLocalName name
3737 , tsInts = [int]
3838 , tsBools = pure bool }
3939
@@ -193,7 +193,7 @@ specParserFailedPath
193193 :: (Show a)
194194 => String -- ^ Name of spec
195195 -> Document
196− -> [Text] -- ^ Expteced path in error
196+ -> DomPath -- ^ Expteced path in error
197197 -> DomParser Identity a
198198 -> Spec
199199specParserFailedPath name doc path parser = it name $ example $ do
@@ -226,7 +226,7 @@ combinationsSpec = do
226226 describe "deep" $ do
227227 let parser = inElem "a" $ inElem "b" textFromDom
228228 specParserFailedPath "docDeepMultiple2" docDeepMultiple2
229− ["root", "a", "b"] parser
229+ (DomPath ["root", "a", "b"]) parser
230230 -- should fail here because first tag "a" does not contains
231231 -- tag "b" which is expected by parser @inElem "b"@
232232
@@ -276,7 +276,7 @@ combinationsSpec = do
276276 specParserEq "docDeepMultiple1" docDeepMultiple1 "content1" parser
277277 specParserEq "docDeepMultiple2" docDeepMultiple2 "content1" parser
278278 describe "fails" $ do
279− specParserFailedPath "docSimple" docSimple ["root", "a", "b"] parser
279+ specParserFailedPath "docSimple" docSimple (DomPath ["root", "a", "b"]) parser
280280 describe "multiple elements" $ do
281281 let
282282 parser = diveElem "a" $ inElemAll "b" textFromDom
@@ -311,9 +311,9 @@ combinationsSpec = do
311311 inElem "a" $ do
312312 checkCurrentName "a"
313313 describe "fails" $ do
314− specParserFailedPath "root element" docSimple ["toor"] $ do
314+ specParserFailedPath "root element" docSimple (DomPath ["toor"]) $ do
315315 checkCurrentName "toor"
316− specParserFailedPath "inner element" docSimple ["root", "b"] $ do
316+ specParserFailedPath "inner element" docSimple (DomPath ["root", "b"]) $ do
317317 inElem "a" $ checkCurrentName "b"
318318
319319contentSpec :: Spec