packages feed

hwm-0.4.0: src/HWM/Domain/Workspace.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE NoImplicitPrelude #-}

module HWM.Domain.Workspace
  ( Workspace,
    WorkGroup (..),
    pkgRegistry,
    PkgRegistry,
    WorkspaceRef (..),
    buildWorkspace,
    resolveWorkspaces,
    forWorkspace,
    forWorkspaceTuple,
    parseWorkspaceRef,
    addWorkgroupMember,
    allPackages,
    resolveWsPkgs,
    WsPkgs,
    checkWorkspaceRefs,
    isMember,
    toWorkspaceRef,
    printPkgWSRef,
  )
where

import Control.Monad.Error.Class
import Data.Aeson
  ( FromJSON (..),
    Options (..),
    ToJSON (toJSON),
    genericToJSON,
    withText,
  )
import Data.Aeson.Types
  ( defaultOptions,
  )
import Data.List (groupBy)
import qualified Data.Map as Map
import qualified Data.Set as S
import qualified Data.Text as T
import HWM.Core.Common (Name)
import HWM.Core.Formatting (StatusM, availableOptions, commonPrefix, displayStatusM, genMaxLen, slugify)
import HWM.Core.Has (Has (..), askEnv)
import HWM.Core.Pkg (Pkg (..), PkgName (..), makePkg)
import HWM.Core.Result
import HWM.Runtime.Files (cleanRelativePath)
import HWM.Runtime.UI (MonadUI, sectionWorkspace, uiLabel, uiSpace, uiSubPathRow)
import Relude

type Workspace = Map Name WorkGroup

data WorkspaceRef = WorkspaceRef
  { wsRefGroupId :: Name,
    wsRefMemberId :: Maybe Name
  }
  deriving (Show, Eq, Generic, Ord)

isMember :: WorkspaceRef -> Pkg -> Bool
isMember (WorkspaceRef groupId Nothing) pkg = pkgGroup pkg == groupId
isMember (WorkspaceRef groupId (Just memberId)) pkg = pkgGroup pkg == groupId && pkgMemberId pkg == memberId

toWorkspaceRef :: Pkg -> WorkspaceRef
toWorkspaceRef Pkg {pkgGroup, pkgMemberId} = WorkspaceRef pkgGroup (Just pkgMemberId)

printPkgWSRef :: Pkg -> Text
printPkgWSRef Pkg {pkgGroup, pkgMemberId} = pkgGroup <> "/" <> pkgMemberId

checkWorkspaceRefs ::
  ( MonadError Issue m,
    MonadReader env m,
    Has env Workspace,
    MonadIO m
  ) =>
  [WorkspaceRef] ->
  m ()
checkWorkspaceRefs ls = do
  pkgs <- allPackages
  let unknown = filter (\ref -> not (any (isMember ref) pkgs)) ls
  unless (null unknown) (throwError $ fromString ("unknown packages: " <> show unknown))

instance FromJSON WorkspaceRef where
  parseJSON = withText "WorkspaceRef" $ pure . parseWorkspaceRef

instance ToJSON WorkspaceRef where
  toJSON (WorkspaceRef g Nothing) = toJSON g
  toJSON (WorkspaceRef g (Just m)) = toJSON $ g <> "/" <> m

parseWorkspaceRef :: Text -> WorkspaceRef
parseWorkspaceRef input = case T.breakOn "/" input of
  (pkg, "") -> WorkspaceRef pkg Nothing -- No slash found
  (grp, rest) -> WorkspaceRef grp (Just (T.drop 1 rest)) -- Drop the "/"

data WorkGroup = WorkGroup
  { dir :: Maybe FilePath,
    members :: [Name],
    prefix :: Maybe Text
  }
  deriving
    ( Generic,
      FromJSON,
      Show
    )

instance ToJSON WorkGroup where
  toJSON = genericToJSON defaultOptions {omitNothingFields = True}

memberPkgs :: (MonadIO m, MonadError Issue m) => (Name, WorkGroup) -> m [Pkg]
memberPkgs (name, WorkGroup {..}) = traverse (makePkg name dir prefix) members

getMembers :: (MonadIO m, MonadError Issue m) => Workspace -> m [Pkg]
getMembers ws = do
  let groups = Map.toList ws
  pkgs <- traverse memberPkgs groups
  pure $ concat pkgs

allPackages ::
  ( MonadReader env m,
    Has env Workspace,
    MonadIO m,
    MonadError Issue m
  ) =>
  m [Pkg]
allPackages = askEnv >>= getMembers

type PkgRegistry = Map PkgName WorkGroup

type WsPkgs = [(Name, [Pkg])]

resolveGroup :: (MonadIO m, MonadError Issue m) => (Name, WorkGroup) -> m PkgRegistry
resolveGroup g = Map.fromList . map ((,snd g) . pkgName) <$> memberPkgs g

pkgRegistry :: (MonadIO m, MonadError Issue m) => Workspace -> m PkgRegistry
pkgRegistry = fmap Map.unions . traverse resolveGroup . Map.toList

