packages feed

cfg-0.0.2.0: src/Cfg/Parser.hs

{-# LANGUAGE DefaultSignatures #-}

-- |
--  Module      : Cfg.Parser
--  Copyright   : © Jonathan Lorimer, 2023
--  License     : MIT
--  Maintainer  : jonathanlorimer@pm.me
--  Stability   : stable
--
-- @since 0.0.2.0
--
-- This module contains the type classes for parsing a configuration type from
-- a source, as well as instances for most basic Haskell types. One important
-- interaction to note is that we use a default instance for 'ConfigParser'
-- that dispatches to a 'ValueParser' instances. This is how we distinguish
-- between a \"parser\" that just navigates the tree representation of our
-- configuration and a parser that actually converts from text to our Haskell
-- type.
module Cfg.Parser
  ( -- * Parser Typeclasses
    ConfigParser (..)
  , ValueParser (..)

    -- * Parser Types
  , Parser
  , ConfigParseError (..)
  )
where

import Control.Error (note)
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as BL
import Data.Functor (void)
import Data.Int
import Data.List.NonEmpty (NonEmpty, fromList)
import Data.Map.Strict qualified as M
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Encoding (encodeUtf8)
import Data.Text.Lazy qualified as TL
import Data.Void (Void)
import Data.Word
import GHC.Generics (Generic)
import KeyTree
import Text.Megaparsec
  ( Parsec
  , anySingle
  , between
  , empty
  , option
  , parseMaybe
  , sepBy
  , sepBy1
  , some
  , takeRest
  , try
  , (<|>)
  )
import Text.Megaparsec.Char (char, digitChar, space1, string, string')
import Text.Megaparsec.Char.Lexer qualified as L

-- | Type alias for our megaparsec parser
--
-- @since 0.0.1.0
type Parser = Parsec Void Text

-- | Type errors we can encounter when parsing
--
-- @since 0.0.2.0
data ConfigParseError
  = -- | We encountered a 'Data.Map.Map' that was missing a key.
    MissingKey
      Text
      -- ^ The record field name that was missing.
      (KeyTree Text Text)
      -- ^ The subtree that was missing an entry.
  | -- | Expected to find a subtree aka a 'Free' with a map in it, but instead
    -- we found a 'Pure'.
    ExpectedKeyFoundValue
      Text
      -- ^ The key that was missing
      Text
      -- ^ The value that was found
  | -- | Expected to find a 'Pure' with a value but instead found a subtree
    ExpectedValueFoundForest
      (KeyTree Text Text)
      -- ^ The subtree that was found instead
  | -- | Ran a 'parser' and was unable to parse value
    ValueParseError
      Text
      -- ^ The parser error
  deriving (Eq, Show, Generic)

-- | This is the instance that allows us to parse a result from our
-- configuration source after we have retrieved it.
--
-- @since 0.0.2.0
class ConfigParser a where
  -- | Takes in the tree representation of our configuration and parses out our Haskell type
  --
  --  The default instance allows us to wrap a 'ValueParser' in a
  --  'ConfigParser', this allows us to use a uniform typeclass for parsing,
  --  but at the same time distinguish between traversing the key structure and
  --  actually parsing the textual value.
  parseConfig :: KeyTree Text Text -> Either ConfigParseError a
  default parseConfig :: (ValueParser a) => KeyTree Text Text -> Either ConfigParseError a
  parseConfig (Pure val) = note (ValueParseError val) $ parseMaybe parser val
  parseConfig kt = Left $ ExpectedValueFoundForest kt

-- | This is a text parser that we use to parse the eventual values we get from a
-- configuration.
--
-- @since 0.0.2.0
class ValueParser a where
  parser :: Parser a

-- | Lexer parser helper.
--
-- @since 0.0.2.0
sp :: Parsec Void Text ()
sp = L.space space1 empty empty

-- | @since 0.0.1.0
instance ValueParser () where
  parser = string "()" >> pure ()

-- | @since 0.0.1.0
instance ConfigParser ()

-- | @since 0.0.1.0
instance ValueParser Bool where
  parser = try (string' "true" >> pure True) <|> (string' "false" >> pure False)

-- | @since 0.0.1.0
instance ConfigParser Bool

-- | @since 0.0.1.0
instance ValueParser Char where
  parser = anySingle

-- | @since 0.0.1.0
instance ConfigParser Char

-- | @since 0.0.1.0
instance ValueParser TL.Text where
  parser = TL.fromStrict <$> takeRest

-- | @since 0.0.1.0
instance ConfigParser TL.Text

-- | @since 0.0.1.0
instance ValueParser BL.ByteString where
  parser = BL.fromStrict . encodeUtf8 <$> takeRest

-- | @since 0.0.1.0
instance ConfigParser BL.ByteString

-- | @since 0.0.1.0
instance ValueParser BS.ByteString where
  parser = encodeUtf8 <$> takeRest

-- | @since 0.0.1.0
instance ConfigParser BS.ByteString

-- | @since 0.0.1.0
instance ValueParser Text where
  parser = takeRest

-- | @since 0.0.1.0
instance ConfigParser Text

-- | @since 0.0.1.0
instance (ValueParser a) => ValueParser [a] where
  parser = between (L.symbol sp "[") (L.symbol sp "]") $ parser @a `sepBy` (L.symbol sp ",")

-- | @since 0.0.1.0
instance (ValueParser a) => ConfigParser [a]

-- | @since 0.0.1.0
instance (ValueParser a) => ValueParser (NonEmpty a) where
  parser = between (L.symbol sp "[") (L.symbol sp "]") $ fromList <$> parser @a `sepBy1` (L.symbol sp ",")

-- | @since 0.0.1.0
instance (ValueParser a) => ConfigParser (NonEmpty a)

-- | @since 0.0.1.0
instance (ValueParser a) => ConfigParser (Maybe a) where
  parseConfig (Free m) =
    if m == M.empty
      then Right Nothing
      else Left $ ExpectedValueFoundForest (Free m)
  parseConfig (Pure v) = Just <$> note (ValueParseError v) (parseMaybe (parser @a) v)

-- Numeric parser helpers

-- | @since 0.0.2.0
rd :: (Read a) => Text -> a
rd = read . T.unpack

-- | @since 0.0.2.0
plus :: Parser Text
plus = char '+' >> number

-- | @since 0.0.2.0
minus :: Parser Text
minus = liftA2 (T.cons) (char '-') number

-- | @since 0.0.2.0
number :: Parser Text
number = T.pack <$> some digitChar

-- | @since 0.0.2.0
decimal :: Parser Text
decimal = option "" $ (T.cons) <$> char '.' <*> number

-- | @since 0.0.2.0
integral :: (Read a) => Parser a
integral = rd <$> (try plus <|> try minus <|> number)

-- | @since 0.0.2.0
fractional :: (Read a) => Parser a
fractional = fmap rd $ liftA2 (<>) integral decimal

-- | @since 0.0.1.0
instance ValueParser Double where
  parser = fractional

-- | @since 0.0.1.0
instance ConfigParser Double

-- | @since 0.0.1.0
instance ValueParser Float where
  parser = fractional

-- | @since 0.0.1.0
instance ConfigParser Float

-- | @since 0.0.1.0
instance ValueParser Int where
  parser = integral

-- | @since 0.0.1.0
instance ConfigParser Int

-- | @since 0.0.1.0
instance ValueParser Int8 where
  parser = integral

-- | @since 0.0.1.0
instance ConfigParser Int8

-- | @since 0.0.1.0
instance ValueParser Int16 where
  parser = integral

-- | @since 0.0.1.0
instance ConfigParser Int16

-- | @since 0.0.1.0
instance ValueParser Int32 where
  parser = integral

-- | @since 0.0.1.0
instance ConfigParser Int32

-- | @since 0.0.1.0
instance ValueParser Int64 where
  parser = integral

-- | @since 0.0.1.0
instance ConfigParser Int64

-- | @since 0.0.1.0
instance ValueParser Integer where
  parser = integral

-- | @since 0.0.1.0
instance ConfigParser Integer

-- | @since 0.0.1.0
instance ValueParser Word where
  parser = rd <$> number

-- | @since 0.0.1.0
instance ConfigParser Word

-- | @since 0.0.1.0
instance ValueParser Word8 where
  parser = rd <$> number

-- | @since 0.0.1.0
instance ConfigParser Word8

-- | @since 0.0.1.0
instance ValueParser Word16 where
  parser = rd <$> number

-- | @since 0.0.1.0
instance ConfigParser Word16

-- | @since 0.0.1.0
instance ValueParser Word32 where
  parser = rd <$> number

-- | @since 0.0.1.0
instance ConfigParser Word32

-- | @since 0.0.1.0
instance ValueParser Word64 where
  parser = rd <$> number

-- | @since 0.0.1.0
instance ConfigParser Word64

-- | @since 0.0.1.0
instance (ValueParser a, ValueParser b) => ValueParser (a, b) where
  parser = between (L.symbol sp "(") (L.symbol sp ")") $ do
    a <- parser @a
    void $ L.symbol sp ","
    b <- parser @b
    pure (a, b)

-- | @since 0.0.1.0
instance (ValueParser a, ValueParser b) => ConfigParser (a, b)