packages feed

proto3-suite-0.7.0: src/Proto3/Suite/DhallPB.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleInstances #-}

#ifdef LARGE_RECORDS
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
#endif

{-# OPTIONS_GHC -fno-warn-orphans #-}

#if !(MIN_VERSION_dhall(1,27,0))
#define FromDhall Interpret
#define ToDhall Inject
#endif

module Proto3.Suite.DhallPB
  ( -- * Modules
    module Dhall
  )
where
import           Data.Functor.Contravariant  (contramap)
import           Data.Int                    (Int32, Int64)
import           Data.Word                   (Word32, Word64)
import           Dhall                       (FromDhall (..), ToDhall (..))
import           GHC.Float                   (double2Float, float2Double)
import           Proto3.Suite.Types          (Enumerated (..), Fixed (..))

import qualified Data.ByteString
import qualified Data.ByteString.Base64
import qualified Data.ByteString.Base64.Lazy
import qualified Data.ByteString.Lazy
import qualified Data.Text.Encoding
import qualified Data.Text.Lazy.Encoding
import qualified Dhall

#if !(MIN_VERSION_dhall(1,27,0))
import qualified Data.Map
#endif

#ifdef LARGE_RECORDS
import Data.Coerce (coerce)
import Data.Proxy (Proxy (..))
import Data.Void (Void)
import qualified Data.Text as Text
import qualified Dhall.Core as Dhall
import qualified Dhall.Map as Dhall
import qualified Dhall.Parser as Dhall
import Data.Record.Generic ((:.:)(Comp), I (..), K (..))
import qualified Data.Record.Generic as LG
import qualified Data.Record.Generic.GHC as LG
import qualified Data.Record.Generic.Rep as LG
#endif

--------------------------------------------------------------------------------
-- Interpret the special 'Enumerated' type

instance Dhall.FromDhall a => Dhall.FromDhall (Enumerated a)

instance Dhall.FromDhall a => Dhall.FromDhall (Either Int32 a)

--------------------------------------------------------------------------------
-- Interpret the strict and lazy ByteString types
--
-- We must base-64 decode a 'ByteString' after encoding it from a Text
-- because it may contain invalid UTF-8 data and Dhall does not have a
-- native type for bytes.

instance Dhall.FromDhall Data.ByteString.Lazy.ByteString where
  autoWith _ = fmap b64Decode Dhall.lazyText
    where
      b64Decode = Data.ByteString.Base64.Lazy.decodeLenient . Data.Text.Lazy.Encoding.encodeUtf8

instance Dhall.FromDhall Data.ByteString.ByteString where
  autoWith _ = fmap b64Decode Dhall.strictText
    where
      b64Decode =  Data.ByteString.Base64.decodeLenient . Data.Text.Encoding.encodeUtf8

--------------------------------------------------------------------------------
-- Interpret integer scalar types

-- Lossy interpretation and integer overflow can happen with the
-- following instances because the 'Dhall.Integer' (Dhall's only
-- integer type) equals Haskell's Integer type. We don't expect these
-- instances to introduce integer overflow because they should only
-- interpret Dhall rendered from protobuf messages created with
-- generated code.
--
-- TODO: we should perform run-time bounds-checking to at least hint
-- to the user that we interpreted something bad.

#if !MIN_VERSION_dhall(1,35,0)
instance Dhall.FromDhall Int where
  autoWith _ = fmap fromInteger Dhall.integer

instance Dhall.FromDhall Int32 where
  autoWith _ = fmap fromInteger Dhall.integer

instance Dhall.FromDhall Int64 where
  autoWith _ = fmap fromInteger Dhall.integer

instance Dhall.FromDhall Word32 where
  autoWith _ = fmap fromIntegral Dhall.integer

instance Dhall.FromDhall Word64 where
  autoWith _ = fmap fromIntegral Dhall.integer
#endif

instance Dhall.FromDhall (Fixed Int32) where
  autoWith = fmap Fixed . Dhall.autoWith

instance Dhall.FromDhall (Fixed Int64) where
  autoWith = fmap Fixed . Dhall.autoWith

instance Dhall.FromDhall (Fixed Word32) where
  autoWith = fmap Fixed . Dhall.autoWith

instance Dhall.FromDhall (Fixed Word64) where
  autoWith = fmap Fixed . Dhall.autoWith

--------------------------------------------------------------------------------
-- Interpret floating point scalar types
--
-- Loss of precision can happen when converting a 'Double' to a
-- 'Float'. We don't expect this instance to introduce loss of
-- precision because it should only interpret Dhall rendered from
-- protobuf messages created with generated code. The Dhall rendering
-- converts from a 'Float' to the 'Dhall.Double' type.

instance Dhall.FromDhall Float where
  autoWith _ = fmap double2Float Dhall.double

#if !(MIN_VERSION_dhall(1,27,0))
--------------------------------------------------------------------------------
-- Interpret maps
--
-- Dhall has no map type.  We resort to an association list,
-- though that is not safe because keys may be repeated.

instance (Dhall.Interpret k, Dhall.Interpret v, Ord k) =>
         Dhall.Interpret (Data.Map.Map k v) where
  autoWith = fmap (fmap Data.Map.fromList) Dhall.autoWith
#endif

--------------------------------------------------------------------------------
-- Inject the special 'Enumerated' type

instance Dhall.ToDhall a => Dhall.ToDhall (Enumerated a)

instance Dhall.ToDhall a => Dhall.ToDhall (Either Int32 a)

--------------------------------------------------------------------------------
-- Inject integer scalar types

#if !(MIN_VERSION_dhall(1,41,0))
instance Dhall.ToDhall Int32 where
  injectWith = fmap (contramap toInteger) Dhall.injectWith

instance Dhall.ToDhall Int64 where
  injectWith = fmap (contramap toInteger) Dhall.injectWith
#endif

instance Dhall.ToDhall (Fixed Int32) where
  injectWith = fmap (contramap fixed) Dhall.injectWith

instance Dhall.ToDhall (Fixed Int64) where
  injectWith = fmap (contramap fixed) Dhall.injectWith

instance Dhall.ToDhall (Fixed Word32) where
  injectWith = fmap (contramap fixed) Dhall.injectWith

instance Dhall.ToDhall (Fixed Word64) where
  injectWith = fmap (contramap fixed) Dhall.injectWith

--------------------------------------------------------------------------------
-- Inject floating point scalar types

instance Dhall.ToDhall Float where
  injectWith = fmap (contramap float2Double) Dhall.injectWith

--------------------------------------------------------------------------------
-- Inject strict and lazy ByteStrings
--
-- We must base-64 encode a 'ByteString' before decoding it to a Text
-- because it may contain invalid UTF-8 data and Dhall does not have a
-- native type for bytes.

instance Dhall.ToDhall Data.ByteString.Lazy.ByteString where
  injectWith = fmap (contramap b64Encode) Dhall.injectWith
    where
      -- 'decodeUtf8' will throw an error on any invalid UTF-8 data
      -- but we should never encounter that case with this usage
      -- because we Base64 encode the ByteString first
      b64Encode = Data.Text.Lazy.Encoding.decodeUtf8 . Data.ByteString.Base64.Lazy.encode

instance Dhall.ToDhall Data.ByteString.ByteString where
  injectWith = fmap (contramap b64Encode) Dhall.injectWith
    where
      -- 'decodeUtf8' will throw an error on any invalid UTF-8 data
      -- but we should never encounter that case with this usage
      -- because we Base64 encode the ByteString first
      b64Encode = Data.Text.Encoding.decodeUtf8 . Data.ByteString.Base64.encode

#if !(MIN_VERSION_dhall(1,27,0))
--------------------------------------------------------------------------------
-- Inject maps
--
-- Dhall has no map type.  We resort to an association list,
-- though that is not safe because keys may be repeated.

instance (Dhall.Inject k, Dhall.Inject v) =>
         Dhall.Inject (Data.Map.Map k v) where
  injectWith = fmap (contramap Data.Map.toAscList) Dhall.injectWith
#endif

#ifdef LARGE_RECORDS

instance (LG.Generic a, LG.Constraints a Dhall.FromDhall) => Dhall.GenericFromDhall a (LG.ThroughLRGenerics a) where
  genericAutoWithNormalizer _ normalizer _ = pure $ coerce $ Dhall.Decoder extract expected
    where
      makeCompRep :: (forall x. Dhall.FromDhall x => K String x -> f (g x)) -> LG.Rep (f :.: g) a
      makeCompRep f = LG.cmap
                        (Proxy @Dhall.FromDhall)
                        (Comp . f)
                        (LG.recordFieldNames $ LG.metadata (Proxy @a))

      extract :: Dhall.Expr Dhall.Src Void -> Dhall.Extractor Dhall.Src Void a
      extract expr =
        case expr of
          Dhall.RecordLit flds -> do
            let getField :: forall x. Dhall.FromDhall x => K String x -> Dhall.Extractor Dhall.Src Void x
                getField (K fld) =
                  let decoder = Dhall.autoWith @x normalizer
                  in maybe
                       (Dhall.typeError expected expr)
                       (Dhall.extract decoder)
                       (Dhall.recordFieldValue <$> Dhall.lookup (Text.pack fld) flds)
            LG.to <$> LG.sequenceA (makeCompRep (fmap I . getField))
          _ -> Dhall.typeError expected expr

      expected :: Dhall.Expector (Dhall.Expr Dhall.Src Void)
      expected = do
        let getField :: forall x. Dhall.FromDhall x => K String x -> Dhall.Expector (Dhall.Text, Dhall.RecordField Dhall.Src Void)
            getField (K fld) = do
              let decoder = Dhall.autoWith @x normalizer
              f <- Dhall.makeRecordField <$> Dhall.expected decoder
              pure (Text.pack fld, f)
        Dhall.Record . Dhall.fromList . LG.collapse <$> LG.sequenceA (makeCompRep (fmap K . getField))

instance (LG.Generic a, LG.Constraints a Dhall.ToDhall) => Dhall.GenericToDhall (LG.ThroughLRGenerics a) where
  genericToDhallWithNormalizer normalizer _ = pure $ coerce $ Dhall.Encoder embed declared
    where
      md = LG.metadata (Proxy @a)
      fieldNames = LG.recordFieldNames md

      embed :: a -> Dhall.Expr Dhall.Src Void
      embed = Dhall.RecordLit
              . Dhall.fromList
              . LG.collapse
              . LG.zipWith (LG.mapKKK $ \n x -> (Text.pack n, Dhall.makeRecordField x)) fieldNames
              . LG.cmap (Proxy @Dhall.ToDhall) (K . Dhall.embed (injectWith normalizer) . LG.unI)
              . LG.from

      declared :: Dhall.Expr Dhall.Src Void
      declared = Dhall.Record
                 $ Dhall.fromList
                 $ LG.collapse
                 $ LG.cmap
                     (Proxy @Dhall.ToDhall)
                     (\(K n :: K String x) ->
                         let typ = Dhall.declared (injectWith @x normalizer)
                         in K (Text.pack n, Dhall.makeRecordField typ))
                     fieldNames

#endif