Skip to content

Commit

Permalink
Add paramListOrNothing function (#1130)
Browse files Browse the repository at this point in the history
* Add paramListOrNothing function

* Fix typo

* Apply suggestions from code review

Co-authored-by: Marc Scholten <marcphilipscholten@gmail.com>

* Start adding tests

* Fix test

* Empty text should be Nothing

* Check empty ByteString

Co-authored-by: Marc Scholten <marcphilipscholten@gmail.com>
  • Loading branch information
amitaibu and mpscholten committed Oct 12, 2021
1 parent 6238395 commit 788c2af
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 14 deletions.
33 changes: 32 additions & 1 deletion IHP/Controller/Param.hs
Expand Up @@ -120,6 +120,37 @@ paramList name =
|> DeepSeq.force
{-# INLINABLE paramList #-}

-- | Similiar to 'paramOrNothing' but works with multiple params. This is useful when submitting multiple
-- input fields with the same name, and some may be empty.
--
-- Given a query like (note the `ingredients` in the middle that has no value):
--
-- > ingredients=milk&ingredients&ingredients=egg
--
-- This will return:
--
-- >>> paramListOrNothing @Text "ingredients"
-- [Just "milk", Nothing, Just "egg"]
--
-- When no parameter with the name is given, an empty list is returned:
--
-- >>> paramListOrNothing @Text "not_given_in_url"
-- []
--
--
paramListOrNothing :: forall valueType. (?context :: ControllerContext, DeepSeq.NFData valueType, ParamReader valueType) => ByteString -> [Maybe valueType]
paramListOrNothing name =
allParams
|> filter (\(paramName, paramValue) -> paramName == name)
|> mapMaybe (\(paramName, paramValue) -> paramValue)
|> map (\paramValue -> if paramValue == "" then Left "Empty ByteString" else readParameter @valueType paramValue)
|> map (\value -> case value of
Left _ -> Nothing
Right val -> Just val
)
|> DeepSeq.force
{-# INLINABLE paramListOrNothing #-}

paramParserErrorMessage name = "param: Parameter '" <> cs name <> "' is invalid"

-- | Thrown when a parameter is missing when calling 'param "myParam"' or related functions
Expand Down Expand Up @@ -214,7 +245,7 @@ paramOrNothing !name =

-- | Like 'param', but returns @Left "Some error message"@ if the parameter is missing or invalid
paramOrError :: forall paramType. (?context :: ControllerContext) => ParamReader paramType => ByteString -> Either ParamException paramType
paramOrError !name =
paramOrError !name =
let
RequestContext { requestBody } = ?context |> get #requestContext
in case requestBody of
Expand Down
39 changes: 26 additions & 13 deletions Test/Controller/ParamSpec.hs
Expand Up @@ -85,6 +85,19 @@ tests = do
let ?context = createControllerContextWithParams []
(paramList @Int "numbers") `shouldBe` []

describe "paramListOrNothing" do
it "should parse valid input" do
let ?context = createControllerContextWithParams [("ingredients", "milk"), ("ingredients", ""), ("ingredients", "egg")]
(paramListOrNothing @Text "ingredients") `shouldBe` [Just "milk", Nothing, Just "egg"]

it "should not fail on invalid input" do
let ?context = createControllerContextWithParams [("numbers", "1"), ("numbers", "NaN")]
(paramListOrNothing @Int "numbers") `shouldBe` [Just 1, Nothing]

it "should deal with empty input" do
let ?context = createControllerContextWithParams []
(paramListOrNothing @Int "numbers") `shouldBe` []

describe "hasParam" do
it "returns True if param given" do
let ?context = createControllerContextWithParams [("a", "test")]
Expand Down Expand Up @@ -357,46 +370,46 @@ tests = do
describe "fill" do
it "should fill provided values if valid" do
let ?context = createControllerContextWithParams [("boolField", "on"), ("colorField", "Red")]

let emptyRecord = FillRecord { boolField = False, colorField = Yellow, meta = def }
let expectedRecord = FillRecord { boolField = True, colorField = Red, meta = def { touchedFields = ["colorField", "boolField"] } }

let filledRecord = emptyRecord |> fill @["boolField", "colorField"]
filledRecord `shouldBe` expectedRecord

it "should not touch fields if a field is missing" do
let ?context = createControllerContextWithParams [("colorField", "Red")]

let emptyRecord = FillRecord { boolField = False, colorField = Yellow, meta = def }
let expectedRecord = FillRecord { boolField = False, colorField = Red, meta = def { touchedFields = ["colorField"] } }

let filledRecord = emptyRecord |> fill @["boolField", "colorField"]
filledRecord `shouldBe` expectedRecord

it "should add validation errors if the parsing fails" do
let ?context = createControllerContextWithParams [("colorField", "invalid color")]

let emptyRecord = FillRecord { boolField = False, colorField = Yellow, meta = def }
let expectedRecord = FillRecord { boolField = False, colorField = Yellow, meta = def { annotations = [("colorField", TextViolation "Invalid value")] } }

let filledRecord = emptyRecord |> fill @["boolField", "colorField"]
filledRecord `shouldBe` expectedRecord

it "should deal with json values" do
let ?context = createControllerContextWithJson "{\"colorField\":\"Red\",\"boolField\":true}"

let emptyRecord = FillRecord { boolField = False, colorField = Yellow, meta = def }
let expectedRecord = FillRecord { boolField = True, colorField = Red, meta = def { touchedFields = ["colorField", "boolField"] } }

let filledRecord = emptyRecord |> fill @["boolField", "colorField"]
filledRecord `shouldBe` expectedRecord

it "should deal with empty json values" do
let ?context = createControllerContextWithJson "{}"

let emptyRecord = FillRecord { boolField = False, colorField = Yellow, meta = def }
let expectedRecord = FillRecord { boolField = False, colorField = Yellow, meta = def }

let filledRecord = emptyRecord |> fill @["boolField", "colorField"]
filledRecord `shouldBe` expectedRecord

Expand Down

0 comments on commit 788c2af

Please sign in to comment.