packages feed

hpack-dhall-0.6.0: library/Hpack/Dhall.hs

{-# LANGUAGE TupleSections #-}

-- |
-- Module: Hpack.Dhall
-- Copyright:
--     © 2018 - 2021 Phil de Joux
--     © 2018 - 2021 Block Scope Limited
-- License: BSD3
-- Maintainer: Phil de Joux <phil.dejoux@blockscope.com>
-- Stability: experimental
-- The functions in this module make it possible to configure an
-- <https://github.com/sol/hpack#readme hpack>
-- package description with
-- <https://github.com/dhall-lang/dhall-lang#readme Dhall>
-- instead of
-- <https://en.wikipedia.org/wiki/YAML YAML>.
-- When doing so, note that all functions resolve imports relative to the location
-- of the given @.dhall@ input file.
module Hpack.Dhall
  ( dhallFileToJson
  , showJson
  , showYaml
  , showDhall
  , packageConfig
  ) where

import Control.Exception (throwIO)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Except (ExceptT (..), runExceptT)
import qualified Control.Monad.Trans.State.Strict as State
import Data.Aeson (ToJSON, Value)
import qualified Data.Aeson.Encode.Pretty as A
import Data.Bifunctor (first)
import qualified Data.ByteString.Lazy as BSL (toStrict)
import Data.Function ((&))
import Data.Maybe (fromMaybe)
import qualified Data.Text as T (Text, unpack)
import Data.Text.Encoding (decodeUtf8)
import qualified Data.Text.IO as T (readFile)
import Data.Void (Void)
import qualified Data.Yaml.Pretty as Y
import Dhall
  ( InputSettings
  , Text
  , defaultInputSettings
  , rootDirectory
  , sourceName
  )
import Dhall.Core (Expr)
import Dhall.Import (emptyStatus, loadWith)
import Dhall.JSON (dhallToJSON, omitNull)
import Dhall.Parser (Src, exprFromText)
import Dhall.Pretty (layoutOpts, prettyExpr)
import Dhall.TypeCheck (typeOf)
import Hpack.Fields (cmp)
import Lens.Micro (set, (^.))
import qualified Prettyprinter as PP
import qualified Prettyprinter.Render.Text as PP
import System.FilePath (takeDirectory)

-- SEE: http://onoffswitch.net/adventures-pretty-printing-json-haskell/
getJson :: ToJSON a => (Text -> Text -> Ordering) -> a -> String
getJson cmp' =
  let cfg = A.defConfig{A.confCompare = cmp'}
   in T.unpack . decodeUtf8 . BSL.toStrict . A.encodePretty' cfg

getYaml :: ToJSON a => (Text -> Text -> Ordering) -> a -> String
getYaml cmp' =
  let cfg = Y.setConfCompare cmp' Y.defConfig
   in T.unpack . decodeUtf8 . Y.encodePretty cfg

-- | The default package file name is @package.dhall@.
packageConfig :: FilePath
packageConfig = "package.dhall"

-- | Pretty prints JSON for the package description.
showJson
  :: Maybe (Text -> Text -> Ordering)
  -- ^ An ordering of JSON fields.
  -> FilePath
  -- ^ Path to a @.dhall@ file
  -> IO String
showJson fieldOrdering file = do
  x <- dhallFileToJson file
  return $ case x of
    Left err -> err
    Right (_, v) -> getJson (fromMaybe cmp fieldOrdering) v

-- | Pretty prints YAML for the package description.
showYaml
  :: Maybe (Text -> Text -> Ordering)
  -- ^ An ordering of YAML fields.
  -> FilePath
  -- ^ Path to a @.dhall@ file
  -> IO String
showYaml fieldOrdering file = do
  x <- dhallFileToJson file
  return $ case x of
    Left err -> err
    Right (_, v) -> getYaml (fromMaybe cmp fieldOrdering) v

-- | Pretty prints the package description Dhall expression, resolving imports
-- relative to the location of the @.dhall@ file.
showDhall
  :: FilePath
  -- ^ Path to a @.dhall@ file
  -> IO String
showDhall file = do
  text <- T.readFile file
  expr <- check (inputSettings file) text
  return . T.unpack $ renderDhall expr

-- | A file decoder for hpack. This should evaluate to a single record with
-- hpack's top-level <https://github.com/sol/hpack#top-level-fields fields>.
dhallFileToJson
  :: FilePath
  -- ^ Path to a @.dhall@ file
  -> IO (Either String ([String], Value))
dhallFileToJson file =
  liftIO (T.readFile file)
    >>= dhallTextToJson (inputSettings file)

inputSettings :: FilePath -> InputSettings
inputSettings file =
  Dhall.defaultInputSettings
    & set rootDirectory (takeDirectory file)
    & set sourceName file

dhallTextToJson
  :: InputSettings
  -> T.Text
  -> IO (Either String ([String], Value))
dhallTextToJson settings text = runExceptT $ do
  expr <- liftIO $ check settings text
  _ <- liftResult $ typeOf expr
  liftResult $ ([],) . omitNull <$> dhallToJSON expr
  where
    liftResult :: (Show b, Monad m) => Either b a -> ExceptT String m a
    liftResult = ExceptT . return . first show

check :: InputSettings -> Text -> IO (Expr Src Void)
check settings text = do
  expr <- either throwIO return $ exprFromText mempty text
  State.evalStateT (loadWith expr) (emptyStatus $ settings ^. rootDirectory)

-- SEE: https://github.com/mstksg/hakyll-dhall
renderDhall :: PP.Pretty a => Expr Src a -> T.Text
renderDhall =
  PP.renderStrict
    . PP.layoutSmart layoutOpts
    . PP.unAnnotate
    . prettyExpr