string-variants (empty) → 0.1.0.0
raw patch · 10 files changed
+643/−0 lines, 10 filesdep +QuickCheckdep +aesondep +base
Dependencies added: QuickCheck, aeson, base, bytestring, mono-traversable, refined, refinery, string-conversions, template-haskell, text
Files
- CHANGELOG.md +5/−0
- LICENSE +19/−0
- src/Data/StringVariants.hs +64/−0
- src/Data/StringVariants/NonEmptyText.hs +144/−0
- src/Data/StringVariants/NonEmptyText/Internal.hs +103/−0
- src/Data/StringVariants/NullableNonEmptyText.hs +118/−0
- src/Data/StringVariants/Prose.hs +11/−0
- src/Data/StringVariants/Prose/Internal.hs +59/−0
- src/Data/StringVariants/Util.hs +31/−0
- string-variants.cabal +89/−0
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Changelog++## [0.1.0.0] - 2022-09-28++- Initial release
+ LICENSE view
@@ -0,0 +1,19 @@+MIT License Copyright (c) 2022 Mercury Technologies, Inc++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is furnished+to do so, subject to the following conditions:++The above copyright notice and this permission notice (including the next+paragraph) shall be included in all copies or substantial portions of the+Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS+OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF+OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ src/Data/StringVariants.hs view
@@ -0,0 +1,64 @@+module Data.StringVariants+ ( -- * Non empty text+ NonEmptyText,++ -- ** Constructing+ mkNonEmptyText,+ unsafeMkNonEmptyText,+ mkNonEmptyTextWithTruncate,+ compileNonEmptyText,+ compileNonEmptyTextKnownLength,++ -- ** Converting+ nonEmptyTextToText,+ convertEmptyTextToNothing,+ maybeTextToTruncateNullableNonEmptyText,+ type (<=),+ widen,++ -- * Operations+ takeNonEmptyText,+ takeNonEmptyTextEnd,+ chunksOfNonEmptyText,+ filterNonEmptyText,+ (<>|),++ -- * Conversions between 'Refined' and 'NonEmptyText'.+ ContainsNonWhitespaceCharacters (..),+ exactLengthRefinedToRange,+ nonEmptyTextFromRefined,+ refinedFromNonEmptyText,++ -- * Non-empty, whitespace-trimmed text with no character limit+ Prose,+ mkProse,+ compileProse,+ proseToText,++ -- * Nullable non empty text+ NullableNonEmptyText (..),+ mkNullableNonEmptyText,+ nullNonEmptyText,+ compileNullableNonEmptyText,++ -- ** Converting+ nonEmptyTextToNullable,+ maybeNonEmptyTextToNullable,+ nullableNonEmptyTextToMaybeText,+ nullableNonEmptyTextToMaybeNonEmptyText,+ parseNullableNonEmptyText,+ fromMaybeNullableText,++ -- ** Information+ isNullNonEmptyText,++ -- * Convenience util if you need a NonEmptyText of a dynamically determined lengths+ useNat,+ natOfLength,+ )+where++import Data.StringVariants.NonEmptyText+import Data.StringVariants.NullableNonEmptyText+import Data.StringVariants.Prose+import Prelude ()
+ src/Data/StringVariants/NonEmptyText.hs view
@@ -0,0 +1,144 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeOperators #-}+{-# OPTIONS_GHC -Wno-redundant-constraints #-}++-- | NonEmptyText: Text that is known not to be either the empty string, or pure+-- whitespace.++module Data.StringVariants.NonEmptyText+ ( -- * Non empty text+ NonEmptyText,+ type (<=),++ -- * Construction+ mkNonEmptyText,+ unsafeMkNonEmptyText,+ nonEmptyTextToText,+ compileNonEmptyText,+ compileNonEmptyTextKnownLength,+ convertEmptyTextToNothing,++ -- * Conversion+ widen,++ -- * Functions+ takeNonEmptyText,+ takeNonEmptyTextEnd,+ chunksOfNonEmptyText,+ filterNonEmptyText,+ (<>|),++ -- * 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.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 Language.Haskell.TH+import Language.Haskell.TH.Quote+import Language.Haskell.TH.Syntax (Lift (..))+import Refined+import Refined.Unsafe (reallyUnsafeRefine)+import Prelude++compileNonEmptyText :: Integer -> QuasiQuoter+compileNonEmptyText n =+ QuasiQuoter+ { quoteExp = compileNonEmptyText'+ , quotePat = error "NonEmptyText is not a pattern; use nonEmptyTextToText instead"+ , quoteDec = error "NonEmptyText is not supported at top-level"+ , quoteType = error "NonEmptyText is not supported as a type"+ }+ 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+ Just txt -> [|$(lift txt) :: NonEmptyText $(pure $ LitT $ NumTyLit n)|]++convertEmptyTextToNothing :: Text -> Maybe Text+convertEmptyTextToNothing t+ | textIsWhitespace t = Nothing+ | otherwise = Just t++nonEmptyTextToText :: NonEmptyText n -> Text+nonEmptyTextToText (NonEmptyText t) = t++-- | 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)++-- | Narrows the maximum length, dropping any remaining trailing characters.+takeNonEmptyText :: forall m n. (KnownNat m, KnownNat n, n <= m) => NonEmptyText m -> NonEmptyText n+takeNonEmptyText (NonEmptyText t) =+ if m == n+ then NonEmptyText t+ else NonEmptyText $ 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 (NonEmptyText t) =+ if m == n+ then NonEmptyText t+ else NonEmptyText $ 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.+chunksOfNonEmptyText ::+ forall chunkSize totalSize.+ (KnownNat chunkSize, KnownNat totalSize) =>+ NonEmptyText totalSize ->+ [NonEmptyText chunkSize]+chunksOfNonEmptyText (NonEmptyText t) =+ map NonEmptyText (T.chunksOf chunkSize t)+ where+ chunkSize = fromIntegral $ natVal (Proxy @chunkSize)++-- | Concat two NonEmptyText values, with the new maximum length being the sum of the two+-- maximum lengths of the inputs.+--+-- Mnemonic: @<>@ for monoid, @|@ from NonEmpty's ':|' operator+(<>|) :: NonEmptyText n -> NonEmptyText m -> NonEmptyText (n + m)+(NonEmptyText l) <>| (NonEmptyText r) = NonEmptyText (l <> r)++-- Refinery++data ContainsNonWhitespaceCharacters = ContainsNonWhitespaceCharacters+ deriving stock (Generic)++instance Predicate ContainsNonWhitespaceCharacters Text where+ validate p txt+ | textHasNoMeaningfulContent txt = throwRefineOtherException (typeRep p) "All characters in Text input are whitespace or control characters"+ | otherwise = Nothing++exactLengthRefinedToRange :: Refined (ContainsNonWhitespaceCharacters && SizeEqualTo n) Text -> NonEmptyText n+exactLengthRefinedToRange = NonEmptyText . unrefine++nonEmptyTextFromRefined :: Refined (ContainsNonWhitespaceCharacters && (SizeLessThan n || SizeEqualTo n)) Text -> NonEmptyText n+nonEmptyTextFromRefined = NonEmptyText . unrefine++refinedFromNonEmptyText :: NonEmptyText n -> Refined (ContainsNonWhitespaceCharacters && (SizeLessThan n || SizeEqualTo n)) Text+refinedFromNonEmptyText = reallyUnsafeRefine . nonEmptyTextToText
+ src/Data/StringVariants/NonEmptyText/Internal.hs view
@@ -0,0 +1,103 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeOperators #-}+-- 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, textIsTooLong, 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 => 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 => 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 => Text -> Maybe (NonEmptyText n)+mkNonEmptyText t+ | textIsTooLong stripped (fromIntegral $ natVal (Proxy @n)) = Nothing+ | textIsWhitespace stripped = Nothing+ | otherwise = Just (NonEmptyText stripped)+ where+ stripped = T.filter (/= '\NUL') $ T.strip t++-- | Make a NonEmptyText when you can manually verify the length+unsafeMkNonEmptyText :: forall n. KnownNat 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"+ }
+ src/Data/StringVariants/NullableNonEmptyText.hs view
@@ -0,0 +1,118 @@+{-# LANGUAGE TemplateHaskell #-}++module Data.StringVariants.NullableNonEmptyText+ ( -- Safe to construct, so we can export the contents+ NullableNonEmptyText (..),++ -- * Constructing+ mkNonEmptyTextWithTruncate,+ compileNullableNonEmptyText,+ mkNullableNonEmptyText,+ parseNullableNonEmptyText,+ nullNonEmptyText,++ -- * Conversion+ maybeTextToTruncateNullableNonEmptyText,+ nonEmptyTextToNullable,+ maybeNonEmptyTextToNullable,+ nullableNonEmptyTextToMaybeText,+ nullableNonEmptyTextToMaybeNonEmptyText,+ fromMaybeNullableText,++ -- * Functions+ isNullNonEmptyText,+ )+where++import Control.Monad+import Data.Aeson+import Data.Aeson qualified as J+import Data.Aeson.Key qualified as J+import Data.Aeson.Types qualified as J+import Data.Data (Proxy (..))+import Data.Maybe (fromMaybe)+import Data.StringVariants.NonEmptyText+import Data.StringVariants.Util (textIsTooLong)+import Data.Text (Text)+import Data.Text qualified as T+import GHC.Generics (Generic)+import GHC.TypeLits (KnownNat, natVal)+import Language.Haskell.TH+import Language.Haskell.TH.Quote+import Language.Haskell.TH.Syntax (Lift (..))+import Prelude++-- | Newtype wrapper around Maybe NonEmptyText that converts empty string to 'Nothing'.+--+-- This is aimed primarily at JSON parsing: make it possible to parse empty+-- string and turn it into @Nothing@, in order to convert everything into+-- @Maybe NonEmptyText@ at the edge of the system.+--+-- While using this for JSON parsing, use @Maybe NullableNonEmptyText@. Aeson+-- special-cases @Maybe@ to allow nulls, so @Maybe@ catches the nulls and+-- @NullableNonEmptyText@ catches the empty strings.+--+-- To extract @Maybe NonEmptyText@ values from @Maybe NullableNonEmptyText@,+-- use 'nullableNonEmptyTextToMaybeNonEmptyText'.+newtype NullableNonEmptyText n = NullableNonEmptyText (Maybe (NonEmptyText n))+ deriving stock (Generic, Show, Read, Lift)+ deriving newtype (Eq, ToJSON)++mkNullableNonEmptyText :: forall n. KnownNat n => Text -> Maybe (NullableNonEmptyText n)+mkNullableNonEmptyText t+ | textIsTooLong t (fromIntegral $ natVal (Proxy @n)) = Nothing -- we can't store text that is too long+ | otherwise = Just $ NullableNonEmptyText $ mkNonEmptyText t++mkNonEmptyTextWithTruncate :: forall n. KnownNat n => Text -> Maybe (NonEmptyText n)+mkNonEmptyTextWithTruncate = mkNonEmptyText . T.take (fromInteger (natVal (Proxy @n)))++nullNonEmptyText :: NullableNonEmptyText n+nullNonEmptyText = NullableNonEmptyText Nothing++isNullNonEmptyText :: NullableNonEmptyText n -> Bool+isNullNonEmptyText = (== nullNonEmptyText)++instance KnownNat n => FromJSON (NullableNonEmptyText n) where+ parseJSON = \case+ J.String t -> case mkNullableNonEmptyText t of+ Just txt -> pure txt+ Nothing -> fail $ "Data/StringVariants/NullableNonEmptyText.hs: When trying to parse a NullableNonEmptyText, expected a String of length < " ++ show (natVal (Proxy @n)) ++ ", but received: " ++ T.unpack t+ 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 fieldName obj = obj .:? J.fromText fieldName .!= nullNonEmptyText++fromMaybeNullableText :: Maybe (NullableNonEmptyText n) -> NullableNonEmptyText n+fromMaybeNullableText = fromMaybe nullNonEmptyText++nonEmptyTextToNullable :: NonEmptyText n -> NullableNonEmptyText n+nonEmptyTextToNullable = NullableNonEmptyText . Just++maybeNonEmptyTextToNullable :: Maybe (NonEmptyText n) -> NullableNonEmptyText n+maybeNonEmptyTextToNullable Nothing = nullNonEmptyText+maybeNonEmptyTextToNullable jt = NullableNonEmptyText jt++maybeTextToTruncateNullableNonEmptyText :: forall n. KnownNat n => Maybe Text -> NullableNonEmptyText n+maybeTextToTruncateNullableNonEmptyText mText = NullableNonEmptyText $ mText >>= mkNonEmptyText . T.take (fromInteger (natVal (Proxy @n)))++nullableNonEmptyTextToMaybeNonEmptyText :: NullableNonEmptyText n -> Maybe (NonEmptyText n)+nullableNonEmptyTextToMaybeNonEmptyText (NullableNonEmptyText t) = t++nullableNonEmptyTextToMaybeText :: NullableNonEmptyText n -> Maybe Text+nullableNonEmptyTextToMaybeText (NullableNonEmptyText t) = nonEmptyTextToText <$> t++compileNullableNonEmptyText :: Integer -> QuasiQuoter+compileNullableNonEmptyText n =+ QuasiQuoter+ { quoteExp = compileNullableNonEmptyText'+ , quotePat = error "NullableNonEmptyText is not a pattern; use `nullableNonEmptyTextToMaybeText` instead"+ , quoteDec = error "NullableNonEmptyText is not supported at top-level"+ , quoteType = error "NullableNonEmptyText is not supported as a type"+ }+ 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+ Just txt -> [|$(lift txt)|]
+ src/Data/StringVariants/Prose.hs view
@@ -0,0 +1,11 @@+-- | Prose type: arbitrary length non-empty text that is trimmed.+module Data.StringVariants.Prose+ ( Prose,+ mkProse,+ compileProse,+ proseToText,+ )+where++import Data.StringVariants.Prose.Internal+import Prelude ()
+ src/Data/StringVariants/Prose/Internal.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE TemplateHaskell #-}++-- | Internal module of Prose, allowing breaking the abstraction.+--+-- Prefer to use "Data.StringVariants.Prose" instead.+module Data.StringVariants.Prose.Internal where++import Data.Aeson (FromJSON, ToJSON, ToJSONKey, withText)+import Data.Aeson.Types (FromJSON (..))+import Data.String.Conversions (ConvertibleStrings (..), cs)+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.Lazy qualified as LT+import Language.Haskell.TH.Quote+import Language.Haskell.TH.Syntax+import Prelude++-- | Whitespace-trimmed, non-empty text, for use with API endpoints.+-- The rationale is that there are many situations where if a client sends+-- text that is empty or all whitespace, there's probably a client error.+-- Not suitable for database fields, as there is no character limit (see+-- 'ProsePersistFieldMsg').+newtype Prose = Prose Text+ deriving stock (Eq, Lift, Ord, Show)+ deriving newtype (Semigroup, ToJSON, ToJSONKey)++instance ConvertibleStrings Prose Text where+ convertString (Prose t) = t++instance ConvertibleStrings Prose LT.Text where+ convertString (Prose t) = cs t++instance FromJSON Prose where+ parseJSON = withText "Prose" $ \t -> case mkProse t of+ Nothing -> fail $ "Model/CustomTypes/StringVariants.hs: invalid Prose: " ++ T.unpack t+ Just t' -> pure t'++mkProse :: Text -> Maybe Prose+mkProse t = case T.strip t of+ "" -> Nothing+ t' -> Just (Prose t')++compileProse :: QuasiQuoter+compileProse =+ QuasiQuoter+ { quoteDec = error "Prose is not supported at top-level"+ , quoteType = error "Prose is not supported as a type"+ , quotePat = error "Prose is not a pattern; use `proseToText` instead"+ , ..+ }+ where+ quoteExp s = case mkProse (T.pack s) of+ Nothing -> fail (msg s)+ Just s' -> [|$(lift s')|]++ msg s = "Invalid Prose: " <> s <> ". Make sure you aren't wrapping the text in quotes."++proseToText :: Prose -> Text+proseToText (Prose txt) = txt
+ src/Data/StringVariants/Util.hs view
@@ -0,0 +1,31 @@+module Data.StringVariants.Util (natOfLength, useNat, textIsTooLong, textIsWhitespace, textHasNoMeaningfulContent) where+import GHC.TypeLits (KnownNat, Nat, SomeNat (..), someNatVal)+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)+++natOfLength :: proxy (n :: Nat) -> f (other n) -> f (other n)+natOfLength _ = id++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+
+ string-variants.cabal view
@@ -0,0 +1,89 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.34.7.+--+-- see: https://github.com/sol/hpack++name: string-variants+version: 0.1.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>+license: MIT+license-file: LICENSE+build-type: Simple+tested-with:+ GHC ==9.2.4 || ==9.4.2+extra-source-files:+ CHANGELOG.md++source-repository head+ type: git+ location: https://github.com/MercuryTechnologies/string-variants++library+ exposed-modules:+ Data.StringVariants+ Data.StringVariants.NonEmptyText+ Data.StringVariants.NonEmptyText.Internal+ Data.StringVariants.NullableNonEmptyText+ Data.StringVariants.Prose+ Data.StringVariants.Prose.Internal+ Data.StringVariants.Util+ other-modules:+ Paths_string_variants+ hs-source-dirs:+ src+ default-extensions:+ AllowAmbiguousTypes+ ApplicativeDo+ BlockArguments+ DataKinds+ DeriveAnyClass+ DeriveFoldable+ DeriveFunctor+ DeriveGeneric+ DeriveLift+ DeriveTraversable+ DerivingVia+ FlexibleContexts+ FlexibleInstances+ FunctionalDependencies+ GADTs+ GeneralizedNewtypeDeriving+ ImportQualifiedPost+ InstanceSigs+ LambdaCase+ LexicalNegation+ 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+ build-depends:+ QuickCheck+ , aeson >=2.0.0.0+ , base >=4.9 && <5+ , bytestring+ , mono-traversable+ , refined+ , refinery+ , string-conversions+ , template-haskell+ , text+ default-language: Haskell2010