-
Notifications
You must be signed in to change notification settings - Fork 25
/
Copy pathPretty.hs
223 lines (200 loc) · 7.9 KB
/
Pretty.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
{-# LANGUAGE OverloadedStrings, RecordWildCards, CPP #-}
-- |Aeson-compatible pretty-printing of JSON 'Value's.
module Data.Aeson.Encode.Pretty (
-- * Simple Pretty-Printing
encodePretty, encodePrettyToTextBuilder,
-- * Pretty-Printing with Configuration Options
encodePretty', encodePrettyToTextBuilder',
Config (..), defConfig,
Indent(..), NumberFormat(..),
-- ** Sorting Keys in Objects
-- |With the Aeson library, the order of keys in objects is undefined due to
-- objects being implemented as HashMaps. To allow user-specified key
-- orders in the pretty-printed JSON, 'encodePretty'' can be configured
-- with a comparison function. These comparison functions can be composed
-- using the 'Monoid' interface. Some other useful helper functions to keep
-- in mind are 'comparing' and 'on'.
--
-- Consider the following deliberately convoluted example, demonstrating
-- the use of comparison functions:
--
-- An object might pretty-print as follows
--
-- > {
-- > "baz": ...,
-- > "bar": ...,
-- > "foo": ...,
-- > "quux": ...,
-- > }
--
-- which is clearly a confusing order of keys. By using a comparison
-- function such as
--
-- > comp :: Text -> Text -> Ordering
-- > comp = keyOrder ["foo","bar"] `mappend` comparing length
--
-- we can achieve the desired neat result:
--
-- > {
-- > "foo": ...,
-- > "bar": ...,
-- > "baz": ...,
-- > "quux": ...,
-- > }
--
mempty,
-- |Serves as an order-preserving (non-)sort function. Re-exported from
-- "Data.Monoid".
compare,
-- |Sort keys in their natural order, i.e. by comparing character codes.
-- Re-exported from the Prelude and "Data.Ord"
keyOrder
) where
#if MIN_VERSION_aeson(2,0,0)
import qualified Data.Aeson.Key as AK
import qualified Data.Aeson.KeyMap as AKM
#endif
import Data.Aeson (Value(..), ToJSON(..))
import qualified Data.Aeson.Text as Aeson
import Data.ByteString.Lazy (ByteString)
import Data.Function (on)
#if !MIN_VERSION_aeson(2,0,0)
import qualified Data.HashMap.Strict as H (toList)
#endif
import Data.List (intersperse, sortBy, elemIndex)
import Data.Maybe (fromMaybe)
#if !MIN_VERSION_base(4,13,0)
import Data.Semigroup ((<>))
#endif
import qualified Data.Scientific as S (Scientific, FPFormat(..))
import Data.Ord (comparing)
import Data.Text (Text)
import Data.Text.Lazy.Builder (Builder, toLazyText)
import Data.Text.Lazy.Builder.Scientific (formatScientificBuilder)
import Data.Text.Lazy.Encoding (encodeUtf8)
import qualified Data.Vector as V (toList)
import Prelude ()
import Prelude.Compat
data PState = PState { pLevel :: Int
, pIndent :: Builder
, pNewline :: Builder
, pItemSep :: Builder
, pKeyValSep :: Builder
, pNumFormat :: NumberFormat
, pSort :: [(Text, Value)] -> [(Text, Value)]
}
-- | Indentation per level of nesting. @'Spaces' 0@ removes __all__ whitespace
-- from the output.
data Indent = Spaces Int | Tab
data NumberFormat
-- | For numbers with absolute value less than 1e19, this follows the standard
-- behaviour of the 'Data.Aeson.encode' function. Uses integer literals for
-- integers (1, 2, 3...), simple decimals for fractional values between 0.1
-- and 9,999,999, and scientific notation otherwise. For numbers with an
-- absolute value larger than 1e19, always uses scientific notation.
= Generic
-- | Scientific notation (e.g. 2.3e123).
| Scientific
-- | Standard decimal notation
| Decimal
-- | Custom formatting function
| Custom (S.Scientific -> Builder)
data Config = Config
{ confIndent :: Indent
-- ^ Indentation per level of nesting
, confCompare :: Text -> Text -> Ordering
-- ^ Function used to sort keys in objects
, confNumFormat :: NumberFormat
, confTrailingNewline :: Bool
-- ^ Whether to add a trailing newline to the output
}
-- |Sort keys by their order of appearance in the argument list.
--
-- Keys that are not present in the argument list are considered to be greater
-- than any key in the list and equal to all keys not in the list. I.e. keys
-- not in the argument list are moved to the end, while their order is
-- preserved.
keyOrder :: [Text] -> Text -> Text -> Ordering
keyOrder ks = comparing $ \k -> fromMaybe maxBound (elemIndex k ks)
-- |The default configuration: indent by four spaces per level of nesting, do
-- not sort objects by key, do not add trailing newline.
--
-- > defConfig = Config { confIndent = Spaces 4, confCompare = mempty, confNumFormat = Generic, confTrailingNewline = False }
defConfig :: Config
defConfig =
Config {confIndent = Spaces 4, confCompare = mempty, confNumFormat = Generic, confTrailingNewline = False}
-- |A drop-in replacement for aeson's 'Aeson.encode' function, producing
-- JSON-ByteStrings for human readers.
--
-- Follows the default configuration in 'defConfig'.
encodePretty :: ToJSON a => a -> ByteString
encodePretty = encodePretty' defConfig
-- |A variant of 'encodePretty' that takes an additional configuration
-- parameter.
encodePretty' :: ToJSON a => Config -> a -> ByteString
encodePretty' conf = encodeUtf8 . toLazyText . encodePrettyToTextBuilder' conf
-- |A drop-in replacement for aeson's 'Aeson.encodeToTextBuilder' function,
-- producing JSON-ByteStrings for human readers.
--
-- Follows the default configuration in 'defConfig'.
encodePrettyToTextBuilder :: ToJSON a => a -> Builder
encodePrettyToTextBuilder = encodePrettyToTextBuilder' defConfig
-- |A variant of 'Aeson.encodeToTextBuilder' that takes an additional configuration
-- parameter.
encodePrettyToTextBuilder' :: ToJSON a => Config -> a -> Builder
encodePrettyToTextBuilder' Config{..} x = fromValue st (toJSON x) <> trail
where
st = PState 0 indent newline itemSep kvSep confNumFormat sortFn
indent = case confIndent of
Spaces n -> mconcat (replicate n " ")
Tab -> "\t"
newline = case confIndent of
Spaces 0 -> ""
_ -> "\n"
itemSep = ","
kvSep = case confIndent of
Spaces 0 -> ":"
_ -> ": "
sortFn = sortBy (confCompare `on` fst)
trail = if confTrailingNewline then "\n" else ""
fromValue :: PState -> Value -> Builder
fromValue st@PState{..} val = go val
where
go (Array v) = fromCompound st ("[","]") fromValue (V.toList v)
go (Object m) = fromCompound st ("{","}") fromPair (pSort (toList' m))
go (Number x) = fromNumber st x
go v = Aeson.encodeToTextBuilder v
#if MIN_VERSION_aeson(2,0,0)
toList' = fmap (\(k, v) -> (AK.toText k, v)) . AKM.toList
#else
toList' = H.toList
#endif
fromCompound :: PState
-> (Builder, Builder)
-> (PState -> a -> Builder)
-> [a]
-> Builder
fromCompound st@PState{..} (delimL,delimR) fromItem items = mconcat
[ delimL
, if null items then mempty
else pNewline <> items' <> pNewline <> fromIndent st
, delimR
]
where
items' = mconcat . intersperse (pItemSep <> pNewline) $
map (\item -> fromIndent st' <> fromItem st' item)
items
st' = st { pLevel = pLevel + 1}
fromPair :: PState -> (Text, Value) -> Builder
fromPair st (k,v) =
Aeson.encodeToTextBuilder (toJSON k) <> pKeyValSep st <> fromValue st v
fromIndent :: PState -> Builder
fromIndent PState{..} = mconcat (replicate pLevel pIndent)
fromNumber :: PState -> S.Scientific -> Builder
fromNumber st x = case pNumFormat st of
Generic
| (x > 1.0e19 || x < -1.0e19) -> formatScientificBuilder S.Exponent Nothing x
| otherwise -> Aeson.encodeToTextBuilder $ Number x
Scientific -> formatScientificBuilder S.Exponent Nothing x
Decimal -> formatScientificBuilder S.Fixed Nothing x
Custom f -> f x