packages feed

hwm-0.0.1: 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
  ( WorkspaceGroup (..),
    pkgGroupName,
    pkgRegistry,
    PkgRegistry,
    memberPkgs,
    resolveTargets,
    selectGroup,
    canPublish,
    buildWorkspaceGroups,
  )
where

import Control.Monad.Error.Class
import Data.Aeson
  ( FromJSON (..),
    Options (..),
    ToJSON (toJSON),
    genericToJSON,
  )
import Data.Aeson.Types
  ( defaultOptions,
  )
import Data.List (groupBy)
import qualified Data.Map as Map
import qualified Data.Text as T
import HWM.Core.Common (Name)
import HWM.Core.Formatting (availableOptions, commonPrefix, slugify)
import HWM.Core.Pkg (Pkg (..), PkgName, makePkg)
import HWM.Core.Result
import HWM.Domain.Dependencies (DependencyGraph, sortByDependencyHierarchy)
import HWM.Runtime.Files (cleanRelativePath)
import Relude

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

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

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

pkgGroupName :: WorkspaceGroup -> Name
pkgGroupName WorkspaceGroup {..} = name

type PkgRegistry = Map PkgName WorkspaceGroup

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

pkgRegistry :: (MonadIO m, MonadError Issue m) => [WorkspaceGroup] -> m PkgRegistry
pkgRegistry = fmap Map.unions . traverse resolveGroup

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

resolveTargets :: (MonadIO m, MonadError Issue m) => [WorkspaceGroup] -> [Name] -> m [Pkg]
resolveTargets ws names = concat <$> traverse (resolveTarget ws) names

resolveTarget :: (MonadIO m, MonadError Issue m) => [WorkspaceGroup] -> Text -> m [Pkg]
resolveTarget ws target = do
  let (g, n) = parseTarget target
  members <- selectGroup g ws >>= memberPkgs
  resolveT members n

selectGroup :: (MonadError Issue m) => Name -> [WorkspaceGroup] -> m WorkspaceGroup
selectGroup name groups =
  maybe (throwError $ fromString $ toString ("Workspace group \"" <> name <> "\" not found! " <> availableOptions (map pkgGroupName groups))) pure (find ((== name) . pkgGroupName) 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

canPublish :: WorkspaceGroup -> Bool
canPublish WorkspaceGroup {publish} = fromMaybe False publish

buildWorkspaceGroups :: (Monad m, MonadError Issue m) => DependencyGraph -> [Pkg] -> m [WorkspaceGroup]
buildWorkspaceGroups graph = fmap concat . traverse groupToWorkspace . groupBy sameGroup . sortOn pkgGroup
  where
    sameGroup left right = pkgGroup left == pkgGroup right
    groupToWorkspace [] = pure []
    groupToWorkspace (pkg : pkgs) = do
      sortPkgs <- sortByDependencyHierarchy graph (pkg : pkgs)
      let (prefix, members) = commonPrefix (map pkgMemberId sortPkgs)
      pure
        [ WorkspaceGroup
            { name = if T.null (pkgGroup pkg) then "libs" else slugify (pkgGroup pkg),
              dir = cleanRelativePath (Just $ toString (pkgGroup pkg)),
              members,
              prefix = prefix,
              publish = derivePublish (pkgGroup pkg : members)
            }
        ]

derivePublish :: [Name] -> Maybe Bool
derivePublish names
  | any (`elem` nonPublish) loweredNames = Nothing
  | otherwise = Just True
  where
    loweredNames = map T.toLower names
    nonPublish = ["examples", "example", "bench", "benchmarks"]