nvfetcher-0.1.0.0: src/NvFetcher/PackageSet.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
-- | Copyright: (c) 2021 berberman
-- SPDX-License-Identifier: MIT
-- Maintainer: berberman <berberman@yandex.com>
-- Stability: experimental
-- Portability: portable
--
-- This module mainly contains two things: 'PackageSet' and 'PkgDSL'.
-- NvFetcher accepts the former one -- a set of packages to produce nix sources expr;
-- the later one is used to construct a single package.
--
-- There are many combinators for defining packages. See the documentation of 'define' for example.
module NvFetcher.PackageSet
( -- * Package set
PackageSetF,
PackageSet,
newPackage,
purePackageSet,
runPackageSet,
-- * Package DSL
-- ** Primitives
PkgDSL (..),
define,
package,
src,
fetch,
-- ** Two-in-one functions
fromGitHub,
fromPypi,
-- ** Version sources
sourceGitHub,
sourceGit,
sourcePypi,
sourceAur,
sourceArchLinux,
sourceManual,
sourceRepology,
-- ** Fetchers
fetchGitHub,
fetchGitHubRelease,
fetchPypi,
fetchGit,
fetchUrl,
-- ** Miscellaneous
Prod,
Member,
NotElem,
coerce,
liftIO,
)
where
import Control.Monad.Free
import Control.Monad.IO.Class
import Data.Coerce (coerce)
import Data.Kind (Constraint, Type)
import Data.HashMap.Strict as HMap
import Data.Maybe (isJust)
import Data.Text (Text)
import GHC.TypeLits
import NvFetcher.NixFetcher
import NvFetcher.Types
--------------------------------------------------------------------------------
-- | Atomic terms of package set
data PackageSetF f
= NewPackage !Package f
| forall a. EmbedIO !(IO a) (a -> f)
instance Functor PackageSetF where
fmap f (NewPackage p g) = NewPackage p $ f g
fmap f (EmbedIO action g) = EmbedIO action $ f <$> g
-- | Package set is a monad equipped with two capabilities:
--
-- 1. Carry defined packages
-- 2. Run IO actions
--
-- Package set is evaluated be for shake runs.
-- Use 'newPackage' to add a new package, 'liftIO' to run an IO action.
type PackageSet = Free PackageSetF
instance MonadIO PackageSet where
liftIO io = liftF $ EmbedIO io id
-- | Add a package to package set
newPackage ::
PackageName ->
VersionSource ->
PackageFetcher ->
PackageSet ()
newPackage name source fetcher = liftF $ NewPackage (Package name source fetcher) ()
-- | Add a list of packages into package set
purePackageSet :: [Package] -> PackageSet ()
purePackageSet = mapM_ (liftF . flip NewPackage ())
-- | Run package set into a set of packages
--
-- Throws exception as more then one packages with the same name
-- are defined
runPackageSet :: PackageSet () -> IO (HashMap PackageKey Package)
runPackageSet = \case
Free (NewPackage p g) ->
runPackageSet g >>= \m ->
if isJust (HMap.lookup (PackageKey $ pname p) m)
then fail $ "Duplicate package name: " <> show (pname p)
else pure $ HMap.insert (PackageKey $ pname p) p m
Free (EmbedIO action g) -> action >>= runPackageSet . g
Pure _ -> pure mempty
--------------------------------------------------------------------------------
-- | Simple HList
data Prod (r :: [Type]) where
Nil :: Prod '[]
Cons :: !x -> Prod xs -> Prod (x ': xs)
-- | Project elements from 'Prod'
class Member (a :: Type) (r :: [Type]) where
proj :: Prod r -> a
instance {-# OVERLAPPING #-} NotElem x xs => Member x (x ': xs) where
proj (Cons x _) = x
instance Member x xs => Member x (_y ': xs) where
proj (Cons _ r) = proj r
instance TypeError (ShowType x :<>: 'Text " is undefined") => Member x '[] where
proj = undefined
-- | Constraint for producing error messages
type family NotElem (x :: Type) (xs :: [Type]) :: Constraint where
NotElem x (x ': xs) = TypeError (ShowType x :<>: 'Text " is defined more than one times")
NotElem x (_ ': xs) = NotElem x xs
NotElem x '[] = ()
--------------------------------------------------------------------------------
-- | A tagless final style DSL for constructing packages
class PkgDSL f where
new :: f PackageName -> f (Prod '[PackageName])
andThen :: f (Prod r) -> f a -> f (Prod (a ': r))
end :: (Member PackageName r, Member VersionSource r, Member PackageFetcher r) => f (Prod r) -> f ()
instance PkgDSL PackageSet where
new e = do
name <- e
pure $ Cons name Nil
andThen e e' = do
p <- e
x <- e'
pure $ Cons x p
end e = do
p <- e
newPackage (proj p) (proj p) (proj p)
-- | 'PkgDSL' version of 'newPackage'
--
-- Example:
--
-- @
-- define $ package "nvfetcher-git" `sourceGit` "nvfetcher" `fetchGitHub` ("berberman", "nvfetcher")
-- @
define ::
( Member PackageName r,
Member VersionSource r,
Member PackageFetcher r
) =>
PackageSet (Prod r) ->
PackageSet ()
define = end
-- | Start chaining with the name of package to define
package :: PackageName -> PackageSet (Prod '[PackageName])
package = new . pure
-- | Attach version sources
src :: PackageSet (Prod r) -> VersionSource -> PackageSet (Prod (VersionSource ': r))
src = (. pure) . andThen
-- | Attach fetchers
fetch ::
PackageSet (Prod r) ->
PackageFetcher ->
PackageSet (Prod (PackageFetcher ': r))
fetch = (. pure) . andThen
--------------------------------------------------------------------------------
-- | A synonym of 'fetchGitHub' and 'sourceGitHub'
fromGitHub ::
PackageSet (Prod r) ->
(Text, Text) ->
PackageSet
(Prod (PackageFetcher : VersionSource : r))
fromGitHub e p = fetchGitHub (sourceGitHub e p) p
-- | A synonym of 'fetchPypi' and 'sourcePypi'
fromPypi ::
PackageSet (Prod r) ->
Text ->
PackageSet
(Prod (PackageFetcher : VersionSource : r))
fromPypi e p = fetchPypi (sourcePypi e p) p
--------------------------------------------------------------------------------
-- | This package follows the latest github release
sourceGitHub ::
PackageSet (Prod r) ->
-- | owner and repo
(Text, Text) ->
PackageSet (Prod (VersionSource : r))
sourceGitHub e (owner, repo) = src e $ GitHubRelease owner repo
-- | This package follows the latest git commit
sourceGit ::
PackageSet (Prod r) ->
-- | git url
Text ->
PackageSet (Prod (VersionSource : r))
sourceGit e vurl = src e Git {..}
-- | This package follows the latest pypi release
sourcePypi ::
PackageSet (Prod r) ->
-- | pypi name
Text ->
PackageSet (Prod (VersionSource : r))
sourcePypi e pypi = src e Pypi {..}
-- | This package follows the version of an Arch Linux package
sourceArchLinux ::
PackageSet (Prod r) ->
-- | package name in Arch Linux repo
Text ->
PackageSet (Prod (VersionSource : r))
sourceArchLinux e archpkg = src e ArchLinux {..}
-- | This package follows the version of an Aur package
sourceAur ::
PackageSet (Prod r) ->
-- | package name in Aur
Text ->
PackageSet (Prod (VersionSource : r))
sourceAur e aur = src e Aur {..}
-- | This package follows a pinned version
sourceManual ::
PackageSet (Prod r) ->
Text ->
PackageSet (Prod (VersionSource : r))
sourceManual e manual = src e Manual {..}
-- | This package follows the version of a repology package
sourceRepology ::
PackageSet (Prod r) ->
-- | repology project name and repo
(Text, Text) ->
PackageSet (Prod (VersionSource : r))
sourceRepology e (project, repo) = src e $ Repology project repo
--------------------------------------------------------------------------------
-- | This package is fetched from a github repo
fetchGitHub ::
PackageSet (Prod r) ->
-- | owner and repo
(Text, Text) ->
PackageSet (Prod (PackageFetcher : r))
fetchGitHub e p = fetch e $ gitHubFetcher p
-- | This package is fetched from a file in github release
fetchGitHubRelease ::
PackageSet (Prod r) ->
-- | owner, repo, and file name
(Text, Text, Text) ->
PackageSet (Prod (PackageFetcher : r))
fetchGitHubRelease e (owner, repo, fp) = fetch e $ gitHubReleaseFetcher (owner, repo) fp
-- | This package is fetched from pypi
fetchPypi ::
PackageSet (Prod r) ->
-- | pypi name
Text ->
PackageSet (Prod (PackageFetcher : r))
fetchPypi e = fetch e . pypiFetcher
-- | This package is fetched from git
fetchGit ::
PackageSet (Prod r) ->
-- | git url
Text ->
PackageSet (Prod (PackageFetcher : r))
fetchGit e = fetch e . gitFetcher
-- | This package is fetched from url
fetchUrl ::
PackageSet (Prod r) ->
-- | url, given a specific version
(Version -> Text) ->
PackageSet (Prod (PackageFetcher : r))
fetchUrl e f = fetch e (urlFetcher . f)
--------------------------------------------------------------------------------