Skip to content

[yesod-test] Adds requireJSONResponse function#1646

Merged
MaxGabriel merged 8 commits intomasterfrom
requireJSONResponse
Dec 1, 2019
Merged

[yesod-test] Adds requireJSONResponse function#1646
MaxGabriel merged 8 commits intomasterfrom
requireJSONResponse

Conversation

@MaxGabriel
Copy link
Copy Markdown
Member

@MaxGabriel MaxGabriel commented Nov 24, 2019

This function checks that a response body is JSON, and parses it into a Haskell value. Having something like this function is pretty essential to using Yesod as a JSON API server, so I think it's a good addition. You can use it to parse a Haskell record directly (usually by adding FromJSON classes to your response types), or parse a Value and pull out individual fields, maybe using something like aeson-lens (though probably a testing-specific library would be better).

I debated over these things:

  1. The name. I was thinking of something like [assert/require/decode/parse]JSON[Response/Body]. I ultimately went with requireJSONResponse:
    • decode/parse sound like the aeson functions that return Either or Maybe, and I wanted this function to throw an error if it failed
    • I'm open to using assertJSONResponse—it matches the other functions (assertEq) better—but I think it reads less like English.
    • I chose Response over Body because (a) It also checks the content-type header, which is not in the body (b) "Body" felt slightly in-the-weeds of HTTP; I think "response" is more approachable.
    • Edit: Oh, just realized the function on the serve is called requireJsonBody. That kind of rules out that name to avoid duplicate identifier issues.
  2. Should it require the JSON content type? You can definitely have a server that returns JSON without JSON content types, but I think that's a such a bad idea, it's more likely requiring it helps people if they accidentally don't add the header.
  3. Should it take a String parameter to add to the error message? This would match assertEq, but other functions like statusIs don't take a message. Ultimately I went without it, because the messages felt like I was repeating myself: (comment :: Comment) <- requireJSONResponse "the response has a comment"

Before submitting your PR, check that you've:

After submitting your PR:

  • Update the Changelog.md file with a link to your PR
  • Check that CI passes (or if it fails, for reasons unrelated to your change, like CI timeouts)

This function checks that a response body is JSON, and parses it into a Haskell value. Having something like this function is pretty essential to using Yesod as a JSON API server, so I think it's a good addition. You can use it to parse a Haskell record directly (usually by adding FromJSON classes to your response types), or parse a Value and pull out individual fields, maybe using something like `aeson-lens` (though probably a testing-specific library would be better).

I debated over these things:

1. The name. I was thinking of something like [assert/require/decode/parse]JSON[Response/Body]. I ultimately went with requireJSONResponse:
	- decode/parse sound like the aeson functions that return Either or Maybe, and I wanted this function to throw an error if it failed
	- I'm open to using `assertJSONResponse`—it matches the other functions (`assertEq`) better—but I think it reads less like English.
	- I chose Response over Body because (a) It also checks the content-type header, which is not in the body (b) "Body" felt slightly in-the-weeds of HTTP; I think "response" is more approachable.
2. Should it require the JSON content type? You can definitely have a server that returns JSON without JSON content types, but I think that's a such a bad idea, it's more likely requiring it helps people if they accidentally don't add the header.
3. Should it take a String parameter to add to the error message? This would match `assertEq`, but other functions like `statusIs` don't take a message. Ultimately I went without it, because the messages felt like I was repeating myself: `(comment :: Comment) <- requireJSONResponse "the response has a comment"`
@MaxGabriel
Copy link
Copy Markdown
Member Author

This closes #1643

isJSONContentType
(failure $ T.pack $ "Expected `Content-Type: application/json` in the headers, got: " ++ show headers)
case eitherDecode' body of
-- TODO: include full body in error message?
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could do this if there's a failure. I think it would mostly be quite useful, but would be large and sometimes extremely large (e.g. Base64 encoded images). Thoughts?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about including the first 256 (arbitrary number) bytes?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems pretty reasonable

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, implemented the body preview

requireJSONResponse = do
withResponse $ \(SResponse _status headers body) -> do
let mContentType = lookup hContentType headers
isJSONContentType = maybe False (\contentType -> BS8.takeWhile (/= ';') contentType == "application/json") mContentType
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copied from acceptsJson, mostly

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it work to use acceptsJson here? That would be preferable, since the set of acceptable JSON mime types may expand in the future.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

acceptsJson is MonadHandler, so we couldn't just drop it in here I don't think, but I could create a new function exposed from yesod-core that takes a list of headers ([(CI ByteString, ByteString)], aka RequestHeaders) and returned if one of the was a JSON content type?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realized that the Accept header is slightly different, since it has a list of preferred MIME types. I added a contentTypeIsJson function for general use on Content-Type.

@MaxGabriel
Copy link
Copy Markdown
Member Author

Ok I think this is ready for final review. One last thing is should the function be renamed requireJsonResponse? This matches Yesod.Core.Json's naming scheme, but not aeson's functions (parseJSON)

Copy link
Copy Markdown
Member

@snoyberg snoyberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Want me to merge or release, or you want to?

@MaxGabriel
Copy link
Copy Markdown
Member Author

I'll take the release 👍

@MaxGabriel MaxGabriel merged commit 3fac351 into master Dec 1, 2019
MaxGabriel added a commit that referenced this pull request Dec 1, 2019
@MaxGabriel
Copy link
Copy Markdown
Member Author

Done 👍

@MaxGabriel
Copy link
Copy Markdown
Member Author

(note: this PR was missing a requirement from yesod-test that it was getting the latest yesod-core for the new content type check. I pushed that directly to master)

@jezen jezen deleted the requireJSONResponse branch December 17, 2019 09:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants