packages feed

arch-hs-0.5.0.0: src/Distribution/ArchHs/Name.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}

-- | Copyright: (c) 2020 berberman
-- SPDX-License-Identifier: MIT
-- Maintainer: berberman <1793913507@qq.com>
-- Stability: experimental
-- Portability: portable
--
-- Naming conversion between haskell package in hackage and archlinux community repo.
--
-- To distribute a haskell package to archlinux, the name of package should be changed according to the naming convention:
--
--   (1) for haskell libraries, their names must have @haskell-@ prefix
--
--   (2) for programs, it depends on circumstances
--
--   (3) names should always be in lower case
--
-- However, it's not enough to prefix the string with @haskell-@ and trasform to lower case; in some special situations, the hackage name
-- may have @haskell-@ prefix already, or the case is irregular, thus we have to a name preset, @NAME_PRESET.json@, manually.
-- Once a package distributed to archlinux, whose name conform to above-mentioned situation, the name preset should be upgraded correspondingly.
--
-- @NAME_PRESET.json@ will be loaded during the compilation, generating haskell code to be called in runtime.
--
-- Converting a community name to hackage name following these steps:
--
--   (1) Find if the name preset contains this rule
--   (2) If it contains, then use it; or remove the @haskell-@ prefix
--
-- Converting a hackage name to community name following these steps:
--
--   (1) Find if the name preset contains this rule
--   (2) If it contains, then use it; or add the @haskell-@ prefix
--
-- For details, see the type 'MyName' and type class 'HasMyName' with its instances.
module Distribution.ArchHs.Name
  ( MyName,
    unMyName,
    HasMyName (..),
    NameRep (..),
    mToCommunityName,
    mToHackageName,
    toCommunityName,
    toHackageName,
    isHaskellPackage,
  )
where

import Data.Char (toLower)
import Data.String (IsString, fromString)
import Distribution.ArchHs.Internal.NamePresetLoader
import Distribution.ArchHs.Internal.Prelude
import Distribution.ArchHs.Types

-- | The representation of a package name.
data NameRep
  = -- |  archlinx community style
    CommunityRep
  | -- | hackage style
    HackageRep

$(loadNamePreset)

-- | Convert a name from community representation to hackage representation, according to the name preset.
-- If the preset doesn't contain this mapping rule, the function will return 'Nothing'.
-- This function is generated from @NAME_PRESET.json@
communityToHackageP :: MyName 'CommunityRep -> Maybe (MyName 'HackageRep)

-- | Convert a name from hackage representation to community representation, according to the name preset.
-- If the preset doesn't contain this mapping rule, the function will return 'Nothing'.
--
-- This function is generated from @NAME_PRESET.json@
hackageToCommunityP :: MyName 'HackageRep -> Maybe (MyName 'CommunityRep)

-- | Special haskell packages in community reop, which should be ignored in the process.
--
-- This function is generated from @NAME_PRESET.json@
falseListP :: [MyName 'CommunityRep]

-- | Community haskell packages of in the name preset.
--
-- This function is generated from @NAME_PRESET.json@
communityListP :: [MyName 'CommunityRep]

-- | A general package name representation.
-- It has a phantom @a@, which indexes this name.
-- Normally, the index should be the data kinds of 'NameRep'.
--
-- In Cabal API, packages' names are represented by the type 'PackageName';
-- in arch-hs, names parsed from @community.db@ are represented by the type 'CommunityName'.
-- It would be tedious to use two converting functions everywhere, so here comes a intermediate data type
-- to unify them, with type level constraints as bonus.
newtype MyName a = MyName
  { -- | Unwrap the value.
    unMyName :: String
  }
  deriving stock (Show, Read, Eq, Ord, Generic)
  deriving anyclass (NFData)

instance IsString (MyName a) where
  fromString = MyName

-- | 'HasMyName' indicates that the type @a@ can be converted to 'MyName'.
-- This is where the actually conversion occurs.
class HasMyName a where
  -- | To 'MyName' in hackage style.
  toHackageRep :: a -> MyName 'HackageRep

  -- | To 'MyName' in community style.
  toCommunityRep :: a -> MyName 'CommunityRep

instance HasMyName (MyName 'CommunityRep) where
  toHackageRep = toHackageRep . CommunityName . unMyName
  toCommunityRep = id

instance HasMyName (MyName 'HackageRep) where
  toHackageRep = id
  toCommunityRep = toCommunityRep . mkPackageName . unMyName

instance HasMyName PackageName where
  toHackageRep = MyName . unPackageName
  toCommunityRep = go . unPackageName
    where
      go s = case hackageToCommunityP (MyName s) of
        Just x -> x
        _ ->
          MyName . fmap toLower $
            ( if "haskell-" `isPrefixOf` s
                then s
                else "haskell-" <> s
            )

instance HasMyName CommunityName where
  toHackageRep = go . unCommunityName
    where
      go s = case communityToHackageP (MyName s) of
        Just x -> x
        _ -> MyName $ drop 8 s
  toCommunityRep = MyName . unCommunityName

-- | Back to 'CommunityName'.
mToCommunityName :: MyName 'CommunityRep -> CommunityName
mToCommunityName = CommunityName . unMyName

-- | Back to 'PackageName'.
mToHackageName :: MyName 'HackageRep -> PackageName
mToHackageName = mkPackageName . unMyName

-- | Convert @n@ to 'CommunityName'.
toCommunityName :: HasMyName n => n -> CommunityName
toCommunityName = mToCommunityName . toCommunityRep

-- | Convert @n@ to 'PackageName'.
toHackageName :: HasMyName n => n -> PackageName
toHackageName = mToHackageName . toHackageRep

-- | Judge if a package in archlinux community repo is haskell package.
--
-- i.e. it is in @preset@ or have @haskell-@ prefix, and is not present in @falseList@ of @NAME_PRESET.json@.
isHaskellPackage :: CommunityName -> Bool
isHaskellPackage name =
  let rep = toCommunityRep name
   in (rep `elem` communityListP || "haskell-" `isPrefixOf` unMyName rep) && rep `notElem` falseListP