Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a Request object and an interpretRequest path #128

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

dmaze
Copy link

@dmaze dmaze commented Nov 19, 2017

There is a mostly-standard JSON request format that's pretty widely support across GraphQL implementations, even if not part of the spec per se. This PR adds a Request type that mirrors that format, and a top-level interpretRequest entry point to run it.

The real goal of this is to allow a Servant path like

type GraphQLAPI = "graphql"
                  :> ReqBody '[JSON] GraphQL.Request
                  :> Post '[JSON] GraphQL.Response

graphQLServer :: Server GraphQLAPI
graphQLServer = liftIO . runRequest

The one big caveat in this PR is that you can't 100% deserialize Value objects without knowing their type context. This assumes that JSON strings are always GraphQL strings (they could also be enums or schema-specific scalar types) and that JSON numbers are always GraphQL floats (they could also be integers and that's probably the common case).

Copy link
Collaborator

@teh teh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really cool - thanks for your work!

I'm a bit confused by the mempty behaviour in this context because I haven't looked at the code in a while.

instance Aeson.FromJSON Name where
parseJSON = Aeson.withText "Name" $ \v ->
case makeName v of
Left _ -> mempty
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is mempty the right thing here? Should we maybe fail instead? I'm not even sure why mempty is a valid value for Name - maybe @jml remembers :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In both this and the other case, you're running inside the Aeson Parser monad. Protolude is a little more aggressive about moving fail out of the base Monad class, so it looks like I need to import Control.Monad.Fail and then it works.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@teh This is a wart in Aeson IMO. Because mempty wasn't prefixed with return (or pure), it's mempty :: Aeson.Parser Name, not mempty :: Name (which wouldn't compile).

If you look at the source for Parser a, it has an implementation of Monoid such that mempty = fail "mempty". I think this kind of sucks.

empty (i.e. failed Alternative, not monoid identity), would be much more correct.

But fail is best, because it's informative.

parseJSON (Aeson.Number x) = return $ ConstFloat $ toRealFloat x
parseJSON (Aeson.Bool x) = return $ ConstBoolean x
parseJSON Aeson.Null = return ConstNull
parseJSON _ = mempty
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the mempty for ConstScalar?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not mempty for ConstScalar, it's mempty for Parser a.

@jml
Copy link
Collaborator

jml commented Nov 20, 2017

Hi @dmaze,

Thanks for this PR—it seems like a good idea. I'd like to have a look over it before merging to make sure I understand it.

Out of curiosity, can you share a little about what you're using graphql-api and servant for?

Thanks,
jml

@dmaze
Copy link
Author

dmaze commented Nov 20, 2017

My day job is using GraphQL pretty successfully, and in particular, the React+Relay+Flow JavaScript stack has been a pretty comfortable way to write front-end applications that don't need much state beyond what they can get from the server. For my personal tasks I prefer to use languages where, say, it's possible for my editor to notice simple typos.

The specific thing I'm actually playing with is an application that reads the MBTA's (Boston public transit agency) real-time data feeds, caches them in a Persistent (SQLite) database, then re-serves the cached data to a front-end that can display the actual travel times between a pair of stops on a day-to-day basis (while the T claims the Red Line has a 92% on-time rate, it feels to my like at least 25% of my commute-time trips have significant delays). If the back-end exposed a GraphQL interface, then the front-end would be very familiar space to me...which brought me here.

Copy link
Collaborator

@jml jml left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Sorry for the delay in reviewing & the multiple round-trips—we're still figuring this out.

@@ -302,6 +325,16 @@ objectFromList xs = Object' <$> OrderedMap.orderedMap xs
unionObjects :: [Object' scalar] -> Maybe (Object' scalar)
unionObjects objects = Object' <$> OrderedMap.unions [obj | Object' obj <- objects]

instance FromJSON scalar => FromJSON (Object' scalar) where
parseJSON = Aeson.withObject "Object" $ \v -> do
-- Order of keys is lost before we get here
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remind me why this is relevant?

-- Note that the 'FromJSON' instance always decodes JSON strings to
-- GraphQL strings (never enums) and JSON numbers to GraphQL floats
-- (never ints); doing a better job of resolving this requires query
-- context.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little uncomfortable about this. The "Zen of Python" advises "in the face of ambiguity, refuse the temptation to guess", and I think that's good advice.

Some options I could think of:

  • don't have a FromJSON instance for this, and instead have a series of functions that take the query (or whatever the minimal necessary context is) and a Value to be parsed, so that we can unambiguously parse these things
  • create a new type (types?) that unites the query context with these values and write FromJSON instances for these new types
  • create a new AmbiguousConstScalar type. Then, have a function that transforms AmbiguousConstScalar -> ConstScalar by also taking a query context. This type would need fewer branches than ConstScalar.
  • make ConstScalar a phantom type, where the phantom parameter is whether it's ambiguous or not. Then, have a function that transforms ConstScalar Ambiguous -> ConstScalar Unambiguous by also taking a query context

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants