haskell-graphql / graphql-api

Write type-safe GraphQL services in Haskell
BSD 3-Clause "New" or "Revised" License
406 stars 35 forks source link

Implement default input values #73

Open teh opened 7 years ago

teh commented 7 years ago

default values are used for input arguments, e.g. type Greet { say(word: String = "hello"): String }

This poses a problem for us because the API is specified as a type, and Haskell does not support encoding terms as types in a straight-forward manner, so we can't make the default value part of the type signature.

We do however need to be able to access defaults independently of running a query for features such as introspection, and validation (input type coercion).

What we can do instead is use a unique type (e.g. newtypeing) and a class instance to return the default value, thus mapping from the type world to the value world, and allowing a type in the signature.

This can be done on a per-type level, e.g.

type Query = Object "Query" '[]
  '[ Argument "age" NewtypedInt :> Field "root" Text ]
instance Defaultable NewtypedInt where defaultFor _ = Just 33

or we could use Symbol kinds to look up defaults. This could be slightly more fragile due to colliding instances, e.g.

type Query = Object "Query" '[]
  '[ ArgumentWithDefault "age" Int "defaultAge" :> Field "root" Text ]
instance Defaultable "defaultAge" where defaultFor = Just 33

The solution above could be made slightly safer by using the whole argument as the instance class variable, though this too can suffer from accidental collisions in large APIs. There may be a unique type combination for indexing but it's not immediately obvious what that would be. Example:

instance Defaultable (Argument "age" Int) where defaultFor = Just 33

An alternative is to have a handler to handle requests, and a parallel handler to return defaults where required. E.g.

buildResolver @Query queryHandler defaultHandler

IMO that imposes a lot of work for handlers that don't have defaults (which seems to be most of them AFAICT).

The solutions so far are all conforming to the spec. We could also compromise conformance and decide to e.g. validate default arguments only in query resolution, and to not export them for introspection.

jml commented 7 years ago

Thanks!

The other issue is that input object types can define default values for fields.

e.g. a Dog input type could look like

{
  name: String,
  legs=4: Int,
}

(sorry, I've got the syntax wrong).

The idea is that if someone sends us a Dog { name = "fido" }, then the user-level resolver code gets Dog { name="fido", legs=4 }.

These defaults are similar to argument defaults, but almost certainly a different code path.

Note that these defaults can be arbitrarily deep.

teh commented 7 years ago

A slightly disgusting hack: We could encode the input object defaults as symbol kinds (aka strings) and then parse them on validation:

type Query = Object "Query" '[]
  '[ ArgumentWithDefault "dogStuff" DogStuff "{toy: \"shoe\", likesTreats: false}" :> Field "root" Text ]
jml commented 7 years ago

Just occurred to me that Defaultable isn't a property of the argument type (e.g. Bool), but rather a property of Argument "foo" Bool.

Maybe doing

instance Defaultable (Argument "foo" Bool) where

Would be more appropriate.

teh commented 7 years ago

Yea, I'm mentioning that option in my summary, it has the downside that you can have only one (Argument "foo" Bool) in your entire program. The super correct way would be to also tag with Query but I'm not confident we can me the type functions for that work out nicely.

jml commented 7 years ago

I guess that's already an issue with the current interface, since you can have only one Bool instance and that can only do one thing when it gets a "foo" argument.

Agree re Query On Thu, 19 Jan 2017 at 20:42, teh notifications@github.com wrote:

Yea, I'm mentioning that option in my summary, it has the downside that you can have only one (Argument "foo" Bool) in your entire program. The super correct way would be to also tag with Query but I'm not confident we can me the type functions for that work out nicely.

— You are receiving this because you commented.

Reply to this email directly, view it on GitHub https://github.com/jml/graphql-api/issues/73#issuecomment-273892553, or mute the thread https://github.com/notifications/unsubscribe-auth/AAHq6guU4WD1_YASH9ZZOix_Ja_nOsT8ks5rT8rBgaJpZM4LkdJB .

jml commented 6 years ago

Isn't this done already? Isn't this Defaultable?

I agree it's not the ideal mechanism, but we have the functionality.