packages feed

postgresql-syntax-0.5.0.0: library-internal/PostgresqlSyntax/Ast/Ident.hs

module PostgresqlSyntax.Ast.Ident where

import qualified Data.Text as Text
import qualified HeadedMegaparsec as Parser
import qualified PostgresqlSyntax.Extras.TextBuilder as TextBuilder
import qualified PostgresqlSyntax.Helpers.Parsers as Parsers
import qualified PostgresqlSyntax.Helpers.Shrinks as Shrinks
import PostgresqlSyntax.IsAst
import qualified PostgresqlSyntax.KeywordSet as KeywordSet
import qualified PostgresqlSyntax.Predicate as Predicate
import PostgresqlSyntax.Prelude hiding (filter, try)
import PostgresqlSyntax.Settings (Settings)
import qualified Test.QuickCheck as Qc
import qualified TextBuilder

-- |
-- ==== References
-- @
-- IDENT
-- @
data Ident = QuotedIdent Text | UnquotedIdent Text
  deriving (Show, Generic, Eq, Ord, Data)

instance IsAst Ident where
  toTextBuilder _settings = \case
    QuotedIdent a -> TextBuilder.char7 '"' <> TextBuilder.text (Text.replace "\"" "\"\"" a) <> TextBuilder.char7 '"'
    UnquotedIdent a -> TextBuilder.text a
  parser _settings = quotedName <|> Parsers.keywordNameByPredicate UnquotedIdent (not . Predicate.keyword)
    where
      quotedName = Parser.filter (const "Empty name") (not . Text.null) (Parsers.quotedString '"') & fmap QuotedIdent

-- |
-- ==== References
-- @
-- ColId:
--   |  IDENT
--   |  unreserved_keyword
--   |  col_name_keyword
-- @
--
-- Most grammar positions that hold an identifier (column\/table\/alias
-- names, ...) are actually @ColId@, not bare @IDENT@ — this is the
-- permissive variant that most 'Ident'-typed fields elsewhere in
-- "PostgresqlSyntax.Ast" should parse with, since 'Ident'\'s own generic
-- 'parser' only accepts the strict @IDENT@ token (no Parsers.keyword fallback).
colId :: Settings -> Parser Ident
colId settings =
  Parser.label "identifier" $
    parser settings
      <|> Parsers.keywordNameFromSet UnquotedIdent (KeywordSet.unreservedKeyword <> KeywordSet.colNameKeyword)

-- |
-- ==== References
-- @
-- ColLabel:
--   |  IDENT
--   |  unreserved_keyword
--   |  col_name_keyword
--   |  type_func_name_keyword
--   |  reserved_keyword
-- @
colLabel :: Settings -> Parser Ident
colLabel settings =
  Parser.label "column label" $
    Parsers.keywordNameFromSet UnquotedIdent KeywordSet.keyword
      <|> parser settings

-- |
-- ==== References
-- @
-- type_function_name:
--   | IDENT
--   | unreserved_keyword
--   | type_func_name_keyword
-- @
typeFunctionName :: Settings -> Parser Ident
typeFunctionName settings =
  Parsers.keywordNameFromSet UnquotedIdent KeywordSet.typeFunctionName
    <|> parser settings

instance Qc.Arbitrary Ident where
  shrink = \case
    QuotedIdent a -> QuotedIdent <$> Shrinks.textTail a
    UnquotedIdent a -> UnquotedIdent <$> Shrinks.textTail a
  arbitrary =
    Qc.frequency
      [ (95, UnquotedIdent <$> unquotedIdentText),
        (5, QuotedIdent <$> quotedIdentText)
      ]
    where
      unquotedIdentText =
        ( do
            firstChar <- Qc.elements startChars
            restLength <- Qc.choose (0, 29)
            rest <- Qc.vectorOf restLength (Qc.elements contChars)
            pure (Text.pack (firstChar : rest))
        )
          `Qc.suchThat` (not . Predicate.keyword)
      startChars = ['a' .. 'z'] <> ['_']
      contChars = startChars <> ['0' .. '9'] <> ['$']
      quotedIdentText = do
        len <- Qc.choose (1, 30)
        Text.pack <$> Qc.vectorOf len (Qc.arbitrary `Qc.suchThat` (not . isControl))