[1.8] Try again: interfaces as Ruby modules#1372
Conversation
|
I got a great question on Slack so I wrote the response here for posterity:
Regarding AS::C, I took a crack at it, even porting most of it to But the problem is, AS::C treats modules in one of two different ways:
Here's the source: https://github.com/rails/rails/blob/master/activesupport/lib/active_support/concern.rb#L114 The problem here is that we need a mix. For example, this doesn't work: module BaseInterface
include GraphQL::Schema::Interface
field_class MyCustomField
# A helper for defining fields:
def self.field_macro(...); end
end
module Commentable
include BaseInterface
field_macro(...)
end It doesn't work because So, what if you made module BaseInterface
extend GraphQL::Schema::Member::Concern
include GraphQL::Schema::Interface
field_class MyCustomField
# A helper for defining fields:
def self.field_macro(...); end
end
module Commentable
include BaseInterface
field_macro(...)
end Then you get So, how can you get both macros from upstream and continue sending them downstream? I really fiddled with this a lot, tweaking various |
|
One little nit on the syntax, is module Commentable
extend GraphQL::Schema::Interface(On an |
|
Another good question was: how about getting rid of the class MyInterface < GraphQL::Interface
field :foo
def foo; end
endThe thing I couldn't figure out here was, how to get a So, I'm not sure how to execute that method in the context of the runtime object. There's another option which I have not explored: perhaps interface fields could be called on an instance of the interface. That is, whenever an interface is requested, you'd initialize an instance of the Interface type, and call methods on it. I'm afraid it would be pretty weird, because, for example, ivars would not be shared between methods, the way they are between ruby modules. Also, it would get tricky if objects override their interface fields in some way. For example, one or more of: re-call |
lib/graphql/schema/member/concern.rb
Outdated
|
|
||
| def included(base = nil, &block) | ||
| if base.nil? | ||
| raise "Multiple included { ... } blocks, merge them" if instance_variable_defined?(:@_included_block) |
There was a problem hiding this comment.
Does it break anything if I make an interface module, and do something like:
module SomeInterface
# what if I flip these two around?
extend ActiveSupport::Concern
include GraphQL::Schema::Interface
end|
Regarding module AbstractNameable
extend_or_include GraphQL::Schema::Interface
field :name, String, null: false
def name
@object.display_name
end
end
module Actor
extend_or_include AbstractNameable
end
class Person < GraphQL::Schema::ObjectType
implements AbstractNameable
end Somehow, we want |
112c259 to
4aa41f0
Compare
|
Ok, I just force-pushed this branch with a much improved implementation, in my opinion. I'm pretty happy with the implementation. Thanks @theorygeek for talking this over with me in Slack, after clarifying exactly what we need (inheritance of instance methods; inheritance of class methods) I got a better sense of what implementation would work for us. So, now it works mostly like this:
I think this is really nice API and a reasonably sane implementation 😊 TODO:
|
| require 'graphql/schema/member/accepts_definition' | ||
| require 'graphql/schema/member/base_dsl_methods' | ||
| require 'graphql/schema/member/cached_graphql_definition' | ||
| require 'graphql/schema/member/graphql_type_names' |
There was a problem hiding this comment.
All of these were extracted so I could keep Schema::Member as a base class, but easily get these methods into Schema::Interface.
| end | ||
|
|
||
| module ClassMethods | ||
| module AcceptsDefinitionClassMethods |
There was a problem hiding this comment.
Here was a weird naming conflict with ClassMethods, case in point for renaming Schema::Interface's inherited module to DefinitionMethods
lib/graphql/schema/interface.rb
Outdated
| if !child_class.is_a?(Class) | ||
| # In this case, it's been included into another interface. | ||
| # This is how interface inheritance is implemented | ||
| child_class.const_set(:ClassMethods, Module.new) |
There was a problem hiding this comment.
TODO: this should only set it if it's not already defined
| # But not the old method | ||
| refute new_object_2.method_defined?(:id) | ||
| # And the inherited method | ||
| assert new_object_2.method_defined?(:id) |
There was a problem hiding this comment.
(This was just a document-how-it-actually-works test, I think the new behavior is better anyway)
4aa41f0 to
8875e09
Compare
8875e09 to
6093d15
Compare
😱 😱 😱 |
|
It is looking pretty nice! I think I was hoping that you would do: module MyInterface
extend GraphQL::Schema::Interface
end
class MyObjectType < GraphQL::Schema::Object
include MyInterface
endBecause then you get these semantics, which seem nice: # Is this type an Interface type?
MyInterface.is_a?(GraphQL::Schema::Interface)
# => true
# Is this type an Interface type?
MyObjectType.is_a?(GraphQL::Schema::Interface)
# => false
# Test for interface implementation
MyObjectType < MyInterface
# => true
# Test if the object instance supports that interface
MyObjectType.new.is_a?(MyInterface)
# => trueHowever... it is probably overrated, and perhaps even a bit confusing. Because right now, you would test if a type is an object type using: MyObjectType < GraphQL::Schema::Object
# => true |
|
Yeah, I can't think of a way to make it consistent! I think I'm going to merge this branch soon, if you want to experiment with the different hooks, I'm open to switching it around a bit, but I'm not optimistic I can make it drastically better than it is now. |
|
I wrote up a little gist that had a real basic example of how it might work: TBH... I am not sure that they are huge improvements. I think the nicest thing is that it solves:
|
|
Thanks for sharing those notes, I took a try at it here: interface-as-module...interface-as-module-extends I'm not quite sure it's worth it because:
|
|
also, I added |
|
I'm gonna drop JRuby from the build. It passes locally. I'll open a failing PR with the issue (and I've reported it to JRuby here: jruby/jruby#3680 (comment) |
Another take on #1286
Before (
.define)This is the previous way to make an interface:
After (
class)This is the current way on 1.8-dev:
After (
module)This is what I'm exploring/proposing in this branch
The main difference is that the
module Implementationis not required to customize field resolution. As a benefit, the implementation of the field sits right next to the field signature.