packages feed

mangrove-cli-0.4.0.0: src/Mangrove/Parser.hs

{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE DeriveFunctor         #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE GADTs                 #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE QuantifiedConstraints #-}
{-# LANGUAGE TypeFamilies          #-}

{-|
Module      : Mangrove.Parser
Copyright   : (c) Quytelda Kahja, 2026
License     : BSD-3-Clause

This module contains types and functions necessary for running
argument parsers. These functions are generic across different parsing
schemes.

Most clients won't import this module directly, since its contents are
re-exported by the "Mangrove" module alongside other helpful symbols.
-}
module Mangrove.Parser
  ( -- * Standard Interface
    parseArguments

    -- * Pure Interface
  , Result(..)
  , runArgumentParser
  , runArgumentParser'

    -- * Feeding Parser Trees
  , satiate
  ) where

import           Control.Applicative
import           Data.Text           (Text)
import qualified Data.Text           as T
import qualified Data.Text.IO        as TIO
import           System.Environment
import           System.Exit
import           System.IO

import           Mangrove.ParseTree
import           Mangrove.Render
import           Mangrove.Resolve
import           Mangrove.Scheme
import           Mangrove.Stream
import           Mangrove.Token

--------------------------------------------------------------------------------
-- Feeding ParseTrees

-- | 'feed' traverses the tree until it activates a parser that
-- consumes input. When a subtree successfully consumes input, it is
-- replaced with an updated subtree and the traversal ceases.
feed :: Scheme s => ParseTree s r -> StreamParser (Request s) (Token s) (ParseTree s r)
feed EmptyNode = empty
feed (ValueNode _) = empty
feed (ParseNode parser) = ValueNode <$> activate parser
feed (ProdNode f l r) =
  (ProdNode f <$> feed l <*> pure r) <|>
  (ProdNode f l <$> feed r)
feed (SumNode l r) = feed l <|> feed r
feed (ManyNode _ tree) =
  ProdNode (:)
  <$> feed tree
  <*> pure (ManyNode False tree)

-- | Repeatedly traverse the tree, each time activating the first
-- parser that can consume available input, until no more input can be
-- consumed.
satiate :: Scheme s => ParseTree s r -> StreamParser (Request s) (Token s) (ParseTree s r)
satiate tree = do
  parseSpecials
  result <- optional $ feed tree
  case result of
    Just tree' -> satiate tree'
    Nothing    -> pure tree

--------------------------------------------------------------------------------
-- Running Parsers

-- | Create a default initial t'StreamState' from a list of arguments.
argsToState :: [Text] -> StreamState s
argsToState args = StreamState args [] False

-- | The result of an argument parsing operation.
data Result req a
  = Success ![Text] !a
  | Failure !Text
  | Request !req
  deriving (Eq, Functor, Show)

-- | Resolve the output of a parsing operation and sink it into a
-- 'Result'.
sinkResult
  :: Scheme s
  => StreamHandler (Request s) (Token s) (ParseTree s r) (Result (Request s) r)
sinkResult = StreamHandler
  { onSuccess = _onSuccess
  , onEmpty   = _onEmpty
  , onFailure = _onFailure
  , onRequest = _onRequest
  }
  where
    _onFailure state' = Failure . formatError (streamContext state')
    _onEmpty = flip _onFailure "empty"
    _onSuccess state' tree' =
      case (streamContent state', resolve tree') of
        (leftovers, Value result) -> Success leftovers result
        ([], EmptyError)          -> _onFailure state' "empty"
        ([], ExpectedError es)    -> _onFailure state' $ renderExpectedError es
        (token:_, _)              -> _onFailure state' $ "unexpected " <> render token
    _onRequest _ = Request

-- | A more general form of 'runArgumentParser' that accepts a custom
-- stream starting state.
runArgumentParser'
  :: Scheme s
  => ParseTree s r
  -> StreamState (Token s)
  -> Result (Request s) r
runArgumentParser' tree =
  runStreamParser (satiate tree) sinkResult

-- | Satiate a 'ParseTree' with all the input it can consume, then
-- attempt to evaluate it. Empty results are treated as failures.
runArgumentParser
  :: Scheme s
  => ParseTree s r
  -> [Text]
  -> Result (Request s) r
runArgumentParser tree =
  runArgumentParser' tree . argsToState

-- | Parse the command line arguments passed to the program, then
-- invoke the program's entrypoint with the results of the parsing. If
-- parsing fails, we instead display an error to stderr and exit.
-- Alternatively, if information was requested, we abandon parsing and
-- print the response to stdout, then exit without indicating an
-- error.
parseArguments
  :: Scheme s
  => ProgramInfo -- ^ Program metadata
  -> ParseTree s r -- ^ Argument parser
  -> (r -> IO a) -- ^ Program entrypoint
  -> IO a
parseArguments info tree action = do
  args <- map T.pack <$> getArgs
  case runArgumentParser tree args of
    Success [] result -> action result
    Success (token:_) _ -> do
      hPutBuilder stderr $ "unexpected " <> render token <> "\n"
      exitFailure
    Failure err -> do
      TIO.hPutStrLn stderr err
      exitFailure
    Request req -> do
      TIO.putStr $ respond req tree info
      exitSuccess