{-# LANGUAGE OverloadedStrings #-} {-| This library exports two functions: `dhallToCsv` and `codeToValue`. The former converts a Dhall expression (with imports resolved already) into a sequence of CSV `NamedRecord`s (from the @cassava@ library) while the latter converts a `Text` containing Dhall code into a list of CSV `NamedRecord`s. Not all Dhall expressions can be converted to CSV since CSV is not a programming language. The only things you can convert are @List@s of records where each field is one of the following types: * @Bool@s * @Natural@s * @Integer@s * @Double@s * @Text@ values * @Optional@ (of valid field types) * unions (of empty alternatives or valid record field types) Dhall @Bool@s translate to either `"true"` or `"false"` in all lowercase letters: > $ dhall-to-csv <<< '[{ exampleBool = True }]' > exampleBool > true > $ dhall-to-csv <<< '[{ exampleBool = False }]' > exampleBool > false Dhall numbers translate to their string representations: > $ dhall-to-csv <<< '[{ exampleInteger = +2 }]' > exampleInteger > 2 > $ dhall-to-csv <<< '[{ exampleNatural = 2 }]' > exampleNatural > 2 > $ dhall-to-csv <<< '[{ exampleDouble = 2.3 }]' > exampleDouble > 2.3 Dhall @Text@ translates directly to CSV. Special CSV characters are enclosed by double quotes: > $ dhall-to-csv <<< '[{ exampleText = "ABC" }]' > exampleText > ABC > $ dhall-to-csv <<< '[{ exampleText = "ABC,ABC" }]' > exampleText > "ABC,ABC" Dhall @Optional@ values translate to the empty string if absent and the unwrapped value otherwise: > $ dhall-to-csv <<< '[{ exampleOptional = None Natural }]' > exampleOptional > > $ dhall-to-csv <<< '[{ exampleOptional = Some 1 }]' > exampleOptional > 1 Dhall unions translate to the wrapped value or the name of the field (in case it is an empty field): > $ dhall-to-csv <<< "[{ exampleUnion = < Left | Right : Natural>.Left }]" > exampleUnion > Left > $ dhall-to-csv <<< "[{ exampleUnion = < Left | Right : Natural>.Right 2 }]" > exampleUnion > 2 Also, all Dhall expressions are normalized before translation to CSV: > $ dhall-to-csv <<< "[{ equality = True == False }]" > equality > false -} module Dhall.Csv ( dhallToCsv , codeToValue -- * Exceptions , CompileError ) where import Control.Exception (Exception, displayException, throwIO) import Data.Csv (NamedRecord, ToField (..)) import Data.Either (fromRight) import Data.Maybe (fromMaybe) import Data.Sequence (Seq) import Data.Text (Text) import Data.Void (Void) import Dhall.Core (DhallDouble (..), Expr) import Dhall.Import (SemanticCacheMode (..)) import Dhall.Util (_ERROR) import Prettyprinter (Pretty) import qualified Data.Csv import qualified Data.Foldable import qualified Data.Text import qualified Dhall.Core as Core import qualified Dhall.Import import qualified Dhall.Map import qualified Dhall.Parser import qualified Dhall.Pretty import qualified Dhall.TypeCheck as TypeCheck import qualified Dhall.Util import qualified Prettyprinter.Render.Text as Pretty import qualified System.FilePath {-| This is the exception type for errors that can arise when converting from Dhall to CSV. It contains information on the specific cases that might fail to give a better insight. -} data CompileError = Unsupported (Expr Void Void) | NotAList (Expr Void Void) | NotARecord (Expr Void Void) | BareNone deriving (Show) instance Exception CompileError where displayException (Unsupported e) = Data.Text.unpack $ _ERROR <> ": Cannot translate record field to CSV \n\ \ \n\ \Explanation: Only the following types of record fields can be converted to CSV: \n\ \ \n\ \● ❰Bool❱ \n\ \● ❰Natural❱ \n\ \● ❰Integer❱ \n\ \● ❰Double❱ \n\ \● ❰Text❱ \n\ \● ❰Optional t❱ (where ❰t❱ is a valid record field type) \n\ \● Unions * \n\ \ \n\ \* Unions can have empty alternatives or alternatives with valid \n\ \ record field types \n\ \ \n\ \The following Dhall expression could not be translated to CSV: \n\ \ \n\ \" <> insert e <> "\n\ \ \n\ \... because it has type: \n\ \ \n\ \" <> insert (fromRight e (TypeCheck.typeOf e)) displayException (NotAList e) = Data.Text.unpack $ _ERROR <> ": Top level object must be of type ❰List❱ \n\ \ \n\ \Explanation: To translate to CSV you must provide a list of records. \n\ \Other types can not be translated directly. \n\ \ \n\ \Expected an expression of type List {...} but instead got the following \n\ \expression: \n\ \ \n\ \" <> insert e <> "\n\ \ \n\ \... which has type: \n\ \ \n\ \" <> insert (fromRight e (TypeCheck.typeOf e)) displayException (NotARecord e) = Data.Text.unpack $ _ERROR <> ": Elements of the top-level list must be records \n\ \ \n\ \Explanation: To translate to CSV you must provide a list of records. \n\ \Other types can not be translated directly. \n\ \ \n\ \Expected a record but instead got the following expression: \n\ \ \n\ \" <> insert e <> "\n\ \ \n\ \... which has type: \n\ \ \n\ \" <> insert (fromRight e (TypeCheck.typeOf e)) displayException BareNone = Data.Text.unpack $ _ERROR <> ": ❰None❱ is not valid on its own \n\ \ \n\ \Explanation: The conversion to CSV does not accept ❰None❱ in isolation as a \n\ \valid way to represent a null value. In Dhall, ❰None❱ is a function whose \n\ \input is a type and whose output is an ❰Optional❱ of that type. \n\ \ \n\ \For example: \n\ \ \n\ \ \n\ \ ┌─────────────────────────────────┐ ❰None❱ is a function whose result is \n\ \ │ None : ∀(a : Type) → Optional a │ an ❰Optional❱ value, but the function \n\ \ └─────────────────────────────────┘ itself is not a valid ❰Optional❱ value \n\ \ \n\ \ \n\ \ ┌─────────────────────────────────┐ ❰None Natural❱ is a valid ❰Optional❱ \n\ \ │ None Natural : Optional Natural │ value (an absent ❰Natural❱ number in \n\ \ └─────────────────────────────────┘ this case) \n\ \ \n\ \ \n\ \ \n\ \The conversion to CSV only translates the fully applied form to empty string. " insert :: Pretty a => a -> Text insert = Pretty.renderStrict . Dhall.Pretty.layout . Dhall.Util.insert {-| Convert a Dhall expression (with resolved imports) to an sequence of CSV @NamedRecord@s. -} dhallToCsv :: Expr s Void -> Either CompileError (Seq NamedRecord) dhallToCsv e0 = listConvert $ Core.normalize e0 where listConvert :: Expr Void Void -> Either CompileError (Seq NamedRecord) listConvert (Core.ListLit _ a) = traverse recordConvert a listConvert e = Left $ NotAList e recordConvert :: Expr Void Void -> Either CompileError NamedRecord recordConvert (Core.RecordLit a) = do a' <- traverse (fieldConvert . Core.recordFieldValue) a return $ Data.Csv.toNamedRecord $ Dhall.Map.toMap a' recordConvert e = Left $ NotARecord e fieldConvert :: Expr Void Void -> Either CompileError Data.Csv.Field fieldConvert (Core.BoolLit True) = return $ toField ("true" :: Text) fieldConvert (Core.BoolLit False) = return $ toField ("false" :: Text) fieldConvert (Core.NaturalLit a) = return $ toField a fieldConvert (Core.IntegerLit a) = return $ toField a fieldConvert (Core.DoubleLit (DhallDouble a)) = return $ toField a fieldConvert (Core.TextLit (Core.Chunks [] a)) = return $ toField a fieldConvert (Core.App (Core.Field (Core.Union _) _) a) = fieldConvert a fieldConvert (Core.Field (Core.Union _) (Core.FieldSelection _ k _)) = return $ toField k fieldConvert (Core.Some e) = fieldConvert e fieldConvert (Core.App Core.None _) = return $ toField ("" :: Text) fieldConvert Core.None = Left BareNone fieldConvert e = Left $ Unsupported e {-| Convert a @Text@ with Dhall code to a list of @NamedRecord@s. -} codeToValue :: Maybe FilePath -> Text -> IO [NamedRecord] codeToValue mFilePath code = do parsedExpression <- Core.throws (Dhall.Parser.exprFromText (fromMaybe "(input)" mFilePath) code) let rootDirectory = case mFilePath of Nothing -> "." Just fp -> System.FilePath.takeDirectory fp resolvedExpression <- Dhall.Import.loadRelativeTo rootDirectory UseSemanticCache parsedExpression _ <- Core.throws (TypeCheck.typeOf resolvedExpression) case dhallToCsv resolvedExpression of Left err -> throwIO err Right csv -> return $ Data.Foldable.toList csv