{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE UndecidableInstances #-} module Options.Harg.Subcommands ( Subcommands (..), ) where import qualified Barbies as B import Data.Functor.Compose (Compose (..)) import Data.Kind (Type) import Data.Proxy (Proxy (..)) import GHC.TypeLits (KnownSymbol, Symbol, symbolVal) import qualified Options.Applicative as Optparse import Options.Harg.Cmdline (mkOptparseParser) import Options.Harg.Het.All (All) import Options.Harg.Het.HList (AssocListF (..)) import Options.Harg.Het.Nat import Options.Harg.Het.Proofs (Proof (..), hgcastWith, type (++)) import Options.Harg.Het.Variant (InjectPosF (..), VariantF) import Options.Harg.Sources (accumSourceResults) import Options.Harg.Sources.Types import Options.Harg.Types -- | This class can be used with an 'AssocList'. It returns the appropriate -- list of 'Optparse.CommandFields' in order to create a subcommand parser. -- Given the sources to use and the association list between the command string -- and the command type, it returns the list of command field modifiers and a -- list of errors. -- -- The result can be used as follows: -- -- @ -- ... -- (errs, commands) = 'mapSubcommand' sources opts -- parser = 'Optparse.subparser' ('mconcat' commands) -- ... -- @ -- -- In order to be able to create a subcommand parser for a heterogeneous list -- of options (rather than a sum with different constructors), the return type -- should also be heterogeneous. Here, we return a Variant, which is a more -- generic version of 'Either'. In order to do that, 'mapSubcommand' traverses -- the association list and creates an injection into the Variant, according to -- the current position. So an 'AssocList' like this: -- -- @ -- opts :: AssocList '["run", "test"] '[RunConfig, TestConfig] Opt -- opts = ... -- @ -- -- Should return @VariantF '[RunConfig, TestConfig] Identity@. In order to do -- that, it will inject @RunConfig@ based on its position (0) using @HereF@, -- and @TestConfig@ using @ThereF . HereF@ because its position is 1. class Subcommands (ts :: [Symbol]) (xs :: [(Type -> Type) -> Type]) where mapSubcommand :: ( All (RunSource s) xs, Applicative f ) => s -> AssocListF ts xs (Compose Opt f) -> ([SourceRunError], [Optparse.Mod Optparse.CommandFields (VariantF xs f)]) instance ExplSubcommands Z ts xs '[] => Subcommands ts xs where mapSubcommand = explMapSubcommand @Z @ts @xs @'[] SZ -- | More general version of 'Subcommands'. class ExplSubcommands (n :: Nat) (ts :: [Symbol]) (xs :: [(Type -> Type) -> Type]) (acc :: [(Type -> Type) -> Type]) where explMapSubcommand :: ( All (RunSource s) xs, Applicative f ) => SNat n -> s -> AssocListF ts xs (Compose Opt f) -> ([SourceRunError], [Optparse.Mod Optparse.CommandFields (VariantF (acc ++ xs) f)]) instance ExplSubcommands n '[] '[] acc where explMapSubcommand _ _ _ = ([], []) -- ok wait -- hear me out: instance ( ExplSubcommands (S n) ts xs (as ++ '[x]), -- get the correct injection into the variant by position InjectPosF n x (as ++ (x ': xs)), B.TraversableB x, B.ApplicativeB x, KnownSymbol t, -- prove that xs ++ (y : ys) ~ (xs ++ [y]) ++ ys Proof as x xs ) => ExplSubcommands n (t ': ts) (x ': xs) as where explMapSubcommand n srcs (ACons opt opts) = (thisErr ++ restErr, sc : rest) where (thisErr, sc) = subcommand (restErr, rest) = hgcastWith (proof @as @x @xs) $ explMapSubcommand @(S n) @ts @xs @(as ++ '[x]) (SS n) srcs opts subcommand = let (errs, src) = accumSourceResults $ runSource srcs opt parser = mkOptparseParser src opt tag = symbolVal (Proxy :: Proxy t) cmd = Optparse.command tag $ injectPosF n <$> Optparse.info (Optparse.helper <*> parser) mempty in (errs, cmd)