packages feed

telescope-0.2.0: src/Telescope/Asdf/Class.hs

{-# LANGUAGE AllowAmbiguousTypes #-}

module Telescope.Asdf.Class where

import Data.List ((!?))
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NE
import Data.Massiv.Array (Array, Prim)
import Data.Massiv.Array qualified as M
import Data.Scientific (fromFloatDigits, toRealFloat)
import Data.Text (Text, pack, unpack)
import Data.Time.Clock (UTCTime)
import Data.Time.Format.ISO8601
import Effectful
import Effectful.Fail
import GHC.Generics
import GHC.Int
import Telescope.Asdf.Encoding.File (BlockSource (..))
import Telescope.Asdf.NDArray
import Telescope.Asdf.Node
import Telescope.Data.Array
import Telescope.Data.Axes
import Telescope.Data.Binary
import Telescope.Data.Parser


{- | Convert a type to an Asdf 'Value' or 'Node'. The generic instance will encode to an 'Object' with field names matching record selectors

> data Example = Example
>   { name :: Text
>   , age :: Int
>   , tags :: [Text]
>   }
>   deriving (Generic, ToAsdf)
>
> instance ToAsdf Example where
>   schema _ = "tag:example.org/schemas/example-1.0.0"
-}
class ToAsdf a where
  -- | Specify how an object encodes to a 'Value'
  --
  -- > instance User ToAsdf where
  -- >   toValue user =
  -- >     Object
  -- >       [ ("name", toNode user.name)
  -- >       , ("age", toNode user.age)
  -- >       ]
  toValue :: a -> Value
  default toValue :: (Generic a, GToObject (Rep a)) => a -> Value
  toValue a = Object $ gToObject (from a)


  -- | Specify the schema for a type
  --
  -- > instance ToAsdf Unit where
  -- >   schema _ = "!unit/unit-1.0.0"
  schema :: a -> SchemaTag
  default schema :: a -> SchemaTag
  schema _ = mempty


  -- | Specify that this node be saved as an anchor
  --
  -- > instance ToAsdf Config where
  -- >   anchor _ = Just "globalConfig"
  anchor :: a -> Maybe Anchor
  default anchor :: a -> Maybe Anchor
  anchor _ = Nothing


  -- | Manually control all aspects of how this is converted to a 'Node'
  toNode :: a -> Node
  default toNode :: a -> Node
  toNode a = Node (schema a) (anchor a) $ toValue a


{- | Parse an Asdf 'Value' or 'Node' into a type. The generic instance will decode an 'Object' with field names matching record selectors

> data Example = Example
>   { name :: Text
>   , age :: Int
>   , tags :: [Text]
>   }
>   deriving (Generic, FromAsdf)
-}
class FromAsdf a where
  -- | Specify how a type is parsed from a 'Value'
  --
  -- > instance FromAsdf Integer where
  -- >   parseValue = \case
  -- >     Integer n -> pure $ fromIntegral n
  -- >     node -> expected "Integer" node
  parseValue :: (Parser :> es) => Value -> Eff es a
  default parseValue :: (Generic a, GParseObject (Rep a), Parser :> es) => Value -> Eff es a
  parseValue (Object o) = to <$> gParseObject o
  parseValue val = expected "Object" val


instance ToAsdf Int where
  toValue n = toValue (fromIntegral @Int @Int64 n)
instance FromAsdf Int where
  parseValue = fmap (fromIntegral @Int64 @Int) <$> parseValue


instance ToAsdf Int8 where
  toValue n = Integer $ fromIntegral n
instance FromAsdf Int8 where
  parseValue = parseInteger


instance ToAsdf Int16 where
  toValue n = Integer $ fromIntegral n
instance FromAsdf Int16 where
  parseValue = parseInteger


instance ToAsdf Int32 where
  toValue n = Integer $ fromIntegral n
instance FromAsdf Int32 where
  parseValue = parseInteger


instance ToAsdf Int64 where
  toValue n = Integer $ fromIntegral n
instance FromAsdf Int64 where
  parseValue = parseInteger


instance ToAsdf Integer where
  toValue n = Integer $ fromIntegral n
instance FromAsdf Integer where
  parseValue = parseInteger


instance ToAsdf Double where
  toValue n = Number $ fromFloatDigits n
instance FromAsdf Double where
  parseValue = \case
    Number n -> pure $ toRealFloat n
    node -> expected "Double" node


instance ToAsdf Float where
  toValue n = Number $ fromFloatDigits n
instance FromAsdf Float where
  parseValue = \case
    Number n -> pure $ toRealFloat n
    node -> expected "Float" node


parseInteger :: (Integral a, Parser :> es) => Value -> Eff es a
parseInteger = \case
  Integer n -> pure $ fromIntegral n
  node -> expected "Integer" node


instance {-# OVERLAPPABLE #-} (FromAsdf a) => FromAsdf [a] where
  parseValue = \case
    Array ns -> mapM (parseNode @a) ns
    node -> expected "Array" node
instance {-# OVERLAPPABLE #-} (ToAsdf a) => ToAsdf [a] where
  toValue as = Array $ fmap toNode as
instance {-# OVERLAPPABLE #-} (FromAsdf a) => FromAsdf (NonEmpty a) where
  parseValue val = do
    as <- parseValue @[a] val
    case as of
      [] -> expected "NonEmpty List" val
      (a : rest) -> pure (a :| rest)
instance {-# OVERLAPPABLE #-} (ToAsdf a) => ToAsdf (NonEmpty a) where
  toValue as = toValue $ NE.toList as


instance (ToAsdf a, ToAsdf b) => ToAsdf (a, b) where
  toValue (a, b) = Array [toNode a, toNode b]
instance (FromAsdf a, FromAsdf b) => FromAsdf (a, b) where
  parseValue = \case
    Array [n1, n2] -> do
      a <- parseValue n1.value
      b <- parseValue n2.value
      pure (a, b)
    node -> expected "[a, b]" node


instance (ToAsdf a, ToAsdf b, ToAsdf c) => ToAsdf (a, b, c) where
  toValue (a, b, c) = Array [toNode a, toNode b, toNode c]
instance (FromAsdf a, FromAsdf b, FromAsdf c) => FromAsdf (a, b, c) where
  parseValue = \case
    Array [na, nb, nc] -> do
      a <- parseValue na.value
      b <- parseValue nb.value
      c <- parseValue nc.value
      pure (a, b, c)
    node -> expected "[a, b, c]" node


instance (ToAsdf a, ToAsdf b, ToAsdf c, ToAsdf d) => ToAsdf (a, b, c, d) where
  toValue (a, b, c, d) = Array [toNode a, toNode b, toNode c, toNode d]
instance (FromAsdf a, FromAsdf b, FromAsdf c, FromAsdf d) => FromAsdf (a, b, c, d) where
  parseValue = \case
    Array [na, nb, nc, nd] -> do
      a <- parseValue na.value
      b <- parseValue nb.value
      c <- parseValue nc.value
      d <- parseValue nd.value
      pure (a, b, c, d)
    node -> expected "[a, b, c, d]" node


-- they will always serialize to Array
instance FromAsdf [Text] where
  parseValue = parseAnyList
instance FromAsdf [Int] where
  parseValue = parseAnyList
instance FromAsdf [Int8] where
  parseValue = parseAnyList
instance FromAsdf [Int16] where
  parseValue = parseAnyList
instance FromAsdf [Int32] where
  parseValue = parseAnyList
instance FromAsdf [Int64] where
  parseValue = parseAnyList
instance FromAsdf [Double] where
  parseValue = parseAnyList


-- | Flexibly parse lists from either Array or NDArray
parseAnyList :: (FromAsdf a, FromNDArray [a], Parser :> es) => Value -> Eff es [a]
parseAnyList = \case
  Array ns -> mapM parseNode ns
  NDArray dat -> fromNDArray dat
  node -> expected "[Double]" node


instance (FromAsdf a) => FromAsdf (Maybe a) where
  parseValue = \case
    Null -> pure Nothing
    val -> Just <$> parseValue @a val
instance (ToAsdf a) => ToAsdf (Maybe a) where
  schema = maybe mempty schema
  anchor = maybe Nothing anchor
  toValue Nothing = Null
  toValue (Just a) = toValue a


instance (BinaryValue a, Prim a, AxesIndex ix) => FromAsdf (Array M.D ix a) where
  parseValue = \case
    NDArray a -> fromNDArray a
    node -> expected "NDArray" node
instance (BinaryValue a, IsDataType a, Prim a, AxesIndex ix, PutArray ix) => ToAsdf (Array M.D ix a) where
  toValue as = NDArray $ ndArrayMassiv as


instance ToAsdf Text where
  toValue = String
instance FromAsdf Text where
  parseValue = \case
    String t -> pure t
    node -> expected "Text" node


instance ToAsdf String where
  toValue = String . pack
instance FromAsdf String where
  parseValue = \case
    String t -> pure $ unpack t
    node -> expected "Text" node


instance ToAsdf Bool where
  toValue = Bool
instance FromAsdf Bool where
  parseValue = \case
    Bool b -> pure b
    node -> expected "Bool" node


instance ToAsdf Value where
  toValue = id
instance FromAsdf Value where
  parseValue = pure


instance ToAsdf Node where
  toValue (Node _ _ val) = val
instance FromAsdf Node where
  parseValue val = pure $ Node mempty Nothing val


instance ToAsdf Tree where
  toValue (Tree o) = Object o
instance FromAsdf Tree where
  parseValue = \case
    Object o -> pure $ Tree o
    val -> expected "Object" val


instance ToAsdf NDArrayData where
  toValue = NDArray
instance FromAsdf NDArrayData where
  parseValue = \case
    NDArray nda -> pure nda
    node -> expected "NDArray" node


instance ToAsdf DataType where
  toValue Float64 = "float64"
  toValue Float32 = "float32"
  toValue Int64 = "int64"
  toValue Int32 = "int32"
  toValue Int16 = "int16"
  toValue Int8 = "int8"
  toValue Bool8 = "bool8"
  toValue (Ucs4 n) = Array ["ucs4", fromValue $ Integer $ fromIntegral n]
instance FromAsdf DataType where
  parseValue = \case
    String "float64" -> pure Float64
    String "float32" -> pure Float32
    String "int64" -> pure Int64
    String "int32" -> pure Int32
    String "int16" -> pure Int16
    String "int8" -> pure Int8
    String "bool8" -> pure Bool8
    Array ["ucs4", Node _ _ (Integer n)] -> pure $ Ucs4 $ fromIntegral n
    val -> expected "DataType" val


instance ToAsdf ByteOrder where
  toValue = \case
    BigEndian -> "big"
    LittleEndian -> "little"
instance FromAsdf ByteOrder where
  parseValue = \case
    String "big" -> pure BigEndian
    String "little" -> pure LittleEndian
    node -> expected "ByteOrder" node


instance ToAsdf (Axes Row) where
  toValue (Axes as) = toValue as


instance ToAsdf BlockSource where
  toValue (BlockSource s) = toValue s


instance ToAsdf UTCTime where
  toValue t = String $ pack $ iso8601Show t
instance FromAsdf UTCTime where
  parseValue v = do
    ts <- parseValue @String v
    res <- runFail $ iso8601ParseM ts
    case res of
      Left e -> parseFail e
      Right a -> pure a


-- | Parse a node, ignoring the schema tag
parseNode :: (FromAsdf a, Parser :> es) => Node -> Eff es a
parseNode (Node _ _ v) = parseValue v


{- | Parse a key from an 'Object'

> instance FromAsdf User where
>   parseValue = \case
>     Object o -> do
>       name <- o .: "name"
>       age <- o .: "age"
>       pure $ User{name, age}
-}
(.:) :: (FromAsdf a, Parser :> es) => Object -> Key -> Eff es a
o .: k = do
  case lookup k o of
    Nothing -> parseFail $ "key " ++ show k ++ " not found"
    Just node ->
      parseAt (Child k) $ parseNode node


-- | Parse an optional key from an 'Object'
(.:?) :: (FromAsdf a, Parser :> es) => Object -> Key -> Eff es (Maybe a)
o .:? k = do
  case lookup k o of
    Nothing -> pure Nothing
    Just a ->
      Just <$> do
        parseAt (Child k) $ parseNode a


{- | Parse a child at the given array index
 -
> instance FromAsdf Friends where
>   parseValue = \case
>     Array ns -> do
>       best <- ns ! 0
>       second <- ns ! 1
>       other <- mapM parseNode ns
>       pure $ Friends{best, second, other}
-}
(!) :: (FromAsdf a, Parser :> es) => [Node] -> Int -> Eff es a
ns ! n = do
  case ns !? n of
    Nothing -> parseFail $ "Index " ++ show n ++ " not found"
    Just node ->
      parseAt (Index n) $ parseNode node


-- | Generically serialize records to an 'Object'
class GToObject f where
  gToObject :: f p -> Object


instance (GToObject f) => GToObject (M1 D c f) where
  gToObject (M1 f) = gToObject f


instance (GToObject f) => GToObject (M1 C c f) where
  gToObject (M1 f) = gToObject f


instance (GToObject f, GToObject g) => GToObject (f :*: g) where
  gToObject (f :*: g) = gToObject f <> gToObject g


instance (GToNode f, Selector s) => GToObject (M1 S s f) where
  gToObject (M1 f) =
    let s = selName (undefined :: M1 S s f p)
     in [(pack s, gToNode f)]


-- | Generically serialize record values to a 'Node'
class GToNode f where
  gToNode :: f p -> Node


instance {-# OVERLAPPABLE #-} (ToAsdf a) => GToNode (K1 R a) where
  gToNode (K1 a) = toNode a


instance {-# OVERLAPPING #-} (ToAsdf a) => GToNode (K1 R (Maybe a)) where
  gToNode (K1 a) = toNode a


-- | Generically parse 'Object's into records
class GParseObject f where
  gParseObject :: (Parser :> es) => Object -> Eff es (f p)


instance (GParseObject f) => GParseObject (M1 D c f) where
  gParseObject o = M1 <$> gParseObject o


instance (GParseObject f) => GParseObject (M1 C c f) where
  gParseObject o = M1 <$> gParseObject o


instance (GParseObject f, GParseObject g) => GParseObject (f :*: g) where
  gParseObject o = do
    f <- gParseObject o
    g <- gParseObject o
    pure $ f :*: g


instance (GParseKey f, Selector s) => GParseObject (M1 S s f) where
  gParseObject o = do
    let k = pack $ selName (undefined :: M1 S s f p)
    M1 <$> gParseKey o k


-- | Generically parse a key from an 'Object' into a record value
class GParseKey f where
  gParseKey :: (Parser :> es) => Object -> Key -> Eff es (f p)


instance {-# OVERLAPPABLE #-} (FromAsdf a) => GParseKey (K1 R a) where
  gParseKey o k = K1 <$> o .: k


instance {-# OVERLAPPABLE #-} (FromAsdf a) => GParseKey (K1 R (Maybe a)) where
  gParseKey o k = K1 <$> o .:? k