{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE Safe #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Data.YAML.Schema
( SchemaResolver(..)
, failsafeSchemaResolver
, jsonSchemaResolver
, coreSchemaResolver
, Scalar(..)
, tagNull, tagBool, tagStr, tagInt, tagFloat, tagSeq, tagMap
) where
import Control.Monad.Except
import qualified Data.Char as C
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified Data.Text as T
import Numeric (readHex, readOct)
import Text.Parsec as P
import Text.Parsec.Text
import Data.YAML.Event (Tag, isUntagged, mkTag, untagged)
import qualified Data.YAML.Event as YE
import Util
data Scalar = SNull
| SBool !Bool
| SFloat !Double
| SInt !Integer
| SStr !Text
| SUnknown !Tag !Text
deriving (Eq,Ord,Show)
data SchemaResolver = SchemaResolver
{ schemaResolverScalar :: Tag -> YE.Style -> T.Text -> Either String Scalar
, schemaResolverSequence :: Tag -> Either String Tag
, schemaResolverMapping :: Tag -> Either String Tag
}
data ScalarTag = ScalarBangTag
| ScalarQMarkTag
| ScalarTag !Tag
scalarTag :: (ScalarTag -> T.Text -> Either String Scalar)
-> Tag -> YE.Style -> T.Text -> Either String Scalar
scalarTag f tag sty val = f tag' val
where
tag' = case sty of
YE.Plain
| tag == untagged -> ScalarQMarkTag
_ | tag == untagged -> ScalarBangTag
| tag == tagBang -> ScalarBangTag
| otherwise -> ScalarTag tag
failsafeSchemaResolver :: SchemaResolver
failsafeSchemaResolver = SchemaResolver{..}
where
schemaResolverScalar = scalarTag go
where
go ScalarBangTag v = Right (SStr v)
go (ScalarTag t) v
| t == tagStr = Right (SStr v)
| otherwise = Right (SUnknown t v)
go ScalarQMarkTag v = Right (SUnknown untagged v)
schemaResolverMapping t
| t == tagBang = Right tagMap
| otherwise = Right t
schemaResolverSequence t
| t == tagBang = Right tagSeq
| otherwise = Right t
jsonSchemaResolver :: SchemaResolver
jsonSchemaResolver = SchemaResolver{..}
where
schemaResolverScalar = scalarTag go
where
go ScalarBangTag v = Right (SStr v)
go (ScalarTag t) v
| t == tagStr = Right (SStr v)
| t == tagNull = if isNullLiteral v then Right SNull else Left ("invalid !!null " ++ show v)
| t == tagInt = maybe (Left $ "invalid !!int " ++ show v) (Right . SInt) $ jsonDecodeInt v
| t == tagFloat = maybe (Left $ "invalid !!float " ++ show v) (Right . SFloat) $ jsonDecodeFloat v
| t == tagBool = maybe (Left $ "invalid !!bool " ++ show v) (Right . SBool) $ jsonDecodeBool v
| otherwise = Right (SUnknown t v)
go ScalarQMarkTag v
| isNullLiteral v = Right SNull
| Just b <- jsonDecodeBool v = Right $! SBool b
| Just i <- jsonDecodeInt v = Right $! SInt i
| Just f <- jsonDecodeFloat v = Right $! SFloat f
| otherwise = Right (SUnknown untagged v)
isNullLiteral = (== "null")
schemaResolverMapping t
| t == tagBang = Right tagMap
| isUntagged t = Right tagMap
| otherwise = Right t
schemaResolverSequence t
| t == tagBang = Right tagSeq
| isUntagged t = Right tagSeq
| otherwise = Right t
coreSchemaResolver :: SchemaResolver
coreSchemaResolver = SchemaResolver{..}
where
schemaResolverScalar = scalarTag go
where
go ScalarBangTag v = Right (SStr v)
go (ScalarTag t) v
| t == tagStr = Right (SStr v)
| t == tagNull = if isNullLiteral v then Right SNull else Left ("invalid !!null " ++ show v)
| t == tagInt = maybe (Left $ "invalid !!int " ++ show v) (Right . SInt) $ coreDecodeInt v
| t == tagFloat = maybe (Left $ "invalid !!float " ++ show v) (Right . SFloat) $ coreDecodeFloat v
| t == tagBool = maybe (Left $ "invalid !!bool " ++ show v) (Right . SBool) $ coreDecodeBool v
| otherwise = Right (SUnknown t v)
go ScalarQMarkTag v
| isNullLiteral v = Right SNull
| Just b <- coreDecodeBool v = Right $! SBool b
| Just i <- coreDecodeInt v = Right $! SInt i
| Just f <- coreDecodeFloat v = Right $! SFloat f
| otherwise = Right (SStr v)
isNullLiteral = flip Set.member (Set.fromList [ "", "null", "NULL", "Null", "~" ])
schemaResolverMapping t
| t == tagBang = Right tagMap
| isUntagged t = Right tagMap
| otherwise = Right t
schemaResolverSequence t
| t == tagBang = Right tagSeq
| isUntagged t = Right tagSeq
| otherwise = Right t
jsonDecodeBool :: T.Text -> Maybe Bool
jsonDecodeBool "false" = Just False
jsonDecodeBool "true" = Just True
jsonDecodeBool _ = Nothing
coreDecodeBool :: T.Text -> Maybe Bool
coreDecodeBool = flip Map.lookup $
Map.fromList [ ("true", True)
, ("True", True)
, ("TRUE", True)
, ("false", False)
, ("False", False)
, ("FALSE", False)
]
jsonDecodeInt :: T.Text -> Maybe Integer
jsonDecodeInt t | T.null t = Nothing
jsonDecodeInt "0" = Just 0
jsonDecodeInt t = do
let tabs | T.isPrefixOf "-" t = T.tail t
| otherwise = t
guard (not (T.null tabs))
guard (T.head tabs /= '0')
guard (T.all C.isDigit tabs)
readMaybe (T.unpack t)
coreDecodeInt :: T.Text -> Maybe Integer
coreDecodeInt t
| T.null t = Nothing
| Just rest <- T.stripPrefix "0x" t
, T.all C.isHexDigit rest
, [(j,"")] <- readHex (T.unpack rest)
= Just $! j
| Just rest <- T.stripPrefix "0o" t
, T.all C.isOctDigit rest
, [(j,"")] <- readOct (T.unpack rest)
= Just $! j
| T.all C.isDigit t
= Just $! read (T.unpack t)
| Just rest <- T.stripPrefix "+" t
, not (T.null rest)
, T.all C.isDigit rest
= Just $! read (T.unpack rest)
| Just rest <- T.stripPrefix "-" t
, not (T.null rest)
, T.all C.isDigit rest
= Just $! read (T.unpack t)
| otherwise = Nothing
jsonDecodeFloat :: T.Text -> Maybe Double
jsonDecodeFloat = either (const Nothing) Just . parse float ""
where
float :: Parser Double
float = do
p0 <- option "" ("-" <$ char '-')
p1 <- do
d <- digit
if (d /= '0')
then (d:) <$> P.many digit
else pure [d]
p2 <- option "" $ (:) <$> char '.' <*> option "0" (many1 digit)
p3 <- option "" $ do
void (char 'e' P.<|> char 'E')
s <- option "" (("-" <$ char '-') P.<|> ("" <$ char '+'))
d <- P.many digit
pure ("e" ++ s ++ d)
eof
let t' = p0++p1++p2++p3
pure $! read t'
coreDecodeFloat :: T.Text -> Maybe Double
coreDecodeFloat t
| Just j <- Map.lookup t literals = Just j
| otherwise = either (const Nothing) Just . parse float "" $ t
where
float :: Parser Double
float = do
p0 <- option "" (("-" <$ char '-') P.<|> "" <$ char '+')
p1 <- (char '.' *> (("0."++) <$> many1 digit))
P.<|> do d1 <- many1 digit
d2 <- option "" $ (:) <$> char '.' <*> option "0" (many1 digit)
pure (d1++d2)
p2 <- option "" $ do
void (char 'e' P.<|> char 'E')
s <- option "" (("-" <$ char '-') P.<|> ("" <$ char '+'))
d <- P.many digit
pure ("e" ++ s ++ d)
eof
let t' = p0++p1++p2
pure $! read t'
literals = Map.fromList
[ ("0" , 0)
, (".nan", (0/0))
, (".NaN", (0/0))
, (".NAN", (0/0))
, (".inf", (1/0))
, (".Inf", (1/0))
, (".INF", (1/0))
, ("+.inf", (1/0))
, ("+.Inf", (1/0))
, ("+.INF", (1/0))
, ("-.inf", (-1/0))
, ("-.Inf", (-1/0))
, ("-.INF", (-1/0))
]
tagNull, tagBool, tagStr, tagInt, tagFloat, tagSeq, tagMap, tagBang :: Tag
tagNull = mkTag "tag:yaml.org,2002:null"
tagStr = mkTag "tag:yaml.org,2002:str"
tagInt = mkTag "tag:yaml.org,2002:int"
tagFloat = mkTag "tag:yaml.org,2002:float"
tagBool = mkTag "tag:yaml.org,2002:bool"
tagSeq = mkTag "tag:yaml.org,2002:seq"
tagMap = mkTag "tag:yaml.org,2002:map"
tagBang = mkTag "!"