packages feed

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 #-}