Hey it is possible to make the API more Ruby like? I know that the whole project was inspired by the official working draft and probably the reference implementation too. However I find it quite difficult to use the API, because lot of Ruby tricks do not work.
For example, it would be nice to use standard class inheritance:
class QueryType < GraphQL::ObjectType
end
Another nice feature would be if the resolve function would accept all callable objects and blocks, not just procs:
class Resolver
def call(object, args, context)
end
end
# field definition
field :something, [types.Array] do
resolve Resolver.new
end
Additionally syntax sugar like this would help do shorten the code:
field :something, [types.Array] do |object, args, context|
# resolve something
end
What do you think about these ideas, @rmosolgo?
use standard class inheritance
My first approach did involve classes. Each type was represented by a class (as in your snippet). Whenever an object of that type came along, the type was "instantiated" and the object was stored as @target_object. That object was passed around for resolving fields, but all other behavior was the same. â€
I felt like I wasn't gaining much by using classes. If @target_object was the only thing that ever changed, why make new "instances" of the type all the time? Why not just make the type once and insert the object as needed? I found it a bit simpler.
Besides, using classes for types felt a little misleading. A user never interacted with instances (and never made instances manually) so why use classes at all?
(Personally, I'm not a huge fan of inheritance for code-sharing, I share this opinion: http://asserttrue.blogspot.com/2009/02/inheritance-as-antipattern.html)
Is there an advantage of using class ... < ... which I overlooked? Maybe some benefit to end users?
resolvefunction would accept all callable objects
Yes, I like this pattern! In fact, I believe it's already supported, for example: https://m.alphasights.com/graphql-ruby-clean-up-your-query-type-d7ab05a47084#.u86z1ovue
However, it's not documented or explicitly tested. I should add those!
shorten the code:
field :something, [types.Array] do |object, args, context| # resolve something end
Yes, I agree this would be an improvement! I would like to shorten the code for defining a custom resolve.
For me, the remaining challenge is, how to fit the remaining pieces in that API:
would they be keyword args to field? Or passed in another way?
†Actually, that implementation had some other elements that made it worse:
ObjectType's built-in methods? would they be keyword args to field? Or passed in another way?
What about supporting both the existent and the shortcut variations?
# Simple field, imho we even can skip the documentation
field :firstname, types.String
# Computed field, but still kinda self-explanatory
field :fullname, types.String do |user|
"#{user.firstname } #{user.lastname}"
end
# More complicated, full API
field :friends do
type types[UserType]
arguments :limit, types.Int
resolve do |user, args|
user.friends.limit(args[:limit])
end
end
To define field resolution, you could implement a method with the same name. What a nightmare! What if a type's fields conflicted with ObjectType's built-in methods?
This is probably the best reason why a custom DSL is better than class inheritance, however I don't like this trend of writing own DSLs. I know Ruby makes this super easy, and it's one of these "wow" features when you start learning Ruby. But it often makes stuff much more complicated than it needs to be. But just imho → Happy Minitest user here :wink:
But it often makes stuff much more complicated
FWIW the .define API is a layer on top of plain ol' attr_accessors. So these are equivalent:
.define: ruby
PersonType = GraphQL::ObjectType.define do
name "Person"
interfaces([NamedThings])
field :name, types.String
field :hometown, TownType do
description "Where this person lived during this year"
argument :year, types.Int
resolve -> (obj, args, ctx) {
year = args[:year] || Time.now.year
residence = Residence.where(person: obj, year: year)
residence && residence.hometown
}
end
end
``` ruby
year_arg = GraphQL::Argument.new
year_arg.name = "year"
year_arg.type = GraphQL::INT_TYPE
hometown_field = GraphQL::Field.new
hometown_field.name = "hometown"
hometown_field.type = TownType
hometown_field.description = "Where this person lived during this year"
hometown_field.arguments["year"] = year_arg
hometown_field.resolve = -> (obj, args, ctx) {
# ...
}
name_field = GraphQL::Field.new
name_field.name = "name"
name_field.type = GraphQL::STRING_TYPE
PersonType = GraphQL::ObjectType.new
PersonType.name = "Person"
PersonType.interfaces = [NamedThings]
PersonType.fields["hometown"] = hometown_field
PersonType.fields["name"] = name_field
```
One thing I've considered is adding more powerful #initialize methods, so you could pass those things when you create the objects. For example,
hometown_field = GraphQL::Field.new({
name: "hometown",
type: TownType,
description: "Where this person lived during this year",
arguments: {
"year" => GraphQL::Argument.new(name: "year", type: GraphQL::INT_TYPE)
},
resolve: -> (obj, args, ctx) { ... },
})
PersonType = GraphQL::ObjectType.new({
name: "Person",
interfaces: [NamedThings],
fields: {
"hometown" => hometown_field,
"name" => name_field,
}
})
Is that a more appealing plain-Ruby API? Or is there a class-based API which doesn't use "magic" helper methods to define types & fields?
Well, I'm not crazy about using classes for types but if you do find some API improvement ideas, feel free to reopen this or open a new issue!
Most helpful comment
My first approach did involve classes. Each type was represented by a class (as in your snippet). Whenever an object of that type came along, the type was "instantiated" and the object was stored as
@target_object. That object was passed around for resolving fields, but all other behavior was the same. â€I felt like I wasn't gaining much by using classes. If
@target_objectwas the only thing that ever changed, why make new "instances" of the type all the time? Why not just make the type once and insert the object as needed? I found it a bit simpler.Besides, using classes for types felt a little misleading. A user never interacted with instances (and never made instances manually) so why use classes at all?
(Personally, I'm not a huge fan of inheritance for code-sharing, I share this opinion: http://asserttrue.blogspot.com/2009/02/inheritance-as-antipattern.html)
Is there an advantage of using
class ... < ...which I overlooked? Maybe some benefit to end users?Yes, I like this pattern! In fact, I believe it's already supported, for example: https://m.alphasights.com/graphql-ruby-clean-up-your-query-type-d7ab05a47084#.u86z1ovue
However, it's not documented or explicitly tested. I should add those!
Yes, I agree this would be an improvement! I would like to shorten the code for defining a custom resolve.
For me, the remaining challenge is, how to fit the remaining pieces in that API:
would they be keyword args to
field? Or passed in another way?†Actually, that implementation had some other elements that made it worse:
ObjectType's built-in methods?