Graphql-ruby: Future API?

Created on 25 Mar 2016  Â·  5Comments  Â·  Source: rmosolgo/graphql-ruby

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?

Most helpful comment

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?

resolve function 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:

  • argument definitions
  • field description
  • deprecation reason

would they be keyword args to field? Or passed in another way?


† Actually, that implementation had some other elements that made it worse:

  • 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?
  • Type classes also handled their own resolution. They received chunks of the AST and generated JSON. It became a very large and busy class! I'm much happier to have definition and execution separated.

All 5 comments

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?

resolve function 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:

  • argument definitions
  • field description
  • deprecation reason

would they be keyword args to field? Or passed in another way?


† Actually, that implementation had some other elements that made it worse:

  • 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?
  • Type classes also handled their own resolution. They received chunks of the AST and generated JSON. It became a very large and busy class! I'm much happier to have definition and execution separated.

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

  • "Plain Ruby":

``` 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!

Was this page helpful?
0 / 5 - 0 ratings