module Utils.Workflow.Lint ( lintWorkflowGraph , WorkflowGraphLinterIssue(..) ) where import Import.NoFoundation import qualified Data.Set as Set import qualified Data.MultiSet as MultiSet import qualified Data.Map as Map import qualified Data.Sequence as Seq {-# ANN module ("HLint: ignore Use newtype instead of data"::String) #-} data WorkflowGraphLinterIssue = WGLUnknownGraphNodeLabel WorkflowGraphNodeLabel | WGLPayloadInvisibleInTargetNode (WorkflowGraphNodeLabel, WorkflowGraphEdgeLabel) WorkflowPayloadLabel | WGLFinalNodeHasOutgoingEdges WorkflowGraphNodeLabel | WGLNonFinalNodeHasNoOutgoingEdges WorkflowGraphNodeLabel | WGLUndefinedFieldOrder (WorkflowGraphNodeLabel, WorkflowGraphEdgeLabel) (NonNull (MultiSet WorkflowPayloadLabel)) | WGLNodeUnreachable WorkflowGraphNodeLabel | WGLNodeUnfinalizable WorkflowGraphNodeLabel deriving (Eq, Ord, Read, Show, Generic, Typeable) instance Exception WorkflowGraphLinterIssue where displayException = \case WGLUnknownGraphNodeLabel nodeLbl -> unpack [st|Unknown GraphNodeLabel: #{tshow (toPathPiece nodeLbl)}|] WGLPayloadInvisibleInTargetNode (nodeLbl, edgeLbl) payloadLbl -> unpack [st|Payload #{tshow (toPathPiece payloadLbl)} has form on edge #{tshow (toPathPiece edgeLbl)} to target node #{tshow (toPathPiece nodeLbl)} but no viewers on target node|] WGLFinalNodeHasOutgoingEdges nodeLbl -> unpack [st|Node #{tshow (toPathPiece nodeLbl)} is marked “final” but has outgoing edges|] WGLNonFinalNodeHasNoOutgoingEdges nodeLbl -> unpack [st|Node #{tshow (toPathPiece nodeLbl)} isn't marked “final” but has no outgoing edges|] WGLUndefinedFieldOrder (nodeLbl, edgeLbl) payloads -> unpack [st|Form for edge #{tshow (toPathPiece edgeLbl)} to target node #{tshow (toPathPiece nodeLbl)} has ill defined field order for payload(s): “#{intercalate ", " (map (tshow . toPathPiece) (MultiSet.elems (toNullable payloads)))}”|] WGLNodeUnreachable nodeLbl -> unpack [st|Node #{tshow (toPathPiece nodeLbl)} is unreachable from all initial edges|] WGLNodeUnfinalizable nodeLbl -> unpack [st|Node #{tshow (toPathPiece nodeLbl)} has no path to a final node|] lintWorkflowGraph :: forall fileid userid. WorkflowGraph fileid userid -> Maybe (NonNull (Set WorkflowGraphLinterIssue)) lintWorkflowGraph graph = fromNullable . Set.fromList $ concatMap ($ graph) [ checkEdgesForUnknownGraphNodeLabel , checkFormPayloadVisibleInTargetNode -- TODO: Satisfiability of automatic edges? , finalMatchesOutgoingEdges , checkUndefinedFieldOrder , checkNodeUnreachable -- TODO: Satisfiability of automatic edges , checkNodeUnfinalizable -- TODO: Satisfiability of automatic edges -- Future ideas: -- - WorkflowRolePayloadReference for unknown payload -- - wgePayloadRestriction for unknown payload -- - FieldReference for payload not defined in same form -- - WorkflowRolePayloadReference to payload without user fields -- - all initial edges have only payload-reference -- - cycles of automatic edges (also consider payload restrictions; computationally equivalent to SAT) -- - unsatisfiable restrictions ] where checkEdgesForUnknownGraphNodeLabel WorkflowGraph{wgNodes} = foldMap (pure . WGLUnknownGraphNodeLabel) $ Set.fromList edgeNodeLabels `Set.difference` Map.keysSet wgNodes where edges = foldMap (Map.elems . wgnEdges) wgNodes edgeNodeLabels = flip foldMap edges $ \case WorkflowGraphEdgeManual{wgeSource} -> pure wgeSource WorkflowGraphEdgeAutomatic{wgeSource} -> pure wgeSource WorkflowGraphEdgeInitial{} -> [] checkFormPayloadVisibleInTargetNode WorkflowGraph{wgNodes} = ifoldMap (\nodeLbl node -> map (\(edgeLbl, payloadLbl) -> WGLPayloadInvisibleInTargetNode (nodeLbl, edgeLbl) payloadLbl) . Set.toList $ doCheck node) wgNodes where doCheck :: WorkflowGraphNode fileid userid -> Set (WorkflowGraphEdgeLabel, WorkflowPayloadLabel) doCheck WGN{wgnEdges, wgnPayloadView} = ifoldMap (\edgeLbl -> Set.map (edgeLbl, ) . doCheck') wgnEdges where doCheck' :: WorkflowGraphEdge fileid userid -> Set WorkflowPayloadLabel doCheck' wge = fromMaybe Set.empty $ do WorkflowGraphEdgeForm{wgefFields} <- wge ^? _wgeForm return $ Map.keysSet wgefFields `Set.difference` Map.keysSet wgnPayloadView finalMatchesOutgoingEdges WorkflowGraph{wgNodes} = foldMap (\nodeLbl -> pure $ bool WGLFinalNodeHasOutgoingEdges WGLNonFinalNodeHasNoOutgoingEdges (nodeLbl `Set.notMember` markedFinalNodes) nodeLbl) $ markedFinalNodes `setSymmDiff` edgeFinalNodes where markedFinalNodes = Set.fromList $ do (nodeLbl, WGN{wgnFinal}) <- Map.toList wgNodes guard $ is _Just wgnFinal return nodeLbl edgeFinalNodes = Set.fromList $ do nodeLbl <- Map.keys wgNodes guard $ noneOf (folded . _wgnEdges . folded . _wgeSource) (== nodeLbl) wgNodes return nodeLbl checkUndefinedFieldOrder WorkflowGraph{wgNodes} = ifoldMap (\nodeLbl node -> map (\(edgeLbl, payloadLbls) -> WGLUndefinedFieldOrder (nodeLbl, edgeLbl) payloadLbls) . Set.toList $ doCheck node) wgNodes where doCheck :: WorkflowGraphNode fileid userid -> Set (WorkflowGraphEdgeLabel, NonNull (MultiSet WorkflowPayloadLabel)) doCheck WGN{wgnEdges} = ifoldMap (\edgeLbl -> foldMap (Set.singleton . (edgeLbl, )) . doCheck') wgnEdges where doCheck' :: WorkflowGraphEdge fileid userid -> [NonNull (MultiSet WorkflowPayloadLabel)] doCheck' wge = do WorkflowGraphEdgeForm{wgefFields} <- hoistMaybe $ wge ^? _wgeForm let MergeMap orderMap = ifoldMap go wgefFields where go :: WorkflowPayloadLabel -> NonNull (Set (NonNull (Map WorkflowGraphEdgeFormOrder (WorkflowPayloadSpec fileid userid)))) -> MergeMap WorkflowGraphEdgeFormOrder (NonNull (MultiSet WorkflowPayloadLabel)) go payloadLbl = foldMap (go' . Map.keysSet . toNullable) . Set.toList . toNullable where go' :: Set WorkflowGraphEdgeFormOrder -> MergeMap WorkflowGraphEdgeFormOrder (NonNull (MultiSet WorkflowPayloadLabel)) go' = foldMap $ \formOrder -> MergeMap . Map.singleton formOrder . impureNonNull $ MultiSet.singleton payloadLbl filter ((> 1) . MultiSet.size . toNullable) $ Map.elems orderMap checkNodeUnreachable WorkflowGraph{wgNodes} = foldMap (pure . WGLNodeUnreachable) $ Map.keysSet wgNodes `Set.difference` reachableNodes where initialNodes = Map.keysSet $ Map.filter isInitial wgNodes where isInitial WGN{wgnEdges} = any (is _WorkflowGraphEdgeInitial) wgnEdges reachableNodes = extendAfter graph initialNodes checkNodeUnfinalizable WorkflowGraph{wgNodes} = foldMap (pure . WGLNodeUnfinalizable) $ Map.keysSet wgNodes `Set.difference` finalizableNodes where finalNodes = Map.keysSet $ Map.filter (has $ _wgnFinal . _Just) wgNodes finalizableNodes = extendBefore graph finalNodes extendAfter, extendBefore :: forall fileid userid. WorkflowGraph fileid userid -> Set WorkflowGraphNodeLabel -> Set WorkflowGraphNodeLabel extendAfter WorkflowGraph{wgNodes} = go Set.empty . Seq.fromList . Set.toList where go :: Set WorkflowGraphNodeLabel -- ^ Already known reachable -> Seq WorkflowGraphNodeLabel -- ^ Queue to check -> Set WorkflowGraphNodeLabel go known Seq.Empty = known go known (n Seq.:<| ns) | n `Set.member` known = go known ns | otherwise = go (Set.insert n known) $ ns `searchStrategy` nextNodes where nextNodes = Map.keysSet $ Map.filter hasSource wgNodes hasSource WGN{wgnEdges} = anyOf (folded . _wgeSource) (== n) wgnEdges extendBefore WorkflowGraph{wgNodes} = go Set.empty . Seq.fromList . Set.toList where go :: Set WorkflowGraphNodeLabel -> Seq WorkflowGraphNodeLabel -> Set WorkflowGraphNodeLabel go known Seq.Empty = known go known (n Seq.:<| ns) | n `Set.member` known = go known ns | otherwise = go (Set.insert n known) $ ns `searchStrategy` prevNodes where prevNodes = flip foldMap (wgNodes Map.!? n) $ \WGN{wgnEdges} -> setOf (folded . _wgeSource) wgnEdges searchStrategy :: Seq WorkflowGraphNodeLabel -> Set WorkflowGraphNodeLabel -> Seq WorkflowGraphNodeLabel -- ^ BFS searchStrategy queue next = queue <> Seq.fromList (Set.toList next)