{-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeOperators #-} {- | Module : Toml.Type.Value Copyright : (c) 2018-2022 Kowainik SPDX-License-Identifier : MPL-2.0 Maintainer : Kowainik Stability : Stable Portability : Portable GADT value for TOML. @since 0.0.0 -} module Toml.Type.Value ( -- * Type of value TValue (..) , showType -- * Value , Value (..) , eqValueList , valueType -- * Type checking , TypeMismatchError (..) , sameValue ) where import Control.DeepSeq (NFData (..), rnf) import Data.String (IsString (..)) import Data.Text (Text) import Data.Time (Day, LocalTime, TimeOfDay, ZonedTime, zonedTimeToUTC) import Data.Type.Equality ((:~:) (..)) import GHC.Generics (Generic) {- | Needed for GADT parameterization @since 0.0.0 -} data TValue = TBool | TInteger | TDouble | TText | TZoned | TLocal | TDay | THours | TArray deriving stock (Eq, Show, Read, Generic) deriving anyclass (NFData) {- | Convert 'TValue' constructors to 'String' without @T@ prefix. @since 0.0.0 -} showType :: TValue -> String showType = drop 1 . show {- | Value in @key = value@ pair. @since 0.0.0 -} data Value (t :: TValue) 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 # hexadecimal oct2 = 0o755 # octal, useful for Unix file permissions bin1 = 0b11010110 # binary @ -} Integer :: Integer -> Value 'TInteger {- | Floating point number: @ # fractional flt1 = +1.0 flt2 = 3.1415 flt3 = -0.01 # exponent flt4 = 5e+22 flt5 = 1e6 flt6 = -2E-2 # both flt7 = 6.626e-34 # infinity sf1 = inf # positive infinity sf2 = +inf # positive infinity sf3 = -inf # negative infinity # not a number sf4 = nan # actual sNaN/qNaN encoding is implementation specific sf5 = +nan # same as \`nan\` sf6 = -nan # same as \`nan\` @ -} Double :: Double -> Value 'TDouble {- | String value: @ # basic string name = \"Orange\" physical.color = "orange" physical.shape = "round" # multiline basic string str1 = """ Roses are red Violets are blue""" # literal string: What you see is what you get. winpath = 'C:\Users\nodejs\templates' winpath2 = '\\ServerX\admin$\system32\' quoted = 'Tom \"Dubs\" Preston-Werner' regex = '<\i\c*\s*>' @ -} Text :: Text -> Value 'TText {- | Offset date-time: @ odt1 = 1979-05-27T07:32:00Z odt2 = 1979-05-27T00:32:00-07:00 odt3 = 1979-05-27T00:32:00.999999-07:00 @ -} Zoned :: ZonedTime -> Value 'TZoned {- | Local date-time (without offset): @ ldt1 = 1979-05-27T07:32:00 ldt2 = 1979-05-27T00:32:00.999999 @ -} Local :: LocalTime -> Value 'TLocal {- | Local date (only day): @ ld1 = 1979-05-27 @ -} Day :: Day -> Value 'TDay {- | Local time (time of the day): @ lt1 = 07:32:00 lt2 = 00:32:00.999999 @ -} Hours :: TimeOfDay -> Value 'THours {- | 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 -- | @since 0.0.0 deriving stock instance Show (Value t) instance NFData (Value t) where rnf (Bool n) = rnf n rnf (Integer n) = rnf n rnf (Double n) = rnf n rnf (Text n) = rnf n rnf (Zoned n) = rnf n rnf (Local n) = rnf n rnf (Day n) = rnf n rnf (Hours n) = rnf n rnf (Array n) = rnf n instance (t ~ 'TInteger) => Num (Value t) where (Integer a) + (Integer b) = Integer $ a + b (Integer a) * (Integer b) = Integer $ a * b abs (Integer a) = Integer (abs a) signum (Integer a) = Integer (signum a) fromInteger = Integer negate (Integer a) = Integer (negate a) instance (t ~ 'TText) => IsString (Value t) where fromString = Text . fromString @Text {-# INLINE fromString #-} instance Eq (Value t) where (Bool b1) == (Bool b2) = b1 == b2 (Integer i1) == (Integer i2) = i1 == i2 (Double f1) == (Double f2) | isNaN f1 && isNaN f2 = True | otherwise = f1 == f2 (Text s1) == (Text s2) = s1 == s2 (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 (Array a1) == (Array a2) = eqValueList a1 a2 {- | Compare list of 'Value' of possibly different types. @since 0.0.0 -} 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 'TValue'. Unfortunately, there's no way to guarantee that 'valueType' will return @t@ for object with type @Value \'t@. @since 0.0.0 -} valueType :: Value t -> TValue valueType (Bool _) = TBool valueType (Integer _) = TInteger valueType (Double _) = TDouble valueType (Text _) = TText valueType (Zoned _) = TZoned valueType (Local _) = TLocal valueType (Day _) = TDay valueType (Hours _) = THours valueType (Array _) = TArray ---------------------------------------------------------------------------- -- Typechecking values ---------------------------------------------------------------------------- {- | Data type that holds expected vs. actual type. @since 0.1.0 -} data TypeMismatchError = TypeMismatchError { typeExpected :: !TValue , typeActual :: !TValue } deriving stock (Eq) -- | @since 0.1.0 instance Show TypeMismatchError where show TypeMismatchError{..} = "Expected type '" ++ showType typeExpected ++ "' but actual type: '" ++ showType typeActual ++ "'" {- | Checks whether two values are the same. This function is used for type checking where first argument is expected type and second argument is actual type. @since 0.0.0 -} sameValue :: Value a -> Value b -> Either TypeMismatchError (a :~: b) sameValue Bool{} Bool{} = Right Refl sameValue Integer{} Integer{} = Right Refl sameValue Double{} Double{} = Right Refl sameValue Text{} Text{} = Right Refl sameValue Zoned{} Zoned{} = Right Refl sameValue Local{} Local{} = Right Refl sameValue Day{} Day{} = Right Refl sameValue Hours{} Hours{} = Right Refl sameValue Array{} Array{} = Right Refl sameValue l r = Left $ TypeMismatchError { typeExpected = valueType l , typeActual = valueType r }