askWorkspace :: (MonadReader env m, Has env Workspace) => m Workspace
askWorkspace = asks obtain

groupByGroupName :: [Pkg] -> WsPkgs
groupByGroupName pkgs =
  let sorted = sortOn pkgGroup pkgs
      grouped = groupBy (\a b -> pkgGroup a == pkgGroup b) sorted
   in [(maybe "" pkgGroup (viaNonEmpty head g), g) | g <- grouped, not (null g)]

resolveWorkspaces :: (MonadIO m, MonadError Issue m, MonadReader env m, Has env Workspace) => [Name] -> m WsPkgs
resolveWorkspaces = resolveWsPkgs . map parseWorkspaceRef

resolveWsPkgs :: (MonadIO m, MonadError Issue m, MonadReader env m, Has env Workspace) => [WorkspaceRef] -> m WsPkgs
resolveWsPkgs = fmap (groupByGroupName . (S.toList . S.fromList) . concat) . traverse resolveWsRef

resolveWsRef :: (MonadIO m, MonadError Issue m, MonadReader env m, Has env Workspace) => WorkspaceRef -> m [Pkg]
resolveWsRef wsRef = do
  ws <- askWorkspace
  members <- selectGroup (wsRefGroupId wsRef) ws >>= memberPkgs . (wsRefGroupId wsRef,)
  resolveT members (wsRefMemberId wsRef)

selectGroup :: (MonadError Issue m) => Name -> Workspace -> m WorkGroup
selectGroup name groups = maybe (throwError $ fromString $ toString ("Workspace group \"" <> name <> "\" not found! " <> availableOptions (Map.keys groups))) pure (Map.lookup name groups)

resolveT :: (MonadError Issue m) => [Pkg] -> Maybe Name -> m [Pkg]
resolveT pkgs Nothing = pure pkgs
resolveT pkgs (Just target) =
  case find (\p -> target == pkgMemberId p) pkgs of
    Just p -> pure [p]
    Nothing -> throwError $ fromString $ toString $ "Target not found: " <> target

buildWorkspace :: (Monad m, MonadError Issue m) => [Pkg] -> m Workspace
buildWorkspace = fmap (Map.fromList . concat) . traverse groupToWorkspace . groupBy sameGroup . sortOn pkgGroup
  where
    sameGroup left right = pkgGroup left == pkgGroup right
    groupToWorkspace [] = pure []
    groupToWorkspace (pkg : pkgs) = do
      let (prefix, members) = commonPrefix (map pkgMemberId (pkg : pkgs))
      pure
        [ ( if T.null (pkgGroup pkg) then "libs" else slugify (pkgGroup pkg),
            WorkGroup
              { dir = cleanRelativePath (Just $ toString (pkgGroup pkg)),
                members = sortBy compareMembers members,
                prefix = prefix
              }
          )
        ]

compareMembers :: Text -> Text -> Ordering
compareMembers "." _ = GT
compareMembers _ "." = LT
compareMembers x y = compare x y

forWorkspace :: (MonadIO m, MonadUI m, MonadIssue m, MonadError Issue m, MonadReader env m, Has env Workspace) => (Pkg -> StatusM m) -> m ()
forWorkspace f = do
  gs <- Map.toList <$> askWorkspace
  sectionWorkspace
    $ for_ gs
    $ \(name, wg) -> do
      uiSpace
      uiLabel name
      pkgs <- memberPkgs (name, wg)
      let maxLen = genMaxLen (map pkgMemberId pkgs)
      for_ pkgs $ \pkg -> do
        status <- displayStatusM (f pkg)
        uiSubPathRow maxLen (pkgMemberId pkg) status

forWorkspaceTuple :: (MonadUI m) => [(Text, [Pkg])] -> (Pkg -> StatusM m) -> m ()
forWorkspaceTuple ws f = sectionWorkspace $ do
  let maxLen = genMaxLen (map pkgMemberId $ concatMap snd ws)
  for_ ws $ \(name, pkgs) -> do
    uiSpace
    uiLabel name
    for_ pkgs $ \pkg -> do
      status <- displayStatusM (f pkg)
      uiSubPathRow maxLen (pkgMemberId pkg) status

addWorkgroupMember :: (MonadIO m, MonadUI m, MonadIssue m, MonadError Issue m, MonadReader env m, Has env Workspace) => Name -> Name -> m (Workspace, WorkGroup)
addWorkgroupMember name memberId = do
  ws <- askWorkspace
  w <- selectGroup name ws
  if memberId `elem` members w
    then
      throwError
        Issue
          { issueTopic = memberId,
            issueMessage = "A member package with name \"" <> memberId <> "\" already exists in workspace group \"" <> name <> "\".",
            issueSeverity = SeverityError,
            issueDetails = Nothing
          }
    else pure (Map.adjust (\g -> g {members = members g <> [memberId]}) name ws, w)