{-# LANGUAGE CPP #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_GHC -Wno-redundant-constraints #-}
module Data.SafeJSON.Internal where
import Control.Applicative ((<|>))
import Control.Monad (when)
import Control.Monad.Fail (MonadFail)
import Data.Aeson
import Data.Aeson.Types (Parser)
import Data.HashMap.Strict as HM (insert, size)
import Data.Int
import qualified Data.List as List (intercalate, lookup)
import Data.Maybe (fromMaybe, isJust, isNothing)
#if MIN_VERSION_base(4,11,0)
#else
import Data.Monoid ((<>))
#endif
import Data.Proxy
import qualified Data.Set as S
import Data.Text (Text)
import Data.Typeable (Typeable, typeRep)
import Test.Tasty.QuickCheck (Arbitrary(..), shrinkIntegral)
class (ToJSON a, FromJSON a) => SafeJSON a where
version :: Version a
version = 0
kind :: Kind a
kind = Base
safeTo :: a -> Contained Value
safeTo = contain . toJSON
safeFrom :: Value -> Contained (Parser a)
safeFrom = contain . parseJSON
typeName :: Proxy a -> String
default typeName :: Typeable a => Proxy a -> String
typeName = typeName0
internalConsistency :: Consistency a
internalConsistency = computeConsistency Proxy
objectProfile :: Profile a
objectProfile = mkProfile Proxy
{-# MINIMAL #-}
class SafeJSON (MigrateFrom a) => Migrate a where
type MigrateFrom a
migrate :: MigrateFrom a -> a
newtype Contained a = Contained {unsafeUnpack :: a}
contain :: a -> Contained a
contain = Contained
newtype Version a = Version {unVersion :: Maybe Int64}
deriving (Eq)
noVersion :: Version a
noVersion = Version Nothing
instance Show (Version a) where
show (Version mi) = "Version " ++ showV mi
liftV :: Integer -> (Int64 -> Int64 -> Int64) -> Maybe Int64 -> Maybe Int64 -> Maybe Int64
liftV _ _ Nothing Nothing = Nothing
liftV i f ma mb = Just $ toZ ma `f` toZ mb
where toZ = fromMaybe $ fromInteger i
instance Num (Version a) where
Version ma + Version mb = Version $ liftV 0 (+) ma mb
Version ma - Version mb = Version $ liftV 0 (-) ma mb
Version ma * Version mb = Version $ liftV 1 (*) ma mb
negate (Version ma) = Version $ negate <$> ma
abs (Version ma) = Version $ abs <$> ma
signum (Version ma) = Version $ signum <$> ma
fromInteger i = Version $ Just $ fromInteger i
instance Arbitrary (Version a) where
arbitrary = Version . Just <$> arbitrary
shrink (Version Nothing) = []
shrink (Version (Just a)) = Version . Just <$> shrinkIntegral a
castVersion :: Version a -> Version b
castVersion (Version i) = Version i
newtype Reverse a = Reverse { unReverse :: a }
data Kind a where
Base :: Kind a
Extends :: Migrate a => Proxy (MigrateFrom a) -> Kind a
Extended :: Migrate (Reverse a) => Kind a -> Kind a
base :: Kind a
base = Base
extension :: (SafeJSON a, Migrate a) => Kind a
extension = Extends Proxy
extended_base :: (SafeJSON a, Migrate (Reverse a)) => Kind a
extended_base = Extended base
extended_extension :: (SafeJSON a, Migrate a, Migrate (Reverse a)) => Kind a
extended_extension = Extended extension
versionField :: Text
versionField = "!v"
dataVersionField :: Text
dataVersionField = "~v"
dataField :: Text
dataField = "~d"
safeToJSON :: forall a. SafeJSON a => a -> Value
safeToJSON a = case thisKind of
Base | i == Nothing -> tojson
Extended Base | i == Nothing -> tojson
_ -> case tojson of
Object o -> Object $ HM.insert versionField (toJSON i) o
other -> object
[ dataVersionField .= i
, dataField .= other
]
where tojson = unsafeUnpack $ safeTo a
Version i = version :: Version a
thisKind = kind :: Kind a
safeFromJSON :: forall a. SafeJSON a => Value -> Parser a
safeFromJSON origVal = checkConsistency p $ \vs -> do
let hasVNil = noVersionPresent vs
case origKind of
Base | i == Nothing -> unsafeUnpack $ safeFrom origVal
Extended k | i == Nothing -> extendedCase hasVNil k
_ -> regularCase hasVNil
where Version i = version :: Version a
origKind = kind :: Kind a
p = Proxy :: Proxy a
safejsonErr s = fail $ "safejson: " ++ s
regularCase hasVNil = case origVal of
Object o -> do
(mVal, v) <- tryIt o
let val = fromMaybe origVal mVal
withVersion v val origKind
_ -> withoutVersion <|> safejsonErr ("unparsable JSON value (not an object): " ++ typeName p)
where withoutVersion = withVersion noVersion origVal origKind
tryIt o
| hasVNil = firstTry o <|> secondTry o <|> pure (Nothing, noVersion)
| otherwise = firstTry o <|> secondTry o
extendedCase :: Migrate (Reverse a) => Bool -> Kind a -> Parser a
extendedCase hasVNil k = case k of { Base -> go; _ -> regularCase hasVNil }
where go = case origVal of
Object o -> tryNew o <|> tryOrig
_ -> tryOrig
tryNew o = do
(mVal, v) <- firstTry o <|> secondTry o
let forwardKind = getForwardKind k
forwardVersion = castVersion v
val = fromMaybe origVal mVal
getForwardParser = withVersion forwardVersion val forwardKind
unReverse . migrate <$> getForwardParser
tryOrig = unsafeUnpack $ safeFrom origVal
withVersion :: forall b. SafeJSON b => Version b -> Value -> Kind b -> Parser b
withVersion v val k = either fail id eResult
where eResult = constructParserFromVersion val v k
firstTry o = do
v <- o .: versionField
return (Nothing, Version $ Just v)
secondTry o = do
v <- o .: dataVersionField
bd <- o .: dataField
when (HM.size o /= 2) $ fail $ "malformed simple data (" ++ show (Version $ Just v) ++ ")"
return (Just bd, Version $ Just v)
constructParserFromVersion :: SafeJSON a => Value -> Version a -> Kind a -> Either String (Parser a)
constructParserFromVersion val origVersion origKind =
worker False origVersion origKind
where
worker :: forall b. SafeJSON b => Bool -> Version b -> Kind b -> Either String (Parser b)
worker fwd thisVersion thisKind
| version == thisVersion = return $ unsafeUnpack $ safeFrom val
| otherwise = case thisKind of
Base -> Left versionNotFound
Extends p -> fmap migrate <$> worker fwd (castVersion thisVersion) (kindFromProxy p)
Extended k -> do
let forwardParser :: Either String (Parser b)
forwardParser = do
if castVersion thisVersion == versionFromProxy reverseProxy
then Right $ unReverse . migrate <$> unsafeUnpack (safeFrom val)
else previousParser
previousParser :: Either String (Parser b)
previousParser = worker True thisVersion k
if fwd || thisVersion == noVersion
then previousParser
else either (const previousParser) Right forwardParser
where versionNotFound = "Cannot find parser associated with: " <> show origVersion
reverseProxy :: Proxy (MigrateFrom (Reverse b))
reverseProxy = Proxy
typeName0 :: Typeable a => Proxy a -> String
typeName0 = show . typeRep
typeName1 :: forall t a. Typeable t => Proxy (t a) -> String
typeName1 _ = show $ typeRep (Proxy :: Proxy t)
typeName2 :: forall t a b. Typeable t => Proxy (t a b) -> String
typeName2 _ = show $ typeRep (Proxy :: Proxy t)
typeName3 :: forall t a b c. Typeable t => Proxy (t a b c) -> String
typeName3 _ = show $ typeRep (Proxy :: Proxy t)
typeName4 :: forall t a b c d. Typeable t => Proxy (t a b c d) -> String
typeName4 _ = show $ typeRep (Proxy :: Proxy t)
typeName5 :: forall t a b c d e. Typeable t => Proxy (t a b c d e) -> String
typeName5 _ = show $ typeRep (Proxy :: Proxy t)
data Profile a = InvalidProfile String
| Profile ProfileVersions
deriving (Eq)
data ProfileVersions = ProfileVersions {
profileCurrentVersion :: Maybe Int64,
profileSupportedVersions :: [(Maybe Int64, String)]
} deriving (Eq)
noVersionPresent :: ProfileVersions -> Bool
noVersionPresent (ProfileVersions c vs) =
isNothing c || isJust (Nothing `List.lookup` vs)
showV :: Maybe Int64 -> String
showV Nothing = "null"
showV (Just i) = show i
showVs :: [(Maybe Int64, String)] -> String
showVs = List.intercalate ", " . fmap go
where go (mi, s) = mconcat ["(", showV mi, ", ", s, ")"]
instance Show ProfileVersions where
show (ProfileVersions cur sup) = mconcat
[ "version ", showV cur, ": ["
, showVs sup, "]"
]
instance Typeable a => Show (Profile a) where
show (InvalidProfile s) = "InvalidProfile: " <> s
show (Profile pv) =
let p = Proxy :: Proxy a
in mconcat [ "Profile for \"", typeName0 p
, "\" (", show pv, ")"
]
mkProfile :: forall a. SafeJSON a => Proxy a -> Profile a
mkProfile p = case computeConsistency p of
NotConsistent t -> InvalidProfile t
Consistent -> Profile $ ProfileVersions {
profileCurrentVersion = unVersion (version @a),
profileSupportedVersions = availableVersions p
}
data Consistency a = Consistent
| NotConsistent String
checkConsistency :: (SafeJSON a, MonadFail m) => Proxy a -> (ProfileVersions -> m b) -> m b
checkConsistency p m =
case mkProfile p of
InvalidProfile s -> fail s
Profile vs -> m vs
computeConsistency :: forall a. SafeJSON a => Proxy a -> Consistency a
computeConsistency p
| isObviouslyConsistent (kind @a) = Consistent
| Just s <- invalidChain p = NotConsistent s
| otherwise = Consistent
{-# INLINE computeConsistency #-}
isObviouslyConsistent :: Kind a -> Bool
isObviouslyConsistent Base = True
isObviouslyConsistent _ = False
availableVersions :: forall a. SafeJSON a => Proxy a -> [(Maybe Int64, String)]
availableVersions _ =
worker False (kind @a)
where
worker :: forall b. SafeJSON b => Bool -> Kind b -> [(Maybe Int64, String)]
worker fwd thisKind = case thisKind of
Base -> [tup]
Extends p' -> tup : worker fwd (kindFromProxy p')
Extended k | not fwd -> worker True (getForwardKind k)
Extended k -> worker True k
where Version v = version @b
name = typeName (Proxy @b)
tup = (v, name)
invalidChain :: forall a. SafeJSON a => Proxy a -> Maybe String
invalidChain _ =
worker mempty mempty (kind @a)
where
worker :: forall b. SafeJSON b => S.Set (Maybe Int64) -> S.Set (Maybe Int64, String) -> Kind b -> Maybe String
worker vs vSs k
| i `S.member` vs = Just $ mconcat
[ "Double occurence of version number '", showV i
, "' (type: ", typeName p
, "). Looping instances if the previous combination of type and version number are found here: "
, showVs $ S.toList vSs
]
| otherwise = case k of
Base -> Nothing
Extends{} | i == Nothing -> Just $ mconcat
[ typeName p, " has defined 'version = noVersion', "
, " but it's 'kind' definition is not 'base' or 'extended_base'"
]
Extends a_proxy -> worker newVSet newVsSet (kindFromProxy a_proxy)
Extended a_kind -> let v@(Version i') = versionFromKind $ getForwardKind a_kind
tup = (i', typeName (proxyFromVersion v))
in worker (S.insert i' vs) (S.insert tup vSs) a_kind
where Version i = version @b
p = Proxy @b
newVSet = S.insert i vs
newVsSet = S.insert (i, typeName p) vSs
proxyFromVersion :: Version a -> Proxy a
proxyFromVersion _ = Proxy
kindFromProxy :: SafeJSON a => Proxy a -> Kind a
kindFromProxy _ = kind
versionFromProxy :: SafeJSON a => Proxy a -> Version a
versionFromProxy _ = version
versionFromKind :: SafeJSON a => Kind a -> Version a
versionFromKind _ = version
getForwardKind :: Migrate (Reverse a) => Kind a -> Kind (MigrateFrom (Reverse a))
getForwardKind _ = kind