packages feed

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 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