packages feed

exon-0.1.0.0: lib/Exon/Class/Exon.hs

-- |Description: Internal
module Exon.Class.Exon where

import Exon.Data.Result (Result (Empty, Result))
import qualified Exon.Data.Segment as Segment
import Exon.Data.Segment (Segment)

data ExonDefault

data KeepWhitespace

{- |
This class is responsible for combining segments of an interpolated string, allowing users to define their own rules
for how the result is constructed.
The default implementation converts each literal part with 'IsString' and uses the result type's 'Monoid' to
concatenate them.

The raw parts are encoded as 'Segment', getting combined into a 'Result'.

The default for 'convertSegment' skips whitespace by encoding it into the 'Result' constructor 'Empty', which is a
unit object.
To change this behavior, it can be easily overridden:

@
newtype Thing = Thing String deriving newtype (IsString, Semigroup, Monoid, Show)

instance Exon ExonDefault Thing where
  convertSegment = \case
    Segment.String s -> Result (Thing s)
    Segment.Expression thing -> Result thing
    Segment.Whitespace _ -> Result (Thing " >>> ")

  insertWhitespace s1 ws s2 =
    appendSegment @ExonDefault (appendSegment @ExonDefault s1 (Segment.Whitespace ws)) s2
@
-}
class Exon (tag :: Type) (a :: Type) where

  -- |This check is used to allow empty expression segments to be skipped when they are empty.
  -- The default is to never skip expressions.
  isEmpty :: a -> Bool
  isEmpty =
    const False

  -- |Convert a 'Segment' to a 'Result'.
  -- The default implementation uses 'IsString' and ignores whitespace, returning 'Empty'.
  convertSegment :: Segment a -> Result a

  default convertSegment :: IsString a => Segment a -> Result a
  convertSegment = \case
    Segment.String a ->
      Result (fromString a)
    Segment.Expression a | isEmpty @tag a ->
      Empty
    Segment.Expression a ->
      Result a
    Segment.Whitespace _ ->
      Empty

  -- |Append a 'Segment' to a 'Result'.
  -- The default implementation uses '(<>)'.
  appendSegment :: Result a -> Segment a -> Result a

  default appendSegment :: Semigroup a => Result a -> Segment a -> Result a
  appendSegment z a =
    z <> convertSegment @tag a

  -- |Append whitespace and a 'Segment' to a 'Result', i.e. joining two parts of the interpolation by whitespace.
  -- The default implementation ignores the whitespace, calling 'appendSegment' with the second argument.
  insertWhitespace :: Result a -> String -> Segment a -> Result a

  default insertWhitespace :: Result a -> String -> Segment a -> Result a
  insertWhitespace s1 _ s2 =
    appendSegment @tag s1 s2

  -- |The entry point for concatenation, taking a list of segments parsed from the interpolation.
  -- The default implementation skips leading whitespace and calls 'appendSegment' and 'insertWhitespace' to
  -- concatenate.
  concatSegments :: NonEmpty (Segment a) -> a

  default concatSegments :: Monoid a => NonEmpty (Segment a) -> a
  concatSegments (h :| t) =
    fold (spin (convertSegment @tag h) t)
    where
      spin :: Result a -> [Segment a] -> Result a
      spin Empty = \case
        [] ->
          Empty
        Segment.Whitespace _ : ss ->
          spin Empty ss
        s1 : ss ->
          spin (convertSegment @tag s1) ss
      spin (Result s1) = \case
        [] ->
          Result s1
        Segment.Whitespace _ : (Segment.Expression a) : ss | isEmpty @tag a ->
          spin (Result s1) ss
        Segment.Whitespace ws : s2 : ss ->
          spin (insertWhitespace @tag (Result s1) ws s2) ss
        [Segment.Whitespace _] ->
          (Result s1)
        s2 : ss ->
          spin (appendSegment @tag (Result s1) s2) ss

instance {-# overlappable #-} (
    Monoid a,
    IsString a
  ) => Exon ExonDefault a where

convertKeepWs ::
  IsString a =>
  Segment a ->
  Result a
convertKeepWs = \case
  Segment.String a ->
    Result (fromString a)
  Segment.Expression a ->
    Result a
  Segment.Whitespace a ->
    Result (fromString a)

concatKeepWs ::
  ∀ tag a .
  Monoid a =>
  Exon tag a =>
  NonEmpty (Segment a) ->
  a
concatKeepWs =
  fold . foldl' (appendSegment @tag) Empty

instance Exon ExonDefault String where
  convertSegment =
    convertKeepWs

  concatSegments =
    concatKeepWs @ExonDefault

instance Exon ExonDefault Text where
  convertSegment =
    convertKeepWs

  concatSegments =
    concatKeepWs @ExonDefault

instance Exon ExonDefault LText where
  convertSegment =
    convertKeepWs

  concatSegments =
    concatKeepWs @ExonDefault

instance Exon ExonDefault ByteString where
  convertSegment =
    convertKeepWs

  concatSegments =
    concatKeepWs @ExonDefault

instance Exon ExonDefault LByteString where
  convertSegment =
    convertKeepWs

  concatSegments =
    concatKeepWs @ExonDefault