packages feed

versioning-0.1.0.0: src/Versioning/JSON.hs

{-# LANGUAGE AllowAmbiguousTypes   #-}
{-# LANGUAGE ConstraintKinds       #-}
{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes            #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TypeApplications      #-}
{-# LANGUAGE TypeFamilies          #-}
{-# LANGUAGE TypeInType            #-}
{-# LANGUAGE TypeOperators         #-}
{-# LANGUAGE UndecidableInstances  #-}
module Versioning.JSON
  ( Applied
  , DecodeAnyVersion
  , WithAnyVersion
  , decodeAnyVersion
  , withAnyVersion
  , withAnyVersionM
  )
where

import           Control.Applicative   ((<|>))
import           Data.Aeson            (FromJSON, decode)
import qualified Data.ByteString.Lazy  as LazyBS
import           Data.Functor.Identity (Identity (..))
import           Data.Kind             (Constraint, Type)
import           GHC.TypeLits

import           Versioning.Base
import           Versioning.Upgrade

-- | Decode a JSON string by trying all the versions decrementally
--   and upgrade the decoded object to the newest version.
decodeAnyVersion
  :: forall v a . DecodeAnyVersion v v a => LazyBS.ByteString -> Maybe (a v)
decodeAnyVersion = decodeAnyVersion' @v @v

-- | Decode a JSON string by trying all the versions decrementally
--   and apply an action to the decoded object at its original version.
withAnyVersionM
  :: forall c v a m
   . (WithAnyVersion v a c, Applicative m, c a v)
  => ApplyM m a c
  -> LazyBS.ByteString
  -> m (Maybe (Applied c a))
withAnyVersionM = withAnyVersion' @v @a @c

-- | Pure version of 'withAnyVersionM'.
withAnyVersion
  :: forall c v a
   . (WithAnyVersion v a c, c a v)
  => Apply a c
  -> LazyBS.ByteString
  -> Maybe (Applied c a)
withAnyVersion action = runIdentity . withAnyVersionM @c @v @a (Identity . action)

-- | The result type of the action that has been applied to the decoded object
--   with 'withAnyVersion'.
type family Applied (c :: (V -> Type) -> V -> Constraint) (a :: V -> Type) :: Type

type ApplyM m a c = forall v. c a v => a v -> m (Applied c a)

type Apply a c = forall v. c a v => a v -> Applied c a

class DecodeAnyVersion (v :: V) (w :: V) (a :: V -> Type) where
    decodeAnyVersion' :: LazyBS.ByteString -> Maybe (a w)

instance {-# OVERLAPPING #-} (FromJSON (a V1), Upgrade V1 w a) => DecodeAnyVersion V1 w a where
    decodeAnyVersion' bs = upgrade @V1 @w <$> decode @(a V1) bs

instance {-# OVERLAPPABLE #-} (DecodeAnyVersion (Decr v V1) w a, FromJSON (a v), FromJSON (a (Decr v V1)), Upgrade v w a)
  => DecodeAnyVersion v w a where
    decodeAnyVersion' bs = upgrade @v @w <$> decode @(a v) bs
                       <|> decodeAnyVersion' @(Decr v V1) @w bs

class WithAnyVersion (v :: V) (a :: V -> Type) c where
    withAnyVersion' :: (Applicative m, c a v) => ApplyM m a c -> LazyBS.ByteString -> m (Maybe (Applied c a))

instance {-# OVERLAPPING #-} (FromJSON (a V1), c a V1) => WithAnyVersion V1 a c where
    withAnyVersion' action bs = case decode @(a V1) bs of
        Just doc -> Just <$> action doc
        Nothing  -> pure Nothing

instance {-# OVERLAPPABLE #-} (WithAnyVersion (Decr v V1) a c, FromJSON (a v), FromJSON (a (Decr v V1)), c a v, c a (Decr v V1))
  => WithAnyVersion v a c where
    withAnyVersion' action bs = case decode @(a v) bs of
        Just doc -> Just <$> action doc
        Nothing  -> withAnyVersion' @(Decr v V1) @a @c action bs

-- | Decrement version until the target version is reached
type family Decr (c :: V) (v :: V) :: V where
    Decr ('V c) ('V v) = Decr' (CmpNat c v) ('V c)

type family Decr' (o :: Ordering) (c :: V) :: V where
    Decr' 'GT ('V c) = 'V (c - 1)
    Decr' 'EQ ('V c) = 'V c
    Decr' 'LT ('V c) = TypeError
                       ( 'Text "Cannot decrement "
                   ':<>: 'ShowType ('V c)
                   ':<>: 'Text " because the target is a higher version"
                       )