dhall-csv (empty) → 1.0.0
raw patch · 11 files changed
+1404/−0 lines, 11 filesdep +ansi-terminaldep +basedep +bytestringsetup-changed
Dependencies added: ansi-terminal, base, bytestring, cassava, containers, dhall, dhall-csv, either, exceptions, filepath, optparse-applicative, prettyprinter, prettyprinter-ansi-terminal, tasty, tasty-hunit, tasty-silver, text, unordered-containers, vector
Files
- CHANGELOG.md +3/−0
- LICENSE +27/−0
- README.md +10/−0
- Setup.hs +2/−0
- csv-to-dhall/Main.hs +184/−0
- dhall-csv.cabal +116/−0
- dhall-to-csv/Main.hs +109/−0
- src/Dhall/Csv.hs +257/−0
- src/Dhall/Csv/Util.hs +48/−0
- src/Dhall/CsvToDhall.hs +549/−0
- tasty/Main.hs +99/−0
+ CHANGELOG.md view
@@ -0,0 +1,3 @@+1.0.0++* Initial release
+ LICENSE view
@@ -0,0 +1,27 @@+Copyright (c) 2021 Gabriel Gonzalez+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice, this+list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright notice,+this list of conditions and the following disclaimer in the documentation+and/or other materials provided with the distribution.++3. Neither the name of the author nor the names of its contributors may be+used to endorse or promote products derived from this software without+specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,10 @@+# `dhall-csv`++For installation or development instructions, see:++* [`dhall-haskell` - `README`](https://github.com/dhall-lang/dhall-haskell/blob/master/README.md)++## Introduction++This `dhall-csv` package provides two executables to convert bidirectionally +between Dhall and CSV files.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ csv-to-dhall/Main.hs view
@@ -0,0 +1,184 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Main where++import Control.Applicative (optional, (<|>))+import Control.Exception (SomeException)+import Data.Version (showVersion)+import Dhall.CsvToDhall+import Dhall.Pretty (CharacterSet (..))+import Data.Text (Text)+import Options.Applicative (Parser, ParserInfo)++import qualified Control.Exception+import qualified Data.Text.IO as Text.IO+import qualified Dhall.Csv.Util+import qualified Dhall.Core+import qualified Dhall.Util+import qualified GHC.IO.Encoding+import qualified Options.Applicative as Options+import qualified Paths_dhall_csv as Meta+import qualified System.IO as IO+import qualified System.Exit++parserInfo :: ParserInfo Options+parserInfo = Options.info+ ( Options.helper <*> parseOptions)+ ( Options.fullDesc+ <> Options.progDesc "Convert a CSV expression to a Dhall expression, given the expected Dhall type"+ )++data Options+ = Default+ { schema :: Maybe Text+ , conversion :: Conversion+ , file :: Maybe FilePath+ , output :: Maybe FilePath+ , ascii :: Bool+ , plain :: Bool+ , noHeader :: Bool+ }+ | Type+ { file :: Maybe FilePath+ , output :: Maybe FilePath+ , ascii :: Bool+ , plain :: Bool+ , noHeader :: Bool+ }+ | Version+ deriving Show++parseOptions :: Parser Options+parseOptions =+ typeCommand+ <|> ( Default+ <$> optional parseSchema+ <*> parseConversion+ <*> optional parseFile+ <*> optional parseOutput+ <*> parseASCII+ <*> parsePlain+ <*> parseNoHeader+ )+ <|> parseVersion+ where+ typeCommand =+ Options.hsubparser+ (Options.command "type" info <> Options.metavar "type")+ where+ info =+ Options.info parser (Options.progDesc "Output the inferred Dhall type from a CSV value (not implemented)")++ parser =+ Type+ <$> optional parseFile+ <*> optional parseOutput+ <*> parseASCII+ <*> parsePlain+ <*> parseNoHeader++ parseSchema =+ Options.strArgument+ ( Options.metavar "SCHEMA"+ <> Options.help "Dhall type (schema). You can omit the schema to let the executable infer the schema from the CSV value (not implemented)"+ )++ parseVersion =+ Options.flag'+ Version+ ( Options.long "version"+ <> Options.short 'V'+ <> Options.help "Display version"+ )++ parseFile =+ Options.strOption+ ( Options.long "file"+ <> Options.help "Read CSV from a file instead of standard input"+ <> Options.metavar "FILE"+ <> Options.action "file"+ )++ parseOutput =+ Options.strOption+ ( Options.long "output"+ <> Options.help "Write Dhall expression to a file instead of standard output"+ <> Options.metavar "FILE"+ <> Options.action "file"+ )++ parseASCII =+ Options.switch+ ( Options.long "ascii"+ <> Options.help "Format code using only ASCII syntax"+ )++ parsePlain =+ Options.switch+ ( Options.long "plain"+ <> Options.help "Disable syntax highlighting"+ )++ parseNoHeader =+ Options.switch+ ( Options.long "no-header"+ <> Options.help "Convert CSV with no header"+ )+++main :: IO ()+main = do+ GHC.IO.Encoding.setLocaleEncoding GHC.IO.Encoding.utf8++ options <- Options.execParser parserInfo++ let toCharacterSet ascii = case ascii of+ True -> ASCII+ False -> Unicode++ let toCsv hasHeader file = do+ text <- case file of+ Nothing -> Text.IO.getContents+ Just path -> Text.IO.readFile path++ case Dhall.Csv.Util.decodeCsvDefault hasHeader text of+ Left err -> fail err+ Right csv -> pure csv++ let toSchema schema = do+ finalSchema <- case schema of+ Just text -> resolveSchemaExpr text+ Nothing -> fail "Please specify a schema. Type inference has not been implemented"++ typeCheckSchemaExpr id finalSchema++ case options of+ Version ->+ putStrLn (showVersion Meta.version)++ Default{..} -> do+ handle $ do+ let characterSet = toCharacterSet ascii++ csv <- toCsv (not noHeader) file++ finalSchema <- toSchema schema++ expression <- Dhall.Core.throws $ dhallFromCsv conversion finalSchema csv++ Dhall.Util.renderExpression characterSet plain output expression++ Type{} -> do+ putStrLn "type command has not been implemented yet"+++handle :: IO a -> IO a+handle = Control.Exception.handle handler+ where+ handler :: SomeException -> IO a+ handler e = do+ IO.hPutStrLn IO.stderr ""+ IO.hPrint IO.stderr e+ System.Exit.exitFailure
+ dhall-csv.cabal view
@@ -0,0 +1,116 @@+Name: dhall-csv+Version: 1.0.0+Cabal-Version: >=1.10+Build-Type: Simple+License: BSD3+License-File: LICENSE+Copyright: 2021 Marcos Lerones+Author: Marcos Lerones+Maintainer: Gabriel439@gmail.com+Bug-Reports: https://github.com/dhall-lang/dhall-haskell/issues+Synopsis: Convert bidirectionally between Dhall and CSV files.+Description:+ Use this package if you want to convert between Dhall expressions and CSV.+ You can use this package as a library or an executable:+ .+ * See the "Dhall.Csv" or "Dhall.CsvToDhall" modules if you want to use+ this package as a library+ .+ * Use the @dhall-to-csv@ or @csv-to-dhall@ programs from+ this package if you want an executable+ .+ The "Dhall.Csv" and "Dhall.CsvToDhall" modules also contains instructions+ for how to use this package+Category: Compiler+Extra-Source-Files:+ CHANGELOG.md+ README.md+Source-Repository head+ Type: git+ Location: https://github.com/dhall-lang/dhall-haskell/tree/master/dhall-csv++Library+ Hs-Source-Dirs: src+ Build-Depends:+ base >= 4.12.0.0 && < 5 ,+ bytestring < 0.12,+ cassava >= 0.5.0.0 && < 0.6 ,+ containers >= 0.5.9 && < 0.7 ,+ either ,+ exceptions >= 0.8.3 && < 0.11,+ dhall >= 1.39.0 && < 1.41,+ filepath < 1.5 ,+ optparse-applicative ,+ prettyprinter >= 1.5.1 && < 1.8 ,+ text >= 0.11.1.0 && < 1.3 ,+ unordered-containers < 0.3 ,+ vector >= 0.12 && < 0.13+ Exposed-Modules:+ Dhall.Csv+ Dhall.CsvToDhall+ Dhall.Csv.Util+ Other-Modules:+ GHC-Options: -Wall+ Default-Language: Haskell2010++Executable dhall-to-csv+ Hs-Source-Dirs: dhall-to-csv+ Main-Is: Main.hs+ Build-Depends:+ ansi-terminal ,+ base ,+ bytestring ,+ cassava ,+ dhall ,+ dhall-csv ,+ optparse-applicative ,+ prettyprinter ,+ prettyprinter-ansi-terminal >= 1.1.1 && < 1.2 ,+ unordered-containers ,+ vector ,+ text+ Other-Modules:+ Paths_dhall_csv+ GHC-Options: -Wall+ Default-Language: Haskell2010++Executable csv-to-dhall+ Hs-Source-Dirs: csv-to-dhall+ Main-Is: Main.hs+ Build-Depends:+ ansi-terminal ,+ base ,+ bytestring ,+ cassava ,+ dhall ,+ dhall-csv ,+ optparse-applicative ,+ prettyprinter ,+ prettyprinter-ansi-terminal >= 1.1.1 && < 1.2 ,+ unordered-containers ,+ vector ,+ text+ Other-Modules:+ Paths_dhall_csv+ GHC-Options: -Wall+ Default-Language: Haskell2010++Test-Suite tasty+ Type: exitcode-stdio-1.0+ Hs-Source-Dirs: tasty+ Main-Is: Main.hs+ Build-Depends:+ base ,+ bytestring ,+ cassava ,+ dhall ,+ dhall-csv ,+ filepath ,+ tasty < 1.5 ,+ tasty-silver < 3.3 ,+ tasty-hunit >= 0.2 ,+ unordered-containers ,+ text ,+ vector+ GHC-Options: -Wall+ Default-Language: Haskell2010
+ dhall-to-csv/Main.hs view
@@ -0,0 +1,109 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module Main where++import Control.Applicative (optional, (<|>))+import Control.Exception (SomeException)+import Data.Version (showVersion)+import Options.Applicative (Parser, ParserInfo)++import qualified Control.Exception+import qualified Data.Text.IO+import qualified Dhall+import qualified Dhall.Csv+import qualified Dhall.Csv.Util+import qualified GHC.IO.Encoding+import qualified Options.Applicative as Options+import qualified Paths_dhall_csv as Meta+import qualified System.IO+import qualified System.Exit++data Options+ = Options+ { explain :: Bool+ , file :: Maybe FilePath+ , output :: Maybe FilePath+ }+ | Version++parseOptions :: Parser Options+parseOptions =+ ( Options+ <$> parseExplain+ <*> optional parseFile+ <*> optional parseOutput+ )+ <|> parseVersion+ where+ parseExplain =+ Options.switch+ ( Options.long "explain"+ <> Options.help "Explain error messages in detail"+ )++ parseVersion =+ Options.flag'+ Version+ ( Options.long "version"+ <> Options.help "Display version"+ )++ parseFile =+ Options.strOption+ ( Options.long "file"+ <> Options.help "Read expression from a file instead of standard input"+ <> Options.metavar "FILE"+ <> Options.action "file"+ )++ parseOutput =+ Options.strOption+ ( Options.long "output"+ <> Options.help "Write CSV to a file instead of standard output"+ <> Options.metavar "FILE"+ <> Options.action "file"+ )++parserInfo :: ParserInfo Options+parserInfo =+ Options.info+ (Options.helper <*> parseOptions)+ ( Options.fullDesc+ <> Options.progDesc "Compile Dhall to CSV"+ )++main :: IO ()+main = do+ GHC.IO.Encoding.setLocaleEncoding GHC.IO.Encoding.utf8++ options <- Options.execParser parserInfo++ case options of+ Version ->+ putStrLn (showVersion Meta.version)+ Options {..} ->+ handle $ do+ let explaining = if explain then Dhall.detailed else id++ text <- case file of+ Nothing -> Data.Text.IO.getContents+ Just path -> Data.Text.IO.readFile path++ csv <- explaining $ Dhall.Csv.codeToValue file text++ let write =+ case output of+ Nothing -> Data.Text.IO.putStr+ Just path -> Data.Text.IO.writeFile path++ write $ Dhall.Csv.Util.encodeCsvDefault csv++handle :: IO a -> IO a+handle = Control.Exception.handle handler+ where+ handler :: SomeException -> IO a+ handler e = do+ System.IO.hPutStrLn System.IO.stderr ""+ System.IO.hPrint System.IO.stderr e+ System.Exit.exitFailure
+ src/Dhall/Csv.hs view
@@ -0,0 +1,257 @@+{-#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, throwIO, displayException)+import Data.Csv (NamedRecord, ToField (..))+import Data.Either (fromRight)+import Data.Maybe (fromMaybe)+import Data.Sequence (Seq)+import Data.Text (Text)+import Data.Text.Prettyprint.Doc (Pretty)+import Data.Void (Void)+import Dhall.Core (Expr, DhallDouble (..))+import Dhall.Import (SemanticCacheMode (..))+import Dhall.Util (_ERROR)++import qualified Data.Csv+import qualified Data.Foldable+import qualified Data.Text+import qualified Data.Text.Prettyprint.Doc.Render.Text as Pretty+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 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
+ src/Dhall/Csv/Util.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE OverloadedStrings #-}++module Dhall.Csv.Util (encodeCsvDefault, decodeCsvDefault) where++import Data.List (sort)+import Data.Text (Text)+import Data.Csv (NamedRecord, Record)++import qualified Data.ByteString.Lazy as ByteString+import qualified Data.ByteString.Lazy.Char8 as ByteString.Char8+import qualified Data.Csv+import qualified Data.HashMap.Strict as HashMap+import qualified Data.Text.Encoding+import qualified Data.Vector as Vector++{-| Utility to convert a list of @NamedRecord@ to Text formatted as a CSV.+-}+encodeCsvDefault :: [NamedRecord] -> Text+encodeCsvDefault csv = Data.Text.Encoding.decodeUtf8 $ ByteString.toStrict $ Data.Csv.encodeByName header csv+ where+ header = case csv of+ [] -> Vector.empty+ (m:_) -> Vector.fromList $ sort $ HashMap.keys m++{-| Utility to decode a CSV into a list of records.+ Must specify whether the CSV to decode has header or not.+-}+decodeCsvDefault :: Bool -> Text -> Either String [NamedRecord]+decodeCsvDefault hasHeader+ | hasHeader = decodeCsvWithHeader+ | otherwise = decodeCsvNoHeader++decodeCsvWithHeader :: Text -> Either String [NamedRecord]+decodeCsvWithHeader txt = do+ (_, vec) <- Data.Csv.decodeByName $ ByteString.fromStrict $ Data.Text.Encoding.encodeUtf8 txt+ return $ Vector.toList vec++decodeCsvNoHeader :: Text -> Either String [NamedRecord]+decodeCsvNoHeader txt = do+ vec <- Data.Csv.decode Data.Csv.NoHeader $ ByteString.fromStrict $ Data.Text.Encoding.encodeUtf8 txt+ return $ map addDefaultHeader $ Vector.toList vec++addDefaultHeader :: Record -> NamedRecord+addDefaultHeader = HashMap.fromList . (zip headerBS) . Vector.toList+ where+ header = map (('_' :) . show) ([1..] :: [Int])+ headerBS = map (ByteString.toStrict . ByteString.Char8.pack) header+
+ src/Dhall/CsvToDhall.hs view
@@ -0,0 +1,549 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++{-| Convert CSV data to Dhall providing an expected Dhall type necessary+ to know which type will be interpreted.++ The translation process will produce a Dhall expression where+ its type is a @List@ of records and the type of each field of the+ records is one of the following:++ * @Bool@s+ * @Natural@s+ * @Integer@s+ * @Double@s+ * @Text@s+ * @Optional@s (of valid field types)+ * unions (of empty alternatives or valid record field types)++ It is exactly the same as @dhall-to-csv@ supported input types.++ You can use this code as a library (this module) or as an executable+ named @csv-to-dhall@, which is used in the examples below.++ For now, @csv-to-dhall@ does not support type inference so you must+ always specify the Dhall type you expect.++> $ cat example.csv+> example+> 1+> $ csv-to-dhall 'List { example : Integer }' < example.csv+> [{ example = +1 }]++ When using the @csv-to-dhall@ executable you can specify that the CSV+ you want to translate does not have a header with the flag `--no-header`.+ In this case the resulting record fields will be named `_1`, `_2`, ...+ in the same order they were in the input CSV. You must still provide the+ expected Dhall type taking this into consideration.++> $ cat no-header-example.csv+> 1,3.14,Hello+> -1,2.68,Goodbye+> $ csv-to-dhall --no-header 'List { _1 : Integer, _2 : Double, _3 : Text } < no-header-example.csv+> [ { _1 = +1, _2 = 3.14, _3 = "Hello" }+> , { _1 = -1, _2 = 2.68, _3 = "Goodbye" }+> ]++== Primitive types++ Strings 'true' and 'false' can translate to Dhall @Bool@s++> $ cat example.csv+> exampleBool+> true+> false+> $ csv-to-dhall 'List { exampleBool : Bool }' < example.csv+> [ { exampleBool = True }, { exampleBool = False } ]++ Numeric strings can translate to Dhall numbers:++> $ cat example.csv+> exampleNatural,exampleInt,exampleDouble+> 1,2,3+> 0,-2,3.14+> 0,+2,-3.14+> $ csv-to-dhall 'List { exampleNatural : Natural, exampleInt : Integer, exampleDouble : Double }' < example.csv+> [ { exampleNatural = 1, exampleInt = +2, exampleDouble = 3.0 }+> , { exampleNatural = 0, exampleInt = -2, exampleDouble = 3.14 }+> , { exampleNatural = 0, exampleInt = +2, exampleDouble = -3.14 }+> ]++ Every CSV Field can translate directly to Dhall @Text@:++> $ cat example.csv+> exampleText+> Hello+> false+>+> ","+> $ csv-to-dhall 'List { exampleText : Text }' < example.csv+> [ { exampleText = "Hello" }+> , { exampleText = "false" }+> , { exampleText = "" }+> , { exampleText = "," }+> ]++== Unions and Optionals++ By default, when a union is expected, the first alternative that+ matches the CSV field is chosen. With the `--unions-strict` flag+ one can make sure that only one alternative matches. With the+ `--unions-none` unions are not allowed.++ An union alternative matches a CSV field if++ * It's an empty alternative and the name is the same as the text in the CSV field.+ * It's a non-empty alternative and the CSV field can be converted to the underlying type.++> $ cat example.csv+> exampleUnion+> Hello+> 1+> 1.11+> $ csv-to-dhall 'List { exampleUnion : <Hello | Nat : Natural | Dob : Double> }' < example.csv+> [ { exampleUnion = <Hello | Nat : Natural | Dob : Double>.Hello }+> , { exampleUnion = <Hello | Nat : Natural | Dob : Double>.Nat 1 }+> , { exampleUnion = <Hello | Nat : Natural | Dob : Double>.Dob 1.11 }+> ]++ Optional values can be either missing or have the expected value.+ The missing value is represented by the empty string.+ If a field's expected value is an Optional and the field is not+ in the CSV, then all the values will be None.++> $ cat example.csv+> exampleOptional+> 1+>+> 3+> $ csv-to-dhall 'List { exampleOptional : Optional Natural, exampleMissing : Optional Natural }' < example.csv+> [ { exampleOptional = Some 1, exampleMissing = None Natural }+> , { exampleOptional = None Natural, exampleMissing = None Natural }+> , { exampleOptional = Some 3, exampleMissing = None Natural }+> ]++-}+++module Dhall.CsvToDhall (+ -- * CSV to Dhall+ dhallFromCsv+ , parseConversion+ , defaultConversion+ , resolveSchemaExpr+ , typeCheckSchemaExpr+ , Conversion(..)++ -- * Exceptions+ , CompileError(..)+ ) where++import Control.Applicative ((<|>))+import Control.Exception (Exception, throwIO, displayException)+import Control.Monad.Catch (MonadCatch, throwM)+import Data.Csv (NamedRecord)+import Data.Either (lefts, rights)+import Data.Either.Combinators (mapRight)+import Data.Foldable (toList)+import Data.List ((\\))+import Data.Text (Text)+import Data.Text.Encoding (decodeUtf8, decodeUtf8', encodeUtf8)+import Data.Text.Encoding.Error (UnicodeException)+import Data.Text.Prettyprint.Doc (Pretty)+import Data.Text.Read (decimal, double, signed)+import Data.Void (Void)+import Dhall.Core (Expr)+import Dhall.Src (Src)+import Dhall.Util (_ERROR)+import Options.Applicative (Parser)++import qualified Data.Csv+import qualified Data.HashMap.Strict as HashMap+import qualified Data.Sequence as Sequence+import qualified Data.Text+import qualified Data.Text.Prettyprint.Doc.Render.Text as Pretty+import qualified Dhall.Core as Core+import qualified Dhall.Import+import qualified Dhall.Map as Map+import qualified Dhall.Parser+import qualified Dhall.Pretty+import qualified Dhall.TypeCheck as TypeCheck+import qualified Dhall.Util+import qualified Options.Applicative as O++-- ----------+-- Conversion+-- ----------++-- | CSV-to-dhall translation options+data Conversion = Conversion+ { strictRecs :: Bool+ , unions :: UnionConv+ } deriving Show++data UnionConv = UFirst | UNone | UStrict deriving (Show, Read, Eq)++-- | Default conversion options+defaultConversion :: Conversion+defaultConversion = Conversion+ { strictRecs = False+ , unions = UFirst+ }++-- ---------------+-- Command options+-- ---------------++-- | Standard parser for options related to the conversion method+parseConversion :: Parser Conversion+parseConversion = Conversion <$> parseStrict+ <*> parseUnion+ where+ parseStrict =+ O.flag' True+ ( O.long "records-strict"+ <> O.help "Fail if any CSV fields are missing from the expected Dhall type"+ )+ <|> O.flag' False+ ( O.long "records-loose"+ <> O.help "Tolerate CSV fields not present within the expected Dhall type"+ )+ <|> pure True+++-- | Parser for command options related to treating union types+parseUnion :: Parser UnionConv+parseUnion =+ uFirst+ <|> uNone+ <|> uStrict+ <|> pure UFirst -- defaulting to UFirst+ where+ uFirst = O.flag' UFirst+ ( O.long "unions-first"+ <> O.help "The first value with the matching type (successfully parsed all the way down the tree) is accepted, even if not the only possible match. (DEFAULT)"+ )+ uNone = O.flag' UNone+ ( O.long "unions-none"+ <> O.help "Unions not allowed"+ )+ uStrict = O.flag' UStrict+ ( O.long "unions-strict"+ <> O.help "Error if more than one union values match the type (and parse successfully)"+ )++type ExprX = Expr Src Void++-- | Parse schema code and resolve imports+resolveSchemaExpr :: Text -- ^ type code (schema)+ -> IO ExprX+resolveSchemaExpr code = do+ parsedExpression <-+ case Dhall.Parser.exprFromText "\n\ESC[1;31m(schema)\ESC[0m" code of+ Left err -> throwIO err+ Right parsedExpression -> return parsedExpression+ Dhall.Import.load parsedExpression++-- | Check that the Dhall type expression actually has type 'Type'+typeCheckSchemaExpr :: (Exception e, MonadCatch m)+ => (CompileError -> e) -> ExprX -> m ExprX+typeCheckSchemaExpr compileException expr =+ case TypeCheck.typeOf expr of -- check if the expression has type+ Left err -> throwM . compileException $ TypeError err+ Right t -> case t of -- check if the expression has type Type+ Core.Const Core.Type -> return expr+ _ -> throwM . compileException $ BadSchemaType t expr+++{-| Convert a list of CSV @NameRecord@ to a Dhall expression given the expected Dhall Type+ of the output.+-}+dhallFromCsv :: Conversion -> ExprX -> [NamedRecord] -> Either CompileError ExprX+dhallFromCsv Conversion{..} typeExpr = listConvert (Core.normalize typeExpr)+ where+ listConvert :: ExprX -> [NamedRecord] -> Either CompileError ExprX+ listConvert (Core.App Core.List recordType@(Core.Record _)) [] = return $ Core.ListLit (Just recordType) Sequence.empty+ listConvert (Core.App Core.List recordType) [] = Left $ NotARecord recordType+ listConvert (Core.App Core.List recordType) csv = do+ a <- traverse (recordConvert recordType) csv+ return $ Core.ListLit Nothing $ Sequence.fromList a+ listConvert e _ = Left $ NotAList e++ recordConvert :: ExprX -> NamedRecord -> Either CompileError ExprX+ recordConvert (Core.Record record) csvRecord+ | badKeys <- lefts (map decodeUtf8' (HashMap.keys csvRecord))+ , not (null badKeys)+ = Left $ UnicodeError (head badKeys) -- Only report first key that failed to be decoded+ | extraKeys <- (map decodeUtf8 $ HashMap.keys csvRecord) \\ Map.keys record+ , strictRecs && not (null extraKeys)+ = Left $ UnhandledFields extraKeys+ | otherwise+ = do+ let f k v = fieldConvert k (Core.recordFieldValue v) (HashMap.lookup (encodeUtf8 k) csvRecord)+ a <- Map.traverseWithKey (\k v -> mapRight Core.makeRecordField (f k v)) record+ return $ Core.RecordLit a+ recordConvert e _ = Left $ NotARecord e++ fieldConvert :: Text -> ExprX -> Maybe Data.Csv.Field -> Either CompileError ExprX+ -- Unions+ fieldConvert recordKey t@(Core.Union tm) maybeField = do+ let f unionKey Nothing =+ case maybeField of+ Nothing -> Left $ MissingField recordKey+ Just field ->+ case decodeUtf8' field of+ Left err -> Left $ UnicodeError err+ Right _field ->+ if _field == unionKey+ then Right $ Core.Field t $ Core.makeFieldSelection unionKey+ else Left $ Mismatch t _field recordKey+ f unionKey (Just _type) = do+ expression <- fieldConvert recordKey _type maybeField+ return (Core.App (Core.Field t $ Core.makeFieldSelection unionKey) expression)++ case (unions, rights (toList (Map.mapWithKey f tm)), maybeField) of+ (UNone , _ , _ ) -> Left $ ContainsUnion t+ (UStrict, xs@(_:_:_), Just field) -> Left $ UndecidableUnion t (decodeUtf8 field) recordKey xs+ (UStrict, xs@(_:_:_), Nothing ) -> Left $ UndecidableMissingUnion t recordKey xs+ (_ , [] , Just field) -> Left $ Mismatch t (decodeUtf8 field) recordKey+ (_ , [] , Nothing ) -> Left $ MissingField recordKey+ (UFirst , x:_ , _ ) -> Right $ x+ (UStrict, [x] , _ ) -> Right $ x++ -- Missing Optionals+ fieldConvert _ (Core.App Core.Optional t) Nothing = return $ Core.App Core.None t++ -- Missing fields+ fieldConvert key _ Nothing = Left $ MissingField key++ -- Bools+ fieldConvert key Core.Bool (Just field) =+ case decodeUtf8' field of+ Left err -> Left $ UnicodeError err+ Right _field ->+ case _field of+ "true" -> Right (Core.BoolLit True)+ "false" -> Right (Core.BoolLit False)+ _ -> Left $ Mismatch Core.Bool _field key++ -- Naturals+ fieldConvert key Core.Natural (Just field) =+ case decodeUtf8' field of+ Left err -> Left $ UnicodeError err+ Right _field ->+ case decimal _field of+ Right (v, "") -> Right $ Core.NaturalLit v+ _ -> Left $ Mismatch Core.Natural _field key++ -- Integers+ fieldConvert key Core.Integer (Just field) =+ case decodeUtf8' field of+ Left err -> Left $ UnicodeError err+ Right _field ->+ case (signed decimal) _field of+ Right (v, "") -> Right $ Core.IntegerLit v+ _ -> Left $ Mismatch Core.Integer _field key++ -- Doubles+ fieldConvert _ Core.Double (Just field) =+ case decodeUtf8' field of+ Left err -> Left $ UnicodeError err+ Right _field ->+ case double _field of+ Right (v, "") -> Right $ Core.DoubleLit $ Core.DhallDouble v+ _ -> Right $ Core.DoubleLit $ Core.DhallDouble (read "NaN")++ -- Text+ fieldConvert _ Core.Text (Just field) =+ case decodeUtf8' field of+ Left err -> Left $ UnicodeError err+ Right _field -> return $ Core.TextLit $ Core.Chunks [] $ _field++ -- Optionals null+ fieldConvert _ (Core.App Core.Optional t) (Just "") = return $ Core.App Core.None t++ -- Optionals+ fieldConvert key (Core.App Core.Optional t) maybeField = do+ expression <- fieldConvert key t maybeField+ return $ Core.Some expression++ fieldConvert _ t _ = Left $ Unsupported t+++{-| This is the exception type for errors that can arise when converting from+ CSV to Dhall.++ It contains information on the specific cases that might+ fail to give a better insight.+-}+data CompileError+ = Unsupported ExprX+ | NotAList ExprX+ | NotARecord ExprX+ | TypeError (TypeCheck.TypeError Src Void)+ | BadSchemaType+ ExprX -- Expression type+ ExprX -- Whole expression+ | MissingField Text+ | UnhandledFields [Text] -- Keys in CSV but not in schema+ | Mismatch+ ExprX -- Expected Dhall Type+ Text -- Actual field+ Text -- Record key+ | ContainsUnion ExprX+ | UndecidableUnion+ ExprX -- Expected Type+ Text -- CSV Field+ Text -- Record Key+ [ExprX] -- Multiple conversions+ | UndecidableMissingUnion+ ExprX -- Expected Type+ Text -- Record Key+ [ExprX] -- Multiple Conversions+ | UnicodeError UnicodeException+ deriving Show++instance Exception CompileError where+ displayException (Unsupported e) =+ Data.Text.unpack $+ _ERROR <> ": Invalid record field type \n\+ \ \n\+ \Explanation: Only the following types of record fields are valid: \n\+ \ \n\+ \● ❰Bool❱ \n\+ \● ❰Natural❱ \n\+ \● ❰Integer❱ \n\+ \● ❰Double❱ \n\+ \● ❰Text❱ \n\+ \● ❰Optional❱ tp (where tp 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\+ \Expected one of the previous types but instead got: \n\+ \ \n\+ \" <> insert e++ displayException (NotAList e) =+ Data.Text.unpack $+ _ERROR <> ": Top level object must be of type ❰List❱ \n\+ \ \n\+ \Explanation: Translation from CSV only returns ❰List❱s of records. \n\+ \Other types can not be translated. \n\+ \ \n\+ \Expected type List {...} but instead got the following type: \n\+ \ \n\+ \" <> insert e++ displayException (NotARecord e) =+ Data.Text.unpack $+ _ERROR <> ": Elements of the top-level list must be records \n\+ \ \n\+ \Explanation: Translation from CSV only returns ❰List❱s of records. \n\+ \Other types can not be translated. \n\+ \ \n\+ \Expected a record type but instead got the following type: \n\+ \ \n\+ \" <> insert e++ displayException (TypeError e) = displayException e++ displayException (BadSchemaType t e) =+ Data.Text.unpack $+ _ERROR <> ": Schema expression parsed successfully but has wrong Dhall type. \n\+ \ \n\+ \Expected schema type: Type \n\+ \ \n\+ \Actual schema type: \n\+ \" <> insert t <>+ " \n\+ \ \n\+ \Schema Expression: \n\+ \" <> insert e++ displayException (MissingField key) =+ Data.Text.unpack $+ _ERROR <> ": Missing field: \'" <> key <> "\'. \n\+ \ \n\+ \Explanation: Field present in Dhall type (and not optional) is not provided \n\+ \in CSV. Please make sure every non-optional field of the schema is \n\+ \present in CSV header. \n\+ \ \n\+ \If working with headerless CSVs, fields in schema should have the fields \n\+ \_1, _2, _3, ... and so forth "++ displayException (UnhandledFields keys) =+ Data.Text.unpack $+ _ERROR <> ": Following field(s): " <> (Data.Text.intercalate ", " keys) <> "\n\+ \are not handled. \n\+ \ \n\+ \Explanation: Fields present in CSV header are not present in schema. \n\+ \You may turn off the --strict-recs flag to ignore this error. "++ displayException (Mismatch t field key) =+ Data.Text.unpack $+ _ERROR <> ": Type mismatch at field: " <> key <> "\n\+ \ \n\+ \Explanation: Could not parse CSV field " <> field <> "\n\+ \into the expected Dhall type: \n\+ \ \n\+ \" <> insert t++ displayException (ContainsUnion e) =+ Data.Text.unpack $+ _ERROR <> ": Dhall type contains a Union type. \n\+ \ \n\+ \Explanation: Dhall type contains a Union type for one of the record fields. \n\+ \This error occurs because flag --unions-none is turned on. If it is desired to \n\+ \have unions in the parsed expression, disable --unions-none flag. \n\+ \ \n\+ \Expected no unions but got the following type in schema: \n\+ \ \n\+ \" <> insert e++ displayException (UndecidableUnion t field key opts) =+ Data.Text.unpack $+ _ERROR <> ": A union typed field can be parsed in more than one way. \n\+ \ \n\+ \Explanation: The CSV field " <> field <> "\n\+ \ with key " <> key <> "\n\+ \can be converted in more than one of the expected union type alternatives. \n\+ \This error occurs because flag --unions-strict is turned on. You may turn off \n\+ \this flag to select the first valid alternative found. \n\+ \ \n\+ \Expected union type: \n\+ \ \n\+ \" <> insert t <> "\n\+ \ \n\+ \... field can be parsed as the following expressions: \n\+ \ \n\+ \" <> Data.Text.intercalate+ "\n------------------------------------------------------------------------------\n"+ (map insert opts)++ displayException (UndecidableMissingUnion t key opts) =+ Data.Text.unpack $+ _ERROR <> ": A union typed field can be parsed in more than one way. \n\+ \ \n\+ \Explanation: The record field key " <> key <> "\n\+ \ missing in CSV \n\+ \can be converted in more than one of the expected union type alternatives. \n\+ \That is to say, there are more than one alternatives with Optional types. \n\+ \This error occurs because flag --unions-strict is turned on. You may turn off \n\+ \this flag to select the first valid alternative found. \n\+ \ \n\+ \Expected union type: \n\+ \ \n\+ \" <> insert t <> "\n\+ \ \n\+ \... missing field can be parsed as the following expressions: \n\+ \ \n\+ \" <> Data.Text.intercalate+ "\n------------------------------------------------------------------------------\n"+ (map insert opts)++ displayException (UnicodeError e) = displayException e++insert :: Pretty a => a -> Text+insert = Pretty.renderStrict . Dhall.Pretty.layout . Dhall.Util.insert
+ tasty/Main.hs view
@@ -0,0 +1,99 @@+{-# LANGUAGE OverloadedStrings #-}++module Main (main) where++import Data.Text (Text)+import Data.Void (Void)+import Dhall.CsvToDhall (defaultConversion, dhallFromCsv, typeCheckSchemaExpr, resolveSchemaExpr)+import Dhall.Core (Expr)+import Dhall.Src (Src)+import Test.Tasty (TestTree)+import Test.Tasty.Silver (findByExtension)+import System.FilePath (takeBaseName, replaceExtension, dropExtension)++import qualified Data.Csv+import qualified Data.Text as Text+import qualified Data.Text.IO+import qualified Dhall.Core as D+import qualified Dhall.Csv+import qualified Dhall.CsvToDhall as CsvToDhall+import qualified Dhall.Csv.Util+import qualified GHC.IO.Encoding+import qualified Test.Tasty+import qualified Test.Tasty.Silver as Silver+++main :: IO ()+main = do+ GHC.IO.Encoding.setLocaleEncoding GHC.IO.Encoding.utf8++ testTree <- goldenTests+ Test.Tasty.defaultMain testTree++goldenTests :: IO TestTree+goldenTests = do+ dhallToCsvTree <- dhallToCsvGolden+ csvToDhallTree <- csvToDhallGolden+ noHeaderCsvToDhallTree <- noHeaderCsvToDhallGolden+ return $ Test.Tasty.testGroup "dhall-csv"+ [ dhallToCsvTree+ , csvToDhallTree+ , noHeaderCsvToDhallTree+ ]++dhallToCsvGolden :: IO TestTree+dhallToCsvGolden = do+ dhallFiles <- findByExtension [".dhall"] "./tasty/data/dhall-to-csv"+ return $ Test.Tasty.testGroup "dhall-to-csv"+ [ Silver.goldenVsAction+ (takeBaseName dhallFile)+ csvFile+ (Dhall.Csv.codeToValue Nothing =<< Data.Text.IO.readFile dhallFile)+ Dhall.Csv.Util.encodeCsvDefault+ | dhallFile <- dhallFiles+ , let csvFile = replaceExtension dhallFile ".csv"+ ]++csvToDhallGolden :: IO TestTree+csvToDhallGolden = do+ csvFiles <- findByExtension [".csv"] "./tasty/data/csv-to-dhall"+ return $ Test.Tasty.testGroup "csv-to-dhall"+ [ Silver.goldenVsAction+ (takeBaseName csvFile)+ dhallFile+ (getSchemaAndCsv csvFile True schema)+ (showExpressionOrError . (uncurry (dhallFromCsv defaultConversion)))+ | csvFile <- csvFiles+ , let dhallFile = replaceExtension csvFile ".dhall"+ , let schema = Text.pack $ (dropExtension csvFile) ++ "_schema.dhall"+ ]++noHeaderCsvToDhallGolden :: IO TestTree+noHeaderCsvToDhallGolden = do+ csvFiles <- findByExtension [".csv"] "./tasty/data/no-header-csv-to-dhall"+ return $ Test.Tasty.testGroup "csv-to-dhall"+ [ Silver.goldenVsAction+ (takeBaseName csvFile)+ dhallFile+ (getSchemaAndCsv csvFile False schema)+ (showExpressionOrError . (uncurry (dhallFromCsv defaultConversion)))+ | csvFile <- csvFiles+ , let dhallFile = replaceExtension csvFile ".dhall"+ , let schema = Text.pack $ (dropExtension csvFile) ++ "_schema.dhall"+ ]++textToCsv :: Bool -> Text -> IO [Data.Csv.NamedRecord]+textToCsv hasHeader txt =+ case Dhall.Csv.Util.decodeCsvDefault hasHeader txt of+ Left err -> fail err+ Right csv -> return csv++getSchemaAndCsv :: FilePath -> Bool -> Text -> IO (Expr Src Void, [Data.Csv.NamedRecord])+getSchemaAndCsv csvFile hasHeader schema = do+ finalSchema <- typeCheckSchemaExpr id =<< resolveSchemaExpr schema+ csv <- textToCsv hasHeader =<< Data.Text.IO.readFile csvFile+ return (finalSchema, csv)++showExpressionOrError :: Either CsvToDhall.CompileError (Expr Src Void) -> Text+showExpressionOrError (Left err) = Text.pack $ show err+showExpressionOrError (Right expr) = (D.pretty expr) <> "\n"