keiro-dsl-0.12.0.0: src/Keiro/Dsl/ProjectionSupply.hs
-- | Order-independent projection-owner resolution for catalog-bound query
-- models. Validation and generated consumers share this analysis so target
-- ownership never acquires a second, list-order-sensitive interpretation.
module Keiro.Dsl.ProjectionSupply
( ResolvedProjectionSupply (..),
ProjectionSupplyIssue (..),
ProjectionSupplyAnalysis (..),
analyzeProjectionSupplies,
)
where
import Data.List (nub, sort, sortOn)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Map.Strict qualified as Map
import Keiro.Dsl.Grammar
-- | One successfully resolved catalog-bound query model. Observed targets are
-- normalized by identity; source locations remain available for structured
-- diagnostics and generated evidence.
data ResolvedProjectionSupply = ResolvedProjectionSupply
{ supplyQueryModel :: !Name,
supplyProjectionOwner :: !Name,
supplyRebuildGroup :: !Name,
supplyObservedTargets :: !(NonEmpty Name),
supplyQueryLoc :: !Loc,
supplyOwnerLoc :: !Loc
}
deriving stock (Eq, Show)
-- | Structural failures found while deriving the relation. The public DSL
-- diagnostic layer decides which issue owns a user-facing diagnostic and which
-- is already covered by an earlier target/group declaration error.
data ProjectionSupplyIssue
= SupplyObservedTargetsEmpty !ReadModelNode
| SupplyObservedTargetUnknown !ReadModelNode !Name
| SupplyObservedTargetOutsideGroup !ReadModelNode !Name
| SupplyObservedTargetWithoutOwner !ReadModelNode !Name
| SupplyObservedTargetWithMultipleOwners !ReadModelNode !Name ![ProjectionOwnerNode]
| SupplyOwnerGroupMismatch !ReadModelNode !Name !ProjectionOwnerNode
| SupplyQueryWithoutOwner !ReadModelNode
| SupplyQueryWithMultipleOwners !ReadModelNode ![ProjectionOwnerNode]
| SupplyLegacyProjectionConflict !ReadModelNode !Aggregate !ProjectionSpec
deriving stock (Eq, Show)
data ProjectionSupplyAnalysis = ProjectionSupplyAnalysis
{ resolvedProjectionSupplies :: ![ResolvedProjectionSupply],
projectionSupplyIssues :: ![ProjectionSupplyIssue]
}
deriving stock (Eq, Show)
analyzeProjectionSupplies :: Spec -> ProjectionSupplyAnalysis
analyzeProjectionSupplies spec =
ProjectionSupplyAnalysis
{ resolvedProjectionSupplies = concatMap (fst . analyzeReadModel) catalogReadModels,
projectionSupplyIssues = concatMap (snd . analyzeReadModel) catalogReadModels
}
where
catalogReadModels =
sortOn
rmName
[ readModel
| NReadModel readModel <- specNodes spec,
rmGroup readModel /= Nothing
]
targetsByName =
Map.fromList
[ (ptName target, target)
| NProjectionTarget target <- specNodes spec
]
groupsByTarget =
Map.fromListWith
(<>)
[ (targetName, [rgName groupNode])
| NRebuildGroup groupNode <- specNodes spec,
targetName <- rgTargets groupNode
]
ownersByTarget =
Map.fromListWith
(<>)
[ (targetName, [owner])
| NProjectionOwner owner <- specNodes spec,
targetName <- poTargets owner
]
legacyProjections =
[ (aggregate, projection)
| NAggregate aggregate <- specNodes spec,
Just projection <- [aggProjection aggregate]
]
analyzeReadModel readModel =
(resolved, sortOn issueSortKey (legacyIssues <> structuralIssues))
where
observedTargets = sort (nub (rmObservedTargets readModel))
queryGroup = maybe (error "catalog read model lost its group") id (rmGroup readModel)
legacyIssues =
[ SupplyLegacyProjectionConflict readModel aggregate projection
| (aggregate, projection) <- legacyProjections,
projTable projection == rmName readModel
]
targetIssues = concatMap (issuesForTarget readModel queryGroup) observedTargets
structuralIssues
| null observedTargets = [SupplyObservedTargetsEmpty readModel]
| not (null targetIssues) = targetIssues
| otherwise = case supplierOwners of
[] -> [SupplyQueryWithoutOwner readModel]
[owner] ->
[ SupplyOwnerGroupMismatch readModel targetName owner
| targetName <- observedTargets,
poGroup owner /= queryGroup
]
owners -> [SupplyQueryWithMultipleOwners readModel owners]
supplierOwners =
sortOn poName
. nubByOwner
$ [ owner
| targetName <- observedTargets,
[owner] <- [Map.findWithDefault [] targetName ownersByTarget]
]
resolved = case structuralIssues of
[] -> case supplierOwners of
[owner] ->
[ ResolvedProjectionSupply
{ supplyQueryModel = rmName readModel,
supplyProjectionOwner = poName owner,
supplyRebuildGroup = queryGroup,
supplyObservedTargets = case observedTargets of
target : rest -> target :| rest
[] -> error "resolved projection supply lost observed targets",
supplyQueryLoc = rmLoc readModel,
supplyOwnerLoc = poLoc owner
}
]
_ -> []
_ -> []
issuesForTarget readModel queryGroup targetName =
case Map.lookup targetName targetsByName of
Nothing -> [SupplyObservedTargetUnknown readModel targetName]
Just _ ->
groupIssues <> ownerIssues
where
groupIssues =
[ SupplyObservedTargetOutsideGroup readModel targetName
| Map.findWithDefault [] targetName groupsByTarget /= [queryGroup]
]
ownerIssues = case sortOn poName (Map.findWithDefault [] targetName ownersByTarget) of
[] -> [SupplyObservedTargetWithoutOwner readModel targetName]
[_] -> []
owners -> [SupplyObservedTargetWithMultipleOwners readModel targetName owners]
nubByOwner = Map.elems . Map.fromList . map (\owner -> (poName owner, owner))
issueSortKey :: ProjectionSupplyIssue -> (Name, Int, Name)
issueSortKey = \case
SupplyObservedTargetsEmpty readModel -> (rmName readModel, 0, "")
SupplyObservedTargetUnknown readModel targetName -> (rmName readModel, 1, targetName)
SupplyObservedTargetOutsideGroup readModel targetName -> (rmName readModel, 2, targetName)
SupplyObservedTargetWithoutOwner readModel targetName -> (rmName readModel, 3, targetName)
SupplyObservedTargetWithMultipleOwners readModel targetName _ -> (rmName readModel, 4, targetName)
SupplyOwnerGroupMismatch readModel targetName _ -> (rmName readModel, 5, targetName)
SupplyQueryWithoutOwner readModel -> (rmName readModel, 6, "")
SupplyQueryWithMultipleOwners readModel _ -> (rmName readModel, 7, "")
SupplyLegacyProjectionConflict readModel aggregate _ -> (rmName readModel, 8, aggName aggregate)