问题
I have a Yesod route handler that returns a JSON with the object
{ id: 1,
title: "foo"
content: "bar"
}
I would like to add a _links
property with some metadata, that doesn't exist in on the Entity itself, e.g.
{ id: 1,
title: "foo"
content: "bar"
_links: {self: http://localhost:3000/events/1}
}
How can I add the _links
to the existing Entity record? Here's my handler:
getEventR :: EventId -> Handler Value
getEventR eid = do
event <- runDB $ get404 eid
render <- getUrlRender
let renderedUrl = render $ EventR eid
let links = object
[ "self" .= renderedUrl
]
let returnVal = object
[ "data" .= (Entity eid event)
, "_links" .= links
]
return returnVal
回答1:
To do this you'll have to convert your Entity
to a Value
manually, then use the functions in unordered-containers:Data.HashMap.Strict
to insert the "_links"
key, then build a Value
out of it again. Using the lens compatible package for aeson
could probably simplify this quite a bit though:
buildEntityWithLink :: Entity -> Text -> Maybe Value
buildEntityWithLink entity renderedUrl = case toJSON entity of
Object obj ->
let links = object ["self" .= renderedUrl]
entityWithLink = HashMap.insert "_links" links obj
in Just (Object entityWithLink)
_ -> Nothing
(I'm assuming that renderedUrl
has type Text
, change if needed)
Then you can just pass in your Entity
and renderedUrl
to get a new Value
with the "_links"
key included. I've used Maybe
here to guard against the case where toJSON :: Entity -> Value
does not return a Value
with the Object
constructor. This will protect you in the future if you change the Entity
type and how it converts to JSON but forget to update all of your codebase to reflect this change.
EDIT: If you were to use lens-aeson
You could write it like this, though this requires swapping the argument order just for neatness:
buildEntityWithLink :: Text -> Entity -> Value
buildEntityWithLink renderedUrl = (
over _Object $
HashMap.insert "_links" $
object ["self" .= renderedUrl]
) . toJSON
This actually lets you drop the Maybe
, you want to anyway, since the way lenses work means that if the Object
isn't the top level then the original value is returned, so buildEntityWithLink "testlink" ([] :: [Entity])
Would just return the same as toJSON ([] :: [Entity])
, namely an empty Array
. The toJSON
has to be on the outside of the lens operation because in order to compose with _Object
it has to be able to be a setter, and toJSON
can't be easily made into a setter. Instead, we just pre-process our Entity
into a Value
, then feed it in to the lens expression. I've added whitespace where I've lined up the arguments to each function, makes it a bit more readable IMO, but this is all technically 1 line of code. A useful feature about this implementation is that it's now easy to relax the type signature to ToJSON a => Text -> a -> Value
, so you can add a _links
to any type you want to.
回答2:
Based on bheklilr's working comment - here's the final code I now have:
import Data.HashMap.Strict as HashMap (insert)
getEventR :: EventId -> Handler Value
getEventR eid = do
event <- runDB $ get404 eid
render <- getUrlRender
let renderedUrl = render $ EventR eid
let returnVal = object
[ "data" .= [buildEntityWithLink (Entity eid event) renderedUrl]]
return returnVal
buildEntityWithLink :: Entity Event -> Text -> Maybe Value
buildEntityWithLink entity renderedUrl =
case toJSON entity of
Object obj ->
let links = object ["self" .= renderedUrl]
entityWithLink = HashMap.insert "_links" links obj
in Just (Object entityWithLink)
_ -> Nothing
And now indeed, the JSON appears as expected with the self
link
来源:https://stackoverflow.com/questions/34907953/add-nested-property-to-returned-record