haskell-servant / servant

Servat is a Haskell DSL for describing, serving, querying, mocking, documenting web applications and more!
https://docs.servant.dev/
1.83k stars 413 forks source link

Read-Only and Write-Only properties in Servant #1025

Open cdupont opened 6 years ago

cdupont commented 6 years ago

Swagger has "readOnly" and "writeOnly" keywords: https://swagger.io/docs/specification/data-models/data-types/#readonly-writeonly They are very practical when some properties appear in the data returned by GET but not in a POST (or vice-versa). For example:

    type: object
    properties:
      id:
        # Returned by GET, not used in POST/PUT/PATCH
        type: integer
        _readOnly: true_
      username:
        type: string
      password:
        # Used in POST/PUT/PATCH, not returned by GET
        type: string
        _writeOnly: true_

How to do that in Servant? I could use Maybes:

data User = User {
   id :: Maybe Text,
   username: Text,
   password: Maybe Text
}

type UserAPI = "users" :> Get '[JSON] [User]
                 :<|> "users" :> ReqBody '[JSON] User :> Post '[JSON] ()

However, the implementer will be responsible to fill or not the id and password. Is there a way to make this in a more type safe way?

phadej commented 6 years ago

Please open this issue in right repository: https://github.com/haskell-servant/servant-swagger

cdupont commented 6 years ago

Hi phadej, I believe this is a generic question... I used the Swagger feature as an example. But this is a very common problem in API design... PS. If you prefer I can remove Swagger from the question.

phadej commented 6 years ago

you can do (google for trees that grow haskell):

data User tag = User
    { userId   :: ReadOnly tag Text
    , userName :: Text
    , userPass :: WriteOnly tag Text
    }

type family ReadOnly tag a :: * where
    ReadOnly Get  a = a
    ReadOnly Post a = ()

type family WriteOnly tag a :: * where
    WriteOnly Get  a = ()
    WriteOnly Post a = a

This approach will leave units () to be filled. It's still worth while when you have a lot of things all in few flavours (like yours read-only / write-only), or when you have few things but modified in a lot ways (syntax trees in compilers).

Alternataively, the separate types

data GetUser = GetUser
    { guserId   :: Text
    , guserName :: Text
    }

data PostUser = PostUser
    { puserName :: Text
    , puserPass :: Text
    }

are easier to start with. You can reduce boilerplate with ad-hoc typeclasses:

-- or lensy variant... Google for "classy lenses"
-- https://talks.bfpg.org/talks/2015-06-09.next_level_mtl.html
class    HasUserName a        where userName :: a -> Text
instance HasUserName GetUser  where userName = guserName
instance HasUserName PostUser where userName = puserName
arianvp commented 6 years ago

The best way to handle this is at your data model level, not the API level.

We distinguish in our code between UserRegistration and User objects. This has the added benefit of not accidentally leaking the password in the implementation of the getCurrentUser function, as it can't even access it.

data UserRegistration = UserRegistration  { username :: Text, password :: Text }

data User =  User { username :: Text,  lastLoggedIn :: Date }

-- | Inserts a new user
registerUser :: UserRegistration -> IO User

getCurrentUser :: IO User

Then our API becomes:


type UserAPI = "users" :> Get '[JSON] [User]
                 :<|> "users" :> ReqBody '[JSON] UserRegistration :> Post '[JSON] ()

server :: Server UserAPI
server =  getUser <|> void . registerUser
arianvp commented 6 years ago

Seems @phadej and I gave the exact same answer at the exact same time :) except @phadej 's approach is even a bit more flexible and more closely models what Swagger is doing :P

jkarni commented 6 years ago

Ha, was going to suggest the same approach as @phadej . You can add a little more type safety by having a synonym/class with type synonym for Get/ReqBody that checks that it's argument is reasonable.

type IsRead a where
    IsRead (f 'Write) = 'False
     IsRead (f (g, 'Write)) = 'False
    -- etc.
     IsRead x = 'True
class GoodGet a b where
    type Get a b
instance (IsRead b ~ True) GoodGet a b where
    type Get a b = Servant.Get a b

And likewise for Write/ReqBody. Not perfect, and probably not worth the effort, but should make making a mistake (receiving a read object, sending a write object) nearly impossible.

cdupont commented 6 years ago

Thanks a lot for your response. Having 2 datatypes, one inside the other as you proposed is usually what I do.

cdupont commented 5 years ago

Actually, I have 3 flavors for my data types: read, write, and internal. Read is used for GET operations, write for POST operations, while internal is used when talking with the database. For example, I have a User type:

{
  name : "John Doe" -- read, write and internal
  date_created : "2018-11-30" -- read and internal
  db_link : "XXXX" -- only internal
}

On this example, name is chosen by the user, stored and output. date_created is only stored and output. Finally db_link is used for internal purpose only, it is not input or output. As of now, I encode it like that:

data User = User {
  name :: Text -- read, write and internal
  date_created :: Maybe Text -- read and internal
  db_link :: Maybe Text -- only internal
}

This is simple but error prone. It also adds unnecessary case analysis: db_link is mandatory when reading from the database, but I still have to check the Nothing value. Should I use the tagged approach?