banwords (empty) → 0.1.0.0
raw patch · 7 files changed
+1186/−0 lines, 7 filesdep +HUnitdep +attoparsecdep +banwordssetup-changed
Dependencies added: HUnit, attoparsec, banwords, base, bytestring, data-default, test-framework, test-framework-hunit, text, vector
Files
- MIT-LICENSE.txt +20/−0
- README.md +3/−0
- Setup.hs +2/−0
- banwords.cabal +48/−0
- src/Heuristics/BanWords.hs +52/−0
- tests/GoodWords.hs +1001/−0
- tests/Unit.hs +60/−0
+ MIT-LICENSE.txt view
@@ -0,0 +1,20 @@+The MIT License (MIT)+Copyright (c) 2015 FanJam, 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 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.
+ README.md view
@@ -0,0 +1,3 @@+# Intro++A few tools for checking if a word is a close match against a blacklist.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ banwords.cabal view
@@ -0,0 +1,48 @@+name: banwords+version: 0.1.0.0+author: Ian Grant Jeffries+maintainer: ian@housejeffries.com+category: Heuristics+synopsis: Generalized word blacklister+homepage: https://github.com/fanjam/banwords+build-type: Simple+license: MIT+license-file: MIT-LICENSE.txt+cabal-version: >=1.10+extra-source-files: README.md++library+ hs-source-dirs: src+ exposed-modules: Heuristics.BanWords+ default-language: Haskell2010+ ghc-options: -Wall+ default-extensions: OverloadedStrings++ build-depends: attoparsec >= 0.12.1 && < 0.13+ , base >= 4.6 && < 5.0+ , bytestring >= 0.10 && < 0.11+ , data-default >= 0.5 && < 0.6+ , text >= 1.1 && < 1.3+ , vector >= 0.10.12 && < 0.11++test-suite unit+ type: exitcode-stdio-1.0+ hs-source-dirs: tests+ main-is: Unit.hs+ other-modules: GoodWords+ default-language: Haskell2010+ ghc-options: -Wall+ default-extensions: OverloadedStrings++ build-depends: attoparsec+ , banwords+ , base+ , text+ , vector+ , HUnit >= 1.2 && < 1.3+ , test-framework >= 0.8 && < 0.9+ , test-framework-hunit >= 0.3 && < 0.4++source-repository head+ type: git+ location: git://github.com/fanjam/banwords.git
+ src/Heuristics/BanWords.hs view
@@ -0,0 +1,52 @@+module Heuristics.BanWords where++import Prelude hiding (notElem)++import Control.Applicative+import Data.Attoparsec.Text+import Data.Foldable+import Data.Monoid+import Data.Text (Text)+import Data.Vector (Vector)+import qualified Data.Vector as V++-- | Block exact matches.+passesBlacklist :: Vector Text -> Text -> Maybe Text+passesBlacklist blacklist t =+ case parseOnly (banExact blacklist) t of+ Right _ -> Nothing+ Left _ -> Just t++-- | Intended for internal use.+banExact :: Vector Text -> Parser Text+banExact bans = asum $ (\x -> string x *> endOfInput *> return x) <$> bans++-- | Block exact matches, or exact matches that are surrounded by only non-alphabetical characters.+passesBlacklistPlus :: Vector Text -> Text -> Maybe Text+passesBlacklistPlus blacklist t =+ case parseOnly (banAlmostExact blacklist) t of+ Right _ -> Nothing+ Left _ -> Just t++-- | Intended for internal use.+banAlmostExact :: Vector Text -> Parser Text+banAlmostExact bans = asum $ (\x -> skipNonAlphabetical *> string x *> skipNonAlphabetical *> endOfInput *> return x) <$> bans++-- | Intended for internal use.+skipNonAlphabetical :: Parser ()+skipNonAlphabetical = skipWhile $ \c -> notElem c (['a'..'z'] <> ['A'..'Z'])++exampleReserved :: Vector Text+exampleReserved = V.fromList+ [ "accounts"+ , "admin"+ , "beta"+ , "billing"+ , "help"+ , "jobs"+ , "mail"+ , "registration"+ , "root"+ , "security"+ , "support"+ ]
+ tests/GoodWords.hs view
@@ -0,0 +1,1001 @@+-- A modified subset of this list:+--+-- https://github.com/first20hours/google-10000-english+-- commit # bdf424fc5bc6d5e558f51ba74bab33174a601785++module GoodWords where++import Data.Text (Text)+import Data.Vector (Vector)+import qualified Data.Vector as V++goodWords :: Vector Text+goodWords = V.fromList+ [ "entry"+ , "stay"+ , "nature"+ , "orders"+ , "availability"+ , "africa"+ , "summary"+ , "turn"+ , "mean"+ , "growth"+ , "notes"+ , "agency"+ , "king"+ , "monday"+ , "european"+ , "activity"+ , "copy"+ , "although"+ , "drug"+ , "pics"+ , "western"+ , "income"+ , "force"+ , "cash"+ , "employment"+ , "overall"+ , "bay"+ , "river"+ , "commission"+ , "ad"+ , "package"+ , "contents"+ , "seen"+ , "players"+ , "engine"+ , "port"+ , "album"+ , "regional"+ , "stop"+ , "supplies"+ , "started"+ , "administration"+ , "bar"+ , "institute"+ , "views"+ , "plans"+ , "double"+ , "dog"+ , "build"+ , "screen"+ , "exchange"+ , "types"+ , "soon"+ , "sponsored"+ , "lines"+ , "electronic"+ , "continue"+ , "across"+ , "benefits"+ , "needed"+ , "season"+ , "apply"+ , "someone"+ , "held"+ , "ny"+ , "anything"+ , "printer"+ , "condition"+ , "effective"+ , "believe"+ , "organization"+ , "effect"+ , "asked"+ , "eur"+ , "mind"+ , "sunday"+ , "selection"+ , "casino"+ , "pdf"+ , "lost"+ , "tour"+ , "menu"+ , "volume"+ , "cross"+ , "anyone"+ , "mortgage"+ , "hope"+ , "silver"+ , "corporation"+ , "wish"+ , "inside"+ , "solution"+ , "mature"+ , "role"+ , "rather"+ , "weeks"+ , "addition"+ , "came"+ , "supply"+ , "nothing"+ , "certain"+ , "usr"+ , "executive"+ , "running"+ , "lower"+ , "necessary"+ , "union"+ , "jewelry"+ , "according"+ , "dc"+ , "clothing"+ , "mon"+ , "com"+ , "particular"+ , "fine"+ , "names"+ , "robert"+ , "homepage"+ , "hour"+ , "gas"+ , "skills"+ , "six"+ , "bush"+ , "islands"+ , "advice"+ , "career"+ , "military"+ , "rental"+ , "decision"+ , "leave"+ , "british"+ , "teens"+ , "pre"+ , "huge"+ , "sat"+ , "woman"+ , "facilities"+ , "zip"+ , "bid"+ , "kind"+ , "sellers"+ , "middle"+ , "move"+ , "cable"+ , "opportunities"+ , "taking"+ , "values"+ , "division"+ , "coming"+ , "tuesday"+ , "object"+ , "lesbian"+ , "appropriate"+ , "machine"+ , "logo"+ , "length"+ , "actually"+ , "nice"+ , "score"+ , "statistics"+ , "client"+ , "ok"+ , "returns"+ , "capital"+ , "follow"+ , "sample"+ , "investment"+ , "sent"+ , "shown"+ , "saturday"+ , "christmas"+ , "england"+ , "culture"+ , "band"+ , "flash"+ , "ms"+ , "lead"+ , "george"+ , "choice"+ , "went"+ , "starting"+ , "fri"+ , "thursday"+ , "courses"+ , "consumer"+ , "hi"+ , "airport"+ , "foreign"+ , "artist"+ , "outside"+ , "furniture"+ , "levels"+ , "channel"+ , "letter"+ , "mode"+ , "phones"+ , "ideas"+ , "wednesday"+ , "structure"+ , "fund"+ , "summer"+ , "allow"+ , "degree"+ , "contract"+ , "button"+ , "releases"+ , "wed"+ , "homes"+ , "super"+ , "male"+ , "matter"+ , "custom"+ , "virginia"+ , "almost"+ , "took"+ , "located"+ , "multiple"+ , "asian"+ , "distribution"+ , "editor"+ , "inn"+ , "industrial"+ , "cause"+ , "potential"+ , "song"+ , "cnet"+ , "ltd"+ , "los"+ , "hp"+ , "focus"+ , "late"+ , "fall"+ , "featured"+ , "idea"+ , "rooms"+ , "female"+ , "responsible"+ , "inc"+ , "communications"+ , "win"+ , "associated"+ , "thomas"+ , "primary"+ , "cancer"+ , "numbers"+ , "reason"+ , "tool"+ , "browser"+ , "spring"+ , "foundation"+ , "answer"+ , "voice"+ , "eg"+ , "friendly"+ , "schedule"+ , "documents"+ , "communication"+ , "purpose"+ , "feature"+ , "bed"+ , "comes"+ , "police"+ , "everyone"+ , "independent"+ , "ip"+ , "approach"+ , "cameras"+ , "brown"+ , "physical"+ , "operating"+ , "hill"+ , "maps"+ , "medicine"+ , "deal"+ , "hold"+ , "ratings"+ , "chicago"+ , "forms"+ , "glass"+ , "happy"+ , "tue"+ , "smith"+ , "wanted"+ , "developed"+ , "thank"+ , "safe"+ , "unique"+ , "survey"+ , "prior"+ , "telephone"+ , "sport"+ , "ready"+ , "feed"+ , "animal"+ , "sources"+ , "mexico"+ , "population"+ , "pa"+ , "regular"+ , "secure"+ , "navigation"+ , "operations"+ , "therefore"+ , "simply"+ , "evidence"+ , "station"+ , "christian"+ , "round"+ , "paypal"+ , "favorite"+ , "understand"+ , "option"+ , "master"+ , "valley"+ , "recently"+ , "probably"+ , "thu"+ , "rentals"+ , "sea"+ , "built"+ , "publications"+ , "blood"+ , "cut"+ , "worldwide"+ , "improve"+ , "connection"+ , "publisher"+ , "hall"+ , "larger"+ , "anti"+ , "networks"+ , "earth"+ , "parents"+ , "nokia"+ , "impact"+ , "transfer"+ , "introduction"+ , "kitchen"+ , "strong"+ , "tel"+ , "carolina"+ , "wedding"+ , "properties"+ , "hospital"+ , "ground"+ , "overview"+ , "ship"+ , "accommodation"+ , "owners"+ , "disease"+ , "tx"+ , "excellent"+ , "paid"+ , "italy"+ , "perfect"+ , "hair"+ , "opportunity"+ , "kit"+ , "classic"+ , "basis"+ , "command"+ , "cities"+ , "william"+ , "express"+ , "award"+ , "distance"+ , "tree"+ , "peter"+ , "assessment"+ , "ensure"+ , "thus"+ , "wall"+ , "ie"+ , "involved"+ , "el"+ , "extra"+ , "especially"+ , "interface"+ , "partners"+ , "budget"+ , "rated"+ , "guides"+ , "success"+ , "maximum"+ , "ma"+ , "operation"+ , "existing"+ , "quite"+ , "selected"+ , "boy"+ , "amazon"+ , "patients"+ , "restaurants"+ , "beautiful"+ , "warning"+ , "wine"+ , "locations"+ , "horse"+ , "vote"+ , "forward"+ , "flowers"+ , "stars"+ , "significant"+ , "lists"+ , "technologies"+ , "owner"+ , "retail"+ , "animals"+ , "useful"+ , "directly"+ , "manufacturer"+ , "ways"+ , "est"+ , "son"+ , "providing"+ , "rule"+ , "mac"+ , "housing"+ , "takes"+ , "iii"+ , "gmt"+ , "bring"+ , "catalog"+ , "searches"+ , "max"+ , "trying"+ , "mother"+ , "authority"+ , "considered"+ , "told"+ , "xml"+ , "traffic"+ , "programme"+ , "joined"+ , "input"+ , "strategy"+ , "feet"+ , "agent"+ , "valid"+ , "bin"+ , "modern"+ , "senior"+ , "ireland"+ , "teaching"+ , "door"+ , "grand"+ , "testing"+ , "trial"+ , "charge"+ , "units"+ , "instead"+ , "canadian"+ , "cool"+ , "normal"+ , "wrote"+ , "enterprise"+ , "ships"+ , "entire"+ , "educational"+ , "md"+ , "leading"+ , "metal"+ , "positive"+ , "fl"+ , "fitness"+ , "chinese"+ , "opinion"+ , "mb"+ , "asia"+ , "football"+ , "abstract"+ , "uses"+ , "output"+ , "funds"+ , "mr"+ , "greater"+ , "likely"+ , "develop"+ , "employees"+ , "artists"+ , "alternative"+ , "processing"+ , "responsibility"+ , "resolution"+ , "java"+ , "guest"+ , "seems"+ , "publication"+ , "pass"+ , "relations"+ , "trust"+ , "van"+ , "contains"+ , "session"+ , "multi"+ , "photography"+ , "republic"+ , "fees"+ , "components"+ , "vacation"+ , "century"+ , "academic"+ , "assistance"+ , "completed"+ , "skin"+ , "graphics"+ , "indian"+ , "prev"+ , "ads"+ , "mary"+ , "il"+ , "expected"+ , "ring"+ , "grade"+ , "dating"+ , "pacific"+ , "mountain"+ , "organizations"+ , "pop"+ , "filter"+ , "mailing"+ , "vehicle"+ , "longer"+ , "consider"+ , "int"+ , "northern"+ , "behind"+ , "panel"+ , "floor"+ , "german"+ , "buying"+ , "match"+ , "proposed"+ , "default"+ , "require"+ , "iraq"+ , "boys"+ , "outdoor"+ , "deep"+ , "morning"+ , "otherwise"+ , "allows"+ , "rest"+ , "protein"+ , "plant"+ , "reported"+ , "hit"+ , "transportation"+ , "mm"+ , "pool"+ , "mini"+ , "politics"+ , "partner"+ , "disclaimer"+ , "authors"+ , "boards"+ , "faculty"+ , "parties"+ , "fish"+ , "membership"+ , "mission"+ , "eye"+ , "string"+ , "sense"+ , "modified"+ , "pack"+ , "released"+ , "stage"+ , "internal"+ , "goods"+ , "recommended"+ , "born"+ , "unless"+ , "richard"+ , "detailed"+ , "japanese"+ , "race"+ , "approved"+ , "background"+ , "target"+ , "except"+ , "character"+ , "usb"+ , "maintenance"+ , "ability"+ , "maybe"+ , "functions"+ , "ed"+ , "moving"+ , "brands"+ , "places"+ , "php"+ , "pretty"+ , "trademarks"+ , "phentermine"+ , "spain"+ , "southern"+ , "yourself"+ , "etc"+ , "winter"+ , "battery"+ , "youth"+ , "pressure"+ , "submitted"+ , "boston"+ , "debt"+ , "keywords"+ , "medium"+ , "television"+ , "interested"+ , "core"+ , "break"+ , "purposes"+ , "throughout"+ , "sets"+ , "dance"+ , "wood"+ , "msn"+ , "itself"+ , "defined"+ , "papers"+ , "playing"+ , "awards"+ , "fee"+ , "studio"+ , "reader"+ , "virtual"+ , "device"+ , "established"+ , "answers"+ , "rent"+ , "las"+ , "remote"+ , "dark"+ , "programming"+ , "external"+ , "apple"+ , "le"+ , "regarding"+ , "instructions"+ , "min"+ , "offered"+ , "theory"+ , "enjoy"+ , "remove"+ , "aid"+ , "surface"+ , "minimum"+ , "visual"+ , "host"+ , "variety"+ , "teachers"+ , "isbn"+ , "martin"+ , "manual"+ , "block"+ , "subjects"+ , "agents"+ , "increased"+ , "repair"+ , "fair"+ , "civil"+ , "steel"+ , "understanding"+ , "songs"+ , "fixed"+ , "wrong"+ , "beginning"+ , "hands"+ , "associates"+ , "finally"+ , "az"+ , "updates"+ , "desktop"+ , "classes"+ , "paris"+ , "ohio"+ , "gets"+ , "sector"+ , "capacity"+ , "requires"+ , "jersey"+ , "un"+ , "fat"+ , "fully"+ , "father"+ , "electric"+ , "saw"+ , "instruments"+ , "quotes"+ , "officer"+ , "driver"+ , "businesses"+ , "dead"+ , "respect"+ , "unknown"+ , "specified"+ , "restaurant"+ , "mike"+ , "trip"+ , "pst"+ , "worth"+ , "mi"+ , "procedures"+ , "poor"+ , "teacher"+ , "eyes"+ , "relationship"+ , "workers"+ , "farm"+ , "fucking"+ , "georgia"+ , "peace"+ , "traditional"+ , "campus"+ , "tom"+ , "showing"+ , "creative"+ , "coast"+ , "benefit"+ , "progress"+ , "funding"+ , "devices"+ , "lord"+ , "grant"+ , "sub"+ , "agree"+ , "fiction"+ , "hear"+ , "sometimes"+ , "watches"+ , "careers"+ , "beyond"+ , "goes"+ , "families"+ , "led"+ , "museum"+ , "themselves"+ , "fan"+ , "transport"+ , "interesting"+ , "blogs"+ , "wife"+ , "evaluation"+ , "accepted"+ , "former"+ , "implementation"+ , "ten"+ , "hits"+ , "zone"+ , "complex"+ , "th"+ , "cat"+ , "galleries"+ , "references"+ , "die"+ , "presented"+ , "jack"+ , "flat"+ , "flow"+ , "agencies"+ , "literature"+ , "respective"+ , "parent"+ , "spanish"+ , "michigan"+ , "columbia"+ , "setting"+ , "dr"+ , "scale"+ , "stand"+ , "economy"+ , "highest"+ , "helpful"+ , "monthly"+ , "critical"+ , "frame"+ , "musical"+ , "definition"+ , "secretary"+ , "angeles"+ , "networking"+ , "path"+ , "australian"+ , "employee"+ , "chief"+ , "gives"+ , "kb"+ , "bottom"+ , "magazines"+ , "packages"+ , "detail"+ , "francisco"+ , "laws"+ , "changed"+ , "pet"+ , "heard"+ , "begin"+ , "individuals"+ , "colorado"+ , "royal"+ , "clean"+ , "switch"+ , "russian"+ , "largest"+ , "african"+ , "guy"+ , "titles"+ , "relevant"+ , "guidelines"+ , "justice"+ , "connect"+ , "bible"+ , "dev"+ , "cup"+ , "basket"+ , "applied"+ , "weekly"+ , "vol"+ , "installation"+ , "described"+ , "demand"+ , "pp"+ , "suite"+ , "vegas"+ , "na"+ , "square"+ , "chris"+ , "attention"+ , "advance"+ , "skip"+ , "diet"+ , "army"+ , "auction"+ , "gear"+ , "lee"+ , "os"+ , "difference"+ , "allowed"+ , "correct"+ , "charles"+ , "nation"+ , "selling"+ , "lots"+ , "piece"+ , "sheet"+ , "firm"+ , "seven"+ , "older"+ , "illinois"+ , "regulations"+ , "elements"+ , "species"+ , "jump"+ , "cells"+ , "module"+ , "resort"+ , "facility"+ , "random"+ , "pricing"+ , "dvds"+ , "certificate"+ , "minister"+ , "motion"+ , "looks"+ , "fashion"+ , "directions"+ , "visitors"+ , "documentation"+ , "monitor"+ , "trading"+ , "forest"+ , "calls"+ , "whose"+ , "coverage"+ , "couple"+ , "giving"+ , "chance"+ , "vision"+ , "ball"+ , "ending"+ , "clients"+ , "actions"+ , "listen"+ , "discuss"+ , "accept"+ , "automotive"+ , "naked"+ , "goal"+ , "successful"+ , "sold"+ , "wind"+ , "communities"+ , "clinical"+ , "situation"+ , "sciences"+ , "markets"+ , "lowest"+ , "highly"+ , "publishing"+ , "appear"+ , "emergency"+ , "developing"+ , "lives"+ , "currency"+ , "leather"+ , "determine"+ , "temperature"+ , "palm"+ , "announcements"+ , "patient"+ , "actual"+ , "historical"+ , "stone"+ , "bob"+ , "commerce"+ , "ringtones"+ , "perhaps"+ , "persons"+ , "difficult"+ , "scientific"+ , "satellite"+ , "fit"+ , "tests"+ , "village"+ , "amateur"+ , "ex"+ , "met"+ , "pain"+ , "xbox"+ , "particularly"+ , "factors"+ , "coffee"+ , "www"+ , "settings"+ , "buyer"+ , "cultural"+ , "steve"+ , "easily"+ , "oral"+ , "ford"+ , "poster"+ , "edge"+ , "functional"+ , "au"+ , "fi"+ , "closed"+ , "holidays"+ , "ice"+ , "pink"+ , "zealand"+ , "balance"+ , "monitoring"+ , "graduate"+ , "replies"+ , "shot"+ , "nc"+ , "architecture"+ , "initial"+ , "label"+ , "thinking"+ , "scott"+ , "llc"+ , "sec"+ , "recommend"+ , "canon"+ , "league"+ , "waste"+ , "minute"+ , "bus"+ , "provider"+ , "optional"+ , "dictionary"+ , "cold"+ , "accounting"+ , "manufacturing"+ , "sections"+ , "chair"+ , "fishing"+ , "effort"+ , "phase"+ , "fields"+ , "bag"+ , "fantasy"+ , "po"+ , "letters"+ , "motor"+ ]
+ tests/Unit.hs view
@@ -0,0 +1,60 @@+module Main where++import Data.Monoid+import Data.Text (Text)+import qualified Data.Text as T+import Data.Vector (Vector)+import qualified Data.Vector as V+import GoodWords+import Heuristics.BanWords+import Test.Framework (defaultMain, testGroup)+import Test.Framework.Providers.HUnit (testCase)+import Test.HUnit hiding (Test)++main :: IO ()+main = defaultMain+ [ testGroup "unit"+ [ testCase "banExact allows words correctly" exactAcceptable+ , testCase "banExact blocks words correctly" exactUnacceptable+ , testCase "banAlmostExact allows words correctly" almostAcceptable+ , testCase "banAlmostExact blocks words correctly" almostUnacceptable+ ]+ ]++exactAcceptable :: Assertion+exactAcceptable = V.mapM_ (assertAllowed $ passesBlacklist exampleReserved) $ goodWords <> goodExtraChars <> badExtraChars++exactUnacceptable :: Assertion+exactUnacceptable = V.mapM_ (assertBanned $ passesBlacklist exampleReserved) (V.fromList ["admin", "security"])++almostAcceptable :: Assertion+almostAcceptable = V.mapM_ (assertAllowed $ passesBlacklistPlus exampleReserved) $ goodWords <> goodExtraChars++almostUnacceptable :: Assertion+almostUnacceptable = V.mapM_ (assertBanned $ passesBlacklistPlus exampleReserved) badExtraChars++goodExtraChars :: Vector Text+goodExtraChars = V.fromList+ [ " hill", "hill "+ , "&$4*hill", "hill&1$*", "&$7@hill$2*&"+ , " &*3 hill", "hill &* 4 ", " *& hill &4 "+ ]++badExtraChars :: Vector Text+badExtraChars = V.fromList+ [ " admin", "admin "+ , "&$4*admin", "admin&1$*", "&$7@admin$2*&"+ , " &*3 admin", "admin &* 4 ", " *& admin &4 "+ ]++assertAllowed :: (Text -> Maybe Text) -> Text -> Assertion+assertAllowed f t =+ case f t of+ Nothing -> assertFailure (T.unpack t <> " should have been allowed")+ Just _ -> return ()++assertBanned :: (Text -> Maybe Text) -> Text -> Assertion+assertBanned f t =+ case f t of+ Nothing -> return ()+ Just _ -> assertFailure (T.unpack t <> " should have been forbidden")