string-variants 0.3.0.0 → 0.3.0.1
raw patch · 7 files changed
+125/−24 lines, 7 filesdep −hspec-coredep −hspec-expectationsdep −refinerynew-uploader
Dependencies removed: hspec-core, hspec-expectations, refinery
Files
- CHANGELOG.md +6/−0
- src/Data/StringVariants/NonEmptyText/Internal.hs +2/−2
- src/Data/StringVariants/NullableNonEmptyText.hs +30/−9
- src/Data/StringVariants/Util.hs +1/−1
- string-variants.cabal +4/−12
- test/Specs/JsonEncodingSpec.hs +72/−0
- test/Specs/NonEmptySpec.hs +10/−0
CHANGELOG.md view
@@ -1,5 +1,11 @@ # Changelog +## [0.3.0.1] - 2024-01-03++- Add support for GHC 9.6+- Fix bug in `mkNullableNonEmptyText` that incorrectly counted leading and trailing whitespace against the character limit.+- Improve documentation for `NullableNonEmptyText`+ ## [0.3.0.0] - 2023-10-24 - Remove incorrect `Semigroup` instance for `NonEmptyText`
src/Data/StringVariants/NonEmptyText/Internal.hs view
@@ -77,8 +77,8 @@ instance TypeError ( 'Text "An instance of 'Semigroup (NonEmptyText n)' would violate the "- ':<>: 'Text "length guarantees."- ':$$: 'Text "Please use '(<>|)' or 'concatWithSpace' to combine the values."+ ' :<>: 'Text "length guarantees."+ ' :$$: 'Text "Please use '(<>|)' or 'concatWithSpace' to combine the values." ) => Semigroup (NonEmptyText n) where (<>) = error "unreachable"
src/Data/StringVariants/NullableNonEmptyText.hs view
@@ -50,24 +50,45 @@ -- | 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.+-- @'NullableNonEmptyText' n@ is used in API types to represent optional text fields when you do not want an empty string to fail to parse.+-- Like 'NonEmptyText', the payload 'Text' is guaranteed to be non-empty, within the character limit, and stripped of whitespace.+-- Unlike 'NonEmptyText', it will successfully parse empty strings as 'nullNonEmptyText'. ----- 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.+-- Since Aeson version 2.2, fields of this type maybe be missing, @null@, or empty without failing to parse.+-- Avoid using @Maybe (NullableNonEmptyText n)@ in API types, since it creates unnecessary edge cases that complicate the code. ----- To extract @Maybe NonEmptyText@ values from @Maybe NullableNonEmptyText@,--- use 'nullableNonEmptyTextToMaybeNonEmptyText'.+-- __NB:__ When using a version of Aeson prior to 2.2, you /must/ use @Maybe (NullableNonEmptyText n)@ if you want to allow missing or null fields to parse.+--+-- @+-- data Person = Person+-- { name :: 'NonEmptyText' 50+-- , catchphrase :: 'NullableNonEmptyText' 500+-- }+-- @+--+-- With this type definition, these four JSON objects below are valid and parse as @Person "Daniel" nullNonEmptyText@.+--+-- > {"name": "Daniel"}+-- > {"name": "Daniel", catchphrase: null}+-- > {"name": "Daniel", catchphrase: ""}+-- > {"name": "Daniel", catchphrase: " "}+--+-- These two JSON objects parses as @Person "Daniel" (mkNullableNonEmptyText "Yabba-Dabba Do!")@+--+-- > {"name": "Daniel", catchphrase: "Yabba-Dabba Do!"}+-- > {"name": "Daniel", catchphrase: " Yabba-Dabba Do! "}+--+-- Use 'nullableNonEmptyTextToMaybeNonEmptyText' to extract @Maybe (NonEmptyText n)@ from @NullableNonEmptyText n@. newtype NullableNonEmptyText n = NullableNonEmptyText (Maybe (NonEmptyText n)) deriving stock (Generic, Show, Read, Lift) deriving newtype (Eq) mkNullableNonEmptyText :: forall n. (KnownNat n, 1 <= n) => Text -> Maybe (NullableNonEmptyText n) mkNullableNonEmptyText t- | T.compareLength t (fromIntegral $ natVal (Proxy @n)) == GT = Nothing -- we can't store text that is too long+ | T.compareLength stripped (fromIntegral $ natVal (Proxy @n)) == GT = Nothing -- we can't store text that is too long | otherwise = Just $ NullableNonEmptyText $ mkNonEmptyText t+ where+ stripped = T.filter (/= '\NUL') $ T.strip t nullNonEmptyText :: NullableNonEmptyText n nullNonEmptyText = NullableNonEmptyText Nothing
src/Data/StringVariants/Util.hs view
@@ -66,4 +66,4 @@ 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."))+ (TypeError ('Text "Invalid NonEmptyText. Needs to be <= " ' :<>: 'ShowType n ' :<>: 'Text " characters. Has " ' :<>: 'ShowType (SymbolLength 0 (UnconsSymbol s)) ' :<>: 'Text " characters."))
string-variants.cabal view
@@ -1,11 +1,11 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.35.2.+-- This file has been generated from package.yaml by hpack version 0.36.0. -- -- see: https://github.com/sol/hpack name: string-variants-version: 0.3.0.0+version: 0.3.0.1 synopsis: Constrained text newtypes description: See README at <https://github.com/MercuryTechnologies/string-variants#readme>. category: Data@@ -16,7 +16,7 @@ license-file: LICENSE build-type: Simple tested-with:- GHC ==9.2.4 || ==9.4.2+ GHC ==9.2.8 || ==9.4.8 || ==9.6.3 extra-source-files: CHANGELOG.md @@ -81,7 +81,6 @@ , bytestring , mono-traversable , refined- , refinery , string-conversions , template-haskell , text@@ -92,6 +91,7 @@ main-is: Main.hs other-modules: Spec+ Specs.JsonEncodingSpec Specs.NonEmptySpec Specs.TextManipulationSpec Paths_string_variants@@ -138,19 +138,11 @@ 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
+ test/Specs/JsonEncodingSpec.hs view
@@ -0,0 +1,72 @@+{-# LANGUAGE CPP #-}++module Specs.JsonEncodingSpec (spec) where++import Data.Aeson+import Data.StringVariants.NonEmptyText+import Data.StringVariants.NonEmptyText.Internal+import Data.StringVariants.NullableNonEmptyText+import GHC.Generics+import Prelude+import Test.Hspec++data Person = Person {name :: NonEmptyText 6, catchphrase :: NullableNonEmptyText 15}+ deriving stock (Eq, Generic, Show)+ deriving anyclass (FromJSON, ToJSON)++spec :: Spec+spec = describe "FromJSON instances" $ do+ describe "NonEmptyText" $ do+ it "rejects strings that are too long" $+ assertParseFailure $+ object ["name" .= replicate 7 'a', "catchphrase" .= String "Yabba-Dabba Do!"]+ it "rejects missing properties" $+ assertParseFailure $+ object ["catchphrase" .= String "Yabba-Dabba Do!"]+ it "rejects null properties" $+ assertParseFailure $+ object ["name" .= Null, "catchphrase" .= String "Yabba-Dabba Do!"]+ it "rejects empty strings" $+ assertParseFailure $+ object ["name" .= String "", "catchphrase" .= String "Yabba-Dabba Do!"]+ it "rejects whitespace strings" $+ assertParseFailure $+ object ["name" .= String " ", "catchphrase" .= String "Yabba-Dabba Do!"]+ it "strips whitespace" $+ object ["name" .= String " Daniel ", "catchphrase" .= String "Yabba-Dabba Do!"]+ `shouldParseAs`+ Person (NonEmptyText "Daniel") (NullableNonEmptyText $ Just $ NonEmptyText "Yabba-Dabba Do!")+ describe "NullableNonEmptyText" $ do+ it "rejects strings that are too long" $+ assertParseFailure $+ object ["name" .= String "Daniel", "catchphrase" .= replicate 16 'a']+#if MIN_VERSION_aeson(2,2,0)+ it "accepts missing properties" $+ object ["name" .= String "Daniel"]+ `shouldParseAs`+ Person (NonEmptyText "Daniel") (NullableNonEmptyText Nothing)+ it "accepts null properties" $+ object ["name" .= String "Daniel", "catchphrase" .= Null]+ `shouldParseAs`+ Person (NonEmptyText "Daniel") (NullableNonEmptyText Nothing)+#endif+ it "accepts empty strings" $+ object ["name" .= String "Daniel", "catchphrase" .= String ""]+ `shouldParseAs`+ Person (NonEmptyText "Daniel") (NullableNonEmptyText Nothing)+ it "accepts whitespace strings" $+ object ["name" .= String "Daniel", "catchphrase" .= String " "]+ `shouldParseAs`+ Person (NonEmptyText "Daniel") (NullableNonEmptyText Nothing)+ it "strips whitespace" $+ object ["name" .= String "Daniel", "catchphrase" .= String " Yabba-Dabba Do! "]+ `shouldParseAs`+ Person (NonEmptyText "Daniel") (NullableNonEmptyText $ Just $ NonEmptyText "Yabba-Dabba Do!")+ where+ assertParseFailure :: HasCallStack => Value -> IO ()+ assertParseFailure val =+ decode @Person (encode val) `shouldBe` Nothing++ shouldParseAs :: HasCallStack => Value -> Person -> IO ()+ shouldParseAs val person =+ decode (encode val) `shouldBe` Just person
test/Specs/NonEmptySpec.hs view
@@ -103,3 +103,13 @@ usePositiveNat n (pure ()) $ \(_ :: proxy n) -> do let mtext = mkNonEmptyTextWithTruncate @n (T.pack $ replicate (n' + 1) 'x') (T.length . nonEmptyTextToText <$> mtext) === Just n'++ describe "mkNullableNonEmptyText" $ do+ it "should reject strings that are too big" $+ mkNullableNonEmptyText @2 "hey" `shouldBe` Nothing+ it "should accept empty strings" $+ mkNullableNonEmptyText @2 "" `shouldBe` Just (NullableNonEmptyText Nothing)+ it "should accept whitespace strings" $+ mkNullableNonEmptyText @2 " " `shouldBe` Just (NullableNonEmptyText Nothing)+ it "should strip whitespace" $+ mkNullableNonEmptyText @2 " hi " `shouldBe` Just (NullableNonEmptyText $ Just $ unsafeMkNonEmptyText "hi")