4
votes

I am running into a problem setting up a simple proof of concept servant API. This is my User datatype and my API type:

data User = User { id :: Int, first_name :: String, last_name :: String } deriving (Eq, Show, Generic)
instance FromRow User
instance ToRow User
$(deriveJSON defaultOptions ''User)

type API = "users" :> ReqBody '[JSON] User :> Post '[JSON] User

The handler method for this uses postgresql-simple like this:

create u = liftIO $ head <$> returning connection "insert into users (first_name, last_name) values (?,?) returning id" [(first_name u, last_name u)]

Boilerplate code such as connecting to the db and the routing method has been elided. The problem is that if I make a POST request, what I want to be doing is creating a new user, so I would supply the JSON:

{ "first_name": "jeff", "last_name": "lebowski" }

but then my program fails at runtime with

Error in $: When parsing the record User of type Lib.User the key id was not present. 

This makes sense because the API specified a User which has an id field. But I do not want to have to pass a bogus id in the request (since they are assigned by postgres sequentially) because that is gross. I also can't move the id field out of the User datatype because then postgres-simple fails because of a model-database mismatch when making a GET request to a different endpoint (which does the obvious thing: get by id. Not included above). What do I do here? Write a custom FromJson instance? I already tried setting the Data.Aeson.TH options flag omitNothingFields to True and setting the id field to be a Maybe Int, but that didn't work either. Any advice would be appreciated.

1
Your concepts seem a little muzzy. Consider only pure Haskell. Does mkUser :: User -> User make sense? You need to allow clients to pass you a message that has only the info necessary to create a User. E.g., data CreateUser = CreateUser { givenName :: Text, surName :: Text }, type API = "users" :> ReqBody '[JSON] CreateUser :> Post '[JSON] User.R B
I have considered that, but having to make an extra datatype for every endpoint like this seems a super non-optimal solution. I was hoping there was some other way to do it. I did implement this actually and found myself using a pattern like User, UserI (UserInput), but this represents just the omission of one field, and it seems like a bad idea to have the input type and the saved type be different. What if refactor User but forget to change UserI? Isn't this part of what servant tries to avoid?asg0451
This POST request represents passing a User to the server, which it then saves. Semantically I would want to keep them the same type, or have UserI be derived from User at compile time, or something. And my concepts are not muzzy, I am not inexperienced at Haskell. I realize that the types mean what they mean, but I was just trying to justify the use of servant by making a proof of concept with the simplest API imaginable (CRUD of one resource - users), something which would e.g. be done in Rails for you automatically. And I was surprised at how non-trivial it is.asg0451
I don't know Rails from Adam, but I would assume the trick there is that Ruby allows the ID field to be null (nil, right?). The impedance mismatch here is that a user without an ID is a different thing from a user with a nullable ID is a different thing than a user with a non-nullable ID. There's not really a way to get the type system to lie. I suppose you could come up with a Template Haskell solution that allows you to effectively knock out fields on an existing datatype to produce a new one. I'm not too quick with the TH though so I hope someone else comes along to snipe the answer.R B
@asg0451 You can use a parametrized datatype holding the id and the saved element. I think you can read this from the persistent library : yesodweb.com/book/persistent#persistent_insertJean-Baptiste Potonnier

1 Answers

4
votes

First you need to understand that a User and a row in a table corresponding to this User are two different things.

A row has an id, a user does not. For example, you can imagine to compare two users without dealing with ids, or the fact that they are saved or not.

Once convinced, you will have to explain this to the type system, or you will have to deal with Maybe fields, which I think is not the solution here.

Some people talked about Template Haskell, I think it would be overkill here, and you need to solve the problem first.

What you can do is use a data type to represent a saved row in your database. Let's call it an Entity.

newtype PrimaryKey = PrimaryKey Int

data Entity b = Entity PrimaryKey b

Then the function saving a User row in your database could take a user as parameter and return a PrimaryKey (In you database monad, of course). Other functions reading from the database would return something using Entity User

Your field declarations are not duplicated, since you are reusing the User type as a parameter.

You will have to adapt FromRow/ToRow and FromJSON/ToJSON accordingly.