Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to deal with incomplete JSON/Record types (IE missing required fields which I'll later fill in)?

EDIT: For those with similar ailments, I found this is related to the "Extensible Records Problem", something I will personally research more into.


EDIT2: I have started to solve this (weeks later now) by being pretty explicit about data types, and having multiple data types per semantic unit of data. For example, if the database holds an X, my code has an XAction for representing things I want to do with an X, and XResponse for relaying Xs to an http client. And then I need to build the supporting code for shuttling bits between instances. Not ideal, but, I like that it's explicit, and hopefully when my models crystallize, it shouldn't really need much up keep, and should be very reliable.


I'm not sure what the correct level of abstraction is for tackling this problem (ie records? or Yesod?) So I'll just lay out the simple case.

Simple Case / TL;DR

I want to decode a request body into a type

data Comment = Comment {userid :: ..., comment :: ...}

but actually I don't want the request body to contain userid, the server will supply that based on their Auth Headers, (or wherever I want to get data to default fill a field).

So they actually pass me something like:

data SimpleComment = SimpleComment {comment :: ...} deriving (Generic, FromJSON)

And I turn it into a Comment. But maintaining both nearly-identical types simultaneously is a hassle, and not DRY.

How do I solve this problem?


Details on Problem

I have a record type:

data Comment = Comment {userid :: ..., comment :: ...}

I have a POST route:

postCommentR :: Handler Value
postCommentR = do
  c <- requireJsonBody :: (Handler Comment)
  insertedComment <- runDB ...
  returnJson insertedComment

Notice that the Route requires that the user supply their userid (in the Comment type, which is at least redundant since their id is associated with their auth headers. At worst, it means I need to check that users are adding their own id, or throwing away their supplied id, in which case why did they supply it in the first case.

So, I want a record type that's Comment minus userid, but I don't know how to do that intelligently.


My Current (awful but working) Solution

So I made a custom type with derived FromJSON (for the request body) which is almost completely redundant with the Comment type.

data SimpleComment = SimpleComment {comment :: ...} deriving (Generic, FromJSON)

Then my new route needs to decode the request body according to this, and then merge a SimpleComment with a userid field to make it a Comment:

postComment2R :: Handler Value
postComment2R = do
  c <- requireJsonBody :: (Handler SimpleComment)
  (uid, _) requireAuthPair
  insertedComment <- runDB $ insertEntity (Comment { commentUserid  = uid
                                                   , commentComment = comment c})
  returnJson ...

Talk about boilerplate. And my use case is more complex than this simple Comment type.

If it factors in, you might be able to tell, I'm using the Yesod Scaffolding.

like image 842
Josh.F Avatar asked Feb 03 '17 20:02

Josh.F


2 Answers

What I usually do to get a type minus a field is just to have a function which take that field and return the type. In your case you just need to declare an JSON instance for UserId -> Comment. Ok it doesn't seem natural and you have to go it manually but it actually works really well, especially as there is only one field of type UserId in Comment.

like image 75
mb14 Avatar answered Nov 16 '22 12:11

mb14


A solution I like is to use a wrapper for things that come from/go to the DB:

data Authenticated a = Authenticated
  { uid :: Uid
  , thing :: a
  } deriving (Show)

Then you can have Comment be just SimpleComment and turn it into an Authenticated Comment once you know the user id.

like image 20
adamse Avatar answered Nov 16 '22 12:11

adamse