packages feed

aeson-match-qq-1.8.0: src/Aeson/Match/QQ/Internal/AesonUtils.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
module Aeson.Match.QQ.Internal.AesonUtils
  ( toJSONE
  , pp
  ) where

import Data.Aeson qualified as Aeson
import Data.Aeson.Encoding.Internal qualified as Aeson (encodingToLazyByteString)
import Data.Aeson.KeyMap qualified as Aeson.KeyMap
import Data.Bool (bool)
import Data.Foldable (toList)
import Data.List qualified as List
import Data.HashMap.Strict (HashMap)
import Data.HashMap.Strict qualified as HashMap
import Data.Int (Int64)
import Data.Scientific (Scientific, floatingOrInteger)
import Data.String (fromString)
import Data.Text (Text)
import Data.Vector (Vector)
import Text.PrettyPrint ((<+>))
import Text.PrettyPrint qualified as PP


-- | This is a round-about way to produce a 'Aeson.Value' from a 'ToJSON' instance.
-- it is written this way to avoid calling 'Aeson.toJSON' which might be undefined
-- for some datatypes that only implement 'toEncoding'.
--
-- It is defined in a separate module due to the TH stage restrictions as we need
-- to 'lift' 'toJSONE' eventually.
toJSONE :: Aeson.ToJSON x => x -> Aeson.Value
toJSONE x =
  let
    ~(Just val) = conv x
    -- ^ the pattern is irrefutable because we assume that it is always possible
    -- to recover a Value from an Encoding generated by Aeson.toEncoding
  in
    val
 where
  conv =
    Aeson.decode . Aeson.encodingToLazyByteString . Aeson.toEncoding

-- | A super-basic re-implementation of aeson-pretty. This function attains 2 goals:
--
--   - we avoid another dependency
--   - it uses the same prettyprinter everything else uses, and thus
--   it is easily integrated.
pp :: Aeson.Value -> PP.Doc
pp = \case
  Aeson.Null ->
    rNull
  Aeson.Bool b ->
    rBool b
  Aeson.Number n ->
    rNumber n
  Aeson.String str ->
    rString str
  Aeson.Array xs ->
    rArray xs
  Aeson.Object o ->
    rObject (Aeson.KeyMap.toHashMapText o)
 where
  rNull :: PP.Doc
  rNull =
    "null"

  rBool :: Bool -> PP.Doc
  rBool =
    bool "false" "true"

  rNumber :: Scientific -> PP.Doc
  rNumber =
    fromString . either (show @Double) (show @Int64) . floatingOrInteger

  rString :: Text -> PP.Doc
  rString =
    fromString . show

  rArray :: Vector Aeson.Value -> PP.Doc
  rArray values =
    case toList values of
      [] ->
        "[]"
      x : xs ->
        PP.vcat $
          ["[" <+> pp x] <>
          map (\x' -> "," <+> pp x') xs <>
          ["]"]

  rObject :: HashMap Text Aeson.Value -> PP.Doc
  rObject values =
    case List.sortOn fst (HashMap.toList values) of
      [] ->
        "{}"
      kv : kvs ->
        PP.vcat $
          ["{" <+> rKeyValue kv] <>
          map (\kv' -> "," <+> rKeyValue kv') kvs <>
          ["}"]
   where
    rKeyValue (key, value) =
      if simpleValue value then
        (rString key <> ":") <+> pp value
      else
        PP.vcat
          [ rString key <> ":"
          , pp value
          ]

  simpleValue :: Aeson.Value -> Bool
  simpleValue = \case
    Aeson.Null {} ->
      True
    Aeson.Bool {} ->
      True
    Aeson.Number {} ->
      True
    Aeson.String {} ->
      True
    Aeson.Array {} ->
      False
    Aeson.Object {} ->
      False