{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE NoMonomorphismRestriction #-} {-# LANGUAGE Rank2Types #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE UndecidableInstances #-} -- for From (Word *) d module Symantic.CLI.Schema where import Control.Applicative (Applicative(..)) import Data.Bool import Data.Char (Char) import Data.Function (($), (.), id) import Data.Functor (Functor(..), (<$>)) import Data.Maybe (Maybe(..), fromMaybe, catMaybes) import Data.Monoid (Monoid(..)) import Data.Semigroup (Semigroup(..)) import Data.String (String, IsString(..)) import Data.Text (Text) import Text.Show (Show(..)) import qualified Data.List as List import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Builder as TLB import qualified Symantic.Document as Doc import Symantic.CLI.API import Symantic.CLI.Fixity -- * Type 'Schema' newtype Schema d f k = Schema { unSchema :: SchemaInh d -> Maybe d } runSchema :: Monoid d => Schema d f k -> SchemaInh d -> d runSchema (Schema s) = fromMaybe mempty . s docSchema :: Monoid d => SchemaDoc d => Schema d f k -> d docSchema s = runSchema s defSchemaInh coerceSchema :: Schema d f k -> Schema d f' k' coerceSchema Schema{..} = Schema{..} -- ** Class 'SchemaDoc' type SchemaDoc d = ( Semigroup d , Monoid d , IsString d , Doc.Colorable16 d , Doc.Decorable d , Doc.Spaceable d , Doc.Indentable d , Doc.Wrappable d , Doc.From (Doc.Word Char) d , Doc.From (Doc.Word Text) d , Doc.From (Doc.Word String) d ) -- ** Type 'SchemaInh' -- | Inherited top-down. data SchemaInh d = SchemaInh { schemaInh_op :: (Infix, Side) -- ^ Parent operator. , schemaInh_define :: Bool -- ^ Whether to print a definition, or not. , schemaInh_or :: d } defSchemaInh :: SchemaDoc d => SchemaInh d defSchemaInh = SchemaInh { schemaInh_op = (infixN0, SideL) , schemaInh_define = True , schemaInh_or = docOrH } pairIfNeeded :: SchemaDoc d => (Infix, Side) -> Infix -> d -> d pairIfNeeded opInh op = if needsParenInfix opInh op then Doc.align . Doc.parens else id instance Semigroup d => Semigroup (Schema d f k) where Schema x <> Schema y = Schema $ x <> y instance (Semigroup d, Monoid d) => Monoid (Schema d f k) where mempty = Schema mempty mappend = (<>) instance (Semigroup d, IsString d) => IsString (Schema d f k) where fromString "" = Schema $ \_inh -> Nothing fromString s = Schema $ \_inh -> Just $ fromString s instance Show (Schema (Doc.Plain TLB.Builder) a k) where show = TL.unpack . TLB.toLazyText . Doc.runPlain . docSchema docOrH, docOrV :: Doc.Spaceable d => Doc.From (Doc.Word Char) d => d docOrH = Doc.space <> Doc.from (Doc.Word '|') <> Doc.space docOrV = Doc.newline <> Doc.from (Doc.Word '|') <> Doc.space {- instance SchemaDoc d => Functor (Schema d f) where _f `fmap` Schema x = Schema $ \inh -> pairIfNeeded (schemaInh_op inh) op <$> x inh{schemaInh_op=(op, SideR)} where op = infixB SideL 10 -} instance SchemaDoc d => App (Schema d) where Schema f <.> Schema x = Schema $ \inh -> case f inh{schemaInh_op=(op, SideL)} of Nothing -> x inh{schemaInh_op=(op, SideR)} Just fd -> case x inh{schemaInh_op=(op, SideR)} of Nothing -> Just fd Just xd -> Just $ pairIfNeeded (schemaInh_op inh) op $ fd <> Doc.space <> xd where op = infixB SideL 10 instance SchemaDoc d => Alt (Schema d) where l r = Schema $ \inh -> -- NOTE: first try to see if both sides are 'Just', -- otherwise does not change the inherited operator context. case (unSchema l inh, unSchema r inh) of (Nothing, Nothing) -> Nothing (Just ld, Nothing) -> Just ld (Nothing, Just rd) -> Just rd (Just{}, Just{}) -> Just $ if needsParenInfix (schemaInh_op inh) op then -- NOTE: when parenthesis are needed -- first try to fit the alternative on a single line, -- otherwise align them on multiple lines. Doc.breakalt (Doc.parens $ -- Doc.withBreakable Nothing $ runSchema l inh { schemaInh_op=(op, SideL) , schemaInh_or=docOrH } <> docOrH <> runSchema r inh { schemaInh_op=(op, SideR) , schemaInh_or=docOrH }) (Doc.align $ Doc.parens $ Doc.space <> runSchema l inh { schemaInh_op=(op, SideL) , schemaInh_or=docOrV } <> docOrV <> runSchema r inh { schemaInh_op=(op, SideR) , schemaInh_or=docOrV } <> Doc.newline) else -- NOTE: when parenthesis are NOT needed -- just concat alternatives using the inherited separator -- (either horizontal or vertical). runSchema l inh{schemaInh_op=(op, SideL)} <> schemaInh_or inh <> runSchema r inh{schemaInh_op=(op, SideR)} where op = infixB SideL 2 alt x y = coerceSchema $ coerceSchema x coerceSchema y opt s = Schema $ \inh -> Just $ Doc.brackets $ runSchema s inh{schemaInh_op=(op, SideL)} where op = infixN0 instance Pro (Schema d) where dimap _a2b _b2a = coerceSchema instance SchemaDoc d => AltApp (Schema d) where many0 s = Schema $ \inh -> Just $ runSchema s inh{schemaInh_op=(op, SideL)}<>"*" where op = infixN 10 many1 s = Schema $ \inh -> Just $ runSchema s inh{schemaInh_op=(op, SideL)}<>"+" where op = infixN 10 instance SchemaDoc d => CLI_Command (Schema d) where -- type CommandConstraint (Schema d) a = () command n s = Schema $ \inh -> Just $ if schemaInh_define inh || List.null n then Doc.align $ runSchema (fromString n <.> coerceSchema s) inh{schemaInh_define = False} else ref where ref = Doc.bold $ Doc.angles $ Doc.magentaer $ Doc.from (Doc.Word n) instance SchemaDoc d => CLI_Var (Schema d) where type VarConstraint (Schema d) a = () var' n = Schema $ \_inh -> Just $ Doc.underline $ Doc.from $ Doc.Word n just _ = Schema $ \_inh -> Nothing nothing = Schema $ \_inh -> Nothing instance SchemaDoc d => CLI_Env (Schema d) where type EnvConstraint (Schema d) a = () env' _n = Schema $ \_inh -> Nothing -- NOTE: environment variables are not shown in the schema, -- only in the help. instance SchemaDoc d => CLI_Tag (Schema d) where type TagConstraint (Schema d) a = () tagged n r = Schema $ \inh -> unSchema (prefix n <.> r) inh where prefix = \case Tag s l -> prefix (TagShort s)<>"|"<>prefix (TagLong l) TagShort s -> fromString ['-', s] TagLong l -> fromString ("--"<>l) endOpts = Schema $ \_inh -> Just $ Doc.brackets "--" instance SchemaDoc d => CLI_Help (Schema d) where type HelpConstraint (Schema d) d' = d ~ d' help _msg = id program n s = Schema $ \inh -> Just $ runSchema (fromString n <.> coerceSchema s) inh{schemaInh_define = False} rule n s = Schema $ \inh -> Just $ if schemaInh_define inh then runSchema s inh{schemaInh_define=False} else ref where ref = Doc.bold $ Doc.angles $ Doc.magentaer $ Doc.from (Doc.Word n) data SchemaResponseArgs a instance SchemaDoc d => CLI_Response (Schema d) where type ResponseConstraint (Schema d) a = () type ResponseArgs (Schema d) a = SchemaResponseArgs a type Response (Schema d) = () response' = Schema $ \_inh -> Nothing -- ** Type 'SchemaPerm' data SchemaPerm d k a = SchemaPerm { schemaPerm_finalizer :: forall b c. Schema d (b->c) c -> Schema d (b->c) c -- ^ Used to implement 'rule'. , schemaPerm_alternatives :: [Schema d (a->k) k] -- ^ Collect alternatives for rendering them all at once in 'runPermutation'. } instance Functor (SchemaPerm d k) where _f`fmap`SchemaPerm fin ps = SchemaPerm fin (coerceSchema <$> ps) instance Applicative (SchemaPerm d k) where pure _a = SchemaPerm id mempty SchemaPerm fd f <*> SchemaPerm fx x = SchemaPerm (fd . fx) $ (coerceSchema <$> f) <> (coerceSchema <$> x) instance SchemaDoc d => Permutable (Schema d) where type Permutation (Schema d) = SchemaPerm d runPermutation (SchemaPerm fin ps) = case ps of [] -> fin $ Schema $ \_inh -> Nothing _ -> fin $ Schema $ \inh -> Just $ Doc.intercalate Doc.breakspace $ catMaybes $ (<$> ps) $ \(Schema s) -> s inh { schemaInh_op=(op, SideL) , schemaInh_or=docOrH } where op = infixN 10 toPermutation = SchemaPerm id . pure toPermDefault _a s = SchemaPerm id $ pure $ Schema $ \inh -> Just $ if needsParenInfix (schemaInh_op inh) op then Doc.brackets $ runSchema s inh{schemaInh_op=(op, SideL), schemaInh_or=docOrH} else runSchema s inh{schemaInh_op=(op, SideL)} where op = infixN0 instance SchemaDoc d => CLI_Help (SchemaPerm d) where type HelpConstraint (SchemaPerm d) d' = d ~ d' help _msg = id program n (SchemaPerm fin ps) = SchemaPerm (program n . fin) ps rule n (SchemaPerm fin ps) = SchemaPerm (rule n . fin) ps