packages feed

string-variants-0.2.0.0: src/Data/StringVariants/NonEmptyText/Internal.hs

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
-- We need redundant constraints in @widen@ to enforce invariants
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}

-- | Internal module of NonEmptyText, allowing breaking the abstraction.
--
--   Prefer to use "Data.StringVariants.NonEmptyText" instead.

module Data.StringVariants.NonEmptyText.Internal where

import Control.Monad (when)
import Data.Aeson (FromJSON (..), ToJSON, withText)
import Data.ByteString
import Data.Coerce
import Data.MonoTraversable
import Data.Proxy
import Data.Sequences
import Data.String.Conversions (ConvertibleStrings (..), cs)
import Data.StringVariants.Util (textHasNoMeaningfulContent, textIsWhitespace)
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Generics (Generic)
import GHC.TypeLits (KnownNat, Nat, natVal, type (<=))
import Language.Haskell.TH.Quote
import Language.Haskell.TH.Syntax (Lift (..), TyLit (..), Type (..))
import Test.QuickCheck
import Prelude

-- | Non Empty Text, requires the input is between 1 and @n@ chars and not just whitespace.
newtype NonEmptyText (n :: Nat) = NonEmptyText Text
  deriving stock (Generic, Show, Read, Lift)
  deriving newtype (Eq, Ord, ToJSON, Semigroup, MonoFoldable)

type instance Element (NonEmptyText _n) = Char

instance (KnownNat n, 1 <= n) => FromJSON (NonEmptyText n) where
  parseJSON = withText "NonEmptyText" $ \t -> do
    performInboundValidations t
    case mkNonEmptyText t of
      Nothing -> fail $ "Data/StringVariants/NonEmptyText.hs: invalid NonEmptyText: " ++ T.unpack t
      Just nonEmptyText -> pure nonEmptyText
    where
      -- These validations are performed at the edge of the system rather than in
      -- mkNonEmptyText because there may be cases where legacy NonEmptyText is
      -- stored in the database that fits one of these situations.
      --
      -- However, we don't want new stuff to send junk data, so we use this FromJSON
      -- instance to validate at the edge.
      performInboundValidations t = do
        when (textHasNoMeaningfulContent t) $
          fail $
            "Data/StringVariants/NonEmptyText.hs: NonEmptyText has no meaningful content in the field: " ++ T.unpack t
        when (T.any (== '\NUL') t) $
          fail $
            "Data/StringVariants/NonEmptyText.hs: NonEmptyText has a \\NUL byte in its contents: " ++ T.unpack t

-- Could add more instances of this (for lazy text, bytestrings, etc) if we think we need them.
instance ConvertibleStrings (NonEmptyText n) Text where
  convertString (NonEmptyText t) = t

instance ConvertibleStrings (NonEmptyText n) String where
  convertString (NonEmptyText t) = cs t

instance ConvertibleStrings (NonEmptyText n) ByteString where
  convertString (NonEmptyText t) = cs t

instance (KnownNat n, 1 <= n) => Arbitrary (NonEmptyText n) where
  arbitrary =
    NonEmptyText @n <$> do
      size <- chooseInt (1, fromIntegral (natVal (Proxy :: Proxy n)) - 1)
      -- Mostly alphanumeric characters, all human readable
      str <- replicateM size $ elements ['0' .. 'z']
      pure $ T.pack str

mkNonEmptyText :: forall n. (KnownNat n, 1 <= n) => Text -> Maybe (NonEmptyText n)
mkNonEmptyText t
  | T.compareLength stripped (fromIntegral $ natVal (Proxy @n)) == GT = Nothing
  | textIsWhitespace stripped = Nothing
  | otherwise = Just (NonEmptyText stripped)
  where
    stripped = T.filter (/= '\NUL') $ T.strip t

mkNonEmptyTextWithTruncate :: forall n. (KnownNat n, 1 <= n) => Text -> Maybe (NonEmptyText n)
mkNonEmptyTextWithTruncate t
  | textIsWhitespace stripped = Nothing
  | otherwise = Just (NonEmptyText $ T.stripEnd $ T.take (fromIntegral $ natVal (Proxy @n)) stripped)
  where
    stripped = T.filter (/= '\NUL') $ T.stripStart t

-- | Make a NonEmptyText when you can manually verify the length
unsafeMkNonEmptyText :: forall n. (KnownNat n, 1 <= n) => Text -> NonEmptyText n
unsafeMkNonEmptyText = NonEmptyText

-- | Converts a 'NonEmptyText' to a wider NonEmptyText
widen :: (1 <= n, n <= m) => NonEmptyText n -> NonEmptyText m
widen = coerce

compileNonEmptyTextKnownLength :: QuasiQuoter
compileNonEmptyTextKnownLength =
  QuasiQuoter
    { quoteExp = \s -> do
        let txt = T.pack s
        if textHasNoMeaningfulContent txt
          then fail "Invalid NonEmptyText. Must have at least one non-whitespace, non-control character."
          else [|NonEmptyText $(lift txt) :: NonEmptyText $(pure $ LitT $ NumTyLit $ fromIntegral $ T.length txt)|]
    , quotePat = error "Known-length NonEmptyText is not currently supported as a pattern"
    , quoteDec = error "NonEmptyText is not supported at top-level"
    , quoteType = error "NonEmptyText is not supported as a type"
    }