{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE Safe #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Data.YAML.Schema.Internal
( SchemaResolver(..)
, failsafeSchemaResolver
, jsonSchemaResolver
, coreSchemaResolver
, Scalar(..)
, SchemaEncoder(..)
, failsafeSchemaEncoder
, jsonSchemaEncoder
, coreSchemaEncoder
, tagNull, tagBool, tagStr, tagInt, tagFloat, tagSeq, tagMap
, isPlainChar , isAmbiguous, defaultSchemaEncoder, setScalarStyle
, encodeDouble, encodeBool, encodeInt
) where
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 (ScalarStyle (..), 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,Generic)
instance NFData Scalar where
rnf SNull = ()
rnf (SBool _) = ()
rnf (SFloat _) = ()
rnf (SInt _) = ()
rnf (SStr _) = ()
rnf (SUnknown t _) = rnf t
data SchemaResolver = SchemaResolver
{ schemaResolverScalar :: Tag -> YE.ScalarStyle -> T.Text -> Either String Scalar
, schemaResolverSequence :: Tag -> Either String Tag
, schemaResolverMapping :: Tag -> Either String Tag
, schemaResolverMappingDuplicates :: Bool
}
data ScalarTag = ScalarBangTag
| ScalarQMarkTag
| ScalarTag !Tag
scalarTag :: (ScalarTag -> T.Text -> Either String Scalar)
-> Tag -> YE.ScalarStyle -> 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
schemaResolverMappingDuplicates = False
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
schemaResolverMappingDuplicates = False
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
schemaResolverMappingDuplicates = False
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.many1 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.many1 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 "!"
data SchemaEncoder = SchemaEncoder
{ schemaEncoderScalar :: Scalar -> Either String (Tag, ScalarStyle, T.Text)
, schemaEncoderSequence :: Tag -> Either String Tag
, schemaEncoderMapping :: Tag -> Either String Tag
}
mappingTag :: Tag -> Either String Tag
mappingTag t
| t == tagMap = Right untagged
| otherwise = Right t
seqTag :: Tag -> Either String Tag
seqTag t
| t == tagSeq = Right untagged
| otherwise = Right t
failsafeSchemaEncoder :: SchemaEncoder
failsafeSchemaEncoder = SchemaEncoder{..}
where
schemaEncoderScalar s = case s of
SNull -> Left "SNull scalar type not supported in failsafeSchemaEncoder"
SBool _ -> Left "SBool scalar type not supported in failsafeSchemaEncoder"
SFloat _ -> Left "SFloat scalar type not supported in failsafeSchemaEncoder"
SInt _ -> Left "SInt scalar type not supported in failsafeSchemaEncoder"
SStr text -> failEncodeStr text
SUnknown t v -> Right (t, DoubleQuoted, v)
schemaEncoderMapping = mappingTag
schemaEncoderSequence = seqTag
jsonSchemaEncoder :: SchemaEncoder
jsonSchemaEncoder = SchemaEncoder{..}
where
schemaEncoderScalar s = case s of
SNull -> Right (untagged, Plain, "null")
SBool bool -> Right (untagged, Plain, encodeBool bool)
SFloat double -> Right (untagged, Plain, encodeDouble double)
SInt int -> Right (untagged, Plain, encodeInt int)
SStr text -> jsonEncodeStr text
SUnknown _ _ -> Left "SUnknown scalar type not supported in jsonSchemaEncoder"
schemaEncoderMapping = mappingTag
schemaEncoderSequence = seqTag
coreSchemaEncoder :: SchemaEncoder
coreSchemaEncoder = SchemaEncoder{..}
where
schemaEncoderScalar s = case s of
SNull -> Right (untagged, Plain, "null")
SBool bool -> Right (untagged, Plain, encodeBool bool)
SFloat double -> Right (untagged, Plain, encodeDouble double)
SInt int -> Right (untagged, Plain, encodeInt int)
SStr text -> coreEncodeStr text
SUnknown t v -> Right (t, DoubleQuoted, v)
schemaEncoderMapping = mappingTag
schemaEncoderSequence = seqTag
encodeBool :: Bool -> T.Text
encodeBool b = if b then "true" else "false"
encodeDouble :: Double -> T.Text
encodeDouble d
| d /= d = ".nan"
| d == (1/0) = ".inf"
| d == (-1/0) = "-.inf"
| otherwise = T.pack . show $ d
encodeInt :: Integer -> T.Text
encodeInt = T.pack . show
failEncodeStr :: T.Text -> Either String (Tag, ScalarStyle, T.Text)
failEncodeStr t
| T.isPrefixOf " " t = Right (untagged, DoubleQuoted, t)
| T.last t == ' ' = Right (untagged, DoubleQuoted, t)
| T.any (not. isPlainChar) t = Right (untagged, DoubleQuoted, t)
| otherwise = Right (untagged, Plain, t)
jsonEncodeStr :: T.Text -> Either String (Tag, ScalarStyle, T.Text)
jsonEncodeStr t
| T.null t = Right (untagged, DoubleQuoted, t)
| T.isPrefixOf " " t = Right (untagged, DoubleQuoted, t)
| T.last t == ' ' = Right (untagged, DoubleQuoted, t)
| T.any (not. isPlainChar) t = Right (untagged, DoubleQuoted, t)
| isAmbiguous jsonSchemaResolver t = Right (untagged, DoubleQuoted, t)
| otherwise = Right (untagged, Plain, t)
coreEncodeStr :: T.Text -> Either String (Tag, ScalarStyle, T.Text)
coreEncodeStr t
| T.null t = Right (untagged, DoubleQuoted, t)
| T.isPrefixOf " " t = Right (untagged, DoubleQuoted, t)
| T.last t == ' ' = Right (untagged, DoubleQuoted, t)
| T.any (not. isPlainChar) t = Right (untagged, DoubleQuoted, t)
| isAmbiguous coreSchemaResolver t = Right (untagged, DoubleQuoted, t)
| otherwise = Right (untagged, Plain, t)
isPlainChar :: Char -> Bool
isPlainChar c = C.isAlphaNum c || c `elem` (" ~$^+=</;._\\" :: String)
isAmbiguous :: SchemaResolver -> T.Text -> Bool
isAmbiguous SchemaResolver{..} t = case schemaResolverScalar untagged Plain t of
Left err -> error err
Right (SStr _ ) -> False
Right _ -> True
defaultSchemaEncoder :: SchemaEncoder
defaultSchemaEncoder = coreSchemaEncoder
setScalarStyle :: (Scalar -> Either String (Tag, ScalarStyle, T.Text)) -> SchemaEncoder -> SchemaEncoder
setScalarStyle customScalarEncoder encoder = encoder { schemaEncoderScalar = customScalarEncoder }