yaml-unscrambler 0.1.0.17 → 0.1.0.18
raw patch · 8 files changed
+210/−155 lines, 8 filesdep ~bytestringdep ~containersPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: bytestring, containers
API changes (from Hackage documentation)
Files
- library/YamlUnscrambler.hs +74/−22
- library/YamlUnscrambler/Err.hs +10/−10
- library/YamlUnscrambler/Expectations.hs +3/−3
- library/YamlUnscrambler/Prelude.hs +3/−2
- library/YamlUnscrambler/Util/Word8.hs +1/−3
- library/YamlUnscrambler/Util/Yaml.hs +5/−5
- test/Main.hs +107/−107
- yaml-unscrambler.cabal +7/−3
library/YamlUnscrambler.hs view
@@ -115,12 +115,14 @@ -- * -- +-- | Parser of any kind of YAML value: scalar, mapping or sequence. data Value a = Value { valueExpectation :: Ex.Value, valueParser :: Yaml.YamlValue -> Yaml.AnchorMap -> Either Err.ErrAtPath a } deriving (Functor) +-- | Specification of various alternative ways of parsing a non-nullable value. value :: [Scalar a] -> Maybe (Mapping a) -> Maybe (Sequence a) -> Value a value scalars mappings sequences = Value expectations parse@@ -165,6 +167,7 @@ Nothing -> Left (Err.ErrAtPath [] (Err.UnknownAnchorErr (fromString anchorName))) +-- | Specification of various alternative ways of parsing a nullable value. nullableValue :: [Scalar a] -> Maybe (Mapping a) -> Maybe (Sequence a) -> Value (Maybe a) nullableValue scalars mappings sequences = value@@ -174,35 +177,43 @@ -- ** Helpers +-- | Value parser, which only expects sequence values. sequenceValue :: Sequence a -> Value a sequenceValue sequence = value [] Nothing (Just sequence) +-- | Value parser, which only expects mapping values. mappingValue :: Mapping a -> Value a mappingValue mapping = value [] (Just mapping) Nothing +-- | Value parser, which only expects scalar values. scalarsValue :: [Scalar a] -> Value a scalarsValue scalars = value scalars Nothing Nothing -- * -- +-- | Scalar value parser. data Scalar a = Scalar { scalarExpectation :: Ex.Scalar, scalarParser :: ByteString -> Libyaml.Tag -> Libyaml.Style -> Either Text a } deriving (Functor) +-- | Custom parser function of a strict bytestring as a scalar value. bytesParsingScalar :: Ex.Scalar -> (ByteString -> Either Text a) -> Scalar a bytesParsingScalar expectation parser = Scalar expectation (\bytes _ _ -> parser bytes) +-- | Custom ASCII attoparsec parser of a scalar value. attoparsedScalar :: Ex.Scalar -> AsciiAtto.Parser a -> Scalar a attoparsedScalar expectation parser =- bytesParsingScalar expectation $- first (const "") . AsciiAtto.parseOnly (parser <* AsciiAtto.endOfInput)+ bytesParsingScalar expectation+ $ first (const "")+ . AsciiAtto.parseOnly (parser <* AsciiAtto.endOfInput) +-- | Add protection on the maximum allowed input size over a scalar parser. sizedScalar :: MaxInputSize -> Scalar a -> Scalar a sizedScalar (MaxInputSize maxInputSize) (Scalar {..}) = Scalar scalarExpectation $ \bytes tag style ->@@ -210,22 +221,27 @@ then scalarParser bytes tag style else Left ("Input is longer then the expected maximum of " <> showAsText maxInputSize <> " bytes") +-- | String scalar parser. stringScalar :: String a -> Scalar a stringScalar (String exp parse) = bytesParsingScalar (Ex.StringScalar exp) (\bytes -> first showAsText (Text.decodeUtf8' bytes) >>= parse) +-- | A parser expecting a null value and resulting in the provided constant value when successful. nullScalar :: a -> Scalar a nullScalar a = Scalar Ex.NullScalar $ \bytes tag _ ->- if tag == Libyaml.NullTag+ if tag+ == Libyaml.NullTag || ByteString.null bytes- || bytes == "~"+ || bytes+ == "~" || ByteString.saysNullInCiAscii bytes then Right a else Left "Not null" +-- | Boolean scalar parser. boolScalar :: Scalar Bool boolScalar = bytesParsingScalar Ex.BoolScalar $ \bytes ->@@ -241,42 +257,52 @@ else Left "Not a boolean" else Left "Not a boolean" +-- | Numeric scalar as scientific parser. scientificScalar :: Scalar Scientific scientificScalar = attoparsedScalar Ex.ScientificScalar AsciiAtto.scientific +-- | Numeric scalar as double parser. doubleScalar :: Scalar Double doubleScalar = attoparsedScalar Ex.DoubleScalar AsciiAtto.double +-- | Numeric scalar as rational parser protected with maximum allowed input size. rationalScalar :: MaxInputSize -> Scalar Rational rationalScalar a =- sizedScalar a $- attoparsedScalar (Ex.RationalScalar a) AsciiAtto.rational+ sizedScalar a+ $ attoparsedScalar (Ex.RationalScalar a) AsciiAtto.rational -- |+-- Numeric scalar parser into a bounded integer value. -- E.g., 'Int', 'Int64', 'Word', but not 'Integer'. boundedIntegerScalar :: (Integral a, FiniteBits a) => Signed -> NumeralSystem -> Scalar a boundedIntegerScalar a b = attoparsedScalar (Ex.BoundedIntegerScalar a b) (AsciiAtto.integralScalar a b) +-- |+-- Numeric scalar parser into any integer value. unboundedIntegerScalar :: MaxInputSize -> Signed -> NumeralSystem -> Scalar Integer unboundedIntegerScalar a b c =- sizedScalar a $- attoparsedScalar (Ex.UnboundedIntegerScalar a b c) (AsciiAtto.integralScalar b c)+ sizedScalar a+ $ attoparsedScalar (Ex.UnboundedIntegerScalar a b c) (AsciiAtto.integralScalar b c) +-- | String scalar parser as 'UTCTime' in ISO-8601. timestampScalar :: Scalar UTCTime timestampScalar = attoparsedScalar Ex.Iso8601TimestampScalar AsciiAtto.utcTimeInISO8601 +-- | String scalar parser as 'Day' in ISO-8601. dayScalar :: Scalar Day dayScalar = attoparsedScalar Ex.Iso8601DayScalar AsciiAtto.dayInISO8601 +-- | String scalar parser as 'TimeOfDay' in ISO-8601. timeScalar :: Scalar TimeOfDay timeScalar = attoparsedScalar Ex.Iso8601TimeScalar AsciiAtto.timeOfDayInISO8601 +-- | String scalar parser as 'UUID'. uuidScalar :: Scalar UUID uuidScalar = bytesParsingScalar Ex.UuidScalar $ \bytes ->@@ -286,6 +312,7 @@ Nothing -> Left "Invalid UUID" +-- | String scalar parser as binary data encoded in Base-64. binaryScalar :: Scalar ByteString binaryScalar = bytesParsingScalar Ex.Base64BinaryScalar $ \bytes ->@@ -299,12 +326,14 @@ -- * -- +-- | Mapping value parser. data Mapping a = Mapping { mappingExpectation :: Ex.Mapping, mappingParser :: [(Text, Yaml.YamlValue)] -> Yaml.AnchorMap -> Either Err.ErrAtPath a } deriving (Functor) +-- | Mapping parser which folds pairs into some final data-structure. foldMapping :: (key -> val -> assoc) -> Fold assoc a -> String key -> Value val -> Mapping a foldMapping zip (Fold foldStep foldInit foldExtract) key val = Mapping@@ -325,6 +354,7 @@ Err.ErrAtPath [] . Err.KeyErr (stringExpectation key) keyInput +-- | Mapping parser which allows the user to look up fields and process them with individual parsers. byKeyMapping :: CaseSensitive -> ByKey Text a -> Mapping a byKeyMapping caseSensitive byKey = Mapping expectation parser@@ -353,20 +383,22 @@ HashMap.lookupFirst (fmap Text.toLower kl) map in byKeyParser byKey id lookup lookupFirst keysErr keys =- Err.ErrAtPath [] $- Err.NoneOfMappingKeysFoundErr (byKeyExpectation byKey) caseSensitive keysAvail (toList keys)+ Err.ErrAtPath []+ $ Err.NoneOfMappingKeysFoundErr (byKeyExpectation byKey) caseSensitive keysAvail (toList keys) where keysAvail = fmap fst input -- * -- +-- | Sequence value parser. data Sequence a = Sequence { sequenceExpectation :: Ex.Sequence, sequenceParser :: [Yaml.YamlValue] -> Yaml.AnchorMap -> Either Err.ErrAtPath a } deriving (Functor) +-- | Homogenous sequence parser which folds into a final data-structure. foldSequence :: Fold a b -> Value a -> Sequence b foldSequence (Fold foldStep foldInit foldExtract) value = Sequence@@ -382,6 +414,7 @@ & first (Err.atSegment (showAsText index)) & fmap (\a -> (succ index, foldStep state a)) +-- | Heterogenous sequence parser by order in the sequence, which lets you apply individual parsers to elements. byOrderSequence :: ByOrder a -> Sequence a byOrderSequence (ByOrder {..}) = Sequence@@ -395,9 +428,10 @@ mapErr = \case NotEnoughElementsByOrderErr a ->- Err.ErrAtPath [] $- Err.NotEnoughElementsErr byOrderExpectation a+ Err.ErrAtPath []+ $ Err.NotEnoughElementsErr byOrderExpectation a +-- | Heterogenous sequence parser by index in the sequence, which lets you apply individual parsers to elements. byKeySequence :: ByKey Int a -> Sequence a byKeySequence (ByKey {..}) = Sequence expectation parser@@ -416,21 +450,24 @@ & either Left (first keysErr) where keysErr keys =- Err.ErrAtPath [] $- Err.NoneOfSequenceKeysFoundErr byKeyExpectation (toList keys)+ Err.ErrAtPath []+ $ Err.NoneOfSequenceKeysFoundErr byKeyExpectation (toList keys) -- * -- +-- | String value parser applicable to string scalars and mapping keys. data String a = String { stringExpectation :: Ex.String, stringParser :: Text -> Either Text a } deriving (Functor) +-- | String as is. textString :: String Text textString = String Ex.AnyString return +-- | Look the string up as a key in the provided dictionary. enumString :: CaseSensitive -> [(Text, a)] -> String a enumString (CaseSensitive caseSensitive) assocList = String expectation parser@@ -459,13 +496,23 @@ Just a -> return a _ -> Left "Unexpected value" -formattedString :: Text -> (Text -> Either Text a) -> String a+-- | String parsed using the provided function.+formattedString ::+ -- | Format name for documentation and expectations.+ Text ->+ (Text -> Either Text a) ->+ String a formattedString format parser = String (Ex.FormattedString format) parser -attoparsedString :: Text -> TextAtto.Parser a -> String a+-- | String parsed using the provided textual attoparsec parser.+attoparsedString ::+ -- | Format name for documentation and expectations.+ Text ->+ TextAtto.Parser a ->+ String a attoparsedString format parser = String (Ex.FormattedString format)@@ -473,6 +520,7 @@ -- * -- +-- | General abstraction for specification of parsers performing lookups by keys. data ByKey key a = ByKey { byKeyExpectation :: Ex.ByKey key, byKeyParser ::@@ -508,6 +556,7 @@ (Ex.EitherByKey le re) (\a b c d -> lp a b c d <|> rp a b c d) +-- | Parse a value at a key using the provided parser. atByKey :: key -> Value a -> ByKey key a atByKey key valueSpec = ByKey@@ -517,12 +566,13 @@ parser renderKey lookup _ env = case lookup key of Just val ->- lift $- first (Err.atSegment (renderKey key)) $- valueParser valueSpec val env+ lift+ $ first (Err.atSegment (renderKey key))+ $ valueParser valueSpec val env Nothing -> throwE (pure key) +-- | Parse a value at one of keys (whichever exists) using the provided parser. atOneOfByKey :: [key] -> Value a -> ByKey key a atOneOfByKey keys valueSpec = ByKey@@ -532,9 +582,9 @@ parser renderKey _ lookup env = case lookup keys of Just (key, val) ->- lift $- first (Err.atSegment (renderKey key)) $- valueParser valueSpec val env+ lift+ $ first (Err.atSegment (renderKey key))+ $ valueParser valueSpec val env Nothing -> throwE (fromList keys) @@ -544,6 +594,7 @@ = NotEnoughElementsByOrderErr Int +-- | Parser which fetches elements by the order in which it is composed. data ByOrder a = ByOrder { byOrderExpectation :: Ex.ByOrder, byOrderParser :: StateT (Int, [Yaml.YamlValue]) (ReaderT Yaml.AnchorMap (ExceptT ByOrderErr (Either Err.ErrAtPath))) a@@ -564,6 +615,7 @@ (Ex.BothByOrder le re) (select lp rp) +-- | Parse the next value using the provided parser. fetchByOrder :: Value a -> ByOrder a fetchByOrder value = ByOrder
library/YamlUnscrambler/Err.hs view
@@ -25,33 +25,33 @@ data Err = KeyErr+ -- | Key expectation. Ex.String- -- ^ Key expectation.+ -- | Key input. Text- -- ^ Key input.+ -- | String parsing error. Text- -- ^ String parsing error. | NoneOfMappingKeysFoundErr (Ex.ByKey Text) CaseSensitive+ -- | Available keys. [Text]- -- ^ Available keys.+ -- | Keys looked up. [Text]- -- ^ Keys looked up. | NoneOfSequenceKeysFoundErr (Ex.ByKey Int) [Int] | ScalarErr+ -- | Expected formats. [Ex.Scalar]- -- ^ Expected formats.+ -- | Input. ByteString- -- ^ Input.+ -- | Tag. Libyaml.Tag- -- ^ Tag.+ -- | Style. Libyaml.Style- -- ^ Style.+ -- | Last error. (Maybe Text)- -- ^ Last error. | UnexpectedScalarErr Ex.Value | UnexpectedMappingErr
library/YamlUnscrambler/Expectations.hs view
@@ -59,12 +59,12 @@ | -- | One of options. Suitable for enumerations. OneOfString CaseSensitive+ -- | Options. [Text]- -- ^ Options. | -- | Must conform to a textually described format. FormattedString+ -- | Description of the format. Text- -- ^ Description of the format. data ByKey key = AnyByKey@@ -72,8 +72,8 @@ | EitherByKey (ByKey key) (ByKey key) | BothByKey (ByKey key) (ByKey key) | LookupByKey+ -- | Keys to lookup. [key]- -- ^ Keys to lookup. Value data ByOrder
library/YamlUnscrambler/Prelude.hs view
@@ -1,3 +1,5 @@+{-# OPTIONS_GHC -Wno-dodgy-imports #-}+ module YamlUnscrambler.Prelude ( module Exports, showAsText,@@ -41,7 +43,7 @@ import Data.Fixed as Exports import Data.Foldable as Exports hiding (toList) import Data.Function as Exports hiding (id, (.))-import Data.Functor as Exports+import Data.Functor as Exports hiding (unzip) import Data.Functor.Compose as Exports import Data.HashMap.Strict as Exports (HashMap) import Data.HashSet as Exports (HashSet)@@ -90,7 +92,6 @@ import System.Mem as Exports import System.Mem.StableName as Exports import System.Timeout as Exports-import Text.ParserCombinators.ReadPrec as Exports (ReadPrec, readP_to_Prec, readPrec_to_P, readPrec_to_S, readS_to_Prec) import Text.Printf as Exports (hPrintf, printf) import Text.Read as Exports (Read (..), readEither, readMaybe) import Unsafe.Coerce as Exports
library/YamlUnscrambler/Util/Word8.hs view
@@ -4,8 +4,6 @@ lowercaseInAscii :: Word8 -> Word8 lowercaseInAscii a =- if 65 <= a && a <= 90- || 192 <= a && a <= 214- || 216 <= a && a <= 222+ if 65 <= a && a <= 90 || 192 <= a && a <= 214 || 216 <= a && a <= 222 then a + 32 else a
library/YamlUnscrambler/Util/Yaml.hs view
@@ -7,8 +7,8 @@ parseByteStringToRawDoc :: ByteString -> Either Text YamlParser.RawDoc parseByteStringToRawDoc input =- first (mappend "YAML AST parsing: " . showAsText) $- unsafePerformIO $- try @SomeException $- Conduit.runConduitRes $- Conduit.fuse (Libyaml.decode input) (YamlParser.sinkRawDoc)+ first (mappend "YAML AST parsing: " . showAsText)+ $ unsafePerformIO+ $ try @SomeException+ $ Conduit.runConduitRes+ $ Conduit.fuse (Libyaml.decode input) (YamlParser.sinkRawDoc)
test/Main.hs view
@@ -11,62 +11,62 @@ main :: IO () main =- defaultMain $- testGroup+ defaultMain+ $ testGroup "All tests"- [ testCase "Should fail on sequence when no sequence is specified" $- let unscrambler =- U.value [] (Just mapping) Nothing- where- mapping =- U.foldMapping (,) Fold.list U.textString value- where- value =- U.value [nullScalar, intScalar] Nothing Nothing- where- nullScalar =- U.nullScalar Nothing- intScalar =- fmap Just $ U.boundedIntegerScalar @Int (U.Signed True) U.DecimalNumeralSystem- input =- [NeatInterpolation.text|+ [ testCase "Should fail on sequence when no sequence is specified"+ $ let unscrambler =+ U.value [] (Just mapping) Nothing+ where+ mapping =+ U.foldMapping (,) Fold.list U.textString value+ where+ value =+ U.value [nullScalar, intScalar] Nothing Nothing+ where+ nullScalar =+ U.nullScalar Nothing+ intScalar =+ fmap Just $ U.boundedIntegerScalar @Int (U.Signed True) U.DecimalNumeralSystem+ input =+ [NeatInterpolation.text| a: 2 b: 3 c: - 1 - 2 |]- in case U.parseText unscrambler input of- Right res ->- assertFailure (show res)- Left failure ->- assertEqual (Text.unpack failure) "Error at path /c. Unexpected sequence value" failure,- testCase "Domain sum-type regression" $- let unscrambler =- doc- where- doc =- U.mappingValue $- U.byKeyMapping (U.CaseSensitive True) $- asum+ in case U.parseText unscrambler input of+ Right res ->+ assertFailure (show res)+ Left failure ->+ assertEqual (Text.unpack failure) "Error at path /c. Unexpected sequence value" failure,+ testCase "Domain sum-type regression"+ $ let unscrambler =+ doc+ where+ doc =+ U.mappingValue+ $ U.byKeyMapping (U.CaseSensitive True)+ $ asum [ Just <$> U.atByKey "sums" sum, pure Nothing ]- sum =- U.mappingValue $- U.foldMapping (,) Fold.list U.textString sumVariant- sumVariant =- U.mappingValue $- U.foldMapping (,) Fold.list U.textString params- params =- U.value [nullScalar, intScalar] Nothing Nothing- where- nullScalar =- U.nullScalar Nothing- intScalar =- fmap Just $ U.boundedIntegerScalar @Int (U.Signed True) U.DecimalNumeralSystem- input =- [NeatInterpolation.text|+ sum =+ U.mappingValue+ $ U.foldMapping (,) Fold.list U.textString sumVariant+ sumVariant =+ U.mappingValue+ $ U.foldMapping (,) Fold.list U.textString params+ params =+ U.value [nullScalar, intScalar] Nothing Nothing+ where+ nullScalar =+ U.nullScalar Nothing+ intScalar =+ fmap Just $ U.boundedIntegerScalar @Int (U.Signed True) U.DecimalNumeralSystem+ input =+ [NeatInterpolation.text| sums: A: a:@@ -74,81 +74,81 @@ - Bool b: Char, Double |]- in case U.parseText unscrambler input of- Right res ->- assertFailure (show res)- Left failure ->- assertEqual "" "Error at path /sums/A/a. Unexpected sequence value" failure,- testCase "Domain sum-type correct" $- let unscrambler =- doc- where- doc =- U.mappingValue $- U.byKeyMapping (U.CaseSensitive True) $- asum+ in case U.parseText unscrambler input of+ Right res ->+ assertFailure (show res)+ Left failure ->+ assertEqual "" "Error at path /sums/A/a. Unexpected sequence value" failure,+ testCase "Domain sum-type correct"+ $ let unscrambler =+ doc+ where+ doc =+ U.mappingValue+ $ U.byKeyMapping (U.CaseSensitive True)+ $ asum [ Just <$> U.atByKey "sums" sum, pure Nothing ]- sum =- U.mappingValue $- U.foldMapping (,) Fold.list U.textString sumVariant- sumVariant =- U.mappingValue $- U.foldMapping (,) Fold.list U.textString params- params =- U.value [nullScalar, valueScalar] Nothing Nothing- where- nullScalar =- U.nullScalar Nothing- valueScalar =- fmap Just $ U.stringScalar U.textString- input =- [NeatInterpolation.text|+ sum =+ U.mappingValue+ $ U.foldMapping (,) Fold.list U.textString sumVariant+ sumVariant =+ U.mappingValue+ $ U.foldMapping (,) Fold.list U.textString params+ params =+ U.value [nullScalar, valueScalar] Nothing Nothing+ where+ nullScalar =+ U.nullScalar Nothing+ valueScalar =+ fmap Just $ U.stringScalar U.textString+ input =+ [NeatInterpolation.text| sums: A: a: Text b: Int |]- in case U.parseText unscrambler input of- Right res ->- assertEqual "" (Just [("A", [("a", Just "Text"), ("b", Just "Int")])]) res- Left failure ->- assertFailure (Text.unpack failure),- testCase "Scalar errors are readable" $- let unscrambler =- doc- where- doc =- U.mappingValue $- U.byKeyMapping (U.CaseSensitive True) $- asum+ in case U.parseText unscrambler input of+ Right res ->+ assertEqual "" (Just [("A", [("a", Just "Text"), ("b", Just "Int")])]) res+ Left failure ->+ assertFailure (Text.unpack failure),+ testCase "Scalar errors are readable"+ $ let unscrambler =+ doc+ where+ doc =+ U.mappingValue+ $ U.byKeyMapping (U.CaseSensitive True)+ $ asum [ Just <$> U.atByKey "sums" sum, pure Nothing ]- sum =- U.mappingValue $- U.foldMapping (,) Fold.list U.textString sumVariant- sumVariant =- U.mappingValue $- U.foldMapping (,) Fold.list U.textString params- params =- U.value [nullScalar, intScalar] Nothing Nothing- where- nullScalar =- U.nullScalar Nothing- intScalar =- fmap Just $ U.boundedIntegerScalar @Int (U.Signed True) U.DecimalNumeralSystem- input =- [NeatInterpolation.text|+ sum =+ U.mappingValue+ $ U.foldMapping (,) Fold.list U.textString sumVariant+ sumVariant =+ U.mappingValue+ $ U.foldMapping (,) Fold.list U.textString params+ params =+ U.value [nullScalar, intScalar] Nothing Nothing+ where+ nullScalar =+ U.nullScalar Nothing+ intScalar =+ fmap Just $ U.boundedIntegerScalar @Int (U.Signed True) U.DecimalNumeralSystem+ input =+ [NeatInterpolation.text| sums: A: a: Int b: Char |]- in case U.parseText unscrambler input of- Right res ->- assertFailure (show res)- Left failure ->- assertEqual "" "Error at path /sums/A/a. Expecting one of the following formats: null, signed decimal. Got input: \"Int\"" failure+ in case U.parseText unscrambler input of+ Right res ->+ assertFailure (show res)+ Left failure ->+ assertEqual "" "Error at path /sums/A/a. Expecting one of the following formats: null, signed decimal. Got input: \"Int\"" failure ]
yaml-unscrambler.cabal view
@@ -1,7 +1,11 @@ cabal-version: 3.0 name: yaml-unscrambler-version: 0.1.0.17+version: 0.1.0.18 synopsis: Flexible declarative YAML parsing toolkit+description:+ Very flexible declarative YAML parsing toolkit with extensive error detalization capabilities and expected schema generation.++category: Parsers, Parser, YAML homepage: https://github.com/nikita-volkov/yaml-unscrambler bug-reports: https://github.com/nikita-volkov/yaml-unscrambler/issues author: Nikita Volkov <nikita.y.volkov@mail.ru>@@ -86,9 +90,9 @@ , attoparsec-time >=1.0.1.2 && <1.1 , base >=4.13 && <5 , base64-bytestring >=1.2.1 && <1.3- , bytestring >=0.10 && <0.12+ , bytestring >=0.10 && <0.13 , conduit >=1.3.2 && <1.4- , containers >=0.6.2 && <0.7+ , containers >=0.6.2 && <0.8 , foldl >=1.4 && <2 , hashable >=1.4 && <2 , libyaml >=0.1.2 && <0.2