packages feed

dhall-toml-1.0.4: src/Dhall/TomlToDhall.hs

{-# LANGUAGE ApplicativeDo     #-}
{-# LANGUAGE BlockArguments    #-}
{-# LANGUAGE GADTs             #-}
{-# LANGUAGE OverloadedLists   #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards   #-}
{-# LANGUAGE ViewPatterns      #-}

{-| 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 (<a : Natural | b : Bool>) }
> $ 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 (<a : Natural | b : Bool>), item : <a : Natural | b : Bool> }
> $ 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(..))
import Data.Bifunctor       (first)
import Data.Either          (rights)
import Data.Foldable        (fold, toList)
import Data.HashMap.Strict  (HashMap)
import Data.List.NonEmpty   (NonEmpty ((:|)))
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(..))
import Toml.Type.Key        (Key(..), Piece(..))
import Toml.Type.PrefixTree (PrefixMap, 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            as 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 Options
import qualified Paths_dhall_toml     as Meta
import qualified Toml.Parser
import qualified Toml.Type.TOML       as TOML
import qualified Toml.Type.Value      as Value

data CompileError
    = Unimplemented String
    | Incompatible (Expr Src Void) Object
    | InvalidToml TomlParseError
    | InternalError String
    | MissingKey String
    deriving (Show)

instance Exception CompileError where
    displayException exception = case exception of
        Unimplemented s ->
            "unimplemented: " <> s
        Incompatible e toml ->
            "incompatible: " <> show e <> " with " <> show toml
        InvalidToml e ->
            "invalid TOML:\n" <> Text.unpack (Toml.Parser.unTomlParseError e)
        InternalError e ->
            "internal error: " <> show e
        MissingKey e ->
            "missing key: " <> show e

tomlToDhall :: Expr Src Void -> TOML -> Either CompileError (Expr Src Void)
tomlToDhall schema toml = objectToDhall (Core.normalize schema) (tomlToObject toml)

valueToDhall
    :: Expr Src Void -> Value t -> Either CompileError (Expr Src Void)
valueToDhall type_ value = case (type_, value) of
    (Core.Bool, Value.Bool a) ->
        Right (Core.BoolLit a)

    (Core.Integer, Value.Integer a) ->
        Right (Core.IntegerLit 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")

    (Core.App Core.List _, Value.Array [] ) ->
        Right (Core.ListLit (Just type_) [])

    (Core.App Core.Optional t, a) -> do
        o <- valueToDhall t a
        return (Core.Some o)

    (Core.App Core.List elementType, Value.Array elements) -> do
        expressions <- mapM (valueToDhall elementType) elements
        return (Core.ListLit Nothing (Seq.fromList expressions))

    -- TODO: allow different types of matching (ex. first, strict, none)
    -- currently we just pick the first enum that matches
    (Core.Union m, _) -> do
        let f key maybeAlternativeType = case maybeAlternativeType of
                Just alternativeType -> do
                    expression <- valueToDhall alternativeType value
                    return (Core.App (Core.Field type_ (Core.makeFieldSelection key)) expression)
                Nothing -> case value of
                    Value.Text a | a == key ->
                        return (Core.Field type_ (Core.makeFieldSelection a))
                    _ -> Left (Incompatible type_ (Prim (AnyValue value)))

        case rights (toList (Map.mapWithKey f m)) of
            []    -> Left (Incompatible type_ (Prim (AnyValue value)))
            x : _ -> Right x

    _ ->
        Left (Incompatible type_ (Prim (AnyValue value)))

-- TODO: keep track of the path for more helpful error messages
objectToDhall :: Expr Src Void -> Object -> Either CompileError (Expr Src Void)
objectToDhall type_ object = case (type_, object) 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, _) -> do
        let f key maybeAlternativeType = case maybeAlternativeType of
                Just alternativeType -> do
                    expression <- objectToDhall alternativeType object
                    return (Core.App (Core.Field type_ (Core.makeFieldSelection key)) expression)
                Nothing -> case object of
                    Prim (AnyValue (Value.Text a)) | a == key ->
                        return (Core.Field type_ (Core.makeFieldSelection a))
                    _ -> Left (Incompatible type_ object)

        case rights (toList (Map.mapWithKey f m)) of
            []    -> Left (Incompatible type_ object)
            x : _ -> Right x

    (Core.Record record, Table table) -> do
        let process key fieldType
                | Just nestedObject <- HashMap.lookup (Piece key) table =
                    objectToDhall fieldType nestedObject
                | Core.App Core.Optional innerType <- fieldType =
                    Right (Core.App Core.None innerType)
                | Core.App Core.List _ <- fieldType =
                    Right (Core.ListLit (Just fieldType) [])
                | otherwise =
                    Left (MissingKey (Text.unpack key))

        expressions <- Map.traverseWithKey process (fmap Core.recordFieldValue record)

        return (Core.RecordLit (fmap Core.makeRecordField expressions))

    (Core.App Core.List (Core.Record [("mapKey", Core.recordFieldValue -> Core.Text), ("mapValue", Core.recordFieldValue -> valueType)]), Table table) -> do
        hashMap <- traverse (objectToDhall valueType) table

        let expressions = Seq.fromList do
                (Piece key, value) <- HashMap.toList hashMap

                let newKey =
                        Core.makeRecordField (Core.TextLit (Core.Chunks [] key))

                let newValue = Core.makeRecordField value

                pure (Core.RecordLit [("mapKey", newKey), ("mapValue", newValue)])

        let listType = if Seq.null expressions then Just type_ else Nothing

        return (Core.ListLit listType expressions)

    (Core.App Core.List t, Array []) ->
        Right (Core.ListLit (Just t) [])

    (Core.App Core.List t, Array elements) -> do
        expressions <- mapM (objectToDhall t) elements
        return (Core.ListLit Nothing (Seq.fromList expressions))

    (_, Prim (AnyValue value)) ->
        valueToDhall type_ value

    (_, obj) ->
        Left (Incompatible type_ 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 AnyValue
    | Array [Object]
    | Table (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

instance Monoid Object where
    mempty = Table HashMap.empty

-- | Creates an arbitrarily nested object
sparseObject :: Key -> Object -> Object
sparseObject (Key (piece :| [])) value =
    Table (HashMap.singleton piece value)
sparseObject (Key (piece :| piece' : pieces)) value =
    Table (HashMap.singleton piece (sparseObject (Key (piece' :| pieces)) value))

tablesToObject :: PrefixMap TOML -> Object
tablesToObject = fold . map prefixTreeToObject . HashMap.elems

prefixTreeToObject :: PrefixTree TOML -> Object
prefixTreeToObject (Leaf key toml) =
    sparseObject key (tomlToObject toml)
prefixTreeToObject (Branch prefix _ toml) =
    sparseObject prefix (tablesToObject toml)

tomlToObject :: TOML -> Object
tomlToObject = pairs <> tables <> tableArrays
  where
    pairs =
          fold
        . HashMap.mapWithKey sparseObject
        . fmap Prim
        . TOML.tomlPairs

    tables =
          fold
        . map prefixTreeToObject
        . HashMap.elems
        . TOML.tomlTables

    tableArrays =
          fold
        . HashMap.mapWithKey sparseObject
        . fmap (Array . fmap tomlToObject . toList)
        . TOML.tomlTableArrays

data Options = Options
    { input :: Maybe FilePath
    , output :: Maybe FilePath
    , schemaFile :: FilePath
    }

parserInfo :: Options.ParserInfo Options
parserInfo = Options.info
    (Options.helper <*> versionOption <*> optionsParser)
    (Options.fullDesc <> Options.progDesc "Convert TOML to Dhall")
  where
    versionOption =
        Options.infoOption (showVersion Meta.version)
            (Options.long "version" <> Options.help "Display version")

    optionsParser = do
        input <- (Options.optional . Options.strOption)
            (  Options.long "file"
            <> Options.help "Read TOML from file instead of standard input"
            <> Options.metavar "FILE"
            <> Options.action "file"
            )
        output <- (Options.optional . Options.strOption)
            (  Options.long "output"
            <> Options.help "Write Dhall to a file instead of standard output"
            <> Options.metavar "FILE"
            <> Options.action "file"
            )
        schemaFile <- Options.strArgument
            (  Options.help "Path to Dhall schema file"
            <> Options.action "file"
            <> Options.metavar "SCHEMA"
            )
        pure Options {..}

tomlToDhallMain :: IO ()
tomlToDhallMain = do
    Options{..} <- Options.execParser parserInfo

    inputText <- case input of
        Just file -> Text.IO.readFile file
        Nothing   -> Text.IO.getContents

    toml <- Core.throws (first InvalidToml (Toml.Parser.parse inputText))

    schema <- fileToDhall schemaFile

    dhall <- Core.throws (tomlToDhall schema toml)

    let outputText = Core.pretty dhall

    case output of
        Just file -> Text.IO.writeFile file outputText
        Nothing   -> Text.IO.putStrLn outputText