At 2023.7.21, Github Copilot Chat beta now available for every organization!!
VSCode extensions are also available.
So, I tried pair programming with Github copilot chat in some specific examples.
Example: Refactoring GraphQL queries by every resolver
The following code is a GraphQL query implementation to get the data for a simple blog application by using graphql-ruby.
ex. query_type.rb
module Types
class QueryType < Types::BaseObject
include GraphQL::Types::Relay::HasNodeField
include GraphQL::Types::Relay::HasNodesField
field :user, Types::UserType, null: true do
description "Find User by ID"
argument :id, ID, required: true
end
def user(id:)
User.find(id)
end
field :users, [Types::UserType], null: false do
description "Fetch All Users"
end
def users(page: nil, items: nil)
User.all
end
field :article, Types::ArticleType, null: true do
argument :id, ID, required: true
end
def article(id:)
Article.find(id)
end
field :articles, [Types::ArticleType], null: true do
argument :user_id, ID, required: false
end
def articles(user_id: nil)
if user_id
Article.where(user_id: user_id)
else
Article.all
end
end
field :comments, [Types::CommentType], null: false do
argument :id, ID, required: false
argument :user_id, ID, required: false
argument :article_id, ID, required: false
end
def comments(id: nil, user_id: nil, article_id: nil)
scope = Comment.all
scope = scope.where(id: id) if id
scope = scope.where(user_id: user_id) if user_id
scope = scope.where(article_id: article_id) if article_id
scope
end
field :tags, [Types::TagType], null: false do
argument :name, String, required: false
end
def tags(name: nil)
if name
Tag.where("name like ?", "#{name}%")
else
Tag.all
end
end
end
end
The problem with this implementation is that query_type.rb contains all the query implementation.
So, I will try to refactor it using Github copilot chat to separate the query implementation for each Resolver class.
First prompt: “Refactor the selected code”
First, I selected all implementations of query_type.rb.
And then, I asked Github Copilot the way.
Oops…, this prompt was too abstract.
Second prompt: “Refactoring the GraphQL query implemented in graphql-ruby to separate each Resolver”
Github Copilot:
module Types
class QueryType < Types::BaseObject
include GraphQL::Types::Relay::HasNodeField
include GraphQL::Types::Relay::HasNodesField
field :user, Types::UserType, null: true do
description "Find User by ID"
argument :id, ID, required: true
end
def user(id:)
User.find(id)
end
field :users, [Types::UserType], null: false do
description "Fetch All Users"
end
def users
User.all
end
field :article, Types::ArticleType, null: true do
argument :id, ID, required: true
end
def article(id:)
Article.find(id)
end
field :articles, [Types::ArticleType], null: true do
argument :user_id, ID, required: false
end
def articles(user_id: nil)
if user_id
Article.where(user_id: user_id)
else
Article.all
end
end
field :comments, [Types::CommentType], null: false do
argument :id, ID, required: false
argument :user_id, ID, required: false
argument :article_id, ID, required: false
end
def comments(id: nil, user_id: nil, article_id: nil)
scope = Comment.all
scope = scope.where(id: id) if id
scope = scope.where(user_id: user_id) if user_id
scope = scope.where(article_id: article_id) if article_id
scope
end
field :tags, [Types::TagType], null: false do
argument :name, String, required: false
end
def tags(name: nil)
if name
Tag.where("name like ?", "#{name}%")
else
Tag.all
end
end
end
end
Hmm….., It is the same as the previous implementation.😓
Third prompt: “No! Now, query_type.rb includes all queries, but I want to divide resolver class.”
Github Copilot:
class UserResolver < GraphQL::Schema::Resolver
description "Find User by ID"
argument :id, ID, required: true
type Types::UserType, null: true
def resolve(id:)
User.find(id)
end
end
class UsersResolver < GraphQL::Schema::Resolver
description "Fetch All Users"
type [Types::UserType], null: false
def resolve
User.all
end
end
class ArticleResolver < GraphQL::Schema::Resolver
argument :id, ID, required: true
type Types::ArticleType, null: true
def resolve(id:)
Article.find(id)
end
end
And the rest of the query output the same pattern of implementation.
module Types
class QueryType < Types::BaseObject
include GraphQL::Types::Relay::HasNodeField
include GraphQL::Types::Relay::HasNodesField
field :user, resolver: UserResolver
field :users, resolver: UsersResolver
field :article, resolver: ArticleResolver
field :articles, resolver: ArticlesResolver
field :comments, resolver: CommentsResolver
field :tags, resolver: TagsResolver
end
end
Sounds good!
Github copilot presented an implementation that separates each Resolver by class.
Forth prompt: “What directory structure should the resolver class have?”
Github Copilot:
app/graphql/
├── resolvers/
│ ├── article_resolver.rb
│ ├── articles_resolver.rb
│ ├── comment_resolver.rb
│ ├── comments_resolver.rb
│ ├── tag_resolver.rb
│ └── tags_resolver.rb
├── types/
│ ├── article_type.rb
│ ├── comment_type.rb
│ ├── mutation_type.rb
│ ├── query_type.rb
│ └── tag_type.rb
└── schema.rb
Almost perfect!!
It seems to me that we could adopt the improvements suggested by Github Copilot.
Github Copilot Chat makes it easy to add implementations.
Like this👇
Final prompt: “Output a Request spec with RSpec using each Resolver.”
Github Copilot:
require 'rails_helper'
RSpec.describe "GraphQL queries", type: :request do
describe "user query" do
let(:user) { create(:user) }
let(:query) do
<<~GQL
query {
user(id: #{user.id}) {
id
name
email
}
}
GQL
end
it "returns the user with the specified ID" do
post '/graphql', params: { query: query }
json = JSON.parse(response.body)
data = json['data']['user']
expect(data).to include(
'id' => user.id.to_s,
'name' => user.name,
'email' => user.email
)
end
end
describe "users query" do
let!(:users) { create_list(:user, 3) }
let(:query) do
<<~GQL
query {
users {
id
name
email
}
}
GQL
end
it "returns all users" do
post '/graphql', params: { query: query }
json = JSON.parse(response.body)
data = json['data']['users']
expect(data.size).to eq(users.size)
data.each_with_index do |user_data, i|
expect(user_data).to include(
'id' => users[i].id.to_s,
'name' => users[i].name,
'email' => users[i].email
)
end
end
end
describe "article query" do
let(:article) { create(:article) }
let(:query) do
<<~GQL
query {
article(id: #{article.id}) {
id
title
body
}
}
GQL
end
it "returns the article with the specified ID" do
post '/graphql', params: { query: query }
json = JSON.parse(response.body)
data = json['data']['article']
expect(data).to include(
'id' => article.id.to_s,
'title' => article.title,
'body' => article.body
)
end
end
describe "articles query" do
let!(:articles) { create_list(:article, 3) }
let(:query) do
<<~GQL
query {
articles {
id
title
body
}
}
GQL
end
it "returns all articles" do
post '/graphql', params: { query: query }
json = JSON.parse(response.body)
data = json['data']['articles']
expect(data.size).to eq(articles.size)
data.each_with_index do |article_data, i|
expect(article_data).to include(
'id' => articles[i].id.to_s,
'title' => articles[i].title,
'body' => articles[i].body
)
end
end
it "returns articles for a specific user" do
user = create(:user)
articles_for_user = create_list(:article, 2, user: user)
post '/graphql', params: { query: query, variables: { userId: user.id } }
json = JSON.parse(response.body)
data = json['data']['articles']
expect(data.size).to eq(articles_for_user.size)
data.each_with_index do |article_data, i|
expect(article_data).to include(
'id' => articles_for_user[i].id.to_s,
'title' => articles_for_user[i].title,
'body' => articles_for_user[i].body
)
end
end
end
describe "comments query" do
let!(:comments) { create_list(:comment, 3) }
let(:query) do
<<~GQL
query {
comments {
id
body
}
}
GQL
end
it "returns all comments" do
post '/graphql', params: { query: query }
json = JSON.parse(response.body)
data = json['data']['comments']
expect(data.size).to eq(comments.size)
data.each_with_index do |comment_data, i|
expect(comment_data).to include(
'id' => comments[i].id.to_s,
'body' => comments[i].body
)
end
end
it "returns comments for a specific user" do
user = create(:user)
comments_for_user = create_list(:comment, 2, user: user)
post '/graphql', params: { query: query, variables: { userId: user.id } }
json = JSON.parse(response.body)
data = json['data']['comments']
expect(data.size).to eq(comments_for_user.size)
data.each_with_index do |comment_data, i|
expect(comment_data).to include(
'id' => comments_for_user[i].id.to_s,
'body' => comments_for_user[i].body
)
end
end
it "returns comments for a specific article" do
article = create(:article)
comments_for_article = create_list(:comment, 2, article: article)
post '/graphql', params: { query: query, variables: { articleId: article.id } }
json = JSON.parse(response.body)
data = json['data']['comments']
expect(data.size).to eq(comments_for_article.size)
data.each_with_index do |comment_data, i|
expect(comment_data).to include(
'id' => comments_for_article[i].id.to_s,
'body' => comments_for_article[i].body
)
end
end
end
describe "tags query" do
let!(:tags) { create_list(:tag, 3) }
let(:query) do
<<~GQL
query {
tags {
id
name
}
}
GQL
end
it "returns all tags" do
post '/graphql', params: { query: query }
json = JSON.parse(response.body)
data = json['data']['tags']
expect(data.size).to eq(tags.size)
data.each_with_index do |tag_data, i|
expect(tag_data).to include(
'id' => tags[i].id.to_s,
'name' => tags[i].name
)
end
end
it "returns tags with a specific name" do
tag = create(:tag, name: "ruby")
post '/graphql', params: { query: query, variables: { name: "ru" } }
json = JSON.parse(response.body)
data = json['data']['tags']
expect(data.size).to eq(1)
expect(data[0]).to include(
'id' => tag.id.to_s,
'name' => tag.name
)
end
end
end
This is amazing.😲
This test seems appropriate!
Summary
Features I've found using Github Copilot Chat
- Github Copilot Chat gives higher quality answers than I expected
- Response time to output answers is also fast
- Easy to add implementations to the editor on VSCode
- It suggests potential next questions