string-variants 0.1.0.2 → 0.2.0.0
raw patch · 13 files changed
+361/−61 lines, 13 filesdep +HUnitdep +hedgehogdep +hspecdep ~basenew-uploader
Dependencies added: HUnit, hedgehog, hspec, hspec-core, hspec-expectations, hspec-hedgehog, string-variants
Dependency ranges changed: base
Files
- CHANGELOG.md +6/−0
- src/Data/StringVariants.hs +0/−4
- src/Data/StringVariants/NonEmptyText.hs +34/−18
- src/Data/StringVariants/NonEmptyText/Internal.hs +8/−7
- src/Data/StringVariants/NullableNonEmptyText.hs +19/−10
- src/Data/StringVariants/Prose.hs +1/−0
- src/Data/StringVariants/Prose/Internal.hs +9/−0
- src/Data/StringVariants/Util.hs +55/−17
- string-variants.cabal +74/−5
- test/Main.hs +11/−0
- test/Spec.hs +1/−0
- test/Specs/NonEmptySpec.hs +105/−0
- test/Specs/TextManipulationSpec.hs +38/−0
CHANGELOG.md view
@@ -1,5 +1,11 @@ # Changelog +## [0.2.0.0] - 2022-12-05++- Add `literalNonEmptyText`, `literalProse`, `literalNullableNonEmptyText` for creation of string variants from the type-level literals.+- Add `concatWithSpace` to concatenate NonEmptyText in a type-safe way.+- Drop support for GHC <9.2.+ ## [0.1.0.2] - 2022-10-18 - Fix `mkNonEmptyTextWithTruncate` to perform truncation after character stripping.
src/Data/StringVariants.hs view
@@ -51,10 +51,6 @@ -- ** Information isNullNonEmptyText,-- -- * Convenience util if you need a NonEmptyText of a dynamically determined lengths- useNat,- natOfLength, ) where
src/Data/StringVariants/NonEmptyText.hs view
@@ -13,6 +13,7 @@ -- * Construction mkNonEmptyText, mkNonEmptyTextWithTruncate,+ literalNonEmptyText, unsafeMkNonEmptyText, nonEmptyTextToText, compileNonEmptyText,@@ -28,27 +29,25 @@ chunksOfNonEmptyText, filterNonEmptyText, (<>|),+ concatWithSpace, -- * Conversions between 'Refined' and 'NonEmptyText'. ContainsNonWhitespaceCharacters (..), exactLengthRefinedToRange, nonEmptyTextFromRefined, refinedFromNonEmptyText,-- -- * Convenience util if you need a NonEmptyText of a dynamically determined lengths- useNat,- natOfLength, ) where import Control.Monad import Data.Data (Proxy (..), typeRep)+import Data.Maybe (mapMaybe) import Data.StringVariants.NonEmptyText.Internal import Data.StringVariants.Util import Data.Text (Text) import Data.Text qualified as T import GHC.Generics (Generic)-import GHC.TypeLits (KnownNat, natVal, type (+), type (<=))+import GHC.TypeLits (KnownNat, KnownSymbol, Symbol, Nat, natVal, symbolVal, type (+), type (<=)) import Language.Haskell.TH import Language.Haskell.TH.Quote import Language.Haskell.TH.Syntax (Lift (..))@@ -66,11 +65,16 @@ } where compileNonEmptyText' :: String -> Q Exp- compileNonEmptyText' s = useNat n $ \p ->- case natOfLength p $ mkNonEmptyText (T.pack s) of- Nothing -> fail $ "Invalid NonEmptyText. Needs to be < " ++ show (n + 1) ++ " characters, and not entirely whitespace: " ++ s+ compileNonEmptyText' s = usePositiveNat n errorMessage $ \(_ :: proxy n) ->+ case mkNonEmptyText @n (T.pack s) of Just txt -> [|$(lift txt) :: NonEmptyText $(pure $ LitT $ NumTyLit n)|]+ Nothing -> errorMessage+ where+ errorMessage = fail $ "Invalid NonEmptyText. Needs to be < " ++ show (n + 1) ++ " characters, and not entirely whitespace: " ++ s +literalNonEmptyText :: forall (s :: Symbol) (n :: Nat). (KnownSymbol s, KnownNat n, SymbolNonEmpty s, SymbolWithNoSpaceAround s, SymbolNoLongerThan s n) => NonEmptyText n+literalNonEmptyText = NonEmptyText (T.pack (symbolVal (Proxy @s)))+ convertEmptyTextToNothing :: Text -> Maybe Text convertEmptyTextToNothing t | textIsWhitespace t = Nothing@@ -82,39 +86,43 @@ -- | Identical to the normal text filter function, but maintains the type-level invariant -- that the text length is <= n, unlike unwrapping the text, filtering, then -- rewrapping the text.-filterNonEmptyText :: (Char -> Bool) -> NonEmptyText n -> NonEmptyText n-filterNonEmptyText f (NonEmptyText t) = NonEmptyText (T.filter f t)+--+-- Will return Nothing if the resulting length is zero.+filterNonEmptyText :: (KnownNat n, 1 <= n) => (Char -> Bool) -> NonEmptyText n -> Maybe (NonEmptyText n)+filterNonEmptyText f (NonEmptyText t) = mkNonEmptyText (T.filter f t) -- | Narrows the maximum length, dropping any remaining trailing characters.-takeNonEmptyText :: forall m n. (KnownNat m, KnownNat n, n <= m) => NonEmptyText m -> NonEmptyText n+takeNonEmptyText :: forall m n. (KnownNat m, KnownNat n, 1 <= n, n <= m) => NonEmptyText m -> NonEmptyText n takeNonEmptyText (NonEmptyText t) = if m == n then NonEmptyText t- else NonEmptyText $ T.take n t+ -- when the input is stripped, taking from it is guaranteed to be not empty+ else NonEmptyText $ T.stripEnd $ T.take n t where m = fromIntegral $ natVal (Proxy @m) n = fromIntegral $ natVal (Proxy @n) -- | Narrows the maximum length, dropping any prefix remaining characters.-takeNonEmptyTextEnd :: forall m n. (KnownNat m, KnownNat n, n <= m) => NonEmptyText m -> NonEmptyText n+takeNonEmptyTextEnd :: forall m n. (KnownNat m, KnownNat n, 1 <= n, n <= m) => NonEmptyText m -> NonEmptyText n takeNonEmptyTextEnd (NonEmptyText t) = if m == n then NonEmptyText t- else NonEmptyText $ T.takeEnd n t+ -- when the input is stripped, taking from it is guaranteed to be not empty+ else NonEmptyText $ T.stripStart $ T.takeEnd n t where m = fromIntegral $ natVal (Proxy @m) n = fromIntegral $ natVal (Proxy @n) -- | /O(n)/ Splits a 'NonEmptyText' into components of length @chunkSize@. The--- last element may be shorter than the other chunks, depending on the length--- of the input.+-- chunks may be shorter than the chunkSize depending on the length+-- of the input and spacing. Each chunk is stripped of whitespace. chunksOfNonEmptyText :: forall chunkSize totalSize.- (KnownNat chunkSize, KnownNat totalSize) =>+ (KnownNat chunkSize, KnownNat totalSize, chunkSize <= totalSize, 1 <= chunkSize) => NonEmptyText totalSize -> [NonEmptyText chunkSize] chunksOfNonEmptyText (NonEmptyText t) =- map NonEmptyText (T.chunksOf chunkSize t)+ mapMaybe mkNonEmptyText (T.chunksOf chunkSize t) where chunkSize = fromIntegral $ natVal (Proxy @chunkSize) @@ -124,6 +132,14 @@ -- Mnemonic: @<>@ for monoid, @|@ from NonEmpty's ':|' operator (<>|) :: NonEmptyText n -> NonEmptyText m -> NonEmptyText (n + m) (NonEmptyText l) <>| (NonEmptyText r) = NonEmptyText (l <> r)++-- | Concat two 'NonEmptyText' values with a space in between them. The new+-- maximum length is the sum of the two maximum lengths of the inputs + 1 for+-- the space.+--+-- Useful for 'unwords'like operations, or combining first and last names.+concatWithSpace :: NonEmptyText n -> NonEmptyText m -> NonEmptyText (n + m + 1)+concatWithSpace (NonEmptyText l) (NonEmptyText r) = NonEmptyText (l <> " " <> r) -- Refinery
src/Data/StringVariants/NonEmptyText/Internal.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-} -- We need redundant constraints in @widen@ to enforce invariants {-# OPTIONS_GHC -fno-warn-redundant-constraints #-} @@ -17,7 +18,7 @@ import Data.Proxy import Data.Sequences import Data.String.Conversions (ConvertibleStrings (..), cs)-import Data.StringVariants.Util (textHasNoMeaningfulContent, textIsTooLong, textIsWhitespace)+import Data.StringVariants.Util (textHasNoMeaningfulContent, textIsWhitespace) import Data.Text (Text) import Data.Text qualified as T import GHC.Generics (Generic)@@ -34,7 +35,7 @@ type instance Element (NonEmptyText _n) = Char -instance KnownNat n => FromJSON (NonEmptyText n) where+instance (KnownNat n, 1 <= n) => FromJSON (NonEmptyText n) where parseJSON = withText "NonEmptyText" $ \t -> do performInboundValidations t case mkNonEmptyText t of@@ -65,7 +66,7 @@ instance ConvertibleStrings (NonEmptyText n) ByteString where convertString (NonEmptyText t) = cs t -instance KnownNat n => Arbitrary (NonEmptyText n) where+instance (KnownNat n, 1 <= n) => Arbitrary (NonEmptyText n) where arbitrary = NonEmptyText @n <$> do size <- chooseInt (1, fromIntegral (natVal (Proxy :: Proxy n)) - 1)@@ -73,15 +74,15 @@ str <- replicateM size $ elements ['0' .. 'z'] pure $ T.pack str -mkNonEmptyText :: forall n. KnownNat n => Text -> Maybe (NonEmptyText n)+mkNonEmptyText :: forall n. (KnownNat n, 1 <= n) => Text -> Maybe (NonEmptyText n) mkNonEmptyText t- | textIsTooLong stripped (fromIntegral $ natVal (Proxy @n)) = Nothing+ | 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 => Text -> Maybe (NonEmptyText n)+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)@@ -89,7 +90,7 @@ stripped = T.filter (/= '\NUL') $ T.stripStart t -- | Make a NonEmptyText when you can manually verify the length-unsafeMkNonEmptyText :: forall n. KnownNat n => Text -> NonEmptyText n+unsafeMkNonEmptyText :: forall n. (KnownNat n, 1 <= n) => Text -> NonEmptyText n unsafeMkNonEmptyText = NonEmptyText -- | Converts a 'NonEmptyText' to a wider NonEmptyText
src/Data/StringVariants/NullableNonEmptyText.hs view
@@ -1,4 +1,6 @@ {-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-} module Data.StringVariants.NullableNonEmptyText ( -- Safe to construct, so we can export the contents@@ -7,6 +9,7 @@ -- * Constructing mkNonEmptyTextWithTruncate, compileNullableNonEmptyText,+ literalNullableNonEmptyText, mkNullableNonEmptyText, parseNullableNonEmptyText, nullNonEmptyText,@@ -32,11 +35,11 @@ import Data.Data (Proxy (..)) import Data.Maybe (fromMaybe) import Data.StringVariants.NonEmptyText-import Data.StringVariants.Util (textIsTooLong)+import Data.StringVariants.Util import Data.Text (Text) import Data.Text qualified as T import GHC.Generics (Generic)-import GHC.TypeLits (KnownNat, natVal)+import GHC.TypeLits (KnownNat, KnownSymbol, Nat, Symbol, natVal) import Language.Haskell.TH import Language.Haskell.TH.Quote import Language.Haskell.TH.Syntax (Lift (..))@@ -58,9 +61,9 @@ deriving stock (Generic, Show, Read, Lift) deriving newtype (Eq, ToJSON) -mkNullableNonEmptyText :: forall n. KnownNat n => Text -> Maybe (NullableNonEmptyText n)+mkNullableNonEmptyText :: forall n. (KnownNat n, 1 <= n) => Text -> Maybe (NullableNonEmptyText n) mkNullableNonEmptyText t- | textIsTooLong t (fromIntegral $ natVal (Proxy @n)) = Nothing -- we can't store text that is too long+ | T.compareLength t (fromIntegral $ natVal (Proxy @n)) == GT = Nothing -- we can't store text that is too long | otherwise = Just $ NullableNonEmptyText $ mkNonEmptyText t nullNonEmptyText :: NullableNonEmptyText n@@ -69,7 +72,7 @@ isNullNonEmptyText :: NullableNonEmptyText n -> Bool isNullNonEmptyText = (== nullNonEmptyText) -instance KnownNat n => FromJSON (NullableNonEmptyText n) where+instance (KnownNat n, 1 <= n) => FromJSON (NullableNonEmptyText n) where parseJSON = \case J.String t -> case mkNullableNonEmptyText t of Just txt -> pure txt@@ -77,7 +80,7 @@ J.Null -> pure $ NullableNonEmptyText Nothing x -> fail $ "Data/StringVariants/NullableNonEmptyText.hs: When trying to parse a NullableNonEmptyText, expected a String or Null, but received: " ++ show x -parseNullableNonEmptyText :: KnownNat n => Text -> J.Object -> J.Parser (NullableNonEmptyText n)+parseNullableNonEmptyText :: (KnownNat n, 1 <= n) => Text -> J.Object -> J.Parser (NullableNonEmptyText n) parseNullableNonEmptyText fieldName obj = obj .:? J.fromText fieldName .!= nullNonEmptyText fromMaybeNullableText :: Maybe (NullableNonEmptyText n) -> NullableNonEmptyText n@@ -90,7 +93,7 @@ maybeNonEmptyTextToNullable Nothing = nullNonEmptyText maybeNonEmptyTextToNullable jt = NullableNonEmptyText jt -maybeTextToTruncateNullableNonEmptyText :: forall n. KnownNat n => Maybe Text -> NullableNonEmptyText n+maybeTextToTruncateNullableNonEmptyText :: forall n. (KnownNat n, 1 <= n) => Maybe Text -> NullableNonEmptyText n maybeTextToTruncateNullableNonEmptyText mText = NullableNonEmptyText $ mText >>= mkNonEmptyText . T.take (fromInteger (natVal (Proxy @n))) nullableNonEmptyTextToMaybeNonEmptyText :: NullableNonEmptyText n -> Maybe (NonEmptyText n)@@ -109,7 +112,13 @@ } where compileNullableNonEmptyText' :: String -> Q Exp- compileNullableNonEmptyText' s = useNat n $ \p ->- case natOfLength p $ mkNullableNonEmptyText (T.pack s) of- Nothing -> fail $ "Invalid NullableNonEmptyText. Needs to be < " ++ show (n + 1) ++ " characters, and not entirely whitespace: " ++ s+ compileNullableNonEmptyText' s = usePositiveNat n errorMessage $ \(_ :: proxy n) ->+ case mkNullableNonEmptyText @n (T.pack s) of Just txt -> [|$(lift txt)|]+ Nothing -> errorMessage+ where+ errorMessage = fail $ "Invalid NullableNonEmptyText. Needs to be < " ++ show (n + 1) ++ " characters, and not entirely whitespace: " ++ s++-- | This requires the text to be non-empty. For the empty text just use the constructor `NullableNonEmptyText Nothing`+literalNullableNonEmptyText :: forall (s :: Symbol) (n :: Nat). (KnownSymbol s, KnownNat n, SymbolNonEmpty s, SymbolWithNoSpaceAround s, SymbolNoLongerThan s n) => NullableNonEmptyText n+literalNullableNonEmptyText = NullableNonEmptyText (Just (literalNonEmptyText @s @n))
src/Data/StringVariants/Prose.hs view
@@ -3,6 +3,7 @@ ( Prose, mkProse, compileProse,+ literalProse, proseToText, ) where
src/Data/StringVariants/Prose/Internal.hs view
@@ -1,5 +1,8 @@ {-# LANGUAGE TemplateHaskell #-} +-- GHC considers the constraints for the prose symbol redundant.+{-# OPTIONS_GHC -Wno-redundant-constraints #-}+ -- | Internal module of Prose, allowing breaking the abstraction. -- -- Prefer to use "Data.StringVariants.Prose" instead.@@ -8,9 +11,12 @@ import Data.Aeson (FromJSON, ToJSON, ToJSONKey, withText) import Data.Aeson.Types (FromJSON (..)) import Data.String.Conversions (ConvertibleStrings (..), cs)+import Data.StringVariants.Util (SymbolWithNoSpaceAround)+import Data.Proxy import Data.Text (Text) import Data.Text qualified as T import Data.Text.Lazy qualified as LT+import GHC.TypeLits (KnownSymbol, Symbol, symbolVal) import Language.Haskell.TH.Quote import Language.Haskell.TH.Syntax import Prelude@@ -54,6 +60,9 @@ Just s' -> [|$(lift s')|] msg s = "Invalid Prose: " <> s <> ". Make sure you aren't wrapping the text in quotes."++literalProse :: forall (s :: Symbol). (KnownSymbol s, SymbolWithNoSpaceAround s) => Prose+literalProse = Prose (T.pack (symbolVal (Proxy @s))) proseToText :: Prose -> Text proseToText (Prose txt) = txt
src/Data/StringVariants/Util.hs view
@@ -1,31 +1,69 @@-module Data.StringVariants.Util (natOfLength, useNat, textIsTooLong, textIsWhitespace, textHasNoMeaningfulContent) where-import GHC.TypeLits (KnownNat, Nat, SomeNat (..), someNatVal)+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++module Data.StringVariants.Util (SymbolNonEmpty, SymbolWithNoSpaceAround, SymbolNoLongerThan, usePositiveNat, textIsWhitespace, textHasNoMeaningfulContent) where++import Data.Proxy+import Data.Kind (Constraint)+import Data.Type.Bool+import Data.Type.Equality+import GHC.TypeLits import Prelude import Data.Text (Text) import qualified Data.Text as T import Data.Char (isSpace, isControl) -textIsTooLong :: Text -> Int -> Bool--- why take? because of stream fusion,--- length isn't O(1), it's O(n). which means--- it's better to cut off the text at @n@--- and then evaluate the resulting stream, than--- to evaluate the entire stream. very unintuitive,--- but that's just the way things are :(-textIsTooLong t n = T.length (T.take (n + 1) t) >= (n + 1)- textIsWhitespace :: Text -> Bool textIsWhitespace = T.all isSpace textHasNoMeaningfulContent :: Text -> Bool textHasNoMeaningfulContent = T.all (\c -> isSpace c || isControl c) +-- | If the integer is >= 1, evaluate the function with the proof of that. Otherwise, return the default value+usePositiveNat :: Integer -> a -> (forall n proxy. (KnownNat n, 1 <= n) => proxy n -> a) -> a+usePositiveNat n a f = case someNatVal n of+ Nothing -> a+ Just (SomeNat p) -> case cmpNat (Proxy @1) p of+ EQI -> f p+ LTI -> f p+ GTI -> a -natOfLength :: proxy (n :: Nat) -> f (other n) -> f (other n)-natOfLength _ = id+-- Logic ported from base-4.17 Data.Char.isSpace.+-- Hopefully, GHC would add this type-level function+type IsCharWhitespace c+ = If+ (CharToNat c <=? 0x376)+ (CharToNat c == 32 || CharToNat c - 0x9 <=? 4 || CharToNat c == 0xa0)+ (IsUnicodeWhitespaceCodePoint (CharToNat c)) -useNat :: Integer -> (forall n proxy. KnownNat n => proxy n -> x) -> x-useNat n f = case someNatVal n of- Nothing -> error (show n ++ " isn't a valid Nat")- Just (SomeNat p) -> f p+-- It checks the whitespace characters (Unicode property White_Space=yes) defined in Unicode 15.0.0 and above the ASCII range.+type IsUnicodeWhitespaceCodePoint n+ = n == 0x1680+ || (0x2000 <=? n && n <=? 0x200A)+ || n == 0x2028 || n == 0x2029 || n == 0x202F || n == 0x205F || n == 0x3000 +type family SymbolLength (length :: Nat) (s :: Maybe (Char, Symbol)) :: Nat where+ SymbolLength length 'Nothing = length+ SymbolLength length ('Just '(_, s)) = SymbolLength (length + 1) (UnconsSymbol s)++type family SymbolNonEmpty (s :: Symbol) :: Constraint where+ SymbolNonEmpty "" = TypeError ('Text "Symbol is empty")+ SymbolNonEmpty _ = ()++type family SymbolNoLeadingSpace (s :: Maybe (Char, Symbol)) :: Constraint where+ SymbolNoLeadingSpace 'Nothing = ()+ SymbolNoLeadingSpace ('Just '(c, _)) = If (IsCharWhitespace c) (TypeError ('Text "Symbol has leading whitespace")) (() :: Constraint)++type family SymbolNoTrailingSpace (s :: Maybe (Char, Symbol)) :: Constraint where+ SymbolNoTrailingSpace 'Nothing = ()+ SymbolNoTrailingSpace ('Just '(c, "")) = If (IsCharWhitespace c) (TypeError ('Text "Symbol has trailing whitespace")) (() :: Constraint)+ SymbolNoTrailingSpace ('Just '(_, s)) = SymbolNoTrailingSpace (UnconsSymbol s)++-- The type family IsCharWhitespace is large but it is only evaluated for the first and last symbols+type SymbolWithNoSpaceAround s = (SymbolNoLeadingSpace (UnconsSymbol s), SymbolNoTrailingSpace (UnconsSymbol s)) ++type family SymbolNoLongerThan (s :: Symbol) (n :: Nat) :: Constraint where+ SymbolNoLongerThan s n = If (SymbolLength 0 (UnconsSymbol s) <=? n)+ (() :: Constraint)+ (TypeError ('Text "Invalid NonEmptyText. Needs to be <= " ':<>: 'ShowType n ':<>: 'Text " characters. Has " ':<>: 'ShowType (SymbolLength 0 (UnconsSymbol s)) ':<>: 'Text " characters."))
string-variants.cabal view
@@ -1,22 +1,22 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.34.7.+-- This file has been generated from package.yaml by hpack version 0.35.0. -- -- see: https://github.com/sol/hpack name: string-variants-version: 0.1.0.2+version: 0.2.0.0 synopsis: Constrained text newtypes description: See README at <https://github.com/MercuryTechnologies/string-variants#readme>. category: Data homepage: https://github.com/MercuryTechnologies/string-variants#readme bug-reports: https://github.com/MercuryTechnologies/string-variants/issues-maintainer: Jade Lovelace <jadel@mercury.com>+maintainer: Jade Lovelace <jadel@mercury.com>, Borys Lykakh <borys@fastmail.com> license: MIT license-file: LICENSE build-type: Simple tested-with:- GHC ==8.10.7 || ==9.2.4 || ==9.4.2+ GHC ==9.2.4 || ==9.4.2 extra-source-files: CHANGELOG.md @@ -77,12 +77,81 @@ build-depends: QuickCheck , aeson >=2.0.0.0- , base >=4.9 && <5+ , base >=4.16 && <5 , bytestring , mono-traversable , refined , refinery , string-conversions+ , template-haskell+ , text+ default-language: Haskell2010++test-suite test+ type: exitcode-stdio-1.0+ main-is: Main.hs+ other-modules:+ Spec+ Specs.NonEmptySpec+ Specs.TextManipulationSpec+ Paths_string_variants+ hs-source-dirs:+ test+ default-extensions:+ AllowAmbiguousTypes+ ApplicativeDo+ BlockArguments+ DataKinds+ DeriveAnyClass+ DeriveFoldable+ DeriveFunctor+ DeriveGeneric+ DeriveLift+ DeriveTraversable+ DerivingVia+ FlexibleContexts+ FlexibleInstances+ FunctionalDependencies+ GADTs+ GeneralizedNewtypeDeriving+ ImportQualifiedPost+ InstanceSigs+ LambdaCase+ MonoLocalBinds+ MultiWayIf+ NamedFieldPuns+ NumericUnderscores+ OverloadedStrings+ PatternSynonyms+ PolyKinds+ RankNTypes+ RecordWildCards+ RecursiveDo+ ScopedTypeVariables+ StandaloneDeriving+ StandaloneKindSignatures+ TypeApplications+ TypeFamilies+ ViewPatterns+ ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-export-lists -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-missing-local-signatures -Wno-monomorphism-restriction -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-missing-kind-signatures -threaded -rtsopts -with-rtsopts=-N -Wno-incomplete-uni-patterns -O0+ build-tool-depends:+ hspec-discover:hspec-discover+ build-depends:+ HUnit+ , QuickCheck+ , aeson >=2.0.0.0+ , base >=4.16 && <5+ , bytestring+ , hedgehog+ , hspec+ , hspec-core+ , hspec-expectations+ , hspec-hedgehog+ , mono-traversable+ , refined+ , refinery+ , string-conversions+ , string-variants , template-haskell , text default-language: Haskell2010
+ test/Main.hs view
@@ -0,0 +1,11 @@+module Main where++import Spec qualified+import Test.Hspec+import Test.Hspec.Runner (defaultConfig, hspecWith)+import Prelude++main :: IO ()+main =+ hspecWith defaultConfig $+ parallel Spec.spec
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=Spec #-}
+ test/Specs/NonEmptySpec.hs view
@@ -0,0 +1,105 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE QuasiQuotes #-}++module Specs.NonEmptySpec (spec) where++import Data.Maybe+import Data.Text (Text)+import Data.Text qualified as T+import Data.StringVariants.NonEmptyText+import Data.StringVariants.NullableNonEmptyText+import Data.StringVariants.Util (usePositiveNat)+import GHC.TypeLits+import Hedgehog+import Hedgehog.Gen qualified as Gen+import Hedgehog.Range qualified as Range+import Language.Haskell.TH.Quote (QuasiQuoter (quoteExp))+import Prelude+import Test.Hspec+import Test.Hspec.Hedgehog (hedgehog)++mkNonEmptyText299 :: Text -> Maybe (NonEmptyText 299)+mkNonEmptyText299 = mkNonEmptyText++maybeTextToTruncateNullableNonEmptyText299 :: Maybe Text -> NullableNonEmptyText 299+maybeTextToTruncateNullableNonEmptyText299 = maybeTextToTruncateNullableNonEmptyText++mkNonEmptyText299WithTruncate :: Text -> Maybe (NonEmptyText 299)+mkNonEmptyText299WithTruncate = mkNonEmptyTextWithTruncate++matchLength :: proxy (n :: Nat) -> other n -> other n+matchLength _ = id++spec :: Spec+spec = describe "NonEmptyText variants" $ do+ describe "NonEmptyText299" $ do+ describe "compileNonEmptyText" $ do+ it "should work" $ do+ mkNonEmptyText299 "hi" `shouldBe` Just (widen [compileNonEmptyTextKnownLength|hi|])+ describe "compileNullableNonEmptyText" $ do+ it "should work" $ do+ $(quoteExp (compileNullableNonEmptyText 2) "yo") `shouldBe` NullableNonEmptyText (Just [compileNonEmptyTextKnownLength|yo|])+ describe "nonEmptyTextToText" $ do+ it "should work" $ do+ nonEmptyTextToText [compileNonEmptyTextKnownLength|yo|] `shouldBe` "yo"++ describe "Generalized NonEmptyText" $ do+ describe "mkNonEmptyText" $+ it "should work" $ do+ mkNonEmptyText299 "" `shouldBe` Nothing+ mkNonEmptyText299 " " `shouldBe` Nothing+ mkNonEmptyText299 "\n" `shouldBe` Nothing+ mkNonEmptyText299 "\t" `shouldBe` Nothing+ mkNonEmptyText299 "\NUL" `shouldBe` Nothing+ mkNonEmptyText299 "x" `shouldSatisfy` isJust++ describe "literalNonEmptyText" $ do+ it "should work" $ do+ Just (literalNonEmptyText @"abc def") `shouldBe` mkNonEmptyText299 "abc def"++-- test cases for bad strings. Alas, the -fdefer-type-errors defers the errors but then happily creates invalid values in runtime. Is this a GHC bug?++ -- describe "rejects invalid strings" $ do+ -- it "rejects string too long" $ do+ -- print (literalNonEmptyText @"abcdefghijkl" :: NonEmptyText 10)+ -- `shouldThrow` (("Invalid NonEmptyText. Needs to be <= 10 characters. Has 12 characters." `isInfixOf`) . show)+ -- it "rejects empty string" $ do+ -- print (literalNonEmptyText @"" :: NonEmptyText 10)+ -- `shouldThrow` (("Symbol is empty" `isInfixOf`) . show)+ -- it "rejects string with leading whitespace" $ do+ -- print (literalNonEmptyText @" abc" :: NonEmptyText 10)+ -- `shouldThrow` (("Symbol has leading whitespace" `isInfixOf`) . show)+ -- it "rejects string with trailing whitespace" $ do+ -- print (literalNonEmptyText @"abc " :: NonEmptyText 10)+ -- `shouldThrow` (("Symbol has leading whitespace" `isInfixOf`) . show)+ -- it "rejects string with leading unicode whitespace" $ do+ -- print (literalNonEmptyText @"\x2000abc" :: NonEmptyText 10)+ -- `shouldThrow` (("Symbol has leading whitespace" `isInfixOf`) . show)++ describe "maybeTextToTruncateNullableNonEmptyText" $ do+ it "common behavior" $ do+ maybeTextToTruncateNullableNonEmptyText299 Nothing `shouldBe` NullableNonEmptyText Nothing+ maybeTextToTruncateNullableNonEmptyText299 (Just "") `shouldBe` NullableNonEmptyText Nothing+ maybeTextToTruncateNullableNonEmptyText299 (Just " ") `shouldBe` NullableNonEmptyText Nothing+ it "type-dependent behavior" $+ hedgehog $ do+ n <- forAll $ Gen.integral $ Range.constant 1 20000+ let n' = fromInteger n+ usePositiveNat n (pure ()) $ \p -> do+ let NullableNonEmptyText mtext =+ matchLength p $+ maybeTextToTruncateNullableNonEmptyText+ (Just $ T.pack $ replicate (n' + 1) 'x')+ (T.length . nonEmptyTextToText <$> mtext) === Just n'++ describe "mkNonEmptyTextWithTruncate" $ do+ it "common behavior" $ do+ mkNonEmptyText299WithTruncate "" `shouldBe` Nothing+ mkNonEmptyText299WithTruncate " " `shouldBe` Nothing+ it "type-dependent behavior" $+ hedgehog $ do+ n <- forAll $ Gen.integral $ Range.constant 1 20000+ let n' = fromInteger n+ usePositiveNat n (pure ()) $ \(_ :: proxy n) -> do+ let mtext = mkNonEmptyTextWithTruncate @n (T.pack $ replicate (n' + 1) 'x')+ (T.length . nonEmptyTextToText <$> mtext) === Just n'
+ test/Specs/TextManipulationSpec.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE QuasiQuotes #-}++module Specs.TextManipulationSpec (spec) where++import Data.Char+import Data.StringVariants.NonEmptyText+import Test.Hspec+import Prelude++spec :: Spec+spec = describe "Text manipulation" do+ describe "filterNonEmptyText" $ do+ it "filterNonEmptyText returns Nothing when filtered string is empty" do+ filterNonEmptyText (const False) [compileNonEmptyTextKnownLength|abc|] `shouldBe` Nothing++ it "filterNonEmptyText returns Nothing when filtered string only has spaces" do+ filterNonEmptyText isSpace [compileNonEmptyTextKnownLength|a b|] `shouldBe` Nothing++ it "filterNonEmptyText returns stripped text" do+ filterNonEmptyText (/= 'a') [compileNonEmptyTextKnownLength|a b a|] `shouldBe` Just (widen [compileNonEmptyTextKnownLength|b|])++ describe "takeNonEmptyText" $ do+ it "takeNonEmptyText returns stripped text" do+ -- taking two characters results in a trailing space+ takeNonEmptyText [compileNonEmptyTextKnownLength|a b|] `shouldBe` (widen [compileNonEmptyTextKnownLength|a|] :: NonEmptyText 2)++ describe "takeNonEmptyTextEnd" $ do+ it "takeNonEmptyTextEnd returns stripped text" do+ -- taking two characters results in a leading space+ takeNonEmptyTextEnd [compileNonEmptyTextKnownLength|a b|] `shouldBe` (widen [compileNonEmptyTextKnownLength|b|] :: NonEmptyText 2)++ describe "chunksOfNonEmptyText" $ do+ it "chunksOfNonEmptyText drops empty chunks" $ do+ chunksOfNonEmptyText [compileNonEmptyTextKnownLength|a b|]+ `shouldBe` ([[compileNonEmptyTextKnownLength|a|], [compileNonEmptyTextKnownLength|b|]] :: [NonEmptyText 1])+ it "chunksOfNonEmptyText strips chunks" $ do+ chunksOfNonEmptyText [compileNonEmptyTextKnownLength|a b|]+ `shouldBe` ([widen [compileNonEmptyTextKnownLength|a|], widen [compileNonEmptyTextKnownLength|b|]] :: [NonEmptyText 2])