packages feed

symantic-cli-2.4.0.20190719: Symantic/CLI/Schema.hs

{-# 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 SchemaDoc d => Sequenceable (Schema d) where
	type Sequence (Schema d) = SchemaSeq d
	runSequence (SchemaSeq fin ps) =
		case ps of
		 [] -> fin $ Schema $ \_inh -> Nothing
		 _ -> fin $ Schema $ \inh -> Just $
			pairIfNeeded (schemaInh_op inh) op $
			Doc.intercalate Doc.breakspace $
			catMaybes $ (<$> ps) $ \(Schema s) ->
				s inh
				 { schemaInh_op=(op, SideL)
				 , schemaInh_or=docOrH }
		where op = infixN 10
	toSequence = SchemaSeq id . pure
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 $
			pairIfNeeded (schemaInh_op inh) op $
			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 Pro (Schema d) where
	dimap _a2b _b2a = coerceSchema
instance SchemaDoc d => AltApp (Schema d) where
	many0 s = Schema $ \inh -> Just $
		pairIfNeeded (schemaInh_op inh) op $
		runSchema s inh{schemaInh_op=(op, SideL)}<>"*"
		where op = infixN 11
	many1 s = Schema $ \inh -> Just $
		pairIfNeeded (schemaInh_op inh) op $
		runSchema s inh{schemaInh_op=(op, SideL)}<>"+"
		where op = infixN 11
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 = ()
	tag 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 'SchemaSeq'
data SchemaSeq d k a = SchemaSeq
 { schemaSeq_finalizer :: forall b c.
                           Schema d (b->c) c ->
                           Schema d (b->c) c
   -- ^ Used to implement 'rule'.
 , schemaSeq_alternatives :: [Schema d (a->k) k]
   -- ^ Collect alternatives for rendering them all at once in 'runSequence'.
 }
instance Functor (SchemaSeq d k) where
	_f`fmap`SchemaSeq fin ps = SchemaSeq fin (coerceSchema <$> ps)
instance Applicative (SchemaSeq d k) where
	pure _a = SchemaSeq id mempty
	SchemaSeq fd f <*> SchemaSeq fx x =
		SchemaSeq (fd . fx) $ (coerceSchema <$> f) <> (coerceSchema <$> x)
instance SchemaDoc d => CLI_Help (SchemaSeq d) where
	type HelpConstraint (SchemaSeq d) d' = d ~ d'
	help _msg = id
	program n (SchemaSeq fin ps) = SchemaSeq (program n . fin) ps
	rule n (SchemaSeq fin ps) = SchemaSeq (rule n . fin) ps

-- ** 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 => 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