tomland-0.2.1: src/Toml/Type.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeOperators #-}
{- | Contains specification of TOML via Haskell ADT. -}
module Toml.Type
( -- * Main type
TOML (..)
-- * Values
, ValueType (..)
, Value (..)
, AnyValue (..)
, UValue (..)
, DateTime (..)
, matchBool
, matchInteger
, matchDouble
, matchText
, matchDate
, matchArray
, showType
, valueType
-- * Internal functions
, typeCheck
) where
import Data.HashMap.Strict (HashMap)
import Data.Text (Text)
import Data.Time (Day, LocalTime, TimeOfDay, ZonedTime, zonedTimeToUTC)
import Data.Type.Equality ((:~:) (..))
import Toml.PrefixTree (Key (..), PrefixMap)
-- TODO: describe how some TOML document will look like with this type
{- | Represents TOML configuration value. -}
data TOML = TOML
{ tomlPairs :: HashMap Key AnyValue
, tomlTables :: PrefixMap TOML
-- tomlTableArrays :: HashMap Key (NonEmpty TOML)
} deriving (Show, Eq)
-- | Needed for GADT parameterization
data ValueType = TBool | TInt | TFloat | TString | TDate | TArray
deriving (Eq, Show)
showType :: ValueType -> String
showType = drop 1 . show
-- TODO: examples are copy-pasted from TOML specification. Probably most of them
-- will be moved into parsing module in future.
-- | Value in @key = value@ pair.
data Value (t :: ValueType) where
{- | Boolean value:
@
bool1 = true
bool2 = false
@
-}
Bool :: Bool -> Value 'TBool
{- | Integer value:
@
int1 = +99
int2 = 42
int3 = 0
int4 = -17
int5 = 5_349_221
hex1 = 0xDEADBEEF
oct2 = 0o755 # useful for Unix file permissions
bin1 = 0b11010110
@
-}
Int :: Integer -> Value 'TInt
{- | Floating point number:
@
flt1 = -3.1415 # fractional
flt2 = 1e6 # exponent
flt3 = 6.626e-34 # both
flt4 = 9_224_617.445_991_228_313
@
-}
Float :: Double -> Value 'TFloat
{- | String value:
@
key = "value"
bare_key = "value"
bare-key = "value"
@
-}
String :: Text -> Value 'TString
-- | Date-time. See documentation for 'DateTime' type.
Date :: DateTime -> Value 'TDate
{- | Array of values. According to TOML specification all values in array
should have the same type. This is guaranteed statically with this type.
@
arr1 = [ 1, 2, 3 ]
arr2 = [ "red", "yellow", "green" ]
arr3 = [ [ 1, 2 ], [3, 4, 5] ]
arr4 = [ "all", 'strings', """are the same""", '''type''']
arr5 = [ [ 1, 2 ], ["a", "b", "c"] ]
arr6 = [ 1, 2.0 ] # INVALID
@
-}
Array :: [Value t] -> Value 'TArray
deriving instance Show (Value t)
instance Eq (Value t) where
(Bool b1) == (Bool b2) = b1 == b2
(Int i1) == (Int i2) = i1 == i2
(Float f1) == (Float f2) = f1 == f2
(String s1) == (String s2) = s1 == s2
(Date d1) == (Date d2) = d1 == d2
(Array a1) == (Array a2) = eqValueList a1 a2
eqValueList :: [Value a] -> [Value b] -> Bool
eqValueList [] [] = True
eqValueList (x:xs) (y:ys) = case sameValue x y of
Right Refl -> x == y && eqValueList xs ys
Left _ -> False
eqValueList _ _ = False
-- | Reifies type of 'Value' into 'ValueType'. Unfortunately, there's no way to
-- guarante that 'valueType' will return @t@ for object with type @Value \'t@.
valueType :: Value t -> ValueType
valueType (Bool _) = TBool
valueType (Int _) = TInt
valueType (Float _) = TFloat
valueType (String _) = TString
valueType (Date _) = TDate
valueType (Array _) = TArray
----------------------------------------------------------------------------
-- Matching functions for values
----------------------------------------------------------------------------
-- | Extract 'Bool' from 'Value'.
matchBool :: Value f -> Maybe Bool
matchBool (Bool b) = Just b
matchBool _ = Nothing
-- | Extract 'Integer' from 'Value'.
matchInteger :: Value f -> Maybe Integer
matchInteger (Int n) = Just n
matchInteger _ = Nothing
-- | Extract 'Double' from 'Value'.
matchDouble :: Value f -> Maybe Double
matchDouble (Float f) = Just f
matchDouble _ = Nothing
-- | Extract 'Text' from 'Value'.
matchText :: Value f -> Maybe Text
matchText (String s) = Just s
matchText _ = Nothing
-- | Extract 'DateTime' from 'Value'.
matchDate :: Value f -> Maybe DateTime
matchDate (Date d) = Just d
matchDate _ = Nothing
-- | Extract list of elements of type @a@ from array.
matchArray :: (forall t . Value t -> Maybe a) -> Value f -> Maybe [a]
matchArray matchElement (Array a) = mapM matchElement a
matchArray _ _ = Nothing
----------------------------------------------------------------------------
-- Untyped value
----------------------------------------------------------------------------
-- TODO: move into Toml.Type.Internal module then?.. But it uses 'DateTime' which is not internal...
-- | Untyped value of 'TOML'. You shouldn't use this type in your code. Use
-- 'Value' instead.
data UValue
= UBool !Bool
| UInt !Integer
| UFloat !Double
| UString !Text
| UDate !DateTime
| UArray ![UValue]
-- | Existential wrapper for 'Value'.
data AnyValue = forall (t :: ValueType) . AnyValue (Value t)
instance Show AnyValue where
show (AnyValue v) = show v
instance Eq AnyValue where
(AnyValue (Bool b1)) == (AnyValue (Bool b2)) = b1 == b2
(AnyValue (Int i1)) == (AnyValue (Int i2)) = i1 == i2
(AnyValue (Float f1)) == (AnyValue (Float f2)) = f1 == f2
(AnyValue (String s1)) == (AnyValue (String s2)) = s1 == s2
(AnyValue (Date d1)) == (AnyValue (Date d2)) = d1 == d2
(AnyValue (Array a1)) == (AnyValue (Array a2)) = eqValueList a1 a2
_ == _ = False
data DateTime
{- | Offset date-time:
@
odt1 = 1979-05-27T07:32:00Z
odt2 = 1979-05-27T00:32:00-07:00
@
-}
= Zoned !ZonedTime
{- | Local date-time (without offset):
@
ldt1 = 1979-05-27T07:32:00
ldt2 = 1979-05-27T00:32:00.999999
@
-}
| Local !LocalTime
{- | Local date (only day):
@
ld1 = 1979-05-27
@
-}
| Day !Day
{- | Local time (time of the day):
@
lt1 = 07:32:00
lt2 = 00:32:00.999999
@
-}
| Hours !TimeOfDay
deriving (Show)
instance Eq DateTime where
(Zoned a) == (Zoned b) = zonedTimeToUTC a == zonedTimeToUTC b
(Local a) == (Local b) = a == b
(Day a) == (Day b) = a == b
(Hours a) == (Hours b) = a == b
_ == _ = False
-- | Data type that holds expected vs. actual type.
data TypeMismatchError = TypeMismatchError
{ typeExpected :: ValueType
, typeActual :: ValueType
} deriving (Eq)
instance Show TypeMismatchError where
show TypeMismatchError{..} = "Expected type '" ++ showType typeExpected
++ "' but actual type: '" ++ showType typeActual ++ "'"
-- | Ensures that 'UValue's represents type-safe version of @toml@.
typeCheck :: UValue -> Either TypeMismatchError AnyValue
typeCheck (UBool b) = rightAny $ Bool b
typeCheck (UInt n) = rightAny $ Int n
typeCheck (UFloat f) = rightAny $ Float f
typeCheck (UString s) = rightAny $ String s
typeCheck (UDate d) = rightAny $ Date d
typeCheck (UArray a) = case a of
[] -> rightAny $ Array []
x:xs -> do
AnyValue v <- typeCheck x
AnyValue . Array <$> checkElem v xs
where
checkElem :: Value t -> [UValue] -> Either TypeMismatchError [Value t]
checkElem v [] = Right [v]
checkElem v (x:xs) = do
AnyValue vx <- typeCheck x
Refl <- sameValue v vx
(v :) <$> checkElem vx xs
rightAny :: Value t -> Either l AnyValue
rightAny = Right . AnyValue
sameValue :: Value a -> Value b -> Either TypeMismatchError (a :~: b)
sameValue Bool{} Bool{} = Right Refl
sameValue Int{} Int{} = Right Refl
sameValue Float{} Float{} = Right Refl
sameValue String{} String{} = Right Refl
sameValue Date{} Date{} = Right Refl
sameValue Array{} Array{} = Right Refl
sameValue l r = Left $ TypeMismatchError
{ typeExpected = valueType l
, typeActual = valueType r
}