packages feed

scrod-0.2026.2.21: source/library/Scrod/Json/ToJson.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}

-- | Type class for converting values into JSON.
module Scrod.Json.ToJson where

import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Proxy as Proxy
import qualified Data.Text as Text
import qualified GHC.Generics as Generics
import qualified GHC.TypeLits as TypeLits
import qualified Numeric.Natural as Natural
import qualified Scrod.Json.Value as Json

-- | Convert a value to a JSON 'Json.Value'.
class ToJson a where
  toJson :: a -> Json.Value

instance ToJson Bool where
  toJson = Json.boolean

instance ToJson Text.Text where
  toJson = Json.text

instance ToJson Int where
  toJson = Json.integral

instance ToJson Natural.Natural where
  toJson = Json.integral

instance (ToJson a) => ToJson (Maybe a) where
  toJson = maybe Json.null toJson

instance (ToJson a) => ToJson [a] where
  toJson = Json.arrayOf toJson

instance (ToJson a) => ToJson (NonEmpty.NonEmpty a) where
  toJson = toJson . NonEmpty.toList

-- | Generic JSON encoding. Dispatches between record encoding (single
-- constructor produces a JSON object) and tagged encoding (sum type
-- produces @{\"type\": \"Name\", \"value\": ...}@ objects).
--
-- Use @deriving via 'Generics.Generically'@ with a 'Generics.Generic'
-- instance to derive 'ToJson' for record types, enum types, and tagged
-- sum types.
class GToJson f where
  gToJson :: f p -> Json.Value

instance (GToJson f) => GToJson (Generics.M1 Generics.D c f) where
  gToJson (Generics.M1 x) = gToJson x

instance (GToJsonFields f) => GToJson (Generics.M1 Generics.C c f) where
  gToJson (Generics.M1 x) =
    Json.object (filter (\(_, v) -> v /= Json.null) (gToJsonFields x))

instance (GToJsonSum f, GToJsonSum g) => GToJson (f Generics.:+: g) where
  gToJson = gToJsonSum

-- | Extract record fields as key-value pairs for JSON object encoding.
class GToJsonFields f where
  gToJsonFields :: f p -> [(String, Json.Value)]

instance
  (TypeLits.KnownSymbol name, ToJson a) =>
  GToJsonFields (Generics.M1 Generics.S ('Generics.MetaSel ('Just name) su ss ds) (Generics.K1 i a))
  where
  gToJsonFields (Generics.M1 (Generics.K1 x)) =
    [(TypeLits.symbolVal (Proxy.Proxy :: Proxy.Proxy name), toJson x)]

instance (GToJsonFields f, GToJsonFields g) => GToJsonFields (f Generics.:*: g) where
  gToJsonFields (f Generics.:*: g) = gToJsonFields f <> gToJsonFields g

instance GToJsonFields Generics.U1 where
  gToJsonFields Generics.U1 = []

-- | Tagged encoding for sum type constructors.
class GToJsonSum f where
  gToJsonSum :: f p -> Json.Value

instance (GToJsonSum f, GToJsonSum g) => GToJsonSum (f Generics.:+: g) where
  gToJsonSum (Generics.L1 x) = gToJsonSum x
  gToJsonSum (Generics.R1 x) = gToJsonSum x

instance
  (TypeLits.KnownSymbol name) =>
  GToJsonSum (Generics.M1 Generics.C ('Generics.MetaCons name fix rec) Generics.U1)
  where
  gToJsonSum _ =
    Json.object [("type", Json.string (TypeLits.symbolVal (Proxy.Proxy :: Proxy.Proxy name)))]

instance
  (TypeLits.KnownSymbol name, ToJson a) =>
  GToJsonSum (Generics.M1 Generics.C ('Generics.MetaCons name fix rec) (Generics.M1 Generics.S sel (Generics.K1 i a)))
  where
  gToJsonSum (Generics.M1 (Generics.M1 (Generics.K1 x))) =
    Json.tagged (TypeLits.symbolVal (Proxy.Proxy :: Proxy.Proxy name)) (toJson x)

instance (Generics.Generic a, GToJson (Generics.Rep a)) => ToJson (Generics.Generically a) where
  toJson (Generics.Generically x) = gToJson (Generics.from x)