git.iko.soy/typeable/dom-parser
on master help sign in
15 files changed+639−376
since splitsnapshots
unifiedsplit
.travis.yml+7−5 CHANGELOG.md+44 README.md+5 dom-parser.cabal+15−11 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+49−184 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+42−115 src/Text/XML/DOM/Parser/Types.hs+82−53 src/Text/XML/Soap.hs+46 stack.yaml+1−6
.travis.yml+7−5
@@ -4,6 +4,12 @@ sudo: false
44# Do not choose a language; we provide our own build tools.
55language: generic
66
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+
713# Caching so the next build will be fast too.
814cache:
915 directories:
@@ -21,10 +27,6 @@ before_install:
2127- export PATH=$HOME/.local/bin:$PATH
2228- travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack'
2329
24−install:
25−# Build dependencies
26−- stack --no-terminal --install-ghc build --only-dependencies
27−
2830script:
2931# Build the package, its tests, and its docs and run the tests
30−- stack --no-terminal test --haddock --no-haddock-deps
32+- stack --no-terminal --install-ghc test --haddock --no-haddock-deps
CHANGELOG.md+44
@@ -1,10 +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+
345## 1.0.0
46+
447* Added support for parsing attributes
548* Changed public interface for 'parseContent'
649* Added useful functions
750* Dropped unnecessary dependency from shakespeare
851
952## 0.1.0
53+
1054* 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+15−11
@@ -1,5 +1,5 @@
11name: dom-parser
2−version: 1.0.0
2+version: 3.2.0
33synopsis: Simple monadic DOM parser
44license: MIT
55license-file: LICENSE
@@ -7,12 +7,13 @@ author: Typeable.io contributors
77maintainer: makeit@typeable.io
88category: XML
99build-type: Simple
10−extra-source-files: CHANGELOG.md
1110cabal-version: >=1.10
12−
13−extra-source-files: CHANGELOG.md
14−
1511homepage: 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
1617
1718source-repository head
1819 type: git
@@ -43,23 +44,26 @@ library
4344 , TypeFamilies
4445 , TypeOperators
4546 , UndecidableInstances
46− build-depends: base >= 4.7 && < 5
47+ build-depends: base >= 4.11 && < 5
4748 , case-insensitive
4849 , containers
4950 , lens
50− , mtl
51− , open-union >= 0.2
51+ , mtl < 2.3 || > 2.3
5252 , scientific
53− , semigroups
5453 , text
5554 , transformers
56− , type-fun
5755 , xml-conduit
56+ , xml-conduit-writer
5857 , xml-lens
5958 exposed-modules: Text.XML.DOM.Parser
60− , Text.XML.DOM.Parser.Class
59+ , Text.XML.DOM.Parser.Attributes
60+ , Text.XML.DOM.Parser.Buildable
6161 , Text.XML.DOM.Parser.Combinators
62+ , Text.XML.DOM.Parser.Content
63+ , Text.XML.DOM.Parser.FromAttribute
64+ , Text.XML.DOM.Parser.FromDom
6265 , Text.XML.DOM.Parser.Types
66+ , Text.XML.Soap
6367
6468test-suite test
6569 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+49−184
@@ -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,30 @@ 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
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
4831
4932
50−-- | Generic function to traverse arbitrary inner cursors.
33+-- | Generic function to traverse arbitrary inner elements.
5134traverseElems
5235 :: (Monad m, Foldable g, Traversable f)
5336 => ([Element] -> DomParserT g m (f (DomPath, Element)))
5437 -- ^ Takes list of current elements and returns container with
55− -- pairs of subpath (relatively to current element) and element
38+ -- pairs of subpath (relatively to current elements) and element
5639 -- to run parser in
5740 -> DomParserT Identity m a
58− -- ^ Parser will be runned for each element found in traversable
41+ -- ^ Parser to run for each element found in traversable 'f'
5942 -> DomParserT g m (f a)
6043traverseElems trav parser = do
6144 pd <- ask
@@ -65,68 +48,73 @@ traverseElems trav parser = do
6548 newpd = ParserData
6649 { _pdElements = Identity e
6750 , _pdPath = pd ^. pdPath <> subpath }
68− -- type of reader has been changed, local won't work here
69− magnify (to $ const newpd) parser
51+ lift $ runReaderT parser newpd
7052
71−-- | Takes function filtering
53+-- | Traverses elements located in same path using filtering function
7254inFilteredTrav
73− :: (Monad m, Foldable g, DomTraversable f)
55+ :: (Monad m, Foldable g, Buildable f)
7456 => ([Element] -> (DomPath, [Element]))
75− -- ^ Function returning some filtered elements with path suffixes which will
76− -- be appended to parser's state
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
7760 -> DomParserT Identity m a
7861 -> DomParserT g m (f a)
7962inFilteredTrav deeper = traverseElems trav
8063 where
8164 trav e = do
8265 let (path, elems) = deeper e
83− case buildDomTraversable elems of
66+ case build elems of
8467 Nothing -> throwParserError $ PENotFound . (<> path)
8568 Just tr -> return $ fmap (path,) tr
8669
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.
8774inElemTrav
88− :: (Monad m, Foldable g, DomTraversable f)
89− => NameMatcher -- ^ Name of tag to traverse in
75+ :: (Monad m, Foldable g, Buildable f)
76+ => ElemMatcher -- ^ Tag(s) matcher to traverse in
9077 -> DomParserT Identity m a
9178 -> DomParserT g m (f a)
9279inElemTrav n = inFilteredTrav deeper
9380 where
94− deeper = (DomPath [_nmShow n],) . toListOf (folded . nodes . folded . _Element . elMatchName n)
81+ elemsFold = folded . nodes . folded . _Element . elMatch n
82+ deeper = (DomPath [_emShow n],) . toListOf elemsFold
9583
96−-- | Runs parser inside first children element with given name
84+-- | Runs parser inside first children element matched by macher
9785inElem
9886 :: (Monad m, Foldable g)
99− => NameMatcher
87+ => ElemMatcher
10088 -> DomParserT Identity m a
10189 -> DomParserT g m a
10290inElem n = fmap runIdentity . inElemTrav n
10391
10492inElemAll
10593 :: (Monad m, Foldable g)
106− => NameMatcher
94+ => ElemMatcher
10795 -> DomParserT Identity m a
10896 -> DomParserT g m [a]
10997inElemAll = inElemTrav
11098
11199inElemMay
112100 :: (Monad m, Foldable g)
113− => NameMatcher
101+ => ElemMatcher
114102 -> DomParserT Identity m a
115103 -> DomParserT g m (Maybe a)
116104inElemMay = inElemTrav
117105
118106inElemNe
119107 :: (Monad m, Foldable g)
120− => NameMatcher
108+ => ElemMatcher
121109 -> DomParserT Identity m a
122110 -> DomParserT g m (NonEmpty a)
123111inElemNe = inElemTrav
124112
125113{- | 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
114+["a", "b"]@ differs from @inElem "a" $ inElem "b"@. Namely the first variant
127115will not fail if occured tag "a" which does not contains tag "b". This
128116behaviour 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
117+to pull tags located in some path. The other difference is in traversing inner
130118elements. Consider this code
131119
132120@
@@ -144,26 +132,31 @@ which translates like: @a > b > c > fromDom@
144132As you can see, inElem always takes first element and runs inner parser in this
145133single element, unlike 'divePath' which runs inner parser @in all@ descendants
146134in 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.
147140-}
148141
149142divePath
150143 :: forall m g a
151144 . (Monad m, Foldable g)
152− => [NameMatcher]
145+ => [ElemMatcher]
153146 -> DomParserT [] m a
154147 -> DomParserT g m a
155148divePath path = magnify $ to modElems
156149 where
157150 modElems
158151 = over pdElements (toListOf $ folded . diver)
159− . over pdPath (<> DomPath (map _nmShow path))
152+ . over pdPath (<> DomPath (L.map _emShow path))
160153 diver :: Fold Element Element
161− diver = foldr (.) id $ map toDive path
162− toDive n = nodes . folded . _Element . elMatchName n
154+ diver = F.foldr (.) id $ L.map toDive path
155+ toDive n = nodes . folded . _Element . elMatch n
163156
164157diveElem
165158 :: (Monad m, Foldable g)
166− => NameMatcher
159+ => ElemMatcher
167160 -> DomParserT [] m a
168161 -> DomParserT g m a
169162diveElem p = divePath [p]
@@ -190,7 +183,7 @@ ignoreEmpty
190183 -> DomParserT Identity m (Maybe a)
191184ignoreEmpty = ignoreElem test
192185 where
193− test e = null $ e ^. nodes
186+ test e = L.null $ e ^. nodes
194187
195188-- | If all current elements contains blank content, or contains nothing at all
196189-- , then returns Nothing, else runs parser.
@@ -204,134 +197,6 @@ ignoreBlank = ignoreElem test
204197 let
205198 elems = e ^.. nodes . folded . _Element
206199 cont = mconcat $ e ^.. nodes . folded . _Content
207− in if | not $ null elems -> False
200+ in if | not $ L.null elems -> False
208201 | T.null $ T.strip cont -> True
209202 | otherwise -> False
210−
211−-- | Returns name of current element.
212−--
213−-- @since 1.0.0
214−getCurrentName :: (Monad m) => DomParserT Identity m Name
215−getCurrentName = view $ pdElements . to runIdentity . name
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− => NameMatcher
223− -> DomParserT Identity m ()
224−checkCurrentName n = do
225− cn <- getCurrentName
226− unless ((n ^. nmMatch) cn) $ do
227− p <- view pdPath
228− let pinit = if null (unDomPath p) then [] else init $ unDomPath p
229− throwError $ ParserErrors [PENotFound $ DomPath $ pinit ++ [_nmShow 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
292− :: (Monad m)
293− => DomParserT Identity m (M.Map Name Text)
294−getCurrentAttributes = view $ pdElements . to runIdentity . attrs
295−
296−-- | Returns element with given name or 'Nothing'
297−--
298−-- @since 1.0.0
299−getCurrentAttribute
300− :: (Monad m)
301− => NameMatcher
302− -> DomParserT Identity m (Maybe Text)
303−getCurrentAttribute attrName
304− = preview $ pdElements . to runIdentity . attrs . to M.toList
305− . traversed . filtered (views _1 (attrName ^. nmMatch)) . _2
306−
307−-- | Parses attribute with given name, throws error if attribute is not found.
308−--
309−-- @since 1.0.0
310−parseAttribute
311− :: (Monad m)
312− => NameMatcher
313− -- ^ Attribute name
314− -> (Text -> Either Text a)
315− -- ^ Attribute content parser
316− -> DomParserT Identity m a
317−parseAttribute attrName parser =
318− parseAttributeMaybe attrName parser >>= \case
319− Nothing -> throwParserError $ PEAttributeNotFound attrName
320− Just a -> return a
321−
322−-- | Parses attribute with given name.
323−--
324−-- @since 1.0.0
325−parseAttributeMaybe
326− :: (Monad m)
327− => NameMatcher
328− -- ^ Attribute name
329− -> (Text -> Either Text a)
330− -- ^ Attribute content parser
331− -> DomParserT Identity m (Maybe a)
332−parseAttributeMaybe attrName parser =
333− getCurrentAttribute attrName >>= \case
334− Nothing -> return Nothing
335− Just aval -> case parser aval of
336− Left err -> throwParserError $ PEAttributeWrongFormat attrName err
337− Right a -> return $ Just 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+42−115
@@ -1,40 +1,32 @@
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
11− , readChar
1210 , intFromDom
1311 , integerFromDom
1412 , doubleFromDom
1513 , fixedFromDom
1614 , boolFromDom
17− , readBool
1815 , unitFromDom
1916 , voidFromDom
2017 , scientificFromDom
21− -- * FromAttribute
22− , FromAttribute(..)
2318 ) where
2419
2520import Control.Applicative
2621import Control.Lens
2722import Data.Fixed
28−import Data.Monoid
29−import Data.OpenUnion
3023import Data.Scientific
3124import Data.Text as T hiding (empty)
3225import Data.Typeable
3326import Data.Void
3427import Text.XML
35−import Text.XML.DOM.Parser.Combinators
28+import Text.XML.DOM.Parser.Content
3629import Text.XML.DOM.Parser.Types
37−import TypeFun.Data.List hiding (Union)
3830
3931proxyFromDom
4032 :: forall proxy m a
@@ -43,151 +35,86 @@ proxyFromDom
4335 -> DomParserT Identity m a
4436proxyFromDom _ = fromDom
4537
46−-- | 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'
4740class FromDom a where
4841 fromDom :: (Monad m) => DomParserT Identity m a
4942
5043instance FromDom () where
5144 fromDom = unitFromDom
5245
46+-- | Always successfully parses any DOM to @()@
47+unitFromDom :: (Monad m) => DomParserT Identity m ()
48+unitFromDom = pure ()
49+
5350instance FromDom Void where
5451 fromDom = voidFromDom
5552
53+-- | Never parses successfully. It is just 'empty'
54+voidFromDom :: (Monad m) => DomParserT Identity m Void
55+voidFromDom = empty
56+
5657instance FromDom Text where
5758 fromDom = textFromDom
5859
59−instance FromDom String where
60− fromDom = stringFromDom
61−
62−instance FromDom Char where
63− fromDom = charFromDom
64−
65−instance FromDom Int where
66− fromDom = intFromDom
67−
68−instance FromDom Integer where
69− fromDom = integerFromDom
70−
71−instance FromDom Double where
72− fromDom = doubleFromDom
73−
74−instance (HasResolution a, Typeable a) => FromDom (Fixed a) where
75− fromDom = fixedFromDom
76−
77−instance FromDom Scientific where
78− fromDom = scientificFromDom
79−
80−instance FromDom Bool where
81− fromDom = boolFromDom
82−
83−instance FromDom (Union '[]) where
84− fromDom = empty
85−
86−instance
87− ( Typeable a, FromDom a, FromDom (Union as)
88− , SubList as (a ': as) )
89− => FromDom (Union (a ': as)) where
90− -- fromDom :: forall m. (DomParserMonad m) => m a
91− fromDom = (liftUnion <$> (proxyFromDom (Proxy :: Proxy a)))
92− <|> (reUnion <$> (proxyFromDom (Proxy :: Proxy (Union as))))
93−
94−instance FromDom Element where
95− fromDom = elementFromDom
96−
97−elementFromDom :: (Monad m) => DomParserT Identity m Element
98−elementFromDom = view $ pdElements . to runIdentity
99−
100−unionFromDom
101− :: (Monad m, FromDom (Union as))
102− => proxy as
103− -> DomParserT Identity m (Union as)
104−unionFromDom _ = fromDom
105−
10660textFromDom :: (Monad m) => DomParserT Identity m Text
10761textFromDom = parseContent Right
10862
63+instance FromDom String where
64+ fromDom = stringFromDom
65+
10966stringFromDom :: (Monad m) => DomParserT Identity m String
11067stringFromDom = parseContent $ Right . T.unpack
11168
69+instance FromDom Char where
70+ fromDom = charFromDom
71+
11272charFromDom :: (Monad m) => DomParserT Identity m Char
11373charFromDom = parseContent readChar
11474
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"
75+instance FromDom Int where
76+ fromDom = intFromDom
12277
12378intFromDom :: (Monad m) => DomParserT Identity m Int
12479intFromDom = parseContent readContent
12580
81+instance FromDom Integer where
82+ fromDom = integerFromDom
83+
12684integerFromDom :: (Monad m) => DomParserT Identity m Integer
12785integerFromDom = parseContent readContent
12886
87+instance FromDom Double where
88+ fromDom = doubleFromDom
89+
12990doubleFromDom :: (Monad m) => DomParserT Identity m Double
13091doubleFromDom = parseContent readContent
13192
93+instance (HasResolution a, Typeable a) => FromDom (Fixed a) where
94+ fromDom = fixedFromDom
95+
13296fixedFromDom
13397 :: (Monad m, Typeable a, HasResolution a)
13498 => DomParserT Identity m (Fixed a)
13599fixedFromDom = parseContent readContent
136100
101+instance FromDom Scientific where
102+ fromDom = scientificFromDom
103+
137104scientificFromDom :: Monad m => DomParserT Identity m Scientific
138105scientificFromDom = parseContent readContent
139106
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.
107+instance FromDom Bool where
108+ fromDom = boolFromDom
109+
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.
143113boolFromDom :: (Monad m) => DomParserT Identity m Bool
144114boolFromDom = parseContent readBool
145115
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
169− :: Text
170− -- ^ Attribute contents to parse
171− -> Either Text a
172− -- ^ Either error message or result
173−
174−instance FromAttribute () where
175− fromAttribute _ = Right ()
176−
177−instance FromAttribute Text where
178− fromAttribute = Right
179−
180−instance FromAttribute Char where
181− fromAttribute = readChar
182−
183−instance FromAttribute Int where
184− fromAttribute = readContent
185−
186−instance FromAttribute Integer where
187− fromAttribute = readContent
188−
189−instance (Typeable a, HasResolution a) => FromAttribute (Fixed a) where
190− fromAttribute = readContent
116+instance FromDom Element where
117+ fromDom = elementFromDom
191118
192−instance FromAttribute Scientific where
193− fromAttribute = readContent
119+elementFromDom :: (Monad m) => DomParserT Identity m Element
120+elementFromDom = view $ pdElements . to runIdentity
src/Text/XML/DOM/Parser/Types.hs+82−53
@@ -1,12 +1,17 @@
11module Text.XML.DOM.Parser.Types
2− ( -- * Local name
3− NameMatcher(..)
2+ (-- * Element matching
3+ ElemMatcher(..)
4+ , emMatch
5+ , emShow
6+ , matchElemName
7+ , elMatch
8+ -- * Name matching
9+ , NameMatcher(..)
410 , nmMatch
511 , nmShow
612 , matchName
713 , matchLocalName
814 , matchCILocalName
9− , elMatchName
1015 -- * Parser internals
1116 , DomPath(..)
1217 , ParserError(..)
@@ -18,15 +23,12 @@ module Text.XML.DOM.Parser.Types
1823 , ParserData(..)
1924 , pdElements
2025 , pdPath
21− -- * Parser type
2226 , DomParserT
2327 , DomParser
2428 , runDomParserT
2529 , runDomParser
2630 -- * Auxiliary
27− , DomTraversable(..)
2831 , throwParserError
29− , throwWrongFormat
3032 ) where
3133
3234import Control.Exception
@@ -34,73 +36,125 @@ import Control.Lens
3436import Control.Monad.Except
3537import Control.Monad.Reader
3638import Data.CaseInsensitive as CI
37−import Data.List.NonEmpty as NE
38−import Data.Maybe
3939import Data.String
4040import Data.Text as T
4141import GHC.Generics (Generic)
4242import Text.XML
4343import Text.XML.Lens
4444
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+
4578-- | Arbitrary name matcher. Match name any way you want, but
4679-- considered to be used as comparator with some name with some rules
80+--
81+-- @since 2.0.0
4782data NameMatcher = NameMatcher
4883 { _nmMatch :: Name -> Bool
4984 -- ^ Name matching function, usually should be simple comparsion
5085 -- function takin in account only local name or other components
5186 -- of 'Name'
5287 , _nmShow :: Text
53− -- ^ How to show the name matcher, since there is no 'Read'
54− -- instance we can do it any way we want
88+ -- ^ Field for 'Show' instance and bulding usefull errors
5589 }
5690
57−makeLenses ''NameMatcher
58−
91+-- | Instance use 'matchCILocalName' as most general and liberal
92+-- matching strategy (while XML is often malformed).
93+--
94+-- @since 2.0.0
5995instance IsString NameMatcher where
6096 fromString = matchCILocalName . T.pack
6197
6298instance Show NameMatcher where
6399 show = T.unpack . _nmShow
64100
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
65105matchLocalName :: Text -> NameMatcher
66106matchLocalName tname = NameMatcher
67107 { _nmMatch = \n -> nameLocalName n == tname
68108 , _nmShow = tname
69109 }
70110
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
71116matchCILocalName :: Text -> NameMatcher
72117matchCILocalName tname = NameMatcher
73118 { _nmMatch = \n -> CI.mk (nameLocalName n) == CI.mk tname
74119 , _nmShow = tname
75120 }
76121
122+makeLenses ''NameMatcher
123+
124+-- | Makes matcher which match name by 'Eq' with given
125+--
126+-- @since 2.0.0
77127matchName :: Name -> NameMatcher
78128matchName n = NameMatcher
79129 { _nmMatch = (== n)
80130 , _nmShow = nameLocalName n
81131 }
82132
83−elMatchName :: NameMatcher -> Traversal' Element Element
84−elMatchName nm = filtered (views name $ nm ^. nmMatch)
85−
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.
86135newtype DomPath = DomPath
87136 { unDomPath :: [Text]
88− } deriving (Eq, Ord, Show, Monoid)
137+ } deriving (Eq, Ord, Show, Semigroup, Monoid)
89138
90139-- | DOM parser error description.
91140data ParserError
92141 -- | Tag not found which should be.
93142 = PENotFound
94143 { _pePath :: DomPath
144+ -- ^ Path of element error occured in
95145 }
96146
97− -- | Tag contents has wrong format, (could not read text to value)
98− | PEWrongFormat
99− { _peDetails :: Text
100− , _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
101153 }
102154
103155 -- | Could not parse attribute
156+ --
157+ -- @since 1.0.0
104158 | PEAttributeWrongFormat
105159 { _peAttributeName :: NameMatcher
106160 , _peDetails :: Text
@@ -112,10 +166,10 @@ data ParserError
112166 { _pePath :: DomPath
113167 }
114168
115− -- | Expected attribute but not found
116− | PEAttributeNotFound
117− { _peAttributeName :: NameMatcher
118− , _pePath :: DomPath
169+ -- | Tag contents has wrong format, (could not read text to value)
170+ | PEContentWrongFormat
171+ { _peDetails :: Text
172+ , _pePath :: DomPath
119173 }
120174
121175 -- | Some other error
@@ -130,15 +184,16 @@ instance Exception ParserError
130184
131185newtype ParserErrors = ParserErrors
132186 { unParserErrors :: [ParserError]
133− } deriving (Show, Monoid, Generic)
187+ } deriving (Show, Semigroup, Monoid, Generic)
134188
135189makePrisms ''ParserErrors
136190
137191instance Exception ParserErrors
138192
139193
140−{- | Parser scope parser runs in. Functor argument is usually @Identity@ or
141−@[]@.
194+{- | Parser scope.
195+
196+Functor argument is usually @Identity@ or @[]@.
142197
143198If functor is @Identity@ then parser expects exactly ONE current element. This
144199is common behavior for content parsers, or parsers expecting strict XML
@@ -184,25 +239,6 @@ runDomParser
184239 -> Either ParserErrors a
185240runDomParser doc par = runIdentity $ runDomParserT doc par
186241
187−-- | Class of traversable functors which may be constructed from list. Or may
188−-- not.
189−class Traversable f => DomTraversable f where
190− -- | If method return Nothing this means we can not build traversable from
191− -- given list. In this case combinator should fail traversing.
192− buildDomTraversable :: [a] -> Maybe (f a)
193−
194−instance DomTraversable Identity where
195− buildDomTraversable = fmap Identity . listToMaybe
196−
197−instance DomTraversable [] where
198− buildDomTraversable = Just
199−
200−instance DomTraversable Maybe where
201− buildDomTraversable = Just . listToMaybe
202−
203−instance DomTraversable NonEmpty where
204− buildDomTraversable = NE.nonEmpty
205−
206242throwParserError
207243 :: (MonadError ParserErrors m, MonadReader (ParserData f) m)
208244 => (DomPath -> ParserError)
@@ -210,10 +246,3 @@ throwParserError
210246throwParserError mkerr = do
211247 path <- view pdPath
212248 throwError $ ParserErrors $ [mkerr path]
213−
214−-- | Throw 'PEWrongFormat' as very common case
215−throwWrongFormat
216− :: (MonadError ParserErrors m, MonadReader (ParserData f) m)
217− => Text
218− -> m a
219−throwWrongFormat err = throwParserError $ PEWrongFormat err
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.yamlmode 100644 → 120000+1−6
@@ -1,6 +0,0 @@
1−packages:
2− - .
3−extra-deps:
4− - open-union-0.2.0.0
5− - type-fun-0.1.1
6−resolver: lts-6.25
@@ -0,0 +1 @@
1+ghc-963.yaml