-- | This module provides some tools to encode multiple versions -- of a data model in a single data-type parametrized by version number. -- The addition or removal of a field can be expressed through the -- 'Since' and 'Until' type families. -- -- Example: -- -- > data Rec v = Rec -- > { foo :: Int -- this field exists in all versions -- > , bar :: Since V2 v Bool -- this field has been introduced in V2 -- > , baz :: Until V2 v Double -- this field has been removed in V3 -- > } -- -- Besides reducing the number of data declarations, -- this approach also has other advantages: -- -- * It makes migrations declarative and self-documenting. -- -- * It allows for less verbose version-upcasting functions, -- since the fields that have a non-parametric type do not need to be copied. -- -- * It is a foundation on which other useful abstractions can be built. -- -- Please note that some classes may require a separate standalone deriving clause -- for each version of a data-type or some kind of inductive deriving mechanism. {-# LANGUAGE DataKinds #-} {-# LANGUAGE ExplicitNamespaces #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} module Versioning.Base ( V(..) , GetV , versionNumber , Since , Until , NA , na , V1 , V2 , V3 , V4 , V5 , V6 , V7 , V8 , V9 , V10 , V11 , V12 , V13 , V14 , V15 , V16 , V17 , V18 , V19 , V20 ) where import Data.Proxy (Proxy (..)) import GHC.TypeNats (type (-), KnownNat, Nat, natVal) import Numeric.Natural (Natural) import Versioning.Internal.Base (Bare) -- | The version of a data model newtype V = V Nat -- | Get the type-level natural of a version type family GetV (v :: V) :: Nat where GetV ('V n) = n -- | Get the version number of a versioned value versionNumber :: forall a v. KnownNat (GetV v) => a v -> Natural versionNumber _ = natVal (Proxy :: Proxy (GetV v)) -- | This allows us to express that a field is only present since -- a given version. -- The first parameter is the version in which the field has been introduced, -- the second parameter is the actual version of the data-type. type family Since (s :: V) (v :: V) a :: * where Since ('V 1) ('V v) a = a Since ('V s) ('V 1) a = NA Since ('V s) ('V v) a = Since ('V (s - 1)) ('V (v - 1)) a -- | This allows us to express that a field is only present until -- a given version. -- The first parameter is the last version in which the field is present, -- the second parameter is the actual version of the data-type. type family Until (u :: V) (v :: V) a :: * where Until ('V u) ('V 1) a = a Until ('V 1) ('V v) a = NA Until ('V u) ('V v) a = Until ('V (u - 1)) ('V (v - 1)) a -- | A type indicating absence. -- The 'Maybe' is a hack needed to let aeson parse a record successfully even -- if a field of type 'NA' is missing. -- -- Ideally we would like to define it as -- -- > data NA = NA -- -- but this would not work with 'FromJSON' instances that are derived -- with Generic. type NA = Maybe Bare -- | A placeholder for an absent value. na :: NA na = Nothing -- Handy type synonyms to minimize parenthesis type V1 = 'V 1 type V2 = 'V 2 type V3 = 'V 3 type V4 = 'V 4 type V5 = 'V 5 type V6 = 'V 6 type V7 = 'V 7 type V8 = 'V 8 type V9 = 'V 9 type V10 = 'V 10 type V11 = 'V 11 type V12 = 'V 12 type V13 = 'V 13 type V14 = 'V 14 type V15 = 'V 15 type V16 = 'V 16 type V17 = 'V 17 type V18 = 'V 18 type V19 = 'V 19 type V20 = 'V 20