-
Notifications
You must be signed in to change notification settings - Fork 35
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
Guaranteed correct name in AST #42
Conversation
Instead of using it in Server, use the already-existing getFooDefinition methods.
Should point out that you can tell where we can use more structure in error type by searching for |
@@ -78,25 +81,31 @@ data Argument (name :: Symbol) (argType :: Type) | |||
-- https://hackage.haskell.org/package/optional-args-1.0.1) | |||
data DefaultArgument (name :: Symbol) (argType :: Type) | |||
|
|||
-- | Convert a type-level 'Symbol' into a GraphQL 'Name'. | |||
nameFromSymbol :: forall (n :: Symbol) (proxy :: Symbol -> *). KnownSymbol n => proxy n -> Either NameError Name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if you feel adventurous you could use type application here as well for consistency with the rest of our APIs. I.e.: nameFromSymbol @name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I felt a little bit adventurous and tried this earlier, but couldn't figure it out in the time I had allotted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ack, if you add a todo I'll do it
-- | A name in GraphQL. | ||
-- | ||
-- https://facebook.github.io/graphql/#sec-Names | ||
newtype Name = Name { getNameText :: Text } deriving (Eq, Ord, Show) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where is the getNameText
style from? I find it a bit weird but can of course get used to it. I guess the main point is so people can write point-free instead of unpacking in the argument? I find the argument-unpacking more readable personally because it doesn't introduce a new identifier that I have to map back to the original type in my head, but that's very subjective.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's just record field syntax. It's pretty common for newtypes
as a way of unwrapping the type.
Main point is composition, not point-free style. (e.g. map getNameText names
). Encoder has a couple of motivating examples. Everything could be written with pattern matching, but we'd have to do much more typing.
Also note that if we wanted to use argument unpacking, we'd have to export a view pattern, because we don't want to export the constructor.
Maybe renaming it to unName
or nameToText
would make it clearer? Similarly, I could write a whole separate function definition that does it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see, thanks for explaining! Not exporting the constructor is a good point. I think I'd then prefer getName
or unName
(like unwrap
in https://github.com/purescript/purescript-newtype/blob/master/src/Data/Newtype.purs)
@@ -76,12 +130,12 @@ data OperationDefinition = Query { getNode :: Node } | |||
| Mutation { getNode :: Node } | |||
deriving (Eq,Show) | |||
|
|||
data Node = Node Name [VariableDefinition] [Directive] SelectionSet | |||
data Node = Node (Maybe Name) [VariableDefinition] [Directive] SelectionSet |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Node is one of those types that would benefit from named fields IMO. Why is the name Maybe
? Should node creation fail if the name is invalid?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Imagine:
{
me {
name
}
}
The top-level node has no name. There can only be one such node in a document. I'm open to better ideas on how to represent this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In #40, I added tests demonstrating the broken behaviour of the parser in this area. We probably will have to change the types to deal with this, but I'd rather not in this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see, it's a parser limitation. OK for now then, though not a huge fan. Can you add a note / todo?
@@ -76,12 +130,12 @@ data OperationDefinition = Query { getNode :: Node } | |||
| Mutation { getNode :: Node } | |||
deriving (Eq,Show) | |||
|
|||
data Node = Node Name [VariableDefinition] [Directive] SelectionSet | |||
data Node = Node (Maybe Name) [VariableDefinition] [Directive] SelectionSet | |||
deriving (Eq,Show) | |||
|
|||
-- XXX: Lots of things have names. Maybe we should define a typeclass for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-1 from me I think. We already have some pretty weird compiler errors because we're writing generic code. Nailing certain types into place (instead of type classes or type families) does reduce the overall movement and makes compiler errors more readable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be clear, are you suggesting that I remove the HasName
type class and all of its instances and replace them with specifically named functions that each do the job at hand?
Even golang supports this level of genericness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No it's fine I misunderstood what you were suggesting.
<> ":" | ||
<> type_ ty | ||
AST.getNameText name <> optempty argumentsDefinition args | ||
<> ":" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indentation seems off?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
@@ -54,7 +54,7 @@ operationDefinition = | |||
<?> "operationDefinition error!" | |||
|
|||
node :: Parser AST.Node | |||
node = AST.Node <$> AST.nameParser | |||
node = AST.Node <$> (pure <$> AST.nameParser) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same comment as above: if the name is invalid why not bail?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not invalid. It's an anonymous operation.
@@ -64,7 +64,7 @@ validate :: Alternative m => AST.Document -> m ValidDocument | |||
validate = pure . Valid | |||
|
|||
data ValidationError | |||
= DuplicateOperation AST.Name | |||
= DuplicateOperation (Maybe AST.Name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure why this is Maybe
either .. what's a DuplicateOperation
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An "operation" in a GraphQL document is a query or a mutation. e.g.
query foo { ... }
mutation bar { ... }
You cannot have more than one operation with the same name.
DuplicateOperation
is the error that means that you have more than one operation with the same name.
Because GraphQL allows anonymous queries, you can theoretically write:
{
me {
name
}
}
{
me {
age
}
}
But this is also invalid, because it has more than one anonymous query.
This validation code takes the cheap approach of treating that as a DuplicateOperation Nothing
. I could adjust the data type to have separate errors for these cases, if you'd like
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know it's extra work but maybe it's worth adding a new Maybe-like sum type for unnamed queries? It's not super clear what Maybe means in this context. I do know now though, so up to you.
, HasObjectDefinition object | ||
) => RunUnion m (object :<|> rest) where | ||
runUnion (lh :<|> rh) fragment@(AST.SelectionInlineFragment (AST.InlineFragment (AST.NamedType queryTypeName) [] subSelection)) = | ||
case getDefinition @object of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that's the right place to handle definition errors, design wise. IMO we should get the definition once before running any resolver code, and catch all errors at that stage. By moving the getDefinition
error handling into the resolver we obfuscate what's going on.
tl;dr - I'd vastly prefer not to have this code here but instead in a pre-resolver step.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tl;dr - I'd vastly prefer not to have this code here but instead in a pre-resolver step.
Me too, but this is the first step to getting there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Specifically, as long as this code needs to read out of symbols (either via getDefinition
or by arbitrarily duplicating some of the logic in getDefinition
), it will need to handle errors like this. Our options are to panic (which means the compiler does less work for us and we need to be more careful), to mask the error (which we were doing before), or to represent the error explicitly.
By making it explicit, we're giving ourselves to tools to improve it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm bad at explaining. I don't think we should check schema validity at all in buildResolver
. IIUC we can do a full validity check by running getDefinition
on the top level once, and then call buildResolver
. I.e. our top-level entry point would look like this:
buildServer = do
d <- getDefinition @IO @API
case d of
Left err -> throwError "why are you doing this to me"
Right _ -> buildResolver @IO @API
does that make sense? I think control flow in buildResolver is complicated enough without us bringing getDefinition
in.
There's also a secondary concern which is that we're now checking validity on each request, even though the schema is compiled and therefore never changes. I suspect that the GHC optimizer can float number of checks up but I'd rather have a execute-once by construction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so yea, what I am saying is we should ignore the error (raise a patter match failure) for easier control flow
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But I can see your reaction already :)
I.e. leave it in, I'll work with what we have
Build failure is CircleCI being buggy. In the end I've made very few changes because I'm not clear what the actionable feedback is. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm though I'm not super happy about pulling in HasObjectDefinition
and schema checks at runtime as mentioned.
where typeName = toS (symbolVal (Proxy :: Proxy typeName)) | ||
, HasGraph m object | ||
, HasObjectDefinition object | ||
) => RunUnion m (object :<|> rest) where |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replacing "Object .." works in this instance because we only have Objects to stick into unions but in the general case we need to spell out the full Object name interfaces fields
for the type checker. (not actionable but please don't replace any of the other instances)
Survived Christmas :) Can I merge this? |
Merged. Sorry for the delay. |
Value
toAST
AST
or anywhere elseHasName
type class that is implemented by all named things in the schema (can be extended to other things later, this is just the only place I needed it)getFooDefinition
methods to returnEither NameError Foo
. This involves changing most of the code to be applicative, which in turn involves a fair chunk of using the composition operator. Sorry.QueryError
. I think we can do better than this, but await your error handling branch.New TODOs: