/
Yaml.hs
397 lines (344 loc) · 13 KB
/
Yaml.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PackageImports #-}
{-|
As a bit of background, this package is built on a few other packages I wrote.
yaml is a low-level wrapper around the C libyaml library, with an enumerator
interface. data-object is a package defining a data type:
@
data Object k v = Scalar v
| Sequence [Object k v]
| Mapping [(k, Object k v)]
@
In other words, it can represent JSON data fully, and YAML data almost fully.
In particular, it doesn't handle cyclical aliases, which I hope doesn't really
occur too much in real life.
Another package to deal with is failure: it basically replaces using an Either
for error-handling into a typeclass. It has instances for Maybe, IO and lists
by default.
The last package is convertible-text, which is a fork of John Goerzen's
convertible package. The difference is it supports both conversions that are
guaranteed to succeed (Int -> String) and ones which may fail (String -> Int),
and also supports various textual datatypes (String, lazy\/strict ByteString,
lazy\/string Text).
/YamlScalar and YamlObject/
We have a @type YamlObject = Object YamlScalar YamlScalar@, where a YamlScalar
is just a ByteString value with a tag and a style. A \"style\" is how the data
was represented in the underlying YAML file: single quoted, double quoted, etc.
Then there is an IsYamlScalar typeclass, which provides fromYamlScalar and
toYamlScalar conversion functions. There are instances for all the
\"text-like\" datatypes: String, ByteString and Text. The built-in instances
all assume a UTF-8 data encoding. And around this we have toYamlObject and
fromYamlObject functions, which do exactly what they sound like.
/Encoding and decoding/
There are two encoding files: encode and encodeFile. You can guess the
different: the former produces a ByteString (strict) and the latter writes to a
file. They both take an Object, whose keys and values must be an instance of
IsYamlScalar. So, for example:
@
encodeFile "myfile.yaml" $ Mapping
[ ("Michael", Mapping
[ ("age", Scalar "26")
, ("color", Scalar "blue")
])
, ("Eliezer", Mapping
[ ("age", Scalar "2")
, ("color", Scalar "green")
])
]
@
decoding is only slightly more complicated, since the decoding can fail. In
particular, the return type is an IO wrapped around a Failure. For example, you
could use:
@
maybeObject <- decodeFile "myfile.yaml"
case maybeObject of
Nothing -> putStrLn "Error parsing YAML file."
Just object -> putStrLn "Successfully parsed."
@
If you just want to throw any parse errors as IO exception, you can use join:
@
import Control.Monad (join)
object <- join $ decodeFile "myfile.yaml"
@
This takes advantage of the IO instance of Failure.
/Parsing an Object/
In order to pull the data out of an Object, you can use the helper functions
from Data.Object. For example:
@
import Data.Object
import Data.Object.Yaml
import Control.Monad
main = do
object <- join $ decodeFile "myfile.yaml"
people <- fromMapping object
michael <- lookupMapping "Michael" people
age <- lookupScalar "age" michael
putStrLn $ "Michael is " ++ age ++ " years old."
@
lookupScalar and friends implement Maybe, so you can test for optional
attributes by switching on Nothing/Just a:
@
name <- lookupScalar "middleName" michael :: Maybe String
@
/And that's it/
There's really not more to know about this library. Enjoy!
-}
module Data.Object.Yaml
( -- * Definition of 'YamlObject'
YamlScalar (..)
, YamlObject
-- * Automatic scalar conversions
, IsYamlScalar (..)
, toYamlObject
, fromYamlObject
-- * Encoding/decoding
, encode
, encodeFile
, decode
, decodeFile
-- * Exceptions
, ParseException (..)
) where
import qualified Text.Libyaml as Y
import Text.Libyaml hiding (encode, decode, encodeFile, decodeFile)
import Data.Object
import Data.ByteString (ByteString)
import qualified Data.Map as Map
import System.IO.Unsafe (unsafePerformIO)
import Control.Failure
import qualified Data.Text
import qualified Data.Text.Lazy
import qualified Data.ByteString
import qualified Data.ByteString.Lazy
import Data.Convertible.Text (cs)
import Data.Data
import Control.Monad.Trans.Class
import Control.Monad.IO.Class
import Control.Monad.Trans.State
import Control.Monad
import qualified Data.Enumerator as E
import Data.Enumerator (($$))
import Prelude hiding (catch)
import Control.Exception (throwIO, Exception)
import Data.String (IsString (fromString))
-- | Equality depends on 'value' and 'tag', not 'style'.
data YamlScalar = YamlScalar
{ value :: ByteString
, tag :: Tag
, style :: Style
}
deriving (Show, Read, Data, Typeable)
instance Eq YamlScalar where
(YamlScalar v t _) == (YamlScalar v' t' _) = v == v' && t == t'
instance IsString YamlScalar where
fromString = toYamlScalar
type YamlObject = Object YamlScalar YamlScalar
class (Eq a) => IsYamlScalar a where
fromYamlScalar :: YamlScalar -> a
toYamlScalar :: a -> YamlScalar
instance IsYamlScalar YamlScalar where
fromYamlScalar = id
toYamlScalar = id
instance IsYamlScalar Data.Text.Text where
fromYamlScalar = cs . value
toYamlScalar t = YamlScalar (cs t) NoTag Any
instance IsYamlScalar Data.Text.Lazy.Text where
fromYamlScalar = cs . value
toYamlScalar t = YamlScalar (cs t) NoTag Any
instance IsYamlScalar [Char] where
fromYamlScalar = cs . value
toYamlScalar s = YamlScalar (cs s) NoTag Any
instance IsYamlScalar Data.ByteString.ByteString where
fromYamlScalar = value
toYamlScalar b = YamlScalar b NoTag Any
instance IsYamlScalar Data.ByteString.Lazy.ByteString where
fromYamlScalar = cs . value
toYamlScalar b = YamlScalar (cs b) NoTag Any
-- | Merge assoc-lists by keys.
-- First list overrides second:
-- [(k1, x), (k2, y)] `mergeAssocLists` [(k3, z)] == [(k1, x), (k2, y), (k3, z)]
-- [(k1, x), (k2, y)] `mergeAssocLists` [(k2, z)] == [(k1, x), (k2, y)]
mergeAssocLists :: (Eq k) => [(k, v)] -> [(k, v)] -> [(k, v)]
mergeAssocLists a [] = a
mergeAssocLists [] b = b
mergeAssocLists a ((bk, bv):bs) =
case lookup bk a of
Nothing -> (bk, bv) : mergeAssocLists a bs
Just av -> (bk, av) : mergeAssocLists (filter (\(x, _) -> x /= bk) a) bs
toYamlObject :: IsYamlScalar k
=> IsYamlScalar v
=> Object k v
-> YamlObject
toYamlObject = mapKeysValues toYamlScalar toYamlScalar
fromYamlObject :: IsYamlScalar k
=> IsYamlScalar v
=> YamlObject
-> Object k v
fromYamlObject = mapKeysValues fromYamlScalar fromYamlScalar
encode :: (IsYamlScalar k, IsYamlScalar v) => Object k v -> ByteString
encode obj = unsafePerformIO $ do
x <- E.run $ E.enumList 1 (objToEvents $ toYamlObject obj) $$ Y.encode
case x of
Left err -> throwIO err
Right y -> return y
encodeFile :: (IsYamlScalar k, IsYamlScalar v)
=> FilePath
-> Object k v
-> IO ()
encodeFile fp obj = do
x <- E.run $ E.enumList 1 (objToEvents $ toYamlObject obj)
$$ Y.encodeFile fp
case x of
Left err -> throwIO err
Right () -> return ()
objToEvents :: YamlObject -> [Y.Event]
objToEvents o = (:) EventStreamStart
. (:) EventDocumentStart
$ objToEvents' o
[ EventDocumentEnd
, EventStreamEnd
]
scalarToEvent :: YamlScalar -> Event
scalarToEvent (YamlScalar v t s) = EventScalar v t s Nothing
objToEvents' :: YamlObject -> [Y.Event] -> [Y.Event]
objToEvents' (Scalar s) rest = scalarToEvent s : rest
objToEvents' (Sequence list) rest =
EventSequenceStart Nothing
: foldr ($) (EventSequenceEnd : rest) (map objToEvents' list)
objToEvents' (Mapping pairs) rest =
EventMappingStart Nothing
: foldr ($) (EventMappingEnd : rest) (map pairToEvents pairs)
pairToEvents :: (YamlScalar, YamlObject) -> [Y.Event] -> [Y.Event]
pairToEvents (k, v) rest =
scalarToEvent k
: objToEvents' v rest
-- Parsing
data ParseException = NonScalarKey
| UnknownAlias { _anchorName :: Y.AnchorName }
| UnexpectedEvent { _received :: Maybe Event
, _expected :: Maybe Event
}
| InvalidYaml (Maybe String)
deriving (Show, Typeable)
instance Exception ParseException
newtype PErrorT m a = PErrorT { runPErrorT :: m (Either ParseException a) }
instance Monad m => Monad (PErrorT m) where
return = PErrorT . return . Right
(PErrorT m) >>= f = PErrorT $ do
e <- m
case e of
Left e' -> return $ Left e'
Right a -> runPErrorT $ f a
instance MonadTrans PErrorT where
lift = PErrorT . liftM Right
instance MonadIO m => MonadIO (PErrorT m) where
liftIO = lift . liftIO
pfailure :: Monad m => ParseException -> PErrorT m a
pfailure = PErrorT . return . Left
type Parser = PErrorT (StateT (Map.Map String YamlObject) IO)
requireEvent :: Event -> E.Iteratee Event Parser ()
requireEvent e = do
f <- E.head
if f == Just e
then return ()
else lift $ pfailure $ UnexpectedEvent f $ Just e
parse :: E.Iteratee Event Parser YamlObject
parse = do
requireEvent EventStreamStart
requireEvent EventDocumentStart
res <- parseO
requireEvent EventDocumentEnd
requireEvent EventStreamEnd
return res
parseScalar :: ByteString -> Tag -> Style -> Anchor
-> E.Iteratee Event Parser YamlScalar
parseScalar v t s a = do
let res = YamlScalar v t s
case a of
Nothing -> return res
Just an -> do
lift $ lift $ modify (Map.insert an $ Scalar res)
return res
parseO :: E.Iteratee Event Parser YamlObject
parseO = do
me <- E.head
case me of
Just (EventScalar v t s a) -> Scalar `liftM` parseScalar v t s a
Just (EventSequenceStart a) -> parseS a id
Just (EventMappingStart a) -> parseM a id
Just (EventAlias an) -> do
m <- lift $ lift get
case Map.lookup an m of
Nothing -> lift $ pfailure $ UnknownAlias an
Just v -> return v
_ -> lift $ pfailure $ UnexpectedEvent me Nothing
parseS :: Y.Anchor
-> ([YamlObject] -> [YamlObject])
-> E.Iteratee Event Parser YamlObject
parseS a front = do
me <- E.peek
case me of
Just EventSequenceEnd -> do
E.drop 1
let res = Sequence $ front []
case a of
Nothing -> return res
Just an -> do
lift $ lift $ modify $ Map.insert an res
return res
_ -> do
o <- parseO
parseS a $ front . (:) o
parseM :: Y.Anchor
-> ([(YamlScalar, YamlObject)] -> [(YamlScalar, YamlObject)])
-> E.Iteratee Event Parser YamlObject
parseM a front = do
me <- E.peek
case me of
Just EventMappingEnd -> do
E.drop 1
let res = Mapping $ front []
case a of
Nothing -> return res
Just an -> do
lift $ lift $ modify $ Map.insert an res
return res
_ -> do
me' <- E.head
s <- case me' of
Just (EventScalar v t s a') -> parseScalar v t s a'
_ -> lift $ pfailure $ UnexpectedEvent me' Nothing
o <- parseO
let al = mergeAssocLists [(s, o)] $ front []
al' = if fromYamlScalar s == "<<"
then case o of
Scalar _ -> al
Mapping l -> mergeAssocLists al l
Sequence l -> mergeAssocLists al $ foldl merge' [] l
else al
parseM a (`mergeAssocLists` al')
where merge' :: (Eq k) => [(k, Object k v)] -> Object k v -> [(k, Object k v)]
merge' al (Mapping om) = mergeAssocLists al om
merge' al _ = al
decode :: (Failure ParseException m, IsYamlScalar k, IsYamlScalar v)
=> ByteString
-> m (Object k v)
decode bs = unsafePerformIO $ do
x <- flip evalStateT Map.empty $ runPErrorT $ E.run $ Y.decode bs $$ parse
case x of
Left err -> return $ failure err
Right (Left err) -> return $ failure $ InvalidYaml $ Just $ show err
Right (Right y) -> return $ return $ fromYamlObject y
decodeFile :: (Failure ParseException m, IsYamlScalar k, IsYamlScalar v)
=> FilePath
-> IO (m (Object k v))
decodeFile fp = do
x <- flip evalStateT Map.empty $ runPErrorT $ E.run $ Y.decodeFile fp $$ parse
case x of
Left err -> return $ failure err
Right (Left err) -> return $ failure $ InvalidYaml $ Just $ show err
Right (Right y) -> return $ return $ fromYamlObject y