{-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE RecordWildCards #-} {-| This module exports the `tomlToDhall` function for translating a TOML syntax tree from @tomland@ to a Dhall syntax tree. For now, this package does not have type inference so a Dhall type is needed. For converting source code into a Dhall syntax tree see the @dhall@ package, and for converting the TOML syntax tree to source code see the @tomland@ package. This module also exports `tomlToDhallMain` which implements the @toml-to-dhall@ command which converts TOML source directly into Dhall source. In theory all TOML objects should be converted but there are some known failure cases: * Arrays of arrays of objects - not supported by @tomland@ * Arrays of heterogeneous primitive types - not supported by @tomland@ * Arrays of objects of different types are allowed (note that this requires conversion to a Dhall union) TOML bools translate to Dhall @Bool@s: > $ cat schema.dhall > { b : Bool } > $ toml-to-dhall schema.dhall <<< 'b = true' > { b = True } TOML numbers translate to Dhall numbers: > $ cat schema.dhall > { n : Natural, d : Double } > $ toml-to-dhall schema.dhall << EOF > n = 1 > d = 3.14 > EOF > { d = 3.14, n = 1} TOML text translates to Dhall @Text@: > $ cat schema.dhall > { t : Text } > $ toml-to-dhall schema.dhall << EOF > t = "Hello!" > EOF > { t = "Hello!" } TOML arrays and table arrays translate to Dhall @List@: > $ cat schema.dhall > { nums : List Natural, tables : List { a : Natural, b : Text } } > $ toml-to-dhall schema.dhall << EOF > nums = [1, 2, 3] > > [[tables]] > a = 1 > b = "Hello," > [[tables]] > a = 2 > b = " World!" > EOF > { nums = [ 1, 2, 3 ] > , tables = [ { a = 1, b = "Hello," }, { a = 2, b = " World!" } ] > } Note, [lists of lists of objects](https://github.com/kowainik/tomland/issues/373) and [heterogeneous lists](https://github.com/kowainik/tomland/issues/373) are not supported by @tomland@ so a paraser error will be returned: > $ cat schema.dhall > { list : List () } > $ toml-to-dhall schema.dhall << EOF > list = [1, true] > EOF > toml-to-dhall: invalid TOML: > 1:12: > | > 1 | list = [1, true] > | ^ > unexpected 't' > expecting ',', ']', or integer Because of this, unions have limited use in lists, but can be used fully in tables: > $ cat schema.dhall > { list : List (), item : } > $ toml-to-dhall schema.dhall << EOF > list = [1, 2] > item = true > EOF > { item = < a : Natural | b : Bool >.b True > , list = [ < a : Natural | b : Bool >.a 1, < a : Natural | b : Bool >.a 2 ] > } TOML tables translate to Dhall records: > $ cat schema.dhall > { num : Natural, table : { num1 : Natural, table1 : { num2 : Natural } } } > $ toml-to-dhall schema.dhall << EOF > num = 0 > > [table] > num1 = 1 > > [table.table1] > num2 = 2 > EOF > { num = 0, table = { num1 = 1, table1.num2 = 2 } } -} module Dhall.TomlToDhall ( tomlToDhall , tomlToDhallMain , CompileError ) where import Control.Exception (Exception, throwIO) import Data.Either (rights) import Data.Foldable (foldl', toList) import Data.List.NonEmpty (NonEmpty ((:|))) import Data.Text (Text) import Data.Version (showVersion) import Data.Void (Void) import Dhall.Core (DhallDouble (..), Expr) import Dhall.Parser (Src) import Dhall.Toml.Utils (fileToDhall) import Toml.Parser (TomlParseError) import Toml.Type.AnyValue (AnyValue (AnyValue)) import Toml.Type.Key (Key (Key), Piece (Piece)) import Toml.Type.PrefixTree (PrefixTree) import Toml.Type.TOML (TOML) import Toml.Type.Value (Value) import qualified Data.HashMap.Strict as HashMap import qualified Data.Sequence as Seq import qualified Data.Text import qualified Data.Text.IO as Text.IO import qualified Dhall.Core as Core import qualified Dhall.Map as Map import qualified Options.Applicative as OA import qualified Paths_dhall_toml as Meta import qualified Toml.Parser import qualified Toml.Type.AnyValue as Toml.AnyValue import qualified Toml.Type.PrefixTree as Toml.PrefixTree import qualified Toml.Type.TOML as Toml.TOML import qualified Toml.Type.Value as Value data CompileError = Unimplemented String | Incompatible (Expr Src Void) Object | InvalidToml TomlParseError | InternalError String | MissingKey String instance Show CompileError where show (Unimplemented s) = "unimplemented: " ++ s show (Incompatible e toml) = "incompatible: " ++ (show e) ++ " with " ++ (show toml) show (InvalidToml e) = "invalid TOML:\n" ++ (Data.Text.unpack $ Toml.Parser.unTomlParseError e) show (InternalError e) = "internal error: " ++ show e show (MissingKey e) = "missing key: " ++ show e instance Exception CompileError tomlToDhall :: Expr Src Void -> TOML -> Either CompileError (Expr Src Void) tomlToDhall schema toml = toDhall (Core.normalize schema) (tomlToObject toml) tomlValueToDhall :: Expr Src Void -> Value t -> Either CompileError (Expr Src Void) tomlValueToDhall exprType v = case (exprType, v) of (Core.Bool , Value.Bool a ) -> Right $ Core.BoolLit a (Core.Natural , Value.Integer a) -> Right $ Core.NaturalLit $ fromInteger a (Core.Double , Value.Double a ) -> Right $ Core.DoubleLit $ DhallDouble a (Core.Text , Value.Text a ) -> Right $ Core.TextLit $ Core.Chunks [] a (_ , Value.Zoned _ ) -> Left $ Unimplemented "toml time values" (_ , Value.Local _ ) -> Left $ Unimplemented "toml time values" (_ , Value.Day _ ) -> Left $ Unimplemented "toml time values" (t@(Core.App Core.List _) , Value.Array [] ) -> Right $ Core.ListLit (Just t) [] (Core.App Core.Optional t , a ) -> do o <- tomlValueToDhall t a return $ Core.Some o (Core.App Core.List t , Value.Array a ) -> do l <- mapM (tomlValueToDhall t) a return $ Core.ListLit Nothing (Seq.fromList l) -- TODO: allow different types of matching (ex. first, strict, none) -- currently we just pick the first enum that matches (Core.Union m , _) -> let f key maybeType = case maybeType of Just ty -> do expr <- tomlValueToDhall ty v return $ Core.App (Core.Field exprType $ Core.makeFieldSelection key) expr Nothing -> case v of Value.Text a | a == key -> return $ Core.Field exprType (Core.makeFieldSelection a) _ -> Left $ Incompatible exprType (Prim (AnyValue v)) in case rights (toList (Map.mapWithKey f m)) of [] -> Left $ Incompatible exprType (Prim (AnyValue v)) x:_ -> Right $ x _ -> Left $ Incompatible exprType (Prim (AnyValue v)) -- TODO: keep track of the path for more helpful error messages toDhall :: Expr Src Void -> Object -> Either CompileError (Expr Src Void) toDhall exprType value = case (exprType, value) of (_, Invalid) -> Left $ InternalError "invalid object" -- TODO: allow different types of matching (ex. first, strict, none) -- currently we just pick the first enum that matches (Core.Union m , _) -> let f key maybeType = case maybeType of Just ty -> do expr <- toDhall ty value return $ Core.App (Core.Field exprType $ Core.makeFieldSelection key) expr Nothing -> case value of Prim (AnyValue (Value.Text a)) | a == key -> return $ Core.Field exprType (Core.makeFieldSelection a) _ -> Left $ Incompatible exprType value in case rights (toList (Map.mapWithKey f m)) of [] -> Left $ Incompatible exprType value x:_ -> Right $ x (Core.App Core.List t, Array []) -> Right $ Core.ListLit (Just t) [] (Core.App Core.List t, Array a) -> do l <- mapM (toDhall t) a return $ Core.ListLit Nothing (Seq.fromList l) (Core.Record r, Table t) -> let f :: Text -> (Expr Src Void) -> Either CompileError (Expr Src Void) f k ty | Just val <- HashMap.lookup (Piece k) t = toDhall ty val | Core.App Core.Optional ty' <- ty = Right $ (Core.App Core.None ty') | Core.App Core.List _ <- ty = Right $ Core.ListLit (Just ty) [] | otherwise = Left $ MissingKey $ Data.Text.unpack k in do values <- Map.traverseWithKey f (Core.recordFieldValue <$> r) return $ Core.RecordLit (Core.makeRecordField <$> values) (_, Prim (AnyValue v)) -> tomlValueToDhall exprType v (ty, obj) -> Left $ Incompatible ty obj -- | An intermediate object created from a 'TOML' before an 'Expr'. -- It does two things, firstly joining the tomlPairs, tomlTables, -- and tomlTableArrays parts of the TOML. Second, it turns the dense -- paths (ex. a.b.c = 1) into sparse paths (ex. a = { b = { c = 1 }}). data Object = Prim Toml.AnyValue.AnyValue | Array [Object] | Table (HashMap.HashMap Piece Object) | Invalid deriving (Show) instance Semigroup Object where (Table ls) <> (Table rs) = Table (ls <> rs) -- this shouldn't happen because tomland has already verified correctness -- of the toml object _ <> _ = Invalid -- | Creates an arbitrarily nested object sparseObject :: Key -> Object -> Object sparseObject (Key (piece :| [])) value = Table $ HashMap.singleton piece value sparseObject (Key (piece :| rest:rest')) value = Table $ HashMap.singleton piece (sparseObject (Key $ rest :| rest') value) pairsToObject :: HashMap.HashMap Key Toml.AnyValue.AnyValue -> Object pairsToObject pairs = foldl' (<>) (Table HashMap.empty) $ HashMap.mapWithKey sparseObject $ fmap Prim pairs tablesToObject :: Toml.PrefixTree.PrefixMap TOML -> Object tablesToObject tables = foldl' (<>) (Table HashMap.empty) $ map prefixTreeToObject $ HashMap.elems tables prefixTreeToObject :: PrefixTree TOML -> Object prefixTreeToObject (Toml.PrefixTree.Leaf key toml) = sparseObject key (tomlToObject toml) prefixTreeToObject (Toml.PrefixTree.Branch prefix _ toml) = sparseObject prefix (tablesToObject toml) tableArraysToObject :: HashMap.HashMap Key (NonEmpty TOML) -> Object tableArraysToObject arrays = foldl' (<>) (Table HashMap.empty) $ HashMap.mapWithKey sparseObject $ fmap (Array . fmap tomlToObject . toList) arrays tomlToObject :: TOML -> Object tomlToObject toml = pairs <> tables <> tableArrays where pairs = pairsToObject $ Toml.TOML.tomlPairs toml tables = tablesToObject $ Toml.TOML.tomlTables toml tableArrays = tableArraysToObject $ Toml.TOML.tomlTableArrays toml data Options = Options { input :: Maybe FilePath , output :: Maybe FilePath , schemaFile :: FilePath } parserInfo :: OA.ParserInfo Options parserInfo = OA.info (OA.helper <*> versionOption <*> optionsParser) (OA.fullDesc <> OA.progDesc "Convert TOML to Dhall") where versionOption = OA.infoOption (showVersion Meta.version) $ OA.long "version" <> OA.help "Display version" optionsParser = do input <- OA.optional . OA.strOption $ OA.long "file" <> OA.help "Read TOML from file instead of standard input" <> fileOpts output <- OA.optional . OA.strOption $ OA.long "output" <> OA.help "Write Dhall to a file instead of standard output" <> fileOpts schemaFile <- OA.strArgument $ OA.help "Path to Dhall schema file" <> OA.action "file" <> OA.metavar "SCHEMA" pure Options {..} fileOpts = OA.metavar "FILE" <> OA.action "file" tomlToDhallMain :: IO () tomlToDhallMain = do Options {..} <- OA.execParser parserInfo text <- maybe Text.IO.getContents Text.IO.readFile input toml <- case Toml.Parser.parse text of Left tomlErr -> throwIO (InvalidToml tomlErr) Right toml -> return toml schema <- fileToDhall schemaFile dhall <- case tomlToDhall schema toml of Left err -> throwIO err Right dhall -> return dhall maybe Text.IO.putStrLn Text.IO.writeFile output $ Core.pretty dhall