core-data-0.2.1.9: lib/Core/Encoding/Json.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
--
-- As currently implemented this module, in conjunction with
-- Core.Text, is the opposite of efficient. The idea right now is to
-- experiment with the surface API. If it stabilizes, then the fact
-- that our string objects are already in UTF-8 will make for a very
-- efficient emitter.
--
-- |
-- Encoding and decoding UTF-8 JSON content.
--
-- This module is a thin wrapper around the most excellent __aeson__ library,
-- which has rich and powerful facilities for encoding Haskell types into
-- JSON.
--
-- Quite often, however, you find yourself having to create a Haskell type
-- /just/ to read some JSON coming from an external web service or API. This
-- can be challenging when the source of the JSON is complex or varying its
-- schema over time. For ease of exploration this module simply defines an
-- easy to use intermediate type representing JSON as a format.
--
-- Often you'll be working with literals directly in your code. While you can
-- write:
--
-- @
-- j = 'JsonObject' ('intoMap' [('JsonKey' "answer", 'JsonNumber' 42)])
-- @
--
-- and it would be correct, enabling:
--
-- @
-- \{\-\# LANGUAGE OverloadedStrings \#\-\}
-- \{\-\# LANGUAGE OverloadedLists \#\-\}
-- @
--
-- allows you to write:
--
-- @
-- j = 'JsonObject' [("answer", 42)]
-- @
--
-- which you is somewhat less cumbersome in declaration-heavy code. You're
-- certainly welcome to use the constructors if you find it makes for more
-- readable code or if you need the type annotations.
module Core.Encoding.Json
( -- * Encoding and Decoding
encodeToUTF8,
decodeFromUTF8,
JsonValue (..),
JsonKey (..),
-- * Syntax highlighting
-- |
-- Support for pretty-printing JSON values with syntax highlighting using the
-- __prettyprinter__ library. To output a JSON structure to terminal
-- colourized with ANSI escape codes you can use the 'Render' instance:
--
-- @
-- debug "j" (render j)
-- @
--
-- will get you:
--
-- @
-- 23:46:04Z (00000.007) j =
-- {
-- "answer": 42.0
-- }
-- @
JsonToken (..),
colourizeJson,
prettyKey,
prettyValue,
)
where
import Core.Data.Structures (Key, Map, fromMap, intoMap)
import Core.Text.Bytes (Bytes, fromBytes, intoBytes)
import Core.Text.Rope (Rope, Textual, fromRope, intoRope)
import Core.Text.Utilities
( brightBlue,
brightGrey,
brightMagenta,
dullBlue,
dullCyan,
dullGreen,
dullYellow,
AnsiColour,
Render(Token, highlight, colourize) )
import qualified Data.Aeson as Aeson
import Data.Coerce
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import Data.Hashable (Hashable)
import Data.Scientific (Scientific)
import Data.String (IsString (..))
import qualified Data.Text as T
import Data.Text.Prettyprint.Doc
( Doc,
Pretty (..),
annotate,
comma,
dquote,
group,
hcat,
indent,
lbrace,
lbracket,
line,
line',
nest,
punctuate,
rbrace,
rbracket,
sep,
unAnnotate,
viaShow,
vsep,
(<+>),
)
import qualified Data.Vector as V
import GHC.Generics
-- |
-- Given a JSON value, encode it to UTF-8 bytes
--
-- I know we're not /supposed/ to rely on types to document functions, but
-- really, this one does what it says on the tin.
encodeToUTF8 :: JsonValue -> Bytes
encodeToUTF8 = intoBytes . Aeson.encode . intoAeson
-- |
-- Given an array of bytes, attempt to decode it as a JSON value.
decodeFromUTF8 :: Bytes -> Maybe JsonValue
decodeFromUTF8 b =
let x :: Maybe Aeson.Value
x = Aeson.decodeStrict' (fromBytes b)
in fmap fromAeson x
-- |
-- A JSON value.
data JsonValue
= JsonObject (Map JsonKey JsonValue)
| JsonArray [JsonValue]
| JsonString Rope
| JsonNumber Scientific
| JsonBool Bool
| JsonNull
deriving (Eq, Show, Generic)
--
-- Overloads so that Haskell code literals can be interpreted as JSON
-- values. Obviously these are a lot on the partial side, but what else are
-- you supposed to do? This is all Haskell gives us for getting at
-- literals.
--
instance IsString JsonValue where
fromString :: String -> JsonValue
fromString = JsonString . intoRope
instance Num JsonValue where
fromInteger = JsonNumber . fromInteger
(+) = error "Sorry, you can't add JsonValues"
(-) = error "Sorry, you can't negate JsonValues"
(*) = error "Sorry, you can't multiply JsonValues"
abs = error "Sorry, not applicable for JsonValues"
signum = error "Sorry, not applicable for JsonValues"
instance Fractional JsonValue where
fromRational :: Rational -> JsonValue
fromRational = JsonNumber . fromRational
(/) = error "Sorry, you can't do division on JsonValues"
intoAeson :: JsonValue -> Aeson.Value
intoAeson value = case value of
JsonObject xm ->
let kvs = fromMap xm
tvs = fmap (\(k, v) -> (fromRope (coerce k), intoAeson v)) kvs
tvm :: HashMap T.Text Aeson.Value
tvm = HashMap.fromList tvs
in Aeson.Object tvm
JsonArray xs ->
let vs = fmap intoAeson xs
in Aeson.Array (V.fromList vs)
JsonString x -> Aeson.String (fromRope x)
JsonNumber x -> Aeson.Number x
JsonBool x -> Aeson.Bool x
JsonNull -> Aeson.Null
-- |
-- Keys in a JSON object.
newtype JsonKey
= JsonKey Rope
deriving (Eq, Show, Generic, IsString, Ord)
instance Hashable JsonKey
instance Key JsonKey
-- FIXME what is this instance?
instance Aeson.ToJSON Rope where
toJSON text = Aeson.toJSON (fromRope text :: T.Text) -- BAD
instance Textual JsonKey where
fromRope t = coerce t
intoRope x = coerce x
fromAeson :: Aeson.Value -> JsonValue
fromAeson value = case value of
Aeson.Object o ->
let tvs = HashMap.toList o
kvs = fmap (\(k, v) -> (JsonKey (intoRope k), fromAeson v)) tvs
kvm :: Map JsonKey JsonValue
kvm = intoMap kvs
in JsonObject kvm
Aeson.Array v -> JsonArray (fmap fromAeson (V.toList v))
Aeson.String t -> JsonString (intoRope t)
Aeson.Number n -> JsonNumber n
Aeson.Bool x -> JsonBool x
Aeson.Null -> JsonNull
--
-- Pretty printing
--
data JsonToken
= SymbolToken
| QuoteToken
| KeyToken
| StringToken
| EscapeToken
| NumberToken
| BooleanToken
| LiteralToken
instance Render JsonValue where
type Token JsonValue = JsonToken
colourize = colourizeJson
highlight = prettyValue
instance Render JsonKey where
type Token JsonKey = JsonToken
colourize = colourizeJson
highlight = prettyKey
instance Render Aeson.Value where
type Token Aeson.Value = JsonToken
colourize = colourizeJson
highlight = prettyValue . fromAeson
--
-- Ugh. If you want to experiment with narrower output, then:
--
-- . layoutPretty (LayoutOptions {layoutPageWidth = AvailablePerLine 15 1.0}) . prettyValue
--
-- |
-- Used by the 'Render' instance to turn symbolic annotations into ANSI colours annotations.
-- If you're curious, the render pipeline looks like:
--
-- @
-- render = 'intoText' . 'renderStrict' . 'reAnnotateS' 'colourize'
-- . 'layoutPretty' 'defaultLayoutOptions' . 'prettyValue'
-- @
colourizeJson :: JsonToken -> AnsiColour
colourizeJson token = case token of
SymbolToken -> brightGrey
QuoteToken -> brightGrey
KeyToken -> brightBlue
StringToken -> dullCyan
EscapeToken -> dullYellow
NumberToken -> dullGreen
BooleanToken -> brightMagenta
LiteralToken -> dullBlue
instance Pretty JsonKey where
pretty = unAnnotate . prettyKey
prettyKey :: JsonKey -> Doc JsonToken
prettyKey (JsonKey t) =
annotate QuoteToken dquote
<> annotate KeyToken (pretty (fromRope t :: T.Text))
<> annotate QuoteToken dquote
instance Pretty JsonValue where
pretty = unAnnotate . prettyValue
prettyValue :: JsonValue -> Doc JsonToken
prettyValue value = case value of
JsonObject xm ->
let pairs = fromMap xm
entries = fmap (\(k, v) -> (prettyKey k) <> annotate SymbolToken ":" <+> clear v (prettyValue v)) pairs
clear v doc = case v of
(JsonObject _) -> line <> doc
(JsonArray _) -> group doc
_ -> doc
in if length entries == 0
then annotate SymbolToken (lbrace <> rbrace)
else annotate SymbolToken lbrace <> line <> indent 4 (vsep (punctuate (annotate SymbolToken comma) entries)) <> line <> annotate SymbolToken rbrace
JsonArray xs ->
let entries = fmap prettyValue xs
in line'
<> nest
4
( annotate SymbolToken lbracket
<> line' -- first line not indented
<> sep (punctuate (annotate SymbolToken comma) entries)
)
<> line'
<> annotate SymbolToken rbracket
JsonString x ->
annotate QuoteToken dquote
<> annotate StringToken (escapeText x)
<> annotate QuoteToken dquote
JsonNumber x -> annotate NumberToken (viaShow x)
JsonBool x -> case x of
True -> annotate BooleanToken "true"
False -> annotate BooleanToken "false"
JsonNull -> annotate LiteralToken "null"
{-# INLINEABLE prettyValue #-}
escapeText :: Rope -> Doc JsonToken
escapeText text =
let t = fromRope text :: T.Text
ts = T.split (== '"') t
ds = fmap pretty ts
in hcat (punctuate (annotate EscapeToken "\\\"") ds)
{-# INLINEABLE escapeText #-}