Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add paramListOrNothing function #1130

Merged
merged 7 commits into from
Oct 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion IHP/Controller/Param.hs
Original file line number Diff line number Diff line change
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"]
amitaibu marked this conversation as resolved.
Show resolved Hide resolved
--
-- 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)
amitaibu marked this conversation as resolved.
Show resolved Hide resolved
|> 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
Original file line number Diff line number Diff line change
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