packages feed

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

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
module Aeson.Match.QQ.Internal.PrettyPrint
  ( pp
  ) where

import Control.Monad ((<=<))
import Data.Aeson qualified as Aeson
import Data.Bool (bool)
import Data.ByteString.Lazy qualified as ByteString.Lazy
import Data.CaseInsensitive (CI)
import Data.CaseInsensitive qualified as CI
import Data.Char qualified as Char
import Data.Foldable (toList)
import Data.HashMap.Strict (HashMap)
import Data.HashMap.Strict qualified as HashMap
import Data.Int (Int64)
import Data.List qualified as List
import Data.List.NonEmpty (NonEmpty)
import Data.Scientific (Scientific, floatingOrInteger)
import Data.String (fromString)
import Data.Text qualified as Text
import Data.Text.Encoding qualified as Text
import Data.Text (Text)
import Data.Vector (Vector)
import Text.PrettyPrint ((<+>))
import Text.PrettyPrint qualified as PP

import Aeson.Match.QQ.Internal.Value
  ( Matcher(..)
  , Type(..)
  , Box(..)
  )


pp :: Matcher Aeson.Value -> PP.Doc
pp value =
  PP.vcat
    [ "[qq|"
    , PP.nest 2 (rValue value)
    , "|]"
    ]

rValue :: Matcher Aeson.Value -> PP.Doc
rValue = \case
  Null ->
    rNull
  Bool b ->
    rBool b
  Number n ->
    rNumber n
  String str ->
    rString str
  StringCI str ->
    rStringCI str
  Array xs ->
    rArray xs
  ArrayUO xs ->
    rArrayUO xs
  Object o ->
    rObject o
  Sig type_ nullable v ->
    rSig type_ nullable v
  Var name ->
    rVar name
  Ext ext ->
    rExt ext

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

rStringCI :: CI Text -> PP.Doc
rStringCI str =
  PP.vcat
    [ "(ci)"
    , rString (CI.original str)
    ]

rArray :: Box (Vector (Matcher Aeson.Value)) -> PP.Doc
rArray Box {values, extra} =
  case toList values of
    [] ->
      "[]"
    x : xs ->
      PP.vcat $
        ["[" <+> rValue x] <>
        map (\x' -> "," <+> rValue x') xs <>
        [bool PP.empty ", ..." extra, "]"]

rArrayUO :: Box (Vector (Matcher Aeson.Value)) -> PP.Doc
rArrayUO box =
  PP.vcat
    [ "(unordered)"
    , rArray box
    ]

rObject :: Box (HashMap Text (NonEmpty (Matcher Aeson.Value))) -> PP.Doc
rObject Box {values, extra} =
  case toKeyValues values of
    [] ->
      "{}"
    kv : kvs ->
      PP.vcat $
        ["{" <+> rKeyValue kv] <>
        map (\kv' -> "," <+> rKeyValue kv') kvs <>
        [bool PP.empty ", ..." extra, "}"]
 where
  rKeyValue (key, value) =
    if simpleValue value then
      (rName key <> ":") <+> rValue value
    else
      PP.vcat
        [ rName key <> ":"
        , rValue value
        ]

toKeyValues :: (Ord k, Foldable t) => HashMap k (t v) -> [(k, v)]
toKeyValues =
  traverse toList <=< List.sortOn fst . HashMap.toList

rSig :: Type -> Bool -> Matcher Aeson.Value -> PP.Doc
rSig type_ nullable val =
  rValue val <+> ((":" <+> rType type_) <> bool PP.empty "?" nullable)
 where
  rType = \case
    AnyT -> "any"
    BoolT -> "bool"
    NumberT -> "number"
    StringT -> "string"
    StringCIT -> "ci-string"
    ArrayT -> "array"
    ArrayUOT -> "unordered-array"
    ObjectT -> "object"

rVar :: Text -> PP.Doc
rVar name =
  "_" <> rName name

rName :: Text -> PP.Doc
rName name =
  PP.text (bool (Text.unpack name) (show name) (hasSpaces name))
 where
  hasSpaces =
    Text.any Char.isSpace

rExt :: Aeson.Value -> PP.Doc
rExt =
  fromString . Text.unpack . Text.decodeUtf8 . ByteString.Lazy.toStrict . Aeson.encode

simpleValue :: Matcher Aeson.Value -> Bool
simpleValue = \case
  Null {} ->
    True
  Bool {} ->
    True
  Number {} ->
    True
  String {} ->
    True
  StringCI {} ->
    True
  Array {} ->
    False
  ArrayUO {} ->
    False
  Object {} ->
    False
  Sig {} ->
    True
  Var {} ->
    True
  Ext {} ->
    True