wireform-yaml-0.1.0.0: src/YAML/Class.hs
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
-- | Typeclass-based YAML serialization with GHC Generics support.
module YAML.Class (
ToYAML (..),
FromYAML (..),
encodeYAML,
encodeYAMLBS,
decodeYAML,
decodeYAMLBS,
GToYAML (..),
GFromYAML (..),
validateFieldNames,
) where
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as BSL
import Data.Functor.Compose (Compose (..))
import Data.Functor.Const (Const (..))
import Data.Functor.Identity (Identity (..))
import Data.Functor.Product qualified as FProduct
import Data.Functor.Sum qualified as FSum
import Data.HashSet qualified as HS
import Data.Int (Int16, Int32, Int64, Int8)
import Data.IntMap.Strict (IntMap)
import Data.IntMap.Strict qualified as IntMap
import Data.IntSet (IntSet)
import Data.IntSet qualified as IntSet
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NE
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Monoid qualified as Mon
import Data.Ord (Down (..))
import Data.Ratio (Ratio, denominator, numerator, (%))
import Data.Semigroup qualified as Semi
import Data.Sequence (Seq)
import Data.Sequence qualified as Seq
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Encoding qualified as TEnc
import Data.Text.Lazy qualified as TL
import Data.Text.Lazy.Builder qualified as TB
import Data.Text.Lazy.Builder.Int qualified as BI
import Data.Text.Lazy.Builder.RealFloat qualified as BF
import Data.Vector (Vector)
import Data.Vector qualified as V
import Data.Version (Version, makeVersion, versionBranch)
import Data.Word (Word16, Word32, Word64, Word8)
import GHC.Float (double2Float, float2Double)
import GHC.Generics
import Numeric.Natural (Natural)
import YAML.Decode qualified as YD
import YAML.Encode qualified as YE
import YAML.Value qualified as YV
-- ---------------------------------------------------------------------------
-- Class
-- ---------------------------------------------------------------------------
class ToYAML a where
toYAML :: a -> YV.Value
default toYAML :: (Generic a, GToYAML (Rep a)) => a -> YV.Value
toYAML = gToYAML . from
class FromYAML a where
fromYAML :: YV.Value -> Either String a
default fromYAML :: (Generic a, GFromYAML (Rep a)) => YV.Value -> Either String a
fromYAML v = to <$> gFromYAML v
encodeYAML :: ToYAML a => a -> Text
encodeYAML = YE.encode . toYAML
encodeYAMLBS :: ToYAML a => a -> ByteString
encodeYAMLBS = YE.encodeBS . toYAML
decodeYAML :: FromYAML a => Text -> Either String a
decodeYAML t = YD.decode t >>= fromYAML
decodeYAMLBS :: FromYAML a => ByteString -> Either String a
decodeYAMLBS b = YD.decodeBS b >>= fromYAML
-- ---------------------------------------------------------------------------
-- Scalar instances
-- ---------------------------------------------------------------------------
instance ToYAML YV.Value where toYAML = id
instance FromYAML YV.Value where fromYAML = Right
instance ToYAML Text where
toYAML = YV.YString
instance FromYAML Text where
fromYAML v = case YV.unwrap v of
YV.YString t -> Right t
YV.YInt n -> Right (TL.toStrict (TB.toLazyText (BI.decimal n)))
YV.YFloat d -> Right (TL.toStrict (TB.toLazyText (BF.realFloat d)))
YV.YBool b -> Right (if b then "true" else "false")
YV.YNull -> Right T.empty
_ -> Left "FromYAML Text: expected scalar"
instance ToYAML Bool where toYAML = YV.YBool
instance FromYAML Bool where
fromYAML v = case YV.unwrap v of
YV.YBool b -> Right b
_ -> Left "FromYAML Bool: expected YBool"
intToYAML :: Integral a => a -> YV.Value
intToYAML = YV.YInt . fromIntegral
intFrom :: forall a. (Integral a, Bounded a) => YV.Value -> Either String a
intFrom v = case YV.unwrap v of
YV.YInt n
| n >= toInteger (minBound :: a), n <= toInteger (maxBound :: a) -> Right (fromInteger n)
| otherwise -> Left "FromYAML Int: integer out of range"
_ -> Left "FromYAML Int: expected YInt"
instance ToYAML Int where toYAML = intToYAML
instance FromYAML Int where fromYAML = intFrom
instance ToYAML Int8 where toYAML = intToYAML
instance FromYAML Int8 where fromYAML = intFrom
instance ToYAML Int16 where toYAML = intToYAML
instance FromYAML Int16 where fromYAML = intFrom
instance ToYAML Int32 where toYAML = intToYAML
instance FromYAML Int32 where fromYAML = intFrom
instance ToYAML Int64 where toYAML = intToYAML
instance FromYAML Int64 where fromYAML = intFrom
instance ToYAML Word where toYAML = intToYAML
instance FromYAML Word where fromYAML = intFrom
instance ToYAML Word8 where toYAML = intToYAML
instance FromYAML Word8 where fromYAML = intFrom
instance ToYAML Word16 where toYAML = intToYAML
instance FromYAML Word16 where fromYAML = intFrom
instance ToYAML Word32 where toYAML = intToYAML
instance FromYAML Word32 where fromYAML = intFrom
instance ToYAML Word64 where toYAML = intToYAML
instance FromYAML Word64 where fromYAML = intFrom
instance ToYAML Integer where toYAML = YV.YInt
instance FromYAML Integer where
fromYAML v = case YV.unwrap v of
YV.YInt n -> Right n
_ -> Left "FromYAML Integer: expected YInt"
instance ToYAML Natural where toYAML = YV.YInt . fromIntegral
instance FromYAML Natural where
fromYAML v = case YV.unwrap v of
YV.YInt n | n >= 0 -> Right (fromIntegral n)
_ -> Left "FromYAML Natural: expected non-negative YInt"
instance ToYAML Double where toYAML = YV.YFloat
instance FromYAML Double where
fromYAML v = case YV.unwrap v of
YV.YFloat d -> Right d
YV.YInt n -> Right (fromIntegral n)
_ -> Left "FromYAML Double: expected YFloat"
instance ToYAML Float where toYAML = YV.YFloat . float2Double
instance FromYAML Float where
fromYAML v = case YV.unwrap v of
YV.YFloat d -> Right (double2Float d)
YV.YInt n -> Right (fromIntegral n)
_ -> Left "FromYAML Float: expected YFloat"
instance ToYAML Char where
toYAML c = YV.YString (T.singleton c)
instance FromYAML Char where
fromYAML v = case YV.unwrap v of
YV.YString t | T.length t == 1 -> Right (T.head t)
_ -> Left "FromYAML Char: expected single-character YString"
instance ToYAML TL.Text where
toYAML = YV.YString . TL.toStrict
instance FromYAML TL.Text where
fromYAML v = TL.fromStrict <$> fromYAML v
{- | Valid UTF-8 bytes use a text scalar. Arbitrary binary data uses a sequence
of byte-valued integers, so this pure instance is total and round-trippable.
-}
instance ToYAML ByteString where
toYAML bytes = case TEnc.decodeUtf8' bytes of
Right text -> YV.YString text
Left _ -> YV.YSeq (V.generate (BS.length bytes) (YV.YInt . toInteger . BS.index bytes))
instance FromYAML ByteString where
fromYAML v = case YV.unwrap v of
YV.YString t -> Right (TEnc.encodeUtf8 t)
YV.YSeq bytes -> BS.pack . V.toList <$> V.mapM (intFrom @Word8) bytes
_ -> Left "FromYAML ByteString: expected text or a byte sequence"
instance ToYAML BSL.ByteString where
toYAML = toYAML . BSL.toStrict
instance FromYAML BSL.ByteString where
fromYAML v = BSL.fromStrict <$> fromYAML v
instance ToYAML () where
toYAML () = YV.YSeq V.empty
instance FromYAML () where
fromYAML v = case YV.unwrap v of
YV.YSeq xs | V.null xs -> Right ()
_ -> Left "FromYAML (): expected empty YSeq"
-- ---------------------------------------------------------------------------
-- Container instances
-- ---------------------------------------------------------------------------
instance ToYAML a => ToYAML [a] where
toYAML xs = YV.YSeq (V.fromList (map toYAML xs))
instance FromYAML a => FromYAML [a] where
fromYAML v = case YV.unwrap v of
YV.YSeq xs -> traverse fromYAML (V.toList xs)
_ -> Left "FromYAML [a]: expected YSeq"
instance ToYAML a => ToYAML (Vector a) where
toYAML xs = YV.YSeq (V.map toYAML xs)
instance FromYAML a => FromYAML (Vector a) where
fromYAML v = case YV.unwrap v of
YV.YSeq xs -> V.mapM fromYAML xs
_ -> Left "FromYAML Vector: expected YSeq"
instance ToYAML a => ToYAML (Maybe a) where
toYAML Nothing = YV.YNull
toYAML (Just x) = toYAML x
instance FromYAML a => FromYAML (Maybe a) where
fromYAML v = case YV.unwrap v of
YV.YNull -> Right Nothing
_ -> Just <$> fromYAML v
instance ToYAML a => ToYAML (NonEmpty a) where
toYAML = toYAML . NE.toList
instance FromYAML a => FromYAML (NonEmpty a) where
fromYAML v = do
xs <- fromYAML v
case xs of
[] -> Left "FromYAML NonEmpty: empty list"
(y : ys) -> Right (y :| ys)
instance (ToYAML a, ToYAML b) => ToYAML (Either a b) where
toYAML (Left x) = YV.YMap (V.singleton (YV.YString "Left", toYAML x))
toYAML (Right x) = YV.YMap (V.singleton (YV.YString "Right", toYAML x))
instance (FromYAML a, FromYAML b) => FromYAML (Either a b) where
fromYAML v = case YV.unwrap v of
YV.YMap kvs | V.length kvs == 1 -> case V.head kvs of
(k, vv) -> case YV.unwrap k of
YV.YString "Left" -> Left <$> fromYAML vv
YV.YString "Right" -> Right <$> fromYAML vv
_ -> Left "FromYAML Either: expected Left/Right key"
_ -> Left "FromYAML Either: expected single-key YMap"
instance ToYAML a => ToYAML (Set a) where
toYAML = YV.YSeq . V.fromList . fmap toYAML . Set.toList
instance (Ord a, FromYAML a) => FromYAML (Set a) where
fromYAML v = Set.fromList <$> fromYAML v
instance ToYAML a => ToYAML (Seq a) where
toYAML s = YV.YSeq (V.fromList (fmap toYAML (foldr (:) [] s)))
instance FromYAML a => FromYAML (Seq a) where
fromYAML v = Seq.fromList <$> fromYAML v
instance ToYAML v => ToYAML (Map Text v) where
toYAML m = YV.YMap (V.fromList (fmap mkPair (Map.toList m)))
where
mkPair (k, val) = (YV.YString k, toYAML val)
instance FromYAML v => FromYAML (Map Text v) where
fromYAML v = case YV.unwrap v of
YV.YMap kvs -> do
V.foldM decodePair Map.empty kvs
where
decodePair values (k, val) = case YV.unwrap k of
YV.YString s -> Map.alterF (\case Nothing -> Just <$> fromYAML val; Just _ -> Left "FromYAML Map: duplicate text key after unwrapping tags") s values
_ -> Left "FromYAML (Map Text v): non-string key"
_ -> Left "FromYAML (Map Text v): expected YMap"
instance ToYAML v => ToYAML (IntMap v) where
toYAML m = YV.YMap (V.fromList (fmap mkPair (IntMap.toList m)))
where
mkPair (k, val) = (YV.YInt (fromIntegral k), toYAML val)
instance FromYAML v => FromYAML (IntMap v) where
fromYAML v = case YV.unwrap v of
YV.YMap kvs -> do
V.foldM decodePair IntMap.empty kvs
where
decodePair values (k, val) = case YV.unwrap k of
YV.YInt _ -> do
key <- fromYAML k :: Either String Int
IntMap.alterF (\case Nothing -> Just <$> fromYAML val; Just _ -> Left "FromYAML IntMap: duplicate integer key") key values
_ -> Left "FromYAML IntMap: non-integer key"
_ -> Left "FromYAML IntMap: expected YMap"
instance ToYAML IntSet where
toYAML = YV.YSeq . V.fromList . fmap toYAML . IntSet.toList
instance FromYAML IntSet where
fromYAML v = IntSet.fromList <$> fromYAML v
-- ---------------------------------------------------------------------------
-- Tuples
-- ---------------------------------------------------------------------------
instance (ToYAML a, ToYAML b) => ToYAML (a, b) where
toYAML (a, b) = YV.YSeq (V.fromList [toYAML a, toYAML b])
instance (FromYAML a, FromYAML b) => FromYAML (a, b) where
fromYAML v = case YV.unwrap v of
YV.YSeq xs
| V.length xs == 2 ->
(,) <$> fromYAML (xs V.! 0) <*> fromYAML (xs V.! 1)
_ -> Left "FromYAML (a,b): expected YSeq of length 2"
instance (ToYAML a, ToYAML b, ToYAML c) => ToYAML (a, b, c) where
toYAML (a, b, c) = YV.YSeq (V.fromList [toYAML a, toYAML b, toYAML c])
instance (FromYAML a, FromYAML b, FromYAML c) => FromYAML (a, b, c) where
fromYAML v = case YV.unwrap v of
YV.YSeq xs
| V.length xs == 3 ->
(,,) <$> fromYAML (xs V.! 0) <*> fromYAML (xs V.! 1) <*> fromYAML (xs V.! 2)
_ -> Left "FromYAML (a,b,c): expected YSeq of length 3"
instance (ToYAML a, ToYAML b, ToYAML c, ToYAML d) => ToYAML (a, b, c, d) where
toYAML (a, b, c, d) =
YV.YSeq (V.fromList [toYAML a, toYAML b, toYAML c, toYAML d])
instance (FromYAML a, FromYAML b, FromYAML c, FromYAML d) => FromYAML (a, b, c, d) where
fromYAML v = case YV.unwrap v of
YV.YSeq xs
| V.length xs == 4 ->
(,,,)
<$> fromYAML (xs V.! 0)
<*> fromYAML (xs V.! 1)
<*> fromYAML (xs V.! 2)
<*> fromYAML (xs V.! 3)
_ -> Left "FromYAML (a,b,c,d): expected YSeq of length 4"
-- ---------------------------------------------------------------------------
-- Functor / monoid newtypes
-- ---------------------------------------------------------------------------
instance ToYAML a => ToYAML (Identity a) where toYAML (Identity x) = toYAML x
instance FromYAML a => FromYAML (Identity a) where
fromYAML v = Identity <$> fromYAML v
instance ToYAML a => ToYAML (Const a b) where toYAML (Const x) = toYAML x
instance FromYAML a => FromYAML (Const a b) where
fromYAML v = Const <$> fromYAML v
instance ToYAML a => ToYAML (Down a) where toYAML (Down x) = toYAML x
instance FromYAML a => FromYAML (Down a) where
fromYAML v = Down <$> fromYAML v
instance ToYAML Version where toYAML = toYAML . versionBranch
instance FromYAML Version where fromYAML v = makeVersion <$> fromYAML v
instance ToYAML a => ToYAML (Ratio a) where
toYAML r = YV.YSeq (V.fromList [toYAML (numerator r), toYAML (denominator r)])
instance (Integral a, FromYAML a) => FromYAML (Ratio a) where
fromYAML v = case YV.unwrap v of
YV.YSeq xs | V.length xs == 2 -> do
n <- fromYAML (xs V.! 0)
d <- fromYAML (xs V.! 1)
if d == 0
then Left "FromYAML Ratio: zero denominator"
else Right (n % d)
_ -> Left "FromYAML Ratio: expected YSeq of length 2"
instance ToYAML a => ToYAML (Mon.Sum a) where toYAML = toYAML . Mon.getSum
instance FromYAML a => FromYAML (Mon.Sum a) where
fromYAML v = Mon.Sum <$> fromYAML v
instance ToYAML a => ToYAML (Mon.Product a) where toYAML = toYAML . Mon.getProduct
instance FromYAML a => FromYAML (Mon.Product a) where
fromYAML v = Mon.Product <$> fromYAML v
instance ToYAML a => ToYAML (Mon.Dual a) where toYAML = toYAML . Mon.getDual
instance FromYAML a => FromYAML (Mon.Dual a) where
fromYAML v = Mon.Dual <$> fromYAML v
instance ToYAML Mon.All where toYAML = toYAML . Mon.getAll
instance FromYAML Mon.All where fromYAML v = Mon.All <$> fromYAML v
instance ToYAML Mon.Any where toYAML = toYAML . Mon.getAny
instance FromYAML Mon.Any where fromYAML v = Mon.Any <$> fromYAML v
instance ToYAML a => ToYAML (Mon.First a) where toYAML = toYAML . Mon.getFirst
instance FromYAML a => FromYAML (Mon.First a) where
fromYAML v = Mon.First <$> fromYAML v
instance ToYAML a => ToYAML (Mon.Last a) where toYAML = toYAML . Mon.getLast
instance FromYAML a => FromYAML (Mon.Last a) where
fromYAML v = Mon.Last <$> fromYAML v
instance ToYAML a => ToYAML (Semi.Min a) where toYAML = toYAML . Semi.getMin
instance FromYAML a => FromYAML (Semi.Min a) where
fromYAML v = Semi.Min <$> fromYAML v
instance ToYAML a => ToYAML (Semi.Max a) where toYAML = toYAML . Semi.getMax
instance FromYAML a => FromYAML (Semi.Max a) where
fromYAML v = Semi.Max <$> fromYAML v
instance ToYAML a => ToYAML (Semi.First a) where toYAML = toYAML . Semi.getFirst
instance FromYAML a => FromYAML (Semi.First a) where
fromYAML v = Semi.First <$> fromYAML v
instance ToYAML a => ToYAML (Semi.Last a) where toYAML = toYAML . Semi.getLast
instance FromYAML a => FromYAML (Semi.Last a) where
fromYAML v = Semi.Last <$> fromYAML v
instance ToYAML a => ToYAML (Semi.WrappedMonoid a) where
toYAML = toYAML . Semi.unwrapMonoid
instance FromYAML a => FromYAML (Semi.WrappedMonoid a) where
fromYAML v = Semi.WrapMonoid <$> fromYAML v
instance (ToYAML a, ToYAML b) => ToYAML (Semi.Arg a b) where
toYAML (Semi.Arg a b) = YV.YSeq (V.fromList [toYAML a, toYAML b])
instance (FromYAML a, FromYAML b) => FromYAML (Semi.Arg a b) where
fromYAML v = case YV.unwrap v of
YV.YSeq xs
| V.length xs == 2 ->
Semi.Arg <$> fromYAML (xs V.! 0) <*> fromYAML (xs V.! 1)
_ -> Left "FromYAML Arg: expected YSeq of length 2"
instance ToYAML (f (g a)) => ToYAML (Compose f g a) where
toYAML = toYAML . getCompose
instance FromYAML (f (g a)) => FromYAML (Compose f g a) where
fromYAML v = Compose <$> fromYAML v
instance (ToYAML (f a), ToYAML (g a)) => ToYAML (FProduct.Product f g a) where
toYAML (FProduct.Pair x y) = YV.YSeq (V.fromList [toYAML x, toYAML y])
instance (FromYAML (f a), FromYAML (g a)) => FromYAML (FProduct.Product f g a) where
fromYAML v = case YV.unwrap v of
YV.YSeq xs
| V.length xs == 2 ->
FProduct.Pair <$> fromYAML (xs V.! 0) <*> fromYAML (xs V.! 1)
_ -> Left "FromYAML Functor.Product: expected YSeq of length 2"
instance (ToYAML (f a), ToYAML (g a)) => ToYAML (FSum.Sum f g a) where
toYAML (FSum.InL x) = YV.YMap (V.singleton (YV.YString "InL", toYAML x))
toYAML (FSum.InR x) = YV.YMap (V.singleton (YV.YString "InR", toYAML x))
instance (FromYAML (f a), FromYAML (g a)) => FromYAML (FSum.Sum f g a) where
fromYAML v = case YV.unwrap v of
YV.YMap kvs | V.length kvs == 1 -> case V.head kvs of
(k, val) -> case YV.unwrap k of
YV.YString "InL" -> FSum.InL <$> fromYAML val
YV.YString "InR" -> FSum.InR <$> fromYAML val
_ -> Left "FromYAML Functor.Sum: expected InL/InR key"
_ -> Left "FromYAML Functor.Sum: expected single-key YMap"
-- ---------------------------------------------------------------------------
-- GHC.Generics support
-- ---------------------------------------------------------------------------
class GToYAML f where
gToYAML :: f p -> YV.Value
class GFromYAML f where
gFromYAML :: YV.Value -> Either String (f p)
instance GToYAML f => GToYAML (M1 D c f) where
gToYAML (M1 x) = gToYAML x
instance GFromYAML f => GFromYAML (M1 D c f) where
gFromYAML v = M1 <$> gFromYAML v
instance GToYAMLFields f => GToYAML (M1 C c f) where
gToYAML (M1 x) = YV.YMap (V.fromList (gToYAMLFields x))
instance GFromYAMLFields f => GFromYAML (M1 C c f) where
gFromYAML v = case YV.unwrap v of
YV.YMap kvs -> do
validateFieldNames kvs
let lkup nm = lookupField nm kvs
M1 <$> gFromYAMLFields lkup
_ -> Left "GFromYAML: expected YMap for record type"
{- | Tags can distinguish YAML mapping keys whose native Text field names are
equal. Reject that ambiguity before a record decoder chooses a field.
-}
validateFieldNames :: Vector (YV.Value, YV.Value) -> Either String ()
validateFieldNames fields = () <$ V.foldM add HS.empty fields
where
add names (key, _) = case YV.unwrap key of
YV.YString name
| HS.member name names -> Left "FromYAML: duplicate field name after unwrapping tags"
| otherwise -> Right (HS.insert name names)
_ -> Right names
lookupField :: Text -> Vector (YV.Value, YV.Value) -> Maybe YV.Value
lookupField nm kvs = go 0
where
!len = V.length kvs
go !i
| i >= len = Nothing
| otherwise = case kvs V.! i of
(k, val) -> case YV.unwrap k of
YV.YString s | s == nm -> Just val
_ -> go (i + 1)
class GToYAMLFields f where
gToYAMLFields :: f p -> [(YV.Value, YV.Value)]
class GFromYAMLFields f where
gFromYAMLFields :: (Text -> Maybe YV.Value) -> Either String (f p)
instance (GToYAMLFields a, GToYAMLFields b) => GToYAMLFields (a :*: b) where
gToYAMLFields (a :*: b) = gToYAMLFields a ++ gToYAMLFields b
instance (GFromYAMLFields a, GFromYAMLFields b) => GFromYAMLFields (a :*: b) where
gFromYAMLFields lkup = (:*:) <$> gFromYAMLFields lkup <*> gFromYAMLFields lkup
instance (Selector s, ToYAML a) => GToYAMLFields (M1 S s (K1 i a)) where
gToYAMLFields m@(M1 (K1 x)) = [(YV.YString (T.pack (selName m)), toYAML x)]
instance (Selector s, FromYAML a) => GFromYAMLFields (M1 S s (K1 i a)) where
gFromYAMLFields lkup =
let nm = T.pack (selName (undefined :: M1 S s (K1 i a) p))
in case lkup nm of
Nothing -> Left $ "GFromYAML: missing field " ++ T.unpack nm
Just v -> M1 . K1 <$> fromYAML v