Fix #232
This commit is contained in:
parent
92da5a3123
commit
52e99bae53
@ -109,6 +109,7 @@ dependencies:
|
|||||||
- clientsession
|
- clientsession
|
||||||
- monad-memo
|
- monad-memo
|
||||||
- xss-sanitize
|
- xss-sanitize
|
||||||
|
- text-metrics
|
||||||
|
|
||||||
other-extensions:
|
other-extensions:
|
||||||
- GeneralizedNewtypeDeriving
|
- GeneralizedNewtypeDeriving
|
||||||
|
|||||||
@ -23,7 +23,8 @@ import Data.Monoid (Sum(..))
|
|||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Data.Universe
|
import Data.Universe
|
||||||
import Data.Universe.Helpers
|
import Data.Universe.Helpers
|
||||||
import Data.UUID.Types
|
import Data.UUID.Types (UUID)
|
||||||
|
import qualified Data.UUID.Types as UUID
|
||||||
|
|
||||||
import Data.Default
|
import Data.Default
|
||||||
|
|
||||||
@ -74,11 +75,11 @@ import System.Random (Random(..))
|
|||||||
import Data.Data (Data)
|
import Data.Data (Data)
|
||||||
|
|
||||||
import Model.Types.Wordlist
|
import Model.Types.Wordlist
|
||||||
|
import Data.Text.Metrics (damerauLevenshtein)
|
||||||
|
|
||||||
instance PathPiece UUID where
|
instance PathPiece UUID where
|
||||||
fromPathPiece = Data.UUID.Types.fromString . unpack
|
fromPathPiece = UUID.fromString . unpack
|
||||||
toPathPiece = pack . toString
|
toPathPiece = pack . UUID.toString
|
||||||
|
|
||||||
instance (CI.FoldCase s, PathPiece s) => PathPiece (CI s) where
|
instance (CI.FoldCase s, PathPiece s) => PathPiece (CI s) where
|
||||||
fromPathPiece = fmap CI.mk . fromPathPiece
|
fromPathPiece = fmap CI.mk . fromPathPiece
|
||||||
@ -439,10 +440,10 @@ data StudyFieldType = FieldPrimary | FieldSecondary
|
|||||||
derivePersistField "StudyFieldType"
|
derivePersistField "StudyFieldType"
|
||||||
|
|
||||||
instance PersistField UUID where
|
instance PersistField UUID where
|
||||||
toPersistValue = PersistDbSpecific . toASCIIBytes
|
toPersistValue = PersistDbSpecific . UUID.toASCIIBytes
|
||||||
fromPersistValue (PersistText t) = maybe (Left "Failed to parse UUID") Right $ fromText t
|
fromPersistValue (PersistText t) = maybe (Left "Failed to parse UUID") Right $ UUID.fromText t
|
||||||
fromPersistValue (PersistByteString bs) = maybe (Left "Failed to parse UUID") Right $ fromASCIIBytes bs
|
fromPersistValue (PersistByteString bs) = maybe (Left "Failed to parse UUID") Right $ UUID.fromASCIIBytes bs
|
||||||
fromPersistValue (PersistDbSpecific bs) = maybe (Left "Failed to parse UUID") Right $ fromASCIIBytes bs
|
fromPersistValue (PersistDbSpecific bs) = maybe (Left "Failed to parse UUID") Right $ UUID.fromASCIIBytes bs
|
||||||
fromPersistValue x = Left $ "Expected UUID, received: " <> tshow x
|
fromPersistValue x = Left $ "Expected UUID, received: " <> tshow x
|
||||||
|
|
||||||
instance PersistFieldSql UUID where
|
instance PersistFieldSql UUID where
|
||||||
@ -620,7 +621,8 @@ instance ToJSON Pseudonym where
|
|||||||
toJSON = toJSON . (review pseudonymWords :: Pseudonym -> [PseudonymWord])
|
toJSON = toJSON . (review pseudonymWords :: Pseudonym -> [PseudonymWord])
|
||||||
|
|
||||||
pseudonymWordlist :: [PseudonymWord]
|
pseudonymWordlist :: [PseudonymWord]
|
||||||
pseudonymWordlist = $(wordlist "config/wordlist.txt")
|
pseudonymCharacters :: Set Char
|
||||||
|
(pseudonymWordlist, pseudonymCharacters) = $(wordlist "config/wordlist.txt")
|
||||||
|
|
||||||
pseudonymWords :: Prism' [PseudonymWord] Pseudonym
|
pseudonymWords :: Prism' [PseudonymWord] Pseudonym
|
||||||
pseudonymWords = prism' pToWords pFromWords
|
pseudonymWords = prism' pToWords pFromWords
|
||||||
@ -643,14 +645,32 @@ pseudonymWords = prism' pToWords pFromWords
|
|||||||
maxWord = 0b111111111111
|
maxWord = 0b111111111111
|
||||||
|
|
||||||
pseudonymText :: Prism' Text Pseudonym
|
pseudonymText :: Prism' Text Pseudonym
|
||||||
pseudonymText = iso tFromWords tToWords . pseudonymWords
|
pseudonymText = prism' tToWords tFromWords . pseudonymWords
|
||||||
where
|
where
|
||||||
tFromWords :: Text -> [PseudonymWord]
|
tFromWords :: Text -> Maybe [PseudonymWord]
|
||||||
tFromWords = map CI.mk . Text.words
|
tFromWords = mapM (disambiguate . CI.mk) . filter (not . null) . Text.split (\c -> not $ Set.member c pseudonymCharacters)
|
||||||
|
|
||||||
|
disambiguate :: CI Text -> Maybe PseudonymWord
|
||||||
|
disambiguate inp
|
||||||
|
| [choice] <- inp ^.. pseudonymWord = Just choice
|
||||||
|
| otherwise = Nothing
|
||||||
|
|
||||||
tToWords :: [PseudonymWord] -> Text
|
tToWords :: [PseudonymWord] -> Text
|
||||||
tToWords = Text.unwords . map CI.original
|
tToWords = Text.unwords . map CI.original
|
||||||
|
|
||||||
|
pseudonymWord :: Fold (CI Text) PseudonymWord
|
||||||
|
pseudonymWord = folding disambiguate
|
||||||
|
where
|
||||||
|
disambiguate :: CI Text -> [PseudonymWord]
|
||||||
|
disambiguate inp
|
||||||
|
| [other] <- filter (== inp) pseudonymWordlist = [other]
|
||||||
|
| otherwise = do
|
||||||
|
other <- pseudonymWordlist
|
||||||
|
let distance = (damerauLevenshtein `on` CI.foldedCase) inp other
|
||||||
|
guard $ distance <= 2 -- Smallest distance in vocabulary is just 1, be slightly more generous
|
||||||
|
return other
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
data AuthTag
|
data AuthTag
|
||||||
= AuthFree
|
= AuthFree
|
||||||
|
|||||||
@ -9,13 +9,21 @@ import Data.Text (Text)
|
|||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
import qualified Data.Text.IO as Text
|
import qualified Data.Text.IO as Text
|
||||||
|
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
|
import qualified Data.Char as Char
|
||||||
|
|
||||||
wordlist :: FilePath -> ExpQ
|
wordlist :: FilePath -> ExpQ
|
||||||
wordlist file = do
|
wordlist file = do
|
||||||
qAddDependentFile file
|
qAddDependentFile file
|
||||||
wordlist' <- runIO $ filter ((||) <$> not . isComment <*> isWord) . Text.lines <$> Text.readFile file
|
wordlist' <- runIO $ filter ((||) <$> not . isComment <*> isWord) . Text.lines <$> Text.readFile file
|
||||||
listE $ map (\(Text.unpack -> word) -> [e|CI.mk $ Text.pack $(lift word)|]) wordlist'
|
let usedChars = Set.unions $ map (Set.fromList . (>>= (\c -> [Char.toUpper c, Char.toLower c])) . Text.unpack) wordlist'
|
||||||
|
tupE
|
||||||
|
[ listE $ map (\(Text.unpack -> word) -> [e|CI.mk $ Text.pack $(lift word)|]) wordlist'
|
||||||
|
, [e|Set.fromList $(lift $ Set.toList usedChars)|]
|
||||||
|
]
|
||||||
|
|
||||||
isWord :: Text -> Bool
|
isWord :: Text -> Bool
|
||||||
isWord t
|
isWord t
|
||||||
|
|||||||
@ -7,6 +7,9 @@ module Model.TypesSpec where
|
|||||||
|
|
||||||
import TestImport
|
import TestImport
|
||||||
|
|
||||||
|
import Control.Lens (review, preview)
|
||||||
|
|
||||||
|
|
||||||
instance Arbitrary Season where
|
instance Arbitrary Season where
|
||||||
arbitrary = elements [minBound..maxBound]
|
arbitrary = elements [minBound..maxBound]
|
||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
@ -18,6 +21,9 @@ instance Arbitrary TermIdentifier where
|
|||||||
return $ TermIdentifier{..}
|
return $ TermIdentifier{..}
|
||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
|
|
||||||
|
instance Arbitrary Pseudonym where
|
||||||
|
arbitrary = Pseudonym <$> arbitraryBoundedIntegral
|
||||||
|
|
||||||
spec :: Spec
|
spec :: Spec
|
||||||
spec = do
|
spec = do
|
||||||
describe "TermIdentifier" $ do
|
describe "TermIdentifier" $ do
|
||||||
@ -28,6 +34,13 @@ spec = do
|
|||||||
, (TermIdentifier 1995 Winter, "W95")
|
, (TermIdentifier 1995 Winter, "W95")
|
||||||
, (TermIdentifier 3068 Winter, "W3068")
|
, (TermIdentifier 3068 Winter, "W3068")
|
||||||
]
|
]
|
||||||
|
describe "Pseudonym" $ do
|
||||||
|
it "has sufficient vocabulary" $
|
||||||
|
(length pseudonymWordlist ^ 2) `shouldBe` (succ $ fromIntegral (maxBound - minBound :: Pseudonym))
|
||||||
|
it "has compatible encoding/decoding to/from Text" . property $
|
||||||
|
\pseudonym -> preview pseudonymText (review pseudonymText pseudonym) == Just pseudonym
|
||||||
|
it "encodes to Text injectively" . property $
|
||||||
|
\p1 p2 -> p1 /= p2 ==> ((/=) `on` review pseudonymText) p1 p2
|
||||||
|
|
||||||
termExample :: (TermIdentifier, Text) -> Expectation
|
termExample :: (TermIdentifier, Text) -> Expectation
|
||||||
termExample (term, encoded) = example $ do
|
termExample (term, encoded) = example $ do
|
||||||
|
|||||||
Reference in New Issue
Block a user