graphql/src/Language/GraphQL/AST/Encoder.hs

290 lines
10 KiB
Haskell
Raw Normal View History

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ExplicitForAll #-}
-- | This module defines a minifier and a printer for the @GraphQL@ language.
module Language.GraphQL.AST.Encoder
2019-08-03 23:57:27 +02:00
( Formatter
, definition
2019-08-14 08:49:07 +02:00
, directive
, document
2019-08-03 23:57:27 +02:00
, minified
, pretty
, type'
, value
2019-07-14 05:58:05 +02:00
) where
2019-12-20 07:58:09 +01:00
import Data.Char (ord)
import Data.Foldable (fold)
import Data.Monoid ((<>))
2019-12-20 07:58:09 +01:00
import qualified Data.List.NonEmpty as NonEmpty
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Lazy as Lazy (Text)
import qualified Data.Text.Lazy as Lazy.Text
import qualified Data.Text.Lazy.Builder as Builder
2019-12-20 07:58:09 +01:00
import Data.Text.Lazy.Builder.Int (decimal, hexadecimal)
import Data.Text.Lazy.Builder.RealFloat (realFloat)
import qualified Language.GraphQL.AST as Full
-- | Instructs the encoder whether the GraphQL document should be minified or
-- pretty printed.
--
-- Use 'pretty' or 'minified' to construct the formatter.
data Formatter
= Minified
2019-08-03 23:57:27 +02:00
| Pretty Word
-- | Constructs a formatter for pretty printing.
2019-08-03 23:57:27 +02:00
pretty :: Formatter
pretty = Pretty 0
-- | Constructs a formatter for minifying.
2019-08-03 23:57:27 +02:00
minified :: Formatter
minified = Minified
-- | Converts a 'Full.Document' into a string.
2019-12-20 07:58:09 +01:00
document :: Formatter -> Full.Document -> Lazy.Text
document formatter defs
2019-12-20 07:58:09 +01:00
| Pretty _ <- formatter = Lazy.Text.intercalate "\n" encodeDocument
| Minified <-formatter = Lazy.Text.snoc (mconcat encodeDocument) '\n'
where
encodeDocument = NonEmpty.toList $ definition formatter <$> defs
-- | Converts a 'Full.Definition' into a string.
2019-12-20 07:58:09 +01:00
definition :: Formatter -> Full.Definition -> Lazy.Text
definition formatter x
2019-12-20 07:58:09 +01:00
| Pretty _ <- formatter = Lazy.Text.snoc (encodeDefinition x) '\n'
| Minified <- formatter = encodeDefinition x
where
encodeDefinition (Full.DefinitionOperation operation)
= operationDefinition formatter operation
encodeDefinition (Full.DefinitionFragment fragment)
= fragmentDefinition formatter fragment
2019-12-20 07:58:09 +01:00
operationDefinition :: Formatter -> Full.OperationDefinition -> Lazy.Text
operationDefinition formatter (Full.OperationSelectionSet sels)
= selectionSet formatter sels
operationDefinition formatter (Full.OperationDefinition Full.Query name vars dirs sels)
= "query " <> node formatter name vars dirs sels
operationDefinition formatter (Full.OperationDefinition Full.Mutation name vars dirs sels)
= "mutation " <> node formatter name vars dirs sels
2019-12-20 07:58:09 +01:00
node :: Formatter ->
Maybe Full.Name ->
[Full.VariableDefinition] ->
[Full.Directive] ->
Full.SelectionSet ->
Lazy.Text
node formatter name vars dirs sels
2019-12-20 07:58:09 +01:00
= Lazy.Text.fromStrict (fold name)
<> optempty (variableDefinitions formatter) vars
<> optempty (directives formatter) dirs
<> eitherFormat formatter " " mempty
<> selectionSet formatter sels
2019-12-20 07:58:09 +01:00
variableDefinitions :: Formatter -> [Full.VariableDefinition] -> Lazy.Text
variableDefinitions formatter
= parensCommas formatter $ variableDefinition formatter
2019-12-20 07:58:09 +01:00
variableDefinition :: Formatter -> Full.VariableDefinition -> Lazy.Text
variableDefinition formatter (Full.VariableDefinition var ty dv)
= variable var
<> eitherFormat formatter ": " ":"
<> type' ty
<> maybe mempty (defaultValue formatter) dv
2019-12-20 07:58:09 +01:00
defaultValue :: Formatter -> Full.Value -> Lazy.Text
defaultValue formatter val
= eitherFormat formatter " = " "="
<> value formatter val
2019-12-20 07:58:09 +01:00
variable :: Full.Name -> Lazy.Text
variable var = "$" <> Lazy.Text.fromStrict var
2019-12-20 07:58:09 +01:00
selectionSet :: Formatter -> Full.SelectionSet -> Lazy.Text
2019-08-03 23:57:27 +02:00
selectionSet formatter
= bracesList formatter (selection formatter)
. NonEmpty.toList
2019-12-20 07:58:09 +01:00
selectionSetOpt :: Formatter -> Full.SelectionSetOpt -> Lazy.Text
2019-08-03 23:57:27 +02:00
selectionSetOpt formatter = bracesList formatter $ selection formatter
2019-12-20 07:58:09 +01:00
selection :: Formatter -> Full.Selection -> Lazy.Text
selection formatter = Lazy.Text.append indent . f
2019-08-03 23:57:27 +02:00
where
f (Full.SelectionField x) = field incrementIndent x
f (Full.SelectionInlineFragment x) = inlineFragment incrementIndent x
f (Full.SelectionFragmentSpread x) = fragmentSpread incrementIndent x
2019-08-03 23:57:27 +02:00
incrementIndent
| Pretty n <- formatter = Pretty $ n + 1
| otherwise = Minified
indent
2019-12-20 07:58:09 +01:00
| Pretty n <- formatter = Lazy.Text.replicate (fromIntegral $ n + 1) " "
2019-08-03 23:57:27 +02:00
| otherwise = mempty
2019-12-20 07:58:09 +01:00
field :: Formatter -> Full.Field -> Lazy.Text
field formatter (Full.Field alias name args dirs selso)
2019-12-20 07:58:09 +01:00
= optempty (`Lazy.Text.append` colon) (Lazy.Text.fromStrict $ fold alias)
<> Lazy.Text.fromStrict name
<> optempty (arguments formatter) args
<> optempty (directives formatter) dirs
<> selectionSetOpt'
where
colon = eitherFormat formatter ": " ":"
selectionSetOpt'
| null selso = mempty
| otherwise = eitherFormat formatter " " mempty <> selectionSetOpt formatter selso
2019-12-20 07:58:09 +01:00
arguments :: Formatter -> [Full.Argument] -> Lazy.Text
arguments formatter = parensCommas formatter $ argument formatter
2019-12-20 07:58:09 +01:00
argument :: Formatter -> Full.Argument -> Lazy.Text
argument formatter (Full.Argument name v)
2019-12-20 07:58:09 +01:00
= Lazy.Text.fromStrict name
<> eitherFormat formatter ": " ":"
<> value formatter v
-- * Fragments
2019-12-20 07:58:09 +01:00
fragmentSpread :: Formatter -> Full.FragmentSpread -> Lazy.Text
fragmentSpread formatter (Full.FragmentSpread name ds)
2019-12-20 07:58:09 +01:00
= "..." <> Lazy.Text.fromStrict name <> optempty (directives formatter) ds
2019-12-20 07:58:09 +01:00
inlineFragment :: Formatter -> Full.InlineFragment -> Lazy.Text
inlineFragment formatter (Full.InlineFragment tc dirs sels)
= "... on "
2019-12-20 07:58:09 +01:00
<> Lazy.Text.fromStrict (fold tc)
<> directives formatter dirs
<> eitherFormat formatter " " mempty
<> selectionSet formatter sels
2019-12-20 07:58:09 +01:00
fragmentDefinition :: Formatter -> Full.FragmentDefinition -> Lazy.Text
fragmentDefinition formatter (Full.FragmentDefinition name tc dirs sels)
2019-12-20 07:58:09 +01:00
= "fragment " <> Lazy.Text.fromStrict name
<> " on " <> Lazy.Text.fromStrict tc
<> optempty (directives formatter) dirs
<> eitherFormat formatter " " mempty
<> selectionSet formatter sels
2019-08-14 08:49:07 +02:00
-- * Miscellaneous
-- | Converts a 'Full.Directive' into a string.
2019-12-20 07:58:09 +01:00
directive :: Formatter -> Full.Directive -> Lazy.Text
directive formatter (Full.Directive name args)
2019-12-20 07:58:09 +01:00
= "@" <> Lazy.Text.fromStrict name <> optempty (arguments formatter) args
2019-12-20 07:58:09 +01:00
directives :: Formatter -> [Full.Directive] -> Lazy.Text
directives formatter@(Pretty _) = Lazy.Text.cons ' ' . spaces (directive formatter)
2019-08-14 08:49:07 +02:00
directives Minified = spaces (directive Minified)
-- | Converts a 'Full.Value' into a string.
2019-12-20 07:58:09 +01:00
value :: Formatter -> Full.Value -> Lazy.Text
value _ (Full.Variable x) = variable x
2019-12-20 07:58:09 +01:00
value _ (Full.Int x) = Builder.toLazyText $ decimal x
value _ (Full.Float x) = Builder.toLazyText $ realFloat x
value _ (Full.Boolean x) = booleanValue x
value _ Full.Null = mempty
2019-12-20 07:58:09 +01:00
value _ (Full.String x) = stringValue x
value _ (Full.Enum x) = Lazy.Text.fromStrict x
value formatter (Full.List x) = listValue formatter x
value formatter (Full.Object x) = objectValue formatter x
2019-12-20 07:58:09 +01:00
booleanValue :: Bool -> Lazy.Text
booleanValue True = "true"
booleanValue False = "false"
2019-12-20 07:58:09 +01:00
stringValue :: Text -> Lazy.Text
stringValue string = Builder.toLazyText
$ quote
2019-12-20 07:58:09 +01:00
<> Text.foldr (mappend . replace) quote string
where
2019-12-20 07:58:09 +01:00
replace char
| char == '\\' = Builder.fromString "\\\\"
| char == '\"' = Builder.fromString "\\\""
| char == '\b' = Builder.fromString "\\b"
| char == '\f' = Builder.fromString "\\f"
| char == '\n' = Builder.fromString "\\n"
| char == '\r' = Builder.fromString "\\r"
| char < '\x0010' = unicode "\\u000" char
| char < '\x0020' = unicode "\\u00" char
| otherwise = Builder.singleton char
quote = Builder.singleton '\"'
2019-12-20 07:58:09 +01:00
unicode prefix char = Builder.fromString prefix <> hexadecimal (ord char)
2019-12-20 07:58:09 +01:00
listValue :: Formatter -> [Full.Value] -> Lazy.Text
listValue formatter = bracketsCommas formatter $ value formatter
2019-12-20 07:58:09 +01:00
objectValue :: Formatter -> [Full.ObjectField] -> Lazy.Text
2019-08-03 23:57:27 +02:00
objectValue formatter = intercalate $ objectField formatter
where
intercalate f
= braces
2019-12-20 07:58:09 +01:00
. Lazy.Text.intercalate (eitherFormat formatter ", " ",")
2019-08-03 23:57:27 +02:00
. fmap f
2019-12-20 07:58:09 +01:00
objectField :: Formatter -> Full.ObjectField -> Lazy.Text
objectField formatter (Full.ObjectField name v)
2019-12-20 07:58:09 +01:00
= Lazy.Text.fromStrict name <> colon <> value formatter v
where
colon
| Pretty _ <- formatter = ": "
| Minified <- formatter = ":"
-- | Converts a 'Full.Type' a type into a string.
2019-12-20 07:58:09 +01:00
type' :: Full.Type -> Lazy.Text
type' (Full.TypeNamed x) = Lazy.Text.fromStrict x
type' (Full.TypeList x) = listType x
type' (Full.TypeNonNull x) = nonNullType x
2019-12-20 07:58:09 +01:00
listType :: Full.Type -> Lazy.Text
listType x = brackets (type' x)
2019-12-20 07:58:09 +01:00
nonNullType :: Full.NonNullType -> Lazy.Text
nonNullType (Full.NonNullTypeNamed x) = Lazy.Text.fromStrict x <> "!"
nonNullType (Full.NonNullTypeList x) = listType x <> "!"
-- * Internal
2019-12-20 07:58:09 +01:00
between :: Char -> Char -> Lazy.Text -> Lazy.Text
between open close = Lazy.Text.cons open . (`Lazy.Text.snoc` close)
2019-12-20 07:58:09 +01:00
parens :: Lazy.Text -> Lazy.Text
parens = between '(' ')'
2019-12-20 07:58:09 +01:00
brackets :: Lazy.Text -> Lazy.Text
brackets = between '[' ']'
2019-12-20 07:58:09 +01:00
braces :: Lazy.Text -> Lazy.Text
braces = between '{' '}'
2019-12-20 07:58:09 +01:00
spaces :: forall a. (a -> Lazy.Text) -> [a] -> Lazy.Text
spaces f = Lazy.Text.intercalate "\SP" . fmap f
2019-12-20 07:58:09 +01:00
parensCommas :: forall a. Formatter -> (a -> Lazy.Text) -> [a] -> Lazy.Text
parensCommas formatter f
= parens
2019-12-20 07:58:09 +01:00
. Lazy.Text.intercalate (eitherFormat formatter ", " ",")
. fmap f
2019-12-20 07:58:09 +01:00
bracketsCommas :: Formatter -> (a -> Lazy.Text) -> [a] -> Lazy.Text
bracketsCommas formatter f
= brackets
2019-12-20 07:58:09 +01:00
. Lazy.Text.intercalate (eitherFormat formatter ", " ",")
. fmap f
2019-12-20 07:58:09 +01:00
bracesList :: forall a. Formatter -> (a -> Lazy.Text) -> [a] -> Lazy.Text
2019-08-03 23:57:27 +02:00
bracesList (Pretty intendation) f xs
2019-12-20 07:58:09 +01:00
= Lazy.Text.snoc (Lazy.Text.intercalate "\n" content) '\n'
<> (Lazy.Text.snoc $ Lazy.Text.replicate (fromIntegral intendation) " ") '}'
2019-08-03 23:57:27 +02:00
where
content = "{" : fmap f xs
2019-12-20 07:58:09 +01:00
bracesList Minified f xs = braces $ Lazy.Text.intercalate "," $ fmap f xs
optempty :: (Eq a, Monoid a, Monoid b) => (a -> b) -> a -> b
optempty f xs = if xs == mempty then mempty else f xs
eitherFormat :: forall a. Formatter -> a -> a -> a
2019-08-03 23:57:27 +02:00
eitherFormat (Pretty _) x _ = x
eitherFormat Minified _ x = x