-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathDecodeM.hs
More file actions
129 lines (115 loc) · 4.19 KB
/
DecodeM.hs
File metadata and controls
129 lines (115 loc) · 4.19 KB
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
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NoFieldSelectors #-}
module KDL.Decoder.Internal.DecodeM (
-- * Decoding errors
module KDL.Decoder.Internal.Error,
-- * DecodeM monad
DecodeM (..),
runDecodeM,
decodeThrow,
failM,
addContext,
) where
import Control.Applicative (Alternative (..))
import Data.Bifunctor (first)
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NonEmpty
import Data.Text (Text)
import KDL.Decoder.Internal.Error
-- | The monad that returns either a 'DecodeError' or a result of type @a@.
--
-- The odd structure here is because of our backtracking semantics. We want to
-- collect all errors that may appear (even if a value is successfully parsed)
-- so that if we get a failure later on, we can return the deepest error, even
-- if it was in a successful branch.
--
-- Take this motivating example: a node takes an arbitrary number of string
-- args. If you pass some strings then a number, it'll successfully parse up to
-- the number and return success, only for the node to fail later with
-- "unexpected argument: 123". But the true error was
-- "unexpected number, expected string".
data DecodeM a
= DecodeM_Found a [BaseDecodeError]
| DecodeM_Fail (NonEmpty BaseDecodeError)
instance Functor DecodeM where
fmap f = \case
DecodeM_Found a es -> DecodeM_Found (f a) es
DecodeM_Fail es -> DecodeM_Fail es
instance Applicative DecodeM where
pure x = DecodeM_Found x []
l <*> r =
case (l, r) of
(DecodeM_Found f es1, DecodeM_Found a es2) -> DecodeM_Found (f a) (mergeErrorsLR es1 es2)
(DecodeM_Found _ es1, DecodeM_Fail es2) -> DecodeM_Fail (mergeErrorsL es1 es2)
(DecodeM_Fail es1, DecodeM_Found _ es2) -> DecodeM_Fail (mergeErrorsR es1 es2)
(DecodeM_Fail es1, DecodeM_Fail es2) -> DecodeM_Fail (mergeErrors es1 es2)
instance Monad DecodeM where
(>>) = (*>)
m >>= k =
case m of
DecodeM_Fail es1 -> DecodeM_Fail es1
DecodeM_Found a es1 ->
case k a of
DecodeM_Found b es2 -> DecodeM_Found b (mergeErrorsLR es1 es2)
DecodeM_Fail es2 -> DecodeM_Fail (mergeErrorsL es1 es2)
instance Alternative DecodeM where
empty = failM "<empty>"
l <|> r =
case l of
DecodeM_Found a es1 -> DecodeM_Found a es1
DecodeM_Fail es1 ->
case r of
DecodeM_Found a es2 -> DecodeM_Found a (NonEmpty.toList $ mergeErrorsR es1 es2)
DecodeM_Fail es2 -> DecodeM_Fail (mergeErrors es1 es2)
-- | Run a 'DecodeM' action and return the result or the deepest error found.
runDecodeM :: DecodeM a -> Either DecodeError a
runDecodeM = \case
DecodeM_Found a _ -> Right a
DecodeM_Fail errors -> Left DecodeError{filepath = Nothing, errors}
mergeErrors ::
NonEmpty BaseDecodeError ->
NonEmpty BaseDecodeError ->
NonEmpty BaseDecodeError
mergeErrors es1 es2 =
case compare (key es1) (key es2) of
LT -> es2
EQ -> es1 <> es2
GT -> es1
where
key = length . fst . NonEmpty.head
mergeErrorsL ::
[BaseDecodeError] ->
NonEmpty BaseDecodeError ->
NonEmpty BaseDecodeError
mergeErrorsL l r = maybe r (\l' -> mergeErrors l' r) (NonEmpty.nonEmpty l)
mergeErrorsR ::
NonEmpty BaseDecodeError ->
[BaseDecodeError] ->
NonEmpty BaseDecodeError
mergeErrorsR l r = maybe l (\r' -> mergeErrors l r') (NonEmpty.nonEmpty r)
mergeErrorsLR ::
[BaseDecodeError] ->
[BaseDecodeError] ->
[BaseDecodeError]
mergeErrorsLR l r =
case (l, r) of
([], _) -> r
(_, []) -> l
(x : xs, y : ys) -> NonEmpty.toList $ mergeErrors (x :| xs) (y :| ys)
mapErrors :: (BaseDecodeError -> BaseDecodeError) -> DecodeM a -> DecodeM a
mapErrors f = \case
DecodeM_Found a es -> DecodeM_Found a (fmap f es)
DecodeM_Fail es -> DecodeM_Fail (fmap f es)
-- | Throw an error.
decodeThrow :: DecodeErrorKind -> DecodeM a
decodeThrow e = DecodeM_Fail . NonEmpty.singleton $ ([], e)
-- | Throw a 'DecodeError_Custom' error.
failM :: Text -> DecodeM a
failM = decodeThrow . DecodeError_Custom
-- | Add context to all errors that occur in the given action.
addContext :: ContextItem -> DecodeM a -> DecodeM a
addContext ctxItem = mapErrors (first (ctxItem :))