{-# LANGUAGE GeneralizedNewtypeDeriving #-} {- | List of types used inside the Registry -} module Data.Registry.Internal.Types where import Data.Dynamic import Data.Hashable import Data.List (elemIndex, intersect) import Data.List.NonEmpty import Data.List.NonEmpty as NonEmpty (head, last) import Data.Registry.Internal.Reflection import Data.Text as T hiding (last) import Prelude (show) import Protolude as P hiding (show) import qualified Protolude as P import Type.Reflection -- | A 'Value' is the 'Dynamic' representation of a Haskell value + its description -- It is either provided by the user of the Registry or created as part of the -- resolution algorithm -- If a `Context` is present for a a created value this means that this value -- has been written as the result of a specialization. The first type of the -- list of types in the context is the types under which the specialization must -- apply and the other types are "parents" of the current value in the value -- graph data Value = CreatedValue Dynamic ValueDescription (Maybe Context) (Maybe Specialization) Dependencies | ProvidedValue Dynamic ValueDescription deriving (Show) instance Hashable Value where hash value = hash (valDescription value) hashWithSalt n value = hashWithSalt n (valDescription value) -- | Description of a value. It might just have -- a description for its type when it is a value -- created by the resolution algorithm data ValueDescription = ValueDescription { _valueType :: Text , _valueValue :: Maybe Text } deriving (Eq, Show) instance Hashable ValueDescription where hash (ValueDescription d v) = hash (d, v) hashWithSalt n (ValueDescription d v) = hashWithSalt n (d, v) -- | Describe a value with its type and actual content describeValue :: (Typeable a, Show a) => a -> ValueDescription describeValue a = ValueDescription (showFullValueType a) (Just . toS $ show a) -- | Describe a value with only its type describeTypeableValue :: (Typeable a) => a -> ValueDescription describeTypeableValue a = ValueDescription (showFullValueType a) Nothing -- | Show a Value from the 'Registry' showValue :: Value -> Text showValue = valDescriptionToText . valDescription -- | Create a Value from a Haskell value, using its Show instance for its description createValue :: (Typeable a, Show a) => a -> Value createValue a = makeProvidedValue (toDyn a) (describeValue a) -- | Make a ProvidedValue makeProvidedValue :: Dynamic -> ValueDescription -> Value makeProvidedValue = ProvidedValue -- | make a CreatedValue in no particular context makeCreatedValue :: Dynamic -> ValueDescription -> Dependencies -> Value makeCreatedValue d desc = CreatedValue d desc Nothing Nothing -- | Create a Value from a Haskell value, with only its 'Typeable' description createTypeableValue :: Typeable a => a -> Value createTypeableValue a = ProvidedValue (toDyn a) (describeTypeableValue a) -- | Create a Value from a 'Dynamic' value and some description createDynValue :: Dynamic -> Text -> Value createDynValue dyn desc = ProvidedValue dyn (ValueDescription desc Nothing) -- | Type representation of a 'Value' valueDynTypeRep :: Value -> SomeTypeRep valueDynTypeRep = dynTypeRep . valueDyn -- | Dynamic representation of a 'Value' valueDyn :: Value -> Dynamic valueDyn (CreatedValue d _ _ _ _) = d valueDyn (ProvidedValue d _) = d -- | The description for a 'Value' valDescription :: Value -> ValueDescription valDescription (CreatedValue _ d _ _ _ ) = d valDescription (ProvidedValue _ d) = d -- | The dependencies for a 'Value' valDependencies :: Value -> Dependencies valDependencies (CreatedValue _ _ _ _ ds) = ds valDependencies (ProvidedValue _ _) = mempty -- | A ValueDescription as 'Text'. If the actual content of the 'Value' -- is provided display the type first then the content valDescriptionToText :: ValueDescription -> Text valDescriptionToText (ValueDescription t Nothing) = t valDescriptionToText (ValueDescription t (Just v)) = t <> ": " <> v -- | Return the creation context for a given value when it was created -- as the result of a "specialization" specializationContext :: Value -> Maybe Context specializationContext (CreatedValue _ _ context _ _) = context specializationContext _ = Nothing -- | Return the specialization used to create a specific values usedSpecialization :: Value -> Maybe Specialization usedSpecialization (CreatedValue _ _ _ specialization _) = specialization usedSpecialization _ = Nothing -- | Return True if a type is part of the specialization context of a Value isInSpecializationContext :: SomeTypeRep -> Value -> Bool isInSpecializationContext target value = case specializationContext value of Just context -> target `elem` (contextTypes context) Nothing -> False -- | Return True if a value has transitives dependencies which are -- specialized values hasSpecializedDependencies :: Specializations -> Value -> Bool hasSpecializedDependencies (Specializations ss) v = let DependenciesTypes ds = dependenciesTypes $ valDependencies v targetTypes = specializationTargetType <$> ss in not . P.null $ targetTypes `intersect` ds -- | A Function is the 'Dynamic' representation of a Haskell function + its description data Function = Function Dynamic FunctionDescription deriving (Show) -- | Create a 'Function' value from a Haskell function createFunction :: (Typeable f) => f -> Function createFunction f = let dynType = toDyn f in Function dynType (describeFunction f) -- | Description of a 'Function' with input types and output type data FunctionDescription = FunctionDescription { _inputTypes :: [Text] , _outputType :: Text } deriving (Eq, Show) -- | Describe a 'Function' (which doesn't have a 'Show' instance) -- that can be put in the 'Registry' describeFunction :: Typeable a => a -> FunctionDescription describeFunction = uncurry FunctionDescription . showFullFunctionType -- | Show a Function as 'Text' using its Description showFunction :: Function -> Text showFunction = funDescriptionToText . funDescription -- | The Description of a 'Function' funDescription :: Function -> FunctionDescription funDescription (Function _ t) = t -- | Dynamic representation of a 'Function' funDyn :: Function -> Dynamic funDyn (Function d _) = d -- | Type representation of a 'Function' funDynTypeRep :: Function -> SomeTypeRep funDynTypeRep = dynTypeRep . funDyn -- | A 'FunctionDescription' as 'Text' funDescriptionToText :: FunctionDescription -> Text funDescriptionToText (FunctionDescription ins out) = T.intercalate " -> " (ins <> [out]) -- | Return True if a 'Function' has some input parameters hasParameters :: Function -> Bool hasParameters = isFunction . funDynTypeRep -- | A Typed value or function can be added to a 'Registry' -- It is either a value, having both 'Show' and 'Typeable' information -- or a function having just 'Typeable' information data Typed a = TypedValue Value | TypedFunction Function -- | This is a list of functions (or "constructors") available for constructing values newtype Functions = Functions [Function] deriving (Show, Semigroup, Monoid) -- | Display a list of constructors describeFunctions :: Functions -> Text describeFunctions (Functions fs) = if P.null fs then "" else unlines (funDescriptionToText . funDescription <$> fs) -- | Add one more Function to the list of Functions addFunction :: Function -> Functions -> Functions addFunction f (Functions fs) = Functions (f : fs) -- | List of values available which can be used as parameters to -- constructors for building other values newtype Values = Values { unValues :: [Value] } deriving (Show, Semigroup, Monoid) -- | Display a list of values describeValues :: Values -> Text describeValues (Values vs) = if P.null vs then "" else unlines (valDescriptionToText . valDescription <$> vs) -- | Add one more Value to the list of Values addValue :: Value -> Values -> Values addValue v (Values vs) = Values (v : vs) -- | The types of values that we are trying to build at a given moment -- of the resolution algorithm. -- We also store the function requiring a given value type to provide -- better error messages -- IMPORTANT: this is a *stack*, the deepest elements in the value -- graph are first in the list data Context = Context { _contextStack :: [(SomeTypeRep, Maybe SomeTypeRep)] } deriving (Eq, Show) instance Semigroup Context where Context c1 <> Context c2 = Context (c1 <> c2) instance Monoid Context where mempty = Context mempty mappend = (<>) -- | Return the target types for a given context contextTypes :: Context -> [SomeTypeRep] contextTypes (Context cs) = fmap fst cs -- | The values that a value depends on newtype Dependencies = Dependencies { unDependencies :: [Value] } deriving (Show, Semigroup, Monoid) -- | The values types that a value depends on newtype DependenciesTypes = DependenciesTypes { unDependenciesTypes :: [SomeTypeRep] } deriving (Eq, Show, Semigroup, Monoid) dependenciesTypes :: Dependencies -> DependenciesTypes dependenciesTypes (Dependencies ds) = DependenciesTypes (valueDynTypeRep <$> ds) -- | The dependencies of a value + the value itself dependenciesOn :: Value -> Dependencies dependenciesOn value = Dependencies $ value : (unDependencies . valDependencies $ value) -- | Specification of values which become available for -- construction when a corresponding type comes in context newtype Specializations = Specializations { unSpecializations :: [Specialization] } deriving (Show, Semigroup, Monoid) -- | A specialization is defined by -- a path of types, from top to bottom in the -- value graph and target value, which is the -- value to use when we need a value on that type -- on that path. -- For example: -- specializationPath = [App, PaymentEngine, TransactionRepository] -- specializationValue = DatabaseConfig "localhost" 5432 -- This means that need to use this `DatabaseConfig` whenever -- trying to find inputs needed to create a TransactionRepository -- if that repository is necessary to create a PaymentEngine, itself -- involved in the creation of the App data Specialization = Specialization { _specializationPath :: SpecializationPath , _specializationValue :: Value } deriving (Show) type SpecializationPath = NonEmpty SomeTypeRep specializationPaths :: Value -> Maybe [SpecializationPath] specializationPaths v = case catMaybes $ usedSpecialization <$> (v : (unDependencies . valDependencies $ v)) of [] -> Nothing ss -> Just (_specializationPath <$> ss) -- | First type of a specialization specializationStart :: Specialization -> SomeTypeRep specializationStart = NonEmpty.head . _specializationPath -- | Last type of a specialization specializationEnd :: Specialization -> SomeTypeRep specializationEnd = NonEmpty.last . _specializationPath -- | Return the type of the replaced value in a specialization specializationTargetType :: Specialization -> SomeTypeRep specializationTargetType = valueDynTypeRep . _specializationValue -- | A specialization is applicable to a context if all its types -- are part of that context, in the right order isContextApplicable :: Context -> Specialization -> Bool isContextApplicable context (Specialization specializationPath _) = P.all (`elem` (contextTypes context)) specializationPath -- | Return the specifications valid in a given context applicableTo :: Specializations -> Context -> Specializations applicableTo (Specializations ss) context = Specializations (P.filter (isContextApplicable context) ss) -- | The depth of a specialization in a context is the -- the index of the 'deepest' type of that specialization -- in the stack of types of that context -- is the one having its "deepest" type (in the value graph) -- the "deepest" in the current context -- If there is a tie we take the "highest" highest type of each specializedContext :: Context -> Specialization -> SpecializedContext specializedContext context specialization = SpecializedContext (specializationStart specialization `elemIndex` (contextTypes context)) (specializationEnd specialization `elemIndex` (contextTypes context)) -- | For a given context this represents the position of a specialization path -- in that context. startRange is the index of the start type of the specialization -- endRange is the index of the last type. data SpecializedContext = SpecializedContext { _startRange :: Maybe Int , _endRange :: Maybe Int } deriving (Eq, Show) -- | A specialization range is preferrable to another one if its types -- are more specific (or "deepest" in the value graph) than the other -- If a path is limited to just one type then a path ending with the same -- type but specifying other types will take precedence -- See TypesSpec for some concrete examples. instance Ord SpecializedContext where SpecializedContext s1 e1 <= SpecializedContext s2 e2 | e1 /= s1 && e2 /= s2 = e1 <= e2 || (e1 == e2 && s1 <= s2) | e1 == s1 && e2 /= s2 = e1 < e2 | otherwise = e1 <= e2 -- | Restrict a given context to the types of a specialization -- specializedContext :: Context -> Specialization -> Context -- specializedContext (Context cs) specialization = Context $ -- P.dropWhile (/= specializationEnd specialization) . -- dropWhileEnd (/= specializationStart specialization) $ cs -- | In a given context, create a value as specified by a specialization -- the full context is necessary since the specificationPath is -- only a subpath of a given creation context createValueFromSpecialization :: Context -> Specialization -> Value createValueFromSpecialization context specialization@(Specialization _ (ProvidedValue d desc)) = -- the creation context for that value CreatedValue d desc (Just context) (Just specialization) mempty -- this is not supposed to happen since specialization are always -- using ProvidedValues createValueFromSpecialization _ v = _specializationValue v -- | Display a list of specializations for the Registry, just showing the -- context (a type) in which a value must be selected describeSpecializations :: Specializations -> Text describeSpecializations (Specializations ss) = if P.null ss then "" else "specializations\n" <> unlines (P.show <$> ss) -- | List of functions modifying some values right after they have been -- built. This enables "tweaking" the creation process with slightly -- different results. Here SomeTypeRep is the target value type 'a' and newtype Modifiers = Modifiers [(SomeTypeRep, ModifierFunction)] deriving (Semigroup, Monoid) type ModifierFunction = Maybe [SpecializationPath] -> Function -- | Create a 'Function' value from a Haskell function createConstModifierFunction :: (Typeable f) => f -> ModifierFunction createConstModifierFunction f = const (createFunction f) instance Show Modifiers where show = toS . describeModifiers -- | Display a list of modifiers for the Registry, just showing the -- type of the modified value describeModifiers :: Modifiers -> Text describeModifiers (Modifiers ms) = if P.null ms then "" else "modifiers for types\n" <> unlines (P.show . fst <$> ms)